- 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>
157 lines
4.5 KiB
TypeScript
157 lines
4.5 KiB
TypeScript
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"
|
||
}`;
|
||
}
|