-- ========================================================= -- 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;