Files
ferrovia-panel/src/pages/perturbations/CreerPerturbation.js
T
AdminGit 4b771e546e V5.0.0 //
-> Refonte du système de GESTION DES ROULEMENTS de matériels roulants
-> Optimisation rendue BDD
2026-10-03 21:54:39 +02:00

985 lines
42 KiB
JavaScript

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 = () => (
<div className="step-container">
<div className="step-header mb-4">
<h2>1. Sélection des circulations ferroviaires</h2>
<p className="text-muted">Recherchez et sélectionnez le ou les trains impactés par l'incident.</p>
</div>
<wcs-card className="wcs-card-form mb-4">
<wcs-card-body>
<div className="d-flex flex-wrap gap-3 mb-3">
<div style={{ minWidth: '220px', flex: 1 }}>
<wcs-form-field>
<label htmlFor="search-date">Date de circulation</label>
<input
id="search-date"
type="date"
className="form-control"
value={searchDate}
onChange={e => setSearchDate(e.target.value)}
/>
</wcs-form-field>
</div>
<div style={{ minWidth: '220px', flex: 1 }}>
<wcs-form-field>
<label htmlFor="search-mode">Mode de recherche</label>
<select
id="search-mode"
className="form-control"
value={searchMode}
onChange={e => { setSearchMode(e.target.value); setSelectedHoraires([]); setPerturbationsArretsMap({}); }}
>
<option value="train">Par Numéro de train / Gare</option>
<option value="ligne">Par Ligne commerciale entière</option>
</select>
</wcs-form-field>
</div>
</div>
{loadingHoraires ? (
<div className="text-center p-4">
<wcs-spinner mode="border" />
<p className="mt-2 text-muted">Chargement des circulations...</p>
</div>
) : (
<>
<form onSubmit={e => { e.preventDefault(); handleSearch(); }} className="d-flex gap-2">
<input
type="text"
className="form-control flex-grow-1"
placeholder={searchMode === 'train' ? "Ex: 842100, Dijon, Lyon..." : "Nom de la ligne commerciale..."}
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onFocus={() => setShowResults(true)}
/>
<wcs-button type="submit" className="wcs-primary" onClick={handleSearch}>
<wcs-mat-icon icon="search" size="s" class="mr-1" />
<span>Rechercher</span>
</wcs-button>
{(searchQuery || selectedHoraires.length > 0) && (
<wcs-button type="button" mode="stroked" className="wcs-secondary" onClick={handleClearSearch}>
<wcs-mat-icon icon="clear" size="s" />
</wcs-button>
)}
</form>
{showResults && searchQuery && (
<div className="mt-2 small text-muted">
<wcs-badge shape="pill" className="wcs-info mr-2">
{filteredHoraires.length} circulation{filteredHoraires.length !== 1 ? 's' : ''}
</wcs-badge>
trouvée{filteredHoraires.length !== 1 ? 's' : ''} pour le {new Date(searchDate).toLocaleDateString('fr-FR')}
</div>
)}
</>
)}
</wcs-card-body>
</wcs-card>
{!loadingHoraires && showResults && (
<div className="horaire-results mb-4">
<div className="d-flex flex-column gap-2" style={{ maxHeight: '380px', overflowY: 'auto' }}>
{filteredHoraires.slice(0, 50).map(h => {
const isSelected = selectedHoraires.some(sh => sh.id === h.id);
return (
<div
key={h.id}
className={`horaire-result-card p-3 rounded border d-flex align-items-center justify-content-between cursor-pointer ${isSelected ? 'border-primary bg-primary-soft' : 'bg-white'}`}
onClick={() => searchMode === 'ligne' ? handleToggleSelectHoraire(h, !isSelected) : handleSelectSingleHoraire(h)}
style={{ cursor: 'pointer' }}
>
<div className="d-flex align-items-center gap-3">
{searchMode === 'ligne' && (
<wcs-mat-icon icon={isSelected ? "check_box" : "check_box_outline_blank"} size="m" class={isSelected ? "text-primary" : "text-muted"} />
)}
<div>
<div className="d-flex align-items-center gap-2 mb-1">
<strong>Train {h.numero_train}</strong>
{h.type_train && <wcs-badge shape="pill" className="wcs-info">{h.type_train}</wcs-badge>}
{h.ligne_nom && <wcs-badge shape="pill" className="wcs-secondary">{h.ligne_nom}</wcs-badge>}
</div>
<div className="d-flex align-items-center gap-2 text-muted small">
<span>{h.gare_depart_nom}</span>
<span className="badge badge-light">{formatHeure(h.heure_depart)}</span>
<wcs-mat-icon icon="arrow_forward" size="s" />
<span>{h.gare_arrivee_nom}</span>
<span className="badge badge-light">{formatHeure(h.heure_arrivee)}</span>
</div>
</div>
</div>
<div>
<wcs-badge className={isSelected ? 'wcs-success' : 'wcs-secondary'} shape="pill">
<wcs-mat-icon icon={isSelected ? 'check' : 'add'} size="s" class="mr-1" />
<span>{isSelected ? 'Sélectionné' : 'Choisir'}</span>
</wcs-badge>
</div>
</div>
);
})}
{filteredHoraires.length === 0 && (
<div className="text-center p-4 bg-light rounded text-muted">
Aucun horaire trouvé pour cette date.
</div>
)}
</div>
</div>
)}
{selectedHoraires.length > 0 && (
<wcs-card className="selected-horaires-summary mb-4">
<wcs-card-body>
<div className="d-flex justify-content-between align-items-center mb-2">
<h4 className="m-0 d-flex align-items-center gap-2">
<wcs-mat-icon icon="check_circle" size="s" class="text-success" />
<span>{selectedHoraires.length} circulation{selectedHoraires.length > 1 ? 's' : ''} sélectionnée{selectedHoraires.length > 1 ? 's' : ''}</span>
</h4>
<wcs-badge shape="pill" className="wcs-success">Prêt pour étape 2</wcs-badge>
</div>
<div className="d-flex flex-wrap gap-2">
{selectedHoraires.map(h => (
<span key={h.id} className="badge badge-secondary p-2 d-inline-flex align-items-center gap-1">
<strong>Train {h.numero_train}</strong> ({h.gare_depart_nom} → {h.gare_arrivee_nom})
</span>
))}
</div>
</wcs-card-body>
</wcs-card>
)}
<div className="step-actions d-flex justify-content-between align-items-center mt-4">
<wcs-button className="wcs-secondary" mode="stroked" onClick={handleCancel}>Annuler</wcs-button>
<wcs-button
className="wcs-primary"
onClick={() => selectedHoraires.length > 0 && setCurrentStep('perturbation')}
disabled={selectedHoraires.length === 0}
>
<span>Étape suivante</span>
<wcs-mat-icon icon="arrow_forward" size="s" class="ml-1" />
</wcs-button>
</div>
</div>
);
// ── Étape 2 : Configuration de la perturbation ────────────────────────────
const renderStepPerturbation = () => (
<div className="step-container">
<div className="step-header mb-4">
<h2>2. Configuration de l'incident & Parcours</h2>
<p className="text-muted">Définissez la cause de l'incident, les retards par arrêt et les éventuelles substitutions.</p>
</div>
{/* Parcours des trains avec Accordéon */}
<wcs-card className="mb-4">
<wcs-card-body>
<div className="d-flex align-items-center gap-2 mb-3">
<wcs-mat-icon icon="alt_route" size="m" class="text-primary"></wcs-mat-icon>
<div>
<h3 className="m-0 font-weight-bold">Parcours {selectedHoraires.length > 1 ? 'des trains' : 'du train'}</h3>
<small className="text-muted">Cliquez sur un train pour configurer les retards et arrêts supprimés</small>
</div>
</div>
{selectedHoraires.map(h => (
<div key={h.id} className="mb-3 border rounded overflow-hidden">
<div
className="p-3 bg-light d-flex justify-content-between align-items-center cursor-pointer"
onClick={() => toggleTrainAccordion(h.id)}
style={{ cursor: 'pointer' }}
>
<div className="d-flex align-items-center gap-2">
<wcs-mat-icon icon="train" size="s" class="text-primary" />
<strong>Train {h.numero_train}</strong>
<span className="text-muted">({h.gare_depart_nom} → {h.gare_arrivee_nom})</span>
</div>
<wcs-mat-icon icon={openTrains[h.id] ? 'expand_less' : 'expand_more'} size="s" />
</div>
{openTrains[h.id] && (
<div className="p-3 bg-white">
{perturbationsArretsMap[h.id]?.map((arret, i) => (
<ArretCard
key={i}
arret={arret}
index={i}
isOpen={openStops[h.id]?.[i] || false}
onToggle={(idx) => 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}
/>
))}
</div>
)}
</div>
))}
</wcs-card-body>
</wcs-card>
{/* Cause globale */}
<wcs-card className="mb-4">
<wcs-card-body>
<div className="d-flex align-items-center gap-2 mb-3">
<wcs-mat-icon icon="report_problem" size="m" class="text-warning"></wcs-mat-icon>
<div>
<h3 className="m-0 font-weight-bold">Cause globale de l'incident</h3>
<small className="text-muted">Motif officiel transmis à l'information voyageurs</small>
</div>
</div>
<wcs-form-field class="mb-3">
<label htmlFor="cause-select">Cause principale *</label>
<select
id="cause-select"
value={cause}
onChange={e => setCause(e.target.value)}
className="form-control"
>
{CAUSES_PERTURBATION.map(c => <option key={c.value} value={c.value}>{c.label}</option>)}
</select>
</wcs-form-field>
<wcs-form-field>
<label htmlFor="cause-details">Détails complémentaires</label>
<textarea
id="cause-details"
placeholder="Précisions sur la cause, voie impactée, consignes régulation..."
value={causeDetails}
onChange={e => setCauseDetails(e.target.value)}
rows="3"
className="form-control"
/>
</wcs-form-field>
</wcs-card-body>
</wcs-card>
{/* Modification de Matériel */}
{selectedHoraires.length === 1 && (
<wcs-card className="mb-4">
<wcs-card-body>
<div className="d-flex align-items-center gap-2 mb-3">
<wcs-mat-icon icon="directions_railway" size="m" class="text-primary"></wcs-mat-icon>
<div>
<h3 className="m-0 font-weight-bold">Matériel Roulant & Substitution</h3>
<small className="text-muted">Réassignation d'une autre rame en cas d'avarie matérielle</small>
</div>
</div>
{assigningMateriel ? (
<div className="text-center p-3">
<wcs-spinner mode="border" />
<p className="mt-2 text-muted">Affectation en cours...</p>
</div>
) : (
<div className="p-3 bg-light rounded border">
<div className="mb-3 d-flex align-items-center gap-2">
<span className="font-weight-bold">Matériel actuel :</span>
{selectedHoraires[0].composition_train && selectedHoraires[0].composition_train.length > 0 ? (
<wcs-badge shape="pill" className="wcs-info">
{selectedHoraires[0].composition_train.map(c => `${c.materiel_nom} (${c.materiel_nom_technique})`).join(', ')}
</wcs-badge>
) : (
<span className="text-muted font-italic">Aucun matériel attribué</span>
)}
</div>
<wcs-form-field>
<label htmlFor="change-mr">Attribuer une autre rame de remplacement</label>
<select
id="change-mr"
className="form-control"
value=""
onChange={(e) => handleAssignNewMaterial(selectedHoraires[0], e.target.value, selectedHoraires[0].id)}
>
<option value="" disabled>Sélectionner un matériel de remplacement...</option>
{materiels
.filter(m => !getMaterielIds(selectedHoraires[0]).includes(m.id))
.map(m => (
<option key={m.id} value={m.id}>
{m.nom} - {m.nom_technique} (S/N: {m.numero_serie}, Capacité: {m.capacite || 'N/A'})
</option>
))
}
</select>
</wcs-form-field>
</div>
)}
</wcs-card-body>
</wcs-card>
)}
{/* Horaires Impactés */}
{selectedHoraires.length === 1 && (
<wcs-card className="mb-4">
<wcs-card-body>
<div className="d-flex align-items-center gap-2 mb-2">
<wcs-mat-icon icon="schedule" size="m" class="text-warning"></wcs-mat-icon>
<div>
<h3 className="m-0 font-weight-bold">Circulations Liées (Prochaines 2h)</h3>
<small className="text-muted">Trains partageant le même matériel pouvant subir un effet domino</small>
</div>
</div>
{(() => {
const impacted = getImpactedHoraires(selectedHoraires[0]);
if (impacted.length === 0) {
return <div className="text-muted font-italic p-2">Aucun autre train impacté dans la tranche horaire.</div>;
}
return (
<div className="d-flex flex-column gap-2 mt-3">
{impacted.map(h => (
<div key={h.id} className="p-3 bg-light border rounded d-flex justify-content-between align-items-center">
<div>
<strong>Train {h.numero_train}</strong> ({h.ligne_nom}) · Départ : <strong>{formatHeure(h.heure_depart)}</strong>
<div className="text-muted small">{h.gare_depart_nom} → {h.gare_arrivee_nom}</div>
</div>
<select
className="form-control"
style={{ maxWidth: '280px' }}
value=""
onChange={(e) => handleAssignNewMaterial(h, e.target.value, selectedHoraires[0].id)}
>
<option value="" disabled>Changer matériel pour ce train...</option>
{materiels
.filter(m => !getMaterielIds(h).includes(m.id))
.map(m => (
<option key={m.id} value={m.id}>
{m.nom} - {m.nom_technique}
</option>
))
}
</select>
</div>
))}
</div>
);
})()}
</wcs-card-body>
</wcs-card>
)}
{/* Sillon de substitution */}
{selectedHoraires.length === 1 && perturbationsArretsMap[selectedHoraires[0].id]?.some(a => (a.types_perturbation || []).some(t => ['arret_supprime', 'modification_parcours'].includes(t))) && (
<wcs-card className="mb-4">
<wcs-card-body>
<div className="d-flex align-items-center gap-2 mb-3">
<wcs-mat-icon icon="swap_calls" size="m" class="text-primary"></wcs-mat-icon>
<div>
<h3 className="m-0 font-weight-bold">Train de Substitution</h3>
<small className="text-muted">Circulation de remplacement pour les voyageurs</small>
</div>
</div>
<wcs-form-field>
<label htmlFor="sillon-sub">Train de substitution (optionnel)</label>
<select
id="sillon-sub"
value={sillonSubstitutionId}
onChange={e => setSillonSubstitutionId(e.target.value)}
className="form-control"
>
<option value="">Aucun</option>
{horaires.filter(h => h.id !== selectedHoraires[0].id).slice(0, 50).map(h => (
<option key={h.id} value={h.id}>{h.numero_train} - {h.gare_depart_nom} vers {h.gare_arrivee_nom}</option>
))}
</select>
</wcs-form-field>
</wcs-card-body>
</wcs-card>
)}
{/* Jours de perturbation */}
<wcs-card className="mb-4">
<wcs-card-body>
<div className="d-flex align-items-center gap-2 mb-3">
<wcs-mat-icon icon="date_range" size="m" class="text-primary"></wcs-mat-icon>
<div>
<h3 className="m-0 font-weight-bold">Période d'application</h3>
<small className="text-muted">Définissez les dates ou plages de validité de la perturbation</small>
</div>
</div>
<JoursPerturbationEditor value={joursPerturbation} onChange={setJoursPerturbation} />
</wcs-card-body>
</wcs-card>
{/* Informations complémentaires & Statut actif */}
<wcs-card className="mb-4">
<wcs-card-body>
<div className="d-flex align-items-center gap-2 mb-3">
<wcs-mat-icon icon="notes" size="m" class="text-secondary"></wcs-mat-icon>
<div>
<h3 className="m-0 font-weight-bold">Informations d'exploitation</h3>
<small className="text-muted">Notes internes et visibilité</small>
</div>
</div>
<wcs-form-field class="mb-3">
<label htmlFor="notes-internes">Notes internes régulation</label>
<textarea
id="notes-internes"
placeholder="Consignes internes d'exploitation..."
value={notes}
onChange={e => setNotes(e.target.value)}
rows="3"
className="form-control"
/>
</wcs-form-field>
<div className="d-flex align-items-center justify-content-between p-3 rounded bg-light border">
<div>
<strong className="d-block">Perturbation active</strong>
<small className="text-muted">Diffuser immédiatement sur les écrans de quai et les flux voyageurs</small>
</div>
<label className="switch-container">
<input
type="checkbox"
checked={actif}
onChange={e => setActif(e.target.checked)}
/>
<span className="switch-slider"></span>
</label>
</div>
</wcs-card-body>
</wcs-card>
{/* Actions */}
<div className="step-actions d-flex justify-content-between align-items-center mt-4">
<wcs-button className="wcs-secondary" mode="stroked" onClick={() => setCurrentStep('horaire')}>
<wcs-mat-icon icon="arrow_back" size="s" class="mr-1" />
<span>Précédent</span>
</wcs-button>
<div className="d-flex gap-2">
<wcs-button className="wcs-secondary" mode="stroked" onClick={handleCancel}>
<span>Annuler</span>
</wcs-button>
<wcs-button
className="wcs-primary"
onClick={handleCreate}
disabled={saving || !canCreatePerturbations}
loading={saving}
>
<wcs-mat-icon icon="check" size="s" class="mr-1" />
<span>Créer la perturbation</span>
</wcs-button>
</div>
</div>
</div>
);
// ── Rendu principal ───────────────────────────────────────────────────────
return (
<div className="sncf-groupe perturbations-page-root">
<div className="panel-layout">
<div className="panel-nav"><NavPanel /></div>
<div className="panel-content perturbations-modern-container">
{/* Header Banner SNCF */}
<header className="page-header-modern">
<div className="header-info">
<div className="perturbations-hero-tags mb-2 d-flex align-items-center gap-2">
<wcs-badge shape="pill" className="wcs-danger">
<wcs-mat-icon icon="add_alert" size="s" class="mr-1" />
Déclaration d'incident
</wcs-badge>
{scopedRegionId && (
<wcs-badge shape="pill" className="wcs-secondary">
Région : {scopedRegionId}
</wcs-badge>
)}
</div>
<h1>Déclarer une perturbation sur les circulations</h1>
<p>Signalez un retard, une suppression d'arrêt ou une modification de parcours en temps réel.</p>
</div>
<div className="header-actions">
<wcs-button className="wcs-secondary" mode="stroked" onClick={handleCancel}>
<wcs-mat-icon icon="close" size="s" class="mr-1" />
<span>Annuler</span>
</wcs-button>
</div>
</header>
{/* Stepper Horizontal SNCF */}
<div className="stepper-horizontal mb-4">
<div
className={`step-item ${currentStep === 'horaire' ? 'active' : 'completed'}`}
onClick={() => setCurrentStep('horaire')}
style={{ cursor: 'pointer' }}
>
<div className="step-icon">
{currentStep === 'perturbation' ? <wcs-mat-icon icon="check" size="s" /> : '1'}
</div>
<span>1. Sélection des trains</span>
</div>
<div className="step-divider"></div>
<div
className={`step-item ${currentStep === 'perturbation' ? 'active' : ''}`}
onClick={() => selectedHoraires.length > 0 && setCurrentStep('perturbation')}
style={{ cursor: selectedHoraires.length > 0 ? 'pointer' : 'not-allowed' }}
>
<div className="step-icon">2</div>
<span>2. Configuration de l'incident & Parcours</span>
</div>
</div>
<div className="creation-layout-modern">
<div className="creation-main-content">
{currentStep === 'horaire' && renderStepHoraire()}
{currentStep === 'perturbation' && renderStepPerturbation()}
</div>
<div className="sticky-sidebar">
{selectedHoraires.length === 1 && (
<ParcoursPreview arrets={perturbationsArretsMap[selectedHoraires[0].id]} />
)}
{selectedHoraires.length > 1 && (
<wcs-card className="summary-card">
<wcs-card-body>
<h3 className="m-0 mb-2 font-weight-bold">Sélection multiple ({selectedHoraires.length} trains)</h3>
<p className="text-muted small m-0">
Configurez le motif commun et ajustez les arrêts de chaque train via les accordéons.
</p>
</wcs-card-body>
</wcs-card>
)}
</div>
</div>
{/* WCS Footer Corporate SNCF */}
<wcs-footer className="sncf-page-footer mt-5">
<div className="d-flex align-items-center gap-3">
<div className="footer-sncf-badge">
<wcs-mat-icon icon="warning" style={{ fontSize: '20px', color: '#ffffff' }} />
</div>
<div>
<strong style={{ color: '#ffffff', fontSize: '14px', display: 'block' }}>
FERROVIA PANEL · CENTRE DE SUPERVISION DES CIRCULATIONS SNCF
</strong>
<span style={{ color: 'rgba(255, 255, 255, 0.75)', fontSize: '12px' }}>
Gestion Opérationnelle des Perturbations & Information Voyageurs
</span>
</div>
</div>
<div slot="end-left">
<span style={{ fontSize: '12px', color: 'rgba(255, 255, 255, 0.65)' }}>
© {new Date().getFullYear()} SNCF · Système National de Régulation
</span>
</div>
<div slot="end-right">
<div className="d-flex align-items-center gap-2">
<wcs-badge className="wcs-success" shape="rounded">
<wcs-mat-icon icon="cloud_done" class="mr-1" />
Supabase Connecté
</wcs-badge>
<span style={{ fontSize: '12px', color: 'rgba(255, 255, 255, 0.7)' }}>
v2.6.0-WCS
</span>
</div>
</div>
</wcs-footer>
</div>
</div>
</div>
);
}