diff --git a/src/app/globals.css b/src/app/globals.css index 314d9e2..5a11a1c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -175,13 +175,36 @@ @apply text-sm text-muted-foreground; } - [data-slot="table-cell"]:has(> [data-slot="button"]), - [data-slot="table-cell"]:has(> a) { + [data-slot="table-head"], + [data-slot="table-cell"] { text-align: center; } - [data-slot="table-cell"] > .flex:has([data-slot="button"]), - [data-slot="table-cell"] > .flex:has(a) { - justify-content: center; + [data-slot="table-cell"] > .flex, + [data-slot="table-head"] > .flex { + justify-content: center !important; + } + + [data-slot="table-cell"] > .flex.flex-col { + align-items: center !important; + } + + [data-slot="table-cell"] > .flex.flex-wrap { + justify-content: center !important; + } + + [data-slot="table-cell"] [data-slot="badge"], + [data-slot="table-cell"] [data-slot="switch"], + [data-slot="table-cell"] [role="checkbox"], + [data-slot="table-cell"] [data-slot="button"], + [data-slot="table-cell"] > a { + margin-inline: auto; + } + + [data-slot="table-cell"]:has(> [data-slot="button"]), + [data-slot="table-cell"]:has(> a), + [data-slot="table-cell"]:has(> [role="checkbox"]), + [data-slot="table-cell"]:has(> [data-slot="switch"]) { + text-align: center; } } diff --git a/src/components/admin/admin-table-export-button.tsx b/src/components/admin/admin-table-export-button.tsx index 27d383e..881d8ee 100644 --- a/src/components/admin/admin-table-export-button.tsx +++ b/src/components/admin/admin-table-export-button.tsx @@ -15,7 +15,10 @@ const OMIT_HEADER_TOKENS = [ "download", ] as const; -function shouldOmitColumn(headerText: string): boolean { +function shouldOmitColumn(cell: Element, headerText: string): boolean { + if (cell.hasAttribute("data-export-ignore")) { + return true; + } const normalized = headerText.trim().toLowerCase(); if (normalized === "") { return true; @@ -33,9 +36,10 @@ function stripOmittedColumns(table: HTMLTableElement): HTMLTableElement { const omitIndexes = Array.from(headerRow.children) .map((cell, index) => ({ index, + cell, text: cell.textContent ?? "", })) - .filter((item) => shouldOmitColumn(item.text)) + .filter((item) => shouldOmitColumn(item.cell, item.text)) .map((item) => item.index) .sort((a, b) => b - a); diff --git a/src/components/admin/confirmable-switch.tsx b/src/components/admin/confirmable-switch.tsx new file mode 100644 index 0000000..e2dcc12 --- /dev/null +++ b/src/components/admin/confirmable-switch.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { Switch } from "@/components/ui/switch"; + +export type ConfirmableSwitchProps = { + checked: boolean; + disabled?: boolean; + confirmBusy?: boolean; + "aria-label": string; + onCheckedChange: (checked: boolean) => void; +}; + +/** + * Controlled switch for flows that confirm before applying state. + * Disables interaction while a confirm dialog is in progress. + */ +export function ConfirmableSwitch({ + checked, + disabled, + confirmBusy, + "aria-label": ariaLabel, + onCheckedChange, +}: ConfirmableSwitchProps) { + return ( + + ); +} diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index cf34b3b..241c713 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) { ) { -
- -
+ + {role.status === 1 ? t("status.enabled") : t("status.disabled")} +
{role.user_count} {role.permission_slugs.length} diff --git a/src/modules/admin-users/admin-users-console.tsx b/src/modules/admin-users/admin-users-console.tsx index eaf0848..294930f 100644 --- a/src/modules/admin-users/admin-users-console.tsx +++ b/src/modules/admin-users/admin-users-console.tsx @@ -16,7 +16,9 @@ import { } from "@/api/admin-users"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; +import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { Badge } from "@/components/ui/badge"; +import { resolveAdminUserStatusTone } from "@/lib/admin-status-tone"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -391,13 +393,9 @@ export function AdminUsersConsole(): React.ReactElement { {row.nickname ?? ""} -
- -
+ + {row.status === 0 ? t("status.enabled") : t("status.disabled")} +
diff --git a/src/modules/config/config-chip-group.tsx b/src/modules/config/config-chip-group.tsx index 9373722..173ca4a 100644 --- a/src/modules/config/config-chip-group.tsx +++ b/src/modules/config/config-chip-group.tsx @@ -25,16 +25,17 @@ type ConfigChipProps = { onClick: () => void; children: ReactNode; disabled?: boolean; + className?: string; }; -export function ConfigChip({ active, onClick, children, disabled }: ConfigChipProps) { +export function ConfigChip({ active, onClick, children, disabled, className }: ConfigChipProps) { return ( + {canManage ? ( - - ) : null} - {canManage && isDraft ? ( <> - - + {isDraft ? ( + <> + + + + ) : null} ) : null}
diff --git a/src/modules/config/config-version-switcher.tsx b/src/modules/config/config-version-switcher.tsx index 979ca86..abc48c1 100644 --- a/src/modules/config/config-version-switcher.tsx +++ b/src/modules/config/config-version-switcher.tsx @@ -1,7 +1,7 @@ "use client"; import { useMemo, useState } from "react"; -import { Layers } from "lucide-react"; +import { Check, ChevronRight, Layers, MoreHorizontal } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; @@ -13,6 +13,12 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Sheet, SheetContent, @@ -41,6 +47,27 @@ export type ConfigVersionSwitcherProps = { rollbackBusy?: boolean; }; +function formatVersionNote( + reason: string | null | undefined, + formatDt: (iso: string | null | undefined) => string, +): string | null { + if (!reason?.trim()) { + return null; + } + const trimmed = reason.trim(); + const isoMatch = trimmed.match(/\d{4}-\d{2}-\d{2}T[\d:.]+Z?/); + if (trimmed.startsWith("draft") && isoMatch) { + return formatDt(isoMatch[0]); + } + if (trimmed.startsWith("seed:")) { + return trimmed.slice(5); + } + if (trimmed.length > 56) { + return `${trimmed.slice(0, 56)}…`; + } + return trimmed; +} + export function ConfigVersionSwitcher({ versions, selectedId, @@ -85,14 +112,13 @@ export function ConfigVersionSwitcher({ [selectedId, sortedVersions], ); - const statusCounts = useMemo( + const visibleSections = useMemo( () => STATUS_ORDER.map((status) => ({ status, - label: t(`versionStatus.${status}`, { ns: "config" }), - count: groupedVersions.get(status)?.length ?? 0, - })), - [groupedVersions, t], + rows: groupedVersions.get(status) ?? [], + })).filter((section) => section.rows.length > 0), + [groupedVersions], ); function switchTo(id: number) { @@ -118,15 +144,14 @@ export function ConfigVersionSwitcher({ return ( <> -
-
+
+
{selectedVersion ? ( <> - + v{selectedVersion.version_no} - - #{selectedVersion.id} + ) : ( @@ -138,12 +163,13 @@ export function ConfigVersionSwitcher({
@@ -151,172 +177,142 @@ export function ConfigVersionSwitcher({ -
- - - {resolvedSheetTitle} - - {resolvedSheetDescription ? ( - - {resolvedSheetDescription} - - ) : null} - -
-
-
- {statusCounts.map((s) => ( -
- {s.label} - {s.count} -
- ))} -
-
-
+ + + {resolvedSheetTitle} + + {resolvedSheetDescription ? ( + + {resolvedSheetDescription} + + ) : null} + + +
{sortedVersions.length === 0 ? ( -
+

{t("versionSwitcher.empty", { ns: "config" })} -

+

) : ( - STATUS_ORDER.map((status) => { - const rows = groupedVersions.get(status) ?? []; - if (rows.length === 0) { - return null; - } - return ( -
-
-
-
-

- {t(`versionStatus.${status}`, { ns: "config" })} -

-
-

- {t("versionSwitcher.count", { ns: "config", count: rows.length })} -

-
-
- {rows.map((v) => { +
+ {visibleSections.map((section) => ( +
+

+ {t(`versionStatus.${section.status}`, { ns: "config" })} +

+
    + {section.rows.map((v) => { const isCurrent = selectedId === String(v.id); + const note = formatVersionNote(v.reason, formatDt); + const effectiveLabel = v.effective_at + ? formatDt(v.effective_at) + : null; + const meta = [effectiveLabel, note].filter(Boolean).join(" · "); + const showMenu = + (onDeleteVersion && v.status !== "active") || + (onRollbackVersion && v.status !== "draft"); + return ( -
    +
  • -
    -
    -
    -
    - + > +
    -
    - - {onRollbackVersion && v.status !== "draft" ? ( - - ) : null} - {onDeleteVersion && v.status !== "active" ? ( - - ) : null} -
    + ) : ( + + )} + + + {showMenu ? ( +
    + + e.stopPropagation()} + > + + + + {onRollbackVersion && v.status !== "draft" ? ( + { + onRollbackVersion(v); + setSheetOpen(false); + }} + > + {t("versionSwitcher.rollback", { ns: "config" })} + + ) : null} + {onDeleteVersion && v.status !== "active" ? ( + setDeleteTarget(v)} + > + {t("versionSwitcher.delete", { ns: "config" })} + + ) : null} + + +
    + ) : null}
    -
    +
  • ); })} -
    +
- ); - }) + ))} +
)}
!open && setDeleteTarget(null)}> - + {t("versionSwitcher.deleteConfirmTitle", { ns: "config" })} diff --git a/src/modules/config/config-version-toolbar-meta.tsx b/src/modules/config/config-version-toolbar-meta.tsx new file mode 100644 index 0000000..3ce1a54 --- /dev/null +++ b/src/modules/config/config-version-toolbar-meta.tsx @@ -0,0 +1,40 @@ +import type { ReactNode } from "react"; + +import { cn } from "@/lib/utils"; + +type ConfigVersionToolbarMetaProps = { + children: ReactNode; + className?: string; + /** Highlights read-only or draft-only hints. */ + emphasis?: boolean; +}; + +/** Single-line meta row under the version toolbar (live version, read-only hints). */ +export function ConfigVersionToolbarMeta({ + children, + className, + emphasis = false, +}: ConfigVersionToolbarMetaProps) { + return ( +
+ {children} +
+ ); +} + +export function ConfigVersionToolbarMetaEmphasis({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return {children}; +} diff --git a/src/modules/config/doc/odds-config-doc-screen.tsx b/src/modules/config/doc/odds-config-doc-screen.tsx index 6b8ea59..9061f00 100644 --- a/src/modules/config/doc/odds-config-doc-screen.tsx +++ b/src/modules/config/doc/odds-config-doc-screen.tsx @@ -16,8 +16,11 @@ import { } from "@/api/admin-config"; import { Button } from "@/components/ui/button"; import { ConfigChip, ConfigChipGroup } from "@/modules/config/config-chip-group"; -import { ConfigContextBanner, ConfigContextEmphasis } from "@/modules/config/config-context-banner"; import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page"; +import { + ConfigVersionToolbarMeta, + ConfigVersionToolbarMetaEmphasis, +} from "@/modules/config/config-version-toolbar-meta"; import { Dialog, DialogContent, @@ -34,6 +37,7 @@ import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher" import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; import { resolveAdminPlayTypeDisplayName } from "@/lib/admin-play-types"; import { adminHasAnyPermission } from "@/lib/admin-permissions"; +import { cn } from "@/lib/utils"; import { PRD_ODDS_MANAGE, PRD_REBATE_MANAGE } from "@/lib/admin-prd"; import { useAdminProfile } from "@/stores/admin-session"; import { LotteryApiBizError } from "@/types/api/errors"; @@ -77,16 +81,25 @@ function filterTypes(tab: CatTab, types: AdminPlayTypeRow[]): AdminPlayTypeRow[] type OddsConfigDocScreenProps = { /** 嵌入「赔率与回水」合并页时去掉外层 ConfigDocPage */ embedded?: boolean; + /** 与回水分区共用版本选择(合并页) */ + versionId?: string; + onVersionIdChange?: (id: string) => void; }; -export function OddsConfigDocScreen({ embedded = false }: OddsConfigDocScreenProps) { +export function OddsConfigDocScreen({ + embedded = false, + versionId: controlledVersionId, + onVersionIdChange, +}: OddsConfigDocScreenProps) { const { t, i18n } = useTranslation(["config", "adminUsers", "common"]); const profile = useAdminProfile(); const canManage = adminHasAnyPermission(profile?.permissions, [PRD_ODDS_MANAGE, PRD_REBATE_MANAGE]); const formatDt = useAdminDateTimeFormatter(); const [types, setTypes] = useState([]); const [list, setList] = useState([]); - const [selectedId, setSelectedId] = useState(""); + const [internalSelectedId, setInternalSelectedId] = useState(""); + const selectedId = controlledVersionId ?? internalSelectedId; + const setSelectedId = onVersionIdChange ?? setInternalSelectedId; const [detail, setDetail] = useState(null); const [draftRows, setDraftRows] = useState([]); const [loadingTypes, setLoadingTypes] = useState(true); @@ -417,38 +430,42 @@ export function OddsConfigDocScreen({ embedded = false }: OddsConfigDocScreenPro ]; const filtersBlock = ( -
- - {catTabs.map((tab) => ( +
+ + {catTabs.map((tab) => ( + setCatTab(tab.id)} + > + {tab.label} + + ))} + + + {filteredTypes.length === 0 ? ( + {t("odds.noPlayTypes", { ns: "config" })} + ) : ( +
+ {filteredTypes.map((type) => ( setCatTab(tab.id)} + key={type.play_code} + active={resolvedPlayCode === type.play_code} + onClick={() => setPlayCode(type.play_code)} + className="shrink-0" > - {tab.label} + {resolveAdminPlayTypeDisplayName(type.play_code, i18n.language, type)} ))} - - - {filteredTypes.length === 0 ? ( - {t("odds.noPlayTypes", { ns: "config" })} - ) : ( - filteredTypes.map((type) => ( - setPlayCode(type.play_code)} - > - {resolveAdminPlayTypeDisplayName(type.play_code, i18n.language, type)} - - )) - )} - -
+
+ )} +
+
); const toolbarBlock = ( void requestPublishConfirm()} /> } + footer={ + !detail ? null : ( + + + {t("odds.activeVersionPrefix", { ns: "config" })} + {activeHead ? ( + <> + v{activeHead.version_no} + {activeHead.effective_at ? ` · ${formatDt(activeHead.effective_at)}` : ""} + + ) : ( + "—" + )} + + {!isDraft ? ( + + {t("odds.readOnlyHint", { ns: "config" })} + + ) : activeHead ? ( + {t("versionToolbar.draftEditing", { ns: "config" })} + ) : null} + + ) + } /> ); - const contextBlock = - embedded || !detail ? null : ( - - {t("odds.activeVersionPrefix", { ns: "config" })} - {activeHead ? ( - <> - v{activeHead.version_no} - {activeHead.effective_at ? ` · ${formatDt(activeHead.effective_at)}` : ""} - - ) : ( - "—" - )} - {!isDraft ? ( - <> - {" "} - — {t("odds.readOnlyHint", { ns: "config" })} - - ) : null} - - ); - const mainBlock = ( <> {error ?

{error}

: null} {loadingDetail || loadingTypes ? ( -
-

{t("odds.loadingDetails", { ns: "config" })}

-
+

+ {t("odds.loadingDetails", { ns: "config" })} +

) : resolvedPlayCode ? ( -
- {PRIZE_SCOPE_ORDER.map((scope) => { - const row = scopeRows[scope]; - const hint = embedded ? null : PRIZE_SCOPE_MULTIPLIER_HINT[scope]; - const idx = row ? rowIndex(resolvedPlayCode, scope) : -1; - return ( -
- - {row && idx >= 0 ? ( -
- {canEditDraft ? ( +
+
+ {PRIZE_SCOPE_ORDER.map((scope) => { + const row = scopeRows[scope]; + const hint = embedded ? null : PRIZE_SCOPE_MULTIPLIER_HINT[scope]; + const idx = row ? rowIndex(resolvedPlayCode, scope) : -1; + return ( +
+ + {row && idx >= 0 ? ( + canEditDraft ? ( @@ -535,46 +559,39 @@ export function OddsConfigDocScreen({ embedded = false }: OddsConfigDocScreenPro } /> ) : ( - + {oddsMultiplierLabel(row.odds_value)} - )} - {!embedded ? ( - - {t("odds.multiplier", { - ns: "config", - value: oddsMultiplierLabel(row.odds_value), - currency: row.currency_code, - })} - - ) : null} -
- ) : ( -

{t("odds.missingScopeRow", { ns: "config", scope })}

- )} -
- ); - })} -
- - {canEditDraft ? ( - setRebateForPlayPercent(e.target.value)} - /> - ) : ( - - {rebatePercentUi} - - )} - {!embedded ? ( -

{t("odds.rebateRateHint", { ns: "config" })}

- ) : null} + ) + ) : ( +

{t("odds.missingScopeRow", { ns: "config", scope })}

+ )} +
+ ); + })} +
+ + {canEditDraft ? ( + setRebateForPlayPercent(e.target.value)} + /> + ) : ( + + {rebatePercentUi} + + )} +
+ {!embedded ? ( +

{t("odds.rebateRateHint", { ns: "config" })}

+ ) : null}
) : null} @@ -649,10 +666,11 @@ export function OddsConfigDocScreen({ embedded = false }: OddsConfigDocScreenPro if (embedded) { return ( -
- {filtersBlock} - {toolbarBlock} - {contextBlock} +
+
+ {toolbarBlock} + {filtersBlock} +
{mainBlock} {dialogs}
@@ -664,7 +682,6 @@ export function OddsConfigDocScreen({ embedded = false }: OddsConfigDocScreenPro title={t("nav.items.odds", { ns: "config" })} filters={filtersBlock} toolbar={toolbarBlock} - context={contextBlock} > {mainBlock} {dialogs} diff --git a/src/modules/config/doc/play-config-doc-screen.tsx b/src/modules/config/doc/play-config-doc-screen.tsx index 8b9f7ea..a1634df 100644 --- a/src/modules/config/doc/play-config-doc-screen.tsx +++ b/src/modules/config/doc/play-config-doc-screen.tsx @@ -10,16 +10,18 @@ import { getPlayConfigVersion, getPlayConfigVersions, postPlayConfigVersion, - patchAdminPlayType, publishPlayConfigVersion, putPlayConfigItems, } from "@/api/admin-config"; import { Button } from "@/components/ui/button"; import { ConfigChipGroup } from "@/modules/config/config-chip-group"; -import { ConfigContextBanner, ConfigContextEmphasis } from "@/modules/config/config-context-banner"; import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page"; +import { + ConfigVersionToolbarMeta, + ConfigVersionToolbarMetaEmphasis, +} from "@/modules/config/config-version-toolbar-meta"; import { ConfigSection } from "@/modules/config/config-section"; -import { Switch } from "@/components/ui/switch"; +import { ConfirmableSwitch } from "@/components/admin/confirmable-switch"; import { Dialog, DialogContent, @@ -136,7 +138,7 @@ function buildPlayConfigSavePayload( export function PlayConfigDocScreen() { const { t } = useTranslation(["config", "adminUsers", "common"]); - const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); + const { request: requestConfirm, ConfirmDialog, busy: confirmBusy } = useConfirmAction(); const profile = useAdminProfile(); const canManage = adminHasAnyPermission(profile?.permissions, [PRD_PLAY_SWITCH_MANAGE]); const formatDt = useAdminDateTimeFormatter(); @@ -269,25 +271,10 @@ export function PlayConfigDocScreen() { setDraftRows((prev) => prev.map((r) => (r.play_code === playCode ? { ...r, ...patch } : r))); } - async function applyPlayToggleInstant(playCode: string, enabled: boolean) { - try { - await patchAdminPlayType(playCode, { is_enabled: enabled }); - } catch (e) { - toast.error( - e instanceof LotteryApiBizError ? e.message : t("play.toggleInstantFailed", { ns: "config" }), - ); - throw e; - } - } - - async function applyBatchSwitch(group: PlayBatchSwitchGroup, enabled: boolean) { - const targets = draftRows.filter(group.match); + function applyBatchSwitch(group: PlayBatchSwitchGroup, enabled: boolean) { setDraftRows((prev) => prev.map((row) => (group.match(row) ? { ...row, is_enabled: enabled } : row)), ); - for (const row of targets) { - await applyPlayToggleInstant(row.play_code, enabled); - } } const batchSwitchStates = useMemo( @@ -448,26 +435,27 @@ export function PlayConfigDocScreen() { } /> } + footer={ + detail ? ( + + {activeHead ? ( + + {t("play.activeVersion", { ns: "config", version: activeHead.version_no })} + {activeHead.effective_at ? ` · ${formatDt(activeHead.effective_at)}` : ""} + + ) : null} + {!isDraft ? ( + + {t("play.readOnlyHint", { ns: "config" })} + + ) : activeHead ? ( + {t("versionToolbar.draftEditing", { ns: "config" })} + ) : null} + + ) : null + } /> } - context={ - detail ? ( - - {activeHead ? ( - <> - {t("play.activeVersion", { ns: "config", version: activeHead.version_no })} - {activeHead.effective_at ? ` · ${formatDt(activeHead.effective_at)}` : ""} - - ) : null} - {!isDraft ? ( - <> - {activeHead ? " — " : ""} - {t("play.readOnlyHint", { ns: "config" })} - - ) : null} - - ) : null - } > {detail ? ( {batchSwitchStates.map((group) => { - const groupOn = group.enabledCount > 0; + const groupOn = group.allEnabled; + const isPartial = + group.total > 0 && group.enabledCount > 0 && group.enabledCount < group.total; return (
{group.label}

{group.total > 0 - ? t("play.batchEnabledCount", { - ns: "config", - enabledCount: group.enabledCount, - total: group.total, - }) + ? isPartial + ? t("play.batchPartialEnabled", { + ns: "config", + enabledCount: group.enabledCount, + total: group.total, + }) + : t("play.batchEnabledCount", { + ns: "config", + enabledCount: group.enabledCount, + total: group.total, + }) : t("play.noPlayTypes", { ns: "config" })}

- {t("play.table.playCode", { ns: "config" })} {t("play.table.category", { ns: "config" })} {t("play.table.status", { ns: "config" })} - {t("play.table.displayName", { ns: "config" })} - {t("play.table.order", { ns: "config" })} + {t("play.table.displayName", { ns: "config" })} + {t("play.table.order", { ns: "config" })} {t("play.table.minBet", { ns: "config" })} {t("play.table.maxBet", { ns: "config" })} {t("play.table.actions", { ns: "config" })} @@ -550,41 +547,47 @@ export function PlayConfigDocScreen() { {row.play_code} {row.category ?? "—"} -
- { - if (!isDraft) { - return; - } - const enabled = checked; - const action = enabled - ? t("play.toggleEnable", { ns: "config" }) - : t("play.toggleDisable", { ns: "config" }); - requestConfirm({ - title: t("play.toggleConfirmTitle", { - ns: "config", - action, - playCode: row.play_code, - }), - description: t("play.toggleConfirmDescription", { ns: "config" }), - confirmVariant: enabled ? "default" : "destructive", - onConfirm: () => { - updateConfigRow(row.play_code, { is_enabled: enabled }); - void applyPlayToggleInstant(row.play_code, enabled); - }, - }); - }} - /> -
+ {isDraft ? ( +
+ { + const action = enabled + ? t("play.toggleEnable", { ns: "config" }) + : t("play.toggleDisable", { ns: "config" }); + requestConfirm({ + title: t("play.toggleConfirmTitle", { + ns: "config", + action, + playCode: row.play_code, + }), + description: t("play.toggleConfirmDescription", { ns: "config" }), + confirmVariant: enabled ? "default" : "destructive", + onConfirm: () => { + updateConfigRow(row.play_code, { is_enabled: enabled }); + }, + }); + }} + /> +
+ ) : ( +
+ + {row.is_enabled + ? t("play.states.enabled", { ns: "config" }) + : t("play.states.disabled", { ns: "config" })} + +
+ )}
- + {isDraft ? ( )} - + {isDraft ? ( { @@ -630,7 +633,7 @@ export function PlayConfigDocScreen() { @@ -651,7 +654,7 @@ export function PlayConfigDocScreen() { diff --git a/src/modules/config/doc/rebate-config-doc-screen.tsx b/src/modules/config/doc/rebate-config-doc-screen.tsx index fa6cf2e..d249e97 100644 --- a/src/modules/config/doc/rebate-config-doc-screen.tsx +++ b/src/modules/config/doc/rebate-config-doc-screen.tsx @@ -14,9 +14,12 @@ import { publishOddsVersion, putOddsItems, } from "@/api/admin-config"; -import { ConfigContextBanner, ConfigContextEmphasis } from "@/modules/config/config-context-banner"; import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page"; -import { Switch } from "@/components/ui/switch"; +import { + ConfigVersionToolbarMeta, + ConfigVersionToolbarMetaEmphasis, +} from "@/modules/config/config-version-toolbar-meta"; +import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value"; @@ -54,9 +57,15 @@ function inferPercentFrom(dim: 2 | 3 | 4, rows: OddsItemRow[], typeList: AdminPl type RebateConfigDocScreenProps = { embedded?: boolean; + versionId?: string; + onVersionIdChange?: (id: string) => void; }; -export function RebateConfigDocScreen({ embedded = false }: RebateConfigDocScreenProps) { +export function RebateConfigDocScreen({ + embedded = false, + versionId: controlledVersionId, + onVersionIdChange, +}: RebateConfigDocScreenProps) { const { t } = useTranslation(["config", "common"]); const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); const profile = useAdminProfile(); @@ -65,7 +74,9 @@ export function RebateConfigDocScreen({ embedded = false }: RebateConfigDocScree const [types, setTypes] = useState([]); const [listRows, setListRows] = useState([]); - const [selectedId, setSelectedId] = useState(""); + const [internalSelectedId, setInternalSelectedId] = useState(""); + const selectedId = controlledVersionId ?? internalSelectedId; + const setSelectedId = onVersionIdChange ?? setInternalSelectedId; const [detail, setDetail] = useState(null); const [draftRows, setDraftRows] = useState([]); const [loading, setLoading] = useState(true); @@ -313,31 +324,34 @@ export function RebateConfigDocScreen({ embedded = false }: RebateConfigDocScree } /> } + footer={ + embedded || !detail ? null : ( + + + {t("rebate.editingVersion", { + ns: "config", + version: detail.version_no, + status: + detail.status === "draft" + ? t("versionStatus.draft", { ns: "config" }) + : detail.status === "active" + ? t("versionStatus.active", { ns: "config" }) + : t("versionStatus.archived", { ns: "config" }), + })} + + {!isDraft ? ( + + {t("rebate.readOnlyHint", { ns: "config" })} + + ) : ( + {t("versionToolbar.draftEditing", { ns: "config" })} + )} + + ) + } /> ); - const contextBlock = - embedded || !detail ? null : ( - - {t("rebate.editingVersion", { - ns: "config", - version: detail.version_no, - status: - detail.status === "draft" - ? t("versionStatus.draft", { ns: "config" }) - : detail.status === "active" - ? t("versionStatus.active", { ns: "config" }) - : t("versionStatus.archived", { ns: "config" }), - })} - {!isDraft ? ( - <> - {" "} - — {t("rebate.readOnlyHint", { ns: "config" })} - - ) : null} - - ); - const fieldsBlock = ( <>
@@ -392,10 +406,10 @@ export function RebateConfigDocScreen({ embedded = false }: RebateConfigDocScree
- - +

{t("rebate.winEnjoy.label", { ns: "config" })}

+ + {t("system.states.enabled", { ns: "config" })} +
{!embedded ? ( @@ -415,8 +429,7 @@ export function RebateConfigDocScreen({ embedded = false }: RebateConfigDocScree if (embedded) { return ( -
- {contextBlock} +
{fieldsBlock}
@@ -427,7 +440,6 @@ export function RebateConfigDocScreen({ embedded = false }: RebateConfigDocScree {fieldsBlock} diff --git a/src/modules/config/doc/risk-cap-doc-screen.tsx b/src/modules/config/doc/risk-cap-doc-screen.tsx index e98fd39..fc75fe4 100644 --- a/src/modules/config/doc/risk-cap-doc-screen.tsx +++ b/src/modules/config/doc/risk-cap-doc-screen.tsx @@ -14,8 +14,11 @@ import { putRiskCapItems, } from "@/api/admin-config"; import { Button } from "@/components/ui/button"; -import { ConfigContextBanner, ConfigContextEmphasis } from "@/modules/config/config-context-banner"; import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page"; +import { + ConfigVersionToolbarMeta, + ConfigVersionToolbarMetaEmphasis, +} from "@/modules/config/config-version-toolbar-meta"; import { ConfigSection } from "@/modules/config/config-section"; import { Dialog, @@ -375,21 +378,27 @@ export function RiskCapDocScreen() { } /> } + footer={ + detail ? ( + + + {t("riskCap.effectiveAt", { + ns: "config", + value: detail.effective_at ? formatDt(detail.effective_at) : "—", + })} + + {!isDraft ? ( + + {t("riskCap.readOnlyHint", { ns: "config" })} + + ) : ( + {t("versionToolbar.draftEditing", { ns: "config" })} + )} + + ) : null + } /> } - context={ - detail ? ( - - {t("riskCap.effectiveAt", { ns: "config", value: detail.effective_at ? formatDt(detail.effective_at) : "—" })} - {!isDraft ? ( - <> - {" "} - — {t("riskCap.readOnlyHint", { ns: "config" })} - - ) : null} - - ) : null - } contentClassName="space-y-8" > {error ?

{error}

: null} @@ -447,8 +456,8 @@ export function RiskCapDocScreen() { {t("riskCap.table.number", { ns: "config" })} {t("riskCap.table.capAmount", { ns: "config" })} - {t("riskCap.table.used", { ns: "config" })} - {t("riskCap.table.remaining", { ns: "config" })} + {t("riskCap.table.used", { ns: "config" })} + {t("riskCap.table.remaining", { ns: "config" })} {t("riskCap.table.soldOut", { ns: "config" })} {t("riskCap.table.actions", { ns: "config" })} @@ -494,8 +503,8 @@ export function RiskCapDocScreen() { )} - - + + {canEditDraft ? ( @@ -546,9 +555,9 @@ export function RiskCapDocScreen() { {t("riskCap.table.number", { ns: "config" })} - {t("riskCap.table.used", { ns: "config" })} - {t("riskCap.table.remaining", { ns: "config" })} - {t("riskCap.table.ratio", { ns: "config" })} + {t("riskCap.table.used", { ns: "config" })} + {t("riskCap.table.remaining", { ns: "config" })} + {t("riskCap.table.ratio", { ns: "config" })} {t("riskCap.table.soldOut", { ns: "config" })} {t("riskCap.table.actions", { ns: "config" })} @@ -557,15 +566,11 @@ export function RiskCapDocScreen() { {occFiltered.map((r) => ( {r.normalized_number} - - - - - - + + + + ))} diff --git a/src/modules/dashboard/dashboard-analytics-panel.tsx b/src/modules/dashboard/dashboard-analytics-panel.tsx index 05321a0..394e43d 100644 --- a/src/modules/dashboard/dashboard-analytics-panel.tsx +++ b/src/modules/dashboard/dashboard-analytics-panel.tsx @@ -21,6 +21,7 @@ import { } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog"; +import { getAdminRequestLocale } from "@/lib/admin-locale"; import { formatAdminMinorUnits, getAdminCurrencyDecimalPlaces } from "@/lib/money"; import { cn } from "@/lib/utils"; import { StatCard } from "@/modules/dashboard/dashboard-visuals"; @@ -52,7 +53,7 @@ function formatMoneyMinor(minor: number, currencyCode: string | null): string { const decimals = getAdminCurrencyDecimalPlaces(code); const major = minor / 10 ** decimals; try { - return new Intl.NumberFormat("zh-CN", { + return new Intl.NumberFormat(getAdminRequestLocale(), { style: "currency", currency: code, minimumFractionDigits: decimals, @@ -287,8 +288,8 @@ export function DashboardAnalyticsPanel({ label={t("analytics.summaryBet")} value={formatMoneyMinor(summary.total_bet_minor, currency)} hint={t("lifetimeActivityHint", { - draws: summary.draw_count.toLocaleString("zh-CN"), - days: summary.business_day_count.toLocaleString("zh-CN"), + draws: summary.draw_count.toLocaleString(getAdminRequestLocale()), + days: summary.business_day_count.toLocaleString(getAdminRequestLocale()), })} icon={} /> diff --git a/src/modules/dashboard/dashboard-console.tsx b/src/modules/dashboard/dashboard-console.tsx index 2aae0d2..c2f342d 100644 --- a/src/modules/dashboard/dashboard-console.tsx +++ b/src/modules/dashboard/dashboard-console.tsx @@ -40,6 +40,7 @@ import { import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog"; import { adminWeekdayKeyForDate, formatAdminCalendarToday } from "@/lib/admin-datetime"; import { normalizeAdminLanguage } from "@/i18n"; +import { getAdminRequestLocale } from "@/lib/admin-locale"; import { formatAdminMinorUnits, getAdminCurrencyDecimalPlaces } from "@/lib/money"; import { cn } from "@/lib/utils"; import { LotteryApiBizError } from "@/types/api/errors"; @@ -63,7 +64,7 @@ function formatMoneyMinor(minor: number, currencyCode: string | null): string { const decimals = getAdminCurrencyDecimalPlaces(code); const major = minor / 10 ** decimals; try { - return new Intl.NumberFormat("zh-CN", { + return new Intl.NumberFormat(getAdminRequestLocale(), { style: "currency", currency: code, minimumFractionDigits: decimals, diff --git a/src/modules/draws/draw-finance-console.tsx b/src/modules/draws/draw-finance-console.tsx index 4409b58..018a1fb 100644 --- a/src/modules/draws/draw-finance-console.tsx +++ b/src/modules/draws/draw-finance-console.tsx @@ -191,10 +191,10 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE {t("table.id", { ns: "common" })} {t("status")} - {t("ticketCount")} - {t("winCount")} - {t("payoutTotal")} - {t("jackpotPayout")} + {t("ticketCount")} + {t("winCount")} + {t("payoutTotal")} + {t("jackpotPayout")} {t("finishedAt")} @@ -207,16 +207,16 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE {settlementBatchStatusLabel(b.status, t)} - + {b.total_ticket_count} - + {b.total_win_count} - + {formatMoney(b.total_payout_amount)} - + {formatMoney(b.total_jackpot_payout_amount)} diff --git a/src/modules/draws/draw-review-console.tsx b/src/modules/draws/draw-review-console.tsx index e63f26d..2feb53a 100644 --- a/src/modules/draws/draw-review-console.tsx +++ b/src/modules/draws/draw-review-console.tsx @@ -204,7 +204,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) { {t("batchId")} {t("version", { version: "" }).replace(" v", "").trim()} {t("numberCount")} - {t("actions")} + {t("actions")} @@ -213,7 +213,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) { {b.id} v{b.result_version} {b.items.length} - + {canManageDraw ? ( {t("closeTime")} {t("drawTime")} {t("status")} - {t("betTotal")} - {t("payoutTotal")} - {t("profitLoss")} - {t("actions")} + {t("betTotal")} + {t("payoutTotal")} + {t("profitLoss")} + {t("actions")} @@ -435,19 +435,19 @@ export function DrawsIndexConsole() { label={drawStatusLabel(row.status, t)} /> - + {row.total_bet_minor != null ? formatAdminMinorUnits(row.total_bet_minor, defaultCurrency) : "—"} - + {row.total_payout_minor != null ? formatAdminMinorUnits(row.total_payout_minor, defaultCurrency) : "—"} @@ -455,8 +455,8 @@ export function DrawsIndexConsole() { ? formatAdminMinorUnits(row.profit_loss_minor, defaultCurrency) : "—"} - -
+ +
{t("table.id", { ns: "common" })} {t("drawNo")} {t("trigger")} - {t("payoutAmount")} - {t("winnerCount")} + {t("payoutAmount")} + {t("winnerCount")} {t("time")} @@ -261,10 +261,10 @@ export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsol {r.id} {r.draw_no ?? "—"} {triggerTypeText(r.trigger_type)} - + {formatAdminMinorUnits(r.total_payout_amount, r.currency_code ?? "NPR")} - {r.winner_count} + {r.winner_count} {formatDt(r.created_at)} @@ -293,7 +293,7 @@ export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsol {t("drawNo")} {t("ticketNo")} {t("player")} - {t("contributionAmount")} + {t("contributionAmount")} {t("time")} @@ -311,7 +311,7 @@ export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsol {r.draw_no ?? "—"} {r.ticket_no ?? "—"} {r.player_username ?? "—"} - + {formatAdminMinorUnits(r.contribution_amount, r.currency_code ?? "NPR")} diff --git a/src/modules/players/players-console.tsx b/src/modules/players/players-console.tsx index d800f9d..48c65c2 100644 --- a/src/modules/players/players-console.tsx +++ b/src/modules/players/players-console.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useConfirmAction } from "@/hooks/use-confirm-action"; +import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; import { useExportLabels } from "@/hooks/use-export-labels"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; @@ -16,8 +17,10 @@ import { } from "@/api/admin-player"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; +import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; +import { ConfirmableSwitch } from "@/components/admin/confirmable-switch"; +import { resolvePlayerStatusTone } from "@/lib/admin-status-tone"; import { Button } from "@/components/ui/button"; -import { Switch } from "@/components/ui/switch"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, @@ -66,7 +69,8 @@ const PLAYER_STATUS_OPTIONS = [ export function PlayersConsole(): React.ReactElement { const { t } = useTranslation(["players", "common"]); - const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); + const { request: requestConfirm, ConfirmDialog, busy: confirmBusy } = useConfirmAction(); + const formatDt = useAdminDateTimeFormatter(); const exportLabels = useExportLabels("players"); const profile = useAdminProfile(); useAdminCurrencyCatalog(); @@ -338,8 +342,8 @@ export function PlayersConsole(): React.ReactElement { {t("username")} {t("nickname")} {t("currency")} - {t("balance")} - {t("available")} + {t("balance")} + {t("available")} {t("status")} {t("lastLogin")} {t("actions")} @@ -365,21 +369,28 @@ export function PlayersConsole(): React.ReactElement { {row.username ?? "—"} {row.nickname ?? "—"} {row.default_currency} - + {row.wallets.length > 0 ? formatAdminMinorUnits(row.wallets[0].balance, row.wallets[0].currency_code) : "—"} - + {row.wallets.length > 0 ? formatAdminMinorUnits(row.wallets[0].available_balance, row.wallets[0].currency_code) : "—"} - {canFreezePlayers ? ( + {row.status === 2 ? (
- + {playerStatusLabelT(row.status, t)} + +
+ ) : canFreezePlayers ? ( +
+ { @@ -407,15 +418,7 @@ export function PlayersConsole(): React.ReactElement { )} - {row.last_login_at - ? new Date(row.last_login_at).toLocaleString("zh-CN", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - }) - : "—"} + {row.last_login_at ? formatDt(row.last_login_at) : "—"} {canManagePlayers || canFreezePlayers ? ( diff --git a/src/modules/reports/reports-console.tsx b/src/modules/reports/reports-console.tsx index eb0351d..2b05bb4 100644 --- a/src/modules/reports/reports-console.tsx +++ b/src/modules/reports/reports-console.tsx @@ -312,20 +312,25 @@ function parsePositiveInteger(value: string): number | null { return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null; } -async function resolveDraw(filters: ReportFilters): Promise<{ id: number; draw_no: string }> { +async function resolveDraw( + filters: ReportFilters, + t: (key: string, options?: { ns?: string; drawNo?: string }) => string, +): Promise<{ id: number; draw_no: string }> { if (filters.drawId && filters.drawNo.trim()) { return { id: filters.drawId, draw_no: filters.drawNo.trim() }; } const drawNo = filters.drawNo.trim(); if (!drawNo) { - throw new LotteryApiBizError("请输入期号", -1, null); + throw new LotteryApiBizError(t("validation.drawNoRequired", { ns: "reports" }), -1, null); } const data = await getAdminDraws({ draw_no: drawNo, page: 1, per_page: 1 }); const matched = data.items.find((item) => item.draw_no === drawNo) ?? data.items[0]; if (!matched) { - throw new LotteryApiBizError("未找到期号", -1, { drawNo }); + throw new LotteryApiBizError(t("validation.drawNoNotFound", { ns: "reports", drawNo }), -1, { + drawNo, + }); } return { id: matched.id, draw_no: matched.draw_no }; } @@ -459,7 +464,7 @@ export function ReportsConsole() { try { switch (selectedReport.key) { case "draw_profit": { - const draw = await resolveDraw(filters); + const draw = await resolveDraw(filters, t); const summary = await getAdminDrawFinanceSummary(draw.id); setResult({ key: "draw_profit", @@ -580,7 +585,7 @@ export function ReportsConsole() { if (!filters.number.trim()) { throw new LotteryApiBizError(t("validation.drawNoNumberRequired"), -1, null); } - const draw = await resolveDraw(filters); + const draw = await resolveDraw(filters, t); const detail = await getAdminRiskPoolDetail(draw.id, filters.number.trim(), { page, per_page: perPage }); const rows: ExportRow[] = [ { @@ -627,7 +632,7 @@ export function ReportsConsole() { break; } case "sold_out_number": { - const draw = await resolveDraw(filters); + const draw = await resolveDraw(filters, t); const payload = await getAdminRiskPools(draw.id, { page, per_page: perPage, sold_out_only: true, sort: "number_asc" }); const rows = payload.items.map((item) => ({ draw_id: payload.draw_id, @@ -1022,22 +1027,22 @@ export function ReportsConsole() { {summary.draw_no} {summary.draw_status} - {summary.order_count} - {summary.ticket_item_count} - {formatPlainMoney(summary.total_bet_minor, summary.currency_code)} - {formatPlainMoney(summary.total_payout_minor, summary.currency_code)} - {formatPlainMoney(summary.approx_house_gross_minor, summary.currency_code)} + {summary.order_count} + {summary.ticket_item_count} + {formatPlainMoney(summary.total_bet_minor, summary.currency_code)} + {formatPlainMoney(summary.total_payout_minor, summary.currency_code)} + {formatPlainMoney(summary.approx_house_gross_minor, summary.currency_code)} {summary.settlement_batches.length} {summary.settlement_batches.map((batch) => ( #{batch.id} {batch.status} - {batch.total_ticket_count} - {batch.total_win_count} - - - {formatPlainMoney(batch.total_payout_amount, summary.currency_code)} - {formatPlainMoney(batch.total_jackpot_payout_amount, summary.currency_code)} + {batch.total_ticket_count} + {batch.total_win_count} + - + {formatPlainMoney(batch.total_payout_amount, summary.currency_code)} + {formatPlainMoney(batch.total_jackpot_payout_amount, summary.currency_code)} {formatTs(batch.finished_at)} ))} @@ -1052,7 +1057,7 @@ export function ReportsConsole() { {optionText(item.username, item.nickname) || item.player_id} {item.direction} {item.status} - {item.currency_code} {item.amount} + {item.currency_code} {item.amount} {item.external_ref_no || "-"} {item.fail_reason || "-"} {formatTs(item.created_at)} @@ -1066,9 +1071,9 @@ export function ReportsConsole() { {result.raw.pool.normalized_number} {result.raw.draw_no} - {formatPlainMoney(result.raw.pool.total_cap_amount, result.raw.currency_code)} - {formatPlainMoney(result.raw.pool.locked_amount, result.raw.currency_code)} - {formatPlainMoney(result.raw.pool.remaining_amount, result.raw.currency_code)} + {formatPlainMoney(result.raw.pool.total_cap_amount, result.raw.currency_code)} + {formatPlainMoney(result.raw.pool.locked_amount, result.raw.currency_code)} + {formatPlainMoney(result.raw.pool.remaining_amount, result.raw.currency_code)} {result.raw.pool.is_sold_out ? t("yes") : t("no")} {result.raw.pool.usage_ratio == null ? "-" : `${result.raw.pool.usage_ratio}%`} v{result.raw.pool.version} @@ -1077,7 +1082,7 @@ export function ReportsConsole() { #{item.id} {item.action_type} - {formatPlainMoney(item.amount, result.raw.currency_code)} + {formatPlainMoney(item.amount, result.raw.currency_code)} {playCodeLabel(item.play_code)} {item.ticket_no || "-"} {item.player_id || "-"} @@ -1094,9 +1099,9 @@ export function ReportsConsole() { {item.normalized_number} {filters.drawNo} - {formatPlainMoney(item.total_cap_amount, null)} - {formatPlainMoney(item.locked_amount, null)} - {formatPlainMoney(item.remaining_amount, null)} + {formatPlainMoney(item.total_cap_amount, null)} + {formatPlainMoney(item.locked_amount, null)} + {formatPlainMoney(item.remaining_amount, null)} {item.is_sold_out ? t("yes") : t("no")} {item.usage_ratio == null ? "-" : `${item.usage_ratio}%`} v{item.version} @@ -1109,9 +1114,9 @@ export function ReportsConsole() { {item.business_date} - - {formatPlainMoney(item.total_bet_minor, "NPR")} - {formatPlainMoney(item.total_payout_minor, "NPR")} - {formatPlainMoney(item.approx_house_gross_minor, "NPR")} + {formatPlainMoney(item.total_bet_minor, "NPR")} + {formatPlainMoney(item.total_payout_minor, "NPR")} + {formatPlainMoney(item.approx_house_gross_minor, "NPR")} - - - @@ -1124,9 +1129,9 @@ export function ReportsConsole() { {item.username} ID {item.player_id} - {formatPlainMoney(item.total_bet_minor, "NPR")} - {formatPlainMoney(item.total_payout_minor, "NPR")} - {formatPlainMoney(item.net_win_loss_minor, "NPR")} + {formatPlainMoney(item.total_bet_minor, "NPR")} + {formatPlainMoney(item.total_payout_minor, "NPR")} + {formatPlainMoney(item.net_win_loss_minor, "NPR")} - - - @@ -1139,9 +1144,9 @@ export function ReportsConsole() { {playCodeLabel(item.play_code)} {item.dimension}D - {formatPlainMoney(item.total_bet_minor, "NPR")} - {formatPlainMoney(item.total_payout_minor, "NPR")} - {formatPlainMoney(item.approx_house_gross_minor, "NPR")} + {formatPlainMoney(item.total_bet_minor, "NPR")} + {formatPlainMoney(item.total_payout_minor, "NPR")} + {formatPlainMoney(item.approx_house_gross_minor, "NPR")} - - - @@ -1154,8 +1159,8 @@ export function ReportsConsole() { {playCodeLabel(item.play_code)} {item.order_count} - {formatPlainMoney(item.total_rebate_minor, "NPR")} - {item.ticket_item_count} + {formatPlainMoney(item.total_rebate_minor, "NPR")} + {item.ticket_item_count} - - - @@ -1294,9 +1299,9 @@ export function ReportsConsole() { {t("preview.columns.primary")} {t("preview.columns.secondary")} - {t("preview.columns.metricA")} - {t("preview.columns.metricB")} - {t("preview.columns.metricC")} + {t("preview.columns.metricA")} + {t("preview.columns.metricB")} + {t("preview.columns.metricC")} {t("preview.columns.status")} {t("preview.columns.extra")} {t("preview.columns.time")} diff --git a/src/modules/risk/risk-index-console.tsx b/src/modules/risk/risk-index-console.tsx index 5eadc4d..a7258b6 100644 --- a/src/modules/risk/risk-index-console.tsx +++ b/src/modules/risk/risk-index-console.tsx @@ -184,7 +184,7 @@ export function RiskIndexConsole() { {t("drawNo")} {t("status")} {t("closeTime")} - {t("actions")} + {t("actions")} @@ -204,7 +204,7 @@ export function RiskIndexConsole() { {row.close_time ? formatDt(row.close_time) : "—"} - + {t("time")} {t("searchNumber")} {t("action")} - {t("amount")} + {t("amount")} {t("source")} {t("ticketNo")} {t("playCode")} @@ -184,7 +184,7 @@ export function RiskLockLogsConsole({ drawId }: { drawId: number }) { {riskActionTypeLabel(row.action_type, t)} - + {formatAdminMinorUnits(row.amount, data?.currency_code ?? "NPR")} diff --git a/src/modules/risk/risk-pool-detail-console.tsx b/src/modules/risk/risk-pool-detail-console.tsx index bf2367a..92e7b1b 100644 --- a/src/modules/risk/risk-pool-detail-console.tsx +++ b/src/modules/risk/risk-pool-detail-console.tsx @@ -163,7 +163,7 @@ export function RiskPoolDetailConsole({ {t("time")} {t("action")} - {t("amount")} + {t("amount")} {t("source")} {t("ticketNo")} {t("playCode")} @@ -176,7 +176,7 @@ export function RiskPoolDetailConsole({ {row.created_at ? formatDt(row.created_at) : "—"} {riskActionTypeLabel(row.action_type, t)} - + {formatAdminMinorUnits(row.amount, currencyCode)} diff --git a/src/modules/risk/risk-pools-console.tsx b/src/modules/risk/risk-pools-console.tsx index 41aa695..3b45f54 100644 --- a/src/modules/risk/risk-pools-console.tsx +++ b/src/modules/risk/risk-pools-console.tsx @@ -249,12 +249,12 @@ export function RiskPoolsConsole({ {t("searchNumber")} - {t("capAmount")} - {t("lockedAmount")} - {t("remainingAmount")} - {t("usageRatio")} + {t("capAmount")} + {t("lockedAmount")} + {t("remainingAmount")} + {t("usageRatio")} {t("poolStatus")} - {t("actions")} + {t("actions")} @@ -275,16 +275,16 @@ export function RiskPoolsConsole({ )} > {row.normalized_number} - + {formatAdminMinorUnits(row.total_cap_amount, currencyCode)} - + {formatAdminMinorUnits(row.locked_amount, currencyCode)} - + {formatAdminMinorUnits(row.remaining_amount, currencyCode)} - + {row.usage_ratio != null ? `${(row.usage_ratio * 100).toFixed(2)}%` : "—"} @@ -301,8 +301,8 @@ export function RiskPoolsConsole({ {row.is_sold_out ? t("soldOut") : highRisk ? t("warning") : t("normal")} - -
+ +
{canManageRiskPools ? (