// app/(app)/devices/LoanDeviceModal.tsx 'use client'; import { useEffect, useMemo, useState } from 'react'; import Modal from '@/components/ui/Modal'; import Button from '@/components/ui/Button'; import Dropdown, { type DropdownSection } from '@/components/ui/Dropdown'; import type { DeviceDetail } from './page'; type LoanDeviceModalProps = { open: boolean; onClose: () => void; device: DeviceDetail; /** * Wird nach erfolgreichem Speichern/Beenden aufgerufen, * damit der Parent den lokalen State aktualisieren kann. */ onUpdated?: (patch: { loanedTo: string | null; loanedFrom: string | null; loanedUntil: string | null; loanComment: string | null; }) => void; }; // gleiche Logik wie bei den User-Gruppen: function getBaseGroupName(name: string): string { const trimmed = name.trim(); if (!trimmed) return ''; const beforeDash = trimmed.split('-')[0].trim() || trimmed; const withoutDigits = beforeDash.replace(/\d+$/, '').trim(); return withoutDigits || beforeDash; } type LoanUserOption = { value: string; // wird in loanedTo gespeichert (z.B. arbeitsname) label: string; // Anzeige-Text im Dropdown group: string; // Hauptgruppe (BaseKey) }; type UsersApiGroup = { id: string; name: string; users: { nwkennung: string; arbeitsname: string; firstName: string | null; lastName: string | null; }[]; }; function toDateInputValue(iso: string | null | undefined): string { if (!iso) return ''; const d = new Date(iso); if (isNaN(d.getTime())) return ''; const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); // lokale Datumskomponenten -> passend für return `${year}-${month}-${day}`; } function fromDateInputValue(v: string): string | 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'); if (isNaN(d.getTime())) return null; return d.toISOString(); } const dtf = new Intl.DateTimeFormat('de-DE', { dateStyle: 'short', }); // "heute" im -Format function todayInputDate(): string { const d = new Date(); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; // z.B. 2025-02-19 } export default function LoanDeviceModal({ open, onClose, device, onUpdated, }: LoanDeviceModalProps) { const [loanedTo, setLoanedTo] = useState(''); const [loanedFrom, setLoanedFrom] = useState(''); const [loanedUntil, setLoanedUntil] = useState(''); const [loanComment, setLoanComment] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [userOptions, setUserOptions] = useState([]); const [optionsLoading, setOptionsLoading] = useState(false); const [optionsError, setOptionsError] = useState(null); // Beim Öffnen / Gerätewechsel Felder setzen useEffect(() => { if (!open) return; const existingFrom = toDateInputValue(device.loanedFrom); const nextLoanedFrom = existingFrom || todayInputDate(); setLoanedTo(device.loanedTo ?? ''); setLoanedFrom(nextLoanedFrom); setLoanedUntil(toDateInputValue(device.loanedUntil)); setLoanComment(device.loanComment ?? ''); setError(null); }, [ open, device.inventoryNumber, device.loanedFrom, device.loanedUntil, device.loanComment, device.loanedTo, ]); // Beim Öffnen User für Dropdown laden (nur User aus Gruppen, gruppiert nach Hauptgruppen) useEffect(() => { if (!open) return; async function loadUsers() { setOptionsLoading(true); setOptionsError(null); try { const res = await fetch('/api/users'); if (!res.ok) { throw new Error(`Fehler beim Laden der Benutzer (HTTP ${res.status})`); } const data = await res.json(); const groups = (data.groups ?? []) as UsersApiGroup[]; const opts: LoanUserOption[] = []; for (const g of groups) { const mainGroup = getBaseGroupName(g.name) || g.name; for (const u of g.users ?? []) { if (!u.arbeitsname) continue; const nameParts: string[] = [u.arbeitsname]; const extra: string[] = []; if (u.firstName) extra.push(u.firstName); if (u.lastName) extra.push(u.lastName); if (extra.length) { nameParts.push('– ' + extra.join(' ')); } opts.push({ value: u.arbeitsname, // in loanedTo speichern wir weiterhin den Arbeitsnamen label: nameParts.join(' '), group: mainGroup, }); } } // bestehenden Wert, der kein User ist, als "Andere"-Option anhängen const currentLoanedTo = device.loanedTo ?? ''; if ( currentLoanedTo && !opts.some((o) => o.value === currentLoanedTo) ) { opts.push({ value: currentLoanedTo, label: `${currentLoanedTo} (bisheriger Eintrag)`, group: 'Andere', }); } // sortieren: erst nach Gruppe, dann nach Label opts.sort((a, b) => { const gComp = a.group.localeCompare(b.group, 'de'); if (gComp !== 0) return gComp; return a.label.localeCompare(b.label, 'de'); }); setUserOptions(opts); } catch (err) { console.error('Fehler beim Laden der Benutzerliste für Verleih', err); setOptionsError('Empfängerliste konnte nicht geladen werden.'); } finally { setOptionsLoading(false); } } loadUsers(); }, [open, device.loanedTo]); // Optionen nach Hauptgruppe gruppieren const groupedOptions = useMemo(() => { const map = new Map(); 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(() => { 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( () => userOptions.find((o) => o.value === loanedTo) ?? null, [userOptions, loanedTo], ); const dropdownLabel = currentSelected?.label || (loanedTo || 'Bitte auswählen …'); const isLoaned = !!device.loanedTo; async function saveLoan() { if (!device.inventoryNumber) return; if (!loanedTo.trim()) { setError('Bitte einen Empfänger (an wen) auswählen.'); return; } setSaving(true); setError(null); try { const res = await fetch( `/api/devices/${encodeURIComponent(device.inventoryNumber)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ // ⚠️ PATCH-Route erwartet alle Felder, nicht nur Verleihfelder! name: device.name, manufacturer: device.manufacturer, model: device.model, serialNumber: device.serialNumber, productNumber: device.productNumber, comment: device.comment, ipv4Address: device.ipv4Address, ipv6Address: device.ipv6Address, macAddress: device.macAddress, username: device.username, passwordHash: (device as any).passwordHash ?? null, group: device.group, location: device.location, tags: device.tags ?? [], // Verleihfelder loanedTo: loanedTo.trim(), loanedFrom: fromDateInputValue(loanedFrom), loanedUntil: fromDateInputValue(loanedUntil), loanComment: loanComment.trim() || null, }), }, ); if (!res.ok) { const data = await res.json().catch(() => null); throw new Error( data?.error ?? `Fehler beim Speichern (HTTP ${res.status})`, ); } const patch = { loanedTo: loanedTo.trim(), loanedFrom: fromDateInputValue(loanedFrom), loanedUntil: fromDateInputValue(loanedUntil), loanComment: loanComment.trim() || null, }; onUpdated?.(patch); onClose(); } catch (err: any) { console.error('Error saving loan', err); setError( err instanceof Error ? err.message : 'Fehler beim Speichern des Verleihstatus.', ); } finally { setSaving(false); } } async function endLoan() { if (!device.inventoryNumber) return; setSaving(true); setError(null); try { const res = await fetch( `/api/devices/${encodeURIComponent(device.inventoryNumber)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ // Alle bisherigen Felder durchreichen name: device.name, manufacturer: device.manufacturer, model: device.model, serialNumber: device.serialNumber, productNumber: device.productNumber, comment: device.comment, ipv4Address: device.ipv4Address, ipv6Address: device.ipv6Address, macAddress: device.macAddress, username: device.username, passwordHash: (device as any).passwordHash ?? null, group: device.group, location: device.location, tags: device.tags ?? [], // Verleih zurücksetzen loanedTo: null, loanedFrom: null, loanedUntil: null, loanComment: null, }), }, ); if (!res.ok) { const data = await res.json().catch(() => null); throw new Error( data?.error ?? `Fehler beim Beenden des Verleihs (HTTP ${res.status})`, ); } onUpdated?.({ loanedTo: null, loanedFrom: null, loanedUntil: null, loanComment: null, }); onClose(); } catch (err: any) { console.error('Error ending loan', err); setError( err instanceof Error ? err.message : 'Fehler beim Beenden des Verleihs.', ); } finally { setSaving(false); } } const title = isLoaned ? `Verleih bearbeiten – ${device.name}` : `Gerät verleihen – ${device.name}`; return (
{/* Aktueller Verleih-Hinweis mit deutlich sichtbarem „Verleih beenden“ */} {isLoaned && (

Verliehen an

{device.loanedTo} {device.loanedFrom && ( <> {' '}seit{' '} {dtf.format(new Date(device.loanedFrom))} )} {device.loanedUntil && ( <> {' '}bis{' '} {dtf.format(new Date(device.loanedUntil))} )} {device.loanComment && ( <> {' '}- Hinweis: {device.loanComment} )}

)}

{isLoaned ? 'Passe die Verleihdaten an oder beende den Verleih, wenn das Gerät zurückgegeben wurde.' : 'Lege fest, an wen das Gerät verliehen wird und in welchem Zeitraum.'}

{/* Formularfelder */}
{optionsLoading && (

Lade Benutzer …

)} {optionsError && (

{optionsError}

)}
setLoanedFrom(e.target.value)} />
setLoanedUntil(e.target.value)} />