feat(i18n): enhance locale support for rebate settings and report exports
- Updated English, Nepali, and Chinese locale files to include new translations for the "apply rebate to payout" feature, enhancing clarity on its functionality. - Added new export options for previewing CSV and Excel files in reports, improving user experience with clearer export capabilities. - Enhanced internationalization support across multiple locales to ensure consistent messaging in the admin interface.
This commit is contained in:
@@ -115,6 +115,7 @@
|
||||
"autoSettlement": "Run settlement automatically",
|
||||
"autoApprove": "Auto-approve settlement batches",
|
||||
"autoPayout": "Auto-credit winnings to wallets",
|
||||
"applyRebateToPayout": "Deduct rebate again on winning payouts",
|
||||
"playRulesHtml": "Play rules HTML (i18n)",
|
||||
"playRulesHtmlDesc": "Rendered on the player play-rules page per locale. Leave empty to fall back to another language or the default empty state."
|
||||
},
|
||||
@@ -123,7 +124,8 @@
|
||||
"cooldownMinutes": "How long to wait after publishing before entering settling. Use 0 to settle immediately.",
|
||||
"autoSettlement": "When disabled, tick will not run settlement automatically and admins must trigger it manually.",
|
||||
"autoApprove": "After cooldown ends and settlement completes, whether batches are automatically marked as approved.",
|
||||
"autoPayout": "After a batch is approved, whether tick automatically credits winnings to player wallets."
|
||||
"autoPayout": "After a batch is approved, whether tick automatically credits winnings to player wallets.",
|
||||
"applyRebateToPayout": "When enabled, payout = gross win × (1 - rebate_rate_snapshot). Default off (rebate already reflected in actual deduct)."
|
||||
},
|
||||
"states": {
|
||||
"enabled": "Enabled",
|
||||
@@ -333,9 +335,11 @@
|
||||
"d4": "4D rebate rate (%)"
|
||||
},
|
||||
"winEnjoy": {
|
||||
"label": "Apply rebate on winning tickets",
|
||||
"description": "Placeholder field. It can later be aligned with risk and settlement rules and persisted.",
|
||||
"pendingNote": "Required by the product spec, but the API has no field yet. This row is informational only—you cannot change it here."
|
||||
"label": "Deduct rebate on winning payouts",
|
||||
"description": "Maps to settlement.apply_rebate_to_payout: when enabled, winning payout uses gross win × (1 - rebate_rate_snapshot).",
|
||||
"hint": "Global switch; affects future settlement payouts immediately (not tied to odds version publish).",
|
||||
"saveSuccess": "Winning-ticket rebate setting updated",
|
||||
"saveFailed": "Update failed"
|
||||
},
|
||||
"effectiveTime": "Effective time (current active odds version)"
|
||||
},
|
||||
|
||||
@@ -24,8 +24,11 @@
|
||||
"csv": "CSV",
|
||||
"excel": "Excel",
|
||||
"csvServer": "Export CSV (full)",
|
||||
"excelServer": "Export Excel (full)"
|
||||
"excelServer": "Export Excel (full)",
|
||||
"csvPreview": "Export preview CSV",
|
||||
"excelPreview": "Export preview Excel"
|
||||
},
|
||||
"exportPreviewHint": "Exports only rows on the current preview page (pagination applies)",
|
||||
"tasks": {
|
||||
"refresh": "Refresh",
|
||||
"download": "Download",
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
"autoSettlement": "सेटलमेन्ट स्वतः चलाउने",
|
||||
"autoApprove": "सेटलमेन्ट ब्याच स्वतः स्वीकृत",
|
||||
"autoPayout": "जित रकम स्वतः वालेटमा जम्मा",
|
||||
"applyRebateToPayout": "जितेको टिकटको पेआउटमा पुनः रिबेट घटाउने",
|
||||
"playRulesHtml": "खेल नियम HTML (बहुभाषी)",
|
||||
"playRulesHtmlDesc": "खेलाडीको नियम पृष्ठमा भाषा अनुसार HTML देखिन्छ। खाली छोड्दा अर्को भाषा वा पूर्वनिर्धारित खाली सूचना देखिन्छ।"
|
||||
},
|
||||
@@ -123,7 +124,8 @@
|
||||
"cooldownMinutes": "प्रकाशनपछि settling मा जानुअघि कति समय पर्खने। 0 राखे तुरुन्त सेटलमेन्ट सुरु हुन्छ।",
|
||||
"autoSettlement": "बन्द हुँदा tick ले सेटलमेन्ट स्वतः चलाउँदैन र एडमिनले म्यानुअल रूपमा ट्रिगर गर्नुपर्छ।",
|
||||
"autoApprove": "कूलडाउन सकिएर सेटलमेन्ट पूरा भएपछि ब्याच स्वतः अनुमोदित हुने हो कि होइन।",
|
||||
"autoPayout": "ब्याच अनुमोदित भएपछि tick ले जित रकम खेलाडीको वालेटमा स्वतः जम्मा गर्ने हो कि होइन।"
|
||||
"autoPayout": "ब्याच अनुमोदित भएपछि tick ले जित रकम खेलाडीको वालेटमा स्वतः जम्मा गर्ने हो कि होइन।",
|
||||
"applyRebateToPayout": "सक्रिय हुँदा पेआउट = gross win × (1 - rebate_rate_snapshot)। पूर्वनिर्धारित बन्द (रिबेट actual deduct मा पहिले नै समायोजित)।"
|
||||
},
|
||||
"states": {
|
||||
"enabled": "सक्रिय",
|
||||
@@ -333,9 +335,11 @@
|
||||
"d4": "4D रिबेट दर (%)"
|
||||
},
|
||||
"winEnjoy": {
|
||||
"label": "जितेका टिकटहरूमा पनि रिबेट लागू गर्ने",
|
||||
"description": "यो placeholder field हो। पछि risk र settlement नियमसँग मिलाएर स्थायी रूपमा राख्न सकिन्छ।",
|
||||
"pendingNote": "उत्पादन विनिर्देशनले यो switch चाहिन्छ, तर API मा field छैन। यहाँ केवल जानकारी देखाइन्छ—यहाँबाट बदल्न मिल्दैन।"
|
||||
"label": "जितेको टिकटको पेआउटमा पुनः रिबेट घटाउने",
|
||||
"description": "settlement.apply_rebate_to_payout सँग जोडिएको: सक्रिय हुँदा जित पेआउटमा rebate_rate_snapshot अनुसार घटाउँछ।",
|
||||
"hint": "वैश्विक switch; odds संस्करण प्रकाशनसँग नजोडिएको, सुरक्षित गर्दा तुरुन्त लागू।",
|
||||
"saveSuccess": "जित टिकट रिबेट सेटिङ अद्यावधिक भयो",
|
||||
"saveFailed": "अद्यावधिक असफल"
|
||||
},
|
||||
"effectiveTime": "लागू समय (हाल सक्रिय अड्स संस्करण)"
|
||||
},
|
||||
|
||||
@@ -24,8 +24,11 @@
|
||||
"csv": "CSV",
|
||||
"excel": "Excel",
|
||||
"csvServer": "CSV निर्यात (पूर्ण)",
|
||||
"excelServer": "Excel निर्यात (पूर्ण)"
|
||||
"excelServer": "Excel निर्यात (पूर्ण)",
|
||||
"csvPreview": "पूर्वावलोकन CSV",
|
||||
"excelPreview": "पूर्वावलोकन Excel"
|
||||
},
|
||||
"exportPreviewHint": "हालको पूर्वावलोकन पृष्ठका पङ्क्तिहरू मात्र (पेजिनेसन लागू)",
|
||||
"tasks": {
|
||||
"refresh": "रिफ्रेस",
|
||||
"download": "डाउनलोड",
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
"autoSettlement": "自动执行结算",
|
||||
"autoApprove": "自动审核结算批次",
|
||||
"autoPayout": "自动派彩入账",
|
||||
"applyRebateToPayout": "中奖注单结算时再扣回水",
|
||||
"playRulesHtml": "玩法规则 HTML(多语言)",
|
||||
"playRulesHtmlDesc": "该内容将直接在玩家端的玩法规则页面作为 HTML 渲染。按语言分别配置;留空则回退其它语言或显示默认提示。"
|
||||
},
|
||||
@@ -123,7 +124,8 @@
|
||||
"cooldownMinutes": "结果发布后等待多久再进入 settling。填 0 表示发布后直接进入结算。",
|
||||
"autoSettlement": "关闭后,tick 不会自动跑结算,只能由后台手工执行。",
|
||||
"autoApprove": "冷静期结束并跑完结算后,是否自动将批次标记为已审核。",
|
||||
"autoPayout": "批次已审核后,是否由 tick 自动把中奖金额打入玩家钱包。"
|
||||
"autoPayout": "批次已审核后,是否由 tick 自动把中奖金额打入玩家钱包。",
|
||||
"applyRebateToPayout": "开启后派彩金额 = 毛赢 × (1 - 回水率快照)。默认关闭(下注实扣已体现回水)。"
|
||||
},
|
||||
"states": {
|
||||
"enabled": "已开启",
|
||||
@@ -333,9 +335,11 @@
|
||||
"d4": "4D 回水比例 (%)"
|
||||
},
|
||||
"winEnjoy": {
|
||||
"label": "中奖注单也应用回水",
|
||||
"description": "这是预留字段,后续可和风控、结算规则对齐后再真正落库存储。",
|
||||
"pendingNote": "产品要求支持该开关,但后端尚未提供配置字段;当前仅展示说明,无法在此修改。"
|
||||
"label": "中奖注单结算时再扣回水",
|
||||
"description": "对应系统参数 settlement.apply_rebate_to_payout:开启后中奖派彩在毛赢基础上再乘 (1 - 回水率快照)。",
|
||||
"hint": "全局开关,保存后立即影响后续结算派彩,不随赔率版本发布。",
|
||||
"saveSuccess": "已更新中奖回水结算开关",
|
||||
"saveFailed": "更新失败"
|
||||
},
|
||||
"effectiveTime": "生效时间(当前赔率生效版本)"
|
||||
},
|
||||
|
||||
@@ -24,8 +24,11 @@
|
||||
"csv": "CSV",
|
||||
"excel": "Excel",
|
||||
"csvServer": "导出 CSV(全量)",
|
||||
"excelServer": "导出 Excel(全量)"
|
||||
"excelServer": "导出 Excel(全量)",
|
||||
"csvPreview": "导出当前页 CSV",
|
||||
"excelPreview": "导出当前页 Excel"
|
||||
},
|
||||
"exportPreviewHint": "仅导出当前预览表格中的数据(受分页限制)",
|
||||
"tasks": {
|
||||
"refresh": "刷新",
|
||||
"download": "下载",
|
||||
|
||||
@@ -25,9 +25,11 @@ export function AccountSettingsConsole() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (adminProfile) {
|
||||
setNickname(adminProfile.nickname ?? "");
|
||||
}
|
||||
queueMicrotask(() => {
|
||||
if (adminProfile) {
|
||||
setNickname(adminProfile.nickname ?? "");
|
||||
}
|
||||
});
|
||||
}, [adminProfile]);
|
||||
|
||||
async function handleUpdateProfile() {
|
||||
|
||||
@@ -3,12 +3,18 @@ import { useTranslation } from "react-i18next";
|
||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||
import { resolveAdminStatusTone } from "@/lib/admin-status-tone";
|
||||
|
||||
export function ConfigStatusBadge({ status }: { status: string }) {
|
||||
export function ConfigStatusBadge({
|
||||
status,
|
||||
className,
|
||||
}: {
|
||||
status: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const { t } = useTranslation("config");
|
||||
const label = t(`versionStatus.${status}`, { defaultValue: status });
|
||||
|
||||
return (
|
||||
<AdminStatusBadge status={status} tone={resolveAdminStatusTone(status)}>
|
||||
<AdminStatusBadge status={status} tone={resolveAdminStatusTone(status)} className={className}>
|
||||
{label}
|
||||
</AdminStatusBadge>
|
||||
);
|
||||
|
||||
@@ -375,7 +375,7 @@ export function OddsConfigDocScreen({
|
||||
clone_from_version_id: rollbackTarget.id,
|
||||
});
|
||||
toast.success(
|
||||
t("odds.rollbackSuccess", {
|
||||
t("versionActions.rollbackSuccess", {
|
||||
ns: "config",
|
||||
fromVersion: rollbackTarget.version_no,
|
||||
version: d.version_no,
|
||||
@@ -388,7 +388,7 @@ export function OddsConfigDocScreen({
|
||||
setRollbackOpen(false);
|
||||
setRollbackTarget(null);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("odds.rollbackFailed", { ns: "config" }));
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("versionActions.rollbackFailed", { ns: "config" }));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -611,9 +611,12 @@ export function OddsConfigDocScreen({
|
||||
<Dialog open={rollbackOpen} onOpenChange={setRollbackOpen}>
|
||||
<DialogContent showCloseButton className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("odds.rollbackDialog.title", { ns: "config" })}</DialogTitle>
|
||||
<DialogTitle>{t("versionActions.rollbackDialog.title", { ns: "config" })}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("odds.rollbackDialog.description", { ns: "config", version: rollbackTarget?.version_no ?? "—" })}
|
||||
{t("versionActions.rollbackDialog.description", {
|
||||
ns: "config",
|
||||
version: rollbackTarget?.version_no ?? "—",
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
@@ -621,7 +624,7 @@ export function OddsConfigDocScreen({
|
||||
{t("actions.cancel", { ns: "adminUsers" })}
|
||||
</Button>
|
||||
<Button type="button" onClick={() => void handleRollback()} disabled={!rollbackTarget || saving}>
|
||||
{t("odds.rollbackDialog.confirm", { ns: "config" })}
|
||||
{t("versionActions.rollbackDialog.confirm", { ns: "config" })}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -19,8 +19,10 @@ import {
|
||||
ConfigVersionToolbarMeta,
|
||||
ConfigVersionToolbarMetaEmphasis,
|
||||
} from "@/modules/config/config-version-toolbar-meta";
|
||||
import { getAdminSettings, updateAdminSetting } from "@/api/admin-settings";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -38,7 +40,7 @@ import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"
|
||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import { pickDefaultConfigVersionId } from "@/lib/config-version-auto-pick";
|
||||
import { PRD_REBATE_MANAGE } from "@/lib/admin-prd";
|
||||
import { PRD_REBATE_MANAGE, PRD_WALLET_RECONCILE_MANAGE } from "@/lib/admin-prd";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type {
|
||||
@@ -50,6 +52,9 @@ import type {
|
||||
|
||||
import { PRIZE_SCOPE_ORDER } from "@/modules/config/doc/prize-scopes";
|
||||
|
||||
const SETTLEMENT_GROUP = "settlement";
|
||||
const APPLY_REBATE_TO_PAYOUT_KEY = "settlement.apply_rebate_to_payout";
|
||||
|
||||
function rateToPercentUi(rateStr: string): string {
|
||||
const n = Number.parseFloat(rateStr);
|
||||
if (!Number.isFinite(n)) {
|
||||
@@ -108,6 +113,13 @@ export function RebateConfigDocScreen({
|
||||
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
||||
const profile = useAdminProfile();
|
||||
const canManage = adminHasAnyPermission(profile?.permissions, [PRD_REBATE_MANAGE]);
|
||||
const canEditWinEnjoy = adminHasAnyPermission(profile?.permissions, [
|
||||
PRD_REBATE_MANAGE,
|
||||
PRD_WALLET_RECONCILE_MANAGE,
|
||||
]);
|
||||
const [applyRebateToPayout, setApplyRebateToPayout] = useState(false);
|
||||
const [winEnjoyLoading, setWinEnjoyLoading] = useState(true);
|
||||
const [winEnjoySaving, setWinEnjoySaving] = useState(false);
|
||||
const formatDt = useAdminDateTimeFormatter();
|
||||
const [types, setTypes] = useState<AdminPlayTypeRow[]>([]);
|
||||
const [listRows, setListRows] = useState<ConfigVersionSummary[]>([]);
|
||||
@@ -147,6 +159,19 @@ export function RebateConfigDocScreen({
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const loadWinEnjoySetting = useCallback(async () => {
|
||||
setWinEnjoyLoading(true);
|
||||
try {
|
||||
const res = await getAdminSettings(SETTLEMENT_GROUP);
|
||||
const hit = res.items.find((item) => item.key === APPLY_REBATE_TO_PAYOUT_KEY);
|
||||
setApplyRebateToPayout(Boolean(hit?.value));
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
|
||||
} finally {
|
||||
setWinEnjoyLoading(false);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(async () => {
|
||||
setLoading(true);
|
||||
@@ -156,6 +181,28 @@ export function RebateConfigDocScreen({
|
||||
});
|
||||
}, [refreshTypes, refreshList]);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
void loadWinEnjoySetting();
|
||||
});
|
||||
}, [loadWinEnjoySetting]);
|
||||
|
||||
async function handleWinEnjoyChange(checked: boolean): Promise<void> {
|
||||
if (!canEditWinEnjoy) {
|
||||
return;
|
||||
}
|
||||
setWinEnjoySaving(true);
|
||||
try {
|
||||
await updateAdminSetting(APPLY_REBATE_TO_PAYOUT_KEY, checked);
|
||||
setApplyRebateToPayout(checked);
|
||||
toast.success(t("rebate.winEnjoy.saveSuccess", { ns: "config" }));
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("rebate.winEnjoy.saveFailed", { ns: "config" }));
|
||||
} finally {
|
||||
setWinEnjoySaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
const loadDetail = useCallback(async (id: number) => {
|
||||
setLoadingDetail(true);
|
||||
try {
|
||||
@@ -511,13 +558,21 @@ export function RebateConfigDocScreen({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert className="border-border/80 bg-muted/30">
|
||||
<AlertDescription className="text-sm leading-relaxed">
|
||||
<span className="font-medium text-foreground">{t("rebate.winEnjoy.label", { ns: "config" })}</span>
|
||||
{" — "}
|
||||
{t("rebate.winEnjoy.pendingNote", { ns: "config" })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="rounded-xl border border-border/60 px-4 py-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<p className="text-sm font-medium">{t("rebate.winEnjoy.label", { ns: "config" })}</p>
|
||||
<p className="text-xs text-muted-foreground">{t("rebate.winEnjoy.description", { ns: "config" })}</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={applyRebateToPayout}
|
||||
disabled={winEnjoyLoading || winEnjoySaving || !canEditWinEnjoy}
|
||||
aria-label={t("rebate.winEnjoy.label", { ns: "config" })}
|
||||
onCheckedChange={(value) => void handleWinEnjoyChange(value)}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">{t("rebate.winEnjoy.hint", { ns: "config" })}</p>
|
||||
</div>
|
||||
|
||||
{!embedded ? (
|
||||
<div className="grid gap-1 text-sm">
|
||||
|
||||
@@ -102,11 +102,15 @@ export function RiskCapRuntimePanel() {
|
||||
}, [appliedNumber, drawId, poolFilter, t]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadDraws();
|
||||
queueMicrotask(() => {
|
||||
void loadDraws();
|
||||
});
|
||||
}, [loadDraws]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadPools();
|
||||
queueMicrotask(() => {
|
||||
void loadPools();
|
||||
});
|
||||
}, [loadPools]);
|
||||
|
||||
const riskBase = drawId ? `/admin/draws/${drawId}/risk` : null;
|
||||
|
||||
@@ -150,7 +150,9 @@ export function DashboardConsole(): ReactElement {
|
||||
}, [i18n.language]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadPlayOptions();
|
||||
queueMicrotask(() => {
|
||||
void loadPlayOptions();
|
||||
});
|
||||
}, [loadPlayOptions]);
|
||||
|
||||
const load = useCallback(async (isRefresh = false) => {
|
||||
|
||||
@@ -47,9 +47,11 @@ export function DrawCreateDialog({
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setForm(resetFormState());
|
||||
}
|
||||
queueMicrotask(() => {
|
||||
if (!open) {
|
||||
setForm(resetFormState());
|
||||
}
|
||||
});
|
||||
}, [open]);
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
|
||||
@@ -51,13 +51,15 @@ export function DrawEditDialog({
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || draw == null) {
|
||||
return;
|
||||
}
|
||||
setDrawTime(isoToScheduleValue(draw.draw_time));
|
||||
setCloseTime(isoToScheduleValue(draw.close_time));
|
||||
setStartTime(isoToScheduleValue(draw.start_time));
|
||||
setDrawNo(draw.draw_no);
|
||||
queueMicrotask(() => {
|
||||
if (!open || draw == null) {
|
||||
return;
|
||||
}
|
||||
setDrawTime(isoToScheduleValue(draw.draw_time));
|
||||
setCloseTime(isoToScheduleValue(draw.close_time));
|
||||
setStartTime(isoToScheduleValue(draw.start_time));
|
||||
setDrawNo(draw.draw_no);
|
||||
});
|
||||
}, [open, draw]);
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
|
||||
@@ -58,7 +58,9 @@ export function ReportJobsPanel({ canExport, refreshToken = 0 }: ReportJobsPanel
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadJobs();
|
||||
queueMicrotask(() => {
|
||||
void loadJobs();
|
||||
});
|
||||
}, [loadJobs, refreshToken]);
|
||||
|
||||
async function handleDownload(job: AdminReportJobRow): Promise<void> {
|
||||
|
||||
@@ -431,7 +431,9 @@ export function ReportsConsole() {
|
||||
}, [i18n.language]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadPlayOptions();
|
||||
queueMicrotask(() => {
|
||||
void loadPlayOptions();
|
||||
});
|
||||
}, [loadPlayOptions]);
|
||||
|
||||
const loadSearchOptions = useCallback(async (kind: SearchKind, query: string) => {
|
||||
@@ -764,14 +766,18 @@ export function ReportsConsole() {
|
||||
}, [canViewReports, filters, page, perPage, selectedReport, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setResult(null);
|
||||
setError(null);
|
||||
setPage(1);
|
||||
queueMicrotask(() => {
|
||||
setResult(null);
|
||||
setError(null);
|
||||
setPage(1);
|
||||
});
|
||||
}, [selectedKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (result && result.key === selectedReport.key && selectedReport.connected) {
|
||||
void queryReport();
|
||||
queueMicrotask(() => {
|
||||
void queryReport();
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, perPage]);
|
||||
@@ -828,14 +834,10 @@ export function ReportsConsole() {
|
||||
}
|
||||
}
|
||||
|
||||
function exportReport(format: ExportFormat): void {
|
||||
function exportPreview(format: ExportFormat): void {
|
||||
if (!canExportReports) {
|
||||
return;
|
||||
}
|
||||
if (usesServerExport) {
|
||||
void exportViaServer(format);
|
||||
return;
|
||||
}
|
||||
if (!result || result.rows.length === 0) {
|
||||
toast.info(t("empty"));
|
||||
return;
|
||||
@@ -851,6 +853,17 @@ export function ReportsConsole() {
|
||||
}
|
||||
}
|
||||
|
||||
function exportReport(format: ExportFormat): void {
|
||||
if (!canExportReports) {
|
||||
return;
|
||||
}
|
||||
if (usesServerExport) {
|
||||
void exportViaServer(format);
|
||||
return;
|
||||
}
|
||||
exportPreview(format);
|
||||
}
|
||||
|
||||
const renderSearchPicker = (kind: SearchKind) => {
|
||||
const value =
|
||||
kind === "draw" ? filters.drawNo : kind === "player" ? filters.player : filters.operator;
|
||||
@@ -1327,39 +1340,52 @@ export function ReportsConsole() {
|
||||
<div>
|
||||
<CardTitle className="admin-list-title">{t("preview.title")}</CardTitle>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2 sm:flex-row sm:items-center">
|
||||
{usesServerExport ? (
|
||||
<p className="text-xs text-muted-foreground sm:mr-2">{t("exportServerHint")}</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground sm:mr-2">{t("exportClientHint")}</p>
|
||||
)}
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<p className="text-xs text-muted-foreground">{t("exportServerHint")}</p>
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={
|
||||
!canExportReports ||
|
||||
exporting !== null ||
|
||||
(!usesServerExport && (!result || result.rows.length === 0))
|
||||
}
|
||||
disabled={!canExportReports || exporting !== null}
|
||||
onClick={() => exportReport("csv")}
|
||||
>
|
||||
<FileDown data-icon="inline-start" />
|
||||
{usesServerExport ? t("formats.csvServer") : t("formats.csv")}
|
||||
{t("formats.csvServer")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={
|
||||
!canExportReports ||
|
||||
exporting !== null ||
|
||||
(!usesServerExport && (!result || result.rows.length === 0))
|
||||
}
|
||||
disabled={!canExportReports || exporting !== null}
|
||||
onClick={() => exportReport("excel")}
|
||||
>
|
||||
<FileSpreadsheet data-icon="inline-start" />
|
||||
{usesServerExport ? t("formats.excelServer") : t("formats.excel")}
|
||||
{t("formats.excelServer")}
|
||||
</Button>
|
||||
</div>
|
||||
{result && result.rows.length > 0 ? (
|
||||
<>
|
||||
<p className="text-xs text-muted-foreground">{t("exportPreviewHint")}</p>
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={!canExportReports || exporting !== null}
|
||||
onClick={() => exportPreview("csv")}
|
||||
>
|
||||
{t("formats.csvPreview")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={!canExportReports || exporting !== null}
|
||||
onClick={() => exportPreview("excel")}
|
||||
>
|
||||
{t("formats.excelPreview")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 pt-4">
|
||||
|
||||
@@ -29,6 +29,7 @@ const DRAW_KEYS = {
|
||||
AUTO_SETTLEMENT: "settlement.auto_run_on_tick",
|
||||
AUTO_APPROVE: "settlement.auto_approve_on_tick",
|
||||
AUTO_PAYOUT: "settlement.auto_payout_on_tick",
|
||||
APPLY_REBATE_TO_PAYOUT: "settlement.apply_rebate_to_payout",
|
||||
} as const;
|
||||
|
||||
const FRONTEND_GROUP = "frontend";
|
||||
@@ -45,6 +46,7 @@ interface RuntimeDraft {
|
||||
autoSettlement: boolean;
|
||||
autoApprove: boolean;
|
||||
autoPayout: boolean;
|
||||
applyRebateToPayout: boolean;
|
||||
playRulesHtmlZh: string;
|
||||
playRulesHtmlEn: string;
|
||||
playRulesHtmlNe: string;
|
||||
@@ -92,6 +94,7 @@ export function SystemSettingsScreen() {
|
||||
autoSettlement: true,
|
||||
autoApprove: true,
|
||||
autoPayout: true,
|
||||
applyRebateToPayout: false,
|
||||
playRulesHtmlZh: "",
|
||||
playRulesHtmlEn: "",
|
||||
playRulesHtmlNe: "",
|
||||
@@ -102,6 +105,7 @@ export function SystemSettingsScreen() {
|
||||
autoSettlement: true,
|
||||
autoApprove: true,
|
||||
autoPayout: true,
|
||||
applyRebateToPayout: false,
|
||||
playRulesHtmlZh: "",
|
||||
playRulesHtmlEn: "",
|
||||
playRulesHtmlNe: "",
|
||||
@@ -131,6 +135,7 @@ export function SystemSettingsScreen() {
|
||||
autoSettlement: Boolean(kv[DRAW_KEYS.AUTO_SETTLEMENT] ?? true),
|
||||
autoApprove: Boolean(kv[DRAW_KEYS.AUTO_APPROVE] ?? true),
|
||||
autoPayout: Boolean(kv[DRAW_KEYS.AUTO_PAYOUT] ?? true),
|
||||
applyRebateToPayout: Boolean(kv[DRAW_KEYS.APPLY_REBATE_TO_PAYOUT] ?? false),
|
||||
playRulesHtmlZh: String(kv[FRONTEND_KEYS.PLAY_RULES_HTML_ZH] ?? legacyHtml),
|
||||
playRulesHtmlEn: String(kv[FRONTEND_KEYS.PLAY_RULES_HTML_EN] ?? ""),
|
||||
playRulesHtmlNe: String(kv[FRONTEND_KEYS.PLAY_RULES_HTML_NE] ?? ""),
|
||||
@@ -167,6 +172,7 @@ export function SystemSettingsScreen() {
|
||||
await updateAdminSetting(DRAW_KEYS.AUTO_SETTLEMENT, draft.autoSettlement);
|
||||
await updateAdminSetting(DRAW_KEYS.AUTO_APPROVE, draft.autoApprove);
|
||||
await updateAdminSetting(DRAW_KEYS.AUTO_PAYOUT, draft.autoPayout);
|
||||
await updateAdminSetting(DRAW_KEYS.APPLY_REBATE_TO_PAYOUT, draft.applyRebateToPayout);
|
||||
await updateAdminSetting(FRONTEND_KEYS.PLAY_RULES_HTML_ZH, draft.playRulesHtmlZh);
|
||||
await updateAdminSetting(FRONTEND_KEYS.PLAY_RULES_HTML_EN, draft.playRulesHtmlEn);
|
||||
await updateAdminSetting(FRONTEND_KEYS.PLAY_RULES_HTML_NE, draft.playRulesHtmlNe);
|
||||
@@ -242,6 +248,21 @@ export function SystemSettingsScreen() {
|
||||
|
||||
<div className="h-px bg-border/60" />
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="min-w-0 space-y-1 pr-4">
|
||||
<Label className="text-sm font-medium">{t("system.fields.applyRebateToPayout", { ns: "config" })}</Label>
|
||||
<p className="text-xs text-muted-foreground">{t("system.hints.applyRebateToPayout", { ns: "config" })}</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={draft.applyRebateToPayout}
|
||||
disabled={loading || saving}
|
||||
aria-label={t("system.fields.applyRebateToPayout", { ns: "config" })}
|
||||
onCheckedChange={(value) => updateDraft("applyRebateToPayout", value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-border/60" />
|
||||
|
||||
<div className="grid max-w-xs gap-2">
|
||||
<Label htmlFor="cooldown-minutes" className="text-sm font-medium">
|
||||
{t("system.fields.cooldownMinutes", { ns: "config" })}
|
||||
|
||||
Reference in New Issue
Block a user