Files
btpxpress-frontend/app/(main)/rapports/ca/page.tsx
2025-10-01 01:39:07 +00:00

647 lines
29 KiB
TypeScript

'use client';
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 = 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 = 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;