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),
|
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 { 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 (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user