MIGRATION VERS GITEA

This commit is contained in:
mathis committed 2026-08-25 01:23:13 +02:00
commit 5f9d991c15
333 files changed
+108514

No files matched your search

@@ -0,0 +1,796 @@
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,
getPrimaryType,
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)
);
}
// Trier par numéro de train
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 })); // Open by default
}, []);
const handleSelectSingleHoraire = useCallback((h) => {
setSelectedHoraires([h]);
setSearchQuery(`${h.numero_train} - ${h.gare_depart_nom} → ${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 = a.retard_minutes || 0; }
if (typeValue === 'arret_prolonge') { a.manual_arret_prolonge = true; a.arret_prolonge_minutes = a.arret_prolonge_minutes || 0; }
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 === 'arret_prolonge') { a.manual_arret_prolonge = false; a.arret_prolonge_minutes = 0; }
if (typeValue === 'arret_supprime') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; }
if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; }
}
a.types_perturbation = types;
a.type_perturbation = getPrimaryType(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]];
nextArr[index] = { ...nextArr[index], [field]: value };
if (field === 'retard_minutes') {
nextArr[index].manual_retard = true;
nextArr[index].retard_minutes = Number(value) || 0;
const types = [...(nextArr[index].types_perturbation || [])];
if (!types.includes('retard')) types.push('retard');
nextArr[index].types_perturbation = types;
nextArr[index].type_perturbation = getPrimaryType(types);
}
if (field === 'arret_prolonge_minutes') {
const mins = Number(value) || 0;
nextArr[index].arret_prolonge_minutes = mins;
nextArr[index].manual_arret_prolonge = mins > 0;
const types = [...(nextArr[index].types_perturbation || [])];
if (mins > 0) { if (!types.includes('arret_prolonge')) types.push('arret_prolonge'); }
else { const i2 = types.indexOf('arret_prolonge'); if (i2 !== -1) types.splice(i2, 1); }
nextArr[index].types_perturbation = types;
nextArr[index].type_perturbation = getPrimaryType(types);
}
nextMap[hId] = recomputePropagation(nextArr);
return nextMap;
});
}, []);
const handleSetNouveauTerminus = useCallback((hId, index) => {
setPerturbationsArretsMap(prev => {
const nextMap = { ...prev };
const nextArr = prev[hId].map((a, idx) => {
if (idx === index) return { ...a, is_nouveau_terminus: true, is_nouvelle_origine: false };
if (idx > index) return { ...a, types_perturbation: ['arret_supprime'], type_perturbation: 'arret_supprime', cause_arret: prev[hId][index]?.cause_arret || '', is_nouveau_terminus: false, 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 = prev[hId].map((a, idx) => {
if (idx === index) return { ...a, is_nouvelle_origine: true, is_nouveau_terminus: false };
if (idx < index) return { ...a, types_perturbation: ['arret_supprime'], type_perturbation: 'arret_supprime', cause_arret: prev[hId][index]?.cause_arret || '', is_nouveau_terminus: false, is_nouvelle_origine: 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 = [...prev[hId]];
const newStop = {
gare_nom: '',
types_perturbation: ['modification_parcours'],
type_perturbation: 'modification_parcours',
is_added_manually: true,
heure_arrivee: '',
heure_depart: ''
};
nextArr.splice(index + 1, 0, newStop);
const indexed = nextArr.map((a, idx) => ({ ...a, index: idx, isFirst: idx === 0, isLast: idx === nextArr.length - 1 }));
nextMap[hId] = recomputePropagation(indexed);
return nextMap;
});
}, []);
const handleDeleteStop = useCallback((hId, index) => {
setPerturbationsArretsMap(prev => {
const nextMap = { ...prev };
const nextArr = [...prev[hId]];
if (nextArr[index].is_added_manually) {
nextArr.splice(index, 1);
} else {
nextArr[index] = { ...nextArr[index], types_perturbation: ['arret_supprime'], type_perturbation: 'arret_supprime' };
}
const indexed = nextArr.map((a, idx) => ({ ...a, index: idx, isFirst: idx === 0, isLast: idx === nextArr.length - 1 }));
nextMap[hId] = recomputePropagation(indexed);
return nextMap;
});
}, []);
// ── Soumission ────────────────────────────────────────────────────────────
const handleCreate = useCallback(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; }
// Check if at least one stop is perturbed for all selected horaires
for (const h of selectedHoraires) {
const arrets = perturbationsArretsMap[h.id];
if (!arrets || !arrets.some(a => (a.types_perturbation || []).length > 0)) {
alert(`Veuillez configurer au moins une perturbation pour le train ${h.numero_train}`); return;
}
}
setSaving(true);
let allSuccess = true;
try {
for (const h of selectedHoraires) {
const { payload, modificationsParcours } = buildPayload({
selectedHoraire: h,
perturbationsArrets: perturbationsArretsMap[h.id],
cause, causeDetails,
sillonSubstitutionId, joursPerturbation, actif, notes,
});
if (scopedRegionId) payload.region_id = scopedRegionId;
const { error } = await createPerturbation(payload, modificationsParcours);
if (error) {
alert(`Erreur lors de la création pour le train ${h.numero_train}`);
console.error(error);
allSuccess = false;
}
}
if (allSuccess) {
alert('Perturbations créées avec succès !');
window.location.href = '/perturbations';
}
} catch (e) {
console.error(e); alert('Une erreur inattendue s\'est produite');
} finally { setSaving(false); }
}, [canCreatePerturbations, selectedHoraires, perturbationsArretsMap, cause, causeDetails, sillonSubstitutionId, joursPerturbation, actif, notes, scopedRegionId]);
const handleCancel = useCallback(() => {
if (window.confirm('Êtes-vous sûr de vouloir annuler ?')) window.location.href = '/perturbations';
}, []);
const getImpactedHoraires = useCallback((h) => {
if (!h) return [];
const selectedMrIds = getMaterielIds(h);
if (selectedMrIds.length === 0) return [];
return horaires.filter(hor => {
if (hor.id === h.id) return false;
const hMrIds = getMaterielIds(hor);
const shareMr = selectedMrIds.some(id => hMrIds.includes(id));
if (!shareMr) return false;
return isWithinTwoHours(h.heure_depart, hor.heure_depart);
});
}, [horaires]);
const handleAssignNewMaterial = useCallback(async (horaireToUpdate, newMaterielId, contextHoraireId) => {
if (!horaireToUpdate || !newMaterielId) return;
const selectedMr = materiels.find(m => String(m.id) === String(newMaterielId));
if (!selectedMr) return;
setAssigningMateriel(true);
try {
const compositionPayload = [{
materiel_id: selectedMr.id,
materiel_nom: selectedMr.nom || '',
materiel_nom_technique: selectedMr.nom_technique || '',
materiel_image_url: selectedMr.image_url || null,
position: 1
}];
const { data, error } = await updateHoraireComposition(horaireToUpdate.id, compositionPayload);
if (error) {
alert("Erreur lors de l'attribution du matériel: " + (error.message || error));
setAssigningMateriel(false);
return;
}
if (selectedHoraires.some(h => h.id === horaireToUpdate.id)) {
// Update selected horaires state
setSelectedHoraires(prev => prev.map(h =>
h.id === horaireToUpdate.id ? { ...h, composition_train: data?.composition_train || compositionPayload } : h
));
setCause('modification_materiel');
setPerturbationsArretsMap(prev => {
const nextMap = { ...prev };
const nextArr = [...nextMap[horaireToUpdate.id]];
if (nextArr.length > 0) {
const first = { ...nextArr[0] };
first.manual_retard = true;
first.retard_minutes = 25;
const types = [...(first.types_perturbation || [])];
if (!types.includes('retard')) types.push('retard');
first.types_perturbation = types;
first.type_perturbation = getPrimaryType(types);
nextArr[0] = first;
}
nextMap[horaireToUpdate.id] = recomputePropagation(nextArr);
return nextMap;
});
} else {
const arrets = buildArretFromHoraire(horaireToUpdate);
if (arrets.length > 0) {
arrets[0].manual_retard = true;
arrets[0].retard_minutes = 25;
const types = [...(arrets[0].types_perturbation || [])];
if (!types.includes('retard')) types.push('retard');
arrets[0].types_perturbation = types;
arrets[0].type_perturbation = getPrimaryType(types);
}
const propagated = recomputePropagation(arrets);
const { payload, modificationsParcours } = buildPayload({
selectedHoraire: horaireToUpdate,
perturbationsArrets: propagated,
cause: 'modification_materiel',
causeDetails: 'Modification de matériel suite à indisponibilité du matériel initial',
sillonSubstitutionId: '',
joursPerturbation: joursPerturbation,
actif: true,
notes: 'Généré automatiquement suite à la réaffectation du matériel roulant.'
});
if (scopedRegionId) payload.region_id = scopedRegionId;
const { error: pertErr } = await createPerturbation(payload, modificationsParcours);
if (pertErr) {
console.error("Erreur lors de la création automatique de la perturbation:", pertErr);
} else {
alert(`Matériel modifié. Une perturbation a été automatiquement créée pour le train ${horaireToUpdate.numero_train} avec 25 minutes de retard.`);
}
}
const regionId = getScopedRegionId(getCurrentUser());
const { data: hData } = await getAllHoraires(regionId);
if (hData) {
setHoraires(hData);
setFilteredHoraires(hData);
// Refresh selected horaires
setSelectedHoraires(prev => prev.map(sh => {
const updated = hData.find(h => h.id === sh.id);
return updated ? updated : sh;
}));
}
} catch (e) {
console.error(e);
alert("Une erreur est survenue lors de l'attribution.");
} finally {
setAssigningMateriel(false);
}
}, [selectedHoraires, materiels, scopedRegionId, joursPerturbation]);
// ── Étape 1 ───────────────────────────────────────────────────────────────
const renderStepHoraire = () => (
<div className="step-container">
<div className="step-header">
<h2>Sélectionner un ou plusieurs horaires</h2>
<p>Recherchez et sélectionnez les horaires qui seront perturbés</p>
</div>
<div className="form-group required">
<label>Options de recherche</label>
<div style={{ display: 'flex', gap: '20px', marginBottom: '15px' }}>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', fontSize: '0.85rem', color: '#555', marginBottom: '4px' }}>Date de circulation</label>
<input type="date" className="form-input" value={searchDate} onChange={e => setSearchDate(e.target.value)} style={{ padding: '8px', border: '1px solid #ccc', borderRadius: '4px', width: '100%' }} />
</div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', fontSize: '0.85rem', color: '#555', marginBottom: '4px' }}>Recherche par</label>
<select className="form-select" value={searchMode} onChange={e => { setSearchMode(e.target.value); setSelectedHoraires([]); setPerturbationsArretsMap({}); }} style={{ padding: '8px', border: '1px solid #ccc', borderRadius: '4px', width: '100%' }}>
<option value="train">Numéro de train</option>
<option value="ligne">Ligne</option>
</select>
</div>
</div>
{loadingHoraires ? (
<div className="loading-inline"><wcs-spinner mode="border" /> Chargement des horaires...</div>
) : (
<>
<form onSubmit={e => { e.preventDefault(); handleSearch(); }} className="search-form">
<input type="text" className="search-input" placeholder={searchMode === 'train' ? "Numéro, gare, ligne..." : "Nom de la ligne..."}
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" /> Rechercher
</wcs-button>
{(searchQuery || selectedHoraires.length > 0) && (
<wcs-button type="button" className="wcs-secondary" onClick={handleClearSearch}>
<wcs-mat-icon icon="clear" />
</wcs-button>
)}
</form>
{showResults && searchQuery && (
<div className="results-count">
{filteredHoraires.length} horaire{filteredHoraires.length !== 1 ? 's' : ''} trouvé{filteredHoraires.length !== 1 ? 's' : ''} circulant le {new Date(searchDate).toLocaleDateString('fr-FR')}
</div>
)}
</>
)}
</div>
{!loadingHoraires && showResults && (
<div className="horaire-results">
{filteredHoraires.slice(0, 50).map(h => {
const isSelected = selectedHoraires.some(sh => sh.id === h.id);
return (
<div key={h.id} className={`horaire-result-item${isSelected ? ' selected' : ''}`}
onClick={() => searchMode === 'ligne' ? handleToggleSelectHoraire(h, !isSelected) : handleSelectSingleHoraire(h)}>
{searchMode === 'ligne' && (
<div style={{ marginRight: '15px' }}>
<input type="checkbox" checked={isSelected} readOnly style={{ transform: 'scale(1.2)', cursor: 'pointer' }} />
</div>
)}
<div style={{ flex: 1 }}>
<div className="horaire-result-header">
<strong>{h.numero_train}</strong>
{h.type_train && <wcs-badge className="wcs-info">{h.type_train}</wcs-badge>}
{h.ligne_nom && <wcs-badge className="wcs-secondary">{h.ligne_nom}</wcs-badge>}
</div>
<div className="horaire-result-route">
{h.gare_depart_nom} <span className="time-chip">{formatHeure(h.heure_depart)}</span>
<wcs-mat-icon icon="arrow_forward" />
{h.gare_arrivee_nom} <span className="time-chip">{formatHeure(h.heure_arrivee)}</span>
</div>
</div>
</div>
);
})}
{filteredHoraires.length === 0 && <div className="no-results">Aucun horaire trouvé pour cette date.</div>}
</div>
)}
{selectedHoraires.length > 0 && (
<div className="horaire-display" style={{ marginTop: '20px' }}>
<div className="horaire-display-header">
<h4>{selectedHoraires.length} Horaire{selectedHoraires.length > 1 ? 's' : ''} sélectionné{selectedHoraires.length > 1 ? 's' : ''}</h4>
<wcs-badge className="wcs-success"><wcs-mat-icon icon="check_circle" /></wcs-badge>
</div>
<div className="horaire-display-content" style={{ maxHeight: '200px', overflowY: 'auto' }}>
{selectedHoraires.map(h => (
<div key={h.id} style={{ padding: '8px 0', borderBottom: '1px solid #eee' }}>
<strong>Train {h.numero_train}</strong> ({h.ligne_nom || 'N/A'}) - {h.gare_depart_nom} → {h.gare_arrivee_nom}
</div>
))}
</div>
</div>
)}
<div className="step-actions">
<wcs-button className="wcs-secondary" onClick={handleCancel}>Annuler</wcs-button>
<wcs-button className="wcs-primary" onClick={() => selectedHoraires.length > 0 && setCurrentStep('perturbation')} disabled={selectedHoraires.length === 0}>
Suivant <wcs-mat-icon icon="arrow_forward" />
</wcs-button>
</div>
</div>
);
// ── Étape 2 ───────────────────────────────────────────────────────────────
const renderStepPerturbation = () => (
<div className="step-container">
<div className="step-header">
<h2>Définir la perturbation</h2>
<p>Configurez les perturbations pour les horaires sélectionnés</p>
</div>
<div className="form-section">
<h3>Parcours {selectedHoraires.length > 1 ? 'des trains' : 'du train'}</h3>
<p className="section-hint">Cliquez sur un train pour configurer les perturbations de ses arrêts</p>
{selectedHoraires.map(h => (
<div key={h.id} style={{ marginBottom: '15px', border: '1px solid #ccc', borderRadius: '8px', overflow: 'hidden' }}>
<div
style={{ padding: '12px 16px', background: '#e9ecef', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
onClick={() => toggleTrainAccordion(h.id)}
>
<h4 style={{ margin: 0, fontSize: '1rem', color: '#333' }}>
Train {h.numero_train} ({h.gare_depart_nom} → {h.gare_arrivee_nom})
</h4>
<wcs-mat-icon icon={openTrains[h.id] ? 'expand_less' : 'expand_more'} style={{ color: '#555' }} />
</div>
{openTrains[h.id] && (
<div className="parcours-accordion" style={{ padding: '15px' }}>
{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>
))}
</div>
<div className="form-section">
<h3>Cause globale de la perturbation</h3>
<div className="form-group required">
<label htmlFor="cause">Cause principale</label>
<select id="cause" value={cause} onChange={e => setCause(e.target.value)} className="form-select">
{CAUSES_PERTURBATION.map(c => <option key={c.value} value={c.value}>{c.label}</option>)}
</select>
</div>
<div className="form-group">
<label htmlFor="causeDetails">Détails supplémentaires</label>
<textarea id="causeDetails" placeholder="Précisions sur la cause..." value={causeDetails}
onChange={e => setCauseDetails(e.target.value)} rows="3" className="form-textarea" />
</div>
</div>
{/* Modification de Matériel */}
{selectedHoraires.length === 1 && (
<div className="form-section">
<h3>Modification de Matériel</h3>
{assigningMateriel ? (
<div className="loading-inline"><wcs-spinner mode="border" /> Affectation en cours...</div>
) : (
<div style={{ background: '#f5f5f7', padding: '16px', borderRadius: '8px', border: '1px solid #e5e5ea' }}>
<div style={{ marginBottom: '12px' }}>
<strong>Matériel actuel :</strong>{' '}
{selectedHoraires[0].composition_train && selectedHoraires[0].composition_train.length > 0 ? (
<span>
{selectedHoraires[0].composition_train.map(c => `${c.materiel_nom} (${c.materiel_nom_technique})`).join(', ')}
</span>
) : (
<span style={{ color: '#8e8e93', fontStyle: 'italic' }}>Aucun matériel attribué</span>
)}
</div>
<div className="form-group" style={{ marginBottom: 0 }}>
<label htmlFor="change-mr">Attribuer un autre matériel</label>
<select
id="change-mr"
className="form-select"
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>
</div>
</div>
)}
</div>
)}
{/* Horaires Impactés */}
{selectedHoraires.length === 1 && (
<div className="form-section">
<h3>Horaires Impactés</h3>
<p className="section-hint">
Sillons utilisant le même matériel dans les 2 prochaines heures
</p>
{(() => {
const impacted = getImpactedHoraires(selectedHoraires[0]);
if (impacted.length === 0) {
return <div style={{ color: '#8e8e93', fontStyle: 'italic', padding: '8px' }}>Aucun autre horaire impacté.</div>;
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{impacted.map(h => (
<div key={h.id} style={{ background: '#fffbeb', border: '1px solid #fde68a', padding: '12px', borderRadius: '6px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<strong>Train {h.numero_train} ({h.ligne_nom})</strong>
<span style={{ fontSize: '0.9rem', color: '#b45309', fontWeight: 'bold' }}>
Départ : {formatHeure(h.heure_depart)}
</span>
</div>
<div>
{h.gare_depart_nom} → {h.gare_arrivee_nom}
</div>
<div style={{ fontSize: '0.9rem', color: '#4b5563' }}>
Matériel actuel : {h.composition_train?.map(c => `${c.materiel_nom}`).join(', ') || 'Aucun'}
</div>
<div style={{ marginTop: '4px' }}>
<select
className="form-select"
style={{ padding: '4px 8px', fontSize: '0.9rem' }}
value=""
onChange={(e) => handleAssignNewMaterial(h, e.target.value, selectedHoraires[0].id)}
>
<option value="" disabled>Attribuer un autre 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>
))}
</div>
);
})()}
</div>
)}
{selectedHoraires.length === 1 && perturbationsArretsMap[selectedHoraires[0].id]?.some(a => (a.types_perturbation || []).some(t => ['arret_supprime', 'modification_parcours'].includes(t))) && (
<div className="form-section">
<h3>Sillon de substitution</h3>
<div className="form-group">
<label htmlFor="sillon">Train de substitution (optionnel)</label>
<select id="sillon" value={sillonSubstitutionId} onChange={e => setSillonSubstitutionId(e.target.value)} className="form-select">
<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} → {h.gare_arrivee_nom}</option>
))}
</select>
</div>
</div>
)}
<div className="form-section">
<h3>Jours de perturbation</h3>
<p className="section-hint">Ajoutez un ou plusieurs jours ou périodes d'application de la perturbation.</p>
<JoursPerturbationEditor value={joursPerturbation} onChange={setJoursPerturbation} />
</div>
<div className="form-section">
<h3>Autres informations</h3>
<div className="form-group">
<label htmlFor="notes">Notes internes</label>
<textarea id="notes" placeholder="Informations complémentaires..." value={notes}
onChange={e => setNotes(e.target.value)} rows="3" className="form-textarea" />
</div>
<div className="form-group">
<label className="checkbox-label">
<input type="checkbox" checked={actif} onChange={e => setActif(e.target.checked)} />
<span>Perturbation active</span>
</label>
</div>
</div>
<div className="step-actions">
<wcs-button className="wcs-secondary" onClick={() => setCurrentStep('horaire')}>
<wcs-mat-icon icon="arrow_back" /> Précédent
</wcs-button>
<div className="step-actions-right">
<wcs-button className="wcs-secondary" onClick={handleCancel}>Annuler</wcs-button>
<wcs-button className="wcs-primary" onClick={handleCreate} disabled={saving || !canCreatePerturbations}>
{saving ? <span><wcs-spinner mode="border" /> Création...</span>
: <span><wcs-mat-icon icon="check" /> Créer la perturbation</span>}
</wcs-button>
</div>
</div>
</div>
);
// ── Rendu principal ───────────────────────────────────────────────────────
return (
<div className="sncf-groupe">
<div className="panel-layout">
<div className="panel-nav"><NavPanel /></div>
<div className="panel-content">
<header className="page-header">
<h1>Créer une perturbation</h1>
<p className="page-description">
Étape {currentStep === 'horaire' ? '1' : '2'} / 2 —{' '}
{currentStep === 'horaire' ? 'Sélection des horaires' : 'Configuration'}
</p>
</header>
<div className="creation-container">
<div className="creation-main">
{currentStep === 'horaire' && renderStepHoraire()}
{currentStep === 'perturbation' && renderStepPerturbation()}
</div>
<div className="creation-preview">
{selectedHoraires.length === 1 && (
<ParcoursPreview arrets={perturbationsArretsMap[selectedHoraires[0].id]} />
)}
{selectedHoraires.length > 1 && (
<div style={{ padding: '15px', color: '#666', fontStyle: 'italic', background: '#f9f9f9', borderRadius: '8px', border: '1px solid #ddd' }}>
Aperçu du parcours non disponible lors de la sélection multiple. Veuillez vous référer aux accordéons détaillés.
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}