"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { getAdminSettings, updateAdminSettingsBatch } from "@/api/admin-settings"; import { useOptionalAdminSettingsData } from "@/modules/settings/admin-settings-data-context"; import { WALLET_GROUP, WALLET_KEYS } from "@/modules/settings/settings-keys"; import { useConfirmAction } from "@/hooks/use-confirm-action"; import { Button } from "@/components/ui/button"; import { ConfigDocPage } from "@/modules/config/config-doc-page"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { adminHasAnyPermission } from "@/lib/admin-permissions"; import { PRD_WALLET_ADJUST_MANAGE, PRD_WALLET_RECONCILE_MANAGE } from "@/lib/admin-prd"; import { useAdminProfile } from "@/stores/admin-session"; import { LotteryApiBizError } from "@/types/api/errors"; function minorUnitsToDisplay(n: unknown, decimals = 2): string { const num = Number(n); if (!Number.isFinite(num)) return ""; return (num / 100).toFixed(decimals); } function displayToMinorUnits(s: string): number | null { const normalized = s.trim(); if (normalized === "") return null; const n = Number(normalized); if (!Number.isFinite(n)) return null; return Math.round(n * 100); } interface Draft { inMin: string; inMax: string; outMin: string; outMax: string; } function draftFromKv(kv: Record): Draft { return { inMin: minorUnitsToDisplay(kv[WALLET_KEYS.IN_MIN] ?? 100), inMax: minorUnitsToDisplay(kv[WALLET_KEYS.IN_MAX] ?? 0), outMin: minorUnitsToDisplay(kv[WALLET_KEYS.OUT_MIN] ?? 100), outMax: minorUnitsToDisplay(kv[WALLET_KEYS.OUT_MAX] ?? 0), }; } type WalletConfigDocScreenProps = { embedded?: boolean; }; function validateDraft(draft: Draft, t: ReturnType>["t"]): string[] { const errors: string[] = []; const values = { inMin: displayToMinorUnits(draft.inMin), inMax: displayToMinorUnits(draft.inMax), outMin: displayToMinorUnits(draft.outMin), outMax: displayToMinorUnits(draft.outMax), }; for (const field of ["inMin", "inMax", "outMin", "outMax"] as const) { if (values[field] === null || values[field] < 1) { errors.push(t("wallet.validation.amountAtLeastMinorUnit", { ns: "config", field: t(`wallet.fields.${field}`, { ns: "config" }), })); } } if (values.inMin !== null && values.inMax !== null && values.inMax < values.inMin) { errors.push(t("wallet.validation.inRangeInvalid", { ns: "config" })); } if (values.outMin !== null && values.outMax !== null && values.outMax < values.outMin) { errors.push(t("wallet.validation.outRangeInvalid", { ns: "config" })); } return [...new Set(errors)]; } export function WalletConfigDocScreen({ embedded = false }: WalletConfigDocScreenProps) { const { t } = useTranslation(["config", "adminUsers", "common"]); const tRef = useRef(t); tRef.current = t; const shared = useOptionalAdminSettingsData(); const profile = useAdminProfile(); const canManage = adminHasAnyPermission(profile?.permissions, [ PRD_WALLET_RECONCILE_MANAGE, PRD_WALLET_ADJUST_MANAGE, ]); const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); const [draft, setDraft] = useState({ inMin: "", inMax: "", outMin: "", outMax: "", }); const [saved, setSaved] = useState({ inMin: "", inMax: "", outMin: "", outMax: "" }); const [standaloneLoading, setStandaloneLoading] = useState(!embedded); const [saving, setSaving] = useState(false); const dirty = draft.inMin !== saved.inMin || draft.inMax !== saved.inMax || draft.outMin !== saved.outMin || draft.outMax !== saved.outMax; const validationErrors = validateDraft(draft, t); const hasValidationError = validationErrors.length > 0; const loading = embedded ? (shared?.loading ?? true) : standaloneLoading; const loadStandalone = useCallback(async () => { setStandaloneLoading(true); try { const res = await getAdminSettings(WALLET_GROUP); const kv: Record = {}; for (const item of res.items) { kv[item.key] = item.value; } const d = draftFromKv(kv); setDraft(d); setSaved(d); } catch { toast.error(tRef.current("wallet.loadFailed", { ns: "config" })); } finally { setStandaloneLoading(false); } }, []); useEffect(() => { if (!embedded) { void loadStandalone(); } }, [embedded, loadStandalone]); useEffect(() => { if (!embedded || shared?.kv === null || shared?.kv === undefined) { return; } const d = draftFromKv(shared.kv); setDraft(d); setSaved(d); }, [embedded, shared?.kv]); const handleChange = (field: keyof Draft, value: string) => { setDraft((prev) => ({ ...prev, [field]: value })); }; const handleSave = async () => { if (hasValidationError) { toast.error(validationErrors[0]); return; } const items = []; if (draft.inMin !== saved.inMin) { items.push({ key: WALLET_KEYS.IN_MIN, value: displayToMinorUnits(draft.inMin) }); } if (draft.inMax !== saved.inMax) { items.push({ key: WALLET_KEYS.IN_MAX, value: displayToMinorUnits(draft.inMax) }); } if (draft.outMin !== saved.outMin) { items.push({ key: WALLET_KEYS.OUT_MIN, value: displayToMinorUnits(draft.outMin) }); } if (draft.outMax !== saved.outMax) { items.push({ key: WALLET_KEYS.OUT_MAX, value: displayToMinorUnits(draft.outMax) }); } if (items.length === 0) { return; } setSaving(true); try { await updateAdminSettingsBatch(items); const updates: Record = {}; for (const item of items) { updates[item.key] = item.value; } shared?.patchKv(updates); toast.success(t("wallet.saveSuccess", { ns: "config" })); setSaved(draft); } catch (error) { toast.error( error instanceof LotteryApiBizError ? error.message : t("wallet.saveFailed", { ns: "config" }), ); } finally { setSaving(false); } }; const content = ( <>
handleChange("inMin", e.target.value)} disabled={!canManage || loading || saving} aria-invalid={hasValidationError} />
handleChange("inMax", e.target.value)} disabled={!canManage || loading || saving} aria-invalid={hasValidationError} />
handleChange("outMin", e.target.value)} disabled={!canManage || loading || saving} aria-invalid={hasValidationError} />
handleChange("outMax", e.target.value)} disabled={!canManage || loading || saving} aria-invalid={hasValidationError} />
{validationErrors.length > 0 && (
{validationErrors.map((error) => (

{error}

))}
)}
{dirty && ( )}
); if (embedded) { return content; } return ( {content} ); }