feat(draws, config, deployment): 合并开奖结果/审核页,新增下注筛选并完善开注商配置
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
This commit is contained in:
@@ -15,14 +15,17 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
/** 改成服务器上的绝对路径,例如 /www/wwwroot/lottery-admin.cjdhr.top */
|
/** 改成服务器上的绝对路径,例如 /www/wwwroot/lottery-admin.cjdhr.top */
|
||||||
const APP_CWD = path.resolve(__dirname);
|
const DEPLOY_ROOT = path.resolve(__dirname);
|
||||||
|
// deploy.sh uploads the standalone bundle to app/. Keep runtime and static assets
|
||||||
|
// on the same build so Next.js chunk references cannot become mismatched.
|
||||||
|
const APP_CWD = path.join(DEPLOY_ROOT, "app");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
apps: [
|
apps: [
|
||||||
{
|
{
|
||||||
name: "lotteryadmin",
|
name: "lotteryadmin",
|
||||||
cwd: APP_CWD,
|
cwd: APP_CWD,
|
||||||
script: ".next/standalone/server.js",
|
script: "server.js",
|
||||||
interpreter: "node",
|
interpreter: "node",
|
||||||
exec_mode: "fork",
|
exec_mode: "fork",
|
||||||
instances: 1,
|
instances: 1,
|
||||||
@@ -31,8 +34,8 @@ module.exports = {
|
|||||||
max_memory_restart: "1G",
|
max_memory_restart: "1G",
|
||||||
time: true,
|
time: true,
|
||||||
merge_logs: true,
|
merge_logs: true,
|
||||||
out_file: path.join(APP_CWD, "logs/pm2-out.log"),
|
out_file: path.join(DEPLOY_ROOT, "logs/pm2-out.log"),
|
||||||
error_file: path.join(APP_CWD, "logs/pm2-error.log"),
|
error_file: path.join(DEPLOY_ROOT, "logs/pm2-error.log"),
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
@@ -41,7 +44,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// LOTTERY_API_UPSTREAM 只写在 .env,勿在此硬编码(会覆盖 .env 导致 API 502)
|
// LOTTERY_API_UPSTREAM 只写在 .env,勿在此硬编码(会覆盖 .env 导致 API 502)
|
||||||
env_file: path.join(APP_CWD, ".env"),
|
env_file: path.join(DEPLOY_ROOT, ".env"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
BIN
public/logo_副本.png
Normal file
BIN
public/logo_副本.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 369 KiB |
@@ -23,6 +23,7 @@ export type AdminDrawListQuery = {
|
|||||||
draw_no?: string;
|
draw_no?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
agent_node_id?: number;
|
agent_node_id?: number;
|
||||||
|
has_player_bets?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getAdminDraws(q: AdminDrawListQuery = {}): Promise<AdminDrawListData> {
|
export async function getAdminDraws(q: AdminDrawListQuery = {}): Promise<AdminDrawListData> {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
import { redirect } from "next/navigation";
|
||||||
import { DrawPublishConsole } from "@/modules/draws/draw-publish-console";
|
|
||||||
import { PRD_DRAW_RESULT_MANAGE } from "@/lib/admin-prd";
|
|
||||||
import { buildPageMetadata } from "@/lib/page-metadata";
|
import { buildPageMetadata } from "@/lib/page-metadata";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
@@ -9,10 +7,6 @@ export const metadata: Metadata = buildPageMetadata("draws", "publishTitle");
|
|||||||
export default async function AdminDrawPublishBatchPage(props: {
|
export default async function AdminDrawPublishBatchPage(props: {
|
||||||
params: Promise<{ drawId: string; batchId: string }>;
|
params: Promise<{ drawId: string; batchId: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { drawId, batchId } = await props.params;
|
const { drawId } = await props.params;
|
||||||
return (
|
redirect(`/admin/draws/${drawId}/results`);
|
||||||
<AdminPermissionGate requiredAny={[PRD_DRAW_RESULT_MANAGE]}>
|
|
||||||
<DrawPublishConsole drawId={drawId} batchId={batchId} />
|
|
||||||
</AdminPermissionGate>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
||||||
import { DrawResultsConsole } from "@/modules/draws/draw-results-console";
|
import { DrawResultsConsole } from "@/modules/draws/draw-results-console";
|
||||||
import { PRD_DRAW_ACCESS_ANY } from "@/lib/admin-prd";
|
import { DrawReviewConsole } from "@/modules/draws/draw-review-console";
|
||||||
|
import { PRD_DRAW_ACCESS_ANY, PRD_DRAW_RESULT_MANAGE } from "@/lib/admin-prd";
|
||||||
|
|
||||||
export default async function AdminDrawResultsPage(props: {
|
export default async function AdminDrawResultsPage(props: {
|
||||||
params: Promise<{ drawId: string }>;
|
params: Promise<{ drawId: string }>;
|
||||||
@@ -8,7 +9,12 @@ export default async function AdminDrawResultsPage(props: {
|
|||||||
const { drawId } = await props.params;
|
const { drawId } = await props.params;
|
||||||
return (
|
return (
|
||||||
<AdminPermissionGate requiredAny={PRD_DRAW_ACCESS_ANY}>
|
<AdminPermissionGate requiredAny={PRD_DRAW_ACCESS_ANY}>
|
||||||
<DrawResultsConsole drawId={drawId} />
|
<div className="space-y-6">
|
||||||
|
<DrawResultsConsole drawId={drawId} />
|
||||||
|
<AdminPermissionGate requiredAny={[PRD_DRAW_RESULT_MANAGE]}>
|
||||||
|
<DrawReviewConsole drawId={drawId} />
|
||||||
|
</AdminPermissionGate>
|
||||||
|
</div>
|
||||||
</AdminPermissionGate>
|
</AdminPermissionGate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
|
import { redirect } from "next/navigation";
|
||||||
import { DrawReviewConsole } from "@/modules/draws/draw-review-console";
|
|
||||||
import { PRD_DRAW_RESULT_MANAGE } from "@/lib/admin-prd";
|
|
||||||
|
|
||||||
export default async function AdminDrawReviewPage(props: {
|
export default async function AdminDrawReviewPage(props: {
|
||||||
params: Promise<{ drawId: string }>;
|
params: Promise<{ drawId: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { drawId } = await props.params;
|
const { drawId } = await props.params;
|
||||||
return (
|
redirect(`/admin/draws/${drawId}/results`);
|
||||||
<AdminPermissionGate requiredAny={[PRD_DRAW_RESULT_MANAGE]}>
|
|
||||||
<DrawReviewConsole drawId={drawId} />
|
|
||||||
</AdminPermissionGate>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 152 KiB |
@@ -32,6 +32,7 @@
|
|||||||
"queryDraw": "Search draw",
|
"queryDraw": "Search draw",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"fuzzyDrawNo": "Fuzzy draw no.",
|
"fuzzyDrawNo": "Fuzzy draw no.",
|
||||||
|
"hasPlayerBets": "Has player bets",
|
||||||
"viewDetails": "View draw details",
|
"viewDetails": "View draw details",
|
||||||
"editDraw": {
|
"editDraw": {
|
||||||
"action": "Edit draw",
|
"action": "Edit draw",
|
||||||
@@ -82,6 +83,10 @@
|
|||||||
"rng": "RNG auto-generated",
|
"rng": "RNG auto-generated",
|
||||||
"manual": "Manual entry"
|
"manual": "Manual entry"
|
||||||
},
|
},
|
||||||
|
"resultGroups": {
|
||||||
|
"starter": "Starter",
|
||||||
|
"consolation": "Consolation"
|
||||||
|
},
|
||||||
"batchStatusOptions": {
|
"batchStatusOptions": {
|
||||||
"pending_review": "Pending review",
|
"pending_review": "Pending review",
|
||||||
"published": "Published"
|
"published": "Published"
|
||||||
|
|||||||
@@ -116,6 +116,7 @@
|
|||||||
"financeOverview": "ड्रअ वित्तीय सारांश",
|
"financeOverview": "ड्रअ वित्तीय सारांश",
|
||||||
"finishedAt": "समाप्त समय",
|
"finishedAt": "समाप्त समय",
|
||||||
"fuzzyDrawNo": "फजी ड्रअ नं.",
|
"fuzzyDrawNo": "फजी ड्रअ नं.",
|
||||||
|
"hasPlayerBets": "खेलाडीको दांव भएको",
|
||||||
"generateFailed": "सिर्जना असफल भयो",
|
"generateFailed": "सिर्जना असफल भयो",
|
||||||
"generatePlan": "ड्रअ योजना सिर्जना",
|
"generatePlan": "ड्रअ योजना सिर्जना",
|
||||||
"generateSuccess": "{{created}} ड्रअ सिर्जना भयो, बफर {{upcoming}}/{{target}}",
|
"generateSuccess": "{{created}} ड्रअ सिर्जना भयो, बफर {{upcoming}}/{{target}}",
|
||||||
@@ -176,6 +177,10 @@
|
|||||||
"manual": "म्यानुअल प्रविष्टि",
|
"manual": "म्यानुअल प्रविष्टि",
|
||||||
"rng": "RNG स्वचालित"
|
"rng": "RNG स्वचालित"
|
||||||
},
|
},
|
||||||
|
"resultGroups": {
|
||||||
|
"starter": "विशेष",
|
||||||
|
"consolation": "सान्त्वना"
|
||||||
|
},
|
||||||
"resultsTitle": "परिणाम",
|
"resultsTitle": "परिणाम",
|
||||||
"reviewAndPublish": "समीक्षा / प्रकाशित",
|
"reviewAndPublish": "समीक्षा / प्रकाशित",
|
||||||
"reviewAndPublishAction": "जाँचेर प्रकाशित गर्नुहोस्",
|
"reviewAndPublishAction": "जाँचेर प्रकाशित गर्नुहोस्",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"queryDraw": "查询期号",
|
"queryDraw": "查询期号",
|
||||||
"reset": "重置",
|
"reset": "重置",
|
||||||
"fuzzyDrawNo": "模糊匹配期号",
|
"fuzzyDrawNo": "模糊匹配期号",
|
||||||
|
"hasPlayerBets": "有玩家下注",
|
||||||
"viewDetails": "查看期号详情",
|
"viewDetails": "查看期号详情",
|
||||||
"editDraw": {
|
"editDraw": {
|
||||||
"action": "编辑期号",
|
"action": "编辑期号",
|
||||||
@@ -82,6 +83,10 @@
|
|||||||
"rng": "RNG 自动生成",
|
"rng": "RNG 自动生成",
|
||||||
"manual": "人工录入"
|
"manual": "人工录入"
|
||||||
},
|
},
|
||||||
|
"resultGroups": {
|
||||||
|
"starter": "特别奖",
|
||||||
|
"consolation": "安慰奖"
|
||||||
|
},
|
||||||
"batchStatusOptions": {
|
"batchStatusOptions": {
|
||||||
"pending_review": "待审核",
|
"pending_review": "待审核",
|
||||||
"published": "已发布"
|
"published": "已发布"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import type { AdminBetProviderRow } from "@/types/api/admin-bet-provider";
|
|||||||
type ProviderFormState = {
|
type ProviderFormState = {
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
short_code: string;
|
||||||
is_enabled: boolean;
|
is_enabled: boolean;
|
||||||
sort_order: string;
|
sort_order: string;
|
||||||
};
|
};
|
||||||
@@ -48,6 +49,7 @@ type ProviderFormState = {
|
|||||||
const EMPTY_FORM: ProviderFormState = {
|
const EMPTY_FORM: ProviderFormState = {
|
||||||
code: "",
|
code: "",
|
||||||
name: "",
|
name: "",
|
||||||
|
short_code: "",
|
||||||
is_enabled: true,
|
is_enabled: true,
|
||||||
sort_order: "0",
|
sort_order: "0",
|
||||||
};
|
};
|
||||||
@@ -56,6 +58,7 @@ function toFormState(row: AdminBetProviderRow): ProviderFormState {
|
|||||||
return {
|
return {
|
||||||
code: row.code,
|
code: row.code,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
short_code: row.short_code,
|
||||||
is_enabled: row.is_enabled,
|
is_enabled: row.is_enabled,
|
||||||
sort_order: String(row.sort_order),
|
sort_order: String(row.sort_order),
|
||||||
};
|
};
|
||||||
@@ -121,8 +124,9 @@ export function BetProviderSettingsPanel() {
|
|||||||
async function handleSubmit(): Promise<void> {
|
async function handleSubmit(): Promise<void> {
|
||||||
const name = form.name.trim();
|
const name = form.name.trim();
|
||||||
const sortOrder = parseSortOrder(form.sort_order);
|
const sortOrder = parseSortOrder(form.sort_order);
|
||||||
if (name === "" || (mode === "create" && form.code.trim() === "")) {
|
const shortCode = form.short_code.trim().toUpperCase();
|
||||||
toast.error(t("betProviders.form.required", { ns: "config", defaultValue: "请填写代码和名称" }));
|
if (name === "" || shortCode.length !== 1 || !/^[A-Z]$/.test(shortCode) || (mode === "create" && form.code.trim() === "")) {
|
||||||
|
toast.error(t("betProviders.form.required", { ns: "config", defaultValue: "请填写代码、名称和一个字母缩写" }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +136,7 @@ export function BetProviderSettingsPanel() {
|
|||||||
await postAdminBetProvider({
|
await postAdminBetProvider({
|
||||||
code: form.code.trim().toUpperCase(),
|
code: form.code.trim().toUpperCase(),
|
||||||
name,
|
name,
|
||||||
|
short_code: shortCode,
|
||||||
is_enabled: form.is_enabled,
|
is_enabled: form.is_enabled,
|
||||||
sort_order: sortOrder,
|
sort_order: sortOrder,
|
||||||
});
|
});
|
||||||
@@ -139,6 +144,7 @@ export function BetProviderSettingsPanel() {
|
|||||||
} else if (editingId !== null) {
|
} else if (editingId !== null) {
|
||||||
await putAdminBetProvider(editingId, {
|
await putAdminBetProvider(editingId, {
|
||||||
name,
|
name,
|
||||||
|
short_code: shortCode,
|
||||||
is_enabled: form.is_enabled,
|
is_enabled: form.is_enabled,
|
||||||
sort_order: sortOrder,
|
sort_order: sortOrder,
|
||||||
});
|
});
|
||||||
@@ -173,6 +179,7 @@ export function BetProviderSettingsPanel() {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="whitespace-nowrap">{t("betProviders.table.code", { ns: "config", defaultValue: "代码" })}</TableHead>
|
<TableHead className="whitespace-nowrap">{t("betProviders.table.code", { ns: "config", defaultValue: "代码" })}</TableHead>
|
||||||
<TableHead>{t("betProviders.table.name", { ns: "config", defaultValue: "名称" })}</TableHead>
|
<TableHead>{t("betProviders.table.name", { ns: "config", defaultValue: "名称" })}</TableHead>
|
||||||
|
<TableHead className="whitespace-nowrap text-center">{t("betProviders.table.shortCode", { ns: "config", defaultValue: "缩写" })}</TableHead>
|
||||||
<TableHead className="whitespace-nowrap">{t("betProviders.table.enabled", { ns: "config", defaultValue: "状态" })}</TableHead>
|
<TableHead className="whitespace-nowrap">{t("betProviders.table.enabled", { ns: "config", defaultValue: "状态" })}</TableHead>
|
||||||
<TableHead className="text-center">{t("betProviders.table.sort", { ns: "config", defaultValue: "排序" })}</TableHead>
|
<TableHead className="text-center">{t("betProviders.table.sort", { ns: "config", defaultValue: "排序" })}</TableHead>
|
||||||
<TableHead className="sticky right-0 z-20 w-14 bg-muted text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
|
<TableHead className="sticky right-0 z-20 w-14 bg-muted text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
|
||||||
@@ -183,12 +190,12 @@ export function BetProviderSettingsPanel() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="text-center text-sm text-muted-foreground">
|
<TableCell colSpan={6} className="text-center text-sm text-muted-foreground">
|
||||||
{t("betProviders.loading", { ns: "config", defaultValue: "加载中..." })}
|
{t("betProviders.loading", { ns: "config", defaultValue: "加载中..." })}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : items.length === 0 ? (
|
) : items.length === 0 ? (
|
||||||
<AdminTableNoResourceRow colSpan={5} cellClassName="text-center" />
|
<AdminTableNoResourceRow colSpan={6} cellClassName="text-center" />
|
||||||
) : (
|
) : (
|
||||||
items.map((row) => (
|
items.map((row) => (
|
||||||
<TableRow key={row.id}>
|
<TableRow key={row.id}>
|
||||||
@@ -201,6 +208,7 @@ export function BetProviderSettingsPanel() {
|
|||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="text-center font-mono font-bold">{row.short_code}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<AdminStatusBadge status={row.is_enabled ? "enabled" : "disabled"}>
|
<AdminStatusBadge status={row.is_enabled ? "enabled" : "disabled"}>
|
||||||
{row.is_enabled
|
{row.is_enabled
|
||||||
@@ -267,6 +275,17 @@ export function BetProviderSettingsPanel() {
|
|||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="provider-short-code">{t("betProviders.form.shortCode", { ns: "config", defaultValue: "缩写(1 个英文字母)" })}</Label>
|
||||||
|
<Input
|
||||||
|
id="provider-short-code"
|
||||||
|
value={form.short_code}
|
||||||
|
maxLength={1}
|
||||||
|
placeholder="S"
|
||||||
|
onChange={(e) => updateForm("short_code", e.target.value.toUpperCase().replace(/[^A-Z]/g, ""))}
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="provider-sort">{t("betProviders.form.sort", { ns: "config", defaultValue: "排序" })}</Label>
|
<Label htmlFor="provider-sort">{t("betProviders.form.sort", { ns: "config", defaultValue: "排序" })}</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -55,14 +55,6 @@ import type { AdminBetProviderRow } from "@/types/api/admin-bet-provider";
|
|||||||
|
|
||||||
import { OddsConfigDraftBar } from "@/modules/config/doc/odds-config-draft-bar";
|
import { OddsConfigDraftBar } from "@/modules/config/doc/odds-config-draft-bar";
|
||||||
import { OddsConfigPlayNav } from "@/modules/config/doc/odds-config-play-nav";
|
import { OddsConfigPlayNav } from "@/modules/config/doc/odds-config-play-nav";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import {
|
import {
|
||||||
buildOddsPlayFilterGroups,
|
buildOddsPlayFilterGroups,
|
||||||
filterOddsPlayTypesByCategory,
|
filterOddsPlayTypesByCategory,
|
||||||
@@ -746,85 +738,10 @@ export function OddsConfigDocScreen({
|
|||||||
return { scope, row, hint, sourceLabel, sourceClassName };
|
return { scope, row, hint, sourceLabel, sourceClassName };
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergedOddsTable = (
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>{t("odds.table.prizeScope", { ns: "config" })}</TableHead>
|
|
||||||
<TableHead className="w-[10rem] text-right">{t("odds.table.multiplier", { ns: "config" })}</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{scopeEditorRows.map(({ scope, row, hint, sourceLabel, sourceClassName }) => (
|
|
||||||
<TableRow key={scope}>
|
|
||||||
<TableCell className="font-medium">
|
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
|
||||||
<span>{prizeScopeLabel(scope, t)}</span>
|
|
||||||
{hint ? <span className="text-xs font-normal text-muted-foreground">{hint}</span> : null}
|
|
||||||
{row ? (
|
|
||||||
<span className={cn("rounded-full border px-1.5 py-0.5 text-[11px] font-semibold", sourceClassName)}>
|
|
||||||
{sourceLabel}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
{row ? (
|
|
||||||
canEditDraft ? (
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
inputMode="decimal"
|
|
||||||
className="ml-auto h-9 w-full max-w-[9rem] text-base font-semibold"
|
|
||||||
disabled={saving}
|
|
||||||
value={oddsMultiplierLabel(row.odds_value)}
|
|
||||||
placeholder={t("odds.placeholders.multiplier", { ns: "config" })}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateOddsForScope(scope, {
|
|
||||||
odds_value: parseOddsMultiplierInput(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ConfigReadonlyValue className="ml-auto h-9 w-full max-w-[9rem] justify-center text-base font-semibold">
|
|
||||||
{oddsMultiplierLabel(row.odds_value)}
|
|
||||||
</ConfigReadonlyValue>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-destructive">
|
|
||||||
{t("odds.missingScopeRow", { ns: "config", scope })}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
<TableRow>
|
|
||||||
<TableCell className="font-medium">{t("odds.rebateRate", { ns: "config" })}</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
{canEditDraft ? (
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
inputMode="decimal"
|
|
||||||
className="ml-auto h-9 w-full max-w-[9rem] text-base font-semibold"
|
|
||||||
disabled={saving}
|
|
||||||
value={rebatePercentUi}
|
|
||||||
placeholder={t("odds.placeholders.rebateRate", { ns: "config" })}
|
|
||||||
onChange={(e) => setRebateForPlayPercent(e.target.value)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ConfigReadonlyValue className="ml-auto h-9 w-full max-w-[9rem] justify-center text-base font-semibold">
|
|
||||||
{rebatePercentUi}
|
|
||||||
</ConfigReadonlyValue>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
|
|
||||||
const classicOddsGrid = (
|
const classicOddsGrid = (
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-4 sm:grid-cols-3">
|
<div className="grid grid-cols-2 gap-3 xl:grid-cols-3">
|
||||||
{scopeEditorRows.map(({ scope, row, hint, sourceLabel, sourceClassName }) => (
|
{scopeEditorRows.map(({ scope, row, hint, sourceLabel, sourceClassName }) => (
|
||||||
<div key={scope} className="grid min-w-0 gap-1.5">
|
<div key={scope} className="grid min-w-0 gap-2 border border-border/60 bg-muted/20 p-3">
|
||||||
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
||||||
<Label className="text-xs font-medium text-muted-foreground">
|
<Label className="text-xs font-medium text-muted-foreground">
|
||||||
{prizeScopeLabel(scope, t)}
|
{prizeScopeLabel(scope, t)}
|
||||||
@@ -861,7 +778,7 @@ export function OddsConfigDocScreen({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="grid min-w-0 gap-1.5">
|
<div className="grid min-w-0 gap-2 border border-border/60 bg-muted/20 p-3">
|
||||||
<Label className="truncate text-xs font-medium text-muted-foreground">
|
<Label className="truncate text-xs font-medium text-muted-foreground">
|
||||||
{t("odds.rebateRate", { ns: "config" })}
|
{t("odds.rebateRate", { ns: "config" })}
|
||||||
</Label>
|
</Label>
|
||||||
@@ -896,7 +813,7 @@ export function OddsConfigDocScreen({
|
|||||||
/>
|
/>
|
||||||
) : resolvedPlayCode ? (
|
) : resolvedPlayCode ? (
|
||||||
<div className={cn(!mergedLayout && embedded ? "rounded-xl border border-border/60 bg-card p-4" : undefined)}>
|
<div className={cn(!mergedLayout && embedded ? "rounded-xl border border-border/60 bg-card p-4" : undefined)}>
|
||||||
{mergedLayout ? mergedOddsTable : classicOddsGrid}
|
{classicOddsGrid}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
@@ -976,8 +893,8 @@ export function OddsConfigDocScreen({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{toolbarBlock}
|
{toolbarBlock}
|
||||||
<div className="grid gap-0 rounded-lg border border-border/60 lg:grid-cols-[minmax(0,13rem)_minmax(0,1fr)]">
|
<div className="grid gap-0 overflow-hidden rounded-lg border border-border/60 lg:grid-cols-[minmax(0,17rem)_minmax(0,1fr)]">
|
||||||
<aside className="space-y-4 border-b border-border/50 px-4 py-4 lg:border-r lg:border-b-0">
|
<aside className="space-y-4 border-b border-border/50 bg-muted/20 px-4 py-4 lg:border-r lg:border-b-0">
|
||||||
<ConfigChipGroup label={t("betProviders.title", { ns: "config", defaultValue: "开注商" })}>
|
<ConfigChipGroup label={t("betProviders.title", { ns: "config", defaultValue: "开注商" })}>
|
||||||
<ConfigChip
|
<ConfigChip
|
||||||
active={providerCode === GLOBAL_PROVIDER_CODE}
|
active={providerCode === GLOBAL_PROVIDER_CODE}
|
||||||
@@ -1004,9 +921,12 @@ export function OddsConfigDocScreen({
|
|||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="border-b border-border/50 px-4 py-2.5 sm:px-5">
|
<div className="flex items-center justify-between border-b border-border/50 px-4 py-3 sm:px-5">
|
||||||
<h3 className="text-base font-semibold">{activePlayLabel}</h3>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">{activeProviderLabel}</p>
|
<p className="text-xs font-medium text-muted-foreground">{activeProviderLabel}</p>
|
||||||
|
<h3 className="mt-0.5 text-lg font-bold">{activePlayLabel}</h3>
|
||||||
|
</div>
|
||||||
|
<span className="rounded-md bg-muted px-2 py-1 font-mono text-xs text-muted-foreground">{resolvedPlayCode}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-4 sm:px-5">{mainBlock}</div>
|
<div className="px-4 py-4 sm:px-5">{mainBlock}</div>
|
||||||
{isDraft && canManage ? (
|
{isDraft && canManage ? (
|
||||||
|
|||||||
@@ -72,8 +72,7 @@ export function OddsConfigPlayNav({
|
|||||||
))}
|
))}
|
||||||
</ConfigChipGroup>
|
</ConfigChipGroup>
|
||||||
|
|
||||||
{/* 小屏:下拉快速切换玩法 */}
|
<div className="space-y-1.5">
|
||||||
<div className="space-y-1.5 lg:hidden">
|
|
||||||
<p className="text-sm font-medium">{t("odds.playType")}</p>
|
<p className="text-sm font-medium">{t("odds.playType")}</p>
|
||||||
<Select
|
<Select
|
||||||
modal={false}
|
modal={false}
|
||||||
@@ -114,45 +113,6 @@ export function OddsConfigPlayNav({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 大屏:侧栏玩法列表,点选即切换 */}
|
|
||||||
<nav className="hidden lg:block" aria-label={t("odds.playType")}>
|
|
||||||
<p className="mb-2 text-sm font-medium">{t("odds.playType")}</p>
|
|
||||||
{filteredTypes.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted-foreground">{t("odds.noPlayTypes")}</p>
|
|
||||||
) : (
|
|
||||||
<div className="max-h-[min(28rem,calc(100vh-16rem))] space-y-4 overflow-y-auto pr-1">
|
|
||||||
{playGroups.map((group) => (
|
|
||||||
<div key={group.key} className="space-y-1">
|
|
||||||
<p className="px-2 text-xs font-medium text-muted-foreground">
|
|
||||||
{t(`odds.playGroups.${group.key}`)}
|
|
||||||
</p>
|
|
||||||
<ul className="space-y-0.5">
|
|
||||||
{group.types.map((type) => {
|
|
||||||
const active = resolvedPlayCode === type.play_code;
|
|
||||||
return (
|
|
||||||
<li key={type.play_code}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
"w-full rounded-md px-2.5 py-2 text-left text-sm transition-colors",
|
|
||||||
active
|
|
||||||
? "bg-muted font-medium text-foreground"
|
|
||||||
: "text-foreground hover:bg-muted/60",
|
|
||||||
)}
|
|
||||||
onClick={() => onPlayCodeChange(type.play_code)}
|
|
||||||
aria-current={active ? "true" : undefined}
|
|
||||||
>
|
|
||||||
{playLabel(type.play_code)}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ export type OddsCategoryTab = "all" | "d4" | "d3" | "d2";
|
|||||||
|
|
||||||
export type OddsPlayFilterGroupKey = "bigSmall" | "combo4" | "number3" | "number2" | "other";
|
export type OddsPlayFilterGroupKey = "bigSmall" | "combo4" | "number3" | "number2" | "other";
|
||||||
|
|
||||||
|
const TRADITIONAL_CODES = new Set([
|
||||||
|
"big", "small", "pos_4a", "pos_4b", "pos_4c", "pos_4d", "pos_4e", "four_any", "four_top", "four_lower",
|
||||||
|
"pos_3a", "pos_3lower", "pos_3b", "pos_3c", "pos_3d", "pos_3e",
|
||||||
|
"pos_2a", "pos_2b", "pos_2c", "pos_2d", "pos_2e", "pos_2any",
|
||||||
|
]);
|
||||||
|
|
||||||
type GroupDef = {
|
type GroupDef = {
|
||||||
key: OddsPlayFilterGroupKey;
|
key: OddsPlayFilterGroupKey;
|
||||||
match: (row: AdminPlayTypeRow) => boolean;
|
match: (row: AdminPlayTypeRow) => boolean;
|
||||||
@@ -39,11 +45,10 @@ export function filterOddsPlayTypesByCategory(
|
|||||||
tab: OddsCategoryTab,
|
tab: OddsCategoryTab,
|
||||||
types: AdminPlayTypeRow[],
|
types: AdminPlayTypeRow[],
|
||||||
): AdminPlayTypeRow[] {
|
): AdminPlayTypeRow[] {
|
||||||
if (tab === "all") {
|
const traditional = types.filter((type) => TRADITIONAL_CODES.has(type.play_code));
|
||||||
return types;
|
if (tab === "all") return traditional;
|
||||||
}
|
|
||||||
const dim = tab === "d4" ? 4 : tab === "d3" ? 3 : 2;
|
const dim = tab === "d4" ? 4 : tab === "d3" ? 3 : 2;
|
||||||
return types.filter((t) => t.dimension === dim);
|
return traditional.filter((t) => t.dimension === dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildOddsPlayFilterGroups(
|
export function buildOddsPlayFilterGroups(
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ type PlayBatchSwitchGroup = {
|
|||||||
match: (row: PlayConfigItemRow) => boolean;
|
match: (row: PlayConfigItemRow) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TRADITIONAL_PLAY_CODES = new Set([
|
||||||
|
"big", "small", "pos_4a", "pos_4b", "pos_4c", "pos_4d", "pos_4e", "four_any", "four_top", "four_lower",
|
||||||
|
"pos_3a", "pos_3lower", "pos_3b", "pos_3c", "pos_3d", "pos_3e",
|
||||||
|
"pos_2a", "pos_2b", "pos_2c", "pos_2d", "pos_2e", "pos_2any",
|
||||||
|
]);
|
||||||
|
|
||||||
const PLAY_BATCH_SWITCH_GROUPS: PlayBatchSwitchGroup[] = [
|
const PLAY_BATCH_SWITCH_GROUPS: PlayBatchSwitchGroup[] = [
|
||||||
{
|
{
|
||||||
key: "d2",
|
key: "d2",
|
||||||
@@ -267,7 +273,7 @@ export function PlayConfigDocScreen() {
|
|||||||
|
|
||||||
const orderedRows = useMemo(
|
const orderedRows = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[...draftRows].sort(
|
draftRows.filter((row) => TRADITIONAL_PLAY_CODES.has(row.play_code)).sort(
|
||||||
(a, b) => a.display_order - b.display_order || a.play_code.localeCompare(b.play_code),
|
(a, b) => a.display_order - b.display_order || a.play_code.localeCompare(b.play_code),
|
||||||
),
|
),
|
||||||
[draftRows],
|
[draftRows],
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ import {
|
|||||||
import { PRIZE_SCOPE_ORDER } from "@/modules/config/doc/prize-scopes";
|
import { PRIZE_SCOPE_ORDER } from "@/modules/config/doc/prize-scopes";
|
||||||
|
|
||||||
const APPLY_REBATE_TO_PAYOUT_KEY = "settlement.apply_rebate_to_payout";
|
const APPLY_REBATE_TO_PAYOUT_KEY = "settlement.apply_rebate_to_payout";
|
||||||
|
const TRADITIONAL_PLAY_CODES = new Set([
|
||||||
|
"big", "small", "pos_4a", "pos_4b", "pos_4c", "pos_4d", "pos_4e", "four_any", "four_top", "four_lower",
|
||||||
|
"pos_3a", "pos_3lower", "pos_3b", "pos_3c", "pos_3d", "pos_3e",
|
||||||
|
"pos_2a", "pos_2b", "pos_2c", "pos_2d", "pos_2e", "pos_2any",
|
||||||
|
]);
|
||||||
|
|
||||||
function dimensionDistinctPrimaryScopePercents(
|
function dimensionDistinctPrimaryScopePercents(
|
||||||
dim: 2 | 3 | 4,
|
dim: 2 | 3 | 4,
|
||||||
@@ -195,9 +200,10 @@ export function RebateConfigDocScreen({
|
|||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setP2(inferRebatePercentFromDimension(2, workspace.draftRows, workspace.types));
|
const traditional = workspace.types.filter((type) => TRADITIONAL_PLAY_CODES.has(type.play_code));
|
||||||
setP3(inferRebatePercentFromDimension(3, workspace.draftRows, workspace.types));
|
setP2(inferRebatePercentFromDimension(2, workspace.draftRows, traditional));
|
||||||
setP4(inferRebatePercentFromDimension(4, workspace.draftRows, workspace.types));
|
setP3(inferRebatePercentFromDimension(3, workspace.draftRows, traditional));
|
||||||
|
setP4(inferRebatePercentFromDimension(4, workspace.draftRows, traditional));
|
||||||
}, [workspace?.draftRows, workspace?.types, workspace]);
|
}, [workspace?.draftRows, workspace?.types, workspace]);
|
||||||
|
|
||||||
async function handleWinEnjoyChange(checked: boolean): Promise<void> {
|
async function handleWinEnjoyChange(checked: boolean): Promise<void> {
|
||||||
@@ -294,7 +300,7 @@ export function RebateConfigDocScreen({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (const dim of [2, 3, 4] as const) {
|
for (const dim of [2, 3, 4] as const) {
|
||||||
if (dimensionDistinctPrimaryScopePercents(dim, resolvedDraftRows, resolvedTypes).size > 1) {
|
if (dimensionDistinctPrimaryScopePercents(dim, resolvedDraftRows, resolvedTypes.filter((type) => TRADITIONAL_PLAY_CODES.has(type.play_code))).size > 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,6 +324,9 @@ export function RebateConfigDocScreen({
|
|||||||
const rate3 = Number.isFinite(r3) ? r3 / 100 : 0;
|
const rate3 = Number.isFinite(r3) ? r3 / 100 : 0;
|
||||||
const rate4 = Number.isFinite(r4) ? r4 / 100 : 0;
|
const rate4 = Number.isFinite(r4) ? r4 / 100 : 0;
|
||||||
return rows.map((row) => {
|
return rows.map((row) => {
|
||||||
|
if (!TRADITIONAL_PLAY_CODES.has(row.play_code)) {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
const t = typesByCode.get(row.play_code);
|
const t = typesByCode.get(row.play_code);
|
||||||
const dim = (t?.dimension ?? 2) as 2 | 3 | 4;
|
const dim = (t?.dimension ?? 2) as 2 | 3 | 4;
|
||||||
const rate = dim === 4 ? rate4 : dim === 3 ? rate3 : rate2;
|
const rate = dim === 4 ? rate4 : dim === 3 ? rate3 : rate2;
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }): React.ReactEl
|
|||||||
) : null}
|
) : null}
|
||||||
{canManageDraw && pendingReview > 0 ? (
|
{canManageDraw && pendingReview > 0 ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/draws/${drawId}/review`}
|
href={`/admin/draws/${drawId}/results`}
|
||||||
className="rounded-md bg-amber-500/15 px-2 py-1 font-medium text-amber-800 dark:text-amber-200"
|
className="rounded-md bg-amber-500/15 px-2 py-1 font-medium text-amber-800 dark:text-amber-200"
|
||||||
>
|
>
|
||||||
{t("batchSummaryPending", { count: pendingReview })}
|
{t("batchSummaryPending", { count: pendingReview })}
|
||||||
@@ -290,7 +290,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }): React.ReactEl
|
|||||||
</div>
|
</div>
|
||||||
) : canManageDraw ? (
|
) : canManageDraw ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/draws/${drawId}/review`}
|
href={`/admin/draws/${drawId}/results`}
|
||||||
className="text-sm font-medium text-primary hover:underline"
|
className="text-sm font-medium text-primary hover:underline"
|
||||||
>
|
>
|
||||||
{t("goToReviewTab")}
|
{t("goToReviewTab")}
|
||||||
@@ -324,4 +324,4 @@ export function DrawDetailConsole({ drawId }: { drawId: string }): React.ReactEl
|
|||||||
<ConfirmDialog />
|
<ConfirmDialog />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
|||||||
open
|
open
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
router.replace(`/admin/draws/${drawId}/review`);
|
router.replace(`/admin/draws/${drawId}/results`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
drawId={idNum}
|
drawId={idNum}
|
||||||
@@ -81,8 +81,8 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
|||||||
}}
|
}}
|
||||||
onDiscarded={() => {
|
onDiscarded={() => {
|
||||||
void refreshDraw();
|
void refreshDraw();
|
||||||
router.replace(`/admin/draws/${drawId}/review`);
|
router.replace(`/admin/draws/${drawId}/results`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,18 +6,8 @@ import { useAsyncEffect } from "@/hooks/use-async-effect";
|
|||||||
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
||||||
|
|
||||||
import { getAdminDrawResultBatches } from "@/api/admin-draws";
|
import { getAdminDrawResultBatches } from "@/api/admin-draws";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import type { AdminDrawBatchRow, AdminDrawBatchesData } from "@/types/api/admin-draws";
|
import type { AdminDrawBatchRow, AdminDrawBatchesData } from "@/types/api/admin-draws";
|
||||||
|
|
||||||
@@ -71,9 +61,11 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
|
|||||||
{published.length === 0 ? (
|
{published.length === 0 ? (
|
||||||
<AdminNoResourceState message={t("noPublishedBatch")} />
|
<AdminNoResourceState message={t("noPublishedBatch")} />
|
||||||
) : (
|
) : (
|
||||||
published.map((batch) => (
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{published.map((batch) => (
|
||||||
<BatchTable key={`${batch.provider_code ?? "GLOBAL"}-${batch.id}`} batch={batch} />
|
<BatchTable key={`${batch.provider_code ?? "GLOBAL"}-${batch.id}`} batch={batch} />
|
||||||
))
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -81,51 +73,85 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
|
|||||||
|
|
||||||
function BatchTable({ batch }: { batch: AdminDrawBatchRow }) {
|
function BatchTable({ batch }: { batch: AdminDrawBatchRow }) {
|
||||||
const { t } = useTranslation("draws");
|
const { t } = useTranslation("draws");
|
||||||
const formatDt = useAdminDateTimeFormatter();
|
const primaryPrizes = ["first", "second", "third"] as const;
|
||||||
|
const itemByPrize = new Map(batch.items.map((item) => [`${item.prize_type}:${item.prize_index}`, item]));
|
||||||
|
const groupNumbers = (prizeType: "starter" | "consolation") =>
|
||||||
|
batch.items
|
||||||
|
.filter((item) => item.prize_type === prizeType)
|
||||||
|
.sort((a, b) => a.prize_index - b.prize_index);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<section className="overflow-hidden rounded-lg border border-border bg-card">
|
||||||
<CardHeader className="flex-row items-center justify-between space-y-0 pb-2">
|
<header className="flex items-center justify-between border-b border-border bg-muted/40 px-3 py-2">
|
||||||
<CardTitle className="text-sm">
|
<h2 className="text-sm font-bold">{batch.provider_name || batch.provider_code || t("provider")}</h2>
|
||||||
{batch.provider_name || batch.provider_code || t("provider")} · {t("version", { version: batch.result_version })}
|
<span className="font-mono text-[11px] text-muted-foreground">v{batch.result_version}</span>
|
||||||
</CardTitle>
|
</header>
|
||||||
<p className="font-mono text-xs text-muted-foreground">
|
<div className="space-y-3 p-3">
|
||||||
{formatDt(batch.confirmed_at)}
|
<div className="grid grid-cols-3 gap-2">
|
||||||
</p>
|
{primaryPrizes.map((prizeType) => {
|
||||||
</CardHeader>
|
const item = itemByPrize.get(`${prizeType}:0`);
|
||||||
<CardContent className="overflow-x-auto pt-0">
|
return (
|
||||||
<Table>
|
<div key={prizeType} className="border border-border/70 bg-background px-2 py-1.5 text-center">
|
||||||
<TableHeader>
|
<p className="text-[10px] text-muted-foreground">{drawPrizeTypeLabel(prizeType, 0, t)}</p>
|
||||||
<TableRow>
|
<ResultNumberDetail item={item} prominent />
|
||||||
<TableHead>{t("prize")}</TableHead>
|
</div>
|
||||||
<TableHead>#</TableHead>
|
);
|
||||||
<TableHead className="font-mono">4D</TableHead>
|
})}
|
||||||
<TableHead className="hidden sm:table-cell">{t("tail3")}</TableHead>
|
</div>
|
||||||
<TableHead className="hidden sm:table-cell">{t("tail2")}</TableHead>
|
<ResultNumberGroup title={t("resultGroups.starter")} items={groupNumbers("starter")} />
|
||||||
<TableHead className="hidden md:table-cell">{t("headTail")}</TableHead>
|
<ResultNumberGroup title={t("resultGroups.consolation")} items={groupNumbers("consolation")} />
|
||||||
</TableRow>
|
</div>
|
||||||
</TableHeader>
|
</section>
|
||||||
<TableBody>
|
);
|
||||||
{batch.items.map((it) => (
|
}
|
||||||
<TableRow key={`${it.prize_type}-${it.prize_index}`}>
|
|
||||||
<TableCell className="text-xs">
|
function ResultNumberGroup({ title, items }: { title: string; items: AdminDrawBatchRow["items"] }) {
|
||||||
{drawPrizeTypeLabel(it.prize_type, it.prize_index, t)}
|
return (
|
||||||
</TableCell>
|
<div>
|
||||||
<TableCell className="font-mono text-xs">{it.prize_index}</TableCell>
|
<p className="mb-1 text-[10px] font-semibold text-muted-foreground">{title}</p>
|
||||||
<TableCell className="font-mono text-sm font-semibold">{it.number_4d}</TableCell>
|
<div className="grid grid-cols-5 gap-1">
|
||||||
<TableCell className="hidden font-mono text-xs sm:table-cell">
|
{items.map((item) => (
|
||||||
{it.suffix_3d ?? "—"}
|
<span key={`${item.prize_type}-${item.prize_index}`} className="bg-muted px-1 py-1 text-center tabular-nums">
|
||||||
</TableCell>
|
<ResultNumberDetail item={item} />
|
||||||
<TableCell className="hidden font-mono text-xs sm:table-cell">
|
</span>
|
||||||
{it.suffix_2d ?? "—"}
|
))}
|
||||||
</TableCell>
|
</div>
|
||||||
<TableCell className="hidden text-xs md:table-cell">
|
</div>
|
||||||
{it.head_digit ?? "—"} / {it.tail_digit ?? "—"}
|
);
|
||||||
</TableCell>
|
}
|
||||||
</TableRow>
|
|
||||||
))}
|
function ResultNumberDetail({
|
||||||
</TableBody>
|
item,
|
||||||
</Table>
|
prominent = false,
|
||||||
</CardContent>
|
}: {
|
||||||
</Card>
|
item: AdminDrawBatchRow["items"][number] | undefined;
|
||||||
|
prominent?: boolean;
|
||||||
|
}) {
|
||||||
|
if (!item) {
|
||||||
|
return <span className="mt-0.5 block font-mono text-sm font-black">—</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className={prominent ? "mt-0.5 block font-mono text-sm font-black tracking-wide" : "block font-mono text-[11px] font-black"}>
|
||||||
|
{item.number_4d}
|
||||||
|
</span>
|
||||||
|
<span className="mt-1 flex items-center justify-center gap-1 font-mono text-[9px] leading-tight tabular-nums">
|
||||||
|
<span className="bg-blue-50 px-1 text-blue-700 dark:bg-blue-950/40 dark:text-blue-300">
|
||||||
|
3D {item.suffix_3d ?? "—"}
|
||||||
|
</span>
|
||||||
|
<span className="bg-amber-50 px-1 text-amber-800 dark:bg-amber-950/40 dark:text-amber-300">
|
||||||
|
2D {item.suffix_2d ?? "—"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="mt-0.5 flex items-center justify-center gap-1 font-mono text-[9px] leading-tight tabular-nums">
|
||||||
|
<span className="bg-emerald-50 px-1 text-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300">
|
||||||
|
H {item.head_digit ?? "—"}
|
||||||
|
</span>
|
||||||
|
<span className="bg-rose-50 px-1 text-rose-800 dark:bg-rose-950/40 dark:text-rose-300">
|
||||||
|
T {item.tail_digit ?? "—"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,14 +25,6 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
|
||||||
@@ -192,65 +184,63 @@ export function DrawReviewConsole({ drawId }: { drawId: string }): React.ReactEl
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-lg border border-border/60">
|
<div className="rounded-lg border border-border/60">
|
||||||
<div className="border-b border-border/60 px-3 py-2.5">
|
<div className="flex items-center justify-between border-b border-border/60 px-3 py-2.5">
|
||||||
<h2 className="text-sm font-semibold">{t("pendingBatches")}</h2>
|
<h2 className="text-sm font-semibold">{t("pendingBatches")}</h2>
|
||||||
|
<span className="font-mono text-xs text-muted-foreground">{pending.length}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
{pending.length === 0 ? (
|
{pending.length === 0 ? (
|
||||||
<AdminNoResourceState className="py-6" message={t("noPendingBatches")} />
|
<AdminNoResourceState className="py-6" message={t("noPendingBatches")} />
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
<TableHeader>
|
{pending.map((b) => (
|
||||||
<TableRow>
|
<section key={b.id} className="border border-border/70 bg-muted/20">
|
||||||
<TableHead>{t("version", { version: "" }).replace(" v", "").trim()}</TableHead>
|
<div className="flex items-start justify-between gap-3 border-b border-border/60 px-3 py-2.5">
|
||||||
<TableHead>{t("provider")}</TableHead>
|
<div className="min-w-0">
|
||||||
<TableHead>{t("numberCount")}</TableHead>
|
<p className="truncate text-sm font-bold">{b.provider_name || b.provider_code || "—"}</p>
|
||||||
<TableHead className="w-14 text-center">{t("table.actions", { ns: "common" })}</TableHead>
|
<p className="mt-0.5 font-mono text-[11px] text-muted-foreground">{b.provider_code || "GLOBAL"}</p>
|
||||||
</TableRow>
|
</div>
|
||||||
</TableHeader>
|
{canManageDraw ? (
|
||||||
<TableBody>
|
<AdminRowActionsMenu
|
||||||
{pending.map((b) => (
|
busy={discardingBatchId === b.id}
|
||||||
<TableRow key={b.id}>
|
actions={[
|
||||||
<TableCell>v{b.result_version}</TableCell>
|
{
|
||||||
<TableCell className="font-mono text-xs">
|
key: "publish",
|
||||||
{b.provider_name || b.provider_code || "—"}
|
label: t("reviewAndPublishAction"),
|
||||||
</TableCell>
|
icon: Rocket,
|
||||||
<TableCell className="tabular-nums">{b.items.length}</TableCell>
|
onClick: () => setPublishBatch(b),
|
||||||
<TableCell className="text-center">
|
},
|
||||||
{canManageDraw ? (
|
{
|
||||||
<AdminRowActionsMenu
|
key: "discard",
|
||||||
busy={discardingBatchId === b.id}
|
label: t("discardPendingBatch"),
|
||||||
actions={[
|
icon: Trash2,
|
||||||
{
|
destructive: true,
|
||||||
key: "publish",
|
disabled: discardingBatchId !== null,
|
||||||
label: t("reviewAndPublishAction"),
|
onClick: () =>
|
||||||
icon: Rocket,
|
requestConfirm({
|
||||||
onClick: () => setPublishBatch(b),
|
title: t("confirm.discardPendingBatchTitle"),
|
||||||
},
|
description: t("confirm.discardPendingBatchDescription"),
|
||||||
{
|
confirmVariant: "destructive",
|
||||||
key: "discard",
|
onConfirm: () => discardPendingBatch(b.id),
|
||||||
label: t("discardPendingBatch"),
|
}),
|
||||||
icon: Trash2,
|
},
|
||||||
destructive: true,
|
]}
|
||||||
disabled: discardingBatchId !== null,
|
/>
|
||||||
onClick: () =>
|
) : null}
|
||||||
requestConfirm({
|
</div>
|
||||||
title: t("confirm.discardPendingBatchTitle"),
|
<div className="grid grid-cols-2 divide-x divide-border/60">
|
||||||
description: t("confirm.discardPendingBatchDescription"),
|
<div className="px-3 py-2">
|
||||||
confirmVariant: "destructive",
|
<p className="text-[10px] text-muted-foreground">{t("version", { version: "" }).replace(" v", "").trim()}</p>
|
||||||
onConfirm: () => discardPendingBatch(b.id),
|
<p className="mt-0.5 font-mono text-sm font-black">v{b.result_version}</p>
|
||||||
}),
|
</div>
|
||||||
},
|
<div className="px-3 py-2">
|
||||||
]}
|
<p className="text-[10px] text-muted-foreground">{t("numberCount")}</p>
|
||||||
/>
|
<p className="mt-0.5 font-mono text-sm font-black">{b.items.length}</p>
|
||||||
) : (
|
</div>
|
||||||
<span className="text-xs text-muted-foreground">—</span>
|
</div>
|
||||||
)}
|
</section>
|
||||||
</TableCell>
|
))}
|
||||||
</TableRow>
|
</div>
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,18 +5,15 @@ import { useMemo } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { AdminSubnav, AdminSubnavLink } from "@/components/admin/admin-subnav";
|
import { AdminSubnav, AdminSubnavLink } from "@/components/admin/admin-subnav";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
import { PRD_RISK_ACCESS_ANY } from "@/lib/admin-prd";
|
import { PRD_RISK_ACCESS_ANY } from "@/lib/admin-prd";
|
||||||
import { canManageDrawResults, canViewDrawFinance, canViewDrawResults } from "@/lib/draw-access";
|
import { canViewDrawFinance, canViewDrawResults } from "@/lib/draw-access";
|
||||||
import { useDrawDetail } from "@/modules/draws/draw-detail-context";
|
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
|
|
||||||
const segments = [
|
const segments = [
|
||||||
{ suffix: "", key: "status", label: "subnav.status", requiresManage: false },
|
{ suffix: "", key: "status", label: "subnav.status", requiresManage: false },
|
||||||
{ suffix: "/results", key: "results", label: "subnav.results", requiresManage: false },
|
{ suffix: "/results", key: "results", label: "subnav.results", requiresManage: false },
|
||||||
{ suffix: "/finance", key: "finance", label: "subnav.finance", requiresManage: false },
|
{ suffix: "/finance", key: "finance", label: "subnav.finance", requiresManage: false },
|
||||||
{ suffix: "/review", key: "review", label: "subnav.review", requiresManage: true },
|
|
||||||
{
|
{
|
||||||
suffix: "/risk/pools",
|
suffix: "/risk/pools",
|
||||||
key: "riskPools",
|
key: "riskPools",
|
||||||
@@ -42,17 +39,6 @@ function isRiskPoolsTabActive(pathname: string, base: string): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isReviewTabActive(pathname: string, base: string): boolean {
|
|
||||||
const reviewPrefix = `${base}/review`;
|
|
||||||
const publishPrefix = `${base}/publish`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
pathname === reviewPrefix
|
|
||||||
|| pathname.startsWith(`${reviewPrefix}/`)
|
|
||||||
|| pathname.startsWith(`${publishPrefix}/`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DrawSubnav({ drawId }: { drawId: string }): React.ReactElement {
|
export function DrawSubnav({ drawId }: { drawId: string }): React.ReactElement {
|
||||||
const { t } = useTranslation("draws");
|
const { t } = useTranslation("draws");
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -61,11 +47,8 @@ export function DrawSubnav({ drawId }: { drawId: string }): React.ReactElement {
|
|||||||
const perms = profile?.permissions ?? [];
|
const perms = profile?.permissions ?? [];
|
||||||
|
|
||||||
const canViewDraw = canViewDrawResults(perms);
|
const canViewDraw = canViewDrawResults(perms);
|
||||||
const canManageDraw = canManageDrawResults(perms);
|
|
||||||
const canViewFinance = canViewDrawFinance(perms);
|
const canViewFinance = canViewDrawFinance(perms);
|
||||||
const canViewRisk = adminHasAnyPermission(perms, [...PRD_RISK_ACCESS_ANY]);
|
const canViewRisk = adminHasAnyPermission(perms, [...PRD_RISK_ACCESS_ANY]);
|
||||||
const { draw } = useDrawDetail();
|
|
||||||
const pendingReview = draw?.result_batch_counts.pending_review ?? 0;
|
|
||||||
|
|
||||||
const visibleSegments = useMemo(
|
const visibleSegments = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -85,7 +68,7 @@ export function DrawSubnav({ drawId }: { drawId: string }): React.ReactElement {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
[canManageDraw, canViewDraw, canViewFinance, canViewRisk],
|
[canViewDraw, canViewFinance, canViewRisk],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -95,9 +78,7 @@ export function DrawSubnav({ drawId }: { drawId: string }): React.ReactElement {
|
|||||||
const active =
|
const active =
|
||||||
suffix === ""
|
suffix === ""
|
||||||
? pathname === base || pathname === `${base}/`
|
? pathname === base || pathname === `${base}/`
|
||||||
: suffix === "/review"
|
: key === "riskPools"
|
||||||
? isReviewTabActive(pathname, base)
|
|
||||||
: key === "riskPools"
|
|
||||||
? isRiskPoolsTabActive(pathname, base)
|
? isRiskPoolsTabActive(pathname, base)
|
||||||
: pathname === href || pathname.startsWith(`${href}/`);
|
: pathname === href || pathname.startsWith(`${href}/`);
|
||||||
|
|
||||||
@@ -105,11 +86,6 @@ export function DrawSubnav({ drawId }: { drawId: string }): React.ReactElement {
|
|||||||
<AdminSubnavLink key={key} href={href} active={active}>
|
<AdminSubnavLink key={key} href={href} active={active}>
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
{t(label)}
|
{t(label)}
|
||||||
{key === "review" && pendingReview > 0 ? (
|
|
||||||
<Badge variant="secondary" className="h-5 min-w-5 px-1 text-[11px] tabular-nums">
|
|
||||||
{pendingReview}
|
|
||||||
</Badge>
|
|
||||||
) : null}
|
|
||||||
</span>
|
</span>
|
||||||
</AdminSubnavLink>
|
</AdminSubnavLink>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -105,8 +105,10 @@ export function DrawsIndexConsole() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [draftDrawNo, setDraftDrawNo] = useState("");
|
const [draftDrawNo, setDraftDrawNo] = useState("");
|
||||||
const [draftStatus, setDraftStatus] = useState("");
|
const [draftStatus, setDraftStatus] = useState("");
|
||||||
|
const [draftHasPlayerBets, setDraftHasPlayerBets] = useState(false);
|
||||||
const [appliedDrawNo, setAppliedDrawNo] = useState("");
|
const [appliedDrawNo, setAppliedDrawNo] = useState("");
|
||||||
const [appliedStatus, setAppliedStatus] = useState("");
|
const [appliedStatus, setAppliedStatus] = useState("");
|
||||||
|
const [appliedHasPlayerBets, setAppliedHasPlayerBets] = useState(false);
|
||||||
const [appliedAgentNodeId, setAppliedAgentNodeId] = useState<number | undefined>(undefined);
|
const [appliedAgentNodeId, setAppliedAgentNodeId] = useState<number | undefined>(undefined);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [perPage, setPerPage] = useState<number>(10);
|
const [perPage, setPerPage] = useState<number>(10);
|
||||||
@@ -140,6 +142,7 @@ export function DrawsIndexConsole() {
|
|||||||
? undefined
|
? undefined
|
||||||
: appliedStatus.trim(),
|
: appliedStatus.trim(),
|
||||||
agent_node_id: appliedAgentNodeId,
|
agent_node_id: appliedAgentNodeId,
|
||||||
|
has_player_bets: appliedHasPlayerBets || undefined,
|
||||||
});
|
});
|
||||||
setData(d);
|
setData(d);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -150,7 +153,7 @@ export function DrawsIndexConsole() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [page, perPage, appliedDrawNo, appliedStatus, appliedAgentNodeId, tRef]);
|
}, [page, perPage, appliedDrawNo, appliedStatus, appliedHasPlayerBets, appliedAgentNodeId, tRef]);
|
||||||
|
|
||||||
async function generatePlan(): Promise<void> {
|
async function generatePlan(): Promise<void> {
|
||||||
setGenerating(true);
|
setGenerating(true);
|
||||||
@@ -173,7 +176,7 @@ export function DrawsIndexConsole() {
|
|||||||
|
|
||||||
useAsyncEffect(() => {
|
useAsyncEffect(() => {
|
||||||
void load();
|
void load();
|
||||||
}, [page, perPage, appliedDrawNo, appliedStatus, appliedAgentNodeId]);
|
}, [page, perPage, appliedDrawNo, appliedStatus, appliedHasPlayerBets, appliedAgentNodeId]);
|
||||||
|
|
||||||
const handleSelectAll = useCallback((checked: boolean) => {
|
const handleSelectAll = useCallback((checked: boolean) => {
|
||||||
if (checked && data) {
|
if (checked && data) {
|
||||||
@@ -336,6 +339,16 @@ export function DrawsIndexConsole() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="admin-list-field">
|
||||||
|
<Checkbox
|
||||||
|
id="draw-filter-has-player-bets"
|
||||||
|
checked={draftHasPlayerBets}
|
||||||
|
onCheckedChange={(checked) => setDraftHasPlayerBets(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="draw-filter-has-player-bets" className="cursor-pointer whitespace-nowrap">
|
||||||
|
{t("hasPlayerBets")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
<div className="admin-list-actions">
|
<div className="admin-list-actions">
|
||||||
<AdminTableExportButton
|
<AdminTableExportButton
|
||||||
tableId="draws-index-table"
|
tableId="draws-index-table"
|
||||||
@@ -347,6 +360,7 @@ export function DrawsIndexConsole() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAppliedDrawNo(draftDrawNo);
|
setAppliedDrawNo(draftDrawNo);
|
||||||
setAppliedStatus(draftStatus);
|
setAppliedStatus(draftStatus);
|
||||||
|
setAppliedHasPlayerBets(draftHasPlayerBets);
|
||||||
setAppliedAgentNodeId(undefined);
|
setAppliedAgentNodeId(undefined);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}}
|
}}
|
||||||
@@ -359,8 +373,10 @@ export function DrawsIndexConsole() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDraftDrawNo("");
|
setDraftDrawNo("");
|
||||||
setDraftStatus("");
|
setDraftStatus("");
|
||||||
|
setDraftHasPlayerBets(false);
|
||||||
setAppliedDrawNo("");
|
setAppliedDrawNo("");
|
||||||
setAppliedStatus("");
|
setAppliedStatus("");
|
||||||
|
setAppliedHasPlayerBets(false);
|
||||||
setAppliedAgentNodeId(undefined);
|
setAppliedAgentNodeId(undefined);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -88,7 +88,11 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
|||||||
const { t } = useTranslation(["jackpot", "common"]);
|
const { t } = useTranslation(["jackpot", "common"]);
|
||||||
const tRef = useTranslationRef(["jackpot", "common"]);
|
const tRef = useTranslationRef(["jackpot", "common"]);
|
||||||
useAdminPlayTypeCatalog();
|
useAdminPlayTypeCatalog();
|
||||||
const playOptions = useCachedPlayTypeOptions();
|
const playOptions = useCachedPlayTypeOptions().filter((option) => new Set([
|
||||||
|
"big", "small", "pos_4a", "pos_4b", "pos_4c", "pos_4d", "pos_4e", "four_any", "four_top", "four_lower",
|
||||||
|
"pos_3a", "pos_3lower", "pos_3b", "pos_3c", "pos_3d", "pos_3e",
|
||||||
|
"pos_2a", "pos_2b", "pos_2c", "pos_2d", "pos_2e", "pos_2any",
|
||||||
|
]).has(option.code));
|
||||||
const profile = useAdminProfile();
|
const profile = useAdminProfile();
|
||||||
const canManageJackpot = adminHasAnyPermission(profile?.permissions, [PRD_JACKPOT_MANAGE]);
|
const canManageJackpot = adminHasAnyPermission(profile?.permissions, [PRD_JACKPOT_MANAGE]);
|
||||||
const canManualBurst = adminHasAnyPermission(profile?.permissions, [PRD_JACKPOT_MANUAL_BURST]);
|
const canManualBurst = adminHasAnyPermission(profile?.permissions, [PRD_JACKPOT_MANUAL_BURST]);
|
||||||
@@ -586,4 +590,4 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
|||||||
<ConfirmActionDialog />
|
<ConfirmActionDialog />
|
||||||
</ModuleScaffold>
|
</ModuleScaffold>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export type AdminBetProviderRow = {
|
|||||||
id: number;
|
id: number;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
short_code: string;
|
||||||
is_enabled: boolean;
|
is_enabled: boolean;
|
||||||
sort_order: number;
|
sort_order: number;
|
||||||
is_default: boolean;
|
is_default: boolean;
|
||||||
@@ -16,12 +17,14 @@ export type AdminBetProviderListData = {
|
|||||||
export type AdminBetProviderCreatePayload = {
|
export type AdminBetProviderCreatePayload = {
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
short_code: string;
|
||||||
is_enabled?: boolean;
|
is_enabled?: boolean;
|
||||||
sort_order?: number;
|
sort_order?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminBetProviderUpdatePayload = {
|
export type AdminBetProviderUpdatePayload = {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
short_code?: string;
|
||||||
is_enabled?: boolean;
|
is_enabled?: boolean;
|
||||||
sort_order?: number;
|
sort_order?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user