Add ACMCC app source, Supabase backend, and project config

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 20:19:26 -04:00
parent 313b51b412
commit 183fe0a93c
1422 changed files with 259271 additions and 0 deletions
@@ -0,0 +1,81 @@
-- Create role enum
CREATE TYPE public.app_role AS ENUM ('admin', 'manager', 'homeowner');
-- Create profiles table
CREATE TABLE public.profiles (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Create user_roles table
CREATE TABLE public.user_roles (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
role app_role NOT NULL,
UNIQUE (user_id, role)
);
-- Enable RLS
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY;
-- Profiles policies
CREATE POLICY "Users can view all profiles" ON public.profiles FOR SELECT TO authenticated USING (true);
CREATE POLICY "Users can update own profile" ON public.profiles FOR UPDATE TO authenticated USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own profile" ON public.profiles FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id);
-- Security definer function for role checks
CREATE OR REPLACE FUNCTION public.has_role(_user_id uuid, _role 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
)
$$;
-- User roles policies
CREATE POLICY "Users can view own roles" ON public.user_roles FOR SELECT TO authenticated USING (auth.uid() = user_id);
CREATE POLICY "Admins can manage roles" ON public.user_roles FOR ALL TO authenticated USING (public.has_role(auth.uid(), 'admin'));
-- Auto-create profile and default homeowner role 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 (user_id, full_name)
VALUES (NEW.id, NEW.raw_user_meta_data ->> 'full_name');
INSERT INTO public.user_roles (user_id, role)
VALUES (NEW.id, 'homeowner');
RETURN NEW;
END;
$$;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
-- Updated_at trigger
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql SET search_path = public;
CREATE TRIGGER update_profiles_updated_at
BEFORE UPDATE ON public.profiles
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
@@ -0,0 +1,131 @@
-- Associations table
CREATE TABLE public.associations (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
address TEXT,
city TEXT,
state TEXT,
zip TEXT,
phone TEXT,
email TEXT,
management_fee NUMERIC(10,2),
fiscal_year_start INTEGER DEFAULT 1,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Units table
CREATE TABLE public.units (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
unit_number TEXT NOT NULL,
address TEXT,
city TEXT,
state TEXT,
zip TEXT,
bedrooms INTEGER,
bathrooms NUMERIC(3,1),
sqft INTEGER,
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'delinquent')),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Owners table
CREATE TABLE public.owners (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL,
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT,
phone TEXT,
mailing_address TEXT,
is_primary BOOLEAN DEFAULT false,
is_tenant BOOLEAN DEFAULT false,
move_in_date DATE,
move_out_date DATE,
balance NUMERIC(12,2) DEFAULT 0,
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Violations table
CREATE TABLE public.violations (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL,
owner_id UUID REFERENCES public.owners(id) ON DELETE SET NULL,
title TEXT NOT NULL,
description TEXT,
category TEXT,
status TEXT DEFAULT 'open' CHECK (status IN ('open', 'pending', 'resolved', 'escalated', 'closed')),
priority TEXT DEFAULT 'medium' CHECK (priority IN ('low', 'medium', 'high', 'critical')),
due_date DATE,
resolved_date DATE,
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- ARC Applications table
CREATE TABLE public.arc_applications (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL,
owner_id UUID REFERENCES public.owners(id) ON DELETE SET NULL,
title TEXT NOT NULL,
description TEXT,
project_type TEXT,
estimated_cost NUMERIC(12,2),
status TEXT DEFAULT 'submitted' CHECK (status IN ('submitted', 'under_review', 'approved', 'denied', 'withdrawn')),
submitted_date DATE DEFAULT CURRENT_DATE,
review_date DATE,
decision_notes TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Enable RLS
ALTER TABLE public.associations ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.units ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.owners ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.violations ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.arc_applications ENABLE ROW LEVEL SECURITY;
-- RLS Policies (authenticated users can read all, admin/manager can write)
CREATE POLICY "Authenticated users can view associations" ON public.associations FOR SELECT TO authenticated USING (true);
CREATE POLICY "Admins can manage associations" ON public.associations FOR ALL TO authenticated USING (public.has_role(auth.uid(), 'admin'));
CREATE POLICY "Managers can manage associations" ON public.associations FOR ALL TO authenticated USING (public.has_role(auth.uid(), 'manager'));
CREATE POLICY "Authenticated users can view units" ON public.units FOR SELECT TO authenticated USING (true);
CREATE POLICY "Admins can manage units" ON public.units FOR ALL TO authenticated USING (public.has_role(auth.uid(), 'admin'));
CREATE POLICY "Managers can manage units" ON public.units FOR ALL TO authenticated USING (public.has_role(auth.uid(), 'manager'));
CREATE POLICY "Authenticated users can view owners" ON public.owners FOR SELECT TO authenticated USING (true);
CREATE POLICY "Admins can manage owners" ON public.owners FOR ALL TO authenticated USING (public.has_role(auth.uid(), 'admin'));
CREATE POLICY "Managers can manage owners" ON public.owners FOR ALL TO authenticated USING (public.has_role(auth.uid(), 'manager'));
CREATE POLICY "Authenticated users can view violations" ON public.violations FOR SELECT TO authenticated USING (true);
CREATE POLICY "Admins can manage violations" ON public.violations FOR ALL TO authenticated USING (public.has_role(auth.uid(), 'admin'));
CREATE POLICY "Managers can manage violations" ON public.violations FOR ALL TO authenticated USING (public.has_role(auth.uid(), 'manager'));
CREATE POLICY "Authenticated users can view arc_applications" ON public.arc_applications FOR SELECT TO authenticated USING (true);
CREATE POLICY "Admins can manage arc_applications" ON public.arc_applications FOR ALL TO authenticated USING (public.has_role(auth.uid(), 'admin'));
CREATE POLICY "Managers can manage arc_applications" ON public.arc_applications FOR ALL TO authenticated USING (public.has_role(auth.uid(), 'manager'));
-- Indexes
CREATE INDEX idx_units_association ON public.units(association_id);
CREATE INDEX idx_owners_association ON public.owners(association_id);
CREATE INDEX idx_owners_unit ON public.owners(unit_id);
CREATE INDEX idx_violations_association ON public.violations(association_id);
CREATE INDEX idx_violations_unit ON public.violations(unit_id);
CREATE INDEX idx_arc_applications_association ON public.arc_applications(association_id);
-- Updated_at triggers
CREATE TRIGGER update_associations_updated_at BEFORE UPDATE ON public.associations FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_units_updated_at BEFORE UPDATE ON public.units FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_owners_updated_at BEFORE UPDATE ON public.owners FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_violations_updated_at BEFORE UPDATE ON public.violations FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_arc_applications_updated_at BEFORE UPDATE ON public.arc_applications FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
@@ -0,0 +1,53 @@
-- Chart of Accounts
CREATE TABLE public.chart_of_accounts (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
account_number TEXT NOT NULL,
account_name TEXT NOT NULL,
account_type TEXT NOT NULL DEFAULT 'expense',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.chart_of_accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can view chart_of_accounts"
ON public.chart_of_accounts FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert chart_of_accounts"
ON public.chart_of_accounts FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "Authenticated users can update chart_of_accounts"
ON public.chart_of_accounts FOR UPDATE TO authenticated USING (true) WITH CHECK (true);
CREATE POLICY "Authenticated users can delete chart_of_accounts"
ON public.chart_of_accounts FOR DELETE TO authenticated USING (true);
-- Journal Entries
CREATE TABLE public.journal_entries (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
chart_of_account_id UUID REFERENCES public.chart_of_accounts(id) ON DELETE SET NULL,
date DATE NOT NULL DEFAULT CURRENT_DATE,
description TEXT,
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
type TEXT NOT NULL DEFAULT 'debit' CHECK (type IN ('debit', 'credit')),
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.journal_entries ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can view journal_entries"
ON public.journal_entries FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert journal_entries"
ON public.journal_entries FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "Authenticated users can update journal_entries"
ON public.journal_entries FOR UPDATE TO authenticated USING (true) WITH CHECK (true);
CREATE POLICY "Authenticated users can delete journal_entries"
ON public.journal_entries FOR DELETE TO authenticated USING (true);
@@ -0,0 +1,35 @@
CREATE TABLE public.billable_expenses (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
date DATE NOT NULL DEFAULT CURRENT_DATE,
description TEXT,
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
category TEXT,
billable_type TEXT,
status TEXT NOT NULL DEFAULT 'pending',
is_credit BOOLEAN NOT NULL DEFAULT false,
credit_reason TEXT,
quantity NUMERIC(10,2) DEFAULT 1,
unit_price NUMERIC(12,2),
vendor_name TEXT,
address TEXT,
receipt_url TEXT,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.billable_expenses ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can view billable_expenses"
ON public.billable_expenses FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert billable_expenses"
ON public.billable_expenses FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "Authenticated users can update billable_expenses"
ON public.billable_expenses FOR UPDATE TO authenticated USING (true) WITH CHECK (true);
CREATE POLICY "Authenticated users can delete billable_expenses"
ON public.billable_expenses FOR DELETE TO authenticated USING (true);
@@ -0,0 +1,26 @@
CREATE TABLE public.board_members (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
member_name TEXT NOT NULL,
member_email TEXT,
phone TEXT,
role TEXT DEFAULT 'Member',
approval_authority BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.board_members ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can view board_members"
ON public.board_members FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert board_members"
ON public.board_members FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "Authenticated users can update board_members"
ON public.board_members FOR UPDATE TO authenticated USING (true) WITH CHECK (true);
CREATE POLICY "Authenticated users can delete board_members"
ON public.board_members FOR DELETE TO authenticated USING (true);
@@ -0,0 +1,60 @@
-- Announcements table
CREATE TABLE public.announcements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
content TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
visibility TEXT NOT NULL DEFAULT 'all',
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.announcements ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can read active announcements"
ON public.announcements FOR SELECT TO authenticated
USING (status = 'active');
CREATE POLICY "Authenticated users can insert announcements"
ON public.announcements FOR INSERT TO authenticated
WITH CHECK (true);
CREATE POLICY "Authors can update their announcements"
ON public.announcements FOR UPDATE TO authenticated
USING (created_by = auth.uid());
CREATE POLICY "Authors can delete their announcements"
ON public.announcements FOR DELETE TO authenticated
USING (created_by = auth.uid());
-- Reminders table
CREATE TABLE public.reminders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
description TEXT,
due_date DATE NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_by UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.reminders ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can read own reminders"
ON public.reminders FOR SELECT TO authenticated
USING (created_by = auth.uid());
CREATE POLICY "Users can create reminders"
ON public.reminders FOR INSERT TO authenticated
WITH CHECK (created_by = auth.uid());
CREATE POLICY "Users can update own reminders"
ON public.reminders FOR UPDATE TO authenticated
USING (created_by = auth.uid());
CREATE POLICY "Users can delete own reminders"
ON public.reminders FOR DELETE TO authenticated
USING (created_by = auth.uid());
@@ -0,0 +1,76 @@
-- Unit Tenants
CREATE TABLE public.unit_tenants (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
unit_id UUID REFERENCES public.units(id) ON DELETE CASCADE NOT NULL,
name TEXT NOT NULL,
email TEXT,
phone TEXT,
lease_start DATE,
lease_end DATE,
status TEXT DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Unit Parking
CREATE TABLE public.unit_parking (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
unit_id UUID REFERENCES public.units(id) ON DELETE CASCADE NOT NULL,
spaces TEXT,
spots TEXT,
type TEXT,
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Unit Pets
CREATE TABLE public.unit_pets (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
unit_id UUID REFERENCES public.units(id) ON DELETE CASCADE NOT NULL,
name TEXT,
type TEXT,
breed TEXT,
weight TEXT,
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Unit General Info
CREATE TABLE public.unit_general_info (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
unit_id UUID REFERENCES public.units(id) ON DELETE CASCADE NOT NULL,
utilities TEXT,
amenities TEXT,
restrictions TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Unit Occupants
CREATE TABLE public.unit_occupants (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
unit_id UUID REFERENCES public.units(id) ON DELETE CASCADE NOT NULL,
name TEXT NOT NULL,
relationship TEXT,
phone TEXT,
email TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Enable RLS on all tables
ALTER TABLE public.unit_tenants ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.unit_parking ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.unit_pets ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.unit_general_info ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.unit_occupants ENABLE ROW LEVEL SECURITY;
-- RLS policies for authenticated users
CREATE POLICY "Authenticated users can manage unit_tenants" ON public.unit_tenants FOR ALL TO authenticated USING (true) WITH CHECK (true);
CREATE POLICY "Authenticated users can manage unit_parking" ON public.unit_parking FOR ALL TO authenticated USING (true) WITH CHECK (true);
CREATE POLICY "Authenticated users can manage unit_pets" ON public.unit_pets FOR ALL TO authenticated USING (true) WITH CHECK (true);
CREATE POLICY "Authenticated users can manage unit_general_info" ON public.unit_general_info FOR ALL TO authenticated USING (true) WITH CHECK (true);
CREATE POLICY "Authenticated users can manage unit_occupants" ON public.unit_occupants FOR ALL TO authenticated USING (true) WITH CHECK (true);
@@ -0,0 +1,440 @@
-- ═══════════════════════════════════════════════════════════
-- CORE OPERATIONS TABLES
-- ═══════════════════════════════════════════════════════════
-- Projects
CREATE TABLE public.projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'active',
priority TEXT DEFAULT 'medium',
start_date DATE,
due_date DATE,
budget NUMERIC(12,2),
assigned_to TEXT,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Tasks
CREATE TABLE public.tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'pending',
priority TEXT DEFAULT 'medium',
due_date DATE,
assigned_to UUID,
project_id UUID REFERENCES public.projects(id) ON DELETE SET NULL,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Documents
CREATE TABLE public.documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
category TEXT DEFAULT 'general',
file_url TEXT,
file_name TEXT,
file_size BIGINT,
uploaded_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Call Log
CREATE TABLE public.call_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
caller_name TEXT NOT NULL,
caller_phone TEXT,
call_type TEXT DEFAULT 'inbound',
subject TEXT,
notes TEXT,
follow_up_required BOOLEAN DEFAULT false,
follow_up_date DATE,
taken_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ═══════════════════════════════════════════════════════════
-- FINANCIAL TABLES
-- ═══════════════════════════════════════════════════════════
-- Invoices
CREATE TABLE public.invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
vendor_name TEXT NOT NULL,
invoice_number TEXT,
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
issue_date DATE DEFAULT CURRENT_DATE,
due_date DATE,
paid_date DATE,
description TEXT,
category TEXT,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Payments (admin-level tracking)
CREATE TABLE public.admin_payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
owner_id UUID REFERENCES public.owners(id) ON DELETE SET NULL,
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
payment_method TEXT DEFAULT 'check',
reference_number TEXT,
payment_date DATE DEFAULT CURRENT_DATE,
description TEXT,
status TEXT NOT NULL DEFAULT 'completed',
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Collections
CREATE TABLE public.collections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
owner_id UUID REFERENCES public.owners(id) ON DELETE SET NULL,
amount_owed NUMERIC(12,2) NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'open',
last_notice_date DATE,
notes TEXT,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Payment Plans
CREATE TABLE public.payment_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
owner_id UUID REFERENCES public.owners(id) ON DELETE SET NULL,
total_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
monthly_payment NUMERIC(12,2) NOT NULL DEFAULT 0,
start_date DATE,
end_date DATE,
status TEXT NOT NULL DEFAULT 'active',
notes TEXT,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Payables
CREATE TABLE public.payables (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
vendor_name TEXT NOT NULL,
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
due_date DATE,
status TEXT NOT NULL DEFAULT 'pending',
description TEXT,
invoice_id UUID REFERENCES public.invoices(id) ON DELETE SET NULL,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ═══════════════════════════════════════════════════════════
-- ADMINISTRATIVE TABLES
-- ═══════════════════════════════════════════════════════════
-- Inspections
CREATE TABLE public.inspections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
inspection_type TEXT DEFAULT 'general',
scheduled_date DATE,
completed_date DATE,
status TEXT NOT NULL DEFAULT 'scheduled',
inspector TEXT,
notes TEXT,
unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Blocked Dates
CREATE TABLE public.blocked_dates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
reason TEXT,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Checklists
CREATE TABLE public.checklists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
description TEXT,
items JSONB DEFAULT '[]'::jsonb,
status TEXT NOT NULL DEFAULT 'active',
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ═══════════════════════════════════════════════════════════
-- ASSOCIATION TABLES
-- ═══════════════════════════════════════════════════════════
-- Bids & Quotes
CREATE TABLE public.bids_quotes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
vendor_name TEXT NOT NULL,
project_id UUID REFERENCES public.projects(id) ON DELETE SET NULL,
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
description TEXT,
status TEXT NOT NULL DEFAULT 'pending',
received_date DATE DEFAULT CURRENT_DATE,
expiry_date DATE,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Bill Approvals
CREATE TABLE public.bill_approvals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
invoice_id UUID REFERENCES public.invoices(id) ON DELETE SET NULL,
vendor_name TEXT NOT NULL,
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
approved_by UUID,
approved_date DATE,
notes TEXT,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Client Requests
CREATE TABLE public.client_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'open',
priority TEXT DEFAULT 'medium',
requester_name TEXT,
requester_email TEXT,
assigned_to UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Homeowner Requests
CREATE TABLE public.homeowner_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
owner_id UUID REFERENCES public.owners(id) ON DELETE SET NULL,
title TEXT NOT NULL,
description TEXT,
category TEXT DEFAULT 'general',
status TEXT NOT NULL DEFAULT 'open',
priority TEXT DEFAULT 'medium',
assigned_to UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Estoppels
CREATE TABLE public.estoppels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL,
address TEXT,
status TEXT NOT NULL DEFAULT 'requested',
requested_date DATE DEFAULT CURRENT_DATE,
completed_date DATE,
fee NUMERIC(12,2),
requestor_name TEXT,
requestor_email TEXT,
notes TEXT,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Legal Matters
CREATE TABLE public.legal_matters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
case_number TEXT,
attorney TEXT,
status TEXT NOT NULL DEFAULT 'open',
category TEXT DEFAULT 'general',
description TEXT,
opened_date DATE DEFAULT CURRENT_DATE,
closed_date DATE,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Status Updates
CREATE TABLE public.status_updates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
content TEXT,
status TEXT NOT NULL DEFAULT 'published',
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Owner Updates
CREATE TABLE public.owner_updates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
content TEXT,
visibility TEXT DEFAULT 'all',
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ═══════════════════════════════════════════════════════════
-- ACCOUNTING TABLES
-- ═══════════════════════════════════════════════════════════
-- Bank Accounts
CREATE TABLE public.bank_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
account_name TEXT NOT NULL,
account_number TEXT,
routing_number TEXT,
bank_name TEXT,
account_type TEXT DEFAULT 'checking',
current_balance NUMERIC(14,2) DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Budgets
CREATE TABLE public.budgets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
fiscal_year INTEGER NOT NULL,
category TEXT NOT NULL,
budgeted_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
actual_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Checks
CREATE TABLE public.checks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
bank_account_id UUID REFERENCES public.bank_accounts(id) ON DELETE SET NULL,
check_number TEXT,
payee TEXT NOT NULL,
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
memo TEXT,
check_date DATE DEFAULT CURRENT_DATE,
status TEXT NOT NULL DEFAULT 'draft',
printed BOOLEAN DEFAULT false,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Calendar Events
CREATE TABLE public.calendar_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
description TEXT,
start_date TIMESTAMPTZ NOT NULL,
end_date TIMESTAMPTZ,
all_day BOOLEAN DEFAULT false,
event_type TEXT DEFAULT 'meeting',
location TEXT,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ═══════════════════════════════════════════════════════════
-- ENABLE RLS ON ALL TABLES
-- ═══════════════════════════════════════════════════════════
ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.call_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.admin_payments ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.collections ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.payment_plans ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.payables ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.inspections ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.blocked_dates ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.checklists ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.bids_quotes ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.bill_approvals ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.client_requests ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.homeowner_requests ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.estoppels ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.legal_matters ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.status_updates ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.owner_updates ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.bank_accounts ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.budgets ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.checks ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.calendar_events ENABLE ROW LEVEL SECURITY;
-- ═══════════════════════════════════════════════════════════
-- RLS POLICIES - Admin/Manager full access for all tables
-- ═══════════════════════════════════════════════════════════
DO $$
DECLARE
tbl TEXT;
BEGIN
FOR tbl IN SELECT unnest(ARRAY[
'projects','tasks','documents','call_logs','invoices','admin_payments',
'collections','payment_plans','payables','inspections','blocked_dates',
'checklists','bids_quotes','bill_approvals','client_requests',
'homeowner_requests','estoppels','legal_matters','status_updates',
'owner_updates','bank_accounts','budgets','checks','calendar_events'
])
LOOP
EXECUTE format(
'CREATE POLICY "Staff full access on %1$s" ON public.%1$s FOR ALL TO authenticated USING (public.has_role(auth.uid(), ''admin'') OR public.has_role(auth.uid(), ''manager'')) WITH CHECK (public.has_role(auth.uid(), ''admin'') OR public.has_role(auth.uid(), ''manager''))',
tbl
);
END LOOP;
END;
$$;
@@ -0,0 +1,221 @@
-- Add columns to chart_of_accounts
ALTER TABLE chart_of_accounts ADD COLUMN IF NOT EXISTS parent_account_id uuid REFERENCES chart_of_accounts(id);
ALTER TABLE chart_of_accounts ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true;
ALTER TABLE chart_of_accounts ADD COLUMN IF NOT EXISTS description text;
-- Add account_category to bank_accounts
ALTER TABLE bank_accounts ADD COLUMN IF NOT EXISTS account_category text NOT NULL DEFAULT 'operating';
-- Create vendors table
CREATE TABLE IF NOT EXISTS vendors (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
association_id uuid NOT NULL REFERENCES associations(id) ON DELETE CASCADE,
name text NOT NULL,
contact_name text,
email text,
phone text,
address text,
tax_id text,
payment_terms text DEFAULT '30',
default_expense_account_id uuid REFERENCES chart_of_accounts(id),
is_active boolean NOT NULL DEFAULT true,
notes text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- Create bank_transactions table (unified register)
CREATE TABLE IF NOT EXISTS bank_transactions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
bank_account_id uuid NOT NULL REFERENCES bank_accounts(id) ON DELETE CASCADE,
association_id uuid NOT NULL REFERENCES associations(id) ON DELETE CASCADE,
date date NOT NULL DEFAULT CURRENT_DATE,
transaction_type text NOT NULL DEFAULT 'payment',
description text,
reference_number text,
debit numeric NOT NULL DEFAULT 0,
credit numeric NOT NULL DEFAULT 0,
is_cleared boolean NOT NULL DEFAULT false,
cleared_date date,
reconciliation_id uuid,
related_entity_type text,
related_entity_id uuid,
created_by uuid,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- Create owner_ledger_entries
CREATE TABLE IF NOT EXISTS owner_ledger_entries (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
association_id uuid NOT NULL REFERENCES associations(id) ON DELETE CASCADE,
owner_id uuid NOT NULL REFERENCES owners(id) ON DELETE CASCADE,
unit_id uuid REFERENCES units(id),
date date NOT NULL DEFAULT CURRENT_DATE,
transaction_type text NOT NULL,
description text,
debit numeric NOT NULL DEFAULT 0,
credit numeric NOT NULL DEFAULT 0,
reference_id uuid,
reference_type text,
created_by uuid,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- Create bills table (proper AP)
CREATE TABLE IF NOT EXISTS bills (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
association_id uuid NOT NULL REFERENCES associations(id) ON DELETE CASCADE,
vendor_id uuid REFERENCES vendors(id),
invoice_number text,
bill_date date NOT NULL DEFAULT CURRENT_DATE,
due_date date,
amount numeric NOT NULL DEFAULT 0,
amount_paid numeric NOT NULL DEFAULT 0,
expense_account_id uuid REFERENCES chart_of_accounts(id),
description text,
status text NOT NULL DEFAULT 'draft',
approved_by uuid,
approved_date date,
paid_date date,
payment_method text,
check_id uuid REFERENCES checks(id),
attachment_url text,
notes text,
created_by uuid,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- Create deposit_batches
CREATE TABLE IF NOT EXISTS deposit_batches (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
association_id uuid NOT NULL REFERENCES associations(id) ON DELETE CASCADE,
bank_account_id uuid NOT NULL REFERENCES bank_accounts(id),
deposit_date date NOT NULL DEFAULT CURRENT_DATE,
total_amount numeric NOT NULL DEFAULT 0,
status text NOT NULL DEFAULT 'open',
memo text,
bank_transaction_id uuid,
created_by uuid,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- Create deposit_batch_items
CREATE TABLE IF NOT EXISTS deposit_batch_items (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
deposit_batch_id uuid NOT NULL REFERENCES deposit_batches(id) ON DELETE CASCADE,
owner_ledger_entry_id uuid REFERENCES owner_ledger_entries(id),
description text,
amount numeric NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now()
);
-- Create bank_transfers
CREATE TABLE IF NOT EXISTS bank_transfers (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
association_id uuid NOT NULL REFERENCES associations(id) ON DELETE CASCADE,
from_bank_account_id uuid NOT NULL REFERENCES bank_accounts(id),
to_bank_account_id uuid NOT NULL REFERENCES bank_accounts(id),
amount numeric NOT NULL,
transfer_date date NOT NULL DEFAULT CURRENT_DATE,
description text,
from_transaction_id uuid,
to_transaction_id uuid,
created_by uuid,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- Create bank_reconciliations
CREATE TABLE IF NOT EXISTS bank_reconciliations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
bank_account_id uuid NOT NULL REFERENCES bank_accounts(id),
association_id uuid NOT NULL REFERENCES associations(id) ON DELETE CASCADE,
statement_date date NOT NULL,
opening_balance numeric NOT NULL DEFAULT 0,
closing_balance numeric NOT NULL DEFAULT 0,
cleared_balance numeric NOT NULL DEFAULT 0,
difference numeric NOT NULL DEFAULT 0,
status text NOT NULL DEFAULT 'in_progress',
reconciled_by uuid,
reconciled_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- Add FK for bank_transactions reconciliation_id
ALTER TABLE bank_transactions
ADD CONSTRAINT bank_transactions_reconciliation_id_fkey
FOREIGN KEY (reconciliation_id) REFERENCES bank_reconciliations(id);
-- Add FK for bank_transfers transaction ids
ALTER TABLE bank_transfers
ADD CONSTRAINT bank_transfers_from_transaction_id_fkey
FOREIGN KEY (from_transaction_id) REFERENCES bank_transactions(id);
ALTER TABLE bank_transfers
ADD CONSTRAINT bank_transfers_to_transaction_id_fkey
FOREIGN KEY (to_transaction_id) REFERENCES bank_transactions(id);
-- Add FK for deposit_batches bank_transaction_id
ALTER TABLE deposit_batches
ADD CONSTRAINT deposit_batches_bank_transaction_id_fkey
FOREIGN KEY (bank_transaction_id) REFERENCES bank_transactions(id);
-- RLS for all new tables
ALTER TABLE vendors ENABLE ROW LEVEL SECURITY;
ALTER TABLE bank_transactions ENABLE ROW LEVEL SECURITY;
ALTER TABLE owner_ledger_entries ENABLE ROW LEVEL SECURITY;
ALTER TABLE bills ENABLE ROW LEVEL SECURITY;
ALTER TABLE deposit_batches ENABLE ROW LEVEL SECURITY;
ALTER TABLE deposit_batch_items ENABLE ROW LEVEL SECURITY;
ALTER TABLE bank_transfers ENABLE ROW LEVEL SECURITY;
ALTER TABLE bank_reconciliations ENABLE ROW LEVEL SECURITY;
-- Staff access policies
CREATE POLICY "Staff full access on vendors" ON vendors FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'));
CREATE POLICY "Staff full access on bank_transactions" ON bank_transactions FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'));
CREATE POLICY "Staff full access on owner_ledger_entries" ON owner_ledger_entries FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'));
CREATE POLICY "Owners can view own ledger" ON owner_ledger_entries FOR SELECT TO authenticated
USING (EXISTS (SELECT 1 FROM owners WHERE owners.id = owner_ledger_entries.owner_id AND owners.user_id = auth.uid()));
CREATE POLICY "Staff full access on bills" ON bills FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'));
CREATE POLICY "Staff full access on deposit_batches" ON deposit_batches FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'));
CREATE POLICY "Staff full access on deposit_batch_items" ON deposit_batch_items FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'));
CREATE POLICY "Staff full access on bank_transfers" ON bank_transfers FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'));
CREATE POLICY "Staff full access on bank_reconciliations" ON bank_reconciliations FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'));
-- Updated_at triggers for new tables
CREATE TRIGGER update_vendors_updated_at BEFORE UPDATE ON vendors FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_bank_transactions_updated_at BEFORE UPDATE ON bank_transactions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_owner_ledger_entries_updated_at BEFORE UPDATE ON owner_ledger_entries FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_bills_updated_at BEFORE UPDATE ON bills FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_deposit_batches_updated_at BEFORE UPDATE ON deposit_batches FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_bank_transfers_updated_at BEFORE UPDATE ON bank_transfers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_bank_reconciliations_updated_at BEFORE UPDATE ON bank_reconciliations FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
@@ -0,0 +1,3 @@
INSERT INTO public.user_roles (user_id, role)
VALUES ('4b811784-cdc5-4e1b-9704-509a7ee5e3ee', 'admin')
ON CONFLICT (user_id, role) DO NOTHING;
@@ -0,0 +1,4 @@
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS zoho_contact_id text;
ALTER TABLE public.owners ADD COLUMN IF NOT EXISTS zoho_contact_id text;
ALTER TABLE public.vendors ADD COLUMN IF NOT EXISTS zoho_contact_id text;
@@ -0,0 +1,2 @@
ALTER TABLE public.owner_ledger_entries ADD COLUMN IF NOT EXISTS zoho_invoice_id text;
ALTER TABLE public.admin_payments ADD COLUMN IF NOT EXISTS zoho_payment_id text;
@@ -0,0 +1,9 @@
ALTER TABLE public.owners
ADD COLUMN IF NOT EXISTS account_number text,
ADD COLUMN IF NOT EXISTS property_address text,
ADD COLUMN IF NOT EXISTS alternate_address_1 text,
ADD COLUMN IF NOT EXISTS alternate_address_2 text,
ADD COLUMN IF NOT EXISTS electronic_consent boolean DEFAULT false,
ADD COLUMN IF NOT EXISTS show_proxy_text boolean DEFAULT true,
ADD COLUMN IF NOT EXISTS exclude_from_signin boolean DEFAULT false;
@@ -0,0 +1 @@
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS zoho_organization_id text;
@@ -0,0 +1,81 @@
-- Create in_app_notifications table
CREATE TABLE public.in_app_notifications (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
type TEXT NOT NULL DEFAULT 'info',
title TEXT NOT NULL,
message TEXT,
is_read BOOLEAN NOT NULL DEFAULT false,
related_item_id UUID,
related_item_type TEXT,
link TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Enable RLS
ALTER TABLE public.in_app_notifications ENABLE ROW LEVEL SECURITY;
-- Users can read their own notifications
CREATE POLICY "Users can read own notifications"
ON public.in_app_notifications FOR SELECT
TO authenticated
USING (user_id = auth.uid());
-- Users can update (mark as read) their own notifications
CREATE POLICY "Users can update own notifications"
ON public.in_app_notifications FOR UPDATE
TO authenticated
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
-- Users can delete their own notifications
CREATE POLICY "Users can delete own notifications"
ON public.in_app_notifications FOR DELETE
TO authenticated
USING (user_id = auth.uid());
-- Admins/managers can insert notifications for any user
CREATE POLICY "Staff can insert notifications"
ON public.in_app_notifications FOR INSERT
TO authenticated
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role) OR user_id = auth.uid());
-- Enable realtime
ALTER PUBLICATION supabase_realtime ADD TABLE public.in_app_notifications;
-- Add last_notified_at to reminders if not exists
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='reminders' AND column_name='last_notified_at') THEN
ALTER TABLE public.reminders ADD COLUMN last_notified_at TIMESTAMP WITH TIME ZONE;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='reminders' AND column_name='assigned_to') THEN
ALTER TABLE public.reminders ADD COLUMN assigned_to UUID;
END IF;
END $$;
-- Create a security definer function to insert notifications (bypasses RLS for edge functions)
CREATE OR REPLACE FUNCTION public.insert_notification(
p_user_id UUID,
p_type TEXT,
p_title TEXT,
p_message TEXT DEFAULT NULL,
p_related_item_id UUID DEFAULT NULL,
p_related_item_type TEXT DEFAULT NULL,
p_link TEXT DEFAULT NULL
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_id UUID;
BEGIN
INSERT INTO public.in_app_notifications (user_id, type, title, message, related_item_id, related_item_type, link)
VALUES (p_user_id, p_type, p_title, p_message, p_related_item_id, p_related_item_type, p_link)
RETURNING id INTO v_id;
RETURN v_id;
END;
$$;
@@ -0,0 +1,3 @@
CREATE EXTENSION IF NOT EXISTS pg_cron WITH SCHEMA pg_catalog;
CREATE EXTENSION IF NOT EXISTS pg_net WITH SCHEMA extensions;
@@ -0,0 +1,24 @@
CREATE TABLE public.association_faqs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
question TEXT NOT NULL,
answer TEXT,
sort_order INT DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.association_faqs ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can view association FAQs"
ON public.association_faqs FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert association FAQs"
ON public.association_faqs FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "Authenticated users can update association FAQs"
ON public.association_faqs FOR UPDATE TO authenticated USING (true) WITH CHECK (true);
CREATE POLICY "Authenticated users can delete association FAQs"
ON public.association_faqs FOR DELETE TO authenticated USING (true);
@@ -0,0 +1,25 @@
CREATE TABLE public.saved_form_templates (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
form_type text NOT NULL,
name text NOT NULL,
association_id uuid REFERENCES public.associations(id) ON DELETE SET NULL,
form_data jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.saved_form_templates ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on saved_form_templates"
ON public.saved_form_templates
FOR ALL
TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
CREATE TRIGGER update_saved_form_templates_updated_at
BEFORE UPDATE ON public.saved_form_templates
FOR EACH ROW
EXECUTE FUNCTION public.update_updated_at_column();
@@ -0,0 +1,53 @@
-- Create violation_responses table for homeowner QR code responses
CREATE TABLE public.violation_responses (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
violation_id UUID NOT NULL REFERENCES public.violations(id) ON DELETE CASCADE,
respondent_name TEXT,
respondent_email TEXT,
respondent_phone TEXT,
response_text TEXT,
date_corrected DATE,
photo_urls JSONB DEFAULT '[]'::jsonb,
status TEXT NOT NULL DEFAULT 'submitted',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Enable RLS
ALTER TABLE public.violation_responses ENABLE ROW LEVEL SECURITY;
-- Allow anyone to INSERT (public form from QR code, no auth required)
CREATE POLICY "Anyone can submit violation responses"
ON public.violation_responses
FOR INSERT
TO anon, authenticated
WITH CHECK (true);
-- Staff can view all responses
CREATE POLICY "Staff can view violation responses"
ON public.violation_responses
FOR SELECT
TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
-- Staff can update/delete responses
CREATE POLICY "Staff can manage violation responses"
ON public.violation_responses
FOR ALL
TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
-- Allow anonymous users to read violation basic info for the response page
CREATE POLICY "Anyone can read violations for response page"
ON public.violations
FOR SELECT
TO anon
USING (true);
-- Update trigger
CREATE TRIGGER update_violation_responses_updated_at
BEFORE UPDATE ON public.violation_responses
FOR EACH ROW
EXECUTE FUNCTION public.update_updated_at_column();
@@ -0,0 +1,51 @@
-- Add missing columns to tasks table
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS client_id uuid REFERENCES public.associations(id);
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS parent_task_id uuid REFERENCES public.tasks(id) ON DELETE CASCADE;
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS template_id uuid;
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS completed_at timestamptz;
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS client_specific boolean DEFAULT false;
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS recurring boolean DEFAULT false;
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS recurring_interval text;
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS recurring_next_date date;
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS property_id uuid;
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS task_sequence integer;
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS days_until_due integer;
-- Create workflow_templates table
CREATE TABLE IF NOT EXISTS public.workflow_templates (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
description text,
client_id uuid REFERENCES public.associations(id),
created_by uuid,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
ALTER TABLE public.workflow_templates ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can manage workflow_templates"
ON public.workflow_templates FOR ALL TO authenticated USING (true) WITH CHECK (true);
-- Create workflow_template_tasks table
CREATE TABLE IF NOT EXISTS public.workflow_template_tasks (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
template_id uuid REFERENCES public.workflow_templates(id) ON DELETE CASCADE NOT NULL,
task_name text NOT NULL,
task_description text DEFAULT '',
days_until_due integer DEFAULT 0,
task_sequence integer DEFAULT 1,
priority text DEFAULT 'medium',
assigned_to text,
created_at timestamptz DEFAULT now()
);
ALTER TABLE public.workflow_template_tasks ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can manage workflow_template_tasks"
ON public.workflow_template_tasks FOR ALL TO authenticated USING (true) WITH CHECK (true);
-- Update template_id FK now that workflow_templates exists
ALTER TABLE public.tasks DROP CONSTRAINT IF EXISTS tasks_template_id_fkey;
ALTER TABLE public.tasks ADD CONSTRAINT tasks_template_id_fkey FOREIGN KEY (template_id) REFERENCES public.workflow_templates(id);
@@ -0,0 +1,22 @@
-- Create violation_types table for configurable violation categories per association
CREATE TABLE IF NOT EXISTS public.violation_types (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
association_id uuid REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
category text NOT NULL,
description text,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
ALTER TABLE public.violation_types ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can manage violation_types"
ON public.violation_types FOR ALL TO authenticated USING (true) WITH CHECK (true);
-- Add missing columns to violations for inspection workflow
ALTER TABLE public.violations ADD COLUMN IF NOT EXISTS address text;
ALTER TABLE public.violations ADD COLUMN IF NOT EXISTS violation_type text;
ALTER TABLE public.violations ADD COLUMN IF NOT EXISTS violation_date date DEFAULT CURRENT_DATE;
ALTER TABLE public.violations ADD COLUMN IF NOT EXISTS notes text;
ALTER TABLE public.violations ADD COLUMN IF NOT EXISTS property_id uuid;
@@ -0,0 +1 @@
ALTER TABLE public.units ADD COLUMN IF NOT EXISTS zoho_customer_number text DEFAULT NULL;
@@ -0,0 +1 @@
ALTER PUBLICATION supabase_realtime ADD TABLE public.owners;
@@ -0,0 +1,9 @@
-- Add staff and employee roles to the enum
ALTER TYPE public.app_role ADD VALUE IF NOT EXISTS 'staff';
ALTER TYPE public.app_role ADD VALUE IF NOT EXISTS 'employee';
-- Add is_blocked column to profiles for blocking access
ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS is_blocked boolean NOT NULL DEFAULT false;
ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS email text;
ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS phone text;
@@ -0,0 +1,59 @@
-- Add logo_url to associations table
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS logo_url text;
-- Create company_settings table for company-level branding
CREATE TABLE IF NOT EXISTS public.company_settings (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
key text NOT NULL UNIQUE,
value text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.company_settings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on company_settings"
ON public.company_settings
FOR ALL
TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'));
CREATE POLICY "Authenticated users can view company_settings"
ON public.company_settings
FOR SELECT
TO authenticated
USING (true);
-- Create public logos storage bucket
INSERT INTO storage.buckets (id, name, public)
VALUES ('logos', 'logos', true)
ON CONFLICT (id) DO NOTHING;
-- Allow authenticated users to upload to logos bucket
CREATE POLICY "Authenticated users can upload logos"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (bucket_id = 'logos');
-- Allow public read access to logos
CREATE POLICY "Public read access to logos"
ON storage.objects
FOR SELECT
TO public
USING (bucket_id = 'logos');
-- Allow authenticated users to update/delete logos
CREATE POLICY "Authenticated users can manage logos"
ON storage.objects
FOR DELETE
TO authenticated
USING (bucket_id = 'logos');
CREATE POLICY "Authenticated users can update logos"
ON storage.objects
FOR UPDATE
TO authenticated
USING (bucket_id = 'logos');
@@ -0,0 +1,49 @@
-- Add new columns to owner_updates table
ALTER TABLE public.owner_updates
ADD COLUMN IF NOT EXISTS unit_id uuid REFERENCES public.units(id),
ADD COLUMN IF NOT EXISTS posted_at timestamptz DEFAULT now(),
ADD COLUMN IF NOT EXISTS attachments jsonb DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS collection_ids jsonb DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS violation_ids jsonb DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS tags jsonb DEFAULT '[]'::jsonb;
-- Create owner_update_tags table
CREATE TABLE IF NOT EXISTS public.owner_update_tags (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
color text NOT NULL DEFAULT 'blue',
association_id uuid REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
created_at timestamptz DEFAULT now(),
UNIQUE(name, association_id)
);
ALTER TABLE public.owner_update_tags ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on owner_update_tags"
ON public.owner_update_tags
FOR ALL
TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
-- Create storage bucket for owner update attachments
INSERT INTO storage.buckets (id, name, public)
VALUES ('owner-update-attachments', 'owner-update-attachments', true)
ON CONFLICT (id) DO NOTHING;
-- Storage policies for owner update attachments
CREATE POLICY "Authenticated users can upload owner update attachments"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'owner-update-attachments');
CREATE POLICY "Anyone can view owner update attachments"
ON storage.objects FOR SELECT TO authenticated
USING (bucket_id = 'owner-update-attachments');
CREATE POLICY "Staff can delete owner update attachments"
ON storage.objects FOR DELETE TO authenticated
USING (bucket_id = 'owner-update-attachments' AND (
(SELECT has_role(auth.uid(), 'admin'::app_role)) OR
(SELECT has_role(auth.uid(), 'manager'::app_role))
));
@@ -0,0 +1,71 @@
-- Add new columns to status_updates
ALTER TABLE public.status_updates
ADD COLUMN IF NOT EXISTS image_urls jsonb DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS requested_action text,
ADD COLUMN IF NOT EXISTS voting_enabled boolean DEFAULT false;
-- Create status_update_comments table
CREATE TABLE IF NOT EXISTS public.status_update_comments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
status_update_id uuid NOT NULL REFERENCES public.status_updates(id) ON DELETE CASCADE,
user_id uuid NOT NULL,
content text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.status_update_comments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can view status_update_comments"
ON public.status_update_comments FOR SELECT TO authenticated
USING (true);
CREATE POLICY "Authenticated users can insert status_update_comments"
ON public.status_update_comments FOR INSERT TO authenticated
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own comments"
ON public.status_update_comments FOR DELETE TO authenticated
USING (user_id = auth.uid());
-- Create status_update_votes table
CREATE TABLE IF NOT EXISTS public.status_update_votes (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
status_update_id uuid NOT NULL REFERENCES public.status_updates(id) ON DELETE CASCADE,
user_id uuid NOT NULL,
vote text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (status_update_id, user_id)
);
ALTER TABLE public.status_update_votes ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can view status_update_votes"
ON public.status_update_votes FOR SELECT TO authenticated
USING (true);
CREATE POLICY "Authenticated users can insert status_update_votes"
ON public.status_update_votes FOR INSERT TO authenticated
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own votes"
ON public.status_update_votes FOR UPDATE TO authenticated
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
-- Create storage bucket for status update images
INSERT INTO storage.buckets (id, name, public)
VALUES ('status-update-images', 'status-update-images', true)
ON CONFLICT (id) DO NOTHING;
-- Storage policies
CREATE POLICY "Authenticated users can upload status update images"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'status-update-images');
CREATE POLICY "Anyone can view status update images"
ON storage.objects FOR SELECT TO public
USING (bucket_id = 'status-update-images');
CREATE POLICY "Authenticated users can delete status update images"
ON storage.objects FOR DELETE TO authenticated
USING (bucket_id = 'status-update-images');
@@ -0,0 +1,76 @@
-- Unit Parking
CREATE TABLE IF NOT EXISTS public.unit_parking (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
unit_id UUID NOT NULL REFERENCES public.units(id) ON DELETE CASCADE,
spaces TEXT,
spots TEXT,
type TEXT,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.unit_parking ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on unit_parking" ON public.unit_parking
FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
-- Unit Pets
CREATE TABLE IF NOT EXISTS public.unit_pets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
unit_id UUID NOT NULL REFERENCES public.units(id) ON DELETE CASCADE,
name TEXT NOT NULL DEFAULT '',
type TEXT DEFAULT '',
breed TEXT DEFAULT '',
weight TEXT DEFAULT '',
notes TEXT DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.unit_pets ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on unit_pets" ON public.unit_pets
FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
-- Unit General Info
CREATE TABLE IF NOT EXISTS public.unit_general_info (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
unit_id UUID NOT NULL REFERENCES public.units(id) ON DELETE CASCADE,
utilities TEXT DEFAULT '',
amenities TEXT DEFAULT '',
restrictions TEXT DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.unit_general_info ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on unit_general_info" ON public.unit_general_info
FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
-- Unit Occupants
CREATE TABLE IF NOT EXISTS public.unit_occupants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
unit_id UUID NOT NULL REFERENCES public.units(id) ON DELETE CASCADE,
name TEXT NOT NULL,
relationship TEXT NOT NULL DEFAULT '',
phone TEXT DEFAULT '',
email TEXT DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.unit_occupants ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on unit_occupants" ON public.unit_occupants
FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
@@ -0,0 +1,114 @@
-- Zoho account mappings (local COA -> Zoho account)
CREATE TABLE public.zoho_account_mappings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
chart_of_account_id UUID NOT NULL REFERENCES public.chart_of_accounts(id) ON DELETE CASCADE,
zoho_account_id TEXT NOT NULL,
zoho_account_name TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(association_id, chart_of_account_id)
);
ALTER TABLE public.zoho_account_mappings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on zoho_account_mappings"
ON public.zoho_account_mappings FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
-- Zoho customer mappings (owner/unit -> Zoho customer)
CREATE TABLE public.zoho_customer_mappings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
owner_id UUID REFERENCES public.owners(id) ON DELETE CASCADE,
unit_id UUID REFERENCES public.units(id) ON DELETE CASCADE,
zoho_customer_id TEXT NOT NULL,
zoho_customer_name TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(association_id, owner_id)
);
ALTER TABLE public.zoho_customer_mappings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on zoho_customer_mappings"
ON public.zoho_customer_mappings FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
-- Zoho sync settings per association (toggle which entry types sync)
CREATE TABLE public.zoho_sync_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE UNIQUE,
sync_invoices BOOLEAN NOT NULL DEFAULT true,
sync_payments BOOLEAN NOT NULL DEFAULT true,
sync_contacts BOOLEAN NOT NULL DEFAULT true,
sync_bills BOOLEAN NOT NULL DEFAULT false,
sync_journal_entries BOOLEAN NOT NULL DEFAULT false,
auto_sync_enabled BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.zoho_sync_settings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on zoho_sync_settings"
ON public.zoho_sync_settings FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
-- Zoho sync log for tracking sync history and errors
CREATE TABLE public.zoho_sync_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
sync_type TEXT NOT NULL, -- 'invoice', 'payment', 'contact', 'bill', etc.
direction TEXT NOT NULL DEFAULT 'push', -- 'push' or 'pull'
status TEXT NOT NULL DEFAULT 'success', -- 'success', 'error', 'partial'
record_count INTEGER DEFAULT 0,
error_message TEXT,
details JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.zoho_sync_log ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on zoho_sync_log"
ON public.zoho_sync_log FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
-- Association fee rules (interest & late fee configuration per association)
CREATE TABLE public.association_fee_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE UNIQUE,
-- Interest settings
interest_enabled BOOLEAN NOT NULL DEFAULT false,
interest_rate NUMERIC NOT NULL DEFAULT 0, -- APR percentage e.g. 18.0
interest_grace_days INTEGER NOT NULL DEFAULT 30,
interest_compound TEXT NOT NULL DEFAULT 'monthly', -- 'monthly', 'daily', 'quarterly'
interest_apply_to TEXT NOT NULL DEFAULT 'assessments', -- 'assessments', 'all_charges', 'assessments_and_fees'
-- Late fee settings
late_fee_enabled BOOLEAN NOT NULL DEFAULT false,
late_fee_type TEXT NOT NULL DEFAULT 'flat', -- 'flat' or 'percentage'
late_fee_amount NUMERIC NOT NULL DEFAULT 0, -- dollar amount or percentage
late_fee_trigger_days INTEGER NOT NULL DEFAULT 15, -- days past due before late fee applies
late_fee_max NUMERIC, -- optional max cap for percentage-based fees
late_fee_recurring BOOLEAN NOT NULL DEFAULT false, -- apply every month?
-- Schedule
auto_apply_enabled BOOLEAN NOT NULL DEFAULT false,
auto_apply_schedule TEXT NOT NULL DEFAULT 'monthly', -- 'monthly', 'quarterly'
auto_apply_day INTEGER NOT NULL DEFAULT 1, -- day of month
-- Push to Zoho
push_to_zoho BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.association_fee_rules ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on association_fee_rules"
ON public.association_fee_rules FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
@@ -0,0 +1,20 @@
CREATE TABLE public.zoho_reporting_tag_mappings (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
association_id uuid NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
zoho_tag_id text NOT NULL,
zoho_tag_option_id text NOT NULL,
zoho_tag_name text,
zoho_option_name text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(association_id, zoho_tag_id)
);
ALTER TABLE public.zoho_reporting_tag_mappings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on zoho_reporting_tag_mappings"
ON public.zoho_reporting_tag_mappings
FOR ALL
TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
@@ -0,0 +1,30 @@
-- Add missing columns to violations table for full violation management
ALTER TABLE public.violations ADD COLUMN IF NOT EXISTS stage text;
ALTER TABLE public.violations ADD COLUMN IF NOT EXISTS photo_url text;
ALTER TABLE public.violations ADD COLUMN IF NOT EXISTS photo_urls jsonb;
ALTER TABLE public.violations ADD COLUMN IF NOT EXISTS notice_level text;
ALTER TABLE public.violations ADD COLUMN IF NOT EXISTS notice_history jsonb;
ALTER TABLE public.violations ADD COLUMN IF NOT EXISTS timeline_entries jsonb;
ALTER TABLE public.violations ADD COLUMN IF NOT EXISTS assigned_to uuid;
-- Add missing columns to violation_types table
ALTER TABLE public.violation_types ADD COLUMN IF NOT EXISTS article_section text;
ALTER TABLE public.violation_types ADD COLUMN IF NOT EXISTS citation text;
ALTER TABLE public.violation_types ADD COLUMN IF NOT EXISTS requested_action text;
-- Create violation-photos storage bucket
INSERT INTO storage.buckets (id, name, public) VALUES ('violation-photos', 'violation-photos', true)
ON CONFLICT (id) DO NOTHING;
-- Allow authenticated users to upload to violation-photos
CREATE POLICY "Authenticated users can upload violation photos" ON storage.objects
FOR INSERT TO authenticated WITH CHECK (bucket_id = 'violation-photos');
-- Allow public read access to violation photos
CREATE POLICY "Public read access for violation photos" ON storage.objects
FOR SELECT TO public USING (bucket_id = 'violation-photos');
-- Allow authenticated users to delete their violation photos
CREATE POLICY "Authenticated users can delete violation photos" ON storage.objects
FOR DELETE TO authenticated USING (bucket_id = 'violation-photos');
@@ -0,0 +1,130 @@
-- Email Senders (SMTP sender identities)
CREATE TABLE public.email_senders (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
sender_name TEXT NOT NULL,
email_address TEXT NOT NULL,
smtp_host TEXT,
smtp_port INTEGER DEFAULT 587,
smtp_username TEXT,
smtp_password TEXT,
use_tls BOOLEAN DEFAULT true,
use_ssl BOOLEAN DEFAULT false,
is_active BOOLEAN DEFAULT true,
verified BOOLEAN DEFAULT false,
is_default BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.email_senders ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users manage own senders" ON public.email_senders FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
-- Email History
CREATE TABLE public.email_history (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
sender_email TEXT,
recipient_email TEXT,
subject TEXT,
body_text TEXT,
sent_at TIMESTAMPTZ DEFAULT now(),
status TEXT DEFAULT 'sent',
feature_type TEXT,
email_headers JSONB,
proof_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.email_history ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users view own email history" ON public.email_history FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
-- Email Routing Rules (inbound email addresses per association)
CREATE TABLE public.email_routing_rules (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
email_address TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID
);
ALTER TABLE public.email_routing_rules ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users manage routing" ON public.email_routing_rules FOR ALL TO authenticated USING (true) WITH CHECK (true);
-- Email Templates (reusable template library)
CREATE TABLE public.email_templates (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
subject TEXT,
body_html TEXT,
thumbnail_url TEXT,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.email_templates ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users manage templates" ON public.email_templates FOR ALL TO authenticated USING (true) WITH CHECK (true);
-- Notify Board Templates
CREATE TABLE public.notify_board_templates (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
subject TEXT,
body TEXT,
attachments JSONB,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.notify_board_templates ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users manage board templates" ON public.notify_board_templates FOR ALL TO authenticated USING (true) WITH CHECK (true);
-- Owner Notification Templates
CREATE TABLE public.owner_notification_templates (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
subject TEXT,
body TEXT,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.owner_notification_templates ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users manage notification templates" ON public.owner_notification_templates FOR ALL TO authenticated USING (true) WITH CHECK (true);
-- Owner Notification Proofs
CREATE TABLE public.owner_notification_proofs (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
client_id UUID,
association_id UUID REFERENCES public.associations(id) ON DELETE SET NULL,
subject TEXT,
body TEXT,
owners_notified JSONB,
attachments JSONB,
delivery_status TEXT,
validation_id TEXT DEFAULT gen_random_uuid()::TEXT,
proof_url TEXT,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.owner_notification_proofs ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users manage proofs" ON public.owner_notification_proofs FOR ALL TO authenticated USING (true) WITH CHECK (true);
-- Email Server Settings (per association SMTP/IMAP config)
CREATE TABLE public.email_server_settings (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
smtp_host TEXT,
smtp_port INTEGER,
smtp_username TEXT,
smtp_password TEXT,
imap_host TEXT,
imap_port INTEGER,
pop3_host TEXT,
pop3_port INTEGER,
is_configured BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(association_id)
);
ALTER TABLE public.email_server_settings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users manage server settings" ON public.email_server_settings FOR ALL TO authenticated USING (true) WITH CHECK (true);
@@ -0,0 +1,102 @@
-- Project comments/discussion table
CREATE TABLE public.project_comments (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
project_id UUID NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.project_comments ENABLE ROW LEVEL SECURITY;
-- Project files table
CREATE TABLE public.project_files (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
project_id UUID NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
file_name TEXT NOT NULL,
file_url TEXT NOT NULL,
file_size BIGINT,
mime_type TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.project_files ENABLE ROW LEVEL SECURITY;
-- Storage bucket for project files
INSERT INTO storage.buckets (id, name, public) VALUES ('project-files', 'project-files', true);
-- Enable realtime for comments
ALTER PUBLICATION supabase_realtime ADD TABLE public.project_comments;
-- RLS for project_comments: authenticated users can read all, insert own
CREATE POLICY "Authenticated users can read project comments"
ON public.project_comments FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert own comments"
ON public.project_comments FOR INSERT TO authenticated
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own comments"
ON public.project_comments FOR UPDATE TO authenticated
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own comments"
ON public.project_comments FOR DELETE TO authenticated
USING (auth.uid() = user_id);
-- RLS for project_files: authenticated users can read all, insert own
CREATE POLICY "Authenticated users can read project files"
ON public.project_files FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can upload files"
ON public.project_files FOR INSERT TO authenticated
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own files"
ON public.project_files FOR DELETE TO authenticated
USING (auth.uid() = user_id);
-- Admin/manager can delete any file
CREATE POLICY "Admins can delete any file"
ON public.project_files FOR DELETE TO authenticated
USING (public.has_role(auth.uid(), 'admin') OR public.has_role(auth.uid(), 'manager'));
-- Storage policies for project-files bucket
CREATE POLICY "Authenticated users can read project files storage"
ON storage.objects FOR SELECT TO authenticated
USING (bucket_id = 'project-files');
CREATE POLICY "Authenticated users can upload project files storage"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'project-files');
CREATE POLICY "Users can delete own project files storage"
ON storage.objects FOR DELETE TO authenticated
USING (bucket_id = 'project-files');
-- Update projects RLS: drop old policy, add new ones allowing clients to create and view
DROP POLICY IF EXISTS "Staff full access on projects" ON public.projects;
-- Admins/managers full access
CREATE POLICY "Admin manager full access on projects"
ON public.projects FOR ALL TO authenticated
USING (public.has_role(auth.uid(), 'admin') OR public.has_role(auth.uid(), 'manager'))
WITH CHECK (public.has_role(auth.uid(), 'admin') OR public.has_role(auth.uid(), 'manager'));
-- All authenticated can read projects
CREATE POLICY "Authenticated users can view projects"
ON public.projects FOR SELECT TO authenticated
USING (true);
-- All authenticated can create projects
CREATE POLICY "Authenticated users can create projects"
ON public.projects FOR INSERT TO authenticated
WITH CHECK (auth.uid() = created_by);
-- Users can update own projects (but not status to completed - enforced in app)
CREATE POLICY "Users can update own projects"
ON public.projects FOR UPDATE TO authenticated
USING (auth.uid() = created_by);
@@ -0,0 +1,45 @@
-- Table to store shared links for folders and documents
CREATE TABLE public.shared_links (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
share_type TEXT NOT NULL DEFAULT 'folder', -- 'folder' or 'document'
folder_name TEXT, -- for folder shares
document_id UUID REFERENCES public.documents(id) ON DELETE CASCADE, -- for document shares
is_public BOOLEAN NOT NULL DEFAULT false,
access_code TEXT NOT NULL,
share_token TEXT NOT NULL UNIQUE DEFAULT encode(gen_random_bytes(16), 'hex'),
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
expires_at TIMESTAMP WITH TIME ZONE -- optional expiry
);
ALTER TABLE public.shared_links ENABLE ROW LEVEL SECURITY;
-- Staff can manage shared links
CREATE POLICY "Staff full access on shared_links"
ON public.shared_links
FOR ALL
TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
-- Public read for validating access codes (anon users need this)
CREATE POLICY "Anyone can validate shared links"
ON public.shared_links
FOR SELECT
TO anon
USING (is_public = true);
-- Also allow authenticated users to read
CREATE POLICY "Authenticated can read shared links"
ON public.shared_links
FOR SELECT
TO authenticated
USING (true);
-- Trigger for updated_at
CREATE TRIGGER update_shared_links_updated_at
BEFORE UPDATE ON public.shared_links
FOR EACH ROW
EXECUTE FUNCTION public.update_updated_at_column();
@@ -0,0 +1,7 @@
-- Allow anonymous users to read documents (needed for shared access page)
CREATE POLICY "Anon can read documents via shared links"
ON public.documents
FOR SELECT
TO anon
USING (true);
@@ -0,0 +1,54 @@
-- Create client_invoices table to track invoices generated from billable expenses and forms
CREATE TABLE public.client_invoices (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
invoice_number TEXT NOT NULL,
association_id UUID NOT NULL REFERENCES public.associations(id),
amount NUMERIC NOT NULL DEFAULT 0,
credits NUMERIC NOT NULL DEFAULT 0,
total NUMERIC NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'sent',
issue_date DATE NOT NULL DEFAULT CURRENT_DATE,
due_date DATE,
paid_date DATE,
source TEXT NOT NULL DEFAULT 'billable_expenses',
notes TEXT,
created_by UUID,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Enable RLS
ALTER TABLE public.client_invoices ENABLE ROW LEVEL SECURITY;
-- Staff full access
CREATE POLICY "Staff full access on client_invoices"
ON public.client_invoices
FOR ALL
TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
-- Create client_invoice_items to store line items
CREATE TABLE public.client_invoice_items (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
invoice_id UUID NOT NULL REFERENCES public.client_invoices(id) ON DELETE CASCADE,
billable_expense_id UUID REFERENCES public.billable_expenses(id),
description TEXT,
category TEXT,
date DATE,
quantity NUMERIC NOT NULL DEFAULT 1,
unit_price NUMERIC NOT NULL DEFAULT 0,
amount NUMERIC NOT NULL DEFAULT 0,
is_credit BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
ALTER TABLE public.client_invoice_items ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on client_invoice_items"
ON public.client_invoice_items
FOR ALL
TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
@@ -0,0 +1,60 @@
-- Add new roles to app_role enum for committee members
ALTER TYPE public.app_role ADD VALUE IF NOT EXISTS 'board_member';
ALTER TYPE public.app_role ADD VALUE IF NOT EXISTS 'arc_member';
ALTER TYPE public.app_role ADD VALUE IF NOT EXISTS 'fining_member';
-- Create role_permissions table for feature-area CRUD permissions
CREATE TABLE public.role_permissions (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
role TEXT NOT NULL,
feature_area TEXT NOT NULL,
can_read BOOLEAN NOT NULL DEFAULT false,
can_create BOOLEAN NOT NULL DEFAULT false,
can_edit BOOLEAN NOT NULL DEFAULT false,
can_delete BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
UNIQUE(role, feature_area)
);
ALTER TABLE public.role_permissions ENABLE ROW LEVEL SECURITY;
-- Only admins can manage permissions
CREATE POLICY "Admins can manage role_permissions"
ON public.role_permissions
FOR ALL
TO authenticated
USING (public.has_role(auth.uid(), 'admin'::app_role))
WITH CHECK (public.has_role(auth.uid(), 'admin'::app_role));
-- All authenticated can read permissions (needed to enforce them client-side)
CREATE POLICY "Authenticated can read role_permissions"
ON public.role_permissions
FOR SELECT
TO authenticated
USING (true);
-- Seed default permissions for all roles and feature areas
INSERT INTO public.role_permissions (role, feature_area, can_read, can_create, can_edit, can_delete)
SELECT r.role, f.feature_area,
CASE WHEN r.role = 'homeowner' THEN
f.feature_area IN ('Owner Portal', 'Documents', 'Announcements', 'Calendar')
ELSE true END,
CASE WHEN r.role = 'homeowner' THEN false
WHEN r.role IN ('board_member','arc_member','fining_member') THEN false
ELSE true END,
CASE WHEN r.role = 'homeowner' THEN false
WHEN r.role IN ('board_member','arc_member','fining_member') THEN false
ELSE true END,
CASE WHEN r.role IN ('admin') THEN true ELSE false END
FROM
(VALUES ('admin'),('manager'),('staff'),('employee'),('homeowner'),('board_member'),('arc_member'),('fining_member')) AS r(role),
(VALUES ('Financial'),('Owners & Units'),('Violations'),('ARC Applications'),('Collections'),('Documents'),('Announcements'),('Calendar'),('Vendors & Bills'),('Projects'),('Email & Notifications'),('Legal Matters'),('Inspections'),('Owner Portal'),('Board Votes'),('Estoppels'),('Parking')) AS f(feature_area)
ON CONFLICT (role, feature_area) DO NOTHING;
-- Trigger for updated_at
CREATE TRIGGER update_role_permissions_updated_at
BEFORE UPDATE ON public.role_permissions
FOR EACH ROW
EXECUTE FUNCTION public.update_updated_at_column();
@@ -0,0 +1,7 @@
-- Add target_month (YYYY-MM-01 format) and completed_at to payables
ALTER TABLE public.payables ADD COLUMN target_month DATE DEFAULT date_trunc('month', CURRENT_DATE)::date;
ALTER TABLE public.payables ADD COLUMN completed_at TIMESTAMP WITH TIME ZONE;
-- Update existing payables to have target_month based on due_date or created_at
UPDATE public.payables SET target_month = COALESCE(date_trunc('month', due_date)::date, date_trunc('month', created_at)::date);
@@ -0,0 +1,29 @@
-- Store validation proof records for forms & letters
CREATE TABLE public.document_validation_proofs (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
document_type TEXT NOT NULL,
document_title TEXT,
generated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
generated_by UUID,
association_id UUID REFERENCES public.associations(id),
metadata JSONB DEFAULT '{}',
verification_hash TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
ALTER TABLE public.document_validation_proofs ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on document_validation_proofs"
ON public.document_validation_proofs
FOR ALL
TO authenticated
USING (public.has_role(auth.uid(), 'admin'::app_role) OR public.has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (public.has_role(auth.uid(), 'admin'::app_role) OR public.has_role(auth.uid(), 'manager'::app_role));
-- Anyone can verify a proof by ID (public read for verification)
CREATE POLICY "Anyone can verify proofs"
ON public.document_validation_proofs
FOR SELECT
TO authenticated
USING (true);
@@ -0,0 +1,42 @@
-- ARC application votes
CREATE TABLE public.arc_application_votes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
application_id UUID NOT NULL REFERENCES public.arc_applications(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
vote TEXT NOT NULL CHECK (vote IN ('approve', 'deny')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (application_id, user_id)
);
ALTER TABLE public.arc_application_votes ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on arc_application_votes"
ON public.arc_application_votes FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
CREATE POLICY "Authenticated users can view arc_application_votes"
ON public.arc_application_votes FOR SELECT TO authenticated
USING (true);
-- ARC application comments
CREATE TABLE public.arc_application_comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
application_id UUID NOT NULL REFERENCES public.arc_applications(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
comment TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.arc_application_comments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on arc_application_comments"
ON public.arc_application_comments FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
CREATE POLICY "Authenticated users can view arc_application_comments"
ON public.arc_application_comments FOR SELECT TO authenticated
USING (true);
@@ -0,0 +1,66 @@
-- Generic entity votes (reusable for bids, board votes, etc.)
CREATE TABLE public.entity_votes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type TEXT NOT NULL,
entity_id UUID NOT NULL,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
vote TEXT NOT NULL CHECK (vote IN ('approve', 'deny')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (entity_type, entity_id, user_id)
);
CREATE INDEX idx_entity_votes_lookup ON public.entity_votes(entity_type, entity_id);
ALTER TABLE public.entity_votes ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on entity_votes"
ON public.entity_votes FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
CREATE POLICY "Authenticated users can view entity_votes"
ON public.entity_votes FOR SELECT TO authenticated
USING (true);
-- Generic entity comments
CREATE TABLE public.entity_comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type TEXT NOT NULL,
entity_id UUID NOT NULL,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
comment TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_entity_comments_lookup ON public.entity_comments(entity_type, entity_id);
ALTER TABLE public.entity_comments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on entity_comments"
ON public.entity_comments FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
CREATE POLICY "Authenticated users can view entity_comments"
ON public.entity_comments FOR SELECT TO authenticated
USING (true);
-- Board votes table for the Board Votes feature
CREATE TABLE public.board_votes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'open',
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.board_votes ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff full access on board_votes"
ON public.board_votes FOR ALL TO authenticated
USING (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role))
WITH CHECK (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role));
CREATE POLICY "Authenticated users can view board_votes"
ON public.board_votes FOR SELECT TO authenticated
USING (true);
@@ -0,0 +1,50 @@
-- Add new columns to legal_matters to match the reference design
ALTER TABLE public.legal_matters
ADD COLUMN IF NOT EXISTS case_type TEXT DEFAULT NULL,
ADD COLUMN IF NOT EXISTS official_case_name TEXT DEFAULT NULL,
ADD COLUMN IF NOT EXISTS current_stage TEXT DEFAULT 'intent_to_lien',
ADD COLUMN IF NOT EXISTS judge TEXT DEFAULT NULL,
ADD COLUMN IF NOT EXISTS amount_due NUMERIC DEFAULT 0,
ADD COLUMN IF NOT EXISTS opposing_counsel TEXT DEFAULT NULL,
ADD COLUMN IF NOT EXISTS unit_id UUID REFERENCES public.units(id) DEFAULT NULL,
ADD COLUMN IF NOT EXISTS collection_id UUID REFERENCES public.collections(id) DEFAULT NULL;
-- Create a DB function that auto-creates a legal matter when a collection is inserted
CREATE OR REPLACE FUNCTION public.auto_create_legal_matter_from_collection()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public'
AS $$
BEGIN
INSERT INTO public.legal_matters (
association_id,
title,
case_type,
category,
current_stage,
amount_due,
collection_id,
status,
description
) VALUES (
NEW.association_id,
'Collection Case - ' || COALESCE(NEW.notes, 'No description'),
'collection',
'collections',
'intent_to_lien',
NEW.amount_owed,
NEW.id,
'open',
NEW.notes
);
RETURN NEW;
END;
$$;
-- Create the trigger
CREATE TRIGGER trg_collection_to_legal_matter
AFTER INSERT ON public.collections
FOR EACH ROW
EXECUTE FUNCTION public.auto_create_legal_matter_from_collection();
@@ -0,0 +1,62 @@
-- Create parking_records table for violation tracking
CREATE TABLE public.parking_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID NOT NULL REFERENCES public.associations(id),
title TEXT NOT NULL DEFAULT '',
description TEXT,
address TEXT,
vehicle_plate TEXT,
vehicle_make TEXT,
vehicle_model TEXT,
photo_url TEXT,
warning_level TEXT NOT NULL DEFAULT 'first_warning' CHECK (warning_level IN ('first_warning', 'second_warning', 'final_warning')),
recorded_by TEXT,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
citation TEXT,
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'closed', 'resolved')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.parking_records ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can view parking records"
ON public.parking_records FOR SELECT TO authenticated USING (true);
CREATE POLICY "Admin/manager can insert parking records"
ON public.parking_records FOR INSERT TO authenticated
WITH CHECK (
public.has_role(auth.uid(), 'admin') OR public.has_role(auth.uid(), 'manager')
);
CREATE POLICY "Admin/manager can update parking records"
ON public.parking_records FOR UPDATE TO authenticated
USING (
public.has_role(auth.uid(), 'admin') OR public.has_role(auth.uid(), 'manager')
);
CREATE POLICY "Admin/manager can delete parking records"
ON public.parking_records FOR DELETE TO authenticated
USING (
public.has_role(auth.uid(), 'admin') OR public.has_role(auth.uid(), 'manager')
);
CREATE TRIGGER update_parking_records_updated_at
BEFORE UPDATE ON public.parking_records
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-- Create parking-photos storage bucket
INSERT INTO storage.buckets (id, name, public) VALUES ('parking-photos', 'parking-photos', true)
ON CONFLICT (id) DO NOTHING;
CREATE POLICY "Anyone can view parking photos"
ON storage.objects FOR SELECT USING (bucket_id = 'parking-photos');
CREATE POLICY "Authenticated can upload parking photos"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'parking-photos');
CREATE POLICY "Authenticated can update parking photos"
ON storage.objects FOR UPDATE TO authenticated
USING (bucket_id = 'parking-photos');
@@ -0,0 +1,3 @@
ALTER TABLE public.owners
ADD COLUMN IF NOT EXISTS street_address TEXT DEFAULT NULL;
@@ -0,0 +1,78 @@
-- Public form templates (designed by admins)
CREATE TABLE public.public_form_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
fields JSONB NOT NULL DEFAULT '[]'::jsonb,
settings JSONB DEFAULT '{}'::jsonb,
is_published BOOLEAN DEFAULT false,
slug TEXT UNIQUE,
require_auth BOOLEAN DEFAULT false,
allow_attachments BOOLEAN DEFAULT true,
created_by UUID,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Public form submissions
CREATE TABLE public.public_form_submissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
template_id UUID NOT NULL REFERENCES public.public_form_templates(id) ON DELETE CASCADE,
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
submitter_name TEXT,
submitter_email TEXT,
form_data JSONB NOT NULL DEFAULT '{}'::jsonb,
attachments JSONB DEFAULT '[]'::jsonb,
status TEXT DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
ALTER TABLE public.public_form_templates ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.public_form_submissions ENABLE ROW LEVEL SECURITY;
-- Staff can manage templates
CREATE POLICY "Staff can manage form templates"
ON public.public_form_templates FOR ALL TO authenticated
USING (true) WITH CHECK (true);
-- Staff can view submissions
CREATE POLICY "Staff can manage form submissions"
ON public.public_form_submissions FOR ALL TO authenticated
USING (true) WITH CHECK (true);
-- Anonymous users can view published templates
CREATE POLICY "Anyone can view published templates"
ON public.public_form_templates FOR SELECT TO anon
USING (is_published = true);
-- Anonymous users can submit forms
CREATE POLICY "Anyone can submit forms"
ON public.public_form_submissions FOR INSERT TO anon
WITH CHECK (true);
-- Triggers
CREATE TRIGGER update_public_form_templates_updated_at
BEFORE UPDATE ON public.public_form_templates
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_public_form_submissions_updated_at
BEFORE UPDATE ON public.public_form_submissions
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-- Storage bucket for public form attachments
INSERT INTO storage.buckets (id, name, public) VALUES ('public-form-attachments', 'public-form-attachments', true);
CREATE POLICY "Anyone can upload form attachments"
ON storage.objects FOR INSERT TO anon
WITH CHECK (bucket_id = 'public-form-attachments');
CREATE POLICY "Anyone can view form attachments"
ON storage.objects FOR SELECT TO anon
USING (bucket_id = 'public-form-attachments');
CREATE POLICY "Auth users manage form attachments"
ON storage.objects FOR ALL TO authenticated
USING (bucket_id = 'public-form-attachments');
@@ -0,0 +1,43 @@
-- Fee schedule subcategories
CREATE TABLE public.fee_schedule_subcategories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
is_custom BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Fee schedules
CREATE TABLE public.fee_schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
description TEXT NOT NULL,
fee NUMERIC NOT NULL DEFAULT 0,
category TEXT,
subcategory TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
ALTER TABLE public.fee_schedule_subcategories ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.fee_schedules ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can manage subcategories"
ON public.fee_schedule_subcategories FOR ALL TO authenticated
USING (true) WITH CHECK (true);
CREATE POLICY "Authenticated users can manage fee schedules"
ON public.fee_schedules FOR ALL TO authenticated
USING (true) WITH CHECK (true);
CREATE TRIGGER update_fee_schedules_updated_at
BEFORE UPDATE ON public.fee_schedules
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-- Seed default subcategories
INSERT INTO public.fee_schedule_subcategories (name, is_custom) VALUES
('Estoppel', false),
('Late Fee', false),
('Legal', false),
('Compliance', false),
('Administrative', false),
('Maintenance', false);
@@ -0,0 +1,38 @@
-- Add attorney and CPA fields to associations
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS attorney_name TEXT;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS attorney_firm TEXT;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS attorney_email TEXT;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS attorney_phone TEXT;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS cpa_name TEXT;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS cpa_firm TEXT;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS cpa_email TEXT;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS cpa_phone TEXT;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS insurance_provider TEXT;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS insurance_expiration DATE;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS website TEXT;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS care_of_address TEXT;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS mailing_address TEXT;
ALTER TABLE public.associations ADD COLUMN IF NOT EXISTS contact_email TEXT;
-- Violation types per association
CREATE TABLE IF NOT EXISTS public.violation_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
category TEXT NOT NULL,
article_section TEXT,
citation_text TEXT,
requested_action TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
ALTER TABLE public.violation_types ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can manage violation types"
ON public.violation_types FOR ALL TO authenticated
USING (true) WITH CHECK (true);
CREATE TRIGGER update_violation_types_updated_at
BEFORE UPDATE ON public.violation_types
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
@@ -0,0 +1,64 @@
-- 1. Create public_form_submission_reports table
CREATE TABLE public.public_form_submission_reports (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
submission_id UUID NOT NULL REFERENCES public.public_form_submissions(id) ON DELETE CASCADE,
template_id UUID NOT NULL REFERENCES public.public_form_templates(id) ON DELETE CASCADE,
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
report_data JSONB,
status TEXT NOT NULL DEFAULT 'generated',
generated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Enable RLS
ALTER TABLE public.public_form_submission_reports ENABLE ROW LEVEL SECURITY;
-- Staff can manage reports
CREATE POLICY "Staff can manage submission reports"
ON public.public_form_submission_reports
FOR ALL
TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'));
-- 2. Add report_styling column to public_form_templates if missing
ALTER TABLE public.public_form_templates ADD COLUMN IF NOT EXISTS report_styling JSONB;
-- 3. Fix shared_links: ensure share_token has a default so INSERT works
ALTER TABLE public.shared_links ALTER COLUMN share_token SET DEFAULT encode(gen_random_bytes(16), 'hex');
-- 4. Fix the documents anon policy - it's too permissive (allows reading ALL documents)
-- Replace with a scoped policy that only allows reading documents referenced by a public shared link
DROP POLICY IF EXISTS "Anon can read documents via shared links" ON public.documents;
CREATE POLICY "Anon can read documents via shared links"
ON public.documents
FOR SELECT
TO anon
USING (
id IN (
SELECT sl.document_id FROM public.shared_links sl WHERE sl.is_public = true AND sl.document_id IS NOT NULL
)
OR
category IN (
SELECT sl.folder_name FROM public.shared_links sl WHERE sl.is_public = true AND sl.share_type = 'folder'
)
);
-- 5. Add employee/staff roles to shared_links policy so non-admin staff can create share links
DROP POLICY IF EXISTS "Staff full access on shared_links" ON public.shared_links;
CREATE POLICY "Staff full access on shared_links"
ON public.shared_links
FOR ALL
TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager') OR has_role(auth.uid(), 'employee') OR has_role(auth.uid(), 'staff'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager') OR has_role(auth.uid(), 'employee') OR has_role(auth.uid(), 'staff'));
-- Trigger for updated_at
CREATE TRIGGER update_public_form_submission_reports_updated_at
BEFORE UPDATE ON public.public_form_submission_reports
FOR EACH ROW
EXECUTE FUNCTION public.update_updated_at_column();
@@ -0,0 +1,96 @@
-- Stripe account mappings per association
CREATE TABLE public.stripe_account_mappings (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
stripe_account_id TEXT NOT NULL,
stripe_public_key TEXT NOT NULL,
stripe_secret_key TEXT,
is_active BOOLEAN NOT NULL DEFAULT true,
pass_processing_fee BOOLEAN NOT NULL DEFAULT false,
processing_fee_percent NUMERIC(5,4) NOT NULL DEFAULT 0.029,
processing_fee_fixed_cents INTEGER NOT NULL DEFAULT 30,
created_by UUID,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
UNIQUE(association_id),
UNIQUE(stripe_account_id)
);
-- Enable RLS
ALTER TABLE public.stripe_account_mappings ENABLE ROW LEVEL SECURITY;
-- Admin/manager full access
CREATE POLICY "Staff can manage stripe mappings"
ON public.stripe_account_mappings
FOR ALL
TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager'));
-- Homeowners can read active mappings (to get public key for their association)
CREATE POLICY "Homeowners can read active stripe mappings"
ON public.stripe_account_mappings
FOR SELECT
TO authenticated
USING (is_active = true);
-- Stripe payment records
CREATE TABLE public.stripe_payments (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE,
owner_id UUID REFERENCES public.owners(id) ON DELETE SET NULL,
unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL,
stripe_payment_intent_id TEXT,
amount_cents INTEGER NOT NULL,
fee_cents INTEGER NOT NULL DEFAULT 0,
total_cents INTEGER NOT NULL,
payment_method_type TEXT NOT NULL DEFAULT 'card',
status TEXT NOT NULL DEFAULT 'pending',
description TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
ALTER TABLE public.stripe_payments ENABLE ROW LEVEL SECURITY;
-- Staff full access
CREATE POLICY "Staff can manage stripe payments"
ON public.stripe_payments
FOR ALL
TO authenticated
USING (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager') OR has_role(auth.uid(), 'employee') OR has_role(auth.uid(), 'staff'))
WITH CHECK (has_role(auth.uid(), 'admin') OR has_role(auth.uid(), 'manager') OR has_role(auth.uid(), 'employee') OR has_role(auth.uid(), 'staff'));
-- Homeowners can see their own payments
CREATE POLICY "Homeowners can view own stripe payments"
ON public.stripe_payments
FOR SELECT
TO authenticated
USING (
owner_id IN (
SELECT o.id FROM public.owners o WHERE o.user_id = auth.uid()
)
);
-- Homeowners can insert their own payments
CREATE POLICY "Homeowners can create own stripe payments"
ON public.stripe_payments
FOR INSERT
TO authenticated
WITH CHECK (
owner_id IN (
SELECT o.id FROM public.owners o WHERE o.user_id = auth.uid()
)
);
-- Triggers
CREATE TRIGGER update_stripe_account_mappings_updated_at
BEFORE UPDATE ON public.stripe_account_mappings
FOR EACH ROW
EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_stripe_payments_updated_at
BEFORE UPDATE ON public.stripe_payments
FOR EACH ROW
EXECUTE FUNCTION public.update_updated_at_column();
@@ -0,0 +1,106 @@
-- Homeowners can read documents belonging to their association
CREATE POLICY "Homeowners can view association documents"
ON public.documents FOR SELECT TO authenticated
USING (
has_role(auth.uid(), 'homeowner'::app_role)
AND association_id IN (
SELECT o.association_id FROM public.owners o WHERE o.user_id = auth.uid()
)
);
-- Homeowners can insert ARC applications for their association
CREATE POLICY "Homeowners can submit ARC applications"
ON public.arc_applications FOR INSERT TO authenticated
WITH CHECK (
has_role(auth.uid(), 'homeowner'::app_role)
AND association_id IN (
SELECT o.association_id FROM public.owners o WHERE o.user_id = auth.uid()
)
);
-- Homeowners can view their own ARC applications
CREATE POLICY "Homeowners can view own ARC applications"
ON public.arc_applications FOR SELECT TO authenticated
USING (
has_role(auth.uid(), 'homeowner'::app_role)
AND owner_id IN (
SELECT o.id FROM public.owners o WHERE o.user_id = auth.uid()
)
);
-- Homeowners can update their own pending ARC applications
CREATE POLICY "Homeowners can update own pending ARC applications"
ON public.arc_applications FOR UPDATE TO authenticated
USING (
has_role(auth.uid(), 'homeowner'::app_role)
AND owner_id IN (
SELECT o.id FROM public.owners o WHERE o.user_id = auth.uid()
)
AND status IN ('pending', 'draft')
);
-- Homeowners can add comments to their own ARC applications
CREATE POLICY "Homeowners can comment on own ARC apps"
ON public.arc_application_comments FOR INSERT TO authenticated
WITH CHECK (
has_role(auth.uid(), 'homeowner'::app_role)
AND application_id IN (
SELECT a.id FROM public.arc_applications a
JOIN public.owners o ON o.id = a.owner_id
WHERE o.user_id = auth.uid()
)
);
-- Board members: add user_id column to board_members for linking
ALTER TABLE public.board_members ADD COLUMN IF NOT EXISTS user_id uuid REFERENCES auth.users(id);
-- Board members get full read access to all documents in their association
CREATE POLICY "Board members can view association documents"
ON public.documents FOR SELECT TO authenticated
USING (
association_id IN (
SELECT bm.association_id FROM public.board_members bm WHERE bm.user_id = auth.uid()
)
);
-- Board members can view all ARC applications in their association
CREATE POLICY "Board members can view association ARC applications"
ON public.arc_applications FOR SELECT TO authenticated
USING (
association_id IN (
SELECT bm.association_id FROM public.board_members bm WHERE bm.user_id = auth.uid()
)
);
-- Board members can vote on ARC applications
CREATE POLICY "Board members can vote on ARC applications"
ON public.arc_application_votes FOR INSERT TO authenticated
WITH CHECK (
application_id IN (
SELECT a.id FROM public.arc_applications a
JOIN public.board_members bm ON bm.association_id = a.association_id
WHERE bm.user_id = auth.uid()
)
);
-- Board members can comment on ARC applications
CREATE POLICY "Board members can comment on ARC applications"
ON public.arc_application_comments FOR INSERT TO authenticated
WITH CHECK (
application_id IN (
SELECT a.id FROM public.arc_applications a
JOIN public.board_members bm ON bm.association_id = a.association_id
WHERE bm.user_id = auth.uid()
)
);
-- Board members can update ARC application status (approve/deny)
CREATE POLICY "Board members can update ARC applications"
ON public.arc_applications FOR UPDATE TO authenticated
USING (
association_id IN (
SELECT bm.association_id FROM public.board_members bm
WHERE bm.user_id = auth.uid() AND bm.approval_authority = true
)
);
@@ -0,0 +1,46 @@
-- Align backend owner-roster access with granular feature permissions while preserving admin/manager access.
CREATE OR REPLACE FUNCTION public.has_feature_permission(_user_id uuid, _feature_area text, _action text)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT
public.has_role(_user_id, 'admin'::public.app_role)
OR public.has_role(_user_id, 'manager'::public.app_role)
OR EXISTS (
SELECT 1
FROM public.user_roles ur
JOIN public.role_permissions rp
ON rp.role = ur.role::text
WHERE ur.user_id = _user_id
AND rp.feature_area = _feature_area
AND CASE _action
WHEN 'read' THEN rp.can_read
WHEN 'create' THEN rp.can_create
WHEN 'edit' THEN rp.can_edit
WHEN 'delete' THEN rp.can_delete
ELSE false
END
);
$$;
CREATE POLICY "Authorized users can create owners"
ON public.owners
FOR INSERT
TO authenticated
WITH CHECK (public.has_feature_permission(auth.uid(), 'Owners & Units', 'create'));
CREATE POLICY "Authorized users can update owners"
ON public.owners
FOR UPDATE
TO authenticated
USING (public.has_feature_permission(auth.uid(), 'Owners & Units', 'edit'))
WITH CHECK (public.has_feature_permission(auth.uid(), 'Owners & Units', 'edit'));
CREATE POLICY "Authorized users can delete owners"
ON public.owners
FOR DELETE
TO authenticated
USING (public.has_feature_permission(auth.uid(), 'Owners & Units', 'delete'));
@@ -0,0 +1,4 @@
-- Add "User Management" feature area for board_member with read + create + edit
INSERT INTO public.role_permissions (role, feature_area, can_read, can_create, can_edit, can_delete)
VALUES ('board_member', 'User Management', true, true, true, false)
ON CONFLICT DO NOTHING;
@@ -0,0 +1 @@
ALTER TABLE public.collections ADD COLUMN unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL;
@@ -0,0 +1,39 @@
-- Allow any authenticated user to insert their own comments
CREATE POLICY "Authenticated users can insert own entity_comments"
ON public.entity_comments
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- Allow any authenticated user to insert their own votes
CREATE POLICY "Authenticated users can insert own entity_votes"
ON public.entity_votes
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- Allow any authenticated user to update their own votes
CREATE POLICY "Authenticated users can update own entity_votes"
ON public.entity_votes
FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Allow any authenticated user to delete their own votes
CREATE POLICY "Authenticated users can delete own entity_votes"
ON public.entity_votes
FOR DELETE
TO authenticated
USING (auth.uid() = user_id);
-- Allow any authenticated user to delete their own comments
CREATE POLICY "Authenticated users can delete own entity_comments"
ON public.entity_comments
FOR DELETE
TO authenticated
USING (auth.uid() = user_id);
-- Enable realtime for both tables
ALTER PUBLICATION supabase_realtime ADD TABLE public.entity_votes;
ALTER PUBLICATION supabase_realtime ADD TABLE public.entity_comments;
@@ -0,0 +1 @@
INSERT INTO storage.buckets (id, name, public) VALUES ('invoices', 'invoices', true) ON CONFLICT (id) DO NOTHING;
@@ -0,0 +1 @@
ALTER TABLE public.bills ADD COLUMN IF NOT EXISTS zoho_bill_id TEXT;
@@ -0,0 +1,17 @@
CREATE TABLE public.bill_comments (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
bill_id UUID NOT NULL REFERENCES public.bills(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
user_name TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
ALTER TABLE public.bill_comments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can read bill comments"
ON public.bill_comments FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert bill comments"
ON public.bill_comments FOR INSERT TO authenticated WITH CHECK (true);
@@ -0,0 +1 @@
ALTER TABLE public.associations ADD CONSTRAINT associations_name_unique UNIQUE (name);
@@ -0,0 +1,2 @@
ALTER TABLE public.owners ADD CONSTRAINT owners_association_id_property_address_unique UNIQUE (association_id, property_address);
ALTER TABLE public.chart_of_accounts ADD CONSTRAINT chart_of_accounts_association_id_account_number_unique UNIQUE (association_id, account_number);
@@ -0,0 +1 @@
ALTER TABLE public.associations ADD COLUMN status text NOT NULL DEFAULT 'active';
@@ -0,0 +1,9 @@
-- Add 'fined' to violations status check
ALTER TABLE public.violations DROP CONSTRAINT violations_status_check;
ALTER TABLE public.violations ADD CONSTRAINT violations_status_check CHECK (status = ANY (ARRAY['open', 'pending', 'resolved', 'escalated', 'closed', 'fined']));
-- Create missing associations
INSERT INTO public.associations (id, name, status) VALUES
('be7cbde1-3bd6-4728-8a92-a43aa0f39d1d', 'Las Palmos Community Association', 'active'),
('b07d0163-6897-4908-9dcf-2d8049afaa96', 'Main Street Community Association', 'active')
ON CONFLICT (id) DO NOTHING;
@@ -0,0 +1 @@
ALTER TABLE public.owners DROP CONSTRAINT IF EXISTS owners_association_id_property_address_unique;
@@ -0,0 +1,3 @@
ALTER TABLE public.calendar_events
ADD COLUMN IF NOT EXISTS visibility text[] NOT NULL DEFAULT ARRAY['admin']::text[],
ADD COLUMN IF NOT EXISTS is_blocked boolean NOT NULL DEFAULT false;
@@ -0,0 +1,10 @@
-- Add a unique constraint on owners for Buildium sync upserts
-- Using association_id + first_name + last_name + property_address to handle multiple owners at same address
CREATE UNIQUE INDEX IF NOT EXISTS owners_buildium_sync_key
ON public.owners (association_id, first_name, last_name, property_address)
WHERE property_address IS NOT NULL;
-- Add buildium_owner_id column for future direct ID mapping
ALTER TABLE public.owners ADD COLUMN IF NOT EXISTS buildium_owner_id text;
CREATE UNIQUE INDEX IF NOT EXISTS owners_buildium_id_key ON public.owners (buildium_owner_id) WHERE buildium_owner_id IS NOT NULL;
@@ -0,0 +1,4 @@
ALTER TABLE public.units ADD COLUMN IF NOT EXISTS buildium_unit_id text;
ALTER TABLE public.units ADD COLUMN IF NOT EXISTS buildium_account_number text;
CREATE UNIQUE INDEX IF NOT EXISTS units_buildium_unit_id_idx ON public.units (buildium_unit_id) WHERE buildium_unit_id IS NOT NULL;
@@ -0,0 +1,25 @@
-- RLS policies for the 'files' storage bucket
CREATE POLICY "Authenticated users can upload to files bucket"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (bucket_id = 'files');
CREATE POLICY "Authenticated users can read files bucket"
ON storage.objects
FOR SELECT
TO authenticated
USING (bucket_id = 'files');
CREATE POLICY "Authenticated users can update files bucket"
ON storage.objects
FOR UPDATE
TO authenticated
USING (bucket_id = 'files')
WITH CHECK (bucket_id = 'files');
CREATE POLICY "Authenticated users can delete from files bucket"
ON storage.objects
FOR DELETE
TO authenticated
USING (bucket_id = 'files');
@@ -0,0 +1,26 @@
-- Create the missing company-assets bucket
INSERT INTO storage.buckets (id, name, public)
VALUES ('company-assets', 'company-assets', true)
ON CONFLICT (id) DO NOTHING;
-- RLS policies for company-assets bucket
CREATE POLICY "Authenticated users can upload to company-assets"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'company-assets');
CREATE POLICY "Authenticated users can read company-assets"
ON storage.objects FOR SELECT TO authenticated
USING (bucket_id = 'company-assets');
CREATE POLICY "Authenticated users can update company-assets"
ON storage.objects FOR UPDATE TO authenticated
USING (bucket_id = 'company-assets');
CREATE POLICY "Authenticated users can delete company-assets"
ON storage.objects FOR DELETE TO authenticated
USING (bucket_id = 'company-assets');
-- Allow public read access since bucket is public
CREATE POLICY "Public can read company-assets"
ON storage.objects FOR SELECT TO anon
USING (bucket_id = 'company-assets');
@@ -0,0 +1,12 @@
-- The documents feature stores public file URLs for the 'files' bucket,
-- so the bucket itself must be public for downloads/previews to work.
UPDATE storage.buckets
SET public = true
WHERE id = 'files';
DROP POLICY IF EXISTS "Public can read files bucket" ON storage.objects;
CREATE POLICY "Public can read files bucket"
ON storage.objects
FOR SELECT
TO anon
USING (bucket_id = 'files');
@@ -0,0 +1,25 @@
-- Create arc-files storage bucket
INSERT INTO storage.buckets (id, name, public)
VALUES ('arc-files', 'arc-files', true)
ON CONFLICT (id) DO NOTHING;
-- Allow authenticated users to upload
CREATE POLICY "Authenticated users can upload arc files"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'arc-files');
-- Allow authenticated users to read
CREATE POLICY "Authenticated users can read arc files"
ON storage.objects FOR SELECT TO authenticated
USING (bucket_id = 'arc-files');
-- Allow public read for previews
CREATE POLICY "Public can read arc files"
ON storage.objects FOR SELECT TO anon
USING (bucket_id = 'arc-files');
-- Allow authenticated users to delete their uploads
CREATE POLICY "Authenticated users can delete arc files"
ON storage.objects FOR DELETE TO authenticated
USING (bucket_id = 'arc-files');
@@ -0,0 +1 @@
ALTER TABLE public.fee_schedules ADD COLUMN IF NOT EXISTS account text;
@@ -0,0 +1,25 @@
CREATE TABLE public.migration_field_mappings (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
mapping_type TEXT NOT NULL CHECK (mapping_type IN ('table', 'column', 'id_value')),
source_table TEXT,
destination_table TEXT,
source_field TEXT,
destination_field TEXT,
source_value TEXT,
destination_value TEXT,
description TEXT,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
created_by UUID REFERENCES auth.users(id)
);
ALTER TABLE public.migration_field_mappings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Admins can manage migration mappings"
ON public.migration_field_mappings
FOR ALL
TO authenticated
USING (public.has_role(auth.uid(), 'admin'))
WITH CHECK (public.has_role(auth.uid(), 'admin'));
@@ -0,0 +1,24 @@
-- Create avatars storage bucket
INSERT INTO storage.buckets (id, name, public) VALUES ('avatars', 'avatars', true)
ON CONFLICT (id) DO NOTHING;
-- Allow authenticated users to upload their own avatar
CREATE POLICY "Users can upload own avatar"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'avatars' AND (storage.foldername(name))[1] = auth.uid()::text);
-- Allow authenticated users to update their own avatar
CREATE POLICY "Users can update own avatar"
ON storage.objects FOR UPDATE TO authenticated
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = auth.uid()::text);
-- Allow authenticated users to delete their own avatar
CREATE POLICY "Users can delete own avatar"
ON storage.objects FOR DELETE TO authenticated
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = auth.uid()::text);
-- Allow public read access to avatars
CREATE POLICY "Public read access for avatars"
ON storage.objects FOR SELECT TO public
USING (bucket_id = 'avatars');
@@ -0,0 +1 @@
ALTER TABLE public.owners ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'active';
@@ -0,0 +1,182 @@
-- Helper function: returns all association_ids the current user belongs to
-- via either the owners table or the board_members table
CREATE OR REPLACE FUNCTION public.get_user_association_ids()
RETURNS SETOF uuid
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT DISTINCT association_id FROM (
SELECT association_id FROM public.owners WHERE user_id = auth.uid()
UNION
SELECT association_id FROM public.board_members WHERE user_id = auth.uid()
) sub
$$;
-- ============================================================
-- ASSOCIATIONS: scope homeowners/board to their own HOAs
-- ============================================================
DROP POLICY IF EXISTS "Authenticated users can view associations" ON public.associations;
CREATE POLICY "Staff can view all associations"
ON public.associations FOR SELECT TO authenticated
USING (
has_role(auth.uid(), 'admin'::app_role)
OR has_role(auth.uid(), 'manager'::app_role)
OR has_role(auth.uid(), 'employee'::app_role)
);
CREATE POLICY "Members can view own associations"
ON public.associations FOR SELECT TO authenticated
USING (
id IN (SELECT get_user_association_ids())
);
-- ============================================================
-- OWNERS: homeowners see only their association's owners
-- ============================================================
DROP POLICY IF EXISTS "Authenticated users can view owners" ON public.owners;
CREATE POLICY "Staff can view all owners"
ON public.owners FOR SELECT TO authenticated
USING (
has_role(auth.uid(), 'admin'::app_role)
OR has_role(auth.uid(), 'manager'::app_role)
OR has_role(auth.uid(), 'employee'::app_role)
);
CREATE POLICY "Members can view own association owners"
ON public.owners FOR SELECT TO authenticated
USING (
association_id IN (SELECT get_user_association_ids())
);
-- ============================================================
-- UNITS: scope to association
-- ============================================================
DROP POLICY IF EXISTS "Authenticated users can view units" ON public.units;
CREATE POLICY "Staff can view all units"
ON public.units FOR SELECT TO authenticated
USING (
has_role(auth.uid(), 'admin'::app_role)
OR has_role(auth.uid(), 'manager'::app_role)
OR has_role(auth.uid(), 'employee'::app_role)
);
CREATE POLICY "Members can view own association units"
ON public.units FOR SELECT TO authenticated
USING (
association_id IN (SELECT get_user_association_ids())
);
-- ============================================================
-- VIOLATIONS: scope authenticated users to their association
-- ============================================================
DROP POLICY IF EXISTS "Authenticated users can view violations" ON public.violations;
CREATE POLICY "Staff can view all violations"
ON public.violations FOR SELECT TO authenticated
USING (
has_role(auth.uid(), 'admin'::app_role)
OR has_role(auth.uid(), 'manager'::app_role)
OR has_role(auth.uid(), 'employee'::app_role)
);
CREATE POLICY "Members can view own association violations"
ON public.violations FOR SELECT TO authenticated
USING (
association_id IN (SELECT get_user_association_ids())
);
-- ============================================================
-- BOARD_MEMBERS: scope to association
-- ============================================================
DROP POLICY IF EXISTS "Authenticated users can view board_members" ON public.board_members;
CREATE POLICY "Staff can view all board_members"
ON public.board_members FOR SELECT TO authenticated
USING (
has_role(auth.uid(), 'admin'::app_role)
OR has_role(auth.uid(), 'manager'::app_role)
OR has_role(auth.uid(), 'employee'::app_role)
);
CREATE POLICY "Members can view own association board_members"
ON public.board_members FOR SELECT TO authenticated
USING (
association_id IN (SELECT get_user_association_ids())
);
-- ============================================================
-- BOARD_VOTES: scope to association
-- ============================================================
DROP POLICY IF EXISTS "Authenticated users can view board_votes" ON public.board_votes;
CREATE POLICY "Members can view own association board_votes"
ON public.board_votes FOR SELECT TO authenticated
USING (
association_id IN (SELECT get_user_association_ids())
);
-- ============================================================
-- ASSOCIATION_FAQS: scope to association
-- ============================================================
DROP POLICY IF EXISTS "Authenticated users can view association FAQs" ON public.association_faqs;
DROP POLICY IF EXISTS "Authenticated users can update association FAQs" ON public.association_faqs;
DROP POLICY IF EXISTS "Authenticated users can delete association FAQs" ON public.association_faqs;
DROP POLICY IF EXISTS "Authenticated users can insert association FAQs" ON public.association_faqs;
CREATE POLICY "Staff can manage association FAQs"
ON public.association_faqs FOR ALL TO authenticated
USING (
has_role(auth.uid(), 'admin'::app_role)
OR has_role(auth.uid(), 'manager'::app_role)
OR has_role(auth.uid(), 'employee'::app_role)
)
WITH CHECK (
has_role(auth.uid(), 'admin'::app_role)
OR has_role(auth.uid(), 'manager'::app_role)
OR has_role(auth.uid(), 'employee'::app_role)
);
CREATE POLICY "Members can view own association FAQs"
ON public.association_faqs FOR SELECT TO authenticated
USING (
association_id IN (SELECT get_user_association_ids())
);
-- ============================================================
-- CALENDAR_EVENTS: add read access for members (staff already has ALL)
-- ============================================================
CREATE POLICY "Members can view own association calendar_events"
ON public.calendar_events FOR SELECT TO authenticated
USING (
association_id IN (SELECT get_user_association_ids())
);
-- ============================================================
-- ANNOUNCEMENTS: scope to association (announcements table has no
-- association_id, so we keep the existing policy but note this)
-- ============================================================
-- Announcements don't have association_id, keeping existing behavior
-- ============================================================
-- Also tighten board_members DELETE/UPDATE/INSERT to staff only
-- ============================================================
DROP POLICY IF EXISTS "Authenticated users can delete board_members" ON public.board_members;
DROP POLICY IF EXISTS "Authenticated users can update board_members" ON public.board_members;
DROP POLICY IF EXISTS "Authenticated users can insert board_members" ON public.board_members;
CREATE POLICY "Staff can manage board_members"
ON public.board_members FOR ALL TO authenticated
USING (
has_role(auth.uid(), 'admin'::app_role)
OR has_role(auth.uid(), 'manager'::app_role)
)
WITH CHECK (
has_role(auth.uid(), 'admin'::app_role)
OR has_role(auth.uid(), 'manager'::app_role)
);
@@ -0,0 +1,16 @@
-- Update documents staff policy to include employee role
DROP POLICY IF EXISTS "Staff full access on documents" ON public.documents;
CREATE POLICY "Staff full access on documents"
ON public.documents FOR ALL TO authenticated
USING (
has_role(auth.uid(), 'admin'::app_role)
OR has_role(auth.uid(), 'manager'::app_role)
OR has_role(auth.uid(), 'employee'::app_role)
)
WITH CHECK (
has_role(auth.uid(), 'admin'::app_role)
OR has_role(auth.uid(), 'manager'::app_role)
OR has_role(auth.uid(), 'employee'::app_role)
);
@@ -0,0 +1,49 @@
-- Create custom_variables table for user-defined template variables
CREATE TABLE public.custom_variables (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE NOT NULL,
variable_name TEXT NOT NULL,
display_label TEXT NOT NULL,
default_value TEXT DEFAULT '',
description TEXT,
category TEXT DEFAULT 'general',
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(association_id, variable_name)
);
-- Enable RLS
ALTER TABLE public.custom_variables ENABLE ROW LEVEL SECURITY;
-- Staff (admin, manager, employee) full access
CREATE POLICY "Staff full access on custom_variables"
ON public.custom_variables
FOR ALL
TO authenticated
USING (
public.has_role(auth.uid(), 'admin'::public.app_role)
OR public.has_role(auth.uid(), 'manager'::public.app_role)
OR public.has_role(auth.uid(), 'employee'::public.app_role)
)
WITH CHECK (
public.has_role(auth.uid(), 'admin'::public.app_role)
OR public.has_role(auth.uid(), 'manager'::public.app_role)
OR public.has_role(auth.uid(), 'employee'::public.app_role)
);
-- Board members can read variables for their associations
CREATE POLICY "Board members read custom_variables"
ON public.custom_variables
FOR SELECT
TO authenticated
USING (
association_id IN (SELECT public.get_user_association_ids())
);
-- Updated_at trigger
CREATE TRIGGER update_custom_variables_updated_at
BEFORE UPDATE ON public.custom_variables
FOR EACH ROW
EXECUTE FUNCTION public.update_updated_at_column();
@@ -0,0 +1,8 @@
ALTER PUBLICATION supabase_realtime ADD TABLE public.tasks;
ALTER PUBLICATION supabase_realtime ADD TABLE public.arc_applications;
ALTER PUBLICATION supabase_realtime ADD TABLE public.homeowner_requests;
ALTER PUBLICATION supabase_realtime ADD TABLE public.bills;
ALTER PUBLICATION supabase_realtime ADD TABLE public.board_votes;
ALTER PUBLICATION supabase_realtime ADD TABLE public.reminders;
ALTER PUBLICATION supabase_realtime ADD TABLE public.projects;
@@ -0,0 +1,22 @@
ALTER TABLE public.bill_approvals
ADD COLUMN IF NOT EXISTS bill_id UUID;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.table_constraints
WHERE constraint_schema = 'public'
AND table_name = 'bill_approvals'
AND constraint_name = 'bill_approvals_bill_id_fkey'
) THEN
ALTER TABLE public.bill_approvals
ADD CONSTRAINT bill_approvals_bill_id_fkey
FOREIGN KEY (bill_id)
REFERENCES public.bills(id)
ON DELETE CASCADE;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_bill_approvals_bill_id
ON public.bill_approvals (bill_id);
@@ -0,0 +1,20 @@
-- Allow board members to READ bill approvals for their associations
CREATE POLICY "Board members can view bill_approvals"
ON public.bill_approvals
FOR SELECT
TO authenticated
USING (
association_id IN (SELECT public.get_user_association_ids())
);
-- Allow board members to UPDATE their own approval status (approve/deny)
CREATE POLICY "Board members can update bill_approvals"
ON public.bill_approvals
FOR UPDATE
TO authenticated
USING (
association_id IN (SELECT public.get_user_association_ids())
)
WITH CHECK (
association_id IN (SELECT public.get_user_association_ids())
);
@@ -0,0 +1,59 @@
DROP POLICY IF EXISTS "Board members can view bill_approvals" ON public.bill_approvals;
DROP POLICY IF EXISTS "Board members can update bill_approvals" ON public.bill_approvals;
CREATE POLICY "Assigned approvers can view bills"
ON public.bills
FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1
FROM public.bill_approvals ba
JOIN public.board_members bm
ON bm.association_id = ba.association_id
AND bm.member_name = ba.vendor_name
WHERE ba.bill_id = public.bills.id
AND bm.user_id = auth.uid()
AND bm.approval_authority = true
)
);
CREATE POLICY "Assigned approvers can view bill_approvals"
ON public.bill_approvals
FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1
FROM public.board_members bm
WHERE bm.association_id = public.bill_approvals.association_id
AND bm.member_name = public.bill_approvals.vendor_name
AND bm.user_id = auth.uid()
AND bm.approval_authority = true
)
);
CREATE POLICY "Assigned approvers can update bill_approvals"
ON public.bill_approvals
FOR UPDATE
TO authenticated
USING (
EXISTS (
SELECT 1
FROM public.board_members bm
WHERE bm.association_id = public.bill_approvals.association_id
AND bm.member_name = public.bill_approvals.vendor_name
AND bm.user_id = auth.uid()
AND bm.approval_authority = true
)
)
WITH CHECK (
EXISTS (
SELECT 1
FROM public.board_members bm
WHERE bm.association_id = public.bill_approvals.association_id
AND bm.member_name = public.bill_approvals.vendor_name
AND bm.user_id = auth.uid()
AND bm.approval_authority = true
)
);
@@ -0,0 +1,70 @@
-- Drop overly restrictive policies that only match by member_name
DROP POLICY IF EXISTS "Assigned approvers can view bills" ON public.bills;
DROP POLICY IF EXISTS "Assigned approvers can view bill_approvals" ON public.bill_approvals;
DROP POLICY IF EXISTS "Assigned approvers can update bill_approvals" ON public.bill_approvals;
-- Board members can view ALL bills for their association
CREATE POLICY "Board members can view association bills"
ON public.bills
FOR SELECT
TO authenticated
USING (
association_id IN (
SELECT bm.association_id
FROM public.board_members bm
WHERE bm.user_id = auth.uid()
)
);
-- Board members can view ALL approvals for bills in their association
CREATE POLICY "Board members can view association bill_approvals"
ON public.bill_approvals
FOR SELECT
TO authenticated
USING (
association_id IN (
SELECT bm.association_id
FROM public.board_members bm
WHERE bm.user_id = auth.uid()
)
);
-- Board members can only update their OWN approval row (matched by member_name)
CREATE POLICY "Board members can update own bill_approvals"
ON public.bill_approvals
FOR UPDATE
TO authenticated
USING (
EXISTS (
SELECT 1
FROM public.board_members bm
WHERE bm.association_id = bill_approvals.association_id
AND bm.member_name = bill_approvals.vendor_name
AND bm.user_id = auth.uid()
)
)
WITH CHECK (
EXISTS (
SELECT 1
FROM public.board_members bm
WHERE bm.association_id = bill_approvals.association_id
AND bm.member_name = bill_approvals.vendor_name
AND bm.user_id = auth.uid()
)
);
-- Board members can insert comments on bills in their association
CREATE POLICY "Board members can insert bill_comments"
ON public.bill_comments
FOR INSERT
TO authenticated
WITH CHECK (
bill_id IN (
SELECT b.id FROM public.bills b
WHERE b.association_id IN (
SELECT bm.association_id
FROM public.board_members bm
WHERE bm.user_id = auth.uid()
)
)
);
@@ -0,0 +1 @@
UPDATE public.owners SET user_id = '84f2c9d4-e831-44ad-89a6-513a5515e75f' WHERE id = '29e86467-2f9a-4ea9-b71f-1ee761aa225b' AND user_id IS NULL;
@@ -0,0 +1,44 @@
-- Direct messages table
CREATE TABLE public.direct_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sender_id UUID NOT NULL,
recipient_id UUID NOT NULL,
message TEXT NOT NULL,
read_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Index for conversation lookups
CREATE INDEX idx_dm_sender ON public.direct_messages(sender_id, created_at DESC);
CREATE INDEX idx_dm_recipient ON public.direct_messages(recipient_id, created_at DESC);
CREATE INDEX idx_dm_conversation ON public.direct_messages(
LEAST(sender_id, recipient_id),
GREATEST(sender_id, recipient_id),
created_at DESC
);
-- Enable RLS
ALTER TABLE public.direct_messages ENABLE ROW LEVEL SECURITY;
-- Users can read messages they sent or received
CREATE POLICY "Users can read own messages"
ON public.direct_messages FOR SELECT
TO authenticated
USING (auth.uid() = sender_id OR auth.uid() = recipient_id);
-- Users can insert messages where they are the sender
CREATE POLICY "Users can send messages"
ON public.direct_messages FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = sender_id);
-- Users can update messages they received (for marking read)
CREATE POLICY "Recipients can mark messages read"
ON public.direct_messages FOR UPDATE
TO authenticated
USING (auth.uid() = recipient_id)
WITH CHECK (auth.uid() = recipient_id);
-- Enable realtime
ALTER PUBLICATION supabase_realtime ADD TABLE public.direct_messages;
@@ -0,0 +1,14 @@
-- Step 1: Add account_number column to units (rename from buildium_account_number)
ALTER TABLE public.units RENAME COLUMN buildium_account_number TO account_number;
-- Step 2: Migrate any account_number data from owners to their linked units (if unit doesn't already have one)
UPDATE public.units u
SET account_number = o.account_number
FROM public.owners o
WHERE o.unit_id = u.id
AND o.account_number IS NOT NULL
AND (u.account_number IS NULL OR u.account_number = '');
-- Step 3: Drop account_number from owners
ALTER TABLE public.owners DROP COLUMN account_number;
@@ -0,0 +1,21 @@
CREATE TABLE public.custom_ledgers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
association_id UUID REFERENCES public.associations(id) ON DELETE SET NULL,
owner_id UUID REFERENCES public.owners(id) ON DELETE SET NULL,
rows JSONB DEFAULT '[]'::jsonb,
settings JSONB DEFAULT '{}'::jsonb,
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.custom_ledgers ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can manage custom_ledgers"
ON public.custom_ledgers
FOR ALL
TO authenticated
USING (true)
WITH CHECK (true);
@@ -0,0 +1,11 @@
-- Add association_ids array column to chart_of_accounts
ALTER TABLE public.chart_of_accounts ADD COLUMN association_ids UUID[] DEFAULT '{}';
-- Migrate existing association_id values into the array
UPDATE public.chart_of_accounts
SET association_ids = ARRAY[association_id]
WHERE association_id IS NOT NULL AND (association_ids IS NULL OR association_ids = '{}');
-- Create index for array containment queries
CREATE INDEX idx_chart_of_accounts_association_ids ON public.chart_of_accounts USING GIN (association_ids);
@@ -0,0 +1,41 @@
CREATE TABLE public.support_chats (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL,
association_id uuid REFERENCES public.associations(id) ON DELETE CASCADE,
status text NOT NULL DEFAULT 'open',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE public.support_chat_messages (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
chat_id uuid NOT NULL REFERENCES public.support_chats(id) ON DELETE CASCADE,
role text NOT NULL DEFAULT 'user',
content text NOT NULL,
escalated boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.support_chats ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.support_chat_messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can manage own support chats" ON public.support_chats
FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
CREATE POLICY "Users can read own chat messages" ON public.support_chat_messages
FOR SELECT TO authenticated
USING (chat_id IN (SELECT id FROM public.support_chats WHERE user_id = auth.uid()));
CREATE POLICY "Users can insert own chat messages" ON public.support_chat_messages
FOR INSERT TO authenticated
WITH CHECK (chat_id IN (SELECT id FROM public.support_chats WHERE user_id = auth.uid()));
CREATE POLICY "Admins can read all support chats" ON public.support_chats
FOR SELECT TO authenticated USING (public.has_role(auth.uid(), 'admin'));
CREATE POLICY "Admins can read all chat messages" ON public.support_chat_messages
FOR SELECT TO authenticated USING (
chat_id IN (SELECT id FROM public.support_chats)
AND public.has_role(auth.uid(), 'admin')
);
@@ -0,0 +1,36 @@
CREATE TABLE public.docusign_envelopes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) NOT NULL,
envelope_id TEXT,
document_name TEXT NOT NULL,
document_url TEXT,
status TEXT NOT NULL DEFAULT 'created',
recipients JSONB NOT NULL DEFAULT '[]'::jsonb,
sent_by UUID REFERENCES auth.users(id),
sent_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.docusign_envelopes ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff can manage docusign envelopes"
ON public.docusign_envelopes
FOR ALL
TO authenticated
USING (
public.has_role(auth.uid(), 'admin'::public.app_role) OR
public.has_role(auth.uid(), 'manager'::public.app_role)
)
WITH CHECK (
public.has_role(auth.uid(), 'admin'::public.app_role) OR
public.has_role(auth.uid(), 'manager'::public.app_role)
);
CREATE TRIGGER update_docusign_envelopes_updated_at
BEFORE UPDATE ON public.docusign_envelopes
FOR EACH ROW
EXECUTE FUNCTION public.update_updated_at_column();
@@ -0,0 +1 @@
ALTER TABLE public.units ADD COLUMN IF NOT EXISTS image_url text;
@@ -0,0 +1,2 @@
ALTER TABLE public.violations
ADD COLUMN IF NOT EXISTS tags text[] NOT NULL DEFAULT ARRAY[]::text[];
@@ -0,0 +1,19 @@
-- Make association_id nullable so company-level accounts can exist
ALTER TABLE public.stripe_account_mappings ALTER COLUMN association_id DROP NOT NULL;
-- Drop the unique constraint on association_id (it's one-to-one) so we can have nulls
ALTER TABLE public.stripe_account_mappings DROP CONSTRAINT IF EXISTS stripe_account_mappings_association_id_fkey;
ALTER TABLE public.stripe_account_mappings DROP CONSTRAINT IF EXISTS stripe_account_mappings_association_id_key;
-- Re-add FK but allow nulls
ALTER TABLE public.stripe_account_mappings
ADD CONSTRAINT stripe_account_mappings_association_id_fkey
FOREIGN KEY (association_id) REFERENCES public.associations(id) ON DELETE CASCADE;
-- Add unique constraint that allows multiple nulls
CREATE UNIQUE INDEX IF NOT EXISTS stripe_account_mappings_association_id_unique
ON public.stripe_account_mappings (association_id) WHERE association_id IS NOT NULL;
-- Add label column for company accounts
ALTER TABLE public.stripe_account_mappings ADD COLUMN IF NOT EXISTS label TEXT;
@@ -0,0 +1,114 @@
CREATE TABLE public.form_inbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_type TEXT NOT NULL,
source_id UUID NOT NULL,
association_id UUID REFERENCES public.associations(id),
title TEXT NOT NULL,
submitter_name TEXT,
submitter_email TEXT,
summary TEXT,
status TEXT NOT NULL DEFAULT 'new',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
reviewed_by UUID,
reviewed_at TIMESTAMPTZ
);
ALTER TABLE public.form_inbox ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Admins and managers can view form inbox"
ON public.form_inbox FOR SELECT TO authenticated
USING (
public.has_role(auth.uid(), 'admin') OR
public.has_role(auth.uid(), 'manager')
);
CREATE POLICY "Admins and managers can update form inbox"
ON public.form_inbox FOR UPDATE TO authenticated
USING (
public.has_role(auth.uid(), 'admin') OR
public.has_role(auth.uid(), 'manager')
);
CREATE POLICY "Anyone can insert into form inbox"
ON public.form_inbox FOR INSERT
WITH CHECK (true);
-- Trigger to auto-create inbox entry for public form submissions
CREATE OR REPLACE FUNCTION public.create_form_inbox_entry_from_submission()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
INSERT INTO public.form_inbox (source_type, source_id, association_id, title, submitter_name, submitter_email, summary)
SELECT
'public_form',
NEW.id,
NEW.association_id,
COALESCE(t.title, 'Form Submission'),
NEW.submitter_name,
NEW.submitter_email,
t.title
FROM public.public_form_templates t
WHERE t.id = NEW.template_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_form_inbox_from_submission
AFTER INSERT ON public.public_form_submissions
FOR EACH ROW
EXECUTE FUNCTION public.create_form_inbox_entry_from_submission();
-- Trigger for violation responses
CREATE OR REPLACE FUNCTION public.create_form_inbox_entry_from_violation_response()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_assoc_id UUID;
v_title TEXT;
BEGIN
SELECT v.association_id, 'Violation Response - ' || COALESCE(vt.category, 'Unknown')
INTO v_assoc_id, v_title
FROM public.violations v
LEFT JOIN public.violation_types vt ON v.violation_type_id = vt.id
WHERE v.id = NEW.violation_id;
INSERT INTO public.form_inbox (source_type, source_id, association_id, title, submitter_name, submitter_email, summary)
VALUES ('violation_response', NEW.id, v_assoc_id, v_title, NEW.respondent_name, NEW.respondent_email, LEFT(NEW.response_text, 200));
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_form_inbox_from_violation_response
AFTER INSERT ON public.violation_responses
FOR EACH ROW
EXECUTE FUNCTION public.create_form_inbox_entry_from_violation_response();
-- Trigger for client requests
CREATE OR REPLACE FUNCTION public.create_form_inbox_entry_from_client_request()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
INSERT INTO public.form_inbox (source_type, source_id, association_id, title, submitter_name, submitter_email, summary)
VALUES ('client_request', NEW.id, NEW.association_id, NEW.title, NEW.requester_name, NEW.requester_email, LEFT(NEW.description, 200));
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_form_inbox_from_client_request
AFTER INSERT ON public.client_requests
FOR EACH ROW
EXECUTE FUNCTION public.create_form_inbox_entry_from_client_request();
-- Enable realtime
ALTER PUBLICATION supabase_realtime ADD TABLE public.form_inbox;
@@ -0,0 +1,3 @@
CREATE POLICY "Anyone can upload violation response photos"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'violation-photos' AND (storage.foldername(name))[1] = 'responses');
@@ -0,0 +1,39 @@
CREATE OR REPLACE FUNCTION public.create_form_inbox_entry_from_violation_response()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public'
AS $function$
DECLARE
v_assoc_id UUID;
v_title TEXT;
BEGIN
SELECT
v.association_id,
'Violation Response - ' || COALESCE(v.category, v.violation_type, v.title, 'Unknown')
INTO v_assoc_id, v_title
FROM public.violations v
WHERE v.id = NEW.violation_id;
INSERT INTO public.form_inbox (
source_type,
source_id,
association_id,
title,
submitter_name,
submitter_email,
summary
)
VALUES (
'violation_response',
NEW.id,
v_assoc_id,
v_title,
NEW.respondent_name,
NEW.respondent_email,
LEFT(COALESCE(NEW.response_text, ''), 200)
);
RETURN NEW;
END;
$function$;
@@ -0,0 +1,43 @@
CREATE OR REPLACE FUNCTION public.create_form_inbox_entry_from_violation_response()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public'
AS $function$
DECLARE
v_assoc_id UUID;
v_title TEXT;
v_summary TEXT;
v_address TEXT;
v_vid_short TEXT;
BEGIN
SELECT
v.association_id,
LEFT(v.id::text, 8),
COALESCE(v.address, ''),
COALESCE(v.category, v.violation_type, v.title, 'Unknown')
INTO v_assoc_id, v_vid_short, v_address, v_title
FROM public.violations v
WHERE v.id = NEW.violation_id;
v_summary := 'Violation ID: V-' || UPPER(v_vid_short);
IF v_address IS NOT NULL AND v_address <> '' THEN
v_summary := v_summary || ' | Address: ' || v_address;
END IF;
IF NEW.response_text IS NOT NULL THEN
v_summary := v_summary || ' | ' || LEFT(NEW.response_text, 150);
END IF;
INSERT INTO public.form_inbox (
source_type, source_id, association_id, title,
submitter_name, submitter_email, summary
)
VALUES (
'violation_response', NEW.id, v_assoc_id,
'Violation Response - ' || v_title,
NEW.respondent_name, NEW.respondent_email, v_summary
);
RETURN NEW;
END;
$function$;
@@ -0,0 +1,7 @@
CREATE POLICY "Admins and managers can delete form inbox"
ON public.form_inbox
FOR DELETE
TO authenticated
USING (
has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role)
);
@@ -0,0 +1,18 @@
CREATE TABLE public.user_dashboard_layouts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
layout JSONB NOT NULL DEFAULT '[]'::jsonb,
cards JSONB NOT NULL DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(user_id)
);
ALTER TABLE public.user_dashboard_layouts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can manage their own dashboard layout"
ON public.user_dashboard_layouts
FOR ALL
TO authenticated
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
@@ -0,0 +1,11 @@
-- Allow authenticated users (homeowners, board members) to read status updates for their associations
CREATE POLICY "Authenticated users can read status updates for their associations"
ON public.status_updates
FOR SELECT
TO authenticated
USING (
association_id IN (SELECT public.get_user_association_ids())
OR public.has_role(auth.uid(), 'admin'::public.app_role)
OR public.has_role(auth.uid(), 'manager'::public.app_role)
OR public.has_role(auth.uid(), 'employee'::public.app_role)
);
@@ -0,0 +1,91 @@
-- Store Google Drive OAuth tokens for admin users
CREATE TABLE public.google_drive_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL UNIQUE,
access_token TEXT NOT NULL,
refresh_token TEXT NOT NULL,
token_expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.google_drive_tokens ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Admins can manage their own tokens"
ON public.google_drive_tokens
FOR ALL
TO authenticated
USING (
user_id = auth.uid()
AND (public.has_role(auth.uid(), 'admin') OR public.has_role(auth.uid(), 'manager'))
)
WITH CHECK (
user_id = auth.uid()
AND (public.has_role(auth.uid(), 'admin') OR public.has_role(auth.uid(), 'manager'))
);
-- Track which Drive files/folders are shared and with whom
CREATE TABLE public.shared_drive_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
drive_file_id TEXT NOT NULL,
drive_file_name TEXT NOT NULL,
drive_mime_type TEXT,
drive_icon_link TEXT,
drive_web_view_link TEXT,
is_folder BOOLEAN NOT NULL DEFAULT false,
shared_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
association_ids UUID[] DEFAULT '{}',
visibility TEXT[] NOT NULL DEFAULT '{admin}',
parent_shared_id UUID REFERENCES public.shared_drive_files(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.shared_drive_files ENABLE ROW LEVEL SECURITY;
-- Staff can manage shared files
CREATE POLICY "Staff can manage shared drive files"
ON public.shared_drive_files
FOR ALL
TO authenticated
USING (
public.has_role(auth.uid(), 'admin') OR public.has_role(auth.uid(), 'manager')
)
WITH CHECK (
public.has_role(auth.uid(), 'admin') OR public.has_role(auth.uid(), 'manager')
);
-- Board members and homeowners can view files shared with them
CREATE POLICY "Users can view files shared with their role or association"
ON public.shared_drive_files
FOR SELECT
TO authenticated
USING (
public.has_role(auth.uid(), 'admin')
OR public.has_role(auth.uid(), 'manager')
OR (
'board_member' = ANY(visibility)
AND EXISTS (
SELECT 1 FROM public.board_members bm
WHERE bm.user_id = auth.uid()
AND bm.association_id = ANY(shared_drive_files.association_ids)
)
)
OR (
'homeowner' = ANY(visibility)
AND EXISTS (
SELECT 1 FROM public.owners o
WHERE o.user_id = auth.uid()
AND o.association_id = ANY(shared_drive_files.association_ids)
)
)
);
CREATE TRIGGER update_google_drive_tokens_updated_at
BEFORE UPDATE ON public.google_drive_tokens
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_shared_drive_files_updated_at
BEFORE UPDATE ON public.shared_drive_files
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
@@ -0,0 +1,29 @@
CREATE TABLE public.forte_account_mappings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
association_id UUID REFERENCES public.associations(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL,
location_id TEXT NOT NULL,
api_access_id TEXT NOT NULL,
api_secure_key TEXT,
is_active BOOLEAN NOT NULL DEFAULT true,
pass_processing_fee BOOLEAN NOT NULL DEFAULT false,
processing_fee_percent NUMERIC NOT NULL DEFAULT 0.029,
processing_fee_fixed_cents INTEGER NOT NULL DEFAULT 30,
label TEXT,
environment TEXT NOT NULL DEFAULT 'sandbox',
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.forte_account_mappings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Authenticated users can view forte mappings"
ON public.forte_account_mappings FOR SELECT TO authenticated
USING (true);
CREATE POLICY "Admins can manage forte mappings"
ON public.forte_account_mappings FOR ALL TO authenticated
USING (public.has_role(auth.uid(), 'admin') OR public.has_role(auth.uid(), 'manager'))
WITH CHECK (public.has_role(auth.uid(), 'admin') OR public.has_role(auth.uid(), 'manager'));

Some files were not shown because too many files have changed in this diff Show More