1244 lines
62 KiB
TypeScript
1244 lines
62 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Card } from 'primereact/card';
|
|
import { Button } from 'primereact/button';
|
|
import { InputText } from 'primereact/inputtext';
|
|
import { InputTextarea } from 'primereact/inputtextarea';
|
|
import { InputNumber } from 'primereact/inputnumber';
|
|
import { Calendar } from 'primereact/calendar';
|
|
import { Dropdown } from 'primereact/dropdown';
|
|
import { Toast } from 'primereact/toast';
|
|
import { Divider } from 'primereact/divider';
|
|
import { Checkbox } from 'primereact/checkbox';
|
|
import { Steps } from 'primereact/steps';
|
|
import { Panel } from 'primereact/panel';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Timeline } from 'primereact/timeline';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { Accordion, AccordionTab } from 'primereact/accordion';
|
|
import PhasesTimelinePreview from '../../../../components/phases/PhasesTimelinePreview';
|
|
import { chantierService, clientService } from '../../../../services/api';
|
|
import type { Chantier, Client } from '../../../../types/btp';
|
|
import type { TypeChantier } from '../../../../types/chantier-templates';
|
|
import type { ChantierFormData, ChantierPreview } from '../../../../types/chantier-form';
|
|
|
|
const NouveauChantierPage = () => {
|
|
const router = useRouter();
|
|
const toast = useRef<Toast>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [submitted, setSubmitted] = useState(false);
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
const [clients, setClients] = useState<any[]>([]);
|
|
const [chantierTypes, setChantierTypes] = useState<any[]>([]);
|
|
const [preview, setPreview] = useState<ChantierPreview | null>(null);
|
|
const [calculatingPreview, setCalculatingPreview] = useState(false);
|
|
|
|
const [chantierForm, setChantierForm] = useState<ChantierFormData>({
|
|
nom: '',
|
|
description: '',
|
|
adresse: '',
|
|
codePostal: '',
|
|
ville: '',
|
|
dateDebut: new Date().toISOString().split('T')[0],
|
|
dateFinPrevue: '',
|
|
montantPrevu: 0,
|
|
clientId: '',
|
|
|
|
// Nouveaux champs avancés
|
|
typeChantier: 'MAISON_INDIVIDUELLE' as TypeChantier,
|
|
surface: 0,
|
|
nombreNiveaux: 1,
|
|
autoGenererPhases: true,
|
|
inclureSousPhases: true,
|
|
ajusterDelais: true,
|
|
|
|
// Spécificités
|
|
specificites: [],
|
|
contraintes: '',
|
|
accesPMR: false,
|
|
performanceEnergetique: 'STANDARD',
|
|
|
|
// Informations techniques
|
|
natureTerrain: 'PLAT',
|
|
presenceReseaux: true,
|
|
accessibiliteChantier: 'FACILE',
|
|
stockagePossible: true,
|
|
|
|
// Réglementation
|
|
permisRequis: true,
|
|
numeroPermis: '',
|
|
dateObtentionPermis: '',
|
|
controleUrbanismeRequis: false,
|
|
|
|
// Options de planification
|
|
margeSecurite: 5,
|
|
periodesTravailRestreintes: [],
|
|
|
|
// Équipes et ressources
|
|
equipePrefereId: '',
|
|
materielsRequis: [],
|
|
|
|
// Champs manquants pour compatibilité
|
|
statut: 'PLANIFIE',
|
|
actif: true
|
|
});
|
|
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
const statuts = [
|
|
{ label: 'Planifié', value: 'PLANIFIE' },
|
|
{ label: 'En cours', value: 'EN_COURS' },
|
|
{ label: 'Terminé', value: 'TERMINE' },
|
|
{ label: 'Annulé', value: 'ANNULE' },
|
|
{ label: 'Suspendu', value: 'SUSPENDU' }
|
|
];
|
|
|
|
const steps = [
|
|
{ label: 'Informations générales' },
|
|
{ label: 'Type de chantier' },
|
|
{ label: 'Configuration technique' },
|
|
{ label: 'Localisation' },
|
|
{ label: 'Planification' },
|
|
{ label: 'Options avancées' },
|
|
{ label: 'Prévisualisation' },
|
|
{ label: 'Validation' }
|
|
];
|
|
|
|
useEffect(() => {
|
|
loadClients();
|
|
loadChantierTypes();
|
|
}, []);
|
|
|
|
// Effet pour calculer la prévisualisation quand le type change
|
|
useEffect(() => {
|
|
if (chantierForm.typeChantier && chantierForm.dateDebut) {
|
|
calculatePreview();
|
|
}
|
|
}, [chantierForm.typeChantier, chantierForm.dateDebut, chantierForm.surface, chantierForm.nombreNiveaux]);
|
|
|
|
const loadClients = async () => {
|
|
try {
|
|
const data = await clientService.getAll();
|
|
setClients(data.map(client => ({
|
|
label: `${client.prenom} ${client.nom}${client.entreprise ? ' - ' + client.entreprise : ''}`,
|
|
value: client.id,
|
|
client: client
|
|
})));
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des clients:', error);
|
|
}
|
|
};
|
|
|
|
const loadChantierTypes = async () => {
|
|
try {
|
|
// Utiliser le service API standardisé avec authentification
|
|
const { typeChantierService } = await import('../../../../services/api');
|
|
const typesParCategorie = await typeChantierService.getByCategorie();
|
|
console.log('Types récupérés depuis l\'API:', typesParCategorie);
|
|
|
|
// Convertir en format attendu par PrimeReact
|
|
const groupedTypes: any[] = [];
|
|
|
|
Object.keys(typesParCategorie).forEach(categorie => {
|
|
const categoryTypes = typesParCategorie[categorie];
|
|
groupedTypes.push({
|
|
label: categorie,
|
|
items: categoryTypes.map((type: any) => ({
|
|
label: type.nom,
|
|
value: type.code
|
|
}))
|
|
});
|
|
});
|
|
|
|
setChantierTypes(groupedTypes);
|
|
} catch (error) {
|
|
console.error('❌ API TypeChantier non disponible. Le backend doit être redémarré.', error);
|
|
|
|
// Afficher un message d'erreur à l'utilisateur
|
|
if (toast.current) {
|
|
toast.current.show({
|
|
severity: 'error',
|
|
summary: 'API non disponible',
|
|
detail: 'Les endpoints TypeChantier ne sont pas encore exposés. Redémarrez le backend.',
|
|
life: 5000
|
|
});
|
|
}
|
|
|
|
setChantierTypes([]);
|
|
}
|
|
};
|
|
|
|
const calculatePreview = async () => {
|
|
if (!chantierForm.typeChantier || !chantierForm.dateDebut) return;
|
|
|
|
setCalculatingPreview(true);
|
|
try {
|
|
let typeChantier: any = null;
|
|
|
|
try {
|
|
// Essayer d'utiliser le service API standardisé avec authentification
|
|
const { typeChantierService } = await import('../../../../services/api');
|
|
typeChantier = await typeChantierService.getByCode(chantierForm.typeChantier);
|
|
console.log('Type de chantier récupéré depuis l\'API:', typeChantier);
|
|
} catch (apiError) {
|
|
console.warn('Impossible de récupérer les données du type de chantier depuis l\'API, utilisation des données par défaut');
|
|
|
|
// Données de fallback pour les types de chantier courants
|
|
const typeChantierDefaults: Record<string, any> = {
|
|
'MAISON_INDIVIDUELLE': {
|
|
code: 'MAISON_INDIVIDUELLE',
|
|
nom: 'Maison individuelle',
|
|
dureeMoyenneJours: 120,
|
|
coutMoyenM2: 1200,
|
|
description: 'Construction de maison individuelle'
|
|
},
|
|
'IMMEUBLE_COLLECTIF': {
|
|
code: 'IMMEUBLE_COLLECTIF',
|
|
nom: 'Immeuble collectif',
|
|
dureeMoyenneJours: 365,
|
|
coutMoyenM2: 1500,
|
|
description: 'Construction d\'immeuble résidentiel collectif'
|
|
},
|
|
'RENOVATION': {
|
|
code: 'RENOVATION',
|
|
nom: 'Rénovation',
|
|
dureeMoyenneJours: 90,
|
|
coutMoyenM2: 800,
|
|
description: 'Travaux de rénovation'
|
|
}
|
|
};
|
|
|
|
typeChantier = typeChantierDefaults[chantierForm.typeChantier] || {
|
|
code: chantierForm.typeChantier,
|
|
nom: chantierForm.typeChantier,
|
|
dureeMoyenneJours: 180,
|
|
coutMoyenM2: 1000,
|
|
description: 'Type de chantier personnalisé'
|
|
};
|
|
|
|
console.log('Utilisation des données par défaut:', typeChantier);
|
|
}
|
|
|
|
const dateDebut = new Date(chantierForm.dateDebut);
|
|
const dateFinEstimee = new Date(dateDebut.getTime() + (typeChantier.dureeMoyenneJours || 180) * 24 * 60 * 60 * 1000);
|
|
|
|
const previewData: ChantierPreview = {
|
|
typeChantier: chantierForm.typeChantier,
|
|
nom: chantierForm.nom || typeChantier.nom,
|
|
dureeEstimee: typeChantier.dureeMoyenneJours || 180,
|
|
dateFinEstimee: dateFinEstimee,
|
|
complexite: {
|
|
niveau: (typeChantier.dureeMoyenneJours || 180) > 300 ? 'COMPLEXE' : 'SIMPLE',
|
|
score: (typeChantier.dureeMoyenneJours || 180) > 300 ? 75 : 25,
|
|
facteurs: []
|
|
},
|
|
phasesCount: 8, // Valeur par défaut en attendant les templates
|
|
sousePhasesCount: 24, // Valeur par défaut en attendant les templates
|
|
specificites: [],
|
|
reglementations: []
|
|
};
|
|
|
|
setPreview(previewData);
|
|
|
|
// Auto-calculer la date de fin
|
|
if (!chantierForm.dateFinPrevue) {
|
|
setChantierForm(prev => ({
|
|
...prev,
|
|
dateFinPrevue: dateFinEstimee.toISOString().split('T')[0]
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Impossible de calculer la prévisualisation - API TypeChantier non disponible:', error);
|
|
|
|
if (toast.current) {
|
|
toast.current.show({
|
|
severity: 'warn',
|
|
summary: 'Prévisualisation indisponible',
|
|
detail: 'Impossible de calculer la prévisualisation sans l\'API TypeChantier',
|
|
life: 3000
|
|
});
|
|
}
|
|
} finally {
|
|
setCalculatingPreview(false);
|
|
}
|
|
};
|
|
|
|
const validateStep = (step: number) => {
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
switch (step) {
|
|
case 0: // Informations générales
|
|
if (!chantierForm.nom.trim()) {
|
|
newErrors.nom = 'Le nom du chantier est obligatoire';
|
|
}
|
|
if (!chantierForm.clientId) {
|
|
newErrors.clientId = 'Le client est obligatoire';
|
|
}
|
|
break;
|
|
|
|
case 1: // Type de chantier
|
|
if (!chantierForm.typeChantier) {
|
|
newErrors.typeChantier = 'Le type de chantier est obligatoire';
|
|
}
|
|
break;
|
|
|
|
case 2: // Configuration technique
|
|
if (chantierForm.surface && chantierForm.surface <= 0) {
|
|
newErrors.surface = 'La surface doit être supérieure à 0';
|
|
}
|
|
break;
|
|
|
|
case 3: // Localisation
|
|
if (!chantierForm.adresse.trim()) {
|
|
newErrors.adresse = 'L\'adresse est obligatoire';
|
|
}
|
|
break;
|
|
|
|
case 4: // Planification
|
|
if (!chantierForm.dateDebut) {
|
|
newErrors.dateDebut = 'La date de début est obligatoire';
|
|
}
|
|
if (!chantierForm.dateFinPrevue) {
|
|
newErrors.dateFinPrevue = 'La date de fin prévue est obligatoire';
|
|
}
|
|
if (chantierForm.dateDebut && chantierForm.dateFinPrevue &&
|
|
new Date(chantierForm.dateDebut) > new Date(chantierForm.dateFinPrevue)) {
|
|
newErrors.dateFinPrevue = 'La date de fin doit être postérieure à la date de début';
|
|
}
|
|
break;
|
|
|
|
case 5: // Options avancées
|
|
if (chantierForm.montantPrevu && chantierForm.montantPrevu <= 0) {
|
|
newErrors.montantPrevu = 'Le montant prévu doit être supérieur à 0';
|
|
}
|
|
break;
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const nextStep = () => {
|
|
if (validateStep(activeIndex)) {
|
|
setActiveIndex(prev => Math.min(prev + 1, steps.length - 1));
|
|
}
|
|
};
|
|
|
|
const prevStep = () => {
|
|
setActiveIndex(prev => Math.max(prev - 1, 0));
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSubmitted(true);
|
|
|
|
if (!validateStep(activeIndex)) {
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Veuillez corriger les erreurs du formulaire',
|
|
life: 3000
|
|
});
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
// Préparer les données pour l'envoi
|
|
const chantierToSave: any = {
|
|
nom: chantierForm.nom,
|
|
description: chantierForm.description || '',
|
|
adresse: chantierForm.adresse,
|
|
dateDebut: chantierForm.dateDebut,
|
|
dateFinPrevue: chantierForm.dateFinPrevue || null,
|
|
statut: chantierForm.statut || 'PLANIFIE',
|
|
montantPrevu: Number(chantierForm.montantPrevu) || 0,
|
|
actif: chantierForm.actif,
|
|
clientId: chantierForm.clientId,
|
|
// Nouveaux champs avancés
|
|
typeChantier: chantierForm.typeChantier,
|
|
surface: chantierForm.surface,
|
|
nombreNiveaux: chantierForm.nombreNiveaux,
|
|
dureeEstimeeJours: preview?.dureeEstimee,
|
|
complexite: preview?.complexite?.niveau
|
|
};
|
|
|
|
console.log('Données à envoyer:', chantierToSave);
|
|
|
|
await chantierService.create(chantierToSave);
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: 'Chantier créé avec succès',
|
|
life: 3000
|
|
});
|
|
|
|
setTimeout(() => {
|
|
router.push('/chantiers');
|
|
}, 1000);
|
|
} catch (error: any) {
|
|
console.error('Erreur lors de la création:', error);
|
|
|
|
// Extraire le message d'erreur du backend
|
|
let errorMessage = 'Impossible de créer le chantier';
|
|
if (error.response?.data?.message) {
|
|
errorMessage = error.response.data.message;
|
|
} else if (error.response?.data?.error) {
|
|
errorMessage = error.response.data.error;
|
|
} else if (error.response?.data) {
|
|
errorMessage = JSON.stringify(error.response.data);
|
|
}
|
|
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: errorMessage,
|
|
life: 5000
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
router.push('/chantiers');
|
|
};
|
|
|
|
const onInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, name: string) => {
|
|
const val = (e.target && e.target.value) || '';
|
|
setChantierForm(prev => ({ ...prev, [name]: val }));
|
|
|
|
// Clear error when user starts typing
|
|
if (errors[name]) {
|
|
const newErrors = { ...errors };
|
|
delete newErrors[name];
|
|
setErrors(newErrors);
|
|
}
|
|
};
|
|
|
|
const onDateChange = (e: any, name: string) => {
|
|
setChantierForm(prev => ({ ...prev, [name]: e.value }));
|
|
|
|
if (errors[name]) {
|
|
const newErrors = { ...errors };
|
|
delete newErrors[name];
|
|
setErrors(newErrors);
|
|
}
|
|
};
|
|
|
|
const onNumberChange = (e: any, name: string) => {
|
|
setChantierForm(prev => ({ ...prev, [name]: e.value }));
|
|
|
|
if (errors[name]) {
|
|
const newErrors = { ...errors };
|
|
delete newErrors[name];
|
|
setErrors(newErrors);
|
|
}
|
|
};
|
|
|
|
const onDropdownChange = (e: any, name: string) => {
|
|
setChantierForm(prev => ({ ...prev, [name]: e.value }));
|
|
|
|
if (errors[name]) {
|
|
const newErrors = { ...errors };
|
|
delete newErrors[name];
|
|
setErrors(newErrors);
|
|
}
|
|
};
|
|
|
|
const onCheckboxChange = (e: any, name: string) => {
|
|
setChantierForm(prev => ({ ...prev, [name]: e.checked }));
|
|
};
|
|
|
|
const renderStepContent = () => {
|
|
switch (activeIndex) {
|
|
case 0: // Informations générales
|
|
return (
|
|
<div className="formgrid grid">
|
|
<div className="field col-12">
|
|
<label htmlFor="nom" className="font-bold">
|
|
Nom du chantier <span className="text-red-500">*</span>
|
|
</label>
|
|
<InputText
|
|
id="nom"
|
|
value={chantierForm.nom}
|
|
onChange={(e) => onInputChange(e, 'nom')}
|
|
className={errors.nom ? 'p-invalid' : ''}
|
|
placeholder="Nom du chantier"
|
|
/>
|
|
{errors.nom && <small className="p-error">{errors.nom}</small>}
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<label htmlFor="clientId" className="font-bold">
|
|
Client <span className="text-red-500">*</span>
|
|
</label>
|
|
<Dropdown
|
|
id="clientId"
|
|
value={chantierForm.clientId}
|
|
options={clients}
|
|
onChange={(e) => onDropdownChange(e, 'clientId')}
|
|
placeholder="Sélectionnez un client"
|
|
className={errors.clientId ? 'p-invalid' : ''}
|
|
filter
|
|
/>
|
|
{errors.clientId && <small className="p-error">{errors.clientId}</small>}
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<label htmlFor="description" className="font-bold">Description</label>
|
|
<InputTextarea
|
|
id="description"
|
|
value={chantierForm.description}
|
|
onChange={(e) => onInputChange(e, 'description')}
|
|
rows={4}
|
|
placeholder="Description détaillée du chantier"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 1: // Type de chantier
|
|
return (
|
|
<div className="formgrid grid">
|
|
<div className="field col-12">
|
|
<label htmlFor="typeChantier" className="font-bold">
|
|
Type de chantier <span className="text-red-500">*</span>
|
|
</label>
|
|
<Dropdown
|
|
id="typeChantier"
|
|
value={chantierForm.typeChantier}
|
|
options={chantierTypes}
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
optionGroupLabel="label"
|
|
optionGroupChildren="items"
|
|
onChange={(e) => onDropdownChange(e, 'typeChantier')}
|
|
placeholder="Sélectionnez le type de chantier"
|
|
className={errors.typeChantier ? 'p-invalid' : ''}
|
|
filter
|
|
/>
|
|
{errors.typeChantier && <small className="p-error">{errors.typeChantier}</small>}
|
|
</div>
|
|
|
|
{/* Options de génération automatique */}
|
|
<div className="field col-12">
|
|
<div className="formgrid grid">
|
|
<div className="field col-12 md:col-4">
|
|
<div className="flex align-items-center">
|
|
<Checkbox
|
|
id="autoGenererPhases"
|
|
checked={chantierForm.autoGenererPhases}
|
|
onChange={(e) => onCheckboxChange(e, 'autoGenererPhases')}
|
|
/>
|
|
<label htmlFor="autoGenererPhases" className="ml-2 font-bold">
|
|
Générer automatiquement les phases
|
|
</label>
|
|
</div>
|
|
<small className="text-600">
|
|
Les phases seront créées selon le type de chantier
|
|
</small>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-4">
|
|
<div className="flex align-items-center">
|
|
<Checkbox
|
|
id="inclureSousPhases"
|
|
checked={chantierForm.inclureSousPhases}
|
|
onChange={(e) => onCheckboxChange(e, 'inclureSousPhases')}
|
|
disabled={!chantierForm.autoGenererPhases}
|
|
/>
|
|
<label htmlFor="inclureSousPhases" className="ml-2 font-bold">
|
|
Inclure les sous-phases
|
|
</label>
|
|
</div>
|
|
<small className="text-600">
|
|
Détailler chaque phase principale
|
|
</small>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-4">
|
|
<div className="flex align-items-center">
|
|
<Checkbox
|
|
id="ajusterDelais"
|
|
checked={chantierForm.ajusterDelais}
|
|
onChange={(e) => onCheckboxChange(e, 'ajusterDelais')}
|
|
disabled={!chantierForm.autoGenererPhases}
|
|
/>
|
|
<label htmlFor="ajusterDelais" className="ml-2 font-bold">
|
|
Ajuster les délais
|
|
</label>
|
|
</div>
|
|
<small className="text-600">
|
|
Adapter selon la complexité
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Prévisualisation des phases */}
|
|
{chantierForm.typeChantier && chantierForm.autoGenererPhases && preview && (
|
|
<div className="field col-12">
|
|
<Card title="Prévisualisation des phases" className="mt-4">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<div className="text-center p-3 border-1 surface-border border-round">
|
|
<div className="text-xl font-bold text-primary">{preview.phasesCount}</div>
|
|
<div className="text-600">Phases principales</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<div className="text-center p-3 border-1 surface-border border-round">
|
|
<div className="text-xl font-bold text-primary">{preview.sousePhasesCount}</div>
|
|
<div className="text-600">Sous-phases détaillées</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<div className="text-center p-3 border-1 surface-border border-round">
|
|
<div className="text-xl font-bold text-orange-500">{preview.dureeEstimee}</div>
|
|
<div className="text-600">Jours estimés</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<div className="text-center p-3 border-1 surface-border border-round">
|
|
<Tag
|
|
value={preview.complexite.niveau}
|
|
severity={
|
|
preview.complexite.niveau === 'SIMPLE' ? 'success' :
|
|
preview.complexite.niveau === 'MOYEN' ? 'warning' :
|
|
preview.complexite.niveau === 'COMPLEXE' ? 'danger' : 'danger'
|
|
}
|
|
/>
|
|
<div className="text-600 mt-1">Complexité estimée</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{calculatingPreview && (
|
|
<div className="flex align-items-center justify-content-center mt-3">
|
|
<ProgressBar mode="indeterminate" style={{ height: '4px' }} />
|
|
<span className="ml-2 text-600">Calcul en cours...</span>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
case 2: // Configuration technique
|
|
return (
|
|
<div className="formgrid grid">
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="surface" className="font-bold">
|
|
Surface (m²)
|
|
</label>
|
|
<InputNumber
|
|
id="surface"
|
|
value={chantierForm.surface}
|
|
onValueChange={(e) => onNumberChange(e, 'surface')}
|
|
mode="decimal"
|
|
locale="fr-FR"
|
|
minFractionDigits={0}
|
|
maxFractionDigits={2}
|
|
className={errors.surface ? 'p-invalid' : ''}
|
|
placeholder="Surface en m²"
|
|
/>
|
|
{errors.surface && <small className="p-error">{errors.surface}</small>}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="nombreNiveaux" className="font-bold">
|
|
Nombre de niveaux
|
|
</label>
|
|
<InputNumber
|
|
id="nombreNiveaux"
|
|
value={chantierForm.nombreNiveaux}
|
|
onValueChange={(e) => onNumberChange(e, 'nombreNiveaux')}
|
|
min={1}
|
|
max={20}
|
|
placeholder="Nombre de niveaux"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="natureTerrain" className="font-bold">Nature du terrain</label>
|
|
<Dropdown
|
|
id="natureTerrain"
|
|
value={chantierForm.natureTerrain}
|
|
options={[
|
|
{ label: 'Terrain plat', value: 'PLAT' },
|
|
{ label: 'Pente légère', value: 'PENTE_LEGERE' },
|
|
{ label: 'Pente forte', value: 'PENTE_FORTE' },
|
|
{ label: 'Terrain rocheux', value: 'ROCHEUX' },
|
|
{ label: 'Terrain humide', value: 'HUMIDE' },
|
|
{ label: 'Terrain instable', value: 'INSTABLE' }
|
|
]}
|
|
onChange={(e) => onDropdownChange(e, 'natureTerrain')}
|
|
placeholder="Sélectionnez la nature du terrain"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="accessibiliteChantier" className="font-bold">Accessibilité du chantier</label>
|
|
<Dropdown
|
|
id="accessibiliteChantier"
|
|
value={chantierForm.accessibiliteChantier}
|
|
options={[
|
|
{ label: 'Facile - Accès direct, terrain plat', value: 'FACILE' },
|
|
{ label: 'Moyenne - Quelques contraintes d\'accès', value: 'MOYENNE' },
|
|
{ label: 'Difficile - Accès restreint, terrain complexe', value: 'DIFFICILE' }
|
|
]}
|
|
onChange={(e) => onDropdownChange(e, 'accessibiliteChantier')}
|
|
placeholder="Sélectionnez l'accessibilité"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<div className="formgrid grid">
|
|
<div className="field col-12 md:col-4">
|
|
<div className="flex align-items-center">
|
|
<Checkbox
|
|
id="presenceReseaux"
|
|
checked={chantierForm.presenceReseaux}
|
|
onChange={(e) => onCheckboxChange(e, 'presenceReseaux')}
|
|
/>
|
|
<label htmlFor="presenceReseaux" className="ml-2 font-bold">
|
|
Réseaux existants
|
|
</label>
|
|
</div>
|
|
<small className="text-600">
|
|
Électricité, eau, gaz présents
|
|
</small>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-4">
|
|
<div className="flex align-items-center">
|
|
<Checkbox
|
|
id="stockagePossible"
|
|
checked={chantierForm.stockagePossible}
|
|
onChange={(e) => onCheckboxChange(e, 'stockagePossible')}
|
|
/>
|
|
<label htmlFor="stockagePossible" className="ml-2 font-bold">
|
|
Stockage possible
|
|
</label>
|
|
</div>
|
|
<small className="text-600">
|
|
Espace pour matériaux disponible
|
|
</small>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-4">
|
|
<div className="flex align-items-center">
|
|
<Checkbox
|
|
id="accesPMR"
|
|
checked={chantierForm.accesPMR}
|
|
onChange={(e) => onCheckboxChange(e, 'accesPMR')}
|
|
/>
|
|
<label htmlFor="accesPMR" className="ml-2 font-bold">
|
|
Accès PMR requis
|
|
</label>
|
|
</div>
|
|
<small className="text-600">
|
|
Accessibilité personnes handicapées
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<label htmlFor="performanceEnergetique" className="font-bold">Performance énergétique</label>
|
|
<Dropdown
|
|
id="performanceEnergetique"
|
|
value={chantierForm.performanceEnergetique}
|
|
options={[
|
|
{ label: 'Standard (réglementation minimale)', value: 'STANDARD' },
|
|
{ label: 'RT 2012', value: 'RT2012' },
|
|
{ label: 'RT 2020', value: 'RT2020' },
|
|
{ label: 'RE 2020 (obligatoire neuf)', value: 'RE2020' },
|
|
{ label: 'Bâtiment passif', value: 'PASSIF' },
|
|
{ label: 'Bâtiment à énergie positive', value: 'POSITIF' }
|
|
]}
|
|
onChange={(e) => onDropdownChange(e, 'performanceEnergetique')}
|
|
placeholder="Sélectionnez la performance énergétique"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 3: // Localisation
|
|
return (
|
|
<div className="formgrid grid">
|
|
<div className="field col-12">
|
|
<label htmlFor="adresse" className="font-bold">
|
|
Adresse du chantier <span className="text-red-500">*</span>
|
|
</label>
|
|
<InputTextarea
|
|
id="adresse"
|
|
value={chantierForm.adresse}
|
|
onChange={(e) => onInputChange(e, 'adresse')}
|
|
rows={3}
|
|
className={errors.adresse ? 'p-invalid' : ''}
|
|
placeholder="Adresse complète du chantier"
|
|
/>
|
|
{errors.adresse && <small className="p-error">{errors.adresse}</small>}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="codePostal" className="font-bold">Code postal</label>
|
|
<InputText
|
|
id="codePostal"
|
|
value={chantierForm.codePostal}
|
|
onChange={(e) => onInputChange(e, 'codePostal')}
|
|
placeholder="Code postal"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="ville" className="font-bold">Ville</label>
|
|
<InputText
|
|
id="ville"
|
|
value={chantierForm.ville}
|
|
onChange={(e) => onInputChange(e, 'ville')}
|
|
placeholder="Ville"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 4: // Planification
|
|
return (
|
|
<div className="formgrid grid">
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="dateDebut" className="font-bold">
|
|
Date de début <span className="text-red-500">*</span>
|
|
</label>
|
|
<Calendar
|
|
id="dateDebut"
|
|
value={chantierForm.dateDebut ? new Date(chantierForm.dateDebut) : null}
|
|
onChange={(e) => onDateChange(e, 'dateDebut')}
|
|
dateFormat="dd/mm/yy"
|
|
showIcon
|
|
className={errors.dateDebut ? 'p-invalid' : ''}
|
|
/>
|
|
{errors.dateDebut && <small className="p-error">{errors.dateDebut}</small>}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="dateFinPrevue" className="font-bold">
|
|
Date de fin prévue
|
|
</label>
|
|
<Calendar
|
|
id="dateFinPrevue"
|
|
value={chantierForm.dateFinPrevue ? new Date(chantierForm.dateFinPrevue) : null}
|
|
onChange={(e) => onDateChange(e, 'dateFinPrevue')}
|
|
dateFormat="dd/mm/yy"
|
|
showIcon
|
|
className={errors.dateFinPrevue ? 'p-invalid' : ''}
|
|
minDate={chantierForm.dateDebut ? new Date(chantierForm.dateDebut) : undefined}
|
|
/>
|
|
{errors.dateFinPrevue && <small className="p-error">{errors.dateFinPrevue}</small>}
|
|
{preview?.dateFinEstimee && (
|
|
<small className="text-600">
|
|
Date calculée automatiquement : {preview.dateFinEstimee.toLocaleDateString('fr-FR')}
|
|
</small>
|
|
)}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="statut" className="font-bold">Statut initial</label>
|
|
<Dropdown
|
|
id="statut"
|
|
value={chantierForm.statut}
|
|
options={statuts}
|
|
onChange={(e) => onDropdownChange(e, 'statut')}
|
|
placeholder="Sélectionnez un statut"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="margeSecurite" className="font-bold">Marge de sécurité (jours)</label>
|
|
<InputNumber
|
|
id="margeSecurite"
|
|
value={chantierForm.margeSecurite}
|
|
onValueChange={(e) => onNumberChange(e, 'margeSecurite')}
|
|
min={0}
|
|
max={30}
|
|
placeholder="Jours de marge"
|
|
/>
|
|
<small className="text-600">
|
|
Délai supplémentaire pour imprévus
|
|
</small>
|
|
</div>
|
|
|
|
{/* Prévisualisation du planning */}
|
|
{preview && (
|
|
<div className="field col-12">
|
|
<Card title="Planning estimé" className="mt-3">
|
|
<Timeline
|
|
value={[
|
|
{
|
|
status: 'Début projet',
|
|
date: chantierForm.dateDebut,
|
|
icon: 'pi pi-play',
|
|
color: '#9C27B0'
|
|
},
|
|
{
|
|
status: 'Fin estimée',
|
|
date: preview.dateFinEstimee.toLocaleDateString('fr-FR'),
|
|
icon: 'pi pi-check',
|
|
color: '#673AB7'
|
|
}
|
|
]}
|
|
opposite={(item) => item.status}
|
|
content={(item) => <small className="text-color-secondary">{item.date}</small>}
|
|
/>
|
|
|
|
<div className="mt-3 p-3 surface-100 border-round">
|
|
<div className="flex align-items-center justify-content-between">
|
|
<span className="font-semibold">Durée totale estimée:</span>
|
|
<Badge value={`${preview.dureeEstimee} jours`} severity="info" />
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
case 5: // Options avancées
|
|
return (
|
|
<div className="formgrid grid">
|
|
<div className="field col-12">
|
|
<label htmlFor="montantPrevu" className="font-bold">
|
|
Montant prévu
|
|
</label>
|
|
<InputNumber
|
|
id="montantPrevu"
|
|
value={chantierForm.montantPrevu}
|
|
onValueChange={(e) => onNumberChange(e, 'montantPrevu')}
|
|
mode="currency"
|
|
currency="EUR"
|
|
locale="fr-FR"
|
|
className={errors.montantPrevu ? 'p-invalid' : ''}
|
|
/>
|
|
{errors.montantPrevu && <small className="p-error">{errors.montantPrevu}</small>}
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<label htmlFor="contraintes" className="font-bold">Contraintes particulières</label>
|
|
<InputTextarea
|
|
id="contraintes"
|
|
value={chantierForm.contraintes}
|
|
onChange={(e) => onInputChange(e, 'contraintes')}
|
|
rows={3}
|
|
placeholder="Décrivez les contraintes particulières du chantier"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<div className="formgrid grid">
|
|
<div className="field col-12 md:col-6">
|
|
<div className="flex align-items-center">
|
|
<Checkbox
|
|
id="permisRequis"
|
|
checked={chantierForm.permisRequis}
|
|
onChange={(e) => onCheckboxChange(e, 'permisRequis')}
|
|
/>
|
|
<label htmlFor="permisRequis" className="ml-2 font-bold">
|
|
Permis de construire requis
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<div className="flex align-items-center">
|
|
<Checkbox
|
|
id="controleUrbanismeRequis"
|
|
checked={chantierForm.controleUrbanismeRequis}
|
|
onChange={(e) => onCheckboxChange(e, 'controleUrbanismeRequis')}
|
|
/>
|
|
<label htmlFor="controleUrbanismeRequis" className="ml-2 font-bold">
|
|
Contrôle urbanisme requis
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{chantierForm.permisRequis && (
|
|
<div className="field col-12">
|
|
<div className="formgrid grid">
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="numeroPermis" className="font-bold">Numéro de permis</label>
|
|
<InputText
|
|
id="numeroPermis"
|
|
value={chantierForm.numeroPermis}
|
|
onChange={(e) => onInputChange(e, 'numeroPermis')}
|
|
placeholder="Numéro du permis de construire"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="dateObtentionPermis" className="font-bold">Date d'obtention</label>
|
|
<Calendar
|
|
id="dateObtentionPermis"
|
|
value={chantierForm.dateObtentionPermis ? new Date(chantierForm.dateObtentionPermis) : null}
|
|
onChange={(e) => onDateChange(e, 'dateObtentionPermis')}
|
|
dateFormat="dd/mm/yy"
|
|
showIcon
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="field col-12">
|
|
<div className="flex align-items-center">
|
|
<Checkbox
|
|
id="actif"
|
|
checked={chantierForm.actif}
|
|
onChange={(e) => onCheckboxChange(e, 'actif')}
|
|
/>
|
|
<label htmlFor="actif" className="ml-2 font-bold">
|
|
Chantier actif
|
|
</label>
|
|
</div>
|
|
<small className="text-600">
|
|
Un chantier inactif n'apparaîtra pas dans les listes de sélection
|
|
</small>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 6: // Prévisualisation
|
|
return (
|
|
<div className="formgrid grid">
|
|
<div className="field col-12">
|
|
<h3 className="text-primary">Récapitulatif du projet</h3>
|
|
<Divider />
|
|
</div>
|
|
|
|
{/* Informations générales dans un layout plus compact */}
|
|
<div className="field col-12">
|
|
<Card title="Informations générales" className="mb-3">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<div className="flex flex-column gap-2">
|
|
<div>
|
|
<span className="font-semibold text-600">Nom du chantier: </span>
|
|
<span>{chantierForm.nom}</span>
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold text-600">Client: </span>
|
|
<span>{clients.find(c => c.value === chantierForm.clientId)?.label}</span>
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold text-600">Type de chantier: </span>
|
|
<span>{chantierTypes.find(group =>
|
|
group.items?.find((item: any) => item.value === chantierForm.typeChantier)
|
|
)?.items?.find((item: any) => item.value === chantierForm.typeChantier)?.label}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<div className="flex flex-column gap-2">
|
|
<div>
|
|
<span className="font-semibold text-600">Surface: </span>
|
|
<span>{chantierForm.surface ? `${chantierForm.surface} m²` : 'Non spécifiée'}</span>
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold text-600">Date de début: </span>
|
|
<span>{chantierForm.dateDebut}</span>
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold text-600">Montant prévu: </span>
|
|
<span>{chantierForm.montantPrevu?.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR' }) || 'Non spécifié'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Planning détaillé des phases */}
|
|
{chantierForm.typeChantier && chantierForm.autoGenererPhases && (
|
|
<div className="field col-12">
|
|
<PhasesTimelinePreview
|
|
typeChantier={chantierForm.typeChantier}
|
|
dateDebut={chantierForm.dateDebut ? new Date(chantierForm.dateDebut) : new Date()}
|
|
surface={chantierForm.surface}
|
|
nombreNiveaux={chantierForm.nombreNiveaux}
|
|
inclureSousPhases={chantierForm.inclureSousPhases}
|
|
ajusterDelais={chantierForm.ajusterDelais}
|
|
margeSecurite={chantierForm.margeSecurite}
|
|
compact={false}
|
|
showDetails={true}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Configuration et options */}
|
|
<div className="field col-12">
|
|
<Card title="Configuration" className="mb-3">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-4">
|
|
<div className="flex align-items-center gap-2 mb-2">
|
|
<i className={`pi ${chantierForm.autoGenererPhases ? 'pi-check text-green-500' : 'pi-times text-red-500'}`}></i>
|
|
<span>Génération automatique des phases</span>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-4">
|
|
<div className="flex align-items-center gap-2 mb-2">
|
|
<i className={`pi ${chantierForm.inclureSousPhases ? 'pi-check text-green-500' : 'pi-times text-red-500'}`}></i>
|
|
<span>Sous-phases incluses</span>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-4">
|
|
<div className="flex align-items-center gap-2 mb-2">
|
|
<i className={`pi ${chantierForm.ajusterDelais ? 'pi-check text-green-500' : 'pi-times text-red-500'}`}></i>
|
|
<span>Ajustement des délais</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 7: // Validation
|
|
return (
|
|
<div className="formgrid grid">
|
|
<div className="field col-12">
|
|
<div className="surface-card p-4 border-round shadow-2">
|
|
<div className="flex align-items-center mb-3">
|
|
<i className="pi pi-check-circle text-4xl text-green-500 mr-3"></i>
|
|
<div>
|
|
<h3 className="m-0 text-green-600">Projet prêt à être créé</h3>
|
|
<p className="text-600 m-0">Vérifiez les informations avant de finaliser</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<div className="border-1 surface-border border-round p-3">
|
|
<h4 className="mt-0 mb-2">Informations générales</h4>
|
|
<ul className="list-none p-0 m-0">
|
|
<li className="pb-2">✓ Nom du chantier défini</li>
|
|
<li className="pb-2">✓ Client sélectionné</li>
|
|
<li className="pb-2">✓ Type de chantier choisi</li>
|
|
<li className="pb-2">✓ Localisation renseignée</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<div className="border-1 surface-border border-round p-3">
|
|
<h4 className="mt-0 mb-2">Configuration</h4>
|
|
<ul className="list-none p-0 m-0">
|
|
<li className="pb-2">✓ Planification configurée</li>
|
|
{chantierForm.autoGenererPhases && <li className="pb-2">✓ Phases automatiques activées</li>}
|
|
{chantierForm.inclureSousPhases && <li className="pb-2">✓ Sous-phases incluses</li>}
|
|
{chantierForm.montantPrevu && <li className="pb-2">✓ Budget défini</li>}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Divider />
|
|
|
|
<div className="text-center">
|
|
<p className="text-600 mb-3">
|
|
En cliquant sur "Créer le chantier", le projet sera enregistré et
|
|
{chantierForm.autoGenererPhases ? ' les phases seront générées automatiquement' : ' vous pourrez ajouter les phases manuellement'}.
|
|
</p>
|
|
<div className="flex justify-content-center gap-2">
|
|
<i className="pi pi-info-circle text-blue-500"></i>
|
|
<small className="text-600">
|
|
Vous pourrez modifier ces informations après la création
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card>
|
|
<Toast ref={toast} />
|
|
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<h2>Nouveau Chantier</h2>
|
|
<Button
|
|
icon="pi pi-arrow-left"
|
|
label="Retour"
|
|
className="p-button-text"
|
|
onClick={handleCancel}
|
|
/>
|
|
</div>
|
|
|
|
<Steps model={steps} activeIndex={activeIndex} className="mb-4" />
|
|
|
|
<form onSubmit={handleSubmit} className="p-fluid">
|
|
{renderStepContent()}
|
|
|
|
<Divider />
|
|
|
|
<div className="flex justify-content-between">
|
|
<Button
|
|
type="button"
|
|
label="Précédent"
|
|
icon="pi pi-chevron-left"
|
|
className="p-button-text"
|
|
onClick={prevStep}
|
|
disabled={activeIndex === 0}
|
|
/>
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="button"
|
|
label="Annuler"
|
|
icon="pi pi-times"
|
|
className="p-button-text"
|
|
onClick={handleCancel}
|
|
disabled={loading}
|
|
/>
|
|
|
|
{activeIndex < steps.length - 1 ? (
|
|
<Button
|
|
type="button"
|
|
label="Suivant"
|
|
icon="pi pi-chevron-right"
|
|
iconPos="right"
|
|
onClick={nextStep}
|
|
/>
|
|
) : (
|
|
<Button
|
|
type="submit"
|
|
label="Créer le chantier"
|
|
icon="pi pi-check"
|
|
loading={loading}
|
|
disabled={loading}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default NouveauChantierPage;
|