'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(null); const [loading, setLoading] = useState(false); const [submitted, setSubmitted] = useState(false); const [activeIndex, setActiveIndex] = useState(0); const [clients, setClients] = useState([]); const [chantierTypes, setChantierTypes] = useState([]); const [preview, setPreview] = useState(null); const [calculatingPreview, setCalculatingPreview] = useState(false); const [chantierForm, setChantierForm] = useState({ 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>({}); 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 (toastRef.current) { toastRef.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 = { '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', 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 (toastRef.current) { toastRef.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 = {}; 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 instanceof Date ? chantierForm.dateDebut.toISOString().split('T')[0] : 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, 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 (
onInputChange(e, 'nom')} className={errors.nom ? 'p-invalid' : ''} placeholder="Nom du chantier" /> {errors.nom && {errors.nom}}
onDropdownChange(e, 'clientId')} placeholder="Sélectionnez un client" className={errors.clientId ? 'p-invalid' : ''} filter /> {errors.clientId && {errors.clientId}}
onInputChange(e, 'description')} rows={4} placeholder="Description détaillée du chantier" />
); case 1: // Type de chantier return (
onDropdownChange(e, 'typeChantier')} placeholder="Sélectionnez le type de chantier" className={errors.typeChantier ? 'p-invalid' : ''} filter /> {errors.typeChantier && {errors.typeChantier}}
{/* Options de génération automatique */}
onCheckboxChange(e, 'autoGenererPhases')} />
Les phases seront créées selon le type de chantier
onCheckboxChange(e, 'inclureSousPhases')} disabled={!chantierForm.autoGenererPhases} />
Détailler chaque phase principale
onCheckboxChange(e, 'ajusterDelais')} disabled={!chantierForm.autoGenererPhases} />
Adapter selon la complexité
{/* Prévisualisation des phases */} {chantierForm.typeChantier && chantierForm.autoGenererPhases && preview && (
{preview.phasesCount}
Phases principales
{preview.sousePhasesCount}
Sous-phases détaillées
{preview.dureeEstimee}
Jours estimés
Complexité estimée
{calculatingPreview && (
Calcul en cours...
)}
)}
); case 2: // Configuration technique return (
onNumberChange(e, 'surface')} mode="decimal" locale="fr-FR" minFractionDigits={0} maxFractionDigits={2} className={errors.surface ? 'p-invalid' : ''} placeholder="Surface en m²" /> {errors.surface && {errors.surface}}
onNumberChange(e, 'nombreNiveaux')} min={1} max={20} placeholder="Nombre de niveaux" />
onDropdownChange(e, 'natureTerrain')} placeholder="Sélectionnez la nature du terrain" />
onDropdownChange(e, 'accessibiliteChantier')} placeholder="Sélectionnez l'accessibilité" />
onCheckboxChange(e, 'presenceReseaux')} />
Électricité, eau, gaz présents
onCheckboxChange(e, 'stockagePossible')} />
Espace pour matériaux disponible
onCheckboxChange(e, 'accesPMR')} />
Accessibilité personnes handicapées
onDropdownChange(e, 'performanceEnergetique')} placeholder="Sélectionnez la performance énergétique" />
); case 3: // Localisation return (
onInputChange(e, 'adresse')} rows={3} className={errors.adresse ? 'p-invalid' : ''} placeholder="Adresse complète du chantier" /> {errors.adresse && {errors.adresse}}
onInputChange(e, 'codePostal')} placeholder="Code postal" />
onInputChange(e, 'ville')} placeholder="Ville" />
); case 4: // Planification return (
onDateChange(e, 'dateDebut')} dateFormat="dd/mm/yy" showIcon className={errors.dateDebut ? 'p-invalid' : ''} /> {errors.dateDebut && {errors.dateDebut}}
onDateChange(e, 'dateFinPrevue')} dateFormat="dd/mm/yy" showIcon className={errors.dateFinPrevue ? 'p-invalid' : ''} minDate={chantierForm.dateDebut ? new Date(chantierForm.dateDebut) : undefined} /> {errors.dateFinPrevue && {errors.dateFinPrevue}} {preview?.dateFinEstimee && ( Date calculée automatiquement : {preview.dateFinEstimee.toLocaleDateString('fr-FR')} )}
onDropdownChange(e, 'statut')} placeholder="Sélectionnez un statut" />
onNumberChange(e, 'margeSecurite')} min={0} max={30} placeholder="Jours de marge" /> Délai supplémentaire pour imprévus
{/* Prévisualisation du planning */} {preview && (
item.status} content={(item) => {item.date}} />
Durée totale estimée:
)}
); case 5: // Options avancées return (
onNumberChange(e, 'montantPrevu')} mode="currency" currency="EUR" locale="fr-FR" className={errors.montantPrevu ? 'p-invalid' : ''} /> {errors.montantPrevu && {errors.montantPrevu}}
onInputChange(e, 'contraintes')} rows={3} placeholder="Décrivez les contraintes particulières du chantier" />
onCheckboxChange(e, 'permisRequis')} />
onCheckboxChange(e, 'controleUrbanismeRequis')} />
{chantierForm.permisRequis && (
onInputChange(e, 'numeroPermis')} placeholder="Numéro du permis de construire" />
onDateChange(e, 'dateObtentionPermis')} dateFormat="dd/mm/yy" showIcon />
)}
onCheckboxChange(e, 'actif')} />
Un chantier inactif n'apparaîtra pas dans les listes de sélection
); case 6: // Prévisualisation return (

Récapitulatif du projet

{/* Informations générales dans un layout plus compact */}
Nom du chantier: {chantierForm.nom}
Client: {clients.find(c => c.value === chantierForm.clientId)?.label}
Type de chantier: {chantierTypes.find(group => group.items?.find((item: any) => item.value === chantierForm.typeChantier) )?.items?.find((item: any) => item.value === chantierForm.typeChantier)?.label}
Surface: {chantierForm.surface ? `${chantierForm.surface} m²` : 'Non spécifiée'}
Date de début: {chantierForm.dateDebut}
Montant prévu: {chantierForm.montantPrevu?.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR' }) || 'Non spécifié'}
{/* Planning détaillé des phases */} {chantierForm.typeChantier && chantierForm.autoGenererPhases && (
)} {/* Configuration et options */}
Génération automatique des phases
Sous-phases incluses
Ajustement des délais
); case 7: // Validation return (

Projet prêt à être créé

Vérifiez les informations avant de finaliser

Informations générales

  • ✓ Nom du chantier défini
  • ✓ Client sélectionné
  • ✓ Type de chantier choisi
  • ✓ Localisation renseignée

Configuration

  • ✓ Planification configurée
  • {chantierForm.autoGenererPhases &&
  • ✓ Phases automatiques activées
  • } {chantierForm.inclureSousPhases &&
  • ✓ Sous-phases incluses
  • } {chantierForm.montantPrevu &&
  • ✓ Budget défini
  • }

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'}.

Vous pourrez modifier ces informations après la création
); default: return null; } }; return (

Nouveau Chantier

{renderStepContent()}
); }; export default NouveauChantierPage;