diff --git a/src/api/admin-agent-settlement.ts b/src/api/admin-agent-settlement.ts index 3865d5d..981047f 100644 --- a/src/api/admin-agent-settlement.ts +++ b/src/api/admin-agent-settlement.ts @@ -141,6 +141,8 @@ export type SettlementCreditLedgerRow = { play_code?: string | null; draw_no?: string | null; ticket_item_id?: number | null; + settlement_bill_id?: number | null; + bill_status?: string | null; }; export async function getCreditLedger(params: { @@ -202,6 +204,15 @@ export type SettlementPaymentRow = { created_at?: string; }; +export type SettlementBillPaymentRow = { + id: number; + amount: number; + status: string; + method?: string | null; + proof?: string | null; + remark?: string | null; +}; + export async function getSettlementPayments(params?: { settlement_period_id?: number; admin_site_id?: number; @@ -245,18 +256,9 @@ export type RebateAllocationRow = { allocation_rule: string; }; -export type SettlementPaymentRow = { - id: number; - amount: number; - status: string; - method?: string | null; - proof?: string | null; - remark?: string | null; -}; - export async function getSettlementBill(billId: number): Promise<{ bill: SettlementBillRow; - payments: SettlementPaymentRow[]; + payments: SettlementBillPaymentRow[]; rebate_allocations: RebateAllocationRow[]; adjustments: Array<{ id: number; amount: number; adjustment_type: string; reason: string | null }>; tier_edge?: string | null; diff --git a/src/app/admin/(shell)/admin-roles/page.tsx b/src/app/admin/(shell)/admin-roles/page.tsx index a30a8be..7b6d67f 100644 --- a/src/app/admin/(shell)/admin-roles/page.tsx +++ b/src/app/admin/(shell)/admin-roles/page.tsx @@ -1,6 +1,8 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { AdminRolesConsole } from "@/modules/admin-roles/admin-roles-console"; import { buildPageMetadata } from "@/lib/page-metadata"; +import { PRD_ADMIN_ROLE_MANAGE } from "@/lib/admin-prd"; import type { Metadata } from "next"; export const metadata: Metadata = buildPageMetadata("adminRoles", "title"); @@ -8,7 +10,9 @@ export const metadata: Metadata = buildPageMetadata("adminRoles", "title"); export default function AdminRolesPage() { return ( - + + + ); } diff --git a/src/app/admin/(shell)/admin-users/page.tsx b/src/app/admin/(shell)/admin-users/page.tsx index 97b6f67..aba0dcc 100644 --- a/src/app/admin/(shell)/admin-users/page.tsx +++ b/src/app/admin/(shell)/admin-users/page.tsx @@ -1,6 +1,8 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { AdminUsersConsole } from "@/modules/admin-users/admin-users-console"; import { buildPageMetadata } from "@/lib/page-metadata"; +import { PRD_ADMIN_USER_MANAGE } from "@/lib/admin-prd"; import type { Metadata } from "next"; export const metadata: Metadata = buildPageMetadata("adminUsers", "title"); @@ -8,7 +10,9 @@ export const metadata: Metadata = buildPageMetadata("adminUsers", "title"); export default function AdminUsersPage() { return ( - + + + ); } diff --git a/src/app/admin/(shell)/audit-logs/page.tsx b/src/app/admin/(shell)/audit-logs/page.tsx index 6b2923b..aad8014 100644 --- a/src/app/admin/(shell)/audit-logs/page.tsx +++ b/src/app/admin/(shell)/audit-logs/page.tsx @@ -1,6 +1,8 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { AuditLogsConsole } from "@/modules/audit/audit-logs-console"; import { buildPageMetadata } from "@/lib/page-metadata"; +import { PRD_AUDIT_VIEW } from "@/lib/admin-prd"; import type { Metadata } from "next"; export const metadata: Metadata = buildPageMetadata("audit", "title"); @@ -8,7 +10,9 @@ export const metadata: Metadata = buildPageMetadata("audit", "title"); export default function AdminAuditLogsPage() { return ( - + + + ); } diff --git a/src/app/admin/(shell)/config/page.tsx b/src/app/admin/(shell)/config/page.tsx index 5aabd77..2a25905 100644 --- a/src/app/admin/(shell)/config/page.tsx +++ b/src/app/admin/(shell)/config/page.tsx @@ -1,9 +1,15 @@ +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { ConfigHubScreen } from "@/modules/config/config-hub-screen"; import { buildPageMetadata } from "@/lib/page-metadata"; +import { PRD_CONFIG_HUB_ACCESS_ANY } from "@/lib/admin-prd"; import type { Metadata } from "next"; export const metadata: Metadata = buildPageMetadata("config", "hub.title"); export default function AdminConfigHubPage() { - return ; + return ( + + + + ); } diff --git a/src/app/admin/(shell)/currencies/page.tsx b/src/app/admin/(shell)/currencies/page.tsx index 8452f69..6d4a4d6 100644 --- a/src/app/admin/(shell)/currencies/page.tsx +++ b/src/app/admin/(shell)/currencies/page.tsx @@ -1,6 +1,8 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { CurrencyManagementScreen } from "@/modules/settings/currency-management-screen"; import { buildPageMetadata } from "@/lib/page-metadata"; +import { PRD_CURRENCY_MANAGE } from "@/lib/admin-prd"; import type { Metadata } from "next"; export const metadata: Metadata = buildPageMetadata("config", "currencies.title"); @@ -8,7 +10,9 @@ export const metadata: Metadata = buildPageMetadata("config", "currencies.title" export default function AdminCurrenciesPage() { return ( - + + + ); } diff --git a/src/app/admin/(shell)/reconcile/page.tsx b/src/app/admin/(shell)/reconcile/page.tsx index eaedaa3..533ef04 100644 --- a/src/app/admin/(shell)/reconcile/page.tsx +++ b/src/app/admin/(shell)/reconcile/page.tsx @@ -1,6 +1,8 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { ReconcileConsole } from "@/modules/reconcile/reconcile-console"; import { buildPageMetadata } from "@/lib/page-metadata"; +import { PRD_WALLET_TX_ACCESS_ANY } from "@/lib/admin-prd"; import type { Metadata } from "next"; export const metadata: Metadata = buildPageMetadata("reconcile", "title"); @@ -8,7 +10,9 @@ export const metadata: Metadata = buildPageMetadata("reconcile", "title"); export default function AdminReconcilePage() { return ( - + + + ); } diff --git a/src/app/admin/(shell)/rules/odds/page.tsx b/src/app/admin/(shell)/rules/odds/page.tsx index 38280db..ca1e302 100644 --- a/src/app/admin/(shell)/rules/odds/page.tsx +++ b/src/app/admin/(shell)/rules/odds/page.tsx @@ -1,9 +1,15 @@ +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { RulesOddsConfigScreen } from "@/modules/rules/rules-odds-config-screen"; import { buildPageMetadata } from "@/lib/page-metadata"; +import { PRD_RULES_ODDS_ACCESS_ANY } from "@/lib/admin-prd"; import type { Metadata } from "next"; export const metadata: Metadata = buildPageMetadata("config", "nav.rulesOddsTitle"); export default function AdminRulesOddsPage() { - return ; + return ( + + + + ); } diff --git a/src/app/admin/(shell)/rules/plays/page.tsx b/src/app/admin/(shell)/rules/plays/page.tsx index 4055146..578a7c2 100644 --- a/src/app/admin/(shell)/rules/plays/page.tsx +++ b/src/app/admin/(shell)/rules/plays/page.tsx @@ -1,14 +1,18 @@ +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { PlayConfigDocScreen } from "@/modules/config/doc/play-config-doc-screen"; import { RulesPageShell } from "@/modules/rules/rules-page-shell"; import { buildPageMetadata } from "@/lib/page-metadata"; +import { PRD_RULES_PLAYS_ACCESS_ANY } from "@/lib/admin-prd"; import type { Metadata } from "next"; export const metadata: Metadata = buildPageMetadata("config", "nav.rulesPlaysTitle"); export default function AdminRulesPlaysPage() { return ( - - - + + + + + ); } diff --git a/src/app/admin/(shell)/settings/page.tsx b/src/app/admin/(shell)/settings/page.tsx index d9a8ce2..eb784b1 100644 --- a/src/app/admin/(shell)/settings/page.tsx +++ b/src/app/admin/(shell)/settings/page.tsx @@ -1,6 +1,8 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { SystemSettingsScreen } from "@/modules/settings/system-settings-screen"; import { buildPageMetadata } from "@/lib/page-metadata"; +import { PRD_SETTINGS_ACCESS_ANY } from "@/lib/admin-prd"; import type { Metadata } from "next"; export const metadata: Metadata = buildPageMetadata("common", "nav.settings"); @@ -8,7 +10,9 @@ export const metadata: Metadata = buildPageMetadata("common", "nav.settings"); export default function AdminSettingsPage() { return ( - + + + ); } diff --git a/src/components/admin/login-form.tsx b/src/components/admin/login-form.tsx index 4aed275..058bb71 100644 --- a/src/components/admin/login-form.tsx +++ b/src/components/admin/login-form.tsx @@ -192,7 +192,7 @@ export function LoginForm() {
- +
- @@ -981,7 +1050,7 @@ export function AgentsPlayersPanel({ })} /> - diff --git a/src/modules/config/doc/play-config-doc-screen.tsx b/src/modules/config/doc/play-config-doc-screen.tsx index 3ef28c9..ab23403 100644 --- a/src/modules/config/doc/play-config-doc-screen.tsx +++ b/src/modules/config/doc/play-config-doc-screen.tsx @@ -48,7 +48,7 @@ import { TableRow, } from "@/components/ui/table"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; -import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state"; +import { AdminLoadingState } from "@/components/admin/admin-loading-state"; import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value"; import { ConfigVersionActions } from "@/modules/config/config-version-actions"; import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher"; @@ -560,7 +560,7 @@ export function PlayConfigDocScreen() {
{t("play.filters.category", { ns: "config" })} - setCategoryFilter(value ?? "all")}> {categoryFilter === "all" diff --git a/src/modules/config/doc/wallet-config-doc-screen.tsx b/src/modules/config/doc/wallet-config-doc-screen.tsx index 16d0c0d..29f63ba 100644 --- a/src/modules/config/doc/wallet-config-doc-screen.tsx +++ b/src/modules/config/doc/wallet-config-doc-screen.tsx @@ -12,6 +12,9 @@ import { Button } from "@/components/ui/button"; import { ConfigDocPage } from "@/modules/config/config-doc-page"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { adminHasAnyPermission } from "@/lib/admin-permissions"; +import { PRD_WALLET_ADJUST_MANAGE, PRD_WALLET_RECONCILE_MANAGE } from "@/lib/admin-prd"; +import { useAdminProfile } from "@/stores/admin-session"; import { LotteryApiBizError } from "@/types/api/errors"; function minorUnitsToDisplay(n: unknown, decimals = 2): string { @@ -20,9 +23,11 @@ function minorUnitsToDisplay(n: unknown, decimals = 2): string { return (num / 100).toFixed(decimals); } -function displayToMinorUnits(s: string): number { - const n = parseFloat(s); - if (Number.isNaN(n) || n < 0) return 0; +function displayToMinorUnits(s: string): number | null { + const normalized = s.trim(); + if (normalized === "") return null; + const n = Number(normalized); + if (!Number.isFinite(n)) return null; return Math.round(n * 100); } @@ -46,11 +51,44 @@ type WalletConfigDocScreenProps = { embedded?: boolean; }; +function validateDraft(draft: Draft, t: ReturnType>["t"]): string[] { + const errors: string[] = []; + const values = { + inMin: displayToMinorUnits(draft.inMin), + inMax: displayToMinorUnits(draft.inMax), + outMin: displayToMinorUnits(draft.outMin), + outMax: displayToMinorUnits(draft.outMax), + }; + + for (const field of ["inMin", "inMax", "outMin", "outMax"] as const) { + if (values[field] === null || values[field] < 1) { + errors.push(t("wallet.validation.amountAtLeastMinorUnit", { + ns: "config", + field: t(`wallet.fields.${field}`, { ns: "config" }), + })); + } + } + + if (values.inMin !== null && values.inMax !== null && values.inMax < values.inMin) { + errors.push(t("wallet.validation.inRangeInvalid", { ns: "config" })); + } + if (values.outMin !== null && values.outMax !== null && values.outMax < values.outMin) { + errors.push(t("wallet.validation.outRangeInvalid", { ns: "config" })); + } + + return [...new Set(errors)]; +} + export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScreenProps) { const { t } = useTranslation(["config", "adminUsers", "common"]); const tRef = useRef(t); tRef.current = t; const shared = useOptionalAdminSettingsData(); + const profile = useAdminProfile(); + const canManage = adminHasAnyPermission(profile?.permissions, [ + PRD_WALLET_RECONCILE_MANAGE, + PRD_WALLET_ADJUST_MANAGE, + ]); const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); const [draft, setDraft] = useState({ inMin: "", @@ -66,6 +104,8 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree draft.inMax !== saved.inMax || draft.outMin !== saved.outMin || draft.outMax !== saved.outMax; + const validationErrors = validateDraft(draft, t); + const hasValidationError = validationErrors.length > 0; const loading = embedded ? (shared?.loading ?? true) : standaloneLoading; @@ -107,6 +147,11 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree }; const handleSave = async () => { + if (hasValidationError) { + toast.error(validationErrors[0]); + return; + } + const items = []; if (draft.inMin !== saved.inMin) { items.push({ key: WALLET_KEYS.IN_MIN, value: displayToMinorUnits(draft.inMin) }); @@ -156,7 +201,8 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree placeholder={t("wallet.placeholders.min", { ns: "config" })} value={draft.inMin} onChange={(e) => handleChange("inMin", e.target.value)} - disabled={loading || saving} + disabled={!canManage || loading || saving} + aria-invalid={hasValidationError} />
@@ -169,7 +215,8 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree placeholder={t("wallet.placeholders.max", { ns: "config" })} value={draft.inMax} onChange={(e) => handleChange("inMax", e.target.value)} - disabled={loading || saving} + disabled={!canManage || loading || saving} + aria-invalid={hasValidationError} />
@@ -182,7 +229,8 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree placeholder={t("wallet.placeholders.min", { ns: "config" })} value={draft.outMin} onChange={(e) => handleChange("outMin", e.target.value)} - disabled={loading || saving} + disabled={!canManage || loading || saving} + aria-invalid={hasValidationError} />
@@ -195,10 +243,18 @@ export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScree placeholder={t("wallet.placeholders.max", { ns: "config" })} value={draft.outMax} onChange={(e) => handleChange("outMax", e.target.value)} - disabled={loading || saving} + disabled={!canManage || loading || saving} + aria-invalid={hasValidationError} />
+ {validationErrors.length > 0 && ( +
+ {validationErrors.map((error) => ( +

{error}

+ ))} +
+ )}
diff --git a/src/modules/dashboard/agent-dashboard-console.tsx b/src/modules/dashboard/agent-dashboard-console.tsx index 5d0fd7d..08124a2 100644 --- a/src/modules/dashboard/agent-dashboard-console.tsx +++ b/src/modules/dashboard/agent-dashboard-console.tsx @@ -5,9 +5,13 @@ import { useCallback, useMemo, useState, type ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { BarChart3, + Flame, + Landmark, Network, RefreshCw, + Sparkles, Ticket, + TrendingUp, Users, Wallet, } from "lucide-react"; @@ -48,7 +52,7 @@ export function AgentDashboardConsole(): ReactElement { const tRef = useTranslationRef(["dashboard", "common"]); const profile = useAdminProfile(); const agent = profile?.agent ?? null; - const permissions = profile?.permissions ?? []; + const permissions = useMemo(() => profile?.permissions ?? [], [profile?.permissions]); const todayLabel = useMemo(() => { const locale = normalizeAdminLanguage(i18n.resolvedLanguage ?? i18n.language); @@ -109,6 +113,7 @@ export function AgentDashboardConsole(): ReactElement { }, []); const currency = "NPR"; + const displayCurrency = overview?.currency_code ?? currency; const quickLinks = useMemo(() => { const links: { href: string; label: string; icon: ReactElement }[] = []; @@ -189,78 +194,268 @@ export function AgentDashboardConsole(): ReactElement { ))}
) : overview ? ( -
- - - {t("agent.creditTitle")} - - -

- {formatDashboardCreditMajor(overview.credit_limit, currency)} -

-

- {t("agent.creditAvailable", { - amount: formatDashboardCreditMajor(overview.available_credit, currency), - })} -

-

- {t("agent.creditAllocated", { - amount: formatDashboardCreditMajor(overview.allocated_credit, currency), - })} - {" · "} - {t("agent.creditUsed", { - amount: formatDashboardCreditMajor(overview.used_credit, currency), - })} -

-

- {t("agent.shareRate", { rate: overview.total_share_rate })} - {" · "} - {t("agent.settlementCycle", { cycle: overview.settlement_cycle })} -

-
-
+
+
+ + +
+
+

+ {t("agent.heroEyebrow")} +

+ + {t("agent.heroTitle", { name: overview.agent_name || overview.agent_code })} + +
+
+ +
+
+
+ +
+
+

{t("agent.todayBet")}

+

+ {formatDashboardMoneyMinor(overview.today_bet_minor, displayCurrency)} +

+
+
+

{t("agent.todayPayout")}

+

+ {formatDashboardMoneyMinor(overview.today_payout_minor, displayCurrency)} +

+
+
+

{t("agent.todayProfit")}

+

+ {formatDashboardMoneyMinor(overview.today_profit_minor, displayCurrency)} +

+
+
+
+
+

{t("agent.activePlayersToday")}

+

{overview.active_player_count_today}

+
+
+

{t("agent.betOrdersToday")}

+

{overview.bet_order_count_today}

+
+
+

{t("agent.pendingBills")}

+

{overview.pending_bill_count}

+
+
+
+ {t("agent.shareRate", { rate: overview.total_share_rate })} + {t("agent.settlementCycle", { cycle: overview.settlement_cycle })} + + {overview.latest_bet_at + ? t("agent.latestBetAt", { time: new Date(overview.latest_bet_at).toLocaleString() }) + : t("agent.noBetToday")} + +
+
+
- - - {t("agent.teamTitle")} - - -
-

{t("agent.directChildren")}

-

{overview.direct_child_count}

-
-
-

{t("agent.directPlayers")}

-

{overview.direct_player_count}

-
-
-

{t("agent.subtreeAgents")}

-

{overview.subtree_agent_count}

-
-
-
+ + + {t("agent.creditTitle")} + + +
+

+ {formatDashboardCreditMajor(overview.credit_limit, displayCurrency)} +

+

+ {t("agent.creditAvailable", { + amount: formatDashboardCreditMajor(overview.available_credit, displayCurrency), + })} +

+
+
+
+

{t("agent.creditAllocatedLabel")}

+

+ {formatDashboardCreditMajor(overview.allocated_credit, displayCurrency)} +

+
+
+

{t("agent.creditUsedLabel")}

+

+ {formatDashboardCreditMajor(overview.used_credit, displayCurrency)} +

+
+
+

+ {t("agent.pendingUnpaid", { + amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, displayCurrency), + })} +

+
+
+
- - - {t("agent.pendingBills")} - {adminHasAnyPermission(permissions, [...PRD_SETTLEMENT_AGENT_ACCESS_ANY]) ? ( - - {t("agent.viewBills")} - - ) : null} - - -

{overview.pending_bill_count}

-

- {t("agent.pendingUnpaid", { - amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, currency), - })} -

-
-
+
+ + + + + {t("agent.sevenDayTitle")} + + + +

+ {formatDashboardMoneyMinor(overview.seven_day_bet_minor, displayCurrency)} +

+

+ {t("agent.sevenDayPayout", { + amount: formatDashboardMoneyMinor(overview.seven_day_payout_minor, displayCurrency), + })} +

+

+ {t("agent.sevenDayProfit", { + amount: formatDashboardMoneyMinor(overview.seven_day_profit_minor, displayCurrency), + })} +

+
+
+ + + + + + {t("agent.teamTitle")} + + + +
+

{t("agent.directChildren")}

+

{overview.direct_child_count}

+
+
+

{t("agent.subtreeAgents")}

+

{overview.subtree_agent_count}

+
+
+

{t("agent.directPlayers")}

+

{overview.direct_player_count}

+
+
+

{t("agent.teamPlayers")}

+

{overview.team_player_count}

+
+
+
+ + + + + + {t("agent.pendingBills")} + + + +

{overview.pending_bill_count}

+

+ {t("agent.pendingUnpaid", { + amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, displayCurrency), + })} +

+
+
+ + + + + + {t("agent.topMomentum")} + + + + {overview.top_agent_today ? ( + <> +

+ {overview.top_agent_today.agent_name || overview.top_agent_today.agent_code} +

+

+ {formatDashboardMoneyMinor(overview.top_agent_today.total_bet_minor, displayCurrency)} +

+

+ {t("agent.topMomentumHint", { + profit: formatDashboardMoneyMinor( + overview.top_agent_today.approx_house_gross_minor, + displayCurrency, + ), + })} +

+ + ) : ( +

{t("agent.noBetToday")}

+ )} +
+
+
+ +
+ + + + + {t("agent.managementFocus")} + + + +
+

{t("agent.focusBet")}

+

+ {formatDashboardMoneyMinor(overview.today_bet_minor, displayCurrency)} +

+
+
+

{t("agent.focusPlayers")}

+

{overview.active_player_count_today}

+
+
+

{t("agent.focusBills")}

+

{overview.pending_bill_count}

+
+
+
+ + + + {t("agent.quickStatsTitle")} + + +
+ {t("agent.canCreateChildAgent")} + + {overview.can_create_child_agent ? t("agent.yes") : t("agent.no")} + +
+
+ {t("agent.canCreatePlayer")} + + {overview.can_create_player ? t("agent.yes") : t("agent.no")} + +
+
+ {t("agent.lineDepth")} + {overview.depth} +
+ {adminHasAnyPermission(permissions, [...PRD_SETTLEMENT_AGENT_ACCESS_ANY]) ? ( + + {t("agent.viewBills")} + + ) : null} +
+
+
) : null} diff --git a/src/modules/draws/draw-detail-console.tsx b/src/modules/draws/draw-detail-console.tsx index 8be79a5..b334c80 100644 --- a/src/modules/draws/draw-detail-console.tsx +++ b/src/modules/draws/draw-detail-console.tsx @@ -35,7 +35,6 @@ import { drawStatusLabel, hallPreviewDiffersFromDbStatus } from "./draw-display" import { DrawStatusBadge } from "./draw-status-badge"; import { PRD_DRAW_REOPEN_MANAGE, - PRD_DRAW_RESULT_MANAGE, PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW, } from "./draw-prd"; @@ -173,7 +172,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) { label: t("manualClose"), variant: "outline", enabled: ["pending", "open"].includes(data.status), - onConfirm: () => postAdminManualCloseDraw(idNum), + onConfirm: async () => { await postAdminManualCloseDraw(idNum); }, confirmTitle: t("confirm.manualCloseTitle"), confirmDescription: t("confirm.manualCloseDescription"), }, @@ -182,7 +181,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) { label: t("cancelBeforeDraw"), variant: "outline", enabled: ["pending", "open", "closing", "closed"].includes(data.status), - onConfirm: () => postAdminCancelDraw(idNum), + onConfirm: async () => { await postAdminCancelDraw(idNum); }, confirmTitle: t("confirm.cancelDrawTitle"), confirmDescription: t("confirm.cancelDrawDescription"), }, @@ -191,7 +190,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) { label: t("rngAutoGenerate"), variant: "outline", enabled: data.status === "closed", - onConfirm: () => postAdminRunDrawRng(idNum), + onConfirm: async () => { await postAdminRunDrawRng(idNum); }, confirmTitle: t("confirm.rngDrawTitle"), confirmDescription: t("confirm.rngDrawDescription"), }, @@ -204,7 +203,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) { label: t("cooldownReopen"), variant: "destructive", enabled: data.status === "cooldown", - onConfirm: () => postAdminReopenDraw(idNum), + onConfirm: async () => { await postAdminReopenDraw(idNum); }, confirmTitle: t("confirm.reopenTitle"), confirmDescription: t("confirm.reopenDescription"), confirmVariant: "destructive", @@ -217,7 +216,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) { label: t("runSettlement"), variant: "outline", enabled: data.status === "settling", - onConfirm: () => postAdminRunDrawSettlement(idNum), + onConfirm: async () => { await postAdminRunDrawSettlement(idNum); }, confirmTitle: t("confirm.runSettlementTitle"), confirmDescription: t("confirm.runSettlementDescription"), }); diff --git a/src/modules/integration/integration-sites-console.tsx b/src/modules/integration/integration-sites-console.tsx index 3a26772..bb4661d 100644 --- a/src/modules/integration/integration-sites-console.tsx +++ b/src/modules/integration/integration-sites-console.tsx @@ -18,7 +18,7 @@ import { putAdminIntegrationSite, } from "@/api/admin-integration-sites"; import { AdminPageCard } from "@/components/admin/admin-page-card"; -import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; +import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { Button } from "@/components/ui/button"; @@ -41,7 +41,7 @@ import { TableRow, } from "@/components/ui/table"; import { Textarea } from "@/components/ui/textarea"; -import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state"; +import { AdminLoadingState } from "@/components/admin/admin-loading-state"; import { adminHasAnyPermission } from "@/lib/admin-permissions"; import { getAdminPageBundle } from "@/lib/admin-permission-bundles"; import { useAdminProfile } from "@/stores/admin-session"; @@ -252,7 +252,7 @@ export function IntegrationSitesConsole({ } finally { setLoading(false); } - }, []); + }, [tRef]); useAsyncEffect(() => { void load(); @@ -572,6 +572,7 @@ export function IntegrationSitesConsole({ key: "connectivity", label: t("integrationSites.connectivityTest"), icon: Link2, + hidden: !canManage, onClick: () => openConnectivity(row), }, { @@ -790,7 +791,7 @@ export function IntegrationSitesConsole({ @@ -381,5 +513,6 @@ export function AgentBillDetail({ ) : null} + ); } diff --git a/src/modules/settlement/settlement-credit-ledger-panel.tsx b/src/modules/settlement/settlement-credit-ledger-panel.tsx index d7a3b67..c6671b3 100644 --- a/src/modules/settlement/settlement-credit-ledger-panel.tsx +++ b/src/modules/settlement/settlement-credit-ledger-panel.tsx @@ -143,6 +143,8 @@ export function SettlementCreditLedgerPanel({ const refLabel = (row: SettlementCreditLedgerRow): string => { const parts: string[] = []; + const ticketLabel = row.ticket_item_id ? `#${row.ticket_item_id}` : null; + const billLabel = row.settlement_bill_id ? `#${row.settlement_bill_id}` : null; if (row.biz_no) { parts.push(row.biz_no); } @@ -152,14 +154,57 @@ export function SettlementCreditLedgerPanel({ if (row.play_code) { parts.push(row.play_code); } - if (row.ticket_item_id) { - parts.push(`#${row.ticket_item_id}`); + if (ticketLabel) { + parts.push(ticketLabel); } - if (row.settlement_bill_id) { - parts.push(`#${row.settlement_bill_id}`); + if (billLabel) { + parts.push(billLabel); } - return parts.length > 0 ? parts.join(" · ") : "—"; + const normalizedParts = parts + .map((part) => { + let normalized = part; + if (billLabel) { + normalized = normalized.replace(new RegExp(`\\bbill${billLabel.replace("#", "\\#")}\\b`, "g"), ""); + } + if (ticketLabel) { + normalized = normalized.replace(new RegExp(`\\bticket_item${ticketLabel.replace("#", "\\#")}\\b`, "g"), ""); + } + + return normalized + .split(" · ") + .map((segment) => segment.trim()) + .filter(Boolean) + .join(" · "); + }) + .filter(Boolean); + + const uniqueParts = normalizedParts.filter((part, index) => { + if (normalizedParts.indexOf(part) !== index) { + return false; + } + if (billLabel && part !== billLabel && part.endsWith(billLabel)) { + return false; + } + if (ticketLabel && part !== ticketLabel && part.endsWith(ticketLabel)) { + return false; + } + + return true; + }); + + return uniqueParts.length > 0 ? uniqueParts.join(" · ") : "—"; + }; + + const statusLabel = (row: SettlementCreditLedgerRow): string => { + if (row.bill_status) { + return settlementBillStatusLabel(row.bill_status, t); + } + if (row.status === "posted") { + return t("creditLedger.status.posted", { defaultValue: "已入账" }); + } + + return row.status || "—"; }; return ( @@ -306,7 +351,7 @@ export function SettlementCreditLedgerPanel({ {row.settlement_bill_id ? `#${row.settlement_bill_id}` : "—"} - {row.bill_status ? settlementBillStatusLabel(row.bill_status, t) : row.status} + {statusLabel(row)} diff --git a/src/modules/tickets/player-tickets-console.tsx b/src/modules/tickets/player-tickets-console.tsx index 69433ed..33eb601 100644 --- a/src/modules/tickets/player-tickets-console.tsx +++ b/src/modules/tickets/player-tickets-console.tsx @@ -406,7 +406,7 @@ export function PlayerTicketsConsole(): React.ReactElement { ? [ { key: "view-player", - label: t("viewDetail", { ns: "players" }), + label: t("viewPlayer", { ns: "tickets" }), icon: Eye, href: adminPlayerDetailPath(row.player_id), }, diff --git a/src/types/api/admin-dashboard.ts b/src/types/api/admin-dashboard.ts index 6373458..2d27093 100644 --- a/src/types/api/admin-dashboard.ts +++ b/src/types/api/admin-dashboard.ts @@ -66,8 +66,27 @@ export type AdminDashboardAgentOverview = { direct_child_count: number; subtree_agent_count: number; direct_player_count: number; + team_player_count: number; + active_player_count_today: number; + bet_order_count_today: number; + today_bet_minor: number; + today_payout_minor: number; + today_profit_minor: number; + seven_day_bet_minor: number; + seven_day_payout_minor: number; + seven_day_profit_minor: number; + currency_code: string | null; pending_bill_count: number; pending_unpaid_minor: number; + latest_bet_at: string | null; + top_agent_today: { + agent_node_id: number; + agent_code: string; + agent_name: string; + total_bet_minor: number; + total_payout_minor: number; + approx_house_gross_minor: number; + } | null; }; /** 全站待审核开奖批次队列(不限于大厅当前期) */ diff --git a/src/types/api/index.ts b/src/types/api/index.ts index 4aab3a5..eacdd80 100644 --- a/src/types/api/index.ts +++ b/src/types/api/index.ts @@ -62,6 +62,7 @@ export type { AdminRoleRow, AdminRoleUpdatePayload, AdminPermissionCatalogData, + AdminUserSiteBinding, AdminUserPermissionListData, AdminUserPermissionRow, AdminUserPermissionSyncData,