updated
This commit is contained in:
parent
2f42c71fe9
commit
0f5d23eb9b
@ -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>
|
||||||
|
|
||||||
|
{/* Zubehör mit Hauptgerät → Prefix fix anzeigen */}
|
||||||
|
{isAccessory && form.parentInventoryNumber ? (
|
||||||
|
<div className="mt-1 flex rounded-md bg-gray-900/40 ring-1 ring-inset ring-gray-700 shadow-xs focus-within:ring-2 focus-within:ring-indigo-500">
|
||||||
|
{/* Unveränderbarer Prefix, dezent dargestellt */}
|
||||||
|
<span className="inline-flex items-center px-2.5 text-sm text-gray-400 border-r border-gray-700">
|
||||||
|
{accessoryPrefix}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Editierbarer Suffix */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="block w-full min-w-0 flex-1 border-0 bg-transparent px-2.5 py-1.5 text-sm text-gray-100 placeholder:text-gray-500 focus:outline-none"
|
||||||
|
value={inventorySuffix}
|
||||||
|
onChange={handleInventorySuffixChange}
|
||||||
|
placeholder="z.B. 1, 2, 3 …"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Normaler Modus (Hauptgerät oder noch kein Hauptgerät gewählt)
|
||||||
<input
|
<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={form.inventoryNumber}
|
value={form.inventoryNumber}
|
||||||
onChange={(e) => handleFieldChange('inventoryNumber', e)}
|
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>
|
||||||
|
|
||||||
|
|||||||
@ -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,7 +66,45 @@ 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="space-y-4">
|
||||||
|
{showAccessoryTab && (
|
||||||
|
<div>
|
||||||
|
<Tabs
|
||||||
|
tabs={[
|
||||||
|
{ id: 'info', label: 'Stammdaten' },
|
||||||
|
{ id: 'zubehoer', label: 'Zubehör' },
|
||||||
|
]}
|
||||||
|
value={activeSection}
|
||||||
|
onChange={(id) =>
|
||||||
|
setActiveSection(id as 'info' | 'zubehoer')
|
||||||
|
}
|
||||||
|
ariaLabel="Geräteansicht auswählen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 🔹 Sektion: Stammdaten (dein bisheriges Grid – nur ohne alte Zubehör-Liste) */}
|
||||||
|
{activeSection === 'info' && (
|
||||||
<div className="grid grid-cols-1 gap-4 text-sm sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 text-sm sm:grid-cols-2">
|
||||||
{/* Inventarnummer (oben links) */}
|
{/* Inventarnummer (oben links) */}
|
||||||
<div>
|
<div>
|
||||||
@ -87,22 +138,28 @@ function DeviceDetailsGrid({ device, onStartLoan }: DeviceDetailsGridProps) {
|
|||||||
{/* Infotext darunter */}
|
{/* Infotext darunter */}
|
||||||
{device.loanedTo && (
|
{device.loanedTo && (
|
||||||
<span className="text-xs text-gray-700 dark:text-gray-200">
|
<span className="text-xs text-gray-700 dark:text-gray-200">
|
||||||
an <span className="font-semibold">{device.loanedTo}</span>
|
an{' '}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{device.loanedTo}
|
||||||
|
</span>
|
||||||
{device.loanedFrom && (
|
{device.loanedFrom && (
|
||||||
<>
|
<>
|
||||||
{' '}seit{' '}
|
{' '}
|
||||||
|
seit{' '}
|
||||||
{dtf.format(new Date(device.loanedFrom))}
|
{dtf.format(new Date(device.loanedFrom))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{device.loanedUntil && (
|
{device.loanedUntil && (
|
||||||
<>
|
<>
|
||||||
{' '}bis{' '}
|
{' '}
|
||||||
|
bis{' '}
|
||||||
{dtf.format(new Date(device.loanedUntil))}
|
{dtf.format(new Date(device.loanedUntil))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{device.loanComment && (
|
{device.loanComment && (
|
||||||
<>
|
<>
|
||||||
{' '}- Hinweis: {device.loanComment}
|
{' '}
|
||||||
|
- Hinweis: {device.loanComment}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@ -114,17 +171,19 @@ function DeviceDetailsGrid({ device, onStartLoan }: DeviceDetailsGridProps) {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={onStartLoan}
|
onClick={onStartLoan}
|
||||||
>
|
>
|
||||||
{isLoaned ? 'Verleih bearbeiten' : 'Gerät verleihen'}
|
{isLoaned
|
||||||
|
? 'Verleih bearbeiten'
|
||||||
|
: 'Gerät verleihen'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 🔹 Trenner nach Verleihstatus */}
|
{/* Trenner nach Verleihstatus */}
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
|
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bezeichnung jetzt UNTER dem Trenner */}
|
{/* Bezeichnung */}
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<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">
|
||||||
Bezeichnung
|
Bezeichnung
|
||||||
@ -255,7 +314,9 @@ function DeviceDetailsGrid({ device, onStartLoan }: DeviceDetailsGridProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">–</p>
|
<p className="mt-1 text-sm text-gray-800 dark:text-gray-400">
|
||||||
|
–
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -294,9 +355,61 @@ function DeviceDetailsGrid({ device, onStartLoan }: DeviceDetailsGridProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 🔹 Sektion: Tabelle für Hauptgerät & Zubehör */}
|
||||||
|
{activeSection === 'zubehoer' && showAccessoryTab && (
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wide text-gray dark:text-gray-500">
|
||||||
|
Zubehör
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 overflow-x-auto rounded-md border border-gray-200 dark:border-gray-700">
|
||||||
|
<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">
|
||||||
|
<tr>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{!hasAccessories && hasParent && (
|
||||||
|
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
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 dark:text-gray-400">
|
||||||
|
Dieses Gerät ist ein Hauptgerät und besitzt die oben
|
||||||
|
aufgeführten Zubehör-Geräte.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function DeviceDetailModal({
|
export default function DeviceDetailModal({
|
||||||
open,
|
open,
|
||||||
inventoryNumber,
|
inventoryNumber,
|
||||||
|
|||||||
@ -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}
|
||||||
@ -256,7 +392,21 @@ 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">
|
||||||
|
{/* 🔹 Tabs im Edit-Body */}
|
||||||
|
<Tabs
|
||||||
|
tabs={[
|
||||||
|
{ id: 'fields', label: 'Stammdaten' },
|
||||||
|
{ id: 'relations', label: 'Zubehör' },
|
||||||
|
]}
|
||||||
|
value={activeTab}
|
||||||
|
onChange={(id) => setActiveTab(id as 'fields' | 'relations')}
|
||||||
|
ariaLabel="Bearbeitungsansicht wählen"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* TAB 1: Stammdaten (dein bisheriges Grid) */}
|
||||||
|
{activeTab === 'fields' && (
|
||||||
|
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
{/* 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">
|
||||||
@ -278,7 +428,7 @@ export default function DeviceEditModal({
|
|||||||
<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.name}
|
value={editDevice.name ?? ""}
|
||||||
onChange={(e) => handleFieldChange('name', e)}
|
onChange={(e) => handleFieldChange('name', e)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -291,8 +441,10 @@ export default function DeviceEditModal({
|
|||||||
<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.manufacturer ?? ""}
|
||||||
onChange={(e) => handleFieldChange('manufacturer', e)}
|
onChange={(e) =>
|
||||||
|
handleFieldChange('manufacturer', e)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -303,7 +455,7 @@ export default function DeviceEditModal({
|
|||||||
<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.model}
|
value={editDevice.model ?? ""}
|
||||||
onChange={(e) => handleFieldChange('model', e)}
|
onChange={(e) => handleFieldChange('model', e)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -317,7 +469,9 @@ export default function DeviceEditModal({
|
|||||||
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.serialNumber ?? ''}
|
value={editDevice.serialNumber ?? ''}
|
||||||
onChange={(e) => handleFieldChange('serialNumber', e)}
|
onChange={(e) =>
|
||||||
|
handleFieldChange('serialNumber', e)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -329,7 +483,9 @@ export default function DeviceEditModal({
|
|||||||
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.productNumber ?? ''}
|
value={editDevice.productNumber ?? ''}
|
||||||
onChange={(e) => handleFieldChange('productNumber', e)}
|
onChange={(e) =>
|
||||||
|
handleFieldChange('productNumber', e)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -367,7 +523,9 @@ export default function DeviceEditModal({
|
|||||||
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.ipv4Address ?? ''}
|
value={editDevice.ipv4Address ?? ''}
|
||||||
onChange={(e) => handleFieldChange('ipv4Address', e)}
|
onChange={(e) =>
|
||||||
|
handleFieldChange('ipv4Address', e)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -379,7 +537,9 @@ export default function DeviceEditModal({
|
|||||||
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.ipv6Address ?? ''}
|
value={editDevice.ipv6Address ?? ''}
|
||||||
onChange={(e) => handleFieldChange('ipv6Address', e)}
|
onChange={(e) =>
|
||||||
|
handleFieldChange('ipv6Address', e)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -391,7 +551,9 @@ export default function DeviceEditModal({
|
|||||||
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.macAddress ?? ''}
|
value={editDevice.macAddress ?? ''}
|
||||||
onChange={(e) => handleFieldChange('macAddress', e)}
|
onChange={(e) =>
|
||||||
|
handleFieldChange('macAddress', e)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -404,7 +566,9 @@ export default function DeviceEditModal({
|
|||||||
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.username ?? ''}
|
value={editDevice.username ?? ''}
|
||||||
onChange={(e) => handleFieldChange('username', e)}
|
onChange={(e) =>
|
||||||
|
handleFieldChange('username', e)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -416,7 +580,9 @@ export default function DeviceEditModal({
|
|||||||
type="password"
|
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"
|
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 ?? ''}
|
value={editDevice.passwordHash ?? ''}
|
||||||
onChange={(e) => handleFieldChange('passwordHash', e)}
|
onChange={(e) =>
|
||||||
|
handleFieldChange('passwordHash', e)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -425,16 +591,28 @@ export default function DeviceEditModal({
|
|||||||
<TagMultiCombobox
|
<TagMultiCombobox
|
||||||
label="Tags"
|
label="Tags"
|
||||||
availableTags={allTags}
|
availableTags={allTags}
|
||||||
value={(editDevice.tags ?? []).map((name) => ({ name }))}
|
value={(editDevice.tags ?? []).map((name) => ({
|
||||||
|
name,
|
||||||
|
}))}
|
||||||
onChange={(next) => {
|
onChange={(next) => {
|
||||||
const names = next.map((t) => t.name);
|
const names = next.map((t) => t.name);
|
||||||
|
|
||||||
setEditDevice((prev) =>
|
setEditDevice((prev) =>
|
||||||
prev ? ({ ...prev, tags: names } as DeviceDetail) : prev,
|
prev
|
||||||
|
? ({
|
||||||
|
...prev,
|
||||||
|
tags: names,
|
||||||
|
} as DeviceDetail)
|
||||||
|
: prev,
|
||||||
);
|
);
|
||||||
|
|
||||||
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)) {
|
||||||
@ -457,11 +635,132 @@ export default function DeviceEditModal({
|
|||||||
rows={3}
|
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.comment ?? ''}
|
value={editDevice.comment ?? ''}
|
||||||
onChange={(e) => handleFieldChange('comment', e)}
|
onChange={(e) =>
|
||||||
|
handleFieldChange('comment', e)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
|
||||||
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>
|
</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 */}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
let groupId: string | null = null;
|
parsedRows.push({
|
||||||
if (groupName) {
|
nwkennung,
|
||||||
groupId = await ensureGroupId(groupName, groupCache);
|
lastName,
|
||||||
if (!groupId) {
|
firstName,
|
||||||
console.warn(
|
arbeitsname,
|
||||||
'Zeile übersprungen (Gruppe konnte nicht angelegt werden):',
|
groupName,
|
||||||
line,
|
rawLine: trimmed,
|
||||||
|
lineNumber: index + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedRows.length === 0) {
|
||||||
|
setImportError(
|
||||||
|
'Keine gültigen Zeilen gefunden. Bitte CSV prüfen.',
|
||||||
);
|
);
|
||||||
skippedCount++;
|
return;
|
||||||
continue;
|
}
|
||||||
|
|
||||||
|
// 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 res = await fetch('/api/users', {
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ names: newGroupNames }),
|
||||||
nwkennung, // ⬅ NEU
|
|
||||||
arbeitsname,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
groupId,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!resGroups.ok) {
|
||||||
const data = await res.json().catch(() => null);
|
const data = await resGroups.json().catch(() => null);
|
||||||
console.error(
|
console.error(
|
||||||
'Fehler beim Anlegen der Person aus CSV:',
|
'Fehler beim Bulk-Anlegen der Gruppen:',
|
||||||
res.status,
|
resGroups.status,
|
||||||
data,
|
data,
|
||||||
'Zeile:',
|
|
||||||
line,
|
|
||||||
);
|
);
|
||||||
skippedCount++;
|
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;
|
||||||
|
|
||||||
|
if (row.groupName) {
|
||||||
|
const gName = row.groupName.trim();
|
||||||
|
if (gName) {
|
||||||
|
const id = groupCache.get(gName);
|
||||||
|
if (!id) {
|
||||||
|
console.warn(
|
||||||
|
'Zeile übersprungen (Gruppe nicht vorhanden):',
|
||||||
|
row.rawLine,
|
||||||
|
);
|
||||||
|
skippedByGroup++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
groupId = id;
|
||||||
createdCount++;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setImportSummary(
|
usersPayload.push({
|
||||||
`Import abgeschlossen: ${createdCount} Personen importiert, ${skippedCount} Zeilen übersprungen.`,
|
nwkennung: row.nwkennung,
|
||||||
|
firstName: row.firstName,
|
||||||
|
lastName: row.lastName,
|
||||||
|
arbeitsname: row.arbeitsname,
|
||||||
|
groupId: groupId ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usersPayload.length === 0) {
|
||||||
|
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}
|
||||||
|
|||||||
@ -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 (a–z)</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 (A–Z)</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 (0–9)</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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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(),
|
||||||
},
|
},
|
||||||
@ -286,6 +363,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,
|
||||||
updatedAt: created.updatedAt.toISOString(),
|
updatedAt: created.updatedAt.toISOString(),
|
||||||
},
|
},
|
||||||
{ status: 201 },
|
{ status: 201 },
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
94
app/api/users/[nwkennung]/password/route.ts
Normal file
94
app/api/users/[nwkennung]/password/route.ts
Normal 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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 },
|
||||||
@ -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
210
components/GlobalSearch.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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
320
components/ui/Combobox.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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,9 +113,199 @@ function renderItemContent(item: DropdownItem) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ───────── Dropdown-Komponente (mit Portal) ───────── */
|
/* ───────── Spezielle Variante: Textfeld + Dropdown (OHNE Menu) ───────── */
|
||||||
|
|
||||||
export function Dropdown({
|
function InputDropdown({
|
||||||
|
label = 'Options',
|
||||||
|
ariaLabel = 'Open options',
|
||||||
|
align = 'left',
|
||||||
|
header,
|
||||||
|
sections,
|
||||||
|
triggerClassName,
|
||||||
|
menuClassName,
|
||||||
|
disabled = false,
|
||||||
|
inputValue,
|
||||||
|
inputPlaceholder,
|
||||||
|
onInputChange,
|
||||||
|
}: DropdownProps) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const anchorRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
const menuRef = React.useRef<HTMLDivElement | null>(null); // 🔹 NEU
|
||||||
|
const [position, setPosition] = React.useState<{
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
width: number;
|
||||||
|
} | 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',
|
label = 'Options',
|
||||||
ariaLabel = 'Open options',
|
ariaLabel = 'Open options',
|
||||||
align = 'right',
|
align = 'right',
|
||||||
@ -125,19 +315,34 @@ export function Dropdown({
|
|||||||
triggerClassName,
|
triggerClassName,
|
||||||
menuClassName,
|
menuClassName,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: DropdownProps) {
|
} = props;
|
||||||
const hasDividers = sections.length > 1;
|
|
||||||
|
const triggerIsInput = triggerVariant === 'input';
|
||||||
|
const triggerIsIcon = triggerVariant === 'icon';
|
||||||
const triggerIsButton = triggerVariant === 'button';
|
const triggerIsButton = triggerVariant === 'button';
|
||||||
|
|
||||||
const buttonRef = React.useRef<HTMLButtonElement | null>(null);
|
// 🔹 Spezialfall: Textfeld + Dropdown (ohne Menu)
|
||||||
|
if (triggerIsInput) {
|
||||||
|
return (
|
||||||
|
<InputDropdown
|
||||||
|
{...props}
|
||||||
|
triggerVariant="input"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDividers = sections.length > 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
|
{({ open }) => {
|
||||||
|
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;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu as="div" className="relative inline-block text-left">
|
|
||||||
{({ open }) => {
|
|
||||||
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,14 +418,15 @@ 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}
|
||||||
@ -224,7 +434,8 @@ export function Dropdown({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{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
|
||||||
|
|||||||
@ -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
@ -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]
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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";
|
|
||||||
@ -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");
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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: [
|
||||||
{
|
{
|
||||||
@ -178,7 +206,12 @@ async function main() {
|
|||||||
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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user