Implemented new API functions to fetch and update agent node profiles, enhancing the management capabilities for agent data. This addition improves the overall functionality of the admin agents console, allowing for better user interaction with agent profiles. Updated related types for improved type safety and clarity in the codebase.
634 lines
22 KiB
TypeScript
634 lines
22 KiB
TypeScript
"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<RiskCapItemRow, "id"> & { 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<ConfigVersionSummary[]>([]);
|
|
const [selectedId, setSelectedId] = useState("");
|
|
const [detail, setDetail] = useState<RiskCapVersionDetail | null>(null);
|
|
const [draftRows, setDraftRows] = useState<DraftRiskRow[]>([]);
|
|
const [loadingList, setLoadingList] = useState(true);
|
|
const [loadingDetail, setLoadingDetail] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const [defaultCapStr, setDefaultCapStr] = useState("");
|
|
const [syncOpen, setSyncOpen] = useState(false);
|
|
const [rollbackOpen, setRollbackOpen] = useState(false);
|
|
const [rollbackTarget, setRollbackTarget] = useState<ConfigVersionSummary | null>(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<DraftRiskRow>) => {
|
|
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 (
|
|
<ConfigDocPage
|
|
title={t("nav.items.risk-cap", { ns: "config" })}
|
|
titleSuffix={detail ? `· v${detail.version_no}` : undefined}
|
|
toolbar={
|
|
<ConfigDocToolbar
|
|
switcher={
|
|
<ConfigVersionSwitcher
|
|
versions={list}
|
|
selectedId={selectedId}
|
|
onSelectedIdChange={setSelectedId}
|
|
loading={loadingList}
|
|
sheetTitle={`${t("nav.items.risk-cap", { ns: "config" })} ${t("versionSwitcher.sheetTitle", { ns: "config" })}`}
|
|
onDeleteVersion={handleDeleteVersion}
|
|
onRollbackVersion={requestRollback}
|
|
rollbackBusy={saving}
|
|
/>
|
|
}
|
|
actions={
|
|
<ConfigVersionActions
|
|
isDraft={isDraft}
|
|
canManage={canManage}
|
|
loadingList={loadingList}
|
|
loadingDetail={loadingDetail}
|
|
saving={saving}
|
|
onRefresh={() => 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 ? (
|
|
<ConfigVersionToolbarMeta emphasis={!isDraft}>
|
|
<span>
|
|
{t("riskCap.effectiveAt", {
|
|
ns: "config",
|
|
value: detail.effective_at ? formatDt(detail.effective_at) : "—",
|
|
})}
|
|
</span>
|
|
{!isDraft ? (
|
|
<ConfigVersionToolbarMetaEmphasis>
|
|
{t("riskCap.readOnlyHint", { ns: "config" })}
|
|
</ConfigVersionToolbarMetaEmphasis>
|
|
) : (
|
|
<span>{t("versionToolbar.draftEditing", { ns: "config" })}</span>
|
|
)}
|
|
</ConfigVersionToolbarMeta>
|
|
) : null
|
|
}
|
|
/>
|
|
}
|
|
contentClassName="space-y-8"
|
|
>
|
|
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
|
|
|
<ConfigSection title={t("riskCap.defaultCap.title", { ns: "config" })}>
|
|
<div className="flex flex-wrap items-end gap-2">
|
|
<div className="grid gap-1">
|
|
<Label htmlFor="default-cap">{t("riskCap.defaultCap.fieldLabel", { ns: "config" })}</Label>
|
|
{canEditDraft ? (
|
|
<Input
|
|
id="default-cap"
|
|
type="number"
|
|
min={0}
|
|
className="w-[220px] font-mono tabular-nums"
|
|
disabled={saving}
|
|
value={defaultCapStr}
|
|
placeholder={t("riskCap.placeholders.defaultCap", { ns: "config" })}
|
|
onChange={(e) => setDefaultCapStr(e.target.value)}
|
|
/>
|
|
) : (
|
|
<ConfigReadonlyValue mono className="w-[220px]">
|
|
{defaultCapStr || formatAdminMinorDecimal(0, amountCurrencyCode)}
|
|
</ConfigReadonlyValue>
|
|
)}
|
|
</div>
|
|
{canEditDraft ? (
|
|
<Button type="button" variant="secondary" disabled={saving} onClick={() => setSyncOpen(true)}>
|
|
{t("riskCap.actions.update", { ns: "config" })}
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
</ConfigSection>
|
|
|
|
<ConfigSection
|
|
title={t("riskCap.specialCaps.title", { ns: "config" })}
|
|
actions={
|
|
canEditDraft ? (
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
disabled={saving}
|
|
onClick={() => setDraftRows((prev) => [...prev, newRow()])}
|
|
>
|
|
{t("riskCap.actions.addSpecialCap", { ns: "config" })}
|
|
</Button>
|
|
) : null
|
|
}
|
|
>
|
|
{loadingDetail ? (
|
|
<AdminLoadingState minHeight="6rem" className="py-4" label={t("riskCap.loadingDetails", { ns: "config" })} />
|
|
) : specialRows.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">{t("riskCap.noDetailRows", { ns: "config" })}</p>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-[110px]">{t("riskCap.table.number", { ns: "config" })}</TableHead>
|
|
<TableHead className="w-[140px]">{t("riskCap.table.capAmount", { ns: "config" })}</TableHead>
|
|
<TableHead className="sticky right-0 z-20 bg-muted w-14 text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">{t("riskCap.table.actions", { ns: "config" })}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{specialRows.map(({ row: r, index: idx }) => (
|
|
<TableRow key={r.clientKey}>
|
|
<TableCell>
|
|
{canEditDraft ? (
|
|
<Input
|
|
className="h-8 font-mono tabular-nums"
|
|
maxLength={4}
|
|
disabled={saving}
|
|
value={r.normalized_number}
|
|
placeholder={t("riskCap.placeholders.number", { ns: "config" })}
|
|
onChange={(e) =>
|
|
updateRow(idx, {
|
|
normalized_number: e.target.value.replace(/\D/g, "").slice(0, 4),
|
|
})
|
|
}
|
|
/>
|
|
) : (
|
|
<ConfigReadonlyValue mono>{r.normalized_number}</ConfigReadonlyValue>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{canEditDraft ? (
|
|
<Input
|
|
type="text"
|
|
inputMode="decimal"
|
|
className="h-8 font-mono tabular-nums"
|
|
disabled={saving}
|
|
value={formatAdminMinorDecimal(r.cap_amount, amountCurrencyCode)}
|
|
placeholder={t("riskCap.placeholders.capAmount", { ns: "config" })}
|
|
onChange={(e) =>
|
|
updateRow(idx, {
|
|
cap_amount:
|
|
parseAdminMajorToMinor(e.target.value, amountCurrencyCode) ?? 0,
|
|
})
|
|
}
|
|
/>
|
|
) : (
|
|
<ConfigReadonlyValue mono>
|
|
{formatAdminMinorDecimal(r.cap_amount, amountCurrencyCode)}
|
|
</ConfigReadonlyValue>
|
|
)}
|
|
</TableCell>
|
|
<TableCell className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
|
|
{canEditDraft ? (
|
|
<AdminRowActionsMenu
|
|
busy={saving}
|
|
actions={[
|
|
{
|
|
key: "delete",
|
|
label: t("actions.delete", { ns: "adminUsers" }),
|
|
icon: Trash2,
|
|
destructive: true,
|
|
onClick: () => removeRow(idx),
|
|
},
|
|
]}
|
|
/>
|
|
) : (
|
|
<span className="text-sm text-muted-foreground">{t("riskCap.readOnly", { ns: "config" })}</span>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</ConfigSection>
|
|
|
|
<RiskCapRuntimePanel />
|
|
|
|
<Dialog open={syncOpen} onOpenChange={setSyncOpen}>
|
|
<DialogContent showCloseButton className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{t("riskCap.syncDialog.title", { ns: "config" })}</DialogTitle>
|
|
<DialogDescription>
|
|
{t("riskCap.syncDialog.description", { ns: "config", value: defaultCapStr || "(empty)" })}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" onClick={() => setSyncOpen(false)}>
|
|
{t("actions.cancel", { ns: "adminUsers" })}
|
|
</Button>
|
|
<Button type="button" onClick={applyDefaultCap}>
|
|
{t("riskCap.syncDialog.confirm", { ns: "config" })}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog open={rollbackOpen} onOpenChange={setRollbackOpen}>
|
|
<DialogContent showCloseButton className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{t("versionActions.rollbackDialog.title", { ns: "config" })}</DialogTitle>
|
|
<DialogDescription>
|
|
{t("versionActions.rollbackDialog.description", {
|
|
ns: "config",
|
|
version: rollbackTarget?.version_no ?? "—",
|
|
})}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" onClick={() => setRollbackOpen(false)}>
|
|
{t("actions.cancel", { ns: "adminUsers" })}
|
|
</Button>
|
|
<Button type="button" onClick={() => void handleRollback()} disabled={!rollbackTarget || saving}>
|
|
{t("versionActions.rollbackDialog.confirm", { ns: "config" })}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<ConfirmDialog />
|
|
</ConfigDocPage>
|
|
);
|
|
}
|