Files
lotteryAdmin/src/modules/draws/draw-finance-console.tsx
kang 6ea0a6feec feat(agents, config, dashboard, i18n): add agent line provision wizard, site deletion, and site dashboard with multi-language support
Added agent line provision wizard page with permission gating, replacing redirect placeholder. Introduced site deletion API and UI with confirmation dialog in integration sites management. Added new site-scoped dashboard panel showing bet metrics, P/L trends, active players, and quick links. Enhanced chart tooltip to support custom formatters and fix indicator color
2026-06-12 20:47:53 +08:00

241 lines
9.5 KiB
TypeScript

"use client";
import Link from "next/link";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { getAdminDrawFinanceSummary } from "@/api/admin-draws";
import { postAdminRunDrawSettlement } from "@/api/admin-settlement";
import { Button, buttonVariants } from "@/components/ui/button";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { DrawStatusBadge } from "@/modules/draws/draw-status-badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { signedMoneyClass } from "@/lib/admin-signed-money";
import { cn } from "@/lib/utils";
import { useAdminProfile } from "@/stores/admin-session";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
import { toast } from "sonner";
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { useConfirmAction } from "@/hooks/use-confirm-action";
import { useExportLabels } from "@/hooks/use-export-labels";
import { formatAdminMinorUnits } from "@/lib/money";
import { drawStatusLabel, settlementBatchStatusLabel } from "./draw-display";
import { PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW } from "./draw-prd";
export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactElement {
const { t } = useTranslation(["draws", "settlement", "common"]);
const tRef = useTranslationRef(["draws", "settlement", "common"]);
useAdminCurrencyCatalog();
const idNum = Number(drawId);
const profile = useAdminProfile();
const canRunSettlement = adminHasAnyPermission(profile?.permissions, [
PRD_PAYOUT_MANAGE,
PRD_PAYOUT_REVIEW,
]);
const [data, setData] = useState<AdminDrawFinanceSummaryData | null>(null);
const formatTs = useAdminDateTimeFormatter();
const exportLabels = useExportLabels("drawFinance", { drawNo: data?.draw_no ?? drawId });
const [err, setErr] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [settling, setSettling] = useState(false);
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const load = useCallback(async () => {
if (!Number.isFinite(idNum) || idNum < 1) {
setErr(tRef.current("invalidDrawId"));
setLoading(false);
return;
}
setLoading(true);
setErr(null);
try {
setData(await getAdminDrawFinanceSummary(idNum));
} catch (e) {
setErr(e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }));
setData(null);
} finally {
setLoading(false);
}
}, [idNum]);
async function runSettlement(): Promise<void> {
if (!Number.isFinite(idNum) || idNum < 1) return;
setSettling(true);
try {
const res = await postAdminRunDrawSettlement(idNum);
toast.success(res.ran ? t("runSettlement") : t("status"));
await load();
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed", { name: t("runSettlement") }));
} finally {
setSettling(false);
}
}
useAsyncEffect(() => {
void load();
}, [idNum]);
if (loading && !data) {
return <AdminLoadingState minHeight="6rem" className="py-6" />;
}
if (err) {
return <p className="text-destructive text-sm">{err}</p>;
}
if (!data) {
return <AdminNoResourceState />;
}
const currencyCode = data.currency_code ?? "NPR";
const formatMoney = (minor: number) => formatAdminMinorUnits(minor, currencyCode);
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-lg">{t("financeOverview")}</CardTitle>
</CardHeader>
<CardContent className="grid gap-3 text-sm sm:grid-cols-2 lg:grid-cols-3">
<div>
<span className="text-muted-foreground">{t("drawNo")}</span>
<p className="font-mono font-semibold">{data.draw_no}</p>
</div>
<div>
<span className="text-muted-foreground">{t("status")}</span>
<p className="mt-1">
<DrawStatusBadge status={data.draw_status} label={drawStatusLabel(data.draw_status, t)} />
</p>
</div>
<div>
<span className="text-muted-foreground">{t("orderAndItemCount")}</span>
<p className="tabular-nums">
{data.order_count} / {data.ticket_item_count}
</p>
</div>
<div>
<span className="text-muted-foreground">{t("actualBet")}</span>
<p className="tabular-nums font-medium">{formatMoney(data.total_bet_minor)}</p>
</div>
<div>
<span className="text-muted-foreground">{t("currentPayout")}</span>
<p className="tabular-nums font-medium">{formatMoney(data.total_payout_minor)}</p>
</div>
<div>
<span className="text-muted-foreground">{t("grossProfit")}</span>
<p className={cn("tabular-nums font-semibold", signedMoneyClass(data.approx_house_gross_minor, true))}>
{formatMoney(data.approx_house_gross_minor)}
</p>
</div>
</CardContent>
</Card>
<div className="flex flex-wrap gap-2">
<Button type="button" variant="secondary" size="sm" onClick={() => void load()}>
{t("actions.refresh", { ns: "common" })}
</Button>
<Button
type="button"
size="sm"
disabled={!canRunSettlement || settling || data.draw_status !== "settling"}
onClick={() =>
requestConfirm({
title: t("confirm.runSettlementTitle"),
description: t("confirm.runSettlementDescription"),
onConfirm: () => runSettlement(),
})
}
>
{settling ? t("processing") : t("runSettlement")}
</Button>
<Link
href="/admin/settlement-batches"
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
>
{t("settlementBatchList")}
</Link>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base">{t("relatedSettlementBatches")}</CardTitle>
</CardHeader>
<CardContent>
{data.settlement_batches.length === 0 ? (
<AdminNoResourceState className="py-4" />
) : (
<div className="overflow-x-auto rounded-md border">
<div className="admin-table-toolbar">
<AdminTableExportButton
tableId={`draw-finance-table-${drawId}`}
filename={exportLabels.filename}
sheetName={exportLabels.sheetName}
/>
</div>
<Table id={`draw-finance-table-${drawId}`}>
<TableHeader>
<TableRow>
<TableHead className="w-20">{t("table.id", { ns: "common" })}</TableHead>
<TableHead>{t("status")}</TableHead>
<TableHead className="text-center">{t("ticketCount")}</TableHead>
<TableHead className="text-center">{t("winCount")}</TableHead>
<TableHead className="text-center">{t("payoutTotal")}</TableHead>
<TableHead className="text-center">{t("jackpotPayout")}</TableHead>
<TableHead>{t("finishedAt")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.settlement_batches.map((b) => (
<TableRow key={b.id}>
<TableCell className="font-mono text-xs">{b.id}</TableCell>
<TableCell>
<AdminStatusBadge status={b.status}>
{settlementBatchStatusLabel(b.status, t)}
</AdminStatusBadge>
</TableCell>
<TableCell className="text-center tabular-nums text-xs">
{b.total_ticket_count}
</TableCell>
<TableCell className="text-center tabular-nums text-xs">
{b.total_win_count}
</TableCell>
<TableCell className="text-center tabular-nums text-xs">
{formatMoney(b.total_payout_amount)}
</TableCell>
<TableCell className="text-center tabular-nums text-xs">
{formatMoney(b.total_jackpot_payout_amount)}
</TableCell>
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
{formatTs(b.finished_at)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
<ConfirmDialog />
</div>
);
}