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>
181 lines
6.8 KiB
SQL
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.';
|