import supabase, { isSupabaseConfigured } from '../supabaseClient'; const normalizeGareName = (value) => String(value || '').trim().toLowerCase(); const isSameGare = (row, refId, refNom) => { if (!row) return false; const rowId = row.gare_id || row.id || null; if (refId && rowId && String(rowId) === String(refId)) return true; const refName = normalizeGareName(refNom); return refName && normalizeGareName(row.gare_nom || row.nom) === refName; }; const propagateQuaisByNumeroTrain = (horaires) => { if (!horaires || horaires.length === 0) return horaires; // 1. S'assurer que tous les horaires ont leur départ/arrivée dans gares_desservies horaires.forEach(h => { if (!Array.isArray(h.gares_desservies)) { h.gares_desservies = []; } const snapshot = Array.isArray(h.snapshot_gares_desservies) ? h.snapshot_gares_desservies : []; const hasDepart = h.gares_desservies.some(gd => Number(gd.ordre) === 0 || isSameGare(gd, h.gare_depart_id, h.gare_depart_nom)); if (!hasDepart && h.gare_depart_nom) { const snapDepart = snapshot.find(s => Number(s.ordre) === 0 || s.is_depart || isSameGare(s, h.gare_depart_id, h.gare_depart_nom)); h.gares_desservies.push({ horaire_id: h.id, gare_id: h.gare_depart_id, gare_nom: h.gare_depart_nom, heure_arrivee: null, heure_depart: h.heure_depart, ordre: 0, quai: snapDepart ? (snapDepart.quai || null) : null, quai_reel: snapDepart ? (snapDepart.quai_reel || null) : null, is_depart: true }); } const hasArrivee = h.gares_desservies.some(gd => isSameGare(gd, h.gare_arrivee_id, h.gare_arrivee_nom)); if (!hasArrivee && h.gare_arrivee_nom) { const snapArrivee = snapshot.find(s => s.is_arrivee || isSameGare(s, h.gare_arrivee_id, h.gare_arrivee_nom)); h.gares_desservies.push({ horaire_id: h.id, gare_id: h.gare_arrivee_id, gare_nom: h.gare_arrivee_nom, heure_arrivee: h.heure_arrivee, heure_depart: null, ordre: snapArrivee ? (snapArrivee.ordre || 9999) : 9999, quai: snapArrivee ? (snapArrivee.quai || null) : null, quai_reel: snapArrivee ? (snapArrivee.quai_reel || null) : null, is_arrivee: true }); } }); const groups = {}; horaires.forEach(h => { if (h.numero_train) { if (!groups[h.numero_train]) groups[h.numero_train] = []; groups[h.numero_train].push(h); } }); Object.keys(groups).forEach(numTrain => { const group = groups[numTrain]; if (group.length <= 1) return; const gareQuais = {}; group.forEach(h => { if (Array.isArray(h.gares_desservies)) { h.gares_desservies.forEach(gd => { const key = normalizeGareName(gd.gare_nom); if (key) { if (!gareQuais[key]) { gareQuais[key] = { quai: null, quai_reel: null }; } if (gd.quai && !gareQuais[key].quai) { gareQuais[key].quai = gd.quai; } if (gd.quai_reel && !gareQuais[key].quai_reel) { gareQuais[key].quai_reel = gd.quai_reel; } } }); } }); const hasAnyQuai = Object.values(gareQuais).some(q => q.quai || q.quai_reel); if (!hasAnyQuai) return; group.forEach(h => { if (Array.isArray(h.gares_desservies)) { h.gares_desservies.forEach(gd => { const key = normalizeGareName(gd.gare_nom); if (key && gareQuais[key]) { if (!gd.quai && gareQuais[key].quai) { gd.quai = gareQuais[key].quai; } if (!gd.quai_reel && gareQuais[key].quai_reel) { gd.quai_reel = gareQuais[key].quai_reel; } } }); } }); }); // Trier de nouveau si on a inséré des gares de départ/arrivée horaires.forEach(h => { if (Array.isArray(h.gares_desservies)) { h.gares_desservies.sort((a, b) => (Number(a.ordre) || 0) - (Number(b.ordre) || 0)); } }); return horaires; }; // ============================================================================ // API HORAIRES (SILLONS) // ============================================================================ /** * Récupère tous les horaires avec leurs gares desservies et matériel roulant * @param {string} [regionId] - Filtrer par région (optionnel) */ export async function getAllHoraires(regionId = null) { if (!isSupabaseConfigured) return { data: [], error: null }; try { // Si on demande un filtrage par région, récupérer d'abord les lignes associées let ligneIds = null; if (regionId) { try { const { data: lignesData, error: lignesError } = await supabase .from('lignes') .select('id') .eq('region_id', regionId); if (lignesError) { // Log the error but don't throw, as we can still filter by horaires.region_id console.error('Error fetching lignes by region:', lignesError); ligneIds = []; } else if (Array.isArray(lignesData)) { ligneIds = lignesData.map(l => l.id).filter(Boolean); } else { ligneIds = []; } } catch (e) { console.error('Exception while fetching ligneIds:', e); ligneIds = []; } } const SELECT_QUERY = ` *, materiel_roulant:materiel-roulant(id, nom, nom_technique, image_url) `; let orConditions = []; if (regionId) { if (ligneIds && ligneIds.length > 0) { orConditions.push(`ligne_id.in.(${ligneIds.join(',')})`); } orConditions.push(`region_id.eq.${regionId}`); } let allData = []; let start = 0; const limit = 1000; let hasMore = true; let currentSelect = SELECT_QUERY; while (hasMore) { let query = supabase .from('horaires') .select(currentSelect) .order('created_at', { ascending: false }) .range(start, start + limit - 1); if (regionId) { query = query.or(orConditions.join(',')); } const { data, error } = await query; if (error) { if (currentSelect !== '*' && (error.message?.includes('materiel-roulant') || error.message?.includes('relationship') || error.code === 'PGRST200')) { console.warn('Relation materiel-roulant absente du cache de schéma PostgREST, repli sur SELECT *.'); currentSelect = '*'; allData = []; start = 0; continue; } console.error('Erreur getAllHoraires fetch:', error); return { data: [], error }; } if (data && data.length > 0) { allData = allData.concat(data); start += limit; } if (!data || data.length < limit) { hasMore = false; } } // Normaliser gares_desservies depuis la colonne JSONB unique const normalizedData = (allData || []).map(h => { const gares = Array.isArray(h.gares_desservies) ? [...h.gares_desservies] : []; gares.sort((a, b) => (Number(a.ordre) || 0) - (Number(b.ordre) || 0)); return { ...h, gares_desservies: gares }; }); const propagatedData = propagateQuaisByNumeroTrain(normalizedData); return { data: propagatedData, error: null }; } catch (err) { console.error('Erreur getAllHoraires:', err); return { data: [], error: err }; } } /** * Récupère un horaire par son ID depuis la table unique horaires */ export async function getHoraireById(id) { if (!isSupabaseConfigured) return { data: null, error: { message: 'Supabase non configuré' } }; try { let { data, error } = await supabase .from('horaires') .select(` *, materiel_roulant:materiel-roulant(id, nom, nom_technique, image_url) `) .eq('id', id) .single(); if (error && (error.message?.includes('materiel-roulant') || error.message?.includes('relationship') || error.code === 'PGRST200')) { const fallback = await supabase .from('horaires') .select('*') .eq('id', id) .single(); data = fallback.data; error = fallback.error; } if (error) return { data: null, error }; if (data) { const gares = Array.isArray(data.gares_desservies) ? [...data.gares_desservies] : []; gares.sort((a, b) => (Number(a.ordre) || 0) - (Number(b.ordre) || 0)); const hCopy = { ...data, gares_desservies: gares }; const propagated = propagateQuaisByNumeroTrain([hCopy]); return { data: propagated[0], error: null }; } return { data: null, error: null }; } catch (err) { console.error('Erreur getHoraireById:', err); return { data: null, error: err }; } } /** * Crée un nouvel horaire avec ses gares desservies * @param {Object} payload - Données de l'horaire * @param {Array} garesDesservies - Liste des gares desservies [{gare_nom, heure_arrivee, heure_depart, ordre}, ...] * @param {Object|null} currentUser - L'utilisateur actuellement connecté */ export async function createHoraire(payload, garesDesservies = [], currentUser = null) { if (!isSupabaseConfigured) { const fakeId = `local-${Date.now()}`; return { data: { id: fakeId, ...payload, gares_desservies: garesDesservies }, error: null }; } try { // Si l'utilisateur est un exploitant, pré-attribuer sa région if (currentUser && currentUser.role === 'exploitant' && currentUser.region_id) { payload.region_id = currentUser.region_id; } // Construire le snapshot JSON des gares desservies const garesDesserviesJson = (garesDesservies || []).map((g, idx) => ({ gare_id: g.gare_id || null, gare_nom: g.gare_nom || '', heure_arrivee: g.heure_arrivee || null, heure_depart: g.heure_depart || null, ordre: g.ordre ?? idx + 1, quai: g.quai || null, quai_reel: g.quai_reel || null })); // Ajouter les entrées départ et arrivée const departEntry = { gare_id: payload.gare_depart_id || null, gare_nom: payload.gare_depart_nom || '', heure_arrivee: null, heure_depart: payload.heure_depart || null, ordre: 0, quai: null, quai_reel: null, is_depart: true }; const maxOrd = garesDesserviesJson.reduce((m, e) => Math.max(m, Number(e.ordre || 0)), 0); const arriveeEntry = { gare_id: payload.gare_arrivee_id || null, gare_nom: payload.gare_arrivee_nom || '', heure_arrivee: payload.heure_arrivee || null, heure_depart: null, ordre: maxOrd + 1, quai: null, quai_reel: null, is_arrivee: true }; const fullGaresDesserviesJson = [departEntry, ...garesDesserviesJson, arriveeEntry]; // Construire le dictionnaire O(1) attributions_quais const attributionsQuais = {}; fullGaresDesserviesJson.forEach(g => { const key = g.gare_id || g.gare_nom; if (key && (g.quai || g.quai_reel)) { attributionsQuais[key] = { quai: g.quai || null, quai_reel: g.quai_reel || null, ordre: g.ordre }; } }); // Insérer de manière atomique dans la table unique horaires let { data: horaire, error: horaireError } = await supabase .from('horaires') .insert([{ ...payload, gares_desservies: fullGaresDesserviesJson, attributions_quais: attributionsQuais, created_at: new Date(), updated_at: new Date() }]) .select(` *, materiel_roulant:materiel-roulant(id, nom, nom_technique, image_url) `) .single(); if (horaireError && (horaireError.message?.includes('materiel-roulant') || horaireError.message?.includes('relationship') || horaireError.code === 'PGRST200')) { const fallback = await supabase .from('horaires') .insert([{ ...payload, gares_desservies: fullGaresDesserviesJson, attributions_quais: attributionsQuais, created_at: new Date(), updated_at: new Date() }]) .select('*') .single(); horaire = fallback.data; horaireError = fallback.error; } if (horaireError) { console.error('Erreur création horaire:', horaireError); return { data: null, error: horaireError }; } return { data: horaire, error: null }; } catch (err) { console.error('Exception createHoraire:', err); return { data: null, error: err }; } } export async function updateHorairesLigneBatch(ids, ligne_id, ligne_nom) { if (!isSupabaseConfigured) return { error: null }; try { const { error } = await supabase .from('horaires') .update({ ligne_id, ligne_nom, updated_at: new Date() }) .in('id', ids); return { error }; } catch (err) { console.error('Exception updateHorairesLigneBatch:', err); return { error: err }; } } /** * Met à jour un horaire existant et ses gares desservies dans la table unique * @param {number|string} id - ID de l'horaire * @param {Object} payload - Données à mettre à jour * @param {Array} garesDesservies - Nouvelle liste des gares desservies (remplace l'existante) */ export async function updateHoraire(id, payload, garesDesservies = null) { if (!isSupabaseConfigured) { return { data: { id, ...payload }, error: null }; } try { let updateFields = { ...payload, updated_at: new Date() }; if (garesDesservies !== null) { const garesDesserviesJson = (garesDesservies || []).map((g, idx) => ({ gare_id: g.gare_id || null, gare_nom: g.gare_nom || '', heure_arrivee: g.heure_arrivee || null, heure_depart: g.heure_depart || null, ordre: g.ordre ?? idx + 1, quai: g.quai || null, quai_reel: g.quai_reel || null })); const hasDepart = garesDesserviesJson.some(g => Number(g.ordre) === 0 || g.is_depart); const departEntry = hasDepart ? null : { gare_id: payload.gare_depart_id || null, gare_nom: payload.gare_depart_nom || '', heure_arrivee: null, heure_depart: payload.heure_depart || null, ordre: 0, quai: null, quai_reel: null, is_depart: true }; const maxOrd = garesDesserviesJson.reduce((m, e) => Math.max(m, Number(e.ordre || 0)), 0); const hasArrivee = garesDesserviesJson.some(g => g.is_arrivee); const arriveeEntry = hasArrivee ? null : { gare_id: payload.gare_arrivee_id || null, gare_nom: payload.gare_arrivee_nom || '', heure_arrivee: payload.heure_arrivee || null, heure_depart: null, ordre: maxOrd + 1, quai: null, quai_reel: null, is_arrivee: true }; const fullList = [ ...(departEntry ? [departEntry] : []), ...garesDesserviesJson, ...(arriveeEntry ? [arriveeEntry] : []) ].sort((a, b) => (Number(a.ordre) || 0) - (Number(b.ordre) || 0)); const attributionsQuais = {}; fullList.forEach(g => { const key = g.gare_id || g.gare_nom; if (key && (g.quai || g.quai_reel)) { attributionsQuais[key] = { quai: g.quai || null, quai_reel: g.quai_reel || null, ordre: g.ordre }; } }); updateFields.gares_desservies = fullList; updateFields.attributions_quais = attributionsQuais; } // Mettre à jour l'horaire de manière atomique let { data: updated, error: updateError } = await supabase .from('horaires') .update(updateFields) .eq('id', id) .select(` *, materiel_roulant:materiel-roulant(id, nom, nom_technique, image_url) `) .single(); if (updateError && (updateError.message?.includes('materiel-roulant') || updateError.message?.includes('relationship') || updateError.code === 'PGRST200')) { const fallback = await supabase .from('horaires') .update(updateFields) .eq('id', id) .select('*') .single(); updated = fallback.data; updateError = fallback.error; } if (updateError) { console.error('Erreur mise à jour horaire:', updateError); return { data: null, error: updateError }; } return { data: updated, error: null }; } catch (err) { console.error('Exception updateHoraire:', err); return { data: null, error: err }; } } /** * Met à jour uniquement la composition du train pour un horaire. * Utilisé par la page d'attribution du matériel roulant. */ export async function updateHoraireComposition(id, compositionTrain = []) { if (!isSupabaseConfigured) { return { data: { id, composition_train: compositionTrain }, error: null }; } try { const normalized = (compositionTrain || []).map((item, idx) => ({ materiel_id: item?.materiel_id, materiel_nom: item?.materiel_nom || '', materiel_nom_technique: item?.materiel_nom_technique || '', materiel_image_url: item?.materiel_image_url || null, position: idx + 1 })); const { data, error } = await supabase .from('horaires') .update({ composition_train: normalized, updated_at: new Date() }) .eq('id', id) .select('*') .single(); return { data, error }; } catch (err) { console.error('Exception updateHoraireComposition:', err); return { data: null, error: err }; } } /** * Supprime un horaire (hard delete) * @param {number|string} id - ID de l'horaire */ export async function deleteHoraire(id) { if (!isSupabaseConfigured) return { error: null }; try { const { error } = await supabase .from('horaires') .delete() .eq('id', id); return { error }; } catch (err) { console.error('Exception deleteHoraire:', err); return { error: err }; } } // ============================================================================ // API GARES DESSERVIES (GESTION DANS LA TABLE UNIQUE HORAIRES) // ============================================================================ /** * Ajoute une gare desservie directement dans l'horaire (colonne JSONB gares_desservies) */ export async function addGareDesservie(horaireId, gareData) { if (!isSupabaseConfigured) { return { data: { id: `local-${Date.now()}`, horaire_id: horaireId, ...gareData }, error: null }; } try { const { data: horaire, error: readErr } = await supabase .from('horaires') .select('gares_desservies, attributions_quais') .eq('id', horaireId) .single(); if (readErr) return { data: null, error: readErr }; const gares = Array.isArray(horaire?.gares_desservies) ? [...horaire.gares_desservies] : []; const newGare = { gare_id: gareData.gare_id || null, gare_nom: gareData.gare_nom || '', heure_arrivee: gareData.heure_arrivee || null, heure_depart: gareData.heure_depart || null, ordre: gareData.ordre ?? gares.length + 1, quai: gareData.quai || null, quai_reel: gareData.quai_reel || null }; gares.push(newGare); gares.sort((a, b) => (Number(a.ordre) || 0) - (Number(b.ordre) || 0)); const attributionsQuais = { ...(horaire?.attributions_quais || {}) }; const key = newGare.gare_id || newGare.gare_nom; if (key && (newGare.quai || newGare.quai_reel)) { attributionsQuais[key] = { quai: newGare.quai || null, quai_reel: newGare.quai_reel || null, ordre: newGare.ordre }; } const { error: updateErr } = await supabase .from('horaires') .update({ gares_desservies: gares, attributions_quais: attributionsQuais, updated_at: new Date() }) .eq('id', horaireId); return { data: newGare, error: updateErr }; } catch (err) { console.error('Exception addGareDesservie:', err); return { data: null, error: err }; } } /** * Met à jour une gare desservie dans l'horaire */ export async function updateGareDesservie(horaireId, gareData) { if (!isSupabaseConfigured) { return { data: gareData, error: null }; } try { const { data: horaire, error: readErr } = await supabase .from('horaires') .select('gares_desservies, attributions_quais') .eq('id', horaireId) .single(); if (readErr) return { data: null, error: readErr }; const gares = Array.isArray(horaire?.gares_desservies) ? [...horaire.gares_desservies] : []; const idx = gares.findIndex(g => (g.gare_id && g.gare_id === gareData.gare_id) || Number(g.ordre) === Number(gareData.ordre)); if (idx !== -1) { gares[idx] = { ...gares[idx], ...gareData }; } else { gares.push(gareData); } gares.sort((a, b) => (Number(a.ordre) || 0) - (Number(b.ordre) || 0)); const attributionsQuais = { ...(horaire?.attributions_quais || {}) }; const key = gareData.gare_id || gareData.gare_nom; if (key && (gareData.quai || gareData.quai_reel)) { attributionsQuais[key] = { quai: gareData.quai || null, quai_reel: gareData.quai_reel || null, ordre: gareData.ordre }; } const { error } = await supabase .from('horaires') .update({ gares_desservies: gares, attributions_quais: attributionsQuais, updated_at: new Date() }) .eq('id', horaireId); return { data: gareData, error }; } catch (err) { console.error('Exception updateGareDesservie:', err); return { data: null, error: err }; } } /** * Supprime une gare desservie de l'horaire */ export async function deleteGareDesservie(horaireId, target) { if (!isSupabaseConfigured) return { error: null }; try { const { data: horaire, error: readErr } = await supabase .from('horaires') .select('gares_desservies, attributions_quais') .eq('id', horaireId) .single(); if (readErr) return { error: readErr }; let gares = Array.isArray(horaire?.gares_desservies) ? [...horaire.gares_desservies] : []; gares = gares.filter(g => g.gare_id !== target && Number(g.ordre) !== Number(target)); const { error } = await supabase .from('horaires') .update({ gares_desservies: gares, updated_at: new Date() }) .eq('id', horaireId); return { error }; } catch (err) { console.error('Exception deleteGareDesservie:', err); return { error: err }; } } /** * Met à jour les quais directement dans la table unique horaires * * @param {string|number} horaireId - ID de l'horaire * @param {Object} quaisParOrdre - Map { ordre -> quai_nom } pour les gares intermédiaires * + clés spéciales "depart" et "arrivee" */ export async function updateQuaisHoraire(horaireId, quaisParOrdre, isPropagation = false) { if (!isSupabaseConfigured) { return { data: null, error: null }; } try { // 1. Lire directement depuis la table unique horaires const { data: horaire, error: readErr } = await supabase .from('horaires') .select('numero_train, gares_desservies, attributions_quais, gare_depart_id, gare_depart_nom, gare_arrivee_id, gare_arrivee_nom, heure_depart, heure_arrivee') .eq('id', horaireId) .single(); if (readErr) { console.error('Erreur lecture horaire:', readErr); return { data: null, error: readErr }; } const baseRows = Array.isArray(horaire.gares_desservies) ? [...horaire.gares_desservies] : []; const existingDepartRow = baseRows.find(r => Number(r.ordre) === 0 || r.is_depart) || baseRows.find(r => isSameGare(r, horaire.gare_depart_id, horaire.gare_depart_nom) && r.heure_depart); const rowsSansDepart = baseRows.filter(r => r !== existingDepartRow); const maxOrdreBase = rowsSansDepart.reduce((m, e) => Math.max(m, Number(e.ordre || 0)), 0); const existingArriveeRow = rowsSansDepart.find(r => r.is_arrivee) || rowsSansDepart.find(r => Number(r.ordre) === maxOrdreBase && isSameGare(r, horaire.gare_arrivee_id, horaire.gare_arrivee_nom)) || rowsSansDepart.find(r => isSameGare(r, horaire.gare_arrivee_id, horaire.gare_arrivee_nom) && (r.heure_arrivee || !r.heure_depart)); const updatedIntermediaires = rowsSansDepart .filter(r => r !== existingArriveeRow) .map((g, idx) => ({ ...g, quai: (quaisParOrdre[g.ordre ?? idx + 1] !== undefined) ? (quaisParOrdre[g.ordre ?? idx + 1] || null) : (g.quai || null) })); const maxOrdre = updatedIntermediaires.reduce((m, e) => Math.max(m, Number(e.ordre || 0)), 0); const arriveeOrdre = maxOrdre + 1; // Construire les entrées départ et arrivée avec les quais mis à jour const departEntry = { ...(existingDepartRow || {}), gare_id: existingDepartRow?.gare_id || horaire.gare_depart_id || null, gare_nom: existingDepartRow?.gare_nom || horaire.gare_depart_nom || '', heure_arrivee: null, heure_depart: existingDepartRow?.heure_depart || horaire.heure_depart || null, ordre: 0, quai: (quaisParOrdre && Object.prototype.hasOwnProperty.call(quaisParOrdre, 'depart')) ? (quaisParOrdre.depart || null) : (existingDepartRow?.quai || null), quai_reel: existingDepartRow?.quai_reel || null, is_depart: true }; const arriveeEntry = { ...(existingArriveeRow || {}), gare_id: existingArriveeRow?.gare_id || horaire.gare_arrivee_id || null, gare_nom: existingArriveeRow?.gare_nom || horaire.gare_arrivee_nom || '', heure_arrivee: existingArriveeRow?.heure_arrivee || horaire.heure_arrivee || null, heure_depart: null, ordre: arriveeOrdre, quai: (quaisParOrdre && Object.prototype.hasOwnProperty.call(quaisParOrdre, 'arrivee')) ? (quaisParOrdre.arrivee || null) : (existingArriveeRow?.quai || null), quai_reel: existingArriveeRow?.quai_reel || null, is_arrivee: true }; const fullSnapshot = [departEntry, ...updatedIntermediaires, arriveeEntry]; // Mettre à jour attributions_quais O(1) const attributionsQuais = { ...(horaire.attributions_quais || {}) }; fullSnapshot.forEach(g => { const key = g.gare_id || g.gare_nom; if (key) { attributionsQuais[key] = { quai: g.quai || null, quai_reel: g.quai_reel || null, ordre: g.ordre }; } }); // Sauvegarder dans la table unique horaires en UNE SEULE requête const { data: saved, error: saveErr } = await supabase .from('horaires') .update({ gares_desservies: fullSnapshot, attributions_quais: attributionsQuais, updated_at: new Date() }) .eq('id', horaireId) .select('gares_desservies') .single(); if (saveErr) { console.error('Erreur sauvegarde quais dans horaires:', saveErr); return { data: null, error: saveErr }; } // Propager aux autres horaires ayant le même numéro de train si applicable if (!isPropagation && horaire.numero_train) { try { const { data: others } = await supabase .from('horaires') .select('id') .eq('numero_train', horaire.numero_train) .neq('id', horaireId); if (others && others.length > 0) { for (const other of others) { await updateQuaisHoraire(other.id, quaisParOrdre, true); } } } catch (propErr) { console.error('Erreur propagation updateQuaisHoraire:', propErr); } } return { data: saved?.gares_desservies || fullSnapshot, error: null }; } catch (err) { console.error('Exception updateQuaisHoraire:', err); return { data: null, error: err }; } } /** * Mise à jour groupée des quais pour plusieurs horaires. * @param {Array<{horaireId: string|number, payload: Object}>} updates */ export async function updateQuaisBatch(updates) { if (!isSupabaseConfigured) return { error: null }; try { const results = []; // Groupe les payloads par horaireId pour minimiser les appels const byHoraire = {}; updates.forEach(u => { if (!byHoraire[u.horaireId]) byHoraire[u.horaireId] = {}; Object.assign(byHoraire[u.horaireId], u.payload); }); // On traite par paquets de 5 pour ne pas saturer Supabase tout en restant rapide const horaireIds = Object.keys(byHoraire); const CHUNK_SIZE = 5; for (let i = 0; i < horaireIds.length; i += CHUNK_SIZE) { const chunk = horaireIds.slice(i, i + CHUNK_SIZE); await Promise.all(chunk.map(async (hid) => { const { data, error } = await updateQuaisHoraire(hid, byHoraire[hid]); results.push({ horaireId: hid, data, error }); })); } return { data: results, error: null }; } catch (err) { console.error('Exception updateQuaisBatch:', err); return { error: err }; } } // ============================================================================ // UTILITAIRES // ============================================================================ /** * Récupère tous les types de trains disponibles (pour le select) */ export function getTypesTrains() { return [ { value: 'TER', label: 'TER' }, { value: 'TGV', label: 'TGV' }, { value: 'Intercités', label: 'Intercités' }, { value: 'Ouigo', label: 'Ouigo' }, { value: 'Transilien', label: 'Transilien' }, { value: 'RER', label: 'RER' }, { value: 'Autocar', label: 'Autocar de substitution' }, { value: 'Autre', label: 'Autre' } ]; } /** * Formate une heure pour l'affichage (HH:MM) */ export function formatHeure(time) { if (!time) return '-'; // Si c'est déjà au format HH:MM ou HH:MM:SS if (typeof time === 'string') { const parts = time.split(':'); if (parts.length >= 2) { return `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}`; } } return time; } /** * Récupère le nom des jours de circulation d'un horaire */ 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 (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 }; } try { // 1. Lire le JSONB actuel depuis horaires const { data: horaire, error: readErr } = await supabase .from('horaires') .select('numero_train, gares_desservies, gare_depart_id, gare_depart_nom, gare_arrivee_id, gare_arrivee_nom, heure_depart, heure_arrivee') .eq('id', horaireId) .single(); if (readErr) { console.error('Erreur lecture horaire:', readErr); return { data: null, error: readErr }; } // 2. Utiliser directement le snapshot JSONB existant const baseRows = Array.isArray(horaire.gares_desservies) ? [...horaire.gares_desservies] : []; const existingDepartRow = baseRows.find(r => Number(r.ordre) === 0 || r.is_depart) || baseRows.find(r => isSameGare(r, horaire.gare_depart_id, horaire.gare_depart_nom) && r.heure_depart); const rowsSansDepart = baseRows.filter(r => r !== existingDepartRow); const maxOrdreBase = rowsSansDepart.reduce((m, e) => Math.max(m, Number(e.ordre || 0)), 0); const existingArriveeRow = rowsSansDepart.find(r => r.is_arrivee) || rowsSansDepart.find(r => Number(r.ordre) === maxOrdreBase && isSameGare(r, horaire.gare_arrivee_id, horaire.gare_arrivee_nom)) || rowsSansDepart.find(r => isSameGare(r, horaire.gare_arrivee_id, horaire.gare_arrivee_nom) && (r.heure_arrivee || !r.heure_depart)); const updatedIntermediaires = rowsSansDepart .filter(r => r !== existingArriveeRow) .map((g, idx) => ({ ...g, quai_reel: (quaisParOrdre[g.ordre ?? idx + 1] !== undefined) ? (quaisParOrdre[g.ordre ?? idx + 1] || null) : (g.quai_reel || null) })); const maxOrdre = updatedIntermediaires.reduce((m, e) => Math.max(m, Number(e.ordre || 0)), 0); const arriveeOrdre = maxOrdre + 1; // 3. Construire les entrées spéciales pour départ et arrivée (si présentes) const departEntry = { id: existingDepartRow?.id || null, gare_id: existingDepartRow?.gare_id || horaire.gare_depart_id || null, gare_nom: existingDepartRow?.gare_nom || horaire.gare_depart_nom || '', heure_arrivee: null, heure_depart: existingDepartRow?.heure_depart || horaire.heure_depart || null, ordre: 0, quai: existingDepartRow?.quai || null, quai_reel: (quaisParOrdre && Object.prototype.hasOwnProperty.call(quaisParOrdre, 'depart')) ? (quaisParOrdre.depart || null) : (existingDepartRow?.quai_reel || null), is_depart: true }; const arriveeEntry = { id: existingArriveeRow?.id || null, gare_id: existingArriveeRow?.gare_id || horaire.gare_arrivee_id || null, gare_nom: existingArriveeRow?.gare_nom || horaire.gare_arrivee_nom || '', heure_arrivee: existingArriveeRow?.heure_arrivee || horaire.heure_arrivee || null, heure_depart: null, ordre: arriveeOrdre, quai: existingArriveeRow?.quai || null, quai_reel: (quaisParOrdre && Object.prototype.hasOwnProperty.call(quaisParOrdre, 'arrivee')) ? (quaisParOrdre.arrivee || null) : (existingArriveeRow?.quai_reel || null), is_arrivee: true }; // 4. Construire le snapshot complet : départ + intermédiaires + arrivée const fullSnapshot = [departEntry, ...updatedIntermediaires, arriveeEntry]; // 5. Écrire le JSONB mis à jour dans horaires.gares_desservies const { data: saved, error: saveErr } = await supabase .from('horaires') .update({ gares_desservies: fullSnapshot, updated_at: new Date() }) .eq('id', horaireId) .select('gares_desservies') .single(); if (saveErr) { console.error('Erreur sauvegarde quais dans horaires.gares_desservies:', saveErr); return { data: null, error: saveErr }; } // 6. Propager aux autres horaires ayant le même numéro de train if (!isPropagation && horaire.numero_train) { try { const { data: others } = await supabase .from('horaires') .select('id') .eq('numero_train', horaire.numero_train) .neq('id', horaireId); if (others && others.length > 0) { for (const other of others) { await updateQuaisReelsHoraire(other.id, quaisParOrdre, true); } } } catch (propErr) { console.error('Erreur lors de la propagation de updateQuaisReelsHoraire:', propErr); } } return { data: saved.gares_desservies, error: null }; } catch (err) { console.error('Exception updateQuaisReelsHoraire:', err); return { data: null, error: err }; } } /** * Récupère le nombre d'horaires attribués à chaque quai d'une gare depuis la table unique horaires. * @param {string} gareId * @param {string} gareNom * @returns {Promise<{data: Object, error: any}>} Map de nom de quai -> compte */ export async function getHorairesCountByQuai(gareId, gareNom) { if (!isSupabaseConfigured) return { data: {}, error: null }; try { const { data, error } = await supabase .from('horaires') .select('gares_desservies, attributions_quais'); if (error) return { data: {}, error }; const counts = {}; if (data) { data.forEach(h => { if (h.attributions_quais && gareId && h.attributions_quais[gareId]?.quai) { const q = h.attributions_quais[gareId].quai; counts[q] = (counts[q] || 0) + 1; } else if (Array.isArray(h.gares_desservies)) { const match = h.gares_desservies.find(g => (gareId && g.gare_id === gareId) || (gareNom && normalizeGareName(g.gare_nom) === normalizeGareName(gareNom)) ); if (match?.quai) { counts[match.quai] = (counts[match.quai] || 0) + 1; } } }); } return { data: counts, error: null }; } catch (err) { return { data: {}, error: err }; } } /** * Réassigne une gare source vers une gare cible directement dans la table unique horaires. */ export async function reassignGareInHoraires(sourceId, targetId, targetNom) { if (!isSupabaseConfigured) return { error: null }; try { // 1. Mettre à jour les colonnes d'origine et destination dans horaires await supabase.from('horaires').update({ gare_depart_id: targetId, gare_depart_nom: targetNom }).eq('gare_depart_id', sourceId); await supabase.from('horaires').update({ gare_arrivee_id: targetId, gare_arrivee_nom: targetNom }).eq('gare_arrivee_id', sourceId); // 2. Mettre à jour le JSONB gares_desservies et attributions_quais const { data: horaires } = await supabase .from('horaires') .select('id, gares_desservies, attributions_quais'); if (horaires) { for (const h of horaires) { if (Array.isArray(h.gares_desservies)) { let modified = false; const newGaresDesservies = h.gares_desservies.map(gd => { if (gd.gare_id === sourceId) { modified = true; return { ...gd, gare_id: targetId, gare_nom: targetNom }; } return gd; }); if (modified) { const attributionsQuais = { ...(h.attributions_quais || {}) }; if (attributionsQuais[sourceId]) { attributionsQuais[targetId] = attributionsQuais[sourceId]; delete attributionsQuais[sourceId]; } await supabase .from('horaires') .update({ gares_desservies: newGaresDesservies, attributions_quais: attributionsQuais, updated_at: new Date() }) .eq('id', h.id); } } } } return { error: null }; } catch (err) { console.error('Exception reassignGareInHoraires:', err); return { error: err }; } }