Files
btpxpress-frontend/components/phases/BudgetPlanningDialog.tsx
dahoud a8825a058b Fix: Corriger toutes les erreurs de build du frontend
- 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>
2025-10-18 13:23:08 +00:00

528 lines
26 KiB
TypeScript

/**
* 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<BudgetPlanningDialogProps> = ({
visible,
onHide,
phase,
onSave
}) => {
const toast = useRef<Toast>(null);
const [activeIndex, setActiveIndex] = useState(0);
const [budgetItems, setBudgetItems] = useState<BudgetItem[]>([]);
const [newItem, setNewItem] = useState<BudgetItem>({
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': 'danger',
'AUTRES': 'secondary'
} as const;
return <Tag value={category?.label} severity={severityMap[rowData.categorie]} />;
};
// 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 = (
<div className="flex justify-content-between">
<Button
label="Annuler"
icon="pi pi-times"
onClick={onHide}
className="p-button-text"
/>
<div className="flex gap-2">
<Button
label="Réinitialiser"
icon="pi pi-refresh"
onClick={() => setBudgetItems([])}
className="p-button-outlined"
/>
<Button
label="Enregistrer le budget"
icon="pi pi-check"
onClick={() => {
const analysis = getBudgetAnalysis();
onSave(analysis);
onHide();
}}
disabled={budgetItems.length === 0}
/>
</div>
</div>
);
const analysis = getBudgetAnalysis();
return (
<>
<Toast ref={toast} />
<Dialog
header={`Planification budgétaire - ${phase?.nom || 'Phase'}`}
visible={visible}
onHide={onHide}
footer={dialogFooter}
style={{ width: '95vw', maxWidth: '1200px' }}
modal
maximizable
>
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
<TabPanel header="Saisie des coûts" leftIcon="pi pi-plus">
<div className="grid">
{/* Formulaire d'ajout */}
<div className="col-12">
<Card title="Ajouter un élément de coût" className="mb-4">
<div className="grid">
<div className="col-12 md:col-3">
<label htmlFor="categorie" className="font-semibold">Catégorie</label>
<Dropdown
id="categorie"
value={newItem.categorie}
options={categories}
onChange={(e) => setNewItem({...newItem, categorie: e.value})}
className="w-full"
/>
</div>
<div className="col-12 md:col-4">
<label htmlFor="designation" className="font-semibold">Désignation</label>
<InputText
id="designation"
value={newItem.designation}
onChange={(e) => setNewItem({...newItem, designation: e.target.value})}
className="w-full"
placeholder="Ex: Béton C25/30"
/>
</div>
<div className="col-12 md:col-2">
<label htmlFor="quantite" className="font-semibold">Quantité</label>
<InputNumber
id="quantite"
value={newItem.quantite}
onValueChange={(e) => setNewItem({...newItem, quantite: e.value || 1})}
className="w-full"
min={0.01}
step={0.01}
/>
</div>
<div className="col-12 md:col-2">
<label htmlFor="unite" className="font-semibold">Unité</label>
<Dropdown
id="unite"
value={newItem.unite}
options={unites}
onChange={(e) => setNewItem({...newItem, unite: e.value})}
className="w-full"
/>
</div>
<div className="col-12 md:col-1">
<label className="font-semibold">&nbsp;</label>
<Button
icon="pi pi-plus"
onClick={addBudgetItem}
className="w-full"
tooltip="Ajouter cet élément"
/>
</div>
</div>
<div className="grid mt-3">
<div className="col-12 md:col-3">
<label htmlFor="prixUnitaire" className="font-semibold">Prix unitaire HT ()</label>
<InputNumber
id="prixUnitaire"
value={newItem.prixUnitaire}
onValueChange={(e) => setNewItem({...newItem, prixUnitaire: e.value || 0})}
className="w-full"
mode="currency"
currency="EUR"
locale="fr-FR"
/>
</div>
<div className="col-12 md:col-2">
<label htmlFor="tauxTVA" className="font-semibold">TVA (%)</label>
<InputNumber
id="tauxTVA"
value={newItem.tauxTVA}
onValueChange={(e) => setNewItem({...newItem, tauxTVA: e.value || 20})}
className="w-full"
suffix="%"
min={0}
max={100}
/>
</div>
<div className="col-12 md:col-3">
<label htmlFor="fournisseur" className="font-semibold">Fournisseur (optionnel)</label>
<InputText
id="fournisseur"
value={newItem.fournisseur || ''}
onChange={(e) => setNewItem({...newItem, fournisseur: e.target.value})}
className="w-full"
placeholder="Nom du fournisseur"
/>
</div>
<div className="col-12 md:col-4">
<label htmlFor="notes" className="font-semibold">Notes (optionnel)</label>
<InputText
id="notes"
value={newItem.notes || ''}
onChange={(e) => setNewItem({...newItem, notes: e.target.value})}
className="w-full"
placeholder="Notes supplémentaires"
/>
</div>
</div>
</Card>
</div>
{/* Liste des éléments */}
<div className="col-12">
<DataTable
value={budgetItems}
emptyMessage="Aucun élément de coût ajouté"
size="small"
header="Éléments du budget"
>
<Column field="categorie" header="Catégorie" body={categorieTemplate} style={{ width: '10rem' }} />
<Column field="designation" header="Désignation" style={{ minWidth: '15rem' }} />
<Column field="quantite" header="Qté" style={{ width: '6rem' }} />
<Column field="unite" header="Unité" style={{ width: '6rem' }} />
<Column
field="prixUnitaire"
header="Prix unit. HT"
body={(rowData) => montantTemplate(rowData, 'prixUnitaire')}
style={{ width: '8rem' }}
/>
<Column
field="montantHT"
header="Montant HT"
body={(rowData) => montantTemplate(rowData, 'montantHT')}
style={{ width: '8rem' }}
/>
<Column
field="montantTTC"
header="Montant TTC"
body={(rowData) => montantTemplate(rowData, 'montantTTC')}
style={{ width: '8rem' }}
/>
<Column
header="Actions"
style={{ width: '6rem' }}
body={(rowData) => (
<Button
icon="pi pi-trash"
className="p-button-text p-button-danger"
onClick={() => removeBudgetItem(rowData.id)}
tooltip="Supprimer"
/>
)}
/>
</DataTable>
</div>
</div>
</TabPanel>
<TabPanel header="Analyse budgétaire" leftIcon="pi pi-chart-bar">
<div className="grid">
<div className="col-12 lg:col-8">
<Card title="Répartition des coûts">
<div className="grid">
<div className="col-6 md:col-3">
<div className="text-center">
<h6 className="m-0 text-color-secondary">Matériel</h6>
<span className="text-xl font-semibold text-primary">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.totalMateriel)}
</span>
<ProgressBar
value={analysis.totalHT > 0 ? (analysis.totalMateriel / analysis.totalHT) * 100 : 0}
className="mt-2"
color="#007ad9"
/>
</div>
</div>
<div className="col-6 md:col-3">
<div className="text-center">
<h6 className="m-0 text-color-secondary">Main d'œuvre</h6>
<span className="text-xl font-semibold text-green-500">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.totalMainOeuvre)}
</span>
<ProgressBar
value={analysis.totalHT > 0 ? (analysis.totalMainOeuvre / analysis.totalHT) * 100 : 0}
className="mt-2"
color="#22c55e"
/>
</div>
</div>
<div className="col-6 md:col-3">
<div className="text-center">
<h6 className="m-0 text-color-secondary">Sous-traitance</h6>
<span className="text-xl font-semibold text-orange-500">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.totalSousTraitance)}
</span>
<ProgressBar
value={analysis.totalHT > 0 ? (analysis.totalSousTraitance / analysis.totalHT) * 100 : 0}
className="mt-2"
color="#f97316"
/>
</div>
</div>
<div className="col-6 md:col-3">
<div className="text-center">
<h6 className="m-0 text-color-secondary">Transport + Autres</h6>
<span className="text-xl font-semibold text-purple-500">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.totalTransport + analysis.totalAutres)}
</span>
<ProgressBar
value={analysis.totalHT > 0 ? ((analysis.totalTransport + analysis.totalAutres) / analysis.totalHT) * 100 : 0}
className="mt-2"
color="#8b5cf6"
/>
</div>
</div>
</div>
</Card>
</div>
<div className="col-12 lg:col-4">
<Card title="Calcul de la marge">
<div className="field">
<label htmlFor="margeObjectif" className="font-semibold">Marge objectif (%)</label>
<InputNumber
id="margeObjectif"
value={margeObjectif}
onValueChange={(e) => setMargeObjectif(e.value || 15)}
className="w-full"
suffix="%"
min={0}
max={100}
/>
</div>
<Divider />
<div className="flex justify-content-between align-items-center mb-2">
<span>Total HT:</span>
<span className="font-semibold">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.totalHT)}
</span>
</div>
<div className="flex justify-content-between align-items-center mb-2">
<span>TVA:</span>
<span className="font-semibold">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.totalTVA)}
</span>
</div>
<div className="flex justify-content-between align-items-center mb-2">
<span>Marge ({margeObjectif}%):</span>
<span className="font-semibold text-green-500">
+{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.margeObjectif)}
</span>
</div>
<Divider />
<div className="flex justify-content-between align-items-center">
<span className="text-lg font-bold">Prix de vente calculé:</span>
<span className="text-xl font-bold text-primary">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(analysis.prixVenteCalcule)}
</span>
</div>
</Card>
</div>
</div>
</TabPanel>
</TabView>
</Dialog>
</>
);
};
export default BudgetPlanningDialog;