-- RV/Boat Lot inventory CREATE TABLE public.rv_boat_lots ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE, lot_number TEXT NOT NULL, lot_type TEXT NOT NULL DEFAULT 'rv', -- 'rv' | 'boat' | 'either' size TEXT, monthly_rate NUMERIC(10,2), notes TEXT, status TEXT NOT NULL DEFAULT 'available', -- 'available' | 'occupied' | 'maintenance' created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (association_id, lot_number) ); -- Active rentals (assignments to owners) CREATE TABLE public.rv_boat_lot_rentals ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE, lot_id UUID NOT NULL REFERENCES public.rv_boat_lots(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, renter_name TEXT NOT NULL, renter_email TEXT, renter_phone TEXT, vehicle_description TEXT, start_date DATE NOT NULL DEFAULT CURRENT_DATE, end_date DATE, monthly_rate NUMERIC(10,2), status TEXT NOT NULL DEFAULT 'active', -- 'active' | 'ended' notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Waitlist CREATE TABLE public.rv_boat_lot_waitlist ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE, position INTEGER NOT NULL, requester_name TEXT NOT NULL, requester_email TEXT, requester_phone TEXT, unit_address TEXT, owner_id UUID REFERENCES public.owners(id) ON DELETE SET NULL, unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL, requested_lot_type TEXT DEFAULT 'either', -- 'rv' | 'boat' | 'either' vehicle_description TEXT, notes TEXT, status TEXT NOT NULL DEFAULT 'waiting', -- 'waiting' | 'fulfilled' | 'removed' source TEXT NOT NULL DEFAULT 'manual', -- 'manual' | 'public_form' fulfilled_at TIMESTAMPTZ, fulfilled_lot_number TEXT, fulfilled_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, joined_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX rv_boat_lots_assoc_idx ON public.rv_boat_lots(association_id); CREATE INDEX rv_boat_lot_rentals_assoc_idx ON public.rv_boat_lot_rentals(association_id); CREATE INDEX rv_boat_lot_rentals_lot_idx ON public.rv_boat_lot_rentals(lot_id); CREATE INDEX rv_boat_lot_waitlist_assoc_pos_idx ON public.rv_boat_lot_waitlist(association_id, position) WHERE status = 'waiting'; -- Updated_at triggers CREATE TRIGGER trg_rv_boat_lots_updated BEFORE UPDATE ON public.rv_boat_lots FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); CREATE TRIGGER trg_rv_boat_lot_rentals_updated BEFORE UPDATE ON public.rv_boat_lot_rentals FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); CREATE TRIGGER trg_rv_boat_lot_waitlist_updated BEFORE UPDATE ON public.rv_boat_lot_waitlist FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); -- Auto-assign next position on insert (for waiting entries) CREATE OR REPLACE FUNCTION public.assign_rv_boat_waitlist_position() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ BEGIN IF NEW.position IS NULL OR NEW.position = 0 THEN SELECT COALESCE(MAX(position), 0) + 1 INTO NEW.position FROM public.rv_boat_lot_waitlist WHERE association_id = NEW.association_id; END IF; RETURN NEW; END; $$; CREATE TRIGGER trg_rv_boat_waitlist_position BEFORE INSERT ON public.rv_boat_lot_waitlist FOR EACH ROW EXECUTE FUNCTION public.assign_rv_boat_waitlist_position(); -- When a rental is created, mark lot occupied; when ended, mark available CREATE OR REPLACE FUNCTION public.sync_rv_boat_lot_status() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ BEGIN IF TG_OP = 'INSERT' AND NEW.status = 'active' THEN UPDATE public.rv_boat_lots SET status = 'occupied', updated_at = now() WHERE id = NEW.lot_id; ELSIF TG_OP = 'UPDATE' AND OLD.status <> NEW.status THEN IF NEW.status = 'active' THEN UPDATE public.rv_boat_lots SET status = 'occupied', updated_at = now() WHERE id = NEW.lot_id; ELSIF NEW.status = 'ended' THEN UPDATE public.rv_boat_lots SET status = 'available', updated_at = now() WHERE id = NEW.lot_id; END IF; END IF; RETURN NEW; END; $$; CREATE TRIGGER trg_sync_rv_boat_lot_status AFTER INSERT OR UPDATE ON public.rv_boat_lot_rentals FOR EACH ROW EXECUTE FUNCTION public.sync_rv_boat_lot_status(); -- Enable RLS ALTER TABLE public.rv_boat_lots ENABLE ROW LEVEL SECURITY; ALTER TABLE public.rv_boat_lot_rentals ENABLE ROW LEVEL SECURITY; ALTER TABLE public.rv_boat_lot_waitlist ENABLE ROW LEVEL SECURITY; -- Lots policies CREATE POLICY "Staff and members can view lots" ON public.rv_boat_lots FOR SELECT USING (public.user_belongs_to_association(auth.uid(), association_id)); CREATE POLICY "Staff can manage lots" ON public.rv_boat_lots FOR ALL 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)); -- Rentals policies CREATE POLICY "Staff and members can view rentals" ON public.rv_boat_lot_rentals FOR SELECT USING (public.user_belongs_to_association(auth.uid(), association_id)); CREATE POLICY "Staff can manage rentals" ON public.rv_boat_lot_rentals FOR ALL 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)); -- Waitlist policies CREATE POLICY "Staff and members can view waitlist" ON public.rv_boat_lot_waitlist FOR SELECT USING (public.user_belongs_to_association(auth.uid(), association_id)); CREATE POLICY "Staff can manage waitlist" ON public.rv_boat_lot_waitlist FOR ALL 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)); -- Public can submit waitlist entries via published associations CREATE POLICY "Public can submit waitlist entries" ON public.rv_boat_lot_waitlist FOR INSERT TO anon, authenticated WITH CHECK ( source = 'public_form' AND status = 'waiting' AND EXISTS ( SELECT 1 FROM public.association_public_pages app WHERE app.association_id = rv_boat_lot_waitlist.association_id AND app.is_published = true ) );