- Correction des erreurs TypeScript dans userService.ts et workflowTester.ts - Ajout des propriétés manquantes aux objets User mockés - Conversion des dates de string vers objets Date - Correction des appels asynchrones et des types incompatibles - Ajout de dynamic rendering pour résoudre les erreurs useSearchParams - Enveloppement de useSearchParams dans Suspense boundary - Configuration de force-dynamic au niveau du layout principal Build réussi: 126 pages générées avec succès 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
399 lines
17 KiB
TypeScript
399 lines
17 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { InputText } from 'primereact/inputtext';
|
|
import { Calendar } from 'primereact/calendar';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Button } from 'primereact/button';
|
|
import { Dropdown } from 'primereact/dropdown';
|
|
import { Divider } from 'primereact/divider';
|
|
import clientService from '../../../../services/clientService';
|
|
|
|
/**
|
|
* Page Historique Clients - BTP Express
|
|
* Suivi complet de l'activité clients avec métriques BTP
|
|
*/
|
|
const HistoriqueClientsPage = () => {
|
|
const [activites, setActivites] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [globalFilter, setGlobalFilter] = useState('');
|
|
const [selectedClient, setSelectedClient] = useState<any>(null);
|
|
const [dateDebut, setDateDebut] = useState<Date | null>(null);
|
|
const [dateFin, setDateFin] = useState<Date | null>(null);
|
|
const [typeActivite, setTypeActivite] = useState<string>('');
|
|
const [metriques, setMetriques] = useState({
|
|
nouveauxClients: 0,
|
|
chantiersTermines: 0,
|
|
caTotal: 0,
|
|
relancesEnvoyees: 0
|
|
});
|
|
|
|
const typesActivite = [
|
|
{ label: 'Toutes', value: '' },
|
|
{ label: 'Création client', value: 'CLIENT_CREATION' },
|
|
{ label: 'Modification client', value: 'CLIENT_UPDATE' },
|
|
{ label: 'Nouveau chantier', value: 'CHANTIER_CREATION' },
|
|
{ label: 'Chantier terminé', value: 'CHANTIER_TERMINE' },
|
|
{ label: 'Devis envoyé', value: 'DEVIS_ENVOYE' },
|
|
{ label: 'Devis accepté', value: 'DEVIS_ACCEPTE' },
|
|
{ label: 'Facture émise', value: 'FACTURE_EMISE' },
|
|
{ label: 'Paiement reçu', value: 'PAIEMENT_RECU' },
|
|
{ label: 'Relance envoyée', value: 'RELANCE_ENVOYEE' }
|
|
];
|
|
|
|
useEffect(() => {
|
|
loadHistorique();
|
|
loadMetriques();
|
|
}, [selectedClient, dateDebut, dateFin, typeActivite]);
|
|
|
|
const loadHistorique = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Simulation données historique BTP
|
|
const mockActivites = [
|
|
{
|
|
id: '1',
|
|
date: new Date('2025-01-30'),
|
|
type: 'CHANTIER_CREATION',
|
|
client: { nom: 'Dupont', prenom: 'Jean', entreprise: 'Maçonnerie Dupont' },
|
|
description: 'Nouveau chantier: Rénovation villa 150m²',
|
|
montant: 45000,
|
|
statut: 'EN_COURS',
|
|
utilisateur: 'M. Laurent (Chef de projet)'
|
|
},
|
|
{
|
|
id: '2',
|
|
date: new Date('2025-01-29'),
|
|
type: 'DEVIS_ACCEPTE',
|
|
client: { nom: 'Martin', prenom: 'Sophie', entreprise: 'Construction Martin' },
|
|
description: 'Devis DEV-2025-001 accepté - Extension maison',
|
|
montant: 28500,
|
|
statut: 'ACCEPTE',
|
|
utilisateur: 'Mme Petit (Commerciale)'
|
|
},
|
|
{
|
|
id: '3',
|
|
date: new Date('2025-01-28'),
|
|
type: 'FACTURE_EMISE',
|
|
client: { nom: 'Bernard', prenom: 'Michel', entreprise: null },
|
|
description: 'Facture FAC-2025-012 émise - Acompte 30%',
|
|
montant: 15600,
|
|
statut: 'ENVOYEE',
|
|
utilisateur: 'Mme Durand (Comptabilité)'
|
|
},
|
|
{
|
|
id: '4',
|
|
date: new Date('2025-01-27'),
|
|
type: 'CHANTIER_TERMINE',
|
|
client: { nom: 'Rousseau', prenom: 'Claire', entreprise: 'Pharmacie Rousseau' },
|
|
description: 'Chantier terminé: Aménagement officine 80m²',
|
|
montant: 32000,
|
|
statut: 'TERMINE',
|
|
utilisateur: 'M. Thomas (Chef équipe)'
|
|
},
|
|
{
|
|
id: '5',
|
|
date: new Date('2025-01-26'),
|
|
type: 'CLIENT_CREATION',
|
|
client: { nom: 'Leroy', prenom: 'Antoine', entreprise: 'Boulangerie Leroy' },
|
|
description: 'Nouveau client créé - Secteur alimentaire',
|
|
montant: 0,
|
|
statut: 'ACTIF',
|
|
utilisateur: 'Mme Petit (Commerciale)'
|
|
},
|
|
{
|
|
id: '6',
|
|
date: new Date('2025-01-25'),
|
|
type: 'PAIEMENT_RECU',
|
|
client: { nom: 'Garcia', prenom: 'Maria', entreprise: 'Restaurant Garcia' },
|
|
description: 'Paiement reçu - Solde chantier cuisine pro',
|
|
montant: 18750,
|
|
statut: 'PAYE',
|
|
utilisateur: 'Système (Automatique)'
|
|
}
|
|
];
|
|
|
|
setActivites(mockActivites);
|
|
} catch (error) {
|
|
console.error('Erreur chargement historique:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadMetriques = () => {
|
|
// Simulation métriques période
|
|
setMetriques({
|
|
nouveauxClients: 8,
|
|
chantiersTermines: 12,
|
|
caTotal: 185600,
|
|
relancesEnvoyees: 3
|
|
});
|
|
};
|
|
|
|
const typeActiviteBodyTemplate = (rowData: any) => {
|
|
const getConfig = (type: string) => {
|
|
switch (type) {
|
|
case 'CLIENT_CREATION': return { label: 'Nouveau client', severity: 'info', icon: 'pi-user-plus' };
|
|
case 'CLIENT_UPDATE': return { label: 'Client modifié', severity: 'warning', icon: 'pi-user-edit' };
|
|
case 'CHANTIER_CREATION': return { label: 'Nouveau chantier', severity: 'success', icon: 'pi-map' };
|
|
case 'CHANTIER_TERMINE': return { label: 'Chantier terminé', severity: 'success', icon: 'pi-check-circle' };
|
|
case 'DEVIS_ENVOYE': return { label: 'Devis envoyé', severity: 'info', icon: 'pi-send' };
|
|
case 'DEVIS_ACCEPTE': return { label: 'Devis accepté', severity: 'success', icon: 'pi-thumbs-up' };
|
|
case 'FACTURE_EMISE': return { label: 'Facture émise', severity: 'warning', icon: 'pi-file-excel' };
|
|
case 'PAIEMENT_RECU': return { label: 'Paiement reçu', severity: 'success', icon: 'pi-euro' };
|
|
case 'RELANCE_ENVOYEE': return { label: 'Relance envoyée', severity: 'danger', icon: 'pi-exclamation-triangle' };
|
|
default: return { label: type, severity: 'secondary', icon: 'pi-info-circle' };
|
|
}
|
|
};
|
|
|
|
const config = getConfig(rowData.type);
|
|
return (
|
|
<div className="flex align-items-center">
|
|
<i className={`pi ${config.icon} mr-2`} />
|
|
<Tag value={config.label} severity={config.severity as any} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const clientBodyTemplate = (rowData: any) => {
|
|
return (
|
|
<div className="flex flex-column">
|
|
<span className="font-medium">{`${rowData.client.prenom} ${rowData.client.nom}`}</span>
|
|
{rowData.client.entreprise && (
|
|
<span className="text-sm text-color-secondary">{rowData.client.entreprise}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const montantBodyTemplate = (rowData: any) => {
|
|
if (rowData.montant === 0) return <span className="text-color-secondary">-</span>;
|
|
|
|
return (
|
|
<span className={rowData.montant > 0 ? 'text-green-500 font-medium' : 'text-red-500 font-medium'}>
|
|
{new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(rowData.montant)}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const dateBodyTemplate = (rowData: any) => {
|
|
return (
|
|
<div className="flex flex-column">
|
|
<span>{rowData.date.toLocaleDateString('fr-FR')}</span>
|
|
<span className="text-sm text-color-secondary">
|
|
{rowData.date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
|
</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const utilisateurBodyTemplate = (rowData: any) => {
|
|
return (
|
|
<div className="flex align-items-center">
|
|
<i className="pi pi-user mr-2 text-color-secondary" />
|
|
<span className="text-sm">{rowData.utilisateur}</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const header = (
|
|
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center gap-3">
|
|
<h5 className="m-0">Historique d'Activité Clients</h5>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<span className="p-input-icon-left">
|
|
<i className="pi pi-search" />
|
|
<InputText
|
|
type="search"
|
|
placeholder="Rechercher..."
|
|
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
|
|
style={{ width: '250px' }}
|
|
/>
|
|
</span>
|
|
|
|
<Calendar
|
|
placeholder="Date début"
|
|
value={dateDebut}
|
|
onChange={(e) => setDateDebut(e.value || null)}
|
|
dateFormat="dd/mm/yy"
|
|
showIcon
|
|
style={{ width: '150px' }}
|
|
/>
|
|
|
|
<Calendar
|
|
placeholder="Date fin"
|
|
value={dateFin}
|
|
onChange={(e) => setDateFin(e.value || null)}
|
|
dateFormat="dd/mm/yy"
|
|
showIcon
|
|
style={{ width: '150px' }}
|
|
/>
|
|
|
|
<Dropdown
|
|
value={typeActivite}
|
|
options={typesActivite}
|
|
onChange={(e) => setTypeActivite(e.value)}
|
|
placeholder="Type d'activité"
|
|
style={{ width: '180px' }}
|
|
/>
|
|
|
|
<Button
|
|
icon="pi pi-filter-slash"
|
|
className="p-button-outlined"
|
|
onClick={() => {
|
|
setDateDebut(null);
|
|
setDateFin(null);
|
|
setTypeActivite('');
|
|
setSelectedClient(null);
|
|
setGlobalFilter('');
|
|
}}
|
|
tooltip="Réinitialiser les filtres"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="grid">
|
|
{/* Métriques de la période */}
|
|
<div className="col-12">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-3">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center">
|
|
<div>
|
|
<div className="text-2xl font-bold text-blue-500">{metriques.nouveauxClients}</div>
|
|
<div className="text-color-secondary">Nouveaux clients</div>
|
|
</div>
|
|
<i className="pi pi-user-plus text-blue-500 text-3xl" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center">
|
|
<div>
|
|
<div className="text-2xl font-bold text-green-500">{metriques.chantiersTermines}</div>
|
|
<div className="text-color-secondary">Chantiers terminés</div>
|
|
</div>
|
|
<i className="pi pi-check-circle text-green-500 text-3xl" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center">
|
|
<div>
|
|
<div className="text-2xl font-bold text-cyan-500">
|
|
{new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
notation: 'compact'
|
|
}).format(metriques.caTotal)}
|
|
</div>
|
|
<div className="text-color-secondary">CA généré</div>
|
|
</div>
|
|
<i className="pi pi-euro text-cyan-500 text-3xl" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center">
|
|
<div>
|
|
<div className="text-2xl font-bold text-orange-500">{metriques.relancesEnvoyees}</div>
|
|
<div className="text-color-secondary">Relances envoyées</div>
|
|
</div>
|
|
<i className="pi pi-exclamation-triangle text-orange-500 text-3xl" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-12">
|
|
<Divider />
|
|
</div>
|
|
|
|
{/* Tableau historique */}
|
|
<div className="col-12">
|
|
<Card>
|
|
<DataTable
|
|
value={activites}
|
|
paginator
|
|
rows={20}
|
|
rowsPerPageOptions={[10, 20, 50]}
|
|
className="datatable-responsive"
|
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
|
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} activités"
|
|
globalFilter={globalFilter}
|
|
emptyMessage="Aucune activité trouvée."
|
|
header={header}
|
|
responsiveLayout="scroll"
|
|
loading={loading}
|
|
sortField="date"
|
|
sortOrder={-1}
|
|
>
|
|
<Column
|
|
field="date"
|
|
header="Date & Heure"
|
|
body={dateBodyTemplate}
|
|
sortable
|
|
style={{ minWidth: '120px' }}
|
|
/>
|
|
|
|
<Column
|
|
field="type"
|
|
header="Type d'activité"
|
|
body={typeActiviteBodyTemplate}
|
|
sortable
|
|
style={{ minWidth: '180px' }}
|
|
/>
|
|
|
|
<Column
|
|
header="Client"
|
|
body={clientBodyTemplate}
|
|
style={{ minWidth: '200px' }}
|
|
/>
|
|
|
|
<Column
|
|
field="description"
|
|
header="Description"
|
|
style={{ minWidth: '300px' }}
|
|
/>
|
|
|
|
<Column
|
|
field="montant"
|
|
header="Montant"
|
|
body={montantBodyTemplate}
|
|
sortable
|
|
style={{ minWidth: '120px' }}
|
|
/>
|
|
|
|
<Column
|
|
field="utilisateur"
|
|
header="Utilisateur"
|
|
body={utilisateurBodyTemplate}
|
|
style={{ minWidth: '180px' }}
|
|
/>
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default HistoriqueClientsPage; |