"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useI18n } from "@/lib/i18n/useI18n"; type CatalogKind = "downtime" | "scrap"; type ApiItem = { id: string; name: string; codeSuffix: string; reasonCode: string; sortOrder: number; active: boolean; }; type ApiCategory = { id: string; kind: string; name: string; codePrefix: string; sortOrder: number; active: boolean; items: ApiItem[]; }; const PREFIX_RE = /^[A-Za-z][A-Za-z0-9-]*$/; /** Matches composeReasonCode in reasonCatalogDb (client-safe). */ function formatPrintedPreview(prefix: string, digits: string): string { const p = String(prefix).trim().toUpperCase(); const d = String(digits).trim(); if (!d) return p.length >= 3 ? `${p}-…` : `${p}…`; if (/^\d+$/.test(d) && p.length >= 3) return `${p}-${d}`; return `${p}${d}`; } async function readJson(res: Response) { const data = await res.json().catch(() => null); return data as Record | null; } export function ReasonCatalogConfig({ disabled }: { disabled?: boolean }) { const { t } = useI18n(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [catalogVersion, setCatalogVersion] = useState(1); const [categories, setCategories] = useState([]); const [kind, setKind] = useState("downtime"); const [selectedCategoryId, setSelectedCategoryId] = useState(null); const [newCatName, setNewCatName] = useState(""); const [newCatPrefix, setNewCatPrefix] = useState(""); const [newDigits, setNewDigits] = useState(""); const [newItemName, setNewItemName] = useState(""); const [busy, setBusy] = useState(false); const [editCatName, setEditCatName] = useState(""); const [editCatPrefix, setEditCatPrefix] = useState(""); const load = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch("/api/settings/reason-catalog"); const data = await readJson(res); if (!res.ok || !data || data.ok !== true) { const msg = typeof data?.error === "string" ? data.error : "Load failed"; throw new Error(msg); } setCatalogVersion(Number(data.catalogVersion ?? 1)); setCategories(Array.isArray(data.categories) ? (data.categories as ApiCategory[]) : []); } catch (e) { setError(e instanceof Error ? e.message : "Load failed"); } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); const forKind = useMemo( () => categories.filter((c) => String(c.kind).toLowerCase() === kind), [categories, kind] ); const selected = useMemo( () => forKind.find((c) => c.id === selectedCategoryId) ?? null, [forKind, selectedCategoryId] ); useEffect(() => { if (!selected) { setEditCatName(""); setEditCatPrefix(""); return; } setEditCatName(selected.name); setEditCatPrefix(selected.codePrefix); }, [selected?.id, selected?.name, selected?.codePrefix]); useEffect(() => { if (!forKind.length) { setSelectedCategoryId(null); return; } if (!selectedCategoryId || !forKind.some((c) => c.id === selectedCategoryId)) { setSelectedCategoryId(forKind[0]?.id ?? null); } }, [forKind, selectedCategoryId]); const onDigitsChange = (raw: string) => { setNewDigits(raw.replace(/\D/g, "")); }; const createCategory = async () => { const name = newCatName.trim(); const codePrefix = newCatPrefix.trim().toUpperCase(); if (!name || !codePrefix) return; if (!PREFIX_RE.test(codePrefix)) { setError(t("settings.reasonCatalog.prefixInvalid")); return; } setBusy(true); setError(null); try { const res = await fetch("/api/settings/reason-catalog/categories", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind, name, codePrefix }), }); const data = await readJson(res); if (!res.ok || !data || data.ok !== true) { const msg = typeof data?.error === "string" ? data.error : "Create failed"; throw new Error(msg); } setNewCatName(""); setNewCatPrefix(""); await load(); const cat = data.category as { id?: string } | undefined; if (cat?.id) setSelectedCategoryId(cat.id); } catch (e) { setError(e instanceof Error ? e.message : "Create failed"); } finally { setBusy(false); } }; const addItem = async () => { if (!selected) return; const digits = newDigits.trim(); const name = newItemName.trim(); if (!digits || !name) return; setBusy(true); setError(null); try { const res = await fetch("/api/settings/reason-catalog/items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ categoryId: selected.id, codeSuffix: digits, name }), }); const data = await readJson(res); if (!res.ok || !data || data.ok !== true) { const msg = typeof data?.error === "string" ? data.error : "Create failed"; throw new Error(msg); } setNewDigits(""); setNewItemName(""); await load(); } catch (e) { setError(e instanceof Error ? e.message : "Create failed"); } finally { setBusy(false); } }; const patchItem = async (itemId: string, patch: Record) => { setBusy(true); setError(null); try { const res = await fetch(`/api/settings/reason-catalog/items/${itemId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch), }); const data = await readJson(res); if (!res.ok || !data || data.ok !== true) { const msg = typeof data?.error === "string" ? data.error : "Update failed"; throw new Error(msg); } await load(); } catch (e) { setError(e instanceof Error ? e.message : "Update failed"); } finally { setBusy(false); } }; const patchCategory = async (categoryId: string, patch: Record) => { setBusy(true); setError(null); try { const res = await fetch(`/api/settings/reason-catalog/categories/${categoryId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch), }); const data = await readJson(res); if (!res.ok || !data || data.ok !== true) { const msg = typeof data?.error === "string" ? data.error : "Update failed"; throw new Error(msg); } await load(); } catch (e) { setError(e instanceof Error ? e.message : "Update failed"); } finally { setBusy(false); } }; const inputCls = "mt-1 w-full rounded-lg border border-white/10 bg-black/30 px-2 py-1.5 text-xs text-white placeholder:text-zinc-600"; const kindBtn = (k: CatalogKind, label: string) => ( ); return (
{t("settings.reasonCatalog.dbVersionHint", { version: catalogVersion })}
{loading ?

{t("settings.loading")}

: null} {error ? (

{error}

) : null}
{t("settings.reasonCatalog.stepKind")}
{kindBtn("downtime", t("settings.reasonCatalog.downtime"))} {kindBtn("scrap", t("settings.reasonCatalog.scrap"))}
{t("settings.reasonCatalog.stepCategory")}
{selected ? (
) : null}
{t("settings.reasonCatalog.newCategorySection")}
{selected ? (
{t("settings.reasonCatalog.stepReason")}

{t("settings.reasonCatalog.digitsOnlyHint")}

{t("settings.reasonCatalog.fullCodePreview")} {formatPrintedPreview(selected.codePrefix, newDigits)}
{t("settings.reasonCatalog.reasonsInCategory")}
{selected.items.length === 0 ? (
{t("settings.reasonCatalog.noItemsYet")}
) : ( selected.items.map((it) => (
{it.reasonCode}
{it.name}
)) )}
) : null}

{t("settings.reasonCatalog.hint")}

); }