import React, { useState, useMemo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { isToday, isYesterday, parseISO, isValid } from 'date-fns'; import { Loader2, Inbox } from 'lucide-react'; import { ActivityFeed, ActivityItem } from '@/components/shared/ActivityFeed'; import { ActivityFilter, ActivityFilterValues } from '@/components/activity/ActivityFilter'; import { useAuditLog, AuditLogEntry } from '@/api/audit'; import { formatDateTime, formatDate } from '@/utils/date'; const PAGE_SIZE = 25; interface DayGroup { key: string; label: string; activities: ActivityItem[]; } function toValidDate(value: string): Date | null { if (!value) return null; const parsed = parseISO(value); return isValid(parsed) ? parsed : null; } function getDayLabel(date: Date): string { if (isToday(date)) return 'Heute'; if (isYesterday(date)) return 'Gestern'; return formatDate(date, 'EEEE, dd.MM.yyyy'); } function groupByDay(entries: AuditLogEntry[]): DayGroup[] { const groups: Map = new Map(); for (const entry of entries) { const date = toValidDate(entry.timestamp); if (!date) continue; const dayKey = formatDate(date, 'yyyy-MM-dd'); if (!groups.has(dayKey)) { groups.set(dayKey, { key: dayKey, label: getDayLabel(date), activities: [], }); } const activity: ActivityItem = { id: `${entry.id}-${entry.timestamp}`, user: entry.user || 'System', action: entry.action || '', time: entry.timestamp ? formatDateTime(entry.timestamp) : '', avatarUrl: null, }; groups.get(dayKey)!.activities.push(activity); } return Array.from(groups.values()).sort((a, b) => b.key.localeCompare(a.key)); } function LoadingSkeleton() { return (
{[1, 2, 3].map((i) => (
{[1, 2, 3].map((j) => (
))}
))}
); } function EmptyState({ message }: { message: string }) { return (
); } export function ActivityTimelinePage() { const { t } = useTranslation(); const [page, setPage] = useState(1); const [filters, setFilters] = useState({}); const [accumulated, setAccumulated] = useState([]); const apiFilters = useMemo( () => ({ user: filters.user, action: filters.action, entity: filters.entity_type, dateFrom: filters.date_from, dateTo: filters.date_to, }), [filters] ); const { data, isLoading, isError, isFetching } = useAuditLog(page, PAGE_SIZE, apiFilters); const entries = data?.items ?? []; const total = data?.total ?? 0; const hasMore = page * PAGE_SIZE < total; useEffect(() => { if (page === 1) { setAccumulated(entries); } else { setAccumulated((prev) => { const existingIds = new Set(prev.map((e) => e.id)); const newEntries = entries.filter((e) => !existingIds.has(e.id)); return [...prev, ...newEntries]; }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [entries, page]); const handleFilterChange = (newFilters: ActivityFilterValues) => { setFilters(newFilters); setPage(1); setAccumulated([]); }; const dayGroups = useMemo(() => groupByDay(accumulated), [accumulated]); const pageTitle = t('activity.title', 'Aktivitäten'); return (

{pageTitle}

{isLoading && page === 1 ? ( ) : isError ? (

{t('activity.loadError', 'Aktivitäten konnten nicht geladen werden.')}

) : dayGroups.length === 0 ? ( ) : ( <>
{dayGroups.map((group) => ( ))}
{hasMore && (
)}

{accumulated.length} / {total} {t('activity.entriesLoaded', 'Einträge')}

)}
); }