feat: agregar control de visibilidad del total tarifado

This commit is contained in:
2026-06-22 10:05:21 -04:00
parent 5adf487bd0
commit 5f5b05e8fd
13 changed files with 1593 additions and 432 deletions
+430
View File
@@ -0,0 +1,430 @@
-- =========================================================
-- TABLERO CDC - Catálogo dinámico de tarifario + guardado seguro de links + RPC de totales
-- Seguro para producción: no borra proyectos, no altera Google Sheets, no cambia n8n.
-- La fuente de mantenimiento del tarifario será Google Sheets; n8n sincroniza hacia esta tabla.
-- =========================================================
-- 1) Catálogo dinámico de tarifas estándar.
-- La app mantiene el tarifario base como respaldo. Esta tabla es el destino normalizado
-- que n8n llenará leyendo el Google Sheet del tarifario. La app solo consume esta tabla.
create table if not exists public.tablero_cdc_tariff_catalog (
id text primary key,
catalog_item_id text not null,
section text not null check (section in ('grafico', 'estrategia')),
category text not null default '',
service text not null default '',
notes text not null default '',
work_type_id text not null default 'reference',
work_type_label text not null default 'Referencia',
work_type_short_label text not null default 'Ref.',
hour_reference text not null default '',
level_id text not null default 'project',
level_label text not null default 'Precio único',
reference text not null default '',
reference_min numeric,
reference_max numeric,
level_hint text not null default '',
sort_order integer not null default 9999,
is_active boolean not null default true,
created_by uuid references auth.users(id),
updated_by uuid references auth.users(id),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists tablero_cdc_tariff_catalog_section_idx
on public.tablero_cdc_tariff_catalog(section, is_active, sort_order);
create index if not exists tablero_cdc_tariff_catalog_item_idx
on public.tablero_cdc_tariff_catalog(catalog_item_id);
alter table public.tablero_cdc_tariff_catalog enable row level security;
drop policy if exists "tablero_cdc_tariff_catalog_select_authenticated" on public.tablero_cdc_tariff_catalog;
drop policy if exists "tablero_cdc_tariff_catalog_insert_pricing_admins" on public.tablero_cdc_tariff_catalog;
drop policy if exists "tablero_cdc_tariff_catalog_update_pricing_admins" on public.tablero_cdc_tariff_catalog;
drop policy if exists "tablero_cdc_tariff_catalog_delete_pricing_admins" on public.tablero_cdc_tariff_catalog;
create policy "tablero_cdc_tariff_catalog_select_authenticated"
on public.tablero_cdc_tariff_catalog
for select
to authenticated
using (true);
create policy "tablero_cdc_tariff_catalog_insert_pricing_admins"
on public.tablero_cdc_tariff_catalog
for insert
to authenticated
with check (
lower(coalesce(auth.jwt() ->> 'email', '')) in (
'gmarrero@gomezleemarketing.com',
'iaracena@gomezleemarketing.com'
)
);
create policy "tablero_cdc_tariff_catalog_update_pricing_admins"
on public.tablero_cdc_tariff_catalog
for update
to authenticated
using (
lower(coalesce(auth.jwt() ->> 'email', '')) in (
'gmarrero@gomezleemarketing.com',
'iaracena@gomezleemarketing.com'
)
)
with check (
lower(coalesce(auth.jwt() ->> 'email', '')) in (
'gmarrero@gomezleemarketing.com',
'iaracena@gomezleemarketing.com'
)
);
-- No recomendamos borrar tarifas usadas históricamente. La app usa activar/desactivar.
-- Aun así dejamos delete restringido por si en pruebas se necesita limpiar un registro creado por error.
create policy "tablero_cdc_tariff_catalog_delete_pricing_admins"
on public.tablero_cdc_tariff_catalog
for delete
to authenticated
using (
lower(coalesce(auth.jwt() ->> 'email', '')) in (
'gmarrero@gomezleemarketing.com',
'iaracena@gomezleemarketing.com'
)
);
create or replace function public.set_tablero_cdc_tariff_catalog_updated_at()
returns trigger
language plpgsql
as $$
begin
new.updated_at = now();
new.updated_by = auth.uid();
if tg_op = 'INSERT' then
new.created_by = coalesce(new.created_by, auth.uid());
end if;
return new;
end;
$$;
drop trigger if exists trg_tablero_cdc_tariff_catalog_updated_at on public.tablero_cdc_tariff_catalog;
create trigger trg_tablero_cdc_tariff_catalog_updated_at
before insert or update on public.tablero_cdc_tariff_catalog
for each row
execute function public.set_tablero_cdc_tariff_catalog_updated_at();
-- 2) Guardado seguro de links de Propuesta / Artes Finales.
-- La app llama esta función para mover links de un campo a otro en una sola operación transaccional.
-- Si cualquier insert falla, el delete también se revierte automáticamente.
create or replace function public.tablero_cdc_replace_project_links(
p_project_id uuid,
p_links jsonb default '[]'::jsonb
)
returns void
language plpgsql
security invoker
set search_path = public
as $$
begin
delete from public.tablero_cdc_project_links
where project_id = p_project_id;
insert into public.tablero_cdc_project_links (project_id, link_type, url, label)
select
p_project_id,
nullif(trim(link_type), ''),
trim(url),
nullif(trim(label), '')
from jsonb_to_recordset(coalesce(p_links, '[]'::jsonb)) as x(
link_type text,
url text,
label text
)
where nullif(trim(url), '') is not null;
end;
$$;
grant execute on function public.tablero_cdc_replace_project_links(uuid, jsonb) to authenticated;
-- 3) RPC paginado actualizado para que el monto interno solo viaje a Marrero e Isaac.
-- No cambia datos. Solo reemplaza la función de lectura si ya existe.
create or replace function public.tablero_cdc_get_projects_paginated(
p_tab text default 'todos',
p_search text default '',
p_page integer default 1,
p_page_size integer default 24,
p_country text default '',
p_brand text default '',
p_client text default '',
p_cm text default ''
)
returns jsonb
language sql
stable
security invoker
set search_path = public
as $$
with params as (
select
lower(coalesce(nullif(trim(p_tab), ''), 'todos')) as tab,
trim(coalesce(p_search, '')) as search_text,
greatest(coalesce(p_page, 1), 1) as page_number,
least(greatest(coalesce(p_page_size, 24), 1), 100) as page_size,
trim(coalesce(p_country, '')) as country_filter,
trim(coalesce(p_brand, '')) as brand_filter,
trim(coalesce(p_client, '')) as client_filter,
trim(coalesce(p_cm, '')) as cm_filter
),
base_filtered as (
select p.*
from public.tablero_cdc_projects p
cross join params prm
where
case
when prm.tab in ('abiertos', 'activos', 'active', 'open') then
coalesce(nullif(trim(p.status), ''), 'Activo') = 'Activo'
when prm.tab in ('cerrados', 'closed') then
coalesce(nullif(trim(p.status), ''), 'Activo') <> 'Activo'
else
true
end
and (
prm.search_text = ''
or coalesce(p.title, '') ilike '%' || prm.search_text || '%'
or coalesce(p.client, '') ilike '%' || prm.search_text || '%'
or coalesce(p.brand, '') ilike '%' || prm.search_text || '%'
or coalesce(p.country, '') ilike '%' || prm.search_text || '%'
or coalesce(p.requested_by, '') ilike '%' || prm.search_text || '%'
or coalesce(p.country_manager, '') ilike '%' || prm.search_text || '%'
or coalesce(p.description, '') ilike '%' || prm.search_text || '%'
)
),
filtered as (
select bf.*
from base_filtered bf
cross join params prm
where
(prm.country_filter = '' or coalesce(bf.country, '') ilike '%' || prm.country_filter || '%')
and (prm.brand_filter = '' or coalesce(bf.brand, '') ilike '%' || prm.brand_filter || '%')
and (prm.client_filter = '' or coalesce(bf.client, '') ilike '%' || prm.client_filter || '%')
and (prm.cm_filter = '' or coalesce(bf.country_manager, '') ilike '%' || prm.cm_filter || '%')
),
total as (
select count(*)::integer as total_count
from filtered
),
paged as (
select f.*
from filtered f
cross join params prm
order by f.created_at desc nulls last, f.id desc
limit (select page_size from params)
offset ((select page_number - 1 from params) * (select page_size from params))
),
option_values as (
select
coalesce(
(
select jsonb_agg(value order by value)
from (
select distinct trim(country) as value
from base_filtered
where nullif(trim(coalesce(country, '')), '') is not null
) countries
),
'[]'::jsonb
) as countries,
coalesce(
(
select jsonb_agg(value order by value)
from (
select distinct trim(brand) as value
from base_filtered
where nullif(trim(coalesce(brand, '')), '') is not null
) brands
),
'[]'::jsonb
) as brands,
coalesce(
(
select jsonb_agg(value order by value)
from (
select distinct trim(client) as value
from base_filtered
where nullif(trim(coalesce(client, '')), '') is not null
) clients
),
'[]'::jsonb
) as clients,
coalesce(
(
select jsonb_agg(value order by value)
from (
select distinct trim(country_manager) as value
from base_filtered
where nullif(trim(coalesce(country_manager, '')), '') is not null
) cms
),
'[]'::jsonb
) as cms
)
select jsonb_build_object(
'total_count', (select total_count from total),
'filter_options', jsonb_build_object(
'countries', (select countries from option_values),
'brands', (select brands from option_values),
'clients', (select clients from option_values),
'cms', (select cms from option_values)
),
'projects', coalesce(
(
select jsonb_agg(
jsonb_build_object(
'id', p.id,
'title', p.title,
'client', p.client,
'brand', p.brand,
'country', p.country,
'requested_by', p.requested_by,
'country_manager', p.country_manager,
'description', p.description,
'status', p.status,
'internal_amount', case
when lower(coalesce(auth.jwt() ->> 'email', '')) in (
'gmarrero@gomezleemarketing.com',
'iaracena@gomezleemarketing.com'
)
then to_jsonb(p.internal_amount)
else 'null'::jsonb
end,
'brief_link', p.brief_link,
'created_at', p.created_at,
'extra_data', p.extra_data,
'project_links', coalesce(
(
select jsonb_agg(
jsonb_build_object(
'id', l.id,
'link_type', l.link_type,
'url', l.url,
'label', l.label
)
order by l.label asc nulls last, l.id asc
)
from public.tablero_cdc_project_links l
where l.project_id = p.id
),
'[]'::jsonb
)
)
order by p.created_at desc nulls last, p.id desc
)
from paged p
),
'[]'::jsonb
)
);
$$;
grant execute on function public.tablero_cdc_get_projects_paginated(text, text, integer, integer, text, text, text, text) to authenticated;
-- 4) RPC de total tarifado global/filtrado.
-- Calcula en Supabase el total de Interno Cargado y la cantidad de proyectos según los mismos filtros de la pantalla.
-- Solo Marrero e Isaac reciben valores reales; otros usuarios reciben 0.
create or replace function public.tablero_cdc_get_pricing_summary(
p_tab text default 'todos',
p_search text default '',
p_country text default '',
p_brand text default '',
p_client text default '',
p_cm text default ''
)
returns jsonb
language sql
stable
security invoker
set search_path = public
as $$
with allowed as (
select lower(coalesce(auth.jwt() ->> 'email', '')) in (
'gmarrero@gomezleemarketing.com',
'iaracena@gomezleemarketing.com'
) as can_view_pricing
),
params as (
select
lower(coalesce(nullif(trim(p_tab), ''), 'todos')) as tab,
trim(coalesce(p_search, '')) as search_text,
trim(coalesce(p_country, '')) as country_filter,
trim(coalesce(p_brand, '')) as brand_filter,
trim(coalesce(p_client, '')) as client_filter,
trim(coalesce(p_cm, '')) as cm_filter
),
base_filtered as (
select p.*
from public.tablero_cdc_projects p
cross join params prm
where
case
when prm.tab in ('abiertos', 'activos', 'active', 'open') then
coalesce(nullif(trim(p.status), ''), 'Activo') = 'Activo'
when prm.tab in ('cerrados', 'closed') then
coalesce(nullif(trim(p.status), ''), 'Activo') <> 'Activo'
else
true
end
and (
prm.search_text = ''
or coalesce(p.title, '') ilike '%' || prm.search_text || '%'
or coalesce(p.client, '') ilike '%' || prm.search_text || '%'
or coalesce(p.brand, '') ilike '%' || prm.search_text || '%'
or coalesce(p.country, '') ilike '%' || prm.search_text || '%'
or coalesce(p.requested_by, '') ilike '%' || prm.search_text || '%'
or coalesce(p.country_manager, '') ilike '%' || prm.search_text || '%'
or coalesce(p.description, '') ilike '%' || prm.search_text || '%'
)
),
filtered as (
select bf.*
from base_filtered bf
cross join params prm
where
(prm.country_filter = '' or coalesce(bf.country, '') ilike '%' || prm.country_filter || '%')
and (prm.brand_filter = '' or coalesce(bf.brand, '') ilike '%' || prm.brand_filter || '%')
and (prm.client_filter = '' or coalesce(bf.client, '') ilike '%' || prm.client_filter || '%')
and (prm.cm_filter = '' or coalesce(bf.country_manager, '') ilike '%' || prm.cm_filter || '%')
),
totals as (
select
count(*)::integer as project_count,
coalesce(
sum(
greatest(
coalesce(nullif(trim(internal_amount::text), '')::numeric, 0),
0
)
),
0
)::numeric as total_amount
from filtered
)
select case
when (select can_view_pricing from allowed) then
jsonb_build_object(
'total_amount', (select total_amount from totals),
'project_count', (select project_count from totals)
)
else
jsonb_build_object(
'total_amount', 0,
'project_count', 0
)
end;
$$;
grant execute on function public.tablero_cdc_get_pricing_summary(text, text, text, text, text, text) to authenticated;
select
'tablero_cdc_tariff_catalog, safe links y RPC de totales listos' as status,
(select count(*) from public.tablero_cdc_tariff_catalog) as tarifas_dinamicas_actuales;