feat(admin, i18n): enhance reports, draws, config, and player workflows

This commit is contained in:
2026-06-08 17:41:55 +08:00
parent af982bb9f7
commit 7e65c53732
55 changed files with 1986 additions and 804 deletions

View File

@@ -9,6 +9,7 @@ import { toast } from "sonner";
import {
getAdminDraw,
getAdminDrawFinanceSummary,
postAdminCancelDraw,
postAdminManualCloseDraw,
postAdminReopenDraw,
@@ -22,11 +23,13 @@ import { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { useConfirmAction } from "@/hooks/use-confirm-action";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
import type { AdminDrawShowData } from "@/types/api/admin-draws";
import { canManageDrawResults } from "@/lib/draw-access";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { useAdminProfile } from "@/stores/admin-session";
import { cn } from "@/lib/utils";
import { formatAdminMinorUnits } from "@/lib/money";
import { drawStatusLabel, hallPreviewDiffersFromDbStatus } from "./draw-display";
import { DrawStatusBadge } from "./draw-status-badge";
@@ -80,6 +83,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [acting, setActing] = useState<string | null>(null);
const [financeSummary, setFinanceSummary] = useState<AdminDrawFinanceSummaryData | null>(null);
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const load = useCallback(async () => {
@@ -91,9 +95,20 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
setLoading(true);
setError(null);
try {
setData(await getAdminDraw(idNum));
const draw = await getAdminDraw(idNum);
setData(draw);
if (draw.capabilities?.can_view_draw_finance !== false) {
try {
setFinanceSummary(await getAdminDrawFinanceSummary(idNum));
} catch {
setFinanceSummary(null);
}
} else {
setFinanceSummary(null);
}
} catch (e) {
setData(null);
setFinanceSummary(null);
setError(e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }));
} finally {
setLoading(false);
@@ -225,6 +240,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
const batch = data.result_batch_counts;
const pendingReview = batch.pending_review ?? 0;
const totalBatches = batch.total ?? batch.published;
const financeCurrency = financeSummary?.currency_code ?? "NPR";
const hasResultActivity =
(canManageDraw && (totalBatches > 0 || pendingReview > 0)) || batch.published > 0;
const showActions =
@@ -236,6 +252,14 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
<CardHeader className="pb-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="mb-3">
<Link
href="/admin/draws"
className="text-sm font-medium text-primary underline-offset-4 hover:underline"
>
{t("backToList")}
</Link>
</div>
<CardTitle className="font-mono text-xl tracking-tight">{data.draw_no}</CardTitle>
<p className="mt-1 text-sm text-muted-foreground">
{t("detailSubtitle", {
@@ -263,6 +287,39 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
</CardHeader>
<CardContent className="space-y-6 border-t pt-6">
<section className="space-y-3">
<h3 className="text-sm font-medium">{t("overviewTitle")}</h3>
<div className="grid gap-3 sm:grid-cols-3">
<div className="rounded-lg border bg-muted/20 px-3 py-2.5">
<p className="text-xs font-medium text-muted-foreground">{t("overviewBetTotal")}</p>
<p className="mt-1 font-mono text-sm tabular-nums">
{formatAdminMinorUnits(
financeSummary?.total_bet_minor ?? data.total_bet_minor ?? 0,
financeCurrency,
)}
</p>
</div>
<div className="rounded-lg border bg-muted/20 px-3 py-2.5">
<p className="text-xs font-medium text-muted-foreground">{t("overviewPayoutTotal")}</p>
<p className="mt-1 font-mono text-sm tabular-nums">
{formatAdminMinorUnits(
financeSummary?.total_payout_minor ?? data.total_payout_minor ?? 0,
financeCurrency,
)}
</p>
</div>
<div className="rounded-lg border bg-muted/20 px-3 py-2.5">
<p className="text-xs font-medium text-muted-foreground">{t("overviewProfitLoss")}</p>
<p className="mt-1 font-mono text-sm tabular-nums">
{formatAdminMinorUnits(
financeSummary?.approx_house_gross_minor ?? data.profit_loss_minor ?? 0,
financeCurrency,
)}
</p>
</div>
</div>
</section>
<section className="space-y-3">
<h3 className="text-sm font-medium">{t("scheduleTitle")}</h3>
<ScheduleTimeline steps={scheduleSteps} />
@@ -307,6 +364,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
) : (
<p className="text-sm text-muted-foreground">
{t("noResultBatchesYet")}
<span className="ml-1">{t("reviewQueueHint")}</span>
{canManageDraw ? (
<>
{" "}