RV/Boat Lots: link rentals to owner/unit + Notice to Vacate toggle

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 18:20:58 -04:00
parent 7c18576390
commit d8465f2297
4 changed files with 98 additions and 4 deletions
+46 -4
View File
@@ -56,6 +56,8 @@ export default function RVBoatLotsPage() {
const [lots, setLots] = useState<Lot[]>([]);
const [rentals, setRentals] = useState<Rental[]>([]);
const [waitlist, setWaitlist] = useState<WaitlistEntry[]>([]);
const [owners, setOwners] = useState<any[]>([]);
const [units, setUnits] = useState<any[]>([]);
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() {
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label>Link to Owner</Label>
<Select value={rentalForm.owner_id || "none"} onValueChange={v => {
if (v === "none") { setRentalForm(f => ({ ...f, owner_id: "" })); return; }
const o = owners.find(x => x.id === v);
setRentalForm(f => ({
...f, owner_id: v,
unit_id: o?.unit_id || f.unit_id,
renter_name: f.renter_name || `${o?.first_name || ""} ${o?.last_name || ""}`.trim(),
renter_email: f.renter_email || o?.email || "",
renter_phone: f.renter_phone || o?.phone || "",
}));
}}>
<SelectTrigger><SelectValue placeholder="Optional" /></SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{owners.map(o => <SelectItem key={o.id} value={o.id}>{`${o.last_name || ""}, ${o.first_name || ""}`.replace(/^, |, $/g, "").trim() || "Unnamed"}</SelectItem>)}
</SelectContent>
</Select>
</div>
<div>
<Label>Link to Unit</Label>
<Select value={rentalForm.unit_id || "none"} onValueChange={v => setRentalForm(f => ({ ...f, unit_id: v === "none" ? "" : v }))}>
<SelectTrigger><SelectValue placeholder="Optional" /></SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{units.map(u => <SelectItem key={u.id} value={u.id}>Unit {u.unit_number}</SelectItem>)}
</SelectContent>
</Select>
</div>
</div>
<div><Label>Renter name *</Label><Input value={rentalForm.renter_name} onChange={e => setRentalForm({ ...rentalForm, renter_name: e.target.value })} /></div>
<div className="grid grid-cols-2 gap-3">
<div><Label>Email</Label><Input type="email" value={rentalForm.renter_email} onChange={e => setRentalForm({ ...rentalForm, renter_email: e.target.value })} /></div>
@@ -566,7 +607,7 @@ export default function RVBoatLotsPage() {
</Dialog>
</CardHeader>
<CardContent>
{(() => { const activeRentals = rentals.filter(r => r.status === "active"); return (
{(() => { const activeRentals = rentals.filter(r => r.status === "active" || r.status === "vacating"); return (
<>
{selectedRentalIds.size > 0 && (
<div className="flex items-center justify-between rounded-md border bg-muted/40 px-3 py-2 mb-3">
@@ -597,6 +638,7 @@ export default function RVBoatLotsPage() {
<Badge variant={r.is_owner ? "default" : "secondary"} className="text-[10px]">
{r.is_owner ? "Owner" : "Renter"}
</Badge>
{r.status === "vacating" && <Badge variant="outline" className="text-[10px] border-amber-300 text-amber-700">Vacating</Badge>}
</div>
<div className="text-xs text-muted-foreground">{r.renter_email}</div>
</TableCell>