import React, { useState, useEffect, useCallback } from 'react'; import NavPanel from '../../components/nav-panel/NavPanel'; import { getAllHoraires, formatHeure, updateHoraireComposition } from '../../api/horaires'; import { createPerturbation } from '../../api/perturbations'; import { getAllGares } from '../../api/gares'; import { getAllMateriels } from '../../api/materielsRoulants'; import { CAUSES_PERTURBATION, buildArretFromHoraire, recomputePropagation, buildPayload, ArretCard, JoursPerturbationEditor, ParcoursPreview, doesHoraireCirculateOnDate, } from './perturbationsUtils'; import { canPerform, getCurrentUser, getScopedRegionId } from '../../utils/userUtils'; import './Perturbations.css'; // ── Utilitaires d'attribution et d'impacts ───────────────────────────────── const timeToMinutes = (t) => { if (!t || typeof t !== 'string') return null; const [h, m] = t.split(':').map(Number); if (isNaN(h) || isNaN(m)) return null; return h * 60 + m; }; const isWithinTwoHours = (t1, t2) => { const m1 = timeToMinutes(t1); const m2 = timeToMinutes(t2); if (m1 === null || m2 === null) return false; let diff = m2 - m1; if (diff < 0) diff += 24 * 60; return diff >= 0 && diff <= 120; }; const getMaterielIds = (horaire) => { if (!horaire) return []; if (Array.isArray(horaire.composition_train)) { return horaire.composition_train.map(item => item.materiel_id).filter(Boolean); } return []; }; export default function CreerPerturbation() { const currentUser = getCurrentUser(); const canCreatePerturbations = canPerform(currentUser, 'perturbations', 'create'); // ── Navigation ──────────────────────────────────────────────────────────── const [currentStep, setCurrentStep] = useState('horaire'); // ── Horaires ────────────────────────────────────────────────────────────── const [horaires, setHoraires] = useState([]); const [gares, setGares] = useState([]); const [loadingHoraires, setLoadingHoraires] = useState(false); const [searchDate, setSearchDate] = useState(new Date().toISOString().split('T')[0]); const [searchMode, setSearchMode] = useState('train'); const [searchQuery, setSearchQuery] = useState(''); const [filteredHoraires, setFilteredHoraires] = useState([]); const [showResults, setShowResults] = useState(false); const [selectedHoraires, setSelectedHoraires] = useState([]); // ── Formulaire perturbation ─────────────────────────────────────────────── const [cause, setCause] = useState('incident_technique'); const [causeDetails, setCauseDetails] = useState(''); const [sillonSubstitutionId, setSillonSubstitutionId] = useState(''); const [joursPerturbation, setJoursPerturbation] = useState([{ type: 'jour', date: '', dateDebut: '', dateFin: '' }]); const [actif, setActif] = useState(true); const [notes, setNotes] = useState(''); const [materiels, setMateriels] = useState([]); const [assigningMateriel, setAssigningMateriel] = useState(false); // ── Arrêts ──────────────────────────────────────────────────────────────── const [perturbationsArretsMap, setPerturbationsArretsMap] = useState({}); const [openTrains, setOpenTrains] = useState({}); const [openStops, setOpenStops] = useState({}); const [saving, setSaving] = useState(false); const [scopedRegionId, setScopedRegionId] = useState(null); // ── Chargement des horaires ─────────────────────────────────────────────── useEffect(() => { const regionId = getScopedRegionId(getCurrentUser()); setScopedRegionId(regionId || null); setLoadingHoraires(true); getAllHoraires(regionId) .then(({ data, error }) => { if (!error && data) { setHoraires(data); setFilteredHoraires(data); } else { setHoraires([]); setFilteredHoraires([]); } }) .catch(() => { setHoraires([]); setFilteredHoraires([]); }) .finally(() => setLoadingHoraires(false)); getAllGares(regionId).then(({ data }) => { if (data) setGares(data); }); getAllMateriels().then(({ data }) => { if (data) setMateriels(data); }); }, []); // ── Recherche ───────────────────────────────────────────────────────────── const handleSearch = useCallback(() => { const q = (searchQuery || '').toLowerCase().trim(); let filtered = horaires.filter(h => doesHoraireCirculateOnDate(h, searchDate)); if (searchMode === 'train') { filtered = filtered.filter(h => [h.numero_train, h.type_train, h.gare_depart_nom, h.gare_arrivee_nom, h.ligne_nom] .map(f => (f || '').toString().toLowerCase()) .some(f => f.includes(q)) ); } else { // mode ligne filtered = filtered.filter(h => (h.ligne_nom || '').toLowerCase().includes(q) ); } filtered.sort((a, b) => { const aNum = a.numero_train || ''; const bNum = b.numero_train || ''; return aNum.toString().localeCompare(bNum.toString(), undefined, { numeric: true }); }); setFilteredHoraires(filtered); setShowResults(true); }, [searchQuery, horaires, searchDate, searchMode]); const handleToggleSelectHoraire = useCallback((h, isSelected) => { setSelectedHoraires(prev => { if (isSelected) return [...prev, h]; return prev.filter(item => item.id !== h.id); }); setPerturbationsArretsMap(prev => { const next = { ...prev }; if (isSelected) { next[h.id] = buildArretFromHoraire(h); } else { delete next[h.id]; } return next; }); setOpenTrains(prev => ({ ...prev, [h.id]: true })); }, []); const handleSelectSingleHoraire = useCallback((h) => { setSelectedHoraires([h]); setSearchQuery(`${h.numero_train} - ${h.gare_depart_nom} vers ${h.gare_arrivee_nom}`); setShowResults(false); setPerturbationsArretsMap({ [h.id]: buildArretFromHoraire(h) }); setOpenTrains({ [h.id]: true }); setOpenStops({}); }, []); const handleClearSearch = useCallback(() => { setSearchQuery(''); setFilteredHoraires(horaires); setShowResults(false); setSelectedHoraires([]); setPerturbationsArretsMap({}); setOpenTrains({}); setOpenStops({}); }, [horaires]); // ── Gestion des perturbations par arrêt ─────────────────────────────────── const toggleTrainAccordion = useCallback((hId) => { setOpenTrains(p => ({ ...p, [hId]: !p[hId] })); }, []); const toggleStopAccordion = useCallback((hId, i) => { setOpenStops(p => { const current = p[hId] || {}; return { ...p, [hId]: { ...current, [i]: !current[i] } }; }); }, []); const handleToggleType = useCallback((hId, index, typeValue, checked) => { setPerturbationsArretsMap(prev => { const nextMap = { ...prev }; const nextArr = [...nextMap[hId]]; const a = { ...nextArr[index] }; let types = [...(a.types_perturbation || [])]; if (checked) { if (!types.includes(typeValue)) types.push(typeValue); if (typeValue === 'retard') { a.manual_retard = true; a.retard_minutes = Number(a.retard_minutes) > 0 ? a.retard_minutes : 15; } if (typeValue === 'reduction_retard') { a.manual_reduction = true; a.reduction_retard_minutes = Number(a.reduction_retard_minutes) > 0 ? a.reduction_retard_minutes : 5; } if (typeValue === 'arret_prolonge') { a.manual_arret_prolonge = true; a.arret_prolonge_minutes = Number(a.arret_prolonge_minutes) > 0 ? a.arret_prolonge_minutes : 5; } if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; } } else { types = types.filter(t => t !== typeValue); if (typeValue === 'retard') { a.manual_retard = false; a.retard_minutes = 0; } if (typeValue === 'reduction_retard') { a.manual_reduction = false; a.reduction_retard_minutes = 0; } if (typeValue === 'arret_prolonge') { a.manual_arret_prolonge = false; a.arret_prolonge_minutes = 0; } if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; } } a.types_perturbation = types; nextArr[index] = a; nextMap[hId] = recomputePropagation(nextArr); return nextMap; }); }, []); const handleArretChange = useCallback((hId, index, field, value) => { setPerturbationsArretsMap(prev => { const nextMap = { ...prev }; const nextArr = [...nextMap[hId]]; const a = { ...nextArr[index], [field]: value }; if (field === 'retard_minutes') a.manual_retard = true; if (field === 'reduction_retard_minutes') a.manual_reduction = true; if (field === 'arret_prolonge_minutes') a.manual_arret_prolonge = true; nextArr[index] = a; nextMap[hId] = recomputePropagation(nextArr); return nextMap; }); }, []); const handleSetNouveauTerminus = useCallback((hId, index) => { setPerturbationsArretsMap(prev => { const nextMap = { ...prev }; const nextArr = nextMap[hId].map((a, i) => { const types = [...(a.types_perturbation || [])]; if (i === index) { if (!types.includes('modification_parcours')) types.push('modification_parcours'); return { ...a, types_perturbation: types, is_nouveau_terminus: true, is_nouvelle_origine: false }; } return { ...a, is_nouveau_terminus: false }; }); nextMap[hId] = recomputePropagation(nextArr); return nextMap; }); }, []); const handleSetNouvelleOrigine = useCallback((hId, index) => { setPerturbationsArretsMap(prev => { const nextMap = { ...prev }; const nextArr = nextMap[hId].map((a, i) => { const types = [...(a.types_perturbation || [])]; if (i === index) { if (!types.includes('modification_parcours')) types.push('modification_parcours'); return { ...a, types_perturbation: types, is_nouvelle_origine: true, is_nouveau_terminus: false }; } return { ...a, is_nouvelle_origine: false }; }); nextMap[hId] = recomputePropagation(nextArr); return nextMap; }); }, []); const handleAddStopAfter = useCallback((hId, index) => { setPerturbationsArretsMap(prev => { const nextMap = { ...prev }; const nextArr = [...nextMap[hId]]; const newStop = { gare_id: null, gare_nom: '', heure_arrivee: '', heure_depart: '', types_perturbation: ['modification_parcours'], type_perturbation: 'modification_parcours', is_added_manually: true, retard_minutes: 0, reduction_retard_minutes: 0, retard_cumule: 0, cause_arret: '', is_nouveau_terminus: false, is_nouvelle_origine: false, manual_retard: false, manual_reduction: false, manual_arret_prolonge: false, }; nextArr.splice(index + 1, 0, newStop); nextMap[hId] = recomputePropagation(nextArr); return nextMap; }); setOpenStops(p => { const current = p[hId] || {}; return { ...p, [hId]: { ...current, [index + 1]: true } }; }); }, []); const handleDeleteStop = useCallback((hId, index) => { setPerturbationsArretsMap(prev => { const nextMap = { ...prev }; const nextArr = nextMap[hId].filter((_, i) => i !== index); nextMap[hId] = recomputePropagation(nextArr); return nextMap; }); }, []); // ── Gestion du Matériel Roulant ─────────────────────────────────────────── const handleAssignNewMaterial = async (horaireTarget, newMatId, sourceHoraireId) => { if (!horaireTarget || !newMatId) return; setAssigningMateriel(true); try { const selectedMat = materiels.find(m => m.id === newMatId); if (!selectedMat) throw new Error("Matériel introuvable"); const newComp = [ { materiel_id: selectedMat.id, materiel_nom: selectedMat.nom, materiel_nom_technique: selectedMat.nom_technique, materiel_image_url: selectedMat.image_url, capacite: selectedMat.capacite, type_train: selectedMat.type_train, position: 1 } ]; const { error } = await updateHoraireComposition(horaireTarget.id, newComp); if (error) throw error; setHoraires(prev => prev.map(h => h.id === horaireTarget.id ? { ...h, composition_train: newComp } : h)); if (horaireTarget.id === sourceHoraireId) { setSelectedHoraires(prev => prev.map(h => h.id === sourceHoraireId ? { ...h, composition_train: newComp } : h)); } alert(`Matériel ${selectedMat.nom} (${selectedMat.nom_technique}) affecté au train ${horaireTarget.numero_train}.`); } catch (err) { alert("Erreur lors de l'affectation du nouveau matériel : " + (err.message || 'Erreur')); } finally { setAssigningMateriel(false); } }; const getImpactedHoraires = (baseHoraire) => { if (!baseHoraire) return []; const matIds = getMaterielIds(baseHoraire); if (matIds.length === 0) return []; return horaires.filter(h => { if (h.id === baseHoraire.id) return false; const otherMatIds = getMaterielIds(h); const sharesMaterial = matIds.some(id => otherMatIds.includes(id)); if (!sharesMaterial) return false; return isWithinTwoHours(baseHoraire.heure_depart, h.heure_depart); }); }; // ── Soumission ──────────────────────────────────────────────────────────── const handleCreate = async () => { if (!canCreatePerturbations) { alert("Vous n'avez pas les droits pour créer une perturbation."); return; } if (selectedHoraires.length === 0) { alert('Veuillez sélectionner au moins un horaire.'); return; } setSaving(true); try { for (const h of selectedHoraires) { const arrets = perturbationsArretsMap[h.id] || []; const { payload, modificationsParcours } = buildPayload({ selectedHoraire: h, cause, causeDetails, sillonSubstitutionId, joursPerturbation, actif, notes, perturbationsArrets: arrets, scopedRegionId, }); const { error } = await createPerturbation(payload, modificationsParcours); if (error) { alert(`Erreur lors de la création pour le train ${h.numero_train} : ` + (error.message || 'Erreur')); setSaving(false); return; } } window.location.href = '/perturbations'; } catch (err) { alert("Une erreur inattendue s'est produite lors de la sauvegarde."); console.error(err); setSaving(false); } }; const handleCancel = () => { window.location.href = '/perturbations'; }; // ── Étape 1 : Sélection des trains ──────────────────────────────────────── const renderStepHoraire = () => (

