// 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 AppCombobox from '@/components/ui/Combobox'; import type { DeviceDetail } from './page'; type LoanDeviceModalProps = { open: boolean; onClose: () => void; device: DeviceDetail; 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 oder Freitext) label: string; // Anzeige-Text in der Combobox group: string; // Hauptgruppe (BaseKey) imageUrl?: string | null; // Avatar / Platzhalter /** Gesamter Suchstring: arbeitsname, nwkennung, Vor-/Nachname, Gruppe */ searchText: string; }; 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'); return `${year}-${month}-${day}`; } function fromDateInputValue(v: string): string | null { if (!v) return null; 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', }); 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}`; } 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 Combobox laden 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(' ')); } 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({ value: u.arbeitsname, // in loanedTo speichern wir weiterhin den Arbeitsnamen label: nameParts.join(' '), group: mainGroup, imageUrl, searchText: searchParts.join(' '), }); } } // bestehenden Wert, der kein User ist, als "Andere"-Option anhängen (ohne Zusatz-Placeholder) const currentLoanedTo = device.loanedTo ?? ''; if ( 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({ value: currentLoanedTo, label: currentLoanedTo, // 👈 kein "(bisheriger Eintrag)" mehr group: 'Andere', imageUrl, searchText: `${currentLoanedTo} 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]); // aktuell ausgewählte Option für Combobox const currentSelected = useMemo( () => userOptions.find((o) => o.value === loanedTo) ?? null, [userOptions, loanedTo], ); 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({ 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 ?? [], 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({ 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 ?? [], 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 (
{/* Hinweis zum aktuellen Verleih */} {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 */}
label="Verliehen an" options={userOptions} value={currentSelected} onChange={(selected) => { if (!selected) { 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`, }; }} />
{optionsLoading && (

Lade Benutzer …

)} {optionsError && (

{optionsError}

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