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 <noreply@anthropic.com>
This commit is contained in:
Arda Samadi 2026-07-25 12:44:59 +03:30
parent cedeef4746
commit 24d4c8a302
2 changed files with 91 additions and 0 deletions

View File

@ -302,3 +302,43 @@ export function useLedgerSellerDetail(id: string | undefined) {
adminGet<LedgerSellerDetail>(`ledger/sellers/${id}/`, token as string), adminGet<LedgerSellerDetail>(`ledger/sellers/${id}/`, token as string),
}); });
} }
async function adminPost<T>(
path: string,
body: Record<string, unknown>,
token: string
): Promise<T> {
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"] });
},
});
}

View File

@ -1,8 +1,11 @@
import { useState } from "react"; import { useState } from "react";
import { Check, X, Loader2 } from "lucide-react";
import { import {
useLedgerPayouts, useLedgerPayouts,
useLedgerPayoutAction,
type LedgerPayoutRow, type LedgerPayoutRow,
} from "~/requestHandler/use-admin-hooks"; } from "~/requestHandler/use-admin-hooks";
import { useToast } from "~/hooks/use-toast";
import { import {
AdminTable, AdminTable,
AdminPagination, AdminPagination,
@ -28,6 +31,8 @@ export default function LedgerPayouts() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const debouncedSearch = useDebouncedValue(search); const debouncedSearch = useDebouncedValue(search);
const action = useLedgerPayoutAction();
const { toast } = useToast();
const { data, isLoading } = useLedgerPayouts({ const { data, isLoading } = useLedgerPayouts({
page, page,
@ -41,6 +46,27 @@ export default function LedgerPayouts() {
setPage(1); 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>[] = [ const columns: Column<LedgerPayoutRow>[] = [
{ {
header: "فروشگاه", header: "فروشگاه",
@ -79,6 +105,31 @@ export default function LedgerPayouts() {
header: "پرداخت", header: "پرداخت",
render: (p) => <span className="text-GRAY whitespace-nowrap">{faDate(p.processedAt)}</span>, 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 ( return (