+
= 0 ? "bg-emerald-500" : "bg-amber-500"),
+ activeMetric === "bet" && "bg-primary",
+ )}
+ style={{ width: `${pct}%` }}
+ />
+
+ {metric === "overview" ? (
+
+ {t("playBreakdownHint", {
+ payout: formatMoney(row.total_payout_minor, currency),
+ profit: formatMoney(row.approx_house_gross_minor, currency),
+ })}
+
+ ) : null}
+
+ );
+ })}
+
+ );
+}
+
+export function PeriodCompareStrip({
+ series,
+ formatMoney,
+ currency,
+}: {
+ series: AdminReportDailyProfitRow[];
+ formatMoney: MoneyFormatter;
+ currency: string | null;
+}): ReactElement {
+ const { t } = useTranslation("dashboard");
+ const totalBet = series.reduce((s, d) => s + d.total_bet_minor, 0);
+ const totalPayout = series.reduce((s, d) => s + d.total_payout_minor, 0);
+ const totalProfit = series.reduce((s, d) => s + d.approx_house_gross_minor, 0);
+ const max = Math.max(totalBet, totalPayout, Math.abs(totalProfit), 1);
+
+ const items = [
+ { key: "bet", label: t("chartLegend.bet"), value: totalBet, className: "bg-primary" },
+ { key: "payout", label: t("chartLegend.payout"), value: totalPayout, className: "bg-rose-500" },
+ {
+ key: "profit",
+ label: t("chartLegend.profit"),
+ value: totalProfit,
+ className: totalProfit >= 0 ? "bg-emerald-500" : "bg-amber-500",
+ },
+ ];
+
+ return (
+
+ {items.map((item) => (
+
+
+
+
+ {item.label}
+
+ {formatMoney(item.value, currency)}
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/modules/dashboard/dashboard-visuals.tsx b/src/modules/dashboard/dashboard-visuals.tsx
index 9c44295..b591884 100644
--- a/src/modules/dashboard/dashboard-visuals.tsx
+++ b/src/modules/dashboard/dashboard-visuals.tsx
@@ -123,8 +123,8 @@ export function FinanceStructureChart({
const payoutRate = ((payout / bet) * 100).toFixed(1);
const segments = [
- { key: "win", width: winW, className: "bg-chart-2", label: t("winPayout"), value: win },
- { key: "jackpot", width: jpW, className: "bg-chart-4", label: t("jackpotPayout"), value: jackpot },
+ { key: "win", width: winW, className: "bg-emerald-500", label: t("winPayout"), value: win },
+ { key: "jackpot", width: jpW, className: "bg-violet-500", label: t("jackpotPayout"), value: jackpot },
{ key: "gross", width: grossW, className: "bg-primary", label: t("houseGross"), value: gross },
].filter((s) => s.width > 0.05);
@@ -176,9 +176,17 @@ export function PayoutCompositionChart({
}
const winPct = (win / total) * 100;
+ const winColor = "oklch(0.62 0.17 162)";
+ const jackpotColor = "oklch(0.56 0.22 303)";
const items = [
- { label: t("winPayout"), value: win, pct: winPct, className: "bg-chart-2" },
- { label: t("jackpotPayout"), value: jackpot, pct: 100 - winPct, className: "bg-chart-4" },
+ { label: t("winPayout"), value: win, pct: winPct, className: "bg-emerald-500", color: winColor },
+ {
+ label: t("jackpotPayout"),
+ value: jackpot,
+ pct: 100 - winPct,
+ className: "bg-violet-500",
+ color: jackpotColor,
+ },
];
return (
@@ -186,7 +194,7 @@ export function PayoutCompositionChart({
{formatMoney(item.value, currency)}
))}
@@ -249,12 +260,12 @@ export function HotUsageBars({ rows }: { rows: AdminRiskPoolRow[] }): ReactEleme
export function SoldOutRing({ buckets }: { buckets: SoldOutBuckets }): ReactElement {
const { t } = useTranslation("dashboard");
- const entries: { key: keyof SoldOutBuckets; label: string; color: string }[] = [
- { key: "d4", label: t("soldOutBuckets.d4"), color: "var(--chart-1)" },
- { key: "d3", label: t("soldOutBuckets.d3"), color: "var(--chart-2)" },
- { key: "d2", label: t("soldOutBuckets.d2"), color: "var(--chart-3)" },
- { key: "special", label: t("soldOutBuckets.special"), color: "var(--chart-4)" },
- { key: "other", label: t("soldOutBuckets.other"), color: "var(--chart-5)" },
+ const entries: { key: keyof SoldOutBuckets; label: string; color: string; swatch: string }[] = [
+ { key: "d4", label: t("soldOutBuckets.d4"), color: "oklch(0.52 0.19 264)", swatch: "bg-blue-600" },
+ { key: "d3", label: t("soldOutBuckets.d3"), color: "oklch(0.62 0.17 162)", swatch: "bg-emerald-500" },
+ { key: "d2", label: t("soldOutBuckets.d2"), color: "oklch(0.72 0.16 75)", swatch: "bg-amber-500" },
+ { key: "special", label: t("soldOutBuckets.special"), color: "oklch(0.56 0.22 303)", swatch: "bg-violet-500" },
+ { key: "other", label: t("soldOutBuckets.other"), color: "oklch(0.58 0.2 25)", swatch: "bg-rose-500" },
];
const total = entries.reduce((s, e) => s + buckets[e.key], 0);
@@ -307,7 +318,7 @@ export function SoldOutRing({ buckets }: { buckets: SoldOutBuckets }): ReactElem
-
+
{e.label}
@@ -381,6 +392,25 @@ export function SettlementStatusChart({
const entries = [...counts.entries()].sort((a, b) => b[1] - a[1]);
const max = Math.max(...entries.map((e) => e[1]));
+ const barTone = (status: string): string => {
+ switch (status) {
+ case "pending_review":
+ return "bg-amber-500";
+ case "approved":
+ return "bg-sky-500";
+ case "paid":
+ case "completed":
+ return "bg-emerald-600";
+ case "running":
+ return "bg-blue-500";
+ case "rejected":
+ case "failed":
+ return "bg-rose-500";
+ default:
+ return "bg-violet-500";
+ }
+ };
+
return (
{entries.map(([status, count]) => (
@@ -391,7 +421,7 @@ export function SettlementStatusChart({
0 ? (count / max) * 100 : 0}%` }}
/>
diff --git a/src/modules/draws/draw-detail-console.tsx b/src/modules/draws/draw-detail-console.tsx
index f4126b2..37a597f 100644
--- a/src/modules/draws/draw-detail-console.tsx
+++ b/src/modules/draws/draw-detail-console.tsx
@@ -18,6 +18,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
+import { useConfirmAction } from "@/hooks/use-confirm-action";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminDrawShowData } from "@/types/api/admin-draws";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
@@ -25,7 +26,11 @@ import { useAdminProfile } from "@/stores/admin-session";
import { cn } from "@/lib/utils";
-import { drawResultSourceLabel, drawStatusLabel } from "./draw-display";
+import {
+ drawResultSourceLabel,
+ drawStatusLabel,
+ hallPreviewDiffersFromDbStatus,
+} from "./draw-display";
import { DrawStatusBadge } from "./draw-status-badge";
import {
PRD_DRAW_REOPEN_MANAGE,
@@ -58,6 +63,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
const [error, setError] = useState
(null);
const [loading, setLoading] = useState(true);
const [acting, setActing] = useState(null);
+ const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const load = useCallback(async () => {
if (!Number.isFinite(idNum)) {
@@ -120,13 +126,15 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
status={data.status}
label={drawStatusLabel(data.status, t)}
/>
-
- {t("hallPreviewStatusLabel")}
-
-
+ {hallPreviewDiffersFromDbStatus(data.status, data.hall_preview_status) ? (
+
+ {t("hallPreviewStatusLabel")}
+
+
+ ) : null}
@@ -186,7 +194,13 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
variant="outline"
size="sm"
disabled={!canManageDraw || acting !== null || !["pending", "open"].includes(data.status)}
- onClick={() => void runAction(t("manualClose"), () => postAdminManualCloseDraw(idNum))}
+ onClick={() =>
+ requestConfirm({
+ title: t("confirm.manualCloseTitle"),
+ description: t("confirm.manualCloseDescription"),
+ onConfirm: () => runAction(t("manualClose"), () => postAdminManualCloseDraw(idNum)),
+ })
+ }
>
{acting === t("manualClose") ? t("processing") : t("manualClose")}
@@ -195,7 +209,13 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
variant="outline"
size="sm"
disabled={!canManageDraw || acting !== null || !["pending", "open", "closing", "closed"].includes(data.status)}
- onClick={() => void runAction(t("cancelDraw"), () => postAdminCancelDraw(idNum))}
+ onClick={() =>
+ requestConfirm({
+ title: t("confirm.cancelDrawTitle"),
+ description: t("confirm.cancelDrawDescription"),
+ onConfirm: () => runAction(t("cancelDraw"), () => postAdminCancelDraw(idNum)),
+ })
+ }
>
{acting === t("cancelDraw") ? t("processing") : t("cancelBeforeDraw")}
@@ -204,7 +224,13 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
variant="outline"
size="sm"
disabled={!canManageDraw || acting !== null || data.status !== "closed"}
- onClick={() => void runAction(t("rngDraw"), () => postAdminRunDrawRng(idNum))}
+ onClick={() =>
+ requestConfirm({
+ title: t("confirm.rngDrawTitle"),
+ description: t("confirm.rngDrawDescription"),
+ onConfirm: () => runAction(t("rngDraw"), () => postAdminRunDrawRng(idNum)),
+ })
+ }
>
{acting === t("rngDraw") ? t("generating") : t("rngAutoGenerate")}
@@ -214,7 +240,14 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
variant="destructive"
size="sm"
disabled={acting !== null || data.status !== "cooldown"}
- onClick={() => void runAction(t("reopen"), () => postAdminReopenDraw(idNum))}
+ onClick={() =>
+ requestConfirm({
+ title: t("confirm.reopenTitle"),
+ description: t("confirm.reopenDescription"),
+ confirmVariant: "destructive",
+ onConfirm: () => runAction(t("reopen"), () => postAdminReopenDraw(idNum)),
+ })
+ }
>
{acting === t("reopen") ? t("processing") : t("cooldownReopen")}
@@ -224,13 +257,20 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
variant="outline"
size="sm"
disabled={!canRunSettlement || acting !== null || data.status !== "settling"}
- onClick={() => void runAction(t("runSettlement"), () => postAdminRunDrawSettlement(idNum))}
+ onClick={() =>
+ requestConfirm({
+ title: t("confirm.runSettlementTitle"),
+ description: t("confirm.runSettlementDescription"),
+ onConfirm: () => runAction(t("runSettlement"), () => postAdminRunDrawSettlement(idNum)),
+ })
+ }
>
{acting === t("runSettlement") ? t("processing") : t("runSettlement")}
) : null}
+
);
}
diff --git a/src/modules/draws/draw-display.ts b/src/modules/draws/draw-display.ts
index 69f5a32..8e06e51 100644
--- a/src/modules/draws/draw-display.ts
+++ b/src/modules/draws/draw-display.ts
@@ -3,6 +3,14 @@ type DrawTranslate = (
options?: { ns?: string; index?: number },
) => string;
+/** 大厅展示态是否与库内期号状态不同(仅 open 等 tick 修正时可能不同) */
+export function hallPreviewDiffersFromDbStatus(
+ dbStatus: string,
+ hallPreviewStatus: string,
+): boolean {
+ return dbStatus !== hallPreviewStatus;
+}
+
/** 期号状态文案(draws.statusOptions) */
export function drawStatusLabel(status: string, t: DrawTranslate): string {
const key = `statusOptions.${status}`;
diff --git a/src/modules/draws/draw-finance-console.tsx b/src/modules/draws/draw-finance-console.tsx
index 10a94ed..4409b58 100644
--- a/src/modules/draws/draw-finance-console.tsx
+++ b/src/modules/draws/draw-finance-console.tsx
@@ -27,6 +27,7 @@ import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance
import { toast } from "sonner";
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
+import { useConfirmAction } from "@/hooks/use-confirm-action";
import { useExportLabels } from "@/hooks/use-export-labels";
import { formatAdminMinorUnits } from "@/lib/money";
@@ -47,6 +48,7 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
const [err, setErr] = useState