feat: actualizar carpeta dist probada en XAMPP y modulos de edicion de tiempo y persistencia UI
This commit is contained in:
@@ -0,0 +1,519 @@
|
||||
-- ============================================================================
|
||||
-- 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.';
|
||||
Reference in New Issue
Block a user