diff --git a/.env b/.env index b33182d..91dff9a 100644 --- a/.env +++ b/.env @@ -11,5 +11,5 @@ DATABASE_URL="file:./dev.db" -NEXTAUTH_URL=http://localhost:3000 +NEXT_PUBLIC_APP_URL=https://10.0.1.25 NEXTAUTH_SECRET=tegvideo7010! \ No newline at end of file diff --git a/app/(app)/devices/DeviceCreateModal.tsx b/app/(app)/devices/DeviceCreateModal.tsx new file mode 100644 index 0000000..dee3d27 --- /dev/null +++ b/app/(app)/devices/DeviceCreateModal.tsx @@ -0,0 +1,405 @@ +'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 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[]; +}; + +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: [], +}; + +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); + + // Formular resetten, wenn Modal neu geöffnet wird + useEffect(() => { + if (open) { + setForm(emptyDevice); + setError(null); + setSaveLoading(false); + } + }, [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 ?? [], + }), + }); + + 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(); + }; + + return ( + } + tone="info" + variant="centered" + size="sm" + footer={ +
+ + + +
+ } + > + {error && ( +

+ {error} +

+ )} + + {/* Body wie bei DeviceDetailModal: einfach Inhalt, Scroll kommt vom Modal */} +
+ {/* Inventarnummer */} +
+

+ Inventar-Nr. * +

+ 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. Drucker, Serverraum, 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 +

+