Files
4ventures-client-portal/sql/content_collab.sql
4Ventures a1a3c26c30 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>
2026-09-23 02:38:44 +02:00

181 lines
6.8 KiB
SQL

-- 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.';