import { useState, useEffect } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { Save, Plus, Trash2, ChevronUp, ChevronDown, GripVertical, Eye, EyeOff, ToggleLeft, ToggleRight, } from 'lucide-react' import api from '../../api/client' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- type Visibility = 'required' | 'optional' | 'hidden' interface StdField { key: string label: string visibility: Visibility } interface CompPair { component_type: string required: boolean } interface Template { id: string name: string category_key: string description?: string | null is_active: boolean standard_fields: any component_schema: any } // --------------------------------------------------------------------------- // All canonical standard field definitions (maps to DB columns in order_items) // --------------------------------------------------------------------------- const ALL_FIELD_DEFS: { key: string; defaultLabel: string }[] = [ { key: 'ebene1', defaultLabel: 'Ebene 1' }, { key: 'ebene2', defaultLabel: 'Ebene 2' }, { key: 'baureihe', defaultLabel: 'Baureihe' }, { key: 'pim_id', defaultLabel: 'PIM-ID' }, { key: 'produkt_baureihe', defaultLabel: 'Produkt / Baureihe' }, { key: 'gewaehltes_produkt', defaultLabel: 'Gewähltes Produkt' }, { key: 'name_cad_modell', defaultLabel: 'Name CAD-Modell' }, { key: 'gewuenschte_bildnummer',defaultLabel: 'Gewünschte Bildnummer' }, { key: 'lagertyp', defaultLabel: 'Lagertyp' }, { key: 'medias_rendering', defaultLabel: 'Medias Rendering' }, ] // --------------------------------------------------------------------------- // Normalisation helpers // --------------------------------------------------------------------------- function normalizeFields(raw: any): StdField[] { // New array format: [{key, label, visibility}] if (Array.isArray(raw) && raw.length > 0 && raw[0].key) { const existing = new Map(raw.map((f: StdField) => [f.key, f])) // Preserve saved order, then append any missing canonical fields const ordered: StdField[] = raw.filter((f: StdField) => ALL_FIELD_DEFS.some((d) => d.key === f.key), ) ALL_FIELD_DEFS.forEach(({ key, defaultLabel }) => { if (!existing.has(key)) { ordered.push({ key, label: defaultLabel, visibility: 'optional' }) } }) return ordered } // Legacy dict format {"0": {label, required}} or empty — use canonical defaults return ALL_FIELD_DEFS.map(({ key, defaultLabel }) => ({ key, label: defaultLabel, visibility: 'optional' as Visibility, })) } function normalizePairs(raw: any): CompPair[] { if (!raw) return [] if (Array.isArray(raw.pairs)) return raw.pairs.map((p: any) => ({ component_type: p.component_type ?? p.part_name ?? '', required: p.required ?? false, })) if (Array.isArray(raw)) return raw.map((p: any) => ({ component_type: p.component_type ?? p.part_name ?? '', required: p.required ?? false, })) return [] } // --------------------------------------------------------------------------- // Small helpers // --------------------------------------------------------------------------- function moveItem(arr: T[], from: number, to: number): T[] { const next = [...arr] const [item] = next.splice(from, 1) next.splice(to, 0, item) return next } const VIS_STYLES: Record = { required: 'bg-accent text-white', optional: 'bg-blue-500 text-white', hidden: 'bg-surface-muted text-content-secondary', } function VisibilityToggle({ value, onChange, }: { value: Visibility onChange: (v: Visibility) => void }) { const cycle: Visibility[] = ['required', 'optional', 'hidden'] const labels: Record = { required: 'Required', optional: 'Optional', hidden: 'Hidden' } return ( ) } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export default function TemplateEditor({ template, onClose, }: { template: Template onClose: () => void }) { const qc = useQueryClient() const [name, setName] = useState(template.name) const [description, setDescription] = useState(template.description ?? '') const [isActive, setIsActive] = useState(template.is_active) const [fields, setFields] = useState(() => normalizeFields(template.standard_fields)) const [pairs, setPairs] = useState(() => normalizePairs(template.component_schema)) const [showHidden, setShowHidden] = useState(false) const [newFieldKey, setNewFieldKey] = useState('') useEffect(() => { setName(template.name) setDescription(template.description ?? '') setIsActive(template.is_active) setFields(normalizeFields(template.standard_fields)) setPairs(normalizePairs(template.component_schema)) }, [template.id]) // eslint-disable-line react-hooks/exhaustive-deps const saveMut = useMutation({ mutationFn: () => api.patch(`/templates/${template.id}`, { name, description: description || null, is_active: isActive, standard_fields: fields, component_schema: { pairs }, }), onSuccess: () => { toast.success('Template saved') qc.invalidateQueries({ queryKey: ['admin-templates'] }) qc.invalidateQueries({ queryKey: ['templates'] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to save'), }) // ---- Standard fields helpers ---- function updateField(i: number, patch: Partial) { setFields((f) => f.map((x, j) => (j === i ? { ...x, ...patch } : x))) } function removeField(i: number) { // Don't delete — mark hidden so it stays in DB but hidden in UI updateField(i, { visibility: 'hidden' }) } const hiddenKeys = new Set(fields.filter((f) => f.visibility === 'hidden').map((f) => f.key)) const availableToAdd = ALL_FIELD_DEFS.filter((d) => hiddenKeys.has(d.key)) function restoreField(key: string) { setFields((f) => f.map((x) => (x.key === key ? { ...x, visibility: 'optional' } : x)), ) setNewFieldKey('') } // ---- Component pair helpers ---- function updatePair(i: number, patch: Partial) { setPairs((p) => p.map((x, j) => (j === i ? { ...x, ...patch } : x))) } function addPair() { setPairs((p) => [...p, { component_type: '', required: false }]) } function removePair(i: number) { setPairs((p) => p.filter((_, j) => j !== i)) } // ---- Visible fields for rendering ---- const visibleFields = showHidden ? fields : fields.filter((f) => f.visibility !== 'hidden') // ---- Shared styles ---- const ROW = 'flex items-center gap-2 px-3 py-2 rounded-lg border border-border-light bg-surface-alt group' const ICON_BTN = 'p-1 rounded text-content-muted hover:text-content-secondary hover:bg-surface transition-colors disabled:opacity-30' const INPUT = 'flex-1 min-w-0 text-sm bg-transparent border-b border-transparent focus:border-accent focus:outline-none py-0.5 text-content' return (
{/* ------------------------------------------------------------------ */} {/* Header */} {/* ------------------------------------------------------------------ */}
{/* Editable name */} setName(e.target.value)} className="text-base font-semibold text-content bg-transparent border-b border-transparent focus:border-accent focus:outline-none w-full" placeholder="Template name" /> {/* Category key (read-only) + active toggle */}
{template.category_key}
{/* Description */} setDescription(e.target.value)} className="text-xs text-content-muted bg-transparent border-b border-transparent focus:border-accent focus:outline-none w-full" placeholder="Description (optional)" />
{/* ---------------------------------------------------------------- */} {/* Standard Fields */} {/* ---------------------------------------------------------------- */}

Standard Fields

Rename, reorder, and set visibility for each column. Hidden fields are excluded from forms.

{visibleFields.map((field, i) => { // Real index in fields array (needed for moveItem / updateField) const realIdx = fields.indexOf(field) const isHidden = field.visibility === 'hidden' return (
{/* Reorder */}
{/* Label */} updateField(realIdx, { label: e.target.value })} className={INPUT} placeholder="Field label" /> {/* Key badge */} {field.key} {/* Visibility */} updateField(realIdx, { visibility: v })} /> {/* Hide button */}
) })}
{/* Restore hidden field */} {availableToAdd.length > 0 && (
{newFieldKey && ( )}
)}
{/* ---------------------------------------------------------------- */} {/* Component Schema */} {/* ---------------------------------------------------------------- */}

Component Schema

Define the expected component types that appear as column pairs in the Excel file (cols 11+).

{pairs.length === 0 && (

No component types defined.

)} {pairs.map((pair, i) => (
{/* Reorder */}
{/* Index badge */} {i + 1} {/* Component type name */} updatePair(i, { component_type: e.target.value })} placeholder="Component type name" className={INPUT} /> {/* Required toggle */} {/* Delete */}
))}
) }