Files
cdc-project-management/supabase_tablero_cdc_dashboard_metrics.sql
T

520 lines
16 KiB
PL/PgSQL

-- ============================================================================
-- TABLERO CDC — MÉTRICAS PARA DASHBOARD FUTURO
-- Fecha: 2026-08-16
--
-- Objetivo:
-- 1) Registrar usuarios/sesiones que realmente abren Tablero CDC.
-- 2) Registrar eventos relevantes de proyectos sin depender del frontend.
-- 3) Exponer un resumen listo para un futuro dashboard.
--
-- Es idempotente: puede ejecutarse nuevamente sin duplicar el backfill.
-- ============================================================================
create extension if not exists pgcrypto;
create table if not exists public.tablero_cdc_app_metrics_events (
id uuid primary key default gen_random_uuid(),
event_type text not null,
event_key text not null unique,
actor_user_id uuid references auth.users(id) on delete set null,
actor_email text,
actor_name text,
project_id uuid references public.tablero_cdc_projects(id) on delete set null,
session_id text,
metadata jsonb not null default '{}'::jsonb,
occurred_at timestamptz not null default now(),
created_at timestamptz not null default now()
);
create index if not exists tablero_cdc_metrics_event_type_idx
on public.tablero_cdc_app_metrics_events (event_type, occurred_at desc);
create index if not exists tablero_cdc_metrics_actor_idx
on public.tablero_cdc_app_metrics_events (actor_user_id, occurred_at desc);
create index if not exists tablero_cdc_metrics_project_idx
on public.tablero_cdc_app_metrics_events (project_id, occurred_at desc)
where project_id is not null;
alter table public.tablero_cdc_app_metrics_events enable row level security;
-- El dashboard futuro podrá leer las métricas únicamente con usuarios activos
-- que ya tienen permiso para ver/controlar el resumen económico del Tablero.
drop policy if exists "tablero_cdc_metrics_select_authorized" on public.tablero_cdc_app_metrics_events;
create policy "tablero_cdc_metrics_select_authorized"
on public.tablero_cdc_app_metrics_events
for select
to authenticated
using (
exists (
select 1
from public.tablero_cdc_allowed_users u
where lower(u.email) = lower(coalesce(auth.jwt() ->> 'email', ''))
and u.is_active = true
and coalesce(u.can_control_pricing_summary, false) = true
)
);
-- No damos INSERT directo al cliente. Las aperturas se registran vía RPC y
-- los eventos de proyecto vía trigger, evitando que el navegador falsifique métricas.
revoke insert, update, delete on public.tablero_cdc_app_metrics_events from authenticated;
grant select on public.tablero_cdc_app_metrics_events to authenticated;
-- --------------------------------------------------------------------------
-- Helper de identidad para los eventos automáticos.
-- --------------------------------------------------------------------------
create or replace function public.tablero_cdc_metric_actor(
p_user_id uuid,
out actor_email text,
out actor_name text
)
language plpgsql
security definer
set search_path = public, auth
as $$
begin
actor_email := null;
actor_name := null;
if p_user_id is null then
return;
end if;
select
u.email,
coalesce(
nullif(u.raw_user_meta_data ->> 'full_name', ''),
nullif(u.raw_user_meta_data ->> 'name', ''),
u.email
)
into actor_email, actor_name
from auth.users u
where u.id = p_user_id
limit 1;
end;
$$;
revoke all on function public.tablero_cdc_metric_actor(uuid) from public;
-- --------------------------------------------------------------------------
-- RPC: una apertura por sesión autenticada/autorizada.
-- --------------------------------------------------------------------------
create or replace function public.tablero_cdc_track_app_open(p_session_id text)
returns void
language plpgsql
security definer
set search_path = public, auth
as $$
declare
v_uid uuid := auth.uid();
v_email text;
v_name text;
v_session text := nullif(trim(coalesce(p_session_id, '')), '');
begin
if v_uid is null then
raise exception 'Usuario no autenticado';
end if;
if v_session is null or length(v_session) > 200 then
raise exception 'session_id inválido';
end if;
select actor_email, actor_name
into v_email, v_name
from public.tablero_cdc_metric_actor(v_uid);
if not exists (
select 1
from public.tablero_cdc_allowed_users u
where lower(u.email) = lower(coalesce(v_email, ''))
and u.is_active = true
) then
raise exception 'Usuario no autorizado para Tablero CDC';
end if;
insert into public.tablero_cdc_app_metrics_events (
event_type,
event_key,
actor_user_id,
actor_email,
actor_name,
session_id,
metadata
) values (
'app_opened',
'app_opened:' || v_uid::text || ':' || v_session,
v_uid,
v_email,
v_name,
v_session,
jsonb_build_object('app', 'tablero_cdc')
)
on conflict (event_key) do nothing;
end;
$$;
revoke all on function public.tablero_cdc_track_app_open(text) from public;
grant execute on function public.tablero_cdc_track_app_open(text) to authenticated;
-- --------------------------------------------------------------------------
-- Trigger de proyectos: creación + cambios de estado relevantes.
-- --------------------------------------------------------------------------
create or replace function public.tablero_cdc_capture_project_metric()
returns trigger
language plpgsql
security definer
set search_path = public, auth
as $$
declare
v_actor uuid;
v_email text;
v_name text;
v_old_status text;
v_new_status text;
v_event text;
begin
v_actor := coalesce(new.updated_by, new.created_by, auth.uid());
select actor_email, actor_name
into v_email, v_name
from public.tablero_cdc_metric_actor(v_actor);
if tg_op = 'INSERT' then
insert into public.tablero_cdc_app_metrics_events (
event_type, event_key, actor_user_id, actor_email, actor_name,
project_id, metadata, occurred_at
) values (
'project_created',
'project_created:' || new.id::text,
v_actor,
v_email,
v_name,
new.id,
jsonb_build_object(
'title', coalesce(new.title, ''),
'client', coalesce(new.client, ''),
'brand', coalesce(new.brand, ''),
'country', coalesce(new.country, '')
),
coalesce(new.created_at, now())
)
on conflict (event_key) do nothing;
return new;
end if;
v_old_status := lower(trim(coalesce(old.status, '')));
v_new_status := lower(trim(coalesce(new.status, '')));
if v_old_status is not distinct from v_new_status then
return new;
end if;
v_event := case
when v_new_status = 'aprobado' then 'project_approved'
when v_new_status = 'no aprobado' then 'project_rejected'
when v_new_status in ('', 'activo') then 'project_reopened'
else 'project_closed'
end;
insert into public.tablero_cdc_app_metrics_events (
event_type, event_key, actor_user_id, actor_email, actor_name,
project_id, metadata
) values (
v_event,
v_event || ':' || new.id::text || ':' || gen_random_uuid()::text,
v_actor,
v_email,
v_name,
new.id,
jsonb_build_object(
'title', coalesce(new.title, ''),
'previous_status', coalesce(old.status, ''),
'new_status', coalesce(new.status, '')
)
);
return new;
end;
$$;
drop trigger if exists trg_tablero_cdc_project_metrics on public.tablero_cdc_projects;
create trigger trg_tablero_cdc_project_metrics
after insert or update of status
on public.tablero_cdc_projects
for each row
execute function public.tablero_cdc_capture_project_metric();
-- --------------------------------------------------------------------------
-- Trigger de tiempo: permite medir uso real y horas registradas históricamente.
-- --------------------------------------------------------------------------
create or replace function public.tablero_cdc_capture_time_metric()
returns trigger
language plpgsql
security definer
set search_path = public, auth
as $$
begin
insert into public.tablero_cdc_app_metrics_events (
event_type, event_key, actor_user_id, actor_email, actor_name,
project_id, metadata, occurred_at
) values (
'time_logged',
'time_logged:' || new.id::text,
new.created_by,
new.created_by_email,
coalesce(nullif(new.created_by_name, ''), new.created_by_email),
new.project_id,
jsonb_build_object(
'country', coalesce(new.country, ''),
'task_name', coalesce(new.task_name, ''),
'duration_minutes', coalesce(new.duration_minutes, 0),
'work_date', new.work_date
),
coalesce(new.created_at, now())
)
on conflict (event_key) do nothing;
return new;
end;
$$;
drop trigger if exists trg_tablero_cdc_time_metrics on public.tablero_cdc_project_time_entries;
create trigger trg_tablero_cdc_time_metrics
after insert
on public.tablero_cdc_project_time_entries
for each row
execute function public.tablero_cdc_capture_time_metric();
-- --------------------------------------------------------------------------
-- Métricas de Briefs (siempre útiles para este Tablero): recibidos/aprobados/
-- rechazados. La tabla ya forma parte del módulo CDC Brief de esta versión.
-- --------------------------------------------------------------------------
create or replace function public.tablero_cdc_capture_brief_metric()
returns trigger
language plpgsql
security definer
set search_path = public, auth
as $$
declare
v_actor uuid;
v_email text;
v_name text;
v_event text;
begin
if tg_op = 'INSERT' then
insert into public.tablero_cdc_app_metrics_events (
event_type, event_key, actor_user_id, actor_email, actor_name, metadata, occurred_at
) values (
'brief_received',
'brief_received:' || new.id::text,
null,
null,
null,
jsonb_build_object('brief_id', coalesce(new.brief_id, ''), 'title', coalesce(new.title, '')),
coalesce(new.created_at, now())
)
on conflict (event_key) do nothing;
return new;
end if;
if old.review_status is not distinct from new.review_status then
return new;
end if;
v_event := case
when new.review_status = 'approved' then 'brief_approved'
when new.review_status = 'rejected' then 'brief_rejected'
else null
end;
if v_event is null then
return new;
end if;
v_actor := coalesce(new.reviewed_by, auth.uid());
select actor_email, actor_name
into v_email, v_name
from public.tablero_cdc_metric_actor(v_actor);
insert into public.tablero_cdc_app_metrics_events (
event_type, event_key, actor_user_id, actor_email, actor_name,
project_id, metadata
) values (
v_event,
v_event || ':' || new.id::text || ':' || gen_random_uuid()::text,
v_actor,
coalesce(new.reviewed_by_email, v_email),
coalesce(new.reviewed_by_name, v_name),
new.approved_project_id,
jsonb_build_object(
'brief_id', coalesce(new.brief_id, ''),
'title', coalesce(new.title, ''),
'review_status', new.review_status
)
);
return new;
end;
$$;
drop trigger if exists trg_tablero_cdc_brief_metrics on public.tablero_cdc_brief_inbox;
create trigger trg_tablero_cdc_brief_metrics
after insert or update of review_status
on public.tablero_cdc_brief_inbox
for each row
execute function public.tablero_cdc_capture_brief_metric();
-- --------------------------------------------------------------------------
-- Backfill seguro: proyectos y briefs existentes para que el futuro dashboard
-- no empiece desde cero. No duplica si se ejecuta otra vez.
-- --------------------------------------------------------------------------
insert into public.tablero_cdc_app_metrics_events (
event_type, event_key, actor_user_id, actor_email, actor_name,
project_id, metadata, occurred_at
)
select
'project_created',
'project_created:' || p.id::text,
p.created_by,
au.email,
coalesce(nullif(au.raw_user_meta_data ->> 'full_name', ''), nullif(au.raw_user_meta_data ->> 'name', ''), au.email),
p.id,
jsonb_build_object(
'title', coalesce(p.title, ''),
'client', coalesce(p.client, ''),
'brand', coalesce(p.brand, ''),
'country', coalesce(p.country, ''),
'backfill', true
),
coalesce(p.created_at, now())
from public.tablero_cdc_projects p
left join auth.users au on au.id = p.created_by
on conflict (event_key) do nothing;
insert into public.tablero_cdc_app_metrics_events (
event_type, event_key, actor_user_id, actor_email, actor_name,
project_id, metadata, occurred_at
)
select
case when lower(trim(p.status)) = 'aprobado' then 'project_approved' else 'project_rejected' end,
case when lower(trim(p.status)) = 'aprobado'
then 'project_approved:backfill:' || p.id::text
else 'project_rejected:backfill:' || p.id::text end,
p.updated_by,
au.email,
coalesce(nullif(au.raw_user_meta_data ->> 'full_name', ''), nullif(au.raw_user_meta_data ->> 'name', ''), au.email),
p.id,
jsonb_build_object('status', p.status, 'backfill', true),
now()
from public.tablero_cdc_projects p
left join auth.users au on au.id = p.updated_by
where lower(trim(coalesce(p.status, ''))) in ('aprobado', 'no aprobado')
on conflict (event_key) do nothing;
insert into public.tablero_cdc_app_metrics_events (
event_type, event_key, actor_user_id, actor_email, actor_name,
project_id, metadata, occurred_at
)
select
'time_logged',
'time_logged:' || t.id::text,
t.created_by,
t.created_by_email,
coalesce(nullif(t.created_by_name, ''), t.created_by_email),
t.project_id,
jsonb_build_object(
'country', coalesce(t.country, ''),
'task_name', coalesce(t.task_name, ''),
'duration_minutes', coalesce(t.duration_minutes, 0),
'work_date', t.work_date,
'backfill', true
),
coalesce(t.created_at, now())
from public.tablero_cdc_project_time_entries t
on conflict (event_key) do nothing;
insert into public.tablero_cdc_app_metrics_events (
event_type, event_key, actor_user_id, actor_email, actor_name,
project_id, metadata, occurred_at
)
select
'brief_received',
'brief_received:' || b.id::text,
null,
null,
null,
b.approved_project_id,
jsonb_build_object('brief_id', coalesce(b.brief_id, ''), 'title', coalesce(b.title, ''), 'backfill', true),
coalesce(b.created_at, now())
from public.tablero_cdc_brief_inbox b
on conflict (event_key) do nothing;
-- --------------------------------------------------------------------------
-- Vista resumen para el dashboard futuro.
-- Los conteos de estado salen del estado ACTUAL de proyectos, evitando inflar
-- métricas cuando un proyecto cambia de estado varias veces.
-- --------------------------------------------------------------------------
create or replace view public.tablero_cdc_dashboard_metrics as
select
(select count(distinct actor_user_id)
from public.tablero_cdc_app_metrics_events
where actor_user_id is not null) as usuarios_que_han_usado_app,
(select count(*)
from public.tablero_cdc_app_metrics_events
where event_type = 'app_opened') as sesiones_registradas,
(select count(*)
from public.tablero_cdc_app_metrics_events
where event_type = 'project_created') as proyectos_creados_historico,
(select count(*) from public.tablero_cdc_projects) as proyectos_actuales,
(select count(*)
from public.tablero_cdc_projects
where lower(trim(coalesce(status, ''))) in ('', 'activo')) as proyectos_activos_actuales,
(select count(*)
from public.tablero_cdc_projects
where lower(trim(coalesce(status, ''))) = 'aprobado') as proyectos_aprobados_actuales,
(select count(*)
from public.tablero_cdc_projects
where lower(trim(coalesce(status, ''))) = 'no aprobado') as proyectos_rechazados_actuales,
(select count(*)
from public.tablero_cdc_app_metrics_events
where event_type = 'project_approved') as aprobaciones_registradas,
(select count(*)
from public.tablero_cdc_app_metrics_events
where event_type = 'project_rejected') as rechazos_registrados,
(select count(*)
from public.tablero_cdc_project_time_entries) as registros_de_tiempo,
(select round(coalesce(sum(duration_minutes), 0)::numeric / 60.0, 2)
from public.tablero_cdc_project_time_entries) as horas_registradas,
(select count(*) from public.tablero_cdc_brief_inbox) as briefs_recibidos,
(select count(*)
from public.tablero_cdc_brief_inbox
where review_status = 'approved') as briefs_aprobados,
(select count(*)
from public.tablero_cdc_brief_inbox
where review_status = 'rejected') as briefs_rechazados,
now() as consultado_en;
grant select on public.tablero_cdc_dashboard_metrics to authenticated;
comment on table public.tablero_cdc_app_metrics_events is
'Eventos de uso y negocio de Tablero CDC para dashboard futuro de aplicaciones GLM.';
comment on view public.tablero_cdc_dashboard_metrics is
'Resumen actual de usuarios, proyectos, tiempos y briefs de Tablero CDC.';