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 = () => (
Recherchez et sélectionnez le ou les trains impactés par l'incident.
Chargement des circulations...
Définissez la cause de l'incident, les retards par arrêt et les éventuelles substitutions.
Affectation en cours...
Signalez un retard, une suppression d'arrêt ou une modification de parcours en temps réel.
Configurez le motif commun et ajustez les arrêts de chaque train via les accordéons.