// app/(app)/devices/DeviceDetailModal.tsx 'use client'; import { useEffect, useState } from 'react'; import Modal from '@/components/ui/Modal'; import { BookOpenIcon } from '@heroicons/react/24/outline'; import DeviceHistorySidebar from './DeviceHistorySidebar'; import Button from '@/components/ui/Button'; import type { DeviceDetail } from './page'; import { DeviceQrCode } from '@/components/DeviceQrCode'; import Tabs from '@/components/ui/Tabs'; import LoanDeviceModal from './LoanDeviceModal'; type DeviceDetailModalProps = { open: boolean; inventoryNumber: string | null; onClose: () => void; }; const dtf = new Intl.DateTimeFormat('de-DE', { dateStyle: 'short', timeStyle: 'short', }); type DeviceDetailsGridProps = { device: DeviceDetail; onStartLoan?: () => void; }; function DeviceDetailsGrid({ device, onStartLoan }: DeviceDetailsGridProps) { const isLoaned = Boolean(device.loanedTo); const now = new Date(); const isOverdue = isLoaned && device.loanedUntil != null && new Date(device.loanedUntil) < now; const statusLabel = !isLoaned ? 'Verfügbar' : isOverdue ? 'Verliehen (überfällig)' : 'Verliehen'; const statusClasses = !isLoaned ? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-100' : isOverdue ? 'bg-rose-100 text-rose-800 dark:bg-rose-900/40 dark:text-rose-100' : 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-100'; const dotClasses = !isLoaned ? 'bg-emerald-500' : isOverdue ? 'bg-rose-500' : 'bg-amber-500'; return (
{/* Inventarnummer (oben links) */}

Inventar-Nr.

{device.inventoryNumber}

{/* Status */}

Status

{/* linke „Spalte“: nur inhaltsbreit */}
{/* Pill nur content-breit */} {statusLabel} {/* Infotext darunter */} {device.loanedTo && ( 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} )} )}
{/* 🔹 Trenner nach Verleihstatus */}
{/* Bezeichnung jetzt UNTER dem Trenner */}

Bezeichnung

{device.name || '–'}

{/* Hersteller / Modell */}

Hersteller

{device.manufacturer || '–'}

Modell

{device.model || '–'}

{/* Seriennummer / Produktnummer */}

Seriennummer

{device.serialNumber || '–'}

Produktnummer

{device.productNumber || '–'}

{/* Standort / Gruppe */}

Standort / Raum

{device.location || '–'}

Gruppe

{device.group || '–'}

{/* Netzwerkdaten */}

IPv4-Adresse

{device.ipv4Address || '–'}

IPv6-Adresse

{device.ipv6Address || '–'}

MAC-Adresse

{device.macAddress || '–'}

{/* Zugangsdaten */}

Benutzername

{device.username || '–'}

Passwort (Hash)

{device.passwordHash || '–'}

{/* Tags */}

Tags

{device.tags && device.tags.length > 0 ? (
{device.tags.map((tag) => ( {tag} ))}
) : (

)}
{/* Kommentar */}

Kommentar

{device.comment && device.comment.trim().length > 0 ? device.comment : '–'}
{/* Metadaten */}

Angelegt am

{device.createdAt ? dtf.format(new Date(device.createdAt)) : '–'}

Zuletzt geändert am

{device.updatedAt ? dtf.format(new Date(device.updatedAt)) : '–'}

); } export default function DeviceDetailModal({ open, inventoryNumber, onClose, }: DeviceDetailModalProps) { const [device, setDevice] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState<'details' | 'history'>('details'); const [loanModalOpen, setLoanModalOpen] = useState(false); const [historyRefresh, setHistoryRefresh] = useState(0); useEffect(() => { if (!open || !inventoryNumber) return; const inv = inventoryNumber; let cancelled = false; setLoading(true); setError(null); setDevice(null); async function loadDevice() { try { const res = await fetch(`/api/devices/${encodeURIComponent(inv)}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, cache: 'no-store', }); if (!res.ok) { if (res.status === 404) { throw new Error('Gerät wurde nicht gefunden.'); } throw new Error( 'Beim Laden der Gerätedaten ist ein Fehler aufgetreten.', ); } const data = (await res.json()) as DeviceDetail; if (!cancelled) setDevice(data); } catch (err: any) { console.error('Error loading device details', err); if (!cancelled) { setError( err instanceof Error ? err.message : 'Netzwerkfehler beim Laden der Gerätedaten.', ); } } finally { if (!cancelled) setLoading(false); } } loadDevice(); return () => { cancelled = true; }; }, [open, inventoryNumber]); const handleClose = () => onClose(); const handleStartLoan = () => { if (!device) return; setLoanModalOpen(true); }; return ( <> } tone="info" variant="centered" size="xl" primaryAction={{ label: 'Schließen', onClick: handleClose, variant: 'primary', }} headerExtras={ device && (
setActiveTab(id as 'details' | 'history')} ariaLabel="Ansicht wählen" />
) } sidebar={ device ? (
{/* QR-Code oben, nicht scrollend */}

{device.inventoryNumber}

{/* Änderungsverlauf: nimmt den Rest der Höhe ein und scrollt intern */}
) : undefined } > {loading && (

Gerätedaten werden geladen …

)} {error && (

{error}

)} {!loading && !error && device && ( <> {/* Mobile-Inhalt (Tabs steuern Ansicht) */}
{activeTab === 'details' ? ( ) : ( )}
{/* Desktop-Inhalt links: nur Details, Verlauf rechts in sidebar */}
)} {device && ( setLoanModalOpen(false)} device={device} onUpdated={(patch) => { // lokalen State aktualisieren, damit Details sofort aktualisiert sind setDevice((prev) => prev ? { ...prev, loanedTo: patch.loanedTo, loanedFrom: patch.loanedFrom, loanedUntil: patch.loanedUntil, loanComment: patch.loanComment, } : prev, ); setHistoryRefresh((prev) => prev + 1); }} /> )} ); }