From d8465f22977c41559bed98748374273037faaea6 Mon Sep 17 00:00:00 2001 From: renee-png Date: Sun, 7 Jun 2026 18:20:58 -0400 Subject: [PATCH] RV/Boat Lots: link rentals to owner/unit + Notice to Vacate toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2: rental form now has optional Owner and Unit selectors (auto-fills renter info/unit from the chosen owner), persisting owner_id/unit_id. Phase 3: add RvRentalVacateButton on Owner and Unit profiles — shows only when that owner/unit has an active/vacating RV-boat rental, and toggles the rental status between active (renting) and vacating. Active Rentals tab now includes vacating rentals with a badge. Co-Authored-By: Claude Opus 4.8 --- src/components/RvRentalVacateButton.tsx | 48 ++++++++++++++++++++++++ src/pages/OwnerProfilePage.tsx | 2 + src/pages/RVBoatLotsPage.tsx | 50 +++++++++++++++++++++++-- src/pages/UnitProfilePage.tsx | 2 + 4 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 src/components/RvRentalVacateButton.tsx diff --git a/src/components/RvRentalVacateButton.tsx b/src/components/RvRentalVacateButton.tsx new file mode 100644 index 0000000..45a3793 --- /dev/null +++ b/src/components/RvRentalVacateButton.tsx @@ -0,0 +1,48 @@ +import { useCallback, useEffect, useState } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; +import { LogOut, RotateCcw } from "lucide-react"; + +/** + * Notice-to-Vacate toggle for an RV/Boat lot rental linked to an owner or unit. + * Renders nothing if there's no active/vacating rental for the given owner/unit. + * Toggles the rental status between "active" (renting) and "vacating". + */ +export default function RvRentalVacateButton({ ownerId, unitId }: { ownerId?: string; unitId?: string }) { + const [rental, setRental] = useState(null); + const [saving, setSaving] = useState(false); + + const fetchRental = useCallback(async () => { + if (!ownerId && !unitId) { setRental(null); return; } + let q = supabase + .from("rv_boat_lot_rentals") + .select("id, status, renter_name") + .in("status", ["active", "vacating"]); + q = ownerId ? q.eq("owner_id", ownerId) : q.eq("unit_id", unitId!); + const { data } = await q.order("start_date", { ascending: false }).limit(1); + setRental(data?.[0] || null); + }, [ownerId, unitId]); + + useEffect(() => { fetchRental(); }, [fetchRental]); + + if (!rental) return null; + const isVacating = rental.status === "vacating"; + + const toggle = async () => { + setSaving(true); + const next = isVacating ? "active" : "vacating"; + const { error } = await supabase.from("rv_boat_lot_rentals").update({ status: next }).eq("id", rental.id); + setSaving(false); + if (error) { toast.error(error.message); return; } + toast.success(isVacating ? "Marked as renting" : "Notice to vacate recorded"); + fetchRental(); + }; + + return ( + + ); +} diff --git a/src/pages/OwnerProfilePage.tsx b/src/pages/OwnerProfilePage.tsx index 3fcd6ce..eb02a9a 100644 --- a/src/pages/OwnerProfilePage.tsx +++ b/src/pages/OwnerProfilePage.tsx @@ -8,6 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; +import RvRentalVacateButton from "@/components/RvRentalVacateButton"; import { ArrowLeft, Mail, Phone, MapPin, Send, AlertTriangle, StickyNote, Download, Filter, FileText, Shield, ClipboardCheck, MessageCircle, @@ -263,6 +264,7 @@ export default function OwnerProfilePage() { + diff --git a/src/pages/RVBoatLotsPage.tsx b/src/pages/RVBoatLotsPage.tsx index b3ebb02..7ccffd1 100644 --- a/src/pages/RVBoatLotsPage.tsx +++ b/src/pages/RVBoatLotsPage.tsx @@ -56,6 +56,8 @@ export default function RVBoatLotsPage() { const [lots, setLots] = useState([]); const [rentals, setRentals] = useState([]); const [waitlist, setWaitlist] = useState([]); + const [owners, setOwners] = useState([]); + const [units, setUnits] = useState([]); const [loading, setLoading] = useState(true); // dialogs @@ -68,7 +70,8 @@ export default function RVBoatLotsPage() { const [lotForm, setLotForm] = useState({ lot_number: "", lot_type: "rv", size: "", monthly_rate: "", notes: "" }); const [rentalForm, setRentalForm] = useState({ lot_id: "", renter_name: "", renter_email: "", renter_phone: "", - vehicle_description: "", start_date: new Date().toISOString().slice(0, 10), monthly_rate: "", notes: "" + vehicle_description: "", start_date: new Date().toISOString().slice(0, 10), monthly_rate: "", notes: "", + owner_id: "", unit_id: "" }); const [waitlistForm, setWaitlistForm] = useState({ requester_name: "", requester_email: "", requester_phone: "", @@ -100,10 +103,12 @@ export default function RVBoatLotsPage() { if (!associationId) return; setLoading(true); try { - const [lotsRes, rentalsRes, wlRes] = await Promise.all([ + const [lotsRes, rentalsRes, wlRes, ownersRes, unitsRes] = await Promise.all([ supabase.from("rv_boat_lots").select("*").eq("association_id", associationId).order("lot_number"), supabase.from("rv_boat_lot_rentals").select("*").eq("association_id", associationId).order("start_date", { ascending: false }), supabase.from("rv_boat_lot_waitlist").select("*").eq("association_id", associationId).order("position"), + supabase.from("owners").select("id, first_name, last_name, unit_id, email, phone").eq("association_id", associationId).eq("status", "active").order("last_name"), + supabase.from("units").select("id, unit_number").eq("association_id", associationId).order("unit_number"), ]); if (lotsRes.error) throw lotsRes.error; if (rentalsRes.error) throw rentalsRes.error; @@ -111,6 +116,8 @@ export default function RVBoatLotsPage() { setLots(lotsRes.data || []); setRentals(rentalsRes.data || []); setWaitlist(wlRes.data || []); + setOwners(ownersRes.data || []); + setUnits(unitsRes.data || []); } catch (e: any) { toast.error(e.message || "Failed to load data"); } finally { @@ -204,12 +211,14 @@ export default function RVBoatLotsPage() { start_date: rentalForm.start_date, monthly_rate: rentalForm.monthly_rate ? Number(rentalForm.monthly_rate) : null, notes: rentalForm.notes || null, + owner_id: rentalForm.owner_id || null, + unit_id: rentalForm.unit_id || null, status: "active", }); if (error) { toast.error(error.message); return; } toast.success("Rental created"); setRentalDialogOpen(false); - setRentalForm({ lot_id: "", renter_name: "", renter_email: "", renter_phone: "", vehicle_description: "", start_date: new Date().toISOString().slice(0, 10), monthly_rate: "", notes: "" }); + setRentalForm({ lot_id: "", renter_name: "", renter_email: "", renter_phone: "", vehicle_description: "", start_date: new Date().toISOString().slice(0, 10), monthly_rate: "", notes: "", owner_id: "", unit_id: "" }); load(); }; @@ -546,6 +555,38 @@ export default function RVBoatLotsPage() { +
+
+ + +
+
+ + +
+
setRentalForm({ ...rentalForm, renter_name: e.target.value })} />
setRentalForm({ ...rentalForm, renter_email: e.target.value })} />
@@ -566,7 +607,7 @@ export default function RVBoatLotsPage() { - {(() => { const activeRentals = rentals.filter(r => r.status === "active"); return ( + {(() => { const activeRentals = rentals.filter(r => r.status === "active" || r.status === "vacating"); return ( <> {selectedRentalIds.size > 0 && (
@@ -597,6 +638,7 @@ export default function RVBoatLotsPage() { {r.is_owner ? "Owner" : "Renter"} + {r.status === "vacating" && Vacating}
{r.renter_email}
diff --git a/src/pages/UnitProfilePage.tsx b/src/pages/UnitProfilePage.tsx index 01c55ef..63c5218 100644 --- a/src/pages/UnitProfilePage.tsx +++ b/src/pages/UnitProfilePage.tsx @@ -8,6 +8,7 @@ import { Input } from "@/components/ui/input"; import { ArrowLeft, Home, Users, KeyRound, AlertTriangle, FileText, Settings, History, MessageSquare, PenTool, ImagePlus, Check, X, BarChart3, Play, RefreshCw, ArrowUpDown, FolderOpen, Clock } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import PropertyImage from "@/components/PropertyImage"; +import RvRentalVacateButton from "@/components/RvRentalVacateButton"; import OwnersTab from "@/components/unit-profile/OwnersTab"; import TenantsTab from "@/components/unit-profile/TenantsTab"; import UnitViolationsTab from "@/components/unit-profile/UnitViolationsTab"; @@ -180,6 +181,7 @@ export default function UnitProfilePage() { +