import { useEffect, useState, type FormEvent } from "react"; import { get, post, put, del } from "../api/client"; import { useAuth } from "../hooks/useAuth"; import type { Schema, SchemaSegment } from "../api/types"; interface EnumEditState { schemaName: string; segmentName: string; code: string; description: string; mode: "add" | "edit" | "delete"; } export function SchemasPage() { const { user } = useAuth(); const isEditor = user?.role === "admin" || user?.role === "editor"; const [schemas, setSchemas] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [expanded, setExpanded] = useState>(new Set()); const [editState, setEditState] = useState(null); const [formError, setFormError] = useState(""); const [submitting, setSubmitting] = useState(false); const loadSchemas = async () => { try { const list = await get("/api/schemas"); setSchemas(list.filter((s) => s.name)); setError(null); } catch (e) { setError(e instanceof Error ? e.message : "Failed to load schemas"); } finally { setLoading(false); } }; useEffect(() => { loadSchemas(); }, []); const toggleExpand = (key: string) => { setExpanded((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); }; const startAdd = (schemaName: string, segmentName: string) => { setEditState({ schemaName, segmentName, code: "", description: "", mode: "add", }); setFormError(""); }; const startEdit = ( schemaName: string, segmentName: string, code: string, description: string, ) => { setEditState({ schemaName, segmentName, code, description, mode: "edit" }); setFormError(""); }; const startDelete = ( schemaName: string, segmentName: string, code: string, ) => { setEditState({ schemaName, segmentName, code, description: "", mode: "delete", }); setFormError(""); }; const cancelEdit = () => { setEditState(null); setFormError(""); }; const handleAddValue = async (e: FormEvent) => { e.preventDefault(); if (!editState) return; setFormError(""); setSubmitting(true); try { await post( `/api/schemas/${editState.schemaName}/segments/${editState.segmentName}/values`, { code: editState.code, description: editState.description }, ); cancelEdit(); await loadSchemas(); } catch (e) { setFormError(e instanceof Error ? e.message : "Failed to add value"); } finally { setSubmitting(false); } }; const handleUpdateValue = async (e: FormEvent) => { e.preventDefault(); if (!editState) return; setFormError(""); setSubmitting(true); try { await put( `/api/schemas/${editState.schemaName}/segments/${editState.segmentName}/values/${editState.code}`, { description: editState.description }, ); cancelEdit(); await loadSchemas(); } catch (e) { setFormError(e instanceof Error ? e.message : "Failed to update value"); } finally { setSubmitting(false); } }; const handleDeleteValue = async () => { if (!editState) return; setSubmitting(true); try { await del( `/api/schemas/${editState.schemaName}/segments/${editState.segmentName}/values/${editState.code}`, ); cancelEdit(); await loadSchemas(); } catch (e) { setFormError(e instanceof Error ? e.message : "Failed to delete value"); } finally { setSubmitting(false); } }; if (loading) return

Loading schemas...

; if (error) return

Error: {error}

; return (

Part Numbering Schemas ({schemas.length})

{schemas.length === 0 ? (
No schemas found.
) : ( schemas.map((schema) => ( )) )}
); } // --- Sub-components (local to this file) --- interface SchemaCardProps { schema: Schema; expanded: Set; toggleExpand: (key: string) => void; isEditor: boolean; editState: EnumEditState | null; formError: string; submitting: boolean; onStartAdd: (schemaName: string, segmentName: string) => void; onStartEdit: ( schemaName: string, segmentName: string, code: string, desc: string, ) => void; onStartDelete: ( schemaName: string, segmentName: string, code: string, ) => void; onCancelEdit: () => void; onAdd: (e: FormEvent) => void; onUpdate: (e: FormEvent) => void; onDelete: () => void; onEditStateChange: (state: EnumEditState | null) => void; } function SchemaCard({ schema, expanded, toggleExpand, isEditor, editState, formError, submitting, onStartAdd, onStartEdit, onStartDelete, onCancelEdit, onAdd, onUpdate, onDelete, onEditStateChange, }: SchemaCardProps) { const segKey = `seg-${schema.name}`; const isExpanded = expanded.has(segKey); return (

{schema.name}

{schema.description && (

{schema.description}

)}

Format: {schema.format}

Version: {schema.version}

{schema.examples && schema.examples.length > 0 && ( <>

Examples:

{schema.examples.map((ex) => ( {ex} ))}
)}
toggleExpand(segKey)} style={{ cursor: "pointer", color: "var(--ctp-sapphire)", userSelect: "none", marginTop: "1rem", }} > {isExpanded ? "\u25BC" : "\u25B6"} View Segments ( {schema.segments.length})
{isExpanded && schema.segments.map((seg) => ( ))}
); } interface SegmentBlockProps { schemaName: string; segment: SchemaSegment; isEditor: boolean; editState: EnumEditState | null; formError: string; submitting: boolean; onStartAdd: (schemaName: string, segmentName: string) => void; onStartEdit: ( schemaName: string, segmentName: string, code: string, desc: string, ) => void; onStartDelete: ( schemaName: string, segmentName: string, code: string, ) => void; onCancelEdit: () => void; onAdd: (e: FormEvent) => void; onUpdate: (e: FormEvent) => void; onDelete: () => void; onEditStateChange: (state: EnumEditState | null) => void; } function SegmentBlock({ schemaName, segment, isEditor, editState, formError, submitting, onStartAdd, onStartEdit, onStartDelete, onCancelEdit, onAdd, onUpdate, onDelete, onEditStateChange, }: SegmentBlockProps) { const isThisSegment = (es: EnumEditState | null) => es !== null && es.schemaName === schemaName && es.segmentName === segment.name; const entries = segment.values ? Object.entries(segment.values).sort((a, b) => a[0].localeCompare(b[0])) : []; return (

{segment.name}

{segment.type}
{segment.description && (

{segment.description}

)} {segment.type === "enum" && entries.length > 0 && (
{isEditor && ( )} {entries.map(([code, desc]) => { const isEditingThis = isThisSegment(editState) && editState!.code === code && editState!.mode === "edit"; const isDeletingThis = isThisSegment(editState) && editState!.code === code && editState!.mode === "delete"; if (isEditingThis) { return ( {isEditor && ); } if (isDeletingThis) { return ( ); } return ( {isEditor && ( )} ); })} {/* Add row */} {isThisSegment(editState) && editState!.mode === "add" && ( {isEditor && )}
Code DescriptionActions
{code}
onEditStateChange({ ...editState!, description: e.target.value, }) } required style={inlineInputStyle} autoFocus />
{formError && (
{formError}
)}
}
{code} Delete this value? {formError && (
{formError}
)}
{code} {desc}
onEditStateChange({ ...editState!, code: e.target.value, }) } placeholder="Code" required style={inlineInputStyle} autoFocus />
onEditStateChange({ ...editState!, description: e.target.value, }) } placeholder="Description" required style={inlineInputStyle} />
{formError && (
{formError}
)}
}
)} {segment.type === "enum" && isEditor && !(isThisSegment(editState) && editState!.mode === "add") && ( )}
); } // --- Styles --- const cardStyle: React.CSSProperties = { backgroundColor: "var(--ctp-surface0)", borderRadius: "0.75rem", padding: "1.25rem", marginBottom: "1rem", }; const codeStyle: React.CSSProperties = { background: "var(--ctp-surface1)", padding: "0.25rem 0.5rem", borderRadius: "0.25rem", fontSize: "0.85rem", }; const segmentStyle: React.CSSProperties = { marginTop: "1rem", padding: "1rem", background: "var(--ctp-base)", borderRadius: "0.5rem", }; const typeBadgeStyle: React.CSSProperties = { display: "inline-block", padding: "0.15rem 0.5rem", borderRadius: "0.25rem", fontSize: "0.75rem", fontWeight: 600, backgroundColor: "rgba(166, 227, 161, 0.15)", color: "var(--ctp-green)", }; const emptyStyle: React.CSSProperties = { textAlign: "center", padding: "2rem", color: "var(--ctp-subtext0)", }; const thStyle: React.CSSProperties = { padding: "0.4rem 0.75rem", textAlign: "left", borderBottom: "1px solid var(--ctp-surface1)", color: "var(--ctp-subtext1)", fontWeight: 600, fontSize: "0.8rem", textTransform: "uppercase", letterSpacing: "0.05em", }; const tdStyle: React.CSSProperties = { padding: "0.3rem 0.75rem", borderBottom: "1px solid var(--ctp-surface1)", fontSize: "0.85rem", }; const btnTinyStyle: React.CSSProperties = { padding: "0.2rem 0.5rem", borderRadius: "0.25rem", border: "none", backgroundColor: "var(--ctp-surface1)", color: "var(--ctp-text)", fontSize: "0.75rem", cursor: "pointer", }; const btnTinyPrimaryStyle: React.CSSProperties = { padding: "0.2rem 0.5rem", borderRadius: "0.25rem", border: "none", backgroundColor: "var(--ctp-mauve)", color: "var(--ctp-crust)", fontSize: "0.75rem", fontWeight: 600, cursor: "pointer", }; const inlineInputStyle: React.CSSProperties = { padding: "0.25rem 0.5rem", backgroundColor: "var(--ctp-surface0)", border: "1px solid var(--ctp-surface1)", borderRadius: "0.25rem", color: "var(--ctp-text)", fontSize: "0.85rem", width: "100%", boxSizing: "border-box", };