613 lines
No EOL
22 KiB
TypeScript
613 lines
No EOL
22 KiB
TypeScript
import React, { useState, useMemo, useEffect } from 'react';
|
|
import { Helmet } from 'react-helmet';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Plus, Eye, Mail, Users, Building2, UserCheck, UserX, X } from 'lucide-react';
|
|
import DataTable from '@/components/DataTable';
|
|
import KPIBar, { PeriodType } from '@/components/KPIBar';
|
|
import { toast } from '@/components/ui/use-toast';
|
|
import { CompanyInfo } from '@/data/mockData';
|
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
|
import { getClients } from '@/store/features/client/thunk';
|
|
import { clientStatus, getAllClients } from '@/store/features/client/selectors';
|
|
import { Client, Contacts } from '@/types/clientType';
|
|
import { selectClient } from '@/store/features/client/slice';
|
|
import PrimaryButton_v2 from '@/components/PrimaryButton_v2';
|
|
import StatusBadgetLettre from '@/components/StatusBadgetLettre';
|
|
import ColumnSelector, { ColumnConfig } from '@/components/common/ColumnSelector';
|
|
import { Commercial } from '@/types/commercialType';
|
|
import { cn } from '@/lib/utils';
|
|
import { useDashboardData } from '@/store/hooks/useAppData';
|
|
|
|
// ============================================
|
|
// TYPES
|
|
// ============================================
|
|
|
|
type FilterType = 'all' | 'active' | 'with_contacts' | 'inactive';
|
|
|
|
interface KPIConfig {
|
|
id: FilterType;
|
|
title: string;
|
|
icon: React.ElementType;
|
|
color: string;
|
|
getValue: (clients: Client[]) => number;
|
|
getSubtitle: (clients: Client[], value: number) => string;
|
|
getChange: (clients: Client[], value: number) => string;
|
|
getTrend: (value: number) => 'up' | 'down' | 'neutral';
|
|
filter: (clients: Client[]) => Client[];
|
|
}
|
|
|
|
// ============================================
|
|
// CONFIGURATION DES COLONNES
|
|
// ============================================
|
|
|
|
const DEFAULT_COLUMNS: ColumnConfig[] = [
|
|
{ key: 'numero', label: 'Numéro', visible: true, locked: true },
|
|
{ key: 'intitule', label: 'Nom', visible: true },
|
|
{ key: 'email', label: 'Email', visible: true },
|
|
{ key: 'telephone', label: 'Téléphone', visible: true },
|
|
{ key: 'contacts', label: 'Contacts', visible: true },
|
|
{ key: 'commercial', label: 'Commercial', visible: true },
|
|
{ key: 'est_actif', label: 'Statut', visible: true },
|
|
];
|
|
|
|
// ============================================
|
|
// CONFIGURATION DES KPIs
|
|
// ============================================
|
|
|
|
const KPI_CONFIG: KPIConfig[] = [
|
|
{
|
|
id: 'all',
|
|
title: 'Total Clients',
|
|
icon: Users,
|
|
color: 'blue',
|
|
getValue: (clients) => clients.length,
|
|
getSubtitle: (clients) => {
|
|
const active = clients.filter(c => c.est_actif !== false).length;
|
|
return `${active} actif${active > 1 ? 's' : ''}`;
|
|
},
|
|
getChange: () => '',
|
|
getTrend: () => 'neutral',
|
|
filter: (clients) => clients,
|
|
},
|
|
{
|
|
id: 'active',
|
|
title: 'Clients Actifs',
|
|
icon: UserCheck,
|
|
color: 'green',
|
|
getValue: (clients) => clients.filter(c => c.est_actif !== false).length,
|
|
getSubtitle: () => 'du portefeuille',
|
|
getChange: (clients, value) => {
|
|
const total = clients.length;
|
|
return total > 0 ? `${((value / total) * 100).toFixed(1)}%` : '0%';
|
|
},
|
|
getTrend: () => 'up',
|
|
filter: (clients) => clients.filter(c => c.est_actif !== false),
|
|
},
|
|
{
|
|
id: 'with_contacts',
|
|
title: 'Avec Contacts',
|
|
icon: Building2,
|
|
color: 'orange',
|
|
getValue: (clients) => clients.filter(c => c.contacts && c.contacts.length > 0).length,
|
|
getSubtitle: () => 'ont des contacts',
|
|
getChange: (clients, value) => {
|
|
const total = clients.length;
|
|
return total > 0 ? `${((value / total) * 100).toFixed(0)}%` : '0%';
|
|
},
|
|
getTrend: () => 'neutral',
|
|
filter: (clients) => clients.filter(c => c.contacts && c.contacts.length > 0),
|
|
},
|
|
{
|
|
id: 'inactive',
|
|
title: 'Inactifs',
|
|
icon: UserX,
|
|
color: 'red',
|
|
getValue: (clients) => clients.filter(c => c.est_actif === false).length,
|
|
getSubtitle: (clients, value) => {
|
|
const total = clients.length;
|
|
return `${total > 0 ? ((value / total) * 100).toFixed(0) : 0}% du total`;
|
|
},
|
|
getChange: () => '',
|
|
getTrend: (value) => (value > 0 ? 'down' : 'up'),
|
|
filter: (clients) => clients.filter(c => c.est_actif === false),
|
|
},
|
|
];
|
|
|
|
// ============================================
|
|
// COMPOSANT PRINCIPAL
|
|
// ============================================
|
|
|
|
const ClientsPage = () => {
|
|
const navigate = useNavigate();
|
|
const dispatch = useAppDispatch();
|
|
|
|
const clients = useAppSelector(getAllClients) as Client[];
|
|
const statusClient = useAppSelector(clientStatus);
|
|
const [period, setPeriod] = useState<PeriodType>('all');
|
|
const { refresh } = useDashboardData();
|
|
|
|
// État du filtre actif
|
|
const [activeFilter, setActiveFilter] = useState<FilterType>('all');
|
|
|
|
// État des colonnes visibles
|
|
const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>(DEFAULT_COLUMNS);
|
|
|
|
const isLoading = statusClient === 'loading' && clients.length === 0;
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
if (statusClient === 'idle' || statusClient === 'failed') {
|
|
await dispatch(getClients()).unwrap();
|
|
}
|
|
};
|
|
load();
|
|
}, [statusClient, dispatch]);
|
|
|
|
// Générer les KPIs avec onClick
|
|
const kpis = useMemo(() => {
|
|
return KPI_CONFIG.map(config => {
|
|
const value = config.getValue(clients);
|
|
return {
|
|
id: config.id,
|
|
title: config.title,
|
|
value,
|
|
change: config.getChange(clients, value),
|
|
trend: config.getTrend(value),
|
|
icon: config.icon,
|
|
subtitle: config.getSubtitle(clients, value),
|
|
color: config.color,
|
|
isActive: activeFilter === config.id,
|
|
onClick: () => {
|
|
// Toggle: si déjà actif, revenir à "all"
|
|
setActiveFilter(prev => (prev === config.id ? 'all' : config.id));
|
|
},
|
|
};
|
|
});
|
|
}, [clients, activeFilter]);
|
|
|
|
// Filtrer les clients selon le filtre actif
|
|
const filteredClients = useMemo(() => {
|
|
const config = KPI_CONFIG.find(k => k.id === activeFilter);
|
|
if (!config) return clients;
|
|
return config.filter(clients);
|
|
}, [clients, activeFilter]);
|
|
|
|
// Tri par intitulé (alphabétique)
|
|
const sortedClients = useMemo(() => {
|
|
return [...filteredClients].sort((a, b) =>
|
|
(a.intitule || '').localeCompare(b.intitule || '')
|
|
);
|
|
}, [filteredClients]);
|
|
|
|
// Label du filtre actif
|
|
const activeFilterLabel = useMemo(() => {
|
|
const config = KPI_CONFIG.find(k => k.id === activeFilter);
|
|
return config?.title || 'Tous';
|
|
}, [activeFilter]);
|
|
|
|
const handleCreate = () => {
|
|
navigate('/home/clients/create');
|
|
};
|
|
|
|
const handleView = (row: Client) => {
|
|
dispatch(selectClient(row));
|
|
navigate(`/home/clients/${row.numero}`);
|
|
};
|
|
|
|
// ============================================
|
|
// COLONNES DYNAMIQUES
|
|
// ============================================
|
|
|
|
const allColumnsDefinition = useMemo(() => {
|
|
return {
|
|
numero: { key: 'numero', label: 'Numéro', sortable: true },
|
|
intitule: { key: 'intitule', label: 'Nom', sortable: true },
|
|
email: { key: 'email', label: 'Email', sortable: true },
|
|
telephone: {
|
|
key: 'telephone',
|
|
label: 'Téléphone',
|
|
sortable: true,
|
|
render: (value: any) => <span>{value}</span>,
|
|
},
|
|
contacts: {
|
|
key: 'contacts',
|
|
label: 'Contacts',
|
|
sortable: false,
|
|
render: (row: Contacts[]) => {
|
|
const count = row?.length || 0;
|
|
const defaultContact = row?.find(c => c.est_defaut);
|
|
|
|
return (
|
|
<div className="flex flex-col">
|
|
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
|
{count} contact{count !== 1 ? 's' : ''}
|
|
</span>
|
|
{defaultContact && (
|
|
<span className="text-xs text-gray-500">
|
|
{defaultContact.nom} {defaultContact.prenom} (défaut)
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
est_actif: {
|
|
key: 'est_actif',
|
|
label: 'Statut',
|
|
sortable: true,
|
|
render: (isActive: any) => (
|
|
<StatusBadgetLettre status={isActive !== false ? 'actif' : 'inactif'} />
|
|
),
|
|
},
|
|
commercial: {
|
|
key: 'commercial',
|
|
label: 'Commercial',
|
|
sortable: true,
|
|
render: (value?: Commercial) => (
|
|
<span>
|
|
{value?.prenom && value?.nom ? `${value.prenom} ${value.nom}` : '_'}
|
|
</span>
|
|
),
|
|
},
|
|
};
|
|
}, []);
|
|
|
|
const visibleColumns = useMemo(() => {
|
|
return columnConfig
|
|
.filter(col => col.visible)
|
|
.map(col => allColumnsDefinition[col.key as keyof typeof allColumnsDefinition])
|
|
.filter(Boolean);
|
|
}, [columnConfig, allColumnsDefinition]);
|
|
|
|
const actions = (row: Client) => (
|
|
<>
|
|
<button
|
|
onClick={e => {
|
|
e.stopPropagation();
|
|
handleView(row);
|
|
}}
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
|
title="Voir détails"
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={e => {
|
|
e.stopPropagation();
|
|
if (row.email) {
|
|
toast({ title: 'Email envoyé', description: `À: ${row.email}` });
|
|
} else {
|
|
toast({
|
|
title: "Pas d'email",
|
|
description: "Ce client n'a pas d'adresse email",
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
}}
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
|
title="Envoyer email"
|
|
>
|
|
<Mail className="w-4 h-4" />
|
|
</button>
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<Helmet>
|
|
<title>Clients - {CompanyInfo.name}</title>
|
|
<meta name="description" content="Gestion de vos clients" />
|
|
</Helmet>
|
|
|
|
<div className="space-y-6">
|
|
<KPIBar
|
|
kpis={kpis}
|
|
loading={statusClient}
|
|
onRefresh={refresh}
|
|
onPeriodChange={setPeriod}
|
|
period={period}
|
|
/>
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div>
|
|
<div className="flex items-center gap-3">
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
Clients
|
|
</h1>
|
|
{/* Badge filtre actif */}
|
|
{activeFilter !== 'all' && (
|
|
<button
|
|
onClick={() => setActiveFilter('all')}
|
|
className={cn(
|
|
"inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium transition-colors",
|
|
"bg-[#007E45]/10 text-[#007E45] hover:bg-[#007E45]/20"
|
|
)}
|
|
>
|
|
{activeFilterLabel}
|
|
<X className="w-3 h-3" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
{activeFilter === 'all' ? (
|
|
<>
|
|
{clients.filter(c => c.est_actif !== false).length} clients actifs sur{' '}
|
|
{clients.length}
|
|
</>
|
|
) : (
|
|
<>
|
|
{filteredClients.length} client{filteredClients.length > 1 ? 's' : ''}{' '}
|
|
({activeFilterLabel.toLowerCase()})
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<ColumnSelector columns={columnConfig} onChange={setColumnConfig} />
|
|
<PrimaryButton_v2 icon={Plus} onClick={handleCreate}>
|
|
Nouveau client
|
|
</PrimaryButton_v2>
|
|
</div>
|
|
</div>
|
|
|
|
<DataTable
|
|
columns={visibleColumns}
|
|
data={sortedClients}
|
|
onRowClick={(row: Client) => handleView(row)}
|
|
actions={actions}
|
|
status={isLoading}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ClientsPage;
|
|
|
|
// import React, { useState, useMemo, useEffect } from 'react';
|
|
// import { Helmet } from 'react-helmet';
|
|
// import { useNavigate } from 'react-router-dom';
|
|
// import { Plus, Eye, Mail, Users, Building2, UserCheck, UserX, UserPlus } from 'lucide-react';
|
|
// import DataTable from '@/components/DataTable';
|
|
// import KPIBar, { PeriodType } from '@/components/KPIBar';
|
|
// import { toast } from '@/components/ui/use-toast';
|
|
// import { CompanyInfo } from '@/data/mockData';
|
|
// import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
|
// import { getClients } from '@/store/features/client/thunk';
|
|
// import { clientStatus, getAllClients } from '@/store/features/client/selectors';
|
|
// import { Client, Contacts } from '@/types/clientType';
|
|
// import { selectClient } from '@/store/features/client/slice';
|
|
// import PrimaryButton_v2 from '@/components/PrimaryButton_v2';
|
|
// import StatusBadgetLettre from '@/components/StatusBadgetLettre';
|
|
// import ColumnSelector, { ColumnConfig } from '@/components/common/ColumnSelector';
|
|
// import { Commercial } from '@/types/commercialType';
|
|
|
|
// // ============================================
|
|
// // CONFIGURATION DES COLONNES
|
|
// // ============================================
|
|
|
|
// const DEFAULT_COLUMNS: ColumnConfig[] = [
|
|
// { key: 'numero', label: 'Numéro', visible: true, locked: true },
|
|
// { key: 'intitule', label: 'Nom', visible: true },
|
|
// { key: 'email', label: 'Email', visible: true },
|
|
// { key: 'telephone', label: 'Téléphone', visible: true },
|
|
// { key: 'contacts', label: 'Contacts', visible: true },
|
|
// { key: 'commercial', label: 'Commercial', visible: true },
|
|
// { key: 'est_actif', label: 'Statut', visible: true },
|
|
// ];
|
|
|
|
// const ClientsPage = () => {
|
|
// const navigate = useNavigate();
|
|
// const dispatch = useAppDispatch();
|
|
|
|
// const clients = useAppSelector(getAllClients) as Client[];
|
|
// const statusClient = useAppSelector(clientStatus);
|
|
// const [period, setPeriod] = useState<PeriodType>('all');
|
|
|
|
// // État des colonnes visibles
|
|
// const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>(DEFAULT_COLUMNS);
|
|
|
|
// const handleRefresh = async () => {
|
|
// await dispatch(getClients()).unwrap();
|
|
// };
|
|
|
|
// const isLoading = statusClient === 'loading' && clients.length === 0;
|
|
|
|
// useEffect(() => {
|
|
// const load = async () => {
|
|
// if (statusClient === 'idle' || statusClient === 'failed') {
|
|
// await dispatch(getClients()).unwrap();
|
|
// }
|
|
// };
|
|
// load();
|
|
// }, [statusClient, dispatch]);
|
|
|
|
// // Tri par intitulé (alphabétique)
|
|
// const sortedClients = useMemo(() => {
|
|
// return [...clients].sort((a, b) => (a.intitule || '').localeCompare(b.intitule || ''));
|
|
// }, [clients]);
|
|
|
|
// const kpis = useMemo(() => {
|
|
// const totalClients = clients.length;
|
|
// const activeClients = clients.filter(c => c.est_actif !== false);
|
|
// const inactiveClients = clients.filter(c => c.est_actif === false);
|
|
// const prospects = clients.filter(c => c.est_prospect === true);
|
|
// const withContacts = clients.filter(c => c.contacts && c.contacts.length > 0);
|
|
|
|
// const activityRate = totalClients > 0 ? ((activeClients.length / totalClients) * 100).toFixed(1) : '0';
|
|
// const contactRate = totalClients > 0 ? ((withContacts.length / totalClients) * 100).toFixed(0) : '0';
|
|
|
|
// return [
|
|
// {
|
|
// title: 'Total Clients',
|
|
// value: totalClients,
|
|
// change: '',
|
|
// trend: 'neutral',
|
|
// icon: Users,
|
|
// subtitle: `${activeClients.length} actif${activeClients.length > 1 ? 's' : ''}`,
|
|
// color: 'blue',
|
|
// },
|
|
// {
|
|
// title: 'Clients Actifs',
|
|
// value: activeClients.length,
|
|
// change: `${activityRate}%`,
|
|
// trend: 'up',
|
|
// icon: UserCheck,
|
|
// subtitle: 'du portefeuille',
|
|
// color: 'green',
|
|
// },
|
|
// {
|
|
// title: 'Avec Contacts',
|
|
// value: withContacts.length,
|
|
// change: `${contactRate}%`,
|
|
// trend: 'neutral',
|
|
// icon: Building2,
|
|
// subtitle: 'ont des contacts',
|
|
// color: 'orange',
|
|
// },
|
|
// {
|
|
// title: 'Inactifs',
|
|
// value: inactiveClients.length,
|
|
// change: '',
|
|
// trend: inactiveClients.length > 0 ? 'down' : 'up',
|
|
// icon: UserX,
|
|
// subtitle: `${totalClients > 0 ? ((inactiveClients.length / totalClients) * 100).toFixed(0) : 0}% du total`,
|
|
// color: 'red',
|
|
// },
|
|
// ];
|
|
// }, [clients]);
|
|
|
|
// const handleCreate = () => {
|
|
// navigate('/home/clients/create');
|
|
// };
|
|
|
|
// const handleView = (row: Client) => {
|
|
// dispatch(selectClient(row));
|
|
// navigate(`/home/clients/${row.numero}`);
|
|
// };
|
|
|
|
// // ============================================
|
|
// // COLONNES DYNAMIQUES
|
|
// // ============================================
|
|
|
|
// const allColumnsDefinition = useMemo(() => {
|
|
// return {
|
|
// numero: { key: 'numero', label: 'Numéro', sortable: true },
|
|
// intitule: { key: 'intitule', label: 'Nom', sortable: true },
|
|
// email: { key: 'email', label: 'Email', sortable: true },
|
|
// telephone: { key: 'telephone', label: 'Téléphone', sortable: true, render: (value: any) => <span>+ {value} </span>, },
|
|
// contacts: {
|
|
// key: 'contacts',
|
|
// label: 'Contacts',
|
|
// sortable: false,
|
|
// render: (row: Contacts[]) => {
|
|
|
|
// const count = row?.length || 0;
|
|
// const defaultContact = row.find(c => c.est_defaut);
|
|
|
|
// return (
|
|
// <div className="flex flex-col">
|
|
// <span className="text-sm font-medium text-gray-900 dark:text-white">
|
|
// {count} contact{count !== 1 ? 's' : ''}
|
|
// </span>
|
|
// {defaultContact && (
|
|
// <span className="text-xs text-gray-500">
|
|
// {defaultContact.nom} {defaultContact.prenom} (défaut)
|
|
// </span>
|
|
// )}
|
|
// </div>
|
|
// );
|
|
// },
|
|
// },
|
|
// est_actif: {
|
|
// key: 'est_actif',
|
|
// label: 'Statut',
|
|
// sortable: true,
|
|
// render: (isActive: any) => <StatusBadgetLettre status={isActive !== false ? 'actif' : 'inactif'} />,
|
|
// },
|
|
// commercial: {
|
|
// key: 'commercial',
|
|
// label: 'Commercial',
|
|
// sortable: true,
|
|
// render: (value?: Commercial) => (
|
|
// <span>{value?.prenom && value?.nom ? `${value.prenom} ${value.nom}` : '_'}</span>
|
|
// )
|
|
// },
|
|
// };
|
|
// }, []);
|
|
|
|
|
|
// const visibleColumns = useMemo(() => {
|
|
// return columnConfig
|
|
// .filter(col => col.visible)
|
|
// .map(col => allColumnsDefinition[col.key as keyof typeof allColumnsDefinition])
|
|
// .filter(Boolean);
|
|
// }, [columnConfig, allColumnsDefinition]);
|
|
|
|
// const actions = (row: Client) => (
|
|
// <>
|
|
// <button
|
|
// onClick={e => {
|
|
// e.stopPropagation();
|
|
// handleView(row);
|
|
// }}
|
|
// className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
|
// title="Voir détails"
|
|
// >
|
|
// <Eye className="w-4 h-4" />
|
|
// </button>
|
|
// <button
|
|
// onClick={e => {
|
|
// e.stopPropagation();
|
|
// if (row.email) {
|
|
// toast({ title: 'Email envoyé', description: `À: ${row.email}` });
|
|
// } else {
|
|
// toast({ title: "Pas d'email", description: "Ce client n'a pas d'adresse email", variant: 'destructive' });
|
|
// }
|
|
// }}
|
|
// className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
|
// title="Envoyer email"
|
|
// >
|
|
// <Mail className="w-4 h-4" />
|
|
// </button>
|
|
// </>
|
|
// );
|
|
|
|
// return (
|
|
// <>
|
|
// <Helmet>
|
|
// <title>Clients - {CompanyInfo.name}</title>
|
|
// <meta name="description" content="Gestion de vos clients" />
|
|
// </Helmet>
|
|
|
|
// <div className="space-y-6">
|
|
// <KPIBar kpis={kpis} loading={statusClient} onRefresh={handleRefresh} onPeriodChange={setPeriod} period={period} />
|
|
|
|
// <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
// <div>
|
|
// <h1 className="text-2xl font-bold text-gray-900 dark:text-white">Clients</h1>
|
|
// <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
// {clients.filter(c => c.est_actif !== false).length} clients actifs sur {clients.length}
|
|
// </p>
|
|
// </div>
|
|
// <div className="flex gap-3">
|
|
// <ColumnSelector columns={columnConfig} onChange={setColumnConfig} />
|
|
// <PrimaryButton_v2 icon={Plus} onClick={handleCreate}>
|
|
// Nouveau client
|
|
// </PrimaryButton_v2>
|
|
// </div>
|
|
// </div>
|
|
|
|
// <DataTable
|
|
// columns={visibleColumns}
|
|
// data={sortedClients}
|
|
// onRowClick={(row: Client) => handleView(row)}
|
|
// actions={actions}
|
|
// status={isLoading}
|
|
// />
|
|
// </div>
|
|
// </>
|
|
// );
|
|
// };
|
|
|
|
// export default ClientsPage;
|