Files
leocrm/frontend/src/pages/ContactsList.tsx
T
leocrm-bot 700b7a71ad 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
2026-06-29 11:01:39 +02:00

150 lines
4.6 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ColumnDef } from '@tanstack/react-table';
import { useContacts, useDeleteContact, Contact } from '@/api/hooks';
import { DataGrid } from '@/components/shared/DataGrid';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { EmptyState } from '@/components/ui/EmptyState';
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
import { useToast } from '@/components/ui/Toast';
import { Avatar } from '@/components/ui/Avatar';
export function ContactsListPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const toast = useToast();
const [page, setPage] = useState(1);
const [pageSize] = useState(25);
const [search, setSearch] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const [deleteTarget, setDeleteTarget] = useState<Contact | null>(null);
const { data, isLoading } = useContacts(page, pageSize, debouncedSearch);
const deleteMutation = useDeleteContact();
React.useEffect(() => {
const timer = setTimeout(() => setDebouncedSearch(search), 300);
return () => clearTimeout(timer);
}, [search]);
const columns = useMemo<ColumnDef<Contact, any>[]>(
() => [
{
id: 'name',
header: t('contacts.fullName'),
cell: ({ row }) => (
<div className="flex items-center gap-2">
<Avatar name={`${row.original.first_name} ${row.original.last_name}`} size="sm" />
<span className="font-medium text-primary-700">
{row.original.first_name} {row.original.last_name}
</span>
</div>
),
},
{
accessorKey: 'email',
header: t('contacts.email'),
cell: (info) =>
info.getValue() ? (
<a href={`mailto:${info.getValue()}`} className="text-primary-600 hover:text-primary-700">
{info.getValue()}
</a>
) : (
'—'
),
},
{
accessorKey: 'phone',
header: t('contacts.phone'),
cell: (info) => info.getValue() || '—',
},
{
accessorKey: 'position',
header: t('contacts.position'),
cell: (info) => info.getValue() || '—',
},
],
[t]
);
const handleDelete = async () => {
if (!deleteTarget) return;
try {
await deleteMutation.mutateAsync(deleteTarget.id);
toast.success(t('contacts.deleted'));
setDeleteTarget(null);
} catch (err: any) {
toast.error(err.message || t('common.error'));
}
};
const contacts = data?.items ?? [];
const total = data?.total ?? 0;
const isEmpty = !isLoading && contacts.length === 0 && !debouncedSearch;
if (isEmpty) {
return (
<div className="p-6 max-w-7xl mx-auto" data-testid="contacts-list-page">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-secondary-900">{t('contacts.title')}</h1>
</div>
<EmptyState
title={t('contacts.emptyTitle')}
description={t('contacts.emptyDescription')}
action={
<Button onClick={() => navigate('/contacts/new')}>
{t('contacts.create')}
</Button>
}
/>
</div>
);
}
return (
<div className="p-6 max-w-7xl mx-auto" data-testid="contacts-list-page">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-secondary-900">{t('contacts.title')}</h1>
<Button onClick={() => navigate('/contacts/new')}>
{t('contacts.create')}
</Button>
</div>
<div className="mb-4">
<Input
type="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t('common.search')}
aria-label={t('common.search')}
/>
</div>
<DataGrid
columns={columns}
data={contacts}
total={total}
page={page}
pageSize={pageSize}
onPageChange={setPage}
onRowClick={(row) => navigate(`/contacts/${row.id}`)}
loading={isLoading}
emptyMessage={t('common.noResults')}
testId="contacts-grid"
/>
<ConfirmDialog
open={!!deleteTarget}
title={t('contacts.deleteConfirmTitle')}
message={t('contacts.deleteConfirm')}
variant="danger"
onConfirm={handleDelete}
onCancel={() => setDeleteTarget(null)}
/>
</div>
);
}