692 lines
23 KiB
TypeScript
692 lines
23 KiB
TypeScript
'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<SetStateAction<TagOption[]>>;
|
||
};
|
||
|
||
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<NewDevice>(emptyDevice);
|
||
const [saveLoading, setSaveLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(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<DeviceOption[]>([]);
|
||
const [optionsLoading, setOptionsLoading] = useState(false);
|
||
const [optionsError, setOptionsError] = useState<string | null>(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<HTMLInputElement | HTMLTextAreaElement>,
|
||
) => {
|
||
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<HTMLInputElement>,
|
||
) => {
|
||
const suffix = e.target.value;
|
||
|
||
setForm((prev) => {
|
||
const prefix =
|
||
isAccessory && prev.parentInventoryNumber
|
||
? `${prev.parentInventoryNumber}-`
|
||
: '';
|
||
|
||
return {
|
||
...prev,
|
||
inventoryNumber: prefix ? `${prefix}${suffix}` : suffix,
|
||
};
|
||
});
|
||
};
|
||
|
||
return (
|
||
<Modal
|
||
open={open}
|
||
onClose={handleClose}
|
||
title="Neues Gerät anlegen"
|
||
icon={<PlusIcon className="size-6" />}
|
||
tone="info"
|
||
variant="centered"
|
||
size="md"
|
||
footer={
|
||
<div className="px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse sm:gap-3">
|
||
<Button
|
||
type="button"
|
||
onClick={handleSave}
|
||
size="md"
|
||
fullWidth
|
||
variant="primary"
|
||
tone="indigo"
|
||
disabled={saveLoading}
|
||
className="sm:w-auto"
|
||
>
|
||
{saveLoading ? 'Anlegen …' : 'Anlegen'}
|
||
</Button>
|
||
|
||
<Button
|
||
type="button"
|
||
onClick={handleClose}
|
||
size="md"
|
||
variant="secondary"
|
||
tone="gray"
|
||
className="mt-3 sm:mt-0 sm:w-auto"
|
||
>
|
||
Abbrechen
|
||
</Button>
|
||
</div>
|
||
}
|
||
>
|
||
{error && (
|
||
<p className="mb-3 text-sm text-red-600 dark:text-red-400">
|
||
{error}
|
||
</p>
|
||
)}
|
||
|
||
<div className="pr-2 grid grid-cols-1 gap-4 text-sm sm:grid-cols-2">
|
||
|
||
{/* 🔹 Block: Gerätetyp & Hauptgerät-Auswahl */}
|
||
<div className="sm:col-span-2">
|
||
<div className="rounded-md border border-gray-700 bg-gray-900/40 px-3 py-3">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Gerätetyp
|
||
</p>
|
||
|
||
{/* 🔹 Hier deine ButtonGroup */}
|
||
<div className="mt-2">
|
||
<ButtonGroup
|
||
options={[
|
||
{ value: 'main', label: 'Eigenständiges Hauptgerät' },
|
||
{ value: 'accessory', label: 'Zubehör zu Hauptgerät' },
|
||
]}
|
||
value={deviceType}
|
||
onChange={(next) => {
|
||
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('');
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
{isAccessory && (
|
||
<div className="mt-3">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Hauptgerät auswählen
|
||
</p>
|
||
|
||
<AppCombobox<DeviceOption>
|
||
// Label im Feld selbst, Überschrift kommt aus dem <p> 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 && (
|
||
<p className="mt-1 text-xs text-gray-500">
|
||
Geräteliste wird geladen …
|
||
</p>
|
||
)}
|
||
|
||
{optionsError && (
|
||
<p className="mt-1 text-xs text-rose-400">
|
||
{optionsError}
|
||
</p>
|
||
)}
|
||
|
||
{!optionsLoading && !optionsError && (
|
||
<p className="mt-1 text-xs text-gray-500">
|
||
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.
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
|
||
{/* Inventarnummer */}
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Inventar-Nr. *
|
||
</p>
|
||
|
||
{/* Zubehör mit Hauptgerät → Prefix fix anzeigen */}
|
||
{isAccessory && form.parentInventoryNumber ? (
|
||
<div className="mt-1 flex rounded-md bg-gray-900/40 ring-1 ring-inset ring-gray-700 shadow-xs focus-within:ring-2 focus-within:ring-indigo-500">
|
||
{/* Unveränderbarer Prefix, dezent dargestellt */}
|
||
<span className="inline-flex items-center px-2.5 text-sm text-gray-400 border-r border-gray-700">
|
||
{accessoryPrefix}
|
||
</span>
|
||
|
||
{/* Editierbarer Suffix */}
|
||
<input
|
||
type="text"
|
||
className="block w-full min-w-0 flex-1 border-0 bg-transparent px-2.5 py-1.5 text-sm text-gray-100 placeholder:text-gray-500 focus:outline-none"
|
||
value={inventorySuffix}
|
||
onChange={handleInventorySuffixChange}
|
||
placeholder="z.B. 1, 2, 3 …"
|
||
/>
|
||
</div>
|
||
) : (
|
||
// Normaler Modus (Hauptgerät oder noch kein Hauptgerät gewählt)
|
||
<input
|
||
type="text"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.inventoryNumber}
|
||
onChange={(e) => handleFieldChange('inventoryNumber', e)}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
{/* Bezeichnung */}
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Bezeichnung *
|
||
</p>
|
||
<input
|
||
type="text"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.name}
|
||
onChange={(e) => handleFieldChange('name', e)}
|
||
/>
|
||
</div>
|
||
|
||
{/* Hersteller / Modell */}
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Hersteller
|
||
</p>
|
||
<input
|
||
type="text"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.manufacturer}
|
||
onChange={(e) => handleFieldChange('manufacturer', e)}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Modell
|
||
</p>
|
||
<input
|
||
type="text"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.model}
|
||
onChange={(e) => handleFieldChange('model', e)}
|
||
/>
|
||
</div>
|
||
|
||
{/* Seriennummer / Produktnummer */}
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Seriennummer
|
||
</p>
|
||
<input
|
||
type="text"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.serialNumber ?? ''}
|
||
onChange={(e) => handleFieldChange('serialNumber', e)}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Produktnummer
|
||
</p>
|
||
<input
|
||
type="text"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.productNumber ?? ''}
|
||
onChange={(e) => handleFieldChange('productNumber', e)}
|
||
/>
|
||
</div>
|
||
|
||
{/* Standort / Gruppe */}
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Standort / Raum
|
||
</p>
|
||
<input
|
||
type="text"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.location ?? ''}
|
||
onChange={(e) => handleFieldChange('location', e)}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Gruppe
|
||
</p>
|
||
<input
|
||
type="text"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.group ?? ''}
|
||
onChange={(e) => handleFieldChange('group', e)}
|
||
/>
|
||
</div>
|
||
|
||
{/* Tags */}
|
||
<div className="sm:col-span-2">
|
||
<TagMultiCombobox
|
||
label="Tags"
|
||
availableTags={allTags}
|
||
value={(form.tags ?? []).map((name) => ({ 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"
|
||
/>
|
||
</div>
|
||
|
||
{/* Netzwerkdaten */}
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
IPv4-Adresse
|
||
</p>
|
||
<input
|
||
type="text"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.ipv4Address ?? ''}
|
||
onChange={(e) => handleFieldChange('ipv4Address', e)}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
IPv6-Adresse
|
||
</p>
|
||
<input
|
||
type="text"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.ipv6Address ?? ''}
|
||
onChange={(e) => handleFieldChange('ipv6Address', e)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="sm:col-span-2">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
MAC-Adresse
|
||
</p>
|
||
<input
|
||
type="text"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.macAddress ?? ''}
|
||
onChange={(e) => handleFieldChange('macAddress', e)}
|
||
/>
|
||
</div>
|
||
|
||
{/* Zugangsdaten */}
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Benutzername
|
||
</p>
|
||
<input
|
||
type="text"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.username ?? ''}
|
||
onChange={(e) => handleFieldChange('username', e)}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Passwort
|
||
</p>
|
||
<input
|
||
type="password"
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.passwordHash ?? ''}
|
||
onChange={(e) => handleFieldChange('passwordHash', e)}
|
||
/>
|
||
</div>
|
||
|
||
{/* Kommentar */}
|
||
<div className="sm:col-span-2">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
|
||
Kommentar
|
||
</p>
|
||
<textarea
|
||
rows={3}
|
||
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
||
value={form.comment ?? ''}
|
||
onChange={(e) => handleFieldChange('comment', e)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
);
|
||
}
|