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
241 lines
9.5 KiB
TypeScript
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>
|
|
);
|
|
}
|