520 lines
16 KiB
PL/PgSQL
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.';
|