195 lines
4.7 KiB
TypeScript
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>
|
|
);
|
|
}
|