Initial commit
This commit is contained in:
397
app/(main)/clients/historique/page.tsx
Normal file
397
app/(main)/clients/historique/page.tsx
Normal file
@@ -0,0 +1,397 @@
|
||||
'use client';
|
||||
|
||||
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} />
|
||||
</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;
|
||||
Reference in New Issue
Block a user