Vitron-Front/app/routes/admin.ledger.sellers.$id.tsx
Arda Samadi cedeef4746 feat(admin): ledger UI — overview/reconciliation, payouts, transactions, per-seller (slice 2a)
Ledger section with sub-tabs: overview (money held/owed, payouts by status, and
a reconciliation warning card), payouts + transactions lists, per-seller balances
list, and a per-seller detail showing recorded vs derived balance + discrepancy,
bank accounts and payout history. Enabled the Ledger nav item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:28:11 +03:30

151 lines
5.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 { Link, useParams } from "@remix-run/react";
import { Loader2, ArrowRight, BadgeCheck } from "lucide-react";
import { useLedgerSellerDetail } from "~/requestHandler/use-admin-hooks";
import {
AdminTable,
StatusBadge,
faToman,
faNum,
faDate,
type Column,
} from "~/components/admin/DataTable";
import type { LedgerPayoutRow } from "~/requestHandler/use-admin-hooks";
import { cn } from "~/lib/utils";
const PAYOUT_STATUS: Record<string, { label: string; tone: "green" | "red" | "yellow" }> = {
pending: { label: "در انتظار", tone: "yellow" },
completed: { label: "پرداخت‌شده", tone: "green" },
rejected: { label: "ردشده", tone: "red" },
};
function Stat({
label,
value,
accent,
}: {
label: string;
value: string;
accent?: "red" | "green";
}) {
return (
<div className="rounded-2xl border border-WHITE3 bg-WHITE p-4">
<p className="text-[12px] text-GRAY">{label}</p>
<p
className={cn(
"font-extrabold tabular-nums mt-1 text-[18px]",
accent === "red" && "text-RED",
accent === "green" && "text-GREEN"
)}
>
{value}
</p>
</div>
);
}
export default function LedgerSellerDetail() {
const { id } = useParams();
const { data, isLoading, isError } = useLedgerSellerDetail(id);
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 discrepant = Math.abs(data.discrepancy) > 1;
const payoutColumns: Column<LedgerPayoutRow>[] = [
{ header: "مبلغ", render: (p) => <span className="tabular-nums whitespace-nowrap">{faToman(p.amount)}</span> },
{
header: "وضعیت",
render: (p) => {
const s = PAYOUT_STATUS[p.status] || { label: p.status, tone: "yellow" as const };
return <StatusBadge label={s.label} tone={s.tone} />;
},
},
{ header: "درخواست", render: (p) => <span className="text-GRAY whitespace-nowrap">{faDate(p.requestedAt)}</span> },
{ header: "پرداخت", render: (p) => <span className="text-GRAY whitespace-nowrap">{faDate(p.processedAt)}</span> },
];
return (
<div>
<div className="flex items-center gap-2 mb-4">
<Link
to="/admin/ledger/sellers"
className="grid h-8 w-8 place-items-center rounded-lg border border-WHITE3 hover:bg-WHITE2"
>
<ArrowRight size={16} />
</Link>
<div>
<h2 className="font-extrabold text-[18px]">{data.seller.storeName}</h2>
<p className="text-GRAY text-[12px]" dir="ltr">
@{data.seller.username} · کمیسیون {faNum(data.seller.platformFee)}٪
</p>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3">
<Stat label="مانده ثبت‌شده" value={faToman(data.recordedBalance)} />
<Stat label="محاسبه‌شده از سفارش‌ها" value={faToman(data.accruedFromOrders)} />
<Stat label="مانده واقعی (تخمینی)" value={faToman(data.derivedBalance)} accent="green" />
<Stat label="برداشت‌های پرداخت‌شده" value={faToman(data.completedPayouts)} />
<Stat label="در انتظار برداشت" value={faToman(data.pendingPayouts)} />
<Stat
label="اختلاف"
value={faToman(data.discrepancy)}
accent={discrepant ? "red" : "green"}
/>
</div>
{discrepant && (
<p className="mt-3 text-[12px] text-RED">
مانده ثبتشده با مقدار محاسبهشده مغایرت دارد (بهاحتمال زیاد بهدلیل عدم
کسر برداشتهای پرداختشده از مانده).
</p>
)}
{/* Bank accounts */}
<h3 className="text-[15px] font-extrabold mt-7 mb-3">حسابهای بانکی</h3>
{data.bankAccounts.length === 0 ? (
<p className="text-GRAY text-[13px]">حساب بانکی ثبت نشده است.</p>
) : (
<div className="flex flex-col gap-2">
{data.bankAccounts.map((b) => (
<div
key={b.id}
className="rounded-xl border border-WHITE3 bg-WHITE p-3 flex items-center justify-between"
>
<div className="text-[13px]">
<p className="font-semibold">{b.accountHolder}</p>
<p className="text-GRAY text-[11px] tabular-nums" dir="ltr">
{b.iban} · {b.bankName}
</p>
</div>
{b.isVerified ? (
<span className="flex items-center gap-1 text-[12px] text-VITROWN_BLUE">
<BadgeCheck size={15} /> تأییدشده
</span>
) : (
<StatusBadge label="تأییدنشده" tone="gray" />
)}
</div>
))}
</div>
)}
{/* Payout history */}
<h3 className="text-[15px] font-extrabold mt-7 mb-3">تاریخچه برداشت</h3>
<AdminTable
columns={payoutColumns}
rows={data.payouts}
getRowKey={(p) => p.id}
emptyText="برداشتی ثبت نشده است"
/>
</div>
);
}