1. Sélection des circulations ferroviaires

Recherchez et sélectionnez le ou les trains impactés par l'incident.

setSearchDate(e.target.value)} />
{loadingHoraires ? (

Chargement des circulations...

) : ( <>
{ e.preventDefault(); handleSearch(); }} className="d-flex gap-2"> setSearchQuery(e.target.value)} onFocus={() => setShowResults(true)} /> Rechercher {(searchQuery || selectedHoraires.length > 0) && ( )}
{showResults && searchQuery && (
{filteredHoraires.length} circulation{filteredHoraires.length !== 1 ? 's' : ''} trouvée{filteredHoraires.length !== 1 ? 's' : ''} pour le {new Date(searchDate).toLocaleDateString('fr-FR')}
)} )}
{!loadingHoraires && showResults && (
{filteredHoraires.slice(0, 50).map(h => { const isSelected = selectedHoraires.some(sh => sh.id === h.id); return (
searchMode === 'ligne' ? handleToggleSelectHoraire(h, !isSelected) : handleSelectSingleHoraire(h)} style={{ cursor: 'pointer' }} >
{searchMode === 'ligne' && ( )}
Train {h.numero_train} {h.type_train && {h.type_train}} {h.ligne_nom && {h.ligne_nom}}
{h.gare_depart_nom} {formatHeure(h.heure_depart)} {h.gare_arrivee_nom} {formatHeure(h.heure_arrivee)}
{isSelected ? 'Sélectionné' : 'Choisir'}
); })} {filteredHoraires.length === 0 && (
Aucun horaire trouvé pour cette date.
)}
)} {selectedHoraires.length > 0 && (

{selectedHoraires.length} circulation{selectedHoraires.length > 1 ? 's' : ''} sélectionnée{selectedHoraires.length > 1 ? 's' : ''}

Prêt pour étape 2
{selectedHoraires.map(h => ( Train {h.numero_train} ({h.gare_depart_nom} → {h.gare_arrivee_nom}) ))}
)}
Annuler selectedHoraires.length > 0 && setCurrentStep('perturbation')} disabled={selectedHoraires.length === 0} > Étape suivante
); // ── Étape 2 : Configuration de la perturbation ──────────────────────────── const renderStepPerturbation = () => (

