Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
f1b1cc8f6d
commit
dc787df0aa
@@ -14,16 +14,470 @@ export type Database = {
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
[_ in never]: never
|
||||
cases: {
|
||||
Row: {
|
||||
assigned_attorney_id: string | null
|
||||
case_number: string
|
||||
client_id: string
|
||||
closed_at: string | null
|
||||
created_at: string
|
||||
created_by: string | null
|
||||
default_hourly_rate: number | null
|
||||
description: string | null
|
||||
id: string
|
||||
opened_at: string
|
||||
practice_area: string | null
|
||||
status: Database["public"]["Enums"]["case_status"]
|
||||
title: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
assigned_attorney_id?: string | null
|
||||
case_number: string
|
||||
client_id: string
|
||||
closed_at?: string | null
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
default_hourly_rate?: number | null
|
||||
description?: string | null
|
||||
id?: string
|
||||
opened_at?: string
|
||||
practice_area?: string | null
|
||||
status?: Database["public"]["Enums"]["case_status"]
|
||||
title: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
assigned_attorney_id?: string | null
|
||||
case_number?: string
|
||||
client_id?: string
|
||||
closed_at?: string | null
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
default_hourly_rate?: number | null
|
||||
description?: string | null
|
||||
id?: string
|
||||
opened_at?: string
|
||||
practice_area?: string | null
|
||||
status?: Database["public"]["Enums"]["case_status"]
|
||||
title?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "cases_client_id_fkey"
|
||||
columns: ["client_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "clients"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
clients: {
|
||||
Row: {
|
||||
address_line1: string | null
|
||||
address_line2: string | null
|
||||
board_members: Json
|
||||
city: string | null
|
||||
client_type: Database["public"]["Enums"]["client_type"]
|
||||
created_at: string
|
||||
created_by: string | null
|
||||
id: string
|
||||
management_company: string | null
|
||||
name: string
|
||||
notes: string | null
|
||||
num_units: number | null
|
||||
postal_code: string | null
|
||||
primary_contact_email: string | null
|
||||
primary_contact_name: string | null
|
||||
primary_contact_phone: string | null
|
||||
state: string | null
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
address_line1?: string | null
|
||||
address_line2?: string | null
|
||||
board_members?: Json
|
||||
city?: string | null
|
||||
client_type?: Database["public"]["Enums"]["client_type"]
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
id?: string
|
||||
management_company?: string | null
|
||||
name: string
|
||||
notes?: string | null
|
||||
num_units?: number | null
|
||||
postal_code?: string | null
|
||||
primary_contact_email?: string | null
|
||||
primary_contact_name?: string | null
|
||||
primary_contact_phone?: string | null
|
||||
state?: string | null
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
address_line1?: string | null
|
||||
address_line2?: string | null
|
||||
board_members?: Json
|
||||
city?: string | null
|
||||
client_type?: Database["public"]["Enums"]["client_type"]
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
id?: string
|
||||
management_company?: string | null
|
||||
name?: string
|
||||
notes?: string | null
|
||||
num_units?: number | null
|
||||
postal_code?: string | null
|
||||
primary_contact_email?: string | null
|
||||
primary_contact_name?: string | null
|
||||
primary_contact_phone?: string | null
|
||||
state?: string | null
|
||||
updated_at?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
documents: {
|
||||
Row: {
|
||||
case_id: string
|
||||
created_at: string
|
||||
description: string | null
|
||||
id: string
|
||||
mime_type: string | null
|
||||
name: string
|
||||
size_bytes: number | null
|
||||
storage_path: string
|
||||
uploaded_by: string | null
|
||||
}
|
||||
Insert: {
|
||||
case_id: string
|
||||
created_at?: string
|
||||
description?: string | null
|
||||
id?: string
|
||||
mime_type?: string | null
|
||||
name: string
|
||||
size_bytes?: number | null
|
||||
storage_path: string
|
||||
uploaded_by?: string | null
|
||||
}
|
||||
Update: {
|
||||
case_id?: string
|
||||
created_at?: string
|
||||
description?: string | null
|
||||
id?: string
|
||||
mime_type?: string | null
|
||||
name?: string
|
||||
size_bytes?: number | null
|
||||
storage_path?: string
|
||||
uploaded_by?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "documents_case_id_fkey"
|
||||
columns: ["case_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "cases"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
expenses: {
|
||||
Row: {
|
||||
amount: number
|
||||
billable: boolean
|
||||
case_id: string
|
||||
created_at: string
|
||||
description: string
|
||||
expense_date: string
|
||||
id: string
|
||||
invoice_id: string | null
|
||||
receipt_storage_path: string | null
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
amount: number
|
||||
billable?: boolean
|
||||
case_id: string
|
||||
created_at?: string
|
||||
description: string
|
||||
expense_date?: string
|
||||
id?: string
|
||||
invoice_id?: string | null
|
||||
receipt_storage_path?: string | null
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
amount?: number
|
||||
billable?: boolean
|
||||
case_id?: string
|
||||
created_at?: string
|
||||
description?: string
|
||||
expense_date?: string
|
||||
id?: string
|
||||
invoice_id?: string | null
|
||||
receipt_storage_path?: string | null
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "expenses_case_id_fkey"
|
||||
columns: ["case_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "cases"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "expenses_invoice_fk"
|
||||
columns: ["invoice_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "invoices"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
invoices: {
|
||||
Row: {
|
||||
amount_paid: number
|
||||
case_id: string | null
|
||||
client_id: string
|
||||
created_at: string
|
||||
created_by: string | null
|
||||
due_date: string | null
|
||||
id: string
|
||||
invoice_number: string
|
||||
issue_date: string
|
||||
notes: string | null
|
||||
paid_at: string | null
|
||||
status: Database["public"]["Enums"]["invoice_status"]
|
||||
subtotal: number
|
||||
tax: number
|
||||
total: number
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
amount_paid?: number
|
||||
case_id?: string | null
|
||||
client_id: string
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
due_date?: string | null
|
||||
id?: string
|
||||
invoice_number: string
|
||||
issue_date?: string
|
||||
notes?: string | null
|
||||
paid_at?: string | null
|
||||
status?: Database["public"]["Enums"]["invoice_status"]
|
||||
subtotal?: number
|
||||
tax?: number
|
||||
total?: number
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
amount_paid?: number
|
||||
case_id?: string | null
|
||||
client_id?: string
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
due_date?: string | null
|
||||
id?: string
|
||||
invoice_number?: string
|
||||
issue_date?: string
|
||||
notes?: string | null
|
||||
paid_at?: string | null
|
||||
status?: Database["public"]["Enums"]["invoice_status"]
|
||||
subtotal?: number
|
||||
tax?: number
|
||||
total?: number
|
||||
updated_at?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "invoices_case_id_fkey"
|
||||
columns: ["case_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "cases"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "invoices_client_id_fkey"
|
||||
columns: ["client_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "clients"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
profiles: {
|
||||
Row: {
|
||||
created_at: string
|
||||
email: string
|
||||
full_name: string
|
||||
id: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
email?: string
|
||||
full_name?: string
|
||||
id: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
email?: string
|
||||
full_name?: string
|
||||
id?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
status_updates: {
|
||||
Row: {
|
||||
body: string
|
||||
case_id: string
|
||||
created_at: string
|
||||
created_by: string | null
|
||||
id: string
|
||||
report_storage_path: string | null
|
||||
title: string
|
||||
}
|
||||
Insert: {
|
||||
body: string
|
||||
case_id: string
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
id?: string
|
||||
report_storage_path?: string | null
|
||||
title: string
|
||||
}
|
||||
Update: {
|
||||
body?: string
|
||||
case_id?: string
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
id?: string
|
||||
report_storage_path?: string | null
|
||||
title?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "status_updates_case_id_fkey"
|
||||
columns: ["case_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "cases"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
time_entries: {
|
||||
Row: {
|
||||
billable: boolean
|
||||
case_id: string
|
||||
created_at: string
|
||||
description: string
|
||||
hourly_rate: number
|
||||
hours: number
|
||||
id: string
|
||||
invoice_id: string | null
|
||||
updated_at: string
|
||||
user_id: string
|
||||
work_date: string
|
||||
}
|
||||
Insert: {
|
||||
billable?: boolean
|
||||
case_id: string
|
||||
created_at?: string
|
||||
description: string
|
||||
hourly_rate?: number
|
||||
hours: number
|
||||
id?: string
|
||||
invoice_id?: string | null
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
work_date?: string
|
||||
}
|
||||
Update: {
|
||||
billable?: boolean
|
||||
case_id?: string
|
||||
created_at?: string
|
||||
description?: string
|
||||
hourly_rate?: number
|
||||
hours?: number
|
||||
id?: string
|
||||
invoice_id?: string | null
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
work_date?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "time_entries_case_id_fkey"
|
||||
columns: ["case_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "cases"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "time_entries_invoice_fk"
|
||||
columns: ["invoice_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "invoices"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
user_roles: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: string
|
||||
role: Database["public"]["Enums"]["app_role"]
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
role: Database["public"]["Enums"]["app_role"]
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
role?: Database["public"]["Enums"]["app_role"]
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
[_ in never]: never
|
||||
can_access_case: {
|
||||
Args: { _case_id: string; _user_id: string }
|
||||
Returns: boolean
|
||||
}
|
||||
has_role: {
|
||||
Args: {
|
||||
_role: Database["public"]["Enums"]["app_role"]
|
||||
_user_id: string
|
||||
}
|
||||
Returns: boolean
|
||||
}
|
||||
is_admin: { Args: { _user_id: string }; Returns: boolean }
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
app_role: "admin" | "attorney" | "staff"
|
||||
case_status:
|
||||
| "intake"
|
||||
| "active"
|
||||
| "on_hold"
|
||||
| "closed_won"
|
||||
| "closed_lost"
|
||||
| "closed"
|
||||
client_type: "hoa" | "individual" | "business"
|
||||
invoice_status: "draft" | "sent" | "paid" | "overdue" | "void"
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
@@ -150,6 +604,18 @@ export type CompositeTypes<
|
||||
|
||||
export const Constants = {
|
||||
public: {
|
||||
Enums: {},
|
||||
Enums: {
|
||||
app_role: ["admin", "attorney", "staff"],
|
||||
case_status: [
|
||||
"intake",
|
||||
"active",
|
||||
"on_hold",
|
||||
"closed_won",
|
||||
"closed_lost",
|
||||
"closed",
|
||||
],
|
||||
client_type: ["hoa", "individual", "business"],
|
||||
invoice_status: ["draft", "sent", "paid", "overdue", "void"],
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
-- =========================================
|
||||
-- ENUMS
|
||||
-- =========================================
|
||||
CREATE TYPE public.app_role AS ENUM ('admin', 'attorney', 'staff');
|
||||
CREATE TYPE public.client_type AS ENUM ('hoa', 'individual', 'business');
|
||||
CREATE TYPE public.case_status AS ENUM ('intake', 'active', 'on_hold', 'closed_won', 'closed_lost', 'closed');
|
||||
CREATE TYPE public.invoice_status AS ENUM ('draft', 'sent', 'paid', 'overdue', 'void');
|
||||
|
||||
-- =========================================
|
||||
-- UPDATED-AT TRIGGER FN
|
||||
-- =========================================
|
||||
CREATE OR REPLACE FUNCTION public.tg_set_updated_at()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql SET search_path = public AS $$
|
||||
BEGIN NEW.updated_at = now(); RETURN NEW; END;
|
||||
$$;
|
||||
|
||||
-- =========================================
|
||||
-- PROFILES (display info for auth users)
|
||||
-- =========================================
|
||||
CREATE TABLE public.profiles (
|
||||
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
full_name TEXT NOT NULL DEFAULT '',
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE TRIGGER profiles_updated_at BEFORE UPDATE ON public.profiles
|
||||
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
||||
|
||||
-- Auto-create profile on signup
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
|
||||
BEGIN
|
||||
INSERT INTO public.profiles (id, email, full_name)
|
||||
VALUES (NEW.id, NEW.email, COALESCE(NEW.raw_user_meta_data->>'full_name', ''));
|
||||
-- First-ever user becomes admin
|
||||
IF (SELECT count(*) FROM public.user_roles) = 0 THEN
|
||||
INSERT INTO public.user_roles (user_id, role) VALUES (NEW.id, 'admin');
|
||||
ELSE
|
||||
INSERT INTO public.user_roles (user_id, role) VALUES (NEW.id, 'staff');
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- =========================================
|
||||
-- USER ROLES
|
||||
-- =========================================
|
||||
CREATE TABLE public.user_roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
role public.app_role NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (user_id, role)
|
||||
);
|
||||
ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.has_role(_user_id UUID, _role public.app_role)
|
||||
RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
|
||||
SELECT EXISTS (SELECT 1 FROM public.user_roles WHERE user_id = _user_id AND role = _role)
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.is_admin(_user_id UUID)
|
||||
RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
|
||||
SELECT EXISTS (SELECT 1 FROM public.user_roles WHERE user_id = _user_id AND role = 'admin')
|
||||
$$;
|
||||
|
||||
-- Now create the trigger (after user_roles exists)
|
||||
CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
||||
|
||||
-- =========================================
|
||||
-- CLIENTS
|
||||
-- =========================================
|
||||
CREATE TABLE public.clients (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
client_type public.client_type NOT NULL DEFAULT 'hoa',
|
||||
name TEXT NOT NULL,
|
||||
primary_contact_name TEXT,
|
||||
primary_contact_email TEXT,
|
||||
primary_contact_phone TEXT,
|
||||
address_line1 TEXT,
|
||||
address_line2 TEXT,
|
||||
city TEXT,
|
||||
state TEXT,
|
||||
postal_code TEXT,
|
||||
notes TEXT,
|
||||
-- HOA-specific
|
||||
management_company TEXT,
|
||||
num_units INTEGER,
|
||||
board_members JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
ALTER TABLE public.clients ENABLE ROW LEVEL SECURITY;
|
||||
CREATE TRIGGER clients_updated_at BEFORE UPDATE ON public.clients
|
||||
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
||||
|
||||
-- =========================================
|
||||
-- CASES
|
||||
-- =========================================
|
||||
CREATE TABLE public.cases (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
client_id UUID NOT NULL REFERENCES public.clients(id) ON DELETE RESTRICT,
|
||||
case_number TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
practice_area TEXT,
|
||||
status public.case_status NOT NULL DEFAULT 'intake',
|
||||
assigned_attorney_id UUID REFERENCES auth.users(id),
|
||||
opened_at DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
closed_at DATE,
|
||||
default_hourly_rate NUMERIC(10,2),
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
ALTER TABLE public.cases ENABLE ROW LEVEL SECURITY;
|
||||
CREATE TRIGGER cases_updated_at BEFORE UPDATE ON public.cases
|
||||
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
||||
CREATE INDEX idx_cases_client ON public.cases(client_id);
|
||||
CREATE INDEX idx_cases_attorney ON public.cases(assigned_attorney_id);
|
||||
|
||||
-- Helper: can current user access this case?
|
||||
CREATE OR REPLACE FUNCTION public.can_access_case(_case_id UUID, _user_id UUID)
|
||||
RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
|
||||
SELECT
|
||||
public.has_role(_user_id, 'admin')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM public.cases c
|
||||
WHERE c.id = _case_id
|
||||
AND (c.assigned_attorney_id = _user_id OR c.created_by = _user_id)
|
||||
)
|
||||
$$;
|
||||
|
||||
-- =========================================
|
||||
-- DOCUMENTS
|
||||
-- =========================================
|
||||
CREATE TABLE public.documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
case_id UUID NOT NULL REFERENCES public.cases(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
storage_path TEXT NOT NULL,
|
||||
mime_type TEXT,
|
||||
size_bytes BIGINT,
|
||||
description TEXT,
|
||||
uploaded_by UUID REFERENCES auth.users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;
|
||||
CREATE INDEX idx_documents_case ON public.documents(case_id);
|
||||
|
||||
-- =========================================
|
||||
-- STATUS UPDATES (case log entries)
|
||||
-- =========================================
|
||||
CREATE TABLE public.status_updates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
case_id UUID NOT NULL REFERENCES public.cases(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
report_storage_path TEXT,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
ALTER TABLE public.status_updates ENABLE ROW LEVEL SECURITY;
|
||||
CREATE INDEX idx_status_updates_case ON public.status_updates(case_id);
|
||||
|
||||
-- =========================================
|
||||
-- TIME ENTRIES
|
||||
-- =========================================
|
||||
CREATE TABLE public.time_entries (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
case_id UUID NOT NULL REFERENCES public.cases(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
work_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
hours NUMERIC(6,2) NOT NULL CHECK (hours > 0),
|
||||
hourly_rate NUMERIC(10,2) NOT NULL DEFAULT 0,
|
||||
description TEXT NOT NULL,
|
||||
billable BOOLEAN NOT NULL DEFAULT true,
|
||||
invoice_id UUID,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
ALTER TABLE public.time_entries ENABLE ROW LEVEL SECURITY;
|
||||
CREATE TRIGGER time_entries_updated_at BEFORE UPDATE ON public.time_entries
|
||||
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
||||
CREATE INDEX idx_time_case ON public.time_entries(case_id);
|
||||
CREATE INDEX idx_time_user ON public.time_entries(user_id);
|
||||
CREATE INDEX idx_time_invoice ON public.time_entries(invoice_id);
|
||||
|
||||
-- =========================================
|
||||
-- EXPENSES
|
||||
-- =========================================
|
||||
CREATE TABLE public.expenses (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
case_id UUID NOT NULL REFERENCES public.cases(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
expense_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
description TEXT NOT NULL,
|
||||
amount NUMERIC(10,2) NOT NULL CHECK (amount >= 0),
|
||||
billable BOOLEAN NOT NULL DEFAULT true,
|
||||
receipt_storage_path TEXT,
|
||||
invoice_id UUID,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
ALTER TABLE public.expenses ENABLE ROW LEVEL SECURITY;
|
||||
CREATE TRIGGER expenses_updated_at BEFORE UPDATE ON public.expenses
|
||||
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
||||
CREATE INDEX idx_expenses_case ON public.expenses(case_id);
|
||||
CREATE INDEX idx_expenses_invoice ON public.expenses(invoice_id);
|
||||
|
||||
-- =========================================
|
||||
-- INVOICES
|
||||
-- =========================================
|
||||
CREATE TABLE public.invoices (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
invoice_number TEXT NOT NULL UNIQUE,
|
||||
client_id UUID NOT NULL REFERENCES public.clients(id) ON DELETE RESTRICT,
|
||||
case_id UUID REFERENCES public.cases(id) ON DELETE SET NULL,
|
||||
status public.invoice_status NOT NULL DEFAULT 'draft',
|
||||
issue_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
due_date DATE,
|
||||
subtotal NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||
tax NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||
total NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||
amount_paid NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||
notes TEXT,
|
||||
paid_at TIMESTAMPTZ,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
ALTER TABLE public.invoices ENABLE ROW LEVEL SECURITY;
|
||||
CREATE TRIGGER invoices_updated_at BEFORE UPDATE ON public.invoices
|
||||
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
||||
CREATE INDEX idx_invoices_client ON public.invoices(client_id);
|
||||
CREATE INDEX idx_invoices_case ON public.invoices(case_id);
|
||||
|
||||
ALTER TABLE public.time_entries
|
||||
ADD CONSTRAINT time_entries_invoice_fk FOREIGN KEY (invoice_id) REFERENCES public.invoices(id) ON DELETE SET NULL;
|
||||
ALTER TABLE public.expenses
|
||||
ADD CONSTRAINT expenses_invoice_fk FOREIGN KEY (invoice_id) REFERENCES public.invoices(id) ON DELETE SET NULL;
|
||||
|
||||
-- =========================================
|
||||
-- RLS POLICIES
|
||||
-- =========================================
|
||||
|
||||
-- profiles: any authenticated user can read names; users can update their own
|
||||
CREATE POLICY "profiles_select_auth" ON public.profiles FOR SELECT TO authenticated USING (true);
|
||||
CREATE POLICY "profiles_update_own" ON public.profiles FOR UPDATE TO authenticated USING (auth.uid() = id);
|
||||
|
||||
-- user_roles: users can see their own roles; admins see all; only admins manage
|
||||
CREATE POLICY "user_roles_select_self" ON public.user_roles FOR SELECT TO authenticated
|
||||
USING (user_id = auth.uid() OR public.is_admin(auth.uid()));
|
||||
CREATE POLICY "user_roles_admin_insert" ON public.user_roles FOR INSERT TO authenticated
|
||||
WITH CHECK (public.is_admin(auth.uid()));
|
||||
CREATE POLICY "user_roles_admin_update" ON public.user_roles FOR UPDATE TO authenticated
|
||||
USING (public.is_admin(auth.uid()));
|
||||
CREATE POLICY "user_roles_admin_delete" ON public.user_roles FOR DELETE TO authenticated
|
||||
USING (public.is_admin(auth.uid()));
|
||||
|
||||
-- clients: any authenticated firm user can view clients (firm-wide).
|
||||
CREATE POLICY "clients_select_auth" ON public.clients FOR SELECT TO authenticated USING (true);
|
||||
CREATE POLICY "clients_insert_auth" ON public.clients FOR INSERT TO authenticated
|
||||
WITH CHECK (auth.uid() IS NOT NULL);
|
||||
CREATE POLICY "clients_update_auth" ON public.clients FOR UPDATE TO authenticated
|
||||
USING (public.is_admin(auth.uid()) OR created_by = auth.uid());
|
||||
CREATE POLICY "clients_delete_admin" ON public.clients FOR DELETE TO authenticated
|
||||
USING (public.is_admin(auth.uid()));
|
||||
|
||||
-- cases: only admin or assigned attorney/creator can see
|
||||
CREATE POLICY "cases_select_assigned" ON public.cases FOR SELECT TO authenticated
|
||||
USING (public.is_admin(auth.uid()) OR assigned_attorney_id = auth.uid() OR created_by = auth.uid());
|
||||
CREATE POLICY "cases_insert_auth" ON public.cases FOR INSERT TO authenticated
|
||||
WITH CHECK (auth.uid() IS NOT NULL);
|
||||
CREATE POLICY "cases_update_assigned" ON public.cases FOR UPDATE TO authenticated
|
||||
USING (public.is_admin(auth.uid()) OR assigned_attorney_id = auth.uid() OR created_by = auth.uid());
|
||||
CREATE POLICY "cases_delete_admin" ON public.cases FOR DELETE TO authenticated
|
||||
USING (public.is_admin(auth.uid()));
|
||||
|
||||
-- documents
|
||||
CREATE POLICY "documents_select_case" ON public.documents FOR SELECT TO authenticated
|
||||
USING (public.can_access_case(case_id, auth.uid()));
|
||||
CREATE POLICY "documents_insert_case" ON public.documents FOR INSERT TO authenticated
|
||||
WITH CHECK (public.can_access_case(case_id, auth.uid()));
|
||||
CREATE POLICY "documents_delete_case" ON public.documents FOR DELETE TO authenticated
|
||||
USING (public.can_access_case(case_id, auth.uid()));
|
||||
|
||||
-- status updates
|
||||
CREATE POLICY "status_select_case" ON public.status_updates FOR SELECT TO authenticated
|
||||
USING (public.can_access_case(case_id, auth.uid()));
|
||||
CREATE POLICY "status_insert_case" ON public.status_updates FOR INSERT TO authenticated
|
||||
WITH CHECK (public.can_access_case(case_id, auth.uid()));
|
||||
CREATE POLICY "status_update_owner" ON public.status_updates FOR UPDATE TO authenticated
|
||||
USING (public.is_admin(auth.uid()) OR created_by = auth.uid());
|
||||
CREATE POLICY "status_delete_owner" ON public.status_updates FOR DELETE TO authenticated
|
||||
USING (public.is_admin(auth.uid()) OR created_by = auth.uid());
|
||||
|
||||
-- time entries
|
||||
CREATE POLICY "time_select_case" ON public.time_entries FOR SELECT TO authenticated
|
||||
USING (public.can_access_case(case_id, auth.uid()));
|
||||
CREATE POLICY "time_insert_case" ON public.time_entries FOR INSERT TO authenticated
|
||||
WITH CHECK (public.can_access_case(case_id, auth.uid()) AND user_id = auth.uid());
|
||||
CREATE POLICY "time_update_owner" ON public.time_entries FOR UPDATE TO authenticated
|
||||
USING (public.is_admin(auth.uid()) OR user_id = auth.uid());
|
||||
CREATE POLICY "time_delete_owner" ON public.time_entries FOR DELETE TO authenticated
|
||||
USING (public.is_admin(auth.uid()) OR user_id = auth.uid());
|
||||
|
||||
-- expenses
|
||||
CREATE POLICY "exp_select_case" ON public.expenses FOR SELECT TO authenticated
|
||||
USING (public.can_access_case(case_id, auth.uid()));
|
||||
CREATE POLICY "exp_insert_case" ON public.expenses FOR INSERT TO authenticated
|
||||
WITH CHECK (public.can_access_case(case_id, auth.uid()) AND user_id = auth.uid());
|
||||
CREATE POLICY "exp_update_owner" ON public.expenses FOR UPDATE TO authenticated
|
||||
USING (public.is_admin(auth.uid()) OR user_id = auth.uid());
|
||||
CREATE POLICY "exp_delete_owner" ON public.expenses FOR DELETE TO authenticated
|
||||
USING (public.is_admin(auth.uid()) OR user_id = auth.uid());
|
||||
|
||||
-- invoices
|
||||
CREATE POLICY "inv_select" ON public.invoices FOR SELECT TO authenticated
|
||||
USING (
|
||||
public.is_admin(auth.uid())
|
||||
OR created_by = auth.uid()
|
||||
OR (case_id IS NOT NULL AND public.can_access_case(case_id, auth.uid()))
|
||||
);
|
||||
CREATE POLICY "inv_insert" ON public.invoices FOR INSERT TO authenticated
|
||||
WITH CHECK (auth.uid() IS NOT NULL);
|
||||
CREATE POLICY "inv_update" ON public.invoices FOR UPDATE TO authenticated
|
||||
USING (
|
||||
public.is_admin(auth.uid())
|
||||
OR created_by = auth.uid()
|
||||
OR (case_id IS NOT NULL AND public.can_access_case(case_id, auth.uid()))
|
||||
);
|
||||
CREATE POLICY "inv_delete_admin" ON public.invoices FOR DELETE TO authenticated
|
||||
USING (public.is_admin(auth.uid()));
|
||||
|
||||
-- =========================================
|
||||
-- STORAGE BUCKETS
|
||||
-- =========================================
|
||||
INSERT INTO storage.buckets (id, name, public) VALUES ('case-documents', 'case-documents', false)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
INSERT INTO storage.buckets (id, name, public) VALUES ('case-receipts', 'case-receipts', false)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
INSERT INTO storage.buckets (id, name, public) VALUES ('case-reports', 'case-reports', false)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Storage policies: file path must start with case-id/ that the user can access
|
||||
CREATE POLICY "case_docs_select" ON storage.objects FOR SELECT TO authenticated
|
||||
USING (
|
||||
bucket_id IN ('case-documents','case-receipts','case-reports')
|
||||
AND public.can_access_case(((storage.foldername(name))[1])::uuid, auth.uid())
|
||||
);
|
||||
CREATE POLICY "case_docs_insert" ON storage.objects FOR INSERT TO authenticated
|
||||
WITH CHECK (
|
||||
bucket_id IN ('case-documents','case-receipts','case-reports')
|
||||
AND public.can_access_case(((storage.foldername(name))[1])::uuid, auth.uid())
|
||||
);
|
||||
CREATE POLICY "case_docs_delete" ON storage.objects FOR DELETE TO authenticated
|
||||
USING (
|
||||
bucket_id IN ('case-documents','case-receipts','case-reports')
|
||||
AND public.can_access_case(((storage.foldername(name))[1])::uuid, auth.uid())
|
||||
);
|
||||
Reference in New Issue
Block a user