// components/ui/Feed.tsx 'use client'; import * as React from 'react'; import clsx from 'clsx'; import PersonAvatar from '@/components/ui/UserAvatar'; /* ───────── Types ───────── */ export type FeedPerson = { /** * Fallback-Anzeigename – z.B. E-Mail, wenn sonst nichts da ist. * In der Anzeige wird aber bevorzugt: * arbeitsname > "Vorname Nachname" > nwkennung > email > name */ name: string; href?: string; imageUrl?: string; // optional: Avatar-Bild // Zusätzliche Infos, damit wir einen schönen Display-Namen bauen können arbeitsname?: string | null; firstName?: string | null; lastName?: string | null; nwkennung?: string | null; email?: string | null; }; export type FeedTag = { name: string; href?: string; /** z.B. 'fill-red-500' */ color?: string; }; export type FeedChange = { field: string; /** Anzeigename, z.B. "Standort" statt "location" */ label?: string; from: string | null; to: string | null; }; export type FeedItem = | { id: string | number; type: 'comment'; person: FeedPerson; imageUrl?: string; comment: string; /** Art des Kommentars – steuert nur noch Text, nicht mehr das Icon */ commentKind?: 'created' | 'deleted' | 'generic' | 'loaned' | 'returned'; } | { id: string | number; type: 'assignment'; person: FeedPerson; assigned: FeedPerson; date: string; } | { id: string | number; type: 'tags'; person: FeedPerson; tags: FeedTag[]; date: string; } | { id: string | number; type: 'change'; person: FeedPerson; changes: FeedChange[]; date: string; }; export interface FeedProps { items: FeedItem[]; className?: string; } /* ───────── Helper ───────── */ // DE Datum+Uhrzeit für Änderungen const dtfDateTime = new Intl.DateTimeFormat('de-DE', { dateStyle: 'short', timeStyle: 'short', }); /** * Erzeugt den eigentlichen Anzeigenamen für eine Person: * arbeitsname > "Vorname Nachname" > nwkennung > email > name */ function getDisplayName(person: FeedPerson): string { return ( person.arbeitsname ?? (person.firstName && person.lastName ? `${person.firstName} ${person.lastName}` : person.nwkennung ?? person.email ?? person.name) ); } /** Werte für "change"-Einträge formatiert darstellen */ function formatChangeValue(field: string, value: string | null): string { if (!value) return '—'; // Für Verleih-Daten: schön als Datum + Uhrzeit anzeigen if (field === 'loanedFrom' || field === 'loanedUntil') { const d = new Date(value); if (!isNaN(d.getTime())) { return dtfDateTime.format(d); } } // alles andere unverändert return value; } // sprechende Zusammenfassung für "change" function getChangeSummary( item: Extract, ): React.ReactNode { const { changes } = item; if (!changes.length) return 'hat Änderungen vorgenommen'; // Sonderfall: nur Tags if (changes.length === 1 && changes[0].field === 'tags') { const c = changes[0]; const beforeList = (c.from ?? '') .split(',') .map((s) => s.trim()) .filter(Boolean); const afterList = (c.to ?? '') .split(',') .map((s) => s.trim()) .filter(Boolean); const beforeLower = beforeList.map((x) => x.toLowerCase()); const afterLower = afterList.map((x) => x.toLowerCase()); const added = afterList.filter( (t) => !beforeLower.includes(t.toLowerCase()), ); const removed = beforeList.filter( (t) => !afterLower.includes(t.toLowerCase()), ); const parts: string[] = []; if (added.length) parts.push(`hinzugefügt: ${added.join(', ')}`); if (removed.length) parts.push(`entfernt: ${removed.join(', ')}`); if (parts.length > 0) { return ( <> hat Tags {parts.join(' · ')} ); } return 'hat Tags angepasst'; } if (changes.length === 1) { const c = changes[0]; const label = c.label ?? c.field; return ( <> hat {label} geändert ); } const labels = changes.map((c) => c.label ?? c.field); const uniqueLabels = Array.from(new Set(labels)); const maxShow = 3; if (uniqueLabels.length <= maxShow) { return ( <> hat{' '} {uniqueLabels.map((label, index) => ( {label} {index < uniqueLabels.length - 1 ? ', ' : ' '} ))} geändert ); } const first = uniqueLabels.slice(0, maxShow); return ( <> hat{' '} {first.map((label, index) => ( {label} {index < first.length - 1 ? ', ' : ' '} ))} und weitere geändert ); } function formatRelativeDate(raw: string): string { const d = new Date(raw); if (isNaN(d.getTime())) { // Falls kein gültiges Datum (z.B. schon formatiert), einfach roh anzeigen return raw; } const now = new Date(); const diffMs = now.getTime() - d.getTime(); const diffSec = Math.floor(diffMs / 1000); if (diffSec < 5) return 'gerade eben'; if (diffSec < 60) return `vor ${diffSec} Sekunden`; const diffMin = Math.floor(diffSec / 60); if (diffMin < 60) { return `vor ${diffMin} Minute${diffMin === 1 ? '' : 'n'}`; } const diffH = Math.floor(diffMin / 60); if (diffH < 24) { return `vor ${diffH} Stunde${diffH === 1 ? '' : 'n'}`; } const diffD = Math.floor(diffH / 24); if (diffD < 30) { return `vor ${diffD} Tag${diffD === 1 ? '' : 'en'}`; } // Fallback: für ältere Einträge wieder absolutes Datum return new Intl.DateTimeFormat('de-DE', { dateStyle: 'short', timeStyle: 'short', }).format(d); } /* ───────── Component ───────── */ export default function Feed({ items, className }: FeedProps) { if (!items.length) { return (

Keine Aktivitäten vorhanden.

); } return ( ); }