Files
ferrovia-panel/schema_sql_complet.sql
T
mathis e017166cb5 RELEASE V.1.2.0 - Voici les fonctionnalités :
-> Mode sombre
-> Updater
-> Patch Host
2026-08-26 00:22:06 +02:00

2764 lines
114 KiB
PL/PgSQL

CREATE TYPE public.service_type AS ENUM ('TER', 'TGV', 'Intercités', 'Fret');
CREATE TYPE public.station_type AS ENUM ('interurbaine', 'ville');
CREATE TYPE public.transport_type AS ENUM ('bus', 'tram', 'metro', 'tram-train', 'train');
-- Script SQL pour Supabase — Base app_ferrovia (exécuter dans SQL editor Supabase)
-- Extensions requises
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Table des utilisateurs
CREATE TABLE IF NOT EXISTS public.utilisateurs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text NOT NULL UNIQUE,
nom text,
prenom text,
password_hash text NOT NULL,
role text NOT NULL DEFAULT 'user',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
CREATE UNIQUE INDEX IF NOT EXISTS utilisateurs_email_lower_idx ON public.utilisateurs ((lower(email)));
-- Table d'audit
CREATE TABLE IF NOT EXISTS public.audit_logs (
id bigserial 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()
);
-- Trigger pour updated_at
CREATE OR REPLACE FUNCTION public.set_updated_at()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS tr_utilisateurs_set_updated_at ON public.utilisateurs;
CREATE TRIGGER tr_utilisateurs_set_updated_at
BEFORE UPDATE ON public.utilisateurs
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
-- Fonction d'audit
CREATE OR REPLACE FUNCTION public.log_audit()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE
v_changed_by_text text;
v_changed_by uuid;
BEGIN
v_changed_by_text := current_setting('jwt.claims.user_id', 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;
END;
$$;
DROP TRIGGER IF EXISTS tr_utilisateurs_audit ON public.utilisateurs;
CREATE TRIGGER tr_utilisateurs_audit
AFTER INSERT OR UPDATE OR DELETE ON public.utilisateurs
FOR EACH ROW EXECUTE FUNCTION public.log_audit();
-- Fonction RPC d'authentification
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) LANGUAGE plpgsql STABLE SECURITY DEFINER AS $$
DECLARE
has_nom boolean;
has_prenom boolean;
has_fullname boolean;
has_deleted_at boolean;
sql text;
BEGIN
SELECT EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='utilisateurs' AND column_name='nom'),
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='utilisateurs' AND column_name='prenom'),
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='utilisateurs' AND column_name='full_name'),
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='utilisateurs' AND column_name='deleted_at')
INTO has_nom, has_prenom, has_fullname, has_deleted_at;
IF has_nom AND has_prenom THEN
sql := '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 '
|| 'FROM public.utilisateurs u '
|| 'WHERE lower(u.email)=lower($1) AND u.password_hash = crypt($2, u.password_hash)';
ELSIF has_fullname THEN
sql := 'SELECT u.id::uuid, u.email::text, NULL::text as nom, NULL::text as prenom, u.full_name::text as name, u.role::text '
|| 'FROM public.utilisateurs u '
|| 'WHERE lower(u.email)=lower($1) AND u.password_hash = crypt($2, u.password_hash)';
ELSE
RAISE EXCEPTION 'Table public.utilisateurs : colonnes nom/prenom ou full_name introuvables';
END IF;
IF has_deleted_at THEN
sql := sql || ' AND u.deleted_at IS NULL';
END IF;
RETURN QUERY EXECUTE sql USING p_email, p_password;
END;
$$;
-- Grants (à adapter selon politique RLS)
GRANT EXECUTE ON FUNCTION public.authenticate_user(text, text) TO anon;
GRANT SELECT ON public.utilisateurs TO anon;
-- Insérer un utilisateur de test (admin)
-- Utiliser nom/prenom pour compatibilité avec le schéma principal
INSERT INTO public.utilisateurs (email, nom, prenom, password_hash, role)
VALUES (
'admin@example.com',
'Admin',
'Example',
crypt('password', gen_salt('bf')),
'admin'
) ON CONFLICT (email) DO UPDATE
SET password_hash = EXCLUDED.password_hash,
nom = EXCLUDED.nom,
prenom = EXCLUDED.prenom,
role = EXCLUDED.role,
updated_at = now();
-- =============================================
-- TABLES EQUIVALENTES POUR 'GARES' (doublon pour compatibilité)
-- =============================================
-- Table principale des gares (nommage français)
CREATE TABLE IF NOT EXISTS public.gares (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
nom text NOT NULL,
type station_type NOT NULL DEFAULT 'ville',
services service_type[] NOT NULL DEFAULT '{}',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
-- Table des quais pour gares
CREATE TABLE IF NOT EXISTS public.gare_quais (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
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()
);
-- Table des transports pour gares
CREATE TABLE IF NOT EXISTS public.gare_transports (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
gare_id uuid NOT NULL REFERENCES public.gares(id) ON DELETE CASCADE,
type transport_type NOT NULL,
ligne text,
created_at timestamptz NOT NULL DEFAULT now()
);
-- Index pour les recherches
CREATE INDEX IF NOT EXISTS idx_gare_quais_gare_id ON public.gare_quais(gare_id);
CREATE INDEX IF NOT EXISTS idx_gare_transports_gare_id ON public.gare_transports(gare_id);
-- Trigger pour updated_at sur gares
DROP TRIGGER IF EXISTS tr_gares_set_updated_at ON public.gares;
CREATE TRIGGER tr_gares_set_updated_at
BEFORE UPDATE ON public.gares
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
-- Trigger d'audit pour gares
DROP TRIGGER IF EXISTS tr_gares_audit ON public.gares;
CREATE TRIGGER tr_gares_audit
AFTER INSERT OR UPDATE OR DELETE ON public.gares
FOR EACH ROW EXECUTE FUNCTION public.log_audit();
-- Grants pour les tables gares
GRANT SELECT, INSERT, UPDATE, DELETE ON public.gares TO anon;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.gare_quais TO anon;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.gare_transports TO anon;
-- Fonction RPC pour récupérer une gare (gare) avec ses quais et transports
CREATE OR REPLACE FUNCTION public.get_gare_details(p_gare_id uuid)
RETURNS TABLE(
id uuid,
nom text,
type station_type,
services service_type[],
quais jsonb,
transports jsonb,
created_at timestamptz,
updated_at timestamptz
) LANGUAGE plpgsql STABLE SECURITY DEFINER AS $$
BEGIN
RETURN QUERY
SELECT
g.id,
g.nom,
g.type,
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;
$$;
GRANT EXECUTE ON FUNCTION public.get_gare_details(uuid) TO anon;
-- ============================================
-- TABLE DES MATERIELS ROULANTS
-- ============================================
create table if not exists "materiel-roulant" (
id bigint generated always as identity primary key,
nom text,
nom_technique text,
capacite integer,
image_url text,
type_train text,
numero_serie text unique,
created_at timestamptz default now(),
updated_at timestamptz
);
-- ============================================================================
-- TABLE : utilisateurs
-- Description : Gestion des utilisateurs de l'application
-- ============================================================================
CREATE TABLE IF NOT EXISTS utilisateurs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
nom VARCHAR(100) NOT NULL,
prenom VARCHAR(100) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'utilisateur',
actif BOOLEAN NOT NULL DEFAULT true,
-- Champs additionnels
password_hash VARCHAR(255),
photo_url TEXT,
region_admin BOOLEAN DEFAULT false,
region_nom VARCHAR(255),
permissions JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Si la table existe déjà, ajouter les colonnes manquantes (migration sûre)
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS password_hash VARCHAR(255);
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS photo_url TEXT;
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS region_admin BOOLEAN DEFAULT false;
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS region_nom VARCHAR(255);
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS permissions JSONB NOT NULL DEFAULT '{}'::jsonb;
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS actif BOOLEAN NOT NULL DEFAULT true;
-- Index sur l'email pour optimiser les recherches
CREATE INDEX IF NOT EXISTS idx_utilisateurs_email ON utilisateurs(email);
-- Index sur le rôle
CREATE INDEX IF NOT EXISTS idx_utilisateurs_role ON utilisateurs(role);
-- Index sur le statut actif
CREATE INDEX IF NOT EXISTS idx_utilisateurs_actif ON utilisateurs(actif);
-- Commentaires
COMMENT ON TABLE utilisateurs IS 'Utilisateurs de l''application avec leurs informations et rôles';
COMMENT ON COLUMN utilisateurs.id IS 'Identifiant unique de l''utilisateur';
COMMENT ON COLUMN utilisateurs.email IS 'Adresse email de l''utilisateur (unique)';
COMMENT ON COLUMN utilisateurs.nom IS 'Nom de famille de l''utilisateur';
COMMENT ON COLUMN utilisateurs.prenom IS 'Prénom de l''utilisateur';
COMMENT ON COLUMN utilisateurs.role IS 'Rôle de l''utilisateur (admin, gestionnaire, utilisateur)';
COMMENT ON COLUMN utilisateurs.actif IS 'Indique si l''utilisateur est actif ou désactivé';
COMMENT ON COLUMN utilisateurs.created_at IS 'Date de création de l''utilisateur';
COMMENT ON COLUMN utilisateurs.updated_at IS 'Date de dernière modification de l''utilisateur';
COMMENT ON COLUMN utilisateurs.password_hash IS 'Hash du mot de passe (ne pas stocker en clair)';
COMMENT ON COLUMN utilisateurs.photo_url IS 'URL de la photo de profil';
COMMENT ON COLUMN utilisateurs.region_admin IS 'Indique si l''utilisateur administre une région';
COMMENT ON COLUMN utilisateurs.region_nom IS 'Nom de la région administrée (si applicable)';
COMMENT ON COLUMN utilisateurs.permissions IS 'Permissions par fonctionnalite stockees en JSONB';
-- Fonction pour mettre à jour automatiquement updated_at
CREATE OR REPLACE FUNCTION update_utilisateurs_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger pour updated_at
DROP TRIGGER IF EXISTS trigger_update_utilisateurs_updated_at ON utilisateurs;
CREATE TRIGGER trigger_update_utilisateurs_updated_at
BEFORE UPDATE ON utilisateurs
FOR EACH ROW
EXECUTE FUNCTION update_utilisateurs_updated_at();
-- =====================================================
-- Fonction RPC d'authentification utilisant la table utilisateurs
-- Cette fonction vérifie le mot de passe via pgcrypto/crypt
-- Retourne id, email, nom, prenom, role si succès
-- =====================================================
-- S'assurer que pgcrypto est disponible
CREATE EXTENSION IF NOT EXISTS pgcrypto;
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) LANGUAGE plpgsql STABLE SECURITY DEFINER AS $$
DECLARE
has_deleted_at boolean;
sql text;
BEGIN
SELECT EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='utilisateurs' AND column_name='deleted_at')
INTO has_deleted_at;
sql := '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 '
|| 'FROM public.utilisateurs u '
|| 'WHERE lower(u.email) = lower($1) '
|| 'AND u.password_hash IS NOT NULL '
|| 'AND u.password_hash = crypt($2, u.password_hash) '
|| 'AND coalesce(u.actif, true) = true';
IF has_deleted_at THEN
sql := sql || ' AND u.deleted_at IS NULL';
END IF;
RETURN QUERY EXECUTE sql USING p_email, p_password;
END;
$$;
-- Grants pour permettre l'appel depuis le client (adapter selon politiques RLS)
GRANT EXECUTE ON FUNCTION public.authenticate_user(text, text) TO anon;
GRANT SELECT ON public.utilisateurs TO anon;
-- Inserer un utilisateur admin de test (mot de passe: 'password')
INSERT INTO utilisateurs (email, nom, prenom, password_hash, role, actif)
VALUES (
'admin@example.com',
'Admin',
'Example',
crypt('password', gen_salt('bf')),
'admin',
true
)
ON CONFLICT (email) DO UPDATE
SET password_hash = EXCLUDED.password_hash,
nom = EXCLUDED.nom,
prenom = EXCLUDED.prenom,
role = EXCLUDED.role,
actif = EXCLUDED.actif,
updated_at = NOW();
-- FIN du fichier
-- ============================================================================
-- TABLE: types_train
-- Description: Stocke les types de train configurables (TER, TGV, etc.)
-- ============================================================================
CREATE TABLE IF NOT EXISTS types_train (
id BIGSERIAL PRIMARY KEY,
nom VARCHAR(255) NOT NULL,
code VARCHAR(50),
description TEXT,
logo_url TEXT,
couleur VARCHAR(20) DEFAULT '#0074d9',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Index
CREATE INDEX IF NOT EXISTS idx_types_train_deleted_at ON types_train(deleted_at);
CREATE INDEX IF NOT EXISTS idx_types_train_code ON types_train(code);
-- ============================================================================
-- MODIFICATION TABLE: horaires
-- Ajout de la colonne ligne et modification de type_train_id
-- ============================================================================
-- -- Ajouter colonne ligne si elle n'existe pas
-- DO $$
-- BEGIN
-- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name='horaires') AND NOT EXISTS (
-- SELECT 1 FROM information_schema.columns
-- WHERE table_name='horaires' AND column_name='ligne'
-- ) THEN
-- ALTER TABLE horaires ADD COLUMN ligne VARCHAR(255);
-- END IF;
-- END $$;
--
-- -- Ajouter colonne type_train_id si elle n'existe pas (relation vers types_train)
-- DO $$
-- BEGIN
-- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name='horaires') AND NOT EXISTS (
-- SELECT 1 FROM information_schema.columns
-- WHERE table_name='horaires' AND column_name='type_train_id'
-- ) THEN
-- ALTER TABLE horaires ADD COLUMN type_train_id BIGINT REFERENCES types_train(id);
-- END IF;
-- END $$;
--
-- -- Index
-- CREATE INDEX IF NOT EXISTS idx_horaires_ligne ON horaires(ligne);
-- CREATE INDEX IF NOT EXISTS idx_horaires_type_train_id ON horaires(type_train_id);
-- ============================================================================
-- DONNÉES INITIALES: types_train
-- Pré-remplir avec des types de train classiques
-- ============================================================================
INSERT INTO types_train (nom, code, description, couleur) VALUES
('TER', 'TER', 'Transport Express Régional', '#0088ce'),
('TGV', 'TGV', 'Train à Grande Vitesse', '#d2232a'),
('Intercités', 'IC', 'Trains Intercités', '#00a88f'),
('TGV INOUI', 'TGVI', 'TGV INOUI', '#d2232a'),
('OUIGO', 'OUI', 'OUIGO', '#ed008c'),
('Transilien', 'TRANS', 'Trains Transilien Île-de-France', '#8d5da8')
ON CONFLICT DO NOTHING;
-- ============================================================================
-- COMMENTAIRES
-- ============================================================================
COMMENT ON TABLE types_train IS 'Types de train configurables avec logos et couleurs';
-- COMMENT ON COLUMN horaires.ligne IS 'Ligne de train (ex: Paris-Lyon, ligne N, etc.)';
-- COMMENT ON COLUMN horaires.type_train_id IS 'Référence vers le type de train configuré';
-- ============================================================================
-- TABLES POUR LES LIGNES ET SERVICES ANNUELS
-- ============================================================================
-- Table des lignes
CREATE TABLE IF NOT EXISTS lignes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nom VARCHAR(255) NOT NULL,
code VARCHAR(50),
gare_depart_id UUID REFERENCES gares(id) ON DELETE SET NULL,
gare_arrivee_id UUID REFERENCES gares(id) ON DELETE SET NULL,
actif BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Table des gares sur le tracé d'une ligne
CREATE TABLE IF NOT EXISTS lignes_gares (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ligne_id UUID REFERENCES lignes(id) ON DELETE CASCADE,
gare_id UUID REFERENCES gares(id) ON DELETE CASCADE,
ordre INTEGER NOT NULL,
desserte VARCHAR(20) NOT NULL CHECK (desserte IN ('tous', 'partielle')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Table des services annuels
CREATE TABLE IF NOT EXISTS services_annuels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nom VARCHAR(255) NOT NULL,
code VARCHAR(50),
date_debut DATE NOT NULL,
date_fin DATE NOT NULL,
description TEXT,
actif BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
--
-- -- Ajouter la colonne service_annuel_id à la table horaires si elle n'existe pas
-- DO $$
-- BEGIN
-- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name='horaires') AND NOT EXISTS (
-- SELECT 1 FROM information_schema.columns
-- WHERE table_name = 'horaires' AND column_name = 'service_annuel_id'
-- ) THEN
-- ALTER TABLE horaires ADD COLUMN service_annuel_id UUID REFERENCES services_annuels(id) ON DELETE SET NULL;
-- END IF;
-- END $$;
--
-- -- Ajouter la colonne ligne_id à la table horaires si elle n'existe pas
-- DO $$
-- BEGIN
-- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name='horaires') AND NOT EXISTS (
-- SELECT 1 FROM information_schema.columns
-- WHERE table_name = 'horaires' AND column_name = 'ligne_id'
-- ) THEN
-- ALTER TABLE horaires ADD COLUMN ligne_id UUID REFERENCES lignes(id) ON DELETE SET NULL;
-- END IF;
-- END $$;
-- Index pour améliorer les performances
CREATE INDEX IF NOT EXISTS idx_lignes_actif ON lignes(actif) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_lignes_gares_ligne ON lignes_gares(ligne_id);
CREATE INDEX IF NOT EXISTS idx_lignes_gares_ordre ON lignes_gares(ligne_id, ordre);
CREATE INDEX IF NOT EXISTS idx_services_annuels_actif ON services_annuels(actif) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_services_annuels_dates ON services_annuels(date_debut, date_fin);
-- Trigger pour mettre à jour updated_at automatiquement
DROP TRIGGER IF EXISTS update_lignes_updated_at ON lignes;
CREATE TRIGGER update_lignes_updated_at
BEFORE UPDATE ON lignes
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS update_services_annuels_updated_at ON services_annuels;
CREATE TRIGGER update_services_annuels_updated_at
BEFORE UPDATE ON services_annuels
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Commentaires sur les tables
COMMENT ON TABLE lignes IS 'Lignes de train avec gares de départ et d''arrivée';
COMMENT ON TABLE lignes_gares IS 'Gares présentes sur le tracé d''une ligne avec type de desserte';
COMMENT ON TABLE services_annuels IS 'Services annuels attribuables aux horaires';
-- ============================================================================
-- TABLES POUR LE SYSTÈME DE GESTION DES HORAIRES (SILLONS)
-- ============================================================================
-- Table principale des horaires (sillons)
CREATE TABLE IF NOT EXISTS horaires (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Informations générales
numero_train VARCHAR(20) NOT NULL,
type_train VARCHAR(50),
type_train_id BIGINT REFERENCES types_train(id) ON DELETE SET NULL,
-- Gare de départ
gare_depart_id UUID REFERENCES gares(id) ON DELETE SET NULL,
gare_depart_nom VARCHAR(255),
heure_depart TIME NOT NULL,
-- Gare d'arrivée (destination)
gare_arrivee_id UUID REFERENCES gares(id) ON DELETE SET NULL,
gare_arrivee_nom VARCHAR(255),
heure_arrivee TIME NOT NULL,
-- Jours de circulation (base)
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,
-- Jours spéciaux
circule_jours_feries BOOLEAN DEFAULT false,
circule_dimanches_feries BOOLEAN DEFAULT false,
-- Jours personnalisés (dates spécifiques de circulation)
jours_personnalises JSONB DEFAULT '[]'::jsonb,
-- Format: [{"date": "2026-01-15", "circule": true}, ...]
-- Jours de non-circulation (dates où le train ne circule pas)
jours_non_circulation JSONB DEFAULT '[]'::jsonb,
-- Format: ["2026-01-20", "2026-02-14", ...]
-- Matériel roulant associé
materiel_roulant_id BIGINT REFERENCES "materiel-roulant"(id) ON DELETE SET NULL,
-- Lien vers la table des lignes (optionnel)
ligne_id UUID REFERENCES lignes(id) ON DELETE SET NULL,
ligne_nom VARCHAR(255),
-- Service Annuel associé (SA)
service_annuel_id UUID REFERENCES services_annuels(id) ON DELETE SET NULL,
service_annuel_nom VARCHAR(255),
-- Substitution (autocar)
est_substitution BOOLEAN DEFAULT false,
motif_substitution VARCHAR(255),
-- Gares desservies (snapshot JSON — copie dénormalisée pour lecture rapide)
gares_desservies JSONB DEFAULT '[]'::jsonb,
-- Format: [{"gare_id": "...", "gare_nom": "...", "heure_arrivee": "HH:MM", "heure_depart": "HH:MM", "ordre": 1}, ...]
-- Composition du train (liste ordonnée des matériels roulants)
composition_train JSONB DEFAULT '[]'::jsonb,
-- Format: [{"materiel_id": 1, "materiel_nom": "Régiolis", "materiel_nom_technique": "X73500", "materiel_image_url": "...", "position": 1}, ...]
-- Métadonnées
actif BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Table des gares desservies (arrêts intermédiaires)
CREATE TABLE IF NOT EXISTS horaires_gares_desservies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
horaire_id UUID NOT NULL REFERENCES horaires(id) ON DELETE CASCADE,
-- Informations de la gare desservie
gare_id UUID REFERENCES gares(id) ON DELETE SET NULL,
gare_nom VARCHAR(255) NOT NULL,
-- Horaires à cette gare
heure_arrivee TIME,
heure_depart TIME,
-- Ordre dans le parcours (1 = première gare desservie après départ)
ordre INTEGER NOT NULL DEFAULT 0,
-- Métadonnées
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Index pour améliorer les performances
CREATE INDEX IF NOT EXISTS idx_horaires_numero_train ON horaires(numero_train);
CREATE INDEX IF NOT EXISTS idx_horaires_gare_depart ON horaires(gare_depart_id);
CREATE INDEX IF NOT EXISTS idx_horaires_gare_arrivee ON horaires(gare_arrivee_id);
CREATE INDEX IF NOT EXISTS idx_horaires_actif ON horaires(actif) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_horaires_type_train ON horaires(type_train_id);
CREATE INDEX IF NOT EXISTS idx_horaires_materiel ON horaires(materiel_roulant_id);
CREATE INDEX IF NOT EXISTS idx_horaires_ligne ON horaires(ligne_id);
CREATE INDEX IF NOT EXISTS idx_horaires_service_annuel ON horaires(service_annuel_id);
CREATE INDEX IF NOT EXISTS idx_horaires_substitution ON horaires(est_substitution);
CREATE INDEX IF NOT EXISTS idx_gares_desservies_horaire ON horaires_gares_desservies(horaire_id);
CREATE INDEX IF NOT EXISTS idx_gares_desservies_ordre ON horaires_gares_desservies(horaire_id, ordre);
-- Trigger pour mettre à jour updated_at automatiquement
DROP TRIGGER IF EXISTS update_horaires_updated_at ON horaires;
CREATE TRIGGER update_horaires_updated_at
BEFORE UPDATE ON horaires
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS update_horaires_gares_desservies_updated_at ON horaires_gares_desservies;
CREATE TRIGGER update_horaires_gares_desservies_updated_at
BEFORE UPDATE ON horaires_gares_desservies
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Commentaires sur les tables
COMMENT ON TABLE horaires IS 'Table principale des horaires (sillons) de trains';
COMMENT ON TABLE horaires_gares_desservies IS 'Gares intermédiaires desservies par chaque horaire';
COMMENT ON COLUMN horaires.numero_train IS 'Numéro unique du train (ex: TER 12345)';
COMMENT ON COLUMN horaires.type_train IS 'Type de train (TER, TGV, Intercités, etc.)';
COMMENT ON COLUMN horaires.type_train_id IS 'Référence vers la table types_train pour le type de train';
COMMENT ON COLUMN horaires.jours_personnalises IS 'Dates spécifiques de circulation au format JSON';
COMMENT ON COLUMN horaires.jours_non_circulation IS 'Dates où le train ne circule pas au format JSON';
COMMENT ON COLUMN horaires.est_substitution IS 'Indique si ce sillon est une substitution autocar';
COMMENT ON COLUMN horaires.motif_substitution IS 'Motif de la substitution (travaux, etc.)';
COMMENT ON COLUMN horaires.ligne_id IS 'Identifiant de la ligne associée au sillon';
COMMENT ON COLUMN horaires.ligne_nom IS 'Nom de la ligne associée (copie pour lecture)';
COMMENT ON COLUMN horaires.service_annuel_id IS 'Identifiant du service annuel associé (SA)';
COMMENT ON COLUMN horaires.service_annuel_nom IS 'Nom du service annuel associé (copie pour lecture)';
COMMENT ON COLUMN horaires.gares_desservies IS 'Snapshot JSON des gares desservies (copie dénormalisée synchronisée avec horaires_gares_desservies)';
COMMENT ON COLUMN horaires.composition_train IS 'Composition ordonnée du train : liste JSON des matériels roulants affectés au sillon (position, id, nom, image)';
COMMENT ON COLUMN horaires_gares_desservies.ordre IS 'Position de la gare dans le parcours (1 = première après départ)';
-- ============================================================================
-- TABLES POUR LE SYSTÈME DE GESTION DES PERTURBATIONS
-- ============================================================================
-- Table principale des perturbations
CREATE TABLE IF NOT EXISTS perturbations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Référence à l'horaire perturbé
horaire_id UUID NOT NULL REFERENCES horaires(id) ON DELETE CASCADE,
-- Type de perturbation
type_perturbation VARCHAR(50) NOT NULL CHECK (type_perturbation IN ('retard', 'suppression', 'modification_parcours')),
-- Informations sur le retard (si applicable)
duree_retard_minutes INTEGER, -- Durée du retard en minutes
-- Cause de la perturbation
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, -- Détails supplémentaires sur la cause
-- Sillon de substitution (si suppression ou modification de parcours)
sillon_substitution_id UUID REFERENCES horaires(id) ON DELETE SET NULL,
-- Période de validité de la perturbation
date_debut TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
date_fin TIMESTAMP WITH TIME ZONE,
-- Statut
actif BOOLEAN DEFAULT true,
-- Notes internes
notes TEXT,
-- Snapshot complet des perturbations par arrêt (état du formulaire)
-- Permet de restaurer exactement le formulaire lors de la modification
perturbations_arrets JSONB DEFAULT NULL,
-- Métadonnées
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Table des modifications de parcours (gares supprimées/ajoutées)
CREATE TABLE IF NOT EXISTS perturbations_modifications_parcours (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
perturbation_id UUID NOT NULL REFERENCES perturbations(id) ON DELETE CASCADE,
-- Type de modification
type_modification VARCHAR(20) NOT NULL CHECK (type_modification IN ('gare_supprimee', 'gare_ajoutee', 'horaire_modifie')),
-- Gare concernée
gare_id UUID REFERENCES gares(id) ON DELETE SET NULL,
gare_nom VARCHAR(255) NOT NULL,
-- Nouvel horaire si modification
nouvelle_heure_arrivee TIME,
nouvelle_heure_depart TIME,
-- Ordre dans le parcours (pour les gares ajoutées)
ordre INTEGER,
-- Métadonnées
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Index pour améliorer les performances
CREATE INDEX IF NOT EXISTS idx_perturbations_horaire ON perturbations(horaire_id);
CREATE INDEX IF NOT EXISTS idx_perturbations_type ON perturbations(type_perturbation);
CREATE INDEX IF NOT EXISTS idx_perturbations_actif ON perturbations(actif) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_perturbations_dates ON perturbations(date_debut, date_fin);
CREATE INDEX IF NOT EXISTS idx_perturbations_sillon_substitution ON perturbations(sillon_substitution_id);
CREATE INDEX IF NOT EXISTS idx_modif_parcours_perturbation ON perturbations_modifications_parcours(perturbation_id);
CREATE INDEX IF NOT EXISTS idx_modif_parcours_gare ON perturbations_modifications_parcours(gare_id);
-- Trigger pour mettre à jour updated_at automatiquement
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
DROP TRIGGER IF EXISTS update_perturbations_updated_at ON perturbations;
CREATE TRIGGER update_perturbations_updated_at
BEFORE UPDATE ON perturbations
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Commentaires sur les tables
COMMENT ON TABLE perturbations IS 'Table des perturbations affectant les horaires de trains';
COMMENT ON TABLE perturbations_modifications_parcours IS 'Détails des modifications de parcours pour les perturbations';
COMMENT ON COLUMN perturbations.type_perturbation IS 'Type de perturbation: retard, suppression, ou modification_parcours';
COMMENT ON COLUMN perturbations.duree_retard_minutes IS 'Durée du retard en minutes (applicable uniquement pour type=retard)';
COMMENT ON COLUMN perturbations.cause IS 'Cause de la perturbation';
COMMENT ON COLUMN perturbations.sillon_substitution_id IS 'Train de substitution proposé (si suppression ou modification)';
-- ============================================================================
-- TABLES POUR LE SYSTÈME D'INFORMATIONS
-- ============================================================================
-- Table des informations trafic
CREATE TABLE IF NOT EXISTS info_trafic (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
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 gares(id) ON DELETE SET NULL,
date_debut TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
date_fin TIMESTAMP WITH TIME ZONE,
actif BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Table des événements
CREATE TABLE IF NOT EXISTS evenements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
titre VARCHAR(255) NOT NULL,
description TEXT,
lieu VARCHAR(255),
gare_id UUID REFERENCES gares(id) ON DELETE SET NULL,
date_debut TIMESTAMP WITH TIME ZONE NOT NULL,
date_fin TIMESTAMP WITH TIME ZONE,
image_url VARCHAR(500),
lien_externe VARCHAR(500),
actif BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Table des actualités
CREATE TABLE IF NOT EXISTS actualites (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
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),
date_publication TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
auteur VARCHAR(100),
actif BOOLEAN DEFAULT true,
mise_en_avant BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Index pour améliorer les performances
CREATE INDEX IF NOT EXISTS idx_info_trafic_actif ON info_trafic(actif) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_info_trafic_dates ON info_trafic(date_debut, date_fin);
CREATE INDEX IF NOT EXISTS idx_evenements_actif ON evenements(actif) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_evenements_dates ON evenements(date_debut, date_fin);
CREATE INDEX IF NOT EXISTS idx_actualites_actif ON actualites(actif) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_actualites_publication ON actualites(date_publication);
-- Trigger pour mettre à jour updated_at automatiquement
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
DROP TRIGGER IF EXISTS update_info_trafic_updated_at ON info_trafic;
CREATE TRIGGER update_info_trafic_updated_at
BEFORE UPDATE ON info_trafic
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS update_evenements_updated_at ON evenements;
CREATE TRIGGER update_evenements_updated_at
BEFORE UPDATE ON evenements
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS update_actualites_updated_at ON actualites;
CREATE TRIGGER update_actualites_updated_at
BEFORE UPDATE ON actualites
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Commentaires sur les tables
COMMENT ON TABLE info_trafic IS 'Informations de trafic (perturbations, travaux, etc.)';
COMMENT ON TABLE evenements IS 'Événements à venir dans les gares ou sur le réseau';
COMMENT ON TABLE actualites IS 'Actualités et nouvelles du réseau';
-- ============================================================================
-- MIGRATION: Système de Gestion d'Annonces Sonores
-- ============================================================================
-- À exécuter dans l'interface SQL de Supabase (SQL Editor)
-- ============================================================================
-- 1. TABLE banque_mots
-- Métadonnées des fichiers audio individuels (mots/sons) de la banque
-- ============================================================================
CREATE TABLE IF NOT EXISTS public.banque_mots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label TEXT NOT NULL, -- Libellé affiché (ex: "Bonjour", "Mesdames et messieurs")
nom_fichier TEXT NOT NULL, -- Nom du fichier dans le storage (ex: "bonjour.mp3")
storage_path TEXT NOT NULL, -- Chemin complet dans le bucket (ex: "mots/salutation_bonjour.mp3")
categorie TEXT NOT NULL DEFAULT 'general', -- 'salutation' | 'signal' | 'destination' | 'numero' | 'temps' | 'perturbation' | 'quai' | 'general'
duree_ms INTEGER, -- Durée du fichier audio en millisecondes
format TEXT NOT NULL DEFAULT 'mp3', -- 'mp3' | 'wav' | 'ogg'
taille_bytes BIGINT, -- Taille du fichier en octets
description TEXT, -- Description optionnelle
actif BOOLEAN NOT NULL DEFAULT true,
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- Index pour recherche rapide par catégorie et label
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;
-- 2. TABLE annonces_templates
-- Templates d'annonces définis par concaténation de mots de la banque
-- ============================================================================
CREATE TABLE IF NOT EXISTS public.annonces_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nom TEXT NOT NULL, -- Nom du template (ex: "Départ standard")
type_annonce TEXT NOT NULL, -- 'depart' | 'arrivee' | 'manoeuvre' | 'passage' | 'retard' | 'supprime'
description TEXT, -- Description optionnelle du template
-- Séquence ordonnée des éléments du template
-- Chaque élément: { type: 'mot' | 'variable' | 'pause', mot_id?: UUID, variable?: string, pause_ms?: number }
sequence JSONB NOT NULL DEFAULT '[]', -- Array d'éléments ordonnés
variables JSONB NOT NULL DEFAULT '[]', -- Array des noms de variables attendues (ex: ["numero_train", "destination"])
tts_texte TEXT, -- Texte TTS alternatif pour prévisualisation rapide
actif BOOLEAN NOT NULL DEFAULT true,
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- Index par type d'annonce
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;
-- 3. TABLE annonces_enregistrees
-- Annonces complètes générées et sauvegardées
-- ============================================================================
CREATE TABLE IF NOT EXISTS public.annonces_enregistrees (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nom TEXT NOT NULL, -- Nom de l'annonce (ex: "TER 12345 départ Paris 14h30")
type_annonce TEXT NOT NULL, -- Même enum que annonces_templates.type_annonce
template_id UUID REFERENCES public.annonces_templates(id),
storage_path TEXT NOT NULL, -- Chemin dans le bucket storage
nom_fichier TEXT NOT NULL, -- Nom du fichier final
variables_values JSONB NOT NULL DEFAULT '{}', -- Valeurs des variables utilisées
mode_generation TEXT NOT NULL DEFAULT 'tts', -- 'tts' | 'concatenation'
duree_ms INTEGER,
taille_bytes BIGINT,
gare_id UUID REFERENCES public.gares(id),
diffuse BOOLEAN NOT NULL DEFAULT false,
date_diffusion TIMESTAMPTZ,
notes TEXT,
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- Index de recherche
CREATE INDEX IF NOT EXISTS idx_annonces_type ON public.annonces_enregistrees(type_annonce);
CREATE INDEX IF NOT EXISTS idx_annonces_gare ON public.annonces_enregistrees(gare_id);
CREATE INDEX IF NOT EXISTS idx_annonces_created ON public.annonces_enregistrees(created_at DESC);
-- 4. TRIGGER updated_at automatique
-- ============================================================================
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
DROP TRIGGER IF EXISTS set_banque_mots_updated_at ON public.banque_mots;
CREATE TRIGGER set_banque_mots_updated_at
BEFORE UPDATE ON public.banque_mots
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
DROP TRIGGER IF EXISTS set_templates_updated_at ON public.annonces_templates;
CREATE TRIGGER set_templates_updated_at
BEFORE UPDATE ON public.annonces_templates
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
DROP TRIGGER IF EXISTS set_annonces_updated_at ON public.annonces_enregistrees;
CREATE TRIGGER set_annonces_updated_at
BEFORE UPDATE ON public.annonces_enregistrees
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-- 5. RLS (Row Level Security)
-- ============================================================================
ALTER TABLE public.banque_mots ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.annonces_templates ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.annonces_enregistrees ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can read banque_mots"
ON public.banque_mots FOR SELECT TO authenticated
USING (deleted_at IS NULL);
CREATE POLICY "Authenticated users can insert banque_mots"
ON public.banque_mots FOR INSERT TO authenticated
WITH CHECK (true);
CREATE POLICY "Authenticated users can update banque_mots"
ON public.banque_mots FOR UPDATE TO authenticated
USING (true);
CREATE POLICY "Authenticated users can read templates"
ON public.annonces_templates FOR SELECT TO authenticated
USING (deleted_at IS NULL);
CREATE POLICY "Authenticated users can insert templates"
ON public.annonces_templates FOR INSERT TO authenticated
WITH CHECK (true);
CREATE POLICY "Authenticated users can update templates"
ON public.annonces_templates FOR UPDATE TO authenticated
USING (true);
CREATE POLICY "Authenticated users can read annonces"
ON public.annonces_enregistrees FOR SELECT TO authenticated
USING (deleted_at IS NULL);
CREATE POLICY "Authenticated users can insert annonces"
ON public.annonces_enregistrees FOR INSERT TO authenticated
WITH CHECK (true);
CREATE POLICY "Authenticated users can update annonces"
ON public.annonces_enregistrees FOR UPDATE TO authenticated
USING (true);
-- 6. STORAGE BUCKET
-- ============================================================================
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'banque-annonces',
'banque-annonces',
true,
52428800, -- 50 MB max par fichier
ARRAY['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/wave', 'audio/ogg', 'audio/webm']
)
ON CONFLICT (id) DO NOTHING;
-- Politiques Storage
CREATE POLICY "Public read access on banque-annonces"
ON storage.objects FOR SELECT TO public
USING (bucket_id = 'banque-annonces');
CREATE POLICY "Authenticated users can upload to banque-annonces"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'banque-annonces');
CREATE POLICY "Authenticated users can delete from banque-annonces"
ON storage.objects FOR DELETE TO authenticated
USING (bucket_id = 'banque-annonces');
-- ============================================================================
-- CONVENTION DE NOMMAGE DES FICHIERS AUDIO
-- ============================================================================
-- Fichiers dans banque-annonces/mots/ :
-- Format: [categorie]_[label-kebab-case].[ext]
-- Exemples:
-- salutation_bonjour.mp3
-- salutation_mesdames-et-messieurs.mp3
-- signal_gong-arrivee.mp3
-- signal_gong-depart.mp3
-- destination_paris-gare-de-lyon.mp3
-- temps_a-destination-de.mp3
-- quai_voie-1.mp3
-- numero_ter.mp3
-- perturbation_ce-train-est-supprime.mp3
-- general_attention.mp3
--
-- Annonces générées dans banque-annonces/annonces/ :
-- Format: [type]_[YYYY-MM-DD]_[uuid-court].[ext]
-- Exemple: depart_2026-07-02_a1b2c3.mp3
-- ============================================================================
-- Table pour la gestion des pages (CMS)
CREATE TABLE IF NOT EXISTS pages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
titre TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
contenu JSONB DEFAULT '[]', -- Liste ordonnée de composants
statut TEXT DEFAULT 'brouillon', -- 'publie', 'brouillon'
region_id VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Index pour accélérer la recherche par slug et region
CREATE INDEX IF NOT EXISTS idx_pages_slug ON pages(slug);
CREATE INDEX IF NOT EXISTS idx_pages_region ON pages(region_id);
-- ============================================================================
-- MIGRATION : Ajout du système de régions
-- Description : Ajout des colonnes region_id/region_nom aux tables concernées
-- Date : 2026-02-07
-- ============================================================================
-- ============================================================================
-- TABLE : utilisateurs - Ajout colonne region_id
-- ============================================================================
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS region_id VARCHAR(100);
COMMENT ON COLUMN utilisateurs.region_id IS 'Identifiant de la région administrée par l''utilisateur';
-- Index sur region_id
CREATE INDEX IF NOT EXISTS idx_utilisateurs_region_id ON utilisateurs(region_id);
-- ============================================================================
-- TABLE : info_trafic - Ajout colonnes région
-- ============================================================================
ALTER TABLE info_trafic ADD COLUMN IF NOT EXISTS region_id VARCHAR(100);
ALTER TABLE info_trafic ADD COLUMN IF NOT EXISTS created_by UUID REFERENCES utilisateurs(id);
COMMENT ON COLUMN info_trafic.region_id IS 'Identifiant de la région concernée par l''info trafic';
COMMENT ON COLUMN info_trafic.created_by IS 'Utilisateur ayant créé l''info trafic';
-- Index
CREATE INDEX IF NOT EXISTS idx_info_trafic_region_id ON info_trafic(region_id);
CREATE INDEX IF NOT EXISTS idx_info_trafic_created_by ON info_trafic(created_by);
-- ============================================================================
-- TABLE : evenements - Ajout colonnes région
-- ============================================================================
ALTER TABLE evenements ADD COLUMN IF NOT EXISTS region_id VARCHAR(100);
ALTER TABLE evenements ADD COLUMN IF NOT EXISTS created_by UUID REFERENCES utilisateurs(id);
COMMENT ON COLUMN evenements.region_id IS 'Identifiant de la région concernée par l''événement';
COMMENT ON COLUMN evenements.created_by IS 'Utilisateur ayant créé l''événement';
-- Index
CREATE INDEX IF NOT EXISTS idx_evenements_region_id ON evenements(region_id);
CREATE INDEX IF NOT EXISTS idx_evenements_created_by ON evenements(created_by);
-- ============================================================================
-- TABLE : actualites - Ajout colonnes région
-- ============================================================================
ALTER TABLE actualites ADD COLUMN IF NOT EXISTS region_id VARCHAR(100);
ALTER TABLE actualites ADD COLUMN IF NOT EXISTS created_by UUID REFERENCES utilisateurs(id);
COMMENT ON COLUMN actualites.region_id IS 'Identifiant de la région concernée par l''actualité';
COMMENT ON COLUMN actualites.created_by IS 'Utilisateur ayant créé l''actualité';
-- Index
CREATE INDEX IF NOT EXISTS idx_actualites_region_id ON actualites(region_id);
CREATE INDEX IF NOT EXISTS idx_actualites_created_by ON actualites(created_by);
-- ============================================================================
-- TABLE : perturbations - Ajout colonnes région (si applicable)
-- ============================================================================
ALTER TABLE perturbations ADD COLUMN IF NOT EXISTS region_id VARCHAR(100);
ALTER TABLE perturbations ADD COLUMN IF NOT EXISTS created_by UUID REFERENCES utilisateurs(id);
COMMENT ON COLUMN perturbations.region_id IS 'Identifiant de la région concernée par la perturbation';
COMMENT ON COLUMN perturbations.created_by IS 'Utilisateur ayant créé la perturbation';
-- Index
CREATE INDEX IF NOT EXISTS idx_perturbations_region_id ON perturbations(region_id);
CREATE INDEX IF NOT EXISTS idx_perturbations_created_by ON perturbations(created_by);
-- ============================================================================
-- TABLE : travaux - Ajout colonnes région (si la table existe)
-- ============================================================================
-- Note: À décommenter si la table travaux existe déjà
-- ALTER TABLE travaux ADD COLUMN IF NOT EXISTS region_id VARCHAR(100);
-- ALTER TABLE travaux ADD COLUMN IF NOT EXISTS created_by UUID REFERENCES utilisateurs(id);
-- COMMENT ON COLUMN travaux.region_id IS 'Identifiant de la région concernée par les travaux';
-- COMMENT ON COLUMN travaux.created_by IS 'Utilisateur ayant créé les travaux';
-- CREATE INDEX IF NOT EXISTS idx_travaux_region_id ON travaux(region_id);
-- CREATE INDEX IF NOT EXISTS idx_travaux_created_by ON travaux(created_by);
-- ============================================================================
-- VUES : Vues pratiques pour filtrer par région
-- ============================================================================
-- Vue : Informations par région
CREATE OR REPLACE VIEW v_info_trafic_par_region AS
SELECT
it.*,
u.nom as createur_nom,
u.prenom as createur_prenom,
u.email as createur_email
FROM info_trafic it
LEFT JOIN utilisateurs u ON it.created_by = u.id
ORDER BY it.created_at DESC;
-- Vue : Événements par région
CREATE OR REPLACE VIEW v_evenements_par_region AS
SELECT
e.*,
u.nom as createur_nom,
u.prenom as createur_prenom,
u.email as createur_email
FROM evenements e
LEFT JOIN utilisateurs u ON e.created_by = u.id
ORDER BY e.created_at DESC;
-- Vue : Actualités par région
CREATE OR REPLACE VIEW v_actualites_par_region AS
SELECT
a.*,
u.nom as createur_nom,
u.prenom as createur_prenom,
u.email as createur_email
FROM actualites a
LEFT JOIN utilisateurs u ON a.created_by = u.id
ORDER BY a.created_at DESC;
-- ============================================================================
-- DONNÉES DE TEST (optionnel)
-- ============================================================================
-- Exemple : Associer un utilisateur existant à une région
-- UPDATE utilisateurs SET region_id = 'ile-de-france' WHERE email = 'admin@example.com';
-- ============================================================================
-- NETTOYAGE : Supprimer l'ancienne colonne region_nom si elle existe
-- ============================================================================
-- Note: Migrer les données avant de supprimer
-- UPDATE utilisateurs SET region_id = LOWER(REPLACE(region_nom, ' ', '-')) WHERE region_nom IS NOT NULL;
-- ALTER TABLE utilisateurs DROP COLUMN IF EXISTS region_nom;
-- ============================================================================
-- MIGRATION : Amélioration des tables événements et actualités
-- Description : Ajout des champs pour gestion complète des événements et actualités
-- Date : 2026-02-07
-- ============================================================================
-- ============================================================================
-- TABLE : evenements - Ajout colonnes manquantes
-- ============================================================================
-- Ajouter colonne visible_site (correspond à "Visible sur le site")
ALTER TABLE evenements ADD COLUMN IF NOT EXISTS visible_site BOOLEAN DEFAULT true;
-- S'assurer que region_id existe (ajouté précédemment mais on vérifie)
ALTER TABLE evenements ADD COLUMN IF NOT EXISTS region_id VARCHAR(100);
-- S'assurer que created_by existe (ajouté précédemment mais on vérifie)
ALTER TABLE evenements ADD COLUMN IF NOT EXISTS created_by UUID REFERENCES utilisateurs(id);
-- Commentaires
COMMENT ON COLUMN evenements.visible_site IS 'Indique si l''événement est visible sur le site public';
COMMENT ON COLUMN evenements.region_id IS 'Identifiant de la région concernée par l''événement';
COMMENT ON COLUMN evenements.created_by IS 'Utilisateur ayant créé l''événement';
COMMENT ON COLUMN evenements.titre IS 'Titre de l''événement';
COMMENT ON COLUMN evenements.description IS 'Description détaillée de l''événement';
-- Index supplémentaires
CREATE INDEX IF NOT EXISTS idx_evenements_visible_site ON evenements(visible_site) WHERE deleted_at IS NULL;
-- ============================================================================
-- TABLE : actualites - Ajout colonnes manquantes
-- ============================================================================
-- Renommer image_url en image_couverture pour plus de clarté
-- Note: PostgreSQL ne permet pas de renommer facilement, on ajoute une nouvelle colonne
ALTER TABLE actualites ADD COLUMN IF NOT EXISTS image_couverture VARCHAR(500);
-- Copier les données de image_url vers image_couverture si image_url existe
UPDATE actualites SET image_couverture = image_url WHERE image_couverture IS NULL AND image_url IS NOT NULL;
-- S'assurer que region_id existe
ALTER TABLE actualites ADD COLUMN IF NOT EXISTS region_id VARCHAR(100);
-- S'assurer que created_by existe
ALTER TABLE actualites ADD COLUMN IF NOT EXISTS created_by UUID REFERENCES utilisateurs(id);
-- Commentaires
COMMENT ON COLUMN actualites.image_couverture IS 'URL de l''image de couverture de l''actualité';
COMMENT ON COLUMN actualites.region_id IS 'Identifiant de la région concernée par l''actualité';
COMMENT ON COLUMN actualites.created_by IS 'Utilisateur ayant créé l''actualité';
COMMENT ON COLUMN actualites.titre IS 'Titre de l''actualité';
COMMENT ON COLUMN actualites.contenu IS 'Contenu complet de l''actualité (peut être HTML)';
COMMENT ON COLUMN actualites.resume IS 'Résumé court de l''actualité';
-- ============================================================================
-- VUES : Amélioration des vues existantes
-- ============================================================================
-- Vue améliorée pour les événements
CREATE OR REPLACE VIEW v_evenements_complets AS
SELECT
e.*,
u.nom as createur_nom,
u.prenom as createur_prenom,
u.email as createur_email,
g.nom as gare_nom,
g.ville as gare_ville
FROM evenements e
LEFT JOIN utilisateurs u ON e.created_by = u.id
LEFT JOIN gares g ON e.gare_id = g.id
WHERE e.deleted_at IS NULL
ORDER BY e.date_debut DESC;
-- Vue améliorée pour les actualités
CREATE OR REPLACE VIEW v_actualites_completes AS
SELECT
a.*,
u.nom as createur_nom,
u.prenom as createur_prenom,
u.email as createur_email
FROM actualites a
LEFT JOIN utilisateurs u ON a.created_by = u.id
WHERE a.deleted_at IS NULL
ORDER BY a.date_publication DESC;
-- ============================================================================
-- DONNÉES DE TEST (optionnel - à décommenter si besoin)
-- ============================================================================
/*
-- Exemple d'événement
INSERT INTO evenements (titre, description, lieu, date_debut, date_fin, visible_site, region_id, actif)
VALUES (
'Portes ouvertes Gare de Lyon',
'Venez découvrir les coulisses de la gare de Lyon lors de nos portes ouvertes exceptionnelles.',
'Gare de Lyon - Paris',
NOW() + INTERVAL '7 days',
NOW() + INTERVAL '7 days' + INTERVAL '8 hours',
true,
'ile-de-france',
true
);
-- Exemple d'actualité
INSERT INTO actualites (titre, contenu, resume, image_couverture, region_id, actif)
VALUES (
'Nouvelle ligne TER en service',
'Une nouvelle ligne TER relie désormais Paris à Orléans avec des arrêts supplémentaires pour faciliter vos déplacements quotidiens. Cette ligne propose 20 allers-retours par jour...',
'Mise en service d''une nouvelle ligne TER Paris-Orléans',
'/images/actualites/ter-paris-orleans.jpg',
'ile-de-france',
true
);
*/
-- ============================================================================
-- NETTOYAGE : Migration de l'ancienne colonne image_url (optionnel)
-- ============================================================================
/*
-- À exécuter APRÈS avoir vérifié que toutes les données sont migrées
-- ALTER TABLE actualites DROP COLUMN IF EXISTS image_url;
*/
-- ============================================================================
-- PERMISSIONS RLS (à adapter selon vos besoins)
-- ============================================================================
-- Activer RLS sur les tables (optionnel - à adapter selon votre architecture Auth)
-- ALTER TABLE evenements ENABLE ROW LEVEL SECURITY;
-- ALTER TABLE actualites ENABLE ROW LEVEL SECURITY;
-- Exemple de policies (à adapter)
/*
-- Tout le monde peut lire les événements visibles
CREATE POLICY "Les événements visibles sont publics"
ON evenements FOR SELECT
USING (visible_site = true AND actif = true AND deleted_at IS NULL);
-- Seuls les créateurs et admins peuvent modifier
CREATE POLICY "Les créateurs peuvent modifier leurs événements"
ON evenements FOR UPDATE
USING (created_by = auth.uid() OR auth.jwt() -> 'user_metadata' ->> 'role' = 'admin');
-- Tout le monde peut lire les actualités actives
CREATE POLICY "Les actualités actives sont publiques"
ON actualites FOR SELECT
USING (actif = true AND deleted_at IS NULL);
-- Seuls les créateurs et admins peuvent modifier
CREATE POLICY "Les créateurs peuvent modifier leurs actualités"
ON actualites FOR UPDATE
USING (created_by = auth.uid() OR auth.jwt() -> 'user_metadata' ->> 'role' = 'admin');
*/
-- Création de la table regles_quais
CREATE TABLE IF NOT EXISTS public.regles_quais (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
gare_id UUID NOT NULL REFERENCES public.gares(id) ON DELETE CASCADE,
ligne_id UUID REFERENCES public.lignes(id) ON DELETE CASCADE,
type_regle TEXT NOT NULL CHECK (type_regle IN ('INTERDIT', 'PREFERENTIEL')),
quais_concernes JSONB NOT NULL DEFAULT '[]'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Index pour la recherche par gare
CREATE INDEX IF NOT EXISTS idx_regles_quais_gare ON public.regles_quais(gare_id);
-- Migration : Ajout de la colonne fh_url à la table lignes
-- Stocke l'URL publique du PDF de la fiche horaire associée à la ligne
ALTER TABLE lignes
ADD COLUMN IF NOT EXISTS fh_url TEXT DEFAULT NULL;
COMMENT ON COLUMN lignes.fh_url IS 'URL publique du fichier PDF de la fiche horaire de la ligne (stocké dans Supabase Storage)';
-- Migration: create table horaires_substitutions
-- Cette table est utilisée par l'API front-end (src/api/horairesSubstitutions.js)
CREATE TABLE IF NOT EXISTS horaires_substitutions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Référence éventuelle vers un horaire source
horaire_id UUID REFERENCES horaires(id) ON DELETE SET NULL,
-- Région (filtrage RBAC)
region_id UUID,
region VARCHAR(255),
-- Informations générales (copie similaire à `horaires`)
numero_train VARCHAR(50),
type_train VARCHAR(100),
type_train_id BIGINT,
gare_depart_id UUID,
gare_depart_nom VARCHAR(255),
heure_depart TIME,
gare_arrivee_id UUID,
gare_arrivee_nom VARCHAR(255),
heure_arrivee TIME,
-- Période de validité de la substitution
date_debut TIMESTAMP WITH TIME ZONE,
date_fin TIMESTAMP WITH TIME ZONE,
-- Jours / exceptions
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,
ligne_id UUID,
ligne_nom VARCHAR(255),
service_annuel_id UUID,
service_annuel_nom VARCHAR(255),
-- Indique qu'il s'agit d'une substitution
est_substitution BOOLEAN DEFAULT true,
motif_substitution VARCHAR(255),
-- Snapshot des gares desservies et composition
gares_desservies JSONB DEFAULT '[]'::jsonb,
composition_train JSONB DEFAULT '[]'::jsonb,
-- Métadonnées
actif BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Indexes utiles
CREATE INDEX IF NOT EXISTS idx_horaires_substitutions_region ON horaires_substitutions(region_id);
CREATE INDEX IF NOT EXISTS idx_horaires_substitutions_horaire ON horaires_substitutions(horaire_id);
CREATE INDEX IF NOT EXISTS idx_horaires_substitutions_actif ON horaires_substitutions(actif) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_horaires_substitutions_datefin ON horaires_substitutions(date_fin);
-- Trigger pour updated_at si la fonction existe
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'update_updated_at_column') THEN
DROP TRIGGER IF EXISTS update_horaires_substitutions_updated_at ON horaires_substitutions;
CREATE TRIGGER update_horaires_substitutions_updated_at
BEFORE UPDATE ON horaires_substitutions
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
END IF;
END$$;
COMMENT ON TABLE horaires_substitutions IS 'Table des horaires de substitution (autocar / remplacement)';
COMMENT ON COLUMN horaires_substitutions.gares_desservies IS 'Liste JSON des gares desservies par la substitution';
COMMENT ON COLUMN horaires_substitutions.composition_train IS 'Composition JSON du matériel roulant (position, id, nom)';
COMMENT ON COLUMN horaires_substitutions.date_fin IS 'Date/heure de fin de validité de la substitution';
-- ============================================================================
-- MIGRATION : Ajout de la colonne composition_train (JSONB) dans la table horaires
-- Date : 2026-03-01
-- Description : Stocke la composition complète et ordonnée des matériels roulants
-- affectés à un horaire (sillon). Chaque élément contient l'id,
-- le nom, le nom technique, l'URL de l'image et la position.
--
-- ⚠️ À EXÉCUTER dans l'éditeur SQL Supabase :
-- https://supabase.com/dashboard → votre projet → SQL Editor
-- ============================================================================
ALTER TABLE public.horaires
ADD COLUMN IF NOT EXISTS composition_train JSONB DEFAULT '[]'::jsonb;
COMMENT ON COLUMN public.horaires.composition_train IS
'Composition ordonnée du train : liste JSON des matériels roulants affectés au sillon. '
'Format : [{"materiel_id": 1, "materiel_nom": "Régiolis", "materiel_nom_technique": "X73500", "materiel_image_url": "https://...", "position": 1}, ...]';
-- ============================================================================
-- MIGRATION : Ajout de la colonne informations (JSONB) dans la table gares
-- Date : 2026-03-01
-- Description : Stocke toutes les informations enrichies d'une gare :
-- adresse, horaires, accessibilité, services, voiture, vélo,
-- et lignes de transports en commun (bus, métro, tramway, car régional).
--
-- ⚠️ À EXÉCUTER dans l'éditeur SQL Supabase :
-- https://supabase.com/dashboard → votre projet → SQL Editor
-- ============================================================================
ALTER TABLE public.gares
ADD COLUMN IF NOT EXISTS informations JSONB DEFAULT '{}'::jsonb;
COMMENT ON COLUMN public.gares.informations IS
'Informations enrichies de la gare au format JSON. Structure :
{
-- Informations Générales
"adresse": "20 Bd Diderot, 75012 Paris",
"horaires_guichet": "6h00 - 21h30",
"horaires_gare": "5h00 - 00h30",
"distributeur_ter": true,
-- Accessibilité
"service_assistance": true,
-- Services
"espace_attente": true,
"distributeur_tgv": false,
"objets_trouves": true,
"toilettes": true,
"wifi_gratuit": true,
"photomaton": false,
"defibrillateur": true,
"galeries_marchandes": false,
-- Voiture
"depose_minute": true,
"espace_taxis": true,
"espace_vtc": false,
"location_voiture": false,
"parking": true,
-- Vélo
"velo_libre_service": true,
"parking_velos": true,
-- Transports en commun enrichis
"lignes_bus": [{"nom": "29", "terminus_a": "Gare Saint-Lazare", "terminus_b": "Montempoivre"}],
"lignes_metro": [{"nom": "1", "terminus_a": "La Défense", "terminus_b": "Château de Vincennes"}],
"lignes_tramway": [],
"lignes_car_regional": []
}';
-- ============================================================================
-- MIGRATION : Ajout de la colonne gares_desservies (JSONB) dans la table horaires
-- Date : 2026-03-01
-- Description : Ajoute une colonne JSONB "gares_desservies" dans la table horaires
-- afin de stocker un snapshot dénormalisé des gares desservies.
-- Cette colonne est alimentée à chaque création/modification d'horaire
-- et est synchronisée avec la table relationnelle horaires_gares_desservies.
-- ============================================================================
-- Ajout de la colonne si elle n'existe pas encore
ALTER TABLE public.horaires
ADD COLUMN IF NOT EXISTS gares_desservies JSONB DEFAULT '[]'::jsonb;
-- Commentaire explicatif sur la colonne
COMMENT ON COLUMN public.horaires.gares_desservies IS
'Snapshot JSON des gares desservies (copie dénormalisée synchronisée avec horaires_gares_desservies). '
'Format : [{"gare_id": "uuid", "gare_nom": "Nom Gare", "heure_arrivee": "HH:MM", "heure_depart": "HH:MM", "ordre": 1}, ...]';
-- ============================================================================
-- Rétro-remplissage : synchroniser la colonne JSON depuis la table relationnelle
-- pour tous les horaires existants ayant déjà des gares desservies.
-- ============================================================================
UPDATE public.horaires h
SET gares_desservies = (
SELECT COALESCE(
jsonb_agg(
jsonb_build_object(
'gare_id', gd.gare_id,
'gare_nom', gd.gare_nom,
'heure_arrivee', gd.heure_arrivee,
'heure_depart', gd.heure_depart,
'ordre', gd.ordre
) ORDER BY gd.ordre
),
'[]'::jsonb
)
FROM public.horaires_gares_desservies gd
WHERE gd.horaire_id = h.id
)
WHERE EXISTS (
SELECT 1 FROM public.horaires_gares_desservies gd WHERE gd.horaire_id = h.id
);
-- ============================================================================
-- (Optionnel) Trigger pour maintenir automatiquement la colonne JSON à jour
-- à chaque INSERT / UPDATE / DELETE dans horaires_gares_desservies.
-- Décommenter si vous souhaitez une synchronisation automatique côté base.
-- ============================================================================
-- CREATE OR REPLACE FUNCTION public.sync_horaire_gares_desservies_json()
-- RETURNS trigger LANGUAGE plpgsql AS $$
-- DECLARE
-- v_horaire_id UUID;
-- BEGIN
-- IF TG_OP = 'DELETE' THEN
-- v_horaire_id := OLD.horaire_id;
-- ELSE
-- v_horaire_id := NEW.horaire_id;
-- END IF;
--
-- UPDATE public.horaires
-- SET gares_desservies = (
-- SELECT COALESCE(
-- jsonb_agg(
-- jsonb_build_object(
-- 'gare_id', gd.gare_id,
-- 'gare_nom', gd.gare_nom,
-- 'heure_arrivee', gd.heure_arrivee,
-- 'heure_depart', gd.heure_depart,
-- 'ordre', gd.ordre
-- ) ORDER BY gd.ordre
-- ),
-- '[]'::jsonb
-- )
-- FROM public.horaires_gares_desservies gd
-- WHERE gd.horaire_id = v_horaire_id
-- )
-- WHERE id = v_horaire_id;
--
-- RETURN NULL;
-- END;
-- $$;
--
-- DROP TRIGGER IF EXISTS tr_sync_gares_desservies_json ON public.horaires_gares_desservies;
-- CREATE TRIGGER tr_sync_gares_desservies_json
-- AFTER INSERT OR UPDATE OR DELETE ON public.horaires_gares_desservies
-- FOR EACH ROW EXECUTE FUNCTION public.sync_horaire_gares_desservies_json();
-- Migration : ajout de la colonne jours_perturbation à la table perturbations
-- Permet de stocker plusieurs jours uniques ou périodes pour une perturbation
ALTER TABLE perturbations
ADD COLUMN IF NOT EXISTS jours_perturbation JSONB DEFAULT NULL;
COMMENT ON COLUMN perturbations.jours_perturbation IS
'Liste des jours/périodes de perturbation. Format : [{type: "jour"|"periode", date: "YYYY-MM-DD", dateDebut: "YYYY-MM-DD", dateFin: "YYYY-MM-DD"}]';
-- Ajoute un champ JSONB pour stocker les permissions par fonctionnalite.
ALTER TABLE IF EXISTS public.utilisateurs
ADD COLUMN IF NOT EXISTS permissions jsonb NOT NULL DEFAULT '{}'::jsonb;
-- Securise les lignes existantes si la colonne etait nullable.
UPDATE public.utilisateurs
SET permissions = '{}'::jsonb
WHERE permissions IS NULL;
-- ============================================================================
-- MIGRATION : Ajout de la colonne perturbations_arrets (JSONB) dans perturbations
-- Date : 2026-03-01
-- Description : Stocke l'état complet des perturbations par arrêt tel que
-- configuré dans le formulaire de création / modification.
-- Ceci permet au formulaire d'édition de restaurer exactement
-- le même état que le formulaire de création.
--
-- ⚠️ À EXÉCUTER dans l'éditeur SQL Supabase :
-- https://supabase.com/dashboard → votre projet → SQL Editor
-- ============================================================================
ALTER TABLE public.perturbations
ADD COLUMN IF NOT EXISTS perturbations_arrets JSONB DEFAULT NULL;
COMMENT ON COLUMN public.perturbations.perturbations_arrets IS
'Snapshot complet des perturbations par arrêt (état du formulaire). '
'Format : [{
"index": 0,
"gare_nom": "Paris Gare de Lyon",
"heure_arrivee": null,
"heure_depart": "07:30",
"type_perturbation": "retard",
"retard_minutes": 15,
"retard_cumule": 0,
"arret_prolonge_minutes": 0,
"cause_arret": "Incident technique",
"is_nouveau_terminus": false,
"is_nouvelle_origine": false,
"isFirst": true,
"isLast": false,
"manual_retard": true,
"manual_arret_prolonge": false
}, ...]';
-- ============================================================================
-- MIGRATION: Ajout du système de gestion des perturbations
-- Date: 2026-02-06
-- Description: Ajoute les tables nécessaires pour gérer les perturbations
-- affectant les horaires de trains
-- ============================================================================
-- Vérifier que les tables n'existent pas déjà
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'perturbations') THEN
RAISE NOTICE 'La table perturbations existe déjà, migration annulée';
RETURN;
END IF;
END $$;
-- Exécuter le script principal
-- supabase_perturbations_tables.sql a déjà été importé précédemment
-- Vérification post-migration
DO $$
DECLARE
perturbations_count INTEGER;
modifications_count INTEGER;
BEGIN
-- Vérifier que les tables ont été créées
SELECT COUNT(*) INTO perturbations_count
FROM information_schema.tables
WHERE table_name = 'perturbations';
SELECT COUNT(*) INTO modifications_count
FROM information_schema.tables
WHERE table_name = 'perturbations_modifications_parcours';
IF perturbations_count = 1 AND modifications_count = 1 THEN
RAISE NOTICE '✓ Migration réussie : Tables créées avec succès';
ELSE
RAISE EXCEPTION '✗ Échec de la migration : Les tables n''ont pas été créées correctement';
END IF;
-- Afficher les colonnes créées
RAISE NOTICE 'Colonnes de la table perturbations:';
FOR rec IN (
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'perturbations'
ORDER BY ordinal_position
) LOOP
RAISE NOTICE ' - % (%)', rec.column_name, rec.data_type;
END LOOP;
END $$;
-- Test d'insertion (rollback automatique)
DO $$
DECLARE
test_horaire_id UUID;
test_perturbation_id UUID;
BEGIN
-- Récupérer un horaire existant pour le test
SELECT id INTO test_horaire_id FROM horaires LIMIT 1;
IF test_horaire_id IS NULL THEN
RAISE NOTICE 'Aucun horaire disponible pour le test';
RETURN;
END IF;
-- Test d'insertion d'une perturbation
INSERT INTO perturbations (
horaire_id,
type_perturbation,
duree_retard_minutes,
cause,
cause_details,
actif
) VALUES (
test_horaire_id,
'retard',
15,
'incident_technique',
'Test de migration',
false
) RETURNING id INTO test_perturbation_id;
IF test_perturbation_id IS NOT NULL THEN
RAISE NOTICE '✓ Test d''insertion réussi';
-- Supprimer le test
DELETE FROM perturbations WHERE id = test_perturbation_id;
ELSE
RAISE EXCEPTION '✗ Échec du test d''insertion';
END IF;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '✗ Erreur lors du test : %', SQLERRM;
END $$;
-- Afficher un résumé
SELECT
'Migration terminée' as statut,
NOW() as date_migration,
(SELECT COUNT(*) FROM perturbations) as nombre_perturbations,
(SELECT COUNT(*) FROM perturbations_modifications_parcours) as nombre_modifications;
-- ============================================================================
-- ROLLBACK (à utiliser en cas de problème)
-- ============================================================================
-- DROP TABLE IF EXISTS perturbations_modifications_parcours CASCADE;
-- DROP TABLE IF EXISTS perturbations CASCADE;
-- ============================================================================
-- Migration : Ajout du champ "quai" dans horaires_gares_desservies
-- Date : 2026-03-02
-- ============================================================================
-- 1. Ajouter la colonne quai à la table horaires_gares_desservies
ALTER TABLE horaires_gares_desservies
ADD COLUMN IF NOT EXISTS quai VARCHAR(50) DEFAULT NULL;
COMMENT ON COLUMN horaires_gares_desservies.quai IS 'Numéro ou nom du quai attribué à cet arrêt (ex: "Voie 3", "Quai A")';
-- 2. Mettre à jour l'index pour accélérer les recherches par gare_id
CREATE INDEX IF NOT EXISTS idx_hgd_gare_id ON horaires_gares_desservies(gare_id);
CREATE INDEX IF NOT EXISTS idx_hgd_horaire_id ON horaires_gares_desservies(horaire_id);
-- Note : Le snapshot JSONB horaires.gares_desservies sera mis à jour
-- automatiquement via l'application lors de la sauvegarde des quais.
-- ============================================================================
-- MIGRATION : Ajout de la colonne quais_hall (JSONB) dans la table gares
-- Date : 2026-06-10
-- Description : Stocke l'association entre les halls et les quais au format JSON.
-- Structure :
-- {
-- "hall": {
-- "Nom du hall": ["Voie A", "Voie B"]
-- }
-- }
--
-- ⚠️ À EXÉCUTER dans l'éditeur SQL Supabase :
-- https://supabase.com/dashboard → votre projet → SQL Editor
-- ============================================================================
ALTER TABLE public.gares
ADD COLUMN IF NOT EXISTS quais_hall JSONB DEFAULT '{"hall": {}}'::jsonb;
COMMENT ON COLUMN public.gares.quais_hall IS
'Association des quais aux halls sous forme JSON : {"hall": {"Nom du Hall": ["Quai 1", "Quai 2"]}}';
-- Ajout du champ region_id (slug) à la table menus_regionaux
ALTER TABLE menus_regionaux ADD COLUMN IF NOT EXISTS region_id VARCHAR(64);
-- Index pour accélérer la recherche par région
CREATE INDEX IF NOT EXISTS idx_menus_regionaux_region_id ON menus_regionaux(region_id);
-- Migration: Ajouter la colonne "region" à la table lignes
-- et créer la table "parametres" pour les paramètres du site
-- 1. Ajout de la colonne region à la table lignes
ALTER TABLE lignes ADD COLUMN IF NOT EXISTS region VARCHAR(100);
-- 2. Création de la table parametres
CREATE TABLE IF NOT EXISTS parametres (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
cle VARCHAR(100) NOT NULL UNIQUE,
valeur JSONB,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Insérer les paramètres par défaut
INSERT INTO parametres (cle, valeur) VALUES
('general', '{"nom_site": "", "description_site": "", "region": "", "logo_header_url": ""}'),
('tarifs', '{"billets": [], "abonnements": [], "remises": []}')
ON CONFLICT (cle) DO NOTHING;
-- 3. Ajout de la colonne quai_defaut à horaires_gares_desservies (pour attribution des quais)
ALTER TABLE horaires_gares_desservies ADD COLUMN IF NOT EXISTS quai_id UUID REFERENCES gare_quais(id);
-- 4. Politique RLS pour parametres (accès complet pour les utilisateurs authentifiés)
ALTER TABLE parametres ENABLE ROW LEVEL SECURITY;
CREATE POLICY "parametres_select_all" ON parametres FOR SELECT USING (true);
CREATE POLICY "parametres_insert_all" ON parametres FOR INSERT WITH CHECK (true);
CREATE POLICY "parametres_update_all" ON parametres FOR UPDATE USING (true);
CREATE POLICY "parametres_delete_all" ON parametres FOR DELETE USING (true);
ALTER TABLE tarifs
ADD COLUMN IF NOT EXISTS details JSONB;
-- Migration : table menus_regionaux
CREATE TABLE IF NOT EXISTS menus_regionaux (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nom VARCHAR(200) NOT NULL,
description TEXT,
actif BOOLEAN DEFAULT false,
structure JSONB DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- RLS
ALTER TABLE menus_regionaux ENABLE ROW LEVEL SECURITY;
CREATE POLICY "menus_select" ON menus_regionaux FOR SELECT USING (true);
CREATE POLICY "menus_insert" ON menus_regionaux FOR INSERT WITH CHECK (true);
CREATE POLICY "menus_update" ON menus_regionaux FOR UPDATE USING (true);
CREATE POLICY "menus_delete" ON menus_regionaux FOR DELETE USING (true);
-- Tables de messagerie interne (V1)
CREATE TABLE IF NOT EXISTS public.messagerie_threads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS public.messagerie_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
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 TABLE IF NOT EXISTS public.messagerie_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
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_participants_user_id ON public.messagerie_participants(user_id);
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);
-- RLS simple: participant du thread uniquement
ALTER TABLE public.messagerie_threads ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.messagerie_participants ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.messagerie_messages ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS messagerie_threads_select ON public.messagerie_threads;
CREATE POLICY messagerie_threads_select ON public.messagerie_threads
FOR SELECT TO anon
USING (
EXISTS (
SELECT 1 FROM public.messagerie_participants p
WHERE p.thread_id = messagerie_threads.id
)
);
DROP POLICY IF EXISTS messagerie_threads_insert ON public.messagerie_threads;
CREATE POLICY messagerie_threads_insert ON public.messagerie_threads
FOR INSERT TO anon
WITH CHECK (true);
DROP POLICY IF EXISTS messagerie_threads_update ON public.messagerie_threads;
CREATE POLICY messagerie_threads_update ON public.messagerie_threads
FOR UPDATE TO anon
USING (
EXISTS (
SELECT 1 FROM public.messagerie_participants p
WHERE p.thread_id = messagerie_threads.id
)
)
WITH CHECK (true);
DROP POLICY IF EXISTS messagerie_participants_select ON public.messagerie_participants;
CREATE POLICY messagerie_participants_select ON public.messagerie_participants
FOR SELECT TO anon
USING (true);
DROP POLICY IF EXISTS messagerie_participants_insert ON public.messagerie_participants;
CREATE POLICY messagerie_participants_insert ON public.messagerie_participants
FOR INSERT TO anon
WITH CHECK (true);
DROP POLICY IF EXISTS messagerie_messages_select ON public.messagerie_messages;
CREATE POLICY messagerie_messages_select ON public.messagerie_messages
FOR SELECT TO anon
USING (
EXISTS (
SELECT 1 FROM public.messagerie_participants p
WHERE p.thread_id = messagerie_messages.thread_id
)
);
DROP POLICY IF EXISTS messagerie_messages_insert ON public.messagerie_messages;
CREATE POLICY messagerie_messages_insert ON public.messagerie_messages
FOR INSERT TO anon
WITH CHECK (
EXISTS (
SELECT 1 FROM public.messagerie_participants p
WHERE p.thread_id = messagerie_messages.thread_id
AND p.user_id = messagerie_messages.sender_id
)
);
-- ============================================================================
-- MIGRATION : Création du bucket Storage "materiel-roulant"
-- Date : 2026-03-01
-- Description : Crée le bucket public "materiel-roulant" dans Supabase Storage
-- et configure les policies RLS.
--
-- ⚠️ IMPORTANT — Authentification custom :
-- Cette application utilise une auth custom (RPC authenticate_user + localStorage)
-- et NON supabase.auth.signInWithPassword(). Par conséquent auth.uid() est
-- toujours NULL côté Supabase. Les policies utilisent donc le rôle "anon"
-- (clé publique anonyme) pour autoriser les opérations Storage depuis le front.
--
-- ⚠️ À EXÉCUTER UNE SEULE FOIS dans l'éditeur SQL de votre projet Supabase
-- (https://supabase.com/dashboard → votre projet → SQL Editor)
-- ============================================================================
-- 1. Créer le bucket s'il n'existe pas encore
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 NOTHING;
-- ============================================================================
-- 2. Policies RLS sur storage.objects
-- ============================================================================
-- Lecture publique
DROP POLICY IF EXISTS "materiel-roulant - lecture publique" ON storage.objects;
CREATE POLICY "materiel-roulant - lecture publique"
ON storage.objects FOR SELECT
USING (bucket_id = 'materiel-roulant');
-- Upload (anon car auth custom — auth.uid() toujours NULL)
DROP POLICY IF EXISTS "materiel-roulant - upload authentifié" ON storage.objects;
CREATE POLICY "materiel-roulant - upload authentifié"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'materiel-roulant');
-- Mise à jour
DROP POLICY IF EXISTS "materiel-roulant - update authentifié" ON storage.objects;
CREATE POLICY "materiel-roulant - update authentifié"
ON storage.objects FOR UPDATE
USING (bucket_id = 'materiel-roulant');
-- Suppression
DROP POLICY IF EXISTS "materiel-roulant - delete authentifié" ON storage.objects;
CREATE POLICY "materiel-roulant - delete authentifié"
ON storage.objects FOR DELETE
USING (bucket_id = 'materiel-roulant');
-- Création de la table "tarifs" pour gérer les offres de billets, abonnements, remises par région
CREATE TABLE IF NOT EXISTS tarifs (
id SERIAL PRIMARY KEY,
nom VARCHAR(255) NOT NULL,
type VARCHAR(50) NOT NULL, -- billet, abonnement, remise
description TEXT,
prix_minimum NUMERIC(10,2),
prix_par_km NUMERIC(10,3),
age_maximum INTEGER,
regions TEXT[] NOT NULL, -- tableau d'identifiants de régions
date_debut DATE,
date_fin DATE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Index pour recherche rapide par région
CREATE INDEX IF NOT EXISTS idx_tarifs_regions ON tarifs USING GIN (regions);
-- v2: add trigger to keep updated_at in sync
CREATE OR REPLACE FUNCTION touch_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_touch_tarifs_updated_at ON tarifs;
CREATE TRIGGER trg_touch_tarifs_updated_at
BEFORE UPDATE ON tarifs
FOR EACH ROW
EXECUTE PROCEDURE touch_updated_at();
-- ============================================================================
-- CRÉATION DES TABLES POUR LA GESTION DES TRAVAUX
-- ============================================================================
-- Table principale des travaux
CREATE TABLE IF NOT EXISTS travaux (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
titre TEXT NOT NULL,
description TEXT,
date_debut DATE NOT NULL,
date_fin DATE NOT NULL,
jours_travaux TEXT[] NOT NULL, -- ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']
heure_debut TIME NOT NULL,
heure_fin TIME NOT NULL,
actif BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT valid_dates CHECK (date_fin >= date_debut),
CONSTRAINT valid_hours CHECK (heure_fin > heure_debut)
);
-- Table de liaison entre travaux et sillons de substitution
CREATE TABLE IF NOT EXISTS travaux_substitutions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
travaux_id UUID NOT NULL REFERENCES travaux(id) ON DELETE CASCADE,
horaire_id UUID NOT NULL REFERENCES horaires(id) ON DELETE CASCADE,
ordre INTEGER DEFAULT 0, -- Ordre d'affichage des substitutions
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
UNIQUE(travaux_id, horaire_id)
);
-- Index pour améliorer les performances
CREATE INDEX IF NOT EXISTS idx_travaux_dates ON travaux(date_debut, date_fin);
CREATE INDEX IF NOT EXISTS idx_travaux_actif ON travaux(actif);
CREATE INDEX IF NOT EXISTS idx_travaux_deleted ON travaux(deleted_at);
CREATE INDEX IF NOT EXISTS idx_travaux_substitutions_travaux ON travaux_substitutions(travaux_id);
CREATE INDEX IF NOT EXISTS idx_travaux_substitutions_horaire ON travaux_substitutions(horaire_id);
-- Fonction pour mettre à jour updated_at automatiquement
CREATE OR REPLACE FUNCTION update_travaux_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger pour mettre à jour updated_at
DROP TRIGGER IF EXISTS trigger_update_travaux_updated_at ON travaux;
CREATE TRIGGER trigger_update_travaux_updated_at
BEFORE UPDATE ON travaux
FOR EACH ROW
EXECUTE FUNCTION update_travaux_updated_at();
-- Commentaires sur les tables
COMMENT ON TABLE travaux IS 'Table principale pour la gestion des travaux sur les lignes';
COMMENT ON TABLE travaux_substitutions IS 'Table de liaison entre travaux et horaires de substitution';
COMMENT ON COLUMN travaux.jours_travaux IS 'Jours de la semaine où les travaux ont lieu';
COMMENT ON COLUMN travaux.actif IS 'Indique si les travaux sont actifs';
COMMENT ON COLUMN travaux.deleted_at IS 'Date de suppression (soft delete)';
COMMENT ON COLUMN travaux_substitutions.ordre IS 'Ordre d''affichage des substitutions';
-- ============================================================================
-- DONNÉES DE TEST (optionnel)
-- ============================================================================
-- Exemple de travaux
INSERT INTO travaux (titre, description, date_debut, date_fin, jours_travaux, heure_debut, heure_fin, actif)
VALUES
('Travaux ligne Paris-Lyon', 'Rénovation des voies', '2026-03-01', '2026-03-31', ARRAY['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi'], '09:00', '17:00', true),
('Maintenance gare de Lyon', 'Travaux de maintenance planifiés', '2026-04-15', '2026-04-20', ARRAY['samedi', 'dimanche'], '06:00', '22:00', true)
ON CONFLICT DO NOTHING;
-- Afficher le résultat
SELECT
'Tables travaux créées avec succès' as statut,
(SELECT COUNT(*) FROM travaux) as nombre_travaux,
(SELECT COUNT(*) FROM travaux_substitutions) as nombre_substitutions;
-- ============================================================================
-- CORRECTIF : Policies RLS du bucket "banque-annonces" et des tables SQL
-- Date : 2026-07-02
--
-- Problème : L'application utilise la clé anon et une authentification custom. Les
-- policies de banque_mots et de storage.objects qui exigent un utilisateur
-- authentifié (TO authenticated) bloquent l'INSERT/UPDATE/DELETE et provoquent
-- l'erreur "new row violates row-level security policy".
--
-- Solution : Recréer des policies pour permettre SELECT, INSERT, UPDATE, DELETE
-- sur le bucket 'banque-annonces' et sur les tables de la base de données.
--
-- ⚠️ À EXÉCUTER dans l'éditeur SQL Supabase :
-- https://app.supabase.com → votre projet → SQL Editor
-- ============================================================================
-- 1. Réglage des politiques RLS pour les tables de base de données
-- ============================================================================
-- Table: banque_mots
DROP POLICY IF EXISTS "Authenticated users can read banque_mots" ON public.banque_mots;
DROP POLICY IF EXISTS "Authenticated users can insert banque_mots" ON public.banque_mots;
DROP POLICY IF EXISTS "Authenticated users can update banque_mots" ON public.banque_mots;
CREATE POLICY "Public read banque_mots" ON public.banque_mots FOR SELECT USING (deleted_at IS NULL);
CREATE POLICY "Public insert banque_mots" ON public.banque_mots FOR INSERT WITH CHECK (true);
CREATE POLICY "Public update banque_mots" ON public.banque_mots FOR UPDATE USING (true);
CREATE POLICY "Public delete banque_mots" ON public.banque_mots FOR DELETE USING (true);
-- Table: annonces_templates
DROP POLICY IF EXISTS "Authenticated users can read templates" ON public.annonces_templates;
DROP POLICY IF EXISTS "Authenticated users can insert templates" ON public.annonces_templates;
DROP POLICY IF EXISTS "Authenticated users can update templates" ON public.annonces_templates;
CREATE POLICY "Public read templates" ON public.annonces_templates FOR SELECT USING (deleted_at IS NULL);
CREATE POLICY "Public insert templates" ON public.annonces_templates FOR INSERT WITH CHECK (true);
CREATE POLICY "Public update templates" ON public.annonces_templates FOR UPDATE USING (true);
CREATE POLICY "Public delete templates" ON public.annonces_templates FOR DELETE USING (true);
-- Table: annonces_enregistrees
DROP POLICY IF EXISTS "Authenticated users can read annonces" ON public.annonces_enregistrees;
DROP POLICY IF EXISTS "Authenticated users can insert annonces" ON public.annonces_enregistrees;
DROP POLICY IF EXISTS "Authenticated users can update annonces" ON public.annonces_enregistrees;
CREATE POLICY "Public read annonces" ON public.annonces_enregistrees FOR SELECT USING (deleted_at IS NULL);
CREATE POLICY "Public insert annonces" ON public.annonces_enregistrees FOR INSERT WITH CHECK (true);
CREATE POLICY "Public update annonces" ON public.annonces_enregistrees FOR UPDATE USING (true);
CREATE POLICY "Public delete annonces" ON public.annonces_enregistrees FOR DELETE USING (true);
-- 2. Réglage des politiques RLS du Bucket Storage "banque-annonces"
-- ============================================================================
-- Supprimer les anciennes politiques storage si elles existent
DROP POLICY IF EXISTS "Public read access on banque-annonces" ON storage.objects;
DROP POLICY IF EXISTS "Authenticated users can upload to banque-annonces" ON storage.objects;
DROP POLICY IF EXISTS "Authenticated users can delete from banque-annonces" ON storage.objects;
DROP POLICY IF EXISTS "Public upload access on banque-annonces" ON storage.objects;
DROP POLICY IF EXISTS "Public update access on banque-annonces" ON storage.objects;
DROP POLICY IF EXISTS "Public delete access on banque-annonces" ON storage.objects;
-- Lecture publique pour tous
CREATE POLICY "Public read access on banque-annonces"
ON storage.objects FOR SELECT
USING (bucket_id = 'banque-annonces');
-- Insertion publique (Upload)
CREATE POLICY "Public upload access on banque-annonces"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'banque-annonces');
-- Mise à jour publique (Upsert)
CREATE POLICY "Public update access on banque-annonces"
ON storage.objects FOR UPDATE
USING (bucket_id = 'banque-annonces');
-- Suppression publique (Delete)
CREATE POLICY "Public delete access on banque-annonces"
ON storage.objects FOR DELETE
USING (bucket_id = 'banque-annonces');
-- ============================================================================
-- CORRECTIF : Policies RLS du bucket "fiches-horaires"
-- Date : 2026-03-01
--
-- Problème : L'application utilise la clé anon et une authentification custom. Les
-- policies de storage.objects qui exigent auth.uid() bloquent l'INSERT
-- et provoquent l'erreur "new row violates row-level security policy".
--
-- Solution : Recréer des policies pour permettre l'INSERT/UPDATE/DELETE/SELECT
-- sur le bucket 'fiches-horaires' en se basant sur bucket_id seulement.
--
-- ⚠️ À EXÉCUTER dans l'éditeur SQL Supabase :
-- https://app.supabase.com → votre projet → SQL Editor
-- ============================================================================
-- Supprimer d'abord les policies existantes (si elles existent)
DROP POLICY IF EXISTS "fiches-horaires - lecture publique" ON storage.objects;
DROP POLICY IF EXISTS "fiches-horaires - upload" ON storage.objects;
DROP POLICY IF EXISTS "fiches-horaires - update" ON storage.objects;
DROP POLICY IF EXISTS "fiches-horaires - delete" ON storage.objects;
-- Créer des policies permissives basées sur le bucket_id
CREATE POLICY "fiches-horaires - lecture publique"
ON storage.objects FOR SELECT
USING (bucket_id = 'fiches-horaires');
CREATE POLICY "fiches-horaires - upload"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'fiches-horaires');
CREATE POLICY "fiches-horaires - update"
ON storage.objects FOR UPDATE
USING (bucket_id = 'fiches-horaires');
CREATE POLICY "fiches-horaires - delete"
ON storage.objects FOR DELETE
USING (bucket_id = 'fiches-horaires');
-- Note : Si vous souhaitez restreindre les uploads aux utilisateurs authentifiés,
-- remplacez la clause WITH CHECK (bucket_id = 'fiches-horaires') par
-- WITH CHECK (bucket_id = 'fiches-horaires' AND auth.uid() IS NOT NULL)
-- et assurez-vous que vos utilisateurs sont bien authentifiés côté front.
-- ============================================================================
-- CORRECTIF : Policies RLS du bucket "materiel-roulant"
-- Date : 2026-03-01
--
-- Problème : L'application utilise une authentification custom (RPC authenticate_user
-- + localStorage). supabase.auth.signIn() n'est jamais appelé, donc
-- auth.uid() est toujours NULL → la policy WITH CHECK auth.uid() IS NOT NULL
-- bloque tous les uploads.
--
-- Solution : Policies ouvertes sur le bucket (accès par clé anon publique).
-- Le bucket est déjà créé — ce script corrige uniquement les policies.
--
-- ⚠️ À EXÉCUTER dans l'éditeur SQL Supabase :
-- https://supabase.com/dashboard → votre projet → SQL Editor
-- ============================================================================
-- Supprimer toutes les policies existantes du bucket
DROP POLICY IF EXISTS "materiel-roulant - lecture publique" ON storage.objects;
DROP POLICY IF EXISTS "materiel-roulant - upload authentifié" ON storage.objects;
DROP POLICY IF EXISTS "materiel-roulant - update authentifié" ON storage.objects;
DROP POLICY IF EXISTS "materiel-roulant - delete authentifié" ON storage.objects;
-- Recréer les policies sans condition auth.uid()
-- (l'app utilise la clé anon, auth.uid() est toujours NULL)
CREATE POLICY "materiel-roulant - lecture publique"
ON storage.objects FOR SELECT
USING (bucket_id = 'materiel-roulant');
CREATE POLICY "materiel-roulant - upload authentifié"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'materiel-roulant');
CREATE POLICY "materiel-roulant - update authentifié"
ON storage.objects FOR UPDATE
USING (bucket_id = 'materiel-roulant');
CREATE POLICY "materiel-roulant - delete authentifié"
ON storage.objects FOR DELETE
USING (bucket_id = 'materiel-roulant');
-- Migration: repair foreign key between horaires_gares_desservies.horaire_id and horaires.id
-- This script will:
-- 1) check the column types of horaires.id and horaires_gares_desservies.horaire_id
-- 2) if types match and FK doesn't exist, create the FK constraint
-- 3) otherwise, print a NOTICE explaining the mismatch for manual resolution
DO $$
DECLARE
t_horaire TEXT;
t_horaire_fk TEXT;
cnt INT;
BEGIN
-- ensure target tables exist
IF NOT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'horaires' AND relkind = 'r') THEN
RAISE NOTICE 'Table public.horaires does not exist in this database/schema.';
RETURN;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'horaires_gares_desservies' AND relkind = 'r') THEN
RAISE NOTICE 'Table public.horaires_gares_desservies does not exist in this database/schema.';
RETURN;
END IF;
-- get formatted types
SELECT pg_catalog.format_type(a.atttypid, a.atttypmod)
INTO t_horaire
FROM pg_attribute a
WHERE a.attrelid = 'public.horaires'::regclass AND a.attname = 'id';
SELECT pg_catalog.format_type(a.atttypid, a.atttypmod)
INTO t_horaire_fk
FROM pg_attribute a
WHERE a.attrelid = 'public.horaires_gares_desservies'::regclass AND a.attname = 'horaire_id';
RAISE NOTICE 'Detected types -> horaires.id: %, horaires_gares_desservies.horaire_id: %', t_horaire, t_horaire_fk;
-- check existing FK from horaires_gares_desservies to horaires
SELECT count(*) INTO cnt
FROM pg_constraint c
WHERE c.conrelid = 'public.horaires_gares_desservies'::regclass
AND c.confrelid = 'public.horaires'::regclass
AND c.contype = 'f';
IF cnt > 0 THEN
RAISE NOTICE 'A foreign-key constraint from horaires_gares_desservies to horaires already exists (count=%). No action taken.', cnt;
RETURN;
END IF;
-- If types match, create the FK
IF t_horaire IS NOT NULL AND t_horaire_fk IS NOT NULL AND t_horaire = t_horaire_fk THEN
EXECUTE 'ALTER TABLE public.horaires_gares_desservies
ADD CONSTRAINT fk_horaires_gares_horaire_id
FOREIGN KEY (horaire_id)
REFERENCES public.horaires(id)
ON DELETE CASCADE';
RAISE NOTICE 'Foreign key constraint fk_horaires_gares_horaire_id created successfully.';
ELSE
RAISE NOTICE 'Column types do not match or could not be determined. Manual intervention required.';
RAISE NOTICE 'Detected types -> horaires.id: %, horaires_gares_desservies.horaire_id: %', t_horaire, t_horaire_fk;
RAISE NOTICE 'If both should be UUID, consider converting [or recreating] the columns and then re-run this script.';
RAISE NOTICE 'If both should be BIGINT, consider altering types to bigint. Do not run automatic conversion without backup.';
END IF;
END $$;
-- ============================================================================
-- DONNÉES DE TEST: Perturbations
-- Description: Jeux de données pour tester le système de perturbations
-- ============================================================================
-- Note: Ces données sont à insérer APRÈS avoir des horaires existants dans la base
-- Exemple 1: Retard sur un train
-- Remplacer 'HORAIRE_ID_1' par un véritable ID d'horaire existant
INSERT INTO perturbations (
horaire_id,
type_perturbation,
duree_retard_minutes,
cause,
cause_details,
date_debut,
date_fin,
actif,
notes
) VALUES (
(SELECT id FROM horaires LIMIT 1), -- Premier horaire de la base
'retard',
25,
'incident_technique',
'Panne de signalisation à Paris-Montparnasse',
NOW(),
NOW() + INTERVAL '2 hours',
true,
'Retard estimé de 25 minutes sur l''ensemble du parcours'
);
-- Exemple 2: Suppression de train avec substitution
INSERT INTO perturbations (
horaire_id,
type_perturbation,
duree_retard_minutes,
cause,
cause_details,
sillon_substitution_id,
date_debut,
date_fin,
actif,
notes
) VALUES (
(SELECT id FROM horaires LIMIT 1 OFFSET 1), -- Deuxième horaire
'suppression',
NULL,
'mouvement_social',
'Grève nationale du personnel roulant',
(SELECT id FROM horaires LIMIT 1 OFFSET 2), -- Train de substitution
NOW(),
NOW() + INTERVAL '1 day',
true,
'Train supprimé - Report sur le train suivant disponible'
);
-- Exemple 3: Modification de parcours
DO $$
DECLARE
v_perturbation_id UUID;
v_horaire_id UUID;
BEGIN
-- Récupérer un horaire avec des gares desservies
SELECT h.id INTO v_horaire_id
FROM horaires h
WHERE EXISTS (
SELECT 1 FROM horaires_gares_desservies hgd
WHERE hgd.horaire_id = h.id
)
LIMIT 1;
IF v_horaire_id IS NULL THEN
RAISE NOTICE 'Aucun horaire avec gares desservies trouvé, test ignoré';
RETURN;
END IF;
-- Créer la perturbation
INSERT INTO perturbations (
horaire_id,
type_perturbation,
duree_retard_minutes,
cause,
cause_details,
date_debut,
actif,
notes
) VALUES (
v_horaire_id,
'modification_parcours',
NULL,
'travaux',
'Travaux de maintenance sur la voie entre Lyon et Valence',
NOW(),
true,
'Modification du parcours : certaines gares ne seront pas desservies'
) RETURNING id INTO v_perturbation_id;
-- Ajouter des modifications de parcours
-- Supprimer une gare du parcours
INSERT INTO perturbations_modifications_parcours (
perturbation_id,
type_modification,
gare_id,
gare_nom,
ordre
) SELECT
v_perturbation_id,
'gare_supprimee',
gare_id,
gare_nom,
ordre
FROM horaires_gares_desservies
WHERE horaire_id = v_horaire_id
LIMIT 1;
RAISE NOTICE 'Perturbation de modification de parcours créée avec succès';
END $$;
-- Exemple 4: Retard causé par les conditions météorologiques
INSERT INTO perturbations (
horaire_id,
type_perturbation,
duree_retard_minutes,
cause,
cause_details,
date_debut,
date_fin,
actif,
notes
) VALUES (
(SELECT id FROM horaires LIMIT 1 OFFSET 3),
'retard',
45,
'conditions_meteorologiques',
'Fortes chutes de neige en Auvergne-Rhône-Alpes',
NOW(),
NOW() + INTERVAL '4 hours',
true,
'Circulation ralentie sur l''ensemble des lignes'
);
-- Exemple 5: Perturbation inactive (historique)
INSERT INTO perturbations (
horaire_id,
type_perturbation,
duree_retard_minutes,
cause,
cause_details,
date_debut,
date_fin,
actif,
notes
) VALUES (
(SELECT id FROM horaires LIMIT 1 OFFSET 4),
'retard',
10,
'affluence',
'Affluence exceptionnelle lors du départ',
NOW() - INTERVAL '1 day',
NOW() - INTERVAL '1 day' + INTERVAL '30 minutes',
false,
'Perturbation terminée - Retard résorbé'
);
-- Afficher un résumé des perturbations de test créées
SELECT
'Données de test créées' as statut,
COUNT(*) as nombre_perturbations_test,
COUNT(*) FILTER (WHERE actif = true) as perturbations_actives,
COUNT(*) FILTER (WHERE actif = false) as perturbations_inactives
FROM perturbations
WHERE notes LIKE '%test%' OR notes LIKE '%Test%';
-- Afficher les perturbations créées
SELECT
p.id,
p.type_perturbation,
p.duree_retard_minutes,
p.cause,
p.actif,
h.numero_train,
h.gare_depart_nom,
h.gare_arrivee_nom
FROM perturbations p
JOIN horaires h ON p.horaire_id = h.id
ORDER BY p.created_at DESC
LIMIT 10;
-- ============================================================================
-- NETTOYAGE DES DONNÉES DE TEST
-- ============================================================================
-- Décommenter pour supprimer toutes les données de test
/*
DELETE FROM perturbations_modifications_parcours
WHERE perturbation_id IN (
SELECT id FROM perturbations
WHERE notes LIKE '%test%' OR notes LIKE '%Test%'
);
DELETE FROM perturbations
WHERE notes LIKE '%test%' OR notes LIKE '%Test%';
SELECT 'Données de test supprimées' as statut;
*/
-- MIGRATION : Ajout de la colonne quais_hall (JSONB) dans la table gares
ALTER TABLE public.gares
ADD COLUMN IF NOT EXISTS quais_hall JSONB DEFAULT '{"hall": {}}'::jsonb;
COMMENT ON COLUMN public.gares.quais_hall IS
'Association des quais aux halls sous forme JSON : {"hall": {"Nom du Hall": ["Quai 1", "Quai 2"]}}';
-- Migration: 20260611000000_secure_all_tables.sql
-- Description: Enable RLS on all public tables and restrict INSERT/UPDATE/DELETE to authenticated users.
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'public'
LOOP
-- Enable RLS
EXECUTE 'ALTER TABLE public.' || quote_ident(r.tablename) || ' ENABLE ROW LEVEL SECURITY;';
-- Clean up existing potentially insecure policies to be sure
EXECUTE 'DROP POLICY IF EXISTS "Public read access" ON public.' || quote_ident(r.tablename) || ';';
EXECUTE 'DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON public.' || quote_ident(r.tablename) || ';';
EXECUTE 'DROP POLICY IF EXISTS "Enable update for authenticated users only" ON public.' || quote_ident(r.tablename) || ';';
EXECUTE 'DROP POLICY IF EXISTS "Enable delete for authenticated users only" ON public.' || quote_ident(r.tablename) || ';';
-- Read policy (SELECT): Allow public read
EXECUTE 'CREATE POLICY "Public read access" ON public.' || quote_ident(r.tablename) || ' FOR SELECT USING (true);';
-- Insert policy: Allow anon (app uses custom auth, no Supabase Auth)
EXECUTE 'CREATE POLICY "Public insert access" ON public.' || quote_ident(r.tablename) || ' FOR INSERT WITH CHECK (true);';
-- Update policy: Allow anon
EXECUTE 'CREATE POLICY "Public update access" ON public.' || quote_ident(r.tablename) || ' FOR UPDATE USING (true);';
-- Delete policy: Prevent hard deletion for everyone (prevents third party deletion)
EXECUTE 'CREATE POLICY "Prevent hard deletion" ON public.' || quote_ident(r.tablename) || ' FOR DELETE USING (false);';
END LOOP;
END $$;
-- Migration: 20260611000001_fix_secure_all_tables.sql
-- Description: Fix RLS policies to allow public insert/update since app uses custom auth, but strictly prevent delete.
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'public'
LOOP
-- Clean up the overly restrictive policies from previous migration
EXECUTE 'DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON public.' || quote_ident(r.tablename) || ';';
EXECUTE 'DROP POLICY IF EXISTS "Enable update for authenticated users only" ON public.' || quote_ident(r.tablename) || ';';
EXECUTE 'DROP POLICY IF EXISTS "Enable delete for authenticated users only" ON public.' || quote_ident(r.tablename) || ';';
-- Insert policy: Allow anon (app uses custom auth, no Supabase Auth)
EXECUTE 'DROP POLICY IF EXISTS "Public insert access" ON public.' || quote_ident(r.tablename) || ';';
EXECUTE 'CREATE POLICY "Public insert access" ON public.' || quote_ident(r.tablename) || ' FOR INSERT WITH CHECK (true);';
-- Update policy: Allow anon
EXECUTE 'DROP POLICY IF EXISTS "Public update access" ON public.' || quote_ident(r.tablename) || ';';
EXECUTE 'CREATE POLICY "Public update access" ON public.' || quote_ident(r.tablename) || ' FOR UPDATE USING (true);';
-- Delete policy: Prevent hard deletion for everyone (prevents third party deletion)
EXECUTE 'DROP POLICY IF EXISTS "Prevent hard deletion" ON public.' || quote_ident(r.tablename) || ';';
EXECUTE 'CREATE POLICY "Prevent hard deletion" ON public.' || quote_ident(r.tablename) || ' FOR DELETE USING (false);';
END LOOP;
END $$;
-- ============================================================================
-- MIGRATION : types_train — ajout de la colonne "logo_color_url"
--
-- Ajoute une colonne "logo_color_url" pour stocker l'URL du logo en couleur.
--
-- À exécuter dans : Supabase Dashboard > SQL Editor
-- ============================================================================
ALTER TABLE "public"."types_train"
ADD COLUMN IF NOT EXISTS "logo_color_url" text;
-- ============================================================================
-- MIGRATION : info_gares — gare_ids uuid[] + ligne_ids uuid[]
--
-- Remplace les colonnes scalaires gare_id / ligne_id par des colonnes tableau
-- afin d'autoriser plusieurs gares et plusieurs lignes par info.
--
-- À exécuter dans : Supabase Dashboard > SQL Editor
-- ============================================================================
-- 1. Ajouter les nouvelles colonnes tableaux (nullables, par défaut vide)
ALTER TABLE "public"."info_gares"
ADD COLUMN IF NOT EXISTS "gare_ids" uuid[] NOT NULL DEFAULT '{}',
ADD COLUMN IF NOT EXISTS "ligne_ids" uuid[] NOT NULL DEFAULT '{}';
-- 2. Migrer les données existantes depuis les colonnes scalaires
UPDATE "public"."info_gares"
SET
gare_ids = CASE WHEN gare_id IS NOT NULL THEN ARRAY[gare_id] ELSE '{}' END,
ligne_ids = CASE WHEN ligne_id IS NOT NULL THEN ARRAY[ligne_id] ELSE '{}' END
WHERE deleted_at IS NULL;
-- 3. Supprimer les anciennes colonnes scalaires
-- (décommentez les lignes ci-dessous après avoir vérifié la migration)
-- ALTER TABLE "public"."info_gares" DROP COLUMN IF EXISTS "gare_id";
-- ALTER TABLE "public"."info_gares" DROP COLUMN IF EXISTS "ligne_id";
-- NOTE : les colonnes gare_id / ligne_id sont conservées pour l'instant.
-- Après validation, décommentez le bloc DROP COLUMN ci-dessus et ré-exécutez.
-- 4. Index GIN pour de meilleures performances sur les requêtes @> (contient)
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);
-- ============================================================================
-- MIGRATION : info_gares — ajout de la colonne "type"
--
-- Ajoute une colonne "type" pour catégoriser les infos gares.
-- Les valeurs possibles sont gérées côté applicatif :
-- 'Information', 'Avertissement', 'Alerte', 'Urgence', 'Alerte Méteo'
--
-- À exécuter dans : Supabase Dashboard > SQL Editor
-- ============================================================================
ALTER TABLE "public"."info_gares"
ADD COLUMN IF NOT EXISTS "type" text NOT NULL DEFAULT 'Information';
-- ============================================================================
-- FIX RLS : table info_gares
--
-- La table info_gares a RLS activé mais aucune policy n'est définie,
-- ce qui bloque toutes les opérations (INSERT, UPDATE, DELETE, SELECT).
--
-- Ce script suit le même pattern que les autres tables du projet
-- (menus_regionaux, parametres) : accès ouvert à tous les rôles
-- utilisant la clé anon/service (gestion côté application).
--
-- À exécuter dans : Supabase Dashboard > SQL Editor
-- ============================================================================
-- 1. S'assurer que RLS est bien activé sur la table
ALTER TABLE "public"."info_gares" ENABLE ROW LEVEL SECURITY;
-- 2. Supprimer les éventuelles policies existantes (idempotent)
DROP POLICY IF EXISTS "info_gares_select" ON "public"."info_gares";
DROP POLICY IF EXISTS "info_gares_insert" ON "public"."info_gares";
DROP POLICY IF EXISTS "info_gares_update" ON "public"."info_gares";
DROP POLICY IF EXISTS "info_gares_delete" ON "public"."info_gares";
-- 3. Créer les 4 policies CRUD (lecture, insertion, modification, suppression)
CREATE POLICY "info_gares_select"
ON "public"."info_gares"
FOR SELECT
USING (true);
CREATE POLICY "info_gares_insert"
ON "public"."info_gares"
FOR INSERT
WITH CHECK (true);
CREATE POLICY "info_gares_update"
ON "public"."info_gares"
FOR UPDATE
USING (true);
CREATE POLICY "info_gares_delete"
ON "public"."info_gares"
FOR DELETE
USING (true);