V5.0.0 //
-> Refonte du système de GESTION DES ROULEMENTS de matériels roulants -> Optimisation rendue BDD
This commit is contained in:
1 parent
36b0c00cbc
commit
f77d59ca1c
25 files changed
+15806
-5437
No files matched your search
@@ -1,2 +1,6 @@
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://supabase-ferrovia.mrpatator.fr
|
||||
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_6dB95QNDKiHcFD81TJETqq_3Mu5rWWU
|
||||
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_k8EI4S2YA_6Hl5av8uCBr3_S1Bj5g_F
|
||||
|
||||
REACT_APP_SUPABASE_URL=https://supabase-ferrovia.mrpatator.fr
|
||||
REACT_APP_SUPABASE_ANON_KEY=sb_publishable_k8EI4S2YA_6Hl5av8uCBr3_S1Bj5g_F
|
||||
REACT_APP_SUPABASE_PUBLISHABLE_KEY=sb_publishable_k8EI4S2YA_6Hl5av8uCBr3_S1Bj5g_F
|
||||
+5
-1
@@ -1,2 +1,6 @@
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://supabase-ferrovia.mrpatator.fr
|
||||
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_6dB95QNDKiHcFD81TJETqq_3Mu5rWWU
|
||||
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_k8EI4S2YA_6Hl5av8uCBr3_S1Bj5g_F
|
||||
|
||||
REACT_APP_SUPABASE_URL=https://supabase-ferrovia.mrpatator.fr
|
||||
REACT_APP_SUPABASE_ANON_KEY=sb_publishable_k8EI4S2YA_6Hl5av8uCBr3_S1Bj5g_F
|
||||
REACT_APP_SUPABASE_PUBLISHABLE_KEY=sb_publishable_k8EI4S2YA_6Hl5av8uCBr3_S1Bj5g_F
|
||||
+1607
File diff suppressed because it is too large.
Load diff
Generated
+1737
-1676
File diff suppressed because it is too large.
Load diff
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sncf/bootstrap-sncf.communication": "^4.3.1-r5",
|
||||
"@sncf/bootstrap-sncf.metier": "^4.3.1-r5",
|
||||
"@supabase/supabase-js": "^2.112.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@import "wcs-core/dist/wcs/wcs.css";
|
||||
@import "@sncf/bootstrap-sncf.communication/dist/bootstrap-sncf.css";
|
||||
@import "@sncf/bootstrap-sncf.metier/dist/bootstrap-sncf.min.css";
|
||||
|
||||
/* styles.scss des différents labels SNCF */
|
||||
@import "wcs-core/design-tokens/dist/sncf-voyageurs.css";
|
||||
|
||||
+19
-4
@@ -35,7 +35,13 @@ function App() {
|
||||
}
|
||||
|
||||
// Synchronisation de session Supabase Auth au démarrage
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
supabase.auth.getSession().then(({ data: { session }, error }) => {
|
||||
if (error) {
|
||||
console.warn('[App] Session Supabase invalide:', error);
|
||||
window.localStorage.removeItem('app_user');
|
||||
setCurrentUser(null);
|
||||
return;
|
||||
}
|
||||
if (session?.user && !window.location.pathname.startsWith('/reinitialiser-mot-de-passe')) {
|
||||
const stored = window.localStorage.getItem('app_user');
|
||||
if (!stored) {
|
||||
@@ -46,7 +52,11 @@ function App() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
}).catch((err) => {
|
||||
console.warn('[App] Erreur lors de la récupération de session:', err);
|
||||
window.localStorage.removeItem('app_user');
|
||||
setCurrentUser(null);
|
||||
});
|
||||
|
||||
// écouter un événement custom pour synchroniser l'état d'authentification
|
||||
const onAuthChanged = () => {
|
||||
@@ -61,8 +71,13 @@ function App() {
|
||||
const onUnhandledRejection = (event) => {
|
||||
const reason = event?.reason;
|
||||
const message = reason?.message || '';
|
||||
if (reason?.name === 'AbortError' || message === 'The operation was aborted.') {
|
||||
// Certaines annulations navigateur/FileReader ne doivent pas afficher l'overlay runtime.
|
||||
if (
|
||||
reason?.name === 'AbortError' ||
|
||||
message === 'The operation was aborted.' ||
|
||||
reason?.name === 'AuthRetryableFetchError' ||
|
||||
message.includes('Unexpected failure, please check server logs')
|
||||
) {
|
||||
// Certaines annulations navigateur ou retries réseau ne doivent pas bloquer l'UI
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import supabase, { isSupabaseConfigured } from '../supabaseClient';
|
||||
|
||||
/**
|
||||
* Récupère la liste complète des dépôts et technicentres enregistrés directement dans la base de données (table depots_technicentres).
|
||||
*/
|
||||
export async function getAllDepots() {
|
||||
if (!isSupabaseConfigured) {
|
||||
return { data: [], error: { message: 'Supabase non configuré' } };
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('depots_technicentres')
|
||||
.select('*')
|
||||
.is('deleted_at', null)
|
||||
.order('nom', { ascending: true });
|
||||
|
||||
if (error) {
|
||||
console.error('Erreur chargement depots_technicentres depuis la BDD:', error);
|
||||
return { data: [], error };
|
||||
}
|
||||
|
||||
return { data: data || [], error: null };
|
||||
} catch (err) {
|
||||
console.error('Erreur API getAllDepots:', err);
|
||||
return { data: [], error: err };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un dépôt / technicentre par son identifiant UUID dans la BDD.
|
||||
*/
|
||||
export async function getDepotById(id) {
|
||||
if (!isSupabaseConfigured) return { data: null, error: { message: 'Supabase non configuré' } };
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('depots_technicentres')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
return { data, error };
|
||||
} catch (err) {
|
||||
return { data: null, error: err };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un nouveau dépôt ou technicentre de maintenance dans Supabase (table depots_technicentres).
|
||||
*/
|
||||
export async function createDepot(payload) {
|
||||
if (!isSupabaseConfigured) return { data: payload, error: null };
|
||||
try {
|
||||
const cleanPayload = {
|
||||
...payload,
|
||||
capacite_max: Number(payload.capacite_max || 20),
|
||||
specialites: Array.isArray(payload.specialites)
|
||||
? payload.specialites
|
||||
: (typeof payload.specialites === 'string'
|
||||
? payload.specialites.split(',').map(s => s.trim()).filter(Boolean)
|
||||
: []),
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('depots_technicentres')
|
||||
.insert([cleanPayload])
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
return { data, error };
|
||||
} catch (err) {
|
||||
return { data: null, error: err };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un dépôt / technicentre existant dans Supabase.
|
||||
*/
|
||||
export async function updateDepot(id, payload) {
|
||||
if (!isSupabaseConfigured) return { data: { id, ...payload }, error: null };
|
||||
try {
|
||||
const cleanPayload = {
|
||||
...payload,
|
||||
capacite_max: payload.capacite_max != null ? Number(payload.capacite_max) : undefined,
|
||||
specialites: payload.specialites !== undefined
|
||||
? (Array.isArray(payload.specialites)
|
||||
? payload.specialites
|
||||
: typeof payload.specialites === 'string'
|
||||
? payload.specialites.split(',').map(s => s.trim()).filter(Boolean)
|
||||
: [])
|
||||
: undefined,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
Object.keys(cleanPayload).forEach(key => cleanPayload[key] === undefined && delete cleanPayload[key]);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('depots_technicentres')
|
||||
.update(cleanPayload)
|
||||
.eq('id', id)
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
return { data, error };
|
||||
} catch (err) {
|
||||
return { data: null, error: err };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un dépôt / technicentre de la BDD (table depots_technicentres).
|
||||
* Détache d'abord les matériels roulants assignés pour éviter toute violation de clé étrangère.
|
||||
*/
|
||||
export async function deleteDepot(id) {
|
||||
if (!isSupabaseConfigured) return { error: null };
|
||||
try {
|
||||
// 1. Détacher les matériels roulants assignés à ce dépôt
|
||||
try {
|
||||
await supabase
|
||||
.from('materiel-roulant')
|
||||
.update({ depot_id: null, depot_attache: null })
|
||||
.eq('depot_id', id);
|
||||
} catch (e) {
|
||||
console.warn('Avertissement détachement matériel:', e);
|
||||
}
|
||||
|
||||
// 2. Supprimer la ligne de la table depots_technicentres
|
||||
const { error } = await supabase
|
||||
.from('depots_technicentres')
|
||||
.delete()
|
||||
.eq('id', id);
|
||||
|
||||
return { error };
|
||||
} catch (err) {
|
||||
return { error: err };
|
||||
}
|
||||
}
|
||||
@@ -518,6 +518,8 @@ export async function updateHoraireComposition(id, compositionTrain = []) {
|
||||
materiel_nom: item?.materiel_nom || '',
|
||||
materiel_nom_technique: item?.materiel_nom_technique || '',
|
||||
materiel_image_url: item?.materiel_image_url || null,
|
||||
capacite: item?.capacite != null ? Number(item.capacite) : null,
|
||||
type_train: item?.type_train || '',
|
||||
position: idx + 1
|
||||
}));
|
||||
|
||||
@@ -535,6 +537,29 @@ export async function updateHoraireComposition(id, compositionTrain = []) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour par lots les compositions de trains optimisées.
|
||||
* @param {Array<{horaireId: string|number, composition: Array}>} updates
|
||||
*/
|
||||
export async function updateHorairesCompositionsBatch(updates = []) {
|
||||
if (!isSupabaseConfigured) return { data: [], error: null };
|
||||
try {
|
||||
const results = [];
|
||||
const CHUNK_SIZE = 5;
|
||||
for (let i = 0; i < updates.length; i += CHUNK_SIZE) {
|
||||
const chunk = updates.slice(i, i + CHUNK_SIZE);
|
||||
await Promise.all(chunk.map(async (u) => {
|
||||
const { data, error } = await updateHoraireComposition(u.horaireId, u.composition);
|
||||
results.push({ horaireId: u.horaireId, data, error });
|
||||
}));
|
||||
}
|
||||
return { data: results, error: null };
|
||||
} catch (err) {
|
||||
console.error('Exception updateHorairesCompositionsBatch:', err);
|
||||
return { data: null, error: err };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un horaire (hard delete)
|
||||
* @param {number|string} id - ID de l'horaire
|
||||
@@ -1162,3 +1187,69 @@ export async function reassignGareInHoraires(sourceId, targetId, targetNom) {
|
||||
return { error: err };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Couple deux circulations horaires en unité multiple synchronisée (tranche commune)
|
||||
* ou découple un train existant.
|
||||
*/
|
||||
export async function updateHoraireCouplage(sourceId, sourceNumeroTrain, targetId, targetNumeroTrain) {
|
||||
if (!isSupabaseConfigured) return { error: null };
|
||||
try {
|
||||
if (targetId) {
|
||||
// 1. Coupler la source avec la cible
|
||||
await supabase.from('horaires').update({
|
||||
couplage_train_id: targetId,
|
||||
couplage_numero_train: targetNumeroTrain,
|
||||
updated_at: new Date()
|
||||
}).eq('id', sourceId);
|
||||
|
||||
// 2. Coupler réciproquement la cible avec la source
|
||||
await supabase.from('horaires').update({
|
||||
couplage_train_id: sourceId,
|
||||
couplage_numero_train: sourceNumeroTrain,
|
||||
updated_at: new Date()
|
||||
}).eq('id', targetId);
|
||||
} else {
|
||||
// Découplage
|
||||
const { data: current } = await supabase.from('horaires').select('couplage_train_id').eq('id', sourceId).single();
|
||||
const oldTargetId = current?.couplage_train_id;
|
||||
|
||||
await supabase.from('horaires').update({
|
||||
couplage_train_id: null,
|
||||
couplage_numero_train: null,
|
||||
updated_at: new Date()
|
||||
}).eq('id', sourceId);
|
||||
|
||||
if (oldTargetId) {
|
||||
await supabase.from('horaires').update({
|
||||
couplage_train_id: null,
|
||||
couplage_numero_train: null,
|
||||
updated_at: new Date()
|
||||
}).eq('id', oldTargetId);
|
||||
}
|
||||
}
|
||||
return { error: null };
|
||||
} catch (err) {
|
||||
console.error('Exception updateHoraireCouplage:', err);
|
||||
return { error: err };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre les manœuvres ferroviaires d'une circulation (mise à quai, garage, attelage).
|
||||
*/
|
||||
export async function updateHoraireManoeuvres(horaireId, manoeuvres = []) {
|
||||
if (!isSupabaseConfigured) return { error: null };
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('horaires')
|
||||
.update({ manoeuvres, updated_at: new Date() })
|
||||
.eq('id', horaireId)
|
||||
.select('manoeuvres')
|
||||
.single();
|
||||
return { data: data?.manoeuvres || manoeuvres, error };
|
||||
} catch (err) {
|
||||
console.error('Exception updateHoraireManoeuvres:', err);
|
||||
return { data: null, error: err };
|
||||
}
|
||||
}
|
||||
+87
-26
@@ -143,32 +143,61 @@ export async function getPerturbationById(id) {
|
||||
|
||||
/**
|
||||
* Crée une nouvelle perturbation
|
||||
* @param {Object} payload - Données de la perturbation
|
||||
* @param {Object} payload - Données de la perturbation (ou objet { payload, modificationsParcours })
|
||||
* @param {Array} modificationsParcours - Liste des modifications de parcours (optionnel)
|
||||
*/
|
||||
export async function createPerturbation(payload, modificationsParcours = []) {
|
||||
if (!isSupabaseConfigured) {
|
||||
const fakeId = `local-${Date.now()}`;
|
||||
return { data: { id: fakeId, ...payload }, error: null };
|
||||
return { data: { id: fakeId, ...(payload?.payload || payload) }, error: null };
|
||||
}
|
||||
|
||||
try {
|
||||
// Supporter à la fois un payload plat ou l'objet composite retourné par buildPayload ({ payload, modificationsParcours })
|
||||
let actualPayload = payload;
|
||||
let modifs = Array.isArray(modificationsParcours) ? [...modificationsParcours] : [];
|
||||
|
||||
if (payload && typeof payload === 'object' && payload.payload) {
|
||||
if (modifs.length === 0 && Array.isArray(payload.modificationsParcours)) {
|
||||
modifs = payload.modificationsParcours;
|
||||
}
|
||||
actualPayload = payload.payload;
|
||||
} else if (payload && Array.isArray(payload.modificationsParcours) && modifs.length === 0) {
|
||||
modifs = payload.modificationsParcours;
|
||||
}
|
||||
|
||||
// Extraire jours_perturbation et le stocker dans perturbations_arrets (colonne JSONB existante)
|
||||
const { jours_perturbation, perturbations_arrets, ...rest } = payload;
|
||||
// Et filtrer strictement les clés qui n'appartiennent pas à la table perturbations
|
||||
const {
|
||||
jours_perturbation,
|
||||
perturbations_arrets,
|
||||
modificationsParcours: _ignore1,
|
||||
modifications_parcours: _ignore2,
|
||||
payload: _ignore3,
|
||||
scopedRegionId: _ignore4,
|
||||
...rest
|
||||
} = actualPayload || {};
|
||||
|
||||
const perturbationsArretsData = {
|
||||
arrets: perturbations_arrets || [],
|
||||
jours_perturbation: jours_perturbation || null
|
||||
};
|
||||
|
||||
// Créer la perturbation principale
|
||||
const { data: perturbation, error: perturbationError } = await supabase
|
||||
.from('perturbations')
|
||||
.insert([{
|
||||
const cleanInsert = {
|
||||
...rest,
|
||||
perturbations_arrets: perturbationsArretsData,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
}])
|
||||
};
|
||||
delete cleanInsert.modificationsParcours;
|
||||
delete cleanInsert.modifications_parcours;
|
||||
delete cleanInsert.payload;
|
||||
delete cleanInsert.scopedRegionId;
|
||||
|
||||
// Créer la perturbation principale
|
||||
const { data: perturbation, error: perturbationError } = await supabase
|
||||
.from('perturbations')
|
||||
.insert([cleanInsert])
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
@@ -178,8 +207,9 @@ export async function createPerturbation(payload, modificationsParcours = []) {
|
||||
}
|
||||
|
||||
// Insérer les modifications de parcours si présentes
|
||||
if (modificationsParcours.length > 0 && perturbation) {
|
||||
const modificationsData = modificationsParcours.map(modif => ({
|
||||
if (modifs && modifs.length > 0 && perturbation) {
|
||||
try {
|
||||
const modificationsData = modifs.map(modif => ({
|
||||
perturbation_id: perturbation.id,
|
||||
...modif
|
||||
}));
|
||||
@@ -189,8 +219,10 @@ export async function createPerturbation(payload, modificationsParcours = []) {
|
||||
.insert(modificationsData);
|
||||
|
||||
if (modifError) {
|
||||
console.error('Erreur lors de l\'insertion des modifications de parcours:', modifError);
|
||||
// On continue même si erreur car la perturbation principale est créée
|
||||
console.warn('Note: insertion perturbations_modifications_parcours non bloquante:', modifError.message || modifError);
|
||||
}
|
||||
} catch (errModif) {
|
||||
console.warn('Note: table perturbations_modifications_parcours non disponible:', errModif.message || errModif);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,30 +236,57 @@ export async function createPerturbation(payload, modificationsParcours = []) {
|
||||
/**
|
||||
* Met à jour une perturbation existante
|
||||
* @param {string} id - ID de la perturbation
|
||||
* @param {Object} payload - Données à mettre à jour
|
||||
* @param {Object} payload - Données à mettre à jour (ou objet { payload, modificationsParcours })
|
||||
* @param {Array} modificationsParcours - Nouvelle liste des modifications de parcours (remplace l'existante)
|
||||
*/
|
||||
export async function updatePerturbation(id, payload, modificationsParcours = null) {
|
||||
if (!isSupabaseConfigured) {
|
||||
return { data: { id, ...payload }, error: null };
|
||||
return { data: { id, ...(payload?.payload || payload) }, error: null };
|
||||
}
|
||||
|
||||
try {
|
||||
let actualPayload = payload;
|
||||
let modifs = modificationsParcours;
|
||||
|
||||
if (payload && typeof payload === 'object' && payload.payload) {
|
||||
if (modifs === null && Array.isArray(payload.modificationsParcours)) {
|
||||
modifs = payload.modificationsParcours;
|
||||
}
|
||||
actualPayload = payload.payload;
|
||||
} else if (payload && Array.isArray(payload.modificationsParcours) && modifs === null) {
|
||||
modifs = payload.modificationsParcours;
|
||||
}
|
||||
|
||||
// Extraire jours_perturbation et le stocker dans perturbations_arrets (colonne JSONB existante)
|
||||
const { jours_perturbation, perturbations_arrets, ...rest } = payload;
|
||||
const {
|
||||
jours_perturbation,
|
||||
perturbations_arrets,
|
||||
modificationsParcours: _ignore1,
|
||||
modifications_parcours: _ignore2,
|
||||
payload: _ignore3,
|
||||
scopedRegionId: _ignore4,
|
||||
...rest
|
||||
} = actualPayload || {};
|
||||
|
||||
const perturbationsArretsData = {
|
||||
arrets: perturbations_arrets || [],
|
||||
jours_perturbation: jours_perturbation || null
|
||||
};
|
||||
|
||||
// Mettre à jour la perturbation principale
|
||||
const { data: perturbation, error: perturbationError } = await supabase
|
||||
.from('perturbations')
|
||||
.update({
|
||||
const cleanUpdate = {
|
||||
...rest,
|
||||
perturbations_arrets: perturbationsArretsData,
|
||||
updated_at: new Date()
|
||||
})
|
||||
};
|
||||
delete cleanUpdate.modificationsParcours;
|
||||
delete cleanUpdate.modifications_parcours;
|
||||
delete cleanUpdate.payload;
|
||||
delete cleanUpdate.scopedRegionId;
|
||||
|
||||
// Mettre à jour la perturbation principale
|
||||
const { data: perturbation, error: perturbationError } = await supabase
|
||||
.from('perturbations')
|
||||
.update(cleanUpdate)
|
||||
.eq('id', id)
|
||||
.select('*')
|
||||
.single();
|
||||
@@ -238,16 +297,15 @@ export async function updatePerturbation(id, payload, modificationsParcours = nu
|
||||
}
|
||||
|
||||
// Si modifications de parcours fournies, supprimer les anciennes et insérer les nouvelles
|
||||
if (modificationsParcours !== null && Array.isArray(modificationsParcours)) {
|
||||
// Supprimer les anciennes modifications
|
||||
if (modifs !== null && Array.isArray(modifs)) {
|
||||
try {
|
||||
await supabase
|
||||
.from('perturbations_modifications_parcours')
|
||||
.delete()
|
||||
.eq('perturbation_id', id);
|
||||
|
||||
// Insérer les nouvelles modifications
|
||||
if (modificationsParcours.length > 0) {
|
||||
const modificationsData = modificationsParcours.map(modif => ({
|
||||
if (modifs.length > 0) {
|
||||
const modificationsData = modifs.map(modif => ({
|
||||
perturbation_id: id,
|
||||
...modif
|
||||
}));
|
||||
@@ -257,9 +315,12 @@ export async function updatePerturbation(id, payload, modificationsParcours = nu
|
||||
.insert(modificationsData);
|
||||
|
||||
if (modifError) {
|
||||
console.error('Erreur lors de la mise à jour des modifications de parcours:', modifError);
|
||||
console.warn('Note: mise à jour perturbations_modifications_parcours non bloquante:', modifError.message || modifError);
|
||||
}
|
||||
}
|
||||
} catch (errModif) {
|
||||
console.warn('Note: table perturbations_modifications_parcours non disponible:', errModif.message || errModif);
|
||||
}
|
||||
}
|
||||
|
||||
return { data: perturbation, error: null };
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+969
-1079
File diff suppressed because it is too large.
Load diff
+1907
-455
File diff suppressed because it is too large.
Load diff
@@ -55,8 +55,13 @@ export default function Login({ onLogin }) {
|
||||
});
|
||||
|
||||
if (authError || !authData.user) {
|
||||
const msg = authError?.message || 'Identifiants invalides';
|
||||
setError(`Erreur d'authentification: ${msg}`);
|
||||
let msg = authError?.message || 'Identifiants invalides';
|
||||
if (msg.includes('Invalid login credentials') || authError?.status === 400) {
|
||||
msg = 'Identifiant ou mot de passe incorrect. Veuillez vérifier votre saisie.';
|
||||
} else if (msg.includes('Email not confirmed')) {
|
||||
msg = 'Votre compte n\'a pas encore été activé. Veuillez utiliser le lien d\'activation reçu par email.';
|
||||
}
|
||||
setError(msg);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -156,8 +161,9 @@ export default function Login({ onLogin }) {
|
||||
<wcs-alert
|
||||
ref={alertRef}
|
||||
intent="error"
|
||||
show
|
||||
closable
|
||||
timeout={4000}
|
||||
timeout={5000}
|
||||
>
|
||||
<span slot="title">Erreur</span>
|
||||
<span slot="subtitle">{error}</span>
|
||||
@@ -173,6 +179,14 @@ export default function Login({ onLogin }) {
|
||||
emailRef.current && (emailRef.current.value = '');
|
||||
passRef.current && (passRef.current.value = '');
|
||||
setError('');
|
||||
try {
|
||||
Object.keys(window.localStorage).forEach(k => {
|
||||
if (k.startsWith('sb-') || k.includes('supabase')) {
|
||||
window.localStorage.removeItem(k);
|
||||
}
|
||||
});
|
||||
supabase.auth.signOut({ scope: 'local' }).catch(() => {});
|
||||
} catch (_) {}
|
||||
}}>
|
||||
Reinitialiser
|
||||
</wcs-button>
|
||||
|
||||
@@ -372,14 +372,19 @@
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.materiel-card-modern.has-open-menu {
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
.materiel-card-modern:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: #cbd5e1;
|
||||
@@ -395,6 +400,8 @@
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-top-left-radius: 15px;
|
||||
border-top-right-radius: 15px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
@@ -488,40 +495,55 @@
|
||||
}
|
||||
|
||||
.materiel-actions-toggle {
|
||||
min-width: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.materiel-actions-toggle:hover {
|
||||
.materiel-actions-toggle:hover,
|
||||
.materiel-actions-toggle.active {
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
border-color: #0088ce;
|
||||
color: #0088ce;
|
||||
box-shadow: 0 0 0 2px rgba(0, 136, 206, 0.15);
|
||||
}
|
||||
|
||||
.materiel-actions-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 6px);
|
||||
z-index: 50;
|
||||
width: 140px;
|
||||
z-index: 100;
|
||||
min-width: 144px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.16);
|
||||
box-shadow: 0 10px 25px -5px rgba(15, 23, 42, 0.2), 0 8px 10px -6px rgba(15, 23, 42, 0.1);
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.materiel-actions-menu.open-up {
|
||||
bottom: calc(100% + 6px);
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.materiel-actions-menu.open-down {
|
||||
top: calc(100% + 6px);
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.materiel-actions-menu button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
@@ -538,6 +560,11 @@
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.materiel-actions-menu button:hover {
|
||||
background: #f1f5f9;
|
||||
color: #0088ce;
|
||||
}
|
||||
|
||||
.materiel-actions-menu button:hover {
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
@@ -1033,3 +1060,38 @@ body.dark-mode .materiels-empty-text,
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
body.dark-mode .materiel-actions-menu,
|
||||
[data-theme="dark"] .materiel-actions-menu {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
body.dark-mode .materiel-actions-menu button,
|
||||
[data-theme="dark"] .materiel-actions-menu button {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
body.dark-mode .materiel-actions-menu button:hover,
|
||||
[data-theme="dark"] .materiel-actions-menu button:hover {
|
||||
background: #334155;
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
body.dark-mode .materiel-actions-toggle,
|
||||
[data-theme="dark"] .materiel-actions-toggle {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
body.dark-mode .materiel-actions-toggle:hover,
|
||||
body.dark-mode .materiel-actions-toggle.active,
|
||||
[data-theme="dark"] .materiel-actions-toggle:hover,
|
||||
[data-theme="dark"] .materiel-actions-toggle.active {
|
||||
background: #27354a;
|
||||
border-color: #38bdf8;
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ function MaterielGroupCard({ imageUrl, items, onEdit, onDelete, onDuplicate, can
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="materiel-card-modern">
|
||||
<div className={`materiel-card-modern ${openActionsFor ? 'has-open-menu' : ''}`}>
|
||||
<div className="materiel-card-media" ref={imgContainerRef}>
|
||||
{image ? (
|
||||
<img
|
||||
@@ -103,7 +103,9 @@ function MaterielGroupCard({ imageUrl, items, onEdit, onDelete, onDuplicate, can
|
||||
</div>
|
||||
|
||||
<div className={`materiel-group-list ${items.length > 2 ? 'scrollable' : ''}`}>
|
||||
{items.map((materiel) => (
|
||||
{items.map((materiel, idx) => {
|
||||
const isLast = idx === items.length - 1 || items.length <= 2;
|
||||
return (
|
||||
<div key={materiel.id} className="materiel-group-row">
|
||||
<div className="materiel-group-main">
|
||||
<span className="materiel-serial-pill">Rame n° {materiel.numero_serie || '-'}</span>
|
||||
@@ -112,17 +114,20 @@ function MaterielGroupCard({ imageUrl, items, onEdit, onDelete, onDuplicate, can
|
||||
{materiel.capacite ? ` (${materiel.capacite} pl.)` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="materiel-row-actions" ref={actionsRef}>
|
||||
<div className="materiel-row-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="materiel-actions-toggle"
|
||||
onClick={() => setOpenActionsFor((prev) => (prev === materiel.id ? null : materiel.id))}
|
||||
className={`materiel-actions-toggle ${openActionsFor === materiel.id ? 'active' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenActionsFor((prev) => (prev === materiel.id ? null : materiel.id));
|
||||
}}
|
||||
aria-label="Ouvrir les actions"
|
||||
>
|
||||
<wcs-mat-icon icon="more_vert" size="s"></wcs-mat-icon>
|
||||
</button>
|
||||
{openActionsFor === materiel.id && (
|
||||
<div className="materiel-actions-menu">
|
||||
<div className={`materiel-actions-menu ${isLast ? 'open-up' : 'open-down'}`}>
|
||||
<button type="button" onClick={() => { setOpenActionsFor(null); onEdit(materiel); }} disabled={!canUpdate}>
|
||||
<wcs-mat-icon icon="edit" size="s"></wcs-mat-icon>
|
||||
Modifier
|
||||
@@ -139,7 +144,8 @@ function MaterielGroupCard({ imageUrl, items, onEdit, onDelete, onDuplicate, can
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+264
-145
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import NavPanel from '../../components/nav-panel/NavPanel';
|
||||
import { createTravaux, getHorairesSubstitution } from '../../api/travaux';
|
||||
import { getAllLignes } from '../../api/lignes';
|
||||
@@ -21,6 +21,9 @@ export default function CreerTravaux() {
|
||||
const currentUser = getCurrentUser();
|
||||
const canCreateTravaux = canPerform(currentUser, 'travaux', 'create');
|
||||
|
||||
const [activeTabKey, setActiveTabKey] = useState('general');
|
||||
const tabsRef = useRef(null);
|
||||
|
||||
const [titre, setTitre] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [dateDebut, setDateDebut] = useState('');
|
||||
@@ -62,6 +65,21 @@ export default function CreerTravaux() {
|
||||
fetchLignes();
|
||||
}, [scopedRegionId]);
|
||||
|
||||
// Synchronisation des wcs-tabs
|
||||
useEffect(() => {
|
||||
const el = tabsRef.current;
|
||||
if (!el) return;
|
||||
const handleTabChange = (e) => {
|
||||
if (e.detail?.selectedKey) {
|
||||
setActiveTabKey(e.detail.selectedKey);
|
||||
}
|
||||
};
|
||||
el.addEventListener('wcsChange', handleTabChange);
|
||||
return () => {
|
||||
el.removeEventListener('wcsChange', handleTabChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSearchHoraires = useCallback(async () => {
|
||||
setLoadingHoraires(true);
|
||||
setShowResults(true);
|
||||
@@ -108,7 +126,7 @@ export default function CreerTravaux() {
|
||||
setJoursTravaux(['samedi', 'dimanche']);
|
||||
};
|
||||
|
||||
const formatHeure = (heure) => heure ? heure.substring(0, 5) : '—';
|
||||
const formatHeure = (heure) => (heure ? heure.substring(0, 5) : '—');
|
||||
|
||||
const navigate = (path) => { window.location.href = path; };
|
||||
|
||||
@@ -165,18 +183,31 @@ export default function CreerTravaux() {
|
||||
</div>
|
||||
|
||||
<div className="panel-content travaux-form-page-content">
|
||||
{/* Header Banner */}
|
||||
{/* Header Banner SNCF avec composants WCS */}
|
||||
<header className="travaux-form-header-modern">
|
||||
<div className="form-header-main">
|
||||
<button type="button" className="btn-back-link" onClick={() => navigate('/travaux')}>
|
||||
<wcs-mat-icon icon="arrow_back"></wcs-mat-icon>
|
||||
<wcs-button
|
||||
mode="stroked"
|
||||
size="s"
|
||||
className="wcs-secondary mb-2"
|
||||
onClick={() => navigate('/travaux')}
|
||||
>
|
||||
<wcs-mat-icon icon="arrow_back" size="s" class="mr-1"></wcs-mat-icon>
|
||||
<span>Retour aux travaux</span>
|
||||
</button>
|
||||
</wcs-button>
|
||||
|
||||
<div className="form-title-group">
|
||||
<span className="travaux-kicker-pill">
|
||||
<wcs-mat-icon icon="add_circle"></wcs-mat-icon>
|
||||
<div className="d-flex align-items-center gap-2 mb-1">
|
||||
<wcs-badge shape="pill" className="wcs-info">
|
||||
<wcs-mat-icon icon="add_circle" size="s" class="mr-1"></wcs-mat-icon>
|
||||
Nouveau Chantier
|
||||
</span>
|
||||
</wcs-badge>
|
||||
{scopedRegionId && (
|
||||
<wcs-badge shape="pill" className="wcs-secondary">
|
||||
Région : {scopedRegionId}
|
||||
</wcs-badge>
|
||||
)}
|
||||
</div>
|
||||
<h1>Déclarer des travaux sur le réseau</h1>
|
||||
<p>Planifiez une coupure, des restrictions de circulation et les substitutions associées.</p>
|
||||
</div>
|
||||
@@ -184,34 +215,37 @@ export default function CreerTravaux() {
|
||||
|
||||
<div className="form-header-actions">
|
||||
<wcs-button className="wcs-secondary" mode="stroked" onClick={() => navigate('/travaux')}>
|
||||
Annuler
|
||||
<span>Annuler</span>
|
||||
</wcs-button>
|
||||
<wcs-button
|
||||
className="wcs-primary"
|
||||
onClick={handleCreate}
|
||||
disabled={saving || !canCreateTravaux}
|
||||
loading={saving}
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<wcs-spinner mode="border" style={{ width: '16px', height: '16px', marginRight: '6px' }}></wcs-spinner>
|
||||
<span>Enregistrement...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<wcs-mat-icon icon="save" style={{ marginRight: '6px' }}></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="save" size="s" class="mr-1"></wcs-mat-icon>
|
||||
<span>Créer le chantier</span>
|
||||
</>
|
||||
)}
|
||||
</wcs-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Navigation par Onglets WCS */}
|
||||
<div className="mb-4">
|
||||
<wcs-tabs ref={tabsRef} selected-key={activeTabKey}>
|
||||
<wcs-tab header="1. Général & Ligne" item-key="general"></wcs-tab>
|
||||
<wcs-tab header="2. Calendrier & Période" item-key="calendrier"></wcs-tab>
|
||||
<wcs-tab header={`3. Substitutions (${horairesSelectionnes.length})`} item-key="substitutions"></wcs-tab>
|
||||
</wcs-tabs>
|
||||
</div>
|
||||
|
||||
<div className="travaux-form-grid">
|
||||
{/* Section 1 : Informations générales */}
|
||||
<div className="travaux-glass-card">
|
||||
<div className="section-title-wrap">
|
||||
{/* Onglet 1 : Informations Générales */}
|
||||
{(activeTabKey === 'general' || activeTabKey === 'all') && (
|
||||
<wcs-card className="wcs-card-form mb-4">
|
||||
<wcs-card-body>
|
||||
<div className="section-title-wrap mb-3">
|
||||
<div className="section-icon-box">
|
||||
<wcs-mat-icon icon="info"></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="info" size="m"></wcs-mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="travaux-section-title">Informations Générales</h2>
|
||||
@@ -219,21 +253,23 @@ export default function CreerTravaux() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Titre des travaux *</label>
|
||||
<wcs-form-field class="mb-3">
|
||||
<label htmlFor="titre-travaux">Titre des travaux *</label>
|
||||
<input
|
||||
id="titre-travaux"
|
||||
type="text"
|
||||
className="premium-input"
|
||||
className="form-control"
|
||||
placeholder="Ex: Rénovation caténaire et voies Paris vers Lyon"
|
||||
value={titre}
|
||||
onChange={(e) => setTitre(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</wcs-form-field>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Ligne ferroviaire associée</label>
|
||||
<wcs-form-field class="mb-3">
|
||||
<label htmlFor="ligne-select">Ligne ferroviaire associée</label>
|
||||
<select
|
||||
className="premium-input"
|
||||
id="ligne-select"
|
||||
className="form-control"
|
||||
value={selectedLigneId}
|
||||
onChange={(e) => setSelectedLigneId(e.target.value)}
|
||||
disabled={loadingLignes}
|
||||
@@ -243,23 +279,27 @@ export default function CreerTravaux() {
|
||||
<option key={l.id} value={l.id}>{l.nom || l.numero || `Ligne ${l.id}`}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</wcs-form-field>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Description détaillée & Informations voyageurs</label>
|
||||
<div className="mb-3">
|
||||
<label className="form-label font-weight-bold d-block mb-2">Description détaillée & Informations voyageurs</label>
|
||||
<RichTextEditor
|
||||
value={description}
|
||||
onChange={setDescription}
|
||||
placeholder="Détaillez les impacts prévus, motifs techniques, gares concernées..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</wcs-card-body>
|
||||
</wcs-card>
|
||||
)}
|
||||
|
||||
{/* Section 2 : Planification & Calendrier */}
|
||||
<div className="travaux-glass-card">
|
||||
<div className="section-title-wrap">
|
||||
{/* Onglet 2 : Planification & Calendrier */}
|
||||
{(activeTabKey === 'calendrier' || activeTabKey === 'all') && (
|
||||
<wcs-card className="wcs-card-form mb-4">
|
||||
<wcs-card-body>
|
||||
<div className="section-title-wrap mb-3">
|
||||
<div className="section-icon-box">
|
||||
<wcs-mat-icon icon="calendar_month"></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="calendar_month" size="m"></wcs-mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="travaux-section-title">Planification & Horaires</h2>
|
||||
@@ -267,54 +307,92 @@ export default function CreerTravaux() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-columns-2">
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Date de début *</label>
|
||||
<input type="date" className="premium-input" value={dateDebut} onChange={(e) => setDateDebut(e.target.value)} />
|
||||
<div className="form-columns-2 mb-3">
|
||||
<wcs-form-field>
|
||||
<label htmlFor="date-debut">Date de début *</label>
|
||||
<input
|
||||
id="date-debut"
|
||||
type="date"
|
||||
className="form-control"
|
||||
value={dateDebut}
|
||||
onChange={(e) => setDateDebut(e.target.value)}
|
||||
/>
|
||||
</wcs-form-field>
|
||||
|
||||
<wcs-form-field>
|
||||
<label htmlFor="date-fin">Date de fin *</label>
|
||||
<input
|
||||
id="date-fin"
|
||||
type="date"
|
||||
className="form-control"
|
||||
value={dateFin}
|
||||
onChange={(e) => setDateFin(e.target.value)}
|
||||
/>
|
||||
</wcs-form-field>
|
||||
</div>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Date de fin *</label>
|
||||
<input type="date" className="premium-input" value={dateFin} onChange={(e) => setDateFin(e.target.value)} />
|
||||
|
||||
<div className="form-columns-2 mb-3">
|
||||
<wcs-form-field>
|
||||
<label htmlFor="heure-debut">Heure de début journalière *</label>
|
||||
<input
|
||||
id="heure-debut"
|
||||
type="time"
|
||||
className="form-control"
|
||||
value={heureDebut}
|
||||
onChange={(e) => setHeureDebut(e.target.value)}
|
||||
/>
|
||||
</wcs-form-field>
|
||||
|
||||
<wcs-form-field>
|
||||
<label htmlFor="heure-fin">Heure de fin journalière *</label>
|
||||
<input
|
||||
id="heure-fin"
|
||||
type="time"
|
||||
className="form-control"
|
||||
value={heureFin}
|
||||
onChange={(e) => setHeureFin(e.target.value)}
|
||||
/>
|
||||
</wcs-form-field>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="days-header-row d-flex justify-content-between align-items-center mb-2">
|
||||
<label className="font-weight-bold mb-0">Jours impactés dans la semaine *</label>
|
||||
<div className="d-flex gap-2">
|
||||
<wcs-button mode="stroked" size="s" onClick={selectAllDays}>Tous les jours</wcs-button>
|
||||
<wcs-button mode="stroked" size="s" onClick={selectWeekdays}>Semaine</wcs-button>
|
||||
<wcs-button mode="stroked" size="s" onClick={selectWeekend}>Week-end</wcs-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-columns-2">
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Heure de début journalière *</label>
|
||||
<input type="time" className="premium-input" value={heureDebut} onChange={(e) => setHeureDebut(e.target.value)} />
|
||||
</div>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Heure de fin journalière *</label>
|
||||
<input type="time" className="premium-input" value={heureFin} onChange={(e) => setHeureFin(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group" style={{ marginTop: '12px' }}>
|
||||
<div className="days-header-row">
|
||||
<label className="premium-label">Jours impactés dans la semaine *</label>
|
||||
<div className="days-shortcuts">
|
||||
<button type="button" className="btn-shortcut" onClick={selectAllDays}>Tous les jours</button>
|
||||
<button type="button" className="btn-shortcut" onClick={selectWeekdays}>Semaine</button>
|
||||
<button type="button" className="btn-shortcut" onClick={selectWeekend}>Week-end</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="premium-checkbox-group">
|
||||
{JOURS_SEMAINE.map((jour) => (
|
||||
<label key={jour.value} className={`premium-checkbox-label ${joursTravaux.includes(jour.value) ? 'selected' : ''}`}>
|
||||
<input type="checkbox" checked={joursTravaux.includes(jour.value)} onChange={() => toggleJour(jour.value)} />
|
||||
<div className="d-flex flex-wrap gap-2 mt-2">
|
||||
{JOURS_SEMAINE.map((jour) => {
|
||||
const isSelected = joursTravaux.includes(jour.value);
|
||||
return (
|
||||
<button
|
||||
key={jour.value}
|
||||
type="button"
|
||||
className={`btn-day-pill ${isSelected ? 'is-selected' : ''}`}
|
||||
onClick={() => toggleJour(jour.value)}
|
||||
>
|
||||
<wcs-mat-icon icon={isSelected ? 'check_circle' : 'circle_outline'} size="s" class="mr-1"></wcs-mat-icon>
|
||||
<span>{jour.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</wcs-card-body>
|
||||
</wcs-card>
|
||||
)}
|
||||
|
||||
{/* Section 3 : Substitutions */}
|
||||
<div className="travaux-glass-card">
|
||||
<div className="section-title-wrap">
|
||||
{/* Onglet 3 : Plan de Substitution */}
|
||||
{(activeTabKey === 'substitutions' || activeTabKey === 'all') && (
|
||||
<wcs-card className="wcs-card-form mb-4">
|
||||
<wcs-card-body>
|
||||
<div className="section-title-wrap mb-3">
|
||||
<div className="section-icon-box">
|
||||
<wcs-mat-icon icon="swap_horiz"></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="swap_horiz" size="m"></wcs-mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="travaux-section-title">Plan de Transport de Substitution</h2>
|
||||
@@ -322,27 +400,29 @@ export default function CreerTravaux() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className={`premium-toggle-switch ${substitutionsActive ? 'active' : ''}`}>
|
||||
{/* Interrupteur WCS Switch pour substitutions */}
|
||||
<div className="d-flex align-items-center justify-content-between p-3 mb-4 rounded border bg-light">
|
||||
<div>
|
||||
<strong className="d-block text-dark">Activer le plan de substitution pour ce chantier</strong>
|
||||
<small className="text-muted">Permet de relier des horaires de cars ou trains de substitution actifs</small>
|
||||
</div>
|
||||
<label className="switch-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={substitutionsActive}
|
||||
onChange={(e) => setSubstitutionsActive(e.target.checked)}
|
||||
/>
|
||||
<span className="switch-slider"></span>
|
||||
<span className="switch-text">
|
||||
<strong>Activer le plan de substitution pour ce chantier</strong>
|
||||
<small>Permet de relier des horaires de cars ou trains de substitution actifs</small>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{substitutionsActive && (
|
||||
<div className="substitutions-module-body">
|
||||
<div className="substitutions-search-bar">
|
||||
<div className="search-input-wrap">
|
||||
<wcs-mat-icon icon="search" className="search-icon"></wcs-mat-icon>
|
||||
<div className="substitutions-search-bar d-flex gap-2 mb-3">
|
||||
<div className="flex-grow-1 position-relative">
|
||||
<input
|
||||
type="text"
|
||||
className="premium-input"
|
||||
className="form-control"
|
||||
placeholder="Rechercher par numéro de train, gare de départ ou terminus..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
@@ -353,44 +433,46 @@ export default function CreerTravaux() {
|
||||
className="wcs-primary"
|
||||
onClick={handleSearchHoraires}
|
||||
disabled={loadingHoraires}
|
||||
loading={loadingHoraires}
|
||||
>
|
||||
<wcs-mat-icon icon="search" style={{ marginRight: '6px' }}></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="search" size="s" class="mr-1"></wcs-mat-icon>
|
||||
<span>Rechercher</span>
|
||||
</wcs-button>
|
||||
</div>
|
||||
|
||||
{/* Horaires sélectionnés */}
|
||||
{horairesSelectionnes.length > 0 && (
|
||||
<div className="selected-subs-container">
|
||||
<div className="selected-subs-header">
|
||||
<h4>
|
||||
<wcs-mat-icon icon="check_circle" style={{ color: '#0088ce', marginRight: '6px' }}></wcs-mat-icon>
|
||||
<div className="selected-subs-container mb-4">
|
||||
<div className="selected-subs-header mb-2 d-flex align-items-center justify-content-between">
|
||||
<h4 className="m-0 text-primary">
|
||||
<wcs-mat-icon icon="check_circle" size="s" class="text-success mr-1"></wcs-mat-icon>
|
||||
{horairesSelectionnes.length} circulation{horairesSelectionnes.length > 1 ? 's' : ''} de substitution sélectionnée{horairesSelectionnes.length > 1 ? 's' : ''}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="selected-subs-list">
|
||||
<div className="selected-subs-list d-flex flex-column gap-2">
|
||||
{horairesSelectionnes.map(h => (
|
||||
<div key={h.id} className="selected-sub-card">
|
||||
<div className="sub-main-col">
|
||||
<div className="sub-num-badge">
|
||||
<wcs-mat-icon icon="directions_bus"></wcs-mat-icon>
|
||||
<div key={h.id} className="selected-sub-card d-flex align-items-center justify-content-between p-2 rounded border bg-white">
|
||||
<div className="d-flex align-items-center gap-3">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<wcs-mat-icon icon="directions_bus" size="s"></wcs-mat-icon>
|
||||
<strong>{h.numero_train || 'Train'}</strong>
|
||||
{h.type_train && <wcs-badge className="wcs-info" shape="round">{h.type_train}</wcs-badge>}
|
||||
{h.type_train && <wcs-badge className="wcs-info" shape="pill">{h.type_train}</wcs-badge>}
|
||||
</div>
|
||||
<div className="sub-route">
|
||||
<div className="sub-route d-flex align-items-center gap-1 text-muted">
|
||||
<span>{h.gare_depart_nom} ({formatHeure(h.heure_depart)})</span>
|
||||
<wcs-mat-icon icon="arrow_forward" className="arrow-icon"></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="arrow_forward" size="s"></wcs-mat-icon>
|
||||
<span>{h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-remove-sub"
|
||||
<wcs-button
|
||||
mode="clear"
|
||||
className="wcs-danger"
|
||||
size="s"
|
||||
onClick={() => toggleHoraireSelection(h)}
|
||||
title="Retirer cette substitution"
|
||||
>
|
||||
<wcs-mat-icon icon="close"></wcs-mat-icon>
|
||||
</button>
|
||||
<wcs-mat-icon icon="close" size="s"></wcs-mat-icon>
|
||||
</wcs-button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -400,44 +482,48 @@ export default function CreerTravaux() {
|
||||
{/* Résultats de recherche */}
|
||||
{showResults && (
|
||||
<div className="search-results-section">
|
||||
<div className="search-results-header">
|
||||
<h4>Résultats disponibles ({horairesDisponibles.length})</h4>
|
||||
<span className="results-hint">Cliquez sur une circulation pour l'ajouter ou la retirer</span>
|
||||
<div className="search-results-header mb-2 d-flex align-items-center justify-content-between">
|
||||
<h4 className="m-0">Résultats disponibles ({horairesDisponibles.length})</h4>
|
||||
<span className="text-muted small">Cliquez sur une circulation pour l'ajouter ou la retirer</span>
|
||||
</div>
|
||||
|
||||
{loadingHoraires ? (
|
||||
<div className="search-loading-box">
|
||||
<div className="text-center p-4">
|
||||
<wcs-spinner mode="border"></wcs-spinner>
|
||||
<p>Recherche des circulations en cours...</p>
|
||||
<p className="mt-2 text-muted">Recherche des circulations en cours...</p>
|
||||
</div>
|
||||
) : horairesDisponibles.length === 0 ? (
|
||||
<div className="search-empty-box">
|
||||
<p>Aucun horaire de substitution trouvé pour « {searchQuery} ».</p>
|
||||
<div className="text-center p-4 bg-light rounded text-muted">
|
||||
<p className="m-0">Aucun horaire de substitution trouvé pour « {searchQuery} ».</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="subs-results-list">
|
||||
<div className="subs-results-list d-flex flex-column gap-2">
|
||||
{horairesDisponibles.map(h => {
|
||||
const isSelected = horairesSelectionnes.some(hs => hs.id === h.id);
|
||||
return (
|
||||
<div
|
||||
key={h.id}
|
||||
className={`sub-result-card ${isSelected ? 'is-selected' : ''}`}
|
||||
className={`sub-result-card d-flex align-items-center justify-content-between p-2 rounded border cursor-pointer ${isSelected ? 'border-primary bg-primary-soft' : 'bg-white'}`}
|
||||
onClick={() => toggleHoraireSelection(h)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="result-info">
|
||||
<div className="result-badge-row">
|
||||
<div className="d-flex align-items-center gap-2 mb-1">
|
||||
<strong>{h.numero_train}</strong>
|
||||
{h.type_train && <wcs-badge className="wcs-info" shape="round">{h.type_train}</wcs-badge>}
|
||||
{h.ligne_nom && <wcs-badge className="wcs-secondary" shape="round">{h.ligne_nom}</wcs-badge>}
|
||||
{h.type_train && <wcs-badge className="wcs-info" shape="pill">{h.type_train}</wcs-badge>}
|
||||
{h.ligne_nom && <wcs-badge className="wcs-secondary" shape="pill">{h.ligne_nom}</wcs-badge>}
|
||||
</div>
|
||||
<div className="result-route-row">
|
||||
<div className="d-flex align-items-center gap-1 text-muted small">
|
||||
<span>{h.gare_depart_nom} ({formatHeure(h.heure_depart)})</span>
|
||||
<wcs-mat-icon icon="arrow_forward" className="arrow-icon"></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="arrow_forward" size="s"></wcs-mat-icon>
|
||||
<span>{h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="result-checkbox">
|
||||
<wcs-mat-icon icon={isSelected ? "check_box" : "check_box_outline_blank"} className={isSelected ? "icon-checked" : ""}></wcs-mat-icon>
|
||||
<div>
|
||||
<wcs-badge className={isSelected ? 'wcs-success' : 'wcs-secondary'} shape="pill">
|
||||
<wcs-mat-icon icon={isSelected ? 'check' : 'add'} size="s"></wcs-mat-icon>
|
||||
<span>{isSelected ? 'Sélectionné' : 'Ajouter'}</span>
|
||||
</wcs-badge>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -448,45 +534,78 @@ export default function CreerTravaux() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</wcs-card-body>
|
||||
</wcs-card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom Action Bar */}
|
||||
<div className="form-sticky-bottom-bar">
|
||||
<label className={`premium-checkbox-label ${actif ? 'selected' : ''}`}>
|
||||
<input type="checkbox" checked={actif} onChange={(e) => setActif(e.target.checked)} />
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<wcs-badge className={actif ? "wcs-success" : "wcs-secondary"} shape="round">
|
||||
<wcs-mat-icon icon={actif ? "visibility" : "visibility_off"} style={{ fontSize: '13px', marginRight: '4px' }}></wcs-mat-icon>
|
||||
{actif ? 'Chantier Actif et visible' : 'Chantier Inactif / Masqué'}
|
||||
</wcs-badge>
|
||||
</span>
|
||||
{/* Barre d'action inférieure sticky avec Switch d'activation et WCS buttons */}
|
||||
<div className="form-sticky-bottom-bar d-flex justify-content-between align-items-center p-3 rounded shadow-sm bg-white border mt-4">
|
||||
<div className="d-flex align-items-center gap-3">
|
||||
<label className="switch-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={actif}
|
||||
onChange={(e) => setActif(e.target.checked)}
|
||||
/>
|
||||
<span className="switch-slider"></span>
|
||||
</label>
|
||||
<div>
|
||||
<strong className="d-block">{actif ? 'Chantier Actif et visible' : 'Chantier Inactif / Masqué'}</strong>
|
||||
<small className="text-muted">{actif ? 'Ce chantier sera diffusé aux usagers et aux agents' : 'Ce chantier restera en brouillon non diffusé'}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bottom-bar-btns">
|
||||
<div className="d-flex gap-2">
|
||||
<wcs-button className="wcs-secondary" mode="stroked" onClick={() => navigate('/travaux')}>
|
||||
Annuler
|
||||
<span>Annuler</span>
|
||||
</wcs-button>
|
||||
<wcs-button
|
||||
className="wcs-primary"
|
||||
onClick={handleCreate}
|
||||
disabled={saving || !canCreateTravaux}
|
||||
loading={saving}
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<wcs-spinner mode="border" style={{ width: '16px', height: '16px', marginRight: '6px' }}></wcs-spinner>
|
||||
<span>Enregistrement...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<wcs-mat-icon icon="check" style={{ marginRight: '6px' }}></wcs-mat-icon>
|
||||
<span>Enregistrer les travaux</span>
|
||||
</>
|
||||
)}
|
||||
<wcs-mat-icon icon="check" size="s" class="mr-1"></wcs-mat-icon>
|
||||
<span>Enregistrer le chantier</span>
|
||||
</wcs-button>
|
||||
</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="construction" style={{ fontSize: '20px', color: '#ffffff' }}></wcs-mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<strong style={{ color: '#ffffff', fontSize: '14px', display: 'block' }}>
|
||||
FERROVIA PANEL · DIRECTION TECHNIQUE & INFRASTRUCTURE SNCF
|
||||
</strong>
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.75)', fontSize: '12px' }}>
|
||||
Module Officiel de Planification des Travaux & Substitutions Routières
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="end-left">
|
||||
<span style={{ fontSize: '12px', color: 'rgba(255, 255, 255, 0.65)' }}>
|
||||
© {new Date().getFullYear()} SNCF · Réseau Ferroviaire National
|
||||
</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"></wcs-mat-icon>
|
||||
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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import NavPanel from '../../components/nav-panel/NavPanel';
|
||||
import { getTravauxById, updateTravaux, getHorairesSubstitution } from '../../api/travaux';
|
||||
import { getAllLignes } from '../../api/lignes';
|
||||
@@ -23,6 +23,9 @@ export default function ModifierTravaux() {
|
||||
const path = window.location.pathname;
|
||||
const id = path.replace('/travaux/modifier/', '').split('/')[0];
|
||||
|
||||
const [activeTabKey, setActiveTabKey] = useState('general');
|
||||
const tabsRef = useRef(null);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
@@ -65,6 +68,21 @@ export default function ModifierTravaux() {
|
||||
fetchLignes();
|
||||
}, [scopedRegionId]);
|
||||
|
||||
// Synchronisation des wcs-tabs
|
||||
useEffect(() => {
|
||||
const el = tabsRef.current;
|
||||
if (!el) return;
|
||||
const handleTabChange = (e) => {
|
||||
if (e.detail?.selectedKey) {
|
||||
setActiveTabKey(e.detail.selectedKey);
|
||||
}
|
||||
};
|
||||
el.addEventListener('wcsChange', handleTabChange);
|
||||
return () => {
|
||||
el.removeEventListener('wcsChange', handleTabChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loadTravail = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -159,7 +177,7 @@ export default function ModifierTravaux() {
|
||||
|
||||
const handleUpdate = async () => {
|
||||
if (!canUpdateTravaux) {
|
||||
alert("Vous n'avez pas les droits pour modifier des travaux.");
|
||||
alert("Vous n'avez pas les droits pour modifier ces travaux.");
|
||||
return;
|
||||
}
|
||||
if (!titre.trim() || !dateDebut || !dateFin || joursTravaux.length === 0 || !heureDebut || !heureFin) {
|
||||
@@ -187,15 +205,16 @@ export default function ModifierTravaux() {
|
||||
ligne_nom: selectedLigneId ? (lignes.find(l => l.id === selectedLigneId)?.nom || null) : null
|
||||
};
|
||||
|
||||
const subsIds = substitutionsActive ? horairesSelectionnes.map(h => h.id) : [];
|
||||
const { error } = await updateTravaux(id, payload, subsIds);
|
||||
const substitutionsIds = substitutionsActive ? horairesSelectionnes.map(h => h.id) : [];
|
||||
const { error } = await updateTravaux(id, payload, substitutionsIds);
|
||||
|
||||
if (error) {
|
||||
alert('Erreur lors de la mise à jour : ' + (error.message || 'Erreur'));
|
||||
alert('Erreur lors de la modification des travaux : ' + (error.message || 'Erreur'));
|
||||
} else {
|
||||
navigate('/travaux');
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Une erreur inattendue s'est produite lors de la mise à jour.");
|
||||
alert("Une erreur inattendue s'est produite");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -206,9 +225,9 @@ export default function ModifierTravaux() {
|
||||
<div className="sncf-groupe travaux-page-root">
|
||||
<div className="panel-layout">
|
||||
<div className="panel-nav"><NavPanel /></div>
|
||||
<div className="panel-content travaux-form-page-content" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '60vh' }}>
|
||||
<wcs-spinner mode="border" style={{ width: '48px', height: '48px', color: '#0088ce' }}></wcs-spinner>
|
||||
<p style={{ marginTop: '16px', color: '#64748b', fontWeight: 600 }}>Chargement des données du chantier...</p>
|
||||
<div className="panel-content text-center p-5">
|
||||
<wcs-spinner mode="border"></wcs-spinner>
|
||||
<p className="mt-3 text-muted">Chargement des données du chantier...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,53 +242,69 @@ export default function ModifierTravaux() {
|
||||
</div>
|
||||
|
||||
<div className="panel-content travaux-form-page-content">
|
||||
{/* Header Banner */}
|
||||
{/* Header Banner SNCF avec WCS */}
|
||||
<header className="travaux-form-header-modern">
|
||||
<div className="form-header-main">
|
||||
<button type="button" className="btn-back-link" onClick={() => navigate('/travaux')}>
|
||||
<wcs-mat-icon icon="arrow_back"></wcs-mat-icon>
|
||||
<wcs-button
|
||||
mode="stroked"
|
||||
size="s"
|
||||
className="wcs-secondary mb-2"
|
||||
onClick={() => navigate('/travaux')}
|
||||
>
|
||||
<wcs-mat-icon icon="arrow_back" size="s" class="mr-1"></wcs-mat-icon>
|
||||
<span>Retour aux travaux</span>
|
||||
</button>
|
||||
</wcs-button>
|
||||
|
||||
<div className="form-title-group">
|
||||
<span className="travaux-kicker-pill">
|
||||
<wcs-mat-icon icon="edit"></wcs-mat-icon>
|
||||
Modification Chantier
|
||||
</span>
|
||||
<h1>Modifier les travaux : {titre || 'Chantier'}</h1>
|
||||
<p>Mettez à jour les caractéristiques, le calendrier et les substitutions associées.</p>
|
||||
<div className="d-flex align-items-center gap-2 mb-1">
|
||||
<wcs-badge shape="pill" className="wcs-warning">
|
||||
<wcs-mat-icon icon="edit" size="s" class="mr-1"></wcs-mat-icon>
|
||||
Modification Chantier #{id}
|
||||
</wcs-badge>
|
||||
{scopedRegionId && (
|
||||
<wcs-badge shape="pill" className="wcs-secondary">
|
||||
Région : {scopedRegionId}
|
||||
</wcs-badge>
|
||||
)}
|
||||
</div>
|
||||
<h1>Modifier le chantier de travaux</h1>
|
||||
<p>Ajustez les dates, les contraintes de voie et le plan de substitution associé.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-header-actions">
|
||||
<wcs-button className="wcs-secondary" mode="stroked" onClick={() => navigate('/travaux')}>
|
||||
Annuler
|
||||
<span>Annuler</span>
|
||||
</wcs-button>
|
||||
<wcs-button
|
||||
className="wcs-primary"
|
||||
onClick={handleUpdate}
|
||||
disabled={saving || !canUpdateTravaux}
|
||||
loading={saving}
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<wcs-spinner mode="border" style={{ width: '16px', height: '16px', marginRight: '6px' }}></wcs-spinner>
|
||||
<span>Enregistrement...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<wcs-mat-icon icon="save" style={{ marginRight: '6px' }}></wcs-mat-icon>
|
||||
<span>Enregistrer les modifications</span>
|
||||
</>
|
||||
)}
|
||||
<wcs-mat-icon icon="save" size="s" class="mr-1"></wcs-mat-icon>
|
||||
<span>Mettre à jour le chantier</span>
|
||||
</wcs-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Navigation par Onglets WCS */}
|
||||
<div className="mb-4">
|
||||
<wcs-tabs ref={tabsRef} selected-key={activeTabKey}>
|
||||
<wcs-tab header="1. Général & Ligne" item-key="general"></wcs-tab>
|
||||
<wcs-tab header="2. Calendrier & Période" item-key="calendrier"></wcs-tab>
|
||||
<wcs-tab header={`3. Substitutions (${horairesSelectionnes.length})`} item-key="substitutions"></wcs-tab>
|
||||
</wcs-tabs>
|
||||
</div>
|
||||
|
||||
<div className="travaux-form-grid">
|
||||
{/* Section 1 : Informations générales */}
|
||||
<div className="travaux-glass-card">
|
||||
<div className="section-title-wrap">
|
||||
{/* Onglet 1 : Informations Générales */}
|
||||
{(activeTabKey === 'general' || activeTabKey === 'all') && (
|
||||
<wcs-card className="wcs-card-form mb-4">
|
||||
<wcs-card-body>
|
||||
<div className="section-title-wrap mb-3">
|
||||
<div className="section-icon-box">
|
||||
<wcs-mat-icon icon="info"></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="info" size="m"></wcs-mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="travaux-section-title">Informations Générales</h2>
|
||||
@@ -277,21 +312,23 @@ export default function ModifierTravaux() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Titre des travaux *</label>
|
||||
<wcs-form-field class="mb-3">
|
||||
<label htmlFor="titre-travaux">Titre des travaux *</label>
|
||||
<input
|
||||
id="titre-travaux"
|
||||
type="text"
|
||||
className="premium-input"
|
||||
className="form-control"
|
||||
placeholder="Ex: Rénovation caténaire et voies Paris vers Lyon"
|
||||
value={titre}
|
||||
onChange={(e) => setTitre(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</wcs-form-field>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Ligne ferroviaire associée</label>
|
||||
<wcs-form-field class="mb-3">
|
||||
<label htmlFor="ligne-select">Ligne ferroviaire associée</label>
|
||||
<select
|
||||
className="premium-input"
|
||||
id="ligne-select"
|
||||
className="form-control"
|
||||
value={selectedLigneId}
|
||||
onChange={(e) => setSelectedLigneId(e.target.value)}
|
||||
disabled={loadingLignes}
|
||||
@@ -301,23 +338,27 @@ export default function ModifierTravaux() {
|
||||
<option key={l.id} value={l.id}>{l.nom || l.numero || `Ligne ${l.id}`}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</wcs-form-field>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Description détaillée & Informations voyageurs</label>
|
||||
<div className="mb-3">
|
||||
<label className="form-label font-weight-bold d-block mb-2">Description détaillée & Informations voyageurs</label>
|
||||
<RichTextEditor
|
||||
value={description}
|
||||
onChange={setDescription}
|
||||
placeholder="Détaillez les impacts prévus, motifs techniques, gares concernées..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</wcs-card-body>
|
||||
</wcs-card>
|
||||
)}
|
||||
|
||||
{/* Section 2 : Planification & Calendrier */}
|
||||
<div className="travaux-glass-card">
|
||||
<div className="section-title-wrap">
|
||||
{/* Onglet 2 : Planification & Calendrier */}
|
||||
{(activeTabKey === 'calendrier' || activeTabKey === 'all') && (
|
||||
<wcs-card className="wcs-card-form mb-4">
|
||||
<wcs-card-body>
|
||||
<div className="section-title-wrap mb-3">
|
||||
<div className="section-icon-box">
|
||||
<wcs-mat-icon icon="calendar_month"></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="calendar_month" size="m"></wcs-mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="travaux-section-title">Planification & Horaires</h2>
|
||||
@@ -325,54 +366,92 @@ export default function ModifierTravaux() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-columns-2">
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Date de début *</label>
|
||||
<input type="date" className="premium-input" value={dateDebut} onChange={(e) => setDateDebut(e.target.value)} />
|
||||
<div className="form-columns-2 mb-3">
|
||||
<wcs-form-field>
|
||||
<label htmlFor="date-debut">Date de début *</label>
|
||||
<input
|
||||
id="date-debut"
|
||||
type="date"
|
||||
className="form-control"
|
||||
value={dateDebut}
|
||||
onChange={(e) => setDateDebut(e.target.value)}
|
||||
/>
|
||||
</wcs-form-field>
|
||||
|
||||
<wcs-form-field>
|
||||
<label htmlFor="date-fin">Date de fin *</label>
|
||||
<input
|
||||
id="date-fin"
|
||||
type="date"
|
||||
className="form-control"
|
||||
value={dateFin}
|
||||
onChange={(e) => setDateFin(e.target.value)}
|
||||
/>
|
||||
</wcs-form-field>
|
||||
</div>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Date de fin *</label>
|
||||
<input type="date" className="premium-input" value={dateFin} onChange={(e) => setDateFin(e.target.value)} />
|
||||
|
||||
<div className="form-columns-2 mb-3">
|
||||
<wcs-form-field>
|
||||
<label htmlFor="heure-debut">Heure de début journalière *</label>
|
||||
<input
|
||||
id="heure-debut"
|
||||
type="time"
|
||||
className="form-control"
|
||||
value={heureDebut}
|
||||
onChange={(e) => setHeureDebut(e.target.value)}
|
||||
/>
|
||||
</wcs-form-field>
|
||||
|
||||
<wcs-form-field>
|
||||
<label htmlFor="heure-fin">Heure de fin journalière *</label>
|
||||
<input
|
||||
id="heure-fin"
|
||||
type="time"
|
||||
className="form-control"
|
||||
value={heureFin}
|
||||
onChange={(e) => setHeureFin(e.target.value)}
|
||||
/>
|
||||
</wcs-form-field>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="days-header-row d-flex justify-content-between align-items-center mb-2">
|
||||
<label className="font-weight-bold mb-0">Jours impactés dans la semaine *</label>
|
||||
<div className="d-flex gap-2">
|
||||
<wcs-button mode="stroked" size="s" onClick={selectAllDays}>Tous les jours</wcs-button>
|
||||
<wcs-button mode="stroked" size="s" onClick={selectWeekdays}>Semaine</wcs-button>
|
||||
<wcs-button mode="stroked" size="s" onClick={selectWeekend}>Week-end</wcs-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-columns-2">
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Heure de début journalière *</label>
|
||||
<input type="time" className="premium-input" value={heureDebut} onChange={(e) => setHeureDebut(e.target.value)} />
|
||||
</div>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Heure de fin journalière *</label>
|
||||
<input type="time" className="premium-input" value={heureFin} onChange={(e) => setHeureFin(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group" style={{ marginTop: '12px' }}>
|
||||
<div className="days-header-row">
|
||||
<label className="premium-label">Jours impactés dans la semaine *</label>
|
||||
<div className="days-shortcuts">
|
||||
<button type="button" className="btn-shortcut" onClick={selectAllDays}>Tous les jours</button>
|
||||
<button type="button" className="btn-shortcut" onClick={selectWeekdays}>Semaine</button>
|
||||
<button type="button" className="btn-shortcut" onClick={selectWeekend}>Week-end</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="premium-checkbox-group">
|
||||
{JOURS_SEMAINE.map((jour) => (
|
||||
<label key={jour.value} className={`premium-checkbox-label ${joursTravaux.includes(jour.value) ? 'selected' : ''}`}>
|
||||
<input type="checkbox" checked={joursTravaux.includes(jour.value)} onChange={() => toggleJour(jour.value)} />
|
||||
<div className="d-flex flex-wrap gap-2 mt-2">
|
||||
{JOURS_SEMAINE.map((jour) => {
|
||||
const isSelected = (joursTravaux || []).includes(jour.value);
|
||||
return (
|
||||
<button
|
||||
key={jour.value}
|
||||
type="button"
|
||||
className={`btn-day-pill ${isSelected ? 'is-selected' : ''}`}
|
||||
onClick={() => toggleJour(jour.value)}
|
||||
>
|
||||
<wcs-mat-icon icon={isSelected ? 'check_circle' : 'circle_outline'} size="s" class="mr-1"></wcs-mat-icon>
|
||||
<span>{jour.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</wcs-card-body>
|
||||
</wcs-card>
|
||||
)}
|
||||
|
||||
{/* Section 3 : Substitutions */}
|
||||
<div className="travaux-glass-card">
|
||||
<div className="section-title-wrap">
|
||||
{/* Onglet 3 : Plan de Substitution */}
|
||||
{(activeTabKey === 'substitutions' || activeTabKey === 'all') && (
|
||||
<wcs-card className="wcs-card-form mb-4">
|
||||
<wcs-card-body>
|
||||
<div className="section-title-wrap mb-3">
|
||||
<div className="section-icon-box">
|
||||
<wcs-mat-icon icon="swap_horiz"></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="swap_horiz" size="m"></wcs-mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="travaux-section-title">Plan de Transport de Substitution</h2>
|
||||
@@ -380,27 +459,29 @@ export default function ModifierTravaux() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className={`premium-toggle-switch ${substitutionsActive ? 'active' : ''}`}>
|
||||
{/* Interrupteur WCS Switch pour substitutions */}
|
||||
<div className="d-flex align-items-center justify-content-between p-3 mb-4 rounded border bg-light">
|
||||
<div>
|
||||
<strong className="d-block text-dark">Activer le plan de substitution pour ce chantier</strong>
|
||||
<small className="text-muted">Permet de relier des horaires de cars ou trains de substitution actifs</small>
|
||||
</div>
|
||||
<label className="switch-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={substitutionsActive}
|
||||
onChange={(e) => setSubstitutionsActive(e.target.checked)}
|
||||
/>
|
||||
<span className="switch-slider"></span>
|
||||
<span className="switch-text">
|
||||
<strong>Activer le plan de substitution pour ce chantier</strong>
|
||||
<small>Permet de relier des horaires de cars ou trains de substitution actifs</small>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{substitutionsActive && (
|
||||
<div className="substitutions-module-body">
|
||||
<div className="substitutions-search-bar">
|
||||
<div className="search-input-wrap">
|
||||
<wcs-mat-icon icon="search" className="search-icon"></wcs-mat-icon>
|
||||
<div className="substitutions-search-bar d-flex gap-2 mb-3">
|
||||
<div className="flex-grow-1 position-relative">
|
||||
<input
|
||||
type="text"
|
||||
className="premium-input"
|
||||
className="form-control"
|
||||
placeholder="Rechercher par numéro de train, gare de départ ou terminus..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
@@ -411,44 +492,46 @@ export default function ModifierTravaux() {
|
||||
className="wcs-primary"
|
||||
onClick={handleSearchHoraires}
|
||||
disabled={loadingHoraires}
|
||||
loading={loadingHoraires}
|
||||
>
|
||||
<wcs-mat-icon icon="search" style={{ marginRight: '6px' }}></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="search" size="s" class="mr-1"></wcs-mat-icon>
|
||||
<span>Rechercher</span>
|
||||
</wcs-button>
|
||||
</div>
|
||||
|
||||
{/* Horaires sélectionnés */}
|
||||
{horairesSelectionnes.length > 0 && (
|
||||
<div className="selected-subs-container">
|
||||
<div className="selected-subs-header">
|
||||
<h4>
|
||||
<wcs-mat-icon icon="check_circle" style={{ color: '#0088ce', marginRight: '6px' }}></wcs-mat-icon>
|
||||
<div className="selected-subs-container mb-4">
|
||||
<div className="selected-subs-header mb-2 d-flex align-items-center justify-content-between">
|
||||
<h4 className="m-0 text-primary">
|
||||
<wcs-mat-icon icon="check_circle" size="s" class="text-success mr-1"></wcs-mat-icon>
|
||||
{horairesSelectionnes.length} circulation{horairesSelectionnes.length > 1 ? 's' : ''} de substitution sélectionnée{horairesSelectionnes.length > 1 ? 's' : ''}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="selected-subs-list">
|
||||
<div className="selected-subs-list d-flex flex-column gap-2">
|
||||
{horairesSelectionnes.map(h => (
|
||||
<div key={h.id} className="selected-sub-card">
|
||||
<div className="sub-main-col">
|
||||
<div className="sub-num-badge">
|
||||
<wcs-mat-icon icon="directions_bus"></wcs-mat-icon>
|
||||
<div key={h.id} className="selected-sub-card d-flex align-items-center justify-content-between p-2 rounded border bg-white">
|
||||
<div className="d-flex align-items-center gap-3">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<wcs-mat-icon icon="directions_bus" size="s"></wcs-mat-icon>
|
||||
<strong>{h.numero_train || 'Train'}</strong>
|
||||
{h.type_train && <wcs-badge className="wcs-info" shape="round">{h.type_train}</wcs-badge>}
|
||||
{h.type_train && <wcs-badge className="wcs-info" shape="pill">{h.type_train}</wcs-badge>}
|
||||
</div>
|
||||
<div className="sub-route">
|
||||
<div className="sub-route d-flex align-items-center gap-1 text-muted">
|
||||
<span>{h.gare_depart_nom} ({formatHeure(h.heure_depart)})</span>
|
||||
<wcs-mat-icon icon="arrow_forward" className="arrow-icon"></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="arrow_forward" size="s"></wcs-mat-icon>
|
||||
<span>{h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-remove-sub"
|
||||
<wcs-button
|
||||
mode="clear"
|
||||
className="wcs-danger"
|
||||
size="s"
|
||||
onClick={() => toggleHoraireSelection(h)}
|
||||
title="Retirer cette substitution"
|
||||
>
|
||||
<wcs-mat-icon icon="close"></wcs-mat-icon>
|
||||
</button>
|
||||
<wcs-mat-icon icon="close" size="s"></wcs-mat-icon>
|
||||
</wcs-button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -458,44 +541,48 @@ export default function ModifierTravaux() {
|
||||
{/* Résultats de recherche */}
|
||||
{showResults && (
|
||||
<div className="search-results-section">
|
||||
<div className="search-results-header">
|
||||
<h4>Résultats disponibles ({horairesDisponibles.length})</h4>
|
||||
<span className="results-hint">Cliquez sur une circulation pour l'ajouter ou la retirer</span>
|
||||
<div className="search-results-header mb-2 d-flex align-items-center justify-content-between">
|
||||
<h4 className="m-0">Résultats disponibles ({horairesDisponibles.length})</h4>
|
||||
<span className="text-muted small">Cliquez sur une circulation pour l'ajouter ou la retirer</span>
|
||||
</div>
|
||||
|
||||
{loadingHoraires ? (
|
||||
<div className="search-loading-box">
|
||||
<div className="text-center p-4">
|
||||
<wcs-spinner mode="border"></wcs-spinner>
|
||||
<p>Recherche des circulations en cours...</p>
|
||||
<p className="mt-2 text-muted">Recherche des circulations en cours...</p>
|
||||
</div>
|
||||
) : horairesDisponibles.length === 0 ? (
|
||||
<div className="search-empty-box">
|
||||
<p>Aucun horaire de substitution trouvé pour « {searchQuery} ».</p>
|
||||
<div className="text-center p-4 bg-light rounded text-muted">
|
||||
<p className="m-0">Aucun horaire de substitution trouvé pour « {searchQuery} ».</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="subs-results-list">
|
||||
<div className="subs-results-list d-flex flex-column gap-2">
|
||||
{horairesDisponibles.map(h => {
|
||||
const isSelected = horairesSelectionnes.some(hs => hs.id === h.id);
|
||||
return (
|
||||
<div
|
||||
key={h.id}
|
||||
className={`sub-result-card ${isSelected ? 'is-selected' : ''}`}
|
||||
className={`sub-result-card d-flex align-items-center justify-content-between p-2 rounded border cursor-pointer ${isSelected ? 'border-primary bg-primary-soft' : 'bg-white'}`}
|
||||
onClick={() => toggleHoraireSelection(h)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="result-info">
|
||||
<div className="result-badge-row">
|
||||
<div className="d-flex align-items-center gap-2 mb-1">
|
||||
<strong>{h.numero_train}</strong>
|
||||
{h.type_train && <wcs-badge className="wcs-info" shape="round">{h.type_train}</wcs-badge>}
|
||||
{h.ligne_nom && <wcs-badge className="wcs-secondary" shape="round">{h.ligne_nom}</wcs-badge>}
|
||||
{h.type_train && <wcs-badge className="wcs-info" shape="pill">{h.type_train}</wcs-badge>}
|
||||
{h.ligne_nom && <wcs-badge className="wcs-secondary" shape="pill">{h.ligne_nom}</wcs-badge>}
|
||||
</div>
|
||||
<div className="result-route-row">
|
||||
<div className="d-flex align-items-center gap-1 text-muted small">
|
||||
<span>{h.gare_depart_nom} ({formatHeure(h.heure_depart)})</span>
|
||||
<wcs-mat-icon icon="arrow_forward" className="arrow-icon"></wcs-mat-icon>
|
||||
<wcs-mat-icon icon="arrow_forward" size="s"></wcs-mat-icon>
|
||||
<span>{h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="result-checkbox">
|
||||
<wcs-mat-icon icon={isSelected ? "check_box" : "check_box_outline_blank"} className={isSelected ? "icon-checked" : ""}></wcs-mat-icon>
|
||||
<div>
|
||||
<wcs-badge className={isSelected ? 'wcs-success' : 'wcs-secondary'} shape="pill">
|
||||
<wcs-mat-icon icon={isSelected ? 'check' : 'add'} size="s"></wcs-mat-icon>
|
||||
<span>{isSelected ? 'Sélectionné' : 'Ajouter'}</span>
|
||||
</wcs-badge>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -506,45 +593,78 @@ export default function ModifierTravaux() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</wcs-card-body>
|
||||
</wcs-card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom Action Bar */}
|
||||
<div className="form-sticky-bottom-bar">
|
||||
<label className={`premium-checkbox-label ${actif ? 'selected' : ''}`}>
|
||||
<input type="checkbox" checked={actif} onChange={(e) => setActif(e.target.checked)} />
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<wcs-badge className={actif ? "wcs-success" : "wcs-secondary"} shape="round">
|
||||
<wcs-mat-icon icon={actif ? "visibility" : "visibility_off"} style={{ fontSize: '13px', marginRight: '4px' }}></wcs-mat-icon>
|
||||
{actif ? 'Chantier Actif et visible' : 'Chantier Inactif / Masqué'}
|
||||
</wcs-badge>
|
||||
</span>
|
||||
{/* Barre d'action inférieure sticky avec Switch d'activation et WCS buttons */}
|
||||
<div className="form-sticky-bottom-bar d-flex justify-content-between align-items-center p-3 rounded shadow-sm bg-white border mt-4">
|
||||
<div className="d-flex align-items-center gap-3">
|
||||
<label className="switch-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={actif}
|
||||
onChange={(e) => setActif(e.target.checked)}
|
||||
/>
|
||||
<span className="switch-slider"></span>
|
||||
</label>
|
||||
<div>
|
||||
<strong className="d-block">{actif ? 'Chantier Actif et visible' : 'Chantier Inactif / Masqué'}</strong>
|
||||
<small className="text-muted">{actif ? 'Ce chantier sera diffusé aux usagers et aux agents' : 'Ce chantier restera en brouillon non diffusé'}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bottom-bar-btns">
|
||||
<div className="d-flex gap-2">
|
||||
<wcs-button className="wcs-secondary" mode="stroked" onClick={() => navigate('/travaux')}>
|
||||
Annuler
|
||||
<span>Annuler</span>
|
||||
</wcs-button>
|
||||
<wcs-button
|
||||
className="wcs-primary"
|
||||
onClick={handleUpdate}
|
||||
disabled={saving || !canUpdateTravaux}
|
||||
loading={saving}
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<wcs-spinner mode="border" style={{ width: '16px', height: '16px', marginRight: '6px' }}></wcs-spinner>
|
||||
<span>Enregistrement...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<wcs-mat-icon icon="check" style={{ marginRight: '6px' }}></wcs-mat-icon>
|
||||
<span>Enregistrer les modifications</span>
|
||||
</>
|
||||
)}
|
||||
<wcs-mat-icon icon="check" size="s" class="mr-1"></wcs-mat-icon>
|
||||
<span>Mettre à jour le chantier</span>
|
||||
</wcs-button>
|
||||
</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="construction" style={{ fontSize: '20px', color: '#ffffff' }}></wcs-mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<strong style={{ color: '#ffffff', fontSize: '14px', display: 'block' }}>
|
||||
FERROVIA PANEL · DIRECTION TECHNIQUE & INFRASTRUCTURE SNCF
|
||||
</strong>
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.75)', fontSize: '12px' }}>
|
||||
Module Officiel de Planification des Travaux & Substitutions Routières
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="end-left">
|
||||
<span style={{ fontSize: '12px', color: 'rgba(255, 255, 255, 0.65)' }}>
|
||||
© {new Date().getFullYear()} SNCF · Réseau Ferroviaire National
|
||||
</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"></wcs-mat-icon>
|
||||
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>
|
||||
|
||||
+51
-3
@@ -1,7 +1,16 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || "https://supabase-ferrovia.mrpatator.fr";
|
||||
const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY || "sb_publishable_6dB95QNDKiHcFD81TJETqq_3Mu5rWWU";
|
||||
const SUPABASE_URL =
|
||||
process.env.REACT_APP_SUPABASE_URL ||
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL ||
|
||||
"https://supabase-ferrovia.mrpatator.fr";
|
||||
|
||||
const SUPABASE_ANON_KEY =
|
||||
process.env.REACT_APP_SUPABASE_ANON_KEY ||
|
||||
process.env.REACT_APP_SUPABASE_PUBLISHABLE_KEY ||
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ||
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
|
||||
"sb_publishable_k8EI4S2YA_6Hl5av8uCBr3_S1Bj5g_F";
|
||||
|
||||
const isSupabaseConfigured = !!(SUPABASE_URL && SUPABASE_ANON_KEY);
|
||||
|
||||
@@ -11,7 +20,46 @@ if (!isSupabaseConfigured) {
|
||||
console.warn('REACT_APP_SUPABASE_URL or REACT_APP_SUPABASE_ANON_KEY is not set. Supabase client will be initialized but requests may fail.');
|
||||
}
|
||||
|
||||
const supabase = createClient(SUPABASE_URL || '', SUPABASE_ANON_KEY || '');
|
||||
/**
|
||||
* Fetch wrapper personnalisé pour intercepter les erreurs 500 sur le refresh_token.
|
||||
* Si le serveur GoTrue de Supabase renvoie un statut 500 sur grant_type=refresh_token au lieu de 400 (invalid_grant),
|
||||
* le client @supabase/supabase-js passe en AuthRetryableFetchError et boucle indéfiniment en spammant le serveur.
|
||||
* En interceptant la 500 pour renvoyer 400 (invalid_grant), le SDK comprend immédiatement que le token est expiré,
|
||||
* purge la session locale et stoppe net la boucle de retry.
|
||||
*/
|
||||
const customFetch = async (input, init) => {
|
||||
try {
|
||||
const response = await fetch(input, init);
|
||||
const url = typeof input === 'string' ? input : (input?.url || '');
|
||||
if (url.includes('/auth/v1/token') && url.includes('grant_type=refresh_token') && response.status === 500) {
|
||||
console.warn('[SupabaseClient] Session expirée ou invalide sur le serveur (500 sur refresh_token). Réinitialisation de la session locale pour stopper la boucle.');
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Token de rafraîchissement invalide ou expiré.'
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
return response;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const supabase = createClient(SUPABASE_URL || '', SUPABASE_ANON_KEY || '', {
|
||||
global: {
|
||||
fetch: customFetch
|
||||
},
|
||||
auth: {
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: true
|
||||
}
|
||||
});
|
||||
|
||||
export { isSupabaseConfigured };
|
||||
export default supabase;
|
||||
+1038
-26
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user