feat: dual agency/client approval on calendar and assets
Private RRSS calendar with unified + per-network tabs, comments, and double-check OK. Assets get review drawer with preview/comments. KPIs gain date presets, North Star strip, channel matrix and per-channel dash. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
180
sql/content_collab.sql
Normal file
180
sql/content_collab.sql
Normal file
@@ -0,0 +1,180 @@
|
||||
-- Dual approval (agency + client) + threaded comments for calendar posts & assets
|
||||
-- Idempotent. Private portal only (no public publish).
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Calendar posts (normalized; private per tenant)
|
||||
-- ---------------------------------------------------------------------------
|
||||
create table if not exists public.calendar_posts (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
tenant_id uuid not null references public.tenants(id) on delete cascade,
|
||||
external_key text,
|
||||
month text,
|
||||
date date not null,
|
||||
time_slot text,
|
||||
platform text not null
|
||||
check (platform in (
|
||||
'instagram', 'facebook', 'linkedin', 'tiktok', 'youtube', 'x', 'other'
|
||||
)),
|
||||
format text,
|
||||
pillar text,
|
||||
title text not null,
|
||||
hook text,
|
||||
body text,
|
||||
cta text,
|
||||
hashtags text,
|
||||
objective_metric text,
|
||||
status text not null default 'draft'
|
||||
check (status in ('draft', 'in_review', 'scheduled', 'published', 'archived')),
|
||||
agency_approved_at timestamptz,
|
||||
agency_approved_by uuid references auth.users(id),
|
||||
client_approved_at timestamptz,
|
||||
client_approved_by uuid references auth.users(id),
|
||||
meta jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (tenant_id, external_key)
|
||||
);
|
||||
|
||||
create index if not exists calendar_posts_tenant_month_idx
|
||||
on public.calendar_posts (tenant_id, month, date);
|
||||
create index if not exists calendar_posts_tenant_platform_idx
|
||||
on public.calendar_posts (tenant_id, platform, date);
|
||||
|
||||
alter table public.calendar_posts enable row level security;
|
||||
|
||||
drop policy if exists calendar_posts_tenant_select on public.calendar_posts;
|
||||
drop policy if exists calendar_posts_tenant_write on public.calendar_posts;
|
||||
|
||||
create policy calendar_posts_tenant_select on public.calendar_posts
|
||||
for select using (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
);
|
||||
|
||||
create policy calendar_posts_tenant_write on public.calendar_posts
|
||||
for all using (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
)
|
||||
with check (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Shared comments (calendar_post | asset)
|
||||
-- ---------------------------------------------------------------------------
|
||||
create table if not exists public.content_comments (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
tenant_id uuid not null references public.tenants(id) on delete cascade,
|
||||
resource_type text not null check (resource_type in ('calendar_post', 'asset')),
|
||||
resource_id text not null,
|
||||
body text not null,
|
||||
author_id uuid references auth.users(id),
|
||||
author_email text,
|
||||
author_party text not null check (author_party in ('agency', 'client')),
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists content_comments_resource_idx
|
||||
on public.content_comments (tenant_id, resource_type, resource_id, created_at);
|
||||
|
||||
alter table public.content_comments enable row level security;
|
||||
|
||||
drop policy if exists content_comments_tenant_select on public.content_comments;
|
||||
drop policy if exists content_comments_tenant_insert on public.content_comments;
|
||||
|
||||
create policy content_comments_tenant_select on public.content_comments
|
||||
for select using (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
);
|
||||
|
||||
create policy content_comments_tenant_insert on public.content_comments
|
||||
for insert with check (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Dual approval events (calendar_post | asset)
|
||||
-- ---------------------------------------------------------------------------
|
||||
create table if not exists public.content_approvals (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
tenant_id uuid not null references public.tenants(id) on delete cascade,
|
||||
resource_type text not null check (resource_type in ('calendar_post', 'asset')),
|
||||
resource_id text not null,
|
||||
party text not null check (party in ('agency', 'client')),
|
||||
approved boolean not null default true,
|
||||
note text,
|
||||
actor_id uuid references auth.users(id),
|
||||
actor_email text,
|
||||
created_at timestamptz not null default now(),
|
||||
unique (tenant_id, resource_type, resource_id, party)
|
||||
);
|
||||
|
||||
alter table public.content_approvals enable row level security;
|
||||
|
||||
drop policy if exists content_approvals_tenant_select on public.content_approvals;
|
||||
drop policy if exists content_approvals_tenant_write on public.content_approvals;
|
||||
|
||||
create policy content_approvals_tenant_select on public.content_approvals
|
||||
for select using (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
);
|
||||
|
||||
create policy content_approvals_tenant_write on public.content_approvals
|
||||
for all using (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
)
|
||||
with check (
|
||||
tenant_id::text = coalesce(
|
||||
auth.jwt() -> 'app_metadata' ->> 'tenant_id',
|
||||
auth.jwt() -> 'user_metadata' ->> 'tenant_id'
|
||||
)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Assets: dual validation columns
|
||||
-- ---------------------------------------------------------------------------
|
||||
alter table public.asset_files
|
||||
add column if not exists review_status text not null default 'in_review';
|
||||
|
||||
alter table public.asset_files
|
||||
drop constraint if exists asset_files_review_status_check;
|
||||
|
||||
alter table public.asset_files
|
||||
add constraint asset_files_review_status_check
|
||||
check (review_status in ('draft', 'in_review', 'validated', 'archived'));
|
||||
|
||||
alter table public.asset_files
|
||||
add column if not exists agency_approved_at timestamptz;
|
||||
alter table public.asset_files
|
||||
add column if not exists agency_approved_by uuid references auth.users(id);
|
||||
alter table public.asset_files
|
||||
add column if not exists client_approved_at timestamptz;
|
||||
alter table public.asset_files
|
||||
add column if not exists client_approved_by uuid references auth.users(id);
|
||||
|
||||
comment on table public.calendar_posts is
|
||||
'Private RRSS calendar per tenant. Dual agency/client approval. No public publish.';
|
||||
comment on table public.content_comments is
|
||||
'Comments on calendar posts or assets (agency + client).';
|
||||
comment on table public.content_approvals is
|
||||
'Dual-check approvals: agency and client must both approve for full OK.';
|
||||
Reference in New Issue
Block a user