feat: panel de creatividades (pipeline editorial) en /dashboard/panel-cliente
Nueva tabla creative_pipeline (separada de creatives, que es solo para anuncios aprobados Meta/Google): Propuesta -> En diseño -> Revisión interna -> Aprobado/Rechazado -> Publicado, con RLS por tenant. - sql/creative_pipeline.sql: esquema + políticas RLS - /api/creative-pipeline: CRUD con transiciones restringidas (el cliente solo puede aprobar/rechazar una pieza en revisión interna; el resto de movimientos y la creación son solo agencia) - /api/creative-pipeline/upload + /media: portada y slides de carrusel sobre el bucket privado tenant-assets ya existente - CreativePipelineBoard: tablero agrupado por mes, con subida de imagen inline y acciones según rol - Alta del módulo "Panel de Creatividades" en tenant-modules y sidebar Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WqGTGPznxTp8mwMg6Yv4Ew
This commit is contained in:
97
sql/creative_pipeline.sql
Normal file
97
sql/creative_pipeline.sql
Normal file
@@ -0,0 +1,97 @@
|
||||
-- Creative pipeline: producción interna de creatividades (Blue Parrot y resto
|
||||
-- de clientes) separada de `creatives` (que es el repositorio de anuncios
|
||||
-- Meta/Google ya aprobados). Aquí vive el flujo editorial completo:
|
||||
-- Propuesta -> En diseño -> Revisión interna -> Aprobado / Rechazado -> Publicado
|
||||
-- Visible también para el cliente en /dashboard/panel-cliente (RLS por tenant).
|
||||
-- Idempotente.
|
||||
|
||||
create table if not exists public.creative_pipeline (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
tenant_id uuid not null references public.tenants(id) on delete cascade,
|
||||
|
||||
-- referencia externa estable usada por los scripts de generación (ej. "oct-04")
|
||||
external_ref text,
|
||||
|
||||
title text not null,
|
||||
format text not null default 'post'
|
||||
check (format in ('post', 'story', 'reel', 'carrusel')),
|
||||
|
||||
-- mes/calendario al que pertenece la pieza (agrupación en el board)
|
||||
month text, -- 'YYYY-MM'
|
||||
|
||||
status text not null default 'propuesta'
|
||||
check (status in (
|
||||
'propuesta', 'en_diseno', 'revision_interna',
|
||||
'aprobado', 'rechazado', 'publicado'
|
||||
)),
|
||||
|
||||
-- portada (todas las piezas tienen una)
|
||||
cover_path text,
|
||||
cover_mime text,
|
||||
|
||||
-- piezas carrusel: array ordenado de storage paths de los slides interiores + CTA
|
||||
-- ej. ["tenant/.../slide-2.jpg", "tenant/.../slide-3.jpg", ...]
|
||||
slide_paths jsonb not null default '[]'::jsonb,
|
||||
|
||||
caption text,
|
||||
notes text,
|
||||
|
||||
created_by uuid references auth.users(id),
|
||||
approved_by uuid references auth.users(id),
|
||||
approved_at timestamptz,
|
||||
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create unique index if not exists creative_pipeline_tenant_ref_idx
|
||||
on public.creative_pipeline (tenant_id, external_ref)
|
||||
where external_ref is not null;
|
||||
|
||||
create index if not exists creative_pipeline_tenant_month_idx
|
||||
on public.creative_pipeline (tenant_id, month, status);
|
||||
|
||||
alter table public.creative_pipeline enable row level security;
|
||||
|
||||
-- Lectura: cliente ve las piezas de su propio tenant (incluye revisión interna,
|
||||
-- ya que este panel es también para el cliente, no solo para el equipo).
|
||||
create policy creative_pipeline_tenant_select on public.creative_pipeline
|
||||
for select using (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
);
|
||||
|
||||
-- Alta/edición de contenido vía API del equipo (service role) — bloqueado para
|
||||
-- clientes directos por RLS; el endpoint PATCH permite al cliente mover el
|
||||
-- estado a 'aprobado'/'rechazado' únicamente mediante el mismo filtro de tenant.
|
||||
create policy creative_pipeline_tenant_insert on public.creative_pipeline
|
||||
for insert with check (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
);
|
||||
|
||||
create policy creative_pipeline_tenant_update on public.creative_pipeline
|
||||
for update using (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
);
|
||||
|
||||
create policy creative_pipeline_tenant_delete on public.creative_pipeline
|
||||
for delete using (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
);
|
||||
|
||||
comment on table public.creative_pipeline is
|
||||
'Pipeline interno de producción de creatividades (RRSS), separado de creatives (ads). Mismo bucket tenant-assets, prefijo pipeline/.';
|
||||
|
||||
-- Storage: reutiliza el bucket privado `tenant-assets` (ver src/lib/assets.ts),
|
||||
-- path = {tenant_id}/pipeline/{id}/cover.jpg y {tenant_id}/pipeline/{id}/slide-N.jpg
|
||||
Reference in New Issue
Block a user