- 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>
749 lines
34 KiB
TypeScript
749 lines
34 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
/**
|
|
* 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<Toast>(null);
|
|
|
|
// États
|
|
const [budgets, setBudgets] = useState<SuiviBudget[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [globalFilter, setGlobalFilter] = useState('');
|
|
const [selectedBudget, setSelectedBudget] = useState<SuiviBudget | null>(null);
|
|
const [showExecutionDialog, setShowExecutionDialog] = useState(false);
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
|
|
// Filtres
|
|
const [statutFilter, setStatutFilter] = useState<string>('');
|
|
const [tendanceFilter, setTendanceFilter] = useState<string>('');
|
|
|
|
// 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 (
|
|
<Tag
|
|
value={rowData.statut}
|
|
severity={severityMap[rowData.statut]}
|
|
icon={`pi ${iconMap[rowData.statut]}`}
|
|
/>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<div className="flex align-items-center gap-2">
|
|
<i className={`pi ${iconMap[rowData.tendance]} ${colorMap[rowData.tendance]}`}></i>
|
|
<span className={colorMap[rowData.tendance]}>{rowData.tendance}</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<div className="text-right">
|
|
<div className={`font-semibold ${couleur} flex align-items-center justify-content-end gap-1`}>
|
|
<i className={`pi ${icone} text-xs`}></i>
|
|
{new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
signDisplay: 'always'
|
|
}).format(rowData.ecart)}
|
|
</div>
|
|
<small className={`${couleur}`}>
|
|
{rowData.ecartPourcentage > 0 ? '+' : ''}{rowData.ecartPourcentage.toFixed(1)}%
|
|
</small>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const budgetTemplate = (rowData: SuiviBudget) => {
|
|
const pourcentageConsomme = (rowData.depenseReelle / rowData.budgetTotal) * 100;
|
|
const couleurBarre = pourcentageConsomme > 100 ? '#dc3545' :
|
|
pourcentageConsomme > 80 ? '#ffc107' : '#22c55e';
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex justify-content-between mb-1">
|
|
<span className="text-sm font-semibold">
|
|
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.depenseReelle)}
|
|
</span>
|
|
<span className="text-sm text-color-secondary">
|
|
/ {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.budgetTotal)}
|
|
</span>
|
|
</div>
|
|
<ProgressBar
|
|
value={Math.min(pourcentageConsomme, 100)}
|
|
className="w-full"
|
|
style={{ height: '8px' }}
|
|
color={couleurBarre}
|
|
/>
|
|
<small className="text-color-secondary">
|
|
{pourcentageConsomme.toFixed(0)}% consommé
|
|
</small>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<div>
|
|
<div className="flex justify-content-between mb-1">
|
|
<span className="text-sm">Phases</span>
|
|
<span className="text-sm font-semibold">{rowData.phasesTerminees}/{rowData.nombrePhases}</span>
|
|
</div>
|
|
<ProgressBar
|
|
value={rowData.avancementTravaux}
|
|
className="w-full mb-1"
|
|
style={{ height: '8px' }}
|
|
/>
|
|
<div className="flex justify-content-between">
|
|
<small className="text-color-secondary">{rowData.avancementTravaux}% avancement</small>
|
|
<small className={`font-semibold ${couleurEfficacite}`}>
|
|
{efficacite > 0 ? '+' : ''}{efficacite.toFixed(0)}% eff.
|
|
</small>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const alertesTemplate = (rowData: SuiviBudget) => {
|
|
if (rowData.alertes === 0) {
|
|
return <Tag value="0" severity="success" icon="pi pi-check" />;
|
|
}
|
|
|
|
const severity = rowData.alertes > 3 ? 'danger' : rowData.alertes > 1 ? 'warning' : 'info';
|
|
return <Tag value={rowData.alertes.toString()} severity={severity} icon="pi pi-bell" />;
|
|
};
|
|
|
|
const actionsTemplate = (rowData: SuiviBudget) => {
|
|
return (
|
|
<div className="flex gap-1">
|
|
<Button
|
|
icon="pi pi-chart-line"
|
|
className="p-button-outlined p-button-sm"
|
|
tooltip="Suivi détaillé"
|
|
onClick={() => {
|
|
setSelectedBudget(rowData);
|
|
setShowExecutionDialog(true);
|
|
}}
|
|
/>
|
|
<Button
|
|
icon="pi pi-eye"
|
|
className="p-button-outlined p-button-sm"
|
|
tooltip="Voir le chantier"
|
|
onClick={() => {
|
|
window.open(`/chantiers/${rowData.id}`, '_blank');
|
|
}}
|
|
/>
|
|
<Button
|
|
icon="pi pi-bell"
|
|
className={`p-button-sm ${rowData.alertes > 0 ? 'p-button-warning' : 'p-button-outlined'}`}
|
|
tooltip={`${rowData.alertes} alerte(s)`}
|
|
onClick={() => {
|
|
// Ouvrir le panneau des alertes
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Alertes',
|
|
detail: `${rowData.alertes} alerte(s) active(s) pour ce chantier`,
|
|
life: 3000
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 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 = (
|
|
<div className="flex align-items-center gap-2">
|
|
<h5 className="m-0">Suivi Budgétaire</h5>
|
|
<Tag value={`${stats.totalChantiers} chantiers`} className="ml-2" />
|
|
{stats.alertesTotales > 0 && (
|
|
<Tag value={`${stats.alertesTotales} alertes`} severity="warning" icon="pi pi-bell" />
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const toolbarEndTemplate = (
|
|
<div className="flex gap-2">
|
|
<span className="p-input-icon-left">
|
|
<i className="pi pi-search" />
|
|
<InputText
|
|
value={globalFilter}
|
|
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
placeholder="Rechercher..."
|
|
/>
|
|
</span>
|
|
<Dropdown
|
|
value={statutFilter}
|
|
options={statutOptions}
|
|
onChange={(e) => setStatutFilter(e.value)}
|
|
placeholder="Statut"
|
|
className="w-10rem"
|
|
/>
|
|
<Dropdown
|
|
value={tendanceFilter}
|
|
options={tendanceOptions}
|
|
onChange={(e) => setTendanceFilter(e.value)}
|
|
placeholder="Tendance"
|
|
className="w-10rem"
|
|
/>
|
|
<Button
|
|
label="Nouvelle dépense"
|
|
icon="pi pi-plus"
|
|
className="p-button-success"
|
|
onClick={() => {
|
|
window.location.href = '/budget/suivi/nouvelle-depense';
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="grid">
|
|
<Toast ref={toast} />
|
|
|
|
<div className="col-12">
|
|
<Card>
|
|
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
|
|
<TabPanel header="Tableau de bord" leftIcon="pi pi-chart-bar">
|
|
{/* KPI principaux */}
|
|
<div className="grid mb-4">
|
|
<div className="col-12 lg:col-3">
|
|
<Card className="bg-blue-50">
|
|
<div className="flex justify-content-between">
|
|
<div>
|
|
<span className="block text-blue-600 font-medium mb-3">Budget Total</span>
|
|
<div className="text-blue-900 font-bold text-xl">
|
|
{new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
minimumFractionDigits: 0
|
|
}).format(stats.budgetTotalPrevu)}
|
|
</div>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-blue-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-euro text-blue-500 text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
<div className="col-12 lg:col-3">
|
|
<Card className="bg-purple-50">
|
|
<div className="flex justify-content-between">
|
|
<div>
|
|
<span className="block text-purple-600 font-medium mb-3">Dépenses Réelles</span>
|
|
<div className="text-purple-900 font-bold text-xl">
|
|
{new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
minimumFractionDigits: 0
|
|
}).format(stats.depenseTotaleReelle)}
|
|
</div>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-purple-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-credit-card text-purple-500 text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
<div className="col-12 lg:col-3">
|
|
<Card className={ecartTotalGlobal > 0 ? "bg-red-50" : "bg-green-50"}>
|
|
<div className="flex justify-content-between">
|
|
<div>
|
|
<span className={`block font-medium mb-3 ${ecartTotalGlobal > 0 ? 'text-red-600' : 'text-green-600'}`}>
|
|
Écart Global
|
|
</span>
|
|
<div className={`font-bold text-xl ${ecartTotalGlobal > 0 ? 'text-red-900' : 'text-green-900'}`}>
|
|
{ecartTotalGlobal > 0 ? '+' : ''}{new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
minimumFractionDigits: 0
|
|
}).format(ecartTotalGlobal)}
|
|
</div>
|
|
<small className={ecartTotalGlobal > 0 ? 'text-red-600' : 'text-green-600'}>
|
|
{ecartPourcentageGlobal > 0 ? '+' : ''}{ecartPourcentageGlobal.toFixed(1)}%
|
|
</small>
|
|
</div>
|
|
<div className={`flex align-items-center justify-content-center border-round ${ecartTotalGlobal > 0 ? 'bg-red-100' : 'bg-green-100'}`} style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className={`pi ${ecartTotalGlobal > 0 ? 'pi-arrow-up text-red-500' : 'pi-arrow-down text-green-500'} text-xl`}></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
<div className="col-12 lg:col-3">
|
|
<Card className="bg-orange-50">
|
|
<div className="flex justify-content-between">
|
|
<div>
|
|
<span className="block text-orange-600 font-medium mb-3">Alertes Actives</span>
|
|
<div className="text-orange-900 font-bold text-xl">
|
|
{stats.alertesTotales}
|
|
</div>
|
|
<small className="text-orange-600">notifications</small>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-orange-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-bell text-orange-500 text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Graphiques */}
|
|
<div className="grid mb-4">
|
|
<div className="col-12 lg:col-6">
|
|
<Card title="Répartition par statut budgétaire">
|
|
<Chart type="doughnut" data={repartitionData} style={{ height: '300px' }} />
|
|
</Card>
|
|
</div>
|
|
<div className="col-12 lg:col-6">
|
|
<Card title="Évolution budget vs dépenses">
|
|
<Chart type="line" data={evolutionData} style={{ height: '300px' }} />
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions prioritaires */}
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card title="Actions prioritaires">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-4">
|
|
<Panel header="🚨 Dépassements critiques" toggleable>
|
|
<p>
|
|
<strong>{stats.chantiersDepassement}</strong> chantiers en dépassement budgétaire.
|
|
</p>
|
|
{stats.chantiersDepassement > 0 && (
|
|
<Button
|
|
label="Voir la liste"
|
|
icon="pi pi-exclamation-triangle"
|
|
className="p-button-danger p-button-sm"
|
|
onClick={() => setStatutFilter('DEPASSEMENT')}
|
|
/>
|
|
)}
|
|
</Panel>
|
|
</div>
|
|
<div className="col-12 md:col-4">
|
|
<Panel header="⚠️ Alertes budgétaires" toggleable>
|
|
<p>
|
|
<strong>{stats.chantiersAlerte}</strong> chantiers nécessitent une surveillance.
|
|
</p>
|
|
{stats.chantiersAlerte > 0 && (
|
|
<Button
|
|
label="Voir la liste"
|
|
icon="pi pi-eye"
|
|
className="p-button-warning p-button-sm"
|
|
onClick={() => setStatutFilter('ALERTE')}
|
|
/>
|
|
)}
|
|
</Panel>
|
|
</div>
|
|
<div className="col-12 md:col-4">
|
|
<Panel header="✅ Conformes" toggleable>
|
|
<p>
|
|
<strong>{stats.chantiersConformes}</strong> chantiers respectent leur budget.
|
|
</p>
|
|
<Button
|
|
label="Voir la liste"
|
|
icon="pi pi-check"
|
|
className="p-button-success p-button-sm"
|
|
onClick={() => setStatutFilter('CONFORME')}
|
|
/>
|
|
</Panel>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Suivi détaillé" leftIcon="pi pi-list">
|
|
<Toolbar
|
|
start={toolbarStartTemplate}
|
|
end={toolbarEndTemplate}
|
|
className="mb-4"
|
|
/>
|
|
|
|
<DataTable
|
|
value={filteredBudgets}
|
|
loading={loading}
|
|
paginator
|
|
rows={20}
|
|
emptyMessage="Aucun chantier trouvé"
|
|
className="p-datatable-lg"
|
|
dataKey="id"
|
|
sortMode="multiple"
|
|
>
|
|
<Column field="chantierNom" header="Chantier" sortable style={{ minWidth: '15rem' }} />
|
|
<Column field="client" header="Client" sortable style={{ width: '12rem' }} />
|
|
<Column
|
|
field="budgetTotal"
|
|
header="Budget / Dépenses"
|
|
body={budgetTemplate}
|
|
sortable
|
|
style={{ width: '14rem' }}
|
|
/>
|
|
<Column
|
|
field="ecart"
|
|
header="Écart"
|
|
body={ecartTemplate}
|
|
sortable
|
|
style={{ width: '10rem' }}
|
|
/>
|
|
<Column
|
|
field="avancementTravaux"
|
|
header="Avancement"
|
|
body={avancementTemplate}
|
|
style={{ width: '12rem' }}
|
|
/>
|
|
<Column
|
|
field="statut"
|
|
header="Statut"
|
|
body={statutTemplate}
|
|
sortable
|
|
style={{ width: '10rem' }}
|
|
/>
|
|
<Column
|
|
field="tendance"
|
|
header="Tendance"
|
|
body={tendanceTemplate}
|
|
sortable
|
|
style={{ width: '10rem' }}
|
|
/>
|
|
<Column
|
|
field="alertes"
|
|
header="Alertes"
|
|
body={alertesTemplate}
|
|
sortable
|
|
style={{ width: '8rem' }}
|
|
/>
|
|
<Column field="responsable" header="Responsable" sortable style={{ width: '10rem' }} />
|
|
<Column
|
|
header="Actions"
|
|
body={actionsTemplate}
|
|
style={{ width: '10rem' }}
|
|
/>
|
|
</DataTable>
|
|
</TabPanel>
|
|
</TabView>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Dialog d'exécution budgétaire */}
|
|
<BudgetExecutionDialog
|
|
visible={showExecutionDialog}
|
|
onHide={() => setShowExecutionDialog(false)}
|
|
phase={selectedBudget ? {
|
|
id: parseInt(selectedBudget.id),
|
|
nom: selectedBudget.chantierNom,
|
|
budgetPrevu: selectedBudget.budgetTotal
|
|
} as any : null}
|
|
onSave={handleSaveBudgetExecution}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BudgetSuiviPage; |