Initial commit

This commit is contained in:
dahoud
2025-10-01 01:39:07 +00:00
commit b430bf3b96
826 changed files with 255287 additions and 0 deletions

View File

@@ -0,0 +1,625 @@
'use client';
/**
* Page principale de planification budgétaire
* Vue d'ensemble de tous les budgets avec possibilité de planification détaillée
*/
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 { Calendar } from 'primereact/calendar';
import { Toast } from 'primereact/toast';
import { Chart } from 'primereact/chart';
import { Divider } from 'primereact/divider';
import { TabView, TabPanel } from 'primereact/tabview';
import { Panel } from 'primereact/panel';
import BudgetPlanningDialog from '../../../../components/phases/BudgetPlanningDialog';
interface BudgetChantier {
id: string;
chantierNom: string;
client: string;
dateDebut: string;
dateFin: string;
budgetTotal: number;
budgetPlanifie: number;
avancementPlanification: number;
nombrePhases: number;
nombrePhasesPlanifiees: number;
statut: 'NON_PLANIFIE' | 'EN_COURS_PLANIFICATION' | 'PLANIFIE' | 'VALIDE';
priorite: 'HAUTE' | 'MOYENNE' | 'BASSE';
responsable: string;
derniereMaj: string;
}
const BudgetPlanificationPage = () => {
const { layoutConfig, setLayoutConfig, layoutState, setLayoutState } = useContext(LayoutContext);
const toast = useRef<Toast>(null);
// États
const [budgets, setBudgets] = useState<BudgetChantier[]>([]);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [selectedBudget, setSelectedBudget] = useState<BudgetChantier | null>(null);
const [showPlanningDialog, setShowPlanningDialog] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
// Filtres
const [statusFilter, setStatusFilter] = useState<string>('');
const [prioriteFilter, setPrioriteFilter] = useState<string>('');
const [dateFilter, setDateFilter] = useState<Date | null>(null);
// Options pour les filtres
const statusOptions = [
{ label: 'Tous les statuts', value: '' },
{ label: 'Non planifié', value: 'NON_PLANIFIE' },
{ label: 'En cours', value: 'EN_COURS_PLANIFICATION' },
{ label: 'Planifié', value: 'PLANIFIE' },
{ label: 'Validé', value: 'VALIDE' }
];
const prioriteOptions = [
{ label: 'Toutes priorités', value: '' },
{ label: 'Haute', value: 'HAUTE' },
{ label: 'Moyenne', value: 'MOYENNE' },
{ label: 'Basse', value: 'BASSE' }
];
// Charger les données
useEffect(() => {
loadBudgets();
}, []);
const loadBudgets = async () => {
try {
setLoading(true);
// Simuler des données de budget (en production, ceci viendrait de l'API)
const budgetsSimules: BudgetChantier[] = [
{
id: '1',
chantierNom: 'Villa Moderne Bordeaux',
client: 'Jean Dupont',
dateDebut: '2025-03-01',
dateFin: '2025-08-30',
budgetTotal: 250000,
budgetPlanifie: 180000,
avancementPlanification: 72,
nombrePhases: 8,
nombrePhasesPlanifiees: 6,
statut: 'EN_COURS_PLANIFICATION',
priorite: 'HAUTE',
responsable: 'Marie Martin',
derniereMaj: '2025-01-28'
},
{
id: '2',
chantierNom: 'Extension Maison Lyon',
client: 'Sophie Lambert',
dateDebut: '2025-02-15',
dateFin: '2025-06-15',
budgetTotal: 120000,
budgetPlanifie: 120000,
avancementPlanification: 100,
nombrePhases: 5,
nombrePhasesPlanifiees: 5,
statut: 'PLANIFIE',
priorite: 'MOYENNE',
responsable: 'Pierre Dubois',
derniereMaj: '2025-01-25'
},
{
id: '3',
chantierNom: 'Rénovation Appartement Paris',
client: 'Michel Robert',
dateDebut: '2025-04-01',
dateFin: '2025-07-31',
budgetTotal: 85000,
budgetPlanifie: 0,
avancementPlanification: 0,
nombrePhases: 6,
nombrePhasesPlanifiees: 0,
statut: 'NON_PLANIFIE',
priorite: 'HAUTE',
responsable: 'Anne Legrand',
derniereMaj: '2025-01-20'
}
];
setBudgets(budgetsSimules);
} catch (error) {
console.error('Erreur lors du chargement des budgets:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les budgets',
life: 3000
});
} finally {
setLoading(false);
}
};
// Templates pour le DataTable
const statutTemplate = (rowData: BudgetChantier) => {
const severityMap = {
'NON_PLANIFIE': 'danger',
'EN_COURS_PLANIFICATION': 'warning',
'PLANIFIE': 'info',
'VALIDE': 'success'
} as const;
const labelMap = {
'NON_PLANIFIE': 'Non planifié',
'EN_COURS_PLANIFICATION': 'En cours',
'PLANIFIE': 'Planifié',
'VALIDE': 'Validé'
};
return <Tag value={labelMap[rowData.statut]} severity={severityMap[rowData.statut]} />;
};
const prioriteTemplate = (rowData: BudgetChantier) => {
const severityMap = {
'HAUTE': 'danger',
'MOYENNE': 'warning',
'BASSE': 'info'
} as const;
return <Tag value={rowData.priorite} severity={severityMap[rowData.priorite]} />;
};
const budgetTemplate = (rowData: BudgetChantier) => {
const pourcentagePlanifie = rowData.budgetTotal > 0 ? (rowData.budgetPlanifie / rowData.budgetTotal) * 100 : 0;
return (
<div>
<div className="flex justify-content-between mb-1">
<span className="text-sm text-color-secondary">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.budgetPlanifie)}
</span>
<span className="text-sm text-color-secondary">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.budgetTotal)}
</span>
</div>
<ProgressBar
value={pourcentagePlanifie}
className="w-full"
style={{ height: '8px' }}
color={pourcentagePlanifie < 50 ? '#dc3545' : pourcentagePlanifie < 80 ? '#ffc107' : '#22c55e'}
/>
<small className="text-color-secondary">{pourcentagePlanifie.toFixed(0)}% planifié</small>
</div>
);
};
const avancementTemplate = (rowData: BudgetChantier) => {
return (
<div>
<div className="flex justify-content-between mb-1">
<span className="text-sm">Phases planifiées</span>
<span className="text-sm font-semibold">{rowData.nombrePhasesPlanifiees}/{rowData.nombrePhases}</span>
</div>
<ProgressBar
value={rowData.avancementPlanification}
className="w-full"
style={{ height: '8px' }}
/>
<small className="text-color-secondary">{rowData.avancementPlanification}% avancement</small>
</div>
);
};
const dateTemplate = (rowData: BudgetChantier, field: keyof BudgetChantier) => {
const date = rowData[field] as string;
return new Date(date).toLocaleDateString('fr-FR');
};
const actionsTemplate = (rowData: BudgetChantier) => {
return (
<div className="flex gap-1">
<Button
icon="pi pi-calculator"
className="p-button-outlined p-button-sm"
tooltip="Planifier le budget"
onClick={() => {
setSelectedBudget(rowData);
setShowPlanningDialog(true);
}}
/>
<Button
icon="pi pi-eye"
className="p-button-outlined p-button-sm"
tooltip="Voir détails"
onClick={() => {
// Navigation vers le détail du chantier
window.open(`/chantiers/${rowData.id}`, '_blank');
}}
/>
<Button
icon="pi pi-check"
className="p-button-success p-button-sm"
tooltip="Valider le budget"
disabled={rowData.statut !== 'PLANIFIE'}
onClick={() => validerBudget(rowData.id)}
/>
</div>
);
};
// Fonctions d'action
const validerBudget = async (budgetId: string) => {
try {
// Appel API pour valider le budget
setBudgets(budgets.map(b =>
b.id === budgetId ? { ...b, statut: 'VALIDE' as const } : b
));
toast.current?.show({
severity: 'success',
summary: 'Budget validé',
detail: 'Le budget a été validé avec succès',
life: 3000
});
} catch (error) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de valider le budget',
life: 3000
});
}
};
const handleSaveBudgetPlanning = async (budgetData: any) => {
if (!selectedBudget) return;
try {
// Mettre à jour le budget avec les nouvelles données
setBudgets(budgets.map(b =>
b.id === selectedBudget.id
? {
...b,
budgetPlanifie: budgetData.prixVenteCalcule,
statut: 'PLANIFIE' as const,
avancementPlanification: 100,
derniereMaj: new Date().toISOString().split('T')[0]
}
: b
));
toast.current?.show({
severity: 'success',
summary: 'Budget planifié',
detail: `Le budget du chantier "${selectedBudget.chantierNom}" a été mis à jour`,
life: 3000
});
} catch (error) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de sauvegarder la planification',
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 (statusFilter) {
matches = matches && budget.statut === statusFilter;
}
if (prioriteFilter) {
matches = matches && budget.priorite === prioriteFilter;
}
return matches;
});
// Statistiques
const stats = {
totalBudgets: budgets.length,
budgetsNonPlanifies: budgets.filter(b => b.statut === 'NON_PLANIFIE').length,
budgetsEnCours: budgets.filter(b => b.statut === 'EN_COURS_PLANIFICATION').length,
budgetsPlanifies: budgets.filter(b => b.statut === 'PLANIFIE').length,
budgetsValides: budgets.filter(b => b.statut === 'VALIDE').length,
montantTotalBudget: budgets.reduce((sum, b) => sum + b.budgetTotal, 0),
montantTotalPlanifie: budgets.reduce((sum, b) => sum + b.budgetPlanifie, 0)
};
// Données pour le graphique
const chartData = {
labels: ['Non planifié', 'En cours', 'Planifié', 'Validé'],
datasets: [
{
data: [
stats.budgetsNonPlanifies,
stats.budgetsEnCours,
stats.budgetsPlanifies,
stats.budgetsValides
],
backgroundColor: ['#dc3545', '#ffc107', '#007ad9', '#22c55e'],
borderWidth: 0
}
]
};
// Template de la toolbar
const toolbarStartTemplate = (
<div className="flex align-items-center gap-2">
<h5 className="m-0">Planification Budgétaire</h5>
<Tag value={`${stats.totalBudgets} chantiers`} className="ml-2" />
</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={statusFilter}
options={statusOptions}
onChange={(e) => setStatusFilter(e.value)}
placeholder="Statut"
className="w-10rem"
/>
<Dropdown
value={prioriteFilter}
options={prioriteOptions}
onChange={(e) => setPrioriteFilter(e.value)}
placeholder="Priorité"
className="w-10rem"
/>
<Button
label="Nouveau budget"
icon="pi pi-plus"
className="p-button-success"
onClick={() => {
// Navigation vers création de nouveau budget
window.location.href = '/budget/planification/nouveau';
}}
/>
</div>
);
return (
<div className="grid">
<Toast ref={toast} />
<div className="col-12">
<Card>
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
<TabPanel header="Vue d'ensemble" leftIcon="pi pi-chart-pie">
{/* Statistiques générales */}
<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.montantTotalBudget)}
</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-green-50">
<div className="flex justify-content-between">
<div>
<span className="block text-green-600 font-medium mb-3">Budget Planifié</span>
<div className="text-green-900 font-bold text-xl">
{new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
}).format(stats.montantTotalPlanifie)}
</div>
</div>
<div className="flex align-items-center justify-content-center bg-green-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
<i className="pi pi-check-circle 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">En Attente</span>
<div className="text-orange-900 font-bold text-xl">
{stats.budgetsNonPlanifies + stats.budgetsEnCours}
</div>
<small className="text-orange-600">chantiers</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-clock text-orange-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">Taux Planification</span>
<div className="text-purple-900 font-bold text-xl">
{stats.totalBudgets > 0 ?
(((stats.budgetsPlanifies + stats.budgetsValides) / stats.totalBudgets) * 100).toFixed(0)
: 0}%
</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-chart-pie text-purple-500 text-xl"></i>
</div>
</div>
</Card>
</div>
</div>
{/* Graphique de répartition */}
<div className="grid mb-4">
<div className="col-12 lg:col-6">
<Card title="Répartition par statut">
<Chart type="doughnut" data={chartData} style={{ height: '300px' }} />
</Card>
</div>
<div className="col-12 lg:col-6">
<Card title="Actions prioritaires">
<div className="flex flex-column gap-3">
<Panel header="Budgets à planifier" toggleable>
<p className="m-0">
<strong>{stats.budgetsNonPlanifies}</strong> chantiers n'ont pas encore de budget planifié.
</p>
{stats.budgetsNonPlanifies > 0 && (
<Button
label="Voir la liste"
icon="pi pi-list"
className="mt-2"
onClick={() => setStatusFilter('NON_PLANIFIE')}
/>
)}
</Panel>
<Panel header="Budgets à valider" toggleable>
<p className="m-0">
<strong>{stats.budgetsPlanifies}</strong> budgets sont prêts à être validés.
</p>
{stats.budgetsPlanifies > 0 && (
<Button
label="Voir la liste"
icon="pi pi-list"
className="mt-2"
onClick={() => setStatusFilter('PLANIFIE')}
/>
)}
</Panel>
</div>
</Card>
</div>
</div>
</TabPanel>
<TabPanel header="Liste des budgets" leftIcon="pi pi-list">
<Toolbar
start={toolbarStartTemplate}
end={toolbarEndTemplate}
className="mb-4"
/>
<DataTable
value={filteredBudgets}
loading={loading}
paginator
rows={20}
emptyMessage="Aucun budget 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="dateDebut"
header="Début"
body={(rowData) => dateTemplate(rowData, 'dateDebut')}
sortable
style={{ width: '8rem' }}
/>
<Column
field="dateFin"
header="Fin prévue"
body={(rowData) => dateTemplate(rowData, 'dateFin')}
sortable
style={{ width: '8rem' }}
/>
<Column
field="budgetTotal"
header="Budget"
body={budgetTemplate}
sortable
style={{ width: '12rem' }}
/>
<Column
field="avancementPlanification"
header="Avancement"
body={avancementTemplate}
style={{ width: '12rem' }}
/>
<Column
field="statut"
header="Statut"
body={statutTemplate}
sortable
style={{ width: '8rem' }}
/>
<Column
field="priorite"
header="Priorité"
body={prioriteTemplate}
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 de planification budgétaire */}
<BudgetPlanningDialog
visible={showPlanningDialog}
onHide={() => setShowPlanningDialog(false)}
phase={selectedBudget ? {
id: parseInt(selectedBudget.id),
nom: selectedBudget.chantierNom,
budgetPrevu: selectedBudget.budgetTotal
} as any : null}
onSave={handleSaveBudgetPlanning}
/>
</div>
);
};
export default BudgetPlanificationPage;

View File

@@ -0,0 +1,747 @@
'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<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;