From 24d4c8a3029545c47a4011fc4833d478fbce6133 Mon Sep 17 00:00:00 2001 From: Arda Samadi Date: Sat, 25 Jul 2026 12:44:59 +0330 Subject: [PATCH] feat(admin): payout approve/reject actions on the ledger (slice 2b) 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 --- app/requestHandler/use-admin-hooks.ts | 40 +++++++++++++++++++++ app/routes/admin.ledger.payouts.tsx | 51 +++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/app/requestHandler/use-admin-hooks.ts b/app/requestHandler/use-admin-hooks.ts index 57f09f3..54def57 100644 --- a/app/requestHandler/use-admin-hooks.ts +++ b/app/requestHandler/use-admin-hooks.ts @@ -302,3 +302,43 @@ export function useLedgerSellerDetail(id: string | undefined) { adminGet(`ledger/sellers/${id}/`, token as string), }); } + +async function adminPost( + path: string, + body: Record, + token: string +): Promise { + const res = await fetch(`${getApiBaseUrl()}/api/adminpanel/v1/${path}`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + if (!res.ok) { + let detail = `Admin action failed (${res.status})`; + try { + const j = await res.json(); + if (j?.detail) detail = j.detail; + } catch { + /* ignore */ + } + throw new Error(detail); + } + return res.json(); +} + +/** Approve (complete → debits balance + journals) or reject a pending payout. */ +export function useLedgerPayoutAction() { + const token = useAuthToken(); + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, action }: { id: string; action: "complete" | "reject" }) => + adminPost(`ledger/payouts/${id}/action/`, { action }, token as string), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["admin", "ledger"] }); + qc.invalidateQueries({ queryKey: ["admin", "overview"] }); + }, + }); +} diff --git a/app/routes/admin.ledger.payouts.tsx b/app/routes/admin.ledger.payouts.tsx index 16d481a..2190b4e 100644 --- a/app/routes/admin.ledger.payouts.tsx +++ b/app/routes/admin.ledger.payouts.tsx @@ -1,8 +1,11 @@ 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, @@ -28,6 +31,8 @@ export default function LedgerPayouts() { const [search, setSearch] = useState(""); const [status, setStatus] = useState(""); const debouncedSearch = useDebouncedValue(search); + const action = useLedgerPayoutAction(); + const { toast } = useToast(); const { data, isLoading } = useLedgerPayouts({ page, @@ -41,6 +46,27 @@ export default function LedgerPayouts() { 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[] = [ { header: "فروشگاه", @@ -79,6 +105,31 @@ export default function LedgerPayouts() { header: "پرداخت", render: (p) => {faDate(p.processedAt)}, }, + { + header: "عملیات", + className: "text-left", + render: (p) => { + if (p.status !== "pending") return ; + if (pendingId === p.id) + return ; + return ( +
+ + +
+ ); + }, + }, ]; return (