import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { RotateCcw, Save, Send } from "lucide-react"; import { api, type ConfigItem, type SystemConfigData } from "../lib/api"; import { notify } from "../lib/notifications"; import Tooltip from "./Tooltip"; // Admin Configuration page: edit DB-overridable operational settings (registry-driven from // the backend — adding a key there makes it appear here automatically). Group order is fixed // where known so logically related settings (e.g. Email/SMTP) stay together. const GROUP_ORDER = ["email"]; export default function ConfigPanel() { const { t } = useTranslation(); const qc = useQueryClient(); const q = useQuery({ queryKey: ["admin-config"], queryFn: api.adminConfig }); const data = q.data; const apply = (fresh: SystemConfigData) => qc.setQueryData(["admin-config"], fresh); const save = useMutation({ mutationFn: ({ key, value }: { key: string; value: string | number | boolean }) => api.setConfig(key, value), onSuccess: (fresh) => { apply(fresh); notify({ level: "success", message: t("config.saved") }); }, onError: () => notify({ level: "error", message: t("config.saveFailed") }), }); const reset = useMutation({ mutationFn: (key: string) => api.resetConfig(key), onSuccess: (fresh) => { apply(fresh); notify({ level: "success", message: t("config.resetDone") }); }, onError: () => notify({ level: "error", message: t("config.saveFailed") }), }); const testEmail = useMutation({ mutationFn: () => api.testEmail(), onSuccess: (r) => notify({ level: "success", message: t("config.testSent", { to: r.to }) }), onError: () => notify({ level: "error", message: t("config.testFailed") }), }); const busy = save.isPending || reset.isPending; if (q.isLoading || !data) { return
{t("config.loading")}
; } const groupKeys = Object.keys(data.groups).sort( (a, b) => (GROUP_ORDER.indexOf(a) + 1 || 99) - (GROUP_ORDER.indexOf(b) + 1 || 99) ); return (

{t("config.intro")}

{groupKeys.map((g) => (
{t(`config.groups.${g}`, g)}
{data.groups[g].map((item) => ( save.mutate({ key: item.key, value })} onReset={() => reset.mutate(item.key)} /> ))}
{g === "email" && (
)}
))}
); } function Field({ item, secretsManageable, busy, onSave, onReset, }: { item: ConfigItem; secretsManageable: boolean; busy: boolean; onSave: (value: string | number) => void; onReset: () => void; }) { const { t } = useTranslation(); const isNum = item.type === "int"; const [draft, setDraft] = useState(item.secret ? "" : String(item.value ?? "")); const secretDisabled = item.secret && !secretsManageable; // Status line for secrets (never reveal the value): stored override / env value / unset. const secretStatus = item.is_set ? t("config.secretSet") : item.default_is_set ? t("config.secretEnv") : t("config.secretUnset"); const canSave = item.secret ? draft.trim().length > 0 && !secretDisabled : true; return (
{t(`config.fields.${item.key}.label`, item.key)}

{t(`config.fields.${item.key}.hint`, "")}

{item.secret ? (

{secretDisabled ? t("config.secretsDisabled") : secretStatus}

) : (

{item.is_set ? t("config.overridden") : t("config.usingDefault")}

)}
setDraft(e.target.value)} min={isNum && item.min != null ? item.min : undefined} max={isNum && item.max != null ? item.max : undefined} placeholder={item.secret ? "••••••••" : String(item.default ?? "")} className="w-52 bg-card border border-border rounded-xl px-3 py-1.5 text-sm outline-none focus:border-accent disabled:opacity-50" />
{item.is_set && ( )}
); }