-- ============================================================================ -- 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. -- ============================================================================