'use client'; import { ChangeEvent, Dispatch, SetStateAction, useCallback, useEffect, useState, } from 'react'; import Modal from '@/components/ui/Modal'; import { PlusIcon } from '@heroicons/react/24/outline'; import TagMultiCombobox, { TagOption } from '@/components/ui/TagMultiCombobox'; import Button from '@/components/ui/Button'; import ButtonGroup from '@/components/ui/ButtonGroup'; // 🔹 NEU import AppCombobox from '@/components/ui/Combobox'; // ⬅️ NEU import type { DeviceDetail } from './page'; type DeviceCreateModalProps = { open: boolean; onClose: () => void; onCreated: (device: DeviceDetail) => void; allTags: TagOption[]; setAllTags: Dispatch>; }; type NewDevice = { inventoryNumber: string; name: string; manufacturer: string; model: string; serialNumber: string | null; productNumber: string | null; comment: string | null; group: string | null; location: string | null; ipv4Address: string | null; ipv6Address: string | null; macAddress: string | null; username: string | null; passwordHash: string | null; tags: string[]; // wenn gesetzt → Gerät ist Zubehör parentInventoryNumber: string | null; }; const emptyDevice: NewDevice = { inventoryNumber: '', name: '', manufacturer: '', model: '', serialNumber: null, productNumber: null, comment: null, group: null, location: null, ipv4Address: null, ipv6Address: null, macAddress: null, username: null, passwordHash: null, tags: [], parentInventoryNumber: null, }; type DeviceOption = { inventoryNumber: string; name: string; parentInventoryNumber?: string | null; group?: string | null; location?: string | null; }; export default function DeviceCreateModal({ open, onClose, onCreated, allTags, setAllTags, }: DeviceCreateModalProps) { const [form, setForm] = useState(emptyDevice); const [saveLoading, setSaveLoading] = useState(false); const [error, setError] = useState(null); // 🔹 State für Gerätetyp (Hauptgerät / Zubehör) const [deviceType, setDeviceType] = useState<'main' | 'accessory'>('main'); // Optionen für Hauptgeräte (aus /api/devices) const [deviceOptions, setDeviceOptions] = useState([]); const [optionsLoading, setOptionsLoading] = useState(false); const [optionsError, setOptionsError] = useState(null); const [parentSearch, setParentSearch] = useState(''); // Formular & Typ resetten, wenn Modal neu geöffnet wird useEffect(() => { if (open) { setForm(emptyDevice); setError(null); setSaveLoading(false); setDeviceType('main'); setParentSearch(''); } }, [open]); // Geräteliste laden (für Hauptgeräte-Auswahl) useEffect(() => { if (!open) return; let cancelled = false; setOptionsLoading(true); setOptionsError(null); async function loadDevices() { try { const res = await fetch('/api/devices', { method: 'GET', headers: { 'Content-Type': 'application/json' }, cache: 'no-store', }); if (!res.ok) { throw new Error('Geräteliste konnte nicht geladen werden.'); } const data = (await res.json()) as DeviceOption[]; if (!cancelled) { setDeviceOptions(data); } } catch (err: any) { console.error('Error loading device options', err); if (!cancelled) { setOptionsError( err instanceof Error ? err.message : 'Netzwerkfehler beim Laden der Geräteliste.', ); } } finally { if (!cancelled) { setOptionsLoading(false); } } } loadDevices(); return () => { cancelled = true; }; }, [open]); const handleFieldChange = ( field: keyof NewDevice, e: ChangeEvent, ) => { const value = e.target.value; setForm((prev) => ({ ...prev, [field]: value === '' && prev[field] === null ? null : value, })); }; const handleSave = useCallback(async () => { if (!form.inventoryNumber.trim() || !form.name.trim()) { setError('Bitte mindestens Inventar-Nr. und Bezeichnung ausfüllen.'); return; } setSaveLoading(true); setError(null); try { const res = await fetch('/api/devices', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ inventoryNumber: form.inventoryNumber.trim(), name: form.name.trim(), manufacturer: form.manufacturer || '', model: form.model || '', serialNumber: form.serialNumber || null, productNumber: form.productNumber || null, comment: form.comment || null, group: form.group || null, location: form.location || null, ipv4Address: form.ipv4Address || null, ipv6Address: form.ipv6Address || null, macAddress: form.macAddress || null, username: form.username || null, passwordHash: form.passwordHash || null, tags: form.tags ?? [], parentInventoryNumber: form.parentInventoryNumber?.trim() || null, }), }); if (!res.ok) { if (res.status === 409) { throw new Error( 'Es existiert bereits ein Gerät mit dieser Inventar-Nr.', ); } throw new Error('Anlegen des Geräts ist fehlgeschlagen.'); } const created = (await res.json()) as DeviceDetail; onCreated(created); onClose(); } catch (err: any) { console.error('Error creating device', err); setError( err instanceof Error ? err.message : 'Netzwerkfehler beim Anlegen des Geräts.', ); } finally { setSaveLoading(false); } }, [form, onCreated, onClose]); const handleClose = () => { if (saveLoading) return; onClose(); }; // 🔹 Gerätetyp ab jetzt über deviceType const isAccessory = deviceType === 'accessory'; // Nur Hauptgeräte (kein parentInventoryNumber) const mainDevices = deviceOptions.filter( (d) => !d.parentInventoryNumber, ); // 🔹 Filter nach Suchtext (Inventar-Nr. ODER Name) const filteredMainDevices = parentSearch.trim().length === 0 ? mainDevices : mainDevices.filter((d) => { const q = parentSearch.toLowerCase(); return ( d.inventoryNumber.toLowerCase().includes(q) || d.name.toLowerCase().includes(q) ); }); const selectedMainDevice = form.parentInventoryNumber && mainDevices.length > 0 ? mainDevices.find( (d) => d.inventoryNumber === form.parentInventoryNumber, ) ?? null : null; const mainDeviceLabel = selectedMainDevice ? `${selectedMainDevice.inventoryNumber} – ${selectedMainDevice.name}` : optionsLoading ? 'Hauptgerät wird geladen …' : mainDevices.length > 0 ? 'Hauptgerät auswählen …' : 'Keine Hauptgeräte vorhanden'; // 🔹 Prefix für Zubehör-Inventarnummer (Hauptgerät-Nummer + "-") const accessoryPrefix = isAccessory && form.parentInventoryNumber ? `${form.parentInventoryNumber}-` : ''; // 🔹 Was im editierbaren Feld steht (nur der Teil NACH dem Prefix) const inventorySuffix = accessoryPrefix && form.inventoryNumber.startsWith(accessoryPrefix) ? form.inventoryNumber.slice(accessoryPrefix.length) : form.inventoryNumber; const handleInventorySuffixChange = ( e: ChangeEvent, ) => { const suffix = e.target.value; setForm((prev) => { const prefix = isAccessory && prev.parentInventoryNumber ? `${prev.parentInventoryNumber}-` : ''; return { ...prev, inventoryNumber: prefix ? `${prefix}${suffix}` : suffix, }; }); }; return ( } tone="info" variant="centered" size="md" footer={
} > {error && (

{error}

)}
{/* 🔹 Block: Gerätetyp & Hauptgerät-Auswahl */}

