252 lines
11 KiB
TypeScript
252 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import { AdminPageCard } from "@/components/admin/admin-page-card";
|
|
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
|
import { SettingsSectionActions } from "@/modules/settings/components/settings-section-actions";
|
|
import { useSettingsSection } from "@/modules/settings/hooks/use-settings-section";
|
|
import { DRAW_KEYS } from "@/modules/settings/settings-keys";
|
|
import type { AdminSettingBatchItem } from "@/api/admin-settings";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
|
import { PRD_DRAW_RESULT_MANAGE } from "@/lib/admin-prd";
|
|
import { useAdminProfile } from "@/stores/admin-session";
|
|
|
|
interface DrawDraft {
|
|
defaultCurrency: string;
|
|
drawIntervalMinutes: string;
|
|
drawBettingWindowSeconds: string;
|
|
drawCloseBeforeDrawSeconds: string;
|
|
drawBufferDrawsAhead: string;
|
|
requireManualReview: boolean;
|
|
cooldownMinutes: string;
|
|
}
|
|
|
|
const INITIAL: DrawDraft = {
|
|
defaultCurrency: "NPR",
|
|
drawIntervalMinutes: "5",
|
|
drawBettingWindowSeconds: "270",
|
|
drawCloseBeforeDrawSeconds: "30",
|
|
drawBufferDrawsAhead: "8",
|
|
requireManualReview: false,
|
|
cooldownMinutes: "15",
|
|
};
|
|
|
|
function fromKv(kv: Record<string, unknown>): DrawDraft {
|
|
return {
|
|
defaultCurrency: String(kv[DRAW_KEYS.DEFAULT_CURRENCY] ?? "NPR"),
|
|
drawIntervalMinutes: String(kv[DRAW_KEYS.DRAW_INTERVAL_MINUTES] ?? 5),
|
|
drawBettingWindowSeconds: String(kv[DRAW_KEYS.DRAW_BETTING_WINDOW_SECONDS] ?? 270),
|
|
drawCloseBeforeDrawSeconds: String(kv[DRAW_KEYS.DRAW_CLOSE_BEFORE_DRAW_SECONDS] ?? 30),
|
|
drawBufferDrawsAhead: String(kv[DRAW_KEYS.DRAW_BUFFER_DRAWS_AHEAD] ?? 8),
|
|
requireManualReview: Boolean(kv[DRAW_KEYS.REQUIRE_MANUAL_REVIEW] ?? false),
|
|
cooldownMinutes: String(kv[DRAW_KEYS.COOLDOWN_MINUTES] ?? 15),
|
|
};
|
|
}
|
|
|
|
function buildDirtyItems(draft: DrawDraft, saved: DrawDraft): AdminSettingBatchItem[] {
|
|
const items: AdminSettingBatchItem[] = [];
|
|
const push = (key: string, value: unknown, changed: boolean) => {
|
|
if (changed) {
|
|
items.push({ key, value });
|
|
}
|
|
};
|
|
|
|
push(
|
|
DRAW_KEYS.DEFAULT_CURRENCY,
|
|
draft.defaultCurrency.trim().toUpperCase() || "NPR",
|
|
draft.defaultCurrency !== saved.defaultCurrency,
|
|
);
|
|
push(
|
|
DRAW_KEYS.DRAW_INTERVAL_MINUTES,
|
|
Math.max(1, Number.parseInt(draft.drawIntervalMinutes || "5", 10) || 5),
|
|
draft.drawIntervalMinutes !== saved.drawIntervalMinutes,
|
|
);
|
|
push(
|
|
DRAW_KEYS.DRAW_BETTING_WINDOW_SECONDS,
|
|
Math.max(10, Number.parseInt(draft.drawBettingWindowSeconds || "270", 10) || 270),
|
|
draft.drawBettingWindowSeconds !== saved.drawBettingWindowSeconds,
|
|
);
|
|
push(
|
|
DRAW_KEYS.DRAW_CLOSE_BEFORE_DRAW_SECONDS,
|
|
Math.max(5, Number.parseInt(draft.drawCloseBeforeDrawSeconds || "30", 10) || 30),
|
|
draft.drawCloseBeforeDrawSeconds !== saved.drawCloseBeforeDrawSeconds,
|
|
);
|
|
push(
|
|
DRAW_KEYS.DRAW_BUFFER_DRAWS_AHEAD,
|
|
Math.max(1, Number.parseInt(draft.drawBufferDrawsAhead || "8", 10) || 8),
|
|
draft.drawBufferDrawsAhead !== saved.drawBufferDrawsAhead,
|
|
);
|
|
push(DRAW_KEYS.REQUIRE_MANUAL_REVIEW, draft.requireManualReview, draft.requireManualReview !== saved.requireManualReview);
|
|
push(
|
|
DRAW_KEYS.COOLDOWN_MINUTES,
|
|
Math.max(0, Number.parseInt(draft.cooldownMinutes || "0", 10) || 0),
|
|
draft.cooldownMinutes !== saved.cooldownMinutes,
|
|
);
|
|
|
|
return items;
|
|
}
|
|
|
|
export function DrawSettingsPanel() {
|
|
const { t } = useTranslation(["config", "adminUsers", "common"]);
|
|
const profile = useAdminProfile();
|
|
const canManage = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_RESULT_MANAGE]);
|
|
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
|
const buildItems = useCallback(buildDirtyItems, []);
|
|
const section = useSettingsSection({
|
|
initialDraft: INITIAL,
|
|
fromKv,
|
|
buildDirtyItems: buildItems,
|
|
saveSuccessKey: "system.saveDrawSuccess",
|
|
saveFailedKey: "system.saveFailed",
|
|
});
|
|
|
|
const { draft, loading, saving, dirty, updateField, discard, save } = section;
|
|
|
|
return (
|
|
<>
|
|
<AdminPageCard
|
|
title={t("system.sections.draw", { ns: "config" })}
|
|
description={t("system.sections.drawDescription", { ns: "config" })}
|
|
>
|
|
<div className="space-y-6">
|
|
<div className="rounded-xl border border-border/70 bg-card overflow-hidden shadow-sm">
|
|
<div className="flex flex-wrap items-center justify-between gap-3 px-4 py-3.5 bg-background/50 transition-colors hover:bg-muted/30">
|
|
<Label className="font-medium cursor-pointer" onClick={() => updateField("requireManualReview", !draft.requireManualReview)}>{t("system.fields.manualReview", { ns: "config" })}</Label>
|
|
<Switch
|
|
checked={draft.requireManualReview}
|
|
disabled={!canManage || loading || saving}
|
|
aria-label={t("system.fields.manualReview", { ns: "config" })}
|
|
onCheckedChange={(value) => updateField("requireManualReview", value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-x-6 gap-y-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="default-currency" className="text-muted-foreground">
|
|
{t("system.fields.defaultCurrency", { ns: "config" })}
|
|
</Label>
|
|
<Input
|
|
id="default-currency"
|
|
value={draft.defaultCurrency}
|
|
placeholder={t("system.placeholders.defaultCurrency", { ns: "config" })}
|
|
onChange={(e) => updateField("defaultCurrency", e.target.value.toUpperCase())}
|
|
disabled={!canManage || loading || saving}
|
|
maxLength={16}
|
|
className="h-10 bg-background/50 transition-colors focus:bg-background"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="draw-interval-minutes" className="text-muted-foreground">
|
|
{t("system.fields.drawIntervalMinutes", { ns: "config" })}
|
|
</Label>
|
|
<Input
|
|
id="draw-interval-minutes"
|
|
type="number"
|
|
min="1"
|
|
max="1440"
|
|
step="1"
|
|
value={draft.drawIntervalMinutes}
|
|
placeholder={t("system.placeholders.drawIntervalMinutes", { ns: "config" })}
|
|
onChange={(e) => updateField("drawIntervalMinutes", e.target.value)}
|
|
disabled={!canManage || loading || saving}
|
|
className="h-10 bg-background/50 transition-colors focus:bg-background"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="draw-betting-window-seconds" className="text-muted-foreground">
|
|
{t("system.fields.drawBettingWindowSeconds", { ns: "config" })}
|
|
</Label>
|
|
<Input
|
|
id="draw-betting-window-seconds"
|
|
type="number"
|
|
min="10"
|
|
step="1"
|
|
value={draft.drawBettingWindowSeconds}
|
|
placeholder={t("system.placeholders.drawBettingWindowSeconds", { ns: "config" })}
|
|
onChange={(e) => updateField("drawBettingWindowSeconds", e.target.value)}
|
|
disabled={!canManage || loading || saving}
|
|
className="h-10 bg-background/50 transition-colors focus:bg-background"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="draw-close-before-seconds" className="text-muted-foreground">
|
|
{t("system.fields.drawCloseBeforeDrawSeconds", { ns: "config" })}
|
|
</Label>
|
|
<Input
|
|
id="draw-close-before-seconds"
|
|
type="number"
|
|
min="5"
|
|
step="1"
|
|
value={draft.drawCloseBeforeDrawSeconds}
|
|
placeholder={t("system.placeholders.drawCloseBeforeDrawSeconds", { ns: "config" })}
|
|
onChange={(e) => updateField("drawCloseBeforeDrawSeconds", e.target.value)}
|
|
disabled={!canManage || loading || saving}
|
|
className="h-10 bg-background/50 transition-colors focus:bg-background"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="draw-buffer-ahead" className="text-muted-foreground">
|
|
{t("system.fields.drawBufferDrawsAhead", { ns: "config" })}
|
|
</Label>
|
|
<Input
|
|
id="draw-buffer-ahead"
|
|
type="number"
|
|
min="1"
|
|
step="1"
|
|
value={draft.drawBufferDrawsAhead}
|
|
placeholder={t("system.placeholders.drawBufferDrawsAhead", { ns: "config" })}
|
|
onChange={(e) => updateField("drawBufferDrawsAhead", e.target.value)}
|
|
disabled={!canManage || loading || saving}
|
|
className="h-10 bg-background/50 transition-colors focus:bg-background"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="cooldown-minutes" className="text-muted-foreground">
|
|
{t("system.fields.cooldownMinutes", { ns: "config" })}
|
|
</Label>
|
|
<Input
|
|
id="cooldown-minutes"
|
|
type="number"
|
|
min="0"
|
|
step="1"
|
|
value={draft.cooldownMinutes}
|
|
placeholder={t("system.placeholders.cooldownMinutes", { ns: "config" })}
|
|
onChange={(e) => updateField("cooldownMinutes", e.target.value)}
|
|
disabled={!canManage || loading || saving}
|
|
className="h-10 bg-background/50 transition-colors focus:bg-background"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<SettingsSectionActions
|
|
dirty={dirty}
|
|
loading={!canManage || loading}
|
|
saving={saving}
|
|
onSave={() =>
|
|
requestConfirm({
|
|
title: t("system.confirmSaveDrawTitle", { ns: "config" }),
|
|
description: t("system.confirmSaveDrawDescription", { ns: "config" }),
|
|
confirmLabel: t("confirm.confirmSave", { ns: "common" }),
|
|
onConfirm: () => {
|
|
void save();
|
|
},
|
|
})
|
|
}
|
|
onDiscard={discard}
|
|
saveLabel={t("actions.save", { ns: "adminUsers" })}
|
|
savingLabel={t("saving", { ns: "adminUsers" })}
|
|
discardLabel={t("system.discard", { ns: "config" })}
|
|
/>
|
|
</div>
|
|
</AdminPageCard>
|
|
<ConfirmDialog />
|
|
</>
|
|
);
|
|
}
|