-- ============================================================================ -- CRUCES GLM — SUPABASE COMPLETO -- GomezLee Marketing -- ============================================================================ -- Ejecutar una vez en SQL Editor. -- -- Supabase queda como fuente de verdad para: -- 1) Usuarios autorizados para iniciar sesión en Cruces GLM. -- 2) Permiso para Agregar / Editar / Eliminar formularios. -- 3) Datos de todos los formularios/cruces del Hub. -- 4) Sumatorias globales: formularios creados, aperturas y logueos. -- 5) Métricas por formulario y por usuario. -- 6) Historial de eventos. -- 7) Consultas y mutaciones mediante RPC; el frontend no necesita acceso -- directo a las tablas. -- -- IMPORTANTE: -- Google/Supabase puede completar el OAuth, pero Cruces GLM consulta -- cruces_glm_get_my_access() inmediatamente. Si is_allowed = false o el correo -- no existe en cruces_glm_permissions, la app cierra esa sesión y no permite -- entrar. Todas las RPC de datos vuelven a validar el acceso en servidor. -- ============================================================================ begin; -- -------------------------------------------------------------------------- -- 1. ACCESOS / PERMISOS -- -------------------------------------------------------------------------- create table if not exists public.cruces_glm_permissions ( email text primary key, display_name text, is_allowed boolean not null default true, can_manage_forms boolean not null default false, updated_at timestamptz not null default now(), constraint cruces_glm_permissions_email_lowercase check (email = lower(email)) ); -- Compatibilidad con versiones anteriores del script. alter table public.cruces_glm_permissions add column if not exists display_name text; alter table public.cruces_glm_permissions add column if not exists is_allowed boolean not null default true; alter table public.cruces_glm_permissions add column if not exists can_manage_forms boolean not null default false; alter table public.cruces_glm_permissions add column if not exists updated_at timestamptz not null default now(); alter table public.cruces_glm_permissions enable row level security; create or replace function public.cruces_glm_set_updated_at() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$; drop trigger if exists cruces_glm_permissions_set_updated_at on public.cruces_glm_permissions; create trigger cruces_glm_permissions_set_updated_at before update on public.cruces_glm_permissions for each row execute function public.cruces_glm_set_updated_at(); -- Lista inicial autorizada. Solo Isaac administra formularios inicialmente. -- ON CONFLICT preserva is_allowed/can_manage_forms para no deshacer cambios -- manuales posteriores si actualizas solamente nombres. insert into public.cruces_glm_permissions ( email, display_name, is_allowed, can_manage_forms ) values ('mgomez@gomezleemarketing.com', 'Máximo Gómez', true, false), ('iaracena@gomezleemarketing.com', 'Isaac Aracena', true, true), ('jgomez@gomezleemarketing.com', 'José Leopoldo Gómez', true, false), ('lmatos@gomezleemarketing.com', 'Luis Matos', true, false), ('ymadera@gomezleemarketing.com', 'Yanelly Madera', true, false), ('iherrera@gomezleemarketing.com', 'Iveth Herrera', true, false), ('msoto@gomezleemarketing.com', 'Mati Soto', true, false), ('vparamo@gomezleemarketing.com', 'Viviana Páramo', true, false), ('asrodriguez@gomezleemarketing.com', 'Ada Rodríguez', true, false), ('administrativonicaragua@gomezleemarketing.com', 'Estefani Dayana Orozco Dávila', true, false), ('administrativo@gomezleemarketing.com', 'Seyling Zambrana', true, false), ('adminregional@gomezleemarketing.com', 'Liliana Benítez', true, false) on conflict (email) do update set display_name = excluded.display_name; -- -------------------------------------------------------------------------- -- 2. FORMULARIOS / CRUCES — FUENTE DE VERDAD -- -------------------------------------------------------------------------- create table if not exists public.cruces_glm_forms ( id text primary key, nombre text not null, descripcion text not null, categoria text not null, url text not null, activo boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), created_by text, updated_by text, constraint cruces_glm_forms_categoria_check check (categoria in ('Seguridad Social', 'IR', 'Reportes')), constraint cruces_glm_forms_nombre_not_blank check (length(trim(nombre)) > 0), constraint cruces_glm_forms_descripcion_not_blank check (length(trim(descripcion)) > 0), constraint cruces_glm_forms_url_not_blank check (length(trim(url)) > 0) ); create index if not exists cruces_glm_forms_categoria_idx on public.cruces_glm_forms (categoria, nombre); create index if not exists cruces_glm_forms_activo_idx on public.cruces_glm_forms (activo, categoria); alter table public.cruces_glm_forms enable row level security; drop trigger if exists cruces_glm_forms_set_updated_at on public.cruces_glm_forms; create trigger cruces_glm_forms_set_updated_at before update on public.cruces_glm_forms for each row execute function public.cruces_glm_set_updated_at(); -- -------------------------------------------------------------------------- -- 3. MÉTRICAS GLOBALES -- -------------------------------------------------------------------------- create table if not exists public.cruces_glm_metrics_summary ( id smallint primary key default 1, total_formularios_colocados bigint not null default 0 check (total_formularios_colocados >= 0), total_formularios_abiertos bigint not null default 0 check (total_formularios_abiertos >= 0), total_logueos bigint not null default 0 check (total_logueos >= 0), updated_at timestamptz not null default now(), constraint cruces_glm_metrics_summary_single_row check (id = 1) ); insert into public.cruces_glm_metrics_summary (id) values (1) on conflict (id) do nothing; alter table public.cruces_glm_metrics_summary enable row level security; -- -------------------------------------------------------------------------- -- 4. MÉTRICAS POR FORMULARIO -- -------------------------------------------------------------------------- create table if not exists public.cruces_glm_form_metrics ( form_id text primary key, form_name text not null, category text, total_publicaciones bigint not null default 1 check (total_publicaciones >= 0), total_aperturas bigint not null default 0 check (total_aperturas >= 0), first_published_at timestamptz not null default now(), last_published_at timestamptz not null default now(), last_opened_at timestamptz ); alter table public.cruces_glm_form_metrics enable row level security; -- -------------------------------------------------------------------------- -- 5. MÉTRICAS POR USUARIO -- -------------------------------------------------------------------------- create table if not exists public.cruces_glm_user_metrics ( email text primary key, total_logueos bigint not null default 0 check (total_logueos >= 0), total_aperturas bigint not null default 0 check (total_aperturas >= 0), last_login_at timestamptz, last_opened_at timestamptz, constraint cruces_glm_user_metrics_email_lowercase check (email = lower(email)) ); alter table public.cruces_glm_user_metrics enable row level security; -- -------------------------------------------------------------------------- -- 6. HISTORIAL DE EVENTOS -- -------------------------------------------------------------------------- create table if not exists public.cruces_glm_metric_events ( id bigint generated by default as identity primary key, event_type text not null check (event_type in ('login', 'form_created', 'form_opened')), user_email text not null, form_id text, form_name text, category text, created_at timestamptz not null default now() ); create index if not exists cruces_glm_metric_events_type_idx on public.cruces_glm_metric_events (event_type, created_at desc); create index if not exists cruces_glm_metric_events_user_idx on public.cruces_glm_metric_events (user_email, created_at desc); create index if not exists cruces_glm_metric_events_form_idx on public.cruces_glm_metric_events (form_id, created_at desc); alter table public.cruces_glm_metric_events enable row level security; -- -------------------------------------------------------------------------- -- 7. SEGURIDAD BASE — SIN ACCESO DIRECTO DESDE EL FRONTEND -- -------------------------------------------------------------------------- -- El navegador usa exclusivamente RPCs SECURITY DEFINER. Esto evita depender -- de filtros construidos por el cliente para permisos o CRUD. revoke all on table public.cruces_glm_permissions from anon, authenticated; revoke all on table public.cruces_glm_forms from anon, authenticated; revoke all on table public.cruces_glm_metrics_summary from anon, authenticated; revoke all on table public.cruces_glm_form_metrics from anon, authenticated; revoke all on table public.cruces_glm_user_metrics from anon, authenticated; revoke all on table public.cruces_glm_metric_events from anon, authenticated; -- Quitar policies antiguas conocidas; el acceso normal será solo vía RPC. drop policy if exists "Cruces GLM - leer permiso propio" on public.cruces_glm_permissions; drop policy if exists "Cruces GLM - administradores leen resumen" on public.cruces_glm_metrics_summary; drop policy if exists "Cruces GLM - administradores leen metricas formularios" on public.cruces_glm_form_metrics; drop policy if exists "Cruces GLM - administradores leen metricas usuarios" on public.cruces_glm_user_metrics; drop policy if exists "Cruces GLM - administradores leen eventos" on public.cruces_glm_metric_events; -- -------------------------------------------------------------------------- -- 8. HELPERS DE AUTORIZACIÓN -- -------------------------------------------------------------------------- create or replace function public.cruces_glm_current_email() returns text language sql stable as $$ select lower(coalesce(auth.jwt() ->> 'email', '')); $$; create or replace function public.cruces_glm_is_allowed() returns boolean language sql stable security definer set search_path = public as $$ select exists ( select 1 from public.cruces_glm_permissions p where p.email = public.cruces_glm_current_email() and p.is_allowed = true ); $$; create or replace function public.cruces_glm_can_manage_forms() returns boolean language sql stable security definer set search_path = public as $$ select exists ( select 1 from public.cruces_glm_permissions p where p.email = public.cruces_glm_current_email() and p.is_allowed = true and p.can_manage_forms = true ); $$; revoke all on function public.cruces_glm_current_email() from public; revoke all on function public.cruces_glm_is_allowed() from public; revoke all on function public.cruces_glm_can_manage_forms() from public; -- -------------------------------------------------------------------------- -- 9. RPC — ACCESO DEL USUARIO ACTUAL -- -------------------------------------------------------------------------- -- Esta es la fuente de verdad del login de Cruces GLM. No existe allowlist -- hardcodeada en el frontend. create or replace function public.cruces_glm_get_my_access() returns table ( allowed boolean, can_manage_forms boolean, display_name text ) language plpgsql stable security definer set search_path = public as $$ declare v_email text := public.cruces_glm_current_email(); begin return query select coalesce(p.is_allowed, false) as allowed, (coalesce(p.is_allowed, false) and coalesce(p.can_manage_forms, false)) as can_manage_forms, p.display_name from (select v_email as email) current_user_email left join public.cruces_glm_permissions p on p.email = current_user_email.email; end; $$; revoke all on function public.cruces_glm_get_my_access() from public; grant execute on function public.cruces_glm_get_my_access() to authenticated; -- -------------------------------------------------------------------------- -- 10. RPC — LISTAR FORMULARIOS -- -------------------------------------------------------------------------- create or replace function public.cruces_glm_get_forms() returns table ( id text, nombre text, descripcion text, categoria text, url text, activo boolean, created_at timestamptz, updated_at timestamptz ) language plpgsql stable security definer set search_path = public as $$ begin if not public.cruces_glm_is_allowed() then raise exception 'Usuario no autorizado para Cruces GLM'; end if; return query select f.id, f.nombre, f.descripcion, f.categoria, f.url, f.activo, f.created_at, f.updated_at from public.cruces_glm_forms f order by f.nombre asc; end; $$; revoke all on function public.cruces_glm_get_forms() from public; grant execute on function public.cruces_glm_get_forms() to authenticated; -- -------------------------------------------------------------------------- -- 11. RPC — CONSULTAR UN FORMULARIO -- -------------------------------------------------------------------------- create or replace function public.cruces_glm_get_form(p_form_id text) returns table ( id text, nombre text, descripcion text, categoria text, url text, activo boolean, created_at timestamptz, updated_at timestamptz ) language plpgsql stable security definer set search_path = public as $$ begin if not public.cruces_glm_is_allowed() then raise exception 'Usuario no autorizado para Cruces GLM'; end if; return query select f.id, f.nombre, f.descripcion, f.categoria, f.url, f.activo, f.created_at, f.updated_at from public.cruces_glm_forms f where f.id = trim(coalesce(p_form_id, '')) limit 1; end; $$; revoke all on function public.cruces_glm_get_form(text) from public; grant execute on function public.cruces_glm_get_form(text) to authenticated; -- -------------------------------------------------------------------------- -- 12. RPC — CREAR / EDITAR FORMULARIO -- -------------------------------------------------------------------------- -- Registra total_formularios_colocados únicamente cuando el ID no existía. -- Editar un formulario no incrementa esa sumatoria. drop function if exists public.cruces_glm_record_form_created(text, text, text); create or replace function public.cruces_glm_save_form( p_form_id text, p_nombre text, p_descripcion text, p_categoria text, p_url text, p_activo boolean ) returns table ( id text, nombre text, descripcion text, categoria text, url text, activo boolean, created_at timestamptz, updated_at timestamptz ) language plpgsql security definer set search_path = public as $$ declare v_email text := public.cruces_glm_current_email(); v_id text := trim(coalesce(p_form_id, '')); v_nombre text := trim(coalesce(p_nombre, '')); v_descripcion text := trim(coalesce(p_descripcion, '')); v_categoria text := trim(coalesce(p_categoria, '')); v_url text := trim(coalesce(p_url, '')); v_activo boolean := coalesce(p_activo, false); v_existed boolean; begin if not public.cruces_glm_can_manage_forms() then raise exception 'Usuario sin permiso para administrar formularios'; end if; if v_id = '' or v_nombre = '' or v_descripcion = '' or v_categoria = '' or v_url = '' then raise exception 'Todos los campos del formulario son obligatorios'; end if; if v_categoria not in ('Seguridad Social', 'IR', 'Reportes') then raise exception 'Categoría no válida'; end if; if v_url !~* '^https?://' then raise exception 'La URL debe comenzar con http:// o https://'; end if; select exists ( select 1 from public.cruces_glm_forms f where f.id = v_id ) into v_existed; insert into public.cruces_glm_forms ( id, nombre, descripcion, categoria, url, activo, created_by, updated_by ) values ( v_id, v_nombre, v_descripcion, v_categoria, v_url, v_activo, v_email, v_email ) on conflict (id) do update set nombre = excluded.nombre, descripcion = excluded.descripcion, categoria = excluded.categoria, url = excluded.url, activo = excluded.activo, updated_by = excluded.updated_by; if not v_existed then insert into public.cruces_glm_metrics_summary ( id, total_formularios_colocados, total_formularios_abiertos, total_logueos, updated_at ) values (1, 1, 0, 0, now()) on conflict (id) do update set total_formularios_colocados = public.cruces_glm_metrics_summary.total_formularios_colocados + 1, updated_at = now(); insert into public.cruces_glm_form_metrics ( form_id, form_name, category, total_publicaciones, total_aperturas, first_published_at, last_published_at ) values (v_id, v_nombre, v_categoria, 1, 0, now(), now()) on conflict (form_id) do update set form_name = excluded.form_name, category = excluded.category, total_publicaciones = public.cruces_glm_form_metrics.total_publicaciones + 1, last_published_at = now(); insert into public.cruces_glm_metric_events ( event_type, user_email, form_id, form_name, category ) values ('form_created', v_email, v_id, v_nombre, v_categoria); else update public.cruces_glm_form_metrics set form_name = v_nombre, category = v_categoria where form_id = v_id; end if; return query select f.id, f.nombre, f.descripcion, f.categoria, f.url, f.activo, f.created_at, f.updated_at from public.cruces_glm_forms f where f.id = v_id; end; $$; revoke all on function public.cruces_glm_save_form(text, text, text, text, text, boolean) from public; grant execute on function public.cruces_glm_save_form(text, text, text, text, text, boolean) to authenticated; -- -------------------------------------------------------------------------- -- 13. RPC — ELIMINAR FORMULARIO -- -------------------------------------------------------------------------- create or replace function public.cruces_glm_delete_form(p_form_id text) returns boolean language plpgsql security definer set search_path = public as $$ declare v_id text := trim(coalesce(p_form_id, '')); v_deleted_count integer; begin if not public.cruces_glm_can_manage_forms() then raise exception 'Usuario sin permiso para administrar formularios'; end if; delete from public.cruces_glm_forms f where f.id = v_id; get diagnostics v_deleted_count = row_count; return v_deleted_count > 0; end; $$; revoke all on function public.cruces_glm_delete_form(text) from public; grant execute on function public.cruces_glm_delete_form(text) to authenticated; -- -------------------------------------------------------------------------- -- 14. RPC — REGISTRAR LOGIN -- -------------------------------------------------------------------------- create or replace function public.cruces_glm_record_login() returns void language plpgsql security definer set search_path = public as $$ declare v_email text := public.cruces_glm_current_email(); begin if not public.cruces_glm_is_allowed() then raise exception 'Usuario no autorizado para Cruces GLM'; end if; insert into public.cruces_glm_metrics_summary ( id, total_formularios_colocados, total_formularios_abiertos, total_logueos, updated_at ) values (1, 0, 0, 1, now()) on conflict (id) do update set total_logueos = public.cruces_glm_metrics_summary.total_logueos + 1, updated_at = now(); insert into public.cruces_glm_user_metrics ( email, total_logueos, total_aperturas, last_login_at ) values (v_email, 1, 0, now()) on conflict (email) do update set total_logueos = public.cruces_glm_user_metrics.total_logueos + 1, last_login_at = now(); insert into public.cruces_glm_metric_events (event_type, user_email) values ('login', v_email); end; $$; revoke all on function public.cruces_glm_record_login() from public; grant execute on function public.cruces_glm_record_login() to authenticated; -- -------------------------------------------------------------------------- -- 15. RPC — REGISTRAR APERTURA -- -------------------------------------------------------------------------- -- Solo recibe el ID. Nombre/categoría/estado salen de la tabla real para evitar -- manipulación desde el navegador. Un formulario inactivo no se contabiliza. drop function if exists public.cruces_glm_record_form_open(text, text, text); create or replace function public.cruces_glm_record_form_open(p_form_id text) returns void language plpgsql security definer set search_path = public as $$ declare v_email text := public.cruces_glm_current_email(); v_form_id text := trim(coalesce(p_form_id, '')); v_form_name text; v_category text; v_active boolean; begin if not public.cruces_glm_is_allowed() then raise exception 'Usuario no autorizado para Cruces GLM'; end if; select f.nombre, f.categoria, f.activo into v_form_name, v_category, v_active from public.cruces_glm_forms f where f.id = v_form_id; if not found then raise exception 'Formulario no encontrado'; end if; if not v_active then raise exception 'Formulario no disponible'; end if; insert into public.cruces_glm_metrics_summary ( id, total_formularios_colocados, total_formularios_abiertos, total_logueos, updated_at ) values (1, 0, 1, 0, now()) on conflict (id) do update set total_formularios_abiertos = public.cruces_glm_metrics_summary.total_formularios_abiertos + 1, updated_at = now(); insert into public.cruces_glm_user_metrics ( email, total_logueos, total_aperturas, last_opened_at ) values (v_email, 0, 1, now()) on conflict (email) do update set total_aperturas = public.cruces_glm_user_metrics.total_aperturas + 1, last_opened_at = now(); insert into public.cruces_glm_form_metrics ( form_id, form_name, category, total_publicaciones, total_aperturas, first_published_at, last_published_at, last_opened_at ) values (v_form_id, v_form_name, v_category, 0, 1, now(), now(), now()) on conflict (form_id) do update set form_name = excluded.form_name, category = excluded.category, total_aperturas = public.cruces_glm_form_metrics.total_aperturas + 1, last_opened_at = now(); insert into public.cruces_glm_metric_events ( event_type, user_email, form_id, form_name, category ) values ('form_opened', v_email, v_form_id, v_form_name, v_category); end; $$; revoke all on function public.cruces_glm_record_form_open(text) from public; grant execute on function public.cruces_glm_record_form_open(text) to authenticated; -- -------------------------------------------------------------------------- -- 16. RPC — CONSULTAS DE MÉTRICAS PARA FUTURO DASHBOARD -- -------------------------------------------------------------------------- create or replace function public.cruces_glm_get_metrics_summary() returns table ( total_formularios_colocados bigint, total_formularios_abiertos bigint, total_logueos bigint, updated_at timestamptz ) language plpgsql stable security definer set search_path = public as $$ begin if not public.cruces_glm_can_manage_forms() then raise exception 'Usuario sin permiso para consultar métricas'; end if; return query select m.total_formularios_colocados, m.total_formularios_abiertos, m.total_logueos, m.updated_at from public.cruces_glm_metrics_summary m where m.id = 1; end; $$; create or replace function public.cruces_glm_get_form_metrics() returns table ( form_id text, form_name text, category text, total_publicaciones bigint, total_aperturas bigint, first_published_at timestamptz, last_published_at timestamptz, last_opened_at timestamptz ) language plpgsql stable security definer set search_path = public as $$ begin if not public.cruces_glm_can_manage_forms() then raise exception 'Usuario sin permiso para consultar métricas'; end if; return query select m.form_id, m.form_name, m.category, m.total_publicaciones, m.total_aperturas, m.first_published_at, m.last_published_at, m.last_opened_at from public.cruces_glm_form_metrics m order by m.total_aperturas desc, m.form_name asc; end; $$; create or replace function public.cruces_glm_get_user_metrics() returns table ( email text, total_logueos bigint, total_aperturas bigint, last_login_at timestamptz, last_opened_at timestamptz ) language plpgsql stable security definer set search_path = public as $$ begin if not public.cruces_glm_can_manage_forms() then raise exception 'Usuario sin permiso para consultar métricas'; end if; return query select m.email, m.total_logueos, m.total_aperturas, m.last_login_at, m.last_opened_at from public.cruces_glm_user_metrics m order by m.total_logueos desc, m.email asc; end; $$; create or replace function public.cruces_glm_get_recent_events(p_limit integer default 100) returns table ( id bigint, event_type text, user_email text, form_id text, form_name text, category text, created_at timestamptz ) language plpgsql stable security definer set search_path = public as $$ begin if not public.cruces_glm_can_manage_forms() then raise exception 'Usuario sin permiso para consultar métricas'; end if; return query select e.id, e.event_type, e.user_email, e.form_id, e.form_name, e.category, e.created_at from public.cruces_glm_metric_events e order by e.created_at desc limit least(greatest(coalesce(p_limit, 100), 1), 500); end; $$; revoke all on function public.cruces_glm_get_metrics_summary() from public; revoke all on function public.cruces_glm_get_form_metrics() from public; revoke all on function public.cruces_glm_get_user_metrics() from public; revoke all on function public.cruces_glm_get_recent_events(integer) from public; grant execute on function public.cruces_glm_get_metrics_summary() to authenticated; grant execute on function public.cruces_glm_get_form_metrics() to authenticated; grant execute on function public.cruces_glm_get_user_metrics() to authenticated; grant execute on function public.cruces_glm_get_recent_events(integer) to authenticated; -- -------------------------------------------------------------------------- -- 17. RPC — ADMINISTRACIÓN DE ACCESOS -- -------------------------------------------------------------------------- -- No hay UI para esto todavía, pero queda listo para una futura pantalla. -- Hoy puedes administrar cruces_glm_permissions directamente en Table Editor. create or replace function public.cruces_glm_admin_get_access_list() returns table ( email text, display_name text, is_allowed boolean, can_manage_forms boolean, updated_at timestamptz ) language plpgsql stable security definer set search_path = public as $$ begin if not public.cruces_glm_can_manage_forms() then raise exception 'Usuario sin permiso para administrar accesos'; end if; return query select p.email, p.display_name, p.is_allowed, p.can_manage_forms, p.updated_at from public.cruces_glm_permissions p order by p.display_name nulls last, p.email; end; $$; create or replace function public.cruces_glm_admin_save_access( p_email text, p_display_name text, p_is_allowed boolean, p_can_manage_forms boolean ) returns void language plpgsql security definer set search_path = public as $$ declare v_email text := lower(trim(coalesce(p_email, ''))); v_name text := nullif(trim(coalesce(p_display_name, '')), ''); begin if not public.cruces_glm_can_manage_forms() then raise exception 'Usuario sin permiso para administrar accesos'; end if; if v_email = '' or position('@' in v_email) <= 1 then raise exception 'Correo no válido'; end if; insert into public.cruces_glm_permissions ( email, display_name, is_allowed, can_manage_forms ) values ( v_email, v_name, coalesce(p_is_allowed, false), coalesce(p_can_manage_forms, false) ) on conflict (email) do update set display_name = excluded.display_name, is_allowed = excluded.is_allowed, can_manage_forms = excluded.can_manage_forms; end; $$; revoke all on function public.cruces_glm_admin_get_access_list() from public; revoke all on function public.cruces_glm_admin_save_access(text, text, boolean, boolean) from public; grant execute on function public.cruces_glm_admin_get_access_list() to authenticated; grant execute on function public.cruces_glm_admin_save_access(text, text, boolean, boolean) to authenticated; commit; -- ============================================================================ -- ADMINISTRACIÓN RÁPIDA DESDE SQL EDITOR / TABLE EDITOR -- ============================================================================ -- Dar acceso a Cruces GLM: -- update public.cruces_glm_permissions -- set is_allowed = true -- where email = 'correo@gomezleemarketing.com'; -- -- Quitar acceso a Cruces GLM sin borrar historial: -- update public.cruces_glm_permissions -- set is_allowed = false -- where email = 'correo@gomezleemarketing.com'; -- -- Dar permiso de Agregar / Editar / Eliminar: -- update public.cruces_glm_permissions -- set can_manage_forms = true -- where email = 'correo@gomezleemarketing.com'; -- -- Quitar permiso de administración: -- update public.cruces_glm_permissions -- set can_manage_forms = false -- where email = 'correo@gomezleemarketing.com'; -- -- Formularios actualmente guardados: -- select * from public.cruces_glm_forms order by nombre; -- -- Métricas globales: -- select * from public.cruces_glm_metrics_summary; -- -- Métricas por formulario: -- select * from public.cruces_glm_form_metrics order by total_aperturas desc; -- -- Métricas por usuario: -- select * from public.cruces_glm_user_metrics order by total_logueos desc; -- -- Eventos recientes: -- select * from public.cruces_glm_metric_events order by created_at desc limit 100; -- ============================================================================