Approve (complete) / reject buttons on pending payouts, hitting the new admin action endpoint (which debits the balance + journals on approve). Toasts + list and overview invalidation on success. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
164 lines
4.9 KiB
TypeScript
164 lines
4.9 KiB
TypeScript
import { useState } from "react";
|
|
import { Check, X, Loader2 } from "lucide-react";
|
|
import {
|
|
useLedgerPayouts,
|
|
useLedgerPayoutAction,
|
|
type LedgerPayoutRow,
|
|
} from "~/requestHandler/use-admin-hooks";
|
|
import { useToast } from "~/hooks/use-toast";
|
|
import {
|
|
AdminTable,
|
|
AdminPagination,
|
|
AdminToolbar,
|
|
AdminSearch,
|
|
AdminSelect,
|
|
StatusBadge,
|
|
faToman,
|
|
faDate,
|
|
useDebouncedValue,
|
|
type Column,
|
|
} from "~/components/admin/DataTable";
|
|
|
|
const PAGE_SIZE = 25;
|
|
const STATUS: Record<string, { label: string; tone: "green" | "red" | "yellow" }> = {
|
|
pending: { label: "در انتظار", tone: "yellow" },
|
|
completed: { label: "پرداختشده", tone: "green" },
|
|
rejected: { label: "ردشده", tone: "red" },
|
|
};
|
|
|
|
export default function LedgerPayouts() {
|
|
const [page, setPage] = useState(1);
|
|
const [search, setSearch] = useState("");
|
|
const [status, setStatus] = useState("");
|
|
const debouncedSearch = useDebouncedValue(search);
|
|
const action = useLedgerPayoutAction();
|
|
const { toast } = useToast();
|
|
|
|
const { data, isLoading } = useLedgerPayouts({
|
|
page,
|
|
page_size: PAGE_SIZE,
|
|
search: debouncedSearch,
|
|
status,
|
|
});
|
|
|
|
const reset = (fn: () => void) => {
|
|
fn();
|
|
setPage(1);
|
|
};
|
|
|
|
const run = (id: string, act: "complete" | "reject") =>
|
|
action.mutate(
|
|
{ id, action: act },
|
|
{
|
|
onSuccess: () =>
|
|
toast({
|
|
title: act === "complete" ? "برداشت تأیید شد" : "برداشت رد شد",
|
|
description:
|
|
act === "complete"
|
|
? "مبلغ از مانده فروشنده کسر و در دفتر ثبت شد."
|
|
: undefined,
|
|
}),
|
|
onError: (e: Error) =>
|
|
toast({ title: "خطا", description: e.message, variant: "destructive" }),
|
|
}
|
|
);
|
|
|
|
const pendingId = action.isPending ? action.variables?.id : null;
|
|
const actBtn =
|
|
"inline-flex items-center gap-1 px-2 py-1 rounded-lg text-[12px] font-semibold transition-colors disabled:opacity-40 cursor-pointer";
|
|
|
|
const columns: Column<LedgerPayoutRow>[] = [
|
|
{
|
|
header: "فروشگاه",
|
|
render: (p) => (
|
|
<div className="min-w-[130px]">
|
|
<p className="font-semibold">{p.sellerName || "—"}</p>
|
|
<p className="text-GRAY text-[11px]" dir="ltr">@{p.sellerUsername}</p>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
header: "مبلغ",
|
|
render: (p) => <span className="tabular-nums whitespace-nowrap">{faToman(p.amount)}</span>,
|
|
},
|
|
{
|
|
header: "وضعیت",
|
|
render: (p) => {
|
|
const s = STATUS[p.status] || { label: p.status, tone: "yellow" as const };
|
|
return <StatusBadge label={s.label} tone={s.tone} />;
|
|
},
|
|
},
|
|
{
|
|
header: "بانک",
|
|
render: (p) => (
|
|
<div className="min-w-[140px] text-[11px] text-GRAY" dir="ltr">
|
|
<p>{p.bankName || "—"}</p>
|
|
<p className="tabular-nums">{p.iban || ""}</p>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
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>,
|
|
},
|
|
{
|
|
header: "عملیات",
|
|
className: "text-left",
|
|
render: (p) => {
|
|
if (p.status !== "pending") return <span className="text-GRAY">—</span>;
|
|
if (pendingId === p.id)
|
|
return <Loader2 size={16} className="animate-spin text-GRAY inline" />;
|
|
return (
|
|
<div className="flex items-center gap-1.5 justify-end">
|
|
<button
|
|
className={`${actBtn} bg-GREEN/10 text-GREEN hover:bg-GREEN/20`}
|
|
onClick={() => run(p.id, "complete")}
|
|
>
|
|
<Check size={13} /> تأیید
|
|
</button>
|
|
<button
|
|
className={`${actBtn} bg-RED/10 text-RED hover:bg-RED/20`}
|
|
onClick={() => run(p.id, "reject")}
|
|
>
|
|
<X size={13} /> رد
|
|
</button>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<AdminToolbar>
|
|
<AdminSearch
|
|
value={search}
|
|
onChange={(v) => reset(() => setSearch(v))}
|
|
placeholder="جستجوی فروشگاه…"
|
|
/>
|
|
<AdminSelect
|
|
value={status}
|
|
onChange={(v) => reset(() => setStatus(v))}
|
|
options={[
|
|
{ value: "", label: "همه وضعیتها" },
|
|
{ value: "pending", label: "در انتظار" },
|
|
{ value: "completed", label: "پرداختشده" },
|
|
{ value: "rejected", label: "ردشده" },
|
|
]}
|
|
/>
|
|
</AdminToolbar>
|
|
<AdminTable
|
|
columns={columns}
|
|
rows={data?.results || []}
|
|
isLoading={isLoading}
|
|
getRowKey={(p) => p.id}
|
|
/>
|
|
<AdminPagination page={page} pageSize={PAGE_SIZE} count={data?.count || 0} onPageChange={setPage} />
|
|
</div>
|
|
);
|
|
}
|