This commit is contained in:
Linrador 2025-11-26 08:02:48 +01:00
parent 2f42c71fe9
commit 0f5d23eb9b
31 changed files with 3896 additions and 963 deletions

View File

@ -12,6 +12,8 @@ import Modal from '@/components/ui/Modal';
import { PlusIcon } from '@heroicons/react/24/outline'; import { PlusIcon } from '@heroicons/react/24/outline';
import TagMultiCombobox, { TagOption } from '@/components/ui/TagMultiCombobox'; import TagMultiCombobox, { TagOption } from '@/components/ui/TagMultiCombobox';
import Button from '@/components/ui/Button'; 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'; import type { DeviceDetail } from './page';
type DeviceCreateModalProps = { type DeviceCreateModalProps = {
@ -38,6 +40,8 @@ type NewDevice = {
username: string | null; username: string | null;
passwordHash: string | null; passwordHash: string | null;
tags: string[]; tags: string[];
// wenn gesetzt → Gerät ist Zubehör
parentInventoryNumber: string | null;
}; };
const emptyDevice: NewDevice = { const emptyDevice: NewDevice = {
@ -56,6 +60,15 @@ const emptyDevice: NewDevice = {
username: null, username: null,
passwordHash: null, passwordHash: null,
tags: [], tags: [],
parentInventoryNumber: null,
};
type DeviceOption = {
inventoryNumber: string;
name: string;
parentInventoryNumber?: string | null;
group?: string | null;
location?: string | null;
}; };
export default function DeviceCreateModal({ export default function DeviceCreateModal({
@ -69,15 +82,75 @@ export default function DeviceCreateModal({
const [saveLoading, setSaveLoading] = useState(false); const [saveLoading, setSaveLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Formular resetten, wenn Modal neu geöffnet wird // 🔹 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(() => { useEffect(() => {
if (open) { if (open) {
setForm(emptyDevice); setForm(emptyDevice);
setError(null); setError(null);
setSaveLoading(false); setSaveLoading(false);
setDeviceType('main');
setParentSearch('');
} }
}, [open]); }, [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 = ( const handleFieldChange = (
field: keyof NewDevice, field: keyof NewDevice,
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
@ -118,6 +191,8 @@ export default function DeviceCreateModal({
username: form.username || null, username: form.username || null,
passwordHash: form.passwordHash || null, passwordHash: form.passwordHash || null,
tags: form.tags ?? [], tags: form.tags ?? [],
parentInventoryNumber:
form.parentInventoryNumber?.trim() || null,
}), }),
}); });
@ -150,6 +225,72 @@ export default function DeviceCreateModal({
onClose(); 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 ( return (
<Modal <Modal
open={open} open={open}
@ -158,7 +299,7 @@ export default function DeviceCreateModal({
icon={<PlusIcon className="size-6" />} icon={<PlusIcon className="size-6" />}
tone="info" tone="info"
variant="centered" variant="centered"
size="sm" size="md"
footer={ footer={
<div className="px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse sm:gap-3"> <div className="px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse sm:gap-3">
<Button <Button
@ -193,19 +334,162 @@ export default function DeviceCreateModal({
</p> </p>
)} )}
{/* Body wie bei DeviceDetailModal: einfach Inhalt, Scroll kommt vom Modal */}
<div className="pr-2 grid grid-cols-1 gap-4 text-sm sm:grid-cols-2"> <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 */} {/* Inventarnummer */}
<div> <div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400"> <p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
Inventar-Nr. * Inventar-Nr. *
</p> </p>
<input
type="text" {/* Zubehör mit Hauptgerät → Prefix fix anzeigen */}
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" {isAccessory && form.parentInventoryNumber ? (
value={form.inventoryNumber} <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">
onChange={(e) => handleFieldChange('inventoryNumber', e)} {/* 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> </div>
{/* Bezeichnung */} {/* Bezeichnung */}
@ -311,7 +595,9 @@ export default function DeviceCreateModal({
})); }));
setAllTags((prev) => { setAllTags((prev) => {
const map = new Map(prev.map((t) => [t.name.toLowerCase(), t])); const map = new Map(
prev.map((t) => [t.name.toLowerCase(), t]),
);
for (const t of next) { for (const t of next) {
const key = t.name.toLowerCase(); const key = t.name.toLowerCase();
if (!map.has(key)) { if (!map.has(key)) {
@ -321,7 +607,7 @@ export default function DeviceCreateModal({
return Array.from(map.values()); return Array.from(map.values());
}); });
}} }}
placeholder="z.B. Drucker, Serverraum, kritisch" placeholder="z.B. Dockingstation, Monitor, kritisch"
/> />
</div> </div>

View File

@ -28,6 +28,19 @@ type DeviceDetailsGridProps = {
}; };
function DeviceDetailsGrid({ device, onStartLoan }: DeviceDetailsGridProps) { function DeviceDetailsGrid({ device, onStartLoan }: DeviceDetailsGridProps) {
const [activeSection, setActiveSection] =
useState<'info' | 'zubehoer'>('info');
const hasParent = !!device.parentInventoryNumber;
// 👉 accessories defensiv normalisieren
const accessories = Array.isArray(device.accessories)
? device.accessories
: [];
const hasAccessories = accessories.length > 0;
const showAccessoryTab = hasParent || hasAccessories;
const isLoaned = Boolean(device.loanedTo); const isLoaned = Boolean(device.loanedTo);
const now = new Date(); const now = new Date();
const isOverdue = const isOverdue =
@ -53,250 +66,350 @@ function DeviceDetailsGrid({ device, onStartLoan }: DeviceDetailsGridProps) {
? 'bg-rose-500' ? 'bg-rose-500'
: 'bg-amber-500'; : 'bg-amber-500';
// 🔹 Nur Zubehör-Zeilen, die wir wirklich anzeigen
const accessoryRows: { inventoryNumber: string; name: string | null }[] = [
// Wenn dieses Gerät selbst Zubehör ist → sich selbst anzeigen
...(hasParent
? [
{
inventoryNumber: device.inventoryNumber,
name: device.name ?? null,
},
]
: []),
// Wenn dieses Gerät Hauptgerät ist → alle Kinder anzeigen
...accessories.map((acc) => ({
inventoryNumber: acc.inventoryNumber,
name: acc.name ?? null,
})),
];
return ( return (
<div className="grid grid-cols-1 gap-4 text-sm sm:grid-cols-2"> <div className="space-y-4">
{/* Inventarnummer (oben links) */} {showAccessoryTab && (
<div> <div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500"> <Tabs
Inventar-Nr. tabs={[
</p> { id: 'info', label: 'Stammdaten' },
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400"> { id: 'zubehoer', label: 'Zubehör' },
{device.inventoryNumber} ]}
</p> value={activeSection}
</div> onChange={(id) =>
setActiveSection(id as 'info' | 'zubehoer')
}
ariaLabel="Geräteansicht auswählen"
/>
</div>
)}
{/* Status */} {/* 🔹 Sektion: Stammdaten (dein bisheriges Grid nur ohne alte Zubehör-Liste) */}
<div> {activeSection === 'info' && (
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500"> <div className="grid grid-cols-1 gap-4 text-sm sm:grid-cols-2">
Status {/* Inventarnummer (oben links) */}
</p> <div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Inventar-Nr.
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.inventoryNumber}
</p>
</div>
<div className="mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"> {/* Status */}
{/* linke „Spalte“: nur inhaltsbreit */} <div>
<div className="flex w-auto shrink-0 flex-col gap-1"> <p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
{/* Pill nur content-breit */} Status
<span </p>
className={`inline-flex w-fit items-center gap-2 rounded-full px-3 py-1 text-xs font-medium ${statusClasses}`}
>
<span
className={`inline-block h-2.5 w-2.5 rounded-full ${dotClasses}`}
/>
<span>{statusLabel}</span>
</span>
{/* Infotext darunter */} <div className="mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
{device.loanedTo && ( {/* linke „Spalte“: nur inhaltsbreit */}
<span className="text-xs text-gray-700 dark:text-gray-200"> <div className="flex w-auto shrink-0 flex-col gap-1">
an <span className="font-semibold">{device.loanedTo}</span> {/* Pill nur content-breit */}
{device.loanedFrom && ( <span
<> className={`inline-flex w-fit items-center gap-2 rounded-full px-3 py-1 text-xs font-medium ${statusClasses}`}
{' '}seit{' '} >
{dtf.format(new Date(device.loanedFrom))} <span
</> className={`inline-block h-2.5 w-2.5 rounded-full ${dotClasses}`}
/>
<span>{statusLabel}</span>
</span>
{/* Infotext darunter */}
{device.loanedTo && (
<span className="text-xs text-gray-700 dark:text-gray-200">
an{' '}
<span className="font-semibold">
{device.loanedTo}
</span>
{device.loanedFrom && (
<>
{' '}
seit{' '}
{dtf.format(new Date(device.loanedFrom))}
</>
)}
{device.loanedUntil && (
<>
{' '}
bis{' '}
{dtf.format(new Date(device.loanedUntil))}
</>
)}
{device.loanComment && (
<>
{' '}
- Hinweis: {device.loanComment}
</>
)}
</span>
)} )}
{device.loanedUntil && ( </div>
<>
{' '}bis{' '} <Button
{dtf.format(new Date(device.loanedUntil))} size="md"
</> variant="primary"
)} onClick={onStartLoan}
{device.loanComment && ( >
<> {isLoaned
{' '}- Hinweis: {device.loanComment} ? 'Verleih bearbeiten'
</> : 'Gerät verleihen'}
)} </Button>
</span> </div>
</div>
{/* Trenner nach Verleihstatus */}
<div className="sm:col-span-2">
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
</div>
{/* Bezeichnung */}
<div className="sm:col-span-2">
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Bezeichnung
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.name || ''}
</p>
</div>
{/* Hersteller / Modell */}
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Hersteller
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.manufacturer || ''}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Modell
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.model || ''}
</p>
</div>
{/* Seriennummer / Produktnummer */}
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Seriennummer
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.serialNumber || ''}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Produktnummer
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.productNumber || ''}
</p>
</div>
{/* Standort / Gruppe */}
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Standort / Raum
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.location || ''}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Gruppe
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.group || ''}
</p>
</div>
{/* Netzwerkdaten */}
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
IPv4-Adresse
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.ipv4Address || ''}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
IPv6-Adresse
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.ipv6Address || ''}
</p>
</div>
<div className="sm:col-span-2">
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
MAC-Adresse
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.macAddress || ''}
</p>
</div>
{/* Zugangsdaten */}
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Benutzername
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.username || ''}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Passwort (Hash)
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400 break-all">
{device.passwordHash || ''}
</p>
</div>
{/* Tags */}
<div className="sm:col-span-2">
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Tags
</p>
{device.tags && device.tags.length > 0 ? (
<div className="mt-1 flex flex-wrap gap-1.5">
{device.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center rounded-full bg-gray-800/60 px-2.5 py-0.5 text-xs font-medium text-gray-100 dark:bg-gray-700/70"
>
{tag}
</span>
))}
</div>
) : (
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
</p>
)} )}
</div> </div>
<Button {/* Kommentar */}
size="md" <div className="sm:col-span-2">
variant="primary" <p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
onClick={onStartLoan} Kommentar
> </p>
{isLoaned ? 'Verleih bearbeiten' : 'Gerät verleihen'} <div className="mt-1 rounded-md border border-gray-700 bg-gray-400/20 px-2.5 py-2 text-sm text-gray-800 dark:bg-gray-900/40 dark:text-gray-400 dark:border-gray-700">
</Button> {device.comment && device.comment.trim().length > 0
</div> ? device.comment
</div> : ''}
</div>
{/* 🔹 Trenner nach Verleihstatus */}
<div className="sm:col-span-2">
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
</div>
{/* Bezeichnung jetzt UNTER dem Trenner */}
<div className="sm:col-span-2">
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Bezeichnung
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.name || ''}
</p>
</div>
{/* Hersteller / Modell */}
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Hersteller
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.manufacturer || ''}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Modell
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.model || ''}
</p>
</div>
{/* Seriennummer / Produktnummer */}
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Seriennummer
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.serialNumber || ''}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Produktnummer
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.productNumber || ''}
</p>
</div>
{/* Standort / Gruppe */}
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Standort / Raum
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.location || ''}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Gruppe
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.group || ''}
</p>
</div>
{/* Netzwerkdaten */}
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
IPv4-Adresse
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.ipv4Address || ''}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
IPv6-Adresse
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.ipv6Address || ''}
</p>
</div>
<div className="sm:col-span-2">
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
MAC-Adresse
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.macAddress || ''}
</p>
</div>
{/* Zugangsdaten */}
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Benutzername
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.username || ''}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Passwort (Hash)
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400 break-all">
{device.passwordHash || ''}
</p>
</div>
{/* Tags */}
<div className="sm:col-span-2">
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Tags
</p>
{device.tags && device.tags.length > 0 ? (
<div className="mt-1 flex flex-wrap gap-1.5">
{device.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center rounded-full bg-gray-800/60 px-2.5 py-0.5 text-xs font-medium text-gray-100 dark:bg-gray-700/70"
>
{tag}
</span>
))}
</div> </div>
) : (
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400"></p>
)}
</div>
{/* Kommentar */} {/* Metadaten */}
<div className="sm:col-span-2"> <div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500"> <p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Kommentar Angelegt am
</p> </p>
<div className="mt-1 rounded-md border border-gray-700 bg-gray-400/20 px-2.5 py-2 text-sm text-gray-800 dark:bg-gray-900/40 dark:text-gray-400 dark:border-gray-700"> <p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.comment && device.comment.trim().length > 0 {device.createdAt
? device.comment ? dtf.format(new Date(device.createdAt))
: ''} : ''}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
Zuletzt geändert am
</p>
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
{device.updatedAt
? dtf.format(new Date(device.updatedAt))
: ''}
</p>
</div>
</div> </div>
</div> )}
{/* Metadaten */} {/* 🔹 Sektion: Tabelle für Hauptgerät & Zubehör */}
<div> {activeSection === 'zubehoer' && showAccessoryTab && (
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500"> <div className="text-sm">
Angelegt am <p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
</p> Zubehör
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400"> </p>
{device.createdAt <div className="mt-2 overflow-x-auto rounded-md border border-gray-200 dark:border-gray-700">
? dtf.format(new Date(device.createdAt)) <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-xs">
: ''} <thead className="bg-gray-50 dark:bg-gray-800/60">
</p> <tr>
</div> <th className="px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-200">
Inventar-Nr.
</th>
<th className="px-3 py-2 text-left font-medium text-gray-700 dark:text-gray-200">
Bezeichnung
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-900/40">
{accessoryRows.map((row) => (
<tr key={`zubehoer-${row.inventoryNumber}`}>
<td className="px-3 py-2 text-gray-900 dark:text-gray-100 font-medium">
{row.inventoryNumber}
</td>
<td className="px-3 py-2 text-gray-700 dark:text-gray-200">
{row.name || ''}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div> {!hasAccessories && hasParent && (
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500"> <p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
Zuletzt geändert am Dieses Gerät ist Zubehör zu einem Hauptgerät, hat aber
</p> selbst kein weiteres Zubehör.
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400"> </p>
{device.updatedAt )}
? dtf.format(new Date(device.updatedAt))
: ''} {hasAccessories && !hasParent && (
</p> <p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
</div> Dieses Gerät ist ein Hauptgerät und besitzt die oben
aufgeführten Zubehör-Geräte.
</p>
)}
</div>
)}
</div> </div>
); );
} }
export default function DeviceDetailModal({ export default function DeviceDetailModal({
open, open,
inventoryNumber, inventoryNumber,

View File

@ -13,7 +13,11 @@ import { PencilIcon, CheckCircleIcon } from '@heroicons/react/24/solid';
import DeviceHistorySidebar from './DeviceHistorySidebar'; import DeviceHistorySidebar from './DeviceHistorySidebar';
import TagMultiCombobox, { TagOption } from '@/components/ui/TagMultiCombobox'; import TagMultiCombobox, { TagOption } from '@/components/ui/TagMultiCombobox';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Tabs from '@/components/ui/Tabs';
import type { DeviceDetail } from './page'; import type { DeviceDetail } from './page';
import { Dropdown } from '@/components/ui/Dropdown';
import AppCombobox from '@/components/ui/Combobox';
type DeviceEditModalProps = { type DeviceEditModalProps = {
open: boolean; open: boolean;
@ -24,6 +28,11 @@ type DeviceEditModalProps = {
setAllTags: Dispatch<SetStateAction<TagOption[]>>; setAllTags: Dispatch<SetStateAction<TagOption[]>>;
}; };
type DeviceOption = {
inventoryNumber: string;
name: string;
};
export default function DeviceEditModal({ export default function DeviceEditModal({
open, open,
inventoryNumber, inventoryNumber,
@ -39,6 +48,14 @@ export default function DeviceEditModal({
const [justSaved, setJustSaved] = useState(false); const [justSaved, setJustSaved] = useState(false);
const [historyRefresh, setHistoryRefresh] = useState(0); const [historyRefresh, setHistoryRefresh] = useState(0);
const [deviceOptions, setDeviceOptions] = useState<DeviceOption[]>([]);
const [optionsLoading, setOptionsLoading] = useState(false);
const [optionsError, setOptionsError] = useState<string | null>(null);
// 👇 NEU: Tabs im Edit-Modal
const [activeTab, setActiveTab] =
useState<'fields' | 'relations'>('fields');
useEffect(() => { useEffect(() => {
if (!open || !inventoryNumber) return; if (!open || !inventoryNumber) return;
@ -102,11 +119,60 @@ export default function DeviceEditModal({
const id = setTimeout(() => { const id = setTimeout(() => {
setJustSaved(false); setJustSaved(false);
}, 1500); // Dauer nach Geschmack anpassen }, 1500);
return () => clearTimeout(id); return () => clearTimeout(id);
}, [justSaved]); }, [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 = ( const handleFieldChange = (
field: keyof DeviceDetail, field: keyof DeviceDetail,
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
@ -125,7 +191,9 @@ export default function DeviceEditModal({
try { try {
const res = await fetch( const res = await fetch(
`/api/devices/${encodeURIComponent(editDevice.inventoryNumber)}`, `/api/devices/${encodeURIComponent(
editDevice.inventoryNumber,
)}`,
{ {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -144,6 +212,9 @@ export default function DeviceEditModal({
username: editDevice.username || null, username: editDevice.username || null,
passwordHash: editDevice.passwordHash || null, passwordHash: editDevice.passwordHash || null,
tags: editDevice.tags ?? [], tags: editDevice.tags ?? [],
// 👇 NEU: Hauptgerät speichern
parentInventoryNumber:
editDevice.parentInventoryNumber?.trim() || null,
}), }),
}, },
); );
@ -159,8 +230,8 @@ export default function DeviceEditModal({
setEditDevice(updated); setEditDevice(updated);
onSaved(updated); onSaved(updated);
// Nur Status setzen NICHT schließen
setJustSaved(true); setJustSaved(true);
setHistoryRefresh((prev) => prev + 1);
} catch (err: any) { } catch (err: any) {
console.error('Error saving device', err); console.error('Error saving device', err);
setEditError( setEditError(
@ -178,6 +249,71 @@ export default function DeviceEditModal({
onClose(); 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 ( return (
<Modal <Modal
open={open} open={open}
@ -215,8 +351,8 @@ export default function DeviceEditModal({
{saveLoading {saveLoading
? 'Speichern …' ? 'Speichern …'
: justSaved : justSaved
? 'Gespeichert' ? 'Gespeichert'
: 'Speichern'} : 'Speichern'}
</Button> </Button>
<Button <Button
@ -256,210 +392,373 @@ export default function DeviceEditModal({
)} )}
{!editLoading && !editError && editDevice && ( {!editLoading && !editError && editDevice && (
<div className="pr-2 mt-3 grid grid-cols-1 gap-4 text-sm sm:grid-cols-2"> <div className="pr-2 mt-3 text-sm">
{/* Inventarnummer */} {/* 🔹 Tabs im Edit-Body */}
<div> <Tabs
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400"> tabs={[
Inventar-Nr. { id: 'fields', label: 'Stammdaten' },
</p> { id: 'relations', label: 'Zubehör' },
<input ]}
type="text" value={activeTab}
className="mt-1 block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 text-sm text-gray-400 shadow-xs ring-1 ring-inset ring-gray-800" onChange={(id) => setActiveTab(id as 'fields' | 'relations')}
value={editDevice.inventoryNumber} ariaLabel="Bearbeitungsansicht wählen"
readOnly />
/>
</div>
{/* Bezeichnung */} {/* TAB 1: Stammdaten (dein bisheriges Grid) */}
<div> {activeTab === 'fields' && (
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400"> <div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
Bezeichnung {/* Inventarnummer */}
</p> <div>
<input <p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
type="text" Inventar-Nr.
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" </p>
value={editDevice.name} <input
onChange={(e) => handleFieldChange('name', e)} 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-400 shadow-xs ring-1 ring-inset ring-gray-800"
</div> value={editDevice.inventoryNumber}
readOnly
/>
</div>
{/* Hersteller / Modell */} {/* Bezeichnung */}
<div> <div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400"> <p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
Hersteller Bezeichnung
</p> </p>
<input <input
type="text" 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" 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={editDevice.manufacturer} value={editDevice.name ?? ""}
onChange={(e) => handleFieldChange('manufacturer', e)} onChange={(e) => handleFieldChange('name', e)}
/> />
</div> </div>
<div> {/* Hersteller / Modell */}
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400"> <div>
Modell <p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
</p> Hersteller
<input </p>
type="text" <input
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" type="text"
value={editDevice.model} 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"
onChange={(e) => handleFieldChange('model', e)} value={editDevice.manufacturer ?? ""}
/> onChange={(e) =>
</div> handleFieldChange('manufacturer', e)
{/* 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={editDevice.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={editDevice.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={editDevice.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={editDevice.group ?? ''}
onChange={(e) => handleFieldChange('group', e)}
/>
</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={editDevice.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={editDevice.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={editDevice.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={editDevice.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={editDevice.passwordHash ?? ''}
onChange={(e) => handleFieldChange('passwordHash', e)}
/>
</div>
{/* Tags */}
<div className="sm:col-span-2">
<TagMultiCombobox
label="Tags"
availableTags={allTags}
value={(editDevice.tags ?? []).map((name) => ({ 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()); />
}); </div>
}}
placeholder="z.B. Drucker, Serverraum, kritisch"
/>
</div>
{/* Kommentar */} <div>
<div className="sm:col-span-2"> <p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400"> Modell
Kommentar </p>
</p> <input
<textarea type="text"
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"
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={editDevice.model ?? ""}
value={editDevice.comment ?? ''} onChange={(e) => handleFieldChange('model', e)}
onChange={(e) => handleFieldChange('comment', e)} />
/> </div>
</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={editDevice.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={editDevice.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={editDevice.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={editDevice.group ?? ''}
onChange={(e) => handleFieldChange('group', e)}
/>
</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={editDevice.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={editDevice.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={editDevice.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={editDevice.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={editDevice.passwordHash ?? ''}
onChange={(e) =>
handleFieldChange('passwordHash', e)
}
/>
</div>
{/* Tags */}
<div className="sm:col-span-2">
<TagMultiCombobox
label="Tags"
availableTags={allTags}
value={(editDevice.tags ?? []).map((name) => ({
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"
/>
</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={editDevice.comment ?? ''}
onChange={(e) =>
handleFieldChange('comment', e)
}
/>
</div>
</div>
)}
{/* TAB 2: Hauptgerät & Zubehör */}
{activeTab === 'relations' && (
<div className="mt-4 space-y-4 text-sm">
{/* Combobox: Hauptgerät (Inventar-Nr.) */}
<div className="w-full">
<AppCombobox<DeviceOption>
label="Hauptgerät"
options={parentOptions}
value={selectedParentOption}
onChange={(selected) => {
setEditDevice((prev) => {
if (!prev || !selected) return prev;
// Spezialfall: "kein Hauptgerät"
if (selected.inventoryNumber === '__NONE__') {
return {
...prev,
parentInventoryNumber: null,
parentName: null,
} as DeviceDetail;
}
return {
...prev,
parentInventoryNumber: selected.inventoryNumber,
parentName: selected.name,
} as DeviceDetail;
});
}}
getKey={(d) => d.inventoryNumber}
getPrimaryLabel={(d) => d.name}
placeholder={
optionsLoading
? 'Hauptgerät wird geladen …'
: 'Hauptgerät auswählen …'
}
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">
Wähle ein Hauptgerät aus oder lasse "Kein Hauptgerät" ausgewählt, wenn dieses Gerät eigenständig ist.
</p>
)}
</div>
{/* Tabelle mit aktuellen Beziehungen */}
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
Aktuelle Verknüpfungen
</p>
<div className="mt-2 overflow-x-auto rounded-md border border-gray-700">
<table className="min-w-full divide-y divide-gray-700 text-xs">
<thead className="bg-gray-800/80">
<tr>
<th className="px-3 py-2 text-left font-medium text-gray-200">
Rolle
</th>
<th className="px-3 py-2 text-left font-medium text-gray-200">
Inventar-Nr.
</th>
<th className="px-3 py-2 text-left font-medium text-gray-200">
Bezeichnung
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700 bg-gray-900/60">
{relationRows.map((row) => (
<tr
key={`${row.role}-${row.inventoryNumber}`}
>
<td className="px-3 py-2 text-gray-200">
{row.role}
</td>
<td className="px-3 py-2 text-gray-100 font-medium">
{row.inventoryNumber}
</td>
<td className="px-3 py-2 text-gray-200">
{row.name || ''}
</td>
</tr>
))}
</tbody>
</table>
</div>
{!hasAccessories && hasParent && (
<p className="mt-2 text-xs text-gray-500">
Dieses Gerät ist Zubehör zu einem Hauptgerät,
hat aber selbst kein weiteres Zubehör.
</p>
)}
{hasAccessories && !hasParent && (
<p className="mt-2 text-xs text-gray-500">
Dieses Gerät ist ein Hauptgerät und besitzt die
oben aufgeführten Zubehör-Geräte.
</p>
)}
</div>
</div>
)}
</div> </div>
)} )}
</Modal> </Modal>

View File

@ -4,17 +4,13 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Dropdown, { type DropdownSection } from '@/components/ui/Dropdown'; import AppCombobox from '@/components/ui/Combobox';
import type { DeviceDetail } from './page'; import type { DeviceDetail } from './page';
type LoanDeviceModalProps = { type LoanDeviceModalProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
device: DeviceDetail; device: DeviceDetail;
/**
* Wird nach erfolgreichem Speichern/Beenden aufgerufen,
* damit der Parent den lokalen State aktualisieren kann.
*/
onUpdated?: (patch: { onUpdated?: (patch: {
loanedTo: string | null; loanedTo: string | null;
loanedFrom: string | null; loanedFrom: string | null;
@ -35,9 +31,12 @@ function getBaseGroupName(name: string): string {
} }
type LoanUserOption = { type LoanUserOption = {
value: string; // wird in loanedTo gespeichert (z.B. arbeitsname) value: string; // wird in loanedTo gespeichert (z.B. arbeitsname oder Freitext)
label: string; // Anzeige-Text im Dropdown label: string; // Anzeige-Text in der Combobox
group: string; // Hauptgruppe (BaseKey) group: string; // Hauptgruppe (BaseKey)
imageUrl?: string | null; // Avatar / Platzhalter
/** Gesamter Suchstring: arbeitsname, nwkennung, Vor-/Nachname, Gruppe */
searchText: string;
}; };
type UsersApiGroup = { type UsersApiGroup = {
@ -60,13 +59,11 @@ function toDateInputValue(iso: string | null | undefined): string {
const month = String(d.getMonth() + 1).padStart(2, '0'); const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0');
// lokale Datumskomponenten -> passend für <input type="date">
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} }
function fromDateInputValue(v: string): string | null { function fromDateInputValue(v: string): string | null {
if (!v) return null; if (!v) return null;
// Wir nehmen 00:00 Uhr lokale Zeit; toISOString() speichert sauber in DB
const d = new Date(v + 'T00:00:00'); const d = new Date(v + 'T00:00:00');
if (isNaN(d.getTime())) return null; if (isNaN(d.getTime())) return null;
return d.toISOString(); return d.toISOString();
@ -76,13 +73,12 @@ const dtf = new Intl.DateTimeFormat('de-DE', {
dateStyle: 'short', dateStyle: 'short',
}); });
// "heute" im <input type="date">-Format
function todayInputDate(): string { function todayInputDate(): string {
const d = new Date(); const d = new Date();
const year = d.getFullYear(); const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0'); const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`; // z.B. 2025-02-19 return `${year}-${month}-${day}`;
} }
export default function LoanDeviceModal({ export default function LoanDeviceModal({
@ -123,7 +119,7 @@ export default function LoanDeviceModal({
device.loanedTo, device.loanedTo,
]); ]);
// Beim Öffnen User für Dropdown laden (nur User aus Gruppen, gruppiert nach Hauptgruppen) // Beim Öffnen User für Combobox laden
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
@ -143,6 +139,7 @@ export default function LoanDeviceModal({
for (const g of groups) { for (const g of groups) {
const mainGroup = getBaseGroupName(g.name) || g.name; const mainGroup = getBaseGroupName(g.name) || g.name;
for (const u of g.users ?? []) { for (const u of g.users ?? []) {
if (!u.arbeitsname) continue; if (!u.arbeitsname) continue;
@ -154,24 +151,51 @@ export default function LoanDeviceModal({
nameParts.push(' ' + extra.join(' ')); nameParts.push(' ' + extra.join(' '));
} }
const fullName =
[u.firstName, u.lastName].filter(Boolean).join(' ') ||
u.arbeitsname;
const avatarName = fullName || u.arbeitsname;
const imageUrl = `https://ui-avatars.com/api/?name=${encodeURIComponent(
avatarName,
)}&background=4f46e5&color=fff&size=64&bold=true`;
const searchParts = [
u.arbeitsname,
u.nwkennung,
u.firstName,
u.lastName,
mainGroup,
g.name,
].filter(Boolean);
opts.push({ opts.push({
value: u.arbeitsname, // in loanedTo speichern wir weiterhin den Arbeitsnamen value: u.arbeitsname, // in loanedTo speichern wir weiterhin den Arbeitsnamen
label: nameParts.join(' '), label: nameParts.join(' '),
group: mainGroup, group: mainGroup,
imageUrl,
searchText: searchParts.join(' '),
}); });
} }
} }
// bestehenden Wert, der kein User ist, als "Andere"-Option anhängen // bestehenden Wert, der kein User ist, als "Andere"-Option anhängen (ohne Zusatz-Placeholder)
const currentLoanedTo = device.loanedTo ?? ''; const currentLoanedTo = device.loanedTo ?? '';
if ( if (
currentLoanedTo && currentLoanedTo &&
!opts.some((o) => o.value === currentLoanedTo) !opts.some((o) => o.value === currentLoanedTo)
) { ) {
const avatarName = currentLoanedTo;
const imageUrl = `https://ui-avatars.com/api/?name=${encodeURIComponent(
avatarName,
)}&background=6b7280&color=fff&size=64&bold=true`;
opts.push({ opts.push({
value: currentLoanedTo, value: currentLoanedTo,
label: `${currentLoanedTo} (bisheriger Eintrag)`, label: currentLoanedTo, // 👈 kein "(bisheriger Eintrag)" mehr
group: 'Andere', group: 'Andere',
imageUrl,
searchText: `${currentLoanedTo} Andere`,
}); });
} }
@ -194,64 +218,12 @@ export default function LoanDeviceModal({
loadUsers(); loadUsers();
}, [open, device.loanedTo]); }, [open, device.loanedTo]);
// Optionen nach Hauptgruppe gruppieren // aktuell ausgewählte Option für Combobox
const groupedOptions = useMemo(() => {
const map = new Map<string, LoanUserOption[]>();
for (const opt of userOptions) {
const key = opt.group || 'Andere';
if (!map.has(key)) {
map.set(key, []);
}
map.get(key)!.push(opt);
}
return Array.from(map.entries()).sort(([a], [b]) =>
a.localeCompare(b, 'de'),
);
}, [userOptions]);
// Dropdown-Sektionen für deine Dropdown-Komponente
const dropdownSections = useMemo<DropdownSection[]>(() => {
const sections: DropdownSection[] = [];
// Erste Sektion: Zurücksetzen / keine Auswahl
sections.push({
id: 'base',
items: [
{
id: 'none',
label: '— Bitte auswählen —',
onClick: () => setLoanedTo(''),
},
],
});
// Danach je Hauptgruppe eine eigene Sektion mit Label als Trenner
for (const [groupName, opts] of groupedOptions) {
sections.push({
id: groupName,
label: groupName, // <-- wird als Trenner angezeigt
items: opts.map((opt) => ({
id: `${groupName}-${opt.value}`,
label: opt.label, // <-- nur der Name, ohne Gruppen-Präfix
onClick: () => setLoanedTo(opt.value),
})),
});
}
return sections;
}, [groupedOptions, setLoanedTo]);
// Aktuell ausgewähltes Label für den Trigger-Button
const currentSelected = useMemo( const currentSelected = useMemo(
() => userOptions.find((o) => o.value === loanedTo) ?? null, () => userOptions.find((o) => o.value === loanedTo) ?? null,
[userOptions, loanedTo], [userOptions, loanedTo],
); );
const dropdownLabel =
currentSelected?.label || (loanedTo || 'Bitte auswählen …');
const isLoaned = !!device.loanedTo; const isLoaned = !!device.loanedTo;
async function saveLoan() { async function saveLoan() {
@ -272,7 +244,6 @@ export default function LoanDeviceModal({
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
// ⚠️ PATCH-Route erwartet alle Felder, nicht nur Verleihfelder!
name: device.name, name: device.name,
manufacturer: device.manufacturer, manufacturer: device.manufacturer,
model: device.model, model: device.model,
@ -288,7 +259,6 @@ export default function LoanDeviceModal({
location: device.location, location: device.location,
tags: device.tags ?? [], tags: device.tags ?? [],
// Verleihfelder
loanedTo: loanedTo.trim(), loanedTo: loanedTo.trim(),
loanedFrom: fromDateInputValue(loanedFrom), loanedFrom: fromDateInputValue(loanedFrom),
loanedUntil: fromDateInputValue(loanedUntil), loanedUntil: fromDateInputValue(loanedUntil),
@ -338,7 +308,6 @@ export default function LoanDeviceModal({
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
// Alle bisherigen Felder durchreichen
name: device.name, name: device.name,
manufacturer: device.manufacturer, manufacturer: device.manufacturer,
model: device.model, model: device.model,
@ -354,7 +323,6 @@ export default function LoanDeviceModal({
location: device.location, location: device.location,
tags: device.tags ?? [], tags: device.tags ?? [],
// Verleih zurücksetzen
loanedTo: null, loanedTo: null,
loanedFrom: null, loanedFrom: null,
loanedUntil: null, loanedUntil: null,
@ -419,7 +387,7 @@ export default function LoanDeviceModal({
useGrayFooter useGrayFooter
> >
<div className="space-y-4 text-sm"> <div className="space-y-4 text-sm">
{/* Aktueller Verleih-Hinweis mit deutlich sichtbarem „Verleih beenden“ */} {/* Hinweis zum aktuellen Verleih */}
{isLoaned && ( {isLoaned && (
<div className="rounded-md border border-amber-300 bg-amber-50 px-3 py-3 text-xs text-amber-900 dark:border-amber-700/70 dark:bg-amber-950/40 dark:text-amber-50"> <div className="rounded-md border border-amber-300 bg-amber-50 px-3 py-3 text-xs text-amber-900 dark:border-amber-700/70 dark:bg-amber-950/40 dark:text-amber-50">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
@ -475,15 +443,45 @@ export default function LoanDeviceModal({
Verliehen an Verliehen an
</label> </label>
<Dropdown <div className="mt-1">
label={dropdownLabel} <AppCombobox<LoanUserOption>
ariaLabel="Empfänger auswählen" label={undefined}
align="left" options={userOptions}
triggerVariant="button" value={currentSelected}
disabled={optionsLoading || userOptions.length === 0} onChange={(selected) => {
sections={dropdownSections} if (!selected) {
triggerClassName="mt-1 w-full justify-between" setLoanedTo('');
/> return;
}
setLoanedTo(selected.value);
}}
getKey={(opt) => opt.value}
getPrimaryLabel={(opt) => opt.label}
getSecondaryLabel={(opt) => opt.group}
getSearchText={(opt) => opt.searchText}
getImageUrl={(opt) => opt.imageUrl ?? null}
placeholder={
optionsLoading
? 'Lade Benutzer …'
: userOptions.length > 0
? 'Bitte auswählen …'
: 'Keine Benutzer gefunden'
}
allowCreateFromQuery
onCreateFromQuery={(query) => {
const trimmed = query.trim();
return {
value: trimmed,
label: trimmed,
group: 'Andere',
imageUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(
trimmed || 'Unbekannt',
)}&background=6b7280&color=fff&size=64&bold=true`,
searchText: `${trimmed} Andere`,
};
}}
/>
</div>
{optionsLoading && ( {optionsLoading && (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">

View File

@ -6,6 +6,7 @@ import { useCallback, useEffect, useState } from 'react';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Table, { TableColumn } from '@/components/ui/Table'; import Table, { TableColumn } from '@/components/ui/Table';
import { Dropdown } from '@/components/ui/Dropdown'; import { Dropdown } from '@/components/ui/Dropdown';
import Tabs from '@/components/ui/Tabs'; // 🔹 NEU
import { import {
BookOpenIcon, BookOpenIcon,
PencilIcon, PencilIcon,
@ -18,43 +19,51 @@ import DeviceEditModal from './DeviceEditModal';
import DeviceDetailModal from './DeviceDetailModal'; import DeviceDetailModal from './DeviceDetailModal';
import DeviceCreateModal from './DeviceCreateModal'; import DeviceCreateModal from './DeviceCreateModal';
export type DeviceRow = { export type AccessorySummary = {
inventoryNumber: string; inventoryNumber: string;
name: string; name: string | null;
manufacturer: string; };
model: string;
export type DeviceDetail = {
inventoryNumber: string;
name: string | null;
manufacturer: string | null;
model: string | null;
serialNumber: string | null; serialNumber: string | null;
productNumber: string | null; productNumber: string | null;
comment: string | null; comment: string | null;
group: string | null;
location: string | null;
ipv4Address: string | null; ipv4Address: string | null;
ipv6Address: string | null; ipv6Address: string | null;
macAddress: string | null; macAddress: string | null;
username: string | null; username: string | null;
passwordHash: string | null; passwordHash: string | null;
group: string | null;
location: string | null;
tags: string[]; tags: string[];
createdAt: string;
updatedAt: string;
loanedTo: string | null; loanedTo: string | null;
loanedFrom: string | null; loanedFrom: string | null;
loanedUntil: string | null; loanedUntil: string | null;
loanComment: string | null; loanComment: string | null;
parentInventoryNumber: string | null;
parentName: string | null;
accessories: {
inventoryNumber: string;
name: string | null;
}[];
createdAt: string | null;
updatedAt: string | null;
}; };
export type DeviceDetail = DeviceRow & { function formatDate(iso: string | null | undefined) {
createdAt?: string; if (!iso) return ''; // oder '' wenn du es leer willst
};
function formatDate(iso: string) {
return new Intl.DateTimeFormat('de-DE', { return new Intl.DateTimeFormat('de-DE', {
dateStyle: 'short', dateStyle: 'short',
timeStyle: 'short', timeStyle: 'short',
}).format(new Date(iso)); }).format(new Date(iso));
} }
const columns: TableColumn<DeviceRow>[] = [ const columns: TableColumn<DeviceDetail>[] = [
{ {
key: 'inventoryNumber', key: 'inventoryNumber',
header: 'Nr.', header: 'Nr.',
@ -129,24 +138,26 @@ const columns: TableColumn<DeviceRow>[] = [
]; ];
export default function DevicesPage() { export default function DevicesPage() {
// Liste aus der API const [devices, setDevices] = useState<DeviceDetail[]>([]);
const [devices, setDevices] = useState<DeviceRow[]>([]);
const [listLoading, setListLoading] = useState(false); const [listLoading, setListLoading] = useState(false);
const [listError, setListError] = useState<string | null>(null); const [listError, setListError] = useState<string | null>(null);
// welches Gerät ist gerade im Edit-Modal geöffnet?
const [editInventoryNumber, setEditInventoryNumber] = useState<string | null>(null); const [editInventoryNumber, setEditInventoryNumber] = useState<string | null>(null);
// welches Gerät ist im Detail-Modal geöffnet?
const [detailInventoryNumber, setDetailInventoryNumber] = useState<string | null>(null); const [detailInventoryNumber, setDetailInventoryNumber] = useState<string | null>(null);
// Create-Modal
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
// Alle bekannten Tags (kannst du später auch aus eigener /api/tags laden)
const [allTags, setAllTags] = useState<TagOption[]>([]); const [allTags, setAllTags] = useState<TagOption[]>([]);
/* ───────── Geräte-Liste laden (auch für "live"-Updates) ───────── */ // 🔹 Tab-Filter: Hauptgeräte / Zubehör / Alle
const [activeTab, setActiveTab] =
useState<'main' | 'accessories' | 'all'>('main');
// 🔹 Counters für Badges
const mainCount = devices.filter((d) => !d.parentInventoryNumber).length;
const accessoriesCount = devices.filter((d) => !!d.parentInventoryNumber).length;
const allCount = devices.length;
/* ───────── Geräte-Liste laden ───────── */
const loadDevices = useCallback(async () => { const loadDevices = useCallback(async () => {
setListLoading(true); setListLoading(true);
@ -163,10 +174,9 @@ export default function DevicesPage() {
return; return;
} }
const data = (await res.json()) as DeviceRow[]; const data = (await res.json()) as DeviceDetail[];
setDevices(data); setDevices(data);
// 🔹 alle Tags aus der Liste ableiten
const tagSet = new Map<string, TagOption>(); const tagSet = new Map<string, TagOption>();
for (const d of data) { for (const d of data) {
(d.tags ?? []).forEach((name) => { (d.tags ?? []).forEach((name) => {
@ -185,22 +195,21 @@ export default function DevicesPage() {
} }
}, []); }, []);
// initial laden
useEffect(() => { useEffect(() => {
loadDevices(); loadDevices();
}, [loadDevices]); }, [loadDevices]);
// ✅ Echte Live-Updates via Socket.IO /* ───────── Live-Updates via Socket.IO ───────── */
useEffect(() => { useEffect(() => {
const socket = getSocket(); const socket = getSocket();
const handleUpdated = (payload: DeviceRow) => { const handleUpdated = (payload: DeviceDetail) => {
setDevices((prev) => { setDevices((prev) => {
const exists = prev.some( const exists = prev.some(
(d) => d.inventoryNumber === payload.inventoryNumber, (d) => d.inventoryNumber === payload.inventoryNumber,
); );
if (!exists) { if (!exists) {
// falls du Updates & Creates über das gleiche Event schickst
return [...prev, payload]; return [...prev, payload];
} }
return prev.map((d) => return prev.map((d) =>
@ -209,7 +218,7 @@ export default function DevicesPage() {
}); });
}; };
const handleCreated = (payload: DeviceRow) => { const handleCreated = (payload: DeviceDetail) => {
setDevices((prev) => { setDevices((prev) => {
if (prev.some((d) => d.inventoryNumber === payload.inventoryNumber)) { if (prev.some((d) => d.inventoryNumber === payload.inventoryNumber)) {
return prev; return prev;
@ -235,7 +244,7 @@ export default function DevicesPage() {
}; };
}, []); }, []);
/* ───────── Edit-Modal Trigger ───────── */ /* ───────── Edit-/Detail-/Create-Modal Trigger ───────── */
const handleEdit = useCallback((inventoryNumber: string) => { const handleEdit = useCallback((inventoryNumber: string) => {
setEditInventoryNumber(inventoryNumber); setEditInventoryNumber(inventoryNumber);
@ -245,6 +254,58 @@ export default function DevicesPage() {
setEditInventoryNumber(null); setEditInventoryNumber(null);
}, []); }, []);
const handleDelete = useCallback(
async (inventoryNumber: string) => {
const confirmed = window.confirm(
`Gerät ${inventoryNumber} wirklich löschen?`,
);
if (!confirmed) return;
try {
const res = await fetch(
`/api/devices/${encodeURIComponent(inventoryNumber)}`,
{
method: 'DELETE',
},
);
if (!res.ok) {
let message = 'Löschen des Geräts ist fehlgeschlagen.';
try {
const data = await res.json();
if (data?.error) {
if (data.error === 'HAS_ACCESSORIES') {
message =
'Das Gerät hat noch Zubehör und kann nicht gelöscht werden. Entferne oder verschiebe zuerst das Zubehör.';
} else if (data.error === 'NOT_FOUND') {
message =
'Gerät wurde nicht gefunden (evtl. bereits gelöscht).';
} else if (typeof data.error === 'string') {
message = data.error;
}
}
} catch {
// ignore JSON-Parse-Error
}
alert(message);
return;
}
// Optimistisch aus lokaler Liste entfernen
// (zusätzlich kommt noch der Socket-Event device:deleted)
setDevices((prev) =>
prev.filter((d) => d.inventoryNumber !== inventoryNumber),
);
} catch (err) {
console.error('Error deleting device', err);
alert('Netzwerkfehler beim Löschen des Geräts.');
}
},
[setDevices],
);
const handleDetails = useCallback((inventoryNumber: string) => { const handleDetails = useCallback((inventoryNumber: string) => {
setDetailInventoryNumber(inventoryNumber); setDetailInventoryNumber(inventoryNumber);
}, []); }, []);
@ -261,6 +322,20 @@ export default function DevicesPage() {
setCreateOpen(false); setCreateOpen(false);
}, []); }, []);
/* ───────── Filter nach Tab ───────── */
const filteredDevices = devices.filter((d) => {
if (activeTab === 'main') {
// Hauptgeräte: kein parent → eigenständig
return !d.parentInventoryNumber;
}
if (activeTab === 'accessories') {
// Zubehör: hat ein Hauptgerät
return !!d.parentInventoryNumber;
}
// "all"
return true;
});
/* ───────── Render ───────── */ /* ───────── Render ───────── */
@ -284,12 +359,40 @@ export default function DevicesPage() {
icon={<PlusIcon className="size-5" />} icon={<PlusIcon className="size-5" />}
aria-label="Neues Gerät anlegen" aria-label="Neues Gerät anlegen"
onClick={openCreateModal} onClick={openCreateModal}
title='Neues Gerät anlegen' title="Neues Gerät anlegen"
> >
Neues Gerät anlegen Neues Gerät anlegen
</Button> </Button>
</div> </div>
{/* 🔹 Tabs für Hauptgeräte/Zubehör/Alle */}
<div className="mt-6">
<Tabs
tabs={[
{
id: 'main',
label: 'Hauptgeräte',
count: mainCount,
},
{
id: 'accessories',
label: 'Zubehör',
count: accessoriesCount,
},
{
id: 'all',
label: 'Alle Geräte',
count: allCount,
},
]}
value={activeTab}
onChange={(id) =>
setActiveTab(id as 'main' | 'accessories' | 'all')
}
ariaLabel="Geräteliste filtern"
/>
</div>
{listLoading && ( {listLoading && (
<p className="mt-4 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-4 text-sm text-gray-500 dark:text-gray-400">
Geräte werden geladen Geräte werden geladen
@ -304,8 +407,8 @@ export default function DevicesPage() {
{/* Tabelle */} {/* Tabelle */}
<div className="mt-8"> <div className="mt-8">
<Table<DeviceRow> <Table<DeviceDetail>
data={devices} data={filteredDevices} // 🔹 statt devices
columns={columns} columns={columns}
getRowId={(row) => row.inventoryNumber} getRowId={(row) => row.inventoryNumber}
selectable selectable
@ -338,9 +441,7 @@ export default function DevicesPage() {
size="md" size="md"
icon={<TrashIcon className="size-5" />} icon={<TrashIcon className="size-5" />}
aria-label={`Gerät ${row.inventoryNumber} löschen`} aria-label={`Gerät ${row.inventoryNumber} löschen`}
onClick={() => onClick={() => handleDelete(row.inventoryNumber)}
console.log('Löschen', row.inventoryNumber)
}
/> />
</div> </div>
@ -366,8 +467,7 @@ export default function DevicesPage() {
label: 'Löschen', label: 'Löschen',
icon: <TrashIcon className="size-4" />, icon: <TrashIcon className="size-4" />,
tone: 'danger', tone: 'danger',
onClick: () => onClick: () => handleDelete(row.inventoryNumber),
console.log('Löschen', row.inventoryNumber),
}, },
], ],
}, },
@ -379,7 +479,7 @@ export default function DevicesPage() {
/> />
</div> </div>
{/* Edit-/Details-Modal */} {/* Modals */}
<DeviceEditModal <DeviceEditModal
open={editInventoryNumber !== null} open={editInventoryNumber !== null}
inventoryNumber={editInventoryNumber} inventoryNumber={editInventoryNumber}
@ -404,7 +504,6 @@ export default function DevicesPage() {
setAllTags={setAllTags} setAllTags={setAllTags}
onCreated={(created) => { onCreated={(created) => {
setDevices((prev) => { setDevices((prev) => {
// falls Live-Update denselben Eintrag schon gebracht hat
if (prev.some((d) => d.inventoryNumber === created.inventoryNumber)) { if (prev.some((d) => d.inventoryNumber === created.inventoryNumber)) {
return prev; return prev;
} }

View File

@ -36,6 +36,8 @@ import ScanModal from '@/components/ScanModal';
import DeviceDetailModal from './devices/DeviceDetailModal'; import DeviceDetailModal from './devices/DeviceDetailModal';
import PersonAvatar from '@/components/ui/UserAvatar'; import PersonAvatar from '@/components/ui/UserAvatar';
import UserMenu from '@/components/UserMenu'; import UserMenu from '@/components/UserMenu';
import GlobalSearch from '@/components/GlobalSearch';
const navigation = [ const navigation = [
{ name: 'Dashboard', href: '/dashboard', icon: HomeIcon }, { name: 'Dashboard', href: '/dashboard', icon: HomeIcon },
@ -287,25 +289,14 @@ export default function AppLayout({ children }: { children: ReactNode }) {
<div className="flex flex-1 items-center gap-x-4 self-stretch lg:gap-x-6"> <div className="flex flex-1 items-center gap-x-4 self-stretch lg:gap-x-6">
{/* Suche */} {/* Suche */}
<form <div className="grid flex-1 grid-cols-1">
action="#" <GlobalSearch
method="GET" onDeviceSelected={(inv) => {
className="grid flex-1 grid-cols-1" setDetailInventoryNumber(inv);
> setDetailOpen(true);
<div className="relative"> }}
<MagnifyingGlassIcon />
aria-hidden="true" </div>
className="pointer-events-none absolute left-3 top-1/2 size-5 -translate-y-1/2 text-gray-400 dark:text-gray-500"
/>
<input
type="search"
name="search"
placeholder="Suchen…"
aria-label="Suchen"
className="block w-full rounded-xl border-0 bg-gray-50 py-1.5 pl-10 pr-3 text-sm text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none dark:bg-gray-800 dark:text-white dark:ring-gray-700 dark:placeholder:text-gray-500"
/>
</div>
</form>
<div className="flex items-center gap-x-4 lg:gap-x-6"> <div className="flex items-center gap-x-4 lg:gap-x-6">
{/* Trennstrich zwischen Suche und Kamera nur mobil */} {/* Trennstrich zwischen Suche und Kamera nur mobil */}

View File

@ -14,6 +14,16 @@ type Props = {
groups: SimpleGroup[]; groups: SimpleGroup[];
}; };
type ParsedRow = {
nwkennung: string;
lastName: string;
firstName: string;
arbeitsname: string;
groupName: string | null;
rawLine: string;
lineNumber: number;
};
export default function UsersCsvImportButton({ groups }: Props) { export default function UsersCsvImportButton({ groups }: Props) {
const router = useRouter(); const router = useRouter();
const fileInputRef = useRef<HTMLInputElement | null>(null); const fileInputRef = useRef<HTMLInputElement | null>(null);
@ -21,35 +31,7 @@ export default function UsersCsvImportButton({ groups }: Props) {
const [importing, setImporting] = useState(false); const [importing, setImporting] = useState(false);
const [importError, setImportError] = useState<string | null>(null); const [importError, setImportError] = useState<string | null>(null);
const [importSummary, setImportSummary] = useState<string | null>(null); const [importSummary, setImportSummary] = useState<string | null>(null);
const [importProgress, setImportProgress] = useState<string | null>(null);
// Hilfsfunktion: Gruppe sicherstellen (existiert oder neu anlegen)
async function ensureGroupId(
name: string,
cache: Map<string, string>,
): Promise<string | null> {
const trimmed = name.trim();
if (!trimmed) return null;
const cached = cache.get(trimmed);
if (cached) return cached;
const res = await fetch('/api/user-groups', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: trimmed }),
});
if (!res.ok) {
const data = await res.json().catch(() => null);
console.error('Fehler beim Anlegen der Gruppe', res.status, data);
return null;
}
const data = await res.json();
const id = data.id as string;
cache.set(trimmed, id);
return id;
}
async function handleImportCsv( async function handleImportCsv(
e: React.ChangeEvent<HTMLInputElement>, e: React.ChangeEvent<HTMLInputElement>,
@ -60,36 +42,29 @@ export default function UsersCsvImportButton({ groups }: Props) {
setImporting(true); setImporting(true);
setImportError(null); setImportError(null);
setImportSummary(null); setImportSummary(null);
setImportProgress('Lese Datei …');
try { try {
const text = await file.text(); const text = await file.text();
setImportProgress('Analysiere CSV …');
// Gruppen-Cache (Name -> ID), Start mit bestehenden Gruppen const lines = text.split(/\r?\n/);
const groupCache = new Map<string, string>();
for (const g of groups) {
groupCache.set(g.name.trim(), g.id);
}
let createdCount = 0; const parsedRows: ParsedRow[] = [];
let skippedCount = 0; let skippedCount = 0;
const lines = text
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l.length > 0);
for (let index = 0; index < lines.length; index++) { for (let index = 0; index < lines.length; index++) {
const line = lines[index]; const raw = lines[index];
const trimmed = raw.trim();
// ⬅️ Erste Zeile immer ignorieren (Header) if (!trimmed) continue;
if (index === 0) {
continue;
}
// Format: NwKennung;Nachname;Vorname;Arbeitsname;Gruppe // erste Zeile = Header
const parts = line.split(';'); if (index === 0) continue;
const parts = trimmed.split(';');
if (parts.length < 4) { if (parts.length < 4) {
console.warn('Zeile übersprungen (falsches Format):', line); console.warn('Zeile übersprungen (falsches Format):', trimmed);
skippedCount++; skippedCount++;
continue; continue;
} }
@ -106,62 +81,182 @@ export default function UsersCsvImportButton({ groups }: Props) {
const lastName = (lastNameRaw ?? '').trim(); const lastName = (lastNameRaw ?? '').trim();
const firstName = (firstNameRaw ?? '').trim(); const firstName = (firstNameRaw ?? '').trim();
const arbeitsname = (arbeitsnameRaw ?? '').trim(); const arbeitsname = (arbeitsnameRaw ?? '').trim();
const groupName = (groupRaw ?? '').trim(); const groupNameRaw = (groupRaw ?? '').trim();
const groupName = groupNameRaw ? groupNameRaw : null;
// NwKennung + Name + Arbeitsname als Pflichtfelder
if (!nwkennung || !lastName || !firstName || !arbeitsname) { if (!nwkennung || !lastName || !firstName || !arbeitsname) {
console.warn( console.warn(
'Zeile übersprungen (Pflichtfelder leer):', 'Zeile übersprungen (Pflichtfelder leer):',
line, trimmed,
); );
skippedCount++; skippedCount++;
continue; continue;
} }
parsedRows.push({
nwkennung,
lastName,
firstName,
arbeitsname,
groupName,
rawLine: trimmed,
lineNumber: index + 1,
});
}
if (parsedRows.length === 0) {
setImportError(
'Keine gültigen Zeilen gefunden. Bitte CSV prüfen.',
);
return;
}
// 1) Gruppen vorbereiten: bestehende + neue
setImportProgress('Ermittle Gruppen …');
const groupCache = new Map<string, string>();
for (const g of groups) {
const name = g.name.trim();
if (name) groupCache.set(name, g.id);
}
const knownGroupNames = new Set<string>(
Array.from(groupCache.keys()),
);
const newGroupNamesSet = new Set<string>();
for (const row of parsedRows) {
if (!row.groupName) continue;
const gName = row.groupName.trim();
if (!gName) continue;
if (!knownGroupNames.has(gName)) {
newGroupNamesSet.add(gName);
}
}
const newGroupNames = Array.from(newGroupNamesSet.values());
// 2) Neue Gruppen in einem Rutsch anlegen
if (newGroupNames.length > 0) {
setImportProgress(
`Lege ${newGroupNames.length} neue Gruppe(n) an …`,
);
const resGroups = await fetch('/api/user-groups', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ names: newGroupNames }),
});
if (!resGroups.ok) {
const data = await resGroups.json().catch(() => null);
console.error(
'Fehler beim Bulk-Anlegen der Gruppen:',
resGroups.status,
data,
);
setImportError(
'Fehler beim Anlegen der Gruppen. Import abgebrochen.',
);
return;
}
const data = await resGroups.json();
const createdGroups = (data.groups ?? []) as SimpleGroup[];
for (const g of createdGroups) {
const name = g.name.trim();
if (name) {
groupCache.set(name, g.id);
knownGroupNames.add(name);
}
}
}
// 3) Benutzer-Payload für Bulk-Import bauen
setImportProgress('Bereite Benutzer-Daten für Import vor …');
type UserPayload = {
nwkennung: string;
firstName: string;
lastName: string;
arbeitsname: string;
groupId?: string | null;
};
const usersPayload: UserPayload[] = [];
let skippedByGroup = 0;
for (const row of parsedRows) {
let groupId: string | null = null; let groupId: string | null = null;
if (groupName) {
groupId = await ensureGroupId(groupName, groupCache); if (row.groupName) {
if (!groupId) { const gName = row.groupName.trim();
console.warn( if (gName) {
'Zeile übersprungen (Gruppe konnte nicht angelegt werden):', const id = groupCache.get(gName);
line, if (!id) {
); console.warn(
skippedCount++; 'Zeile übersprungen (Gruppe nicht vorhanden):',
continue; row.rawLine,
);
skippedByGroup++;
continue;
}
groupId = id;
} }
} }
const res = await fetch('/api/users', { usersPayload.push({
method: 'POST', nwkennung: row.nwkennung,
headers: { 'Content-Type': 'application/json' }, firstName: row.firstName,
body: JSON.stringify({ lastName: row.lastName,
nwkennung, // ⬅ NEU arbeitsname: row.arbeitsname,
arbeitsname, groupId: groupId ?? null,
firstName,
lastName,
groupId,
}),
}); });
if (!res.ok) {
const data = await res.json().catch(() => null);
console.error(
'Fehler beim Anlegen der Person aus CSV:',
res.status,
data,
'Zeile:',
line,
);
skippedCount++;
continue;
}
createdCount++;
} }
setImportSummary( if (usersPayload.length === 0) {
`Import abgeschlossen: ${createdCount} Personen importiert, ${skippedCount} Zeilen übersprungen.`, setImportError(
'Keine gültigen Benutzer-Datensätze nach Gruppenzuordnung. Import abgebrochen.',
);
return;
}
// 4) Bulk-Import der Benutzer
setImportProgress(
`Importiere ${usersPayload.length} Benutzer …`,
); );
const resUsers = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ users: usersPayload }),
});
if (!resUsers.ok) {
const data = await resUsers.json().catch(() => null);
console.error(
'Fehler beim Bulk-Import der Benutzer:',
resUsers.status,
data,
);
setImportError(
'Fehler beim Import der Benutzer. Details siehe Konsole.',
);
return;
}
const result = await resUsers.json();
const createdCount = result.createdCount ?? 0;
const skippedServer = result.skippedCount ?? 0;
const totalSkipped =
skippedCount + skippedByGroup + skippedServer;
setImportSummary(
`Import abgeschlossen: ${createdCount} Personen importiert, ${totalSkipped} Zeilen übersprungen.`,
);
setImportProgress('Import abgeschlossen.');
router.refresh(); router.refresh();
} catch (err: any) { } catch (err: any) {
console.error('Fehler beim CSV-Import', err); console.error('Fehler beim CSV-Import', err);
@ -199,6 +294,12 @@ export default function UsersCsvImportButton({ groups }: Props) {
onChange={handleImportCsv} onChange={handleImportCsv}
/> />
{importProgress && (
<p className="text-[11px] text-gray-500 dark:text-gray-400">
{importProgress}
</p>
)}
{importSummary && ( {importSummary && (
<p className="text-[11px] text-gray-600 dark:text-gray-300"> <p className="text-[11px] text-gray-600 dark:text-gray-300">
{importSummary} {importSummary}

View File

@ -12,16 +12,38 @@ import Modal from '@/components/ui/Modal';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Badge from '@/components/ui/Badge'; import Badge from '@/components/ui/Badge';
import type { User, UserGroup } from '@/generated/prisma/client'; import type { User, UserGroup } from '@/generated/prisma/client';
import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; import { PencilIcon, TrashIcon, KeyIcon, CheckIcon } from '@heroicons/react/24/outline';
import type { GroupWithUsers, SimpleGroup, UserWithAvatar } from './types'; import type { GroupWithUsers, SimpleGroup, UserWithAvatar } from './types';
import AssignGroupForm from './AssignGroupForm'; import AssignGroupForm from './AssignGroupForm';
type Props = { type Props = {
groups: GroupWithUsers[]; groups: GroupWithUsers[];
ungrouped: User[]; ungrouped: User[];
allGroups: SimpleGroup[]; allGroups: SimpleGroup[];
}; };
type PasswordChecks = {
lengthOk: boolean;
lowerOk: boolean;
upperOk: boolean;
digitOk: boolean;
specialOk: boolean;
allOk: boolean;
};
type GroupCluster = {
baseKey: string; // z.B. "Gruppe" oder "Test"
label: string; // Anzeige-Label im Haupt-Tab
groups: GroupWithUsers[]; // alle Gruppen wie "Gruppe1", "Gruppe1-Test", "Test1", "Test2" ...
totalCount: number; // Summe aller User in diesem Cluster
};
type UserRowActionsProps = {
user: UserWithAvatar;
currentUserId: string | null;
};
/* ───────── Helper: Cluster-Key aus Gruppennamen ───────── */ /* ───────── Helper: Cluster-Key aus Gruppennamen ───────── */
/** /**
* Idee: * Idee:
@ -47,20 +69,25 @@ function getBaseGroupName(name: string): string {
return withoutDigits || beforeDash; return withoutDigits || beforeDash;
} }
type GroupCluster = { function evaluatePassword(password: string): PasswordChecks {
baseKey: string; // z.B. "Gruppe" oder "Test" const lengthOk = password.length >= 12;
label: string; // Anzeige-Label im Haupt-Tab const lowerOk = /[a-z]/.test(password);
groups: GroupWithUsers[]; // alle Gruppen wie "Gruppe1", "Gruppe1-Test", "Test1", "Test2" ... const upperOk = /[A-Z]/.test(password);
totalCount: number; // Summe aller User in diesem Cluster const digitOk = /\d/.test(password);
}; const specialOk = /[^A-Za-z0-9]/.test(password);
return {
lengthOk,
lowerOk,
upperOk,
digitOk,
specialOk,
allOk: lengthOk && lowerOk && upperOk && digitOk && specialOk,
};
}
/* ───────── Zeilen-Aktionen: Bearbeiten + Löschen ───────── */ /* ───────── Zeilen-Aktionen: Bearbeiten + Löschen ───────── */
type UserRowActionsProps = {
user: UserWithAvatar;
currentUserId: string | null;
};
function UserRowActions({ user, currentUserId }: UserRowActionsProps) { function UserRowActions({ user, currentUserId }: UserRowActionsProps) {
const router = useRouter(); const router = useRouter();
@ -84,6 +111,26 @@ function UserRowActions({ user, currentUserId }: UserRowActionsProps) {
// Löschen // Löschen
const [deleting, startDeleteTransition] = useTransition(); const [deleting, startDeleteTransition] = useTransition();
// 🔹 NEU: Passwort ändern
const [pwOpen, setPwOpen] = useState(false);
const [newPassword, setNewPassword] = useState('');
const [newPasswordConfirm, setNewPasswordConfirm] = useState('');
const [pwError, setPwError] = useState<string | null>(null);
const [savingPw, startPwTransition] = useTransition();
const pwChecks = useMemo(
() => evaluatePassword(newPassword),
[newPassword],
);
const passwordsMatch =
newPassword.length > 0 && newPassword === newPasswordConfirm;
const canSubmitPw = pwChecks.allOk && passwordsMatch && !savingPw;
async function handleSaveEdit() { async function handleSaveEdit() {
startEditTransition(async () => { startEditTransition(async () => {
try { try {
@ -140,6 +187,61 @@ function UserRowActions({ user, currentUserId }: UserRowActionsProps) {
}); });
} }
async function handleChangePassword() {
setPwError(null);
if (!pwChecks.allOk) {
setPwError(
'Das Passwort erfüllt noch nicht alle Sicherheitskriterien.',
);
return;
}
if (!passwordsMatch) {
setPwError('Die Passwörter stimmen nicht überein.');
return;
}
startPwTransition(async () => {
try {
const res = await fetch(
`/api/users/${encodeURIComponent(user.nwkennung)}/password`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: newPassword }),
},
);
if (!res.ok) {
const data = await res.json().catch(() => null);
console.error(
'Fehler beim Ändern des Passworts',
res.status,
data,
);
setPwError(
data?.error ??
'Fehler beim Ändern des Passworts. Details in der Konsole.',
);
return;
}
// success
setPwOpen(false);
setNewPassword('');
setNewPasswordConfirm('');
setPwError(null);
router.refresh();
window.alert('Passwort wurde aktualisiert.');
} catch (err) {
console.error('Fehler beim Ändern des Passworts', err);
setPwError('Fehler beim Ändern des Passworts.');
}
});
}
return ( return (
<> <>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -153,6 +255,17 @@ function UserRowActions({ user, currentUserId }: UserRowActionsProps) {
onClick={() => setEditOpen(true)} onClick={() => setEditOpen(true)}
/> />
{/* 🔹 NEU: Passwort ändern */}
<Button
type="button"
size="lg"
variant="soft"
tone="indigo"
icon={<KeyIcon className="size-5" />}
onClick={() => setPwOpen(true)}
/>
{/* Löschen nur anzeigen, wenn NICHT eigener User */} {/* Löschen nur anzeigen, wenn NICHT eigener User */}
{!isCurrentUser && ( {!isCurrentUser && (
<Button <Button
@ -182,6 +295,11 @@ function UserRowActions({ user, currentUserId }: UserRowActionsProps) {
label: 'Bearbeiten', label: 'Bearbeiten',
onClick: () => setEditOpen(true), onClick: () => setEditOpen(true),
}, },
{
id: 'change-password',
label: savingPw ? 'Passwort …' : 'Passwort ändern',
onClick: () => setPwOpen(true),
},
// Delete nur, wenn nicht eigener User // Delete nur, wenn nicht eigener User
...(!isCurrentUser ...(!isCurrentUser
? [ ? [
@ -281,6 +399,165 @@ function UserRowActions({ user, currentUserId }: UserRowActionsProps) {
</div> </div>
</form> </form>
</Modal> </Modal>
{/* 🔹 NEU: Passwort ändern-Modal */}
<Modal
open={pwOpen}
onClose={() => setPwOpen(false)}
title="Passwort ändern"
tone="warning"
variant="centered"
size="md"
primaryAction={{
label: savingPw ? 'Speichere …' : 'Passwort setzen',
onClick: handleChangePassword,
variant: 'primary',
disabled: !canSubmitPw,
}}
secondaryAction={{
label: 'Abbrechen',
onClick: () => setPwOpen(false),
variant: 'secondary',
}}
>
<form
onSubmit={(e) => {
e.preventDefault();
handleChangePassword();
}}
className="space-y-3 text-sm"
>
<p className="text-xs text-gray-600 dark:text-gray-400">
Das neue Passwort gilt sofort für den Benutzer{' '}
<strong>{user.arbeitsname || user.nwkennung}</strong>.
</p>
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300">
Neues Passwort *
</label>
<input
type="password"
required
minLength={12}
className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-2.5 py-1.5 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
value={newPassword}
onChange={(e) => {
setNewPassword(e.target.value);
setPwError(null);
}}
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300">
Passwort bestätigen *
</label>
<input
type="password"
required
minLength={12}
className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-2.5 py-1.5 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
value={newPasswordConfirm}
onChange={(e) => {
setNewPasswordConfirm(e.target.value);
setPwError(null);
}}
/>
</div>
<div className="mt-2 rounded-md bg-gray-50 p-2 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
<p className="font-medium mb-1">Sicherheitskriterien:</p>
<ul className="space-y-0.5">
<li
className={
(pwChecks.lengthOk
? 'text-green-700 dark:text-green-500'
: 'text-gray-500 dark:text-gray-400') +
' flex items-center gap-1.5'
}
>
{pwChecks.lengthOk ? (
<CheckIcon className="h-4 w-4 shrink-0" />
) : (
<span className="inline-block w-4 text-center"></span>
)}
<span>Mindestens 12 Zeichen</span>
</li>
<li
className={
(pwChecks.lowerOk
? 'text-green-700 dark:text-green-500'
: 'text-gray-500 dark:text-gray-400') +
' flex items-center gap-1.5'
}
>
{pwChecks.lowerOk ? (
<CheckIcon className="h-4 w-4 shrink-0" />
) : (
<span className="inline-block w-4 text-center"></span>
)}
<span>Mindestens ein Kleinbuchstabe (az)</span>
</li>
<li
className={
(pwChecks.upperOk
? 'text-green-700 dark:text-green-500'
: 'text-gray-500 dark:text-gray-400') +
' flex items-center gap-1.5'
}
>
{pwChecks.upperOk ? (
<CheckIcon className="h-4 w-4 shrink-0" />
) : (
<span className="inline-block w-4 text-center"></span>
)}
<span>Mindestens ein Großbuchstabe (AZ)</span>
</li>
<li
className={
(pwChecks.digitOk
? 'text-green-700 dark:text-green-500'
: 'text-gray-500 dark:text-gray-400') +
' flex items-center gap-1.5'
}
>
{pwChecks.digitOk ? (
<CheckIcon className="h-4 w-4 shrink-0" />
) : (
<span className="inline-block w-4 text-center"></span>
)}
<span>Mindestens eine Ziffer (09)</span>
</li>
<li
className={
(pwChecks.specialOk
? 'text-green-700 dark:text-green-500'
: 'text-gray-500 dark:text-gray-400') +
' flex items-center gap-1.5'
}
>
{pwChecks.specialOk ? (
<CheckIcon className="h-4 w-4 shrink-0" />
) : (
<span className="inline-block w-4 text-center"></span>
)}
<span>Mindestens ein Sonderzeichen (!, ?, #, )</span>
</li>
</ul>
</div>
{pwError && (
<p className="text-xs text-red-600 dark:text-red-400">
{pwError}
</p>
)}
</form>
</Modal>
</> </>
); );
} }

View File

@ -23,6 +23,8 @@ export async function GET(_req: Request, ctx: RouteContext) {
group: true, group: true,
location: true, location: true,
tags: true, tags: true,
parentDevice: true,
accessories: true,
}, },
}); });
@ -42,15 +44,23 @@ export async function GET(_req: Request, ctx: RouteContext) {
ipv6Address: device.ipv6Address, ipv6Address: device.ipv6Address,
macAddress: device.macAddress, macAddress: device.macAddress,
username: device.username, username: device.username,
// passwordHash bewusst weggelassen
group: device.group?.name ?? null, group: device.group?.name ?? null,
location: device.location?.name ?? null, location: device.location?.name ?? null,
tags: device.tags.map((t) => t.name), tags: device.tags.map((t) => t.name),
// Verleih
loanedTo: device.loanedTo, loanedTo: device.loanedTo,
loanedFrom: device.loanedFrom ? device.loanedFrom.toISOString() : null, loanedFrom: device.loanedFrom
loanedUntil: device.loanedUntil ? device.loanedUntil.toISOString() : null, ? device.loanedFrom.toISOString()
: null,
loanedUntil: device.loanedUntil
? device.loanedUntil.toISOString()
: null,
loanComment: device.loanComment, loanComment: device.loanComment,
parentInventoryNumber: device.parentDeviceId,
parentName: device.parentDevice?.name ?? null,
accessories: device.accessories.map((a) => ({
inventoryNumber: a.inventoryNumber,
name: a.name,
})),
createdAt: device.createdAt.toISOString(), createdAt: device.createdAt.toISOString(),
updatedAt: device.updatedAt.toISOString(), updatedAt: device.updatedAt.toISOString(),
}); });
@ -80,7 +90,7 @@ export async function POST(req: Request) {
username, username,
passwordHash, passwordHash,
tags, tags,
// Verleih-Felder parentInventoryNumber,
loanedTo, loanedTo,
loanedFrom, loanedFrom,
loanedUntil, loanedUntil,
@ -149,6 +159,20 @@ export async function POST(req: Request) {
.filter((t) => t.length > 0) .filter((t) => t.length > 0)
: []; : [];
// ⬅️ NEU: Parent/Hauptgerät ermitteln (falls angegeben)
let parentDeviceConnect: Prisma.DeviceCreateInput['parentDevice'] | undefined;
if (typeof parentInventoryNumber === 'string') {
const trimmedParent = parentInventoryNumber.trim();
// nicht auf sich selbst zeigen und nicht leer
if (trimmedParent && trimmedParent !== inventoryNumber) {
parentDeviceConnect = {
connect: { inventoryNumber: trimmedParent },
};
}
}
const created = await prisma.device.create({ const created = await prisma.device.create({
data: { data: {
inventoryNumber, inventoryNumber,
@ -163,18 +187,14 @@ export async function POST(req: Request) {
macAddress: macAddress ?? null, macAddress: macAddress ?? null,
username: username ?? null, username: username ?? null,
passwordHash: passwordHash ?? null, passwordHash: passwordHash ?? null,
// Verleih-Felder
loanedTo: loanedTo ?? null, loanedTo: loanedTo ?? null,
loanedFrom: loanedFrom ? new Date(loanedFrom) : null, loanedFrom: loanedFrom ? new Date(loanedFrom) : null,
loanedUntil: loanedUntil ? new Date(loanedUntil) : null, loanedUntil: loanedUntil ? new Date(loanedUntil) : null,
loanComment: loanComment ?? null, loanComment: loanComment ?? null,
groupId, groupId,
locationId, locationId,
// ⬇️ statt createdBy.connect -> einfach FK setzen
createdById: canConnectUser && userId ? userId : null, createdById: canConnectUser && userId ? userId : null,
...(parentDeviceConnect ? { parentDevice: parentDeviceConnect } : {}),
...(tagNames.length ...(tagNames.length
? { ? {
@ -191,7 +211,9 @@ export async function POST(req: Request) {
group: true, group: true,
location: true, location: true,
tags: true, tags: true,
createdBy: true, // darf trotzdem included werden, Prisma nutzt createdById parentDevice: true,
accessories: true,
createdBy: true,
}, },
}); });
@ -265,7 +287,7 @@ export async function POST(req: Request) {
}); });
} }
return NextResponse.json( return NextResponse.json(
{ {
inventoryNumber: created.inventoryNumber, inventoryNumber: created.inventoryNumber,
name: created.name, name: created.name,
@ -290,6 +312,16 @@ export async function POST(req: Request) {
? created.loanedUntil.toISOString() ? created.loanedUntil.toISOString()
: null, : null,
loanComment: created.loanComment, loanComment: created.loanComment,
// ⬅️ NEU: Parent + Accessories wie im GET
parentInventoryNumber: created.parentDeviceId,
parentName: created.parentDevice?.name ?? null,
accessories: created.accessories.map((a) => ({
inventoryNumber: a.inventoryNumber,
name: a.name,
})),
createdAt: created.createdAt.toISOString(),
updatedAt: created.updatedAt.toISOString(), updatedAt: created.updatedAt.toISOString(),
}, },
{ status: 201 }, { status: 201 },
@ -337,6 +369,7 @@ export async function PATCH(req: Request, ctx: RouteContext) {
group: true, group: true,
location: true, location: true,
tags: true, tags: true,
parentDevice: true,
}, },
}); });
@ -363,6 +396,28 @@ export async function PATCH(req: Request, ctx: RouteContext) {
loanComment: body.loanComment ?? null, loanComment: body.loanComment ?? null,
}; };
// Hauptgerät / Parent
if ('parentInventoryNumber' in body) {
const raw = body.parentInventoryNumber;
if (typeof raw === 'string') {
const trimmed = raw.trim();
if (trimmed.length > 0 && trimmed !== existing.inventoryNumber) {
data.parentDevice = {
connect: { inventoryNumber: trimmed },
};
} else {
// leerer String oder auf sich selbst → trennen
data.parentDevice = { disconnect: true };
}
} else if (raw == null) {
// null / undefined explizit → trennen
data.parentDevice = { disconnect: true };
}
}
if (canConnectUpdatedBy && userId) { if (canConnectUpdatedBy && userId) {
data.updatedBy = { data.updatedBy = {
connect: { nwkennung: userId }, connect: { nwkennung: userId },
@ -425,6 +480,7 @@ export async function PATCH(req: Request, ctx: RouteContext) {
group: true, group: true,
location: true, location: true,
tags: true, tags: true,
parentDevice: true,
}, },
}); });
@ -454,7 +510,8 @@ export async function PATCH(req: Request, ctx: RouteContext) {
| 'loanedTo' | 'loanedTo'
| 'loanedFrom' | 'loanedFrom'
| 'loanedUntil' | 'loanedUntil'
| 'loanComment'; | 'loanComment'
| 'parentDevice';
before: string | null; before: string | null;
after: string | null; after: string | null;
}[] = []; }[] = [];
@ -555,6 +612,29 @@ export async function PATCH(req: Request, ctx: RouteContext) {
}); });
} }
// Parent / Hauptgerät-Diff
const beforeParent =
existing.parentDeviceId != null
? `${existing.parentDeviceId}${
existing.parentDevice?.name ? ' ' + existing.parentDevice.name : ''
}`
: null;
const afterParent =
updated.parentDeviceId != null
? `${updated.parentDeviceId}${
updated.parentDevice?.name ? ' ' + updated.parentDevice.name : ''
}`
: null;
if (beforeParent !== afterParent) {
changes.push({
field: 'parentDevice',
before: beforeParent,
after: afterParent,
});
}
if (changes.length > 0) { if (changes.length > 0) {
const snapshot: Prisma.JsonObject = { const snapshot: Prisma.JsonObject = {
before: { before: {
@ -581,6 +661,8 @@ export async function PATCH(req: Request, ctx: RouteContext) {
? existing.loanedUntil.toISOString() ? existing.loanedUntil.toISOString()
: null, : null,
loanComment: existing.loanComment, loanComment: existing.loanComment,
parentInventoryNumber: existing.parentDeviceId,
parentName: existing.parentDevice?.name ?? null,
createdAt: existing.createdAt.toISOString(), createdAt: existing.createdAt.toISOString(),
updatedAt: existing.updatedAt.toISOString(), updatedAt: existing.updatedAt.toISOString(),
}, },
@ -608,6 +690,8 @@ export async function PATCH(req: Request, ctx: RouteContext) {
? updated.loanedUntil.toISOString() ? updated.loanedUntil.toISOString()
: null, : null,
loanComment: updated.loanComment, loanComment: updated.loanComment,
parentInventoryNumber: updated.parentDeviceId,
parentName: updated.parentDevice?.name ?? null,
createdAt: updated.createdAt.toISOString(), createdAt: updated.createdAt.toISOString(),
updatedAt: updated.updatedAt.toISOString(), updatedAt: updated.updatedAt.toISOString(),
}, },
@ -653,6 +737,8 @@ export async function PATCH(req: Request, ctx: RouteContext) {
? updated.loanedUntil.toISOString() ? updated.loanedUntil.toISOString()
: null, : null,
loanComment: updated.loanComment, loanComment: updated.loanComment,
parentInventoryNumber: updated.parentDeviceId,
parentName: updated.parentDevice?.name ?? null,
updatedAt: updated.updatedAt.toISOString(), updatedAt: updated.updatedAt.toISOString(),
}); });
} }
@ -688,3 +774,118 @@ export async function PATCH(req: Request, ctx: RouteContext) {
return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 }); return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 });
} }
} }
export async function DELETE(_req: Request, ctx: RouteContext) {
const { id } = await ctx.params;
if (!id) {
return NextResponse.json({ error: 'MISSING_ID' }, { status: 400 });
}
try {
const existing = await prisma.device.findUnique({
where: { inventoryNumber: id },
include: {
group: true,
location: true,
tags: true,
accessories: true,
},
});
if (!existing) {
return NextResponse.json({ error: 'NOT_FOUND' }, { status: 404 });
}
// ❗ Optional: Hauptgerät mit Zubehör nicht löschen
if (existing.accessories && existing.accessories.length > 0) {
return NextResponse.json(
{
error: 'HAS_ACCESSORIES',
message:
'Das Gerät hat noch Zubehör und kann nicht gelöscht werden. Entferne oder verschiebe zuerst das Zubehör.',
},
{ status: 409 },
);
}
// User für History ermitteln
const userId = await getCurrentUserId();
let changedById: string | null = null;
if (userId) {
const user = await prisma.user.findUnique({
where: { nwkennung: userId },
select: { nwkennung: true },
});
if (user) {
changedById = userId;
} else {
console.warn(
`[DELETE /api/devices/${id}] User mit nwkennung=${userId} nicht gefunden changedById wird nicht gesetzt.`,
);
}
}
const snapshot: Prisma.JsonObject = {
before: {
inventoryNumber: existing.inventoryNumber,
name: existing.name,
manufacturer: existing.manufacturer,
model: existing.model,
serialNumber: existing.serialNumber,
productNumber: existing.productNumber,
comment: existing.comment,
ipv4Address: existing.ipv4Address,
ipv6Address: existing.ipv6Address,
macAddress: existing.macAddress,
username: existing.username,
passwordHash: existing.passwordHash,
group: existing.group?.name ?? null,
location: existing.location?.name ?? null,
tags: existing.tags.map((t) => t.name),
loanedTo: existing.loanedTo,
loanedFrom: existing.loanedFrom
? existing.loanedFrom.toISOString()
: null,
loanedUntil: existing.loanedUntil
? existing.loanedUntil.toISOString()
: null,
loanComment: existing.loanComment,
createdAt: existing.createdAt.toISOString(),
updatedAt: existing.updatedAt.toISOString(),
},
after: null,
changes: [],
};
// History + Delete in einer Transaktion
await prisma.$transaction([
prisma.deviceHistory.create({
data: {
deviceId: existing.inventoryNumber,
changeType: 'DELETED',
snapshot,
changedById,
},
}),
prisma.device.delete({
where: { inventoryNumber: id },
}),
]);
// Socket.IO Event feuern
const io = (global as any).devicesIo as IOServer | undefined;
if (io) {
io.emit('device:deleted', { inventoryNumber: id });
}
return NextResponse.json({ ok: true });
} catch (err) {
console.error('[DELETE /api/devices/[id]]', err);
return NextResponse.json(
{ error: 'INTERNAL_ERROR' },
{ status: 500 },
);
}
}

View File

@ -12,7 +12,10 @@ export async function GET() {
group: true, group: true,
location: true, location: true,
tags: true, tags: true,
parentDevice: true,
accessories: true,
}, },
orderBy: { inventoryNumber: 'asc' },
}); });
return NextResponse.json( return NextResponse.json(
@ -33,15 +36,31 @@ export async function GET() {
location: d.location?.name ?? null, location: d.location?.name ?? null,
tags: d.tags.map((t) => t.name), tags: d.tags.map((t) => t.name),
loanedTo: d.loanedTo, loanedTo: d.loanedTo,
loanedFrom: d.loanedFrom ? d.loanedFrom.toISOString() : null, loanedFrom: d.loanedFrom
loanedUntil: d.loanedUntil ? d.loanedUntil.toISOString() : null, ? d.loanedFrom.toISOString()
: null,
loanedUntil: d.loanedUntil
? d.loanedUntil.toISOString()
: null,
loanComment: d.loanComment, loanComment: d.loanComment,
createdAt: d.createdAt.toISOString(),
updatedAt: d.updatedAt.toISOString(), updatedAt: d.updatedAt.toISOString(),
// 🔹 wichtig für dein Dropdown:
parentInventoryNumber: d.parentDeviceId, // Hauptgerät-Nummer oder null
parentName: d.parentDevice?.name ?? null,
accessories: d.accessories.map((a) => ({
inventoryNumber: a.inventoryNumber,
name: a.name,
})),
})), })),
); );
} catch (err) { } catch (err) {
console.error('[GET /api/devices]', err); console.error('[GET /api/devices]', err);
return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 }); return NextResponse.json(
{ error: 'INTERNAL_ERROR' },
{ status: 500 },
);
} }
} }
@ -65,11 +84,11 @@ export async function POST(req: Request) {
username, username,
passwordHash, passwordHash,
tags, tags,
// Verleih-Felder
loanedTo, loanedTo,
loanedFrom, loanedFrom,
loanedUntil, loanedUntil,
loanComment, loanComment,
parentInventoryNumber,
} = body; } = body;
if (!inventoryNumber || !name) { if (!inventoryNumber || !name) {
@ -94,8 +113,8 @@ export async function POST(req: Request) {
if (userId) { if (userId) {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: userId }, where: { nwkennung: userId },
select: { id: true }, select: { nwkennung: true },
}); });
if (user) { if (user) {
canConnectUser = true; canConnectUser = true;
@ -132,6 +151,47 @@ export async function POST(req: Request) {
? tags.map((t: unknown) => String(t).trim()).filter(Boolean) ? tags.map((t: unknown) => String(t).trim()).filter(Boolean)
: []; : [];
// 🔹 NEU: Hauptgerät-Relation (optional)
let parentDeviceRelation:
Prisma.DeviceCreateInput['parentDevice'] | undefined;
if (
parentInventoryNumber &&
typeof parentInventoryNumber === 'string' &&
parentInventoryNumber.trim() !== ''
) {
const trimmed = parentInventoryNumber.trim();
// sich selbst als Hauptgerät → Fehler
if (trimmed === inventoryNumber) {
return NextResponse.json(
{
error:
'Ein Gerät kann nicht gleichzeitig sein eigenes Hauptgerät sein.',
},
{ status: 400 },
);
}
const parent = await prisma.device.findUnique({
where: { inventoryNumber: trimmed },
select: { inventoryNumber: true },
});
if (!parent) {
return NextResponse.json(
{
error: `Hauptgerät mit Inventar-Nr. ${trimmed} wurde nicht gefunden.`,
},
{ status: 400 },
);
}
parentDeviceRelation = {
connect: { inventoryNumber: trimmed },
};
}
const created = await prisma.device.create({ const created = await prisma.device.create({
data: { data: {
inventoryNumber, inventoryNumber,
@ -153,6 +213,11 @@ export async function POST(req: Request) {
loanedUntil: loanedUntil ? new Date(loanedUntil) : null, loanedUntil: loanedUntil ? new Date(loanedUntil) : null,
loanComment: loanComment ?? null, loanComment: loanComment ?? null,
// 🔹 optionales Hauptgerät
...(parentDeviceRelation
? { parentDevice: parentDeviceRelation }
: {}),
...(groupId ...(groupId
? { ? {
group: { group: {
@ -169,8 +234,13 @@ export async function POST(req: Request) {
} }
: {}), : {}),
// 🔹 FIX: User via nwkennung verbinden
...(canConnectUser && userId ...(canConnectUser && userId
? { createdBy: { connect: { id: userId } } } ? {
createdBy: {
connect: { nwkennung: userId },
},
}
: {}), : {}),
...(tagNames.length ...(tagNames.length
@ -188,6 +258,10 @@ export async function POST(req: Request) {
group: true, group: true,
location: true, location: true,
tags: true, tags: true,
// 🔹 NEU: parentDevice für Antwort/Event
parentDevice: {
select: { inventoryNumber: true, name: true },
},
}, },
}); });
@ -217,6 +291,9 @@ export async function POST(req: Request) {
? created.loanedUntil.toISOString() ? created.loanedUntil.toISOString()
: null, : null,
loanComment: created.loanComment, loanComment: created.loanComment,
// 🔹 NEU
parentInventoryNumber: created.parentDeviceId,
parentName: created.parentDevice?.name ?? null,
createdAt: created.createdAt.toISOString(), createdAt: created.createdAt.toISOString(),
updatedAt: created.updatedAt.toISOString(), updatedAt: created.updatedAt.toISOString(),
}, },
@ -262,34 +339,37 @@ export async function POST(req: Request) {
} }
return NextResponse.json( return NextResponse.json(
{ {
inventoryNumber: created.inventoryNumber, inventoryNumber: created.inventoryNumber,
name: created.name, name: created.name,
manufacturer: created.manufacturer, manufacturer: created.manufacturer,
model: created.model, model: created.model,
serialNumber: created.serialNumber, serialNumber: created.serialNumber,
productNumber: created.productNumber, productNumber: created.productNumber,
comment: created.comment, comment: created.comment,
ipv4Address: created.ipv4Address, ipv4Address: created.ipv4Address,
ipv6Address: created.ipv6Address, ipv6Address: created.ipv6Address,
macAddress: created.macAddress, macAddress: created.macAddress,
username: created.username, username: created.username,
passwordHash: created.passwordHash, passwordHash: created.passwordHash,
group: created.group?.name ?? null, group: created.group?.name ?? null,
location: created.location?.name ?? null, location: created.location?.name ?? null,
tags: created.tags.map((t) => t.name), tags: created.tags.map((t) => t.name),
loanedTo: created.loanedTo, loanedTo: created.loanedTo,
loanedFrom: created.loanedFrom loanedFrom: created.loanedFrom
? created.loanedFrom.toISOString() ? created.loanedFrom.toISOString()
: null, : null,
loanedUntil: created.loanedUntil loanedUntil: created.loanedUntil
? created.loanedUntil.toISOString() ? created.loanedUntil.toISOString()
: null, : null,
loanComment: created.loanComment, loanComment: created.loanComment,
updatedAt: created.updatedAt.toISOString(), // 🔹 NEU
}, parentInventoryNumber: created.parentDeviceId,
{ status: 201 }, parentName: created.parentDevice?.name ?? null,
); updatedAt: created.updatedAt.toISOString(),
},
{ status: 201 },
);
} catch (err) { } catch (err) {
console.error('[POST /api/devices]', err); console.error('[POST /api/devices]', err);
return NextResponse.json( return NextResponse.json(

View File

@ -4,7 +4,37 @@ import { prisma } from '@/lib/prisma';
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const { name } = await req.json(); const body = await req.json();
// 🔹 BULK: { names: string[] }
if (Array.isArray(body?.names)) {
const rawNames = body.names as unknown[];
const trimmedNames = rawNames
.filter((n): n is string => typeof n === 'string')
.map((n) => n.trim())
.filter((n) => n.length > 0);
const uniqueNames = Array.from(new Set(trimmedNames));
const groups = [];
for (const name of uniqueNames) {
const group = await prisma.userGroup.upsert({
where: { name },
update: {},
create: { name },
});
groups.push({ id: group.id, name: group.name });
}
return NextResponse.json(
{ groups },
{ status: 200 },
);
}
// 🔹 SINGLE: { name: string } wie bisher
const { name } = body;
if (!name || typeof name !== 'string') { if (!name || typeof name !== 'string') {
return NextResponse.json( return NextResponse.json(

View File

@ -0,0 +1,94 @@
// app/api/users/[nwkennung]/password/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { hash } from 'bcryptjs';
function isStrongPassword(password: string): boolean {
const lengthOk = password.length >= 12;
const lowerOk = /[a-z]/.test(password);
const upperOk = /[A-Z]/.test(password);
const digitOk = /\d/.test(password);
const specialOk = /[^A-Za-z0-9]/.test(password);
return lengthOk && lowerOk && upperOk && digitOk && specialOk;
}
export async function PATCH(req: NextRequest) {
try {
const body = (await req.json()) as { password?: string };
if (!body.password || typeof body.password !== 'string') {
return NextResponse.json(
{ error: 'Passwort ist erforderlich.' },
{ status: 400 },
);
}
if (!isStrongPassword(body.password)) {
return NextResponse.json(
{
error:
'Das Passwort muss mindestens 12 Zeichen lang sein und Großbuchstaben, Kleinbuchstaben, Ziffern und Sonderzeichen enthalten.',
},
{ status: 400 },
);
}
// 🔹 nwkennung direkt aus der URL ziehen: /api/users/<nwkennung>/password
const pathname = req.nextUrl.pathname; // z.B. "/api/users/nw083118/password"
const segments = pathname.split('/').filter(Boolean); // ["api","users","nw083118","password"]
const rawParam = segments[2]; // Index 2 = <nwkennung>
console.log(
'[PATCH /api/users/[nwkennung]/password] pathname =',
pathname,
'segments =',
segments,
'rawParam =',
rawParam,
);
if (!rawParam) {
return NextResponse.json(
{ error: 'Pfad-Parameter "nwkennung" fehlt.' },
{ status: 400 },
);
}
const normalizedNwkennung = rawParam.trim().toLowerCase();
const passwordHash = await hash(body.password, 10);
const user = await prisma.user.update({
where: { nwkennung: normalizedNwkennung },
data: {
passwordHash,
},
select: {
nwkennung: true,
},
});
return NextResponse.json(
{ success: true, user },
{ status: 200 },
);
} catch (err: any) {
console.error('[PATCH /api/users/[nwkennung]/password]', err);
if (err?.code === 'P2025') {
return NextResponse.json(
{ error: 'Benutzer wurde nicht gefunden.' },
{ status: 404 },
);
}
return NextResponse.json(
{
error:
'Interner Serverfehler beim Aktualisieren des Passworts.',
},
{ status: 500 },
);
}
}

View File

@ -1,18 +1,18 @@
// app/api/users/[id]/route.ts // app/api/users/[nwkennung]/route.ts
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
// Next 15/16: params ist ein Promise // Next 15/16: params ist ein Promise
type ParamsPromise = Promise<{ id: string }>; type ParamsPromise = Promise<{ nwkennung: string }>;
export async function PATCH( export async function PATCH(
req: Request, req: Request,
{ params }: { params: ParamsPromise }, { params }: { params: ParamsPromise },
) { ) {
try { try {
const { id } = await params; // id == nwkennung const { nwkennung } = await params;
if (!id) { if (!nwkennung) {
return NextResponse.json( return NextResponse.json(
{ error: 'User-ID (nwkennung) fehlt in der URL.' }, { error: 'User-ID (nwkennung) fehlt in der URL.' },
{ status: 400 }, { status: 400 },
@ -38,7 +38,7 @@ export async function PATCH(
} }
const updated = await prisma.user.update({ const updated = await prisma.user.update({
where: { nwkennung: id }, // 🔹 where: { nwkennung },
data, data,
}); });
@ -55,7 +55,7 @@ export async function PATCH(
{ status: 200 }, { status: 200 },
); );
} catch (err) { } catch (err) {
console.error('[PATCH /api/users/[id]]', err); console.error('[PATCH /api/users/[nwkennung]]', err);
return NextResponse.json( return NextResponse.json(
{ error: 'Interner Serverfehler beim Aktualisieren des Users.' }, { error: 'Interner Serverfehler beim Aktualisieren des Users.' },
{ status: 500 }, { status: 500 },
@ -68,9 +68,9 @@ export async function DELETE(
{ params }: { params: ParamsPromise }, { params }: { params: ParamsPromise },
) { ) {
try { try {
const { id } = await params; // id == nwkennung const { nwkennung } = await params;
if (!id || id === 'undefined') { if (!nwkennung || nwkennung === 'undefined') {
return NextResponse.json( return NextResponse.json(
{ error: 'User-ID (nwkennung) fehlt oder ist ungültig.' }, { error: 'User-ID (nwkennung) fehlt oder ist ungültig.' },
{ status: 400 }, { status: 400 },
@ -78,12 +78,12 @@ export async function DELETE(
} }
await prisma.user.delete({ await prisma.user.delete({
where: { nwkennung: id }, // 🔹 where: { nwkennung },
}); });
return NextResponse.json({ ok: true }, { status: 200 }); return NextResponse.json({ ok: true }, { status: 200 });
} catch (err) { } catch (err) {
console.error('[DELETE /api/users/[id]]', err); console.error('[DELETE /api/users/[nwkennung]]', err);
return NextResponse.json( return NextResponse.json(
{ error: 'Interner Serverfehler beim Löschen des Users.' }, { error: 'Interner Serverfehler beim Löschen des Users.' },
{ status: 500 }, { status: 500 },

View File

@ -43,12 +43,98 @@ export async function GET() {
} }
} }
// 🔹 POST: CSV-Import + manuelle Anlage // 🔹 POST: Single + Bulk
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const body = (await req.json()) as { const body = await req.json();
nwkennung?: string | null; // aus CSV oder Formular
email?: string | null; // optional, falls du später Email mit importierst // 🔹 BULK: { users: [...] }
if (Array.isArray((body as any)?.users)) {
type UserPayload = {
nwkennung: string;
email?: string | null;
arbeitsname: string;
firstName: string;
lastName: string;
groupId?: string | null;
};
const users = (body as any).users as UserPayload[];
let createdCount = 0;
let skippedCount = 0;
const errors: { nwkennung: string; error: string }[] = [];
for (const u of users) {
const {
nwkennung,
email,
arbeitsname,
firstName,
lastName,
groupId,
} = u;
if (!nwkennung || !lastName || !firstName || !arbeitsname) {
skippedCount++;
errors.push({
nwkennung: nwkennung ?? '',
error:
'nwkennung, lastName, firstName und arbeitsname sind Pflichtfelder.',
});
continue;
}
const normalizedNwkennung = nwkennung.trim().toLowerCase();
try {
await prisma.user.upsert({
where: { nwkennung: normalizedNwkennung },
update: {
lastName,
firstName,
arbeitsname,
groupId: groupId ?? null,
email: email ?? undefined,
},
create: {
nwkennung: normalizedNwkennung,
lastName,
firstName,
arbeitsname,
groupId: groupId ?? null,
email: email ?? null,
},
});
createdCount++;
} catch (err) {
console.error(
'[POST /api/users] Fehler bei Bulk-Upsert:',
err,
);
skippedCount++;
errors.push({
nwkennung: normalizedNwkennung,
error: 'Datenbankfehler beim Upsert.',
});
}
}
return NextResponse.json(
{
success: true,
createdCount,
skippedCount,
errors,
},
{ status: 200 },
);
}
// 🔹 SINGLE: wie bisher (z.B. für manuelle Anlage)
const single = body as {
nwkennung?: string | null;
email?: string | null;
arbeitsname: string; arbeitsname: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
@ -62,9 +148,8 @@ export async function POST(req: NextRequest) {
firstName, firstName,
lastName, lastName,
groupId, groupId,
} = body; } = single;
// Pflichtfelder: Name + Arbeitsname
if (!nwkennung || !lastName || !firstName || !arbeitsname) { if (!nwkennung || !lastName || !firstName || !arbeitsname) {
return NextResponse.json( return NextResponse.json(
{ {
@ -84,6 +169,7 @@ export async function POST(req: NextRequest) {
firstName, firstName,
arbeitsname, arbeitsname,
groupId: groupId ?? null, groupId: groupId ?? null,
email: email ?? undefined,
}, },
create: { create: {
nwkennung: normalizedNwkennung, nwkennung: normalizedNwkennung,
@ -91,6 +177,7 @@ export async function POST(req: NextRequest) {
firstName, firstName,
arbeitsname, arbeitsname,
groupId: groupId ?? null, groupId: groupId ?? null,
email: email ?? null,
}, },
}); });

210
components/GlobalSearch.tsx Normal file
View File

@ -0,0 +1,210 @@
// /components/GlobalSearch.tsx
'use client';
import { useEffect, useMemo, useState } from 'react';
import { Combobox } from '@headlessui/react';
import { MagnifyingGlassIcon } from '@heroicons/react/20/solid';
import clsx from 'clsx';
type DeviceSearchItem = {
inventoryNumber: string;
name: string | null;
manufacturer: string | null;
group: string | null;
location: string | null;
};
type GlobalSearchProps = {
onDeviceSelected?: (inventoryNumber: string) => void;
};
export default function GlobalSearch({ onDeviceSelected }: GlobalSearchProps) {
const [query, setQuery] = useState('');
const [allDevices, setAllDevices] = useState<DeviceSearchItem[]>([]);
const [loading, setLoading] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const [hasLoaded, setHasLoaded] = useState(false);
// Geräte nur einmal laden, wenn das erste Mal gesucht wird
useEffect(() => {
if (!query.trim() || hasLoaded) return;
let cancelled = false;
setLoading(true);
setLoadError(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 any[];
if (!cancelled) {
const mapped: DeviceSearchItem[] = data.map((d) => ({
inventoryNumber: d.inventoryNumber,
name: d.name ?? null,
manufacturer: d.manufacturer ?? null,
group: d.group ?? null,
location: d.location ?? null,
}));
setAllDevices(mapped);
setHasLoaded(true);
}
} catch (err: any) {
console.error('Fehler beim Laden der Geräte für die Suche', err);
if (!cancelled) {
setLoadError(
err instanceof Error
? err.message
: 'Netzwerkfehler beim Laden der Geräteliste.',
);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
loadDevices();
return () => {
cancelled = true;
};
}, [query, hasLoaded]);
const filteredDevices = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return [] as DeviceSearchItem[];
if (!allDevices.length) return [] as DeviceSearchItem[];
const matches = allDevices.filter((d) => {
const haystack = [
d.inventoryNumber,
d.name ?? '',
d.manufacturer ?? '',
d.group ?? '',
d.location ?? '',
]
.join(' ')
.toLowerCase();
return haystack.includes(q);
});
return matches.slice(0, 10); // max 10 Treffer
}, [query, allDevices]);
const hasMenu =
query.trim().length > 0 && (loading || loadError || filteredDevices.length > 0);
const handleSelect = (item: DeviceSearchItem | null) => {
if (!item) return;
onDeviceSelected?.(item.inventoryNumber);
// Query nach Auswahl leeren
setQuery('');
};
return (
<Combobox value={null} onChange={handleSelect} nullable>
<div className="relative w-full">
{/* Suchfeld */}
<MagnifyingGlassIcon
aria-hidden="true"
className="pointer-events-none absolute left-3 top-1/2 size-5 -translate-y-1/2 text-gray-400 dark:text-gray-500"
/>
<Combobox.Input
className="block w-full rounded-xl border-0 bg-gray-50 py-1.5 pl-10 pr-3 text-sm text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none dark:bg-gray-800 dark:text-white dark:ring-gray-700 dark:placeholder:text-gray-500"
placeholder="Suchen…"
aria-label="Suchen"
displayValue={() => query}
onChange={(event) => setQuery(event.target.value)}
/>
{/* Dropdown-Menü unterhalb */}
{hasMenu && (
<Combobox.Options
className={clsx(
'absolute z-50 mt-1 max-h-80 w-full overflow-auto rounded-md bg-white py-1 text-sm shadow-lg ring-1 ring-black/5',
'dark:bg-gray-800 dark:ring-white/10',
)}
>
{loading && (
<div className="px-3 py-2 text-xs text-gray-500 dark:text-gray-400">
Suche wird vorbereitet
</div>
)}
{loadError && (
<div className="px-3 py-2 text-xs text-rose-500">
{loadError}
</div>
)}
{!loading && !loadError && filteredDevices.length === 0 && (
<div className="px-3 py-2 text-xs text-gray-500 dark:text-gray-400">
Keine Treffer für {query.trim()}.
</div>
)}
{!loading &&
!loadError &&
filteredDevices.map((device) => (
<Combobox.Option
key={device.inventoryNumber}
value={device}
className={({ active }) =>
clsx(
'cursor-pointer px-3 py-2',
'flex flex-col gap-0.5',
active
? 'bg-indigo-50 text-indigo-700 dark:bg-indigo-600/25 dark:text-white'
: 'text-gray-900 dark:text-gray-100',
)
}
>
{({ active }) => (
<>
<div className="flex items-center justify-between">
<span className="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Gerät
</span>
<span className="text-xs font-mono text-gray-500 dark:text-gray-400">
{device.inventoryNumber}
</span>
</div>
<div className="text-sm font-semibold">
{device.name || 'Ohne Bezeichnung'}
</div>
<div className="flex flex-wrap gap-x-2 text-xs text-gray-500 dark:text-gray-400">
{device.manufacturer && (
<span>{device.manufacturer}</span>
)}
{device.group && (
<span className="before:content-['·'] before:px-1">
{device.group}
</span>
)}
{device.location && (
<span className="before:content-['·'] before:px-1">
{device.location}
</span>
)}
</div>
</>
)}
</Combobox.Option>
))}
</Combobox.Options>
)}
</div>
</Combobox>
);
}

View File

@ -36,7 +36,7 @@ export default function ButtonGroup({
{options.map((opt, idx) => { {options.map((opt, idx) => {
const isFirst = idx === 0; const isFirst = idx === 0;
const isLast = idx === options.length - 1; const isLast = idx === options.length - 1;
const isActive = opt.value === value; // aktuell nur für aria, nicht für Style const isActive = opt.value === value;
return ( return (
<button <button
@ -45,8 +45,11 @@ export default function ButtonGroup({
disabled={readOnly} disabled={readOnly}
aria-pressed={isActive} aria-pressed={isActive}
className={clsx( className={clsx(
// 👇 1:1 aus deinem Snippet: 'relative inline-flex items-center px-3 py-2 text-sm font-semibold focus:z-10 inset-ring-1',
'relative inline-flex items-center bg-white px-3 py-2 text-sm font-semibold text-gray-900 inset-ring-1 inset-ring-gray-300 hover:bg-gray-50 focus:z-10 dark:bg-white/10 dark:text-white dark:inset-ring-gray-700 dark:hover:bg-white/20', // 🔹 aktiver Button
isActive
? 'bg-indigo-600 text-white inset-ring-indigo-600 hover:bg-indigo-500 dark:bg-indigo-500 dark:text-white dark:inset-ring-indigo-400'
: 'bg-white text-gray-900 inset-ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:inset-ring-gray-700 dark:hover:bg-white/20',
!isFirst && '-ml-px', !isFirst && '-ml-px',
isFirst && 'rounded-l-md', isFirst && 'rounded-l-md',
isLast && 'rounded-r-md', isLast && 'rounded-r-md',

320
components/ui/Combobox.tsx Normal file
View File

@ -0,0 +1,320 @@
// components/ui/Combobox.ts
'use client';
import {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxOption,
ComboboxOptions,
Label,
} from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/20/solid';
import { UserIcon } from '@heroicons/react/16/solid';
import { useEffect, useMemo, useState } from 'react';
function classNames(...classes: Array<string | boolean | null | undefined>) {
return classes.filter(Boolean).join(' ');
}
export type AppComboboxProps<T> = {
label?: string;
options: T[];
value: T | null;
onChange: (value: T | null) => void;
/** Eindeutiger Key pro Option */
getKey: (option: T) => string | number;
/** Primärer Text (wie 'name') */
getPrimaryLabel: (option: T) => string;
/** Optional: sekundärer Text (z.B. Gruppe) */
getSecondaryLabel?: (option: T) => string | null;
/** Optional: zusätzlicher Suchstring über mehrere Felder */
getSearchText?: (option: T) => string | null;
/** Optional: Avatar-URL */
getImageUrl?: (option: T) => string | null;
/**
* Optional: Status für Punkt-Indikator
* Wird nur benutzt, wenn KEIN Avatar gesetzt ist.
*/
getStatus?: (option: T) => 'online' | 'offline' | 'neutral' | null;
placeholder?: string;
/** Query darf einen neuen Wert erzeugen */
allowCreateFromQuery?: boolean;
/**
* Wenn gesetzt, wird bei Auswahl der "Neuen Wert aus Query"-Option
* diese Funktion aufgerufen und das Ergebnis als Auswahl übernommen.
*/
onCreateFromQuery?: (query: string) => T;
/** Ob auch im Secondary-Text gesucht werden soll */
filterBySecondary?: boolean;
className?: string;
};
export default function AppCombobox<T>({
label = 'Auswahl',
options,
value,
onChange,
getKey,
getPrimaryLabel,
getSecondaryLabel,
getSearchText,
getImageUrl,
getStatus,
placeholder = 'Auswählen…',
allowCreateFromQuery = false,
onCreateFromQuery,
filterBySecondary = true,
className,
}: AppComboboxProps<T>) {
// Einzige Quelle der Wahrheit für den Text im Input
const [query, setQuery] = useState('');
// Wenn sich die ausgewählte Option von außen ändert,
// den Input-Text entsprechend synchronisieren.
useEffect(() => {
if (value) {
setQuery(getPrimaryLabel(value));
} else {
setQuery('');
}
// getPrimaryLabel absichtlich NICHT in deps,
// sonst triggern wir bei jedem Render ein setState.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return options;
return options.filter((opt) => {
// Primär-Label
const primary = getPrimaryLabel(opt).toLowerCase();
if (primary.includes(q)) return true;
// Secondary (optional, z.B. Gruppe)
if (filterBySecondary && getSecondaryLabel) {
const sec = (getSecondaryLabel(opt) ?? '').toLowerCase();
if (sec.includes(q)) return true;
}
// zusätzlicher Suchtext (z.B. Vorname, Nachname, nwkennung, Gruppe)
if (getSearchText) {
const extra = (getSearchText(opt) ?? '').toLowerCase();
if (extra.includes(q)) return true;
}
return false;
});
}, [
options,
query,
getPrimaryLabel,
getSecondaryLabel,
getSearchText,
filterBySecondary,
]);
const showCreateRow =
allowCreateFromQuery &&
query.trim().length > 0 &&
!options.some(
(opt) =>
getPrimaryLabel(opt).toLowerCase() ===
query.trim().toLowerCase(),
);
const handleChange = (selected: any) => {
// Sonderfall: "neuen Wert aus Query" gewählt
if (selected && selected.__isCreateOption && onCreateFromQuery) {
const created = onCreateFromQuery(selected.query as string);
// Query auf Label der neu erzeugten Option setzen
setQuery(getPrimaryLabel(created));
onChange(created);
return;
}
if (selected) {
// Query auf Label der ausgewählten Option setzen
setQuery(getPrimaryLabel(selected));
onChange(selected);
} else {
// Auswahl gelöscht
setQuery('');
onChange(null);
}
};
const renderOptionInner = (
opt: T | { __isCreateOption: true; query: string },
) => {
// Create-Row (Freitext)
if ((opt as any).__isCreateOption) {
const q = (opt as any).query as string;
const hasAvatar = !!getImageUrl;
const hasStatus = !!getStatus && !hasAvatar;
return (
<div className="flex items-center">
{hasAvatar && (
<div className="grid size-6 shrink-0 place-items-center rounded-full bg-gray-300 in-data-focus:bg-white dark:bg-gray-600">
<UserIcon
className="size-4 fill-white in-data-focus:fill-indigo-600 dark:in-data-focus:fill-indigo-500"
aria-hidden="true"
/>
</div>
)}
{hasStatus && (
<span
className="inline-block size-2 shrink-0 rounded-full border border-gray-400 in-aria-active:border-white/75 dark:border-gray-600"
aria-hidden="true"
/>
)}
<span
className={classNames(
'block truncate',
hasAvatar || hasStatus ? 'ml-3' : null,
)}
>
{q}
</span>
</div>
);
}
// Normale Option
const o = opt as T;
const labelText = getPrimaryLabel(o);
const secondary = getSecondaryLabel?.(o) ?? null;
const imageUrl = getImageUrl?.(o) ?? null;
const status = getStatus?.(o) ?? null;
const hasAvatar = !!imageUrl;
const hasStatusDot = !!getStatus && !hasAvatar;
return (
<div className="flex items-center">
{hasAvatar && (
<img
src={imageUrl}
alt=""
className="size-6 shrink-0 rounded-full bg-gray-100 outline -outline-offset-1 outline-black/5 dark:bg-gray-700 dark:outline-white/10"
/>
)}
{hasStatusDot && (
<span
className={classNames(
'inline-block size-2 shrink-0 rounded-full',
status === 'online'
? 'bg-green-400 dark:bg-green-500'
: 'bg-gray-200 dark:bg-white/20',
)}
aria-hidden="true"
/>
)}
<span
className={classNames(
'block truncate',
hasAvatar || hasStatusDot ? 'ml-3' : null,
)}
>
{labelText}
</span>
{secondary && (
<span className="ml-2 block truncate text-gray-500 in-data-focus:text-white dark:text-gray-400 dark:in-data-focus:text-white">
{secondary}
</span>
)}
</div>
);
};
return (
<Combobox
as="div"
value={value}
onChange={handleChange}
className={classNames('w-full', className)}
>
{label && (
<Label className="text-xs font-semibold uppercase tracking-wide text-gray-400">
{label}
</Label>
)}
<div className="relative mt-2">
<ComboboxInput
className="block w-full rounded-md bg-white py-1.5 pr-12 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={placeholder}
/>
<ComboboxButton className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-hidden">
<ChevronDownIcon
className="size-5 text-gray-400"
aria-hidden="true"
/>
</ComboboxButton>
<ComboboxOptions
transition
className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg outline outline-black/5 data-leave:transition data-leave:duration-100 data-leave:ease-in data-closed:data-leave:opacity-0 sm:text-sm dark:bg-gray-800 dark:shadow-none dark:-outline-offset-1 dark:outline-white/10"
>
{/* Freitext-Option */}
{showCreateRow && (
<ComboboxOption
value={{ __isCreateOption: true, query } as any}
className="cursor-default px-3 py-2 text-gray-900 select-none data-focus:bg-indigo-600 data-focus:text-white data-focus:outline-hidden dark:text-white dark:data-focus:bg-indigo-500"
>
{renderOptionInner({
__isCreateOption: true,
query,
} as any)}
</ComboboxOption>
)}
{/* Normale Optionen */}
{filtered.map((opt) => {
const baseKey = String(getKey(opt));
// Position der Option in der *vollen* options-Liste suchen
const idx = options.indexOf(opt);
const key = idx === -1 ? baseKey : `${baseKey}__${idx}`;
return (
<ComboboxOption
key={key}
value={opt}
className="cursor-default px-3 py-2 text-gray-900 select-none data-focus:bg-indigo-600 data-focus:text-white data-focus:outline-hidden dark:text-gray-300 dark:data-focus:bg-indigo-500"
>
{renderOptionInner(opt as any)}
</ComboboxOption>
);
})}
{/* Keine Treffer */}
{filtered.length === 0 && !showCreateRow && (
<div className="px-3 py-2 text-xs text-gray-500 dark:text-gray-400">
Keine Treffer
</div>
)}
</ComboboxOptions>
</div>
</Combobox>
);
}

View File

@ -1,4 +1,3 @@
// components/ui/Dropdown.tsx
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
@ -33,7 +32,8 @@ export type DropdownSection = {
items: DropdownItem[]; items: DropdownItem[];
}; };
export type DropdownTriggerVariant = 'button' | 'icon'; // 🔹 NEU: 'input'
export type DropdownTriggerVariant = 'button' | 'icon' | 'input';
export interface DropdownProps { export interface DropdownProps {
label?: string; label?: string;
@ -48,25 +48,25 @@ export interface DropdownProps {
/** Dropdown komplett deaktivieren (Trigger klickt nicht) */ /** Dropdown komplett deaktivieren (Trigger klickt nicht) */
disabled?: boolean; disabled?: boolean;
/** Nur für triggerVariant="input": gesteuerter Eingabewert */
inputValue?: string;
inputPlaceholder?: string;
onInputChange?: (value: string) => void;
} }
/* ───────── interne Helfer ───────── */ /* ───────── interne Helfer ───────── */
const itemBaseClasses = const itemBaseClasses =
'block px-4 py-2 text-sm ' + 'block px-4 py-2 text-sm ' +
// Default Text
'text-gray-700 dark:text-gray-300 ' + 'text-gray-700 dark:text-gray-300 ' +
// Hover (Maus)
'hover:bg-gray-100 hover:text-gray-900 ' + 'hover:bg-gray-100 hover:text-gray-900 ' +
'dark:hover:bg-white/5 dark:hover:text-white ' + 'dark:hover:bg-white/5 dark:hover:text-white ' +
// Focus Outline weglassen
'focus:outline-none'; 'focus:outline-none';
const itemWithIconClasses = const itemWithIconClasses =
'group flex items-center gap-x-3 px-4 py-2 text-sm ' + 'group flex items-center gap-x-3 px-4 py-2 text-sm ' +
// Default Text
'text-gray-700 dark:text-gray-300 ' + 'text-gray-700 dark:text-gray-300 ' +
// Hover
'hover:bg-gray-100 hover:text-gray-900 ' + 'hover:bg-gray-100 hover:text-gray-900 ' +
'dark:hover:bg-white/5 dark:hover:text-white ' + 'dark:hover:bg-white/5 dark:hover:text-white ' +
'focus:outline-none'; 'focus:outline-none';
@ -113,31 +113,236 @@ function renderItemContent(item: DropdownItem) {
); );
} }
/* ───────── Dropdown-Komponente (mit Portal) ───────── */ /* ───────── Spezielle Variante: Textfeld + Dropdown (OHNE Menu) ───────── */
export function Dropdown({ function InputDropdown({
label = 'Options', label = 'Options',
ariaLabel = 'Open options', ariaLabel = 'Open options',
align = 'right', align = 'left',
triggerVariant = 'button',
header, header,
sections, sections,
triggerClassName, triggerClassName,
menuClassName, menuClassName,
disabled = false, disabled = false,
inputValue,
inputPlaceholder,
onInputChange,
}: DropdownProps) { }: DropdownProps) {
const hasDividers = sections.length > 1; const [open, setOpen] = React.useState(false);
const triggerIsButton = triggerVariant === 'button'; const anchorRef = React.useRef<HTMLDivElement | null>(null);
const menuRef = React.useRef<HTMLDivElement | null>(null); // 🔹 NEU
const buttonRef = React.useRef<HTMLButtonElement | null>(null);
const [position, setPosition] = React.useState<{ const [position, setPosition] = React.useState<{
top: number; top: number;
left: number; left: number;
width: number;
} | null>(null); } | null>(null);
const hasDividers = sections.length > 1;
const effectivePlaceholder = inputPlaceholder ?? label;
// Position neu berechnen, wenn geöffnet
React.useEffect(() => {
if (!open || !anchorRef.current) return;
const rect = anchorRef.current.getBoundingClientRect();
const scrollX =
window.pageXOffset || document.documentElement.scrollLeft;
const scrollY =
window.pageYOffset || document.documentElement.scrollTop;
const leftBase = rect.left + scrollX;
setPosition({
top: rect.bottom + scrollY + 4,
left: leftBase,
width: rect.width,
});
}, [open, align]);
// Klick außerhalb / ESC schließt das Dropdown
React.useEffect(() => {
if (!open) return;
function handleClickOutside(ev: MouseEvent) {
const target = ev.target as Node;
// 🔹 Wenn Klick im Input-Bereich oder im Menü: NICHT schließen
if (
(anchorRef.current && anchorRef.current.contains(target)) ||
(menuRef.current && menuRef.current.contains(target))
) {
return;
}
setOpen(false);
}
function handleKey(ev: KeyboardEvent) {
if (ev.key === 'Escape') {
setOpen(false);
}
}
window.addEventListener('mousedown', handleClickOutside);
window.addEventListener('keydown', handleKey);
return () => {
window.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('keydown', handleKey);
};
}, [open]);
return (
<div className="relative inline-block w-full text-left">
<div
ref={anchorRef}
className={clsx('relative', triggerClassName)}
>
<input
type="text"
value={inputValue ?? ''}
placeholder={effectivePlaceholder}
disabled={disabled}
onChange={(e) => {
onInputChange?.(e.target.value);
if (!disabled && !open) setOpen(true);
}}
onFocus={() => {
if (!disabled) setOpen(true);
}}
className="block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 pr-8 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 dark:bg-gray-900/60 dark:ring-gray-600"
/>
<button
type="button"
aria-label={ariaLabel}
disabled={disabled}
onClick={() => {
if (!disabled) setOpen((prev) => !prev);
}}
className={clsx(
'absolute inset-y-0 right-0 flex items-center pr-2',
disabled && 'cursor-not-allowed opacity-50',
)}
>
<ChevronDownIcon
aria-hidden="true"
className="size-4 text-gray-400 dark:text-gray-500"
/>
</button>
</div>
{open && position && !disabled && (
<Portal>
<div
ref={menuRef} // 🔹 WICHTIG: für Outside-Click-Erkennung
style={{
position: 'fixed',
top: position.top,
left: position.left,
width: position.width,
}}
className={clsx(
'z-[9999] max-h-[60vh] overflow-y-auto rounded-md bg-white shadow-lg outline-1 outline-black/5 ' +
'dark:bg-gray-800 dark:shadow-none dark:-outline-offset-1 dark:outline-white/10',
hasDividers &&
'divide-y divide-gray-100 dark:divide-white/10',
menuClassName,
)}
>
{header && <div className="px-4 py-3">{header}</div>}
{sections.map((section, sectionIndex) => (
<div
key={section.id ?? sectionIndex}
className="py-1"
>
{section.label && (
<div className="px-4 py-1 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{section.label}
</div>
)}
{section.items.map((item, itemIndex) => {
const key = item.id ?? `${sectionIndex}-${itemIndex}`;
const handleClick = () => {
if (item.disabled) return;
item.onClick?.(); // 🔹 Auswahl nach außen melden
setOpen(false); // 🔹 Danach Dropdown schließen
};
if (item.href) {
return (
<a
key={key}
href={item.href}
onClick={handleClick}
className="block"
>
{renderItemContent(item)}
</a>
);
}
return (
<button
key={key}
type="button"
disabled={item.disabled}
onClick={handleClick}
className="block w-full text-left disabled:opacity-60"
>
{renderItemContent(item)}
</button>
);
})}
</div>
))}
</div>
</Portal>
)}
</div>
);
}
/* ───────── Standard-Dropdown (Menu) für Button/Icon ───────── */
export function Dropdown(props: DropdownProps) {
const {
label = 'Options',
ariaLabel = 'Open options',
align = 'right',
triggerVariant = 'button',
header,
sections,
triggerClassName,
menuClassName,
disabled = false,
} = props;
const triggerIsInput = triggerVariant === 'input';
const triggerIsIcon = triggerVariant === 'icon';
const triggerIsButton = triggerVariant === 'button';
// 🔹 Spezialfall: Textfeld + Dropdown (ohne Menu)
if (triggerIsInput) {
return (
<InputDropdown
{...props}
triggerVariant="input"
/>
);
}
const hasDividers = sections.length > 1;
return ( return (
<Menu as="div" className="relative inline-block text-left"> <Menu as="div" className="relative inline-block text-left">
{({ open }) => { {({ open }) => {
const buttonRef =
React.useRef<HTMLButtonElement | null>(null);
const [position, setPosition] = React.useState<{
top: number;
left: number;
} | null>(null);
React.useEffect(() => { React.useEffect(() => {
if (!open || !buttonRef.current) return; if (!open || !buttonRef.current) return;
@ -148,7 +353,9 @@ export function Dropdown({
window.pageYOffset || document.documentElement.scrollTop; window.pageYOffset || document.documentElement.scrollTop;
const left = const left =
align === 'left' ? rect.left + scrollX : rect.right + scrollX; align === 'left'
? rect.left + scrollX
: rect.right + scrollX;
setPosition({ setPosition({
top: rect.bottom + scrollY + 4, top: rect.bottom + scrollY + 4,
@ -165,7 +372,8 @@ export function Dropdown({
triggerIsButton triggerIsButton
? 'inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20' ? 'inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20'
: 'flex items-center rounded-full text-gray-400 hover:text-gray-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:text-gray-400 dark:hover:text-gray-300 dark:focus-visible:outline-indigo-500', : 'flex items-center rounded-full text-gray-400 hover:text-gray-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:text-gray-400 dark:hover:text-gray-300 dark:focus-visible:outline-indigo-500',
disabled && 'opacity-50 cursor-not-allowed hover:bg-white dark:hover:bg-white/10', disabled &&
'opacity-50 cursor-not-allowed hover:bg-white dark:hover:bg-white/10',
triggerClassName, triggerClassName,
)} )}
> >
@ -195,7 +403,8 @@ export function Dropdown({
style={{ style={{
position: 'fixed', position: 'fixed',
top: position.top, top: position.top,
left: align === 'left' ? position.left : undefined, left:
align === 'left' ? position.left : undefined,
right: right:
align === 'right' align === 'right'
? window.innerWidth - position.left ? window.innerWidth - position.left
@ -209,50 +418,52 @@ export function Dropdown({
menuClassName, menuClassName,
)} )}
> >
{header && <div className="px-4 py-3">{header}</div>} {header && (
<div className="px-4 py-3">{header}</div>
)}
{sections.map((section, sectionIndex) => ( {sections.map((section, sectionIndex) => (
<div <div
key={section.id ?? sectionIndex} key={section.id ?? sectionIndex}
className="py-1" className="py-1"
> >
{/* NEU: Gruppen-Label als "Trenner" */} {section.label && (
{section.label && ( <div className="px-4 py-1 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
<div className="px-4 py-1 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"> {section.label}
{section.label} </div>
</div> )}
)}
{section.items.map((item, itemIndex) => { {section.items.map((item, itemIndex) => {
const key = item.id ?? `${sectionIndex}-${itemIndex}`; const key =
item.id ?? `${sectionIndex}-${itemIndex}`;
return ( return (
<MenuItem <MenuItem
key={key} key={key}
disabled={item.disabled} disabled={item.disabled}
> >
{item.href ? ( {item.href ? (
<a <a
href={item.href} href={item.href}
onClick={item.onClick} onClick={item.onClick}
className="block" className="block"
> >
{renderItemContent(item)} {renderItemContent(item)}
</a> </a>
) : ( ) : (
<button <button
type="button" type="button"
onClick={item.onClick} onClick={item.onClick}
className="block w-full text-left" className="block w-full text-left"
> >
{renderItemContent(item)} {renderItemContent(item)}
</button> </button>
)} )}
</MenuItem> </MenuItem>
); );
})} })}
</div> </div>
))} ))}
</MenuItems> </MenuItems>
</Portal> </Portal>
)} )}

View File

@ -21,6 +21,7 @@ export interface ModalAction {
onClick?: () => void; onClick?: () => void;
variant?: 'primary' | 'secondary' | 'danger'; variant?: 'primary' | 'secondary' | 'danger';
autoFocus?: boolean; autoFocus?: boolean;
disabled?: boolean;
} }
export interface ModalProps { export interface ModalProps {
@ -125,6 +126,7 @@ function renderActionButton(
type="button" type="button"
onClick={action.onClick} onClick={action.onClick}
autoFocus={action.autoFocus} autoFocus={action.autoFocus}
disabled={action.disabled}
variant={buttonVariant} variant={buttonVariant}
tone={tone} tone={tone}
size="lg" size="lg"
@ -135,6 +137,7 @@ function renderActionButton(
); );
} }
/* ───────── Modal-Komponente ───────── */ /* ───────── Modal-Komponente ───────── */
export function Modal({ export function Modal({

File diff suppressed because one or more lines are too long

View File

@ -1195,7 +1195,8 @@ export const DeviceScalarFieldEnum = {
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
createdById: 'createdById', createdById: 'createdById',
updatedById: 'updatedById' updatedById: 'updatedById',
parentDeviceId: 'parentDeviceId'
} as const } as const
export type DeviceScalarFieldEnum = (typeof DeviceScalarFieldEnum)[keyof typeof DeviceScalarFieldEnum] export type DeviceScalarFieldEnum = (typeof DeviceScalarFieldEnum)[keyof typeof DeviceScalarFieldEnum]

View File

@ -156,7 +156,8 @@ export const DeviceScalarFieldEnum = {
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
createdById: 'createdById', createdById: 'createdById',
updatedById: 'updatedById' updatedById: 'updatedById',
parentDeviceId: 'parentDeviceId'
} as const } as const
export type DeviceScalarFieldEnum = (typeof DeviceScalarFieldEnum)[keyof typeof DeviceScalarFieldEnum] export type DeviceScalarFieldEnum = (typeof DeviceScalarFieldEnum)[keyof typeof DeviceScalarFieldEnum]

View File

@ -47,6 +47,7 @@ export type DeviceMinAggregateOutputType = {
updatedAt: Date | null updatedAt: Date | null
createdById: string | null createdById: string | null
updatedById: string | null updatedById: string | null
parentDeviceId: string | null
} }
export type DeviceMaxAggregateOutputType = { export type DeviceMaxAggregateOutputType = {
@ -72,6 +73,7 @@ export type DeviceMaxAggregateOutputType = {
updatedAt: Date | null updatedAt: Date | null
createdById: string | null createdById: string | null
updatedById: string | null updatedById: string | null
parentDeviceId: string | null
} }
export type DeviceCountAggregateOutputType = { export type DeviceCountAggregateOutputType = {
@ -97,6 +99,7 @@ export type DeviceCountAggregateOutputType = {
updatedAt: number updatedAt: number
createdById: number createdById: number
updatedById: number updatedById: number
parentDeviceId: number
_all: number _all: number
} }
@ -124,6 +127,7 @@ export type DeviceMinAggregateInputType = {
updatedAt?: true updatedAt?: true
createdById?: true createdById?: true
updatedById?: true updatedById?: true
parentDeviceId?: true
} }
export type DeviceMaxAggregateInputType = { export type DeviceMaxAggregateInputType = {
@ -149,6 +153,7 @@ export type DeviceMaxAggregateInputType = {
updatedAt?: true updatedAt?: true
createdById?: true createdById?: true
updatedById?: true updatedById?: true
parentDeviceId?: true
} }
export type DeviceCountAggregateInputType = { export type DeviceCountAggregateInputType = {
@ -174,6 +179,7 @@ export type DeviceCountAggregateInputType = {
updatedAt?: true updatedAt?: true
createdById?: true createdById?: true
updatedById?: true updatedById?: true
parentDeviceId?: true
_all?: true _all?: true
} }
@ -272,6 +278,7 @@ export type DeviceGroupByOutputType = {
updatedAt: Date updatedAt: Date
createdById: string | null createdById: string | null
updatedById: string | null updatedById: string | null
parentDeviceId: string | null
_count: DeviceCountAggregateOutputType | null _count: DeviceCountAggregateOutputType | null
_min: DeviceMinAggregateOutputType | null _min: DeviceMinAggregateOutputType | null
_max: DeviceMaxAggregateOutputType | null _max: DeviceMaxAggregateOutputType | null
@ -318,6 +325,9 @@ export type DeviceWhereInput = {
updatedAt?: Prisma.DateTimeFilter<"Device"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Device"> | Date | string
createdById?: Prisma.StringNullableFilter<"Device"> | string | null createdById?: Prisma.StringNullableFilter<"Device"> | string | null
updatedById?: Prisma.StringNullableFilter<"Device"> | string | null updatedById?: Prisma.StringNullableFilter<"Device"> | string | null
parentDeviceId?: Prisma.StringNullableFilter<"Device"> | string | null
parentDevice?: Prisma.XOR<Prisma.DeviceNullableScalarRelationFilter, Prisma.DeviceWhereInput> | null
accessories?: Prisma.DeviceListRelationFilter
createdBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null createdBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
updatedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null updatedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
group?: Prisma.XOR<Prisma.DeviceGroupNullableScalarRelationFilter, Prisma.DeviceGroupWhereInput> | null group?: Prisma.XOR<Prisma.DeviceGroupNullableScalarRelationFilter, Prisma.DeviceGroupWhereInput> | null
@ -349,6 +359,9 @@ export type DeviceOrderByWithRelationInput = {
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
createdById?: Prisma.SortOrderInput | Prisma.SortOrder createdById?: Prisma.SortOrderInput | Prisma.SortOrder
updatedById?: Prisma.SortOrderInput | Prisma.SortOrder updatedById?: Prisma.SortOrderInput | Prisma.SortOrder
parentDeviceId?: Prisma.SortOrderInput | Prisma.SortOrder
parentDevice?: Prisma.DeviceOrderByWithRelationInput
accessories?: Prisma.DeviceOrderByRelationAggregateInput
createdBy?: Prisma.UserOrderByWithRelationInput createdBy?: Prisma.UserOrderByWithRelationInput
updatedBy?: Prisma.UserOrderByWithRelationInput updatedBy?: Prisma.UserOrderByWithRelationInput
group?: Prisma.DeviceGroupOrderByWithRelationInput group?: Prisma.DeviceGroupOrderByWithRelationInput
@ -383,6 +396,9 @@ export type DeviceWhereUniqueInput = Prisma.AtLeast<{
updatedAt?: Prisma.DateTimeFilter<"Device"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Device"> | Date | string
createdById?: Prisma.StringNullableFilter<"Device"> | string | null createdById?: Prisma.StringNullableFilter<"Device"> | string | null
updatedById?: Prisma.StringNullableFilter<"Device"> | string | null updatedById?: Prisma.StringNullableFilter<"Device"> | string | null
parentDeviceId?: Prisma.StringNullableFilter<"Device"> | string | null
parentDevice?: Prisma.XOR<Prisma.DeviceNullableScalarRelationFilter, Prisma.DeviceWhereInput> | null
accessories?: Prisma.DeviceListRelationFilter
createdBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null createdBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
updatedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null updatedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
group?: Prisma.XOR<Prisma.DeviceGroupNullableScalarRelationFilter, Prisma.DeviceGroupWhereInput> | null group?: Prisma.XOR<Prisma.DeviceGroupNullableScalarRelationFilter, Prisma.DeviceGroupWhereInput> | null
@ -414,6 +430,7 @@ export type DeviceOrderByWithAggregationInput = {
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
createdById?: Prisma.SortOrderInput | Prisma.SortOrder createdById?: Prisma.SortOrderInput | Prisma.SortOrder
updatedById?: Prisma.SortOrderInput | Prisma.SortOrder updatedById?: Prisma.SortOrderInput | Prisma.SortOrder
parentDeviceId?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.DeviceCountOrderByAggregateInput _count?: Prisma.DeviceCountOrderByAggregateInput
_max?: Prisma.DeviceMaxOrderByAggregateInput _max?: Prisma.DeviceMaxOrderByAggregateInput
_min?: Prisma.DeviceMinOrderByAggregateInput _min?: Prisma.DeviceMinOrderByAggregateInput
@ -445,6 +462,7 @@ export type DeviceScalarWhereWithAggregatesInput = {
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Device"> | Date | string updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Device"> | Date | string
createdById?: Prisma.StringNullableWithAggregatesFilter<"Device"> | string | null createdById?: Prisma.StringNullableWithAggregatesFilter<"Device"> | string | null
updatedById?: Prisma.StringNullableWithAggregatesFilter<"Device"> | string | null updatedById?: Prisma.StringNullableWithAggregatesFilter<"Device"> | string | null
parentDeviceId?: Prisma.StringNullableWithAggregatesFilter<"Device"> | string | null
} }
export type DeviceCreateInput = { export type DeviceCreateInput = {
@ -466,6 +484,8 @@ export type DeviceCreateInput = {
loanComment?: string | null loanComment?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
parentDevice?: Prisma.DeviceCreateNestedOneWithoutAccessoriesInput
accessories?: Prisma.DeviceCreateNestedManyWithoutParentDeviceInput
createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput
updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput
group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput
@ -497,6 +517,8 @@ export type DeviceUncheckedCreateInput = {
updatedAt?: Date | string updatedAt?: Date | string
createdById?: string | null createdById?: string | null
updatedById?: string | null updatedById?: string | null
parentDeviceId?: string | null
accessories?: Prisma.DeviceUncheckedCreateNestedManyWithoutParentDeviceInput
history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput
tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput
} }
@ -520,6 +542,8 @@ export type DeviceUpdateInput = {
loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
parentDevice?: Prisma.DeviceUpdateOneWithoutAccessoriesNestedInput
accessories?: Prisma.DeviceUpdateManyWithoutParentDeviceNestedInput
createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput
updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput
group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput
@ -551,6 +575,8 @@ export type DeviceUncheckedUpdateInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
accessories?: Prisma.DeviceUncheckedUpdateManyWithoutParentDeviceNestedInput
history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput
tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput
} }
@ -578,6 +604,7 @@ export type DeviceCreateManyInput = {
updatedAt?: Date | string updatedAt?: Date | string
createdById?: string | null createdById?: string | null
updatedById?: string | null updatedById?: string | null
parentDeviceId?: string | null
} }
export type DeviceUpdateManyMutationInput = { export type DeviceUpdateManyMutationInput = {
@ -624,6 +651,7 @@ export type DeviceUncheckedUpdateManyInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type DeviceListRelationFilter = { export type DeviceListRelationFilter = {
@ -636,6 +664,11 @@ export type DeviceOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder _count?: Prisma.SortOrder
} }
export type DeviceNullableScalarRelationFilter = {
is?: Prisma.DeviceWhereInput | null
isNot?: Prisma.DeviceWhereInput | null
}
export type DeviceCountOrderByAggregateInput = { export type DeviceCountOrderByAggregateInput = {
inventoryNumber?: Prisma.SortOrder inventoryNumber?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
@ -659,6 +692,7 @@ export type DeviceCountOrderByAggregateInput = {
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
createdById?: Prisma.SortOrder createdById?: Prisma.SortOrder
updatedById?: Prisma.SortOrder updatedById?: Prisma.SortOrder
parentDeviceId?: Prisma.SortOrder
} }
export type DeviceMaxOrderByAggregateInput = { export type DeviceMaxOrderByAggregateInput = {
@ -684,6 +718,7 @@ export type DeviceMaxOrderByAggregateInput = {
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
createdById?: Prisma.SortOrder createdById?: Prisma.SortOrder
updatedById?: Prisma.SortOrder updatedById?: Prisma.SortOrder
parentDeviceId?: Prisma.SortOrder
} }
export type DeviceMinOrderByAggregateInput = { export type DeviceMinOrderByAggregateInput = {
@ -709,11 +744,7 @@ export type DeviceMinOrderByAggregateInput = {
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
createdById?: Prisma.SortOrder createdById?: Prisma.SortOrder
updatedById?: Prisma.SortOrder updatedById?: Prisma.SortOrder
} parentDeviceId?: Prisma.SortOrder
export type DeviceScalarRelationFilter = {
is?: Prisma.DeviceWhereInput
isNot?: Prisma.DeviceWhereInput
} }
export type DeviceCreateNestedManyWithoutCreatedByInput = { export type DeviceCreateNestedManyWithoutCreatedByInput = {
@ -884,10 +915,68 @@ export type DeviceUncheckedUpdateManyWithoutLocationNestedInput = {
deleteMany?: Prisma.DeviceScalarWhereInput | Prisma.DeviceScalarWhereInput[] deleteMany?: Prisma.DeviceScalarWhereInput | Prisma.DeviceScalarWhereInput[]
} }
export type DeviceCreateNestedOneWithoutAccessoriesInput = {
create?: Prisma.XOR<Prisma.DeviceCreateWithoutAccessoriesInput, Prisma.DeviceUncheckedCreateWithoutAccessoriesInput>
connectOrCreate?: Prisma.DeviceCreateOrConnectWithoutAccessoriesInput
connect?: Prisma.DeviceWhereUniqueInput
}
export type DeviceCreateNestedManyWithoutParentDeviceInput = {
create?: Prisma.XOR<Prisma.DeviceCreateWithoutParentDeviceInput, Prisma.DeviceUncheckedCreateWithoutParentDeviceInput> | Prisma.DeviceCreateWithoutParentDeviceInput[] | Prisma.DeviceUncheckedCreateWithoutParentDeviceInput[]
connectOrCreate?: Prisma.DeviceCreateOrConnectWithoutParentDeviceInput | Prisma.DeviceCreateOrConnectWithoutParentDeviceInput[]
createMany?: Prisma.DeviceCreateManyParentDeviceInputEnvelope
connect?: Prisma.DeviceWhereUniqueInput | Prisma.DeviceWhereUniqueInput[]
}
export type DeviceUncheckedCreateNestedManyWithoutParentDeviceInput = {
create?: Prisma.XOR<Prisma.DeviceCreateWithoutParentDeviceInput, Prisma.DeviceUncheckedCreateWithoutParentDeviceInput> | Prisma.DeviceCreateWithoutParentDeviceInput[] | Prisma.DeviceUncheckedCreateWithoutParentDeviceInput[]
connectOrCreate?: Prisma.DeviceCreateOrConnectWithoutParentDeviceInput | Prisma.DeviceCreateOrConnectWithoutParentDeviceInput[]
createMany?: Prisma.DeviceCreateManyParentDeviceInputEnvelope
connect?: Prisma.DeviceWhereUniqueInput | Prisma.DeviceWhereUniqueInput[]
}
export type NullableDateTimeFieldUpdateOperationsInput = { export type NullableDateTimeFieldUpdateOperationsInput = {
set?: Date | string | null set?: Date | string | null
} }
export type DeviceUpdateOneWithoutAccessoriesNestedInput = {
create?: Prisma.XOR<Prisma.DeviceCreateWithoutAccessoriesInput, Prisma.DeviceUncheckedCreateWithoutAccessoriesInput>
connectOrCreate?: Prisma.DeviceCreateOrConnectWithoutAccessoriesInput
upsert?: Prisma.DeviceUpsertWithoutAccessoriesInput
disconnect?: Prisma.DeviceWhereInput | boolean
delete?: Prisma.DeviceWhereInput | boolean
connect?: Prisma.DeviceWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.DeviceUpdateToOneWithWhereWithoutAccessoriesInput, Prisma.DeviceUpdateWithoutAccessoriesInput>, Prisma.DeviceUncheckedUpdateWithoutAccessoriesInput>
}
export type DeviceUpdateManyWithoutParentDeviceNestedInput = {
create?: Prisma.XOR<Prisma.DeviceCreateWithoutParentDeviceInput, Prisma.DeviceUncheckedCreateWithoutParentDeviceInput> | Prisma.DeviceCreateWithoutParentDeviceInput[] | Prisma.DeviceUncheckedCreateWithoutParentDeviceInput[]
connectOrCreate?: Prisma.DeviceCreateOrConnectWithoutParentDeviceInput | Prisma.DeviceCreateOrConnectWithoutParentDeviceInput[]
upsert?: Prisma.DeviceUpsertWithWhereUniqueWithoutParentDeviceInput | Prisma.DeviceUpsertWithWhereUniqueWithoutParentDeviceInput[]
createMany?: Prisma.DeviceCreateManyParentDeviceInputEnvelope
set?: Prisma.DeviceWhereUniqueInput | Prisma.DeviceWhereUniqueInput[]
disconnect?: Prisma.DeviceWhereUniqueInput | Prisma.DeviceWhereUniqueInput[]
delete?: Prisma.DeviceWhereUniqueInput | Prisma.DeviceWhereUniqueInput[]
connect?: Prisma.DeviceWhereUniqueInput | Prisma.DeviceWhereUniqueInput[]
update?: Prisma.DeviceUpdateWithWhereUniqueWithoutParentDeviceInput | Prisma.DeviceUpdateWithWhereUniqueWithoutParentDeviceInput[]
updateMany?: Prisma.DeviceUpdateManyWithWhereWithoutParentDeviceInput | Prisma.DeviceUpdateManyWithWhereWithoutParentDeviceInput[]
deleteMany?: Prisma.DeviceScalarWhereInput | Prisma.DeviceScalarWhereInput[]
}
export type DeviceUncheckedUpdateManyWithoutParentDeviceNestedInput = {
create?: Prisma.XOR<Prisma.DeviceCreateWithoutParentDeviceInput, Prisma.DeviceUncheckedCreateWithoutParentDeviceInput> | Prisma.DeviceCreateWithoutParentDeviceInput[] | Prisma.DeviceUncheckedCreateWithoutParentDeviceInput[]
connectOrCreate?: Prisma.DeviceCreateOrConnectWithoutParentDeviceInput | Prisma.DeviceCreateOrConnectWithoutParentDeviceInput[]
upsert?: Prisma.DeviceUpsertWithWhereUniqueWithoutParentDeviceInput | Prisma.DeviceUpsertWithWhereUniqueWithoutParentDeviceInput[]
createMany?: Prisma.DeviceCreateManyParentDeviceInputEnvelope
set?: Prisma.DeviceWhereUniqueInput | Prisma.DeviceWhereUniqueInput[]
disconnect?: Prisma.DeviceWhereUniqueInput | Prisma.DeviceWhereUniqueInput[]
delete?: Prisma.DeviceWhereUniqueInput | Prisma.DeviceWhereUniqueInput[]
connect?: Prisma.DeviceWhereUniqueInput | Prisma.DeviceWhereUniqueInput[]
update?: Prisma.DeviceUpdateWithWhereUniqueWithoutParentDeviceInput | Prisma.DeviceUpdateWithWhereUniqueWithoutParentDeviceInput[]
updateMany?: Prisma.DeviceUpdateManyWithWhereWithoutParentDeviceInput | Prisma.DeviceUpdateManyWithWhereWithoutParentDeviceInput[]
deleteMany?: Prisma.DeviceScalarWhereInput | Prisma.DeviceScalarWhereInput[]
}
export type DeviceCreateNestedManyWithoutTagsInput = { export type DeviceCreateNestedManyWithoutTagsInput = {
create?: Prisma.XOR<Prisma.DeviceCreateWithoutTagsInput, Prisma.DeviceUncheckedCreateWithoutTagsInput> | Prisma.DeviceCreateWithoutTagsInput[] | Prisma.DeviceUncheckedCreateWithoutTagsInput[] create?: Prisma.XOR<Prisma.DeviceCreateWithoutTagsInput, Prisma.DeviceUncheckedCreateWithoutTagsInput> | Prisma.DeviceCreateWithoutTagsInput[] | Prisma.DeviceUncheckedCreateWithoutTagsInput[]
connectOrCreate?: Prisma.DeviceCreateOrConnectWithoutTagsInput | Prisma.DeviceCreateOrConnectWithoutTagsInput[] connectOrCreate?: Prisma.DeviceCreateOrConnectWithoutTagsInput | Prisma.DeviceCreateOrConnectWithoutTagsInput[]
@ -932,10 +1021,12 @@ export type DeviceCreateNestedOneWithoutHistoryInput = {
connect?: Prisma.DeviceWhereUniqueInput connect?: Prisma.DeviceWhereUniqueInput
} }
export type DeviceUpdateOneRequiredWithoutHistoryNestedInput = { export type DeviceUpdateOneWithoutHistoryNestedInput = {
create?: Prisma.XOR<Prisma.DeviceCreateWithoutHistoryInput, Prisma.DeviceUncheckedCreateWithoutHistoryInput> create?: Prisma.XOR<Prisma.DeviceCreateWithoutHistoryInput, Prisma.DeviceUncheckedCreateWithoutHistoryInput>
connectOrCreate?: Prisma.DeviceCreateOrConnectWithoutHistoryInput connectOrCreate?: Prisma.DeviceCreateOrConnectWithoutHistoryInput
upsert?: Prisma.DeviceUpsertWithoutHistoryInput upsert?: Prisma.DeviceUpsertWithoutHistoryInput
disconnect?: Prisma.DeviceWhereInput | boolean
delete?: Prisma.DeviceWhereInput | boolean
connect?: Prisma.DeviceWhereUniqueInput connect?: Prisma.DeviceWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.DeviceUpdateToOneWithWhereWithoutHistoryInput, Prisma.DeviceUpdateWithoutHistoryInput>, Prisma.DeviceUncheckedUpdateWithoutHistoryInput> update?: Prisma.XOR<Prisma.XOR<Prisma.DeviceUpdateToOneWithWhereWithoutHistoryInput, Prisma.DeviceUpdateWithoutHistoryInput>, Prisma.DeviceUncheckedUpdateWithoutHistoryInput>
} }
@ -959,6 +1050,8 @@ export type DeviceCreateWithoutCreatedByInput = {
loanComment?: string | null loanComment?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
parentDevice?: Prisma.DeviceCreateNestedOneWithoutAccessoriesInput
accessories?: Prisma.DeviceCreateNestedManyWithoutParentDeviceInput
updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput
group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput
location?: Prisma.LocationCreateNestedOneWithoutDevicesInput location?: Prisma.LocationCreateNestedOneWithoutDevicesInput
@ -988,6 +1081,8 @@ export type DeviceUncheckedCreateWithoutCreatedByInput = {
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
updatedById?: string | null updatedById?: string | null
parentDeviceId?: string | null
accessories?: Prisma.DeviceUncheckedCreateNestedManyWithoutParentDeviceInput
history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput
tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput
} }
@ -1021,6 +1116,8 @@ export type DeviceCreateWithoutUpdatedByInput = {
loanComment?: string | null loanComment?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
parentDevice?: Prisma.DeviceCreateNestedOneWithoutAccessoriesInput
accessories?: Prisma.DeviceCreateNestedManyWithoutParentDeviceInput
createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput
group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput
location?: Prisma.LocationCreateNestedOneWithoutDevicesInput location?: Prisma.LocationCreateNestedOneWithoutDevicesInput
@ -1050,6 +1147,8 @@ export type DeviceUncheckedCreateWithoutUpdatedByInput = {
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
createdById?: string | null createdById?: string | null
parentDeviceId?: string | null
accessories?: Prisma.DeviceUncheckedCreateNestedManyWithoutParentDeviceInput
history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput
tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput
} }
@ -1106,6 +1205,7 @@ export type DeviceScalarWhereInput = {
updatedAt?: Prisma.DateTimeFilter<"Device"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Device"> | Date | string
createdById?: Prisma.StringNullableFilter<"Device"> | string | null createdById?: Prisma.StringNullableFilter<"Device"> | string | null
updatedById?: Prisma.StringNullableFilter<"Device"> | string | null updatedById?: Prisma.StringNullableFilter<"Device"> | string | null
parentDeviceId?: Prisma.StringNullableFilter<"Device"> | string | null
} }
export type DeviceUpsertWithWhereUniqueWithoutUpdatedByInput = { export type DeviceUpsertWithWhereUniqueWithoutUpdatedByInput = {
@ -1143,6 +1243,8 @@ export type DeviceCreateWithoutGroupInput = {
loanComment?: string | null loanComment?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
parentDevice?: Prisma.DeviceCreateNestedOneWithoutAccessoriesInput
accessories?: Prisma.DeviceCreateNestedManyWithoutParentDeviceInput
createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput
updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput
location?: Prisma.LocationCreateNestedOneWithoutDevicesInput location?: Prisma.LocationCreateNestedOneWithoutDevicesInput
@ -1172,6 +1274,8 @@ export type DeviceUncheckedCreateWithoutGroupInput = {
updatedAt?: Date | string updatedAt?: Date | string
createdById?: string | null createdById?: string | null
updatedById?: string | null updatedById?: string | null
parentDeviceId?: string | null
accessories?: Prisma.DeviceUncheckedCreateNestedManyWithoutParentDeviceInput
history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput
tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput
} }
@ -1221,6 +1325,8 @@ export type DeviceCreateWithoutLocationInput = {
loanComment?: string | null loanComment?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
parentDevice?: Prisma.DeviceCreateNestedOneWithoutAccessoriesInput
accessories?: Prisma.DeviceCreateNestedManyWithoutParentDeviceInput
createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput
updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput
group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput
@ -1250,6 +1356,8 @@ export type DeviceUncheckedCreateWithoutLocationInput = {
updatedAt?: Date | string updatedAt?: Date | string
createdById?: string | null createdById?: string | null
updatedById?: string | null updatedById?: string | null
parentDeviceId?: string | null
accessories?: Prisma.DeviceUncheckedCreateNestedManyWithoutParentDeviceInput
history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput
tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput
} }
@ -1280,6 +1388,216 @@ export type DeviceUpdateManyWithWhereWithoutLocationInput = {
data: Prisma.XOR<Prisma.DeviceUpdateManyMutationInput, Prisma.DeviceUncheckedUpdateManyWithoutLocationInput> data: Prisma.XOR<Prisma.DeviceUpdateManyMutationInput, Prisma.DeviceUncheckedUpdateManyWithoutLocationInput>
} }
export type DeviceCreateWithoutAccessoriesInput = {
inventoryNumber: string
name: string
manufacturer: string
model: string
serialNumber?: string | null
productNumber?: string | null
comment?: string | null
ipv4Address?: string | null
ipv6Address?: string | null
macAddress?: string | null
username?: string | null
passwordHash?: string | null
loanedTo?: string | null
loanedFrom?: Date | string | null
loanedUntil?: Date | string | null
loanComment?: string | null
createdAt?: Date | string
updatedAt?: Date | string
parentDevice?: Prisma.DeviceCreateNestedOneWithoutAccessoriesInput
createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput
updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput
group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput
location?: Prisma.LocationCreateNestedOneWithoutDevicesInput
history?: Prisma.DeviceHistoryCreateNestedManyWithoutDeviceInput
tags?: Prisma.TagCreateNestedManyWithoutDevicesInput
}
export type DeviceUncheckedCreateWithoutAccessoriesInput = {
inventoryNumber: string
name: string
manufacturer: string
model: string
serialNumber?: string | null
productNumber?: string | null
comment?: string | null
ipv4Address?: string | null
ipv6Address?: string | null
macAddress?: string | null
username?: string | null
passwordHash?: string | null
groupId?: string | null
locationId?: string | null
loanedTo?: string | null
loanedFrom?: Date | string | null
loanedUntil?: Date | string | null
loanComment?: string | null
createdAt?: Date | string
updatedAt?: Date | string
createdById?: string | null
updatedById?: string | null
parentDeviceId?: string | null
history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput
tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput
}
export type DeviceCreateOrConnectWithoutAccessoriesInput = {
where: Prisma.DeviceWhereUniqueInput
create: Prisma.XOR<Prisma.DeviceCreateWithoutAccessoriesInput, Prisma.DeviceUncheckedCreateWithoutAccessoriesInput>
}
export type DeviceCreateWithoutParentDeviceInput = {
inventoryNumber: string
name: string
manufacturer: string
model: string
serialNumber?: string | null
productNumber?: string | null
comment?: string | null
ipv4Address?: string | null
ipv6Address?: string | null
macAddress?: string | null
username?: string | null
passwordHash?: string | null
loanedTo?: string | null
loanedFrom?: Date | string | null
loanedUntil?: Date | string | null
loanComment?: string | null
createdAt?: Date | string
updatedAt?: Date | string
accessories?: Prisma.DeviceCreateNestedManyWithoutParentDeviceInput
createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput
updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput
group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput
location?: Prisma.LocationCreateNestedOneWithoutDevicesInput
history?: Prisma.DeviceHistoryCreateNestedManyWithoutDeviceInput
tags?: Prisma.TagCreateNestedManyWithoutDevicesInput
}
export type DeviceUncheckedCreateWithoutParentDeviceInput = {
inventoryNumber: string
name: string
manufacturer: string
model: string
serialNumber?: string | null
productNumber?: string | null
comment?: string | null
ipv4Address?: string | null
ipv6Address?: string | null
macAddress?: string | null
username?: string | null
passwordHash?: string | null
groupId?: string | null
locationId?: string | null
loanedTo?: string | null
loanedFrom?: Date | string | null
loanedUntil?: Date | string | null
loanComment?: string | null
createdAt?: Date | string
updatedAt?: Date | string
createdById?: string | null
updatedById?: string | null
accessories?: Prisma.DeviceUncheckedCreateNestedManyWithoutParentDeviceInput
history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput
tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput
}
export type DeviceCreateOrConnectWithoutParentDeviceInput = {
where: Prisma.DeviceWhereUniqueInput
create: Prisma.XOR<Prisma.DeviceCreateWithoutParentDeviceInput, Prisma.DeviceUncheckedCreateWithoutParentDeviceInput>
}
export type DeviceCreateManyParentDeviceInputEnvelope = {
data: Prisma.DeviceCreateManyParentDeviceInput | Prisma.DeviceCreateManyParentDeviceInput[]
skipDuplicates?: boolean
}
export type DeviceUpsertWithoutAccessoriesInput = {
update: Prisma.XOR<Prisma.DeviceUpdateWithoutAccessoriesInput, Prisma.DeviceUncheckedUpdateWithoutAccessoriesInput>
create: Prisma.XOR<Prisma.DeviceCreateWithoutAccessoriesInput, Prisma.DeviceUncheckedCreateWithoutAccessoriesInput>
where?: Prisma.DeviceWhereInput
}
export type DeviceUpdateToOneWithWhereWithoutAccessoriesInput = {
where?: Prisma.DeviceWhereInput
data: Prisma.XOR<Prisma.DeviceUpdateWithoutAccessoriesInput, Prisma.DeviceUncheckedUpdateWithoutAccessoriesInput>
}
export type DeviceUpdateWithoutAccessoriesInput = {
inventoryNumber?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
manufacturer?: Prisma.StringFieldUpdateOperationsInput | string
model?: Prisma.StringFieldUpdateOperationsInput | string
serialNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
productNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
ipv4Address?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
ipv6Address?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
macAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
username?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
loanedTo?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
loanedFrom?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
loanedUntil?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
parentDevice?: Prisma.DeviceUpdateOneWithoutAccessoriesNestedInput
createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput
updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput
group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput
location?: Prisma.LocationUpdateOneWithoutDevicesNestedInput
history?: Prisma.DeviceHistoryUpdateManyWithoutDeviceNestedInput
tags?: Prisma.TagUpdateManyWithoutDevicesNestedInput
}
export type DeviceUncheckedUpdateWithoutAccessoriesInput = {
inventoryNumber?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
manufacturer?: Prisma.StringFieldUpdateOperationsInput | string
model?: Prisma.StringFieldUpdateOperationsInput | string
serialNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
productNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
ipv4Address?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
ipv6Address?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
macAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
username?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
locationId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
loanedTo?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
loanedFrom?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
loanedUntil?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput
tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput
}
export type DeviceUpsertWithWhereUniqueWithoutParentDeviceInput = {
where: Prisma.DeviceWhereUniqueInput
update: Prisma.XOR<Prisma.DeviceUpdateWithoutParentDeviceInput, Prisma.DeviceUncheckedUpdateWithoutParentDeviceInput>
create: Prisma.XOR<Prisma.DeviceCreateWithoutParentDeviceInput, Prisma.DeviceUncheckedCreateWithoutParentDeviceInput>
}
export type DeviceUpdateWithWhereUniqueWithoutParentDeviceInput = {
where: Prisma.DeviceWhereUniqueInput
data: Prisma.XOR<Prisma.DeviceUpdateWithoutParentDeviceInput, Prisma.DeviceUncheckedUpdateWithoutParentDeviceInput>
}
export type DeviceUpdateManyWithWhereWithoutParentDeviceInput = {
where: Prisma.DeviceScalarWhereInput
data: Prisma.XOR<Prisma.DeviceUpdateManyMutationInput, Prisma.DeviceUncheckedUpdateManyWithoutParentDeviceInput>
}
export type DeviceCreateWithoutTagsInput = { export type DeviceCreateWithoutTagsInput = {
inventoryNumber: string inventoryNumber: string
name: string name: string
@ -1299,6 +1617,8 @@ export type DeviceCreateWithoutTagsInput = {
loanComment?: string | null loanComment?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
parentDevice?: Prisma.DeviceCreateNestedOneWithoutAccessoriesInput
accessories?: Prisma.DeviceCreateNestedManyWithoutParentDeviceInput
createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput
updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput
group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput
@ -1329,6 +1649,8 @@ export type DeviceUncheckedCreateWithoutTagsInput = {
updatedAt?: Date | string updatedAt?: Date | string
createdById?: string | null createdById?: string | null
updatedById?: string | null updatedById?: string | null
parentDeviceId?: string | null
accessories?: Prisma.DeviceUncheckedCreateNestedManyWithoutParentDeviceInput
history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput history?: Prisma.DeviceHistoryUncheckedCreateNestedManyWithoutDeviceInput
} }
@ -1372,6 +1694,8 @@ export type DeviceCreateWithoutHistoryInput = {
loanComment?: string | null loanComment?: string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
parentDevice?: Prisma.DeviceCreateNestedOneWithoutAccessoriesInput
accessories?: Prisma.DeviceCreateNestedManyWithoutParentDeviceInput
createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput createdBy?: Prisma.UserCreateNestedOneWithoutDevicesCreatedInput
updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput updatedBy?: Prisma.UserCreateNestedOneWithoutDevicesUpdatedInput
group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput group?: Prisma.DeviceGroupCreateNestedOneWithoutDevicesInput
@ -1402,6 +1726,8 @@ export type DeviceUncheckedCreateWithoutHistoryInput = {
updatedAt?: Date | string updatedAt?: Date | string
createdById?: string | null createdById?: string | null
updatedById?: string | null updatedById?: string | null
parentDeviceId?: string | null
accessories?: Prisma.DeviceUncheckedCreateNestedManyWithoutParentDeviceInput
tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput tags?: Prisma.TagUncheckedCreateNestedManyWithoutDevicesInput
} }
@ -1440,6 +1766,8 @@ export type DeviceUpdateWithoutHistoryInput = {
loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
parentDevice?: Prisma.DeviceUpdateOneWithoutAccessoriesNestedInput
accessories?: Prisma.DeviceUpdateManyWithoutParentDeviceNestedInput
createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput
updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput
group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput
@ -1470,6 +1798,8 @@ export type DeviceUncheckedUpdateWithoutHistoryInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
accessories?: Prisma.DeviceUncheckedUpdateManyWithoutParentDeviceNestedInput
tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput
} }
@ -1495,6 +1825,7 @@ export type DeviceCreateManyCreatedByInput = {
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
updatedById?: string | null updatedById?: string | null
parentDeviceId?: string | null
} }
export type DeviceCreateManyUpdatedByInput = { export type DeviceCreateManyUpdatedByInput = {
@ -1519,6 +1850,7 @@ export type DeviceCreateManyUpdatedByInput = {
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
createdById?: string | null createdById?: string | null
parentDeviceId?: string | null
} }
export type DeviceUpdateWithoutCreatedByInput = { export type DeviceUpdateWithoutCreatedByInput = {
@ -1540,6 +1872,8 @@ export type DeviceUpdateWithoutCreatedByInput = {
loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
parentDevice?: Prisma.DeviceUpdateOneWithoutAccessoriesNestedInput
accessories?: Prisma.DeviceUpdateManyWithoutParentDeviceNestedInput
updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput
group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput
location?: Prisma.LocationUpdateOneWithoutDevicesNestedInput location?: Prisma.LocationUpdateOneWithoutDevicesNestedInput
@ -1569,6 +1903,8 @@ export type DeviceUncheckedUpdateWithoutCreatedByInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
accessories?: Prisma.DeviceUncheckedUpdateManyWithoutParentDeviceNestedInput
history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput
tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput
} }
@ -1595,6 +1931,7 @@ export type DeviceUncheckedUpdateManyWithoutCreatedByInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type DeviceUpdateWithoutUpdatedByInput = { export type DeviceUpdateWithoutUpdatedByInput = {
@ -1616,6 +1953,8 @@ export type DeviceUpdateWithoutUpdatedByInput = {
loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
parentDevice?: Prisma.DeviceUpdateOneWithoutAccessoriesNestedInput
accessories?: Prisma.DeviceUpdateManyWithoutParentDeviceNestedInput
createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput
group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput
location?: Prisma.LocationUpdateOneWithoutDevicesNestedInput location?: Prisma.LocationUpdateOneWithoutDevicesNestedInput
@ -1645,6 +1984,8 @@ export type DeviceUncheckedUpdateWithoutUpdatedByInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
accessories?: Prisma.DeviceUncheckedUpdateManyWithoutParentDeviceNestedInput
history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput
tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput
} }
@ -1671,6 +2012,7 @@ export type DeviceUncheckedUpdateManyWithoutUpdatedByInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type DeviceCreateManyGroupInput = { export type DeviceCreateManyGroupInput = {
@ -1695,6 +2037,7 @@ export type DeviceCreateManyGroupInput = {
updatedAt?: Date | string updatedAt?: Date | string
createdById?: string | null createdById?: string | null
updatedById?: string | null updatedById?: string | null
parentDeviceId?: string | null
} }
export type DeviceUpdateWithoutGroupInput = { export type DeviceUpdateWithoutGroupInput = {
@ -1716,6 +2059,8 @@ export type DeviceUpdateWithoutGroupInput = {
loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
parentDevice?: Prisma.DeviceUpdateOneWithoutAccessoriesNestedInput
accessories?: Prisma.DeviceUpdateManyWithoutParentDeviceNestedInput
createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput
updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput
location?: Prisma.LocationUpdateOneWithoutDevicesNestedInput location?: Prisma.LocationUpdateOneWithoutDevicesNestedInput
@ -1745,6 +2090,8 @@ export type DeviceUncheckedUpdateWithoutGroupInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
accessories?: Prisma.DeviceUncheckedUpdateManyWithoutParentDeviceNestedInput
history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput
tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput
} }
@ -1771,6 +2118,7 @@ export type DeviceUncheckedUpdateManyWithoutGroupInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type DeviceCreateManyLocationInput = { export type DeviceCreateManyLocationInput = {
@ -1795,6 +2143,7 @@ export type DeviceCreateManyLocationInput = {
updatedAt?: Date | string updatedAt?: Date | string
createdById?: string | null createdById?: string | null
updatedById?: string | null updatedById?: string | null
parentDeviceId?: string | null
} }
export type DeviceUpdateWithoutLocationInput = { export type DeviceUpdateWithoutLocationInput = {
@ -1816,6 +2165,8 @@ export type DeviceUpdateWithoutLocationInput = {
loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
parentDevice?: Prisma.DeviceUpdateOneWithoutAccessoriesNestedInput
accessories?: Prisma.DeviceUpdateManyWithoutParentDeviceNestedInput
createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput
updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput
group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput
@ -1845,6 +2196,8 @@ export type DeviceUncheckedUpdateWithoutLocationInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
accessories?: Prisma.DeviceUncheckedUpdateManyWithoutParentDeviceNestedInput
history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput
tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput
} }
@ -1871,6 +2224,113 @@ export type DeviceUncheckedUpdateManyWithoutLocationInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type DeviceCreateManyParentDeviceInput = {
inventoryNumber: string
name: string
manufacturer: string
model: string
serialNumber?: string | null
productNumber?: string | null
comment?: string | null
ipv4Address?: string | null
ipv6Address?: string | null
macAddress?: string | null
username?: string | null
passwordHash?: string | null
groupId?: string | null
locationId?: string | null
loanedTo?: string | null
loanedFrom?: Date | string | null
loanedUntil?: Date | string | null
loanComment?: string | null
createdAt?: Date | string
updatedAt?: Date | string
createdById?: string | null
updatedById?: string | null
}
export type DeviceUpdateWithoutParentDeviceInput = {
inventoryNumber?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
manufacturer?: Prisma.StringFieldUpdateOperationsInput | string
model?: Prisma.StringFieldUpdateOperationsInput | string
serialNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
productNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
ipv4Address?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
ipv6Address?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
macAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
username?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
loanedTo?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
loanedFrom?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
loanedUntil?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accessories?: Prisma.DeviceUpdateManyWithoutParentDeviceNestedInput
createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput
updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput
group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput
location?: Prisma.LocationUpdateOneWithoutDevicesNestedInput
history?: Prisma.DeviceHistoryUpdateManyWithoutDeviceNestedInput
tags?: Prisma.TagUpdateManyWithoutDevicesNestedInput
}
export type DeviceUncheckedUpdateWithoutParentDeviceInput = {
inventoryNumber?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
manufacturer?: Prisma.StringFieldUpdateOperationsInput | string
model?: Prisma.StringFieldUpdateOperationsInput | string
serialNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
productNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
ipv4Address?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
ipv6Address?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
macAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
username?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
locationId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
loanedTo?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
loanedFrom?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
loanedUntil?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
accessories?: Prisma.DeviceUncheckedUpdateManyWithoutParentDeviceNestedInput
history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput
tags?: Prisma.TagUncheckedUpdateManyWithoutDevicesNestedInput
}
export type DeviceUncheckedUpdateManyWithoutParentDeviceInput = {
inventoryNumber?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
manufacturer?: Prisma.StringFieldUpdateOperationsInput | string
model?: Prisma.StringFieldUpdateOperationsInput | string
serialNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
productNumber?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
ipv4Address?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
ipv6Address?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
macAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
username?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
passwordHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
groupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
locationId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
loanedTo?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
loanedFrom?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
loanedUntil?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
export type DeviceUpdateWithoutTagsInput = { export type DeviceUpdateWithoutTagsInput = {
@ -1892,6 +2352,8 @@ export type DeviceUpdateWithoutTagsInput = {
loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null loanComment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
parentDevice?: Prisma.DeviceUpdateOneWithoutAccessoriesNestedInput
accessories?: Prisma.DeviceUpdateManyWithoutParentDeviceNestedInput
createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput createdBy?: Prisma.UserUpdateOneWithoutDevicesCreatedNestedInput
updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput updatedBy?: Prisma.UserUpdateOneWithoutDevicesUpdatedNestedInput
group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput group?: Prisma.DeviceGroupUpdateOneWithoutDevicesNestedInput
@ -1922,6 +2384,8 @@ export type DeviceUncheckedUpdateWithoutTagsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
accessories?: Prisma.DeviceUncheckedUpdateManyWithoutParentDeviceNestedInput
history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput history?: Prisma.DeviceHistoryUncheckedUpdateManyWithoutDeviceNestedInput
} }
@ -1948,6 +2412,7 @@ export type DeviceUncheckedUpdateManyWithoutTagsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null updatedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
parentDeviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
} }
@ -1956,11 +2421,13 @@ export type DeviceUncheckedUpdateManyWithoutTagsInput = {
*/ */
export type DeviceCountOutputType = { export type DeviceCountOutputType = {
accessories: number
history: number history: number
tags: number tags: number
} }
export type DeviceCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type DeviceCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
accessories?: boolean | DeviceCountOutputTypeCountAccessoriesArgs
history?: boolean | DeviceCountOutputTypeCountHistoryArgs history?: boolean | DeviceCountOutputTypeCountHistoryArgs
tags?: boolean | DeviceCountOutputTypeCountTagsArgs tags?: boolean | DeviceCountOutputTypeCountTagsArgs
} }
@ -1975,6 +2442,13 @@ export type DeviceCountOutputTypeDefaultArgs<ExtArgs extends runtime.Types.Exten
select?: Prisma.DeviceCountOutputTypeSelect<ExtArgs> | null select?: Prisma.DeviceCountOutputTypeSelect<ExtArgs> | null
} }
/**
* DeviceCountOutputType without action
*/
export type DeviceCountOutputTypeCountAccessoriesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.DeviceWhereInput
}
/** /**
* DeviceCountOutputType without action * DeviceCountOutputType without action
*/ */
@ -2013,6 +2487,9 @@ export type DeviceSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
updatedAt?: boolean updatedAt?: boolean
createdById?: boolean createdById?: boolean
updatedById?: boolean updatedById?: boolean
parentDeviceId?: boolean
parentDevice?: boolean | Prisma.Device$parentDeviceArgs<ExtArgs>
accessories?: boolean | Prisma.Device$accessoriesArgs<ExtArgs>
createdBy?: boolean | Prisma.Device$createdByArgs<ExtArgs> createdBy?: boolean | Prisma.Device$createdByArgs<ExtArgs>
updatedBy?: boolean | Prisma.Device$updatedByArgs<ExtArgs> updatedBy?: boolean | Prisma.Device$updatedByArgs<ExtArgs>
group?: boolean | Prisma.Device$groupArgs<ExtArgs> group?: boolean | Prisma.Device$groupArgs<ExtArgs>
@ -2045,6 +2522,8 @@ export type DeviceSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extens
updatedAt?: boolean updatedAt?: boolean
createdById?: boolean createdById?: boolean
updatedById?: boolean updatedById?: boolean
parentDeviceId?: boolean
parentDevice?: boolean | Prisma.Device$parentDeviceArgs<ExtArgs>
createdBy?: boolean | Prisma.Device$createdByArgs<ExtArgs> createdBy?: boolean | Prisma.Device$createdByArgs<ExtArgs>
updatedBy?: boolean | Prisma.Device$updatedByArgs<ExtArgs> updatedBy?: boolean | Prisma.Device$updatedByArgs<ExtArgs>
group?: boolean | Prisma.Device$groupArgs<ExtArgs> group?: boolean | Prisma.Device$groupArgs<ExtArgs>
@ -2074,6 +2553,8 @@ export type DeviceSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extens
updatedAt?: boolean updatedAt?: boolean
createdById?: boolean createdById?: boolean
updatedById?: boolean updatedById?: boolean
parentDeviceId?: boolean
parentDevice?: boolean | Prisma.Device$parentDeviceArgs<ExtArgs>
createdBy?: boolean | Prisma.Device$createdByArgs<ExtArgs> createdBy?: boolean | Prisma.Device$createdByArgs<ExtArgs>
updatedBy?: boolean | Prisma.Device$updatedByArgs<ExtArgs> updatedBy?: boolean | Prisma.Device$updatedByArgs<ExtArgs>
group?: boolean | Prisma.Device$groupArgs<ExtArgs> group?: boolean | Prisma.Device$groupArgs<ExtArgs>
@ -2103,10 +2584,13 @@ export type DeviceSelectScalar = {
updatedAt?: boolean updatedAt?: boolean
createdById?: boolean createdById?: boolean
updatedById?: boolean updatedById?: boolean
parentDeviceId?: boolean
} }
export type DeviceOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"inventoryNumber" | "name" | "manufacturer" | "model" | "serialNumber" | "productNumber" | "comment" | "ipv4Address" | "ipv6Address" | "macAddress" | "username" | "passwordHash" | "groupId" | "locationId" | "loanedTo" | "loanedFrom" | "loanedUntil" | "loanComment" | "createdAt" | "updatedAt" | "createdById" | "updatedById", ExtArgs["result"]["device"]> export type DeviceOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"inventoryNumber" | "name" | "manufacturer" | "model" | "serialNumber" | "productNumber" | "comment" | "ipv4Address" | "ipv6Address" | "macAddress" | "username" | "passwordHash" | "groupId" | "locationId" | "loanedTo" | "loanedFrom" | "loanedUntil" | "loanComment" | "createdAt" | "updatedAt" | "createdById" | "updatedById" | "parentDeviceId", ExtArgs["result"]["device"]>
export type DeviceInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type DeviceInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
parentDevice?: boolean | Prisma.Device$parentDeviceArgs<ExtArgs>
accessories?: boolean | Prisma.Device$accessoriesArgs<ExtArgs>
createdBy?: boolean | Prisma.Device$createdByArgs<ExtArgs> createdBy?: boolean | Prisma.Device$createdByArgs<ExtArgs>
updatedBy?: boolean | Prisma.Device$updatedByArgs<ExtArgs> updatedBy?: boolean | Prisma.Device$updatedByArgs<ExtArgs>
group?: boolean | Prisma.Device$groupArgs<ExtArgs> group?: boolean | Prisma.Device$groupArgs<ExtArgs>
@ -2116,12 +2600,14 @@ export type DeviceInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs
_count?: boolean | Prisma.DeviceCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.DeviceCountOutputTypeDefaultArgs<ExtArgs>
} }
export type DeviceIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type DeviceIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
parentDevice?: boolean | Prisma.Device$parentDeviceArgs<ExtArgs>
createdBy?: boolean | Prisma.Device$createdByArgs<ExtArgs> createdBy?: boolean | Prisma.Device$createdByArgs<ExtArgs>
updatedBy?: boolean | Prisma.Device$updatedByArgs<ExtArgs> updatedBy?: boolean | Prisma.Device$updatedByArgs<ExtArgs>
group?: boolean | Prisma.Device$groupArgs<ExtArgs> group?: boolean | Prisma.Device$groupArgs<ExtArgs>
location?: boolean | Prisma.Device$locationArgs<ExtArgs> location?: boolean | Prisma.Device$locationArgs<ExtArgs>
} }
export type DeviceIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type DeviceIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
parentDevice?: boolean | Prisma.Device$parentDeviceArgs<ExtArgs>
createdBy?: boolean | Prisma.Device$createdByArgs<ExtArgs> createdBy?: boolean | Prisma.Device$createdByArgs<ExtArgs>
updatedBy?: boolean | Prisma.Device$updatedByArgs<ExtArgs> updatedBy?: boolean | Prisma.Device$updatedByArgs<ExtArgs>
group?: boolean | Prisma.Device$groupArgs<ExtArgs> group?: boolean | Prisma.Device$groupArgs<ExtArgs>
@ -2131,6 +2617,8 @@ export type DeviceIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Exten
export type $DevicePayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type $DevicePayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "Device" name: "Device"
objects: { objects: {
parentDevice: Prisma.$DevicePayload<ExtArgs> | null
accessories: Prisma.$DevicePayload<ExtArgs>[]
createdBy: Prisma.$UserPayload<ExtArgs> | null createdBy: Prisma.$UserPayload<ExtArgs> | null
updatedBy: Prisma.$UserPayload<ExtArgs> | null updatedBy: Prisma.$UserPayload<ExtArgs> | null
group: Prisma.$DeviceGroupPayload<ExtArgs> | null group: Prisma.$DeviceGroupPayload<ExtArgs> | null
@ -2161,6 +2649,7 @@ export type $DevicePayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
updatedAt: Date updatedAt: Date
createdById: string | null createdById: string | null
updatedById: string | null updatedById: string | null
parentDeviceId: string | null
}, ExtArgs["result"]["device"]> }, ExtArgs["result"]["device"]>
composites: {} composites: {}
} }
@ -2555,6 +3044,8 @@ readonly fields: DeviceFieldRefs;
*/ */
export interface Prisma__DeviceClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> { export interface Prisma__DeviceClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise" readonly [Symbol.toStringTag]: "PrismaPromise"
parentDevice<T extends Prisma.Device$parentDeviceArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Device$parentDeviceArgs<ExtArgs>>): Prisma.Prisma__DeviceClient<runtime.Types.Result.GetResult<Prisma.$DevicePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
accessories<T extends Prisma.Device$accessoriesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Device$accessoriesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$DevicePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
createdBy<T extends Prisma.Device$createdByArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Device$createdByArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> createdBy<T extends Prisma.Device$createdByArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Device$createdByArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
updatedBy<T extends Prisma.Device$updatedByArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Device$updatedByArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> updatedBy<T extends Prisma.Device$updatedByArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Device$updatedByArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
group<T extends Prisma.Device$groupArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Device$groupArgs<ExtArgs>>): Prisma.Prisma__DeviceGroupClient<runtime.Types.Result.GetResult<Prisma.$DeviceGroupPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> group<T extends Prisma.Device$groupArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Device$groupArgs<ExtArgs>>): Prisma.Prisma__DeviceGroupClient<runtime.Types.Result.GetResult<Prisma.$DeviceGroupPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
@ -2612,6 +3103,7 @@ export interface DeviceFieldRefs {
readonly updatedAt: Prisma.FieldRef<"Device", 'DateTime'> readonly updatedAt: Prisma.FieldRef<"Device", 'DateTime'>
readonly createdById: Prisma.FieldRef<"Device", 'String'> readonly createdById: Prisma.FieldRef<"Device", 'String'>
readonly updatedById: Prisma.FieldRef<"Device", 'String'> readonly updatedById: Prisma.FieldRef<"Device", 'String'>
readonly parentDeviceId: Prisma.FieldRef<"Device", 'String'>
} }
@ -3007,6 +3499,49 @@ export type DeviceDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Intern
limit?: number limit?: number
} }
/**
* Device.parentDevice
*/
export type Device$parentDeviceArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Device
*/
select?: Prisma.DeviceSelect<ExtArgs> | null
/**
* Omit specific fields from the Device
*/
omit?: Prisma.DeviceOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.DeviceInclude<ExtArgs> | null
where?: Prisma.DeviceWhereInput
}
/**
* Device.accessories
*/
export type Device$accessoriesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Device
*/
select?: Prisma.DeviceSelect<ExtArgs> | null
/**
* Omit specific fields from the Device
*/
omit?: Prisma.DeviceOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.DeviceInclude<ExtArgs> | null
where?: Prisma.DeviceWhereInput
orderBy?: Prisma.DeviceOrderByWithRelationInput | Prisma.DeviceOrderByWithRelationInput[]
cursor?: Prisma.DeviceWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.DeviceScalarFieldEnum | Prisma.DeviceScalarFieldEnum[]
}
/** /**
* Device.createdBy * Device.createdBy
*/ */

View File

@ -151,7 +151,7 @@ export type DeviceHistoryGroupByArgs<ExtArgs extends runtime.Types.Extensions.In
export type DeviceHistoryGroupByOutputType = { export type DeviceHistoryGroupByOutputType = {
id: string id: string
deviceId: string deviceId: string | null
changeType: $Enums.DeviceChangeType changeType: $Enums.DeviceChangeType
snapshot: runtime.JsonValue snapshot: runtime.JsonValue
changedAt: Date changedAt: Date
@ -181,18 +181,18 @@ export type DeviceHistoryWhereInput = {
OR?: Prisma.DeviceHistoryWhereInput[] OR?: Prisma.DeviceHistoryWhereInput[]
NOT?: Prisma.DeviceHistoryWhereInput | Prisma.DeviceHistoryWhereInput[] NOT?: Prisma.DeviceHistoryWhereInput | Prisma.DeviceHistoryWhereInput[]
id?: Prisma.StringFilter<"DeviceHistory"> | string id?: Prisma.StringFilter<"DeviceHistory"> | string
deviceId?: Prisma.StringFilter<"DeviceHistory"> | string deviceId?: Prisma.StringNullableFilter<"DeviceHistory"> | string | null
changeType?: Prisma.EnumDeviceChangeTypeFilter<"DeviceHistory"> | $Enums.DeviceChangeType changeType?: Prisma.EnumDeviceChangeTypeFilter<"DeviceHistory"> | $Enums.DeviceChangeType
snapshot?: Prisma.JsonFilter<"DeviceHistory"> snapshot?: Prisma.JsonFilter<"DeviceHistory">
changedAt?: Prisma.DateTimeFilter<"DeviceHistory"> | Date | string changedAt?: Prisma.DateTimeFilter<"DeviceHistory"> | Date | string
changedById?: Prisma.StringNullableFilter<"DeviceHistory"> | string | null changedById?: Prisma.StringNullableFilter<"DeviceHistory"> | string | null
changedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null changedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
device?: Prisma.XOR<Prisma.DeviceScalarRelationFilter, Prisma.DeviceWhereInput> device?: Prisma.XOR<Prisma.DeviceNullableScalarRelationFilter, Prisma.DeviceWhereInput> | null
} }
export type DeviceHistoryOrderByWithRelationInput = { export type DeviceHistoryOrderByWithRelationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
deviceId?: Prisma.SortOrder deviceId?: Prisma.SortOrderInput | Prisma.SortOrder
changeType?: Prisma.SortOrder changeType?: Prisma.SortOrder
snapshot?: Prisma.SortOrder snapshot?: Prisma.SortOrder
changedAt?: Prisma.SortOrder changedAt?: Prisma.SortOrder
@ -206,18 +206,18 @@ export type DeviceHistoryWhereUniqueInput = Prisma.AtLeast<{
AND?: Prisma.DeviceHistoryWhereInput | Prisma.DeviceHistoryWhereInput[] AND?: Prisma.DeviceHistoryWhereInput | Prisma.DeviceHistoryWhereInput[]
OR?: Prisma.DeviceHistoryWhereInput[] OR?: Prisma.DeviceHistoryWhereInput[]
NOT?: Prisma.DeviceHistoryWhereInput | Prisma.DeviceHistoryWhereInput[] NOT?: Prisma.DeviceHistoryWhereInput | Prisma.DeviceHistoryWhereInput[]
deviceId?: Prisma.StringFilter<"DeviceHistory"> | string deviceId?: Prisma.StringNullableFilter<"DeviceHistory"> | string | null
changeType?: Prisma.EnumDeviceChangeTypeFilter<"DeviceHistory"> | $Enums.DeviceChangeType changeType?: Prisma.EnumDeviceChangeTypeFilter<"DeviceHistory"> | $Enums.DeviceChangeType
snapshot?: Prisma.JsonFilter<"DeviceHistory"> snapshot?: Prisma.JsonFilter<"DeviceHistory">
changedAt?: Prisma.DateTimeFilter<"DeviceHistory"> | Date | string changedAt?: Prisma.DateTimeFilter<"DeviceHistory"> | Date | string
changedById?: Prisma.StringNullableFilter<"DeviceHistory"> | string | null changedById?: Prisma.StringNullableFilter<"DeviceHistory"> | string | null
changedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null changedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
device?: Prisma.XOR<Prisma.DeviceScalarRelationFilter, Prisma.DeviceWhereInput> device?: Prisma.XOR<Prisma.DeviceNullableScalarRelationFilter, Prisma.DeviceWhereInput> | null
}, "id"> }, "id">
export type DeviceHistoryOrderByWithAggregationInput = { export type DeviceHistoryOrderByWithAggregationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
deviceId?: Prisma.SortOrder deviceId?: Prisma.SortOrderInput | Prisma.SortOrder
changeType?: Prisma.SortOrder changeType?: Prisma.SortOrder
snapshot?: Prisma.SortOrder snapshot?: Prisma.SortOrder
changedAt?: Prisma.SortOrder changedAt?: Prisma.SortOrder
@ -232,7 +232,7 @@ export type DeviceHistoryScalarWhereWithAggregatesInput = {
OR?: Prisma.DeviceHistoryScalarWhereWithAggregatesInput[] OR?: Prisma.DeviceHistoryScalarWhereWithAggregatesInput[]
NOT?: Prisma.DeviceHistoryScalarWhereWithAggregatesInput | Prisma.DeviceHistoryScalarWhereWithAggregatesInput[] NOT?: Prisma.DeviceHistoryScalarWhereWithAggregatesInput | Prisma.DeviceHistoryScalarWhereWithAggregatesInput[]
id?: Prisma.StringWithAggregatesFilter<"DeviceHistory"> | string id?: Prisma.StringWithAggregatesFilter<"DeviceHistory"> | string
deviceId?: Prisma.StringWithAggregatesFilter<"DeviceHistory"> | string deviceId?: Prisma.StringNullableWithAggregatesFilter<"DeviceHistory"> | string | null
changeType?: Prisma.EnumDeviceChangeTypeWithAggregatesFilter<"DeviceHistory"> | $Enums.DeviceChangeType changeType?: Prisma.EnumDeviceChangeTypeWithAggregatesFilter<"DeviceHistory"> | $Enums.DeviceChangeType
snapshot?: Prisma.JsonWithAggregatesFilter<"DeviceHistory"> snapshot?: Prisma.JsonWithAggregatesFilter<"DeviceHistory">
changedAt?: Prisma.DateTimeWithAggregatesFilter<"DeviceHistory"> | Date | string changedAt?: Prisma.DateTimeWithAggregatesFilter<"DeviceHistory"> | Date | string
@ -245,12 +245,12 @@ export type DeviceHistoryCreateInput = {
snapshot: Prisma.JsonNullValueInput | runtime.InputJsonValue snapshot: Prisma.JsonNullValueInput | runtime.InputJsonValue
changedAt?: Date | string changedAt?: Date | string
changedBy?: Prisma.UserCreateNestedOneWithoutHistoryEntriesInput changedBy?: Prisma.UserCreateNestedOneWithoutHistoryEntriesInput
device: Prisma.DeviceCreateNestedOneWithoutHistoryInput device?: Prisma.DeviceCreateNestedOneWithoutHistoryInput
} }
export type DeviceHistoryUncheckedCreateInput = { export type DeviceHistoryUncheckedCreateInput = {
id?: string id?: string
deviceId: string deviceId?: string | null
changeType: $Enums.DeviceChangeType changeType: $Enums.DeviceChangeType
snapshot: Prisma.JsonNullValueInput | runtime.InputJsonValue snapshot: Prisma.JsonNullValueInput | runtime.InputJsonValue
changedAt?: Date | string changedAt?: Date | string
@ -263,12 +263,12 @@ export type DeviceHistoryUpdateInput = {
snapshot?: Prisma.JsonNullValueInput | runtime.InputJsonValue snapshot?: Prisma.JsonNullValueInput | runtime.InputJsonValue
changedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string changedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
changedBy?: Prisma.UserUpdateOneWithoutHistoryEntriesNestedInput changedBy?: Prisma.UserUpdateOneWithoutHistoryEntriesNestedInput
device?: Prisma.DeviceUpdateOneRequiredWithoutHistoryNestedInput device?: Prisma.DeviceUpdateOneWithoutHistoryNestedInput
} }
export type DeviceHistoryUncheckedUpdateInput = { export type DeviceHistoryUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
deviceId?: Prisma.StringFieldUpdateOperationsInput | string deviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
changeType?: Prisma.EnumDeviceChangeTypeFieldUpdateOperationsInput | $Enums.DeviceChangeType changeType?: Prisma.EnumDeviceChangeTypeFieldUpdateOperationsInput | $Enums.DeviceChangeType
snapshot?: Prisma.JsonNullValueInput | runtime.InputJsonValue snapshot?: Prisma.JsonNullValueInput | runtime.InputJsonValue
changedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string changedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@ -277,7 +277,7 @@ export type DeviceHistoryUncheckedUpdateInput = {
export type DeviceHistoryCreateManyInput = { export type DeviceHistoryCreateManyInput = {
id?: string id?: string
deviceId: string deviceId?: string | null
changeType: $Enums.DeviceChangeType changeType: $Enums.DeviceChangeType
snapshot: Prisma.JsonNullValueInput | runtime.InputJsonValue snapshot: Prisma.JsonNullValueInput | runtime.InputJsonValue
changedAt?: Date | string changedAt?: Date | string
@ -293,7 +293,7 @@ export type DeviceHistoryUpdateManyMutationInput = {
export type DeviceHistoryUncheckedUpdateManyInput = { export type DeviceHistoryUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
deviceId?: Prisma.StringFieldUpdateOperationsInput | string deviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
changeType?: Prisma.EnumDeviceChangeTypeFieldUpdateOperationsInput | $Enums.DeviceChangeType changeType?: Prisma.EnumDeviceChangeTypeFieldUpdateOperationsInput | $Enums.DeviceChangeType
snapshot?: Prisma.JsonNullValueInput | runtime.InputJsonValue snapshot?: Prisma.JsonNullValueInput | runtime.InputJsonValue
changedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string changedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@ -428,12 +428,12 @@ export type DeviceHistoryCreateWithoutChangedByInput = {
changeType: $Enums.DeviceChangeType changeType: $Enums.DeviceChangeType
snapshot: Prisma.JsonNullValueInput | runtime.InputJsonValue snapshot: Prisma.JsonNullValueInput | runtime.InputJsonValue
changedAt?: Date | string changedAt?: Date | string
device: Prisma.DeviceCreateNestedOneWithoutHistoryInput device?: Prisma.DeviceCreateNestedOneWithoutHistoryInput
} }
export type DeviceHistoryUncheckedCreateWithoutChangedByInput = { export type DeviceHistoryUncheckedCreateWithoutChangedByInput = {
id?: string id?: string
deviceId: string deviceId?: string | null
changeType: $Enums.DeviceChangeType changeType: $Enums.DeviceChangeType
snapshot: Prisma.JsonNullValueInput | runtime.InputJsonValue snapshot: Prisma.JsonNullValueInput | runtime.InputJsonValue
changedAt?: Date | string changedAt?: Date | string
@ -470,7 +470,7 @@ export type DeviceHistoryScalarWhereInput = {
OR?: Prisma.DeviceHistoryScalarWhereInput[] OR?: Prisma.DeviceHistoryScalarWhereInput[]
NOT?: Prisma.DeviceHistoryScalarWhereInput | Prisma.DeviceHistoryScalarWhereInput[] NOT?: Prisma.DeviceHistoryScalarWhereInput | Prisma.DeviceHistoryScalarWhereInput[]
id?: Prisma.StringFilter<"DeviceHistory"> | string id?: Prisma.StringFilter<"DeviceHistory"> | string
deviceId?: Prisma.StringFilter<"DeviceHistory"> | string deviceId?: Prisma.StringNullableFilter<"DeviceHistory"> | string | null
changeType?: Prisma.EnumDeviceChangeTypeFilter<"DeviceHistory"> | $Enums.DeviceChangeType changeType?: Prisma.EnumDeviceChangeTypeFilter<"DeviceHistory"> | $Enums.DeviceChangeType
snapshot?: Prisma.JsonFilter<"DeviceHistory"> snapshot?: Prisma.JsonFilter<"DeviceHistory">
changedAt?: Prisma.DateTimeFilter<"DeviceHistory"> | Date | string changedAt?: Prisma.DateTimeFilter<"DeviceHistory"> | Date | string
@ -521,7 +521,7 @@ export type DeviceHistoryUpdateManyWithWhereWithoutDeviceInput = {
export type DeviceHistoryCreateManyChangedByInput = { export type DeviceHistoryCreateManyChangedByInput = {
id?: string id?: string
deviceId: string deviceId?: string | null
changeType: $Enums.DeviceChangeType changeType: $Enums.DeviceChangeType
snapshot: Prisma.JsonNullValueInput | runtime.InputJsonValue snapshot: Prisma.JsonNullValueInput | runtime.InputJsonValue
changedAt?: Date | string changedAt?: Date | string
@ -532,12 +532,12 @@ export type DeviceHistoryUpdateWithoutChangedByInput = {
changeType?: Prisma.EnumDeviceChangeTypeFieldUpdateOperationsInput | $Enums.DeviceChangeType changeType?: Prisma.EnumDeviceChangeTypeFieldUpdateOperationsInput | $Enums.DeviceChangeType
snapshot?: Prisma.JsonNullValueInput | runtime.InputJsonValue snapshot?: Prisma.JsonNullValueInput | runtime.InputJsonValue
changedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string changedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
device?: Prisma.DeviceUpdateOneRequiredWithoutHistoryNestedInput device?: Prisma.DeviceUpdateOneWithoutHistoryNestedInput
} }
export type DeviceHistoryUncheckedUpdateWithoutChangedByInput = { export type DeviceHistoryUncheckedUpdateWithoutChangedByInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
deviceId?: Prisma.StringFieldUpdateOperationsInput | string deviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
changeType?: Prisma.EnumDeviceChangeTypeFieldUpdateOperationsInput | $Enums.DeviceChangeType changeType?: Prisma.EnumDeviceChangeTypeFieldUpdateOperationsInput | $Enums.DeviceChangeType
snapshot?: Prisma.JsonNullValueInput | runtime.InputJsonValue snapshot?: Prisma.JsonNullValueInput | runtime.InputJsonValue
changedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string changedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@ -545,7 +545,7 @@ export type DeviceHistoryUncheckedUpdateWithoutChangedByInput = {
export type DeviceHistoryUncheckedUpdateManyWithoutChangedByInput = { export type DeviceHistoryUncheckedUpdateManyWithoutChangedByInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
deviceId?: Prisma.StringFieldUpdateOperationsInput | string deviceId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
changeType?: Prisma.EnumDeviceChangeTypeFieldUpdateOperationsInput | $Enums.DeviceChangeType changeType?: Prisma.EnumDeviceChangeTypeFieldUpdateOperationsInput | $Enums.DeviceChangeType
snapshot?: Prisma.JsonNullValueInput | runtime.InputJsonValue snapshot?: Prisma.JsonNullValueInput | runtime.InputJsonValue
changedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string changedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@ -593,7 +593,7 @@ export type DeviceHistorySelect<ExtArgs extends runtime.Types.Extensions.Interna
changedAt?: boolean changedAt?: boolean
changedById?: boolean changedById?: boolean
changedBy?: boolean | Prisma.DeviceHistory$changedByArgs<ExtArgs> changedBy?: boolean | Prisma.DeviceHistory$changedByArgs<ExtArgs>
device?: boolean | Prisma.DeviceDefaultArgs<ExtArgs> device?: boolean | Prisma.DeviceHistory$deviceArgs<ExtArgs>
}, ExtArgs["result"]["deviceHistory"]> }, ExtArgs["result"]["deviceHistory"]>
export type DeviceHistorySelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type DeviceHistorySelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@ -604,7 +604,7 @@ export type DeviceHistorySelectCreateManyAndReturn<ExtArgs extends runtime.Types
changedAt?: boolean changedAt?: boolean
changedById?: boolean changedById?: boolean
changedBy?: boolean | Prisma.DeviceHistory$changedByArgs<ExtArgs> changedBy?: boolean | Prisma.DeviceHistory$changedByArgs<ExtArgs>
device?: boolean | Prisma.DeviceDefaultArgs<ExtArgs> device?: boolean | Prisma.DeviceHistory$deviceArgs<ExtArgs>
}, ExtArgs["result"]["deviceHistory"]> }, ExtArgs["result"]["deviceHistory"]>
export type DeviceHistorySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type DeviceHistorySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@ -615,7 +615,7 @@ export type DeviceHistorySelectUpdateManyAndReturn<ExtArgs extends runtime.Types
changedAt?: boolean changedAt?: boolean
changedById?: boolean changedById?: boolean
changedBy?: boolean | Prisma.DeviceHistory$changedByArgs<ExtArgs> changedBy?: boolean | Prisma.DeviceHistory$changedByArgs<ExtArgs>
device?: boolean | Prisma.DeviceDefaultArgs<ExtArgs> device?: boolean | Prisma.DeviceHistory$deviceArgs<ExtArgs>
}, ExtArgs["result"]["deviceHistory"]> }, ExtArgs["result"]["deviceHistory"]>
export type DeviceHistorySelectScalar = { export type DeviceHistorySelectScalar = {
@ -630,26 +630,26 @@ export type DeviceHistorySelectScalar = {
export type DeviceHistoryOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "deviceId" | "changeType" | "snapshot" | "changedAt" | "changedById", ExtArgs["result"]["deviceHistory"]> export type DeviceHistoryOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "deviceId" | "changeType" | "snapshot" | "changedAt" | "changedById", ExtArgs["result"]["deviceHistory"]>
export type DeviceHistoryInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type DeviceHistoryInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
changedBy?: boolean | Prisma.DeviceHistory$changedByArgs<ExtArgs> changedBy?: boolean | Prisma.DeviceHistory$changedByArgs<ExtArgs>
device?: boolean | Prisma.DeviceDefaultArgs<ExtArgs> device?: boolean | Prisma.DeviceHistory$deviceArgs<ExtArgs>
} }
export type DeviceHistoryIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type DeviceHistoryIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
changedBy?: boolean | Prisma.DeviceHistory$changedByArgs<ExtArgs> changedBy?: boolean | Prisma.DeviceHistory$changedByArgs<ExtArgs>
device?: boolean | Prisma.DeviceDefaultArgs<ExtArgs> device?: boolean | Prisma.DeviceHistory$deviceArgs<ExtArgs>
} }
export type DeviceHistoryIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type DeviceHistoryIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
changedBy?: boolean | Prisma.DeviceHistory$changedByArgs<ExtArgs> changedBy?: boolean | Prisma.DeviceHistory$changedByArgs<ExtArgs>
device?: boolean | Prisma.DeviceDefaultArgs<ExtArgs> device?: boolean | Prisma.DeviceHistory$deviceArgs<ExtArgs>
} }
export type $DeviceHistoryPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type $DeviceHistoryPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "DeviceHistory" name: "DeviceHistory"
objects: { objects: {
changedBy: Prisma.$UserPayload<ExtArgs> | null changedBy: Prisma.$UserPayload<ExtArgs> | null
device: Prisma.$DevicePayload<ExtArgs> device: Prisma.$DevicePayload<ExtArgs> | null
} }
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string id: string
deviceId: string deviceId: string | null
changeType: $Enums.DeviceChangeType changeType: $Enums.DeviceChangeType
snapshot: runtime.JsonValue snapshot: runtime.JsonValue
changedAt: Date changedAt: Date
@ -1049,7 +1049,7 @@ readonly fields: DeviceHistoryFieldRefs;
export interface Prisma__DeviceHistoryClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> { export interface Prisma__DeviceHistoryClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise" readonly [Symbol.toStringTag]: "PrismaPromise"
changedBy<T extends Prisma.DeviceHistory$changedByArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.DeviceHistory$changedByArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> changedBy<T extends Prisma.DeviceHistory$changedByArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.DeviceHistory$changedByArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
device<T extends Prisma.DeviceDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.DeviceDefaultArgs<ExtArgs>>): Prisma.Prisma__DeviceClient<runtime.Types.Result.GetResult<Prisma.$DevicePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions> device<T extends Prisma.DeviceHistory$deviceArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.DeviceHistory$deviceArgs<ExtArgs>>): Prisma.Prisma__DeviceClient<runtime.Types.Result.GetResult<Prisma.$DevicePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
/** /**
* Attaches callbacks for the resolution and/or rejection of the Promise. * Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved. * @param onfulfilled The callback to execute when the Promise is resolved.
@ -1499,6 +1499,25 @@ export type DeviceHistory$changedByArgs<ExtArgs extends runtime.Types.Extensions
where?: Prisma.UserWhereInput where?: Prisma.UserWhereInput
} }
/**
* DeviceHistory.device
*/
export type DeviceHistory$deviceArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Device
*/
select?: Prisma.DeviceSelect<ExtArgs> | null
/**
* Omit specific fields from the Device
*/
omit?: Prisma.DeviceOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.DeviceInclude<ExtArgs> | null
where?: Prisma.DeviceWhereInput
}
/** /**
* DeviceHistory without action * DeviceHistory without action
*/ */

View File

@ -10,6 +10,9 @@
"lint": "eslint", "lint": "eslint",
"seed": "prisma db seed" "seed": "prisma db seed"
}, },
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.9", "@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",

View File

@ -1,8 +0,0 @@
/*
Warnings:
- You are about to drop the column `name` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "name";

View File

@ -1,16 +0,0 @@
/*
Warnings:
- You are about to drop the column `username` on the `User` table. All the data in the column will be lost.
- A unique constraint covering the columns `[nwkennung]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "User_username_key";
-- AlterTable
ALTER TABLE "User" DROP COLUMN "username",
ADD COLUMN "nwkennung" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "User_nwkennung_key" ON "User"("nwkennung");

View File

@ -1,43 +0,0 @@
/*
Warnings:
- The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `User` table. All the data in the column will be lost.
- Made the column `nwkennung` on table `User` required. This step will fail if there are existing NULL values in that column.
*/
-- DropForeignKey
ALTER TABLE "Device" DROP CONSTRAINT "Device_createdById_fkey";
-- DropForeignKey
ALTER TABLE "Device" DROP CONSTRAINT "Device_updatedById_fkey";
-- DropForeignKey
ALTER TABLE "DeviceHistory" DROP CONSTRAINT "DeviceHistory_changedById_fkey";
-- DropForeignKey
ALTER TABLE "UserRole" DROP CONSTRAINT "UserRole_userId_fkey";
-- DropIndex
DROP INDEX "Device_inventoryNumber_key";
-- DropIndex
DROP INDEX "User_nwkennung_key";
-- AlterTable
ALTER TABLE "User" DROP CONSTRAINT "User_pkey",
DROP COLUMN "id",
ALTER COLUMN "nwkennung" SET NOT NULL,
ADD CONSTRAINT "User_pkey" PRIMARY KEY ("nwkennung");
-- AddForeignKey
ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("nwkennung") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Device" ADD CONSTRAINT "Device_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("nwkennung") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Device" ADD CONSTRAINT "Device_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES "User"("nwkennung") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DeviceHistory" ADD CONSTRAINT "DeviceHistory_changedById_fkey" FOREIGN KEY ("changedById") REFERENCES "User"("nwkennung") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -3,10 +3,8 @@ CREATE TYPE "DeviceChangeType" AS ENUM ('CREATED', 'UPDATED', 'DELETED');
-- CreateTable -- CreateTable
CREATE TABLE "User" ( CREATE TABLE "User" (
"id" TEXT NOT NULL, "nwkennung" TEXT NOT NULL,
"email" TEXT, "email" TEXT,
"username" TEXT,
"name" TEXT,
"arbeitsname" TEXT, "arbeitsname" TEXT,
"firstName" TEXT, "firstName" TEXT,
"lastName" TEXT, "lastName" TEXT,
@ -15,7 +13,7 @@ CREATE TABLE "User" (
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id") CONSTRAINT "User_pkey" PRIMARY KEY ("nwkennung")
); );
-- CreateTable -- CreateTable
@ -83,6 +81,7 @@ CREATE TABLE "Device" (
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
"createdById" TEXT, "createdById" TEXT,
"updatedById" TEXT, "updatedById" TEXT,
"parentDeviceId" TEXT,
CONSTRAINT "Device_pkey" PRIMARY KEY ("inventoryNumber") CONSTRAINT "Device_pkey" PRIMARY KEY ("inventoryNumber")
); );
@ -98,7 +97,7 @@ CREATE TABLE "Tag" (
-- CreateTable -- CreateTable
CREATE TABLE "DeviceHistory" ( CREATE TABLE "DeviceHistory" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"deviceId" TEXT NOT NULL, "deviceId" TEXT,
"changeType" "DeviceChangeType" NOT NULL, "changeType" "DeviceChangeType" NOT NULL,
"snapshot" JSONB NOT NULL, "snapshot" JSONB NOT NULL,
"changedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "changedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -118,9 +117,6 @@ CREATE TABLE "_DeviceToTag" (
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- CreateIndex -- CreateIndex
CREATE INDEX "User_groupId_idx" ON "User"("groupId"); CREATE INDEX "User_groupId_idx" ON "User"("groupId");
@ -136,9 +132,6 @@ CREATE UNIQUE INDEX "DeviceGroup_name_key" ON "DeviceGroup"("name");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Location_name_key" ON "Location"("name"); CREATE UNIQUE INDEX "Location_name_key" ON "Location"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Device_inventoryNumber_key" ON "Device"("inventoryNumber");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Device_ipv4Address_key" ON "Device"("ipv4Address"); CREATE UNIQUE INDEX "Device_ipv4Address_key" ON "Device"("ipv4Address");
@ -163,6 +156,9 @@ CREATE INDEX "Device_groupId_idx" ON "Device"("groupId");
-- CreateIndex -- CreateIndex
CREATE INDEX "Device_locationId_idx" ON "Device"("locationId"); CREATE INDEX "Device_locationId_idx" ON "Device"("locationId");
-- CreateIndex
CREATE INDEX "Device_parentDeviceId_idx" ON "Device"("parentDeviceId");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name"); CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name");
@ -173,10 +169,19 @@ CREATE INDEX "_DeviceToTag_B_index" ON "_DeviceToTag"("B");
ALTER TABLE "User" ADD CONSTRAINT "User_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "UserGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "User" ADD CONSTRAINT "User_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "UserGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("nwkennung") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Device" ADD CONSTRAINT "Device_parentDeviceId_fkey" FOREIGN KEY ("parentDeviceId") REFERENCES "Device"("inventoryNumber") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Device" ADD CONSTRAINT "Device_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("nwkennung") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Device" ADD CONSTRAINT "Device_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES "User"("nwkennung") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Device" ADD CONSTRAINT "Device_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "DeviceGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Device" ADD CONSTRAINT "Device_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "DeviceGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@ -185,16 +190,10 @@ ALTER TABLE "Device" ADD CONSTRAINT "Device_groupId_fkey" FOREIGN KEY ("groupId"
ALTER TABLE "Device" ADD CONSTRAINT "Device_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Device" ADD CONSTRAINT "Device_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Device" ADD CONSTRAINT "Device_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "DeviceHistory" ADD CONSTRAINT "DeviceHistory_changedById_fkey" FOREIGN KEY ("changedById") REFERENCES "User"("nwkennung") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Device" ADD CONSTRAINT "Device_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "DeviceHistory" ADD CONSTRAINT "DeviceHistory_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device"("inventoryNumber") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DeviceHistory" ADD CONSTRAINT "DeviceHistory_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device"("inventoryNumber") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DeviceHistory" ADD CONSTRAINT "DeviceHistory_changedById_fkey" FOREIGN KEY ("changedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "_DeviceToTag" ADD CONSTRAINT "_DeviceToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "Device"("inventoryNumber") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "_DeviceToTag" ADD CONSTRAINT "_DeviceToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "Device"("inventoryNumber") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -94,6 +94,11 @@ model Device {
createdById String? createdById String?
updatedById String? updatedById String?
// 🔹 Self-Relation Hauptgerät/Zubehör
parentDeviceId String?
parentDevice Device? @relation(name: "DeviceAccessories", fields: [parentDeviceId], references: [inventoryNumber], onDelete: SetNull)
accessories Device[] @relation("DeviceAccessories")
createdBy User? @relation("DeviceCreatedBy", fields: [createdById], references: [nwkennung]) createdBy User? @relation("DeviceCreatedBy", fields: [createdById], references: [nwkennung])
updatedBy User? @relation("DeviceUpdatedBy", fields: [updatedById], references: [nwkennung]) updatedBy User? @relation("DeviceUpdatedBy", fields: [updatedById], references: [nwkennung])
@ -106,6 +111,7 @@ model Device {
@@index([inventoryNumber]) @@index([inventoryNumber])
@@index([groupId]) @@index([groupId])
@@index([locationId]) @@index([locationId])
@@index([parentDeviceId])
} }
model Tag { model Tag {
@ -116,14 +122,14 @@ model Tag {
model DeviceHistory { model DeviceHistory {
id String @id @default(uuid()) id String @id @default(uuid())
deviceId String deviceId String?
changeType DeviceChangeType changeType DeviceChangeType
snapshot Json snapshot Json
changedAt DateTime @default(now()) changedAt DateTime @default(now())
changedById String? changedById String?
changedBy User? @relation("DeviceHistoryChangedBy", fields: [changedById], references: [nwkennung]) changedBy User? @relation("DeviceHistoryChangedBy", fields: [changedById], references: [nwkennung])
device Device @relation(fields: [deviceId], references: [inventoryNumber]) device Device? @relation(fields: [deviceId], references: [inventoryNumber], onDelete: SetNull)
} }
enum DeviceChangeType { enum DeviceChangeType {

View File

@ -1,6 +1,6 @@
// prisma/seed.ts // prisma/seed.ts
import 'dotenv/config'; import 'dotenv/config';
import { PrismaClient } from '@/generated/prisma/client'; import { PrismaClient } from '../generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaPg } from '@prisma/adapter-pg';
import { hash } from 'bcryptjs'; import { hash } from 'bcryptjs';
@ -29,18 +29,9 @@ async function main() {
// User anlegen / aktualisieren // User anlegen / aktualisieren
const user = await prisma.user.upsert({ const user = await prisma.user.upsert({
where: { nwkennung }, // 🔹 where: { nwkennung },
update: { update: { email, arbeitsname, passwordHash },
email, create: { nwkennung, email, arbeitsname, passwordHash },
arbeitsname,
passwordHash,
},
create: {
nwkennung,
email,
arbeitsname,
passwordHash,
},
}); });
// Rollen anlegen // Rollen anlegen
@ -116,8 +107,6 @@ async function main() {
locationId: raum112.id, locationId: raum112.id,
createdById: user.nwkennung, createdById: user.nwkennung,
updatedById: user.nwkennung, updatedById: user.nwkennung,
// Tags für Gerät 1
tags: { tags: {
connectOrCreate: [ connectOrCreate: [
{ {
@ -137,6 +126,46 @@ async function main() {
}, },
}); });
// Zubehör-Gerät 1-1 (Dockingstation zu Gerät 1)
const device1_1 = await prisma.device.upsert({
where: { inventoryNumber: '1-1' },
update: {
parentDeviceId: device1.inventoryNumber,
groupId: dienstrechnerGroup.id,
locationId: raum112.id,
},
create: {
inventoryNumber: '1-1',
name: 'Dockingstation zu Sachbearbeitung 1',
manufacturer: 'Dell',
model: 'WD19',
serialNumber: 'SN-DOCK-1',
productNumber: 'PN-DOCK-1',
comment: 'Zubehör zu Gerät 1',
ipv4Address: null,
ipv6Address: null,
macAddress: null,
username: null,
groupId: dienstrechnerGroup.id,
locationId: raum112.id,
createdById: user.nwkennung,
updatedById: user.nwkennung,
parentDeviceId: device1.inventoryNumber,
tags: {
connectOrCreate: [
{
where: { name: 'Zubehör' },
create: { name: 'Zubehör' },
},
{
where: { name: 'Dockingstation' },
create: { name: 'Dockingstation' },
},
],
},
},
});
const device2 = await prisma.device.upsert({ const device2 = await prisma.device.upsert({
where: { inventoryNumber: '2' }, where: { inventoryNumber: '2' },
update: {}, update: {},
@ -154,10 +183,9 @@ async function main() {
username: null, username: null,
groupId: monitoreGroup.id, groupId: monitoreGroup.id,
locationId: lagerKeller.id, locationId: lagerKeller.id,
createdById: user.id, createdById: user.nwkennung,
updatedById: user.id, updatedById: user.nwkennung,
// Tags für Gerät 2
tags: { tags: {
connectOrCreate: [ connectOrCreate: [
{ {
@ -174,11 +202,16 @@ async function main() {
}); });
console.log('Test-User und Beispieldaten angelegt/aktualisiert:'); console.log('Test-User und Beispieldaten angelegt/aktualisiert:');
console.log(` Email: ${user.email}`); console.log(` Email: ${user.email}`);
console.log(` Arbeitsname: ${user.arbeitsname}`); console.log(` Arbeitsname: ${user.arbeitsname}`);
console.log(` NW-Kennung: ${user.nwkennung}`); console.log(` NW-Kennung: ${user.nwkennung}`);
console.log(` Passwort: ${password}`); console.log(` Passwort: ${password}`);
console.log(' Devices: ', device1.inventoryNumber, device2.inventoryNumber); console.log(
' Devices: ',
device1.inventoryNumber,
device1_1.inventoryNumber,
device2.inventoryNumber,
);
} }
main() main()