- 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>
649 lines
29 KiB
TypeScript
649 lines
29 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { Button } from 'primereact/button';
|
|
import { Calendar } from 'primereact/calendar';
|
|
import { Chart } from 'primereact/chart';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Toast } from 'primereact/toast';
|
|
import { Toolbar } from 'primereact/toolbar';
|
|
import { Dropdown } from 'primereact/dropdown';
|
|
import { TabView, TabPanel } from 'primereact/tabview';
|
|
import { Knob } from 'primereact/knob';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { Tag } from 'primereact/tag';
|
|
|
|
interface CAData {
|
|
periode: string;
|
|
chiffreAffaires: number;
|
|
objectif: number;
|
|
factures: number;
|
|
devis: number;
|
|
croissance: number;
|
|
tauxReussite: number;
|
|
}
|
|
|
|
interface CADetail {
|
|
id: string;
|
|
date: Date;
|
|
client: string;
|
|
chantier: string;
|
|
montant: number;
|
|
type: 'FACTURE' | 'DEVIS_ACCEPTE' | 'AVENANT';
|
|
statut: 'PAYE' | 'EN_ATTENTE' | 'RETARD';
|
|
mode: 'VIREMENT' | 'CHEQUE' | 'ESPECES' | 'CARTE';
|
|
}
|
|
|
|
const ChiffreAffairesPage = () => {
|
|
const [loading, setLoading] = useState(true);
|
|
const [caData, setCaData] = useState<CAData[]>([]);
|
|
const [caDetails, setCaDetails] = useState<CADetail[]>([]);
|
|
const [dateDebut, setDateDebut] = useState<Date>(new Date(new Date().getFullYear(), 0, 1));
|
|
const [dateFin, setDateFin] = useState<Date>(new Date());
|
|
const [selectedPeriod, setSelectedPeriod] = useState('annee');
|
|
const [selectedView, setSelectedView] = useState('mensuel');
|
|
const [globalFilter, setGlobalFilter] = useState('');
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
const toast = useRef<Toast>(null);
|
|
const dt = useRef<DataTable<CADetail[]>>(null);
|
|
|
|
const periodOptions = [
|
|
{ label: 'Cette semaine', value: 'semaine' },
|
|
{ label: 'Ce mois', value: 'mois' },
|
|
{ label: 'Ce trimestre', value: 'trimestre' },
|
|
{ label: 'Cette année', value: 'annee' },
|
|
{ label: 'Personnalisé', value: 'custom' }
|
|
];
|
|
|
|
const viewOptions = [
|
|
{ label: 'Vue Mensuelle', value: 'mensuel' },
|
|
{ label: 'Vue Trimestrielle', value: 'trimestriel' },
|
|
{ label: 'Vue Annuelle', value: 'annuel' }
|
|
];
|
|
|
|
useEffect(() => {
|
|
loadCAData();
|
|
}, [dateDebut, dateFin, selectedView]);
|
|
|
|
const loadCAData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Données mockées
|
|
const mockCAData: CAData[] = [
|
|
{ periode: 'Janvier 2024', chiffreAffaires: 145000, objectif: 150000, factures: 12, devis: 8, croissance: 12.5, tauxReussite: 75 },
|
|
{ periode: 'Février 2024', chiffreAffaires: 165000, objectif: 160000, factures: 15, devis: 10, croissance: 13.8, tauxReussite: 82 },
|
|
{ periode: 'Mars 2024', chiffreAffaires: 180000, objectif: 170000, factures: 18, devis: 12, croissance: 9.1, tauxReussite: 85 },
|
|
{ periode: 'Avril 2024', chiffreAffaires: 175000, objectif: 175000, factures: 16, devis: 11, croissance: -2.8, tauxReussite: 78 },
|
|
{ periode: 'Mai 2024', chiffreAffaires: 195000, objectif: 180000, factures: 20, devis: 14, croissance: 11.4, tauxReussite: 88 },
|
|
{ periode: 'Juin 2024', chiffreAffaires: 210000, objectif: 185000, factures: 22, devis: 16, croissance: 7.7, tauxReussite: 90 }
|
|
];
|
|
|
|
const mockCADetails: CADetail[] = [
|
|
{
|
|
id: '1',
|
|
date: new Date('2024-06-15'),
|
|
client: 'Kouassi Jean',
|
|
chantier: 'Résidence Les Palmiers',
|
|
montant: 25000,
|
|
type: 'FACTURE',
|
|
statut: 'PAYE',
|
|
mode: 'VIREMENT'
|
|
},
|
|
{
|
|
id: '2',
|
|
date: new Date('2024-06-10'),
|
|
client: 'Traoré Fatou',
|
|
chantier: 'Immeuble Commercial',
|
|
montant: 35000,
|
|
type: 'FACTURE',
|
|
statut: 'EN_ATTENTE',
|
|
mode: 'VIREMENT'
|
|
},
|
|
{
|
|
id: '3',
|
|
date: new Date('2024-06-08'),
|
|
client: 'Diabaté Mamadou',
|
|
chantier: 'Villa Moderne',
|
|
montant: 18000,
|
|
type: 'DEVIS_ACCEPTE',
|
|
statut: 'PAYE',
|
|
mode: 'CHEQUE'
|
|
},
|
|
{
|
|
id: '4',
|
|
date: new Date('2024-06-05'),
|
|
client: 'Koné Mariame',
|
|
chantier: 'Rénovation Bureau',
|
|
montant: 12000,
|
|
type: 'FACTURE',
|
|
statut: 'RETARD',
|
|
mode: 'VIREMENT'
|
|
},
|
|
{
|
|
id: '5',
|
|
date: new Date('2024-06-02'),
|
|
client: 'Ouattara Ibrahim',
|
|
chantier: 'Garage Automobile',
|
|
montant: 28000,
|
|
type: 'AVENANT',
|
|
statut: 'PAYE',
|
|
mode: 'VIREMENT'
|
|
}
|
|
];
|
|
|
|
setCaData(mockCAData);
|
|
setCaDetails(mockCADetails);
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible de charger les données du chiffre d\'affaires',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const onPeriodChange = (e: any) => {
|
|
setSelectedPeriod(e.value);
|
|
|
|
const now = new Date();
|
|
let debut = new Date();
|
|
|
|
switch (e.value) {
|
|
case 'semaine':
|
|
debut = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7);
|
|
break;
|
|
case 'mois':
|
|
debut = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
break;
|
|
case 'trimestre':
|
|
debut = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1);
|
|
break;
|
|
case 'annee':
|
|
debut = new Date(now.getFullYear(), 0, 1);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
setDateDebut(debut);
|
|
setDateFin(now);
|
|
};
|
|
|
|
const exportPDF = () => {
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Export PDF',
|
|
detail: 'Génération du rapport PDF...',
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const exportExcel = () => {
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Export Excel',
|
|
detail: 'Génération du rapport Excel...',
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const leftToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex flex-wrap gap-2 align-items-center">
|
|
<Dropdown
|
|
value={selectedPeriod}
|
|
options={periodOptions}
|
|
onChange={onPeriodChange}
|
|
placeholder="Période"
|
|
/>
|
|
<Dropdown
|
|
value={selectedView}
|
|
options={viewOptions}
|
|
onChange={(e) => setSelectedView(e.value)}
|
|
placeholder="Vue"
|
|
/>
|
|
{selectedPeriod === 'custom' && (
|
|
<>
|
|
<Calendar
|
|
value={dateDebut}
|
|
onChange={(e) => setDateDebut(e.value || new Date())}
|
|
dateFormat="dd/mm/yy"
|
|
placeholder="Date début"
|
|
/>
|
|
<Calendar
|
|
value={dateFin}
|
|
onChange={(e) => setDateFin(e.value || new Date())}
|
|
dateFormat="dd/mm/yy"
|
|
placeholder="Date fin"
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const rightToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
label="PDF"
|
|
icon="pi pi-file-pdf"
|
|
severity="danger"
|
|
onClick={exportPDF}
|
|
/>
|
|
<Button
|
|
label="Excel"
|
|
icon="pi pi-file-excel"
|
|
severity="success"
|
|
onClick={exportExcel}
|
|
/>
|
|
<Button
|
|
label="Actualiser"
|
|
icon="pi pi-refresh"
|
|
onClick={loadCAData}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Calculs pour les indicateurs
|
|
const totalCA = caData.reduce((sum, item) => sum + item.chiffreAffaires, 0);
|
|
const totalObjectif = caData.reduce((sum, item) => sum + item.objectif, 0);
|
|
const totalFactures = caData.reduce((sum, item) => sum + item.factures, 0);
|
|
const moyenneCroissance = caData.length > 0 ? caData.reduce((sum, item) => sum + item.croissance, 0) / caData.length : 0;
|
|
const tauxAtteinte = totalObjectif > 0 ? (totalCA / totalObjectif) * 100 : 0;
|
|
|
|
// Données pour les graphiques
|
|
const chartData = {
|
|
labels: caData.map(item => item.periode),
|
|
datasets: [
|
|
{
|
|
label: 'Chiffre d\'affaires',
|
|
data: caData.map(item => item.chiffreAffaires),
|
|
borderColor: '#3B82F6',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
tension: 0.4,
|
|
fill: true
|
|
},
|
|
{
|
|
label: 'Objectif',
|
|
data: caData.map(item => item.objectif),
|
|
borderColor: '#EF4444',
|
|
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
borderDash: [5, 5],
|
|
tension: 0.4,
|
|
fill: false
|
|
}
|
|
]
|
|
};
|
|
|
|
const barChartData = {
|
|
labels: caData.map(item => item.periode),
|
|
datasets: [
|
|
{
|
|
label: 'Factures',
|
|
data: caData.map(item => item.factures),
|
|
backgroundColor: '#10B981',
|
|
borderColor: '#047857',
|
|
borderWidth: 1
|
|
},
|
|
{
|
|
label: 'Devis',
|
|
data: caData.map(item => item.devis),
|
|
backgroundColor: '#F59E0B',
|
|
borderColor: '#D97706',
|
|
borderWidth: 1
|
|
}
|
|
]
|
|
};
|
|
|
|
const chartOptions = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom' as const
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: function(value: any) {
|
|
return new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
minimumFractionDigits: 0
|
|
}).format(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
return new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(amount);
|
|
};
|
|
|
|
const typeBodyTemplate = (rowData: CADetail) => {
|
|
let severity: "success" | "warning" | "danger" | "info" = 'info';
|
|
let label: string = rowData.type;
|
|
|
|
switch (rowData.type) {
|
|
case 'FACTURE':
|
|
severity = 'success';
|
|
label = 'Facture';
|
|
break;
|
|
case 'DEVIS_ACCEPTE':
|
|
severity = 'info';
|
|
label = 'Devis Accepté';
|
|
break;
|
|
case 'AVENANT':
|
|
severity = 'warning';
|
|
label = 'Avenant';
|
|
break;
|
|
}
|
|
|
|
return <Tag value={label} severity={severity} />;
|
|
};
|
|
|
|
const statutBodyTemplate = (rowData: CADetail) => {
|
|
let severity: "success" | "warning" | "danger" = 'success';
|
|
let label: string = rowData.statut;
|
|
|
|
switch (rowData.statut) {
|
|
case 'PAYE':
|
|
severity = 'success';
|
|
label = 'Payé';
|
|
break;
|
|
case 'EN_ATTENTE':
|
|
severity = 'warning';
|
|
label = 'En attente';
|
|
break;
|
|
case 'RETARD':
|
|
severity = 'danger';
|
|
label = 'En retard';
|
|
break;
|
|
}
|
|
|
|
return <Tag value={label} severity={severity} />;
|
|
};
|
|
|
|
const montantBodyTemplate = (rowData: CADetail) => {
|
|
return formatCurrency(rowData.montant);
|
|
};
|
|
|
|
const dateBodyTemplate = (rowData: CADetail) => {
|
|
return rowData.date.toLocaleDateString('fr-FR');
|
|
};
|
|
|
|
const header = (
|
|
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
|
|
<h5 className="m-0">Détail des Transactions</h5>
|
|
<span className="block mt-2 md:mt-0 p-input-icon-left">
|
|
<i className="pi pi-search" />
|
|
<input
|
|
type="search"
|
|
placeholder="Rechercher..."
|
|
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
className="p-inputtext p-component"
|
|
/>
|
|
</span>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card>
|
|
<Toast ref={toast} />
|
|
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
|
|
|
|
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
|
|
<TabPanel header="Vue d'Ensemble" leftIcon="pi pi-chart-line mr-2">
|
|
<div className="grid">
|
|
{/* Indicateurs principaux */}
|
|
<div className="col-12 md:col-3">
|
|
<Card className="text-center">
|
|
<div className="text-6xl text-primary mb-2">
|
|
<i className="pi pi-money-bill"></i>
|
|
</div>
|
|
<div className="text-3xl font-bold text-primary mb-1">
|
|
{formatCurrency(totalCA)}
|
|
</div>
|
|
<div className="text-lg text-color-secondary">
|
|
Chiffre d'Affaires
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card className="text-center">
|
|
<div className="text-6xl text-orange-500 mb-2">
|
|
<i className="pi pi-target"></i>
|
|
</div>
|
|
<div className="text-3xl font-bold text-orange-500 mb-1">
|
|
{formatCurrency(totalObjectif)}
|
|
</div>
|
|
<div className="text-lg text-color-secondary">
|
|
Objectif
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card className="text-center">
|
|
<div className="text-6xl text-green-500 mb-2">
|
|
<i className="pi pi-file"></i>
|
|
</div>
|
|
<div className="text-3xl font-bold text-green-500 mb-1">
|
|
{totalFactures}
|
|
</div>
|
|
<div className="text-lg text-color-secondary">
|
|
Factures
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card className="text-center">
|
|
<div className="text-6xl text-purple-500 mb-2">
|
|
<i className="pi pi-trending-up"></i>
|
|
</div>
|
|
<div className="text-3xl font-bold text-purple-500 mb-1">
|
|
{moyenneCroissance.toFixed(1)}%
|
|
</div>
|
|
<div className="text-lg text-color-secondary">
|
|
Croissance Moyenne
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Taux d'atteinte objectif */}
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Taux d'Atteinte des Objectifs">
|
|
<div className="text-center">
|
|
<Knob
|
|
value={tauxAtteinte}
|
|
size={200}
|
|
valueColor={tauxAtteinte >= 100 ? "#10B981" : tauxAtteinte >= 80 ? "#F59E0B" : "#EF4444"}
|
|
rangeColor="#E5E7EB"
|
|
textColor="#374151"
|
|
/>
|
|
<div className="text-lg text-color-secondary mt-2">
|
|
{tauxAtteinte.toFixed(1)}% de l'objectif atteint
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Performance mensuelle */}
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Performance Mensuelle">
|
|
<div className="grid">
|
|
{caData.slice(-3).map((month, index) => (
|
|
<div key={index} className="col-12">
|
|
<div className="flex justify-content-between align-items-center mb-2">
|
|
<span className="font-semibold">{month.periode}</span>
|
|
<span className={`font-bold ${month.croissance >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
|
{month.croissance >= 0 ? '+' : ''}{month.croissance.toFixed(1)}%
|
|
</span>
|
|
</div>
|
|
<ProgressBar value={(month.chiffreAffaires / month.objectif) * 100} />
|
|
<div className="flex justify-content-between text-sm text-color-secondary mt-1">
|
|
<span>{formatCurrency(month.chiffreAffaires)}</span>
|
|
<span>Obj: {formatCurrency(month.objectif)}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Graphique évolution */}
|
|
<div className="col-12">
|
|
<Card title="Évolution du Chiffre d'Affaires">
|
|
<Chart
|
|
type="line"
|
|
data={chartData}
|
|
options={chartOptions}
|
|
style={{ height: '400px' }}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Graphique factures/devis */}
|
|
<div className="col-12">
|
|
<Card title="Volume Factures vs Devis">
|
|
<Chart
|
|
type="bar"
|
|
data={barChartData}
|
|
options={chartOptions}
|
|
style={{ height: '400px' }}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Détail Transactions" leftIcon="pi pi-list mr-2">
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card>
|
|
<DataTable
|
|
ref={dt}
|
|
value={caDetails}
|
|
paginator
|
|
rows={10}
|
|
rowsPerPageOptions={[5, 10, 25]}
|
|
className="datatable-responsive"
|
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
|
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} transactions"
|
|
globalFilter={globalFilter}
|
|
emptyMessage="Aucune transaction trouvée."
|
|
header={header}
|
|
loading={loading}
|
|
>
|
|
<Column field="date" header="Date" body={dateBodyTemplate} sortable />
|
|
<Column field="client" header="Client" sortable />
|
|
<Column field="chantier" header="Chantier" sortable />
|
|
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
|
|
<Column field="montant" header="Montant" body={montantBodyTemplate} sortable />
|
|
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
|
|
<Column field="mode" header="Mode Paiement" sortable />
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Analyse Comparative" leftIcon="pi pi-chart-bar mr-2">
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card title="Comparaison Périodique">
|
|
<DataTable
|
|
value={caData}
|
|
loading={loading}
|
|
emptyMessage="Aucune donnée"
|
|
>
|
|
<Column field="periode" header="Période" sortable />
|
|
<Column
|
|
field="chiffreAffaires"
|
|
header="CA Réalisé"
|
|
body={(rowData) => formatCurrency(rowData.chiffreAffaires)}
|
|
sortable
|
|
/>
|
|
<Column
|
|
field="objectif"
|
|
header="Objectif"
|
|
body={(rowData) => formatCurrency(rowData.objectif)}
|
|
sortable
|
|
/>
|
|
<Column
|
|
field="ecart"
|
|
header="Écart"
|
|
body={(rowData) => {
|
|
const ecart = rowData.chiffreAffaires - rowData.objectif;
|
|
return (
|
|
<span className={ecart >= 0 ? 'text-green-500' : 'text-red-500'}>
|
|
{formatCurrency(ecart)}
|
|
</span>
|
|
);
|
|
}}
|
|
sortable
|
|
/>
|
|
<Column
|
|
field="tauxAtteinte"
|
|
header="% Objectif"
|
|
body={(rowData) => {
|
|
const taux = (rowData.chiffreAffaires / rowData.objectif) * 100;
|
|
return (
|
|
<span className={taux >= 100 ? 'text-green-500' : taux >= 80 ? 'text-orange-500' : 'text-red-500'}>
|
|
{taux.toFixed(1)}%
|
|
</span>
|
|
);
|
|
}}
|
|
sortable
|
|
/>
|
|
<Column
|
|
field="croissance"
|
|
header="Croissance"
|
|
body={(rowData) => (
|
|
<span className={rowData.croissance >= 0 ? 'text-green-500' : 'text-red-500'}>
|
|
{rowData.croissance >= 0 ? '+' : ''}{rowData.croissance.toFixed(1)}%
|
|
</span>
|
|
)}
|
|
sortable
|
|
/>
|
|
<Column
|
|
field="tauxReussite"
|
|
header="Taux Réussite"
|
|
body={(rowData) => (
|
|
<ProgressBar value={rowData.tauxReussite} showValue={false} />
|
|
)}
|
|
/>
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
</TabView>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ChiffreAffairesPage; |