Refactor: clients → companies schema v2
This commit is contained in:
Executable
+1
@@ -0,0 +1 @@
|
||||
v2.78.1
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
v2.188.1
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
postgresql://postgres.fqflxxqvennhvoeywrdw@aws-0-us-west-2.pooler.supabase.com:5432/postgres
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
17.6.1.084
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
fqflxxqvennhvoeywrdw
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
v14.4
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
fix-optimized-search-function
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
v1.37.7
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
|
||||
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY');
|
||||
const FROM = 'Fourge Branding <hello@fourgebranding.com>';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const { type, to, data } = await req.json();
|
||||
|
||||
let subject = '';
|
||||
let html = '';
|
||||
|
||||
if (type === 'new_request') {
|
||||
subject = `New Request: ${data.serviceType} — ${data.clientName}`;
|
||||
html = `
|
||||
<h2>New Request Received</h2>
|
||||
<p><strong>From:</strong> ${data.clientName} (${data.clientEmail})</p>
|
||||
<p><strong>Company:</strong> ${data.company || '—'}</p>
|
||||
<p><strong>Service:</strong> ${data.serviceType}</p>
|
||||
<p><strong>Project:</strong> ${data.projectName}</p>
|
||||
${data.deadline ? `<p><strong>Deadline:</strong> ${data.deadline}</p>` : ''}
|
||||
<hr />
|
||||
<p><strong>Description:</strong></p>
|
||||
<p>${data.description}</p>
|
||||
<br />
|
||||
<a href="https://portal.fourgebranding.com/tasks/${data.taskId}" style="background:#F5A523;color:#1a1a1a;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">View Job</a>
|
||||
`;
|
||||
}
|
||||
|
||||
else if (type === 'sent_to_client') {
|
||||
subject = `Your ${data.serviceType} is ready for review!`;
|
||||
html = `
|
||||
<h2>Hi ${data.clientFirstName},</h2>
|
||||
<p>Your <strong>${data.serviceType}</strong> for <strong>${data.projectName}</strong> is ready for review.</p>
|
||||
${data.message ? `<p>${data.message}</p>` : ''}
|
||||
<p>Please log in to the portal to review and approve or request changes.</p>
|
||||
<br />
|
||||
<a href="https://portal.fourgebranding.com/my-requests/${data.taskId}" style="background:#F5A523;color:#1a1a1a;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">Review Your Work</a>
|
||||
<br /><br />
|
||||
<p style="color:#666;font-size:13px;">— The Fourge Branding Team</p>
|
||||
`;
|
||||
}
|
||||
|
||||
else if (type === 'revision_submitted') {
|
||||
subject = `Revision Request: ${data.serviceType} — ${data.clientName}`;
|
||||
html = `
|
||||
<h2>Revision Requested</h2>
|
||||
<p><strong>From:</strong> ${data.clientName}</p>
|
||||
<p><strong>Job:</strong> ${data.serviceType} — ${data.projectName}</p>
|
||||
<p><strong>New Version:</strong> ${data.version}</p>
|
||||
${data.deadline ? `<p><strong>New Deadline:</strong> ${data.deadline}</p>` : ''}
|
||||
<hr />
|
||||
<p><strong>Requested changes:</strong></p>
|
||||
<p>${data.description}</p>
|
||||
<br />
|
||||
<a href="https://portal.fourgebranding.com/tasks/${data.taskId}" style="background:#F5A523;color:#1a1a1a;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">View Job</a>
|
||||
`;
|
||||
}
|
||||
|
||||
else if (type === 'client_approved') {
|
||||
subject = `Approved! ${data.serviceType} — ${data.clientName}`;
|
||||
html = `
|
||||
<h2>Job Approved ✓</h2>
|
||||
<p><strong>${data.clientName}</strong> has approved <strong>${data.serviceType}</strong> for <strong>${data.projectName}</strong>.</p>
|
||||
<p>This job is now complete.</p>
|
||||
<br />
|
||||
<a href="https://portal.fourgebranding.com/tasks/${data.taskId}" style="background:#F5A523;color:#1a1a1a;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">View Job</a>
|
||||
`;
|
||||
}
|
||||
|
||||
const res = await fetch('https://api.resend.com/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${RESEND_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ from: FROM, to, subject, html }),
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: res.ok ? 200 : 400,
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
return new Response(JSON.stringify({ error: err.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
});
|
||||
Executable
+276
@@ -0,0 +1,276 @@
|
||||
-- ============================================================
|
||||
-- Fourge Branding Portal — Database Schema v2
|
||||
-- Paste this entire file into Supabase → SQL Editor → Run
|
||||
-- ============================================================
|
||||
|
||||
-- Companies (client companies — created and managed by team)
|
||||
create table public.companies (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
name text not null,
|
||||
email text default '',
|
||||
phone text default '',
|
||||
created_at timestamptz default now() not null
|
||||
);
|
||||
alter table public.companies enable row level security;
|
||||
|
||||
-- Profiles (extends auth.users — auto-created on signup)
|
||||
create table public.profiles (
|
||||
id uuid references auth.users on delete cascade primary key,
|
||||
name text not null default '',
|
||||
email text default '',
|
||||
role text not null check (role in ('team', 'client')) default 'client',
|
||||
company_id uuid references public.companies(id) on delete set null,
|
||||
created_at timestamptz default now() not null
|
||||
);
|
||||
alter table public.profiles enable row level security;
|
||||
|
||||
-- Projects (belong to a company)
|
||||
create table public.projects (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
company_id uuid references public.companies(id) on delete cascade not null,
|
||||
name text not null,
|
||||
description text default '',
|
||||
status text not null check (status in ('active', 'completed')) default 'active',
|
||||
created_at timestamptz default now() not null
|
||||
);
|
||||
alter table public.projects enable row level security;
|
||||
|
||||
-- Tasks (belong to a project)
|
||||
create table public.tasks (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
project_id uuid references public.projects(id) on delete cascade not null,
|
||||
title text not null,
|
||||
assigned_to uuid references public.profiles(id) on delete set null,
|
||||
assigned_name text,
|
||||
status text not null check (status in ('not_started', 'in_progress', 'on_hold', 'client_review', 'client_approved')) default 'not_started',
|
||||
current_version integer default 0 not null,
|
||||
invoiced boolean default false,
|
||||
submitted_at timestamptz default now() not null,
|
||||
completed_at timestamptz
|
||||
);
|
||||
alter table public.tasks enable row level security;
|
||||
|
||||
-- Submissions (each version of a request)
|
||||
create table public.submissions (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
task_id uuid references public.tasks(id) on delete cascade not null,
|
||||
version_number integer not null,
|
||||
type text not null check (type in ('initial', 'revision')) default 'initial',
|
||||
service_type text default '',
|
||||
deadline date,
|
||||
description text default '',
|
||||
submitted_by uuid references public.profiles(id) on delete set null,
|
||||
submitted_by_name text default '',
|
||||
submitted_at timestamptz default now() not null
|
||||
);
|
||||
alter table public.submissions enable row level security;
|
||||
|
||||
-- Submission Files
|
||||
create table public.submission_files (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
submission_id uuid references public.submissions(id) on delete cascade not null,
|
||||
name text not null,
|
||||
storage_path text,
|
||||
size bigint default 0
|
||||
);
|
||||
alter table public.submission_files enable row level security;
|
||||
|
||||
-- Deliveries (work sent to client)
|
||||
create table public.deliveries (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
submission_id uuid references public.submissions(id) on delete cascade not null unique,
|
||||
sent_at timestamptz default now() not null,
|
||||
sent_by text default '',
|
||||
message text default ''
|
||||
);
|
||||
alter table public.deliveries enable row level security;
|
||||
|
||||
-- Delivery Files
|
||||
create table public.delivery_files (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
delivery_id uuid references public.deliveries(id) on delete cascade not null,
|
||||
name text not null,
|
||||
storage_path text,
|
||||
size bigint default 0
|
||||
);
|
||||
alter table public.delivery_files enable row level security;
|
||||
|
||||
-- Company Prices (per service type, per company)
|
||||
create table public.company_prices (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
company_id uuid references public.companies(id) on delete cascade not null,
|
||||
service_type text not null,
|
||||
price numeric(10,2) default 0,
|
||||
unique(company_id, service_type)
|
||||
);
|
||||
alter table public.company_prices enable row level security;
|
||||
|
||||
-- Invoices
|
||||
create table public.invoices (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
company_id uuid references public.companies(id) on delete cascade not null,
|
||||
invoice_number text not null,
|
||||
status text not null check (status in ('draft', 'sent', 'paid')) default 'draft',
|
||||
invoice_date date default current_date,
|
||||
due_date date,
|
||||
total numeric(10,2) default 0,
|
||||
notes text default '',
|
||||
created_by uuid references public.profiles(id) on delete set null,
|
||||
created_at timestamptz default now() not null
|
||||
);
|
||||
alter table public.invoices enable row level security;
|
||||
|
||||
-- Invoice Items
|
||||
create table public.invoice_items (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
invoice_id uuid references public.invoices(id) on delete cascade not null,
|
||||
task_id uuid references public.tasks(id) on delete set null,
|
||||
description text not null,
|
||||
quantity numeric(10,2) default 1,
|
||||
unit_price numeric(10,2) default 0,
|
||||
created_at timestamptz default now() not null
|
||||
);
|
||||
alter table public.invoice_items enable row level security;
|
||||
|
||||
-- ============================================================
|
||||
-- Helpers
|
||||
-- ============================================================
|
||||
|
||||
create or replace function public.get_my_role()
|
||||
returns text as $$
|
||||
select role from public.profiles where id = auth.uid();
|
||||
$$ language sql security definer stable;
|
||||
|
||||
create or replace function public.get_my_company_id()
|
||||
returns uuid as $$
|
||||
select company_id from public.profiles where id = auth.uid();
|
||||
$$ language sql security definer stable;
|
||||
|
||||
-- ============================================================
|
||||
-- RLS Policies
|
||||
-- ============================================================
|
||||
|
||||
-- Companies
|
||||
create policy "Team all companies" on public.companies for all using (get_my_role() = 'team');
|
||||
create policy "Client reads own company" on public.companies for select using (id = get_my_company_id());
|
||||
|
||||
-- Profiles
|
||||
create policy "Own profile select" on public.profiles for select using (id = auth.uid());
|
||||
create policy "Team reads all profiles" on public.profiles for select using (get_my_role() = 'team');
|
||||
create policy "Own profile update" on public.profiles for update using (id = auth.uid());
|
||||
create policy "Team update profiles" on public.profiles for update using (get_my_role() = 'team');
|
||||
|
||||
-- Projects
|
||||
create policy "Team all projects" on public.projects for all using (get_my_role() = 'team');
|
||||
create policy "Client reads company projects" on public.projects for select using (company_id = get_my_company_id());
|
||||
create policy "Client inserts company projects" on public.projects for insert with check (company_id = get_my_company_id());
|
||||
|
||||
-- Tasks
|
||||
create policy "Team all tasks" on public.tasks for all using (get_my_role() = 'team');
|
||||
create policy "Client reads company tasks" on public.tasks for select using (
|
||||
project_id in (select id from public.projects where company_id = get_my_company_id())
|
||||
);
|
||||
create policy "Client insert task" on public.tasks for insert with check (
|
||||
project_id in (select id from public.projects where company_id = get_my_company_id())
|
||||
);
|
||||
create policy "Client updates company tasks" on public.tasks for update using (
|
||||
project_id in (select id from public.projects where company_id = get_my_company_id())
|
||||
);
|
||||
|
||||
-- Submissions
|
||||
create policy "Team all submissions" on public.submissions for all using (get_my_role() = 'team');
|
||||
create policy "Client reads 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 p.company_id = get_my_company_id()
|
||||
)
|
||||
);
|
||||
create policy "Client inserts submissions" on public.submissions for insert with check (submitted_by = auth.uid());
|
||||
|
||||
-- Submission Files
|
||||
create policy "Team all submission_files" on public.submission_files for all using (get_my_role() = 'team');
|
||||
create policy "Client reads 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 p.company_id = get_my_company_id()
|
||||
)
|
||||
);
|
||||
create policy "Client inserts submission_files" on public.submission_files for insert with check (true);
|
||||
|
||||
-- Deliveries
|
||||
create policy "Team all deliveries" on public.deliveries for all using (get_my_role() = 'team');
|
||||
create policy "Client reads 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 p.company_id = get_my_company_id()
|
||||
)
|
||||
);
|
||||
|
||||
-- Delivery Files
|
||||
create policy "Team all delivery_files" on public.delivery_files for all using (get_my_role() = 'team');
|
||||
create policy "Client reads 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 p.company_id = get_my_company_id()
|
||||
)
|
||||
);
|
||||
|
||||
-- Company Prices
|
||||
create policy "Team all company_prices" on public.company_prices for all using (get_my_role() = 'team');
|
||||
create policy "Client reads own company prices" on public.company_prices for select using (company_id = get_my_company_id());
|
||||
|
||||
-- Invoices
|
||||
create policy "Team all invoices" on public.invoices for all using (get_my_role() = 'team');
|
||||
create policy "Client reads company invoices" on public.invoices for select using (company_id = get_my_company_id());
|
||||
|
||||
-- Invoice Items
|
||||
create policy "Team all invoice_items" on public.invoice_items for all using (get_my_role() = 'team');
|
||||
create policy "Client reads company invoice_items" on public.invoice_items for select using (
|
||||
invoice_id in (select id from public.invoices where company_id = get_my_company_id())
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Storage Buckets
|
||||
-- ============================================================
|
||||
insert into storage.buckets (id, name, public) values ('submissions', 'submissions', false);
|
||||
insert into storage.buckets (id, name, public) values ('deliveries', 'deliveries', false);
|
||||
|
||||
create policy "Auth users upload to submissions" on storage.objects
|
||||
for insert to authenticated with check (bucket_id = 'submissions');
|
||||
create policy "Auth users read submissions" on storage.objects
|
||||
for select to authenticated using (bucket_id = 'submissions');
|
||||
|
||||
create policy "Team upload deliveries" on storage.objects
|
||||
for insert to authenticated with check (bucket_id = 'deliveries' and get_my_role() = 'team');
|
||||
create policy "Auth users read deliveries" on storage.objects
|
||||
for select to authenticated using (bucket_id = 'deliveries');
|
||||
|
||||
-- ============================================================
|
||||
-- Trigger: auto-create profile on signup
|
||||
-- Team assigns company_id later via the Companies page
|
||||
-- ============================================================
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger as $$
|
||||
begin
|
||||
insert into public.profiles (id, name, email, role)
|
||||
values (
|
||||
new.id,
|
||||
coalesce(new.raw_user_meta_data->>'name', ''),
|
||||
new.email,
|
||||
coalesce(new.raw_user_meta_data->>'role', 'client')
|
||||
);
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute procedure public.handle_new_user();
|
||||
Reference in New Issue
Block a user