From 90231bff835519cd642e021bae7e986aed28b7d7 Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:26:43 +0100 Subject: [PATCH] updated --- app/(app)/devices/DeviceEditModal.tsx | 408 ++++++++++++++++ app/(app)/devices/DeviceHistorySidebar.tsx | 213 ++++++--- app/(app)/devices/page.tsx | 438 ++++-------------- app/api/auth/[...nextauth]/route.ts | 79 +--- app/api/devices/[id]/history/route.ts | 62 ++- app/api/devices/[id]/route.ts | 133 +++++- app/api/devices/route.ts | 41 +- app/globals.css | 2 - components/ui/Badge.tsx | 161 +++++++ components/ui/Feed.tsx | 378 +++++++++------ components/ui/Modal.tsx | 12 +- components/ui/TagMultiCombobox.tsx | 233 ++++++++++ lib/auth-options.ts | 72 +++ lib/auth.ts | 71 +-- lib/socketClient.ts | 16 + lib/socketServer.ts | 7 + package-lock.json | 322 ++++++++++++- package.json | 2 + pages/api/socketio.ts | 46 ++ prisma/create-test-user.ts | 50 +- prisma/dev.db | Bin 143360 -> 167936 bytes .../migrations/20251117092638/migration.sql | 22 + .../migration.sql | 2 + prisma/schema.prisma | 32 +- 24 files changed, 2045 insertions(+), 757 deletions(-) create mode 100644 app/(app)/devices/DeviceEditModal.tsx create mode 100644 components/ui/Badge.tsx create mode 100644 components/ui/TagMultiCombobox.tsx create mode 100644 lib/auth-options.ts create mode 100644 lib/socketClient.ts create mode 100644 lib/socketServer.ts create mode 100644 pages/api/socketio.ts create mode 100644 prisma/migrations/20251117092638/migration.sql create mode 100644 prisma/migrations/20251117134852_add_device_history_changed_by/migration.sql diff --git a/app/(app)/devices/DeviceEditModal.tsx b/app/(app)/devices/DeviceEditModal.tsx new file mode 100644 index 0000000..25f54fe --- /dev/null +++ b/app/(app)/devices/DeviceEditModal.tsx @@ -0,0 +1,408 @@ +// app/(app)/devices/DeviceEditModal.tsx +'use client'; + +import { useCallback, useEffect, useState, ChangeEvent, Dispatch, SetStateAction } from 'react'; +import Modal from '@/components/ui/Modal'; +import { PencilIcon } from '@heroicons/react/24/outline'; +import DeviceHistorySidebar from './DeviceHistorySidebar'; +import TagMultiCombobox, { TagOption } from '@/components/ui/TagMultiCombobox'; +import type { DeviceDetail } from './page'; // Typ aus page.tsx (siehe unten) + +type DeviceEditModalProps = { + open: boolean; + inventoryNumber: string | null; + onClose: () => void; + onSaved: (device: DeviceDetail) => void; + allTags: TagOption[]; + setAllTags: Dispatch>; +}; + +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); + + // Gerät laden, wenn Modal geöffnet wird + useEffect(() => { + if (!open || !inventoryNumber) { + setEditDevice(null); + setEditError(null); + return; + } + + let cancelled = false; + + async function loadDevice() { + setEditLoading(true); + setEditError(null); + setEditDevice(null); + + try { + const res = await fetch( + `/api/devices/${encodeURIComponent(inventoryNumber)}`, + { + 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]); + + 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 ?? [], + }), + }, + ); + + 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); // Tabelle im Parent aktualisieren + } 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(); + }; + + return ( + } + tone="info" + variant="centered" + size="lg" + primaryAction={{ + label: saveLoading ? 'Speichern …' : 'Speichern', + onClick: handleSave, + autoFocus: true, + }} + secondaryAction={{ + label: 'Abbrechen', + variant: 'secondary', + onClick: handleClose, + }} + sidebar={ + editDevice ? ( + + ) : undefined + } + > + {editLoading && ( +

+ Gerätedaten werden geladen … +

+ )} + + {editError && ( +

{editError}

+ )} + + {!editLoading && !editError && editDevice && ( +
+ {/* 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)} + /> +
+ + {/* Tags */} +
+ ({ name }))} + onChange={(next) => { + const names = next.map((t) => t.name); + + // in editDevice speichern + setEditDevice((prev) => + prev ? ({ ...prev, tags: names } as DeviceDetail) : prev, + ); + + // allTags im Parent erweitern + 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" + /> +
+ + {/* Netzwerkdaten */} +
+

+ IPv4-Adresse +

+ handleFieldChange('ipv4Address', e)} + /> +
+ +
+

+ IPv6-Adresse +

+ handleFieldChange('ipv6Address', e)} + /> +
+ +
+

+ MAC-Adresse +

+ handleFieldChange('macAddress', e)} + /> +
+ +
+

+ Benutzername +

+ handleFieldChange('username', e)} + /> +
+ +
+

+ Passwort +

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

+ Kommentar +

+