geraete/app/(app)/devices/DeviceHistorySidebar.tsx
2025-11-18 14:44:36 +01:00

207 lines
4.9 KiB
TypeScript

// app/(app)/devices/DeviceHistorySidebar.tsx
'use client';
import { useEffect, useState, ElementType } from 'react';
import Feed, {
FeedItem,
FeedChange,
} from '@/components/ui/Feed';
import clsx from 'clsx';
type Props = {
inventoryNumber: string;
/** Wenn true: wird als Inhalt für Modal.sidebar gerendert (ohne eigenes <aside>/Border) */
asSidebar?: boolean;
};
type ApiHistoryEntry = {
id: string;
changeType: 'CREATED' | 'UPDATED' | 'DELETED';
changedAt: string;
changedBy: string | null;
changes: {
field: string;
from: string | null;
to: string | null;
}[];
};
function formatDateTime(iso: string) {
return new Intl.DateTimeFormat('de-DE', {
dateStyle: 'short',
timeStyle: 'short',
}).format(new Date(iso));
}
function mapFieldLabel(field: string): string {
switch (field) {
case 'name':
return 'Bezeichnung';
case 'manufacturer':
return 'Hersteller';
case 'model':
return 'Modell';
case 'serialNumber':
return 'Seriennummer';
case 'productNumber':
return 'Produktnummer';
case 'comment':
return 'Kommentar';
case 'ipv4Address':
return 'IPv4-Adresse';
case 'ipv6Address':
return 'IPv6-Adresse';
case 'macAddress':
return 'MAC-Adresse';
case 'username':
return 'Benutzernamen';
case 'passwordHash':
return 'Passwort';
case 'group':
return 'Gruppe';
case 'location':
return 'Standort';
case 'tags':
return 'Tags';
default:
return field;
}
}
export default function DeviceHistorySidebar({
inventoryNumber,
asSidebar = false,
}: Props) {
const [items, setItems] = useState<FeedItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
async function loadHistory() {
setLoading(true);
setError(null);
try {
const res = await fetch(
`/api/devices/${encodeURIComponent(inventoryNumber)}/history`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
},
);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = (await res.json()) as ApiHistoryEntry[];
if (cancelled) return;
const mapped: FeedItem[] = data.map((entry) => {
const person = {
name: entry.changedBy ?? 'Unbekannter Benutzer',
};
const date = formatDateTime(entry.changedAt);
if (entry.changeType === 'UPDATED' && entry.changes.length > 0) {
const changes: FeedChange[] = entry.changes.map((c) => ({
field: c.field,
label: mapFieldLabel(c.field),
from: c.from,
to: c.to,
}));
return {
id: entry.id,
type: 'change',
person,
date,
changes,
};
}
let comment = '';
let commentKind: 'created' | 'deleted' | 'generic' = 'generic';
if (entry.changeType === 'CREATED') {
comment = 'hat das Gerät angelegt.';
commentKind = 'created';
} else if (entry.changeType === 'DELETED') {
comment = 'hat das Gerät gelöscht.';
commentKind = 'deleted';
} else {
comment = 'hat das Gerät geändert.';
commentKind = 'generic';
}
return {
id: entry.id,
type: 'comment',
person,
date,
comment,
commentKind,
};
});
setItems(mapped);
} catch (err) {
console.error('Error loading device history', err);
if (!cancelled) {
setError('Historie konnte nicht geladen werden.');
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
loadHistory();
return () => {
cancelled = true;
};
}, [inventoryNumber]);
// Root-Tag & Klassen abhängig vom Einsatz
const Root: ElementType = asSidebar ? 'div' : 'aside';
const rootClassName = asSidebar
? 'flex h-full min-h-0 flex-col'
: undefined;
return (
<Root className={rootClassName}>
<h2 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
Änderungsverlauf
</h2>
{loading && (
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
Historie wird geladen
</p>
)}
{error && (
<p className="mt-2 text-xs text-red-600 dark:text-red-400">
{error}
</p>
)}
{!loading && !error && (
<div
className={clsx(
'mt-3',
asSidebar && 'min-h-0 flex-1 overflow-y-auto pr-1'
)}
>
<Feed items={items} />
</div>
)}
</Root>
);
}