2. Configuration de l'incident & Parcours

Définissez la cause de l'incident, les retards par arrêt et les éventuelles substitutions.

{/* Parcours des trains avec Accordéon */}

Parcours {selectedHoraires.length > 1 ? 'des trains' : 'du train'}

Cliquez sur un train pour configurer les retards et arrêts supprimés
{selectedHoraires.map(h => (
toggleTrainAccordion(h.id)} style={{ cursor: 'pointer' }} >
Train {h.numero_train} ({h.gare_depart_nom} → {h.gare_arrivee_nom})
{openTrains[h.id] && (
{perturbationsArretsMap[h.id]?.map((arret, i) => ( toggleStopAccordion(h.id, idx)} onToggleType={(idx, val, checked) => handleToggleType(h.id, idx, val, checked)} onArretChange={(idx, field, val) => handleArretChange(h.id, idx, field, val)} onNouveauTerminus={(idx) => handleSetNouveauTerminus(h.id, idx)} onNouvelleOrigine={(idx) => handleSetNouvelleOrigine(h.id, idx)} onAddStopAfter={(idx) => handleAddStopAfter(h.id, idx)} onDeleteStop={(idx) => handleDeleteStop(h.id, idx)} garesList={gares} /> ))}
)}
))}
{/* Cause globale */}

Cause globale de l'incident

Motif officiel transmis à l'information voyageurs