- 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>
149 lines
6.3 KiB
TypeScript
149 lines
6.3 KiB
TypeScript
import {
|
||
Loader2,
|
||
Wallet,
|
||
Landmark,
|
||
ArrowDownCircle,
|
||
ArrowUpCircle,
|
||
AlertTriangle,
|
||
Clock,
|
||
CheckCircle2,
|
||
} from "lucide-react";
|
||
import { useLedgerOverview } from "~/requestHandler/use-admin-hooks";
|
||
import { KpiCard } from "~/components/admin/KpiCard";
|
||
import { faToman, faNum } from "~/components/admin/DataTable";
|
||
|
||
export default function LedgerOverview() {
|
||
const { data, isLoading, isError } = useLedgerOverview();
|
||
|
||
if (isLoading)
|
||
return (
|
||
<div className="flex min-h-[40vh] items-center justify-center">
|
||
<Loader2 className="h-7 w-7 animate-spin text-GRAY" />
|
||
</div>
|
||
);
|
||
if (isError || !data)
|
||
return <p className="text-RED text-[14px]">خطا در بارگذاری دفتر مالی</p>;
|
||
|
||
const { payouts, reconciliation, journal } = data;
|
||
const hasDiscrepancy = reconciliation.completedPayoutsNotDebited > 0;
|
||
const driftClean =
|
||
Math.abs(journal.sellerPayableDrift) < 1 && Math.abs(journal.walletHeldDrift) < 1;
|
||
|
||
return (
|
||
<div>
|
||
{/* Money held / owed */}
|
||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||
<KpiCard label="موجودی کیف پول کاربران" value={faToman(data.walletHeld)} icon={Wallet} />
|
||
<KpiCard label="مانده قابل پرداخت فروشندگان" value={faToman(data.sellerPayable)} icon={Landmark} />
|
||
<KpiCard label="کل واریزها (شارژ کیف پول)" value={faToman(data.depositsTotal)} icon={ArrowDownCircle} accent="green" />
|
||
<KpiCard label="کل خریدها" value={faToman(data.purchasesTotal)} icon={ArrowUpCircle} />
|
||
</div>
|
||
|
||
{/* Payouts by status */}
|
||
<h2 className="text-[15px] font-extrabold mt-7 mb-3">برداشتها</h2>
|
||
<div className="grid grid-cols-3 gap-3">
|
||
<KpiCard
|
||
label="در انتظار"
|
||
value={faToman(payouts.pending.amount)}
|
||
sub={`${faNum(payouts.pending.count)} درخواست`}
|
||
icon={Clock}
|
||
accent={payouts.pending.count > 0 ? "red" : "default"}
|
||
/>
|
||
<KpiCard
|
||
label="پرداختشده"
|
||
value={faToman(payouts.completed.amount)}
|
||
sub={`${faNum(payouts.completed.count)} مورد`}
|
||
icon={CheckCircle2}
|
||
accent="green"
|
||
/>
|
||
<KpiCard
|
||
label="ردشده"
|
||
value={faToman(payouts.rejected.amount)}
|
||
sub={`${faNum(payouts.rejected.count)} مورد`}
|
||
/>
|
||
</div>
|
||
|
||
{/* Journal-derived truth (Slice 2c) */}
|
||
<div className="mt-7 flex items-center gap-2 mb-3">
|
||
<h2 className="text-[15px] font-extrabold">دفتر روزنامهای (منبع حقیقت)</h2>
|
||
<span
|
||
className={`text-[11px] font-semibold px-2 py-0.5 rounded-full ${
|
||
driftClean ? "bg-GREEN/10 text-GREEN" : "bg-yellow-500/10 text-yellow-600"
|
||
}`}
|
||
>
|
||
{driftClean ? "همخوان با ثبتشده" : "نیازمند بکفیل"}
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3">
|
||
<KpiCard
|
||
label="مانده فروشندگان (روزنامه)"
|
||
value={faToman(journal.sellerPayable)}
|
||
sub={`اختلاف با ثبتشده: ${faToman(journal.sellerPayableDrift)}`}
|
||
icon={Landmark}
|
||
accent={Math.abs(journal.sellerPayableDrift) < 1 ? "green" : "red"}
|
||
/>
|
||
<KpiCard
|
||
label="کیف پول کاربران (روزنامه)"
|
||
value={faToman(journal.walletHeld)}
|
||
sub={`اختلاف با ثبتشده: ${faToman(journal.walletHeldDrift)}`}
|
||
icon={Wallet}
|
||
accent={Math.abs(journal.walletHeldDrift) < 1 ? "green" : "red"}
|
||
/>
|
||
<KpiCard
|
||
label="درآمد پلتفرم (کارمزد)"
|
||
value={faToman(journal.platformRevenue)}
|
||
icon={ArrowUpCircle}
|
||
accent="green"
|
||
/>
|
||
</div>
|
||
|
||
{/* Reconciliation warning */}
|
||
{hasDiscrepancy && (
|
||
<div className="mt-7 rounded-2xl border border-RED/30 bg-RED/5 p-4">
|
||
<div className="flex items-start gap-3">
|
||
<AlertTriangle className="h-5 w-5 text-RED shrink-0 mt-0.5" />
|
||
<div className="flex-1">
|
||
<h3 className="font-extrabold text-[15px] text-RED">
|
||
مغایرت حسابداری شناسایی شد
|
||
</h3>
|
||
<p className="text-[13px] text-BLACK2 mt-1 leading-6">
|
||
مبلغ{" "}
|
||
<b className="tabular-nums">
|
||
{faToman(reconciliation.completedPayoutsNotDebited)}
|
||
</b>{" "}
|
||
بهعنوان برداشت «پرداختشده» ثبت شده اما از مانده فروشندگان کسر
|
||
نشده است. به همین دلیل «مانده قابل پرداخت» فعلی احتمالاً بیش از
|
||
مقدار واقعی است.
|
||
</p>
|
||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3 mt-3">
|
||
<div className="rounded-xl bg-WHITE border border-WHITE3 p-3">
|
||
<p className="text-[12px] text-GRAY">مانده ثبتشده</p>
|
||
<p className="font-bold tabular-nums mt-1">
|
||
{faToman(reconciliation.sellerPayableRecorded)}
|
||
</p>
|
||
</div>
|
||
<div className="rounded-xl bg-WHITE border border-WHITE3 p-3">
|
||
<p className="text-[12px] text-GRAY">برآورد مانده واقعی</p>
|
||
<p className="font-bold tabular-nums mt-1 text-GREEN">
|
||
{faToman(reconciliation.sellerPayableEstimatedTrue)}
|
||
</p>
|
||
</div>
|
||
<div className="rounded-xl bg-WHITE border border-WHITE3 p-3">
|
||
<p className="text-[12px] text-GRAY">اختلاف</p>
|
||
<p className="font-bold tabular-nums mt-1 text-RED">
|
||
{faToman(reconciliation.completedPayoutsNotDebited)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-[12px] text-GRAY mt-3">
|
||
این مورد در فاز بعدی (دفتر روزنامهای و اصلاح جریان پول) رفع خواهد
|
||
شد.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|