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:
leocrm-bot
2026-06-29 11:01:39 +02:00
parent 22976abe92
commit 700b7a71ad
47 changed files with 4089 additions and 157 deletions
+167
View File
@@ -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>
);
}