diff --git a/.env b/.env index a82982d..3e9e543 100644 --- a/.env +++ b/.env @@ -1,2 +1,6 @@ NEXT_PUBLIC_SUPABASE_URL=https://supabase-ferrovia.mrpatator.fr -NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_6dB95QNDKiHcFD81TJETqq_3Mu5rWWU \ No newline at end of file +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 \ No newline at end of file diff --git a/.env.local b/.env.local index a82982d..3e9e543 100644 --- a/.env.local +++ b/.env.local @@ -1,2 +1,6 @@ NEXT_PUBLIC_SUPABASE_URL=https://supabase-ferrovia.mrpatator.fr -NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_6dB95QNDKiHcFD81TJETqq_3Mu5rWWU \ No newline at end of file +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 \ No newline at end of file diff --git a/SCHEME.sql b/SCHEME.sql new file mode 100644 index 0000000..aa64900 --- /dev/null +++ b/SCHEME.sql @@ -0,0 +1,1607 @@ +-- ============================================================================ +-- FERROVIA PANEL — SCHÉMA COMPLET ET UNIFIÉ DE LA BASE DE DONNÉES +-- Fichier : SCHEME.sql (Racine du projet) +-- Compatible : Supabase PostgreSQL (Cloud & Self-Hosted), Supabase Auth (GoTrue), Supabase Studio +-- Date de révision : 2026-10-03 +-- ============================================================================ +-- Ce script SQL contient l'intégralité de l'infrastructure de la base de données : +-- 1. Extensions (pgcrypto, uuid-ossp) +-- 2. Types ENUM (service_type, station_type, transport_type) +-- 3. Fonctions utilitaires & Triggers updated_at +-- 4. Fonction d'audit (audit_logs) +-- 5. Les 37 Tables complètes avec types exacts et contraintes +-- 6. Index de performance (B-Tree, GIN JSONB, clés étrangères) +-- 7. Synchronisation native Supabase Auth (auth.users <-> public.utilisateurs) +-- 8. Fonctions RPC métier (authenticate_user, get_gare_details, etc.) +-- 9. Politiques de sécurité RLS (Row Level Security) +-- 10. Configuration des Buckets Supabase Storage (fiches-horaires, materiel-roulant, banque-annonces) +-- 11. Privilèges & Grants (anon, authenticated, service_role) +-- 12. Données initiales (Comptes Administrateurs, Paramètres, Types de train) +-- ============================================================================ + +-- ============================================================================ +-- 1. EXTENSIONS POSTGRESQL +-- ============================================================================ +CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA extensions; +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA extensions; + +-- Rendre les fonctions accessibles sans préfixe +SET search_path = public, extensions, auth, pg_temp; + +-- ============================================================================ +-- 2. TYPES ENUM +-- ============================================================================ +DO $$ BEGIN + CREATE TYPE "public"."service_type" AS ENUM ('TER', 'TGV', 'Intercités', 'Fret'); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + CREATE TYPE "public"."station_type" AS ENUM ('interurbaine', 'ville'); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + CREATE TYPE "public"."transport_type" AS ENUM ('bus', 'tram', 'metro', 'tram-train', 'train'); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +-- ============================================================================ +-- 3. FONCTIONS UTILITAIRES & TRIGGERS UPDATED_AT +-- ============================================================================ +CREATE OR REPLACE FUNCTION "public"."set_updated_at"() +RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$; + +CREATE OR REPLACE FUNCTION "public"."handle_updated_at"() +RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$; + +CREATE OR REPLACE FUNCTION "public"."update_updated_at_column"() +RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$; + +CREATE OR REPLACE FUNCTION "public"."update_travaux_updated_at"() +RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$; + +CREATE OR REPLACE FUNCTION "public"."update_utilisateurs_updated_at"() +RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$; + +-- Stub RPC GTFS +CREATE OR REPLACE FUNCTION "public"."import_gtfs_rpc"(payload json) +RETURNS json +LANGUAGE plpgsql AS $$ +BEGIN + RETURN json_build_object('status', 'ok', 'created', 0, 'warnings', json_build_array()); +EXCEPTION WHEN OTHERS THEN + RETURN json_build_object('status', 'error', 'message', SQLERRM); +END; +$$; + +-- ============================================================================ +-- 4. FONCTION D'AUDIT +-- ============================================================================ +CREATE OR REPLACE FUNCTION "public"."log_audit"() +RETURNS trigger +LANGUAGE plpgsql SECURITY DEFINER +SET search_path = public, extensions, auth, pg_temp +AS $$ +DECLARE + v_changed_by_text text; + v_changed_by uuid; +BEGIN + -- Tenter de récupérer l'UUID de l'utilisateur connecté via JWT + v_changed_by_text := COALESCE( + current_setting('request.jwt.claim.sub', true), + current_setting('jwt.claims.user_id', true), + current_setting('jwt.claims.sub', true) + ); + + IF v_changed_by_text IS NOT NULL AND v_changed_by_text <> '' THEN + BEGIN + v_changed_by := v_changed_by_text::uuid; + EXCEPTION WHEN others THEN + v_changed_by := NULL; + END; + ELSE + v_changed_by := NULL; + END IF; + + IF (TG_OP = 'INSERT') THEN + INSERT INTO public.audit_logs(table_name, operation, record_id, changed_by, new_data, changed_at) + VALUES (TG_TABLE_NAME, 'INSERT', NEW.id, v_changed_by, row_to_json(NEW)::jsonb, NOW()); + RETURN NEW; + ELSIF (TG_OP = 'UPDATE') THEN + INSERT INTO public.audit_logs(table_name, operation, record_id, changed_by, old_data, new_data, changed_at) + VALUES (TG_TABLE_NAME, 'UPDATE', NEW.id, v_changed_by, row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb, NOW()); + RETURN NEW; + ELSIF (TG_OP = 'DELETE') THEN + INSERT INTO public.audit_logs(table_name, operation, record_id, changed_by, old_data, changed_at) + VALUES (TG_TABLE_NAME, 'DELETE', OLD.id, v_changed_by, row_to_json(OLD)::jsonb, NOW()); + RETURN OLD; + END IF; + RETURN NULL; +EXCEPTION WHEN OTHERS THEN + -- Ne jamais bloquer la transaction principale en cas d'erreur de journalisation + RETURN COALESCE(NEW, OLD); +END; +$$; + +-- ============================================================================ +-- 5. TABLES (ORDONNÉES PAR DÉPENDANCES) +-- ============================================================================ + +-- ---------------------------------------------------------------------------- +-- 5.01 audit_logs +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."audit_logs" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "table_name" text NOT NULL, + "operation" text NOT NULL, + "record_id" uuid, + "changed_by" uuid, + "old_data" jsonb, + "new_data" jsonb, + "changed_at" timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_audit_logs_table" ON "public"."audit_logs" ("table_name"); +CREATE INDEX IF NOT EXISTS "idx_audit_logs_record" ON "public"."audit_logs" ("record_id"); +CREATE INDEX IF NOT EXISTS "idx_audit_logs_changed_by" ON "public"."audit_logs" ("changed_by"); +CREATE INDEX IF NOT EXISTS "idx_audit_logs_changed_at" ON "public"."audit_logs" ("changed_at" DESC); + +-- ---------------------------------------------------------------------------- +-- 5.02 utilisateurs (Profils métier et administrateurs du panel) +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."utilisateurs" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "email" varchar(255) NOT NULL UNIQUE, + "nom" varchar(100) NOT NULL DEFAULT '', + "prenom" varchar(100) NOT NULL DEFAULT '', + "role" varchar(50) NOT NULL DEFAULT 'utilisateur', + "actif" boolean NOT NULL DEFAULT true, + "password_hash" varchar(255), + "photo_url" text, + "region_admin" boolean DEFAULT false, + "region_id" varchar(100), + "region_nom" varchar(255), + "permissions" jsonb NOT NULL DEFAULT '{}'::jsonb, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE UNIQUE INDEX IF NOT EXISTS "utilisateurs_email_lower_idx" ON "public"."utilisateurs" (lower(email::text)); +CREATE INDEX IF NOT EXISTS "idx_utilisateurs_email" ON "public"."utilisateurs" ("email"); +CREATE INDEX IF NOT EXISTS "idx_utilisateurs_role" ON "public"."utilisateurs" ("role"); +CREATE INDEX IF NOT EXISTS "idx_utilisateurs_actif" ON "public"."utilisateurs" ("actif"); +CREATE INDEX IF NOT EXISTS "idx_utilisateurs_region_id" ON "public"."utilisateurs" ("region_id"); + +-- ---------------------------------------------------------------------------- +-- 5.03 profils_utilisateurs (Voyageurs / App mobile & site public) +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."profils_utilisateurs" ( + "id" uuid NOT NULL PRIMARY KEY, + "email" text NOT NULL, + "prenom" text, + "nom" text, + "date_naissance" date, + "telephone" text, + "carte_reduction" text, + "gares_favorites" text[] DEFAULT '{}'::text[], + "lignes_favorites" text[] DEFAULT '{}'::text[], + "cartes_bancaires" jsonb DEFAULT '[]'::jsonb, + "mode_paiement_favori" text, + "commandes" jsonb DEFAULT '[]'::jsonb, + "panier" jsonb DEFAULT '[]'::jsonb, + "trajets_favoris" text[] DEFAULT '{}'::text[], + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_profils_email" ON "public"."profils_utilisateurs" ("email"); + +-- ---------------------------------------------------------------------------- +-- 5.04 gares +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."gares" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "nom" text NOT NULL, + "type" varchar(50) NOT NULL DEFAULT 'ville', + "services" "public"."service_type"[] NOT NULL DEFAULT '{}'::service_type[], + "informations" jsonb DEFAULT '{}'::jsonb, + "quais_hall" jsonb DEFAULT '[]'::jsonb, + "created_at" timestamptz NOT NULL DEFAULT now(), + "updated_at" timestamptz NOT NULL DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_gares_nom" ON "public"."gares" ("nom"); +CREATE INDEX IF NOT EXISTS "idx_gares_type" ON "public"."gares" ("type"); + +-- ---------------------------------------------------------------------------- +-- 5.05 gare_quais +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."gare_quais" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "gare_id" uuid NOT NULL REFERENCES "public"."gares"("id") ON DELETE CASCADE, + "nom" text NOT NULL, + "distance_metres" integer NOT NULL DEFAULT 0, + "created_at" timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_gare_quais_gare_id" ON "public"."gare_quais" ("gare_id"); + +-- ---------------------------------------------------------------------------- +-- 5.06 gare_transports +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."gare_transports" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "gare_id" uuid NOT NULL REFERENCES "public"."gares"("id") ON DELETE CASCADE, + "type" "public"."transport_type" NOT NULL, + "ligne" text, + "created_at" timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_gare_transports_gare_id" ON "public"."gare_transports" ("gare_id"); + +-- ---------------------------------------------------------------------------- +-- 5.07 types_train +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."types_train" ( + "id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "nom" varchar(100) NOT NULL, + "code" varchar(20), + "description" text, + "logo_url" text, + "logo_color_url" text, + "couleur" varchar(50), + "type_train" varchar(100), + "color" varchar(50), + "actif" boolean DEFAULT true, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_types_train_nom" ON "public"."types_train" ("nom"); +CREATE INDEX IF NOT EXISTS "idx_types_train_actif" ON "public"."types_train" ("actif"); + +-- ---------------------------------------------------------------------------- +-- 5.08 depots_technicentres (Dépôts, Technicentres et Ateliers de Maintenance) +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."depots_technicentres" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "nom" varchar(255) NOT NULL, + "code" varchar(50) UNIQUE, + "ville" varchar(100) NOT NULL, + "gare_proche" varchar(255), + "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "capacite_max" integer NOT NULL DEFAULT 20, + "voies_disponibles" integer, + "specialites" jsonb DEFAULT '[]'::jsonb, + "region_id" varchar(100), + "region_nom" varchar(255), + "adresse" text, + "description" text, + "actif" boolean NOT NULL DEFAULT true, + "created_at" timestamptz NOT NULL DEFAULT now(), + "updated_at" timestamptz NOT NULL DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_depots_nom" ON "public"."depots_technicentres" ("nom"); +CREATE INDEX IF NOT EXISTS "idx_depots_code" ON "public"."depots_technicentres" ("code"); +CREATE INDEX IF NOT EXISTS "idx_depots_ville" ON "public"."depots_technicentres" ("ville"); +CREATE INDEX IF NOT EXISTS "idx_depots_actif" ON "public"."depots_technicentres" ("actif") WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS "idx_depots_region" ON "public"."depots_technicentres" ("region_id"); + +-- ---------------------------------------------------------------------------- +-- 5.09 "materiel-roulant" +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."materiel-roulant" ( + "id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "nom" varchar(255) NOT NULL, + "nom_technique" varchar(255), + "capacite" integer, + "type_train" varchar(100), + "numero_serie" varchar(100), + "image_url" text, + "images" jsonb DEFAULT '[]'::jsonb, + "serie" varchar(100), + "type" varchar(100), + "places_assises" integer, + "vitesse_max" integer, + "alimentation" varchar(100), + "photo_url" text, + "description" text, + "actif" boolean DEFAULT true, + "depot_id" uuid REFERENCES "public"."depots_technicentres"("id") ON DELETE SET NULL, + "depot_attache" varchar(255), + "statut_maintenance" varchar(100) DEFAULT 'OPERATIONNEL', + "date_prochaine_maintenance" date, + "kilometrage" integer DEFAULT 0, + "couplage_famille" varchar(100), + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now() +); + +-- Migration idempotente pour tables déjà créées +DO $$ +BEGIN + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "nom_technique" varchar(255); + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "capacite" integer; + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "type_train" varchar(100); + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "numero_serie" varchar(100); + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "image_url" text; + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "images" jsonb DEFAULT '[]'::jsonb; + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "places_assises" integer; + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "vitesse_max" integer; + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "alimentation" varchar(100); + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "photo_url" text; + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "description" text; + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "actif" boolean DEFAULT true; + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "depot_id" uuid REFERENCES "public"."depots_technicentres"("id") ON DELETE SET NULL; + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "depot_attache" varchar(255); + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "statut_maintenance" varchar(100) DEFAULT 'OPERATIONNEL'; + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "date_prochaine_maintenance" date; + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "kilometrage" integer DEFAULT 0; + ALTER TABLE "public"."materiel-roulant" ADD COLUMN IF NOT EXISTS "couplage_famille" varchar(100); +EXCEPTION WHEN OTHERS THEN + NULL; +END $$; + +CREATE INDEX IF NOT EXISTS "idx_materiel_serie" ON "public"."materiel-roulant" ("serie"); +CREATE INDEX IF NOT EXISTS "idx_materiel_actif" ON "public"."materiel-roulant" ("actif"); +CREATE INDEX IF NOT EXISTS "idx_materiel_type_train" ON "public"."materiel-roulant" ("type_train"); +CREATE INDEX IF NOT EXISTS "idx_materiel_numero_serie" ON "public"."materiel-roulant" ("numero_serie"); +CREATE INDEX IF NOT EXISTS "idx_materiel_depot_id" ON "public"."materiel-roulant" ("depot_id"); +CREATE INDEX IF NOT EXISTS "idx_materiel_depot_attache" ON "public"."materiel-roulant" ("depot_attache"); +CREATE INDEX IF NOT EXISTS "idx_materiel_statut_maintenance" ON "public"."materiel-roulant" ("statut_maintenance"); + +-- ---------------------------------------------------------------------------- +-- 5.09 services_annuels +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."services_annuels" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "nom" varchar(255) NOT NULL, + "code" varchar(50), + "date_debut" date NOT NULL, + "date_fin" date NOT NULL, + "description" text, + "actif" boolean DEFAULT false, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_services_annuels_dates" ON "public"."services_annuels" ("date_debut", "date_fin"); +CREATE INDEX IF NOT EXISTS "idx_services_annuels_actif" ON "public"."services_annuels" ("actif") WHERE deleted_at IS NULL; + +-- ---------------------------------------------------------------------------- +-- 5.10 lignes +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."lignes" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "nom" varchar(255) NOT NULL, + "code" varchar(50), + "gare_depart_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "gare_arrivee_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "actif" boolean DEFAULT true, + "fh_url" text, + "region" varchar(100), + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_lignes_actif" ON "public"."lignes" ("actif") WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS "idx_lignes_fh_url" ON "public"."lignes" ("fh_url"); +CREATE INDEX IF NOT EXISTS "idx_lignes_region" ON "public"."lignes" ("region"); + +-- ---------------------------------------------------------------------------- +-- 5.11 lignes_gares +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."lignes_gares" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "ligne_id" uuid NOT NULL REFERENCES "public"."lignes"("id") ON DELETE CASCADE, + "gare_id" uuid NOT NULL REFERENCES "public"."gares"("id") ON DELETE CASCADE, + "ordre" integer NOT NULL DEFAULT 1, + "desserte" varchar(50) DEFAULT 'tous', + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_lignes_gares_ligne_id" ON "public"."lignes_gares" ("ligne_id"); +CREATE INDEX IF NOT EXISTS "idx_lignes_gares_gare_id" ON "public"."lignes_gares" ("gare_id"); +CREATE INDEX IF NOT EXISTS "idx_lignes_gares_ordre" ON "public"."lignes_gares" ("ligne_id", "ordre"); + +-- ---------------------------------------------------------------------------- +-- 5.12 horaires (Circulations principales) +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."horaires" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "numero_train" varchar(50) NOT NULL, + "nom_train" varchar(255), + "code_mission" varchar(50), + "sens" varchar(20) DEFAULT 'aller', + "type_train" varchar(100), + "type_train_id" bigint REFERENCES "public"."types_train"("id") ON DELETE SET NULL, + "transporteur" varchar(100) DEFAULT 'SNCF Voyageurs', + "gare_depart_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "gare_depart_nom" varchar(255) NOT NULL, + "heure_depart" time, + "gare_arrivee_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "gare_arrivee_nom" varchar(255) NOT NULL, + "heure_arrivee" time, + "gares_desservies" jsonb NOT NULL DEFAULT '[]'::jsonb, + "attributions_quais" jsonb NOT NULL DEFAULT '{}'::jsonb, + "snapshot_gares_desservies" jsonb DEFAULT '[]'::jsonb, + "circule_lundi" boolean DEFAULT true, + "circule_mardi" boolean DEFAULT true, + "circule_mercredi" boolean DEFAULT true, + "circule_jeudi" boolean DEFAULT true, + "circule_vendredi" boolean DEFAULT true, + "circule_samedi" boolean DEFAULT true, + "circule_dimanche" boolean DEFAULT true, + "circule_jours_feries" boolean DEFAULT false, + "circule_dimanches_feries" boolean DEFAULT false, + "date_debut" timestamptz, + "date_fin" timestamptz, + "jours_personnalises" jsonb NOT NULL DEFAULT '[]'::jsonb, + "jours_non_circulation" jsonb NOT NULL DEFAULT '[]'::jsonb, + "calendrier_circulation" jsonb NOT NULL DEFAULT '[]'::jsonb, + "periode_validite" jsonb NOT NULL DEFAULT '{}'::jsonb, + "materiel_roulant_id" bigint REFERENCES "public"."materiel-roulant"("id") ON DELETE SET NULL, + "composition_train" jsonb NOT NULL DEFAULT '[]'::jsonb, + "ligne_id" uuid REFERENCES "public"."lignes"("id") ON DELETE SET NULL, + "ligne_nom" varchar(255), + "region_id" varchar(100), + "region" varchar(255), + "service_annuel_id" uuid REFERENCES "public"."services_annuels"("id") ON DELETE SET NULL, + "service_annuel_nom" varchar(255), + "service_type" varchar(50) DEFAULT 'TER', + "est_substitution" boolean NOT NULL DEFAULT false, + "motif_substitution" varchar(255), + "substitution_disponible" boolean NOT NULL DEFAULT false, + "horaire_id" uuid REFERENCES "public"."horaires"("id") ON DELETE SET NULL, + "substitutions_associees" jsonb NOT NULL DEFAULT '[]'::jsonb, + "sillon_id" text, + "tranche" varchar(50), + "gestionnaire_infrastructure" varchar(100) DEFAULT 'SNCF Réseau', + "vitesse_max_kmh" integer, + "statut" varchar(50) NOT NULL DEFAULT 'a_l_heure', + "retard_minutes" integer NOT NULL DEFAULT 0, + "retard_motif" text, + "informations_voyageurs" text, + "actif" boolean NOT NULL DEFAULT true, + "metadata" jsonb NOT NULL DEFAULT '{}'::jsonb, + "notes" text, + "created_at" timestamptz NOT NULL DEFAULT now(), + "updated_at" timestamptz NOT NULL DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_horaires_numero_train" ON "public"."horaires" ("numero_train"); +CREATE INDEX IF NOT EXISTS "idx_horaires_gare_depart" ON "public"."horaires" ("gare_depart_id"); +CREATE INDEX IF NOT EXISTS "idx_horaires_gare_arrivee" ON "public"."horaires" ("gare_arrivee_id"); +CREATE INDEX IF NOT EXISTS "idx_horaires_departs" ON "public"."horaires" ("gare_depart_id", "heure_depart"); +CREATE INDEX IF NOT EXISTS "idx_horaires_arrivees" ON "public"."horaires" ("gare_arrivee_id", "heure_arrivee"); +CREATE INDEX IF NOT EXISTS "idx_horaires_actif" ON "public"."horaires" ("actif") WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS "idx_horaires_type_train" ON "public"."horaires" ("type_train_id"); +CREATE INDEX IF NOT EXISTS "idx_horaires_materiel" ON "public"."horaires" ("materiel_roulant_id"); +CREATE INDEX IF NOT EXISTS "idx_horaires_ligne" ON "public"."horaires" ("ligne_id"); +CREATE INDEX IF NOT EXISTS "idx_horaires_service_annuel" ON "public"."horaires" ("service_annuel_id"); +CREATE INDEX IF NOT EXISTS "idx_horaires_substitution" ON "public"."horaires" ("est_substitution"); +CREATE INDEX IF NOT EXISTS "idx_horaires_region_id" ON "public"."horaires" ("region_id"); +CREATE INDEX IF NOT EXISTS "idx_horaires_dates" ON "public"."horaires" ("date_debut", "date_fin"); +CREATE INDEX IF NOT EXISTS "idx_horaires_statut" ON "public"."horaires" ("statut"); +CREATE INDEX IF NOT EXISTS "idx_horaires_gares_desservies_gin" ON "public"."horaires" USING gin ("gares_desservies" jsonb_path_ops); +CREATE INDEX IF NOT EXISTS "idx_horaires_attributions_quais_gin" ON "public"."horaires" USING gin ("attributions_quais"); +CREATE INDEX IF NOT EXISTS "idx_horaires_calendrier_gin" ON "public"."horaires" USING gin ("calendrier_circulation" jsonb_path_ops); +CREATE INDEX IF NOT EXISTS "idx_horaires_metadata_gin" ON "public"."horaires" USING gin ("metadata"); + +DO $$ +BEGIN + ALTER TABLE "public"."horaires" ADD COLUMN IF NOT EXISTS "couplage_train_id" uuid REFERENCES "public"."horaires"("id") ON DELETE SET NULL; + ALTER TABLE "public"."horaires" ADD COLUMN IF NOT EXISTS "couplage_numero_train" varchar(50); + ALTER TABLE "public"."horaires" ADD COLUMN IF NOT EXISTS "manoeuvres" jsonb DEFAULT '[]'::jsonb; +EXCEPTION WHEN OTHERS THEN + NULL; +END $$; + +-- ---------------------------------------------------------------------------- +-- 5.13 horaires_gares_desservies +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."horaires_gares_desservies" ( + "id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "horaire_id" uuid REFERENCES "public"."horaires"("id") ON DELETE CASCADE, + "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "gare_nom" varchar(255) NOT NULL, + "heure_arrivee" time, + "heure_depart" time, + "ordre" integer NOT NULL DEFAULT 0, + "quai" varchar(50), + "quai_id" uuid REFERENCES "public"."gare_quais"("id") ON DELETE SET NULL, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_gares_desservies_horaire" ON "public"."horaires_gares_desservies" ("horaire_id"); +CREATE INDEX IF NOT EXISTS "idx_gares_desservies_ordre" ON "public"."horaires_gares_desservies" ("horaire_id", "ordre"); +CREATE INDEX IF NOT EXISTS "idx_hgd_gare_id" ON "public"."horaires_gares_desservies" ("gare_id"); + +-- ---------------------------------------------------------------------------- +-- 5.14 horaires_substitutions +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."horaires_substitutions" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "horaire_id" uuid REFERENCES "public"."horaires"("id") ON DELETE SET NULL, + "region_id" varchar(100), + "region" varchar(255), + "numero_train" varchar(50), + "type_train" varchar(100), + "type_train_id" bigint REFERENCES "public"."types_train"("id") ON DELETE SET NULL, + "gare_depart_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "gare_depart_nom" varchar(255), + "heure_depart" time, + "gare_arrivee_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "gare_arrivee_nom" varchar(255), + "heure_arrivee" time, + "date_debut" timestamptz, + "date_fin" timestamptz, + "circule_lundi" boolean DEFAULT true, + "circule_mardi" boolean DEFAULT true, + "circule_mercredi" boolean DEFAULT true, + "circule_jeudi" boolean DEFAULT true, + "circule_vendredi" boolean DEFAULT true, + "circule_samedi" boolean DEFAULT true, + "circule_dimanche" boolean DEFAULT true, + "circule_jours_feries" boolean DEFAULT false, + "jours_personnalises" jsonb DEFAULT '[]'::jsonb, + "jours_non_circulation" jsonb DEFAULT '[]'::jsonb, + "materiel_roulant_id" bigint REFERENCES "public"."materiel-roulant"("id") ON DELETE SET NULL, + "ligne_id" uuid REFERENCES "public"."lignes"("id") ON DELETE SET NULL, + "ligne_nom" varchar(255), + "service_annuel_id" uuid REFERENCES "public"."services_annuels"("id") ON DELETE SET NULL, + "service_annuel_nom" varchar(255), + "est_substitution" boolean DEFAULT true, + "motif_substitution" varchar(255), + "gares_desservies" jsonb DEFAULT '[]'::jsonb, + "composition_train" jsonb DEFAULT '[]'::jsonb, + "actif" boolean DEFAULT true, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_horaires_substitutions_region" ON "public"."horaires_substitutions" ("region_id"); +CREATE INDEX IF NOT EXISTS "idx_horaires_substitutions_horaire" ON "public"."horaires_substitutions" ("horaire_id"); +CREATE INDEX IF NOT EXISTS "idx_horaires_substitutions_actif" ON "public"."horaires_substitutions" ("actif") WHERE deleted_at IS NULL; + +-- ---------------------------------------------------------------------------- +-- 5.15 perturbations +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."perturbations" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "horaire_id" uuid NOT NULL REFERENCES "public"."horaires"("id") ON DELETE CASCADE, + "type_perturbation" varchar(50) NOT NULL CHECK (type_perturbation IN ('retard','suppression','modification_parcours')), + "duree_retard_minutes" integer, + "cause" varchar(100) NOT NULL CHECK (cause IN ('incident_technique','accident_personne','conditions_meteorologiques','travaux','mouvement_social','intervention_police','affluence','malveillance','autre')), + "cause_details" text, + "sillon_substitution_id" uuid REFERENCES "public"."horaires"("id") ON DELETE SET NULL, + "date_debut" timestamptz NOT NULL DEFAULT now(), + "date_fin" timestamptz, + "actif" boolean DEFAULT true, + "notes" text, + "region_id" varchar(100), + "created_by" uuid, + "perturbations_arrets" jsonb, + "jours_perturbation" jsonb, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_perturbations_horaire" ON "public"."perturbations" ("horaire_id"); +CREATE INDEX IF NOT EXISTS "idx_perturbations_type" ON "public"."perturbations" ("type_perturbation"); +CREATE INDEX IF NOT EXISTS "idx_perturbations_actif" ON "public"."perturbations" ("actif") WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS "idx_perturbations_dates" ON "public"."perturbations" ("date_debut","date_fin"); +CREATE INDEX IF NOT EXISTS "idx_perturbations_sillon_substitution" ON "public"."perturbations" ("sillon_substitution_id"); +CREATE INDEX IF NOT EXISTS "idx_perturbations_region_id" ON "public"."perturbations" ("region_id"); +CREATE INDEX IF NOT EXISTS "idx_perturbations_created_by" ON "public"."perturbations" ("created_by"); + +-- ---------------------------------------------------------------------------- +-- 5.16 perturbations_modifications_parcours +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."perturbations_modifications_parcours" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "perturbation_id" uuid NOT NULL REFERENCES "public"."perturbations"("id") ON DELETE CASCADE, + "type_modification" varchar(20) NOT NULL CHECK (type_modification IN ('gare_supprimee','gare_ajoutee','horaire_modifie')), + "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "gare_nom" varchar(255) NOT NULL, + "nouvelle_heure_arrivee" time, + "nouvelle_heure_depart" time, + "ordre" integer, + "created_at" timestamptz DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_modif_parcours_perturbation" ON "public"."perturbations_modifications_parcours" ("perturbation_id"); +CREATE INDEX IF NOT EXISTS "idx_modif_parcours_gare" ON "public"."perturbations_modifications_parcours" ("gare_id"); + +-- ---------------------------------------------------------------------------- +-- 5.17 info_trafic +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."info_trafic" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "titre" varchar(255) NOT NULL, + "description" text, + "type" varchar(50) NOT NULL CHECK (type IN ('perturbation','travaux','greve','meteo','autre')), + "severite" varchar(20) NOT NULL CHECK (severite IN ('info','warning','critical')), + "ligne" varchar(100), + "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "date_debut" timestamptz NOT NULL DEFAULT now(), + "date_fin" timestamptz, + "actif" boolean DEFAULT true, + "region_id" varchar(100), + "created_by" uuid, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_info_trafic_actif" ON "public"."info_trafic" ("actif") WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS "idx_info_trafic_dates" ON "public"."info_trafic" ("date_debut","date_fin"); +CREATE INDEX IF NOT EXISTS "idx_info_trafic_region_id" ON "public"."info_trafic" ("region_id"); +CREATE INDEX IF NOT EXISTS "idx_info_trafic_created_by" ON "public"."info_trafic" ("created_by"); + +-- ---------------------------------------------------------------------------- +-- 5.18 evenements +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."evenements" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "titre" varchar(255) NOT NULL, + "description" text, + "lieu" varchar(255), + "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "date_debut" timestamptz NOT NULL, + "date_fin" timestamptz, + "image_url" varchar(500), + "lien_externe" varchar(500), + "actif" boolean DEFAULT true, + "visible_site" boolean DEFAULT true, + "region_id" varchar(100), + "created_by" uuid, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_evenements_actif" ON "public"."evenements" ("actif") WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS "idx_evenements_dates" ON "public"."evenements" ("date_debut","date_fin"); +CREATE INDEX IF NOT EXISTS "idx_evenements_region_id" ON "public"."evenements" ("region_id"); +CREATE INDEX IF NOT EXISTS "idx_evenements_created_by" ON "public"."evenements" ("created_by"); + +-- ---------------------------------------------------------------------------- +-- 5.19 actualites +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."actualites" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "titre" varchar(255) NOT NULL, + "contenu" text NOT NULL, + "resume" varchar(500), + "categorie" varchar(50) CHECK (categorie IN ('general','service','promotion','partenariat','autre')), + "image_url" varchar(500), + "image_couverture" text, + "date_publication" timestamptz NOT NULL DEFAULT now(), + "auteur" varchar(100), + "actif" boolean DEFAULT true, + "mise_en_avant" boolean DEFAULT false, + "region_id" varchar(100), + "created_by" uuid, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_actualites_actif" ON "public"."actualites" ("actif") WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS "idx_actualites_publication" ON "public"."actualites" ("date_publication"); +CREATE INDEX IF NOT EXISTS "idx_actualites_region_id" ON "public"."actualites" ("region_id"); +CREATE INDEX IF NOT EXISTS "idx_actualites_created_by" ON "public"."actualites" ("created_by"); + +-- ---------------------------------------------------------------------------- +-- 5.20 travaux +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."travaux" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "titre" text NOT NULL, + "description" text, + "date_debut" date NOT NULL, + "date_fin" date NOT NULL, + "jours_travaux" text[] NOT NULL, + "heure_debut" time NOT NULL, + "heure_fin" time NOT NULL, + "actif" boolean DEFAULT true, + "ligne_id" uuid REFERENCES "public"."lignes"("id") ON DELETE SET NULL, + "ligne_nom" text, + "region_id" varchar(100), + "created_by" uuid REFERENCES "public"."utilisateurs"("id") ON DELETE SET NULL, + "substitutions_attribuees" jsonb DEFAULT '[]'::jsonb, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz, + CONSTRAINT "valid_dates" CHECK (date_fin >= date_debut) +); +CREATE INDEX IF NOT EXISTS "idx_travaux_dates" ON "public"."travaux" ("date_debut","date_fin"); +CREATE INDEX IF NOT EXISTS "idx_travaux_actif" ON "public"."travaux" ("actif"); +CREATE INDEX IF NOT EXISTS "idx_travaux_deleted" ON "public"."travaux" ("deleted_at"); +CREATE INDEX IF NOT EXISTS "idx_travaux_ligne_id" ON "public"."travaux" ("ligne_id"); +CREATE INDEX IF NOT EXISTS "idx_travaux_region_id" ON "public"."travaux" ("region_id"); +CREATE INDEX IF NOT EXISTS "idx_travaux_created_by" ON "public"."travaux" ("created_by"); +CREATE INDEX IF NOT EXISTS "idx_travaux_substitutions_attribuees_gin" ON "public"."travaux" USING gin ("substitutions_attribuees"); + +-- ---------------------------------------------------------------------------- +-- 5.21 travaux_substitutions +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."travaux_substitutions" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "travaux_id" uuid NOT NULL REFERENCES "public"."travaux"("id") ON DELETE CASCADE, + "horaire_id" uuid NOT NULL REFERENCES "public"."horaires"("id") ON DELETE CASCADE, + "ordre" integer DEFAULT 0, + "created_at" timestamptz DEFAULT now(), + UNIQUE ("travaux_id","horaire_id") +); +CREATE INDEX IF NOT EXISTS "idx_travaux_substitutions_travaux" ON "public"."travaux_substitutions" ("travaux_id"); +CREATE INDEX IF NOT EXISTS "idx_travaux_substitutions_horaire" ON "public"."travaux_substitutions" ("horaire_id"); + +-- ---------------------------------------------------------------------------- +-- 5.22 infos_sillons +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."infos_sillons" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "titre" varchar(255) NOT NULL, + "description" text, + "type" varchar(50) DEFAULT 'information', + "ligne_id" uuid REFERENCES "public"."lignes"("id") ON DELETE SET NULL, + "ligne_nom" varchar(255), + "travaux_id" uuid REFERENCES "public"."travaux"("id") ON DELETE SET NULL, + "date_debut" timestamptz, + "date_fin" timestamptz, + "actif" boolean DEFAULT true, + "auto_generated" boolean DEFAULT false, + "region_id" varchar(100), + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_infos_sillons_ligne_id" ON "public"."infos_sillons" ("ligne_id"); +CREATE INDEX IF NOT EXISTS "idx_infos_sillons_travaux_id" ON "public"."infos_sillons" ("travaux_id"); +CREATE INDEX IF NOT EXISTS "idx_infos_sillons_region_id" ON "public"."infos_sillons" ("region_id"); +CREATE INDEX IF NOT EXISTS "idx_infos_sillons_actif" ON "public"."infos_sillons" ("actif"); + +-- ---------------------------------------------------------------------------- +-- 5.23 fiches_horaires +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."fiches_horaires" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "titre" varchar(255) NOT NULL, + "ligne_id" varchar(100), + "ligne_nom" varchar(255), + "code_ligne" varchar(50), + "service_annuel_id" varchar(100), + "service_annuel_nom" varchar(255), + "code_service_annuel" varchar(50), + "direction" varchar(50) DEFAULT 'aller', + "statut" varchar(50) DEFAULT 'Projet (Brouillon)', + "pdf_url" text, + "pdf_filename" varchar(255), + "pdf_storage_path" text, + "config" jsonb DEFAULT '{}'::jsonb, + "circulations_count" integer DEFAULT 0, + "gares_count" integer DEFAULT 0, + "region_id" varchar(100), + "created_by" varchar(255), + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_fiches_horaires_ligne_id" ON "public"."fiches_horaires" ("ligne_id"); +CREATE INDEX IF NOT EXISTS "idx_fiches_horaires_service_id" ON "public"."fiches_horaires" ("service_annuel_id"); +CREATE INDEX IF NOT EXISTS "idx_fiches_horaires_statut" ON "public"."fiches_horaires" ("statut"); +CREATE INDEX IF NOT EXISTS "idx_fiches_horaires_region" ON "public"."fiches_horaires" ("region_id"); + +-- ---------------------------------------------------------------------------- +-- 5.24 tarifs +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."tarifs" ( + "id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "nom" varchar(255) NOT NULL, + "type" varchar(50) NOT NULL, + "description" text, + "prix_minimum" numeric(10,2), + "prix_par_km" numeric(10,3), + "age_maximum" integer, + "regions" text[] NOT NULL, + "date_debut" date, + "date_fin" date, + "ordre" integer NOT NULL DEFAULT 0, + "page_dediee" boolean DEFAULT false, + "infos" jsonb DEFAULT '{}'::jsonb, + "details" jsonb, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_tarifs_ordre" ON "public"."tarifs" ("ordre"); +CREATE INDEX IF NOT EXISTS "idx_tarifs_regions" ON "public"."tarifs" USING gin ("regions"); + +-- ---------------------------------------------------------------------------- +-- 5.25 parametres +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."parametres" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "cle" varchar(100) NOT NULL UNIQUE, + "valeur" jsonb, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_parametres_cle" ON "public"."parametres" ("cle"); + +-- ---------------------------------------------------------------------------- +-- 5.26 menus_regionaux +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."menus_regionaux" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "nom" varchar(200) NOT NULL, + "description" text, + "actif" boolean DEFAULT false, + "structure" jsonb DEFAULT '[]'::jsonb, + "region_id" varchar(64), + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_menus_regionaux_region_id" ON "public"."menus_regionaux" ("region_id"); + +-- ---------------------------------------------------------------------------- +-- 5.27 pages (CMS) +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."pages" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "titre" text NOT NULL, + "slug" text, + "contenu" jsonb DEFAULT '[]'::jsonb, + "statut" text DEFAULT 'brouillon', + "region_id" varchar(255), + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_pages_slug" ON "public"."pages" ("slug"); +CREATE INDEX IF NOT EXISTS "idx_pages_region_id" ON "public"."pages" ("region_id"); + +-- ---------------------------------------------------------------------------- +-- 5.28 page_personnalisation +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."page_personnalisation" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "page" text NOT NULL UNIQUE, + "config" jsonb DEFAULT '{}'::jsonb, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now() +); + +-- ---------------------------------------------------------------------------- +-- 5.29 info_gares +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."info_gares" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "titre" text, + "description" text, + "debut_affichage" timestamptz, + "fin_affichage" timestamptz, + "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "ligne_id" uuid REFERENCES "public"."lignes"("id") ON DELETE SET NULL, + "gare_ids" uuid[] DEFAULT '{}'::uuid[], + "ligne_ids" uuid[] DEFAULT '{}'::uuid[], + "type" text DEFAULT 'Information', + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_info_gares_gare_id" ON "public"."info_gares" ("gare_id"); +CREATE INDEX IF NOT EXISTS "idx_info_gares_ligne_id" ON "public"."info_gares" ("ligne_id"); +CREATE INDEX IF NOT EXISTS "idx_info_gares_gare_ids" ON "public"."info_gares" USING gin ("gare_ids"); +CREATE INDEX IF NOT EXISTS "idx_info_gares_ligne_ids" ON "public"."info_gares" USING gin ("ligne_ids"); + +-- ---------------------------------------------------------------------------- +-- 5.30 regles_quais +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."regles_quais" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "gare_id" uuid NOT NULL REFERENCES "public"."gares"("id") ON DELETE CASCADE, + "quai_id" uuid NOT NULL REFERENCES "public"."gare_quais"("id") ON DELETE CASCADE, + "type_train" varchar(50), + "sens" varchar(20), + "priorite" integer DEFAULT 1, + "actif" boolean DEFAULT true, + "description" text, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_regles_quais_gare" ON "public"."regles_quais" ("gare_id"); +CREATE INDEX IF NOT EXISTS "idx_regles_quais_quai" ON "public"."regles_quais" ("quai_id"); + +-- ---------------------------------------------------------------------------- +-- 5.31 messagerie_threads +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."messagerie_threads" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "created_at" timestamptz NOT NULL DEFAULT now(), + "updated_at" timestamptz NOT NULL DEFAULT now() +); + +-- ---------------------------------------------------------------------------- +-- 5.32 messagerie_participants +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."messagerie_participants" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "thread_id" uuid NOT NULL REFERENCES "public"."messagerie_threads"("id") ON DELETE CASCADE, + "user_id" uuid NOT NULL REFERENCES "public"."utilisateurs"("id") ON DELETE CASCADE, + "created_at" timestamptz NOT NULL DEFAULT now(), + UNIQUE ("thread_id","user_id") +); +CREATE INDEX IF NOT EXISTS "idx_messagerie_participants_user_id" ON "public"."messagerie_participants" ("user_id"); + +-- ---------------------------------------------------------------------------- +-- 5.33 messagerie_messages +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."messagerie_messages" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "thread_id" uuid NOT NULL REFERENCES "public"."messagerie_threads"("id") ON DELETE CASCADE, + "sender_id" uuid NOT NULL REFERENCES "public"."utilisateurs"("id") ON DELETE CASCADE, + "contenu" text NOT NULL, + "created_at" timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_messagerie_messages_thread_id" ON "public"."messagerie_messages" ("thread_id"); +CREATE INDEX IF NOT EXISTS "idx_messagerie_messages_created_at" ON "public"."messagerie_messages" ("created_at" DESC); + +-- ---------------------------------------------------------------------------- +-- 5.34 banque_mots (Annonces sonores) +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."banque_mots" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "label" text, + "nom_fichier" text, + "storage_path" text, + "categorie" text DEFAULT 'general', + "format" text DEFAULT 'mp3', + "taille_bytes" bigint, + "description" text, + "duree_ms" integer, + "actif" boolean NOT NULL DEFAULT true, + "created_by" uuid, + "created_at" timestamptz NOT NULL DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_banque_mots_categorie" ON "public"."banque_mots" ("categorie"); +CREATE INDEX IF NOT EXISTS "idx_banque_mots_label" ON "public"."banque_mots" ("label"); +CREATE INDEX IF NOT EXISTS "idx_banque_mots_actif" ON "public"."banque_mots" ("actif") WHERE deleted_at IS NULL; + +-- ---------------------------------------------------------------------------- +-- 5.35 annonces_templates +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."annonces_templates" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "nom" text NOT NULL, + "type_annonce" text NOT NULL, + "description" text, + "sequence" jsonb NOT NULL DEFAULT '[]'::jsonb, + "variables" jsonb NOT NULL DEFAULT '[]'::jsonb, + "tts_texte" text, + "actif" boolean NOT NULL DEFAULT true, + "created_by" uuid, + "created_at" timestamptz NOT NULL DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_templates_type" ON "public"."annonces_templates" ("type_annonce"); +CREATE INDEX IF NOT EXISTS "idx_templates_actif" ON "public"."annonces_templates" ("actif") WHERE deleted_at IS NULL; + +-- ---------------------------------------------------------------------------- +-- 5.36 annonces_enregistrees +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."annonces_enregistrees" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "nom" text, + "type_annonce" text, + "template_id" uuid REFERENCES "public"."annonces_templates"("id") ON DELETE SET NULL, + "storage_path" text, + "nom_fichier" text, + "variables_values" jsonb DEFAULT '{}'::jsonb, + "mode_generation" text DEFAULT 'tts', + "duree_ms" integer, + "taille_bytes" bigint, + "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "diffuse" boolean DEFAULT false, + "date_diffusion" timestamptz, + "notes" text, + "created_by" uuid, + "created_at" timestamptz NOT NULL DEFAULT now(), + "updated_at" timestamptz DEFAULT now(), + "deleted_at" timestamptz +); +CREATE INDEX IF NOT EXISTS "idx_annonces_enregistrees_template" ON "public"."annonces_enregistrees" ("template_id"); +CREATE INDEX IF NOT EXISTS "idx_annonces_enregistrees_gare" ON "public"."annonces_enregistrees" ("gare_id"); + +-- ---------------------------------------------------------------------------- +-- 5.37 annonces_diffusions +-- ---------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS "public"."annonces_diffusions" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + "annonce_id" uuid REFERENCES "public"."annonces_enregistrees"("id") ON DELETE SET NULL, + "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, + "audio_path" text, + "tts_texte" text, + "created_at" timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS "idx_annonces_diffusions_annonce" ON "public"."annonces_diffusions" ("annonce_id"); +CREATE INDEX IF NOT EXISTS "idx_annonces_diffusions_gare" ON "public"."annonces_diffusions" ("gare_id"); + +-- ============================================================================ +-- 6. TRIGGERS AUTOMATIQUES UPDATED_AT & AUDIT +-- ============================================================================ + +-- updated_at triggers +DO $$ +DECLARE + t text; + tables text[] := ARRAY[ + 'utilisateurs', 'gares', 'types_train', 'depots_technicentres', 'materiel-roulant', 'services_annuels', + 'lignes', 'lignes_gares', 'horaires', 'horaires_gares_desservies', 'horaires_substitutions', + 'perturbations', 'info_trafic', 'evenements', 'actualites', 'travaux', + 'infos_sillons', 'fiches_horaires', 'tarifs', 'parametres', 'menus_regionaux', + 'pages', 'page_personnalisation', 'info_gares', 'regles_quais', 'messagerie_threads', + 'profils_utilisateurs', 'banque_mots', 'annonces_templates', 'annonces_enregistrees' + ]; +BEGIN + FOREACH t IN ARRAY tables LOOP + EXECUTE format('DROP TRIGGER IF EXISTS "tr_%s_set_updated_at" ON "public".%I;', t, t); + EXECUTE format('CREATE TRIGGER "tr_%s_set_updated_at" BEFORE UPDATE ON "public".%I FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();', t, t); + END LOOP; +END $$; + +-- audit_logs triggers on sensitive tables +DO $$ +DECLARE + t text; + tables text[] := ARRAY[ + 'utilisateurs', 'gares', 'lignes', 'horaires', 'perturbations', + 'info_trafic', 'evenements', 'actualites', 'travaux', 'tarifs', 'parametres' + ]; +BEGIN + FOREACH t IN ARRAY tables LOOP + EXECUTE format('DROP TRIGGER IF EXISTS "tr_%s_audit" ON "public".%I;', t, t); + EXECUTE format('CREATE TRIGGER "tr_%s_audit" AFTER INSERT OR UPDATE OR DELETE ON "public".%I FOR EACH ROW EXECUTE FUNCTION "public"."log_audit"();', t, t); + END LOOP; +END $$; + +-- ============================================================================ +-- 7. SYNCHRONISATION NATIVE SUPABASE AUTH (auth.users <-> public.utilisateurs) +-- ============================================================================ +-- Cette fonction est déclenchée lors de la création ou modification d'un compte +-- dans Supabase Auth (via Dashboard Supabase Studio ou supabase.auth.signUp). +-- Elle met à jour automatiquement public.utilisateurs sans jamais bloquer GoTrue. + +CREATE OR REPLACE FUNCTION public.handle_new_auth_user() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, extensions, auth, pg_temp +AS $$ +DECLARE + v_nom text; + v_prenom text; + v_role text; + v_region_id text; + v_region_nom text; + v_photo_url text; + v_actif boolean; + v_region_admin boolean; + v_permissions jsonb; +BEGIN + -- Extraction sécurisée des métadonnées Supabase Auth + v_nom := COALESCE( + NEW.raw_user_meta_data->>'nom', + NEW.raw_user_meta_data->>'last_name', + split_part(NEW.email, '@', 1), + 'Utilisateur' + ); + v_prenom := COALESCE( + NEW.raw_user_meta_data->>'prenom', + NEW.raw_user_meta_data->>'first_name', + '' + ); + v_role := COALESCE(NEW.raw_user_meta_data->>'role', 'utilisateur'); + v_region_id := NEW.raw_user_meta_data->>'region_id'; + v_region_nom := NEW.raw_user_meta_data->>'region_nom'; + v_photo_url := NEW.raw_user_meta_data->>'photo_url'; + v_actif := COALESCE((NEW.raw_user_meta_data->>'actif')::boolean, true); + v_region_admin := COALESCE((NEW.raw_user_meta_data->>'region_admin')::boolean, false); + v_permissions := COALESCE(NEW.raw_user_meta_data->'permissions', '{}'::jsonb); + + -- Insertion ou synchronisation dans public.utilisateurs + INSERT INTO public.utilisateurs ( + id, email, nom, prenom, role, actif, region_admin, + region_id, region_nom, permissions, photo_url, updated_at + ) VALUES ( + NEW.id, + lower(NEW.email), + v_nom, + v_prenom, + v_role, + v_actif, + v_region_admin, + v_region_id, + v_region_nom, + v_permissions, + v_photo_url, + NOW() + ) + ON CONFLICT (email) DO UPDATE SET + id = EXCLUDED.id, + nom = CASE WHEN EXCLUDED.nom <> '' AND EXCLUDED.nom <> 'Utilisateur' THEN EXCLUDED.nom ELSE public.utilisateurs.nom END, + prenom = CASE WHEN EXCLUDED.prenom <> '' THEN EXCLUDED.prenom ELSE public.utilisateurs.prenom END, + role = CASE WHEN public.utilisateurs.role = 'admin' THEN 'admin' ELSE EXCLUDED.role END, + actif = EXCLUDED.actif, + region_admin = COALESCE(public.utilisateurs.region_admin, EXCLUDED.region_admin), + region_id = COALESCE(EXCLUDED.region_id, public.utilisateurs.region_id), + region_nom = COALESCE(EXCLUDED.region_nom, public.utilisateurs.region_nom), + permissions = CASE WHEN public.utilisateurs.permissions <> '{}'::jsonb THEN public.utilisateurs.permissions ELSE EXCLUDED.permissions END, + photo_url = COALESCE(EXCLUDED.photo_url, public.utilisateurs.photo_url), + updated_at = NOW(); + + RETURN NEW; +EXCEPTION WHEN OTHERS THEN + -- Ne JAMAIS faire échouer l'insertion dans auth.users + RAISE WARNING 'handle_new_auth_user warning: %', SQLERRM; + RETURN NEW; +END; +$$; + +-- Attacher le trigger sur auth.users si le schéma auth existe +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'auth' AND table_name = 'users') THEN + DROP TRIGGER IF EXISTS "on_auth_user_created" ON auth.users; + CREATE TRIGGER "on_auth_user_created" + AFTER INSERT OR UPDATE ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.handle_new_auth_user(); + END IF; +END $$; + +-- ============================================================================ +-- 8. FONCTIONS RPC MÉTIER +-- ============================================================================ + +-- Authentification personnalisée de secours (RPC authenticate_user) +CREATE OR REPLACE FUNCTION "public"."authenticate_user"(p_email text, p_password text) +RETURNS TABLE(id uuid, email text, nom text, prenom text, name text, role text, region_admin boolean, region_id text) +LANGUAGE plpgsql STABLE SECURITY DEFINER +SET search_path = public, extensions, auth, pg_temp +AS $$ +BEGIN + RETURN QUERY + SELECT + u.id::uuid, + u.email::text, + u.nom::text, + u.prenom::text, + (COALESCE(u.nom,'') || ' ' || COALESCE(u.prenom,''))::text as name, + u.role::text, + COALESCE(u.region_admin, false)::boolean as region_admin, + u.region_id::text + FROM public.utilisateurs u + WHERE lower(u.email) = lower(p_email) + AND u.password_hash IS NOT NULL + AND u.password_hash = crypt(p_password, u.password_hash) + AND COALESCE(u.actif, true) = true + AND u.deleted_at IS NULL; +END; +$$; + +-- Création utilisateur avec mot de passe hashé +CREATE OR REPLACE FUNCTION "public"."create_user_with_password"( + p_email text, p_nom text, p_prenom text, + p_role text DEFAULT 'utilisateur', p_actif boolean DEFAULT true, + p_password text DEFAULT NULL, p_photo_url text DEFAULT NULL, + p_region_admin boolean DEFAULT false, p_region_id text DEFAULT NULL +) +RETURNS TABLE(id uuid, email text, nom text, prenom text, role text, actif boolean, photo_url text, region_admin boolean, region_id text) +LANGUAGE plpgsql SECURITY DEFINER +SET search_path = public, extensions, auth, pg_temp +AS $$ +DECLARE + new_id uuid; +BEGIN + INSERT INTO public.utilisateurs ( + email, nom, prenom, role, actif, password_hash, photo_url, region_admin, region_id, created_at + ) VALUES ( + lower(p_email), p_nom, p_prenom, p_role, p_actif, + CASE WHEN p_password IS NOT NULL AND p_password != '' THEN crypt(p_password, gen_salt('bf')) ELSE NULL END, + p_photo_url, p_region_admin, p_region_id, NOW() + ) + RETURNING utilisateurs.id INTO new_id; + + RETURN QUERY SELECT + u.id, u.email::text, u.nom::text, u.prenom::text, u.role::text, + u.actif, u.photo_url::text, u.region_admin, u.region_id::text + FROM public.utilisateurs u WHERE u.id = new_id; +END; +$$; + +-- Mise à jour mot de passe +CREATE OR REPLACE FUNCTION "public"."update_user_password"(p_user_id uuid, p_password text) +RETURNS void +LANGUAGE plpgsql SECURITY DEFINER +SET search_path = public, extensions, auth, pg_temp +AS $$ +BEGIN + IF p_password IS NULL OR p_password = '' THEN + RAISE EXCEPTION 'Le mot de passe ne peut pas être vide'; + END IF; + + UPDATE public.utilisateurs + SET password_hash = crypt(p_password, gen_salt('bf')), + updated_at = NOW() + WHERE id = p_user_id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'Utilisateur non trouvé'; + END IF; +END; +$$; + +-- Détails agrégés d'une gare (quais + transports) +CREATE OR REPLACE FUNCTION "public"."get_gare_details"(p_gare_id uuid) +RETURNS TABLE(id uuid, nom text, type text, services "public"."service_type"[], quais jsonb, transports jsonb, created_at timestamptz, updated_at timestamptz) +LANGUAGE plpgsql STABLE SECURITY DEFINER +SET search_path = public, extensions, auth, pg_temp +AS $$ +BEGIN + RETURN QUERY SELECT + g.id, + g.nom, + g.type::text, + g.services, + COALESCE(( + SELECT jsonb_agg(jsonb_build_object('id', q.id, 'nom', q.nom, 'distance_metres', q.distance_metres)) + FROM public.gare_quais q WHERE q.gare_id = g.id + ), '[]'::jsonb) as quais, + COALESCE(( + SELECT jsonb_agg(jsonb_build_object('id', t.id, 'type', t.type, 'ligne', t.ligne)) + FROM public.gare_transports t WHERE t.gare_id = g.id + ), '[]'::jsonb) as transports, + g.created_at, + g.updated_at + FROM public.gares g + WHERE g.id = p_gare_id AND g.deleted_at IS NULL; +END; +$$; + +-- ============================================================================ +-- 9. ROW LEVEL SECURITY (RLS) & POLICIES +-- ============================================================================ +-- Activer le RLS sur l'ensemble des 37 tables du schéma public +DO $$ +DECLARE + t text; + tables text[] := ARRAY[ + 'audit_logs', 'utilisateurs', 'profils_utilisateurs', 'gares', 'gare_quais', + 'gare_transports', 'types_train', 'depots_technicentres', 'materiel-roulant', 'services_annuels', + 'lignes', 'lignes_gares', 'horaires', 'horaires_gares_desservies', 'horaires_substitutions', + 'perturbations', 'perturbations_modifications_parcours', 'info_trafic', 'evenements', + 'actualites', 'travaux', 'travaux_substitutions', 'infos_sillons', 'fiches_horaires', + 'tarifs', 'parametres', 'menus_regionaux', 'pages', 'page_personnalisation', + 'info_gares', 'regles_quais', 'messagerie_threads', 'messagerie_participants', + 'messagerie_messages', 'banque_mots', 'annonces_templates', 'annonces_enregistrees', + 'annonces_diffusions' + ]; +BEGIN + FOREACH t IN ARRAY tables LOOP + EXECUTE format('ALTER TABLE "public".%I ENABLE ROW LEVEL SECURITY;', t); + EXECUTE format('DROP POLICY IF EXISTS "policy_all_%s" ON "public".%I;', t, t); + EXECUTE format('CREATE POLICY "policy_all_%s" ON "public".%I FOR ALL TO anon, authenticated, service_role USING (true) WITH CHECK (true);', t, t); + END LOOP; +END $$; + +-- Politiques spécifiques pour l'app mobile (profils_utilisateurs) +DROP POLICY IF EXISTS "Lecture profil propre" ON "public"."profils_utilisateurs"; +DROP POLICY IF EXISTS "Insertion profil propre" ON "public"."profils_utilisateurs"; +DROP POLICY IF EXISTS "Mise à jour profil propre" ON "public"."profils_utilisateurs"; +DROP POLICY IF EXISTS "Suppression profil propre" ON "public"."profils_utilisateurs"; + +CREATE POLICY "Lecture profil propre" ON "public"."profils_utilisateurs" FOR SELECT USING (true); +CREATE POLICY "Insertion profil propre" ON "public"."profils_utilisateurs" FOR INSERT WITH CHECK (true); +CREATE POLICY "Mise à jour profil propre" ON "public"."profils_utilisateurs" FOR UPDATE USING (true) WITH CHECK (true); +CREATE POLICY "Suppression profil propre" ON "public"."profils_utilisateurs" FOR DELETE USING (true); + +-- ============================================================================ +-- 10. SUPABASE STORAGE BUCKETS & POLICIES +-- ============================================================================ +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'storage') THEN + + -- 1. Buckets fiches horaires + INSERT INTO storage.buckets ("id", "name", "public", "file_size_limit", "allowed_mime_types") + VALUES ('fiches-horaires', 'fiches-horaires', true, 52428800, ARRAY['application/pdf']) + ON CONFLICT ("id") DO UPDATE SET "public" = true; + + INSERT INTO storage.buckets ("id", "name", "public", "file_size_limit", "allowed_mime_types") + VALUES ('Fiches_Horaires', 'Fiches_Horaires', true, 52428800, ARRAY['application/pdf']) + ON CONFLICT ("id") DO UPDATE SET "public" = true; + + -- 2. Bucket matériel roulant (photos de trains) + INSERT INTO storage.buckets ("id", "name", "public", "file_size_limit", "allowed_mime_types") + VALUES ('materiel-roulant', 'materiel-roulant', true, 10485760, ARRAY['image/jpeg','image/png','image/webp','image/gif','image/svg+xml']) + ON CONFLICT ("id") DO UPDATE SET "public" = true; + + -- 3. Bucket annonces sonores (jingles et fichiers audio) + INSERT INTO storage.buckets ("id", "name", "public", "file_size_limit", "allowed_mime_types") + VALUES ('banque-annonces', 'banque-annonces', true, 20971520, ARRAY['audio/mpeg','audio/wav','audio/mp3','audio/ogg']) + ON CONFLICT ("id") DO UPDATE SET "public" = true; + + -- Politiques Storage pour l'accès aux objets + DROP POLICY IF EXISTS "Public access on storage fiches-horaires" ON storage.objects; + DROP POLICY IF EXISTS "Public access on storage materiel-roulant" ON storage.objects; + DROP POLICY IF EXISTS "Public access on storage banque-annonces" ON storage.objects; + + CREATE POLICY "Public access on storage fiches-horaires" + ON storage.objects FOR ALL TO anon, authenticated, service_role + USING (bucket_id IN ('fiches-horaires', 'Fiches_Horaires')) + WITH CHECK (bucket_id IN ('fiches-horaires', 'Fiches_Horaires')); + + CREATE POLICY "Public access on storage materiel-roulant" + ON storage.objects FOR ALL TO anon, authenticated, service_role + USING (bucket_id = 'materiel-roulant') + WITH CHECK (bucket_id = 'materiel-roulant'); + + CREATE POLICY "Public access on storage banque-annonces" + ON storage.objects FOR ALL TO anon, authenticated, service_role + USING (bucket_id = 'banque-annonces') + WITH CHECK (bucket_id = 'banque-annonces'); + + END IF; +EXCEPTION WHEN OTHERS THEN + RAISE NOTICE 'Storage buckets notice: %', SQLERRM; +END $$; + +-- ============================================================================ +-- 11. PRIVILÈGES & GRANTS (anon, authenticated, service_role) +-- ============================================================================ +GRANT USAGE ON SCHEMA "public" TO "postgres", "anon", "authenticated", "service_role"; + +-- Accès complet sur toutes les tables +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA "public" TO "anon", "authenticated", "service_role"; + +-- Séquences +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "public" TO "anon", "authenticated", "service_role"; + +-- Fonctions +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA "public" TO "anon", "authenticated", "service_role"; + +-- Privilèges par défaut pour les futures créations +ALTER DEFAULT PRIVILEGES IN SCHEMA "public" GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "anon", "authenticated", "service_role"; +ALTER DEFAULT PRIVILEGES IN SCHEMA "public" GRANT USAGE, SELECT ON SEQUENCES TO "anon", "authenticated", "service_role"; +ALTER DEFAULT PRIVILEGES IN SCHEMA "public" GRANT EXECUTE ON FUNCTIONS TO "anon", "authenticated", "service_role"; + +-- Schéma extensions +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'extensions') THEN + GRANT USAGE ON SCHEMA "extensions" TO "postgres", "anon", "authenticated", "service_role"; + GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA "extensions" TO "postgres", "anon", "authenticated", "service_role"; + END IF; +END $$; + +-- ============================================================================ +-- 12. DONNÉES INITIALES (SEEDING) +-- ============================================================================ + +-- 12.1 Paramètres système par défaut +INSERT INTO "public"."parametres" (cle, valeur) VALUES + ('general', '{"nom_site": "Ferrovia Panel", "description_site": "Système de supervision et gestion ferroviaire", "region": "Nationale", "logo_header_url": ""}'::jsonb), + ('tarifs', '{"billets": [], "abonnements": [], "remises": []}'::jsonb), + ('theme', '{"primary_color": "#0088CE", "dark_mode": false}'::jsonb) +ON CONFLICT (cle) DO NOTHING; + +-- 12.2 Types de train par défaut +INSERT INTO "public"."types_train" (id, nom, code, description, couleur, color, type_train, actif) VALUES + (1, 'TER', 'TER', 'Transport Express Régional', '#0088CE', '#0088CE', 'TER', true), + (2, 'TGV INOUI', 'TGV', 'Train à Grande Vitesse INOUI', '#7A9A01', '#7A9A01', 'TGV', true), + (3, 'OUIGO', 'OUIGO', 'Offre grande vitesse low-cost', '#009AA6', '#009AA6', 'OUIGO', true), + (4, 'Intercités', 'IC', 'Lignes classiques nationales et de nuit', '#CD0037', '#CD0037', 'Intercités', true), + (5, 'Transilien', 'TRA', 'Réseau de banlieue Île-de-France', '#0055A5', '#0055A5', 'Transilien', true), + (6, 'Fret', 'FRET', 'Transport ferroviaire de marchandises', '#64748B', '#64748B', 'Fret', true) +ON CONFLICT (id) DO UPDATE SET + nom = EXCLUDED.nom, + code = EXCLUDED.code, + couleur = EXCLUDED.couleur, + color = EXCLUDED.color, + actif = EXCLUDED.actif; + +-- 12.3 Dépôts et Technicentres de maintenance réseau par défaut +INSERT INTO "public"."depots_technicentres" ("nom", "code", "ville", "gare_proche", "capacite_max", "specialites") VALUES + ('Technicentre Paris-Nord (La Chapelle)', 'TPN-LCP', 'Paris', 'Paris Gare du Nord', 24, '["TGV", "TER", "Eurostar", "Maintenance Lourde"]'::jsonb), + ('Technicentre Atlantique (Châtillon)', 'TPA-CHT', 'Paris', 'Paris Montparnasse', 30, '["TGV InOui", "OUIGO", "Grandes Lignes"]'::jsonb), + ('Technicentre Est Européen (Pantin)', 'TEE-PAN', 'Paris', 'Paris Gare de l''Est', 18, '["TGV", "ICE", "Révision mi-vie"]'::jsonb), + ('Technicentre Paris-Sud-Est (Villeneuve)', 'TPSE-VLL', 'Paris', 'Paris Gare de Lyon', 28, '["TGV Sud-Est", "Automotrices", "Climatisation"]'::jsonb), + ('Technicentre Bretagne (Rennes)', 'TB-REN', 'Rennes', 'Rennes', 16, '["TER BreizhGo", "Régiolis", "Z2N", "Bogies"]'::jsonb), + ('Dépôt Pays de la Loire (Nantes-Blottereau)', 'DPL-NTS', 'Nantes', 'Nantes', 14, '["TER Aléop", "ZTER", "AGC Diesel/Electrique"]'::jsonb), + ('Technicentre Auvergne-Rhône-Alpes (Gerland)', 'TARA-GER', 'Lyon', 'Lyon Part-Dieu', 22, '["TER AURA", "TGV", "Maintenance Préventive"]'::jsonb), + ('Dépôt Marseille-Blancarde', 'DMB-MRS', 'Marseille', 'Marseille Saint-Charles', 15, '["TER Zou", "Rames Réversibles", "Electrique"]'::jsonb), + ('Technicentre Nouvelle-Aquitaine (Bordeaux)', 'TNA-BDX', 'Bordeaux', 'Bordeaux Saint-Jean', 16, '["TER Nouvelle-Aquitaine", "Coradia Polyvalent"]'::jsonb), + ('Technicentre Nord-Pas-de-Calais (Hellemmes)', 'TNPDC-HLM', 'Lille', 'Lille Flandres', 20, '["TER Hauts-de-France", "Grand Carénage"]'::jsonb), + ('Dépôt Grand Est (Strasbourg-Cronenbourg)', 'DGE-STB', 'Strasbourg', 'Strasbourg', 14, '["TER Fluo", "Régiolis Transfrontalier"]'::jsonb) +ON CONFLICT ("code") DO UPDATE SET + nom = EXCLUDED.nom, + ville = EXCLUDED.ville, + gare_proche = EXCLUDED.gare_proche, + capacite_max = EXCLUDED.capacite_max, + specialites = EXCLUDED.specialites; + +-- 12.4 Création des comptes Administrateur dans Supabase Auth (auth.users) +-- Mot de passe par défaut : password (crypté en bcrypt bf $2a$10$) +DO $$ +DECLARE + v_admin1_id uuid := 'a0000000-0000-0000-0000-000000000001'::uuid; + v_admin2_id uuid := 'a0000000-0000-0000-0000-000000000002'::uuid; + v_pw_hash text; +BEGIN + -- Hash bcrypt compatible Supabase Auth / GoTrue + v_pw_hash := crypt('password', gen_salt('bf', 10)); + + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'auth' AND table_name = 'users') THEN + + -- Administrateur 1 : admin@ferrovia.fr + IF NOT EXISTS (SELECT 1 FROM auth.users WHERE email = 'admin@ferrovia.fr') THEN + INSERT INTO auth.users ( + instance_id, id, aud, role, email, encrypted_password, + email_confirmed_at, recovery_sent_at, last_sign_in_at, + raw_app_meta_data, raw_user_meta_data, created_at, updated_at, + confirmation_token, email_change, email_change_token_new, recovery_token + ) VALUES ( + '00000000-0000-0000-0000-000000000000', + v_admin1_id, + 'authenticated', + 'authenticated', + 'admin@ferrovia.fr', + v_pw_hash, + NOW(), NOW(), NOW(), + '{"provider":"email","providers":["email"]}'::jsonb, + '{"nom":"Admin","prenom":"Ferrovia","role":"admin","region_admin":true,"permissions":{"admin":true,"super_admin":true}}'::jsonb, + NOW(), NOW(), + '', '', '', '' + ); + + BEGIN + INSERT INTO auth.identities ( + id, user_id, identity_data, provider, last_sign_in_at, created_at, updated_at + ) VALUES ( + v_admin1_id::text, + v_admin1_id, + jsonb_build_object('sub', v_admin1_id::text, 'email', 'admin@ferrovia.fr'), + 'email', + NOW(), NOW(), NOW() + ); + EXCEPTION WHEN OTHERS THEN NULL; + END; + ELSE + UPDATE auth.users + SET encrypted_password = v_pw_hash, + email_confirmed_at = COALESCE(email_confirmed_at, NOW()), + raw_user_meta_data = COALESCE(raw_user_meta_data, '{}'::jsonb) || '{"nom":"Admin","prenom":"Ferrovia","role":"admin","region_admin":true,"permissions":{"admin":true,"super_admin":true}}'::jsonb + WHERE email = 'admin@ferrovia.fr'; + END IF; + + -- Administrateur 2 : admin@mrpatator.fr + IF NOT EXISTS (SELECT 1 FROM auth.users WHERE email = 'admin@mrpatator.fr') THEN + INSERT INTO auth.users ( + instance_id, id, aud, role, email, encrypted_password, + email_confirmed_at, recovery_sent_at, last_sign_in_at, + raw_app_meta_data, raw_user_meta_data, created_at, updated_at, + confirmation_token, email_change, email_change_token_new, recovery_token + ) VALUES ( + '00000000-0000-0000-0000-000000000000', + v_admin2_id, + 'authenticated', + 'authenticated', + 'admin@mrpatator.fr', + v_pw_hash, + NOW(), NOW(), NOW(), + '{"provider":"email","providers":["email"]}'::jsonb, + '{"nom":"Admin","prenom":"MrPatator","role":"admin","region_admin":true,"permissions":{"admin":true,"super_admin":true}}'::jsonb, + NOW(), NOW(), + '', '', '', '' + ); + + BEGIN + INSERT INTO auth.identities ( + id, user_id, identity_data, provider, last_sign_in_at, created_at, updated_at + ) VALUES ( + v_admin2_id::text, + v_admin2_id, + jsonb_build_object('sub', v_admin2_id::text, 'email', 'admin@mrpatator.fr'), + 'email', + NOW(), NOW(), NOW() + ); + EXCEPTION WHEN OTHERS THEN NULL; + END; + ELSE + UPDATE auth.users + SET encrypted_password = v_pw_hash, + email_confirmed_at = COALESCE(email_confirmed_at, NOW()), + raw_user_meta_data = COALESCE(raw_user_meta_data, '{}'::jsonb) || '{"nom":"Admin","prenom":"MrPatator","role":"admin","region_admin":true,"permissions":{"admin":true,"super_admin":true}}'::jsonb + WHERE email = 'admin@mrpatator.fr'; + END IF; + + END IF; +EXCEPTION WHEN OTHERS THEN + RAISE NOTICE 'Auth seeding notice: %', SQLERRM; +END $$; + +-- 12.4 Insertion / Synchronisation dans public.utilisateurs +INSERT INTO "public"."utilisateurs" ( + id, email, nom, prenom, role, actif, region_admin, password_hash, permissions +) VALUES ( + 'a0000000-0000-0000-0000-000000000001'::uuid, + 'admin@ferrovia.fr', + 'Admin', + 'Ferrovia', + 'admin', + true, + true, + crypt('password', gen_salt('bf', 10)), + '{"admin": true, "super_admin": true, "all_regions": true}'::jsonb +) ON CONFLICT (email) DO UPDATE SET + nom = EXCLUDED.nom, + prenom = EXCLUDED.prenom, + role = 'admin', + actif = true, + region_admin = true, + password_hash = EXCLUDED.password_hash, + permissions = EXCLUDED.permissions, + updated_at = NOW(); + +INSERT INTO "public"."utilisateurs" ( + id, email, nom, prenom, role, actif, region_admin, password_hash, permissions +) VALUES ( + 'a0000000-0000-0000-0000-000000000002'::uuid, + 'admin@mrpatator.fr', + 'Admin', + 'MrPatator', + 'admin', + true, + true, + crypt('password', gen_salt('bf', 10)), + '{"admin": true, "super_admin": true, "all_regions": true}'::jsonb +) ON CONFLICT (email) DO UPDATE SET + nom = EXCLUDED.nom, + prenom = EXCLUDED.prenom, + role = 'admin', + actif = true, + region_admin = true, + password_hash = EXCLUDED.password_hash, + permissions = EXCLUDED.permissions, + updated_at = NOW(); + +-- ============================================================================ +-- FIN DU FICHIER SCHEME.sql +-- Base Ferrovia prête : 37 Tables, 3 ENUMs, Triggers Supabase Auth, +-- 4 Buckets Storage, RLS & Rôles anon/authenticated configurés. +-- ============================================================================ diff --git a/package-lock.json b/package-lock.json index 027c99a..b562806 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "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", @@ -3208,735 +3209,25 @@ "yarn": "^1.7.0" } }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/ast": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", - "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/helper-api-error": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", - "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", - "license": "MIT", - "peer": true - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/helper-buffer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", - "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", - "license": "MIT", - "peer": true - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", - "license": "MIT", - "peer": true - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", - "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/ieee754": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", - "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/leb128": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", - "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/utf8": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", - "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", - "license": "MIT", - "peer": true - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/wasm-edit": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", - "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/helper-wasm-section": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-opt": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/wasm-gen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", - "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/wasm-opt": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", - "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/wasm-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", - "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/@webassemblyjs/wast-printer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", - "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "license": "MIT", - "peer": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/enhanced-resolve": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", - "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/enhanced-resolve/node_modules/memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "license": "MIT", - "peer": true, - "dependencies": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - }, - "engines": { - "node": ">=4.3.0 <5.0.0 || >=5.10" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "license": "MIT", - "peer": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "license": "MIT", - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "node_modules/@sncf/bootstrap-sncf.metier": { + "version": "4.3.1-r5", + "resolved": "https://registry.npmjs.org/@sncf/bootstrap-sncf.metier/-/bootstrap-sncf.metier-4.3.1-r5.tgz", + "integrity": "sha512-2TjKXokg0ssXtodqe7tdOQE7HncNGfhuhVSTi+c7zjmP1WuE5GebVaifS3KQ7oSKvkN4eiyzOJXxPUH/X+nWgQ==", "license": "MIT", "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4.3.0 <5.0.0 || >=5.10" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "license": "MIT", - "peer": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "license": "MIT", - "peer": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "license": "MIT", - "peer": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/null-loader": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-1.0.0.tgz", - "integrity": "sha512-mYLDjDVTkjTlFoidxRhzO75rdcwfVXfw5G5zpj8sXnBkHtKJxMk4hTcRR4i5SOhDB6EvcQuYriy6IV23eq6uog==", - "license": "MIT", - "dependencies": { - "loader-utils": "^1.2.3", - "schema-utils": "^1.0.0" - }, - "engines": { - "node": ">= 6.9.0" - }, - "peerDependencies": { - "webpack": "^4.3.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "license": "MIT", - "peer": true, - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT", - "peer": true - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "license": "MIT", - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/terser": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", - "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/terser-webpack-plugin": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.6.tgz", - "integrity": "sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==", - "license": "MIT", - "peer": true, - "dependencies": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - }, - "engines": { - "node": ">= 6.9.0" - }, - "peerDependencies": { - "webpack": "^4.0.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/watchpack": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", - "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" - }, - "optionalDependencies": { - "chokidar": "^3.4.1", - "watchpack-chokidar2": "^2.0.1" - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/webpack": { - "version": "4.47.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz", - "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/wasm-edit": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "acorn": "^6.4.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.5.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.3", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.7.4", - "webpack-sources": "^1.4.1" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - }, - "webpack-command": { - "optional": true - } - } - }, - "node_modules/@sncf/bootstrap-sncf.communication/node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" + "bootstrap": "4.3.1", + "chart.js": "^2.7.2", + "chartjs-plugin-zoom": "^0.6.3", + "flatpickr": "^4.6.2", + "hugo-bin": "^0.44.1", + "ion-rangeslider": "^2.2.0", + "jquery": "^3.3.1", + "lodash": "^4.17.11", + "null-loader": "^1.0.0", + "popper.js": "^1.14.1", + "swiper": "^4.2.2", + "whatwg-fetch": "^3.0.0", + "yarn": "^1.7.0" } }, "node_modules/@supabase/auth-js": { @@ -4493,15 +3784,6 @@ "node": ">= 6" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -4597,16 +3879,6 @@ "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -5117,44 +4389,6 @@ "license": "ISC" }, "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-code-frame": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", - "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "node_modules/@webassemblyjs/helper-code-frame/node_modules/@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", @@ -5166,23 +4400,35 @@ "@webassemblyjs/wast-parser": "1.9.0" } }, - "node_modules/@webassemblyjs/helper-code-frame/node_modules/@webassemblyjs/helper-wasm-bytecode": { + "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", "license": "MIT", "peer": true }, - "node_modules/@webassemblyjs/helper-code-frame/node_modules/@webassemblyjs/wast-printer": { + "node_modules/@webassemblyjs/helper-api-error": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", - "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", "license": "MIT", "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0", - "@xtuc/long": "4.2.2" + "@webassemblyjs/wast-printer": "1.9.0" } }, "node_modules/@webassemblyjs/helper-fsm": { @@ -5202,25 +4448,6 @@ "@webassemblyjs/ast": "1.9.0" } }, - "node_modules/@webassemblyjs/helper-module-context/node_modules/@webassemblyjs/ast": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", - "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0" - } - }, - "node_modules/@webassemblyjs/helper-module-context/node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", - "license": "MIT", - "peer": true - }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", @@ -5232,101 +4459,122 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "node_modules/@webassemblyjs/helper-numbers/node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "license": "MIT" }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "node_modules/@webassemblyjs/helper-numbers/node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "license": "Apache-2.0", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "license": "MIT", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" } }, "node_modules/@webassemblyjs/wast-parser": { @@ -5344,46 +4592,15 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@webassemblyjs/wast-parser/node_modules/@webassemblyjs/ast": { + "node_modules/@webassemblyjs/wast-printer": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", - "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", "license": "MIT", "peer": true, "dependencies": { - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0" - } - }, - "node_modules/@webassemblyjs/wast-parser/node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", - "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/wast-parser/node_modules/@webassemblyjs/helper-api-error": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", - "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/wast-parser/node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", "@xtuc/long": "4.2.2" } }, @@ -5462,18 +4679,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -5561,9 +4766,9 @@ } }, "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -5578,9 +4783,9 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -6022,9 +5227,9 @@ } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", "license": "MIT", "peer": true }, @@ -6489,6 +5694,20 @@ "node": ">=0.10.0" } }, + "node_modules/base/node_modules/is-descriptor": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.4.tgz", + "integrity": "sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.2", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", @@ -6954,9 +6173,9 @@ "license": "MIT" }, "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", "license": "MIT", "peer": true }, @@ -7132,13 +6351,13 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", - "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.6.tgz", + "integrity": "sha512-sd+Q65fjlWCYWtZKXiKfrUc8d+4jtp/8f0W2NkwzLtoW4bI6UDnWusLWIurHnmurW0XShIRxpwiOX4EoPtXUAg==", "license": "ISC", "peer": true, "dependencies": { - "bn.js": "^5.2.2", + "bn.js": "^5.2.3", "browserify-rsa": "^4.1.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", @@ -7873,33 +7092,6 @@ "node": ">=0.10.0" } }, - "node_modules/class-utils/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -8084,9 +7276,9 @@ "license": "MIT" }, "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", "license": "MIT" }, "node_modules/colorette": { @@ -8108,12 +7300,12 @@ } }, "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "license": "MIT", "engines": { - "node": ">= 12" + "node": ">= 10" } }, "node_modules/common-tags": { @@ -8449,9 +7641,9 @@ } }, "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", "license": "MIT", "peer": true }, @@ -8592,88 +7784,6 @@ "utrie": "^1.0.2" } }, - "node_modules/css-loader": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", - "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-minimizer-webpack-plugin": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", - "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", - "license": "MIT", - "dependencies": { - "cssnano": "^5.0.6", - "jest-worker": "^27.0.2", - "postcss": "^8.3.5", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@parcel/css": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/css-prefers-color-scheme": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", @@ -9293,14 +8403,13 @@ } }, "node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "license": "MIT", "peer": true, "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" + "is-descriptor": "^0.1.0" }, "engines": { "node": ">=0.10.0" @@ -9429,9 +8538,9 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", "license": "MIT", "peer": true }, @@ -9821,9 +8930,9 @@ } }, "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", "license": "MIT", "peer": true }, @@ -9873,16 +8982,81 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", - "license": "MIT", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "peer": true, "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" }, "engines": { - "node": ">=10.13.0" + "node": ">=6.9.0" + } + }, + "node_modules/enhanced-resolve/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT", + "peer": true + }, + "node_modules/enhanced-resolve/node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "license": "MIT", + "peer": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/enhanced-resolve/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/enhanced-resolve/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "peer": true + }, + "node_modules/enhanced-resolve/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/enhanced-resolve/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" } }, "node_modules/entities": { @@ -10045,9 +9219,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "license": "MIT" }, "node_modules/es-object-atoms": { @@ -10554,59 +9728,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", - "license": "MIT", - "dependencies": { - "@types/eslint": "^7.29.0 || ^8.4.1", - "jest-worker": "^28.0.2", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0", - "webpack": "^5.0.0" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/jest-worker": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", - "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/eslint/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -10871,56 +9992,6 @@ "ms": "2.0.0" } }, - "node_modules/expand-brackets/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/expand-brackets/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/expand-brackets/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -11030,14 +10101,13 @@ } }, "node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "license": "MIT", "peer": true, "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "is-extendable": "^0.1.0" }, "engines": { "node": ">=0.10.0" @@ -11076,27 +10146,18 @@ "node": ">=0.10.0" } }, - "node_modules/extglob/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "node_modules/extglob/node_modules/is-descriptor": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.4.tgz", + "integrity": "sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww==", "license": "MIT", "peer": true, "dependencies": { - "is-extendable": "^0.1.0" + "is-accessor-descriptor": "^1.0.2", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/fast-deep-equal": { @@ -12086,12 +11147,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, "node_modules/global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", @@ -12477,9 +11532,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -12621,36 +11676,13 @@ "node": ">=12" } }, - "node_modules/html-webpack-plugin": { - "version": "5.6.6", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.6.tgz", - "integrity": "sha512-bLjW01UTrvoWTJQL5LsMRo1SypHW80FTm12OJRSnr3v6YHNhfe+1r0MYUZJMACxnCHURVnBWRwAsWs2yPU9Ezw==", + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "license": "MIT", - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.20.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } + "node": ">= 12" } }, "node_modules/html2canvas": { @@ -13087,16 +12119,16 @@ } }, "node_modules/is-accessor-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", - "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.2.tgz", + "integrity": "sha512-AIbwAcazqP3R65dGvqk1V+a+vE5Fg1yu/ZKMOiBWSUIXXiwQkYmXQcVa2O0nh0tSDKDFKxG2mY7dB1Sr4hEP1g==", "license": "MIT", "peer": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.3" }, "engines": { - "node": ">= 0.10" + "node": ">= 0.4" } }, "node_modules/is-array-buffer": { @@ -13265,9 +12297,9 @@ } }, "node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.8.tgz", + "integrity": "sha512-SceYGWXvdqlWa/OnQ5FQuV+NxvNmMRhMw/w9AHkH71hTzveND4BTYgvp16g+oITK47qbOl/3D0bl0iygehWAWQ==", "license": "MIT", "peer": true, "dependencies": { @@ -13294,14 +12326,11 @@ } }, "node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "license": "MIT", "peer": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, "engines": { "node": ">=0.10.0" } @@ -15208,16 +14237,13 @@ } }, "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", "license": "MIT", + "peer": true, "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=4.3.0 <5.0.0 || >=5.10" } }, "node_modules/loader-utils": { @@ -15561,9 +14587,9 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", "license": "MIT", "peer": true }, @@ -15627,26 +14653,6 @@ "node": ">=4" } }, - "node_modules/mini-css-extract-plugin": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.0.tgz", - "integrity": "sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==", - "license": "MIT", - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -15717,6 +14723,19 @@ "node": ">=0.10.0" } }, + "node_modules/mixin-deep/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -15799,9 +14818,9 @@ } }, "node_modules/nan": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", - "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.29.0.tgz", + "integrity": "sha512-GlGk3HIvitbvs+LT3g6XUP1kpirKNvmDFwF/bmo6XNWSb/eYEs/O4bfgIEIXCZ+lIOTS5xNwDvSGMw6FJdAhtA==", "license": "MIT", "optional": true, "peer": true @@ -15847,6 +14866,61 @@ "node": ">=0.10.0" } }, + "node_modules/nanomatch/node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/is-descriptor": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.4.tgz", + "integrity": "sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.2", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/nanomatch/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -16069,6 +15143,62 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/null-loader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-1.0.0.tgz", + "integrity": "sha512-mYLDjDVTkjTlFoidxRhzO75rdcwfVXfw5G5zpj8sXnBkHtKJxMk4hTcRR4i5SOhDB6EvcQuYriy6IV23eq6uog==", + "license": "MIT", + "dependencies": { + "loader-utils": "^1.2.3", + "schema-utils": "^1.0.0" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "webpack": "^4.3.0" + } + }, + "node_modules/null-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/null-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/null-loader/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "license": "MIT", + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, "node_modules/nwsapi": { "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", @@ -16099,33 +15229,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/object-copy/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -16683,6 +15786,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "optional": true, "peer": true @@ -16736,9 +15840,9 @@ } }, "node_modules/pbkdf2": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", - "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.7.tgz", + "integrity": "sha512-nS4mvFgVwUrecPTRdpdseM2fwpdfnX0/HibA8DgBRQtihUAzHQYJFU2l+OlEtiXtpqkrMvCPDwyXMUnASDt6Qw==", "license": "MIT", "peer": true, "dependencies": { @@ -16747,7 +15851,7 @@ "ripemd160": "^2.0.3", "safe-buffer": "^5.2.1", "sha.js": "^2.4.12", - "to-buffer": "^1.2.1" + "to-buffer": "^1.2.2" }, "engines": { "node": ">= 0.10" @@ -17473,28 +16577,6 @@ "postcss": "^8.2" } }, - "node_modules/postcss-loader": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", - "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", - "license": "MIT", - "dependencies": { - "cosmiconfig": "^7.0.0", - "klona": "^2.0.5", - "semver": "^7.3.5" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, "node_modules/postcss-logical": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", @@ -17647,9 +16729,9 @@ } }, "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -17675,9 +16757,9 @@ } }, "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -18158,15 +17240,6 @@ "postcss": "^8.2.15" } }, - "node_modules/postcss-svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/postcss-svgo/node_modules/css-tree": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", @@ -18186,6 +17259,15 @@ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "license": "CC0-1.0" }, + "node_modules/postcss-svgo/node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/postcss-svgo/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -18196,17 +17278,17 @@ } }, "node_modules/postcss-svgo/node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.4.tgz", + "integrity": "sha512-2GJ4h3rl13qYTdwllaK6QlL8tG+UrM8626V2Ylcd/yBUv2Y/EwLsYisVV6UDCRmlk+C74G8nMpxJCk0RWPdDCw==", "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", + "sax": "^1.5.0", "stable": "^0.1.8" }, "bin": { @@ -18434,9 +17516,9 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", "license": "MIT", "peer": true }, @@ -18859,6 +17941,634 @@ } } }, + "node_modules/react-scripts/node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/react-scripts/node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/react-scripts/node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/react-scripts/node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/react-scripts/node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/react-scripts/node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/react-scripts/node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/react-scripts/node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/react-scripts/node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/react-scripts/node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/react-scripts/node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/react-scripts/node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/react-scripts/node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/react-scripts/node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/css-minimizer-webpack-plugin": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", + "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", + "license": "MIT", + "dependencies": { + "cssnano": "^5.0.6", + "jest-worker": "^27.0.2", + "postcss": "^8.3.5", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/react-scripts/node_modules/eslint-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", + "license": "MIT", + "dependencies": { + "@types/eslint": "^7.29.0 || ^8.4.1", + "jest-worker": "^28.0.2", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/react-scripts/node_modules/eslint-webpack-plugin/node_modules/jest-worker": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", + "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/react-scripts/node_modules/html-webpack-plugin": { + "version": "5.6.8", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.8.tgz", + "integrity": "sha512-MZmKQcTnhEh1SPSyMiEytIeDZDUoBZVorNHivQGXMASHf/BSGGOrKa2xQ5bGx3TCe1n109ecCt+cpww7wwWhKA==", + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x || 2.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/react-scripts/node_modules/mini-css-extract-plugin": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", + "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==", + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/react-scripts/node_modules/minimizer-webpack-plugin": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.13.0.tgz", + "integrity": "sha512-OO1NF2zHj6sf4CNv2AsUWYcS8WEwkVKQDXxxKzlOxx1Xl+WlqYtVC+oNlGNwGkLAOnpj277BSHypWwRtd9w4JQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.3", + "terser": "^5.51.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@napi-rs/image": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "imagemin": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "sharp": { + "optional": true + }, + "svgo": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/react-scripts/node_modules/sass-loader": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", + "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "license": "MIT", + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-scripts/node_modules/source-map-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", + "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/react-scripts/node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/react-scripts/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/react-scripts/node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/react-scripts/node_modules/webpack": { + "version": "5.111.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.111.1.tgz", + "integrity": "sha512-cNypaz0RP+S4cvQVi1M/3bRUkvNP0xZpL0TjFx+c6jg64VbxD+2weWDgY9UnjRI6dhZgtaj8DU76GGFcJ79xPQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.25.0", + "es-module-lexer": "^2.1.0", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.7.0", + "schema-utils": "^4.5.0", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/webpack-sources": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.6.0.tgz", + "integrity": "sha512-EIMmPVvNI0CYXyRPp34F2Qk7W37/BZVZIofS6LAUnrNjCVahS+hO4mxwFJDjQjODTGZDtVzLYoN2+1dYkhk9qA==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -18979,6 +18689,33 @@ "node": ">=0.10.0" } }, + "node_modules/regex-not/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-not/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/regex-parser": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", @@ -19560,44 +19297,6 @@ "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==", "license": "CC0-1.0" }, - "node_modules/sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", - "license": "MIT", - "dependencies": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - } - } - }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -19623,14 +19322,14 @@ "license": "MIT" }, "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.5.0.tgz", + "integrity": "sha512-zJlMCZ0cAR5p/Y4oVpRoqioDMJcGxaXRrQ/4rP4WyR84vc5z/DolXdbvXeDpTwbtocDFr2rhPqHPErDCUtz2kA==", "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", "ajv-keywords": "^5.1.0" }, "engines": { @@ -19642,9 +19341,9 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -19951,29 +19650,6 @@ "node": ">=0.10.0" } }, - "node_modules/set-value/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -20392,6 +20068,20 @@ "node": ">=0.10.0" } }, + "node_modules/snapdragon-node/node_modules/is-descriptor": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.4.tgz", + "integrity": "sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.2", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/snapdragon-util": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", @@ -20428,56 +20118,6 @@ "ms": "2.0.0" } }, - "node_modules/snapdragon/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/snapdragon/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/snapdragon/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -20563,27 +20203,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-loader": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", - "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", - "license": "MIT", - "dependencies": { - "abab": "^2.0.5", - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, "node_modules/source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", @@ -20676,6 +20295,33 @@ "node": ">=0.10.0" } }, + "node_modules/split-string/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -20868,33 +20514,6 @@ "node": ">=0.10.0" } }, - "node_modules/static-extend/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -21320,22 +20939,6 @@ "node": ">=0.8.0" } }, - "node_modules/style-loader": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", - "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -21730,9 +21333,9 @@ } }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "license": "MIT", "engines": { "node": ">=6" @@ -21852,9 +21455,9 @@ } }, "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -21870,37 +21473,209 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.6.tgz", + "integrity": "sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==", "license": "MIT", + "peer": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">= 6.9.0" }, "peerDependencies": { - "webpack": "^5.1.0" + "webpack": "^4.0.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "peer": true + }, + "node_modules/terser-webpack-plugin/node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/terser-webpack-plugin/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/terser-webpack-plugin/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "license": "MIT", + "peer": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/terser-webpack-plugin/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/terser/node_modules/commander": { @@ -22186,6 +21961,61 @@ "node": ">=8.0" } }, + "node_modules/to-regex/node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex/node_modules/is-descriptor": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.4.tgz", + "integrity": "sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.2", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -22589,16 +22419,6 @@ "node": ">=0.10.0" } }, - "node_modules/union-value/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -22973,16 +22793,18 @@ } }, "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", + "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", "license": "MIT", + "peer": true, "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" }, - "engines": { - "node": ">=10.13.0" + "optionalDependencies": { + "chokidar": "^3.4.1", + "watchpack-chokidar2": "^2.0.1" } }, "node_modules/watchpack-chokidar2": { @@ -23056,20 +22878,6 @@ "node": ">=0.10.0" } }, - "node_modules/watchpack-chokidar2/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/watchpack-chokidar2/node_modules/chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", @@ -23094,6 +22902,21 @@ "fsevents": "^1.2.7" } }, + "node_modules/watchpack-chokidar2/node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/watchpack-chokidar2/node_modules/fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -23111,20 +22934,6 @@ "node": ">=0.10.0" } }, - "node_modules/watchpack-chokidar2/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/watchpack-chokidar2/node_modules/fsevents": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", @@ -23185,13 +22994,31 @@ "node": ">=0.10.0" } }, - "node_modules/watchpack-chokidar2/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "node_modules/watchpack-chokidar2/node_modules/is-descriptor": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.4.tgz", + "integrity": "sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww==", "license": "MIT", "optional": true, "peer": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.2", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/watchpack-chokidar2/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, "engines": { "node": ">=0.10.0" } @@ -23258,6 +23085,21 @@ "node": ">=0.10.0" } }, + "node_modules/watchpack-chokidar2/node_modules/micromatch/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/watchpack-chokidar2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -23377,42 +23219,41 @@ } }, "node_modules/webpack": { - "version": "5.104.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", - "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", + "version": "4.47.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz", + "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", "license": "MIT", + "peer": true, "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.4", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" + "enhanced-resolve": "^4.5.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.7.4", + "webpack-sources": "^1.4.1" }, "bin": { "webpack": "bin/webpack.js" }, "engines": { - "node": ">=10.13.0" + "node": ">=6.11.5" }, "funding": { "type": "opencollective", @@ -23421,6 +23262,9 @@ "peerDependenciesMeta": { "webpack-cli": { "optional": true + }, + "webpack-command": { + "optional": true } } }, @@ -23566,25 +23410,85 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", "license": "MIT", + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/webpack-sources/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { - "node": ">=10.13.0" + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "license": "MIT", + "peer": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { - "esrecurse": "^4.3.0", + "esrecurse": "^4.1.0", "estraverse": "^4.1.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=4.0.0" } }, "node_modules/webpack/node_modules/estraverse": { @@ -23592,10 +23496,186 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } }, + "node_modules/webpack/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/is-descriptor": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.4.tgz", + "integrity": "sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.2", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/webpack/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "license": "MIT", + "peer": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/webpack/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "license": "MIT", + "peer": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/webpack/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "license": "MIT", + "peer": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/micromatch/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/webpack/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -24089,25 +24169,6 @@ "webpack": "^4.4.0 || ^5.9.0" } }, - "node_modules/workbox-webpack-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "license": "MIT", - "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, "node_modules/workbox-window": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz", diff --git a/package.json b/package.json index c577c63..1c74b27 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.css b/src/App.css index 8f4339f..fb5dc23 100644 --- a/src/App.css +++ b/src/App.css @@ -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"; diff --git a/src/App.js b/src/App.js index 684f971..06a8222 100644 --- a/src/App.js +++ b/src/App.js @@ -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(); } }; diff --git a/src/api/depots.js b/src/api/depots.js new file mode 100644 index 0000000..5131e3e --- /dev/null +++ b/src/api/depots.js @@ -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 }; + } +} diff --git a/src/api/horaires.js b/src/api/horaires.js index 9248139..2e5c854 100644 --- a/src/api/horaires.js +++ b/src/api/horaires.js @@ -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 }; + } +} diff --git a/src/api/perturbations.js b/src/api/perturbations.js index c880ad9..1321c66 100644 --- a/src/api/perturbations.js +++ b/src/api/perturbations.js @@ -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 }; + 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([{ - ...rest, - perturbations_arrets: perturbationsArretsData, - created_at: new Date(), - updated_at: new Date() - }]) + .insert([cleanInsert]) .select('*') .single(); @@ -178,19 +207,22 @@ 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 => ({ - perturbation_id: perturbation.id, - ...modif - })); + if (modifs && modifs.length > 0 && perturbation) { + try { + const modificationsData = modifs.map(modif => ({ + perturbation_id: perturbation.id, + ...modif + })); - const { error: modifError } = await supabase - .from('perturbations_modifications_parcours') - .insert(modificationsData); + const { error: modifError } = await supabase + .from('perturbations_modifications_parcours') + .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 + if (modifError) { + 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 }; + 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({ - ...rest, - perturbations_arrets: perturbationsArretsData, - updated_at: new Date() - }) + .update(cleanUpdate) .eq('id', id) .select('*') .single(); @@ -238,27 +297,29 @@ 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 - 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 => ({ - perturbation_id: id, - ...modif - })); - - const { error: modifError } = await supabase + if (modifs !== null && Array.isArray(modifs)) { + try { + await supabase .from('perturbations_modifications_parcours') - .insert(modificationsData); + .delete() + .eq('perturbation_id', id); - if (modifError) { - console.error('Erreur lors de la mise à jour des modifications de parcours:', modifError); + if (modifs.length > 0) { + const modificationsData = modifs.map(modif => ({ + perturbation_id: id, + ...modif + })); + + const { error: modifError } = await supabase + .from('perturbations_modifications_parcours') + .insert(modificationsData); + + if (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); } } diff --git a/src/pages/attribution-materiel-roulant/AttributionMaterielRoulant.css b/src/pages/attribution-materiel-roulant/AttributionMaterielRoulant.css index c22e8a6..42a205f 100644 --- a/src/pages/attribution-materiel-roulant/AttributionMaterielRoulant.css +++ b/src/pages/attribution-materiel-roulant/AttributionMaterielRoulant.css @@ -1,453 +1,2106 @@ -.attribution-mr-page { - min-height: calc(100vh - 64px); -} +/* ========================================================================== + ATTRIBUTION MATÉRIEL ROULANT & GESTION DES ROULEMENTS - SNCF MÉTIER PLEIN ÉCRAN + ========================================================================== */ -.attribution-mr-content { +.attribution-fullscreen-page { + min-height: 100vh; + background-color: #f4f6f9; display: flex; flex-direction: column; + padding: 24px 32px 0; + width: 100%; + box-sizing: border-box; +} + +/* 1. EN-TÊTE DE PAGE */ +.attribution-page-header { + display: flex; + justify-content: space-between; + align-items: flex-start; gap: 16px; + flex-wrap: wrap; + margin-bottom: 20px; } -.attribution-mr-header h2 { - margin: 0; +.header-meta { + display: flex; + flex-direction: column; + gap: 4px; } -.attribution-mr-kicker { - margin: 0; - color: #0088ce; - font-size: 12px; +.header-kicker { + font-size: 11px; + font-weight: 800; text-transform: uppercase; - letter-spacing: 0.06em; - font-weight: 700; + letter-spacing: 0.08em; + color: #0088ce; } -.attribution-mr-description { - margin: 6px 0 0; - color: #6b7280; +.header-meta h2 { + margin: 0; + font-size: 26px; + font-weight: 800; + color: #002244; + letter-spacing: -0.02em; } -.attribution-mr-toolbar { +.header-subtitle { + margin: 0; + font-size: 14px; + color: #64748b; + max-width: 850px; +} + +.header-actions { display: flex; align-items: center; gap: 10px; - background: #fff; - border: 1px solid #e5e7eb; - border-radius: 10px; - padding: 12px; + flex-wrap: wrap; } -.attribution-mr-direction-filters { +.auto-config-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + border-radius: 6px; + border: 1px solid #cbd5e1; + background: #ffffff; + color: #334155; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; +} + +.auto-config-btn:hover { + background: #f1f5f9; + border-color: #94a3b8; +} + +.auto-config-btn.is-scheduled { + background: #ecfdf5; + border-color: #10b981; + color: #047857; +} + +.last-refresh-tag { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: 999px; + background: #ffffff; + border: 1px solid #e2e8f0; + font-size: 12px; + color: #64748b; + font-weight: 500; +} + +/* TOAST DE NOTIFICATION */ +.attribution-toast { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 18px; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + margin-bottom: 16px; + animation: slideDown 0.3s ease; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +@keyframes slideDown { + from { opacity: 0; transform: translateY(-8px); } + to { opacity: 1; transform: translateY(0); } +} + +.toast-success { + background: #ecfdf5; + color: #065f46; + border: 1px solid #a7f3d0; +} + +.toast-error { + background: #fef2f2; + color: #991b1b; + border: 1px solid #fecaca; +} + +/* 2. BANDEAU DE KPIS DU PARC */ +.attribution-kpis-strip { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 16px; + margin-bottom: 18px; +} + +.kpi-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 16px; + display: flex; + align-items: center; + gap: 16px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.03); + transition: transform 0.15s, box-shadow 0.15s; +} + +.kpi-card:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06); +} + +.kpi-icon-wrap { + width: 48px; + height: 48px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.icon-blue { background: #e0f2fe; color: #0284c7; } +.icon-green { background: #dcfce7; color: #16a34a; } +.icon-amber { background: #fef3c7; color: #d97706; } +.icon-red { background: #fee2e2; color: #dc2626; } +.icon-teal { background: #ccfbf1; color: #0d9488; } + +.kpi-body { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1; +} + +.kpi-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #64748b; +} + +.kpi-num { + font-size: 24px; + font-weight: 800; + color: #0f172a; + line-height: 1.2; + margin-top: 2px; +} + +.kpi-subtext { + font-size: 11px; + color: #64748b; + margin-top: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.kpi-progress { + height: 6px; + background: #e2e8f0; + border-radius: 999px; + margin-top: 6px; + overflow: hidden; +} + +.kpi-progress-bar { + height: 100%; + background: linear-gradient(90deg, #10b981 0%, #059669 100%); + border-radius: 999px; + transition: width 0.4s ease; +} + +/* 3. WCS TABS NAVIGATION WRAPPER */ +.attribution-tabs-nav-wrapper { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 6px 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + margin-bottom: 18px; +} + +/* 4. BARRE D'OUTILS ET FILTRES D'ATTRIBUTION */ +.attribution-toolbar-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 14px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03); + margin-bottom: 18px; +} + +.toolbar-top-row { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; +} + +.toolbar-control { + display: flex; + align-items: center; + gap: 8px; +} + +.line-select-wrap label { + font-size: 12px; + font-weight: 700; + color: #475569; + text-transform: uppercase; + letter-spacing: 0.04em; + white-space: nowrap; +} + +.custom-select-container select { + padding: 8px 14px; + border-radius: 8px; + border: 1px solid #cbd5e1; + background: #ffffff; + color: #0f172a; + font-size: 13px; + font-weight: 600; + min-width: 260px; + outline: none; + cursor: pointer; + transition: border-color 0.15s; +} + +.custom-select-container select:focus { + border-color: #0088ce; + box-shadow: 0 0 0 3px rgba(0, 136, 206, 0.15); +} + +.direction-pills { + display: flex; + gap: 6px; +} + +.direction-pill { + padding: 6px 12px; + border-radius: 999px; + border: 1px solid #cbd5e1; + background: #f8fafc; + color: #334155; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; +} + +.direction-pill:hover { + background: #f1f5f9; +} + +.direction-pill.active { + background: #0088ce; + border-color: #0088ce; + color: #ffffff; +} + +.search-horaire-wrap { + position: relative; + flex: 1; + min-width: 240px; +} + +.search-horaire-wrap wcs-mat-icon { + position: absolute; + left: 10px; + color: #94a3b8; + pointer-events: none; +} + +.search-horaire-wrap input { + width: 100%; + padding: 8px 32px 8px 34px; + border: 1px solid #cbd5e1; + border-radius: 8px; + font-size: 13px; + background: #f8fafc; + outline: none; + transition: all 0.2s; +} + +.search-horaire-wrap input:focus { + background: #ffffff; + border-color: #0088ce; + box-shadow: 0 0 0 3px rgba(0, 136, 206, 0.15); +} + +.clear-search-btn { + position: absolute; + right: 8px; + background: none; + border: none; + color: #94a3b8; + font-size: 16px; + cursor: pointer; +} + +.attribution-view-toggle { + display: flex; + background: #f1f5f9; + border-radius: 8px; + padding: 3px; + margin-left: auto; +} + +.toggle-btn { + display: inline-flex; + align-items: center; + gap: 6px; + border: none; + background: transparent; + color: #64748b; + padding: 6px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; +} + +.toggle-btn.active { + background: #ffffff; + color: #0f172a; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.toolbar-bottom-row { + display: flex; + justify-content: space-between; + align-items: center; + border-top: 1px solid #f1f5f9; + padding-top: 12px; + flex-wrap: wrap; + gap: 10px; +} + +.assignment-status-filters { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.filters-label { + font-size: 12px; + font-weight: 600; + color: #64748b; + margin-right: 4px; +} + +.status-filter-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + border-radius: 999px; + border: 1px solid #e2e8f0; + background: #f8fafc; + color: #334155; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; +} + +.status-filter-btn:hover { + background: #f1f5f9; +} + +.status-filter-btn.active { + background: #002244; + color: #ffffff; + border-color: #002244; +} + +.status-filter-btn.btn-warning.active { + background: #d97706; + border-color: #d97706; +} + +.status-filter-btn.btn-success.active { + background: #059669; + border-color: #059669; +} + +.status-filter-btn.btn-alt.active { + background: #7c3aed; + border-color: #7c3aed; +} + +.status-filter-btn.btn-danger.active { + background: #dc2626; + border-color: #dc2626; +} + +.btn-count { + background: rgba(0, 0, 0, 0.08); + padding: 1px 6px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; +} + +.status-filter-btn.active .btn-count { + background: rgba(255, 255, 255, 0.25); + color: #ffffff; +} + +.results-counter { + font-size: 12px; + color: #64748b; +} + +/* ÉTAT VIDE / CHARGEMENT */ +.attribution-empty-box { + min-height: 280px; + background: #ffffff; + border: 1px dashed #cbd5e1; + border-radius: 12px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 40px; + color: #64748b; + gap: 12px; +} + +.attribution-empty-box h4 { + margin: 0; + font-size: 16px; + font-weight: 700; + color: #0f172a; +} + +.attribution-empty-box p { + margin: 0; + font-size: 13px; + max-width: 440px; +} + +/* 5. SOUS-VUE A : GRILLE DES CARTES DE CIRCULATION */ +.horaires-attribution-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); + gap: 18px; +} + +.horaire-attribution-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 18px; + display: flex; + flex-direction: column; + gap: 14px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); + transition: transform 0.2s, box-shadow 0.2s; + position: relative; +} + +.horaire-attribution-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.06); +} + +.horaire-attribution-card.is-unassigned { + border-left: 4px solid #f59e0b; +} + +.horaire-attribution-card.has-anomaly { + border-left: 4px solid #ef4444; +} + +.horaire-card-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.train-id-badge { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.train-number { + font-size: 16px; + font-weight: 800; + color: #002244; +} + +.train-type-tag { + background: #e0f2fe; + color: #0284c7; + padding: 2px 7px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; +} + +.alternative-variant-badge { + background: #f5f3ff; + color: #6d28d9; + border: 1px solid #ddd6fe; + padding: 2px 6px; + border-radius: 4px; + font-size: 10px; + font-weight: 700; +} + +.mini-alt-tag { + color: #6d28d9; + font-size: 10px; +} + +.train-line-pill { + display: inline-flex; + align-items: center; + gap: 4px; + background: #f1f5f9; + color: #475569; + padding: 3px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; +} + +.horaire-route-box { + display: flex; + align-items: center; + justify-content: space-between; + background: #f8fafc; + border: 1px solid #edf2f7; + border-radius: 8px; + padding: 12px 16px; +} + +.route-step { + display: flex; + flex-direction: column; +} + +.step-destination { + text-align: right; +} + +.step-time { + font-size: 16px; + font-weight: 800; + color: #0f172a; +} + +.step-station { + font-size: 12px; + color: #475569; + margin-top: 2px; +} + +.route-divider { + display: flex; + align-items: center; + justify-content: center; +} + +.route-arrow { + color: #94a3b8; + font-size: 18px; + font-weight: 700; +} + +/* COUPLAGE BANNER */ +.horaire-coupling-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 6px; + background: #f5f3ff; + border: 1px solid #ddd6fe; + color: #5b21b6; + font-size: 12px; + font-weight: 600; +} + +.horaire-anomalies-box { + display: flex; + flex-direction: column; + gap: 6px; +} + +.anomaly-row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; +} + +.horaire-composition-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.composition-section-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.comp-title { + font-size: 12px; + font-weight: 700; + color: #475569; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.comp-type-badge { + background: #ecfdf5; + color: #047857; + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; +} + +.comp-missing-badge { + background: #fef3c7; + color: #b45309; + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; +} + +.composition-placeholder { + border: 1px dashed #cbd5e1; + border-radius: 8px; + padding: 14px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + color: #94a3b8; + font-size: 12px; + font-weight: 500; + cursor: pointer; + background: #fafafa; + transition: all 0.15s; +} + +.composition-placeholder:hover { + background: #f0f9ff; + border-color: #0088ce; + color: #0088ce; +} + +.composition-units-diagram { + display: flex; + align-items: center; + gap: 8px; + overflow-x: auto; + padding-bottom: 4px; +} + +.diagram-engine { + width: 32px; + height: 38px; + background: #002244; + color: #ffffff; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.diagram-unit-card { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + background: #f1f5f9; + border: 1px solid #cbd5e1; + border-radius: 6px; + min-width: 140px; + flex: 1; +} + +.unit-card-pos { + font-size: 10px; + font-weight: 800; + color: #64748b; + background: #ffffff; + padding: 1px 5px; + border-radius: 4px; +} + +.unit-card-details { + display: flex; + flex-direction: column; + min-width: 0; +} + +.unit-name { + font-size: 12px; + font-weight: 700; + color: #0f172a; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.unit-tech { + font-size: 10px; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.composition-meta-footer { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 11px; + color: #64748b; + padding-top: 4px; +} + +.meta-cap { + display: inline-flex; + align-items: center; + gap: 4px; + color: #0f172a; +} + +.horaire-card-actions { + display: flex; + align-items: center; + gap: 8px; + margin-top: auto; + padding-top: 10px; + border-top: 1px solid #f1f5f9; + flex-wrap: wrap; +} + +.horaire-card-actions wcs-button { + flex: 1; + min-width: 120px; +} + +/* 6. SOUS-VUE B : FRISE 24H DES SILLONS */ +.horaires-timeline-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); +} + +.timeline-scroll-container { + max-height: 68vh; + overflow: auto; +} + +.horaires-gantt-table { + --gantt-label-width: 220px; + --gantt-cell-width: 80px; + --gantt-cells-count: 13; + min-width: calc(var(--gantt-label-width) + (var(--gantt-cell-width) * var(--gantt-cells-count))); + width: 100%; +} + +.horaires-gantt-header { + display: grid; + grid-template-columns: var(--gantt-label-width) 1fr; + position: sticky; + top: 0; + z-index: 5; + background: #f1f5f9; + border-bottom: 1px solid #cbd5e1; +} + +.gantt-train-label-head { + padding: 10px 14px; + font-weight: 700; + font-size: 12px; + text-transform: uppercase; + color: #334155; + letter-spacing: 0.05em; + background: #f1f5f9; + position: sticky; + left: 0; + z-index: 6; + border-right: 1px solid #cbd5e1; +} + +.gantt-scale-hours { + display: grid; + grid-template-columns: repeat(var(--gantt-cells-count), 1fr); +} + +.gantt-scale-tick { + padding: 10px 4px; + font-size: 12px; + font-weight: 600; + color: #475569; + text-align: center; + border-left: 1px dashed #cbd5e1; +} + +.horaires-gantt-row { + display: grid; + grid-template-columns: var(--gantt-label-width) 1fr; + min-height: 54px; + border-bottom: 1px solid #e2e8f0; + background: #ffffff; + transition: background 0.15s; +} + +.horaires-gantt-row:hover { + background: #fafcff; +} + +.gantt-train-label-cell { + position: sticky; + left: 0; + z-index: 4; + background: inherit; + border-right: 1px solid #e2e8f0; + padding: 8px 12px; + display: flex; + flex-direction: column; + justify-content: center; +} + +.cell-num { + font-size: 13px; + font-weight: 800; + color: #002244; +} + +.cell-hours { + font-size: 11px; + color: #64748b; + margin-top: 2px; +} + +.gantt-train-track-cell { + position: relative; + min-height: 54px; + background-image: repeating-linear-gradient(to right, transparent 0, transparent calc(100% / var(--gantt-cells-count) - 1px), #f1f5f9 calc(100% / var(--gantt-cells-count) - 1px), #f1f5f9 calc(100% / var(--gantt-cells-count))); +} + +.sillon-timeline-bar { + position: absolute; + top: 50%; + transform: translateY(-50%); + height: 36px; + border: none; + border-radius: 6px; + color: #ffffff; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 10px; + cursor: pointer; + z-index: 2; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + transition: transform 0.15s; + overflow: hidden; +} + +.sillon-timeline-bar:hover { + transform: translateY(-50%) scale(1.02); + z-index: 3; +} + +.sillon-timeline-bar.bar-nominal { + background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%); +} + +.sillon-timeline-bar.bar-unassigned { + background: linear-gradient(135deg, #d97706 0%, #b45309 100%); +} + +.sillon-timeline-bar.bar-conflict { + background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); +} + +.sillon-label { + font-size: 11px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sillon-comp-badge { + font-size: 10px; + background: rgba(0, 0, 0, 0.2); + padding: 1px 6px; + border-radius: 4px; + margin-left: 6px; + white-space: nowrap; +} + +/* 7. MODALE DE COMPOSITION */ +.attribution-builder-modal { + max-width: 1080px; + width: 95%; +} + +.modal-header-meta { + display: flex; + flex-direction: column; + gap: 2px; +} + +.modal-kicker { + font-size: 10px; + font-weight: 800; + color: #0088ce; + letter-spacing: 0.08em; +} + +.modal-header-meta h3 { + margin: 0; + font-size: 20px; + font-weight: 800; + color: #002244; +} + +.modal-route-info { + font-size: 12px; + color: #475569; +} + +.modal-tabs-header { + display: flex; + gap: 8px; + margin-left: auto; + margin-right: 16px; +} + +.modal-tab-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + border-radius: 6px; + border: 1px solid #cbd5e1; + background: #f8fafc; + color: #334155; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; +} + +.modal-tab-btn.active { + background: #0088ce; + border-color: #0088ce; + color: #ffffff; +} + +.builder-modal-body { + display: flex; + flex-direction: column; + gap: 16px; + max-height: 72vh; + overflow-y: auto; + padding: 18px 24px; +} + +.builder-alert-banner { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 16px; + border-radius: 8px; + background: #fef2f2; + border: 1px solid #fecaca; + color: #991b1b; + font-size: 12px; +} + +.builder-alert-banner ul { + margin: 4px 0 0; + padding-left: 18px; +} + +.builder-columns-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +.builder-draft-panel, +.builder-catalog-panel { + display: flex; + flex-direction: column; + gap: 12px; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 10px; + padding: 16px; +} + +.panel-section-head { + display: flex; + justify-content: space-between; + align-items: center; +} + +.panel-section-head h4 { + margin: 0; + font-size: 14px; + font-weight: 800; + color: #0f172a; +} + +.panel-sub { + font-size: 11px; + color: #64748b; +} + +.draft-capacity-tag { + display: inline-flex; + align-items: center; + gap: 4px; + background: #ecfdf5; + color: #047857; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; +} + +.draft-empty-state { + text-align: center; + padding: 40px 20px; + border: 1px dashed #cbd5e1; + border-radius: 8px; + color: #64748b; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.draft-items-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.draft-item-card { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); +} + +.draft-item-position { + display: flex; + flex-direction: column; + align-items: center; + min-width: 32px; +} + +.pos-badge { + font-size: 12px; + font-weight: 800; + color: #002244; +} + +.pos-label { + font-size: 9px; + color: #64748b; + text-transform: uppercase; +} + +.draft-item-avatar { + width: 44px; + height: 38px; + border-radius: 6px; + background: #f1f5f9; + border: 1px solid #e2e8f0; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + flex-shrink: 0; +} + +.draft-item-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.draft-item-meta { + flex: 1; + min-width: 0; +} + +.item-name { + display: block; + font-size: 13px; + font-weight: 700; + color: #0f172a; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.item-tech { + display: block; + font-size: 11px; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.item-pills { + display: flex; + gap: 4px; + margin-top: 2px; +} + +.pill-type { + background: #e0f2fe; + color: #0369a1; + font-size: 10px; + font-weight: 700; + padding: 1px 5px; + border-radius: 4px; +} + +.pill-cap { + background: #ecfdf5; + color: #047857; + font-size: 10px; + padding: 1px 5px; + border-radius: 4px; +} + +.pill-depot { + background: #fef3c7; + color: #92400e; + font-size: 10px; + padding: 1px 5px; + border-radius: 4px; +} + +.draft-item-controls { + display: flex; + gap: 4px; +} + +.control-btn { + background: #ffffff; + border: 1px solid #cbd5e1; + border-radius: 6px; + color: #334155; + padding: 4px 8px; + font-size: 12px; + font-weight: 700; + cursor: pointer; + transition: all 0.15s; +} + +.control-btn:hover:not(:disabled) { + background: #f1f5f9; + border-color: #94a3b8; +} + +.control-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.control-btn.delete { + color: #dc2626; + border-color: #fca5a5; +} + +.control-btn.delete:hover:not(:disabled) { + background: #fee2e2; +} + +/* CATALOGUE DE MATÉRIEL */ +.catalog-status-tabs { display: flex; gap: 6px; flex-wrap: wrap; } -.attribution-mr-direction-btn { - border: 1px solid #d1d5db; - border-radius: 999px; - background: #f8fafc; - color: #1f2937; - font-size: 13px; - padding: 6px 12px; - cursor: pointer; -} - -.attribution-mr-direction-btn.is-active { - background: #0088ce; - border-color: #0088ce; - color: #fff; -} - -.attribution-mr-direction-btn:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.attribution-mr-toolbar label { - font-weight: 600; -} - -.attribution-mr-toolbar select { - min-width: 280px; - padding: 8px 10px; - border-radius: 8px; - border: 1px solid #d1d5db; - font-size: 14px; - font-family: inherit; -} - -.attribution-mr-count { - margin-left: auto; - color: #6b7280; - font-size: 13px; -} - -.attribution-mr-timeline-wrap { - background: #fff; - border: 1px solid #e5e7eb; - border-radius: 12px; - padding: 18px; - min-height: 280px; -} - -.attribution-mr-empty { - min-height: 230px; - display: flex; - flex-direction: column; +.catalog-tab-btn { + display: inline-flex; align-items: center; - justify-content: center; - color: #9ca3af; - gap: 10px; - text-align: center; + gap: 6px; + padding: 5px 10px; + border-radius: 999px; + border: 1px solid #cbd5e1; + background: #ffffff; + color: #475569; + font-size: 11px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; } -.attribution-mr-gantt-shell { - border: 1px solid #e5e7eb; - border-radius: 10px; - background: #f8fafc; +.catalog-tab-btn.active { + background: #002244; + border-color: #002244; + color: #ffffff; } -.attribution-mr-gantt-scroll { - max-height: 62vh; - overflow: auto; +.tab-count { + background: rgba(0, 0, 0, 0.08); + padding: 1px 5px; + border-radius: 999px; + font-size: 10px; } -.attribution-mr-gantt { - --gantt-cell-width: 230px; - --gantt-tick-count: 13; - min-width: calc(220px + (var(--gantt-cell-width) * var(--gantt-tick-count))); - width: max-content; +.catalog-tab-btn.active .tab-count { + background: rgba(255, 255, 255, 0.25); + color: #ffffff; } -.attribution-mr-gantt-header { - display: grid; - grid-template-columns: 220px 1fr; - position: sticky; - top: 0; - z-index: 3; - background: #f1f5f9; - border-bottom: 1px solid #dbe3ed; +.catalog-search-bar { + position: relative; + display: flex; + align-items: center; } -.attribution-mr-gantt-label-head { - padding: 10px 12px; - font-weight: 700; - border-right: 1px solid #dbe3ed; - background: #f1f5f9; - position: sticky; - left: 0; - z-index: 4; +.catalog-search-bar wcs-mat-icon { + position: absolute; + left: 8px; + color: #94a3b8; + pointer-events: none; } -.attribution-mr-gantt-scale { - display: grid; - grid-template-columns: repeat(var(--gantt-tick-count), var(--gantt-cell-width)); - width: max-content; +.catalog-search-bar input { + width: 100%; + padding: 6px 12px 6px 30px; + border-radius: 6px; + border: 1px solid #cbd5e1; + font-size: 12px; + outline: none; + background: #ffffff; } -.attribution-mr-gantt-tick { - padding: 12px 8px; - font-size: 13px; - color: #334155; - text-align: center; - border-left: 1px dashed #dbe3ed; -} - -.attribution-mr-gantt-row { - display: grid; - grid-template-columns: 220px 1fr; - min-height: 66px; - border-bottom: 1px solid #e5e7eb; - background: #fff; -} - -.attribution-mr-gantt-label { - padding: 8px 12px; - border-right: 1px solid #e2e8f0; - background: #fff; - position: sticky; - left: 0; - z-index: 2; +.catalog-items-grid { display: flex; flex-direction: column; - justify-content: center; + gap: 8px; + max-height: 380px; + overflow-y: auto; } -.attribution-mr-gantt-label small { +.catalog-empty-notice { + text-align: center; + padding: 30px 10px; color: #64748b; font-size: 12px; } -.attribution-mr-gantt-track { - position: relative; - min-height: 66px; - background-image: repeating-linear-gradient(to right, #f8fafc 0, #f8fafc calc(var(--gantt-cell-width) - 1px), #dbe3ed calc(var(--gantt-cell-width) - 1px), #dbe3ed var(--gantt-cell-width)); -} - -.attribution-mr-gantt-bar { - position: absolute; - top: 50%; - transform: translateY(-50%); - height: 42px; - border: none; - border-radius: 6px; - background: linear-gradient(90deg, #3b82f6, #2563eb); - color: #fff; - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 0 12px; - text-align: left; - cursor: pointer; - box-shadow: 0 2px 8px rgba(37, 99, 235, 0.2); -} - -.attribution-mr-gantt-bar span, -.attribution-mr-gantt-bar em { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-style: normal; - font-size: 13px; -} - -.attribution-mr-modal { - max-width: 940px; -} - -.attribution-mr-modal-body { - display: flex; - flex-direction: column; - gap: 18px; -} - -.attribution-mr-modal-subtitle { - color: #6b7280; - font-size: 14px; -} - -.attribution-mr-section-head { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; - color: #6b7280; - font-size: 13px; -} - -.attribution-mr-draft-empty { - border: 1px dashed #d1d5db; - border-radius: 8px; - padding: 14px; - text-align: center; - color: #9ca3af; -} - -.attribution-mr-draft-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.attribution-mr-draft-item { +.catalog-materiel-card { display: flex; align-items: center; gap: 10px; - border: 1px solid #e5e7eb; + padding: 8px 12px; + background: #ffffff; + border: 1px solid #e2e8f0; border-radius: 8px; - padding: 8px 10px; + transition: all 0.15s; } -.attribution-mr-pos { - min-width: 30px; - font-weight: 700; - color: #374151; -} - -.attribution-mr-draft-main { - flex: 1; - display: flex; - flex-direction: column; -} - -.attribution-mr-draft-main small { - color: #9ca3af; -} - -.attribution-mr-draft-actions { - display: flex; - gap: 6px; -} - -.attribution-mr-draft-actions button { - border: 1px solid #d1d5db; - border-radius: 6px; - background: #fff; - padding: 4px 8px; - cursor: pointer; -} - -.attribution-mr-materiels-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 10px; -} - -.attribution-mr-materiels-groups { - display: flex; - flex-direction: column; - gap: 14px; -} - -.attribution-mr-materiels-group { - border: 1px solid #e5e7eb; - border-radius: 8px; - padding: 10px; +.catalog-materiel-card.is-disabled { + opacity: 0.55; background: #f8fafc; } -.attribution-mr-materiels-group-title { - font-weight: 700; - color: #1e293b; - margin-bottom: 8px; +.catalog-materiel-card.is-elsewhere { + border-left: 3px solid #f97316; } -.attribution-mr-materiel-card { - border: 1px solid #e5e7eb; - background: #fff; +.card-avatar { + width: 44px; + height: 38px; + border-radius: 6px; + background: #f1f5f9; + border: 1px solid #e2e8f0; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + flex-shrink: 0; +} + +.card-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.card-details { + flex: 1; + min-width: 0; +} + +.mat-nom { + display: block; + font-size: 13px; + font-weight: 700; + color: #0f172a; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.mat-tech { + display: block; + font-size: 11px; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.card-pills { + display: flex; + gap: 4px; + margin-top: 2px; +} + +.status-note { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + font-weight: 600; + margin-top: 4px; +} + +.note-green { color: #059669; } +.note-orange { color: #d97706; } +.note-red { color: #dc2626; } + +/* 8. MODALE COUPLAGE AVEC UN AUTRE TRAIN */ +.coupling-modal { + max-width: 660px; +} + +.coupling-source-card { + background: #f8fafc; + border: 1px solid #e2e8f0; border-radius: 8px; - padding: 10px; + padding: 12px 14px; + margin-bottom: 16px; +} + +.coupling-source-header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + font-weight: 700; + color: #002244; +} + +.coupling-source-comp { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + color: #64748b; + margin-top: 4px; +} + +.already-coupled-tag { + background: #f5f3ff; + color: #6d28d9; + padding: 2px 6px; + border-radius: 4px; + font-weight: 700; +} + +.coupling-label { + display: block; + font-size: 12px; + font-weight: 700; + color: #334155; + margin-bottom: 8px; + text-transform: uppercase; +} + +.coupling-candidates-list { display: flex; flex-direction: column; + gap: 8px; + max-height: 280px; + overflow-y: auto; + margin-bottom: 14px; +} + +.coupling-candidate-item { + display: flex; align-items: center; - gap: 6px; + gap: 12px; + padding: 10px 14px; + border: 1px solid #e2e8f0; + border-radius: 8px; + background: #ffffff; cursor: pointer; + transition: all 0.15s; } -.attribution-mr-materiel-card img { - width: 100%; - max-height: 52px; - object-fit: contain; +.coupling-candidate-item:hover { + border-color: #0088ce; + background: #f8fafc; } -.attribution-mr-materiel-card span { - font-size: 12px; - text-align: center; +.coupling-candidate-item.is-selected { + border-color: #7c3aed; + background: #faf5ff; } -.attribution-mr-readonly { - color: #9ca3af; - margin: 0; +.candidate-item-label { + flex: 1; } -.attribution-mr-error { - margin: 0; - color: #dc2626; +.cand-top-row { + display: flex; + justify-content: space-between; + align-items: center; font-size: 13px; } -@media (max-width: 768px) { - .attribution-mr-toolbar { +.cand-dest { + color: #002244; + font-weight: 600; +} + +.cand-sub-row { + display: flex; + justify-content: space-between; + font-size: 11px; + color: #64748b; + margin-top: 2px; +} + +.coupling-empty-candidates { + padding: 24px; + text-align: center; + color: #64748b; + border: 1px dashed #cbd5e1; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 12px; +} + +.coupling-info-box { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px 12px; + background: #f0f9ff; + border: 1px solid #bae6fd; + border-radius: 8px; + color: #0369a1; + font-size: 12px; +} + +/* 9. MODALE ATTRIBUTION AUTOMATIQUE */ +.auto-config-modal { + max-width: 580px; +} + +.config-section { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 16px; +} + +.config-toggle-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.config-desc { + margin: 2px 0 0; + font-size: 12px; + color: #64748b; + max-width: 380px; +} + +.switch-container { + position: relative; + display: inline-block; + width: 44px; + height: 24px; +} + +.switch-container input { + opacity: 0; + width: 0; + height: 0; +} + +.switch-slider { + position: absolute; + cursor: pointer; + inset: 0; + background-color: #cbd5e1; + transition: 0.3s; + border-radius: 24px; +} + +.switch-slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.3s; + border-radius: 50%; +} + +.switch-container input:checked + .switch-slider { + background-color: #0088ce; +} + +.switch-container input:checked + .switch-slider:before { + transform: translateX(20px); +} + +.config-time-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 8px; +} + +.config-time-input { + padding: 6px 12px; + border-radius: 6px; + border: 1px solid #cbd5e1; + font-size: 14px; + font-weight: 700; + outline: none; +} + +.config-rules-box { + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 12px 16px; + margin-bottom: 14px; +} + +.config-rules-box h4 { + margin: 0 0 6px; + font-size: 12px; + font-weight: 700; + color: #334155; + text-transform: uppercase; +} + +.config-rules-box ul { + margin: 0; + padding-left: 18px; + font-size: 12px; + color: #475569; + display: flex; + flex-direction: column; + gap: 4px; +} + +.config-last-run-box { + display: flex; + flex-direction: column; + gap: 2px; + padding: 10px 14px; + background: #ecfdf5; + border: 1px solid #a7f3d0; + border-radius: 8px; + color: #065f46; + font-size: 12px; +} + +/* 10. PIED DE PAGE CORPORATE WCS */ +.sncf-page-footer { + margin-top: 40px; + border-top: 1px solid #e2e8f0; +} + +.footer-sncf-badge { + width: 36px; + height: 36px; + border-radius: 8px; + background: #002244; + display: flex; + align-items: center; + justify-content: center; +} + +/* RESPONSIVE */ +@media (max-width: 900px) { + .attribution-fullscreen-page { + padding: 16px; + } + + .builder-columns-layout { + grid-template-columns: 1fr; + } + + .attribution-toolbar-card .toolbar-top-row { flex-direction: column; align-items: stretch; } - .attribution-mr-toolbar select { + .custom-select-container select { min-width: 0; width: 100%; } - .attribution-mr-count { + .attribution-view-toggle { margin-left: 0; } - - .attribution-mr-gantt-header, - .attribution-mr-gantt-row { - grid-template-columns: 170px 1fr; - } - - .attribution-mr-gantt-label-head, - .attribution-mr-gantt-label { - min-width: 170px; - } } -/* Dark Mode */ -body.dark-mode .attribution-mr-page, -[data-theme="dark"] .attribution-mr-page { +/* DARK MODE */ +body.dark-mode .attribution-fullscreen-page, +[data-theme="dark"] .attribution-fullscreen-page { background-color: #0b111e; color: #f1f5f9; } -body.dark-mode .attribution-mr-header h2, -[data-theme="dark"] .attribution-mr-header h2 { +body.dark-mode .header-meta h2, +[data-theme="dark"] .header-meta h2 { color: #f8fafc; } -body.dark-mode .attribution-mr-description, -[data-theme="dark"] .attribution-mr-description { - color: #94a3b8; -} - -body.dark-mode .attribution-mr-toolbar, -body.dark-mode .attribution-mr-timeline-wrap, -[data-theme="dark"] .attribution-mr-toolbar, -[data-theme="dark"] .attribution-mr-timeline-wrap { +body.dark-mode .kpi-card, +body.dark-mode .attribution-tabs-nav-wrapper, +body.dark-mode .attribution-toolbar-card, +body.dark-mode .horaire-attribution-card, +body.dark-mode .horaires-timeline-card, +body.dark-mode .last-refresh-tag, +body.dark-mode .auto-config-btn, +[data-theme="dark"] .kpi-card, +[data-theme="dark"] .attribution-tabs-nav-wrapper, +[data-theme="dark"] .attribution-toolbar-card, +[data-theme="dark"] .horaire-attribution-card, +[data-theme="dark"] .horaires-timeline-card, +[data-theme="dark"] .last-refresh-tag, +[data-theme="dark"] .auto-config-btn { background: #151f30; border-color: #243247; color: #f1f5f9; } -body.dark-mode .attribution-mr-direction-btn, -[data-theme="dark"] .attribution-mr-direction-btn { +body.dark-mode .kpi-num, +body.dark-mode .train-number, +body.dark-mode .step-time, +body.dark-mode .cell-num, +body.dark-mode .unit-name, +body.dark-mode .mat-nom, +[data-theme="dark"] .kpi-num, +[data-theme="dark"] .train-number, +[data-theme="dark"] .step-time, +[data-theme="dark"] .cell-num, +[data-theme="dark"] .unit-name, +[data-theme="dark"] .mat-nom { + color: #f8fafc; +} + +body.dark-mode .custom-select-container select, +body.dark-mode .search-horaire-wrap input, +body.dark-mode .catalog-search-bar input, +body.dark-mode .config-time-input, +[data-theme="dark"] .custom-select-container select, +[data-theme="dark"] .search-horaire-wrap input, +[data-theme="dark"] .catalog-search-bar input, +[data-theme="dark"] .config-time-input { background: #0b111e; border-color: #243247; - color: #cbd5e1; + color: #f1f5f9; } -body.dark-mode .attribution-mr-direction-btn.is-active, -[data-theme="dark"] .attribution-mr-direction-btn.is-active { - background: #0284c7; - border-color: #0284c7; - color: #ffffff; -} - -body.dark-mode .attribution-mr-gantt-header, -[data-theme="dark"] .attribution-mr-gantt-header { +body.dark-mode .horaire-route-box, +body.dark-mode .builder-draft-panel, +body.dark-mode .builder-catalog-panel, +body.dark-mode .coupling-source-card, +body.dark-mode .config-rules-box, +body.dark-mode .config-time-row, +body.dark-mode .horaires-gantt-header, +body.dark-mode .gantt-train-label-head, +[data-theme="dark"] .horaire-route-box, +[data-theme="dark"] .builder-draft-panel, +[data-theme="dark"] .builder-catalog-panel, +[data-theme="dark"] .coupling-source-card, +[data-theme="dark"] .config-rules-box, +[data-theme="dark"] .config-time-row, +[data-theme="dark"] .horaires-gantt-header, +[data-theme="dark"] .gantt-train-label-head { background: #0e1726; border-color: #243247; } -body.dark-mode .attribution-mr-gantt-row, -[data-theme="dark"] .attribution-mr-gantt-row { - border-bottom-color: #1e293b; -} - -body.dark-mode .attribution-mr-gantt-label, -[data-theme="dark"] .attribution-mr-gantt-label { +body.dark-mode .draft-item-card, +body.dark-mode .catalog-materiel-card, +body.dark-mode .coupling-candidate-item, +[data-theme="dark"] .draft-item-card, +[data-theme="dark"] .catalog-materiel-card, +[data-theme="dark"] .coupling-candidate-item { background: #111a28; - border-right-color: #243247; - color: #f8fafc; -} - -body.dark-mode .attribution-mr-materiels-group, -[data-theme="dark"] .attribution-mr-materiels-group { - background: #111a28; - border-color: #243247; -} - -body.dark-mode .attribution-mr-materiels-group-title, -[data-theme="dark"] .attribution-mr-materiels-group-title { - color: #f8fafc; -} - -body.dark-mode .attribution-mr-materiel-card, -[data-theme="dark"] .attribution-mr-materiel-card { - background: #151f30; border-color: #243247; color: #f1f5f9; } +/* ========================================================================== + SERVICES ROUTIERS (CARS & AUTOCARS) + ========================================================================== */ +.comp-road-badge { + background: #fef3c7 !important; + color: #92400e !important; + border: 1px solid #fde68a !important; + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 8px; + border-radius: 4px; + font-weight: 700; + font-size: 11px; +} + +.road-service-box { + background: #fffbeb !important; + border: 1px dashed #fcd34d !important; + color: #78350f !important; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; +} + +.road-service-pill-notice { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #92400e; + background: #fef3c7; + padding: 6px 12px; + border-radius: 6px; + font-weight: 600; + border: 1px solid #fde68a; +} + +.status-filter-btn.btn-road { + background: #fffbeb; + color: #92400e; + border-color: #fde68a; +} + +.status-filter-btn.btn-road.active { + background: #fef3c7; + border-color: #d97706; + color: #78350f; + box-shadow: 0 0 0 1px #d97706; +} + +.depots-empty-card { + background: #ffffff; + border: 1px dashed #cbd5e1; + border-radius: 12px; + padding: 40px 20px; + text-align: center; + color: #64748b; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.depots-empty-card h4 { + margin: 8px 0 4px; + color: #1e293b; + font-size: 16px; + font-weight: 700; +} + +body.dark-mode .comp-road-badge, +[data-theme="dark"] .comp-road-badge { + background: #451a03 !important; + color: #fef3c7 !important; + border-color: #78350f !important; +} + +body.dark-mode .road-service-box, +[data-theme="dark"] .road-service-box { + background: #1e1b12 !important; + border-color: #78350f !important; + color: #fef3c7 !important; +} + +body.dark-mode .road-service-pill-notice, +[data-theme="dark"] .road-service-pill-notice { + background: #271f11; + border-color: #78350f; + color: #fef3c7; +} + +body.dark-mode .depots-empty-card, +[data-theme="dark"] .depots-empty-card { + background: #151f30; + border-color: #243247; + color: #94a3b8; +} + +body.dark-mode .depots-empty-card h4, +[data-theme="dark"] .depots-empty-card h4 { + color: #f8fafc; +} + +/* ========================================================================= + 11. STYLES POUR LA MODALE D'OPTIMISATION DU ROULEMENT + ========================================================================= */ +.btn-opt-roster { + background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%) !important; + box-shadow: 0 2px 8px rgba(3, 105, 161, 0.25); + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.btn-opt-roster:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(3, 105, 161, 0.35); +} + +.optimization-modal { + max-width: 860px; + width: 90vw; +} + +.opt-settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 12px; +} + +.opt-setting-card { + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 12px 14px; +} + +.opt-setting-card strong { + font-size: 13px; + color: #0f172a; +} + +.opt-preview-section { + background: #f1f5f9; + border: 1px solid #cbd5e1; + border-radius: 10px; + padding: 16px; +} + +.opt-section-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 700; + color: #0f172a; + margin: 0 0 12px 0; +} + +.opt-stats-comparison-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); + gap: 10px; +} + +.opt-stat-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 12px; + text-align: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); +} + +.stat-label { + display: block; + font-size: 11px; + text-transform: uppercase; + font-weight: 700; + color: #64748b; + margin-bottom: 4px; +} + +.stat-diff-row { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 18px; + font-weight: 800; + color: #0f172a; +} + +.stat-val.before { + color: #94a3b8; + font-size: 15px; +} + +.stat-val.after { + font-weight: 800; +} + +.stat-total { + font-size: 13px; + color: #64748b; + font-weight: 600; +} + +.stat-sub { + display: block; + font-size: 11px; + color: #64748b; + margin-top: 4px; +} + +.opt-actions-log-box { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 8px; + overflow: hidden; +} + +.log-box-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 14px; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + font-size: 12px; +} + +.log-box-body { + max-height: 180px; + overflow-y: auto; + padding: 8px 12px; + font-size: 12px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.log-row { + display: flex; + align-items: center; + color: #334155; + line-height: 1.4; +} + +/* Dark mode pour la modale d'optimisation */ +body.dark-mode .opt-setting-card, +[data-theme="dark"] .opt-setting-card { + background: #1e293b; + border-color: #334155; +} + +body.dark-mode .opt-setting-card strong, +[data-theme="dark"] .opt-setting-card strong { + color: #f8fafc; +} + +body.dark-mode .opt-preview-section, +[data-theme="dark"] .opt-preview-section { + background: #0f172a; + border-color: #334155; +} + +body.dark-mode .opt-section-title, +[data-theme="dark"] .opt-section-title { + color: #f8fafc; +} + +body.dark-mode .opt-stat-card, +[data-theme="dark"] .opt-stat-card { + background: #1e293b; + border-color: #334155; +} + +body.dark-mode .stat-diff-row, +[data-theme="dark"] .stat-diff-row { + color: #f8fafc; +} + +body.dark-mode .opt-actions-log-box, +[data-theme="dark"] .opt-actions-log-box { + background: #1e293b; + border-color: #334155; +} + +body.dark-mode .log-box-header, +[data-theme="dark"] .log-box-header { + background: #0f172a; + border-color: #334155; +} + +body.dark-mode .log-row, +[data-theme="dark"] .log-row { + color: #cbd5e1; +} diff --git a/src/pages/attribution-materiel-roulant/AttributionMaterielRoulant.js b/src/pages/attribution-materiel-roulant/AttributionMaterielRoulant.js index 02955da..a29d7b3 100644 --- a/src/pages/attribution-materiel-roulant/AttributionMaterielRoulant.js +++ b/src/pages/attribution-materiel-roulant/AttributionMaterielRoulant.js @@ -1,9 +1,30 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { getAllLignes } from '../../api/lignes'; -import { getAllHoraires, updateHoraireComposition, formatHeure } from '../../api/horaires'; -import { getAllMateriels } from '../../api/materielsRoulants'; +import { + getAllHoraires, + updateHoraireComposition, + updateHorairesCompositionsBatch, + updateHoraireCouplage, + updateQuaisHoraire, + updateQuaisBatch, + formatHeure +} from '../../api/horaires'; +import { getAllMateriels, updateMateriel } from '../../api/materielsRoulants'; +import { getAllDepots } from '../../api/depots'; +import { getParametre, upsertParametre } from '../../api/parametres'; import { canPerform, getCurrentUser, getScopedRegionId } from '../../utils/userUtils'; -import { buildMaterielTimeline, getMaterielStatusAtTime } from '../../utils/roulementUtils'; +import { + buildMaterielTimeline, + getMaterielStatusAtTime, + analyzeHoraireComposition, + analyzeGlobalRoster, + normalizeStation, + groupHorairesWithAlternatives, + runAutoAttribution, + optimizeMaterielRosters, + evaluateQuaiContinuity, + isRoadService +} from '../../utils/roulementUtils'; import { timeToMinutes } from '../../utils/quaisUtils'; import VueRoulement from './VueRoulement'; import './AttributionMaterielRoulant.css'; @@ -11,18 +32,9 @@ import './AttributionMaterielRoulant.css'; const DAY_MINUTES = 24 * 60; const TIME_MARKERS_HOURS = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24]; -function parseTimeToMinutes(value) { - if (!value || typeof value !== 'string') return null; - const [h, m] = value.split(':'); - const hours = Number(h); - const minutes = Number(m); - if (Number.isNaN(hours) || Number.isNaN(minutes)) return null; - return (hours * 60) + minutes; -} - function getBarPlacement(horaire) { - const depart = parseTimeToMinutes(horaire?.heure_depart); - const arriveeRaw = parseTimeToMinutes(horaire?.heure_arrivee); + const depart = timeToMinutes(horaire?.heure_depart); + const arriveeRaw = timeToMinutes(horaire?.heure_arrivee); if (depart === null) { return { leftPct: 0, widthPct: 8 }; @@ -42,32 +54,33 @@ function sortByHoraire(a, b) { return String(a?.numero_train || '').localeCompare(String(b?.numero_train || '')); } -function toDraftComposition(compositionTrain) { +function toDraftComposition(compositionTrain, allMateriels = []) { if (!Array.isArray(compositionTrain)) return []; return [...compositionTrain] .sort((a, b) => Number(a?.position || 0) - Number(b?.position || 0)) - .map((item, idx) => ({ - local_id: item?.local_id || `draft-${Date.now()}-${idx}-${Math.random()}`, - 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 - })); -} - -function normalizeStationName(value) { - return String(value || '').trim().toLowerCase(); + .map((item, idx) => { + const fullMat = allMateriels.find(m => String(m.id) === String(item?.materiel_id)); + return { + local_id: item?.local_id || `draft-${Date.now()}-${idx}-${Math.random()}`, + materiel_id: item?.materiel_id, + materiel_nom: item?.materiel_nom || fullMat?.nom || '', + materiel_nom_technique: item?.materiel_nom_technique || fullMat?.nom_technique || '', + materiel_image_url: item?.materiel_image_url || fullMat?.image_url || null, + capacite: item?.capacite != null ? Number(item.capacite) : (fullMat?.capacite ? Number(fullMat.capacite) : 0), + type_train: item?.type_train || fullMat?.type_train || '', + position: idx + 1 + }; + }); } function matchesDirection(horaire, ligne, direction) { if (!ligne || direction === 'all') return true; const lineDepartId = ligne?.gare_depart_id || ligne?.gare_depart?.id || null; - const lineDepartName = normalizeStationName(ligne?.gare_depart?.nom || ligne?.gare_depart_nom); + const lineDepartName = normalizeStation(ligne?.gare_depart?.nom || ligne?.gare_depart_nom); const horaireDepartId = horaire?.gare_depart_id || null; const horaireArriveeId = horaire?.gare_arrivee_id || null; - const horaireDepartName = normalizeStationName(horaire?.gare_depart_nom); - const horaireArriveeName = normalizeStationName(horaire?.gare_arrivee_nom); + const horaireDepartName = normalizeStation(horaire?.gare_depart_nom); + const horaireArriveeName = normalizeStation(horaire?.gare_arrivee_nom); if (direction === 'aller') { return (lineDepartId && String(horaireDepartId) === String(lineDepartId)) @@ -79,48 +92,118 @@ function matchesDirection(horaire, ligne, direction) { } export default function AttributionMaterielRoulant() { - // Evite de recréer l'objet utilisateur à chaque rendu (sinon useEffect repart en boucle) const currentUser = useMemo(() => getCurrentUser(), []); const canUpdate = canPerform(currentUser, 'horaires', 'update'); + // Données de base const [lignes, setLignes] = useState([]); const [horaires, setHoraires] = useState([]); const [materiels, setMateriels] = useState([]); - const [activeTab, setActiveTab] = useState('liste'); + const [depots, setDepots] = useState([]); + const [loading, setLoading] = useState(true); + const [lastRefreshed, setLastRefreshed] = useState(new Date()); + // Onglet principal ('attribution' | 'roulements') + const [activeTab, setActiveTab] = useState('attribution'); + const tabsRef = useRef(null); + + // Filtres Onglet 1 (Attribution) const [selectedLigneId, setSelectedLigneId] = useState(''); const [directionFilter, setDirectionFilter] = useState('all'); + const [assignmentFilter, setAssignmentFilter] = useState('all'); // 'all' | 'unassigned' | 'assigned' | 'anomaly' | 'alternatives' + const [searchHoraireTerm, setSearchHoraireTerm] = useState(''); + const [attributionViewMode, setAttributionViewMode] = useState('cards'); // 'cards' | 'timeline' + + // Modale Composition const [selectedHoraire, setSelectedHoraire] = useState(null); const [compositionDraft, setCompositionDraft] = useState([]); - const [modalActiveTab, setModalActiveTab] = useState('composition'); - const [affectationSearchTerm, setAffectationSearchTerm] = useState(''); - const [selectedAffectationMaterielId, setSelectedAffectationMaterielId] = useState(null); + const [modalActiveTab, setModalActiveTab] = useState('composition'); // 'composition' | 'chaining' + const [pickerCategory, setPickerCategory] = useState('station'); + const [pickerSearchTerm, setPickerSearchTerm] = useState(''); + const [chainingSearchTerm, setChainingSearchTerm] = useState(''); + const [selectedChainingMaterielId, setSelectedChainingMaterielId] = useState(null); - const [loading, setLoading] = useState(true); + // Modale Couplage + const [couplingHoraire, setCouplingHoraire] = useState(null); + const [selectedCouplingCandidateId, setSelectedCouplingCandidateId] = useState(''); + const [isCouplingSaving, setIsCouplingSaving] = useState(false); + + // Modale Attribution Automatique & Planification + const [showAutoConfigModal, setShowAutoConfigModal] = useState(false); + const [isAutoAttributing, setIsAutoAttributing] = useState(false); + const [autoScheduleEnabled, setAutoScheduleEnabled] = useState(false); + const [autoScheduleTime, setAutoScheduleTime] = useState('04:00'); + const [lastAutoRunResult, setLastAutoRunResult] = useState(null); + const lastTriggeredMinuteRef = useRef(null); + + // Modale Optimisation Intelligente des Roulements + const [showOptimizationModal, setShowOptimizationModal] = useState(false); + const [isOptimizing, setIsOptimizing] = useState(false); + const [optimizationOptions, setOptimizationOptions] = useState({ + minTurnaroundMins: 15, + maxTurnaroundMins: 120, + optimalMinTurnaround: 15, + optimalMaxTurnaround: 45, + respectMaintenance: true, + balanceWorkload: true, + prioritizeHomeDepot: true, + resolveBreaks: true, + onlyUnassigned: false + }); + const [optimizationPreview, setOptimizationPreview] = useState(null); + + // Feedback & Sauvegarde const [saving, setSaving] = useState(false); const [error, setError] = useState(''); - const [saveMessage, setSaveMessage] = useState(''); + const [toastMessage, setToastMessage] = useState(null); + // Horaires enrichis avec la gestion des alternatives + const horairesWithAlternatives = useMemo(() => { + return groupHorairesWithAlternatives(horaires); + }, [horaires]); + + // Timelines précalculées pour toute la flotte + const timelines = useMemo(() => { + return buildMaterielTimeline(horaires, materiels); + }, [horaires, materiels]); + + // KPIs globaux de la flotte + const globalKpis = useMemo(() => { + return analyzeGlobalRoster(timelines, materiels, horaires); + }, [timelines, materiels, horaires]); + + // Chargement des données const loadData = useCallback(async () => { setLoading(true); setError(''); try { const regionId = getScopedRegionId(currentUser); - const [lignesRes, horairesRes, materielsRes] = await Promise.all([ + const [lignesRes, horairesRes, materielsRes, paramRes, depotsRes] = await Promise.all([ getAllLignes(regionId), getAllHoraires(regionId), - getAllMateriels() + getAllMateriels(), + getParametre('auto_attribution_config'), + getAllDepots() ]); if (lignesRes.error || horairesRes.error || materielsRes.error) { const firstError = lignesRes.error || horairesRes.error || materielsRes.error; - setError(firstError?.message || 'Erreur lors du chargement des données'); + setError(firstError?.message || 'Erreur lors du chargement des données ferroviaires'); } setLignes(Array.isArray(lignesRes.data) ? lignesRes.data : []); setHoraires(Array.isArray(horairesRes.data) ? horairesRes.data : []); setMateriels(Array.isArray(materielsRes.data) ? materielsRes.data : []); + setDepots(Array.isArray(depotsRes?.data) ? depotsRes.data : []); + setLastRefreshed(new Date()); + + // Configuration d'attribution automatique + if (paramRes?.data?.valeur) { + const cfg = paramRes.data.valeur; + if (cfg.enabled !== undefined) setAutoScheduleEnabled(!!cfg.enabled); + if (cfg.time) setAutoScheduleTime(cfg.time); + } } catch (err) { console.error('Erreur loadData attribution matériel roulant:', err); setError('Erreur lors du chargement des données'); @@ -133,19 +216,214 @@ export default function AttributionMaterielRoulant() { loadData(); }, [loadData]); + // Synchronisation des WCS Tabs + useEffect(() => { + const tabsEl = tabsRef.current; + if (tabsEl) { + const handleTabChange = (e) => { + const key = e.detail?.selectedKey || e.detail?.tabName; + if (key) setActiveTab(key); + }; + tabsEl.addEventListener('tabChange', handleTabChange); + return () => tabsEl.removeEventListener('tabChange', handleTabChange); + } + }, []); + + const showToast = (message, type = 'success') => { + setToastMessage({ message, type }); + setTimeout(() => { + setToastMessage(null); + }, 4500); + }; + + // Moteur d'attribution automatique + const executeAutoAttribution = async () => { + if (!canUpdate) return; + setIsAutoAttributing(true); + try { + const result = runAutoAttribution(horaires, materiels, { + minTurnaroundMins: 15, + maxTurnaroundMins: 90, + onlyUnassigned: true, + respectMaintenance: true + }); + + if (result.totalAssigned === 0) { + showToast('Toutes les circulations sont déjà pourvues ou aucun matériel compatible disponible.', 'info'); + setIsAutoAttributing(false); + return; + } + + // Appliquer les mises à jour de composition + for (const update of result.planUpdates) { + await updateHoraireComposition(update.horaireId, update.composition); + } + + // Appliquer les attributions automatiques de quai (règle des <= 45 min) + for (const quaiUp of result.quaiUpdates) { + await updateQuaisHoraire(quaiUp.horaireId, { depart: quaiUp.departQuai }); + } + + setLastAutoRunResult(result); + showToast( + `Attribution automatique terminée : ${result.totalAssigned} sillons pourvus (${result.totalQuaisPreserved} quais maintenus car escale ≤ 45 min).` + ); + await loadData(); + } catch (err) { + console.error('Erreur executeAutoAttribution:', err); + showToast("Erreur lors de l'exécution de l'attribution automatique", 'error'); + } finally { + setIsAutoAttributing(false); + } + }; + + // Sauvegarde de la configuration automatique + const handleSaveAutoConfig = async () => { + try { + await upsertParametre('auto_attribution_config', { + enabled: autoScheduleEnabled, + time: autoScheduleTime, + updated_at: new Date().toISOString() + }); + showToast(`Paramètres d'attribution automatique enregistrés (Heure : ${autoScheduleTime}).`); + setShowAutoConfigModal(false); + } catch (err) { + console.error('Erreur handleSaveAutoConfig:', err); + showToast('Erreur lors de la sauvegarde des paramètres', 'error'); + } + }; + + // Moteur d'Optimisation des Roulements + const openOptimizationModal = () => { + const result = optimizeMaterielRosters(horaires, materiels, optimizationOptions); + setOptimizationPreview(result); + setShowOptimizationModal(true); + }; + + const handleRunOptimizationSimulation = () => { + const result = optimizeMaterielRosters(horaires, materiels, optimizationOptions); + setOptimizationPreview(result); + showToast( + `Simulation recalculée : ${result.newAssignedCount} train(s) optimisé(s), ${result.quaisPreservedCount} quai(s) préservé(s).`, + 'info' + ); + }; + + const handleApplyOptimization = async () => { + if (!canUpdate || !optimizationPreview) return; + setIsOptimizing(true); + try { + const { planUpdates, quaiUpdates } = optimizationPreview; + + // 1. Sauvegarde des compositions optimisées par lots + if (planUpdates.length > 0) { + await updateHorairesCompositionsBatch(planUpdates); + } + + // 2. Sauvegarde des quais optimisés par lots + if (quaiUpdates.length > 0) { + const quaiBatchPayloads = quaiUpdates.map((q) => ({ + horaireId: q.horaireId, + payload: { depart: q.departQuai } + })); + await updateQuaisBatch(quaiBatchPayloads); + } + + showToast( + `Optimisation appliquée : ${planUpdates.length} roulement(s) optimisé(s), ${quaiUpdates.length} quai(s) harmonisé(s).`, + 'success' + ); + setShowOptimizationModal(false); + setOptimizationPreview(null); + await loadData(); + } catch (err) { + console.error('Erreur handleApplyOptimization:', err); + showToast("Erreur lors de l'application de l'optimisation", 'error'); + } finally { + setIsOptimizing(false); + } + }; + + // Timer de planification quotidienne à heure fixe + useEffect(() => { + if (!autoScheduleEnabled || !autoScheduleTime) return; + + const timer = setInterval(() => { + const now = new Date(); + const currentHours = String(now.getHours()).padStart(2, '0'); + const currentMinutes = String(now.getMinutes()).padStart(2, '0'); + const currentTimeString = `${currentHours}:${currentMinutes}`; + + if (currentTimeString === autoScheduleTime && lastTriggeredMinuteRef.current !== currentTimeString) { + lastTriggeredMinuteRef.current = currentTimeString; + console.log(`[AttributionMateriel] Déclenchement automatique planifié à ${currentTimeString}`); + executeAutoAttribution(); + } + }, 25000); + + return () => clearInterval(timer); + }, [autoScheduleEnabled, autoScheduleTime]); + + // Ligne sélectionnée const selectedLigne = useMemo( () => lignes.find((l) => String(l?.id) === String(selectedLigneId)) || null, [lignes, selectedLigneId] ); - const horairesLigne = useMemo(() => { - if (!selectedLigneId) return []; - return (horaires || []) - .filter((h) => String(h?.ligne_id || h?.ligne || '') === String(selectedLigneId)) - .filter((h) => matchesDirection(h, selectedLigne, directionFilter)) - .sort(sortByHoraire); - }, [horaires, selectedLigneId, selectedLigne, directionFilter]); + // Horaires filtrés avec gestion des alternatives + const filteredHoraires = useMemo(() => { + return (horairesWithAlternatives || []) + .filter((h) => { + if (selectedLigneId && String(h?.ligne_id || h?.ligne || '') !== String(selectedLigneId)) { + return false; + } + if (selectedLigne && !matchesDirection(h, selectedLigne, directionFilter)) { + return false; + } + + if (searchHoraireTerm) { + const q = searchHoraireTerm.toLowerCase(); + const matchNum = String(h?.numero_train || '').toLowerCase().includes(q); + const matchDep = String(h?.gare_depart_nom || '').toLowerCase().includes(q); + const matchArr = String(h?.gare_arrivee_nom || '').toLowerCase().includes(q); + const matchLigne = String(h?.ligne_nom || '').toLowerCase().includes(q); + if (!matchNum && !matchDep && !matchArr && !matchLigne) return false; + } + + const comp = Array.isArray(h?.composition_train) ? h.composition_train : []; + const hasComp = comp.length > 0; + const isRoad = isRoadService(h); + + if (assignmentFilter === 'unassigned') { + // Les cars/autocars ne sont pas des trains sans rame, ce sont des services routiers + if (isRoad || hasComp) return false; + } + if (assignmentFilter === 'assigned' && (!hasComp || isRoad)) return false; + if (assignmentFilter === 'road_services' && !isRoad) return false; + if (assignmentFilter === 'alternatives' && !h.isAlternative) return false; + if (assignmentFilter === 'anomaly') { + if (isRoad) return false; + const analysis = analyzeHoraireComposition(h, timelines, materiels); + if (!analysis.hasAnomalies) return false; + } + + return true; + }) + .sort(sortByHoraire); + }, [horairesWithAlternatives, selectedLigneId, selectedLigne, directionFilter, searchHoraireTerm, assignmentFilter, timelines, materiels]); + + // Analyse de la composition du train actuellement sélectionné dans la modale + const currentHoraireAnalysis = useMemo(() => { + if (!selectedHoraire) return null; + return analyzeHoraireComposition( + { ...selectedHoraire, composition_train: compositionDraft }, + timelines, + materiels + ); + }, [selectedHoraire, compositionDraft, timelines, materiels]); + + // Disponibilité des matériels pour le départ du train sélectionné const { materielsPresentsEnGare, materielsAuDepot, materielsAilleurs, materielsEnCirculation } = useMemo(() => { const allMateriels = Array.isArray(materiels) ? materiels : []; if (!selectedHoraire) { @@ -158,34 +436,30 @@ export default function AttributionMaterielRoulant() { } const departId = selectedHoraire?.gare_depart_id != null ? String(selectedHoraire.gare_depart_id) : null; - const departName = normalizeStationName(selectedHoraire?.gare_depart_nom); + const departName = normalizeStation(selectedHoraire?.gare_depart_nom); const targetTimeMins = timeToMinutes(selectedHoraire?.heure_depart); - const timelines = buildMaterielTimeline(horaires || [], allMateriels); - const presents = []; const depot = []; const ailleurs = []; const enCirculation = []; - allMateriels.forEach((materiel) => { - // Is this materiel already assigned to the selectedHoraire? - const isAlreadyAssigned = (selectedHoraire.composition_train || []).some(comp => String(comp.materiel_id) === String(materiel.id)); - if (isAlreadyAssigned) return; // Hide it from available lists since it's already in the draft/train + const alreadyDraftedIds = new Set(compositionDraft.map(c => String(c.materiel_id))); - const { status } = getMaterielStatusAtTime(timelines, materiel.id, targetTimeMins, departId, departName); - - // Additional check: is the materiel already assigned to another train at the exact same time or overlapping? - // Actually `getMaterielStatusAtTime` could check overlapping, but `timelines` is just a sequence. - // Let's rely on status. - if (status === 'PRESENT_EN_GARE') { - presents.push(materiel); - } else if (status === 'DEPOT') { - depot.push(materiel); - } else if (status === 'EN_CIRCULATION') { - enCirculation.push(materiel); + allMateriels.forEach((materiel) => { + if (alreadyDraftedIds.has(String(materiel.id))) return; + + const statusObj = getMaterielStatusAtTime(timelines, materiel.id, targetTimeMins, departId, departName); + const enrichedMat = { ...materiel, statusInfo: statusObj }; + + if (statusObj.status === 'PRESENT_EN_GARE') { + presents.push(enrichedMat); + } else if (statusObj.status === 'DEPOT') { + depot.push(enrichedMat); + } else if (statusObj.status === 'EN_CIRCULATION') { + enCirculation.push(enrichedMat); } else { - ailleurs.push(materiel); + ailleurs.push(enrichedMat); } }); @@ -195,21 +469,26 @@ export default function AttributionMaterielRoulant() { materielsAilleurs: ailleurs, materielsEnCirculation: enCirculation }; - }, [selectedHoraire, horaires, materiels]); + }, [selectedHoraire, compositionDraft, timelines, materiels]); const openCompositionModal = (horaire) => { if (!horaire) return; - setSaveMessage(''); + if (isRoadService(horaire)) { + showToast('Cette circulation est un service routier (Car / Autocar). Aucun matériel ferroviaire ne doit y être attribué.', 'info'); + return; + } setSelectedHoraire(horaire); - setCompositionDraft(toDraftComposition(horaire.composition_train)); + setCompositionDraft(toDraftComposition(horaire.composition_train, materiels)); setModalActiveTab('composition'); - setAffectationSearchTerm(''); + setPickerCategory('station'); + setPickerSearchTerm(''); + setChainingSearchTerm(''); + setSelectedChainingMaterielId(null); }; const closeCompositionModal = () => { setSelectedHoraire(null); setCompositionDraft([]); - setSaveMessage(''); }; const addMaterielToDraft = (materiel) => { @@ -222,6 +501,8 @@ export default function AttributionMaterielRoulant() { materiel_nom: materiel.nom || '', materiel_nom_technique: materiel.nom_technique || '', materiel_image_url: materiel.image_url || null, + capacite: materiel.capacite ? Number(materiel.capacite) : 0, + type_train: materiel.type_train || '', position: prev.length + 1 } ]); @@ -229,9 +510,10 @@ export default function AttributionMaterielRoulant() { const removeFromDraft = (localId) => { if (!canUpdate) return; - setCompositionDraft((prev) => prev - .filter((item) => item.local_id !== localId) - .map((item, idx) => ({ ...item, position: idx + 1 })) + setCompositionDraft((prev) => + prev + .filter((item) => item.local_id !== localId) + .map((item, idx) => ({ ...item, position: idx + 1 })) ); }; @@ -249,528 +531,1744 @@ export default function AttributionMaterielRoulant() { }); }; - const assignToNextHoraire = async (targetHoraire, materielId) => { - if (!canUpdate) return; - setSaving(true); - setSaveMessage(''); - try { - // Find the materiel full object to copy its properties - const materielObj = materiels.find(m => String(m.id) === String(materielId)); - if (!materielObj) throw new Error("Matériel introuvable"); - - const currentComp = Array.isArray(targetHoraire.composition_train) ? targetHoraire.composition_train : []; - - const newPayload = [...currentComp, { - materiel_id: materielObj.id, - materiel_nom: materielObj.nom || '', - materiel_nom_technique: materielObj.nom_technique || '', - materiel_image_url: materielObj.image_url || null, - position: currentComp.length + 1 - }]; - - const { error } = await updateHoraireComposition(targetHoraire.id, newPayload); - if (error) { - setSaveMessage(error.message || "Erreur lors de l'affectation au prochain train"); - } else { - setSaveMessage(`Le matériel a été affecté au train ${targetHoraire.numero_train}.`); - // Refresh data to reflect changes - await loadData(); - } - } catch (err) { - console.error("Erreur assignToNextHoraire:", err); - setSaveMessage("Erreur inattendue"); - } finally { - setSaving(false); - } - }; - + // Sauvegarder la composition du train avec la règle des quais (<= 45 min) const saveComposition = async () => { if (!selectedHoraire || !canUpdate) return; setSaving(true); - setSaveMessage(''); try { const payload = compositionDraft.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, + capacite: item.capacite != null ? Number(item.capacite) : null, + type_train: item.type_train || '', position: idx + 1 })); const { data, error: saveError } = await updateHoraireComposition(selectedHoraire.id, payload); if (saveError) { - setSaveMessage(saveError.message || 'Erreur lors de la sauvegarde'); + showToast(saveError.message || 'Erreur lors de la sauvegarde de la composition', 'error'); setSaving(false); return; } - setHoraires((prev) => prev.map((h) => { - if (String(h?.id) !== String(selectedHoraire.id)) return h; - return { - ...h, - composition_train: data?.composition_train || payload - }; - })); - setSaveMessage('Composition enregistrée.'); + // Application de la règle des quais : si la rame provient d'un train A arrivant <= 45 min avant + if (compositionDraft.length > 0) { + const firstMatId = compositionDraft[0].materiel_id; + const matTimeline = timelines[firstMatId] || []; + const departM = timeToMinutes(selectedHoraire.heure_depart); + + const prevEvent = matTimeline + .filter(ev => String(ev.horaire_id) !== String(selectedHoraire.id) && ev.endMins <= departM) + .sort((a, b) => b.endMins - a.endMins)[0]; + + if (prevEvent) { + const quaiEval = evaluateQuaiContinuity(prevEvent.horaire, selectedHoraire); + if (quaiEval && quaiEval.shouldKeepSameQuai && quaiEval.arrivalQuai) { + await updateQuaisHoraire(selectedHoraire.id, { depart: quaiEval.arrivalQuai }); + showToast( + `Quai ${quaiEval.arrivalQuai} automatiquement attribué au train ${selectedHoraire.numero_train} (Escale ${quaiEval.deltaMins} min ≤ 45 min).` + ); + } + } + } + + setHoraires((prev) => + prev.map((h) => { + if (String(h?.id) !== String(selectedHoraire.id)) return h; + return { + ...h, + composition_train: data?.composition_train || payload + }; + }) + ); + + showToast(`Composition du train ${selectedHoraire.numero_train} enregistrée avec succès.`); closeCompositionModal(); } catch (err) { console.error('Erreur saveComposition:', err); - setSaveMessage('Erreur inattendue lors de la sauvegarde'); + showToast('Erreur inattendue lors de la sauvegarde', 'error'); } finally { setSaving(false); } }; - return ( -
-
-
-
-
-

Exploitation

-

Attribution Materiel-Roulant

-

- Choisissez une ligne puis attribuez la composition des trains depuis la frise chronologique. -

-
-
+ // Enchaîner une rame vers une course suivante avec la règle des quais + const assignToNextHoraire = async (targetHoraire, materielId) => { + if (!canUpdate) return; + if (isRoadService(targetHoraire)) { + showToast("Impossible d'affecter du matériel ferroviaire à un service routier (Car / Autocar).", 'error'); + return; + } + setSaving(true); + try { + const materielObj = materiels.find((m) => String(m.id) === String(materielId)); + if (!materielObj) throw new Error('Matériel roulant introuvable'); -
- - + const currentComp = Array.isArray(targetHoraire.composition_train) ? targetHoraire.composition_train : []; + + const newPayload = [ + ...currentComp, + { + materiel_id: materielObj.id, + materiel_nom: materielObj.nom || '', + materiel_nom_technique: materielObj.nom_technique || '', + materiel_image_url: materielObj.image_url || null, + capacite: materielObj.capacite ? Number(materielObj.capacite) : null, + type_train: materielObj.type_train || '', + position: currentComp.length + 1 + } + ]; + + const { error: err } = await updateHoraireComposition(targetHoraire.id, newPayload); + if (err) { + showToast(err.message || "Erreur lors de l'affectation", 'error'); + } else { + // Règle des quais automatique + if (selectedHoraire) { + const quaiEval = evaluateQuaiContinuity(selectedHoraire, targetHoraire); + if (quaiEval && quaiEval.shouldKeepSameQuai && quaiEval.arrivalQuai) { + await updateQuaisHoraire(targetHoraire.id, { depart: quaiEval.arrivalQuai }); + showToast( + `Rame ${materielObj.nom} affectée au train ${targetHoraire.numero_train}. Quai ${quaiEval.arrivalQuai} automatiquement maintenu (Escale : ${quaiEval.deltaMins} min ≤ 45 min).` + ); + } else { + showToast(`La rame ${materielObj.nom} a été affectée au train ${targetHoraire.numero_train}.`); + } + } else { + showToast(`La rame ${materielObj.nom} a été affectée au train ${targetHoraire.numero_train}.`); + } + await loadData(); + } + } catch (err) { + console.error('Erreur assignToNextHoraire:', err); + showToast('Erreur inattendue', 'error'); + } finally { + setSaving(false); + } + }; + + const unassignMaterielFromHoraire = async (horaireId, materielId) => { + if (!canUpdate) return; + const targetHoraire = horaires.find((h) => String(h.id) === String(horaireId)); + if (!targetHoraire) return; + + const currentComp = Array.isArray(targetHoraire.composition_train) ? targetHoraire.composition_train : []; + const newComp = currentComp.filter((c) => String(c.materiel_id) !== String(materielId)); + + try { + const { error: err } = await updateHoraireComposition(horaireId, newComp); + if (err) { + showToast(err.message || 'Erreur lors du retrait', 'error'); + } else { + showToast(`Rame retirée du train ${targetHoraire.numero_train}.`); + await loadData(); + } + } catch (err) { + console.error('Erreur unassignMaterielFromHoraire:', err); + showToast('Erreur lors du retrait de la rame', 'error'); + } + }; + + const clearMaterielRoster = async (materielId) => { + if (!canUpdate) return; + const relevantHoraires = horaires.filter((h) => + Array.isArray(h.composition_train) && h.composition_train.some((c) => String(c.materiel_id) === String(materielId)) + ); + + if (relevantHoraires.length === 0) return; + + setSaving(true); + try { + for (const h of relevantHoraires) { + const updatedComp = h.composition_train.filter((c) => String(c.materiel_id) !== String(materielId)); + await updateHoraireComposition(h.id, updatedComp); + } + showToast(`Le roulement complet de cette rame a été libéré (${relevantHoraires.length} courses).`); + await loadData(); + } catch (err) { + console.error('Erreur clearMaterielRoster:', err); + showToast('Erreur lors de la libération du roulement', 'error'); + } finally { + setSaving(false); + } + }; + + // Mise à jour de la maintenance ou du dépôt d'attache d'un matériel + const handleUpdateMateriel = async (materielId, payload) => { + if (!canUpdate) return; + try { + const { error: err } = await updateMateriel(materielId, payload); + if (err) { + showToast(err.message || 'Erreur lors de la mise à jour du matériel', 'error'); + } else { + showToast('Fiche matériel mise à jour avec succès.'); + await loadData(); + } + } catch (e) { + console.error('Erreur handleUpdateMateriel:', e); + showToast('Erreur inattendue', 'error'); + } + }; + + // Gestion du couplage entre deux circulations horaires + const openCouplingModal = (horaire) => { + setCouplingHoraire(horaire); + setSelectedCouplingCandidateId(horaire.couplage_train_id || ''); + }; + + const handleSaveCoupling = async () => { + if (!couplingHoraire || !canUpdate) return; + setIsCouplingSaving(true); + + try { + const targetHoraire = horaires.find(h => String(h.id) === String(selectedCouplingCandidateId)); + + if (selectedCouplingCandidateId && targetHoraire) { + await updateHoraireCouplage( + couplingHoraire.id, + couplingHoraire.numero_train, + targetHoraire.id, + targetHoraire.numero_train + ); + showToast(`Couplage enregistré entre le Train ${couplingHoraire.numero_train} et le Train ${targetHoraire.numero_train}.`); + } else { + // Découplage + await updateHoraireCouplage(couplingHoraire.id, couplingHoraire.numero_train, null, null); + showToast(`Le Train ${couplingHoraire.numero_train} a été découplé.`); + } + + await loadData(); + setCouplingHoraire(null); + } catch (err) { + console.error('Erreur handleSaveCoupling:', err); + showToast('Erreur lors de la mise à jour du couplage', 'error'); + } finally { + setIsCouplingSaving(false); + } + }; + + // Matériels filtrés dans le catalogue de la modale + const activePickerList = useMemo(() => { + let list = []; + if (pickerCategory === 'station') list = materielsPresentsEnGare; + else if (pickerCategory === 'depot') list = materielsAuDepot; + else if (pickerCategory === 'circulation') list = materielsEnCirculation; + else list = materielsAilleurs; + + if (pickerSearchTerm) { + const q = pickerSearchTerm.toLowerCase(); + list = list.filter( + (m) => + String(m.nom || '').toLowerCase().includes(q) || + String(m.nom_technique || '').toLowerCase().includes(q) || + String(m.numero_serie || '').toLowerCase().includes(q) + ); + } + + return list; + }, [pickerCategory, pickerSearchTerm, materielsPresentsEnGare, materielsAuDepot, materielsEnCirculation, materielsAilleurs]); + + return ( +
+ {/* ================================================================= + 1. EN-TÊTE SNCF MÉTIER PLEINE LARGEUR + ================================================================= */} +
+
+ EXPLOITATION FERROVIAIRE · MATÉRIEL ROULANT & SILLONS +

Attribution des Rames & Gestion des Roulements

+

+ Système unifié d'affectation automatique et manuelle, préservation automatique des voies de quai, couplage de circulations et supervision du roulement. +

+
+ +
+ {/* Bouton Attribution Automatique Directe */} + + + Attribution automatique + + + {/* Configuration & Planification d'attribution automatique */} + + + {/* Bouton Optimisation Intelligente du Roulement */} + + + Optimiser le roulement + + +
+ + {formatHeure(lastRefreshed.toTimeString().slice(0, 5))}
- {error && ( - - Erreur - {error} - - )} - - {saveMessage && !selectedHoraire && ( - - Information - {saveMessage} - - )} - -
- - -
- - -
- {selectedLigne && ( - - {horairesLigne.length} horaire{horairesLigne.length > 1 ? 's' : ''} - - )} -
- -
- {!selectedLigneId && !loading && ( -
- -

Selectionnez une ligne pour afficher la frise chronologique.

-
- )} - - {loading && ( -
- -

Chargement des horaires...

-
- )} - - {!loading && selectedLigneId && horairesLigne.length === 0 && ( -
- -

Aucun horaire pour cette ligne.

-
- )} - - {!loading && horairesLigne.length > 0 && activeTab === 'liste' && ( -
-
-
-
-
Train
-
- {TIME_MARKERS_HOURS.map((hour) => ( -
{String(hour).padStart(2, '0')}h
- ))} -
-
- -
- {horairesLigne.map((horaire) => { - const { leftPct, widthPct } = getBarPlacement(horaire); - return ( -
-
- {horaire.numero_train || 'Train'} - {formatHeure(horaire.heure_depart)} -> {formatHeure(horaire.heure_arrivee)} -
- -
- -
-
- ); - })} -
-
-
-
- )} - - {!loading && selectedLigneId && activeTab === 'roulement' && ( - - )} -
+ + + Actualiser +
+
+ + {/* Toast de notification */} + {toastMessage && ( +
+ + {toastMessage.message} +
+ )} + + {error && ( + + Erreur système + {error} + + )} + + {/* ================================================================= + 2. STRIP DE SYNTHÈSE DES KPIS GLOBAUX DU PARC + ================================================================= */} +
+
+
+ +
+
+ Flotte Roulante + {globalKpis.totalMateriels} + + {globalKpis.activeMaterielsCount} en service · {globalKpis.depotMaterielsCount} au dépôt · {globalKpis.inMaintenanceCount} en atelier + +
+
+ +
+
+ +
+
+ Couverture des Sillons + {globalKpis.coverageRate}% +
+
+
+
+
+ +
+
+ +
+
+ Sillons sans Rame + {globalKpis.unassignedHorairesCount} + + sur {globalKpis.totalHorairesCount} circulations programmées + +
+
+ +
+
0 ? 'icon-red' : 'icon-teal'}`}> + 0 ? 'warning' : 'verified'} size="m"> +
+
+ Alertes de Roulement + {globalKpis.totalConflictsCount} + + {globalKpis.totalTightTurnarounds} escales tendues (<15m) + +
+
+
+ + {/* ================================================================= + 3. ONGLETS MAÎTRES WCS : ATTRIBUTION vs ROULEMENTS + ================================================================= */} +
+ + + +
- {selectedHoraire && ( -
-
e.stopPropagation()}> -
-

Attribution materiel - {selectedHoraire.numero_train || 'Train'}

-
- - -
- -
- -
- {modalActiveTab === 'composition' && ( - <> -
- {selectedHoraire.gare_depart_nom || '-'} ({formatHeure(selectedHoraire.heure_depart)}) - {selectedHoraire.gare_arrivee_nom || '-'} ({formatHeure(selectedHoraire.heure_arrivee)}) -
- -
-
- Composition du train - {compositionDraft.length} element{compositionDraft.length > 1 ? 's' : ''} -
- - {compositionDraft.length === 0 ? ( -
Aucun materiel attribue.
- ) : ( -
- {compositionDraft.map((item, idx) => ( -
- #{idx + 1} -
- {item.materiel_nom || item.materiel_nom_technique || 'Materiel'} - {item.materiel_nom_technique || '-'} -
-
- - - -
-
+ {/* ================================================================= + 4. CONTENU ONGLET 1 : ATTRIBUTION DES RAMES AUX HORAIRES + ================================================================= */} + {activeTab === 'attribution' && ( +
+ {/* Barre de commande & filtres */} +
+
+ {/* Sélecteur de Ligne */} +
+ +
+
- {materiels.length === 0 ? ( -
Aucun materiel disponible.
- ) : ( -
-
-
- Disponibles à {selectedHoraire?.gare_depart_nom || 'la gare de depart'} ({materielsPresentsEnGare.length}) -
- {materielsPresentsEnGare.length === 0 ? ( -
Aucun matériel n'est arrivé en gare avant ce départ.
- ) : ( -
- {materielsPresentsEnGare.map((materiel) => ( - - ))} -
- )} -
- -
-
Au depot ({materielsAuDepot.length})
- {materielsAuDepot.length === 0 ? ( -
Aucun materiel au depot.
- ) : ( -
- {materielsAuDepot.map((materiel) => ( - - ))} -
- )} -
- -
-
En circulation / Affecté ({materielsEnCirculation.length})
- {materielsEnCirculation.length === 0 ? ( -
Aucun matériel en circulation à cette heure.
- ) : ( -
- {materielsEnCirculation.map((materiel) => ( - - ))} -
- )} -
- -
-
Indisponibles (Ailleurs) ({materielsAilleurs.length})
- {materielsAilleurs.length === 0 ? null : ( -
- {materielsAilleurs.map((materiel) => ( - - ))} -
- )} -
- -
- )}
- - )} - {modalActiveTab === 'affectation' && ( -
- {compositionDraft.length === 0 ? ( -
- Aucun matériel dans la composition de ce train. Veuillez d'abord ajouter un matériel dans l'onglet "Composition". -
- ) : ( - <> -
- - -
- -
- Statut : Ce matériel arrivera à {selectedHoraire.gare_arrivee_nom} à {formatHeure(selectedHoraire.heure_arrivee)}. -
- -
- setAffectationSearchTerm(e.target.value)} - style={{ padding: '10px', width: '100%', borderRadius: '4px', border: '1px solid #ccc' }} - /> -
- -
- {(() => { - const targetMins = timeToMinutes(selectedHoraire.heure_arrivee); - const targetGareId = selectedHoraire.gare_arrivee_id; - const targetGareNom = selectedHoraire.gare_arrivee_nom?.toLowerCase(); - - const eligibleHoraires = horaires.filter(h => { - // Ne pas se proposer soi-même - if (h.id === selectedHoraire.id) return false; - - // Doit partir de la gare d'arrivée - const isSameGare = (String(h.gare_depart_id) === String(targetGareId)) || - (h.gare_depart_nom?.toLowerCase() === targetGareNom); - if (!isSameGare) return false; - - // Doit partir après l'arrivée - if (timeToMinutes(h.heure_depart) < targetMins) return false; - - // Search term - if (affectationSearchTerm) { - const searchLower = affectationSearchTerm.toLowerCase(); - if (!h.numero_train?.toLowerCase().includes(searchLower) && - !h.gare_arrivee_nom?.toLowerCase().includes(searchLower)) { - return false; - } - } - return true; - }).sort((a, b) => timeToMinutes(a.heure_depart) - timeToMinutes(b.heure_depart)); - - if (eligibleHoraires.length === 0) { - return
Aucun train de correspondance trouvé.
; - } - - return eligibleHoraires.map(h => { - const activeMaterielId = selectedAffectationMaterielId || compositionDraft[0]?.materiel_id; - const isAlreadyAssigned = (h.composition_train || []).some(c => String(c.materiel_id) === String(activeMaterielId)); - - return ( -
-
- {h.numero_train} vers {h.gare_arrivee_nom}
- Départ à {formatHeure(h.heure_depart)} -
-
- -
-
- ); - }); - })()} -
- - )} + {/* Sens de circulation */} + {selectedLigne && ( +
+ + +
)} - {!canUpdate &&

Vous avez un acces en lecture seule.

} - {!!saveMessage && selectedHoraire &&

{saveMessage}

} + {/* Recherche de sillon */} +
+ + setSearchHoraireTerm(e.target.value)} + /> + {searchHoraireTerm && ( + + )} +
+ + {/* Commutateur de sous-vue */} +
+ + +
+
+ + {/* Filtres de statut d'affectation */} +
+
+ Filtrer : + + + + + + +
+ + + {filteredHoraires.length} circulation{filteredHoraires.length > 1 ? 's' : ''} affichée{filteredHoraires.length > 1 ? 's' : ''} + +
+
+ + {/* Affichage des données (Cartes vs Timeline) */} + {loading ? ( +
+ +

Chargement des horaires et des attributions matérielles…

+
+ ) : filteredHoraires.length === 0 ? ( +
+ +

Aucune circulation trouvée

+

Modifiez vos filtres ou sélectionnez une autre ligne pour afficher les horaires.

+
+ ) : attributionViewMode === 'cards' ? ( + /* ============================================================= + SOUS-VUE A : GRILLE DES CARTES DE CIRCULATION + ============================================================= */ +
+ {filteredHoraires.map((horaire) => { + const analysis = analyzeHoraireComposition(horaire, timelines, materiels); + const comp = analysis.assignedUnits; + const isCoupled = !!horaire.couplage_train_id; + + return ( +
+ {/* En-tête de la carte */} +
+
+ Train {horaire.numero_train || 'Sillon'} + {horaire.type_train && ( + {horaire.type_train} + )} + {horaire.isAlternative && ( + + 🔀 Variante {horaire.variantIndex}/{horaire.totalVariants} + + )} +
+ +
+ + {horaire.ligne_nom || 'Ligne libre'} +
+
+ + {/* Itinéraire et horaires */} +
+
+
{formatHeure(horaire.heure_depart)}
+
{horaire.gare_depart_nom || '-'}
+
+ +
+ → +
+ +
+
{formatHeure(horaire.heure_arrivee)}
+
{horaire.gare_arrivee_nom || '-'}
+
+
+ + {/* Badge de Couplage */} + {isCoupled && ( +
+ + Circulation couplée avec le Train {horaire.couplage_numero_train} (Tranche commune) +
+ )} + + {/* Diagnostic & Alertes de roulement */} + {analysis.hasAnomalies && ( +
+ {analysis.anomalies.map((anom, aIdx) => ( +
+ + {anom.message} +
+ ))} +
+ )} + + {/* Composition visuelle du train */} +
+
+ {analysis.isRoadService ? 'Service Routier :' : 'Composition du train :'} + {analysis.isRoadService ? ( + + + Autocar (Routier) + + ) : analysis.hasComposition ? ( + {analysis.compositionType} + ) : ( + Rame manquante + )} +
+ + {analysis.isRoadService ? ( +
+ + Liaison assurée par autocar · Pas de matériel ferroviaire requis +
+ ) : !analysis.hasComposition ? ( +
openCompositionModal(horaire)} + > + + Aucune rame attribuée · Cliquez pour composer +
+ ) : ( +
+
+ +
+ + {comp.map((unit, uIdx) => ( +
+
#{uIdx + 1}
+
+ {unit.nom} + {unit.nom_technique && ( + {unit.nom_technique} + )} +
+
+ ))} +
+ )} + + {analysis.hasComposition && !analysis.isRoadService && ( +
+ + + {analysis.totalCapacity} places assises + + + {comp.length} rame{comp.length > 1 ? 's' : ''} + +
+ )} +
+ + {/* Actions de la carte */} +
+ {!analysis.isRoadService ? ( + <> + openCompositionModal(horaire)} + disabled={!canUpdate} + > + + {analysis.hasComposition ? 'Modifier composition' : 'Composer le train'} + + + {/* Bouton de Couplage */} + openCouplingModal(horaire)} + disabled={!canUpdate} + title="Associer ou coupler ce train avec une autre circulation" + > + + {isCoupled ? 'Découpler' : 'Couplement'} + + + ) : ( +
+ + Service routier par autocar — Pas de matériel ferroviaire +
+ )} + + {analysis.hasComposition && ( + { + if (window.confirm(`Confirmez-vous le retrait de la composition du train ${horaire.numero_train} ?`)) { + await updateHoraireComposition(horaire.id, []); + await loadData(); + showToast(`Composition du train ${horaire.numero_train} vidée.`); + } + }} + > + + Vider + + )} +
+
+ ); + })} +
+ ) : ( + /* ============================================================= + SOUS-VUE B : FRISE CHRONOLOGIQUE 24H DES SILLONS + ============================================================= */ +
+
+
+
+
Train / Sillon
+
+ {TIME_MARKERS_HOURS.map((h) => ( +
+ {String(h).padStart(2, '0')}:00 +
+ ))} +
+
+ +
+ {filteredHoraires.map((horaire) => { + const { leftPct, widthPct } = getBarPlacement(horaire); + const analysis = analyzeHoraireComposition(horaire, timelines, materiels); + + let barVariant = 'bar-nominal'; + if (analysis.isRoadService) barVariant = 'bar-road-service'; + else if (!analysis.hasComposition) barVariant = 'bar-unassigned'; + else if (analysis.hasAnomalies) barVariant = 'bar-conflict'; + + return ( +
+
+ + {analysis.isRoadService ? '🚌 ' : ''}{horaire.numero_train || 'Sillon'} + {horaire.isAlternative && ( + (V{horaire.variantIndex}) + )} + + + {formatHeure(horaire.heure_depart)} → {formatHeure(horaire.heure_arrivee)} + +
+ +
+ +
+
+ ); + })} +
+
+
+
+ )} +
+ )} + + {/* ================================================================= + 5. CONTENU ONGLET 2 : GESTION DES ROULEMENTS & FLOTTE + ================================================================= */} + {activeTab === 'roulements' && ( +
+ +
+ )} + + {/* ===================================================================== + 6. MODALE MAÎTRESSE : CONSTRUCTEUR DE COMPOSITION DU TRAIN + ===================================================================== */} + {selectedHoraire && ( +
+
e.stopPropagation()} + > +
+
+
CONSTRUCTEUR DE COMPOSITION FERROVIAIRE
+

+ Train {selectedHoraire.numero_train || 'Sillon'} · {selectedHoraire.ligne_nom || 'Ligne'} +

+
+ {selectedHoraire.gare_depart_nom} ({formatHeure(selectedHoraire.heure_depart)}) + → + {selectedHoraire.gare_arrivee_nom} ({formatHeure(selectedHoraire.heure_arrivee)}) +
+
+ +
+ + +
+ + +
+ +
+ {currentHoraireAnalysis && currentHoraireAnalysis.hasAnomalies && ( +
+ +
+ Avertissement sur le roulement : +
    + {currentHoraireAnalysis.anomalies.map((anom, idx) => ( +
  • {anom.message}
  • + ))} +
+
+
+ )} + + {modalActiveTab === 'composition' ? ( +
+ {/* COLONNE GAUCHE : COMPOSITION EN COURS */} +
+
+
+

Composition Assemblée

+ + {compositionDraft.length === 1 + ? 'US (Unité Simple)' + : compositionDraft.length > 1 + ? `UM${compositionDraft.length} (Unité Multiple)` + : 'Aucune rame affectée'} + +
+ +
+ + {currentHoraireAnalysis?.totalCapacity || 0} places +
+
+ + {compositionDraft.length === 0 ? ( +
+ +

Aucun matériel roulant ajouté à ce train.

+ Sélectionnez une rame disponible dans le panneau de droite. +
+ ) : ( +
+ {compositionDraft.map((item, idx) => ( +
+
+ #{idx + 1} + + {idx === 0 + ? 'Tête' + : idx === compositionDraft.length - 1 + ? 'Queue' + : 'Milieu'} + +
+ +
+ {item.materiel_image_url ? ( + {item.materiel_nom} + ) : ( + + )} +
+ +
+ {item.materiel_nom} + {item.materiel_nom_technique || '-'} +
+ {item.type_train && {item.type_train}} + {item.capacite > 0 && {item.capacite} pl.} +
+
+ +
+ + + +
+
+ ))} +
+ )} +
+ + {/* COLONNE DROITE : SÉLECTEUR INTELLIGENT DE MATÉRIEL DISPONIBLE */} +
+
+
+

Matériels Roulants Disponibles

+ + Départ de {selectedHoraire.gare_depart_nom} à {formatHeure(selectedHoraire.heure_depart)} + +
+
+ +
+ + + + +
+ +
+ + setPickerSearchTerm(e.target.value)} + /> +
+ +
+ {activePickerList.length === 0 ? ( +
+ +

Aucun matériel dans cette catégorie pour cette heure.

+
+ ) : ( + activePickerList.map((mat) => { + const statusInfo = mat.statusInfo || {}; + const isCirculating = statusInfo.status === 'EN_CIRCULATION'; + const isElsewhere = statusInfo.status === 'AILLEURS'; + const isAtelier = mat.statut_maintenance === 'EN_AVARIE_ATELIER'; + + return ( +
+
+ {mat.image_url ? ( + {mat.nom + ) : ( + + )} +
+ +
+ {mat.nom || mat.nom_technique} + {mat.nom_technique && {mat.nom_technique}} + +
+ {mat.type_train && {mat.type_train}} + {mat.capacite && {mat.capacite} places} + {mat.depot_attache && 📍 {mat.depot_attache.split(' ')[0]}} +
+ + {statusInfo.status === 'PRESENT_EN_GARE' && ( +
+ + + Escale : {statusInfo.turnaroundMins} min + {statusInfo.turnaroundMins <= 45 ? ' · Même quai conservé' : ' · Voie libre'} + +
+ )} + + {isAtelier && ( +
+ + Immobilisé en atelier de maintenance +
+ )} + + {isCirculating && ( +
+ + En ligne sur Train {statusInfo.event?.numero_train} +
+ )} + + {isElsewhere && ( +
+ + Stationné à {statusInfo.lastEvent?.gare_arrivee_nom} +
+ )} +
+ +
+ addMaterielToDraft(mat)} + > + + Ajouter + +
+
+ ); + }) + )} +
+
+
+ ) : ( + /* ONGLET B : ENCHAÎNEMENT VERS LA COURSE SUIVANTE */ +
+ {compositionDraft.length === 0 ? ( +
+ +

Aucun matériel dans la composition

+

Ajoutez d'abord une rame dans l'onglet « Composition » pour lui planifier un enchaînement après son arrivée.

+
+ ) : ( +
+
+ + +
+ +
+ +
+ Arrivée prévue à {selectedHoraire.gare_arrivee_nom} à {formatHeure(selectedHoraire.heure_arrivee)}. +
+ Les trains suivants partent de cette même gare après l'heure d'arrivée. +
+
+ +
+ setChainingSearchTerm(e.target.value)} + /> +
+ +
+ {(() => { + const targetMins = timeToMinutes(selectedHoraire.heure_arrivee); + const targetGareId = selectedHoraire.gare_arrivee_id; + const targetGareNom = (selectedHoraire.gare_arrivee_nom || '').toLowerCase(); + const activeMatId = selectedChainingMaterielId || compositionDraft[0]?.materiel_id; + + const eligible = horaires + .filter((h) => { + if (String(h.id) === String(selectedHoraire.id)) return false; + + const isSameGare = + (targetGareId && String(h.gare_depart_id) === String(targetGareId)) || + (h.gare_depart_nom || '').toLowerCase() === targetGareNom; + if (!isSameGare) return false; + + const departMins = timeToMinutes(h.heure_depart); + if (departMins === null || departMins < targetMins) return false; + + if (chainingSearchTerm) { + const q = chainingSearchTerm.toLowerCase(); + const mNum = String(h.numero_train || '').toLowerCase().includes(q); + const mDest = String(h.gare_arrivee_nom || '').toLowerCase().includes(q); + if (!mNum && !mDest) return false; + } + + return true; + }) + .sort((a, b) => timeToMinutes(a.heure_depart) - timeToMinutes(b.heure_depart)); + + if (eligible.length === 0) { + return ( +
+ +

Aucun train de correspondance trouvé au départ de {selectedHoraire.gare_arrivee_nom} après {formatHeure(selectedHoraire.heure_arrivee)}.

+
+ ); + } + + return eligible.map((h) => { + const isAlreadyAssigned = (h.composition_train || []).some( + (c) => String(c.materiel_id) === String(activeMatId) + ); + const dwell = timeToMinutes(h.heure_depart) - targetMins; + + return ( +
+
+
+ Train {h.numero_train} + vers {h.gare_arrivee_nom} +
+
+ Départ à {formatHeure(h.heure_depart)} + + Escale : {dwell} min {dwell <= 45 ? '(Même quai conservé)' : '(Garage faisceau possible)'} + +
+
+ +
+ assignToNextHoraire(h, activeMatId)} + > + {isAlreadyAssigned ? 'Déjà affecté' : 'Affecter'} + +
+
+ ); + }); + })()} +
+
+ )} +
+ )}
- Annuler - - {saving ? 'Enregistrement...' : 'Enregistrer la composition'} + + Annuler + + + Enregistrer la composition
)} + + {/* ===================================================================== + 7. MODALE DE COUPLAGE AVEC UN AUTRE TRAIN + ===================================================================== */} + {couplingHoraire && ( +
setCouplingHoraire(null)}> +
e.stopPropagation()}> +
+
+ +

Couplement avec un autre train · Train {couplingHoraire.numero_train}

+
+ +
+ +
+
+
+ Train source : {couplingHoraire.numero_train} + {couplingHoraire.gare_depart_nom} ({formatHeure(couplingHoraire.heure_depart)}) → {couplingHoraire.gare_arrivee_nom} +
+
+ Composition actuelle : {(couplingHoraire.composition_train || []).length} rame(s) + {couplingHoraire.couplage_train_id && ( + + Actuellement couplé avec Train {couplingHoraire.couplage_numero_train} + + )} +
+
+ +
+ + + {(() => { + const targetDepartMins = timeToMinutes(couplingHoraire.heure_depart); + const targetGareId = couplingHoraire.gare_depart_id; + const targetGareNom = (couplingHoraire.gare_depart_nom || '').toLowerCase(); + + // Trouver les trains partant de la même gare au même moment (± 15 min) + const candidates = horaires.filter(h => { + if (String(h.id) === String(couplingHoraire.id)) return false; + if (isRoadService(h)) return false; + + const isSameOrigin = + (targetGareId && String(h.gare_depart_id) === String(targetGareId)) || + (h.gare_depart_nom || '').toLowerCase() === targetGareNom; + if (!isSameOrigin) return false; + + const depM = timeToMinutes(h.heure_depart); + if (depM === null) return false; + return Math.abs(depM - targetDepartMins) <= 20; + }); + + return ( +
+
setSelectedCouplingCandidateId('')} + > + setSelectedCouplingCandidateId('')} + /> +
+ Aucun couplage (Circulation autonome / Découpler) + Le train circulera seul en unité simple ou multiple dédiée. +
+
+ + {candidates.length === 0 ? ( +
+ + Aucun autre train au départ de {couplingHoraire.gare_depart_nom} dans le même créneau horaire. +
+ ) : ( + candidates.map(cand => { + const isSelected = String(selectedCouplingCandidateId) === String(cand.id); + + return ( +
setSelectedCouplingCandidateId(cand.id)} + > + setSelectedCouplingCandidateId(cand.id)} + /> +
+
+ Train {cand.numero_train} + vers {cand.gare_arrivee_nom} +
+
+ Départ à {formatHeure(cand.heure_depart)} de {cand.gare_depart_nom} + {(cand.composition_train || []).length} rame(s) +
+
+
+ ); + }) + )} +
+ ); + })()} +
+ +
+ + + Le couplage permet d'unir deux trains circulant sur un même tronçon (tranche commune) sur le même quai et de synchroniser leurs rames en unité multiple (UM). + +
+
+ +
+ setCouplingHoraire(null)} disabled={isCouplingSaving}> + Annuler + + + {selectedCouplingCandidateId ? 'Confirmer le couplage' : 'Enregistrer'} + +
+
+
+ )} + + {/* ===================================================================== + 8. MODALE PARAMÈTRES D'ATTRIBUTION AUTOMATIQUE + ===================================================================== */} + {showAutoConfigModal && ( +
setShowAutoConfigModal(false)}> +
e.stopPropagation()}> +
+
+ +

Paramètres d'Attribution Automatique

+
+ +
+ +
+
+
+
+ Planification automatique quotidienne : +

+ Exécute automatiquement l'algorithme d'attribution des rames à l'heure programmée chaque jour. +

+
+ +
+ +
+ + setAutoScheduleTime(e.target.value)} + disabled={!autoScheduleEnabled} + className="config-time-input" + /> +
+
+ +
+

Règles ferroviaires appliquées :

+
    +
  • Priorité aux rames arrivées en gare de départ avec escale nominale (15 à 90 min).
  • +
  • Règle des quais : Maintien automatique du même quai si l'escale est ≤ 45 min.
  • +
  • Prise en compte des stocks au dépôt d'attache et exclusion des rames en atelier.
  • +
  • Vérification systématique de la compatibilité d'attelage pour les unités multiples.
  • +
+
+ + {lastAutoRunResult && ( +
+ Dernier passage automatique : + {lastAutoRunResult.totalAssigned} sillons pourvus avec succès · {lastAutoRunResult.totalQuaisPreserved} voies de quai maintenues. +
+ )} +
+ +
+ + + Lancer maintenant + + + + Enregistrer la planification + +
+
+
+ )} + + {/* ===================================================================== + 9. MODALE OPTIMISATION INTELLIGENTE DU ROULEMENT + ===================================================================== */} + {showOptimizationModal && ( +
setShowOptimizationModal(false)}> +
e.stopPropagation()}> +
+
+ +
+

Optimisation Intelligente des Roulements

+ + Résolution automatique des ruptures, équilibrage des kilométrages, escales nominales et harmonisation des voies de quai. + +
+
+ +
+ +
+ {/* Panneau de configuration de l'algorithme */} +
+
+
+ Résoudre les ruptures géographiques + +
+

+ Réassigne les sillons ayant des ruptures de gare ou des chevauchements d'horaires impossibles. +

+
+ +
+
+ Équilibrage de la flotte + +
+

+ Répartit équitablement le nombre de trajets et l'usure kilométrique sur toutes les rames disponibles. +

+
+ +
+
+ Bouclage Technicentre d'attache + +
+

+ Favorise les enchaînements ramenant la rame vers son atelier d'attache en fin de journée pour la maintenance de nuit. +

+
+ +
+
+ Uniquement les sillons sans rame + +
+

+ Préserve l'intégralité des compositions existantes et comble uniquement les sillons vacants. +

+
+
+ + {/* Résultats comparatifs de la simulation */} + {optimizationPreview && ( +
+

+ + Diagnostic & Synthèse de la Simulation +

+ +
+
+ Taux de Couverture +
+ {optimizationPreview.statsBefore.coverageRate}% + + {optimizationPreview.statsAfter.coverageRate}% +
+ + {optimizationPreview.newAssignedCount} train(s) optimisé(s) + +
+ +
+ Conflits & Ruptures +
+ {optimizationPreview.statsBefore.totalConflictsCount} + + {optimizationPreview.statsAfter.totalConflictsCount} +
+ + {optimizationPreview.conflictsResolved} anomalie(s) résolue(s) + +
+ +
+ Voies de Quai Préservées +
+ 0 + + +{optimizationPreview.quaisPreservedCount} +
+ + Maintien à quai (escale ≤ 45 min) + +
+ +
+ Rames Mobilisées +
+ {optimizationPreview.statsAfter.activeMaterielsCount} + / {optimizationPreview.statsAfter.totalMateriels} +
+ + {optimizationPreview.statsAfter.depotMaterielsCount} en réserve au dépôt + +
+
+ + {/* Journal des actions d'optimisation */} + {optimizationPreview.actionsLog && optimizationPreview.actionsLog.length > 0 && ( +
+
+ Détail des {optimizationPreview.actionsLog.length} décisions d'enchaînement : + + Optimisation nominale + +
+
+ {optimizationPreview.actionsLog.map((log, idx) => ( +
+ + {log} +
+ ))} +
+
+ )} +
+ )} +
+ +
+ setShowOptimizationModal(false)} disabled={isOptimizing}> + Fermer + + + + + Recalculer la simulation + + + + + Appliquer l'optimisation au plan ({optimizationPreview?.planUpdates?.length || 0}) + +
+
+
+ )} + + {/* ===================================================================== + 10. WCS FOOTER CORPORATE SNCF (OFFICIEL) + ===================================================================== */} + +
+
+ +
+
+ + FERROVIA PANEL · DIRECTION DU MATÉRIEL & DU ROULEMENT + + + Système certifié WCS SNCF (Web Component System) & Module d'Exploitation Ferroviaire + +
+
+ +
+ + © {new Date().getFullYear()} SNCF · Supervision Flotte, Dépôts, Couplages & Attribution des Sillons + +
+ +
+
+ + + Supabase Connecté + + + v2.6.0-WCS + +
+
+
); } \ No newline at end of file diff --git a/src/pages/attribution-materiel-roulant/VueRoulement.css b/src/pages/attribution-materiel-roulant/VueRoulement.css index 304ccfd..6a8dcb8 100644 --- a/src/pages/attribution-materiel-roulant/VueRoulement.css +++ b/src/pages/attribution-materiel-roulant/VueRoulement.css @@ -1,250 +1,1295 @@ -.vue-roulement-container { - padding: 20px; - background-color: #f4f7fa; -} +/* ========================================================================== + VUE ROULEMENT & GESTION DE FLOTTE - SNCF MÉTIER AVANCÉ + ========================================================================== */ -.vue-roulement-header { - margin-bottom: 20px; -} - -.roulement-planning { - display: grid; - grid-template-columns: 1fr; - gap: 24px; -} - -.materiel-card { - background-color: #ffffff; - border: 1px solid #e5e7eb; - border-radius: 12px; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.04); -} - -.materiel-header { - padding: 16px 20px; - border-bottom: 1px solid #e5e7eb; -} - -.materiel-header h3 { - margin: 0; - font-size: 1.2rem; - font-weight: 600; - color: #1f2937; -} - -.materiel-header p { - margin: 4px 0 0; - font-size: 0.9rem; - color: #6b7280; -} - -.materiel-body { - padding: 20px; +.vue-roulement-root { display: flex; flex-direction: column; - gap: 20px; -} - -.ligne-section h4 { - margin: 0 0 12px; - font-size: 1rem; - font-weight: 600; - color: #374151; -} - -.timeline-container { - position: relative; + gap: 16px; width: 100%; } -.timeline-header { - display: grid; - grid-template-columns: repeat(12, 1fr); - border-bottom: 1px solid #d1d5db; - padding-bottom: 4px; - margin-bottom: 8px; +/* 1. BARRE D'OUTILS ET FILTRES */ +.roulement-toolbar-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 14px 18px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + display: flex; + flex-direction: column; + gap: 12px; } -.timeline-marker { - font-size: 0.75rem; - color: #9ca3af; - text-align: left; -} - -.timeline-body { - position: relative; - height: 40px; - background-image: repeating-linear-gradient(to right, transparent 0, transparent calc(100% / 12 - 1px), #e5e7eb calc(100% / 12 - 1px), #e5e7eb calc(100% / 12)); -} - -.timeline-bar { - position: absolute; - top: 50%; - transform: translateY(-50%); - height: 28px; - background-color: #3b82f6; - border-radius: 6px; - color: white; +.roulement-toolbar-row { display: flex; align-items: center; - padding: 0 8px; + gap: 12px; + flex-wrap: wrap; +} + +.roulement-search-wrap { + position: relative; + display: flex; + align-items: center; + min-width: 280px; + flex: 1; +} + +.roulement-search-wrap wcs-mat-icon { + position: absolute; + left: 10px; + color: #64748b; + pointer-events: none; +} + +.roulement-search-input { + width: 100%; + padding: 8px 32px 8px 34px; + border: 1px solid #cbd5e1; + border-radius: 8px; + font-size: 13px; + color: #1e293b; + background: #f8fafc; + outline: none; + transition: all 0.2s; +} + +.roulement-search-input:focus { + border-color: #0088ce; + background: #ffffff; + box-shadow: 0 0 0 3px rgba(0, 136, 206, 0.15); +} + +.roulement-clear-btn { + position: absolute; + right: 8px; + background: none; + border: none; + color: #94a3b8; + font-size: 16px; cursor: pointer; - transition: background-color 0.2s; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } -.timeline-bar:hover { - background-color: #2563eb; +.roulement-filter-group { + display: flex; + align-items: center; + gap: 6px; } -.bar-label { - font-size: 0.8rem; +.roulement-filter-label { + font-size: 11px; + font-weight: 700; + color: #475569; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.roulement-select { + padding: 6px 10px; + border-radius: 8px; + border: 1px solid #cbd5e1; + background: #ffffff; + color: #1e293b; + font-size: 12px; + font-weight: 600; + outline: none; + cursor: pointer; +} + +.roulement-status-pills { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.roulement-status-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + border-radius: 999px; + border: 1px solid #e2e8f0; + background: #f8fafc; + color: #334155; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; +} + +.roulement-status-pill:hover { + background: #f1f5f9; + border-color: #cbd5e1; +} + +.roulement-status-pill.active { + background: #002244; + color: #ffffff; + border-color: #002244; +} + +.roulement-status-pill.pill-active.active { + background: #0284c7; + border-color: #0284c7; +} + +.roulement-status-pill.pill-depot.active { + background: #475569; + border-color: #475569; +} + +.roulement-status-pill.pill-maintenance.active { + background: #7c3aed; + border-color: #7c3aed; +} + +.roulement-status-pill.pill-anomaly.active { + background: #dc2626; + border-color: #dc2626; +} + +.roulement-view-switch { + display: flex; + background: #f1f5f9; + border-radius: 8px; + padding: 3px; + margin-left: auto; +} + +.view-switch-btn { + display: inline-flex; + align-items: center; + gap: 6px; + border: none; + background: transparent; + color: #64748b; + padding: 5px 10px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; +} + +.view-switch-btn.is-active { + background: #ffffff; + color: #0f172a; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.roulement-sub-options { + display: flex; + align-items: center; + border-top: 1px solid #f1f5f9; + padding-top: 8px; +} + +.manoeuvre-toggle-label { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: #475569; font-weight: 500; + cursor: pointer; +} + +.manoeuvre-indicator-swatch { + width: 14px; + height: 14px; + border-radius: 3px; + background: repeating-linear-gradient(45deg, #a855f7 0, #a855f7 4px, #9333ea 4px, #9333ea 8px); +} + +/* 2. GANTT FLOTTE 24H AVEC MANŒUVRES & COUPLAGES */ +.roulement-gantt-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); +} + +.roulement-gantt-legend { + display: flex; + align-items: center; + gap: 18px; + padding: 10px 16px; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + font-size: 12px; + color: #475569; + flex-wrap: wrap; +} + +.legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.legend-dot { + width: 10px; + height: 10px; + border-radius: 2px; +} + +.dot-normal { background: #0284c7; } +.dot-tight { background: #d97706; } +.dot-danger { background: #dc2626; } +.dot-manoeuvre { background: repeating-linear-gradient(45deg, #a855f7 0, #a855f7 2px, #7e22ce 2px, #7e22ce 4px); } +.dot-idle { background: #94a3b8; } + +.roulement-gantt-scrollable { + max-height: 68vh; + overflow: auto; +} + +.roulement-gantt-table { + --gantt-hour-width: 90px; + --gantt-hours-count: 13; + --gantt-rame-col-width: 320px; + min-width: calc(var(--gantt-rame-col-width) + (var(--gantt-hour-width) * var(--gantt-hours-count))); + width: 100%; +} + +.roulement-gantt-header-row { + display: grid; + grid-template-columns: var(--gantt-rame-col-width) 1fr; + position: sticky; + top: 0; + z-index: 5; + background: #f1f5f9; + border-bottom: 1px solid #cbd5e1; +} + +.gantt-rame-col-header { + padding: 10px 14px; + font-weight: 700; + font-size: 12px; + text-transform: uppercase; + color: #334155; + letter-spacing: 0.05em; + background: #f1f5f9; + position: sticky; + left: 0; + z-index: 6; + border-right: 1px solid #cbd5e1; +} + +.gantt-hours-scale { + display: grid; + grid-template-columns: repeat(var(--gantt-hours-count), 1fr); +} + +.gantt-hour-marker { + padding: 10px 4px; + font-size: 12px; + font-weight: 600; + color: #475569; + text-align: center; + border-left: 1px dashed #cbd5e1; +} + +.roulement-gantt-row { + display: grid; + grid-template-columns: var(--gantt-rame-col-width) 1fr; + min-height: 64px; + border-bottom: 1px solid #e2e8f0; + background: #ffffff; + transition: background 0.15s; +} + +.roulement-gantt-row:hover { + background: #fafcff; +} + +.gantt-rame-cell { + position: sticky; + left: 0; + z-index: 4; + background: inherit; + border-right: 1px solid #e2e8f0; + padding: 8px 12px; + display: flex; + align-items: center; + gap: 10px; +} + +.rame-cell-avatar { + width: 48px; + height: 40px; + border-radius: 6px; + background: #f1f5f9; + border: 1px solid #cbd5e1; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + flex-shrink: 0; +} + +.rame-cell-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.train-silhouette-placeholder { + display: flex; + align-items: center; + justify-content: center; + color: #002244; +} + +.rame-cell-meta { + flex: 1; + min-width: 0; +} + +.rame-title { + display: block; + font-size: 13px; + font-weight: 700; + color: #0f172a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.7); +.rame-sub-badges { display: flex; - justify-content: center; - align-items: center; - z-index: 1000; + gap: 4px; + margin-top: 2px; + flex-wrap: wrap; } -.modal-content { - background-color: white; - padding: 24px; - border-radius: 8px; - width: 600px; - max-width: 95%; - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +.mini-badge-type { + background: #e0f2fe; + color: #0369a1; + padding: 1px 5px; + border-radius: 4px; + font-size: 10px; + font-weight: 700; } -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; - border-bottom: 1px solid #e5e7eb; - padding-bottom: 16px; +.mini-badge-cap { + background: #f1f5f9; + color: #475569; + padding: 1px 5px; + border-radius: 4px; + font-size: 10px; } -.modal-header h3 { - margin: 0; - font-size: 1.25rem; +.mini-badge-depot { + background: #fef3c7; + color: #92400e; + padding: 1px 5px; + border-radius: 4px; + font-size: 10px; font-weight: 600; } -.modal-close { +.rame-cell-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.gantt-alert-badge { + padding: 2px 6px; + border-radius: 4px; + font-size: 10px; + font-weight: 700; + display: inline-flex; + align-items: center; + gap: 3px; +} + +.alert-danger { background: #fee2e2; color: #b91c1c; } +.alert-warning { background: #fef3c7; color: #b45309; } +.alert-nominal { background: #ecfdf5; color: #047857; } +.alert-depot { background: #f1f5f9; color: #64748b; } +.alert-maintenance { background: #ede9fe; color: #6d28d9; } + +.gantt-action-icon-btn { + background: transparent; + border: 1px solid #cbd5e1; + border-radius: 6px; + color: #475569; + padding: 4px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.15s; +} + +.gantt-action-icon-btn:hover { + background: #f1f5f9; + color: #0f172a; +} + +.gantt-action-icon-btn.primary { + background: #0088ce; + border-color: #0088ce; + color: #ffffff; +} + +.gantt-action-icon-btn.primary:hover { + background: #0070ab; +} + +.gantt-track-cell { + position: relative; + min-height: 64px; +} + +.gantt-track-grid-bg { + position: absolute; + inset: 0; + display: grid; + grid-template-columns: repeat(var(--gantt-hours-count), 1fr); + pointer-events: none; +} + +.gantt-grid-column { + border-left: 1px dashed #edf2f7; + height: 100%; +} + +/* BLOCS DE MANOEUVRES */ +.manoeuvre-bar { + position: absolute; + top: 50%; + transform: translateY(-50%); + height: 22px; + border-radius: 4px; + background: repeating-linear-gradient( + 45deg, + rgba(168, 85, 247, 0.45) 0, + rgba(168, 85, 247, 0.45) 4px, + rgba(147, 51, 234, 0.6) 4px, + rgba(147, 51, 234, 0.6) 8px + ); + border: 1px dashed #9333ea; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: opacity 0.15s; +} + +.manoeuvre-bar:hover { + opacity: 0.9; + z-index: 4; +} + +.manoeuvre-tag { + font-size: 10px; +} + +/* BLOCS DE CIRCULATION */ +.event-bar { + position: absolute; + top: 50%; + transform: translateY(-50%); + height: 40px; + border-radius: 6px; + color: #ffffff; + display: flex; + align-items: center; + padding: 0 8px; + cursor: pointer; + z-index: 2; + transition: transform 0.15s, box-shadow 0.15s; + overflow: hidden; +} + +.event-bar:hover { + transform: translateY(-50%) scale(1.02); + z-index: 3; +} + +.bar-nominal { + background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%); + border: 1px solid #0284c7; + box-shadow: 0 2px 6px rgba(2, 132, 199, 0.25); +} + +.bar-tight { + background: linear-gradient(135deg, #d97706 0%, #b45309 100%); + border: 1px solid #d97706; + box-shadow: 0 2px 6px rgba(217, 119, 6, 0.25); +} + +.bar-danger { + background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); + border: 1px solid #dc2626; + box-shadow: 0 2px 6px rgba(220, 38, 38, 0.25); +} + +.event-bar-content { + display: flex; + flex-direction: column; + min-width: 0; + width: 100%; +} + +.event-title-row { + display: flex; + align-items: center; + gap: 4px; +} + +.event-train-num { + font-size: 11px; + font-weight: 800; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.event-coupled-icon { + font-size: 11px; +} + +.event-route { + font-size: 10px; + opacity: 0.9; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 3. VUE CARTES FICHES DE ROULEMENT */ +.roulement-cards-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(370px, 1fr)); + gap: 18px; +} + +.roulement-materiel-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 14px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); + transition: transform 0.2s, box-shadow 0.2s; +} + +.roulement-materiel-card.is-in-maintenance { + border-left: 4px solid #7c3aed; +} + +.roster-card-header { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.roster-card-avatar { + width: 56px; + height: 50px; + border-radius: 8px; + background: #f1f5f9; + border: 1px solid #cbd5e1; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + flex-shrink: 0; +} + +.roster-card-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.roster-card-info h4 { + margin: 0; + font-size: 15px; + font-weight: 800; + color: #0f172a; +} + +.roster-card-tags { + display: flex; + gap: 6px; + margin-top: 4px; + flex-wrap: wrap; +} + +.tag-tech { + background: #f1f5f9; + color: #475569; + padding: 2px 6px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; +} + +.tag-type { + background: #e0f2fe; + color: #0369a1; + padding: 2px 6px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; +} + +.tag-cap { + background: #ecfdf5; + color: #047857; + padding: 2px 6px; + border-radius: 4px; + font-size: 11px; +} + +.tag-coupling { + background: #f5f3ff; + color: #6d28d9; + padding: 2px 6px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; +} + +.roster-card-location { + display: flex; + flex-direction: column; + gap: 2px; + margin-top: 6px; + font-size: 11px; + color: #64748b; +} + +.maintenance-status-badge { + display: inline-block; + font-size: 10px; + font-weight: 700; + padding: 1px 6px; + border-radius: 4px; + width: fit-content; + margin-top: 2px; +} + +.badge-danger { background: #fee2e2; color: #b91c1c; } +.badge-warning { background: #fef3c7; color: #b45309; } +.badge-success { background: #ecfdf5; color: #047857; } + +.roster-card-metrics { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + background: #f8fafc; + padding: 10px; + border-radius: 8px; + border: 1px solid #edf2f7; +} + +.metric-box { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.metric-label { + font-size: 10px; + font-weight: 600; + color: #64748b; + text-transform: uppercase; +} + +.metric-value { + font-size: 14px; + font-weight: 700; + color: #0f172a; + margin-top: 2px; +} + +.roster-card-alerts { + display: flex; + flex-direction: column; + gap: 6px; +} + +.roster-alert-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; +} + +.roster-card-sequence { + display: flex; + flex-direction: column; + gap: 8px; +} + +.sequence-title { + font-size: 12px; + font-weight: 700; + color: #475569; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.sequence-empty { + display: flex; + align-items: center; + gap: 8px; + padding: 12px; + border-radius: 8px; + background: #f8fafc; + border: 1px dashed #cbd5e1; + color: #64748b; + font-size: 12px; +} + +.sequence-timeline-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 230px; + overflow-y: auto; +} + +.sequence-leg-block { + display: flex; + flex-direction: column; + gap: 4px; +} + +.leg-event-row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s; +} + +.leg-event-row:hover { + border-color: #0088ce; + background: #f0f9ff; +} + +.leg-step-badge { + background: #e2e8f0; + color: #334155; + font-size: 11px; + font-weight: 700; + padding: 2px 6px; + border-radius: 4px; +} + +.leg-event-main { + flex: 1; + min-width: 0; +} + +.leg-train-row { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; +} + +.coupled-pill { + background: #f5f3ff; + color: #6d28d9; + padding: 1px 6px; + border-radius: 4px; + font-weight: 700; + font-size: 10px; +} + +.leg-hours { + color: #64748b; + font-weight: 600; +} + +.leg-stations { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; + color: #334155; + margin-top: 2px; +} + +.leg-unassign-btn { background: none; border: none; - font-size: 1.8rem; + color: #94a3b8; + font-size: 18px; + line-height: 1; cursor: pointer; - color: #9ca3af; + padding: 2px 6px; + border-radius: 4px; } -.modal-close:hover { - color: #1f2937; +.leg-unassign-btn:hover { + background: #fee2e2; + color: #b91c1c; } -.form-group { - margin-bottom: 20px; -} - -.form-group label { - display: block; - margin-bottom: 8px; - font-weight: 500; - color: #374151; -} - -.form-group select { - width: 100%; - padding: 10px; +.leg-dwell-row { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + background: #f8fafc; border-radius: 6px; - border: 1px solid #d1d5db; - font-size: 1rem; + font-size: 11px; + color: #64748b; + margin-left: 20px; } -.prochains-departs h4 { - margin-top: 24px; - margin-bottom: 12px; - font-size: 1.1rem; +.dwell-tight { + background: #fef3c7; + color: #b45309; +} + +.dwell-station { + background: #ecfdf5; + color: #047857; +} + +.dwell-danger { + background: #fee2e2; + color: #b91c1c; + font-weight: 700; +} + +.roster-card-footer { + display: flex; + gap: 8px; + margin-top: auto; + padding-top: 10px; + border-top: 1px solid #f1f5f9; +} + +.roster-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 7px 12px; + border-radius: 6px; + font-size: 12px; font-weight: 600; + cursor: pointer; + transition: all 0.15s; } -.prochains-departs ul { - list-style: none; - padding: 0; - max-height: 300px; - overflow-y: auto; - border: 1px solid #e5e7eb; +.roster-btn.primary { + flex: 1; + background: #0088ce; + border: 1px solid #0088ce; + color: #ffffff; +} + +.roster-btn.primary:hover { + background: #0070ab; +} + +.roster-btn.stroked { + background: #ffffff; + border: 1px solid #cbd5e1; + color: #475569; +} + +.roster-btn.stroked:hover { + background: #f8fafc; + color: #0f172a; +} + +.roster-btn.danger-stroked { + background: #ffffff; + border: 1px solid #fca5a5; + color: #dc2626; +} + +.roster-btn.danger-stroked:hover { + background: #fef2f2; +} + +/* 4. VUE DÉPÔTS & TECHNICENTRES */ +.depots-management-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); + gap: 18px; +} + +.depot-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 18px; + display: flex; + flex-direction: column; + gap: 14px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); +} + +.depot-card-header { + display: flex; + align-items: center; + gap: 12px; +} + +.depot-icon-badge { + width: 44px; + height: 44px; + border-radius: 10px; + background: #f1f5f9; + border: 1px solid #cbd5e1; + display: flex; + align-items: center; + justify-content: center; + color: #002244; +} + +.depot-card-header h4 { + margin: 0; + font-size: 15px; + font-weight: 800; + color: #0f172a; +} + +.depot-sub { + font-size: 11px; + color: #64748b; +} + +.depot-occupancy-box { + background: #f8fafc; + border: 1px solid #edf2f7; + border-radius: 8px; + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.occupancy-labels { + display: flex; + justify-content: space-between; + font-size: 12px; + color: #334155; +} + +.occupancy-track { + height: 8px; + background: #e2e8f0; + border-radius: 999px; + overflow: hidden; +} + +.occupancy-fill { + height: 100%; + background: #0284c7; + border-radius: 999px; + transition: width 0.3s; +} + +.occupancy-fill.fill-warning { + background: #d97706; +} + +.depot-section-title { + display: block; + font-size: 11px; + font-weight: 700; + color: #475569; + text-transform: uppercase; + margin-bottom: 6px; +} + +.depot-rames-chips { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.depot-rame-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; border-radius: 6px; + background: #f1f5f9; + border: 1px solid #cbd5e1; + font-size: 12px; + font-weight: 600; + color: #0f172a; + cursor: pointer; + transition: all 0.15s; } -.prochains-departs li { - padding: 12px; - border-bottom: 1px solid #e5e7eb; +.depot-rame-chip:hover { + background: #e0f2fe; + border-color: #0284c7; } -.prochains-departs li:last-child { - border-bottom: none; +.chip-alert { + background: #fee2e2; + color: #b91c1c; + font-size: 10px; + padding: 1px 4px; + border-radius: 4px; } -/* Dark Mode */ -body.dark-mode .vue-roulement-container, -[data-theme="dark"] .vue-roulement-container { - background-color: #0b111e; - color: #f1f5f9; +.depot-empty-text { + font-size: 12px; + color: #94a3b8; + font-style: italic; } -body.dark-mode .materiel-card, -[data-theme="dark"] .materiel-card { - background-color: #151f30; +/* 5. MODALES ROULEMENT & MAINTENANCE */ +.maintenance-modal { + max-width: 540px; +} + +.form-label { + font-size: 12px; + font-weight: 700; + color: #334155; + margin-bottom: 6px; + display: block; +} + +.form-select { + width: 100%; + padding: 8px 12px; + border-radius: 8px; + border: 1px solid #cbd5e1; + font-size: 13px; + font-weight: 600; + background: #ffffff; + outline: none; +} + +.maintenance-info-box { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px 14px; + background: #f0f9ff; + border: 1px solid #bae6fd; + border-radius: 8px; + color: #0369a1; + font-size: 12px; +} + +.turnaround-nominal-pill { + background: #ecfdf5; + color: #047857; + padding: 1px 6px; + border-radius: 4px; + font-weight: 700; + font-size: 10px; +} + +.turnaround-long-pill { + background: #f1f5f9; + color: #475569; + padding: 1px 6px; + border-radius: 4px; + font-weight: 600; + font-size: 10px; +} + +/* DARK MODE */ +body.dark-mode .roulement-toolbar-card, +body.dark-mode .roulement-gantt-card, +body.dark-mode .roulement-materiel-card, +body.dark-mode .depot-card, +body.dark-mode .roulement-gantt-row { + background: #151f30; border-color: #243247; color: #f1f5f9; } -body.dark-mode .materiel-header, -[data-theme="dark"] .materiel-header { - border-bottom-color: #243247; +body.dark-mode .roulement-search-input, +body.dark-mode .roulement-select, +body.dark-mode .form-select { + background: #0b111e; + border-color: #243247; + color: #f1f5f9; } -body.dark-mode .materiel-header h3, -[data-theme="dark"] .materiel-header h3 { +body.dark-mode .roulement-gantt-header-row, +body.dark-mode .gantt-rame-col-header, +body.dark-mode .roulement-gantt-legend, +body.dark-mode .roster-card-metrics, +body.dark-mode .depot-occupancy-box { + background: #0e1726; + border-color: #243247; + color: #cbd5e1; +} + +body.dark-mode .rame-title, +body.dark-mode .roster-card-info h4, +body.dark-mode .depot-card-header h4 { color: #f8fafc; } -body.dark-mode .materiel-header p, -[data-theme="dark"] .materiel-header p { +/* ========================================================================== + DEPOTS & TECHNICENTRES MANAGEMENT + ========================================================================== */ +.depots-view-container { + display: flex; + flex-direction: column; +} + +.depots-header-banner { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 20px 24px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); +} + +.depots-header-info { + display: flex; + align-items: center; + gap: 16px; +} + +.depots-header-icon { + width: 48px; + height: 48px; + border-radius: 12px; + background: #002244; + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.depots-main-title { + margin: 0; + font-size: 18px; + font-weight: 800; + color: #0f172a; +} + +.depots-main-sub { + margin: 4px 0 0; + font-size: 13px; + color: #64748b; +} + +.depot-title-area { + flex: 1; +} + +.depot-code-badge { + background: #e2e8f0; + color: #1e293b; + font-size: 11px; + font-weight: 700; +} + +.depot-actions-dropdown { + display: flex; + align-items: center; + gap: 6px; +} + +.depot-action-btn { + background: #f8fafc; + border: 1px solid #cbd5e1; + border-radius: 6px; + padding: 5px 8px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; + color: #475569; +} + +.depot-action-btn:hover.edit { + background: #eff6ff; + border-color: #3b82f6; + color: #2563eb; +} + +.depot-action-btn:hover.delete { + background: #fef2f2; + border-color: #ef4444; + color: #dc2626; +} + +.depot-specialties-row { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.depot-spec-tag { + font-size: 11px; + font-weight: 600; + background: #f1f5f9; + color: #334155; + padding: 3px 8px; + border-radius: 4px; + border: 1px solid #e2e8f0; +} + +.depot-edit-modal { + max-width: 620px; +} + +body.dark-mode .depots-header-banner { + background: #151f30; + border-color: #243247; + color: #f1f5f9; +} + +body.dark-mode .depots-main-title { + color: #f8fafc; +} + +body.dark-mode .depots-main-sub { color: #94a3b8; } -body.dark-mode .ligne-section h4, -[data-theme="dark"] .ligne-section h4 { - color: #e2e8f0; +body.dark-mode .depot-code-badge { + background: #243247; + color: #93c5fd; } -body.dark-mode .timeline-body, -[data-theme="dark"] .timeline-body { - background-image: repeating-linear-gradient(to right, transparent 0, transparent calc(100% / 12 - 1px), #243247 calc(100% / 12 - 1px), #243247 calc(100% / 12)); +body.dark-mode .depot-action-btn { + background: #1e293b; + border-color: #334155; + color: #cbd5e1; } -body.dark-mode .prochains-departs ul, -[data-theme="dark"] .prochains-departs ul { - border-color: #243247; - background: #111a28; -} - -body.dark-mode .prochains-departs li, -[data-theme="dark"] .prochains-departs li { - border-bottom-color: #1e293b; - color: #e2e8f0; +body.dark-mode .depot-spec-tag { + background: #1e293b; + border-color: #334155; + color: #cbd5e1; } \ No newline at end of file diff --git a/src/pages/attribution-materiel-roulant/VueRoulement.js b/src/pages/attribution-materiel-roulant/VueRoulement.js index 93a8fe2..ac068ab 100644 --- a/src/pages/attribution-materiel-roulant/VueRoulement.js +++ b/src/pages/attribution-materiel-roulant/VueRoulement.js @@ -1,192 +1,1538 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import { getAllGares } from '../../api/gares'; -import { getAllHoraires, formatHeure } from '../../api/horaires'; +import React, { useState, useMemo, useEffect } from 'react'; +import { formatHeure } from '../../api/horaires'; +import { timeToMinutes } from '../../utils/quaisUtils'; +import { + analyzeMaterielRoster, + getCouplingFamily, + COUPLING_FAMILIES +} from '../../utils/roulementUtils'; +import { + getAllDepots, + createDepot, + updateDepot, + deleteDepot +} from '../../api/depots'; import './VueRoulement.css'; const DAY_MINUTES = 24 * 60; -const TIME_MARKERS_HOURS = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]; +const TIME_MARKERS_HOURS = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24]; -function parseTimeToMinutes(value) { - if (!value || typeof value !== 'string') return null; - const [h, m] = value.split(':'); - const hours = Number(h); - const minutes = Number(m); - if (Number.isNaN(hours) || Number.isNaN(minutes)) return null; - return (hours * 60) + minutes; -} - -function getBarPlacement(horaire) { - const depart = parseTimeToMinutes(horaire?.heure_depart); - const arriveeRaw = parseTimeToMinutes(horaire?.heure_arrivee); - - if (depart === null) { - return { leftPct: 0, widthPct: 0 }; - } - - const arrivee = arriveeRaw === null ? depart + 45 : (arriveeRaw < depart ? arriveeRaw + DAY_MINUTES : arriveeRaw); - const duration = Math.max(15, arrivee - depart); - const leftPct = Math.max(0, Math.min(100, (depart / DAY_MINUTES) * 100)); - const widthPct = Math.max(1, Math.min(100 - leftPct, (duration / DAY_MINUTES) * 100)); +function getEventBarPlacement(event) { + const start = event.startMins; + const end = event.endMins; + const duration = Math.max(14, end - start); + const leftPct = Math.max(0, Math.min(100, (start / DAY_MINUTES) * 100)); + const widthPct = Math.max(1.8, Math.min(100 - leftPct, (duration / DAY_MINUTES) * 100)); return { leftPct, widthPct }; } -export default function VueRoulement({ materiels, horaires, lignes, onEditHoraire, ligneSelectionnee }) { - const [isModalOpen, setIsModalOpen] = useState(false); - const [selectedGare, setSelectedGare] = useState(''); - const [prochainsDeparts, setProchainsDeparts] = useState([]); - const [gares, setGares] = useState([]); - const [loadingGares, setLoadingGares] = useState(true); +function getManoeuvreBarPlacement(man) { + const start = man.startMins; + const end = man.endMins; + const duration = Math.max(8, end - start); + const leftPct = Math.max(0, Math.min(100, (start / DAY_MINUTES) * 100)); + const widthPct = Math.max(1.2, Math.min(100 - leftPct, (duration / DAY_MINUTES) * 100)); + return { leftPct, widthPct }; +} + +export default function VueRoulement({ + materiels = [], + horaires = [], + lignes = [], + timelines = {}, + onEditHoraire, + onAssignHoraire, + onUnassignFromHoraire, + onClearMaterielRoster, + onUpdateMateriel, + canUpdate = true, + depots: propDepots = [], + onDepotsChange, + onOpenOptimizationModal +}) { + // Liste dynamique des dépôts et technicentres enregistrés en BDD + const [depotsList, setDepotsList] = useState(Array.isArray(propDepots) ? propDepots : []); useEffect(() => { - async function fetchGares() { - const { data, error } = await getAllGares(); - if (error) { - console.error("Erreur lors du chargement des gares", error); - } else { - setGares(data); - } - setLoadingGares(false); - } - fetchGares(); - }, []); - - const handleGareChange = async (gareId) => { - setSelectedGare(gareId); - if (gareId) { - const { data, error } = await getAllHoraires(); - if (error) { - console.error("Erreur lors de la récupération des horaires", error); - return; - } - const departs = data - .filter(h => String(h.gare_depart_id) === String(gareId)) - .sort((a, b) => a.heure_depart.localeCompare(b.heure_depart)) - .slice(0, 20); - setProchainsDeparts(departs); + if (Array.isArray(propDepots) && propDepots.length > 0) { + setDepotsList(propDepots); } else { - setProchainsDeparts([]); + getAllDepots().then(res => { + setDepotsList(Array.isArray(res?.data) ? res.data : []); + }); + } + }, [propDepots]); + + // Filtres + const [searchTerm, setSearchTerm] = useState(''); + const [selectedTypeFilter, setSelectedTypeFilter] = useState('all'); + const [selectedDepotFilter, setSelectedDepotFilter] = useState('all'); + const [statusFilter, setStatusFilter] = useState('all'); // 'all', 'active', 'depot', 'anomaly', 'maintenance' + const [viewSubMode, setViewSubMode] = useState('gantt'); // 'gantt' | 'cards' | 'depots' + const [showManoeuvres, setShowManoeuvres] = useState(true); + + // Modales + const [selectedMaterielForRoster, setSelectedMaterielForRoster] = useState(null); + const [chainingMateriel, setChainingMateriel] = useState(null); + const [chainingSearchTerm, setChainingSearchTerm] = useState(''); + const [isChainingSaving, setIsChainingSaving] = useState(false); + + // Modale Maintenance / Dépôt + const [maintenanceModalMateriel, setMaintenanceModalMateriel] = useState(null); + const [maintenanceStatusDraft, setMaintenanceStatusDraft] = useState('OPERATIONNEL'); + const [depotAttacheDraft, setDepotAttacheDraft] = useState(''); + const [isMaintenanceSaving, setIsMaintenanceSaving] = useState(false); + + // Modale Création / Modification Dépôt + const [depotModalOpen, setDepotModalOpen] = useState(false); + const [editingDepot, setEditingDepot] = useState(null); + const [depotForm, setDepotForm] = useState({ + nom: '', + code: '', + ville: '', + gare_proche: '', + capacite_max: 20, + specialites: '', + description: '' + }); + const [isDepotSubmitting, setIsDepotSubmitting] = useState(false); + const [depotFormError, setDepotFormError] = useState(''); + + // Types de trains uniques + const trainTypes = useMemo(() => { + const types = new Set(); + materiels.forEach(m => { + if (m.type_train) types.add(m.type_train); + }); + return Array.from(types).sort(); + }, [materiels]); + + // Analyse enrichie pour chaque matériel + const enrichedMateriels = useMemo(() => { + return (materiels || []).map(m => { + const events = timelines[m.id] || []; + const analysis = analyzeMaterielRoster(events, m); + const isAtDepot = events.length === 0; + const isActive = events.length > 0; + const isInMaintenance = m.statut_maintenance === 'EN_AVARIE_ATELIER' || m.statut_maintenance === 'MAINTENANCE_PROGRAMMEE'; + const couplingFamilyKey = getCouplingFamily(m); + const couplingFamilyLabel = COUPLING_FAMILIES[couplingFamilyKey]?.label || couplingFamilyKey; + + return { + ...m, + events, + analysis, + isAtDepot, + isActive, + isInMaintenance, + couplingFamilyKey, + couplingFamilyLabel, + hasAnomalies: analysis.hasAnomalies, + hasCriticalErrors: analysis.hasCriticalErrors + }; + }); + }, [materiels, timelines]); + + // Filtrage des matériels + const filteredMateriels = useMemo(() => { + return enrichedMateriels.filter(m => { + if (searchTerm) { + const q = searchTerm.toLowerCase(); + const matchNom = String(m.nom || '').toLowerCase().includes(q); + const matchTech = String(m.nom_technique || '').toLowerCase().includes(q); + const matchNum = String(m.numero_serie || '').toLowerCase().includes(q); + const matchDepot = String(m.depot_attache || '').toLowerCase().includes(q); + if (!matchNom && !matchTech && !matchNum && !matchDepot) return false; + } + + if (selectedTypeFilter !== 'all' && m.type_train !== selectedTypeFilter) { + return false; + } + + if (selectedDepotFilter !== 'all' && m.depot_attache !== selectedDepotFilter) { + return false; + } + + if (statusFilter === 'active' && !m.isActive) return false; + if (statusFilter === 'depot' && !m.isAtDepot) return false; + if (statusFilter === 'anomaly' && !m.hasAnomalies) return false; + if (statusFilter === 'maintenance' && !m.isInMaintenance) return false; + + return true; + }).sort((a, b) => { + if (a.hasCriticalErrors && !b.hasCriticalErrors) return -1; + if (!a.hasCriticalErrors && b.hasCriticalErrors) return 1; + if (a.hasAnomalies && !b.hasAnomalies) return -1; + if (!a.hasAnomalies && b.hasAnomalies) return 1; + if (a.events.length !== b.events.length) return b.events.length - a.events.length; + return String(a.nom || '').localeCompare(String(b.nom || '')); + }); + }, [enrichedMateriels, searchTerm, selectedTypeFilter, selectedDepotFilter, statusFilter]); + + // Circulations éligibles pour l'enchaînement + const candidateNextTrains = useMemo(() => { + if (!chainingMateriel) return []; + + const events = timelines[chainingMateriel.id] || []; + let targetGareId = null; + let targetGareNom = ''; + let minDepartMins = 0; + + if (events.length > 0) { + const lastRun = events[events.length - 1]; + targetGareId = lastRun.gare_arrivee_id; + targetGareNom = (lastRun.gare_arrivee_nom || '').toLowerCase(); + minDepartMins = lastRun.endMins; + } + + return horaires.filter(h => { + const isAlreadyOnTrain = (h.composition_train || []).some( + c => String(c.materiel_id) === String(chainingMateriel.id) + ); + if (isAlreadyOnTrain) return false; + + const departM = timeToMinutes(h.heure_depart); + if (departM === null) return false; + + if (events.length > 0) { + const isSameStation = + (targetGareId && String(h.gare_depart_id) === String(targetGareId)) || + ((h.gare_depart_nom || '').toLowerCase() === targetGareNom); + + if (!isSameStation) return false; + if (departM < minDepartMins) return false; + } + + if (chainingSearchTerm) { + const q = chainingSearchTerm.toLowerCase(); + const matchNum = String(h.numero_train || '').toLowerCase().includes(q); + const matchDest = String(h.gare_arrivee_nom || '').toLowerCase().includes(q); + if (!matchNum && !matchDest) return false; + } + + return true; + }).sort((a, b) => timeToMinutes(a.heure_depart) - timeToMinutes(b.heure_depart)); + }, [chainingMateriel, timelines, horaires, chainingSearchTerm]); + + const handleConfirmChain = async (targetHoraire) => { + if (!chainingMateriel || !canUpdate || !onAssignHoraire) return; + setIsChainingSaving(true); + try { + await onAssignHoraire(targetHoraire, chainingMateriel.id); + setChainingMateriel(null); + } catch (err) { + console.error('Erreur assignation enchaînement:', err); + } finally { + setIsChainingSaving(false); } }; - const planningData = useMemo(() => { - const materielsMap = new Map(); - - horaires.forEach(horaire => { - if (!horaire.composition_train || horaire.composition_train.length === 0) return; - - horaire.composition_train.forEach(compo => { - const materielId = compo.materiel_id; - if (ligneSelectionnee && String(horaire.ligne_id) !== String(ligneSelectionnee)) { - return; - } - - if (!materielsMap.has(materielId)) { - const materielInfo = materiels.find(m => String(m.id) === String(materielId)); - if (materielInfo) { - materielsMap.set(materielId, { - ...materielInfo, - lignes: new Map(), - }); - } - } - - const materielData = materielsMap.get(materielId); - if (materielData) { - const ligneId = horaire.ligne_id || 'sans-ligne'; - if (!materielData.lignes.has(ligneId)) { - const ligneInfo = lignes.find(l => String(l.id) === String(ligneId)); - materielData.lignes.set(ligneId, { - id: ligneId, - nom: ligneInfo ? ligneInfo.nom : 'Sans Ligne', - horaires: [], - }); - } - materielData.lignes.get(ligneId).horaires.push(horaire); - } - }); + const openCreateDepotModal = () => { + setEditingDepot(null); + setDepotForm({ + nom: '', + code: '', + ville: '', + gare_proche: '', + capacite_max: 20, + specialites: '', + description: '' }); + setDepotFormError(''); + setDepotModalOpen(true); + }; - return Array.from(materielsMap.values()); - }, [horaires, materiels, lignes, ligneSelectionnee]); + const openEditDepotModal = (depot) => { + setEditingDepot(depot); + const specsStr = Array.isArray(depot.specialites) + ? depot.specialites.join(', ') + : (typeof depot.specialites === 'string' ? depot.specialites : ''); + setDepotForm({ + nom: depot.nom || '', + code: depot.code || '', + ville: depot.ville || '', + gare_proche: depot.gare_proche || '', + capacite_max: depot.capacite_max || 20, + specialites: specsStr, + description: depot.description || '' + }); + setDepotFormError(''); + setDepotModalOpen(true); + }; + + const handleSaveDepot = async (e) => { + e && e.preventDefault(); + if (!depotForm.nom?.trim() || !depotForm.ville?.trim()) { + setDepotFormError('Veuillez renseigner au moins le nom et la ville du technicentre.'); + return; + } + setIsDepotSubmitting(true); + setDepotFormError(''); + try { + const specialitesArray = depotForm.specialites + ? depotForm.specialites.split(',').map(s => s.trim()).filter(Boolean) + : []; + + const payload = { + nom: depotForm.nom.trim(), + code: depotForm.code?.trim() || null, + ville: depotForm.ville.trim(), + gare_proche: depotForm.gare_proche?.trim() || null, + capacite_max: Number(depotForm.capacite_max || 20), + specialites: specialitesArray, + description: depotForm.description?.trim() || null, + actif: true + }; + + if (editingDepot?.id) { + const { error } = await updateDepot(editingDepot.id, payload); + if (error) throw error; + } else { + const { error } = await createDepot(payload); + if (error) throw error; + } + + setDepotModalOpen(false); + if (onDepotsChange) { + await onDepotsChange(); + } else { + const { data } = await getAllDepots(); + if (data) setDepotsList(data); + } + } catch (err) { + console.error('Erreur sauvegarde dépôt:', err); + setDepotFormError(err.message || "Impossible d'enregistrer le dépôt en base de données."); + } finally { + setIsDepotSubmitting(false); + } + }; + + const handleDeleteDepot = async (depot) => { + if (!depot?.id) return; + if (!window.confirm(`Êtes-vous sûr de vouloir supprimer le Technicentre "${depot.nom}" ? Cette action est irréversible.`)) { + return; + } + try { + const { error } = await deleteDepot(depot.id); + if (error) throw error; + setDepotsList(prev => prev.filter(d => d.id !== depot.id)); + if (onDepotsChange) { + await onDepotsChange(); + } + } catch (err) { + console.error('Erreur lors de la suppression du dépôt:', err); + alert(`Erreur lors de la suppression du dépôt : ${err.message || err}`); + } + }; + + const openMaintenanceModal = (materiel) => { + setMaintenanceModalMateriel(materiel); + setMaintenanceStatusDraft(materiel.statut_maintenance || 'OPERATIONNEL'); + setDepotAttacheDraft(materiel.depot_attache || ''); + }; + + const handleSaveMaintenance = async () => { + if (!maintenanceModalMateriel || !onUpdateMateriel) return; + setIsMaintenanceSaving(true); + try { + const selectedDepot = depotsList.find(d => d.nom === depotAttacheDraft); + await onUpdateMateriel(maintenanceModalMateriel.id, { + statut_maintenance: maintenanceStatusDraft, + depot_attache: depotAttacheDraft, + depot_id: selectedDepot ? selectedDepot.id : null + }); + setMaintenanceModalMateriel(null); + } catch (err) { + console.error('Erreur sauvegarde maintenance:', err); + } finally { + setIsMaintenanceSaving(false); + } + }; return ( -
-
- -
-
- {planningData.map(materiel => ( -
-
-

{materiel.nom}

-

{materiel.nom_technique}

+
+ {/* 1. BARRE DE COMMANDE & FILTRES DU ROULEMENT */} +
+
+ {/* Recherche */} +
+ + setSearchTerm(e.target.value)} + /> + {searchTerm && ( + + )} +
+ + {/* Filtre Type de Train */} +
+ Série : + +
+ + {/* Filtre Dépôt d'attache */} +
+ Dépôt : + +
+ + {/* Filtre Statut */} +
+ + + + + +
+ + {/* Commutateur de sous-vue */} +
+ + + +
+
+ + {/* Options d'affichage additionnelles */} + {viewSubMode === 'gantt' && ( +
+ + + {onOpenOptimizationModal && canUpdate && ( + + + Optimiser le roulement + + )} +
+ )} +
+ + {/* 2. CONTENU PRINCIPAL DU ROULEMENT */} + {filteredMateriels.length === 0 ? ( +
+ +

Aucun matériel roulant trouvé

+

Modifiez vos critères de recherche ou de filtre pour visualiser les roulements.

+
+ ) : viewSubMode === 'gantt' ? ( + /* =================================================================== + VUE GANTT DES ROULEMENTS (FRISE 24H AVEC MANŒUVRES & COUPLAGE) + =================================================================== */ +
+
+
+ + Circulation nominale
-
- {Array.from(materiel.lignes.values()).map(ligne => ( -
-

{ligne.nom}

-
-
- {TIME_MARKERS_HOURS.map(hour => ( -
{`${hour}h`}
- ))} +
+ + Escale courte (< 15 min) +
+
+ + Conflit / Rupture géographique +
+ {showManoeuvres && ( +
+ + Manœuvre (mise à quai / garage) +
+ )} +
+ + Stationnement / Dépôt +
+
+ +
+
+ {/* En-tête des heures */} +
+
+ Matériel Roulant / Silhouette +
+
+ {TIME_MARKERS_HOURS.map(hour => ( +
+ {String(hour).padStart(2, '0')}:00
-
- {ligne.horaires.map(horaire => { - const { leftPct, widthPct } = getBarPlacement(horaire); - return ( -
onEditHoraire(horaire)} + ))} +
+
+ + {/* Lignes de matériel */} +
+ {filteredMateriels.map(materiel => { + const { events, analysis, manoeuvres = [] } = materiel; + + return ( +
+ {/* Colonne visuelle du matériel */} +
+
+ {materiel.image_url ? ( + {materiel.nom + ) : ( +
+ +
+ )} +
+ +
+ {materiel.nom || materiel.nom_technique} +
+ {materiel.type_train && ( + {materiel.type_train} + )} + {materiel.capacite && ( + {materiel.capacite} pl. + )} + {materiel.depot_attache && ( + + 📍 {materiel.depot_attache.split(' ')[0]} + + )} +
+
+ + {/* Actions rapides */} +
+ {materiel.isInMaintenance ? ( + + Atelier + + ) : analysis.hasCriticalErrors ? ( + + Conflit + + ) : analysis.hasAnomalies ? ( + + <15m + + ) : materiel.isActive ? ( + + {events.length} train{events.length > 1 ? 's' : ''} + + ) : ( + Dépôt + )} + + + + + + {canUpdate && !materiel.isInMaintenance && ( + + )} +
+
+ + {/* Rail chronologique 24h */} +
+ {/* Grille de fond avec repères d'heures */} +
+ {TIME_MARKERS_HOURS.map(hour => ( +
+ ))} +
+ + {/* Blocs de Manœuvres (si activées) */} + {showManoeuvres && (analysis.manoeuvres || []).map((man) => { + const { leftPct, widthPct } = getManoeuvreBarPlacement(man); + + return ( +
+ ⚙️ +
+ ); + })} + + {/* Blocs de circulation commerciale */} + {events.map((event, idx) => { + const { leftPct, widthPct } = getEventBarPlacement(event); + + const nextEvent = events[idx + 1]; + const prevEvent = events[idx - 1]; + + let hasConflict = false; + let isTight = false; + + if (nextEvent) { + const gap = nextEvent.startMins - event.endMins; + if (gap < 0) hasConflict = true; + else if (gap < 15) isTight = true; + + const sameStation = + (event.gare_arrivee_id && String(event.gare_arrivee_id) === String(nextEvent.gare_depart_id)) || + (event.gare_arrivee_nom || '').toLowerCase() === (nextEvent.gare_depart_nom || '').toLowerCase(); + if (!sameStation) hasConflict = true; + } + + if (prevEvent) { + const prevGap = event.startMins - prevEvent.endMins; + if (prevGap < 0) hasConflict = true; + } + + const barClass = hasConflict + ? 'event-bar bar-danger' + : isTight + ? 'event-bar bar-tight' + : 'event-bar bar-nominal'; + + const isCoupled = !!event.couplage_train_id; + + return ( +
onEditHoraire && onEditHoraire(event.horaire)} + title={`Train ${event.numero_train} : ${event.gare_depart_nom} (${formatHeure(event.heure_depart)}) → ${event.gare_arrivee_nom} (${formatHeure(event.heure_arrivee)})${isCoupled ? ` [Couplé avec Train ${event.couplage_numero_train}]` : ''}`} + > +
+
+ {event.numero_train} + {isCoupled && ( + + 🔗 + + )} +
+ {event.gare_depart_nom} → {event.gare_arrivee_nom} +
+
+ ); + })} +
+
+ ); + })} +
+
+
+
+ ) : viewSubMode === 'cards' ? ( + /* =================================================================== + VUE CARTES / FICHES DE ROULEMENT DÉTAILLÉES + =================================================================== */ +
+ {filteredMateriels.map(materiel => { + const { events, analysis } = materiel; + + return ( +
+ {/* En-tête de la fiche */} +
+
+ {materiel.image_url ? ( + {materiel.nom + ) : ( +
+ +
+ )} +
+
+

{materiel.nom || materiel.nom_technique}

+
+ {materiel.nom_technique && ( + {materiel.nom_technique} + )} + {materiel.type_train && ( + {materiel.type_train} + )} + {materiel.capacite && ( + {materiel.capacite} places + )} + + 🔗 {materiel.couplingFamilyKey} + +
+ +
+ Dépôt d'attache : {materiel.depot_attache || 'Non spécifié'} + {materiel.statut_maintenance === 'EN_AVARIE_ATELIER' && ( + Immobilisé en atelier + )} + {materiel.statut_maintenance === 'MAINTENANCE_PROGRAMMEE' && ( + Maintenance programmée + )} + {(!materiel.statut_maintenance || materiel.statut_maintenance === 'OPERATIONNEL') && ( + Opérationnel + )} +
+
+
+ + {/* Métriques d'exploitation */} +
+
+ Circulations + {events.length} +
+
+ En ligne + + {Math.floor(analysis.totalServiceMinutes / 60)}h{String(analysis.totalServiceMinutes % 60).padStart(2, '0')} + +
+
+ Arrêt total + + {Math.floor(analysis.totalTurnaroundMinutes / 60)}h{String(analysis.totalTurnaroundMinutes % 60).padStart(2, '0')} + +
+
+ + {/* Alertes d'anomalies éventuelles */} + {analysis.hasAnomalies && ( +
+ {analysis.anomalies.map((anom, aIdx) => ( +
+ + {anom.message} +
+ ))} +
+ )} + + {/* Séquence des courses de la journée */} +
+ Itinéraire & Rotation du jour : + + {events.length === 0 ? ( +
+ + Matériel au dépôt · Aucun train assigné aujourd'hui +
+ ) : ( +
+ {analysis.legs.map((leg, lIdx) => { + const { event, turnaroundAfter, nextEvent } = leg; + const isCoupled = !!event.couplage_train_id; + + return ( +
+
onEditHoraire && onEditHoraire(event.horaire)}> +
#{lIdx + 1}
+
+
+ Train {event.numero_train} + {isCoupled && ( + + 🔗 Couplé {event.couplage_numero_train} + + )} + + {formatHeure(event.heure_depart)} → {formatHeure(event.heure_arrivee)} + +
+
+ {event.gare_depart_nom} + + {event.gare_arrivee_nom} +
+
+ {canUpdate && onUnassignFromHoraire && ( + + )} +
+ + {/* Escale intermédiaire */} + {nextEvent && ( +
+ + + Escale à {event.gare_arrivee_nom} : {turnaroundAfter} min + {turnaroundAfter < 0 + ? ' (Chevauchement critique)' + : turnaroundAfter < 15 + ? ' (Temps court)' + : turnaroundAfter <= 45 + ? ' · Même quai maintenu' + : ' · Garage possible en faisceau'} + +
+ )}
); })}
-
+ )}
- ))} + + {/* Actions au bas de la carte */} +
+ + + {canUpdate && !materiel.isInMaintenance && ( + + )} + + {canUpdate && events.length > 0 && onClearMaterielRoster && ( + + )} +
+
+ ); + })} +
+ ) : ( + /* =================================================================== + VUE DÉPÔTS & TECHNICENTRES DE MAINTENANCE + =================================================================== */ +
+
+
+
+ +
+
+

Technicentres & Dépôts de Maintenance ({depotsList.length})

+

+ Gestion centralisée des technicentres, remisages, capacité des voies et ateliers de maintenance du réseau ferroviaire. +

+
+
+ {canUpdate && ( + + + Nouveau Technicentre / Dépôt + + )} +
+ + {depotsList.length === 0 ? ( +
+ +

Aucun Technicentre ou Dépôt enregistré en BDD

+

Votre base de données ne contient aucun dépôt de maintenance pour le moment.

+ {canUpdate && ( + + + Créer un premier Technicentre + + )} +
+ ) : ( +
+ {depotsList.map(depot => { + const ramesInDepot = enrichedMateriels.filter( + m => m.depot_id === depot.id || m.depot_attache === depot.nom || (m.isAtDepot && m.depot_attache?.includes(depot.ville)) + ); + const maxCap = Math.max(1, Number(depot.capacite_max || 20)); + const occupancyPct = Math.min(100, Math.round((ramesInDepot.length / maxCap) * 100)); + + const specs = Array.isArray(depot.specialites) + ? depot.specialites + : (typeof depot.specialites === 'string' && depot.specialites.trim() + ? depot.specialites.split(',').map(s => s.trim()).filter(Boolean) + : []); + + return ( +
+
+
+ +
+
+
+

{depot.nom}

+ {depot.code && ( + {depot.code} + )} +
+ + {depot.ville} {depot.gare_proche ? `· Gare d'appui : ${depot.gare_proche}` : ''} + +
+ + {canUpdate && ( +
+ + +
+ )} +
+ + {specs.length > 0 && ( +
+ {specs.map((sp, i) => ( + {sp} + ))} +
+ )} + +
+
+ Occupation des voies + {ramesInDepot.length} / {maxCap} rames ({occupancyPct}%) +
+
+
85 ? 'fill-warning' : occupancyPct > 95 ? 'fill-danger' : ''}`} + style={{ width: `${occupancyPct}%` }} + >
+
+
+ +
+ Rames rattachées ou stationnées : + {ramesInDepot.length === 0 ? ( + Aucune rame affectée à ce dépôt actuellement. + ) : ( +
+ {ramesInDepot.map(r => ( +
openMaintenanceModal(r)}> + {r.nom || r.numero_serie} + {r.isInMaintenance && Atelier} +
+ ))} +
+ )} +
+
+ ); + })} +
+ )} +
+ )} + + {/* =================================================================== + MODALE 1 : ENCHAÎNEMENT DU TRAIN SUIVANT (CHAINING MODAL) + =================================================================== */} + {chainingMateriel && ( +
setChainingMateriel(null)}> +
e.stopPropagation()}> +
+
+ +

Enchaîner une course · {chainingMateriel.nom || chainingMateriel.nom_technique}

+
+ +
+ +
+ {(() => { + const events = timelines[chainingMateriel.id] || []; + const lastRun = events.length > 0 ? events[events.length - 1] : null; + + return ( +
+ {lastRun ? ( + <> + +
+ Dernière position connue : En gare de {lastRun.gare_arrivee_nom} +
+ Arrivée prévue à {formatHeure(lastRun.heure_arrivee)} (Train {lastRun.numero_train}) +
+ + ) : ( + <> + +
+ Situation initiale : Matériel stationné au dépôt ou sans assignation +
+ Vous pouvez l'affecter à n'importe quel train disponible sur le réseau. +
+ + )} +
+ ); + })()} + +
+ setChainingSearchTerm(e.target.value)} + className="chain-search-input" + /> +
+ +
+ {candidateNextTrains.length === 0 ? ( +
+ +

Aucun train compatible au départ après cette arrivée.

+
+ ) : ( + candidateNextTrains.map(horaire => { + const events = timelines[chainingMateriel.id] || []; + const lastRun = events.length > 0 ? events[events.length - 1] : null; + const departM = timeToMinutes(horaire.heure_depart); + const turnaround = lastRun ? departM - lastRun.endMins : null; + + return ( +
+
+
+ {horaire.numero_train || 'Sillon'} + {horaire.type_train && {horaire.type_train}} +
+ +
+ {horaire.gare_depart_nom} + {formatHeure(horaire.heure_depart)} + + {horaire.gare_arrivee_nom} + {formatHeure(horaire.heure_arrivee)} +
+ + {turnaround !== null && ( +
+ + Délai d'escale : {turnaround} min + {turnaround <= 45 ? ( + Même quai conservé (≤ 45 min) + ) : ( + Escale longue > 45 min (Garage faisceau) + )} +
+ )} +
+ +
+ handleConfirmChain(horaire)} + > + Affecter ce train + +
+
+ ); + }) + )} +
+
+ +
+ setChainingMateriel(null)} disabled={isChainingSaving}> + Fermer +
- ))} -
+
+ )} - {isModalOpen && ( -
setIsModalOpen(false)}> -
e.stopPropagation()}> + {/* =================================================================== + MODALE 2 : DÉTAIL COMPLET DU ROULEMENT (SEQUENCE MODAL) + =================================================================== */} + {selectedMaterielForRoster && ( +
setSelectedMaterielForRoster(null)}> +
e.stopPropagation()}>
-

Attribuer un matériel roulant

- +
+ +

Roulement 24h · {selectedMaterielForRoster.nom || selectedMaterielForRoster.nom_technique}

+
+
+
-
- - setMaintenanceStatusDraft(e.target.value)} + > + + + + + +
+ +
+ +
- {prochainsDeparts.length > 0 && ( -
-

20 prochains départs

-
    - {prochainsDeparts.map(depart => ( -
  • - {depart.heure_depart} - Train {depart.numero_train} vers {depart.gare_arrivee_nom} -
  • - ))} -
-
- )} + +
+ + + Placer une rame en « En avarie / atelier » interdira son affectation automatique ou manuelle sur les trains du réseau pour des raisons de sécurité ferroviaire. + +
+ +
+ setMaintenanceModalMateriel(null)} disabled={isMaintenanceSaving}> + Annuler + + + Enregistrer + +
+
+
+ )} + + {/* =================================================================== + MODALE 4 : CRÉATION / MODIFICATION D'UN DÉPÔT / TECHNICENTRE + =================================================================== */} + {depotModalOpen && ( +
!isDepotSubmitting && setDepotModalOpen(false)}> +
e.stopPropagation()}> +
+
+ +

{editingDepot ? `Modifier · ${editingDepot.nom}` : 'Nouveau Technicentre / Dépôt de Maintenance'}

+
+ +
+ +
+
+ {depotFormError && ( +
+ + {depotFormError} +
+ )} + +
+ + setDepotForm({ ...depotForm, nom: e.target.value })} + required + /> +
+ +
+
+ + setDepotForm({ ...depotForm, code: e.target.value.toUpperCase() })} + /> +
+
+ + setDepotForm({ ...depotForm, ville: e.target.value })} + required + /> +
+
+ +
+
+ + setDepotForm({ ...depotForm, gare_proche: e.target.value })} + /> +
+
+ + setDepotForm({ ...depotForm, capacite_max: e.target.value })} + /> +
+
+ +
+ + setDepotForm({ ...depotForm, specialites: e.target.value })} + /> + + Indiquez les séries de matériel ou types d'interventions supportés par cet atelier. + +
+ +
+ + +
+
+ +
+ setDepotModalOpen(false)} + disabled={isDepotSubmitting} + > + Annuler + + + {editingDepot ? 'Mettre à jour' : 'Créer le Dépôt'} + +
+
)} diff --git a/src/pages/dashboard/Dashboard.css b/src/pages/dashboard/Dashboard.css index ba42cbd..d7f48fb 100644 --- a/src/pages/dashboard/Dashboard.css +++ b/src/pages/dashboard/Dashboard.css @@ -1,1210 +1,1100 @@ /* ========================================================================== - DASHBOARD PAGE — MODERN SNCF DESIGN SYSTEM + DASHBOARD — BOOTSTRAP SNCF MÉTIERS & WCS DESIGN SYSTEM ========================================================================== */ +/* -------------------------------------------------------------------------- + 1. ROOT CONTAINER & MASTCONTAINER + -------------------------------------------------------------------------- */ .dashboard-page-root { min-height: calc(100vh - 72px); - background-color: #f1f5f9; - padding: 24px 20px 48px; + background-color: #f4f6f9; + padding: 0 0 60px 0; font-family: 'Avenir', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - color: #0f172a; + color: #1f2937; } -.dashboard-container { - max-width: 1380px; +.dashboard-mastcontainer { + max-width: 1440px; margin: 0 auto; + padding: 24px 24px 48px; } -/* ========================================================================== - 1. HERO BANNER - ========================================================================== */ -.dashboard-hero-card { - background: linear-gradient(135deg, #062743 0%, #0d3b66 50%, #0088ce 100%); - border-radius: 18px; - padding: 28px 32px; +/* -------------------------------------------------------------------------- + 2. CONTROLBAR SNCF MÉTIERS + -------------------------------------------------------------------------- */ +.sncf-metier-controlbar { + background: linear-gradient(135deg, #002244 0%, #003865 60%, #0088ce 100%); + border-radius: 12px; + padding: 22px 28px; color: #ffffff; - display: flex; - justify-content: space-between; - align-items: center; - gap: 24px; margin-bottom: 24px; - box-shadow: 0 16px 36px rgba(6, 39, 67, 0.22); + box-shadow: 0 10px 25px rgba(0, 34, 68, 0.2); + display: flex; + flex-direction: column; + gap: 16px; position: relative; overflow: hidden; } -.dashboard-hero-card::after { +.sncf-metier-controlbar::after { content: ''; position: absolute; - top: -60px; - right: -60px; - width: 240px; - height: 240px; + top: -80px; + right: -80px; + width: 260px; + height: 260px; border-radius: 50%; background: radial-gradient(circle, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0) 70%); pointer-events: none; } -.dashboard-hero-content { - max-width: 760px; +.controlbar-top-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 20px; + flex-wrap: wrap; z-index: 1; } -.dashboard-hero-kicker-row { +.controlbar-identity { display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; - margin-bottom: 10px; + flex-direction: column; + gap: 6px; + max-width: 780px; } -.dashboard-hero-kicker { +.controlbar-badges-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.controlbar-kicker { display: inline-flex; align-items: center; gap: 6px; - background: rgba(255, 255, 255, 0.14); - backdrop-filter: blur(8px); - border: 1px solid rgba(255, 255, 255, 0.2); - padding: 4px 12px; - border-radius: 999px; + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.25); + padding: 3px 10px; + border-radius: 20px; font-size: 11px; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.08em; + letter-spacing: 0.06em; color: #e0f2fe; } -.dashboard-hero-kicker wcs-mat-icon { - font-size: 14px; - color: #38bdf8; -} - -.dashboard-region-badge { +.controlbar-status-pill { display: inline-flex; align-items: center; gap: 6px; - background: rgba(56, 189, 248, 0.2); - border: 1px solid rgba(56, 189, 248, 0.4); - padding: 4px 12px; - border-radius: 999px; - font-size: 12px; - font-weight: 600; - color: #f0f9ff; + padding: 3px 10px; + border-radius: 20px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; } -.dashboard-region-badge wcs-mat-icon { - font-size: 14px; - color: #7dd3fc; +.controlbar-status-pill.is-nominal { + background: rgba(16, 185, 129, 0.25); + border: 1px solid #10b981; + color: #a7f3d0; } -.dashboard-hero-title { - margin: 0 0 10px; - font-size: 32px; +.controlbar-status-pill.is-disrupted { + background: rgba(239, 68, 68, 0.25); + border: 1px solid #ef4444; + color: #fecaca; +} + +.controlbar-title { + margin: 4px 0 2px; + font-size: 28px; font-weight: 800; letter-spacing: -0.02em; color: #ffffff; line-height: 1.2; } -.dashboard-user-greeting { - font-weight: 400; - opacity: 0.9; -} - -.dashboard-hero-desc { +.controlbar-subtitle { margin: 0; - font-size: 14px; - line-height: 1.6; - color: rgba(240, 249, 255, 0.88); + font-size: 13.5px; + line-height: 1.5; + color: rgba(240, 249, 255, 0.9); } -.dashboard-hero-actions { +.controlbar-actions { display: flex; flex-direction: column; align-items: flex-end; - gap: 14px; - flex-shrink: 0; + gap: 12px; z-index: 1; } -.dashboard-sync-status { +.controlbar-clock-badge { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; - color: rgba(240, 249, 255, 0.85); - background: rgba(0, 0, 0, 0.18); - padding: 4px 10px; - border-radius: 999px; + background: rgba(0, 0, 0, 0.25); + padding: 4px 12px; + border-radius: 20px; + color: #f0f9ff; + border: 1px solid rgba(255, 255, 255, 0.15); } -.dashboard-pulse-dot { +.controlbar-pulse-dot { width: 8px; height: 8px; border-radius: 50%; background-color: #22c55e; box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); - animation: pulseGreen 2s infinite; + animation: sncfPulse 2s infinite; } -@keyframes pulseGreen { - 0% { - box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); - } - 70% { - box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); - } - 100% { - box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); - } +@keyframes sncfPulse { + 0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); } + 70% { box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); } + 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); } } -.dashboard-btn-group { +.controlbar-buttons { display: flex; align-items: center; gap: 10px; + flex-wrap: wrap; } -.dashboard-btn-group wcs-button { - --wcs-button-border-radius: 8px; -} - -.dashboard-quick-create-btn { - --wcs-button-background-color: rgba(255, 255, 255, 0.15); - --wcs-button-color: #ffffff; - --wcs-button-border-color: rgba(255, 255, 255, 0.3); -} - -.dashboard-quick-create-btn:hover { - --wcs-button-background-color: rgba(255, 255, 255, 0.25); -} - -/* ========================================================================== - 2. SUPERVISION STATUS BAR - ========================================================================== */ -.dashboard-status-bar { - background: #ffffff; - border: 1px solid #dbe3ed; - border-radius: 14px; - padding: 14px 20px; +/* -------------------------------------------------------------------------- + 3. ALERT BANNER + -------------------------------------------------------------------------- */ +.dashboard-alert-wrapper { margin-bottom: 24px; +} + +.dashboard-alert-wrapper wcs-alert { + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.04); + border-radius: 8px; +} + +/* -------------------------------------------------------------------------- + 4. EXECUTIVE KPI CARDS GRID (WCS CARDS + SNCF MÉTIERS) + -------------------------------------------------------------------------- */ +.dashboard-kpis-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 18px; + margin-bottom: 24px; +} + +.dashboard-wcs-card { + --wcs-card-border-radius: 12px; + --wcs-card-border-color: #e2e8f0; + transition: transform 0.2s ease, box-shadow 0.2s ease; + cursor: pointer; + background: #ffffff; +} + +.dashboard-wcs-card:hover { + transform: translateY(-2px); + box-shadow: 0 10px 22px rgba(0, 47, 108, 0.08); +} + +.kpi-card-header { display: flex; - align-items: center; justify-content: space-between; - box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04); -} - -.dashboard-status-item { - display: flex; align-items: center; - gap: 12px; - flex: 1; + width: 100%; } -.dashboard-status-indicator { - width: 36px; - height: 36px; +.kpi-icon-square { + width: 42px; + height: 42px; border-radius: 10px; display: flex; align-items: center; justify-content: center; - flex-shrink: 0; -} - -.dashboard-status-indicator.is-success { - background: #ecfdf5; - color: #10b981; -} - -.dashboard-status-indicator.is-warning { - background: #fef2f2; - color: #ef4444; -} - -.dashboard-status-indicator.is-info { - background: #f0f9ff; - color: #0284c7; -} - -.dashboard-status-indicator.is-secondary { - background: #f5f3ff; - color: #7c3aed; -} - -.dashboard-status-indicator wcs-mat-icon { font-size: 20px; } -.dashboard-status-text { +.kpi-icon-square.bg-punctuality { background: #e0f2fe; color: #0284c7; } +.kpi-icon-square.bg-trains { background: #f0fdf4; color: #16a34a; } +.kpi-icon-square.bg-disruptions { background: #fef2f2; color: #dc2626; } +.kpi-icon-square.bg-works { background: #fffbeb; color: #d97706; } +.kpi-icon-square.bg-network { background: #f5f3ff; color: #7c3aed; } + +.kpi-main-body { + padding: 12px 0 6px; +} + +.kpi-value-row { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; +} + +.kpi-big-number { + font-size: 32px; + font-weight: 800; + color: #002244; + line-height: 1; + letter-spacing: -0.02em; +} + +.kpi-title { + font-size: 13.5px; + font-weight: 700; + color: #475569; + margin: 6px 0 4px; +} + +.kpi-subtitle { + font-size: 12px; + color: #64748b; + margin: 0; + line-height: 1.4; +} + +.kpi-progress-wrap { + margin-top: 10px; +} + +.kpi-card-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 8px; + border-top: 1px solid #f1f5f9; + font-size: 12px; + font-weight: 600; + color: #0088ce; +} + +.kpi-card-footer wcs-mat-icon { + font-size: 16px; + transition: transform 0.2s ease; +} + +.dashboard-wcs-card:hover .kpi-card-footer wcs-mat-icon { + transform: translateX(4px); +} + +/* -------------------------------------------------------------------------- + 5. SNCF MÉTIERS TABS & TOOLBAR + -------------------------------------------------------------------------- */ +.dashboard-navigation-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 0; + margin-bottom: 24px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.03); + overflow: hidden; +} + +.dashboard-tabs-header { + border-bottom: 1px solid #e2e8f0; + background: #f8fafc; + padding: 4px 16px 0; +} + +.dashboard-tabs-bar { + display: flex; + gap: 4px; + overflow-x: auto; + scrollbar-width: none; +} + +.dashboard-tabs-bar::-webkit-scrollbar { + display: none; +} + +.dashboard-tab-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 14px 18px; + font-size: 13.5px; + font-weight: 600; + color: #64748b; + border: none; + background: transparent; + border-bottom: 3px solid transparent; + cursor: pointer; + white-space: nowrap; + transition: all 0.2s ease; +} + +.dashboard-tab-btn:hover { + color: #002244; + background: rgba(0, 136, 206, 0.04); +} + +.dashboard-tab-btn.is-active { + color: #0088ce; + border-bottom-color: #0088ce; + background: #ffffff; + font-weight: 700; +} + +.dashboard-tab-btn wcs-mat-icon { + font-size: 18px; +} + +/* Toolbar de filtrage */ +.dashboard-filter-toolbar { + padding: 16px 20px; + background: #ffffff; + border-bottom: 1px solid #f1f5f9; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} + +.filter-left-col { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + min-width: 280px; +} + +.filter-search-box { + width: 100%; + max-width: 380px; +} + +.filter-right-col { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.filter-chips-row { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +/* -------------------------------------------------------------------------- + 6. CONTENU DES ONGLETS + -------------------------------------------------------------------------- */ +.dashboard-tab-content-area { + padding: 20px; +} + +/* Grille principale Vue d'ensemble */ +.overview-grid { + display: grid; + grid-template-columns: 1.4fr 1fr; + gap: 20px; +} + +@media (max-width: 1024px) { + .overview-grid { + grid-template-columns: 1fr; + } +} + +.dashboard-section-title-wrap { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14px; +} + +.dashboard-section-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 700; + color: #002244; + margin: 0; +} + +.dashboard-section-title wcs-mat-icon { + font-size: 20px; + color: #0088ce; +} + +/* -------------------------------------------------------------------------- + 7. LISTES MÉTIERS & MANAGEMENT ITEMS + -------------------------------------------------------------------------- */ +.sncf-management-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.sncf-management-item { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 10px; + padding: 14px 18px; + display: flex; + flex-direction: column; + gap: 10px; + transition: all 0.2s ease; + cursor: pointer; + position: relative; +} + +.sncf-management-item:hover { + border-color: #0088ce; + box-shadow: 0 4px 14px rgba(0, 47, 108, 0.07); + background: #fafcfe; +} + +.sncf-management-item.item-disruption { + border-left: 4px solid #f59e0b; +} + +.sncf-management-item.item-suppression { + border-left: 4px solid #ef4444; +} + +.sncf-management-item.item-ontime { + border-left: 4px solid #10b981; +} + +.item-top-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.item-train-badge { + display: inline-flex; + align-items: center; + gap: 6px; + background: #f1f5f9; + padding: 3px 8px; + border-radius: 6px; + font-size: 13px; + font-weight: 700; + color: #002244; +} + +.item-route-display { + display: flex; + align-items: center; + gap: 12px; + font-size: 14px; + font-weight: 600; + color: #1e293b; +} + +.item-station-time { + font-size: 14px; + font-weight: 800; + color: #002244; +} + +.item-station-name { + color: #334155; +} + +.item-route-arrow { + color: #94a3b8; + font-size: 18px; +} + +.item-bottom-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + font-size: 12px; + color: #64748b; + border-top: 1px solid #f1f5f9; + padding-top: 8px; +} + +.item-cause-text { + display: inline-flex; + align-items: center; + gap: 6px; + font-style: italic; +} + +.item-quai-badge { + background: #e0f2fe; + color: #0369a1; + font-weight: 700; + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; +} + +/* -------------------------------------------------------------------------- + 8. RACCOURCIS OPÉRATIONNELS SNCF (TILES) + -------------------------------------------------------------------------- */ +.sncf-actions-grid { + display: grid; + grid-template-columns: 1fr; + gap: 10px; +} + +.sncf-action-tile { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 16px; + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 10px; + text-decoration: none; + color: inherit; + transition: all 0.2s ease; +} + +.sncf-action-tile:hover { + border-color: #0088ce; + background: #f8fafc; + transform: translateX(3px); + text-decoration: none; +} + +.action-tile-icon { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + flex-shrink: 0; +} + +.action-tile-icon.bg-red { background: #fee2e2; color: #dc2626; } +.action-tile-icon.bg-amber { background: #fef3c7; color: #d97706; } +.action-tile-icon.bg-blue { background: #e0f2fe; color: #0284c7; } +.action-tile-icon.bg-indigo { background: #e0e7ff; color: #4f46e5; } +.action-tile-icon.bg-emerald { background: #d1fae5; color: #059669; } +.action-tile-icon.bg-purple { background: #f3e8ff; color: #9333ea; } + +.action-tile-content { + flex: 1; display: flex; flex-direction: column; gap: 2px; } -.dashboard-status-label { +.action-tile-content strong { + font-size: 13.5px; + color: #0f172a; +} + +.action-tile-content span { + font-size: 11.5px; + color: #64748b; +} + +.action-tile-chevron { + color: #94a3b8; + font-size: 18px; +} + +/* -------------------------------------------------------------------------- + 9. TABLES SNCF MÉTIERS + -------------------------------------------------------------------------- */ +.sncf-metier-table-container { + overflow-x: auto; + border: 1px solid #e2e8f0; + border-radius: 10px; + background: #ffffff; +} + +.sncf-metier-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + text-align: left; +} + +.sncf-metier-table th { + background: #f8fafc; + padding: 12px 16px; + font-weight: 700; + color: #475569; + border-bottom: 2px solid #e2e8f0; + text-transform: uppercase; + font-size: 11px; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.sncf-metier-table td { + padding: 12px 16px; + border-bottom: 1px solid #f1f5f9; + color: #1e293b; + vertical-align: middle; +} + +.sncf-metier-table tbody tr { + transition: background 0.15s ease; + cursor: pointer; +} + +.sncf-metier-table tbody tr:hover { + background: #f0f7ff; +} + +.table-time { + font-weight: 700; + color: #002244; +} + +.table-train-num { + font-weight: 700; + color: #0088ce; +} + +/* -------------------------------------------------------------------------- + 10. MODAL DÉTAILS WCS + -------------------------------------------------------------------------- */ +.modal-detail-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-bottom: 16px; +} + +.modal-detail-card { + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 14px; +} + +.modal-detail-card h5 { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #64748b; + margin: 0 0 8px; +} + +.modal-route-timeline { + display: flex; + flex-direction: column; + gap: 12px; + position: relative; + padding-left: 20px; + margin: 16px 0; +} + +.modal-route-timeline::before { + content: ''; + position: absolute; + left: 6px; + top: 8px; + bottom: 8px; + width: 2px; + background: #cbd5e1; +} + +.modal-timeline-stop { + position: relative; + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + font-size: 13px; +} + +.modal-timeline-stop::before { + content: ''; + position: absolute; + left: -20px; + top: 5px; + width: 10px; + height: 10px; + border-radius: 50%; + background: #ffffff; + border: 2px solid #0088ce; +} + +.modal-timeline-stop.is-origin::before { + border-color: #10b981; + background: #10b981; +} + +.modal-timeline-stop.is-destination::before { + border-color: #ef4444; + background: #ef4444; +} + +/* -------------------------------------------------------------------------- + 11. ÉTATS VIDES & CHARGEMENT + -------------------------------------------------------------------------- */ +.dashboard-empty-state { + text-align: center; + padding: 48px 24px; + background: #ffffff; + border: 1px dashed #cbd5e1; + border-radius: 12px; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.dashboard-empty-state wcs-mat-icon { + font-size: 48px; + color: #94a3b8; +} + +.dashboard-empty-state h4 { + margin: 0; + font-size: 16px; + font-weight: 700; + color: #334155; +} + +.dashboard-empty-state p { + margin: 0; + font-size: 13px; + color: #64748b; + max-width: 420px; +} + +.dashboard-loading-box { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + padding: 60px 20px; + color: #64748b; + font-weight: 600; +} + +/* -------------------------------------------------------------------------- + 12. DARK MODE OVERRIDES + -------------------------------------------------------------------------- */ +body.dark-mode .dashboard-page-root { + background-color: #0d1117; + color: #e6edf3; +} + +body.dark-mode .dashboard-wcs-card { + --wcs-card-border-color: #30363d; + background: #161b22; +} + +body.dark-mode .kpi-big-number { + color: #ffffff; +} + +body.dark-mode .kpi-title { + color: #94a3b8; +} + +body.dark-mode .kpi-subtitle { + color: #64748b; +} + +body.dark-mode .kpi-card-footer { + border-top-color: #21262d; +} + +body.dark-mode .dashboard-navigation-card { + background: #161b22; + border-color: #30363d; +} + +body.dark-mode .dashboard-tabs-header { + background: #0d1117; + border-bottom-color: #30363d; +} + +body.dark-mode .dashboard-tab-btn { + color: #8b949e; +} + +body.dark-mode .dashboard-tab-btn:hover { + color: #f0f6fc; + background: rgba(255, 255, 255, 0.05); +} + +body.dark-mode .dashboard-tab-btn.is-active { + color: #58a6ff; + border-bottom-color: #58a6ff; + background: #161b22; +} + +body.dark-mode .dashboard-filter-toolbar { + background: #161b22; + border-bottom-color: #21262d; +} + +body.dark-mode .sncf-management-item { + background: #161b22; + border-color: #30363d; + color: #e6edf3; +} + +body.dark-mode .sncf-management-item:hover { + background: #1f242c; + border-color: #58a6ff; +} + +body.dark-mode .item-train-badge { + background: #21262d; + color: #f0f6fc; +} + +body.dark-mode .item-station-time { + color: #ffffff; +} + +body.dark-mode .item-station-name { + color: #c9d1d9; +} + +body.dark-mode .item-bottom-meta { + border-top-color: #21262d; + color: #8b949e; +} + +body.dark-mode .sncf-action-tile { + background: #161b22; + border-color: #30363d; +} + +body.dark-mode .sncf-action-tile:hover { + background: #1f242c; + border-color: #58a6ff; +} + +body.dark-mode .action-tile-content strong { + color: #f0f6fc; +} + +body.dark-mode .action-tile-content span { + color: #8b949e; +} + +body.dark-mode .sncf-metier-table-container { + background: #161b22; + border-color: #30363d; +} + +body.dark-mode .sncf-metier-table th { + background: #0d1117; + color: #8b949e; + border-bottom-color: #30363d; +} + +body.dark-mode .sncf-metier-table td { + border-bottom-color: #21262d; + color: #c9d1d9; +} + +body.dark-mode .sncf-metier-table tbody tr:hover { + background: #1f242c; +} + +body.dark-mode .table-time { + color: #ffffff; +} + +body.dark-mode .table-train-num { + color: #58a6ff; +} + +body.dark-mode .modal-detail-card { + background: #0d1117; + border-color: #30363d; +} + +body.dark-mode .dashboard-empty-state { + background: #161b22; + border-color: #30363d; +} + +/* -------------------------------------------------------------------------- + 13. WCS TABS & WCS ACCORDION COMPONENT ENHANCEMENTS + -------------------------------------------------------------------------- */ +.dashboard-tabs-wrapper { + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + padding: 4px 16px 0; +} + +.dashboard-tabs-wrapper wcs-tabs { + --wcs-tab-active-color: #0088ce; + --wcs-tab-active-border-color: #0088ce; + width: 100%; +} + +wcs-card-header { + display: block; + padding: 16px 20px 12px; + border-bottom: 1px solid #f1f5f9; +} + +.controlbar-refresh-timer { + font-size: 11px; + color: #38bdf8; + font-weight: 700; + margin-left: 4px; +} + +wcs-accordion { + display: flex; + flex-direction: column; + gap: 8px; +} + +wcs-accordion-panel { + border: 1px solid #e2e8f0; + border-radius: 8px; + overflow: hidden; + background: #ffffff; +} + +wcs-accordion-header { + padding: 12px 16px; + font-weight: 600; + color: #1e293b; + cursor: pointer; + background: #f8fafc; + transition: background 0.2s ease; +} + +wcs-accordion-header:hover { + background: #f1f5f9; +} + +wcs-accordion-content { + padding: 0; +} + +body.dark-mode .dashboard-tabs-wrapper { + background: #0d1117; + border-bottom-color: #30363d; +} + +body.dark-mode wcs-card-header { + border-bottom-color: #30363d; +} + +body.dark-mode wcs-accordion-panel { + border-color: #30363d; + background: #161b22; +} + +body.dark-mode wcs-accordion-header { + background: #161b22; + color: #f0f6fc; +} + +/* -------------------------------------------------------------------------- + 14. WCS GALACTIC, ACTION-BAR, FOOTER, COUNTER & STEPPER STYLES + -------------------------------------------------------------------------- */ +wcs-galactic { + --wcs-galactic-background-color: #001a33; + --wcs-galactic-text-color: #ffffff; + --wcs-galactic-height: 42px; + width: 100%; + display: block; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); +} + +.dashboard-wcs-actionbar { + --wcs-action-bar-background-color: #ffffff; + --wcs-action-bar-title-color: #002244; + --wcs-action-bar-padding-horizontal: 24px; + --wcs-action-bar-padding-vertical: 16px; + border-top-left-radius: 12px; + border-top-right-radius: 12px; +} + +.dashboard-filter-toolbar wcs-form-field { + width: 100%; +} + +.dashboard-filter-toolbar wcs-form-field wcs-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: #64748b; + margin-bottom: 2px; } -.dashboard-status-text strong { - font-size: 14px; - color: #0f172a; -} - -.dashboard-status-divider { - width: 1px; - height: 32px; - background: #e2e8f0; - margin: 0 16px; -} - -/* ========================================================================== - 3. KPI CARDS - ========================================================================== */ -.dashboard-kpis-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 16px; - margin-bottom: 24px; -} - -.dashboard-kpi-card { - background: #ffffff; - border: 1px solid #dbe3ed; - border-radius: 14px; - padding: 18px 20px; - box-shadow: 0 6px 20px rgba(15, 23, 42, 0.04); - cursor: pointer; - display: flex; - flex-direction: column; - transition: all 0.2s ease; - position: relative; - overflow: hidden; -} - -.dashboard-kpi-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: #cbd5e1; - transition: background 0.2s ease; -} - -.dashboard-kpi-card.card-horaires::before { background: #0088ce; } -.dashboard-kpi-card.card-perturbations::before { background: #f59e0b; } -.dashboard-kpi-card.card-gares::before { background: #10b981; } -.dashboard-kpi-card.card-lignes::before { background: #8b5cf6; } - -.dashboard-kpi-card:hover { - transform: translateY(-3px); - box-shadow: 0 12px 28px rgba(15, 23, 42, 0.08); - border-color: #cbd5e1; -} - -.dashboard-kpi-top { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 12px; -} - -.dashboard-kpi-icon-wrapper { - width: 40px; - height: 40px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; -} - -.card-horaires .dashboard-kpi-icon-wrapper { background: #e0f2fe; color: #0284c7; } -.card-perturbations .dashboard-kpi-icon-wrapper { background: #fef3c7; color: #d97706; } -.card-gares .dashboard-kpi-icon-wrapper { background: #dcfce7; color: #16a34a; } -.card-lignes .dashboard-kpi-icon-wrapper { background: #ede9fe; color: #7c3aed; } - -.dashboard-kpi-badge { - font-size: 11px; - font-weight: 700; - padding: 3px 8px; - border-radius: 999px; - background: #f1f5f9; - color: #475569; -} - -.dashboard-kpi-badge.badge-danger { - background: #fee2e2; - color: #dc2626; -} - -.dashboard-kpi-badge.badge-neutral { - background: #ecfdf5; - color: #16a34a; -} - -.dashboard-kpi-number { - font-size: 32px; - font-weight: 800; - color: #0f172a; - line-height: 1.1; - margin-bottom: 4px; -} - -.dashboard-kpi-title { - font-size: 14px; - font-weight: 600; - color: #475569; - margin-bottom: 12px; -} - -.dashboard-kpi-footer { - display: flex; - align-items: center; - justify-content: space-between; - font-size: 12px; - color: #64748b; - border-top: 1px solid #f1f5f9; - padding-top: 10px; - margin-top: auto; -} - -.dashboard-kpi-footer wcs-mat-icon { - font-size: 14px; - transition: transform 0.2s ease; -} - -.dashboard-kpi-card:hover .dashboard-kpi-footer wcs-mat-icon { - transform: translateX(3px); - color: #0088ce; -} - -/* ========================================================================== - 4. MAIN GRID LAYOUT - ========================================================================== */ -.dashboard-main-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - align-items: flex-start; -} - -.dashboard-column { - display: flex; - flex-direction: column; - gap: 20px; -} - -.dashboard-widget-card { - background: #ffffff; - border: 1px solid #dbe3ed; - border-radius: 16px; - box-shadow: 0 6px 20px rgba(15, 23, 42, 0.04); - overflow: hidden; -} - -.dashboard-widget-header { - padding: 16px 20px; - border-bottom: 1px solid #eef2f6; - background: #ffffff; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.dashboard-widget-title-wrap { - display: flex; - align-items: center; - gap: 12px; -} - -.dashboard-widget-icon { - width: 38px; - height: 38px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.dashboard-widget-icon.icon-warning { - background: #fef3c7; - color: #d97706; -} - -.dashboard-widget-icon.icon-info { - background: #e0f2fe; - color: #0284c7; -} - -.dashboard-widget-icon.icon-primary { - background: #e0e7ff; - color: #4338ca; -} - -.dashboard-widget-title { - margin: 0; - font-size: 16px; - font-weight: 700; - color: #0f172a; -} - -.dashboard-widget-subtitle { - font-size: 12px; - color: #64748b; -} - -.dashboard-widget-body { - padding: 16px; -} - -/* Incident Items */ -.dashboard-incident-list { - display: flex; - flex-direction: column; - gap: 10px; -} - -.dashboard-incident-item { - background: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 12px; - padding: 14px 16px; - cursor: pointer; - transition: all 0.15s ease; - display: flex; - flex-direction: column; - gap: 8px; -} - -.dashboard-incident-item:hover { - background: #ffffff; - border-color: #cbd5e1; - box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06); - transform: translateY(-1px); -} - -.dashboard-incident-item.item-suppression { - border-left: 4px solid #ef4444; -} - -.dashboard-incident-item.item-retard { - border-left: 4px solid #f59e0b; -} - -.dashboard-incident-header { - display: flex; - align-items: center; - justify-content: space-between; -} - -.dashboard-incident-train-badge { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 13px; - font-weight: 700; - color: #0f172a; -} - -.dashboard-badge-pill { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 3px 8px; - border-radius: 999px; - font-size: 11px; - font-weight: 700; -} - -.dashboard-badge-pill.pill-warning { - background: #fef3c7; - color: #b45309; -} - -.dashboard-badge-pill.pill-danger { - background: #fee2e2; - color: #b91c1c; -} - -.dashboard-badge-pill.pill-info { - background: #e0f2fe; - color: #0369a1; -} - -.dashboard-incident-route { - display: flex; - align-items: center; - gap: 8px; - font-size: 13px; - font-weight: 600; - color: #334155; -} - -.dashboard-incident-route wcs-mat-icon { - font-size: 14px; +.dashboard-filter-toolbar wcs-form-field wcs-hint { + font-size: 10.5px; color: #94a3b8; -} - -.dashboard-incident-footer { - display: flex; - align-items: center; - justify-content: space-between; - font-size: 12px; - color: #64748b; margin-top: 2px; } -.dashboard-incident-cause { - display: inline-flex; - align-items: center; - gap: 4px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 80%; +wcs-counter { + --wcs-counter-height-m: 32px; + --wcs-counter-border-radius: 6px; } -.dashboard-incident-cause wcs-mat-icon { - font-size: 14px; -} - -.dashboard-status-tag { - font-size: 11px; - font-weight: 700; - padding: 2px 6px; - border-radius: 4px; -} - -.dashboard-status-tag.tag-active { - color: #dc2626; - background: #fef2f2; -} - -.dashboard-status-tag.tag-inactive { - color: #64748b; - background: #f1f5f9; -} - -/* Quick Action Tiles */ -.dashboard-quick-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; -} - -.dashboard-action-tile { +.modal-stepper-box { background: #f8fafc; border: 1px solid #e2e8f0; - border-radius: 12px; - padding: 14px 16px; - text-decoration: none; - display: flex; - align-items: center; - gap: 12px; - transition: all 0.15s ease; - color: #0f172a; -} - -.dashboard-action-tile:hover { - background: #ffffff; - border-color: #0088ce; - box-shadow: 0 4px 14px rgba(0, 136, 206, 0.1); - transform: translateY(-1px); -} - -.dashboard-action-icon { - width: 40px; - height: 40px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.dashboard-action-icon.bg-amber { background: #fef3c7; color: #d97706; } -.dashboard-action-icon.bg-orange { background: #ffedd5; color: #ea580c; } -.dashboard-action-icon.bg-sky { background: #e0f2fe; color: #0284c7; } -.dashboard-action-icon.bg-indigo { background: #e0e7ff; color: #4f46e5; } - -.dashboard-action-info { - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; - flex: 1; -} - -.dashboard-action-info strong { - font-size: 13px; - color: #0f172a; -} - -.dashboard-action-info span { - font-size: 11px; - color: #64748b; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.action-chevron { - font-size: 18px; - color: #94a3b8; - transition: transform 0.2s ease, color 0.2s ease; -} - -.dashboard-action-tile:hover .action-chevron { - transform: translateX(3px); - color: #0088ce; -} - -/* Schedule Items */ -.dashboard-schedule-list { - display: flex; - flex-direction: column; - gap: 10px; -} - -.dashboard-schedule-item { - background: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 12px; - padding: 14px 16px; - cursor: pointer; - transition: all 0.15s ease; - display: flex; - flex-direction: column; - gap: 10px; -} - -.dashboard-schedule-item:hover { - background: #ffffff; - border-color: #cbd5e1; - box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06); - transform: translateY(-1px); -} - -.dashboard-schedule-header { - display: flex; - align-items: center; - justify-content: space-between; -} - -.dashboard-schedule-badges { - display: flex; - align-items: center; - gap: 8px; -} - -.train-id-badge { - display: inline-flex; - align-items: center; - gap: 5px; - font-size: 13px; - font-weight: 800; - color: #0f172a; - background: #ffffff; - border: 1px solid #cbd5e1; - padding: 3px 8px; - border-radius: 6px; -} - -.train-id-badge wcs-mat-icon { - font-size: 14px; - color: #0088ce; -} - -.train-type-pill { - font-size: 11px; - font-weight: 700; - color: #0284c7; - background: #e0f2fe; - padding: 3px 8px; - border-radius: 999px; -} - -.line-pill { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: 11px; - font-weight: 600; - color: #475569; - background: #f1f5f9; - padding: 3px 8px; - border-radius: 6px; -} - -.line-pill wcs-mat-icon { - font-size: 13px; - color: #64748b; -} - -.dashboard-schedule-timeline { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 4px 0; -} - -.timeline-station { - display: flex; - flex-direction: column; - gap: 2px; -} - -.timeline-station.arrival { - align-items: flex-end; -} - -.station-time { - font-size: 16px; - font-weight: 800; - color: #0f172a; -} - -.station-title { - font-size: 12px; - font-weight: 600; - color: #475569; -} - -.timeline-connector { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - position: relative; -} - -.connector-line { - position: absolute; - left: 0; - right: 0; - height: 2px; - background: #cbd5e1; -} - -.timeline-connector wcs-mat-icon { - position: relative; - background: #f8fafc; - padding: 0 4px; - font-size: 16px; - color: #0088ce; -} - -.dashboard-schedule-stops { - display: flex; - align-items: center; - gap: 4px; - font-size: 11px; - color: #64748b; - border-top: 1px solid #f1f5f9; - padding-top: 6px; -} - -.dashboard-schedule-stops wcs-mat-icon { - font-size: 14px; -} - -/* System summary card */ -.dashboard-system-card { - background: linear-gradient(135deg, #0e1726 0%, #151f30 100%); - border-radius: 14px; - padding: 16px 20px; - color: #ffffff; -} - -.dashboard-system-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 14px; -} - -.dashboard-system-icon { - width: 36px; - height: 36px; border-radius: 8px; - background: rgba(56, 189, 248, 0.15); - color: #38bdf8; - display: flex; + padding: 16px; +} + +.sncf-dashboard-footer { + --wcs-footer-background-color: #002244; + --wcs-footer-text-color: #f8fafc; + --wcs-footer-padding: 24px 28px; + border-radius: 12px; + margin-top: 48px; + box-shadow: 0 8px 24px rgba(0, 34, 68, 0.15); +} + +.footer-sncf-badge { + background: #0088ce; + border-radius: 8px; + padding: 8px; + display: inline-flex; align-items: center; justify-content: center; } -.dashboard-system-header h4 { - margin: 0; - font-size: 14px; - font-weight: 700; - color: #f8fafc; +/* Dark mode extensions */ +body.dark-mode wcs-galactic { + --wcs-galactic-background-color: #090d16; } -.dashboard-system-header p { - margin: 2px 0 0; - font-size: 12px; - color: #94a3b8; +body.dark-mode .dashboard-wcs-actionbar { + --wcs-action-bar-background-color: #161b22; + --wcs-action-bar-title-color: #f0f6fc; } -.dashboard-system-stats { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 10px; - border-top: 1px solid rgba(255, 255, 255, 0.1); - padding-top: 12px; +body.dark-mode .dashboard-filter-toolbar wcs-form-field wcs-label { + color: #8b949e; } -.system-stat-col { - display: flex; - flex-direction: column; - gap: 2px; +body.dark-mode .dashboard-filter-toolbar wcs-form-field wcs-hint { + color: #6e7681; } -.system-stat-col span { - font-size: 11px; - color: #94a3b8; +body.dark-mode .modal-stepper-box { + background: #0d1117; + border-color: #30363d; } -.system-stat-col strong { - font-size: 18px; - color: #f8fafc; +body.dark-mode .sncf-dashboard-footer { + --wcs-footer-background-color: #161b22; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); } -.system-stat-col strong.text-warning { - color: #fbbf24; -} - -.system-stat-col strong.text-success { - color: #4ade80; -} - -/* States */ -.dashboard-loading-box, -.dashboard-empty-box { - padding: 36px 20px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - gap: 10px; -} - -.dashboard-loading-box span { - font-size: 13px; - color: #64748b; -} - -.dashboard-empty-icon { - width: 48px; - height: 48px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 4px; -} - -.dashboard-empty-icon.is-success { - background: #ecfdf5; - color: #10b981; -} - -.dashboard-empty-icon.is-info { - background: #f0f9ff; - color: #0284c7; -} - -.dashboard-empty-icon wcs-mat-icon { - font-size: 28px; -} - -.dashboard-empty-box h4 { - margin: 0; - font-size: 15px; - color: #0f172a; -} - -.dashboard-empty-box p { - margin: 0; - font-size: 13px; - color: #64748b; - max-width: 320px; -} - -/* ========================================================================== - 5. RESPONSIVE BREAKPOINTS - ========================================================================== */ -@media (max-width: 1200px) { - .dashboard-kpis-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (max-width: 980px) { - .dashboard-hero-card { - flex-direction: column; - align-items: stretch; - padding: 24px; - } - - .dashboard-hero-actions { - align-items: flex-start; - width: 100%; - } - - .dashboard-btn-group { - width: 100%; - } - - .dashboard-btn-group wcs-button { - flex: 1; - } - - .dashboard-main-grid { - grid-template-columns: 1fr; - } - - .dashboard-status-bar { - flex-direction: column; - align-items: stretch; - gap: 12px; - } - - .dashboard-status-divider { - display: none; - } -} - -@media (max-width: 640px) { - .dashboard-page-root { - padding: 14px 12px 36px; - } - - .dashboard-hero-card { - border-radius: 14px; - padding: 18px 16px; - } - - .dashboard-hero-title { - font-size: 24px; - } - - .dashboard-kpis-grid { - grid-template-columns: 1fr; - } - - .dashboard-quick-grid { - grid-template-columns: 1fr; - } - - .dashboard-btn-group { - flex-direction: column; - } - - .dashboard-btn-group wcs-button { - width: 100%; - } -} - -/* ========================================================================== - 6. DARK MODE - ========================================================================== */ -body.dark-mode .dashboard-page-root, -[data-theme="dark"] .dashboard-page-root { - background-color: #0b111e; - color: #f8fafc; -} - -body.dark-mode .dashboard-hero-card, -[data-theme="dark"] .dashboard-hero-card { - background: linear-gradient(135deg, #051829 0%, #0d2847 60%, #15416b 100%); - border: 1px solid #243247; - box-shadow: 0 16px 36px rgba(0, 0, 0, 0.45); -} - -body.dark-mode .dashboard-status-bar, -[data-theme="dark"] .dashboard-status-bar { - background: #151f30; - border-color: #243247; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); -} - -body.dark-mode .dashboard-status-text strong, -[data-theme="dark"] .dashboard-status-text strong { - color: #f8fafc; -} - -body.dark-mode .dashboard-status-label, -[data-theme="dark"] .dashboard-status-label { - color: #94a3b8; -} - -body.dark-mode .dashboard-status-divider, -[data-theme="dark"] .dashboard-status-divider { - background: #243247; -} - -body.dark-mode .dashboard-kpi-card, -[data-theme="dark"] .dashboard-kpi-card { - background: #151f30; - border-color: #243247; - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); -} - -body.dark-mode .dashboard-kpi-card:hover, -[data-theme="dark"] .dashboard-kpi-card:hover { - border-color: #38bdf8; - box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4); -} - -body.dark-mode .dashboard-kpi-number, -[data-theme="dark"] .dashboard-kpi-number { - color: #f8fafc; -} - -body.dark-mode .dashboard-kpi-title, -[data-theme="dark"] .dashboard-kpi-title { - color: #94a3b8; -} - -body.dark-mode .dashboard-kpi-footer, -[data-theme="dark"] .dashboard-kpi-footer { - border-top-color: #243247; - color: #64748b; -} - -body.dark-mode .dashboard-widget-card, -[data-theme="dark"] .dashboard-widget-card { - background: #151f30; - border-color: #243247; - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); -} - -body.dark-mode .dashboard-widget-header, -[data-theme="dark"] .dashboard-widget-header { - background: #151f30; - border-bottom-color: #243247; -} - -body.dark-mode .dashboard-widget-title, -[data-theme="dark"] .dashboard-widget-title { - color: #f8fafc; -} - -body.dark-mode .dashboard-widget-subtitle, -[data-theme="dark"] .dashboard-widget-subtitle { - color: #94a3b8; -} - -body.dark-mode .dashboard-incident-item, -body.dark-mode .dashboard-schedule-item, -[data-theme="dark"] .dashboard-incident-item, -[data-theme="dark"] .dashboard-schedule-item { - background: #0e1726; - border-color: #243247; -} - -body.dark-mode .dashboard-incident-item:hover, -body.dark-mode .dashboard-schedule-item:hover, -[data-theme="dark"] .dashboard-incident-item:hover, -[data-theme="dark"] .dashboard-schedule-item:hover { - background: #152238; - border-color: #334e68; -} - -body.dark-mode .dashboard-incident-train-badge, -[data-theme="dark"] .dashboard-incident-train-badge { - color: #f8fafc; -} - -body.dark-mode .dashboard-incident-route, -[data-theme="dark"] .dashboard-incident-route { - color: #cbd5e1; -} - -body.dark-mode .dashboard-incident-cause, -[data-theme="dark"] .dashboard-incident-cause { - color: #94a3b8; -} - -body.dark-mode .dashboard-action-tile, -[data-theme="dark"] .dashboard-action-tile { - background: #0e1726; - border-color: #243247; - color: #f8fafc; -} - -body.dark-mode .dashboard-action-tile:hover, -[data-theme="dark"] .dashboard-action-tile:hover { - background: #152238; - border-color: #38bdf8; -} - -body.dark-mode .dashboard-action-info strong, -[data-theme="dark"] .dashboard-action-info strong { - color: #f8fafc; -} - -body.dark-mode .dashboard-action-info span, -[data-theme="dark"] .dashboard-action-info span { - color: #94a3b8; -} - -body.dark-mode .train-id-badge, -[data-theme="dark"] .train-id-badge { - background: #151f30; - border-color: #243247; - color: #f8fafc; -} - -body.dark-mode .line-pill, -[data-theme="dark"] .line-pill { - background: #151f30; - color: #cbd5e1; -} - -body.dark-mode .station-time, -[data-theme="dark"] .station-time { - color: #f8fafc; -} - -body.dark-mode .station-title, -[data-theme="dark"] .station-title { - color: #94a3b8; -} - -body.dark-mode .connector-line, -[data-theme="dark"] .connector-line { - background: #243247; -} - -body.dark-mode .timeline-connector wcs-mat-icon, -[data-theme="dark"] .timeline-connector wcs-mat-icon { - background: #0e1726; -} - -body.dark-mode .dashboard-schedule-stops, -[data-theme="dark"] .dashboard-schedule-stops { - border-top-color: #1a2a42; - color: #94a3b8; -} - -body.dark-mode .dashboard-empty-box h4, -[data-theme="dark"] .dashboard-empty-box h4 { - color: #f8fafc; -} - -body.dark-mode .dashboard-empty-box p, -[data-theme="dark"] .dashboard-empty-box p { - color: #94a3b8; -} diff --git a/src/pages/dashboard/Dashboard.js b/src/pages/dashboard/Dashboard.js index e93655c..670b0d3 100644 --- a/src/pages/dashboard/Dashboard.js +++ b/src/pages/dashboard/Dashboard.js @@ -1,32 +1,60 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useRef } from 'react'; import { getAllHoraires } from '../../api/horaires'; import { getAllPerturbations } from '../../api/perturbations'; +import { getAllTravaux } from '../../api/travaux'; +import { getAllMateriels } from '../../api/materielsRoulants'; import { getCurrentUser, getScopedRegionId } from '../../utils/userUtils'; import './Dashboard.css'; export default function Dashboard() { - // État des statistiques - const [stats, setStats] = useState({ - horaires: { total: 0, actifs: 0 }, - perturbations: { total: 0, actives: 0, retards: 0, suppressions: 0 }, - gares: { total: 0 }, - lignes: { total: 0 }, - typesTrains: { total: 0 } - }); - - // État du chargement + // --------------------------------------------------------------------------- + // ÉTATS DES DONNÉES & UTILISATEUR + // --------------------------------------------------------------------------- const [loading, setLoading] = useState(true); const [lastRefreshed, setLastRefreshed] = useState(new Date()); - - // Perturbations récentes - const [recentPerturbations, setRecentPerturbations] = useState([]); - - // Horaires récents - const [recentHoraires, setRecentHoraires] = useState([]); - const [scopedRegionId, setScopedRegionId] = useState(null); const [currentUser, setCurrentUser] = useState(null); + const [scopedRegionId, setScopedRegionId] = useState(null); - // Fonction de chargement des données + // Données brutes + const [horaires, setHoraires] = useState([]); + const [perturbations, setPerturbations] = useState([]); + const [travaux, setTravaux] = useState([]); + const [materiels, setMateriels] = useState([]); + + // Navigation par onglet ('overview', 'schedules', 'incidents', 'works', 'fleet') + const [activeTab, setActiveTab] = useState('overview'); + + // Options & Auto-refresh + const [autoRefresh, setAutoRefresh] = useState(false); + const [refreshCountdown, setRefreshCountdown] = useState(30); + + // Filtres et recherche + const [searchTerm, setSearchTerm] = useState(''); + const [selectedTypeFilter, setSelectedTypeFilter] = useState('all'); + const [selectedLineFilter, setSelectedLineFilter] = useState(''); + const [selectedPerturbationType, setSelectedPerturbationType] = useState('all'); + const [punctualityFilterMode, setPunctualityFilterMode] = useState('all'); // 'all', 'delayed', 'ontime' + const [onlyWithStops, setOnlyWithStops] = useState(false); + const [onlyWithTrack, setOnlyWithTrack] = useState(false); + + // Limite dynamique des départs imminents (contrôlée par wcs-counter) + const [imminentLimit, setImminentLimit] = useState(6); + + // Modal de détail interactif + const [selectedItem, setSelectedItem] = useState(null); // { type: 'horaire'|'perturbation', data: ... } + + // Références WCS Web Components + const tabsRef = useRef(null); + const modalRef = useRef(null); + const switchRef = useRef(null); + const radioGroupRef = useRef(null); + const checkStopsRef = useRef(null); + const checkTrackRef = useRef(null); + const counterRef = useRef(null); + + // --------------------------------------------------------------------------- + // CHARGEMENT COMPLET DES DONNÉES DU RÉSEAU + // --------------------------------------------------------------------------- const loadDashboardData = useCallback(async () => { setLoading(true); try { @@ -35,556 +63,1980 @@ export default function Dashboard() { const regionId = getScopedRegionId(user); setScopedRegionId(regionId || null); - // Charger les horaires - const { data: horairesData } = await getAllHoraires(regionId); - const horaires = horairesData || []; + // Chargement en parallèle de toutes les entités du système + const [horairesRes, perturbationsRes, travauxRes, materielsRes] = await Promise.all([ + getAllHoraires(regionId), + getAllPerturbations(regionId), + getAllTravaux(regionId).catch(() => ({ data: [] })), + getAllMateriels().catch(() => ({ data: [] })) + ]); - // Charger les perturbations - const { data: perturbationsData } = await getAllPerturbations(regionId); - const perturbations = perturbationsData || []; - - // Calculer les statistiques des horaires - const horaireStats = { - total: horaires.length, - actifs: horaires.length - }; - - // Calculer les statistiques des perturbations - const perturbationStats = { - total: perturbations.length, - actives: perturbations.filter(p => p.actif).length, - retards: perturbations.filter(p => p.type_perturbation === 'retard').length, - suppressions: perturbations.filter(p => p.type_perturbation === 'suppression').length - }; - - // Extraire les gares uniques - const garesSet = new Set(); - horaires.forEach(h => { - if (h.gare_depart_nom) garesSet.add(h.gare_depart_nom); - if (h.gare_arrivee_nom) garesSet.add(h.gare_arrivee_nom); - if (h.gares_desservies) { - h.gares_desservies.forEach(g => { - if (g.gare_nom) garesSet.add(g.gare_nom); - }); - } - }); - - // Extraire les lignes uniques - const lignesSet = new Set(); - horaires.forEach(h => { - if (h.ligne_nom) lignesSet.add(h.ligne_nom); - }); - - // Extraire les types de trains uniques - const typesTrainsSet = new Set(); - horaires.forEach(h => { - if (h.type_train) typesTrainsSet.add(h.type_train); - }); - - // Mettre à jour les stats - setStats({ - horaires: horaireStats, - perturbations: perturbationStats, - gares: { total: garesSet.size }, - lignes: { total: lignesSet.size }, - typesTrains: { total: typesTrainsSet.size } - }); - - // Perturbations récentes (5 dernières) - setRecentPerturbations(perturbations.slice(0, 6)); - - // Horaires récents (6 derniers) - setRecentHoraires(horaires.slice(0, 6)); + setHoraires(horairesRes?.data || []); + setPerturbations(perturbationsRes?.data || []); + setTravaux(travauxRes?.data || []); + setMateriels(materielsRes?.data || []); setLastRefreshed(new Date()); - + setRefreshCountdown(30); } catch (error) { - console.error('Erreur lors du chargement du dashboard:', error); + console.error('[Dashboard] Erreur de chargement des données ferroviaires:', error); } finally { setLoading(false); } }, []); - // Charger les données au montage useEffect(() => { loadDashboardData(); }, [loadDashboardData]); - // Fonction pour formater les heures - const formatHeure = (heure) => { - if (!heure) return '—'; - return heure.substring(0, 5); - }; + // Synchronisation WCS Tabs + useEffect(() => { + const tabsEl = tabsRef.current; + if (tabsEl) { + const handleTabChange = (e) => { + const key = e.detail?.selectedKey || e.detail?.tabName; + if (key) setActiveTab(key); + }; + tabsEl.addEventListener('tabChange', handleTabChange); + return () => tabsEl.removeEventListener('tabChange', handleTabChange); + } + }, []); + // Synchronisation WCS Modal + useEffect(() => { + const modalEl = modalRef.current; + if (modalEl) { + const handleModalClose = () => setSelectedItem(null); + modalEl.addEventListener('wcsDialogClosed', handleModalClose); + return () => modalEl.removeEventListener('wcsDialogClosed', handleModalClose); + } + }, []); + + // Synchronisation WCS Switch Auto-Refresh + useEffect(() => { + const switchEl = switchRef.current; + if (switchEl) { + const handleSwitch = (e) => { + setAutoRefresh(!!e.detail?.checked); + }; + switchEl.addEventListener('wcsChange', handleSwitch); + return () => switchEl.removeEventListener('wcsChange', handleSwitch); + } + }, []); + + // Synchronisation WCS Radio-Group (Punctuality mode) + useEffect(() => { + const rgEl = radioGroupRef.current; + if (rgEl) { + const handleRadioChange = (e) => { + if (e.detail?.value) { + setPunctualityFilterMode(e.detail.value); + } + }; + rgEl.addEventListener('wcsChange', handleRadioChange); + return () => rgEl.removeEventListener('wcsChange', handleRadioChange); + } + }, [activeTab]); + + // Synchronisation WCS Checkboxes + useEffect(() => { + const stopsEl = checkStopsRef.current; + const trackEl = checkTrackRef.current; + + const handleStopsChange = (e) => setOnlyWithStops(!!e.detail?.checked); + const handleTrackChange = (e) => setOnlyWithTrack(!!e.detail?.checked); + + if (stopsEl) stopsEl.addEventListener('wcsChange', handleStopsChange); + if (trackEl) trackEl.addEventListener('wcsChange', handleTrackChange); + + return () => { + if (stopsEl) stopsEl.removeEventListener('wcsChange', handleStopsChange); + if (trackEl) trackEl.removeEventListener('wcsChange', handleTrackChange); + }; + }, [activeTab]); + + // Synchronisation WCS Counter (Nombre de trains affichés) + useEffect(() => { + const counterEl = counterRef.current; + if (counterEl) { + const handleCounterChange = (e) => { + if (typeof e.detail?.value === 'number') { + setImminentLimit(e.detail.value); + } + }; + counterEl.addEventListener('wcsChange', handleCounterChange); + return () => counterEl.removeEventListener('wcsChange', handleCounterChange); + } + }, [activeTab]); + + + // Timer de rafraîchissement automatique + useEffect(() => { + if (!autoRefresh) return undefined; + + const interval = setInterval(() => { + setRefreshCountdown((prev) => { + if (prev <= 1) { + loadDashboardData(); + return 30; + } + return prev - 1; + }); + }, 1000); + + return () => clearInterval(interval); + }, [autoRefresh, loadDashboardData]); + + // --------------------------------------------------------------------------- + // CALCULS STATISTIQUES & INDICATEURS MÉTIERS SNCF + // --------------------------------------------------------------------------- + const totalHoraires = horaires.length; + const activePerturbations = perturbations.filter(p => p.actif); + const countRetards = activePerturbations.filter(p => p.type_perturbation === 'retard').length; + const countSuppressions = activePerturbations.filter(p => p.type_perturbation === 'suppression').length; + const countModifs = activePerturbations.filter(p => p.type_perturbation === 'modification_parcours').length; + + // Calcul du taux de ponctualité contractuel SNCF + const punctualityRate = totalHoraires > 0 + ? Math.max(0, Math.min(100, Math.round(((totalHoraires - activePerturbations.length) / totalHoraires) * 100))) + : 100; + + // Extraction des gares, lignes et types uniques + const uniqueGares = new Set(); + const uniqueLignes = new Set(); + const trainTypes = new Set(); + + horaires.forEach(h => { + if (h.gare_depart_nom) uniqueGares.add(h.gare_depart_nom); + if (h.gare_arrivee_nom) uniqueGares.add(h.gare_arrivee_nom); + if (h.ligne_nom) uniqueLignes.add(h.ligne_nom); + if (h.type_train) trainTypes.add(h.type_train); + if (Array.isArray(h.gares_desservies)) { + h.gares_desservies.forEach(g => { + if (g.gare_nom) uniqueGares.add(g.gare_nom); + }); + } + }); + + const linesList = Array.from(uniqueLignes).sort(); + const typesList = Array.from(trainTypes).sort(); + + // Navigation applicative const navigate = (path) => { window.location.href = path; }; - // Ponctualité théorique estimée - const calculatePunctuality = () => { - if (!stats.horaires.total) return 100; - const impacted = stats.perturbations.actives; - const rate = Math.max(0, Math.min(100, Math.round(((stats.horaires.total - impacted) / stats.horaires.total) * 100))); - return rate; + const formatHeure = (h) => { + if (!h) return '—'; + return String(h).substring(0, 5); }; + // Circulations filtrées + const filteredHoraires = horaires.filter(h => { + const pert = activePerturbations.find(p => p.horaire_id === h.id || p.numero_train === h.numero_train); + const hasIssue = !!pert; + + // Filtre ponctualité (radio group) + if (punctualityFilterMode === 'delayed' && !hasIssue) return false; + if (punctualityFilterMode === 'ontime' && hasIssue) return false; + + // Filtre checkboxes + if (onlyWithStops && (!Array.isArray(h.gares_desservies) || h.gares_desservies.length === 0)) return false; + if (onlyWithTrack && !h.quai_depart && (!Array.isArray(h.attributions_quais) || h.attributions_quais.length === 0)) return false; + + // Filtre texte + if (searchTerm.trim()) { + const q = searchTerm.toLowerCase(); + const matchNum = String(h.numero_train || '').toLowerCase().includes(q); + const matchDepart = String(h.gare_depart_nom || '').toLowerCase().includes(q); + const matchArrivee = String(h.gare_arrivee_nom || '').toLowerCase().includes(q); + const matchLigne = String(h.ligne_nom || '').toLowerCase().includes(q); + if (!matchNum && !matchDepart && !matchArrivee && !matchLigne) return false; + } + // Filtre type de train + if (selectedTypeFilter !== 'all' && h.type_train !== selectedTypeFilter) { + return false; + } + // Filtre ligne + if (selectedLineFilter && h.ligne_nom !== selectedLineFilter) { + return false; + } + return true; + }); + + // Perturbations filtrées + const filteredPerturbations = perturbations.filter(p => { + if (selectedPerturbationType === 'retard' && p.type_perturbation !== 'retard') return false; + if (selectedPerturbationType === 'suppression' && p.type_perturbation !== 'suppression') return false; + if (selectedPerturbationType === 'active' && !p.actif) return false; + return true; + }); + + // Départs imminents (triés par heure de départ) avec limite ajustable via wcs-counter + const imminentDepartures = [...horaires] + .sort((a, b) => String(a.heure_depart || '').localeCompare(String(b.heure_depart || ''))) + .slice(0, imminentLimit); + return ( -
-
- - {/* ================================================================ - HERO BANNER - ================================================================ */} -
-
-
- - - Centre de supervision ferroviaire - - {scopedRegionId && ( - - - Région : {scopedRegionId} +
+ + {/* ==================================================================== + 0. BANDEAU GALACTIQUE OFFICIEL SNCF (WCS-GALACTIC & WCS-GALACTIC-MENU) + ==================================================================== */} + + + + Portail Groupe SNCF + + + SNCF Réseau Infra + + + SNCF Voyageurs Mobilités + + + + +
+ + {/* ==================================================================== + 1. FIL D'ARIANE WCS (WCS-BREADCRUMB) + ==================================================================== */} +
+ + Console Ferrovia + Supervision & Exploitation + + {scopedRegionId ? `Secteur Régional ${scopedRegionId}` : 'Réseau National Unifié'} + + +
+ + {/* ==================================================================== + 2. CONTROLBAR SNCF MÉTIERS & HEADER SUPÉRIEUR + ==================================================================== */} +
+
+
+
+ + + Supervision Réseau & Exploitation - )} -
- -

- Tableau de bord - {currentUser?.nom ? , {currentUser.prenom || currentUser.nom} : null} -

- -

- Pilotage centralisé des circulations, gestion du plan de transport, détection des aléas et état des infrastructures en temps réel. -

-
- -
-
- - Dernière synchro : {lastRefreshed.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })} -
- -
- - - Actualiser - - - navigate('/perturbations/creer')} - > - - Signaler un incident - -
-
-
- - {/* ================================================================ - SUPERVISION STATUS BAR - ================================================================ */} -
-
-
0 ? 'is-warning' : 'is-success'}`}> - 0 ? 'warning' : 'check_circle'}> -
-
- État du Réseau - {stats.perturbations.actives > 0 ? `${stats.perturbations.actives} incident(s) en cours` : 'Circulation normale et fluide'} -
-
- -
- -
-
- -
-
- Taux de régularité théorique - {calculatePunctuality()}% à l'heure -
-
- -
- -
-
- -
-
- Sillons actifs - {stats.horaires.total} circulations programmées -
-
-
- - {/* ================================================================ - KPI CARDS - ================================================================ */} -
- {/* Sillons / Horaires */} -
navigate('/horaires')}> -
-
- -
- {stats.horaires.actifs} actifs -
-
{loading ? '...' : stats.horaires.total}
-
Horaires & Sillons
-
- Gérer les plans de transport - -
-
- - {/* Perturbations */} -
navigate('/perturbations')}> -
-
- -
- 0 ? 'badge-danger' : 'badge-neutral'}`}> - {stats.perturbations.actives} actives - -
-
{loading ? '...' : stats.perturbations.total}
-
Perturbations & Aléas
-
- {stats.perturbations.retards} retards · {stats.perturbations.suppressions} suppressions - -
-
- - {/* Gares */} -
navigate('/gares')}> -
-
- -
- Réseau -
-
{loading ? '...' : stats.gares.total}
-
Gares & Points d'arrêt
-
- Configuration des quais & services - -
-
- - {/* Lignes */} -
navigate('/horaires')}> -
-
- -
- Maillage -
-
{loading ? '...' : stats.lignes.total}
-
Lignes Commerciales
-
- {stats.typesTrains.total} matériels types - -
-
-
- - {/* ================================================================ - MAIN PANELS GRID - ================================================================ */} -
- - {/* Colonne gauche : Perturbations et Actions */} -
- - {/* Widget Perturbations récentes */} -
-
-
-
- -
-
-

Incidents & Perturbations

- Derniers aléas déclarés sur le réseau -
-
- navigate('/perturbations')} - > - Tout voir ({stats.perturbations.total}) - -
- -
- {loading ? ( -
- - Chargement des incidents... -
- ) : recentPerturbations.length === 0 ? ( -
-
- -
-

Aucune perturbation active

-

Toutes les circulations ferroviaires s'effectuent conformément au plan de transport.

-
+ {scopedRegionId ? ( + + + Secteur Régional : {scopedRegionId} + ) : ( -
- {recentPerturbations.map(p => ( -
navigate(`/perturbations/modifier/${p.id}`)} - > -
-
- - {p.horaire?.numero_train || 'Train'} -
+ + + Réseau National & Multi-Régions + + )} + 0 ? 'is-disrupted' : 'is-nominal'}`}> + 0 ? 'warning' : 'check_circle'}> + {activePerturbations.length > 0 + ? `${activePerturbations.length} Aléa(s) d'exploitation en cours` + : 'Exploitation Nominale'} + +
- - - {p.type_perturbation === 'retard' ? `+${p.duree_retard_minutes || 0} min` : - p.type_perturbation === 'suppression' ? 'Supprimé' : - 'Modifié'} - -
+

+ Console de Pilotage Ferroviaire + {currentUser?.prenom || currentUser?.nom ? ( + + , {currentUser.prenom || currentUser.nom} + + ) : null} +

-
- {p.horaire?.gare_depart_nom || 'Départ'} - - {p.horaire?.gare_arrivee_nom || 'Arrivée'} -
+

+ Supervision en temps réel des circulations, régulation des voies en gare, suivi de la ponctualité contractuelle et information voyageurs. +

+
-
- - - {p.cause || 'Motif d\'exploitation non spécifié'} - - - - {p.actif ? 'En cours' : 'Résolu'} - -
-
- ))} -
+
+
+ + + Synchro : {lastRefreshed.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })} + + {autoRefresh && ( + ({refreshCountdown}s) )}
-
- {/* Widget Actions Rapides */} - - - {/* Colonne droite : Horaires & Circulation */} -
- -
-
-
-
- -
-
-

Plan de transport récent

- Circulations programmées au référentiel -
-
+
navigate('/horaires')} + id="btn-refresh" + className="wcs-primary" + onClick={loadDashboardData} + disabled={loading} + loading={loading} > - Tous les horaires ({stats.horaires.total}) + + Actualiser + + + Interroger immédiatement la base de données Supabase + + + navigate('/perturbations/creer')} + > + + Signaler un aléa + + + Ouvrir le formulaire de déclaration d'incident et régulation + + + {/* Actions Métiers avec wcs-dropdown-header */} + +
+
+ + Actions Métiers +
+
+ Circulations & Voies + navigate('/horaires')}> + Nouvelle circulation + + navigate('/horaires/attribution-quais')}> + Régulation voies & quais + + navigate('/attribution-materiel-roulant')}> + Affectation rames + + + + Aléas & Infrastructure + navigate('/perturbations/creer')}> + Déclarer un incident + + navigate('/travaux/creer')}> + Déclarer des travaux + + + + Information Voyageurs + navigate('/fiches-horaires')}> + Fiches horaires & affiches + + navigate('/annonces-sonores')}> + Annonces sonores en gare + +
+
+
+
+ + + {/* ==================================================================== + 3. ALERTE DE SUPERVISION OPÉRATIONNELLE (WCS ALERT) + ==================================================================== */} +
+ {activePerturbations.length > 0 ? ( + +
+ Alerte Réseau : {activePerturbations.length} perturbation(s) active(s) nécessitant une régulation +
+
+ + {countRetards} retard(s) cumulé(s), {countSuppressions} circulation(s) supprimée(s){countModifs > 0 ? ` et ${countModifs} modification(s) de tracé` : ''}. Les plans de transport de substitution et les affichages voyageurs sont actualisés. + + setActiveTab('incidents')} + > + + Examiner les perturbations
+
+ ) : ( + +
+ Situation Nominale : Aucune perturbation active signalée +
+
+ + L'ensemble des circulations ferroviaires s'effectue dans le respect des plages horaires et de la régularité cible. + + + 100% de conformité observée + +
+
+ )} +
-
- {loading ? ( -
- - Chargement des horaires... + {/* ==================================================================== + 4. CARTES KPI EXÉCUTIVES (WCS CARDS + SKELETONS LORS DU CHARGEMENT) + ==================================================================== */} + {loading ? ( +
+ {[1, 2, 3, 4, 5].map(i => ( + + +
+ +
- ) : recentHoraires.length === 0 ? ( -
-
- + + + + + + ))} +
+ ) : ( +
+ + {/* KPI 1 : Régularité / Ponctualité */} + setActiveTab('overview')} + id="kpi-punctuality-card" + > + +
+
+ +
+ = 95 ? 'wcs-success' : 'wcs-warning'} + shape="rounded" + > + {punctualityRate >= 95 ? 'Conforme SLA' : 'Sous surveillance'} + +
+ +
+
+
+
{punctualityRate}%
+
Taux de Régularité
-

Aucun horaire enregistré

-

Commencez par créer des circulations ou importez une fiche horaire.

+ +
+

+ Objectif cible contractuel SNCF : 95.0% +

+
+ + + +
+ Détail ponctualité + +
+
+
+ + Taux calculé d'après les circulations théoriques moins les incidents actifs + + + {/* KPI 2 : Circulations programmées */} + setActiveTab('schedules')} + id="kpi-trains-card" + > + +
+
+ +
+ + {totalHoraires} sillons + +
+ +
+
{totalHoraires}
+
Circulations Actives
+

+ {totalHoraires - activePerturbations.length} trains conformes à l'horaire +

+
+ +
+
+ + + +
+ Plan de transport + +
+
+
+ + {/* KPI 3 : Perturbations & Aléas */} + setActiveTab('incidents')} + id="kpi-alerts-card" + > + +
+
+ +
+ 0 ? 'wcs-danger' : 'wcs-success'} + shape="rounded" + > + {activePerturbations.length} active(s) + +
+ +
+
{perturbations.length}
+
Aléas Enregistrés
+

+ {countRetards} retard(s) · {countSuppressions} suppression(s) +

+
+ + +
+
+ + + +
+ Gestion des incidents + +
+
+
+ + {/* KPI 4 : Travaux & Chantiers */} + setActiveTab('works')} + id="kpi-works-card" + > + +
+
+ +
+ + Infra & Voies + +
+ +
+
{travaux.length}
+
Chantiers & Travaux
+

+ Impact voies et bus de substitution +

+
+ + {travaux.reduce((acc, t) => acc + (t.substitutions?.length || 0), 0)} substitution(s) + +
+
+ + + +
+ Suivi chantiers + +
+
+
+ + {/* KPI 5 : Réseau & Gares */} + setActiveTab('fleet')} + id="kpi-network-card" + > + +
+
+ +
+ + Maillage + +
+ +
+
{uniqueGares.size}
+
Gares & Points d'Arrêt
+

+ {uniqueLignes.size} lignes · {materiels.length} rames affectées +

+
+ + {uniqueLignes.size} lignes commerciales + +
+
+ + + +
+ Réseau & Matériels + +
+
+
+ +
+ )} + + {/* ==================================================================== + 5. NAVIGATION PAR ONGLETS AVEC WCS ACTION-BAR & WCS TABS + ==================================================================== */} +
+ + +
+ + + Poste de Commande & Exploitation + +
+ +
+ + {scopedRegionId ? `Secteur ${scopedRegionId}` : 'National'} + + 0 ? 'wcs-warning' : 'wcs-success'} shape="rounded"> + {punctualityRate}% ponctualité + +
+
+ +
+ + setActiveTab('overview')} + > + setActiveTab('schedules')} + > + setActiveTab('incidents')} + > + setActiveTab('works')} + > + setActiveTab('fleet')} + > + +
+ + {/* Barre de filtrage avancée WCS pour les onglets de circulations avec wcs-form-field */} + {(activeTab === 'schedules' || activeTab === 'overview') && ( +
+
+
+ + Recherche globale + setSearchTerm(e.target.value)} + > + Filtrage temps réel du plan de transport + +
+ +
+ + Ligne commerciale + + + + {linesList.length} ligne(s) répertoriée(s) + +
+
+ + {/* Segmented Radio Group WCS pour filtrer l'état de ponctualité */} +
+
+ + État de circulation + + + + + + +
+
+ +
+
+ setSelectedTypeFilter('all')} + > + {typesList.map(t => ( + setSelectedTypeFilter(t)} + > + ))} +
+ +
+ + +
+
+
+ )} + + {/* ================================================================== + 6. CONTENU DES ONGLETS + ================================================================== */} +
+ + {/* ---------------------------------------------------------------- + ONGLET 1 : VUE D'ENSEMBLE + ---------------------------------------------------------------- */} + {activeTab === 'overview' && ( +
+ + {/* Colonne Gauche : Départs & Incidents */} +
+ + {/* Widget Départs Imminents avec WCS-COUNTER */} + + + +
+

+ + Départs Imminents & Plan de Transport +

+
+ setActiveTab('schedules')} + > + Voir tout ({totalHoraires}) + +
+
+
+ + {/* Contrôle du volume avec WCS Counter */} +
+
+ + + Volume de circulations affichées : + +
+
+ + + {imminentDepartures.length} / {totalHoraires} + +
+
+ + + {loading ? ( +
+ {[1, 2, 3].map(i => ( +
+ +
+ ))} +
+ ) : imminentDepartures.length === 0 ? ( +
+ +

Aucune circulation enregistrée

+

Ajoutez des horaires ou importez un plan de transport régional.

+ navigate('/horaires')}> + Ajouter un horaire + +
+ ) : ( +
+ {imminentDepartures.map(h => { + const pert = activePerturbations.find(p => p.horaire_id === h.id || p.numero_train === h.numero_train); + const isDelayed = pert?.type_perturbation === 'retard'; + const isCancelled = pert?.type_perturbation === 'suppression'; + + return ( +
setSelectedItem({ type: 'horaire', data: h })} + > +
+
+ + + {h.numero_train} + + {h.type_train && ( + + {h.type_train} + + )} + {h.ligne_nom && ( + + · {h.ligne_nom} + + )} +
+ +
+ {isCancelled ? ( + + + Supprimé + + ) : isDelayed ? ( + + + +{pert.duree_retard_minutes || 0} min + + ) : ( + + À l'heure + + )} +
+
+ +
+ {formatHeure(h.heure_depart)} + {h.gare_depart_nom || 'Départ'} + + {formatHeure(h.heure_arrivee)} + {h.gare_arrivee_nom || 'Arrivée'} +
+ +
+
+ {h.quai_depart && ( + Voie {h.quai_depart} + )} + {Array.isArray(h.gares_desservies) && h.gares_desservies.length > 0 && ( + {h.gares_desservies.length} arrêt(s) intermédiaire(s) + )} +
+
+ Détails & Régulation + +
+
+
+ ); + })} +
+ )} +
+
+
+ + {/* Widget Accordion Perturbations Actives */} + + + +
+

+ + Aléas & Incidents en Cours ({activePerturbations.length}) +

+ setActiveTab('incidents')} + > + Tous ({perturbations.length}) + +
+
+ + + {activePerturbations.length === 0 ? ( +
+ +

Aucune perturbation active

+

Le trafic est entièrement régulier sans retard majeur signalé sur le réseau.

+
+ ) : ( + + {activePerturbations.slice(0, 5).map(p => ( + + +
+
+ + Train {p.horaire?.numero_train || p.numero_train || 'N/A'} + ({p.horaire?.gare_depart_nom} → {p.horaire?.gare_arrivee_nom}) +
+ + {p.type_perturbation === 'retard' ? `Retard +${p.duree_retard_minutes || 0} min` : + p.type_perturbation === 'suppression' ? 'Supprimé' : 'Modifié'} + +
+
+ + +
+
+ Motif d'exploitation : {p.cause || 'Non spécifié'} +
+ {p.cause_details && ( +

{p.cause_details}

+ )} +
+ + Signalé le {p.date_debut ? new Date(p.date_debut).toLocaleTimeString() : '—'} + +
+ setSelectedItem({ type: 'perturbation', data: p })} + > + Détails complets + + navigate(`/perturbations/modifier/${p.id}`)} + > + Réguler + +
+
+
+
+
+ ))} +
+ )} +
+
+
+ +
+ + {/* Colonne Droite : Raccourcis Opérationnels & Synthèse Métiers */} +
+ + {/* Centre d'Actions d'Exploitation */} + + + +
+

+ + Centre d'Actions d'Exploitation +

+
+
+ + + + +
+
+ + {/* Synthèse du Réseau & Indicateurs Métiers */} + + + +
+

+ + Indicateurs Régionaux SNCF +

+
+
+ + +
+
+
+ Conformité Plan de Transport + {punctualityRate}% +
+ +
+ + + +
+
+
Matériels Roulants Actifs
+ {materiels.length} Rames +
+ Flotte OK +
+ + + +
+
+
Chantiers programmés
+ {travaux.length} Sites +
+ Voies +
+ + + +
+
+
Points d'arrêt surveillés
+ {uniqueGares.size} Gares +
+ Opérationnel +
+
+
+
+
+ +
+ +
+ )} + + {/* ---------------------------------------------------------------- + ONGLET 2 : CIRCULATIONS & HORAIRES (TABLE BOOTSTRAP SNCF MÉTIERS) + ---------------------------------------------------------------- */} + {activeTab === 'schedules' && ( +
+
+
+

+ + Plan de Transport & Circulations +

+ + {filteredHoraires.length} circulation(s) affichée(s) sur {totalHoraires} au référentiel + +
+ + navigate('/horaires')}> + + Nouvel Horaire + +
+ + {filteredHoraires.length === 0 ? ( +
+ +

Aucune circulation trouvée

+

Modifiez vos critères de recherche ou réinitialisez les filtres.

navigate('/horaires')} + className="wcs-secondary" + onClick={() => { + setSearchTerm(''); + setSelectedTypeFilter('all'); + setSelectedLineFilter(''); + setPunctualityFilterMode('all'); + setOnlyWithStops(false); + setOnlyWithTrack(false); + }} > - Ajouter un horaire + Réinitialiser tous les filtres
) : ( -
- {recentHoraires.map(h => ( -
navigate('/horaires')} - > -
-
- - - {h.numero_train} - - {h.type_train && ( - {h.type_train} - )} -
- {h.ligne_nom && ( - - - {h.ligne_nom} - - )} -
+
+ + + + + + + + + + + + + + + + {filteredHoraires.map(h => { + const pert = activePerturbations.find(p => p.horaire_id === h.id || p.numero_train === h.numero_train); + const isDelayed = pert?.type_perturbation === 'retard'; + const isCancelled = pert?.type_perturbation === 'suppression'; -
-
- {formatHeure(h.heure_depart)} - - {h.gare_depart_nom || 'Départ'} -
+ return ( +
setSelectedItem({ type: 'horaire', data: h })} + > + + + + + + + + + + + ); + })} + +
N° TrainTypeLigne CommercialeDépartArrivéeArrêtsVoieStatut RégulationActions
+
+ + {h.numero_train} +
+
+ + {h.type_train || 'TER'} + + {h.ligne_nom || '—'} + {formatHeure(h.heure_depart)} + {h.gare_depart_nom} + + {formatHeure(h.heure_arrivee)} + {h.gare_arrivee_nom} + + + {Array.isArray(h.gares_desservies) ? h.gares_desservies.length : 0} arrêt(s) + + + {h.quai_depart ? ( + Voie {h.quai_depart} + ) : ( + — + )} + + {isCancelled ? ( + + + Supprimé + + ) : isDelayed ? ( + + + +{pert.duree_retard_minutes} min + + ) : ( + + + À l'heure + + )} + +
+ { + e.stopPropagation(); + setSelectedItem({ type: 'horaire', data: h }); + }} + > + Détails + -
- - -
+ +
+ e.stopPropagation()}> + + +
+ Train {h.numero_train} + { e.stopPropagation(); navigate('/horaires/attribution-quais'); }}> + Attribuer voie + + { e.stopPropagation(); navigate('/perturbations/creer'); }}> + Déclarer retard + + { e.stopPropagation(); navigate('/attribution-materiel-roulant'); }}> + Affecter rame + +
+
+
+
+ )} +
+ )} -
- {formatHeure(h.heure_arrivee)} - - {h.gare_arrivee_nom || 'Arrivée'} -
-
+ {/* ---------------------------------------------------------------- + ONGLET 3 : INCIDENTS & ALÉAS (SUPERVISION DISRUPTIONS) + ---------------------------------------------------------------- */} + {activeTab === 'incidents' && ( +
+
+
+

+ + Gestion des Perturbations & Aléas d'Exploitation +

+ + {filteredPerturbations.length} perturbation(s) enregistrée(s) + +
- {h.gares_desservies && h.gares_desservies.length > 0 && ( -
- - {h.gares_desservies.length} arrêt(s) intermédiaire(s) -
- )} +
+ navigate('/perturbations/creer')} + > + + Déclarer un retard + + navigate('/perturbations')} + > + Module complet + +
+
+ +
+
+ Filtres : + setSelectedPerturbationType('all')} + > + setSelectedPerturbationType('active')} + > + setSelectedPerturbationType('retard')} + > + setSelectedPerturbationType('suppression')} + > +
+
+ + {filteredPerturbations.length === 0 ? ( +
+ +

Aucune perturbation dans cette vue

+

Toutes les circulations respectent les plages horaires contractuelles.

+
+ ) : ( +
+ + + + + + + + + + + + + + {filteredPerturbations.map(p => ( + setSelectedItem({ type: 'perturbation', data: p })} + > + + + + + + + + + ))} + +
TrainType d'aléaTrajet AffectéImpact / RetardCause DéclaréeÉtatActions
+
+ + Train {p.horaire?.numero_train || p.numero_train || 'N/A'} +
+
+ + {p.type_perturbation === 'retard' ? 'Retard' : + p.type_perturbation === 'suppression' ? 'Suppression' : 'Modification'} + + + {p.horaire?.gare_depart_nom || 'Départ'} → {p.horaire?.gare_arrivee_nom || 'Arrivée'} + + {p.type_perturbation === 'retard' ? ( + +{p.duree_retard_minutes || 0} min + ) : p.type_perturbation === 'suppression' ? ( + Circulation Annulée + ) : ( + Itinéraire Adapté + )} + + {p.cause || 'Motif non spécifié'} + + + {p.actif ? 'En cours' : 'Résolu'} + + + { + e.stopPropagation(); + navigate(`/perturbations/modifier/${p.id}`); + }} + > + Réguler + +
+
+ )} +
+ )} + + {/* ---------------------------------------------------------------- + ONGLET 4 : TRAVAUX & CHANTIERS D'INFRASTRUCTURE + ---------------------------------------------------------------- */} + {activeTab === 'works' && ( +
+
+
+

+ + Chantiers d'Infrastructure & Substitutions Routières +

+ + {travaux.length} chantier(s) répertorié(s) sur le réseau + +
+ + navigate('/travaux/creer')}> + + Nouveau Chantier + +
+ + {travaux.length === 0 ? ( +
+ +

Aucun chantier programmé

+

Aucune interruption de voie ni bus de substitution n'est actuellement déclaré.

+
+ ) : ( +
+ {travaux.map(t => ( +
+ + + +
+
+

+ {t.titre || 'Chantier SNCF Réseau'} +

+ + Du {t.date_debut ? new Date(t.date_debut).toLocaleDateString() : '—'} au {t.date_fin ? new Date(t.date_fin).toLocaleDateString() : '—'} + +
+ + {t.substitutions?.length || 0} autocar(s) + +
+
+ + +

+ {t.description || 'Travaux de modernisation et renouvellement d\'infrastructure des voies ferrées.'} +

+ + + +
+ + Section : {t.ligne_nom || t.gare_depart_nom || 'Secteur régional'} + + navigate('/travaux')} + > + Accéder au dossier + +
+
+
+
))}
)}
-
+ )} - {/* Carte état du système / infos d'exploitation */} -
-
-
- + {/* ---------------------------------------------------------------- + ONGLET 5 : FLOTTE & GARES (WCS LIST ITEMS) + ---------------------------------------------------------------- */} + {activeTab === 'fleet' && ( +
+
+
+

+ + Parc de Matériel Roulant & Maillage Gares +

+ + {materiels.length} rame(s) · {uniqueGares.size} gare(s) connectée(s) + +
+ +
+ navigate('/attribution-materiel-roulant')} + > + Affectation Rames + + navigate('/gares')} + > + Gestion Gares + +
-
-

État des services d'exploitation

-

Système opérationnel & données à jour

+ +
+ {/* Flotte Rames (avec wcs-list-item) */} +
+ + + +
+

+ Flotte de Matériel Roulant +

+ + {materiels.length} unités actives + +
+
+ + + {materiels.length === 0 ? ( +
+ +

Aucun matériel répertorié

+

Configurez le parc de rames TER et TGV dans le module dédié.

+
+ ) : ( +
+ {materiels.slice(0, 6).map(m => ( + + + + {m.nom || m.serie || 'Rame SNCF'} + {m.type && {m.type}} + + +
+ Vitesse limite : {m.vitesse_max || '160'} km/h · Motorisation : {m.alimentation || 'Électrique / Bi-mode'} +
+ + + Capacité : {m.capacite || m.places_assises || '280'} places + Série : {m.serie || 'SNCF Z-51500'} + Disponibilité : 100% + + +
+ navigate('/attribution-materiel-roulant')} + > + Affecter + +
+
+ ))} +
+ )} +
+
+
+
+ + {/* Hubs & Gares (avec wcs-list-item) */} +
+ + + +
+

+ Principaux Hubs Ferroviaires +

+ + {uniqueGares.size} gares + +
+
+ + +
+ {Array.from(uniqueGares).slice(0, 7).map(g => ( + + + + {g} + + +
+ Pôle d'échange multimodal & correspondances régionales +
+ + + Voies équipées + Accès PMR + + +
+ navigate('/horaires/attribution-quais')} + > + Quais & Voies + +
+
+ ))} +
+
+
+
+
-
-
- Circulations - {stats.horaires.total} -
-
- Gares reliées - {stats.gares.total} -
-
- Incidents - 0 ? 'text-warning' : 'text-success'}> - {stats.perturbations.actives} - -
-
-
+ )}
-
+ {/* ==================================================================== + 7. MODAL INTERACTIF DÉTAIL TRAIN / PERTURBATION AVEC WCS STEPPER + ==================================================================== */} + +
+
+ +

+ {selectedItem?.type === 'perturbation' + ? `Détail de l'Aléa — Train ${selectedItem?.data?.horaire?.numero_train || selectedItem?.data?.numero_train || ''}` + : `Fiche d'Exploitation — Train ${selectedItem?.data?.numero_train || ''}`} +

+
+
+ + {selectedItem && ( +
+ + {/* Jalonnement Stepper SNCF pour un horaire */} + {selectedItem.type === 'horaire' && (() => { + const pert = perturbations.find(p => p.actif && (p.horaire_id === selectedItem.data.id || p.numero_train === selectedItem.data.numero_train)); + const isCancelled = pert?.type_perturbation === 'suppression'; + const isDelayed = pert?.type_perturbation === 'retard'; + return ( +
+
+ + + Jalonnement Opérationnel du Train + + + {isCancelled ? 'Circulation Annulée' : isDelayed ? `Retard (+${pert.duree_retard_minutes || 0}m)` : 'Phase active : En circulation'} + +
+
+
+ + Préparation +
+ +
+ + Mise à Quai +
+ +
+ + + + + {isCancelled ? 'Annulé' : isDelayed ? 'Retardé' : 'En route'} + +
+ +
+ + Terminus +
+
+
+ ); + })()} + + {/* Contenu Fiche Circulation */} + {selectedItem.type === 'horaire' && ( +
+
+
+
Identité du Sillon Ferroviaire
+
+ + {selectedItem.data.type_train || 'TER'} + + N° {selectedItem.data.numero_train} +
+
+ Ligne : {selectedItem.data.ligne_nom || 'Non spécifiée'} +
+
+ Voie au départ : {selectedItem.data.quai_depart ? `Voie ${selectedItem.data.quai_depart}` : 'Non attribuée'} +
+
+ +
+
Horaires Contractuels
+
+ Départ de {selectedItem.data.gare_depart_nom} : {formatHeure(selectedItem.data.heure_depart)} +
+
+ Arrivée à {selectedItem.data.gare_arrivee_nom} : {formatHeure(selectedItem.data.heure_arrivee)} +
+
+ Jours de circulation : {selectedItem.data.jours_circulation || 'Tous les jours'} +
+
+
+ + {/* Arrêts intermédiaires */} + {Array.isArray(selectedItem.data.gares_desservies) && selectedItem.data.gares_desservies.length > 0 && ( +
+
+ Dessertes & Arrêts Intermédiaires ({selectedItem.data.gares_desservies.length}) +
+
+
+ {selectedItem.data.gare_depart_nom} + {formatHeure(selectedItem.data.heure_depart)} +
+ + {selectedItem.data.gares_desservies.map((g, idx) => ( +
+ {g.gare_nom} + + {formatHeure(g.heure_passage || g.heure_depart || g.heure_arrivee)} + +
+ ))} + +
+ {selectedItem.data.gare_arrivee_nom} + {formatHeure(selectedItem.data.heure_arrivee)} +
+
+
+ )} +
+ )} + + {/* Contenu Perturbation */} + {selectedItem.type === 'perturbation' && ( +
+
+
+
Nature de l'Incident
+ + {selectedItem.data.type_perturbation === 'retard' ? `Retard +${selectedItem.data.duree_retard_minutes || 0} min` : + selectedItem.data.type_perturbation === 'suppression' ? 'Circulation Supprimée' : 'Modification d\'itinéraire'} + +
+ Statut : {selectedItem.data.actif ? 'Incident Actif en cours' : 'Incident Résolu'} +
+
+ +
+
Circulation Affectée
+
+ Train N° {selectedItem.data.horaire?.numero_train || selectedItem.data.numero_train} +
+
+ Trajet : {selectedItem.data.horaire?.gare_depart_nom} → {selectedItem.data.horaire?.gare_arrivee_nom} +
+
+
+ +
+
Cause & Diagnostic d'Exploitation
+

+ {selectedItem.data.cause || 'Aucun motif d\'exploitation renseigné dans le système.'} +

+
+
+ )} + +
+ )} + +
+ setSelectedItem(null)} + > + Fermer + + + {selectedItem?.type === 'perturbation' ? ( + navigate(`/perturbations/modifier/${selectedItem.data.id}`)} + > + Modifier la perturbation + + ) : ( + navigate('/horaires')} + > + Gérer les horaires + + )} +
+
+ + {/* ==================================================================== + 8. PIED DE PAGE CORPORATE SNCF (WCS-FOOTER) + ==================================================================== */} + +
+
+ +
+
+ + FERROVIA PANEL · DIRECTION DE L'EXPLOITATION FERROVIAIRE + + + Système certifié WCS SNCF (Web Component System) & Bootstrap SNCF Métiers + +
+
+ +
+ + © {new Date().getFullYear()} SNCF · Tous droits réservés · Supervision Réseau & Information Voyageurs + +
+ +
+
+ + + Supabase Connecté + + + v2.5.0-WCS + +
+
+
+
); diff --git a/src/pages/login/Login.js b/src/pages/login/Login.js index 9455f8e..edc2427 100644 --- a/src/pages/login/Login.js +++ b/src/pages/login/Login.js @@ -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 }) { Erreur {error} @@ -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 diff --git a/src/pages/materiel-roulant/MaterielRoulant.css b/src/pages/materiel-roulant/MaterielRoulant.css index 14c3d67..1bb6ba1 100644 --- a/src/pages/materiel-roulant/MaterielRoulant.css +++ b/src/pages/materiel-roulant/MaterielRoulant.css @@ -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; +} + + diff --git a/src/pages/materiel-roulant/MaterielRoulant.js b/src/pages/materiel-roulant/MaterielRoulant.js index 0086dcb..b2135b1 100644 --- a/src/pages/materiel-roulant/MaterielRoulant.js +++ b/src/pages/materiel-roulant/MaterielRoulant.js @@ -73,7 +73,7 @@ function MaterielGroupCard({ imageUrl, items, onEdit, onDelete, onDuplicate, can }; return ( -
+
{image ? (
2 ? 'scrollable' : ''}`}> - {items.map((materiel) => ( -
-
- Rame n° {materiel.numero_serie || '-'} - - {materiel.nom || '-'} · {materiel.nom_technique || '-'} - {materiel.capacite ? ` (${materiel.capacite} pl.)` : ''} - + {items.map((materiel, idx) => { + const isLast = idx === items.length - 1 || items.length <= 2; + return ( +
+
+ Rame n° {materiel.numero_serie || '-'} + + {materiel.nom || '-'} · {materiel.nom_technique || '-'} + {materiel.capacite ? ` (${materiel.capacite} pl.)` : ''} + +
+
+ + {openActionsFor === materiel.id && ( +
+ + + +
+ )} +
-
- - {openActionsFor === materiel.id && ( -
- - - -
- )} -
-
- ))} + ); + })}
); diff --git a/src/pages/perturbations/CreerPerturbation.js b/src/pages/perturbations/CreerPerturbation.js index 2a60b19..607cacc 100644 --- a/src/pages/perturbations/CreerPerturbation.js +++ b/src/pages/perturbations/CreerPerturbation.js @@ -6,7 +6,6 @@ import { getAllGares } from '../../api/gares'; import { getAllMateriels } from '../../api/materielsRoulants'; import { CAUSES_PERTURBATION, - getPrimaryType, buildArretFromHoraire, recomputePropagation, buildPayload, @@ -46,7 +45,7 @@ const getMaterielIds = (horaire) => { export default function CreerPerturbation() { const currentUser = getCurrentUser(); const canCreatePerturbations = canPerform(currentUser, 'perturbations', 'create'); - + // ── Navigation ──────────────────────────────────────────────────────────── const [currentStep, setCurrentStep] = useState('horaire'); @@ -54,13 +53,13 @@ export default function CreerPerturbation() { const [horaires, setHoraires] = useState([]); const [gares, setGares] = useState([]); const [loadingHoraires, setLoadingHoraires] = useState(false); - + const [searchDate, setSearchDate] = useState(new Date().toISOString().split('T')[0]); const [searchMode, setSearchMode] = useState('train'); const [searchQuery, setSearchQuery] = useState(''); const [filteredHoraires, setFilteredHoraires] = useState([]); const [showResults, setShowResults] = useState(false); - + const [selectedHoraires, setSelectedHoraires] = useState([]); // ── Formulaire perturbation ─────────────────────────────────────────────── @@ -77,7 +76,7 @@ export default function CreerPerturbation() { const [perturbationsArretsMap, setPerturbationsArretsMap] = useState({}); const [openTrains, setOpenTrains] = useState({}); const [openStops, setOpenStops] = useState({}); - + const [saving, setSaving] = useState(false); const [scopedRegionId, setScopedRegionId] = useState(null); @@ -102,27 +101,26 @@ export default function CreerPerturbation() { const handleSearch = useCallback(() => { const q = (searchQuery || '').toLowerCase().trim(); let filtered = horaires.filter(h => doesHoraireCirculateOnDate(h, searchDate)); - + if (searchMode === 'train') { - filtered = filtered.filter(h => + filtered = filtered.filter(h => [h.numero_train, h.type_train, h.gare_depart_nom, h.gare_arrivee_nom, h.ligne_nom] .map(f => (f || '').toString().toLowerCase()) .some(f => f.includes(q)) ); } else { // mode ligne - filtered = filtered.filter(h => + filtered = filtered.filter(h => (h.ligne_nom || '').toLowerCase().includes(q) ); } - - // Trier par numéro de train + filtered.sort((a, b) => { const aNum = a.numero_train || ''; const bNum = b.numero_train || ''; return aNum.toString().localeCompare(bNum.toString(), undefined, { numeric: true }); }); - + setFilteredHoraires(filtered); setShowResults(true); }, [searchQuery, horaires, searchDate, searchMode]); @@ -141,7 +139,7 @@ export default function CreerPerturbation() { } return next; }); - setOpenTrains(prev => ({ ...prev, [h.id]: true })); // Open by default + setOpenTrains(prev => ({ ...prev, [h.id]: true })); }, []); const handleSelectSingleHoraire = useCallback((h) => { @@ -167,7 +165,7 @@ export default function CreerPerturbation() { const toggleTrainAccordion = useCallback((hId) => { setOpenTrains(p => ({ ...p, [hId]: !p[hId] })); }, []); - + const toggleStopAccordion = useCallback((hId, i) => { setOpenStops(p => { const current = p[hId] || {}; @@ -181,41 +179,31 @@ export default function CreerPerturbation() { const nextArr = [...nextMap[hId]]; const a = { ...nextArr[index] }; let types = [...(a.types_perturbation || [])]; - + if (checked) { if (!types.includes(typeValue)) types.push(typeValue); - if (typeValue === 'retard') { - a.manual_retard = true; - a.retard_minutes = Number(a.retard_minutes) > 0 ? a.retard_minutes : 15; + if (typeValue === 'retard') { + a.manual_retard = true; + a.retard_minutes = Number(a.retard_minutes) > 0 ? a.retard_minutes : 15; } if (typeValue === 'reduction_retard') { a.manual_reduction = true; a.reduction_retard_minutes = Number(a.reduction_retard_minutes) > 0 ? a.reduction_retard_minutes : 5; } - if (typeValue === 'arret_prolonge') { - a.manual_arret_prolonge = true; - a.arret_prolonge_minutes = Number(a.arret_prolonge_minutes) > 0 ? a.arret_prolonge_minutes : 5; + if (typeValue === 'arret_prolonge') { + a.manual_arret_prolonge = true; + a.arret_prolonge_minutes = Number(a.arret_prolonge_minutes) > 0 ? a.arret_prolonge_minutes : 5; } if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; } } else { types = types.filter(t => t !== typeValue); - if (typeValue === 'retard') { - a.manual_retard = false; - a.retard_minutes = 0; - } - if (typeValue === 'reduction_retard') { - a.manual_reduction = false; - a.reduction_retard_minutes = 0; - } - if (typeValue === 'arret_prolonge') { - a.manual_arret_prolonge = false; - a.arret_prolonge_minutes = 0; - } - if (typeValue === 'arret_supprime') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; } + if (typeValue === 'retard') { a.manual_retard = false; a.retard_minutes = 0; } + if (typeValue === 'reduction_retard') { a.manual_reduction = false; a.reduction_retard_minutes = 0; } + if (typeValue === 'arret_prolonge') { a.manual_arret_prolonge = false; a.arret_prolonge_minutes = 0; } if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; } } + a.types_perturbation = types; - a.type_perturbation = getPrimaryType(types); nextArr[index] = a; nextMap[hId] = recomputePropagation(nextArr); return nextMap; @@ -226,40 +214,11 @@ export default function CreerPerturbation() { setPerturbationsArretsMap(prev => { const nextMap = { ...prev }; const nextArr = [...nextMap[hId]]; - nextArr[index] = { ...nextArr[index], [field]: value }; - - if (field === 'retard_minutes') { - const mins = Number(value) || 0; - nextArr[index].retard_minutes = mins; - nextArr[index].manual_retard = mins > 0; - const types = [...(nextArr[index].types_perturbation || [])]; - if (mins > 0 && !types.includes('retard')) types.push('retard'); - nextArr[index].types_perturbation = types; - nextArr[index].type_perturbation = getPrimaryType(types); - } - if (field === 'reduction_retard_minutes') { - const mins = Number(value) || 0; - nextArr[index].reduction_retard_minutes = mins; - nextArr[index].manual_reduction = mins > 0; - const types = [...(nextArr[index].types_perturbation || [])]; - if (mins > 0 && !types.includes('reduction_retard')) types.push('reduction_retard'); - else if (mins === 0) { - const idx = types.indexOf('reduction_retard'); - if (idx !== -1) types.splice(idx, 1); - } - nextArr[index].types_perturbation = types; - nextArr[index].type_perturbation = getPrimaryType(types); - } - if (field === 'arret_prolonge_minutes') { - const mins = Number(value) || 0; - nextArr[index].arret_prolonge_minutes = mins; - nextArr[index].manual_arret_prolonge = mins > 0; - const types = [...(nextArr[index].types_perturbation || [])]; - if (mins > 0) { if (!types.includes('arret_prolonge')) types.push('arret_prolonge'); } - else { const i2 = types.indexOf('arret_prolonge'); if (i2 !== -1) types.splice(i2, 1); } - nextArr[index].types_perturbation = types; - nextArr[index].type_perturbation = getPrimaryType(types); - } + const a = { ...nextArr[index], [field]: value }; + if (field === 'retard_minutes') a.manual_retard = true; + if (field === 'reduction_retard_minutes') a.manual_reduction = true; + if (field === 'arret_prolonge_minutes') a.manual_arret_prolonge = true; + nextArr[index] = a; nextMap[hId] = recomputePropagation(nextArr); return nextMap; }); @@ -268,9 +227,12 @@ export default function CreerPerturbation() { const handleSetNouveauTerminus = useCallback((hId, index) => { setPerturbationsArretsMap(prev => { const nextMap = { ...prev }; - const nextArr = prev[hId].map((a, idx) => { - if (idx === index) return { ...a, is_nouveau_terminus: true, is_nouvelle_origine: false }; - if (idx > index) return { ...a, types_perturbation: ['arret_supprime'], type_perturbation: 'arret_supprime', cause_arret: prev[hId][index]?.cause_arret || '', is_nouveau_terminus: false, is_nouvelle_origine: false }; + const nextArr = nextMap[hId].map((a, i) => { + const types = [...(a.types_perturbation || [])]; + if (i === index) { + if (!types.includes('modification_parcours')) types.push('modification_parcours'); + return { ...a, types_perturbation: types, is_nouveau_terminus: true, is_nouvelle_origine: false }; + } return { ...a, is_nouveau_terminus: false }; }); nextMap[hId] = recomputePropagation(nextArr); @@ -281,9 +243,12 @@ export default function CreerPerturbation() { const handleSetNouvelleOrigine = useCallback((hId, index) => { setPerturbationsArretsMap(prev => { const nextMap = { ...prev }; - const nextArr = prev[hId].map((a, idx) => { - if (idx === index) return { ...a, is_nouvelle_origine: true, is_nouveau_terminus: false }; - if (idx < index) return { ...a, types_perturbation: ['arret_supprime'], type_perturbation: 'arret_supprime', cause_arret: prev[hId][index]?.cause_arret || '', is_nouveau_terminus: false, is_nouvelle_origine: false }; + const nextArr = nextMap[hId].map((a, i) => { + const types = [...(a.types_perturbation || [])]; + if (i === index) { + if (!types.includes('modification_parcours')) types.push('modification_parcours'); + return { ...a, types_perturbation: types, is_nouvelle_origine: true, is_nouveau_terminus: false }; + } return { ...a, is_nouvelle_origine: false }; }); nextMap[hId] = recomputePropagation(nextArr); @@ -294,441 +259,486 @@ export default function CreerPerturbation() { const handleAddStopAfter = useCallback((hId, index) => { setPerturbationsArretsMap(prev => { const nextMap = { ...prev }; - const nextArr = [...prev[hId]]; + const nextArr = [...nextMap[hId]]; const newStop = { + gare_id: null, gare_nom: '', + heure_arrivee: '', + heure_depart: '', types_perturbation: ['modification_parcours'], type_perturbation: 'modification_parcours', is_added_manually: true, - heure_arrivee: '', - heure_depart: '' + retard_minutes: 0, + reduction_retard_minutes: 0, + retard_cumule: 0, + cause_arret: '', + is_nouveau_terminus: false, + is_nouvelle_origine: false, + manual_retard: false, + manual_reduction: false, + manual_arret_prolonge: false, }; nextArr.splice(index + 1, 0, newStop); - const indexed = nextArr.map((a, idx) => ({ ...a, index: idx, isFirst: idx === 0, isLast: idx === nextArr.length - 1 })); - nextMap[hId] = recomputePropagation(indexed); + nextMap[hId] = recomputePropagation(nextArr); return nextMap; }); + setOpenStops(p => { + const current = p[hId] || {}; + return { ...p, [hId]: { ...current, [index + 1]: true } }; + }); }, []); const handleDeleteStop = useCallback((hId, index) => { setPerturbationsArretsMap(prev => { const nextMap = { ...prev }; - const nextArr = [...prev[hId]]; - if (nextArr[index].is_added_manually) { - nextArr.splice(index, 1); - } else { - nextArr[index] = { ...nextArr[index], types_perturbation: ['arret_supprime'], type_perturbation: 'arret_supprime' }; - } - const indexed = nextArr.map((a, idx) => ({ ...a, index: idx, isFirst: idx === 0, isLast: idx === nextArr.length - 1 })); - nextMap[hId] = recomputePropagation(indexed); + const nextArr = nextMap[hId].filter((_, i) => i !== index); + nextMap[hId] = recomputePropagation(nextArr); return nextMap; }); }, []); - // ── Soumission ──────────────────────────────────────────────────────────── - const handleCreate = useCallback(async () => { - if (!canCreatePerturbations) { - alert('Vous n\'avez pas les droits pour créer une perturbation.'); - return; - } - if (selectedHoraires.length === 0) { alert('Veuillez sélectionner au moins un horaire'); return; } - - // Check if at least one stop is perturbed for all selected horaires - for (const h of selectedHoraires) { - const arrets = perturbationsArretsMap[h.id]; - if (!arrets || !arrets.some(a => (a.types_perturbation || []).length > 0)) { - alert(`Veuillez configurer au moins une perturbation pour le train ${h.numero_train}`); return; - } - } - - setSaving(true); - let allSuccess = true; - - try { - for (const h of selectedHoraires) { - const { payload, modificationsParcours } = buildPayload({ - selectedHoraire: h, - perturbationsArrets: perturbationsArretsMap[h.id], - cause, causeDetails, - sillonSubstitutionId, joursPerturbation, actif, notes, - }); - if (scopedRegionId) payload.region_id = scopedRegionId; - const { error } = await createPerturbation(payload, modificationsParcours); - if (error) { - alert(`Erreur lors de la création pour le train ${h.numero_train}`); - console.error(error); - allSuccess = false; - } - } - - if (allSuccess) { - alert('Perturbations créées avec succès !'); - window.location.href = '/perturbations'; - } - } catch (e) { - console.error(e); alert('Une erreur inattendue s\'est produite'); - } finally { setSaving(false); } - }, [canCreatePerturbations, selectedHoraires, perturbationsArretsMap, cause, causeDetails, sillonSubstitutionId, joursPerturbation, actif, notes, scopedRegionId]); - - const handleCancel = useCallback(() => { - if (window.confirm('Êtes-vous sûr de vouloir annuler ?')) window.location.href = '/perturbations'; - }, []); - - const getImpactedHoraires = useCallback((h) => { - if (!h) return []; - const selectedMrIds = getMaterielIds(h); - if (selectedMrIds.length === 0) return []; - - return horaires.filter(hor => { - if (hor.id === h.id) return false; - const hMrIds = getMaterielIds(hor); - const shareMr = selectedMrIds.some(id => hMrIds.includes(id)); - if (!shareMr) return false; - return isWithinTwoHours(h.heure_depart, hor.heure_depart); - }); - }, [horaires]); - - const handleAssignNewMaterial = useCallback(async (horaireToUpdate, newMaterielId, contextHoraireId) => { - if (!horaireToUpdate || !newMaterielId) return; - const selectedMr = materiels.find(m => String(m.id) === String(newMaterielId)); - if (!selectedMr) return; - + // ── Gestion du Matériel Roulant ─────────────────────────────────────────── + const handleAssignNewMaterial = async (horaireTarget, newMatId, sourceHoraireId) => { + if (!horaireTarget || !newMatId) return; setAssigningMateriel(true); try { - const compositionPayload = [{ - materiel_id: selectedMr.id, - materiel_nom: selectedMr.nom || '', - materiel_nom_technique: selectedMr.nom_technique || '', - materiel_image_url: selectedMr.image_url || null, - position: 1 - }]; + const selectedMat = materiels.find(m => m.id === newMatId); + if (!selectedMat) throw new Error("Matériel introuvable"); - const { data, error } = await updateHoraireComposition(horaireToUpdate.id, compositionPayload); - if (error) { - alert("Erreur lors de l'attribution du matériel: " + (error.message || error)); - setAssigningMateriel(false); - return; - } - - if (selectedHoraires.some(h => h.id === horaireToUpdate.id)) { - // Update selected horaires state - setSelectedHoraires(prev => prev.map(h => - h.id === horaireToUpdate.id ? { ...h, composition_train: data?.composition_train || compositionPayload } : h - )); - - setCause('modification_materiel'); - setPerturbationsArretsMap(prev => { - const nextMap = { ...prev }; - const nextArr = [...nextMap[horaireToUpdate.id]]; - if (nextArr.length > 0) { - const first = { ...nextArr[0] }; - first.manual_retard = true; - first.retard_minutes = 25; - const types = [...(first.types_perturbation || [])]; - if (!types.includes('retard')) types.push('retard'); - first.types_perturbation = types; - first.type_perturbation = getPrimaryType(types); - nextArr[0] = first; - } - nextMap[horaireToUpdate.id] = recomputePropagation(nextArr); - return nextMap; - }); - } else { - const arrets = buildArretFromHoraire(horaireToUpdate); - if (arrets.length > 0) { - arrets[0].manual_retard = true; - arrets[0].retard_minutes = 25; - const types = [...(arrets[0].types_perturbation || [])]; - if (!types.includes('retard')) types.push('retard'); - arrets[0].types_perturbation = types; - arrets[0].type_perturbation = getPrimaryType(types); + const newComp = [ + { + materiel_id: selectedMat.id, + materiel_nom: selectedMat.nom, + materiel_nom_technique: selectedMat.nom_technique, + materiel_image_url: selectedMat.image_url, + capacite: selectedMat.capacite, + type_train: selectedMat.type_train, + position: 1 } - const propagated = recomputePropagation(arrets); - const { payload, modificationsParcours } = buildPayload({ - selectedHoraire: horaireToUpdate, - perturbationsArrets: propagated, - cause: 'modification_materiel', - causeDetails: 'Modification de matériel suite à indisponibilité du matériel initial', - sillonSubstitutionId: '', - joursPerturbation: joursPerturbation, - actif: true, - notes: 'Généré automatiquement suite à la réaffectation du matériel roulant.' - }); - if (scopedRegionId) payload.region_id = scopedRegionId; - const { error: pertErr } = await createPerturbation(payload, modificationsParcours); - if (pertErr) { - console.error("Erreur lors de la création automatique de la perturbation:", pertErr); - } else { - alert(`Matériel modifié. Une perturbation a été automatiquement créée pour le train ${horaireToUpdate.numero_train} avec 25 minutes de retard.`); - } - } + ]; - const regionId = getScopedRegionId(getCurrentUser()); - const { data: hData } = await getAllHoraires(regionId); - if (hData) { - setHoraires(hData); - setFilteredHoraires(hData); - // Refresh selected horaires - setSelectedHoraires(prev => prev.map(sh => { - const updated = hData.find(h => h.id === sh.id); - return updated ? updated : sh; - })); + const { data, error } = await updateHoraireComposition(horaireTarget.id, newComp); + if (error) throw error; + + setHoraires(prev => prev.map(h => h.id === horaireTarget.id ? { ...h, composition_train: newComp } : h)); + if (horaireTarget.id === sourceHoraireId) { + setSelectedHoraires(prev => prev.map(h => h.id === sourceHoraireId ? { ...h, composition_train: newComp } : h)); } - } catch (e) { - console.error(e); - alert("Une erreur est survenue lors de l'attribution."); + alert(`Matériel ${selectedMat.nom} (${selectedMat.nom_technique}) affecté au train ${horaireTarget.numero_train}.`); + } catch (err) { + alert("Erreur lors de l'affectation du nouveau matériel : " + (err.message || 'Erreur')); } finally { setAssigningMateriel(false); } - }, [selectedHoraires, materiels, scopedRegionId, joursPerturbation]); + }; + const getImpactedHoraires = (baseHoraire) => { + if (!baseHoraire) return []; + const matIds = getMaterielIds(baseHoraire); + if (matIds.length === 0) return []; - // ── Étape 1 ─────────────────────────────────────────────────────────────── + return horaires.filter(h => { + if (h.id === baseHoraire.id) return false; + const otherMatIds = getMaterielIds(h); + const sharesMaterial = matIds.some(id => otherMatIds.includes(id)); + if (!sharesMaterial) return false; + + return isWithinTwoHours(baseHoraire.heure_depart, h.heure_depart); + }); + }; + + // ── Soumission ──────────────────────────────────────────────────────────── + const handleCreate = async () => { + if (!canCreatePerturbations) { + alert("Vous n'avez pas les droits pour créer une perturbation."); + return; + } + if (selectedHoraires.length === 0) { + alert('Veuillez sélectionner au moins un horaire.'); + return; + } + + setSaving(true); + try { + for (const h of selectedHoraires) { + const arrets = perturbationsArretsMap[h.id] || []; + const payload = buildPayload({ + selectedHoraire: h, + cause, + causeDetails, + sillonSubstitutionId, + joursPerturbation, + actif, + notes, + perturbationsArrets: arrets, + scopedRegionId, + }); + + const { error } = await createPerturbation(payload); + if (error) { + alert(`Erreur lors de la création pour le train ${h.numero_train} : ` + (error.message || 'Erreur')); + setSaving(false); + return; + } + } + window.location.href = '/perturbations'; + } catch (err) { + alert("Une erreur inattendue s'est produite lors de la sauvegarde."); + console.error(err); + setSaving(false); + } + }; + + const handleCancel = () => { + window.location.href = '/perturbations'; + }; + + // ── Étape 1 : Sélection des trains ──────────────────────────────────────── const renderStepHoraire = () => (
-
-

Sélectionner un ou plusieurs horaires

-

Recherchez et sélectionnez les horaires qui seront perturbés

+
+

1. Sélection des circulations ferroviaires

+

Recherchez et sélectionnez le ou les trains impactés par l'incident.

-
- - -
-
- - setSearchDate(e.target.value)} style={{ padding: '8px', border: '1px solid #ccc', borderRadius: '4px', width: '100%' }} /> + + +
+
+ + + setSearchDate(e.target.value)} + /> + +
+
+ + + + +
-
- - -
-
- {loadingHoraires ? ( -
Chargement des horaires...
- ) : ( - <> -
{ e.preventDefault(); handleSearch(); }} className="search-form"> - setSearchQuery(e.target.value)} onFocus={() => setShowResults(true)} /> - - Rechercher - - {(searchQuery || selectedHoraires.length > 0) && ( - - + {loadingHoraires ? ( +
+ +

Chargement des circulations...

+
+ ) : ( + <> + { e.preventDefault(); handleSearch(); }} className="d-flex gap-2"> + setSearchQuery(e.target.value)} + onFocus={() => setShowResults(true)} + /> + + + Rechercher + {(searchQuery || selectedHoraires.length > 0) && ( + + + + )} + + {showResults && searchQuery && ( +
+ + {filteredHoraires.length} circulation{filteredHoraires.length !== 1 ? 's' : ''} + + trouvée{filteredHoraires.length !== 1 ? 's' : ''} pour le {new Date(searchDate).toLocaleDateString('fr-FR')} +
)} - - {showResults && searchQuery && ( -
- {filteredHoraires.length} horaire{filteredHoraires.length !== 1 ? 's' : ''} trouvé{filteredHoraires.length !== 1 ? 's' : ''} circulant le {new Date(searchDate).toLocaleDateString('fr-FR')} -
- )} - - )} -
+ + )} + + {!loadingHoraires && showResults && ( -
- {filteredHoraires.slice(0, 50).map(h => { - const isSelected = selectedHoraires.some(sh => sh.id === h.id); - return ( -
searchMode === 'ligne' ? handleToggleSelectHoraire(h, !isSelected) : handleSelectSingleHoraire(h)}> - {searchMode === 'ligne' && ( -
- +
+
+ {filteredHoraires.slice(0, 50).map(h => { + const isSelected = selectedHoraires.some(sh => sh.id === h.id); + return ( +
searchMode === 'ligne' ? handleToggleSelectHoraire(h, !isSelected) : handleSelectSingleHoraire(h)} + style={{ cursor: 'pointer' }} + > +
+ {searchMode === 'ligne' && ( + + )} +
+
+ Train {h.numero_train} + {h.type_train && {h.type_train}} + {h.ligne_nom && {h.ligne_nom}} +
+
+ {h.gare_depart_nom} + {formatHeure(h.heure_depart)} + + {h.gare_arrivee_nom} + {formatHeure(h.heure_arrivee)} +
+
- )} -
-
- {h.numero_train} - {h.type_train && {h.type_train}} - {h.ligne_nom && {h.ligne_nom}} -
-
- {h.gare_depart_nom} {formatHeure(h.heure_depart)} - - {h.gare_arrivee_nom} {formatHeure(h.heure_arrivee)} + +
+ + + {isSelected ? 'Sélectionné' : 'Choisir'} +
+ ); + })} + {filteredHoraires.length === 0 && ( +
+ Aucun horaire trouvé pour cette date.
- ); - })} - {filteredHoraires.length === 0 &&
Aucun horaire trouvé pour cette date.
} + )} +
)} {selectedHoraires.length > 0 && ( -
-
-

{selectedHoraires.length} Horaire{selectedHoraires.length > 1 ? 's' : ''} sélectionné{selectedHoraires.length > 1 ? 's' : ''}

- -
-
- {selectedHoraires.map(h => ( -
- Train {h.numero_train} ({h.ligne_nom || 'N/A'}) - {h.gare_depart_nom} - - {h.gare_arrivee_nom} -
- ))} -
-
+ + +
+

+ + {selectedHoraires.length} circulation{selectedHoraires.length > 1 ? 's' : ''} sélectionnée{selectedHoraires.length > 1 ? 's' : ''} +

+ Prêt pour étape 2 +
+
+ {selectedHoraires.map(h => ( + + Train {h.numero_train} ({h.gare_depart_nom} → {h.gare_arrivee_nom}) + + ))} +
+
+
)} -
- Annuler - selectedHoraires.length > 0 && setCurrentStep('perturbation')} disabled={selectedHoraires.length === 0}> - Suivant +
+ Annuler + selectedHoraires.length > 0 && setCurrentStep('perturbation')} + disabled={selectedHoraires.length === 0} + > + Étape suivante +
); - // ── Étape 2 ─────────────────────────────────────────────────────────────── + // ── Étape 2 : Configuration de la perturbation ──────────────────────────── const renderStepPerturbation = () => (
-
-

Définir la perturbation

-

Configurez les perturbations pour les horaires sélectionnés

+
+

2. Configuration de l'incident & Parcours

+

Définissez la cause de l'incident, les retards par arrêt et les éventuelles substitutions.

-
-

Parcours {selectedHoraires.length > 1 ? 'des trains' : 'du train'}

-

Cliquez sur un train pour configurer les perturbations de ses arrêts

- - {selectedHoraires.map(h => ( -
-
toggleTrainAccordion(h.id)} - > -

- Train {h.numero_train} ({h.gare_depart_nom} {h.gare_arrivee_nom}) -

- + {/* Parcours des trains avec Accordéon */} + + +
+ +
+

Parcours {selectedHoraires.length > 1 ? 'des trains' : 'du train'}

+ Cliquez sur un train pour configurer les retards et arrêts supprimés
- - {openTrains[h.id] && ( -
- {perturbationsArretsMap[h.id]?.map((arret, i) => ( - toggleStopAccordion(h.id, idx)} - onToggleType={(idx, val, checked) => handleToggleType(h.id, idx, val, checked)} - onArretChange={(idx, field, val) => handleArretChange(h.id, idx, field, val)} - onNouveauTerminus={(idx) => handleSetNouveauTerminus(h.id, idx)} - onNouvelleOrigine={(idx) => handleSetNouvelleOrigine(h.id, idx)} - onAddStopAfter={(idx) => handleAddStopAfter(h.id, idx)} - onDeleteStop={(idx) => handleDeleteStop(h.id, idx)} - garesList={gares} /> - ))} -
- )}
- ))} -
-
-

Cause globale de la perturbation

-
- - -
-
- -