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:
parent
cedeef4746
commit
24d4c8a302
@ -302,3 +302,43 @@ export function useLedgerSellerDetail(id: string | undefined) {
|
||||
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"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -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<LedgerPayoutRow>[] = [
|
||||
{
|
||||
header: "فروشگاه",
|
||||
@ -79,6 +105,31 @@ export default function LedgerPayouts() {
|
||||
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 (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user