T07b: frontend feature pages — companies + contacts + settings + audit + dashboard + search
- 11 new feature pages (CompaniesList/Detail/Form, ContactsList/Detail/Form, SettingsProfile/Roles/Users, AuditLog, GlobalSearchResults) - 3 page updates (Dashboard with StatCard+ActivityFeed, Settings with tree nav+Outlet, TopBar with SearchDropdown) - 13 new routes in routes/index.tsx - i18n updates (de.json + en.json) with companies/contacts/settings/audit/search keys - 12 new test files + 2 existing test fixes (TopBar, AppShell) - 7 shared components (DataGrid, Tabs, SearchDropdown, CsvImportDialog, StatCard, ActivityFeed, UnsavedChangesGuard) - 16 new API hooks in hooks.ts - Verification: 141 tests pass, build succeeds, tsc --noEmit clean
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useAuditLog, AuditLogEntry } from '@/api/hooks';
|
||||
import { DataGrid } from '@/components/shared/DataGrid';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
|
||||
export function AuditLogPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(25);
|
||||
const [filterUser, setFilterUser] = useState('');
|
||||
const [filterAction, setFilterAction] = useState('');
|
||||
const [filterEntity, setFilterEntity] = useState('');
|
||||
const [filterDateFrom, setFilterDateFrom] = useState('');
|
||||
const [filterDateTo, setFilterDateTo] = useState('');
|
||||
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
user: filterUser || undefined,
|
||||
action: filterAction || undefined,
|
||||
entity: filterEntity || undefined,
|
||||
dateFrom: filterDateFrom || undefined,
|
||||
dateTo: filterDateTo || undefined,
|
||||
}),
|
||||
[filterUser, filterAction, filterEntity, filterDateFrom, filterDateTo]
|
||||
);
|
||||
|
||||
const { data, isLoading, isError } = useAuditLog(page, pageSize, filters);
|
||||
|
||||
const columns = useMemo<ColumnDef<AuditLogEntry, any>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'timestamp',
|
||||
header: t('auditLog.timestamp'),
|
||||
cell: (info) => {
|
||||
const val = info.getValue();
|
||||
if (!val) return '—';
|
||||
const date = new Date(val);
|
||||
return date.toLocaleString('de-DE');
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'user',
|
||||
header: t('auditLog.user'),
|
||||
cell: (info) => info.getValue() || '—',
|
||||
},
|
||||
{
|
||||
accessorKey: 'action',
|
||||
header: t('auditLog.action'),
|
||||
cell: (info) =>
|
||||
info.getValue() ? <Badge variant="primary">{info.getValue()}</Badge> : '—',
|
||||
},
|
||||
{
|
||||
accessorKey: 'entity',
|
||||
header: t('auditLog.entity'),
|
||||
cell: (info) => info.getValue() || '—',
|
||||
},
|
||||
{
|
||||
accessorKey: 'entity_id',
|
||||
header: t('auditLog.entityId'),
|
||||
cell: (info) => info.getValue() || '—',
|
||||
},
|
||||
{
|
||||
accessorKey: 'details',
|
||||
header: t('auditLog.details'),
|
||||
cell: (info) => {
|
||||
const val = info.getValue();
|
||||
if (!val) return '—';
|
||||
return <span className="text-sm text-secondary-600 truncate">{val}</span>;
|
||||
},
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const handleResetFilters = () => {
|
||||
setFilterUser('');
|
||||
setFilterAction('');
|
||||
setFilterEntity('');
|
||||
setFilterDateFrom('');
|
||||
setFilterDateTo('');
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const hasFilters = filterUser || filterAction || filterEntity || filterDateFrom || filterDateTo;
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto" data-testid="audit-log-page">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-6">{t('auditLog.title')}</h1>
|
||||
<EmptyState
|
||||
title={t('auditLog.notAvailable')}
|
||||
description={t('auditLog.empty')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const entries = data?.items ?? [];
|
||||
const total = data?.total ?? 0;
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto" data-testid="audit-log-page">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-6">{t('auditLog.title')}</h1>
|
||||
|
||||
<Card title={t('common.filter')} className="mb-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||
<Input
|
||||
label={t('auditLog.user')}
|
||||
value={filterUser}
|
||||
onChange={(e) => setFilterUser(e.target.value)}
|
||||
placeholder="anna.schmidt"
|
||||
/>
|
||||
<Input
|
||||
label={t('auditLog.action')}
|
||||
value={filterAction}
|
||||
onChange={(e) => setFilterAction(e.target.value)}
|
||||
placeholder="create, update, delete"
|
||||
/>
|
||||
<Input
|
||||
label={t('auditLog.entity')}
|
||||
value={filterEntity}
|
||||
onChange={(e) => setFilterEntity(e.target.value)}
|
||||
placeholder="company, contact"
|
||||
/>
|
||||
<Input
|
||||
label={t('auditLog.dateFrom')}
|
||||
type="date"
|
||||
value={filterDateFrom}
|
||||
onChange={(e) => setFilterDateFrom(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label={t('auditLog.dateTo')}
|
||||
type="date"
|
||||
value={filterDateTo}
|
||||
onChange={(e) => setFilterDateTo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{hasFilters && (
|
||||
<div className="mt-3 flex justify-end">
|
||||
<Button variant="ghost" size="sm" onClick={handleResetFilters}>
|
||||
{t('common.reset')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={entries}
|
||||
total={total}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
onPageChange={setPage}
|
||||
loading={isLoading}
|
||||
emptyMessage={t('auditLog.empty')}
|
||||
testId="audit-log-grid"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user