'use client'; /** * Page principale de suivi budgétaire * Tableau de bord des dépenses réelles vs budget prévu avec analyse des écarts */ import React, { useState, useRef, useEffect, useContext } from 'react'; import { LayoutContext } from '../../../../layout/context/layoutcontext'; import { Card } from 'primereact/card'; import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; import { Button } from 'primereact/button'; import { Tag } from 'primereact/tag'; import { ProgressBar } from 'primereact/progressbar'; import { Toolbar } from 'primereact/toolbar'; import { InputText } from 'primereact/inputtext'; import { Dropdown } from 'primereact/dropdown'; import { Toast } from 'primereact/toast'; import { Chart } from 'primereact/chart'; import { TabView, TabPanel } from 'primereact/tabview'; import { Panel } from 'primereact/panel'; import { Divider } from 'primereact/divider'; import BudgetExecutionDialog from '../../../../components/phases/BudgetExecutionDialog'; interface SuiviBudget { id: string; chantierNom: string; client: string; budgetTotal: number; depenseReelle: number; ecart: number; ecartPourcentage: number; avancementTravaux: number; nombrePhases: number; phasesTerminees: number; statut: 'CONFORME' | 'ALERTE' | 'DEPASSEMENT' | 'CRITIQUE'; tendance: 'STABLE' | 'AMELIORATION' | 'DETERIORATION'; responsable: string; derniereMiseAJour: string; alertes: number; prochainJalon: string; } const BudgetSuiviPage = () => { const { layoutConfig, setLayoutConfig, layoutState, setLayoutState } = useContext(LayoutContext); const toast = useRef(null); // États const [budgets, setBudgets] = useState([]); const [loading, setLoading] = useState(true); const [globalFilter, setGlobalFilter] = useState(''); const [selectedBudget, setSelectedBudget] = useState(null); const [showExecutionDialog, setShowExecutionDialog] = useState(false); const [activeIndex, setActiveIndex] = useState(0); // Filtres const [statutFilter, setStatutFilter] = useState(''); const [tendanceFilter, setTendanceFilter] = useState(''); // Options pour les filtres const statutOptions = [ { label: 'Tous les statuts', value: '' }, { label: 'Conforme', value: 'CONFORME' }, { label: 'Alerte', value: 'ALERTE' }, { label: 'Dépassement', value: 'DEPASSEMENT' }, { label: 'Critique', value: 'CRITIQUE' } ]; const tendanceOptions = [ { label: 'Toutes tendances', value: '' }, { label: 'Stable', value: 'STABLE' }, { label: 'Amélioration', value: 'AMELIORATION' }, { label: 'Détérioration', value: 'DETERIORATION' } ]; // Charger les données useEffect(() => { loadSuiviBudgets(); }, []); const loadSuiviBudgets = async () => { try { setLoading(true); // Simuler des données de suivi budgétaire const budgetsSimules: SuiviBudget[] = [ { id: '1', chantierNom: 'Villa Moderne Bordeaux', client: 'Jean Dupont', budgetTotal: 250000, depenseReelle: 180000, ecart: -70000, ecartPourcentage: -28, avancementTravaux: 65, nombrePhases: 8, phasesTerminees: 5, statut: 'CONFORME', tendance: 'STABLE', responsable: 'Marie Martin', derniereMiseAJour: '2025-01-30', alertes: 0, prochainJalon: 'Finitions - 15/02/2025' }, { id: '2', chantierNom: 'Extension Maison Lyon', client: 'Sophie Lambert', budgetTotal: 120000, depenseReelle: 135000, ecart: 15000, ecartPourcentage: 12.5, avancementTravaux: 85, nombrePhases: 5, phasesTerminees: 4, statut: 'DEPASSEMENT', tendance: 'DETERIORATION', responsable: 'Pierre Dubois', derniereMiseAJour: '2025-01-29', alertes: 3, prochainJalon: 'Réception - 28/02/2025' }, { id: '3', chantierNom: 'Rénovation Appartement Paris', client: 'Michel Robert', budgetTotal: 85000, depenseReelle: 92000, ecart: 7000, ecartPourcentage: 8.2, avancementTravaux: 75, nombrePhases: 6, phasesTerminees: 4, statut: 'ALERTE', tendance: 'DETERIORATION', responsable: 'Anne Legrand', derniereMiseAJour: '2025-01-28', alertes: 1, prochainJalon: 'Électricité - 10/02/2025' }, { id: '4', chantierNom: 'Construction Bureaux Toulouse', client: 'Entreprise ABC', budgetTotal: 450000, depenseReelle: 520000, ecart: 70000, ecartPourcentage: 15.6, avancementTravaux: 90, nombrePhases: 12, phasesTerminees: 10, statut: 'CRITIQUE', tendance: 'DETERIORATION', responsable: 'Paul Moreau', derniereMiseAJour: '2025-01-30', alertes: 5, prochainJalon: 'Livraison - 05/02/2025' } ]; setBudgets(budgetsSimules); } catch (error) { console.error('Erreur lors du chargement du suivi budgétaire:', error); toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Impossible de charger le suivi budgétaire', life: 3000 }); } finally { setLoading(false); } }; // Templates pour le DataTable const statutTemplate = (rowData: SuiviBudget) => { const severityMap = { 'CONFORME': 'success', 'ALERTE': 'warning', 'DEPASSEMENT': 'danger', 'CRITIQUE': 'danger' } as const; const iconMap = { 'CONFORME': 'pi-check-circle', 'ALERTE': 'pi-exclamation-triangle', 'DEPASSEMENT': 'pi-times-circle', 'CRITIQUE': 'pi-ban' }; return ( ); }; const tendanceTemplate = (rowData: SuiviBudget) => { const iconMap = { 'STABLE': 'pi-minus', 'AMELIORATION': 'pi-arrow-up', 'DETERIORATION': 'pi-arrow-down' }; const colorMap = { 'STABLE': 'text-blue-500', 'AMELIORATION': 'text-green-500', 'DETERIORATION': 'text-red-500' }; return (
{rowData.tendance}
); }; const ecartTemplate = (rowData: SuiviBudget) => { const isPositif = rowData.ecart > 0; const couleur = isPositif ? 'text-red-500' : 'text-green-500'; const icone = isPositif ? 'pi-arrow-up' : 'pi-arrow-down'; return (
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', signDisplay: 'always' }).format(rowData.ecart)}
{rowData.ecartPourcentage > 0 ? '+' : ''}{rowData.ecartPourcentage.toFixed(1)}%
); }; const budgetTemplate = (rowData: SuiviBudget) => { const pourcentageConsomme = (rowData.depenseReelle / rowData.budgetTotal) * 100; const couleurBarre = pourcentageConsomme > 100 ? '#dc3545' : pourcentageConsomme > 80 ? '#ffc107' : '#22c55e'; return (
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.depenseReelle)} / {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.budgetTotal)}
{pourcentageConsomme.toFixed(0)}% consommé
); }; const avancementTemplate = (rowData: SuiviBudget) => { // Calculer l'efficacité budgétaire (avancement vs consommation budget) const consommationBudget = (rowData.depenseReelle / rowData.budgetTotal) * 100; const efficacite = rowData.avancementTravaux - consommationBudget; const couleurEfficacite = efficacite > 0 ? 'text-green-500' : efficacite < -10 ? 'text-red-500' : 'text-orange-500'; return (
Phases {rowData.phasesTerminees}/{rowData.nombrePhases}
{rowData.avancementTravaux}% avancement {efficacite > 0 ? '+' : ''}{efficacite.toFixed(0)}% eff.
); }; const alertesTemplate = (rowData: SuiviBudget) => { if (rowData.alertes === 0) { return ; } const severity = rowData.alertes > 3 ? 'danger' : rowData.alertes > 1 ? 'warning' : 'info'; return ; }; const actionsTemplate = (rowData: SuiviBudget) => { return (
); }; // Gérer la sauvegarde de l'exécution budgétaire const handleSaveBudgetExecution = async (executionData: any) => { if (!selectedBudget) return; try { // Mettre à jour les données de suivi setBudgets(budgets.map(b => b.id === selectedBudget.id ? { ...b, depenseReelle: executionData.coutTotal, ecart: executionData.coutTotal - b.budgetTotal, ecartPourcentage: ((executionData.coutTotal - b.budgetTotal) / b.budgetTotal) * 100, statut: executionData.ecartPourcentage > 15 ? 'CRITIQUE' : executionData.ecartPourcentage > 10 ? 'DEPASSEMENT' : executionData.ecartPourcentage > 5 ? 'ALERTE' : 'CONFORME', derniereMiseAJour: new Date().toISOString().split('T')[0] } as SuiviBudget : b )); toast.current?.show({ severity: 'success', summary: 'Suivi mis à jour', detail: `Le suivi budgétaire du chantier "${selectedBudget.chantierNom}" a été actualisé`, life: 3000 }); } catch (error) { toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Impossible de mettre à jour le suivi budgétaire', life: 3000 }); } }; // Filtrer les données const filteredBudgets = budgets.filter(budget => { let matches = true; if (globalFilter) { const search = globalFilter.toLowerCase(); matches = matches && ( budget.chantierNom.toLowerCase().includes(search) || budget.client.toLowerCase().includes(search) || budget.responsable.toLowerCase().includes(search) ); } if (statutFilter) { matches = matches && budget.statut === statutFilter; } if (tendanceFilter) { matches = matches && budget.tendance === tendanceFilter; } return matches; }); // Statistiques const stats = { totalChantiers: budgets.length, chantiersConformes: budgets.filter(b => b.statut === 'CONFORME').length, chantiersAlerte: budgets.filter(b => b.statut === 'ALERTE').length, chantiersDepassement: budgets.filter(b => b.statut === 'DEPASSEMENT' || b.statut === 'CRITIQUE').length, budgetTotalPrevu: budgets.reduce((sum, b) => sum + b.budgetTotal, 0), depenseTotaleReelle: budgets.reduce((sum, b) => sum + b.depenseReelle, 0), ecartTotalAbsolu: budgets.reduce((sum, b) => sum + Math.abs(b.ecart), 0), alertesTotales: budgets.reduce((sum, b) => sum + b.alertes, 0) }; const ecartTotalGlobal = stats.depenseTotaleReelle - stats.budgetTotalPrevu; const ecartPourcentageGlobal = stats.budgetTotalPrevu > 0 ? (ecartTotalGlobal / stats.budgetTotalPrevu) * 100 : 0; // Données pour les graphiques const repartitionData = { labels: ['Conforme', 'Alerte', 'Dépassement'], datasets: [ { data: [stats.chantiersConformes, stats.chantiersAlerte, stats.chantiersDepassement], backgroundColor: ['#22c55e', '#ffc107', '#dc3545'], borderWidth: 0 } ] }; const evolutionData = { labels: ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun'], datasets: [ { label: 'Budget prévu', data: [150000, 180000, 220000, 280000, 350000, 420000], borderColor: '#007ad9', backgroundColor: 'rgba(0, 122, 217, 0.1)', tension: 0.4 }, { label: 'Dépenses réelles', data: [145000, 190000, 240000, 310000, 380000, 460000], borderColor: '#dc3545', backgroundColor: 'rgba(220, 53, 69, 0.1)', tension: 0.4 } ] }; // Template de la toolbar const toolbarStartTemplate = (
Suivi Budgétaire
{stats.alertesTotales > 0 && ( )}
); const toolbarEndTemplate = (
setGlobalFilter(e.target.value)} placeholder="Rechercher..." /> setStatutFilter(e.value)} placeholder="Statut" className="w-10rem" /> setTendanceFilter(e.value)} placeholder="Tendance" className="w-10rem" />
); return (
setActiveIndex(e.index)}> {/* KPI principaux */}
Budget Total
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0 }).format(stats.budgetTotalPrevu)}
Dépenses Réelles
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0 }).format(stats.depenseTotaleReelle)}
0 ? "bg-red-50" : "bg-green-50"}>
0 ? 'text-red-600' : 'text-green-600'}`}> Écart Global
0 ? 'text-red-900' : 'text-green-900'}`}> {ecartTotalGlobal > 0 ? '+' : ''}{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0 }).format(ecartTotalGlobal)}
0 ? 'text-red-600' : 'text-green-600'}> {ecartPourcentageGlobal > 0 ? '+' : ''}{ecartPourcentageGlobal.toFixed(1)}%
0 ? 'bg-red-100' : 'bg-green-100'}`} style={{ width: '2.5rem', height: '2.5rem' }}> 0 ? 'pi-arrow-up text-red-500' : 'pi-arrow-down text-green-500'} text-xl`}>
Alertes Actives
{stats.alertesTotales}
notifications
{/* Graphiques */}
{/* Actions prioritaires */}

{stats.chantiersDepassement} chantiers en dépassement budgétaire.

{stats.chantiersDepassement > 0 && (

{stats.chantiersAlerte} chantiers nécessitent une surveillance.

{stats.chantiersAlerte > 0 && (

{stats.chantiersConformes} chantiers respectent leur budget.

{/* Dialog d'exécution budgétaire */} setShowExecutionDialog(false)} phase={selectedBudget ? { id: parseInt(selectedBudget.id), nom: selectedBudget.chantierNom, budgetPrevu: selectedBudget.budgetTotal } as any : null} onSave={handleSaveBudgetExecution} />
); }; export default BudgetSuiviPage;