/** * Dialog de planification budgétaire avancée pour les phases * Permet l'estimation détaillée des coûts par catégorie */ import React, { useState, useRef } from 'react'; import { Dialog } from 'primereact/dialog'; import { Button } from 'primereact/button'; import { InputNumber } from 'primereact/inputnumber'; import { InputText } from 'primereact/inputtext'; import { Dropdown } from 'primereact/dropdown'; import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; import { Card } from 'primereact/card'; import { TabView, TabPanel } from 'primereact/tabview'; import { Toast } from 'primereact/toast'; import { Divider } from 'primereact/divider'; import { Tag } from 'primereact/tag'; import { ProgressBar } from 'primereact/progressbar'; import { PhaseChantier } from '../../types/btp-extended'; interface BudgetItem { id?: string; categorie: 'MATERIEL' | 'MAIN_OEUVRE' | 'SOUS_TRAITANCE' | 'TRANSPORT' | 'AUTRES'; designation: string; quantite: number; unite: string; prixUnitaire: number; montantHT: number; tauxTVA: number; montantTTC: number; fournisseur?: string; notes?: string; } interface BudgetAnalysis { totalMateriel: number; totalMainOeuvre: number; totalSousTraitance: number; totalTransport: number; totalAutres: number; totalHT: number; totalTVA: number; totalTTC: number; margeObjectif: number; tauxMarge: number; prixVenteCalcule: number; } interface BudgetPlanningDialogProps { visible: boolean; onHide: () => void; phase: PhaseChantier | null; onSave: (budgetData: BudgetAnalysis) => void; } export const BudgetPlanningDialog: React.FC = ({ visible, onHide, phase, onSave }) => { const toast = useRef(null); const [activeIndex, setActiveIndex] = useState(0); const [budgetItems, setBudgetItems] = useState([]); const [newItem, setNewItem] = useState({ categorie: 'MATERIEL', designation: '', quantite: 1, unite: 'unité', prixUnitaire: 0, montantHT: 0, tauxTVA: 20, montantTTC: 0 }); const [margeObjectif, setMargeObjectif] = useState(15); // 15% par défaut const categories = [ { label: 'Matériel', value: 'MATERIEL' }, { label: 'Main d\'œuvre', value: 'MAIN_OEUVRE' }, { label: 'Sous-traitance', value: 'SOUS_TRAITANCE' }, { label: 'Transport', value: 'TRANSPORT' }, { label: 'Autres', value: 'AUTRES' } ]; const unites = [ { label: 'Unité', value: 'unité' }, { label: 'Heure', value: 'h' }, { label: 'Jour', value: 'j' }, { label: 'Mètre', value: 'm' }, { label: 'Mètre carré', value: 'm²' }, { label: 'Mètre cube', value: 'm³' }, { label: 'Kilogramme', value: 'kg' }, { label: 'Tonne', value: 't' }, { label: 'Forfait', value: 'forfait' } ]; // Calculer automatiquement les montants const calculateAmounts = (item: BudgetItem) => { const montantHT = item.quantite * item.prixUnitaire; const montantTVA = montantHT * (item.tauxTVA / 100); const montantTTC = montantHT + montantTVA; return { ...item, montantHT, montantTTC }; }; // Ajouter un nouvel élément au budget const addBudgetItem = () => { if (!newItem.designation.trim()) { toast.current?.show({ severity: 'warn', summary: 'Champ requis', detail: 'Veuillez saisir une désignation', life: 3000 }); return; } const calculatedItem = calculateAmounts({ ...newItem, id: `budget_${Date.now()}` }); setBudgetItems([...budgetItems, calculatedItem]); setNewItem({ categorie: 'MATERIEL', designation: '', quantite: 1, unite: 'unité', prixUnitaire: 0, montantHT: 0, tauxTVA: 20, montantTTC: 0 }); }; // Supprimer un élément du budget const removeBudgetItem = (itemId: string) => { setBudgetItems(budgetItems.filter(item => item.id !== itemId)); }; // Calculer l'analyse budgétaire const getBudgetAnalysis = (): BudgetAnalysis => { const totalMateriel = budgetItems .filter(item => item.categorie === 'MATERIEL') .reduce((sum, item) => sum + item.montantHT, 0); const totalMainOeuvre = budgetItems .filter(item => item.categorie === 'MAIN_OEUVRE') .reduce((sum, item) => sum + item.montantHT, 0); const totalSousTraitance = budgetItems .filter(item => item.categorie === 'SOUS_TRAITANCE') .reduce((sum, item) => sum + item.montantHT, 0); const totalTransport = budgetItems .filter(item => item.categorie === 'TRANSPORT') .reduce((sum, item) => sum + item.montantHT, 0); const totalAutres = budgetItems .filter(item => item.categorie === 'AUTRES') .reduce((sum, item) => sum + item.montantHT, 0); const totalHT = totalMateriel + totalMainOeuvre + totalSousTraitance + totalTransport + totalAutres; const totalTVA = budgetItems.reduce((sum, item) => sum + (item.montantHT * item.tauxTVA / 100), 0); const totalTTC = totalHT + totalTVA; const montantMarge = totalHT * (margeObjectif / 100); const prixVenteCalcule = totalHT + montantMarge; return { totalMateriel, totalMainOeuvre, totalSousTraitance, totalTransport, totalAutres, totalHT, totalTVA, totalTTC, margeObjectif: montantMarge, tauxMarge: margeObjectif, prixVenteCalcule }; }; // Template pour afficher la catégorie const categorieTemplate = (rowData: BudgetItem) => { const category = categories.find(cat => cat.value === rowData.categorie); const severityMap = { 'MATERIEL': 'info', 'MAIN_OEUVRE': 'success', 'SOUS_TRAITANCE': 'warning', 'TRANSPORT': 'help', 'AUTRES': 'secondary' } as const; return ; }; // Template pour afficher les montants const montantTemplate = (rowData: BudgetItem, field: keyof BudgetItem) => { const value = rowData[field] as number; return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value); }; // Footer du dialog const dialogFooter = (
); const analysis = getBudgetAnalysis(); return ( <> setActiveIndex(e.index)}>
{/* Formulaire d'ajout */}
setNewItem({...newItem, categorie: e.value})} className="w-full" />
setNewItem({...newItem, designation: e.target.value})} className="w-full" placeholder="Ex: Béton C25/30" />
setNewItem({...newItem, quantite: e.value || 1})} className="w-full" min={0.01} step={0.01} />
setNewItem({...newItem, unite: e.value})} className="w-full" />
setNewItem({...newItem, prixUnitaire: e.value || 0})} className="w-full" mode="currency" currency="EUR" locale="fr-FR" />
setNewItem({...newItem, tauxTVA: e.value || 20})} className="w-full" suffix="%" min={0} max={100} />
setNewItem({...newItem, fournisseur: e.target.value})} className="w-full" placeholder="Nom du fournisseur" />
setNewItem({...newItem, notes: e.target.value})} className="w-full" placeholder="Notes supplémentaires" />
{/* Liste des éléments */}
montantTemplate(rowData, 'prixUnitaire')} style={{ width: '8rem' }} /> montantTemplate(rowData, 'montantHT')} style={{ width: '8rem' }} /> montantTemplate(rowData, 'montantTTC')} style={{ width: '8rem' }} /> (
Matériel
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.totalMateriel)} 0 ? (analysis.totalMateriel / analysis.totalHT) * 100 : 0} className="mt-2" color="#007ad9" />
Main d'œuvre
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.totalMainOeuvre)} 0 ? (analysis.totalMainOeuvre / analysis.totalHT) * 100 : 0} className="mt-2" color="#22c55e" />
Sous-traitance
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.totalSousTraitance)} 0 ? (analysis.totalSousTraitance / analysis.totalHT) * 100 : 0} className="mt-2" color="#f97316" />
Transport + Autres
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.totalTransport + analysis.totalAutres)} 0 ? ((analysis.totalTransport + analysis.totalAutres) / analysis.totalHT) * 100 : 0} className="mt-2" color="#8b5cf6" />
setMargeObjectif(e.value || 15)} className="w-full" suffix="%" min={0} max={100} />
Total HT: {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.totalHT)}
TVA: {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.totalTVA)}
Marge ({margeObjectif}%): +{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.margeObjectif)}
Prix de vente calculé: {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.prixVenteCalcule)}
); }; export default BudgetPlanningDialog;