geraete/app/(app)/devices/DeviceHistorySidebar.tsx
2025-11-17 15:26:43 +01:00

195 lines
4.7 KiB
TypeScript

// app/(app)/devices/DeviceHistorySidebar.tsx
'use client';
import { useEffect, useState, ElementType } from 'react';
import Feed, {
FeedItem,
FeedChange,
} from '@/components/ui/Feed';
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 'die Bezeichnung';
case 'manufacturer':
return 'den Hersteller';
case 'model':
return 'das Modell';
case 'serialNumber':
return 'die Seriennummer';
case 'productNumber':
return 'die Produktnummer';
case 'comment':
return 'den Kommentar';
case 'ipv4Address':
return 'die IPv4-Adresse';
case 'ipv6Address':
return 'die IPv6-Adresse';
case 'macAddress':
return 'die MAC-Adresse';
case 'username':
return 'den Benutzernamen';
case 'passwordHash':
return 'das Passwort';
case 'group':
return 'die Gruppe';
case 'location':
return 'den Standort';
case 'tags':
return 'die 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 = '';
if (entry.changeType === 'CREATED') {
comment = 'Gerät angelegt.';
} else if (entry.changeType === 'DELETED') {
comment = 'Gerät gelöscht.';
} else {
comment = 'Gerät geändert.';
}
return {
id: entry.id,
type: 'comment',
person,
date,
comment,
};
});
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 rootClasses = asSidebar
? 'flex h-full flex-col text-sm'
: 'flex h-full flex-col border-l border-gray-200 px-4 py-4 text-sm dark:border-white/10';
return (
<Root className={rootClasses}>
<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="mt-3 flex-1 min-h-0">
<Feed items={items} className="h-full" />
</div>
)}
</Root>
);
}