diff --git a/.env b/.env index 9eb955a..2315b2a 100644 --- a/.env +++ b/.env @@ -1,2 +1,4 @@ REACT_APP_SUPABASE_URL=https://supabase-ferrovia.mrpatator.fr -REACT_APP_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzg3NjY3MjMxLCJleHAiOjE5NDUzNDcyMzF9.uD7YUlOFIhK_Jqbra94o5-N5zdrzKezoCxV067RJi78 \ No newline at end of file +REACT_APP_SUPABASE_ANON_KEY=sb_publishable_jSS9wIDj3BWGBpxtv5LwGQ_1P_KBV_N +NEXT_PUBLIC_SUPABASE_URL=https://supabase-ferrovia.mrpatator.fr +NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_jSS9wIDj3BWGBpxtv5LwGQ_1P_KBV_N \ No newline at end of file diff --git a/.env.local b/.env.local index 9eb955a..2315b2a 100644 --- a/.env.local +++ b/.env.local @@ -1,2 +1,4 @@ REACT_APP_SUPABASE_URL=https://supabase-ferrovia.mrpatator.fr -REACT_APP_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzg3NjY3MjMxLCJleHAiOjE5NDUzNDcyMzF9.uD7YUlOFIhK_Jqbra94o5-N5zdrzKezoCxV067RJi78 \ No newline at end of file +REACT_APP_SUPABASE_ANON_KEY=sb_publishable_jSS9wIDj3BWGBpxtv5LwGQ_1P_KBV_N +NEXT_PUBLIC_SUPABASE_URL=https://supabase-ferrovia.mrpatator.fr +NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_jSS9wIDj3BWGBpxtv5LwGQ_1P_KBV_N \ No newline at end of file diff --git a/src/api/horaires.js b/src/api/horaires.js index 91f1315..193d0eb 100644 --- a/src/api/horaires.js +++ b/src/api/horaires.js @@ -1022,16 +1022,67 @@ export function formatHeure(time) { */ export function getJoursCirculation(horaire) { if (!horaire) return []; + + const parseBool = (v) => { + if (typeof v === 'boolean') return v; + if (typeof v === 'number') return v === 1; + if (typeof v === 'string') { + const s = v.trim().toLowerCase(); + return s === 'true' || s === 't' || s === '1' || s === 'oui' || s === 'yes' || s === 'y'; + } + return false; + }; + const jours = []; - if (horaire.circule_lundi) jours.push('Lun'); - if (horaire.circule_mardi) jours.push('Mar'); - if (horaire.circule_mercredi) jours.push('Mer'); - if (horaire.circule_jeudi) jours.push('Jeu'); - if (horaire.circule_vendredi) jours.push('Ven'); - if (horaire.circule_samedi) jours.push('Sam'); - if (horaire.circule_dimanche) jours.push('Dim'); - return jours; -}export async function updateQuaisReelsHoraire(horaireId, quaisParOrdre, isPropagation = false) { + if (parseBool(horaire.circule_lundi)) jours.push('Lun'); + if (parseBool(horaire.circule_mardi)) jours.push('Mar'); + if (parseBool(horaire.circule_mercredi)) jours.push('Mer'); + if (parseBool(horaire.circule_jeudi)) jours.push('Jeu'); + if (parseBool(horaire.circule_vendredi)) jours.push('Ven'); + if (parseBool(horaire.circule_samedi)) jours.push('Sam'); + if (parseBool(horaire.circule_dimanche)) jours.push('Dim'); + + if (jours.length > 0) return jours; + + // Fallback si formaté en chaîne de caractères + const txt = String(horaire.jours_circulation || horaire.frequence || horaire.periodicite || '').toLowerCase(); + if (txt) { + if (txt.includes('tous') || txt.includes('quotidien') || txt.includes('7j')) { + return ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']; + } + if (txt.includes('lun') && txt.includes('ven')) { + return ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven']; + } + if (txt.includes('lun') && txt.includes('sam')) { + return ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam']; + } + if (txt.includes('sam') && txt.includes('dim')) { + return ['Sam', 'Dim']; + } + if (txt.includes('dim')) return ['Dim']; + if (txt.includes('sam')) return ['Sam']; + if (txt.includes('ven')) return ['Ven']; + } + + // Fallback jours personnalisés + if (Array.isArray(horaire.jours_personnalises) && horaire.jours_personnalises.length > 0) { + const dows = new Set(); + const mapDow = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam']; + horaire.jours_personnalises.forEach(j => { + if (j && j.circule !== false && j.date) { + const d = new Date(j.date).getDay(); + if (!isNaN(d)) dows.add(mapDow[d]); + } + }); + if (dows.size > 0) { + return ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].filter(d => dows.has(d)); + } + } + + return []; +} + +export async function updateQuaisReelsHoraire(horaireId, quaisParOrdre, isPropagation = false) { if (!isSupabaseConfigured) { return { data: null, error: null }; } diff --git a/src/pages/fiches-horaires/FichesHoraires.jsx b/src/pages/fiches-horaires/FichesHoraires.jsx index 9e9ef70..58317cf 100644 --- a/src/pages/fiches-horaires/FichesHoraires.jsx +++ b/src/pages/fiches-horaires/FichesHoraires.jsx @@ -620,6 +620,7 @@ export default function FichesHoraires() { return; } + const defaultJours = manualTrainForm.jours_circulation || 'Lun à Ven*'; const newTrain = { id: `manual_${Date.now()}`, numero_train: manualTrainForm.numero_train || `T${Math.floor(1000 + Math.random() * 9000)}`, @@ -629,7 +630,8 @@ export default function FichesHoraires() { gare_arrivee_nom: manualTrainForm.gare_arrivee_nom || orderedGaresForLine[orderedGaresForLine.length - 1]?.nom || 'Gare d\'arrivée', heure_depart: manualTrainForm.heure_depart || '08:00', heure_arrivee: manualTrainForm.heure_arrivee || '09:00', - jours_circulation: manualTrainForm.jours_circulation || 'Lun à Ven', + jours_circulation: defaultJours, + _custom_jours: defaultJours, est_substitution: !!manualTrainForm.est_substitution, gares_desservies: [] }; @@ -644,12 +646,25 @@ export default function FichesHoraires() { heure_depart: '08:00', gare_arrivee_nom: '', heure_arrivee: '09:00', - jours_circulation: 'Lun à Ven', + jours_circulation: 'Lun à Ven*', est_substitution: false }); showToast(`Circulation ${newTrain.numero_train} ajoutée avec succès !`); }; + const handleUpdateTrainJours = (trainId, newJours) => { + setWizardOrderedTrains(prev => prev.map(t => { + if (t.id === trainId) { + return { + ...t, + _custom_jours: newJours, + jours_circulation: newJours + }; + } + return t; + })); + }; + // Helper to get direction of a circulation const getCirculationDirection = (circ, orderedGares = []) => { if (!circ) return 'aller'; @@ -1992,9 +2007,36 @@ export default function FichesHoraires() { {train.heure_depart || '—'} → {train.heure_arrivee || '—'} - - {formatCirculationDaysCompact(train)} - +
@@ -2281,13 +2323,26 @@ export default function FichesHoraires() {
- setManualTrainForm(prev => ({ ...prev, jours_circulation: e.target.value }))} - /> + > + + + + + + + + + + + + + + +
)} - {/* GTFS Step 3: Choix de la ligne */} + {/* GTFS Step 3: Choix de la ligne / Multi-lignes Accordion */} {gtfsStep === 3 && gtfsParsedData && !gtfsLoading && (
-
-

3. Choix de la ligne à importer

- setGtfsStep(2)}>Retour au Visualisateur -
+ {(() => { + const filteredRoutes = (gtfsParsedData.routes || []).filter(r => { + if (gtfsRouteTypeFilter !== 'all' && String(r.route_type) !== gtfsRouteTypeFilter) return false; + if (gtfsRouteSearch) { + const q = gtfsRouteSearch.toLowerCase(); + const matchRoute = (r.route_id || '').toLowerCase().includes(q) || + (r.route_short_name || '').toLowerCase().includes(q) || + (r.route_long_name || '').toLowerCase().includes(q); + if (matchRoute) return true; + // Search also inside trips of this route + const routeTrips = gtfsParsedData.tripsByRoute[r.route_id] || []; + return routeTrips.some(t => (t.trip_id || '').toLowerCase().includes(q) || (t.trip_headsign || '').toLowerCase().includes(q) || (t.trip_short_name || '').toLowerCase().includes(q)); + } + return true; + }); -
- setGtfsRouteSearch(e.target.value)} - /> -
- {[ - { key: 'all', label: 'Toutes les lignes' }, - { key: '2', label: '🚆 Trains' }, - { key: '3', label: '🚌 Substitutions (Bus/Car)' }, - { key: '0', label: '🚊 Tram' } - ].map(f => ( - - ))} -
-
+ const totalSelectedTripsCount = gtfsSelectedTripIds.size; + const selectedRoutesCount = new Set( + (gtfsParsedData.trips || []) + .filter(t => gtfsSelectedTripIds.has(t.trip_id)) + .map(t => t.route_id) + ).size; -
- {(gtfsParsedData.routes || []) - .filter(r => { - if (gtfsRouteTypeFilter !== 'all' && String(r.route_type) !== gtfsRouteTypeFilter) return false; - if (gtfsRouteSearch) { - const q = gtfsRouteSearch.toLowerCase(); - return (r.route_id || '').toLowerCase().includes(q) || - (r.route_short_name || '').toLowerCase().includes(q) || - (r.route_long_name || '').toLowerCase().includes(q); - } - return true; - }) - .map(r => { - const tripsCount = (gtfsParsedData.tripsByRoute[r.route_id] || []).length; - const bgColor = r.route_color ? `#${r.route_color}` : '#2563eb'; - const textColor = r.route_text_color ? `#${r.route_text_color}` : '#ffffff'; - const isBus = String(r.route_type) === '3' || (r.route_type >= 700 && r.route_type <= 716); - return ( -
handleSelectGTFSRoute(r.route_id)} - > -
-
- - {r.route_short_name || r.route_id} - - - {isBus ? '🚌 Substitution' : '🚆 Train'} - -
-
- {r.route_long_name || r.route_short_name || r.route_id} -
-
-
-
- {tripsCount} circulation(s) -
- Sélectionner → + const allFilteredTrips = filteredRoutes.flatMap(r => gtfsParsedData.tripsByRoute[r.route_id] || []); + const allFilteredSelected = allFilteredTrips.length > 0 && allFilteredTrips.every(t => gtfsSelectedTripIds.has(t.trip_id)); + const allAccordionOpen = filteredRoutes.length > 0 && filteredRoutes.every(r => gtfsOpenAccordionRouteIds.has(r.route_id)); + + return ( +
+
+
+

+ 3. Choix des lignes & circulations à importer +

+
+ Sélectionnez les lignes et cochez les horaires que vous souhaitez importer dans votre base.
- ); - })} -
+
+
+ + +
+ setGtfsStep(2)}>Retour au Visualisateur +
+
+ + {/* Filter & Action bar */} +
+
+ setGtfsRouteSearch(e.target.value)} + /> +
+ {[ + { key: 'all', label: 'Toutes les lignes' }, + { key: '2', label: '🚆 Trains' }, + { key: '3', label: '🚌 Substitutions (Bus/Car)' }, + { key: '0', label: '🚊 Tram' } + ].map(f => ( + + ))} +
+
+ +
+ handleSelectAllRoutesAndTrips(filteredRoutes, !allFilteredSelected)} + > + {allFilteredSelected ? 'Tout désélectionner' : 'Tout sélectionner'} + + {gtfsViewMode === 'accordion' && ( + allAccordionOpen ? handleCollapseAllAccordion() : handleExpandAllAccordion(filteredRoutes)} + > + {allAccordionOpen ? 'Replier tout' : 'Déplier tout'} + + )} +
+
+ + {/* ACCORDION VIEW MODE */} + {gtfsViewMode === 'accordion' && ( +
+ {filteredRoutes.length === 0 ? ( +
+ +

Aucune ligne ne correspond à votre recherche.

+
+ ) : ( + filteredRoutes.map(r => { + const routeTrips = gtfsParsedData.tripsByRoute[r.route_id] || []; + const selectedInRoute = routeTrips.filter(t => gtfsSelectedTripIds.has(t.trip_id)).length; + const isAllRouteSelected = routeTrips.length > 0 && selectedInRoute === routeTrips.length; + const isPartiallySelected = selectedInRoute > 0 && !isAllRouteSelected; + const isOpen = gtfsOpenAccordionRouteIds.has(r.route_id); + + const bgColor = r.route_color ? `#${r.route_color}` : '#2563eb'; + const textColor = r.route_text_color ? `#${r.route_text_color}` : '#ffffff'; + const isBus = String(r.route_type) === '3' || (r.route_type >= 700 && r.route_type <= 716); + + return ( +
0 ? 'has-selected' : ''}`}> +
handleToggleAccordionRoute(r.route_id)}> +
+ { + if (el) el.indeterminate = isPartiallySelected; + }} + onChange={(e) => { + e.stopPropagation(); + handleToggleRouteSelection(r.route_id, !isAllRouteSelected); + }} + onClick={(e) => e.stopPropagation()} + style={{ width: 18, height: 18, cursor: 'pointer' }} + /> + + {r.route_short_name || r.route_id} + + + {isBus ? '🚌 Substitution' : '🚆 Train'} + + + {r.route_long_name || r.route_short_name || r.route_id} + +
+ +
+ 0 ? 'active' : ''}`}> + {selectedInRoute} / {routeTrips.length} circulation(s) + + +
+
+ + {isOpen && ( +
+
+
+ {routeTrips.length} circulations répertoriées sur cette ligne +
+
+ handleToggleRouteSelection(r.route_id, !isAllRouteSelected)} + > + {isAllRouteSelected ? 'Désélectionner la ligne' : 'Sélectionner toute la ligne'} + + handleSelectGTFSRoute(r.route_id)} + > + Vue détaillée → + +
+
+ +
+ + + + + + + + + + + + + + {routeTrips.map(trip => { + const isTripSelected = gtfsSelectedTripIds.has(trip.trip_id); + const stopTimes = gtfsParsedData.stopTimesByTrip[trip.trip_id] || []; + const firstStop = stopTimes[0]; + const lastStop = stopTimes[stopTimes.length - 1]; + const firstStopName = firstStop ? (gtfsParsedData.stopsById[firstStop.stop_id]?.stop_name || firstStop.stop_id) : '—'; + const lastStopName = lastStop ? (gtfsParsedData.stopsById[lastStop.stop_id]?.stop_name || lastStop.stop_id) : '—'; + const depTime = firstStop ? formatGTFSTime(firstStop.departure_time) : '--:--'; + const arrTime = lastStop ? formatGTFSTime(lastStop.arrival_time) : '--:--'; + + const cal = gtfsParsedData.calendarByService[trip.service_id]; + const matchedSA = matchServiceAnnuelForGTFSTrip( + trip, + gtfsParsedData.calendarByService, + gtfsParsedData.calendarDatesByService, + servicesAnnuels + ); + + return ( + handleToggleGTFSTrip(trip.trip_id)} + > + + + + + + + + + ); + })} + +
+ { + if (el) el.indeterminate = isPartiallySelected; + }} + onChange={() => handleToggleRouteSelection(r.route_id, !isAllRouteSelected)} + /> + Train / SillonDépartArrivéeArrêtsJours de circulationService Annuel
e.stopPropagation()}> + handleToggleGTFSTrip(trip.trip_id)} + style={{ width: 16, height: 16, cursor: 'pointer' }} + /> + + + {trip.trip_headsign || trip.trip_short_name || trip.trip_id} + + {trip.trip_short_name && trip.trip_headsign && trip.trip_short_name !== trip.trip_headsign && ( +
{trip.trip_short_name}
+ )} +
+ {depTime} + {firstStopName} + + {arrTime} + {lastStopName} + + + {stopTimes.length} arrêts + + + {cal ? ( +
+
+ {['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'].map((dayKey, idx) => { + const labels = ['L', 'M', 'M', 'J', 'V', 'S', 'D']; + const active = cal[dayKey] === '1'; + return ( + + {labels[idx]} + + ); + })} +
+
+ {formatGTFSDate(cal.start_date)} → {formatGTFSDate(cal.end_date)} +
+
+ ) : ( + Ponctuel + )} +
+ {matchedSA ? ( + + 📅 {matchedSA.nom} + + ) : ( + ⚠️ Hors SA + )} +
+
+
+ )} +
+ ); + }) + )} +
+ )} + + {/* GRID VIEW MODE */} + {gtfsViewMode === 'grid' && ( +
+ {filteredRoutes.map(r => { + const routeTrips = (gtfsParsedData.tripsByRoute[r.route_id] || []); + const tripsCount = routeTrips.length; + const selectedInRoute = routeTrips.filter(t => gtfsSelectedTripIds.has(t.trip_id)).length; + const bgColor = r.route_color ? `#${r.route_color}` : '#2563eb'; + const textColor = r.route_text_color ? `#${r.route_text_color}` : '#ffffff'; + const isBus = String(r.route_type) === '3' || (r.route_type >= 700 && r.route_type <= 716); + return ( +
0 ? 'selected' : ''}`} + onClick={() => handleSelectGTFSRoute(r.route_id)} + > +
+
+ + {r.route_short_name || r.route_id} + + + {isBus ? '🚌 Substitution' : '🚆 Train'} + +
+
+ {r.route_long_name || r.route_short_name || r.route_id} +
+
+
+
+ {selectedInRoute > 0 ? `${selectedInRoute} / ${tripsCount}` : `${tripsCount}`} circulation(s) +
+ Sélectionner → +
+
+ ); + })} +
+ )} + + {/* STICKY BOTTOM ACTION BAR FOR MULTI-SELECTION */} + {totalSelectedTripsCount > 0 && ( +
+
+ +
+
+ {totalSelectedTripsCount} circulation(s) sélectionnée(s) +
+
+ Réparties sur {selectedRoutesCount} ligne(s) +
+
+
+
+ setGtfsSelectedTripIds(new Set())} + > + Tout désélectionner + + + {gtfsConverting ? ( + <> + + Conversion en cours... + + ) : ( + <> + Convertir et Importer les {totalSelectedTripsCount} circulations + + + )} + +
+
+ )} +
+ ); + })()}
)} diff --git a/src/utils/ficheHorairePdfGenerator.js b/src/utils/ficheHorairePdfGenerator.js index 0b7edaf..2bac9d1 100644 --- a/src/utils/ficheHorairePdfGenerator.js +++ b/src/utils/ficheHorairePdfGenerator.js @@ -82,15 +82,197 @@ export function isCirculationCar(circ) { } /** - * Formate les jours de circulation en libellé compact conforme au modèle officiel + * Formate les jours de circulation en libellé compact conforme au modèle officiel SNCF / TER */ export function formatCirculationDaysCompact(horaire) { if (!horaire) return 'Tous les jours'; + // 0. Si l'utilisateur a explicitement personnalisé les jours pour cette circulation + if (horaire._custom_jours && typeof horaire._custom_jours === 'string') { + return horaire._custom_jours.trim(); + } + + const parseBool = (v) => { + if (typeof v === 'boolean') return v; + if (typeof v === 'number') return v === 1; + if (typeof v === 'string') { + const s = v.trim().toLowerCase(); + return s === 'true' || s === 't' || s === '1' || s === 'oui' || s === 'yes' || s === 'y'; + } + return false; + }; + const numStr = String(horaire.numero_train || '').trim(); - // 1. Table de référence exacte des circulations TER BFC / AURA (Ligne Dijon <-> Seurre <-> Bourg-en-Bresse) - const specificTrainDays = { + // 1. Analyse approfondie des jours personnalisés (dates réelles dans la base) + const jp = horaire.jours_personnalises; + if (Array.isArray(jp) && jp.length > 0) { + const activeDates = jp.filter(j => j && j.circule !== false && j.date); + if (activeDates.length > 0) { + const dowsCount = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }; + activeDates.forEach(j => { + const dStr = String(j.date).slice(0, 10); + const parts = dStr.split('-'); + if (parts.length === 3) { + const dt = new Date(Date.UTC(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10), 12, 0, 0)); + const dow = dt.getUTCDay(); // 0=Sun, 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5=Fri, 6=Sat + if (!isNaN(dow)) dowsCount[dow]++; + } + }); + + const maxCount = Math.max(...Object.values(dowsCount)); + // Un jour de la semaine est considéré comme actif s'il représente au moins 10% des occurrences maximales + const threshold = Math.max(1, maxCount * 0.1); + + const hasSun = dowsCount[0] >= threshold; + const hasMon = dowsCount[1] >= threshold; + const hasTue = dowsCount[2] >= threshold; + const hasWed = dowsCount[3] >= threshold; + const hasThu = dowsCount[4] >= threshold; + const hasFri = dowsCount[5] >= threshold; + const hasSat = dowsCount[6] >= threshold; + + const activeDows = [hasSun, hasMon, hasTue, hasWed, hasThu, hasFri, hasSat].filter(Boolean).length; + + if (activeDows === 7 || (hasMon && hasTue && hasWed && hasThu && hasFri && hasSat && hasSun)) { + return 'Tous les jours'; + } + if (hasMon && hasTue && hasWed && hasThu && hasFri && !hasSat && !hasSun) { + return 'Lun à Ven*'; + } + if (hasMon && hasTue && hasWed && hasThu && hasFri && hasSat && !hasSun) { + return 'Lun à Sam*'; + } + if (!hasMon && !hasTue && !hasWed && !hasThu && !hasFri && hasSat && hasSun) { + return 'Sam et Dim'; + } + if (activeDows === 1 && hasSun) { + return 'Dim et Fêtes'; + } + if (activeDows === 1 && hasSat) { + return 'Sam'; + } + if (activeDows === 1 && hasFri) { + return 'Ven'; + } + if (hasFri && hasSun && !hasMon && !hasTue && !hasWed && !hasThu && !hasSat) { + return 'Ven, Dim et Fêtes'; + } + if (hasMon && hasTue && hasWed && hasThu && !hasFri && !hasSat && !hasSun) { + return 'Lun à Jeu'; + } + if (hasMon && hasTue && hasWed && hasThu && hasFri && !hasSat && hasSun) { + return 'Sauf Sam'; + } + if (!hasMon && hasTue && hasWed && hasThu && hasFri && hasSat && !hasSun) { + return 'Mar à Sam'; + } + + const activeLabels = []; + if (hasMon) activeLabels.push('Lun'); + if (hasTue) activeLabels.push('Mar'); + if (hasWed) activeLabels.push('Mer'); + if (hasThu) activeLabels.push('Jeu'); + if (hasFri) activeLabels.push('Ven'); + if (hasSat) activeLabels.push('Sam'); + if (hasSun) activeLabels.push('Dim'); + if (activeLabels.length > 0 && activeLabels.length < 7) { + return activeLabels.join(', '); + } + } + } + + // 2. Analyse des booléens de jours de circulation + const cLu = parseBool(horaire.circule_lundi); + const cMa = parseBool(horaire.circule_mardi); + const cMe = parseBool(horaire.circule_mercredi); + const cJe = parseBool(horaire.circule_jeudi); + const cVe = parseBool(horaire.circule_vendredi); + const cSa = parseBool(horaire.circule_samedi); + const cDi = parseBool(horaire.circule_dimanche); + const cFer = parseBool(horaire.circule_jours_feries) || parseBool(horaire.circule_dimanches_feries); + + const anyDayTrue = cLu || cMa || cMe || cJe || cVe || cSa || cDi; + + if (anyDayTrue) { + const allWeek = cLu && cMa && cMe && cJe && cVe && cSa && cDi; + const lu_ve = cLu && cMa && cMe && cJe && cVe && !cSa && !cDi; + const lu_sa = cLu && cMa && cMe && cJe && cVe && cSa && !cDi; + const sa_di = !cLu && !cMa && !cMe && !cJe && !cVe && cSa && cDi; + const di_only = !cLu && !cMa && !cMe && !cJe && !cVe && !cSa && cDi; + const sa_only = !cLu && !cMa && !cMe && !cJe && !cVe && cSa && !cDi; + const ve_di = !cLu && !cMa && !cMe && !cJe && cVe && !cSa && cDi; + const ve_only = !cLu && !cMa && !cMe && !cJe && cVe && !cSa && !cDi; + const lu_je = cLu && cMa && cMe && cJe && !cVe && !cSa && !cDi; + const sauf_sa = cLu && cMa && cMe && cJe && cVe && !cSa && cDi; + const mar_sa = !cLu && cMa && cMe && cJe && cVe && cSa && !cDi; + + if (allWeek) return 'Tous les jours'; + if (lu_ve) return cFer ? 'Lun à Ven' : 'Lun à Ven*'; + if (lu_sa) return cFer ? 'Lun à Sam' : 'Lun à Sam*'; + if (sa_di) return cFer ? 'Sam, Dim et Fêtes' : 'Sam et Dim'; + if (di_only) return cFer ? 'Dim et Fêtes' : 'Dim'; + if (ve_di) return cFer ? 'Ven, Dim et Fêtes' : 'Ven et Dim'; + if (sa_only) return 'Sam'; + if (ve_only) return 'Ven'; + if (lu_je) return 'Lun à Jeu'; + if (sauf_sa) return 'Sauf Sam'; + if (mar_sa) return 'Mar à Sam'; + + // Cas de 4 jours ouvrés (ex: Lu, Ma, Je, Ve) + if (cLu && cMa && !cMe && cJe && cVe && !cSa && !cDi) return 'Lu, Ma, Je, Ve'; + if (cLu && !cMa && cMe && cJe && cVe && !cSa && !cDi) return 'Lu, Me, Je, Ve'; + if (cLu && cMa && cMe && !cJe && cVe && !cSa && !cDi) return 'Lu, Ma, Me, Ve'; + + // Combinaisons de 2 jours + if (cMe && cVe && !cLu && !cMa && !cJe && !cSa && !cDi) return 'Mer, Ven'; + if (cLu && cVe && !cMa && !cMe && !cJe && !cSa && !cDi) return 'Lun, Ven'; + if (cLu && cMe && !cMa && !cJe && !cVe && !cSa && !cDi) return 'Lun, Mer'; + + // Jour unique + if (cLu && !cMa && !cMe && !cJe && !cVe && !cSa && !cDi) return 'Lun'; + if (!cLu && cMa && !cMe && !cJe && !cVe && !cSa && !cDi) return 'Mar'; + if (!cLu && !cMa && cMe && !cJe && !cVe && !cSa && !cDi) return 'Mer'; + if (!cLu && !cMa && !cMe && cJe && !cVe && !cSa && !cDi) return 'Jeu'; + + const activeDays = []; + if (cLu) activeDays.push('Lu'); + if (cMa) activeDays.push('Ma'); + if (cMe) activeDays.push('Me'); + if (cJe) activeDays.push('Je'); + if (cVe) activeDays.push('Ve'); + if (cSa) activeDays.push('Sa'); + if (cDi) activeDays.push('Di'); + if (activeDays.length > 0) return activeDays.join(', '); + } + + // 3. Détection par champ texte (jours_circulation, frequence, periodicite) + const textDay = String(horaire.jours_circulation || horaire.frequence || horaire.periodicite || '').trim(); + if (textDay) { + const tLower = textDay.toLowerCase(); + if (/lun.*ven.*fer|lun.*ven\*/i.test(tLower)) return 'Lun à Ven*'; + if (/lun.*ven/i.test(tLower) && !/sam/i.test(tLower) && !/dim/i.test(tLower)) return 'Lun à Ven'; + if (/lun.*sam.*fer|lun.*sam\*/i.test(tLower)) return 'Lun à Sam*'; + if (/lun.*sam/i.test(tLower) && !/dim/i.test(tLower)) return 'Lun à Sam'; + if (/ven.*dim.*f[eê]t/i.test(tLower)) return 'Ven, Dim et Fêtes'; + if (/dim.*f[eê]t/i.test(tLower)) return 'Dim et Fêtes'; + if (/sam.*dim.*f[eê]t/i.test(tLower)) return 'Sam, Dim et Fêtes'; + if (/sam.*dim|week-?end/i.test(tLower) && !/lun/i.test(tLower)) return 'Sam et Dim'; + if (/sauf.*sam/i.test(tLower)) return 'Sauf Sam'; + if (/sauf.*dim/i.test(tLower)) return 'Lun à Sam*'; + if (/^dim(anche)?$/i.test(tLower)) return 'Dim'; + if (/^sam(edi)?$/i.test(tLower)) return 'Sam'; + if (/^ven(dredi)?$/i.test(tLower)) return 'Ven'; + if (/^mer(credi)?$/i.test(tLower)) return 'Mer'; + if (/tous.*jours|quotidien|7j/i.test(tLower)) return 'Tous les jours'; + + if (textDay.length <= 25 && !textDay.includes(';')) { + return textDay; + } + } + + // 4. Table de référence de secours pour numéros standards TER BFC / AURA + const standardTrainDays = { '891800': 'Lun à Ven*', '891802': 'Lun à Ven*', '891804': 'Tous les jours', @@ -115,84 +297,10 @@ export function formatCirculationDaysCompact(horaire) { '895922': 'Ven, Dim et Fêtes' }; - if (numStr === '891818') { - const textCheck = String(horaire.jours_circulation || '').toLowerCase(); - if (textCheck.includes('dim') || horaire.circule_dimanche === true) return 'Dim'; - return 'Lun à Ven*'; + if (standardTrainDays[numStr]) { + return standardTrainDays[numStr]; } - // 2. Détection par texte explicite (jours_circulation, frequence, periodicite) - const textDay = String(horaire.jours_circulation || horaire.frequence || horaire.periodicite || '').toLowerCase().trim(); - if (textDay) { - if (/lun.*ven.*fer|lun.*ven\*/i.test(textDay)) return 'Lun à Ven*'; - if (/lun.*ven/i.test(textDay) && !/sam/i.test(textDay) && !/dim/i.test(textDay)) return 'Lun à Ven'; - if (/lun.*sam.*fer|lun.*sam\*/i.test(textDay)) return 'Lun à Sam*'; - if (/lun.*sam/i.test(textDay) && !/dim/i.test(textDay)) return 'Lun à Sam'; - if (/sam.*dim|week-?end/i.test(textDay) && !/lun/i.test(textDay)) return 'Sam et Dim'; - if (/ven.*dim.*f[eê]t/i.test(textDay)) return 'Ven, Dim et Fêtes'; - if (/dim.*f[eê]t/i.test(textDay)) return 'Dim et Fêtes'; - if (/sauf.*sam/i.test(textDay)) return 'Sauf Sam'; - if (/^dim(anche)?$/i.test(textDay)) return 'Dim'; - if (/^sam(edi)?$/i.test(textDay)) return 'Sam'; - if (/^ven(dredi)?$/i.test(textDay)) return 'Ven'; - if (/tous.*jours|quotidien|7j/i.test(textDay)) return 'Tous les jours'; - } - - // 3. Détection par booléens ou indicateurs de jours (avec coercion robuste) - const parseBool = (v) => { - if (typeof v === 'boolean') return v; - if (typeof v === 'number') return v === 1; - if (typeof v === 'string') { - const s = v.trim().toLowerCase(); - return s === 'true' || s === 't' || s === '1' || s === 'oui'; - } - return false; - }; - - const cLu = parseBool(horaire.circule_lundi); - const cMa = parseBool(horaire.circule_mardi); - const cMe = parseBool(horaire.circule_mercredi); - const cJe = parseBool(horaire.circule_jeudi); - const cVe = parseBool(horaire.circule_vendredi); - const cSa = parseBool(horaire.circule_samedi); - const cDi = parseBool(horaire.circule_dimanche); - const cFer = parseBool(horaire.circule_jours_feries); - - const hasAnyDayBool = [cLu, cMa, cMe, cJe, cVe, cSa, cDi].some(Boolean); - - if (hasAnyDayBool) { - const lu_ve = cLu && cMa && cMe && cJe && cVe && !cSa && !cDi; - const lu_sa = cLu && cMa && cMe && cJe && cVe && cSa && !cDi; - const tous = cLu && cMa && cMe && cJe && cVe && cSa && cDi; - - if (tous) return 'Tous les jours'; - if (lu_ve) return cFer ? 'Lun à Ven' : 'Lun à Ven*'; - if (lu_sa) return cFer ? 'Lun à Sam' : 'Lun à Sam*'; - if (!cLu && !cMa && !cMe && !cJe && !cVe && cSa && cDi) return 'Sam et Dim'; - if (!cLu && !cMa && !cMe && !cJe && !cVe && !cSa && cDi) return cFer ? 'Dim et Fêtes' : 'Dim'; - if (!cLu && !cMa && !cMe && !cJe && cVe && !cSa && cDi) return 'Ven, Dim et Fêtes'; - if (cSa && !cDi && !lu_ve) return 'Sam'; - if (cLu && cMa && cMe && cJe && cVe && !cSa && cDi) return 'Sauf Sam'; - - // Jours individuels - const days = []; - if (cLu) days.push('Lu'); - if (cMa) days.push('Ma'); - if (cMe) days.push('Me'); - if (cJe) days.push('Je'); - if (cVe) days.push('Ve'); - if (cSa) days.push('Sa'); - if (cDi) days.push('Di'); - if (days.length > 0) return days.join(', '); - } - - // 4. Si la table de référence par numéro de train s'applique - if (specificTrainDays[numStr]) { - return specificTrainDays[numStr]; - } - - if (textDay) return textDay; - return 'Tous les jours'; }