Vitron-Front/app/routes/admin.ledger.journal.tsx
Arda Samadi 40052b7002 admin ledger: Slice 2c journal UI (derived balances + journal history)
- use-admin-hooks: LedgerOverview.journal, LedgerEntryRow,
  LedgerSellerDetail journal fields, useLedgerJournal.
- new "دفتر روزنامه" sub-tab + admin.ledger.journal route.
- ledger overview: journal-derived KPI row + recorded-vs-journal drift badge.
- seller detail: journal-balance stat + journal history table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 13:24:06 +03:30

157 lines
4.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from "react";
import {
useLedgerJournal,
type LedgerEntryRow,
} from "~/requestHandler/use-admin-hooks";
import {
AdminTable,
AdminPagination,
AdminToolbar,
AdminSearch,
AdminSelect,
StatusBadge,
faToman,
faDate,
useDebouncedValue,
type Column,
} from "~/components/admin/DataTable";
const PAGE_SIZE = 25;
// Journal entry_type → Persian label + tone.
const ENTRY: Record<string, { label: string; tone: "green" | "blue" | "red" }> = {
deposit: { label: "واریز", tone: "green" },
purchase: { label: "خرید", tone: "blue" },
seller_credit: { label: "اعتبار فروشنده", tone: "green" },
platform_fee: { label: "کارمزد پلتفرم", tone: "blue" },
refund: { label: "بازپرداخت", tone: "red" },
seller_reversal: { label: "برگشت فروشنده", tone: "red" },
payout: { label: "تسویه", tone: "red" },
};
const ACCOUNT: Record<string, string> = {
user_wallet: "کیف پول کاربر",
seller_balance: "موجودی فروشنده",
platform: "پلتفرم",
external: "خارجی",
};
export default function LedgerJournal() {
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
const [account, setAccount] = useState("");
const [entryType, setEntryType] = useState("");
const debouncedSearch = useDebouncedValue(search);
const { data, isLoading } = useLedgerJournal({
page,
page_size: PAGE_SIZE,
search: debouncedSearch,
account,
entry_type: entryType,
});
const reset = (fn: () => void) => {
fn();
setPage(1);
};
const columns: Column<LedgerEntryRow>[] = [
{
header: "نوع",
render: (e) => {
const t = ENTRY[e.entryType] || { label: e.entryType, tone: "blue" as const };
return <StatusBadge label={t.label} tone={t.tone} />;
},
},
{
header: "حساب",
render: (e) => (
<span className="text-GRAY text-[12px] whitespace-nowrap">
{ACCOUNT[e.account] || e.account}
</span>
),
},
{
header: "مبلغ",
render: (e) => (
<span
className={cnDelta(e.delta)}
dir="ltr"
>
{e.delta >= 0 ? "+" : ""}
{faToman(Math.abs(e.delta))}
</span>
),
},
{
header: "طرف",
render: (e) => (
<span className="text-GRAY text-[11px] tabular-nums" dir="ltr">
{e.sellerName || e.userPhone || "—"}
</span>
),
},
{
header: "سفارش",
render: (e) => (
<span className="text-GRAY tabular-nums">{e.order ? e.order.slice(0, 8) : "—"}</span>
),
},
{
header: "تاریخ",
render: (e) => <span className="text-GRAY whitespace-nowrap">{faDate(e.createdAt)}</span>,
},
];
return (
<div>
<AdminToolbar>
<AdminSearch
value={search}
onChange={(v) => reset(() => setSearch(v))}
placeholder="جستجوی فروشگاه / تلفن / توضیح…"
/>
<AdminSelect
value={account}
onChange={(v) => reset(() => setAccount(v))}
options={[
{ value: "", label: "همه حساب‌ها" },
{ value: "user_wallet", label: "کیف پول کاربر" },
{ value: "seller_balance", label: "موجودی فروشنده" },
{ value: "platform", label: "پلتفرم" },
{ value: "external", label: "خارجی" },
]}
/>
<AdminSelect
value={entryType}
onChange={(v) => reset(() => setEntryType(v))}
options={[
{ value: "", label: "همه انواع" },
{ value: "deposit", label: "واریز" },
{ value: "purchase", label: "خرید" },
{ value: "seller_credit", label: "اعتبار فروشنده" },
{ value: "platform_fee", label: "کارمزد پلتفرم" },
{ value: "refund", label: "بازپرداخت" },
{ value: "seller_reversal", label: "برگشت فروشنده" },
{ value: "payout", label: "تسویه" },
]}
/>
</AdminToolbar>
<AdminTable
columns={columns}
rows={data?.results || []}
isLoading={isLoading}
getRowKey={(e) => e.id}
/>
<AdminPagination page={page} pageSize={PAGE_SIZE} count={data?.count || 0} onPageChange={setPage} />
</div>
);
}
function cnDelta(delta: number) {
return `tabular-nums whitespace-nowrap font-semibold ${
delta >= 0 ? "text-GREEN" : "text-RED"
}`;
}