Gerätetyp

{/* 🔹 Hier deine ButtonGroup */}
{ const value = next as 'main' | 'accessory'; setDeviceType(value); if (value === 'main') { setForm((prev) => ({ ...prev, parentInventoryNumber: null, inventoryNumber: '', location: '', group: '', })); setParentSearch(''); // Suche leeren } else { setForm((prev) => ({ ...prev, // initial leeren String erlauben, bis ein Hauptgerät gewählt ist parentInventoryNumber: prev.parentInventoryNumber ?? '', })); // optional: setParentSearch(''); } }} />
{isAccessory && (

Hauptgerät auswählen

// Label im Feld selbst, Ăśberschrift kommt aus dem

darüber label={undefined} options={mainDevices} value={selectedMainDevice} onChange={(selected) => { setForm((prev) => { // Falls irgendwie auf "keine Auswahl" gesetzt würde if (!selected) { return { ...prev, parentInventoryNumber: null, }; } const prefix = `${selected.inventoryNumber}-`; const nextInventoryNumber = !prev.inventoryNumber || !prev.inventoryNumber.startsWith(prefix) ? prefix : prev.inventoryNumber; return { ...prev, parentInventoryNumber: selected.inventoryNumber, inventoryNumber: nextInventoryNumber, // 👇 Standort / Raum & Gruppe vom Hauptgerät übernehmen, // aber nur, wenn das Hauptgerät dort Werte hat location: selected.location ?? prev.location, group: selected.group ?? prev.group, }; }); }} getKey={(d) => d.inventoryNumber} getPrimaryLabel={(d) => `${d.inventoryNumber} – ${d.name}`} getSecondaryLabel={(d) => { const parts = [d.location, d.group].filter(Boolean); return parts.length ? parts.join(' · ') : null; }} placeholder={ optionsLoading ? 'Hauptgerät wird geladen …' : mainDevices.length > 0 ? 'Hauptgerät auswählen …' : 'Keine Hauptgeräte vorhanden' } allowCreateFromQuery={false} /> {optionsLoading && (

Geräteliste wird geladen …

)} {optionsError && (

{optionsError}

)} {!optionsLoading && !optionsError && (

Nur Geräte ohne eigenes Hauptgerät werden als mögliche Hauptgeräte angezeigt. Wähle „Eigenständiges Hauptgerät“, wenn dieses Gerät kein Zubehör ist.

)}
)}
{/* Inventarnummer */}

Inventar-Nr. *

{/* Zubehör mit Hauptgerät → Prefix fix anzeigen */} {isAccessory && form.parentInventoryNumber ? (
{/* Unveränderbarer Prefix, dezent dargestellt */} {accessoryPrefix} {/* Editierbarer Suffix */}
) : ( // Normaler Modus (Hauptgerät oder noch kein Hauptgerät gewählt) handleFieldChange('inventoryNumber', e)} /> )}
{/* Bezeichnung */}

Bezeichnung *

handleFieldChange('name', e)} />
{/* Hersteller / Modell */}

Hersteller

handleFieldChange('manufacturer', e)} />

Modell

handleFieldChange('model', e)} />
{/* Seriennummer / Produktnummer */}

Seriennummer

handleFieldChange('serialNumber', e)} />

Produktnummer

handleFieldChange('productNumber', e)} />
{/* Standort / Gruppe */}

Standort / Raum

handleFieldChange('location', e)} />

Gruppe

handleFieldChange('group', e)} />
{/* Tags */}
({ name }))} onChange={(next) => { const names = next.map((t) => t.name); setForm((prev) => ({ ...prev, tags: names, })); setAllTags((prev) => { const map = new Map( prev.map((t) => [t.name.toLowerCase(), t]), ); for (const t of next) { const key = t.name.toLowerCase(); if (!map.has(key)) { map.set(key, t); } } return Array.from(map.values()); }); }} placeholder="z.B. Dockingstation, Monitor, kritisch" />
{/* Netzwerkdaten */}

IPv4-Adresse

handleFieldChange('ipv4Address', e)} />

IPv6-Adresse

handleFieldChange('ipv6Address', e)} />

MAC-Adresse

handleFieldChange('macAddress', e)} />
{/* Zugangsdaten */}

Benutzername

handleFieldChange('username', e)} />

Passwort

handleFieldChange('passwordHash', e)} />
{/* Kommentar */}

Kommentar