'use client'; import { useCallback, useEffect, useState, ChangeEvent, Dispatch, SetStateAction, } from 'react'; import Modal from '@/components/ui/Modal'; import { PencilIcon, CheckCircleIcon } from '@heroicons/react/24/solid'; import DeviceHistorySidebar from './DeviceHistorySidebar'; import TagMultiCombobox, { TagOption } from '@/components/ui/TagMultiCombobox'; import Button from '@/components/ui/Button'; import Tabs from '@/components/ui/Tabs'; import type { DeviceDetail } from './page'; import { Dropdown } from '@/components/ui/Dropdown'; import AppCombobox from '@/components/ui/Combobox'; type DeviceEditModalProps = { open: boolean; inventoryNumber: string | null; onClose: () => void; onSaved: (device: DeviceDetail) => void; allTags: TagOption[]; setAllTags: Dispatch>; }; type DeviceOption = { inventoryNumber: string; name: string; }; export default function DeviceEditModal({ open, inventoryNumber, onClose, onSaved, allTags, setAllTags, }: DeviceEditModalProps) { const [editDevice, setEditDevice] = useState(null); const [editLoading, setEditLoading] = useState(false); const [editError, setEditError] = useState(null); const [saveLoading, setSaveLoading] = useState(false); const [justSaved, setJustSaved] = useState(false); const [historyRefresh, setHistoryRefresh] = useState(0); const [deviceOptions, setDeviceOptions] = useState([]); const [optionsLoading, setOptionsLoading] = useState(false); const [optionsError, setOptionsError] = useState(null); // 👇 NEU: Tabs im Edit-Modal const [activeTab, setActiveTab] = useState<'fields' | 'relations'>('fields'); useEffect(() => { if (!open || !inventoryNumber) return; const inv = inventoryNumber; let cancelled = false; setEditLoading(true); setEditError(null); setJustSaved(false); setEditDevice(null); async function loadDevice() { try { const res = await fetch( `/api/devices/${encodeURIComponent(inv)}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, cache: 'no-store', }, ); if (!res.ok) { if (res.status === 404) { throw new Error('Gerät wurde nicht gefunden.'); } throw new Error( 'Beim Laden der Gerätedaten ist ein Fehler aufgetreten.', ); } const data = (await res.json()) as DeviceDetail; if (!cancelled) { setEditDevice(data); } } catch (err: any) { console.error('Error loading device', err); if (!cancelled) { setEditError( err instanceof Error ? err.message : 'Netzwerkfehler beim Laden der Gerätedaten.', ); } } finally { if (!cancelled) { setEditLoading(false); } } } loadDevice(); return () => { cancelled = true; }; }, [open, inventoryNumber]); useEffect(() => { if (!justSaved) return; const id = setTimeout(() => { setJustSaved(false); }, 1500); return () => clearTimeout(id); }, [justSaved]); useEffect(() => { if (!open) return; let cancelled = false; setOptionsLoading(true); setOptionsError(null); async function loadDeviceOptions() { 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 { inventoryNumber: string; name: string; }[]; 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); } } } loadDeviceOptions(); return () => { cancelled = true; }; }, [open]); const handleFieldChange = ( field: keyof DeviceDetail, e: ChangeEvent, ) => { const value = e.target.value; setEditDevice((prev) => prev ? ({ ...prev, [field]: value } as DeviceDetail) : prev, ); }; const handleSave = useCallback(async () => { if (!editDevice) return; setSaveLoading(true); setEditError(null); try { const res = await fetch( `/api/devices/${encodeURIComponent( editDevice.inventoryNumber, )}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: editDevice.name, manufacturer: editDevice.manufacturer, model: editDevice.model, serialNumber: editDevice.serialNumber || null, productNumber: editDevice.productNumber || null, comment: editDevice.comment || null, group: editDevice.group || null, location: editDevice.location || null, ipv4Address: editDevice.ipv4Address || null, ipv6Address: editDevice.ipv6Address || null, macAddress: editDevice.macAddress || null, username: editDevice.username || null, passwordHash: editDevice.passwordHash || null, tags: editDevice.tags ?? [], // 👇 NEU: Hauptgerät speichern parentInventoryNumber: editDevice.parentInventoryNumber?.trim() || null, }), }, ); if (!res.ok) { if (res.status === 404) { throw new Error('Gerät wurde nicht gefunden.'); } throw new Error('Speichern der Änderungen ist fehlgeschlagen.'); } const updated = (await res.json()) as DeviceDetail; setEditDevice(updated); onSaved(updated); setJustSaved(true); setHistoryRefresh((prev) => prev + 1); } catch (err: any) { console.error('Error saving device', err); setEditError( err instanceof Error ? err.message : 'Netzwerkfehler beim Speichern der Gerätedaten.', ); } finally { setSaveLoading(false); } }, [editDevice, onSaved]); const handleClose = () => { if (saveLoading) return; onClose(); }; // 🔹 Hilfswerte für „Verknüpfungen“-Tab const hasParent = !!editDevice?.parentInventoryNumber; const hasAccessories = !!editDevice && Array.isArray(editDevice.accessories) && editDevice.accessories.length > 0; const relationRows = editDevice == null ? [] : ([ { role: hasParent ? 'Zubehör' : 'Hauptgerät', inventoryNumber: editDevice.inventoryNumber, name: editDevice.name ?? null, }, ...(hasParent ? [ { role: 'Hauptgerät', inventoryNumber: editDevice.parentInventoryNumber!, name: editDevice.parentName ?? null, }, ] : []), ...(hasAccessories ? editDevice.accessories!.map((acc) => ({ role: 'Zubehör', inventoryNumber: acc.inventoryNumber, name: acc.name ?? null, })) : []), ] satisfies { role: string; inventoryNumber: string; name: string | null; }[]); // Geräte, die als Hauptgerät in Frage kommen (nicht das Gerät selbst) const selectableParents = editDevice == null ? [] : deviceOptions.filter( (d) => d.inventoryNumber !== editDevice.inventoryNumber, ); // Spezielle Option "kein Hauptgerät" const noParentOption: DeviceOption = { inventoryNumber: '__NONE__', name: 'Kein Hauptgerät (eigenständiges Gerät)', }; const parentOptions: DeviceOption[] = [noParentOption, ...selectableParents]; // Welche Option ist aktuell gewählt? const selectedParentOption: DeviceOption = editDevice && editDevice.parentInventoryNumber ? parentOptions.find( (d) => d.inventoryNumber === editDevice.parentInventoryNumber, ) ?? noParentOption : noParentOption; return ( } tone={justSaved ? 'success' : 'info'} variant="centered" size="xl" footer={
} sidebar={ editDevice ? ( ) : undefined } > {editLoading && (

Gerätedaten werden geladen …

)} {editError && (

{editError}

)} {!editLoading && !editError && editDevice && (
{/* 🔹 Tabs im Edit-Body */} setActiveTab(id as 'fields' | 'relations')} ariaLabel="Bearbeitungsansicht wählen" /> {/* TAB 1: Stammdaten (dein bisheriges Grid) */} {activeTab === 'fields' && (
{/* Inventarnummer */}

Inventar-Nr.

{/* 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)} />
{/* 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) } />
{/* Tags */}
({ name, }))} onChange={(next) => { const names = next.map((t) => t.name); setEditDevice((prev) => prev ? ({ ...prev, tags: names, } as DeviceDetail) : prev, ); 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. Drucker, Serverraum, kritisch" />
{/* Kommentar */}

Kommentar