744 lines
38 KiB
TypeScript
Executable File
744 lines
38 KiB
TypeScript
Executable File
'use client';
|
|
|
|
/**
|
|
* Étape 3 : Prévisualisation et génération avec planning intégré
|
|
* Interface finale de validation et génération des phases
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Timeline } from 'primereact/timeline';
|
|
import { TabView, TabPanel } from 'primereact/tabview';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Divider } from 'primereact/divider';
|
|
import { Message } from 'primereact/message';
|
|
import { Panel } from 'primereact/panel';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { Calendar } from 'primereact/calendar';
|
|
import { Accordion, AccordionTab } from 'primereact/accordion';
|
|
import { Chart } from 'primereact/chart';
|
|
import { WizardConfiguration } from '../PhaseGenerationWizard';
|
|
import { Chantier } from '../../../types/btp';
|
|
import budgetCoherenceService from '../../../services/budgetCoherenceService';
|
|
|
|
interface PreviewGenerationStepProps {
|
|
configuration: WizardConfiguration;
|
|
onConfigurationChange: (config: WizardConfiguration) => void;
|
|
chantier: Chantier;
|
|
}
|
|
|
|
interface PhasePreview {
|
|
id: string;
|
|
nom: string;
|
|
ordre: number;
|
|
dateDebut: Date;
|
|
dateFin: Date;
|
|
duree: number;
|
|
budget: number;
|
|
categorie: string;
|
|
sousPhases: SousPhasePreview[];
|
|
prerequis: string[];
|
|
competences: string[];
|
|
status: 'pending' | 'ready' | 'blocked';
|
|
}
|
|
|
|
interface SousPhasePreview {
|
|
id: string;
|
|
nom: string;
|
|
dateDebut: Date;
|
|
dateFin: Date;
|
|
duree: number;
|
|
budget: number;
|
|
}
|
|
|
|
const PreviewGenerationStep: React.FC<PreviewGenerationStepProps> = ({
|
|
configuration,
|
|
onConfigurationChange,
|
|
chantier
|
|
}) => {
|
|
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
|
const [phasesPreview, setPhasesPreview] = useState<PhasePreview[]>([]);
|
|
const [chartData, setChartData] = useState<any>({});
|
|
const [validationBudget, setValidationBudget] = useState<any>(null);
|
|
const [chargementValidation, setChargementValidation] = useState(false);
|
|
|
|
// Calculer la prévisualisation des phases avec dates
|
|
useEffect(() => {
|
|
if (configuration.phasesSelectionnees.length > 0) {
|
|
calculatePhasesPreview();
|
|
}
|
|
}, [configuration.phasesSelectionnees, configuration.dateDebutSouhaitee]);
|
|
|
|
// Validation budgétaire séparée quand la prévisualisation change
|
|
useEffect(() => {
|
|
if (phasesPreview.length > 0) {
|
|
validerBudgetPhases();
|
|
}
|
|
}, [phasesPreview]);
|
|
|
|
const calculatePhasesPreview = () => {
|
|
const dateDebut = configuration.dateDebutSouhaitee || new Date();
|
|
let currentDate = new Date(dateDebut);
|
|
|
|
const previews: PhasePreview[] = configuration.phasesSelectionnees
|
|
.sort((a, b) => a.ordre - b.ordre)
|
|
.map((phase) => {
|
|
const dateDebutPhase = new Date(currentDate);
|
|
const dateFinPhase = new Date(currentDate);
|
|
const dureePhaseDays = phase.dureeEstimee || 1; // Défaut 1 jour si non défini
|
|
dateFinPhase.setDate(dateFinPhase.getDate() + dureePhaseDays);
|
|
|
|
// Calculer les sous-phases
|
|
let currentSousPhaseDate = new Date(dateDebutPhase);
|
|
const sousPhases: SousPhasePreview[] = phase.sousPhases.map((sp) => {
|
|
const debutSp = new Date(currentSousPhaseDate);
|
|
const finSp = new Date(currentSousPhaseDate);
|
|
const dureeSousPhasedays = sp.dureeEstimee || 1; // Défaut 1 jour si non défini
|
|
finSp.setDate(finSp.getDate() + dureeSousPhasedays);
|
|
|
|
currentSousPhaseDate = finSp;
|
|
|
|
return {
|
|
id: sp.id,
|
|
nom: sp.nom,
|
|
dateDebut: debutSp,
|
|
dateFin: finSp,
|
|
duree: sp.dureeEstimee || 0,
|
|
budget: sp.budgetEstime || 0
|
|
};
|
|
});
|
|
|
|
// Déterminer le statut
|
|
let status: 'pending' | 'ready' | 'blocked' = 'ready';
|
|
if (phase.prerequis.length > 0) {
|
|
// Vérifier si tous les prérequis sont satisfaits
|
|
const prerequisSatisfaits = phase.prerequis.every(prereq =>
|
|
configuration.phasesSelectionnees.some(p =>
|
|
p.nom.includes(prereq) && p.ordre < phase.ordre
|
|
)
|
|
);
|
|
status = prerequisSatisfaits ? 'ready' : 'blocked';
|
|
}
|
|
|
|
const preview: PhasePreview = {
|
|
id: phase.id,
|
|
nom: phase.nom,
|
|
ordre: phase.ordre,
|
|
dateDebut: dateDebutPhase,
|
|
dateFin: dateFinPhase,
|
|
duree: phase.dureeEstimee || 0,
|
|
budget: phase.budgetEstime || 0,
|
|
categorie: phase.categorieMetier,
|
|
sousPhases,
|
|
prerequis: phase.prerequis,
|
|
competences: phase.competencesRequises,
|
|
status
|
|
};
|
|
|
|
// Passer à la phase suivante
|
|
currentDate = new Date(dateFinPhase);
|
|
currentDate.setDate(currentDate.getDate() + 1); // 1 jour de battement
|
|
|
|
return preview;
|
|
});
|
|
|
|
setPhasesPreview(previews);
|
|
prepareChartData(previews);
|
|
};
|
|
|
|
const prepareChartData = (previews: PhasePreview[]) => {
|
|
// Données pour le graphique de répartition budgétaire
|
|
const budgetData = {
|
|
labels: previews.map(p => p.nom.substring(0, 15) + '...'),
|
|
datasets: [{
|
|
data: previews.map(p => p.budget),
|
|
backgroundColor: [
|
|
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0',
|
|
'#9966FF', '#FF9F40', '#FF6384', '#C9CBCF'
|
|
],
|
|
borderWidth: 0
|
|
}]
|
|
};
|
|
|
|
// Données pour le planning (Gantt simplifié)
|
|
const planningData = {
|
|
labels: previews.map(p => p.nom.substring(0, 10)),
|
|
datasets: [{
|
|
label: 'Durée (jours)',
|
|
data: previews.map(p => p.duree),
|
|
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
borderWidth: 1
|
|
}]
|
|
};
|
|
|
|
setChartData({ budget: budgetData, planning: planningData });
|
|
};
|
|
|
|
// Validation budgétaire
|
|
const validerBudgetPhases = async () => {
|
|
if (phasesPreview.length === 0 || chargementValidation) return;
|
|
|
|
setChargementValidation(true);
|
|
try {
|
|
const budgetTotal = phasesPreview.reduce((sum, p) => sum + (p.budget || 0), 0);
|
|
if (budgetTotal > 0) {
|
|
const validation = await budgetCoherenceService.validerBudgetPhases(
|
|
chantier.id.toString(),
|
|
budgetTotal
|
|
);
|
|
setValidationBudget(validation);
|
|
}
|
|
} catch (error) {
|
|
console.warn('Erreur lors de la validation budgétaire:', error);
|
|
setValidationBudget(null);
|
|
} finally {
|
|
setChargementValidation(false);
|
|
}
|
|
};
|
|
|
|
// Templates pour les tableaux
|
|
const statusTemplate = (rowData: PhasePreview) => {
|
|
const severityMap = {
|
|
'ready': 'success',
|
|
'pending': 'warning',
|
|
'blocked': 'danger'
|
|
} as const;
|
|
|
|
const labelMap = {
|
|
'ready': 'Prête',
|
|
'pending': 'En attente',
|
|
'blocked': 'Bloquée'
|
|
};
|
|
|
|
return (
|
|
<Tag
|
|
value={labelMap[rowData.status]}
|
|
severity={severityMap[rowData.status]}
|
|
icon={`pi ${rowData.status === 'ready' ? 'pi-check' : rowData.status === 'blocked' ? 'pi-times' : 'pi-clock'}`}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const dateTemplate = (rowData: PhasePreview, field: 'dateDebut' | 'dateFin') => (
|
|
<span>{rowData[field].toLocaleDateString('fr-FR')}</span>
|
|
);
|
|
|
|
const budgetTemplate = (rowData: PhasePreview) => (
|
|
<span className="font-semibold">
|
|
{new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
minimumFractionDigits: 0
|
|
}).format(rowData.budget)}
|
|
</span>
|
|
);
|
|
|
|
const sousPhaseTemplate = (rowData: PhasePreview) => (
|
|
<Badge
|
|
value={rowData.sousPhases.length}
|
|
severity="info"
|
|
className="mr-2"
|
|
/>
|
|
);
|
|
|
|
const prerequisTemplate = (rowData: PhasePreview) => (
|
|
<div className="flex flex-wrap gap-1">
|
|
{rowData.prerequis.slice(0, 2).map((prereq, index) => (
|
|
<span
|
|
key={index}
|
|
className="inline-block bg-orange-50 text-orange-700 px-2 py-1 border-round text-xs"
|
|
>
|
|
{prereq}
|
|
</span>
|
|
))}
|
|
{rowData.prerequis.length > 2 && (
|
|
<span className="inline-block bg-gray-100 text-gray-600 px-2 py-1 border-round text-xs">
|
|
+{rowData.prerequis.length - 2}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
// Timeline des phases
|
|
const timelineEvents = phasesPreview.map((phase, index) => ({
|
|
status: phase.status === 'ready' ? 'success' : phase.status === 'blocked' ? 'danger' : 'warning',
|
|
date: phase.dateDebut.toLocaleDateString('fr-FR'),
|
|
icon: phase.status === 'ready' ? 'pi-check' : phase.status === 'blocked' ? 'pi-times' : 'pi-clock',
|
|
color: phase.status === 'ready' ? '#22c55e' : phase.status === 'blocked' ? '#ef4444' : '#f59e0b',
|
|
title: phase.nom,
|
|
subtitle: `${phase.duree} jours • ${new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0 }).format(phase.budget)}`
|
|
}));
|
|
|
|
// Statistiques récapitulatives
|
|
const stats = {
|
|
totalPhases: phasesPreview.length,
|
|
totalSousPhases: phasesPreview.reduce((sum, p) => sum + p.sousPhases.length, 0),
|
|
dureeTotal: phasesPreview.reduce((sum, p) => sum + p.duree, 0),
|
|
budgetTotal: phasesPreview.reduce((sum, p) => sum + p.budget, 0),
|
|
phasesReady: phasesPreview.filter(p => p.status === 'ready').length,
|
|
phasesBlocked: phasesPreview.filter(p => p.status === 'blocked').length,
|
|
dateDebut: phasesPreview.length > 0 ? phasesPreview[0].dateDebut : null,
|
|
dateFin: phasesPreview.length > 0 ? phasesPreview[phasesPreview.length - 1].dateFin : null
|
|
};
|
|
|
|
const budgetAvecMarges = configuration.optionsAvancees.appliquerMarges ?
|
|
stats.budgetTotal * (1 + configuration.optionsAvancees.taux.margeCommerciale / 100) *
|
|
(1 + configuration.optionsAvancees.taux.alea / 100) *
|
|
(1 + configuration.optionsAvancees.taux.tva / 100) :
|
|
stats.budgetTotal;
|
|
|
|
// Panel de récapitulatif exécutif
|
|
const recapitulatifExecutif = () => (
|
|
<Card title="Récapitulatif Exécutif" className="mb-4">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center p-3 surface-100 border-round">
|
|
<i className="pi pi-building text-primary text-3xl mb-3"></i>
|
|
<h6 className="text-color m-0 mb-2">Chantier</h6>
|
|
<div className="text-color font-bold">{chantier.nom}</div>
|
|
<small className="text-color-secondary">
|
|
{configuration.typeChantier?.nom || chantier.typeChantier}
|
|
</small>
|
|
{chantier.montantPrevu && (
|
|
<div className="text-xs text-color-secondary mt-1">
|
|
Budget: {chantier.montantPrevu.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR' })}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center p-3 surface-100 border-round">
|
|
<i className="pi pi-sitemap text-primary text-3xl mb-3"></i>
|
|
<h6 className="text-color m-0 mb-2">Phases</h6>
|
|
<div className="text-color font-bold text-xl">{stats.totalPhases}</div>
|
|
<small className="text-color-secondary">+ {stats.totalSousPhases} sous-phases</small>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center p-3 surface-100 border-round">
|
|
<i className="pi pi-calendar text-primary text-3xl mb-3"></i>
|
|
<h6 className="text-color m-0 mb-2">Planning</h6>
|
|
<div className="text-color font-bold text-xl">{stats.dureeTotal}</div>
|
|
<small className="text-color-secondary">jours ouvrés</small>
|
|
{stats.dateDebut && stats.dateFin && (
|
|
<div className="mt-2 text-xs text-color-secondary">
|
|
{stats.dateDebut.toLocaleDateString('fr-FR')} → {stats.dateFin.toLocaleDateString('fr-FR')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center p-3 surface-100 border-round">
|
|
<i className="pi pi-euro text-primary text-3xl mb-3"></i>
|
|
<h6 className="text-color m-0 mb-2">Budget</h6>
|
|
<div className="text-color font-bold text-lg">
|
|
{new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
minimumFractionDigits: 0
|
|
}).format(budgetAvecMarges)}
|
|
</div>
|
|
<small className="text-color-secondary">
|
|
{configuration.optionsAvancees.appliquerMarges ? 'avec marges' : 'hors marges'}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Alertes et validations */}
|
|
<Divider />
|
|
<div className="grid">
|
|
<div className="col-12 md:col-4">
|
|
{stats.phasesBlocked > 0 ? (
|
|
<Message
|
|
severity="warn"
|
|
text={`${stats.phasesBlocked} phase(s) bloquée(s) par des prérequis manquants`}
|
|
/>
|
|
) : (
|
|
<Message
|
|
severity="success"
|
|
text="Toutes les phases sont prêtes à être générées"
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="col-12 md:col-4">
|
|
{chargementValidation ? (
|
|
<Message
|
|
severity="info"
|
|
text="Validation budgétaire en cours..."
|
|
/>
|
|
) : validationBudget ? (
|
|
<Message
|
|
severity={validationBudget.valide ? "success" : "warn"}
|
|
text={validationBudget.message}
|
|
/>
|
|
) : (
|
|
<Message
|
|
severity="info"
|
|
text="Validation budgétaire indisponible"
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="col-12 md:col-4">
|
|
{configuration.optionsAvancees.calculerBudgetAuto ? (
|
|
<Message
|
|
severity="info"
|
|
text="Budgets calculés automatiquement avec coefficients"
|
|
/>
|
|
) : (
|
|
<Message
|
|
severity="info"
|
|
text="Budgets basés sur la saisie utilisateur"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recommandation budgétaire si nécessaire */}
|
|
{validationBudget && !validationBudget.valide && validationBudget.recommandation && (
|
|
<div className="mt-3">
|
|
<Message
|
|
severity="warn"
|
|
text={
|
|
validationBudget.recommandation === 'METTRE_A_JOUR_CHANTIER'
|
|
? `💡 Recommandation : Mettre à jour le budget du chantier à ${validationBudget.nouveauBudgetSuggere ? new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(validationBudget.nouveauBudgetSuggere) : 'calculer'}`
|
|
: validationBudget.recommandation === 'AJUSTER_PHASES'
|
|
? '💡 Recommandation : Ajuster les budgets des phases pour correspondre au budget du chantier'
|
|
: 'Vérification budgétaire recommandée'
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex align-items-center gap-3 mb-4">
|
|
<i className="pi pi-eye text-primary text-2xl"></i>
|
|
<div>
|
|
<h4 className="m-0">Prévisualisation & Génération</h4>
|
|
<p className="m-0 text-color-secondary">
|
|
Vérifiez la configuration finale avant génération
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{recapitulatifExecutif()}
|
|
|
|
<TabView activeIndex={activeTabIndex} onTabChange={(e) => setActiveTabIndex(e.index)}>
|
|
<TabPanel header="Planning Détaillé" leftIcon="pi pi-calendar">
|
|
<div className="grid">
|
|
<div className="col-12 lg:col-8">
|
|
<Card title="Tableau des Phases">
|
|
<DataTable
|
|
value={phasesPreview}
|
|
paginator={false}
|
|
emptyMessage="Aucune phase à afficher"
|
|
className="p-datatable-sm"
|
|
>
|
|
<Column
|
|
field="ordre"
|
|
header="#"
|
|
style={{ width: '60px' }}
|
|
body={(rowData) => <Badge value={rowData.ordre} />}
|
|
/>
|
|
<Column
|
|
field="nom"
|
|
header="Phase"
|
|
style={{ minWidth: '200px' }}
|
|
/>
|
|
<Column
|
|
field="dateDebut"
|
|
header="Début"
|
|
body={(rowData) => dateTemplate(rowData, 'dateDebut')}
|
|
style={{ width: '100px' }}
|
|
/>
|
|
<Column
|
|
field="dateFin"
|
|
header="Fin"
|
|
body={(rowData) => dateTemplate(rowData, 'dateFin')}
|
|
style={{ width: '100px' }}
|
|
/>
|
|
<Column
|
|
field="duree"
|
|
header="Durée"
|
|
body={(rowData) => `${rowData.duree}j`}
|
|
style={{ width: '80px' }}
|
|
/>
|
|
<Column
|
|
field="budget"
|
|
header="Budget"
|
|
body={budgetTemplate}
|
|
style={{ width: '120px' }}
|
|
/>
|
|
<Column
|
|
field="sousPhases"
|
|
header="S.-phases"
|
|
body={sousPhaseTemplate}
|
|
style={{ width: '90px' }}
|
|
/>
|
|
<Column
|
|
field="status"
|
|
header="Statut"
|
|
body={statusTemplate}
|
|
style={{ width: '100px' }}
|
|
/>
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
<div className="col-12 lg:col-4">
|
|
<Card title="Timeline">
|
|
<Timeline
|
|
value={timelineEvents}
|
|
content={(item) => (
|
|
<div>
|
|
<div className="font-semibold text-sm">{item.title}</div>
|
|
<small className="text-color-secondary">{item.subtitle}</small>
|
|
</div>
|
|
)}
|
|
className="w-full"
|
|
/>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Analyse Budgétaire" leftIcon="pi pi-chart-pie">
|
|
<div className="grid">
|
|
<div className="col-12 lg:col-6">
|
|
<Card title="Répartition Budgétaire par Phase">
|
|
{chartData.budget && (
|
|
<Chart
|
|
type="doughnut"
|
|
data={chartData.budget}
|
|
style={{ height: '300px' }}
|
|
/>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
<div className="col-12 lg:col-6">
|
|
<Card title="Durée par Phase">
|
|
{chartData.planning && (
|
|
<Chart
|
|
type="bar"
|
|
data={chartData.planning}
|
|
style={{ height: '300px' }}
|
|
/>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
{configuration.optionsAvancees.appliquerMarges && (
|
|
<Card title="Détail des Marges Appliquées" className="mt-4">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<div className="flex justify-content-between mb-2">
|
|
<span>Budget base (phases):</span>
|
|
<span className="font-semibold">
|
|
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(stats.budgetTotal)}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-content-between mb-2">
|
|
<span>+ Marge commerciale ({configuration.optionsAvancees.taux.margeCommerciale}%):</span>
|
|
<span className="text-blue-600">
|
|
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(
|
|
stats.budgetTotal * configuration.optionsAvancees.taux.margeCommerciale / 100
|
|
)}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-content-between mb-2">
|
|
<span>+ Aléa/Imprévus ({configuration.optionsAvancees.taux.alea}%):</span>
|
|
<span className="text-orange-600">
|
|
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(
|
|
stats.budgetTotal * (1 + configuration.optionsAvancees.taux.margeCommerciale / 100) *
|
|
configuration.optionsAvancees.taux.alea / 100
|
|
)}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-content-between mb-2">
|
|
<span>+ TVA ({configuration.optionsAvancees.taux.tva}%):</span>
|
|
<span className="text-purple-600">
|
|
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(
|
|
stats.budgetTotal * (1 + configuration.optionsAvancees.taux.margeCommerciale / 100) *
|
|
(1 + configuration.optionsAvancees.taux.alea / 100) *
|
|
configuration.optionsAvancees.taux.tva / 100
|
|
)}
|
|
</span>
|
|
</div>
|
|
<Divider />
|
|
<div className="flex justify-content-between">
|
|
<span className="font-bold">Total TTC:</span>
|
|
<span className="font-bold text-green-600 text-lg">
|
|
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(budgetAvecMarges)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<ProgressBar
|
|
value={85}
|
|
showValue={false}
|
|
className="mb-2"
|
|
style={{ height: '20px' }}
|
|
/>
|
|
<small className="text-color-secondary">
|
|
Marge totale appliquée: {(((budgetAvecMarges - stats.budgetTotal) / stats.budgetTotal) * 100).toFixed(1)}%
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Cohérence Budgétaire" leftIcon="pi pi-calculator">
|
|
<Card title="Analyse Budgétaire Détaillée">
|
|
{chargementValidation ? (
|
|
<div className="text-center p-4">
|
|
<ProgressBar mode="indeterminate" style={{ height: '6px' }} />
|
|
<p className="mt-3">Vérification de la cohérence budgétaire...</p>
|
|
</div>
|
|
) : validationBudget ? (
|
|
<div>
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<h6>Résumé de la validation</h6>
|
|
<div className="p-3 border-round" style={{
|
|
backgroundColor: validationBudget.valide ? '#f0f9ff' : '#fefce8',
|
|
border: `1px solid ${validationBudget.valide ? '#0ea5e9' : '#eab308'}`
|
|
}}>
|
|
<div className="flex align-items-center gap-2 mb-2">
|
|
<i className={`pi ${validationBudget.valide ? 'pi-check-circle text-green-600' : 'pi-exclamation-triangle text-yellow-600'} text-lg`}></i>
|
|
<span className="font-semibold">
|
|
{validationBudget.valide ? 'Budget cohérent' : 'Attention budgétaire'}
|
|
</span>
|
|
</div>
|
|
<p className="m-0 text-sm">{validationBudget.message}</p>
|
|
|
|
{validationBudget.recommandation && (
|
|
<div className="mt-3 p-2 bg-white border-round">
|
|
<strong>Recommandation :</strong>
|
|
<br />
|
|
{validationBudget.recommandation === 'METTRE_A_JOUR_CHANTIER' &&
|
|
`Mettre à jour le budget du chantier à ${validationBudget.nouveauBudgetSuggere ? new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(validationBudget.nouveauBudgetSuggere) : 'calculer'}`
|
|
}
|
|
{validationBudget.recommandation === 'AJUSTER_PHASES' &&
|
|
'Ajuster les budgets des phases pour correspondre au budget du chantier'
|
|
}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<h6>Répartition budgétaire</h6>
|
|
<div className="text-sm">
|
|
<div className="flex justify-content-between mb-2">
|
|
<span>Budget total des phases :</span>
|
|
<span className="font-semibold">
|
|
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(stats.budgetTotal)}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-content-between mb-2">
|
|
<span>Nombre de phases :</span>
|
|
<span className="font-semibold">{stats.totalPhases}</span>
|
|
</div>
|
|
<div className="flex justify-content-between mb-2">
|
|
<span>Budget moyen par phase :</span>
|
|
<span className="font-semibold">
|
|
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(stats.totalPhases > 0 ? stats.budgetTotal / stats.totalPhases : 0)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<Message severity="warn" text="Validation budgétaire non disponible" />
|
|
)}
|
|
</Card>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Détails Techniques" leftIcon="pi pi-cog">
|
|
<Accordion>
|
|
{phasesPreview.map((phase, index) => (
|
|
<AccordionTab
|
|
key={phase.id}
|
|
header={
|
|
<div className="flex align-items-center gap-3 w-full">
|
|
<Badge value={phase.ordre} />
|
|
<span className="font-semibold">{phase.nom}</span>
|
|
{statusTemplate(phase)}
|
|
<span className="ml-auto text-color-secondary">
|
|
{phase.duree}j • {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(phase.budget)}
|
|
</span>
|
|
</div>
|
|
}
|
|
>
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<h6>Informations générales</h6>
|
|
<div className="flex flex-column gap-2">
|
|
<div>
|
|
<span className="font-semibold">Catégorie:</span>
|
|
<Tag value={phase.categorie} severity="info" className="ml-2" />
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">Période:</span>
|
|
<span className="ml-2">
|
|
{phase.dateDebut.toLocaleDateString('fr-FR')} → {phase.dateFin.toLocaleDateString('fr-FR')}
|
|
</span>
|
|
</div>
|
|
{phase.prerequis.length > 0 && (
|
|
<div>
|
|
<span className="font-semibold">Prérequis:</span>
|
|
<div className="mt-1">
|
|
{prerequisTemplate(phase)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<h6>Compétences requises</h6>
|
|
<div className="flex flex-wrap gap-1">
|
|
{phase.competences.map((comp, idx) => (
|
|
<Badge key={idx} value={comp} severity="info" />
|
|
))}
|
|
</div>
|
|
|
|
{phase.sousPhases.length > 0 && (
|
|
<>
|
|
<h6 className="mt-4">Sous-phases ({phase.sousPhases.length})</h6>
|
|
<div className="flex flex-column gap-2">
|
|
{phase.sousPhases.map((sp, idx) => (
|
|
<div key={sp.id} className="p-2 bg-gray-50 border-round">
|
|
<div className="flex justify-content-between">
|
|
<span className="font-semibold text-sm">{sp.nom}</span>
|
|
<span className="text-sm text-color-secondary">
|
|
{sp.duree}j • {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(sp.budget)}
|
|
</span>
|
|
</div>
|
|
<small className="text-color-secondary">
|
|
{sp.dateDebut.toLocaleDateString('fr-FR')} → {sp.dateFin.toLocaleDateString('fr-FR')}
|
|
</small>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</AccordionTab>
|
|
))}
|
|
</Accordion>
|
|
</TabPanel>
|
|
</TabView>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PreviewGenerationStep; |