Vitron-Front/app/routes/admin.ledger._index.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

149 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 {
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>
);
}