"use client"; import { Trash2 } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { deleteRiskCapVersion, getAllConfigVersions, getRiskCapVersion, getRiskCapVersions, postRiskCapVersion, publishRiskCapVersion, putRiskCapItems, } from "@/api/admin-config"; import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; import { Button } from "@/components/ui/button"; import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page"; import { ConfigVersionToolbarMeta, ConfigVersionToolbarMetaEmphasis, } from "@/modules/config/config-version-toolbar-meta"; import { ConfigSection } from "@/modules/config/config-section"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state"; import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher"; import { RiskCapRuntimePanel } from "@/modules/config/risk-cap-runtime-panel"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value"; import { ConfigVersionActions } from "@/modules/config/config-version-actions"; import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; import { useAsyncEffect } from "@/hooks/use-async-effect"; import { useConfirmAction } from "@/hooks/use-confirm-action"; import { useTranslationRef } from "@/hooks/use-translation-ref"; import { adminHasAnyPermission } from "@/lib/admin-permissions"; import { formatAdminMinorDecimal, parseAdminMajorToMinor } from "@/lib/money"; import { PRD_RISK_CAP_MANAGE, PRD_RISK_CAP_VIEW } from "@/lib/admin-prd"; import { useAdminProfile } from "@/stores/admin-session"; import { LotteryApiBizError } from "@/types/api/errors"; import { pickDefaultConfigVersionId } from "@/lib/config-version-auto-pick"; import type { ConfigVersionSummary, RiskCapItemRow, RiskCapVersionDetail, } from "@/types/api/admin-config"; type DraftRiskRow = Omit & { clientKey: string }; function newRow(): DraftRiskRow { return { clientKey: `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, draw_id: null, normalized_number: "0000", cap_amount: 0, cap_type: "per_number", }; } function isDefaultRiskRow(row: DraftRiskRow): boolean { return row.cap_type === "default"; } function defaultRiskRowFromAmount(amount: number): DraftRiskRow { return { clientKey: `default-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, draw_id: null, normalized_number: "0000", cap_amount: amount, cap_type: "default", }; } export function RiskCapDocScreen() { const { t } = useTranslation(["config", "adminUsers", "common"]); const tRef = useTranslationRef(["config", "common"]); const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); const profile = useAdminProfile(); const canManage = adminHasAnyPermission(profile?.permissions, [PRD_RISK_CAP_MANAGE]); const formatDt = useAdminDateTimeFormatter(); const [list, setList] = useState([]); const [selectedId, setSelectedId] = useState(""); const [detail, setDetail] = useState(null); const [draftRows, setDraftRows] = useState([]); const [loadingList, setLoadingList] = useState(true); const [loadingDetail, setLoadingDetail] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [defaultCapStr, setDefaultCapStr] = useState(""); const [syncOpen, setSyncOpen] = useState(false); const [rollbackOpen, setRollbackOpen] = useState(false); const [rollbackTarget, setRollbackTarget] = useState(null); const amountCurrencyCode = "NPR"; const refreshList = useCallback(async () => { setLoadingList(true); setError(null); try { const d = await getAllConfigVersions(getRiskCapVersions); setList(d.items); } catch (e) { const msg = e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }); setError(msg); setList([]); } finally { setLoadingList(false); } }, []); useAsyncEffect(() => { void refreshList(); }, []); function syncDefaultCapFromRows(rows: DraftRiskRow[]) { const defaultRow = rows.find(isDefaultRiskRow); if (!defaultRow) { setDefaultCapStr(""); return; } setDefaultCapStr(formatAdminMinorDecimal(defaultRow.cap_amount, amountCurrencyCode)); } const loadDetail = useCallback(async (id: number) => { setLoadingDetail(true); try { const d = await getRiskCapVersion(id); setDetail(d); const mapped = d.items.map((it) => ({ clientKey: `srv-${it.id}`, draw_id: it.draw_id, normalized_number: it.normalized_number, cap_amount: it.cap_amount, cap_type: it.cap_type, })); setDraftRows(mapped); syncDefaultCapFromRows(mapped); } catch (e) { toast.error( e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }), ); setDetail(null); setDraftRows([]); syncDefaultCapFromRows([]); } finally { setLoadingDetail(false); } }, []); useEffect(() => { if (list.length === 0) { if (selectedId !== "") { queueMicrotask(() => { setSelectedId(""); setDetail(null); setDraftRows([]); syncDefaultCapFromRows([]); }); } return; } if (selectedId !== "" && list.some((x) => String(x.id) === selectedId)) { return; } queueMicrotask(() => { const pickId = pickDefaultConfigVersionId(list); if (pickId) { setSelectedId(pickId); } }); }, [list, selectedId]); useEffect(() => { if (selectedId === "") { return; } const id = Number(selectedId); if (!Number.isFinite(id)) { return; } queueMicrotask(() => { void loadDetail(id); }); }, [selectedId, loadDetail]); const selectedVersionSummary = useMemo( () => list.find((x) => String(x.id) === selectedId) ?? null, [list, selectedId], ); const isSelectedDetail = detail !== null && String(detail.id) === selectedId; const selectedStatus = isSelectedDetail ? detail.status : selectedVersionSummary?.status; const isDraft = selectedStatus === "draft"; const canEditDraft = isDraft && canManage; const updateRow = (idx: number, patch: Partial) => { setDraftRows((prev) => prev.map((r, i) => (i === idx ? { ...r, ...patch } : r))); }; function removeRow(idx: number) { setDraftRows((prev) => prev.filter((_, i) => i !== idx)); } async function handleSave() { if (!detail || !canEditDraft) { return; } if (draftRows.length === 0) { toast.error(t("riskCap.validation.requireAtLeastOne", { ns: "config" })); return; } for (const r of draftRows) { if (isDefaultRiskRow(r)) { if (r.cap_amount <= 0) { toast.error(t("riskCap.validation.defaultGreaterThanZero", { ns: "config" })); return; } continue; } if (!/^[0-9]{4}$/.test(r.normalized_number)) { toast.error(t("riskCap.validation.numberMustBe4Digits", { ns: "config", number: r.normalized_number })); return; } } setSaving(true); try { const payload = draftRows.map((r) => ({ draw_id: r.draw_id && r.draw_id > 0 ? r.draw_id : null, normalized_number: r.normalized_number, cap_amount: r.cap_amount, cap_type: r.cap_type, })); const d = await putRiskCapItems(detail.id, payload); setDetail(d); const saved = d.items.map((it) => ({ clientKey: `srv-${it.id}`, draw_id: it.draw_id, normalized_number: it.normalized_number, cap_amount: it.cap_amount, cap_type: it.cap_type, })); setDraftRows(saved); syncDefaultCapFromRows(saved); toast.success(t("versionActions.saveDraft", { ns: "config" })); void refreshList(); } catch (e) { toast.error(e instanceof LotteryApiBizError ? e.message : t("versionActions.saveFailed", { ns: "config" })); } finally { setSaving(false); } } async function handlePublish() { if (!detail || !canEditDraft) { return; } setSaving(true); try { const d = await publishRiskCapVersion(detail.id); setDetail(d); const pub = d.items.map((it) => ({ clientKey: `srv-${it.id}`, draw_id: it.draw_id, normalized_number: it.normalized_number, cap_amount: it.cap_amount, cap_type: it.cap_type, })); setDraftRows(pub); syncDefaultCapFromRows(pub); toast.success(t("versionActions.publishCurrent", { ns: "config" })); void refreshList(); setSelectedId(String(d.id)); } catch (e) { toast.error(e instanceof LotteryApiBizError ? e.message : t("riskCap.publishFailed", { ns: "config" })); } finally { setSaving(false); } } async function handleNewDraft() { setSaving(true); try { const active = list.find((x) => x.status === "active"); const d = await postRiskCapVersion({ reason: `draft ${new Date().toISOString()}`, clone_from_version_id: active?.id ?? null, }); toast.success(t("riskCap.createDraftSuccess", { ns: "config", version: d.version_no })); await refreshList(); setSelectedId(String(d.id)); setDetail(d); const nd = d.items.map((it) => ({ clientKey: `srv-${it.id}`, draw_id: it.draw_id, normalized_number: it.normalized_number, cap_amount: it.cap_amount, cap_type: it.cap_type, })); setDraftRows(nd); syncDefaultCapFromRows(nd); } catch (e) { toast.error(e instanceof LotteryApiBizError ? e.message : t("riskCap.createDraftFailed", { ns: "config" })); } finally { setSaving(false); } } function applyDefaultCap() { const n = parseAdminMajorToMinor(defaultCapStr, amountCurrencyCode); if (n == null || !Number.isFinite(n) || n <= 0) { toast.error(t("riskCap.validation.enterValidCapAmount", { ns: "config" })); return; } setDraftRows((prev) => { const next = prev.filter((row) => !isDefaultRiskRow(row)); return [defaultRiskRowFromAmount(n), ...next]; }); setSyncOpen(false); toast.message(t("riskCap.savedLocalDraft", { ns: "config" })); } const specialRows = useMemo( () => draftRows.map((row, index) => ({ row, index })).filter(({ row }) => !isDefaultRiskRow(row)), [draftRows], ); async function handleDeleteVersion(row: ConfigVersionSummary) { try { await deleteRiskCapVersion(row.id); toast.success(t("versionSwitcher.delete", { ns: "config" })); await refreshList(); } catch (e) { toast.error(e instanceof LotteryApiBizError ? e.message : t("riskCap.deleteFailed", { ns: "config" })); throw e; } } function requestRollback(row: ConfigVersionSummary) { setRollbackTarget(row); setRollbackOpen(true); } async function handleRollback() { if (!rollbackTarget) { return; } setSaving(true); try { const d = await postRiskCapVersion({ reason: `rollback from v${rollbackTarget.version_no}`, clone_from_version_id: rollbackTarget.id, }); toast.success( t("versionActions.rollbackSuccess", { ns: "config", fromVersion: rollbackTarget.version_no, version: d.version_no, }), ); await refreshList(); setSelectedId(String(d.id)); setDetail(d); const mapped = d.items.map((it) => ({ clientKey: `srv-${it.id}`, draw_id: it.draw_id, normalized_number: it.normalized_number, cap_amount: it.cap_amount, cap_type: it.cap_type, })); setDraftRows(mapped); syncDefaultCapFromRows(mapped); setRollbackOpen(false); setRollbackTarget(null); } catch (e) { toast.error(e instanceof LotteryApiBizError ? e.message : t("versionActions.rollbackFailed", { ns: "config" })); } finally { setSaving(false); } } return ( } actions={ void refreshList()} onNewDraft={() => void handleNewDraft()} onSaveDraft={() => void handleSave()} onPublish={() => requestConfirm({ title: t("riskCap.publishDialog.title", { ns: "config" }), description: t("riskCap.publishDialog.description", { ns: "config" }), confirmLabel: t("riskCap.publishDialog.confirm", { ns: "config" }), confirmVariant: "destructive", onConfirm: () => handlePublish(), }) } /> } footer={ detail ? ( {t("riskCap.effectiveAt", { ns: "config", value: detail.effective_at ? formatDt(detail.effective_at) : "—", })} {!isDraft ? ( {t("riskCap.readOnlyHint", { ns: "config" })} ) : ( {t("versionToolbar.draftEditing", { ns: "config" })} )} ) : null } /> } contentClassName="space-y-8" > {error ?

{error}

: null}
{canEditDraft ? ( setDefaultCapStr(e.target.value)} /> ) : ( {defaultCapStr || formatAdminMinorDecimal(0, amountCurrencyCode)} )}
{canEditDraft ? ( ) : null}
setDraftRows((prev) => [...prev, newRow()])} > {t("riskCap.actions.addSpecialCap", { ns: "config" })} ) : null } > {loadingDetail ? ( ) : specialRows.length === 0 ? (

{t("riskCap.noDetailRows", { ns: "config" })}

) : ( {t("riskCap.table.number", { ns: "config" })} {t("riskCap.table.capAmount", { ns: "config" })} {t("riskCap.table.actions", { ns: "config" })} {specialRows.map(({ row: r, index: idx }) => ( {canEditDraft ? ( updateRow(idx, { normalized_number: e.target.value.replace(/\D/g, "").slice(0, 4), }) } /> ) : ( {r.normalized_number} )} {canEditDraft ? ( updateRow(idx, { cap_amount: parseAdminMajorToMinor(e.target.value, amountCurrencyCode) ?? 0, }) } /> ) : ( {formatAdminMinorDecimal(r.cap_amount, amountCurrencyCode)} )} {canEditDraft ? ( removeRow(idx), }, ]} /> ) : ( {t("riskCap.readOnly", { ns: "config" })} )} ))}
)}
{t("riskCap.syncDialog.title", { ns: "config" })} {t("riskCap.syncDialog.description", { ns: "config", value: defaultCapStr || "(empty)" })} {t("versionActions.rollbackDialog.title", { ns: "config" })} {t("versionActions.rollbackDialog.description", { ns: "config", version: rollbackTarget?.version_no ?? "—", })}
); }