Files
ferrovia-panel/SCHEME.sql
T
AdminGit f77d59ca1c V5.0.0 //
-> Refonte du système de GESTION DES ROULEMENTS de matériels roulants
-> Optimisation rendue BDD
2026-10-03 17:33:53 +02:00

1608 lines
77 KiB
PL/PgSQL

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