Fix file sharing load speed and move error; misc updates

- Remove recursive directory size calculations (single Seafile API call per list)
- Remove 'Used in this location' usage display
- Fix move using v2 per-type endpoints instead of broken batch endpoint
- Send entry type from frontend for correct move routing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-05-13 14:20:38 -04:00
parent c9e7816e28
commit eee0885811
117 changed files with 17592 additions and 4057 deletions
@@ -0,0 +1,12 @@
create or replace function public.get_database_size_bytes()
returns bigint
language sql
security definer
set search_path = public
as $$
select pg_database_size(current_database());
$$;
revoke all on function public.get_database_size_bytes() from public;
grant execute on function public.get_database_size_bytes() to authenticated;
grant execute on function public.get_database_size_bytes() to service_role;
@@ -0,0 +1,22 @@
create table if not exists public.server_status_overrides (
id boolean primary key default true,
supabase_egress_bytes bigint,
vercel_fast_data_transfer_bytes bigint,
vercel_edge_requests bigint,
vercel_function_invocations bigint,
vercel_active_cpu_hours numeric(10,4),
updated_at timestamptz not null default now()
);
alter table public.server_status_overrides enable row level security;
drop policy if exists "Team all server_status_overrides" on public.server_status_overrides;
create policy "Team all server_status_overrides"
on public.server_status_overrides
for all
using (get_my_role() = 'team')
with check (get_my_role() = 'team');
insert into public.server_status_overrides (id)
values (true)
on conflict (id) do nothing;
@@ -0,0 +1,24 @@
with affected_tasks as (
select s.task_id
from public.submissions s
group by s.task_id
having min(s.version_number) filter (where s.type <> 'amendment') = 1
and count(*) filter (where s.type <> 'amendment' and s.version_number = 0) = 0
)
update public.submissions s
set version_number = s.version_number - 1
from affected_tasks a
where s.task_id = a.task_id;
with corrected_versions as (
select
s.task_id,
coalesce(max(s.version_number) filter (where s.type <> 'amendment'), 0) as corrected_current_version
from public.submissions s
group by s.task_id
)
update public.tasks t
set current_version = cv.corrected_current_version
from corrected_versions cv
where t.id = cv.task_id
and t.current_version is distinct from cv.corrected_current_version;
@@ -0,0 +1,14 @@
update public.submissions
set service_type = 'Other'
where coalesce(service_type, '') not in (
'Logo Design',
'Brand Identity',
'Brand Guidelines',
'Brand Book',
'Social Media Graphics',
'Print Design',
'Business Cards',
'Packaging Design',
'Web Design',
'Other'
);
@@ -0,0 +1 @@
alter table public.invoices add column if not exists bill_to text;
@@ -0,0 +1,17 @@
insert into storage.buckets (id, name, public)
select 'fourge-files', 'fourge-files', false
where not exists (
select 1 from storage.buckets where id = 'fourge-files'
);
create policy "Team reads fourge files storage" on storage.objects
for select to authenticated
using (bucket_id = 'fourge-files' and get_my_role() = 'team');
create policy "Team inserts fourge files storage" on storage.objects
for insert to authenticated
with check (bucket_id = 'fourge-files' and get_my_role() = 'team');
create policy "Team deletes fourge files storage" on storage.objects
for delete to authenticated
using (bucket_id = 'fourge-files' and get_my_role() = 'team');
@@ -1,9 +1,3 @@
-- ============================================================
-- Migration: Add external role and project_members table
-- Run this in Supabase → SQL Editor → Run
-- ============================================================
-- 1. Update profiles.role check constraint to include 'external'
do $$
declare
cname text;
@@ -13,7 +7,8 @@ begin
where table_schema = 'public'
and table_name = 'profiles'
and constraint_type = 'CHECK'
and constraint_name ilike '%role%';
and constraint_name = 'profiles_role_check';
if cname is not null then
execute 'alter table public.profiles drop constraint ' || quote_ident(cname);
end if;
@@ -23,48 +18,51 @@ $$;
alter table public.profiles
add constraint profiles_role_check check (role in ('team', 'client', 'external'));
-- 2. project_members table
create table public.project_members (
create table if not exists public.project_members (
id uuid default gen_random_uuid() primary key,
project_id uuid references public.projects(id) on delete cascade not null,
profile_id uuid references public.profiles(id) on delete cascade not null,
created_at timestamptz default now() not null,
unique(project_id, profile_id)
);
alter table public.project_members enable row level security;
-- 3. Helper function
create or replace function public.is_external()
returns boolean as $$
select get_my_role() = 'external';
$$ language sql security definer stable;
-- 4. RLS: project_members
drop policy if exists "Team all project_members" on public.project_members;
create policy "Team all project_members" on public.project_members
for all using (get_my_role() = 'team');
drop policy if exists "External reads own memberships" on public.project_members;
create policy "External reads own memberships" on public.project_members
for select using (profile_id = auth.uid());
-- 5. RLS: projects (external reads assigned only)
drop policy if exists "External reads assigned projects" on public.projects;
create policy "External reads assigned projects" on public.projects
for select using (
get_my_role() = 'external' and
id in (select project_id from public.project_members where profile_id = auth.uid())
);
-- 6. RLS: tasks (external reads + updates assigned projects)
drop policy if exists "External reads assigned tasks" on public.tasks;
create policy "External reads assigned tasks" on public.tasks
for select using (
get_my_role() = 'external' and
project_id in (select project_id from public.project_members where profile_id = auth.uid())
);
drop policy if exists "External updates assigned tasks" on public.tasks;
create policy "External updates assigned tasks" on public.tasks
for update using (
get_my_role() = 'external' and
project_id in (select project_id from public.project_members where profile_id = auth.uid())
);
-- 7. RLS: submissions
drop policy if exists "External reads assigned submissions" on public.submissions;
create policy "External reads assigned submissions" on public.submissions
for select using (
get_my_role() = 'external' and
@@ -74,12 +72,14 @@ create policy "External reads assigned submissions" on public.submissions
where pm.profile_id = auth.uid()
)
);
drop policy if exists "External inserts submissions" on public.submissions;
create policy "External inserts submissions" on public.submissions
for insert with check (
get_my_role() = 'external' and submitted_by = auth.uid()
);
-- 8. RLS: submission_files
drop policy if exists "External reads assigned submission_files" on public.submission_files;
create policy "External reads assigned submission_files" on public.submission_files
for select using (
get_my_role() = 'external' and
@@ -90,10 +90,12 @@ create policy "External reads assigned submission_files" on public.submission_fi
where pm.profile_id = auth.uid()
)
);
drop policy if exists "External inserts submission_files" on public.submission_files;
create policy "External inserts submission_files" on public.submission_files
for insert with check (get_my_role() = 'external');
-- 9. RLS: deliveries (read only)
drop policy if exists "External reads assigned deliveries" on public.deliveries;
create policy "External reads assigned deliveries" on public.deliveries
for select using (
get_my_role() = 'external' and
@@ -105,7 +107,7 @@ create policy "External reads assigned deliveries" on public.deliveries
)
);
-- 10. RLS: delivery_files (read only)
drop policy if exists "External reads assigned delivery_files" on public.delivery_files;
create policy "External reads assigned delivery_files" on public.delivery_files
for select using (
get_my_role() = 'external' and
@@ -117,7 +119,3 @@ create policy "External reads assigned delivery_files" on public.delivery_files
where pm.profile_id = auth.uid()
)
);
-- 11. RLS: profiles (external reads own profile only — already covered by existing policy)
-- "Own profile select" policy already handles this with: id = auth.uid()
-- No additional policy needed.
@@ -0,0 +1,23 @@
alter table public.submissions
add column if not exists revision_type text;
do $$
begin
if not exists (
select 1
from pg_constraint
where conname = 'submissions_revision_type_check'
and conrelid = 'public.submissions'::regclass
) then
alter table public.submissions
add constraint submissions_revision_type_check
check (revision_type in ('fourge_error', 'client_revision'));
end if;
end;
$$;
alter table public.submissions
add column if not exists invoiced boolean not null default false;
alter table public.invoice_items
add column if not exists submission_id uuid references public.submissions(id) on delete set null;
@@ -0,0 +1,28 @@
drop policy if exists "Client inserts submission_files" on public.submission_files;
drop policy if exists "External inserts submission_files" on public.submission_files;
create policy "Client inserts submission_files" on public.submission_files
for insert with check (
get_my_role() = 'client'
and submission_id in (
select s.id
from public.submissions s
join public.tasks t on t.id = s.task_id
join public.projects p on p.id = t.project_id
where p.company_id = get_my_company_id()
and s.submitted_by = auth.uid()
)
);
create policy "External inserts submission_files" on public.submission_files
for insert with check (
get_my_role() = 'external'
and submission_id in (
select s.id
from public.submissions s
join public.tasks t on t.id = s.task_id
join public.project_members pm on pm.project_id = t.project_id
where pm.profile_id = auth.uid()
and s.submitted_by = auth.uid()
)
);
@@ -0,0 +1,10 @@
drop policy if exists "Own profile update" on public.profiles;
create policy "Own profile update" on public.profiles
for update
using (id = auth.uid())
with check (
id = auth.uid()
and role = (select role from public.profiles where id = auth.uid())
and company_id is not distinct from (select company_id from public.profiles where id = auth.uid())
);
@@ -0,0 +1,118 @@
drop policy if exists "Auth users upload to submissions" on storage.objects;
drop policy if exists "Auth users read submissions" on storage.objects;
drop policy if exists "Team upload deliveries" on storage.objects;
drop policy if exists "Auth users read deliveries" on storage.objects;
drop policy if exists "Team reads submissions storage" on storage.objects;
create policy "Team reads submissions storage" on storage.objects
for select to authenticated
using (bucket_id = 'submissions' and get_my_role() = 'team');
drop policy if exists "Client reads submissions storage" on storage.objects;
create policy "Client reads submissions storage" on storage.objects
for select to authenticated
using (
bucket_id = 'submissions'
and get_my_role() = 'client'
and split_part(name, '/', 1) in (
select t.id::text
from public.tasks t
join public.projects p on p.id = t.project_id
where p.company_id = get_my_company_id()
)
);
drop policy if exists "External reads submissions storage" on storage.objects;
create policy "External reads submissions storage" on storage.objects
for select to authenticated
using (
bucket_id = 'submissions'
and get_my_role() = 'external'
and split_part(name, '/', 1) in (
select t.id::text
from public.tasks t
join public.project_members pm on pm.project_id = t.project_id
where pm.profile_id = auth.uid()
)
);
drop policy if exists "Team inserts submissions storage" on storage.objects;
create policy "Team inserts submissions storage" on storage.objects
for insert to authenticated
with check (bucket_id = 'submissions' and get_my_role() = 'team');
drop policy if exists "Client inserts submissions storage" on storage.objects;
create policy "Client inserts submissions storage" on storage.objects
for insert to authenticated
with check (
bucket_id = 'submissions'
and get_my_role() = 'client'
and split_part(name, '/', 1) in (
select t.id::text
from public.tasks t
join public.projects p on p.id = t.project_id
where p.company_id = get_my_company_id()
)
);
drop policy if exists "External inserts submissions storage" on storage.objects;
create policy "External inserts submissions storage" on storage.objects
for insert to authenticated
with check (
bucket_id = 'submissions'
and get_my_role() = 'external'
and split_part(name, '/', 1) in (
select t.id::text
from public.tasks t
join public.project_members pm on pm.project_id = t.project_id
where pm.profile_id = auth.uid()
)
);
drop policy if exists "Team deletes submissions storage" on storage.objects;
create policy "Team deletes submissions storage" on storage.objects
for delete to authenticated
using (bucket_id = 'submissions' and get_my_role() = 'team');
drop policy if exists "Team reads deliveries storage" on storage.objects;
create policy "Team reads deliveries storage" on storage.objects
for select to authenticated
using (bucket_id = 'deliveries' and get_my_role() = 'team');
drop policy if exists "Client reads deliveries storage" on storage.objects;
create policy "Client reads deliveries storage" on storage.objects
for select to authenticated
using (
bucket_id = 'deliveries'
and get_my_role() = 'client'
and split_part(name, '/', 1) in (
select t.id::text
from public.tasks t
join public.projects p on p.id = t.project_id
where p.company_id = get_my_company_id()
)
);
drop policy if exists "External reads deliveries storage" on storage.objects;
create policy "External reads deliveries storage" on storage.objects
for select to authenticated
using (
bucket_id = 'deliveries'
and get_my_role() = 'external'
and split_part(name, '/', 1) in (
select t.id::text
from public.tasks t
join public.project_members pm on pm.project_id = t.project_id
where pm.profile_id = auth.uid()
)
);
drop policy if exists "Team inserts deliveries storage" on storage.objects;
create policy "Team inserts deliveries storage" on storage.objects
for insert to authenticated
with check (bucket_id = 'deliveries' and get_my_role() = 'team');
drop policy if exists "Team deletes deliveries storage" on storage.objects;
create policy "Team deletes deliveries storage" on storage.objects
for delete to authenticated
using (bucket_id = 'deliveries' and get_my_role() = 'team');
@@ -0,0 +1,11 @@
drop policy if exists "Client updates own company" on public.companies;
create policy "Client updates own company" on public.companies
for update
using (id = get_my_company_id())
with check (id = get_my_company_id());
drop policy if exists "Client updates own company projects" on public.projects;
create policy "Client updates own company projects" on public.projects
for update
using (company_id = get_my_company_id())
with check (company_id = get_my_company_id());
@@ -0,0 +1,4 @@
create policy "Team updates fourge files storage" on storage.objects
for update to authenticated
using (bucket_id = 'fourge-files' and get_my_role() = 'team')
with check (bucket_id = 'fourge-files' and get_my_role() = 'team');
@@ -0,0 +1,18 @@
create table if not exists public.fourge_passwords (
id uuid default gen_random_uuid() primary key,
service_name text not null,
service_url text default '',
username text not null default '',
encrypted_password text not null,
password_iv text not null,
notes text default '',
created_by uuid references public.profiles(id) on delete set null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table public.fourge_passwords enable row level security;
create policy "Team all fourge_passwords" on public.fourge_passwords
for all using (get_my_role() = 'team')
with check (get_my_role() = 'team');
@@ -0,0 +1,14 @@
create table public.expenses (
id uuid default gen_random_uuid() primary key,
date date default current_date not null,
description text not null,
category text not null default 'Other',
amount numeric(10,2) not null,
notes text default '',
receipt_path text,
receipt_name text,
created_by uuid references public.profiles(id) on delete set null,
created_at timestamptz default now() not null
);
alter table public.expenses enable row level security;
create policy "Team all expenses" on public.expenses for all using (get_my_role() = 'team');
@@ -0,0 +1 @@
alter table public.invoices add column if not exists paid_at timestamptz;
@@ -0,0 +1,2 @@
alter table public.expenses add column if not exists receipt_path text;
alter table public.expenses add column if not exists receipt_name text;
@@ -0,0 +1,22 @@
alter table public.expenses
add column if not exists receipt_path text,
add column if not exists receipt_name text;
insert into storage.buckets (id, name, public)
values ('expense-receipts', 'expense-receipts', false)
on conflict (id) do nothing;
drop policy if exists "Team reads expense receipts storage" on storage.objects;
drop policy if exists "Team inserts expense receipts storage" on storage.objects;
drop policy if exists "Team updates expense receipts storage" on storage.objects;
drop policy if exists "Team deletes expense receipts storage" on storage.objects;
create policy "Team reads expense receipts storage" on storage.objects
for select to authenticated using (bucket_id = 'expense-receipts' and get_my_role() = 'team');
create policy "Team inserts expense receipts storage" on storage.objects
for insert to authenticated with check (bucket_id = 'expense-receipts' and get_my_role() = 'team');
create policy "Team updates expense receipts storage" on storage.objects
for update to authenticated using (bucket_id = 'expense-receipts' and get_my_role() = 'team')
with check (bucket_id = 'expense-receipts' and get_my_role() = 'team');
create policy "Team deletes expense receipts storage" on storage.objects
for delete to authenticated using (bucket_id = 'expense-receipts' and get_my_role() = 'team');
@@ -0,0 +1,153 @@
create table if not exists public.company_members (
id uuid default gen_random_uuid() primary key,
company_id uuid references public.companies(id) on delete cascade not null,
profile_id uuid references public.profiles(id) on delete cascade not null,
created_at timestamptz default now() not null,
unique(company_id, profile_id)
);
alter table public.company_members enable row level security;
insert into public.company_members (company_id, profile_id)
select company_id, id
from public.profiles
where company_id is not null
on conflict (company_id, profile_id) do nothing;
create or replace function public.has_company_access(company uuid)
returns boolean as $$
select exists (
select 1
from public.profiles p
where p.id = auth.uid()
and (
p.company_id = company
or exists (
select 1
from public.company_members cm
where cm.profile_id = auth.uid()
and cm.company_id = company
)
)
);
$$ language sql security definer stable;
drop policy if exists "Team all company_members" on public.company_members;
drop policy if exists "Users read own company memberships" on public.company_members;
create policy "Team all company_members" on public.company_members
for all using (get_my_role() = 'team') with check (get_my_role() = 'team');
create policy "Users read own company memberships" on public.company_members
for select using (profile_id = auth.uid());
drop policy if exists "Client reads own company" on public.companies;
drop policy if exists "Client updates own company" on public.companies;
create policy "Client reads assigned companies" on public.companies
for select using (has_company_access(id));
create policy "Client updates primary company" on public.companies
for update using (id = get_my_company_id()) with check (id = get_my_company_id());
drop policy if exists "Client reads company projects" on public.projects;
drop policy if exists "Client inserts company projects" on public.projects;
drop policy if exists "Client updates own company projects" on public.projects;
create policy "Client reads assigned company projects" on public.projects
for select using (has_company_access(company_id));
create policy "Client inserts assigned company projects" on public.projects
for insert with check (get_my_role() = 'client' and has_company_access(company_id));
create policy "Client updates assigned company projects" on public.projects
for update using (get_my_role() = 'client' and has_company_access(company_id))
with check (get_my_role() = 'client' and has_company_access(company_id));
drop policy if exists "Client reads company tasks" on public.tasks;
drop policy if exists "Client insert task" on public.tasks;
drop policy if exists "Client updates company tasks" on public.tasks;
create policy "Client reads assigned company tasks" on public.tasks for select using (
project_id in (select id from public.projects where has_company_access(company_id))
);
create policy "Client inserts assigned company tasks" on public.tasks for insert with check (
get_my_role() = 'client'
and project_id in (select id from public.projects where has_company_access(company_id))
);
create policy "Client updates assigned company tasks" on public.tasks
for update
using (
get_my_role() = 'client'
and project_id in (select id from public.projects where has_company_access(company_id))
)
with check (
get_my_role() = 'client'
and project_id in (select id from public.projects where has_company_access(company_id))
);
drop policy if exists "Client reads company submissions" on public.submissions;
drop policy if exists "Client inserts submissions" on public.submissions;
create policy "Client reads assigned company submissions" on public.submissions for select using (
task_id in (
select t.id from public.tasks t
join public.projects p on p.id = t.project_id
where has_company_access(p.company_id)
)
);
create policy "Client inserts assigned company submissions" on public.submissions for insert with check (
get_my_role() = 'client'
and submitted_by = auth.uid()
and task_id in (
select t.id from public.tasks t
join public.projects p on p.id = t.project_id
where has_company_access(p.company_id)
)
);
drop policy if exists "Client reads company submission_files" on public.submission_files;
drop policy if exists "Client inserts submission_files" on public.submission_files;
create policy "Client reads assigned company submission_files" on public.submission_files for select using (
submission_id in (
select s.id from public.submissions s
join public.tasks t on t.id = s.task_id
join public.projects p on p.id = t.project_id
where has_company_access(p.company_id)
)
);
create policy "Client inserts assigned company submission_files" on public.submission_files for insert with check (
get_my_role() = 'client'
and submission_id in (
select s.id from public.submissions s
join public.tasks t on t.id = s.task_id
join public.projects p on p.id = t.project_id
where has_company_access(p.company_id)
and s.submitted_by = auth.uid()
)
);
drop policy if exists "Client reads company deliveries" on public.deliveries;
create policy "Client reads assigned company deliveries" on public.deliveries for select using (
submission_id in (
select s.id from public.submissions s
join public.tasks t on t.id = s.task_id
join public.projects p on p.id = t.project_id
where has_company_access(p.company_id)
)
);
drop policy if exists "Client reads company delivery_files" on public.delivery_files;
create policy "Client reads assigned company delivery_files" on public.delivery_files for select using (
delivery_id in (
select d.id from public.deliveries d
join public.submissions s on s.id = d.submission_id
join public.tasks t on t.id = s.task_id
join public.projects p on p.id = t.project_id
where has_company_access(p.company_id)
)
);
drop policy if exists "Client reads own company prices" on public.company_prices;
create policy "Client reads assigned company prices" on public.company_prices
for select using (has_company_access(company_id));
drop policy if exists "Client reads company invoices" on public.invoices;
create policy "Client reads assigned company invoices" on public.invoices
for select using (has_company_access(company_id));
drop policy if exists "Client reads company invoice_items" on public.invoice_items;
create policy "Client reads assigned company invoice_items" on public.invoice_items for select using (
invoice_id in (select id from public.invoices where has_company_access(company_id))
);
@@ -0,0 +1,2 @@
alter table public.invoices
add column if not exists invoice_email text;
@@ -0,0 +1,18 @@
create table if not exists public.subcontractor_payments (
id uuid default gen_random_uuid() primary key,
profile_id uuid references public.profiles(id) on delete set null,
date date default current_date not null,
description text not null,
amount numeric(10,2) not null,
status text not null default 'pending' check (status in ('pending', 'paid')),
paid_at date,
notes text default '',
created_by uuid references public.profiles(id) on delete set null,
created_at timestamptz default now() not null
);
alter table public.subcontractor_payments enable row level security;
drop policy if exists "Team all subcontractor_payments" on public.subcontractor_payments;
create policy "Team all subcontractor_payments" on public.subcontractor_payments
for all using (get_my_role() = 'team') with check (get_my_role() = 'team');
@@ -0,0 +1,9 @@
delete from public.company_members cm
using public.profiles p
where p.id = cm.profile_id
and p.role = 'external';
update public.profiles
set company_id = null
where role = 'external'
and company_id is not null;
@@ -0,0 +1,52 @@
alter table public.subcontractor_payments
add column if not exists po_number text,
add column if not exists project_id uuid references public.projects(id) on delete set null,
add column if not exists due_date date,
add column if not exists terms text default 'Net 15',
add column if not exists sent_at timestamptz,
add column if not exists approved_at timestamptz,
add column if not exists cancelled_at timestamptz;
alter table public.subcontractor_payments
drop constraint if exists subcontractor_payments_status_check;
update public.subcontractor_payments
set status = 'ready_to_pay'
where status = 'pending';
alter table public.subcontractor_payments
add constraint subcontractor_payments_status_check
check (status in ('draft', 'sent', 'approved', 'ready_to_pay', 'paid', 'cancelled'));
with numbered as (
select
id,
'PO-' || to_char(coalesce(created_at, now()), 'YYYY') || '-' || lpad((row_number() over (order by created_at, id))::text, 4, '0') as generated_po_number
from public.subcontractor_payments
where po_number is null
)
update public.subcontractor_payments sp
set po_number = numbered.generated_po_number
from numbered
where sp.id = numbered.id;
create unique index if not exists subcontractor_payments_po_number_key
on public.subcontractor_payments(po_number)
where po_number is not null;
drop policy if exists "External reads own subcontractor purchase orders" on public.subcontractor_payments;
create policy "External reads own subcontractor purchase orders" on public.subcontractor_payments
for select using (
get_my_role() = 'external'
and profile_id = auth.uid()
);
drop policy if exists "External updates own subcontractor purchase orders" on public.subcontractor_payments;
create policy "External updates own subcontractor purchase orders" on public.subcontractor_payments
for update using (
get_my_role() = 'external'
and profile_id = auth.uid()
) with check (
get_my_role() = 'external'
and profile_id = auth.uid()
);
@@ -0,0 +1,33 @@
create table if not exists public.subcontractor_po_items (
id uuid default gen_random_uuid() primary key,
po_id uuid references public.subcontractor_payments(id) on delete cascade not null,
task_id uuid references public.tasks(id) on delete set null,
description text not null,
amount numeric(10,2) not null,
sort_order integer default 0 not null,
created_at timestamptz default now() not null
);
alter table public.subcontractor_po_items enable row level security;
drop policy if exists "Team all subcontractor_po_items" on public.subcontractor_po_items;
create policy "Team all subcontractor_po_items" on public.subcontractor_po_items
for all using (get_my_role() = 'team') with check (get_my_role() = 'team');
drop policy if exists "External reads own subcontractor_po_items" on public.subcontractor_po_items;
create policy "External reads own subcontractor_po_items" on public.subcontractor_po_items
for select using (
get_my_role() = 'external'
and po_id in (
select id from public.subcontractor_payments
where profile_id = auth.uid()
)
);
insert into public.subcontractor_po_items (po_id, task_id, description, amount, sort_order)
select id, null, description, amount, 0
from public.subcontractor_payments sp
where not exists (
select 1 from public.subcontractor_po_items item
where item.po_id = sp.id
);
@@ -0,0 +1,10 @@
update public.projects
set name = 'Untitled Project ' || left(id::text, 8)
where length(trim(coalesce(name, ''))) = 0;
alter table public.projects
drop constraint if exists projects_name_not_blank;
alter table public.projects
add constraint projects_name_not_blank
check (length(trim(name)) > 0);
@@ -0,0 +1,75 @@
create or replace function public.prevent_duplicate_active_subcontractor_po_task()
returns trigger as $$
declare
current_po_status text;
duplicate_po_number text;
begin
if new.task_id is null then
return new;
end if;
select status into current_po_status
from public.subcontractor_payments
where id = new.po_id;
if current_po_status = 'cancelled' then
return new;
end if;
select sp.po_number into duplicate_po_number
from public.subcontractor_po_items item
join public.subcontractor_payments sp on sp.id = item.po_id
where item.task_id = new.task_id
and item.id <> coalesce(new.id, '00000000-0000-0000-0000-000000000000'::uuid)
and sp.status <> 'cancelled'
limit 1;
if duplicate_po_number is not null then
raise exception 'Task is already attached to active subcontractor PO %', duplicate_po_number
using errcode = '23505';
end if;
return new;
end;
$$ language plpgsql security definer;
drop trigger if exists prevent_duplicate_active_subcontractor_po_task on public.subcontractor_po_items;
create trigger prevent_duplicate_active_subcontractor_po_task
before insert or update of task_id, po_id on public.subcontractor_po_items
for each row execute function public.prevent_duplicate_active_subcontractor_po_task();
create or replace function public.prevent_duplicate_on_subcontractor_po_reactivation()
returns trigger as $$
declare
duplicate_task_title text;
begin
if old.status = new.status or new.status = 'cancelled' then
return new;
end if;
select t.title into duplicate_task_title
from public.subcontractor_po_items current_item
join public.subcontractor_po_items other_item
on other_item.task_id = current_item.task_id
and other_item.po_id <> current_item.po_id
join public.subcontractor_payments other_po
on other_po.id = other_item.po_id
and other_po.status <> 'cancelled'
left join public.tasks t on t.id = current_item.task_id
where current_item.po_id = new.id
and current_item.task_id is not null
limit 1;
if duplicate_task_title is not null then
raise exception 'Cannot reactivate PO because task "%" is already attached to another active subcontractor PO', duplicate_task_title
using errcode = '23505';
end if;
return new;
end;
$$ language plpgsql security definer;
drop trigger if exists prevent_duplicate_on_subcontractor_po_reactivation on public.subcontractor_payments;
create trigger prevent_duplicate_on_subcontractor_po_reactivation
before update of status on public.subcontractor_payments
for each row execute function public.prevent_duplicate_on_subcontractor_po_reactivation();
@@ -0,0 +1,30 @@
-- Allow client revision returns to clear stale assignments while keeping
-- assignment fields protected for normal client updates.
create or replace function public.guard_task_update()
returns trigger as $$
declare
caller_role text;
begin
select role into caller_role from public.profiles where id = auth.uid();
if caller_role = 'client' then
new.project_id := old.project_id;
new.invoiced := old.invoiced;
if not (
new.status = 'not_started'
and coalesce(new.current_version, 0) > coalesce(old.current_version, 0)
and new.assigned_to is null
and new.assigned_name is null
) then
new.assigned_to := old.assigned_to;
new.assigned_name := old.assigned_name;
end if;
elsif caller_role = 'external' then
new.project_id := old.project_id;
new.invoiced := old.invoiced;
end if;
return new;
end;
$$ language plpgsql security definer;
@@ -0,0 +1,15 @@
create table public.meeting_notes (
id uuid default gen_random_uuid() primary key,
meeting_at timestamptz default now() not null,
title text not null,
attendees text default '',
notes text not null default '',
created_by uuid references public.profiles(id) on delete set null,
created_at timestamptz default now() not null,
updated_at timestamptz default now() not null
);
alter table public.meeting_notes enable row level security;
create policy "Team all meeting_notes" on public.meeting_notes
for all using (get_my_role() = 'team') with check (get_my_role() = 'team');
@@ -0,0 +1,2 @@
alter table public.submissions
add column if not exists is_hot boolean not null default false;
@@ -0,0 +1,16 @@
alter table public.tasks
add column if not exists request_key uuid;
alter table public.submissions
add column if not exists request_key uuid;
create unique index if not exists tasks_request_key_key
on public.tasks (request_key)
where request_key is not null;
create unique index if not exists submissions_request_key_key
on public.submissions (request_key)
where request_key is not null;
create unique index if not exists projects_company_normalized_name_key
on public.projects (company_id, lower(btrim(name)));
@@ -0,0 +1,27 @@
-- Auto-add subcontractor to project_members when a PO is created or updated
-- so RLS allows them to read all tasks in their assigned project.
create or replace function public.sync_subcontractor_project_member()
returns trigger as $$
begin
if new.profile_id is not null and new.project_id is not null then
insert into public.project_members (project_id, profile_id)
values (new.project_id, new.profile_id)
on conflict (project_id, profile_id) do nothing;
end if;
return new;
end;
$$ language plpgsql security definer;
drop trigger if exists trg_sync_subcontractor_project_member on public.subcontractor_payments;
create trigger trg_sync_subcontractor_project_member
after insert or update of profile_id, project_id
on public.subcontractor_payments
for each row execute function public.sync_subcontractor_project_member();
-- Backfill existing POs
insert into public.project_members (project_id, profile_id)
select distinct project_id, profile_id
from public.subcontractor_payments
where profile_id is not null and project_id is not null
on conflict (project_id, profile_id) do nothing;
@@ -0,0 +1,5 @@
-- Remove allowed MIME type restrictions from submissions and deliveries buckets
-- so clients and team can upload any file type (ZIP, etc.)
update storage.buckets
set allowed_mime_types = null
where id in ('submissions', 'deliveries');
@@ -0,0 +1,20 @@
-- Allow clients to delete tasks in their own company's projects.
-- Previously only team members could delete tasks (no client delete policy existed),
-- so client-side "Delete Request" silently failed with 0 rows deleted.
drop policy if exists "Client deletes company tasks" on public.tasks;
create policy "Client deletes company tasks" on public.tasks
for delete using (
get_my_role() = 'client'
and project_id in (
select id from public.projects where has_company_access(company_id)
)
);
-- Also allow clients to delete projects (needed when the last task in a project is deleted).
drop policy if exists "Client deletes company projects" on public.projects;
create policy "Client deletes company projects" on public.projects
for delete using (
get_my_role() = 'client'
and has_company_access(company_id)
);
@@ -1,13 +0,0 @@
-- Add cover page fields to brand_books table
ALTER TABLE brand_books
ADD COLUMN IF NOT EXISTS creation_date date,
ADD COLUMN IF NOT EXISTS revision_date date,
ADD COLUMN IF NOT EXISTS customer_name text,
ADD COLUMN IF NOT EXISTS customer_address text,
ADD COLUMN IF NOT EXISTS project_logo_path text,
ADD COLUMN IF NOT EXISTS client_logo_url text,
ADD COLUMN IF NOT EXISTS client_contact_name text,
ADD COLUMN IF NOT EXISTS client_contact_email text,
ADD COLUMN IF NOT EXISTS client_contact_phone text,
ADD COLUMN IF NOT EXISTS approved_date date,
ADD COLUMN IF NOT EXISTS approval_notes text;
@@ -1,4 +0,0 @@
-- Add template and inventory map fields to brand_books table
ALTER TABLE brand_books
ADD COLUMN IF NOT EXISTS template text DEFAULT 'fourge',
ADD COLUMN IF NOT EXISTS inventory_map_path text;
@@ -1,29 +0,0 @@
-- Add brand book / cover page fields to companies
ALTER TABLE companies
ADD COLUMN IF NOT EXISTS address text,
ADD COLUMN IF NOT EXISTS contact_name text,
ADD COLUMN IF NOT EXISTS contact_email text,
ADD COLUMN IF NOT EXISTS contact_phone text,
ADD COLUMN IF NOT EXISTS client_logo_url text;
-- Create public bucket for company logos
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM storage.buckets WHERE id = 'company-logos') THEN
INSERT INTO storage.buckets (id, name, public) VALUES ('company-logos', 'company-logos', true);
END IF;
END $$;
-- Storage policies for company-logos
DROP POLICY IF EXISTS "Authenticated users can manage company logos" ON storage.objects;
DROP POLICY IF EXISTS "Public can read company logos" ON storage.objects;
CREATE POLICY "Authenticated users can manage company logos"
ON storage.objects FOR ALL
TO authenticated
USING (bucket_id = 'company-logos')
WITH CHECK (bucket_id = 'company-logos');
CREATE POLICY "Public can read company logos"
ON storage.objects FOR SELECT
USING (bucket_id = 'company-logos');
-17
View File
@@ -1,17 +0,0 @@
-- ============================================================
-- Migration: Add price_type to company_prices (new vs revision)
-- Run in Supabase → SQL Editor → Run
-- ============================================================
-- Add price_type column (existing rows default to 'new')
alter table public.company_prices
add column price_type text not null default 'new'
check (price_type in ('new', 'revision'));
-- Drop old unique constraint and add new one that includes price_type
alter table public.company_prices
drop constraint company_prices_company_id_service_type_key;
alter table public.company_prices
add constraint company_prices_company_id_service_type_price_type_key
unique (company_id, service_type, price_type);
@@ -1,15 +0,0 @@
-- ============================================================
-- Migration: Add revision billing tracking
-- Run in Supabase → SQL Editor → Run
-- ============================================================
-- Add revision_type and invoiced to submissions
alter table public.submissions
add column revision_type text check (revision_type in ('fourge_error', 'client_revision'));
alter table public.submissions
add column invoiced boolean not null default false;
-- Add submission_id to invoice_items (links a line item to a specific revision)
alter table public.invoice_items
add column submission_id uuid references public.submissions(id) on delete set null;