RV/Boat Lots: sync lots to public rental_calendar amenities

Add a "Sync to Public Page" button that creates/updates one rental_calendar
amenity per lot (name, size · rate · availability in the description, rate in
booking_config, shown on the public page). Idempotent via amenities.source_rv_lot_id;
removes synced amenities for deleted lots.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 21:06:21 -04:00
parent f549f21c21
commit 69f643a51e
2 changed files with 58 additions and 0 deletions
+54
View File
@@ -110,6 +110,7 @@ export default function RVBoatLotsPage() {
const [mapZoom, setMapZoom] = useState<number>(17); const [mapZoom, setMapZoom] = useState<number>(17);
const [mapLockedView, setMapLockedView] = useState<{ lat: number; lng: number; zoom: number } | null>(null); const [mapLockedView, setMapLockedView] = useState<{ lat: number; lng: number; zoom: number } | null>(null);
const [savingMap, setSavingMap] = useState(false); const [savingMap, setSavingMap] = useState(false);
const [syncingPublic, setSyncingPublic] = useState(false);
const toggleSet = (setter: React.Dispatch<React.SetStateAction<Set<string>>>, id: string) => const toggleSet = (setter: React.Dispatch<React.SetStateAction<Set<string>>>, id: string) =>
setter(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }); setter(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
@@ -278,6 +279,54 @@ export default function RVBoatLotsPage() {
} }
}; };
const syncLotsToPublic = async () => {
if (!associationId || lots.length === 0) { toast.error("No lots to sync"); return; }
setSyncingPublic(true);
try {
const db = supabase as any;
const { data: existing } = await db
.from("amenities").select("id, source_rv_lot_id")
.eq("association_id", associationId).not("source_rv_lot_id", "is", null);
const byLot = new Map((existing || []).map((a: any) => [a.source_rv_lot_id, a.id]));
const lotIds = new Set(lots.map(l => l.id));
let count = 0;
for (const lot of lots) {
const occupied = lot.status === "occupied" ||
rentals.some(r => r.lot_id === lot.id && (r.status === "active" || r.status === "vacating"));
const payload: any = {
association_id: associationId,
name: `Lot ${lot.lot_number}${lot.lot_type ? ` (${vehicleTypeLabel(lot.lot_type)})` : ""}`,
description: [
`Size: ${lot.size || "—"}`,
`Rate: ${lot.monthly_rate ? `$${lot.monthly_rate}/mo` : "—"}`,
`Availability: ${occupied ? "Occupied" : "Available"}`,
].join(" · "),
amenity_type: "rental_calendar",
booking_config: { slot_duration: 60, start_time: "09:00", end_time: "17:00", rental_fee: lot.monthly_rate ?? null },
show_on_public_page: true,
is_active: true,
source_rv_lot_id: lot.id,
updated_at: new Date().toISOString(),
};
const existingId = byLot.get(lot.id);
if (existingId) {
await db.from("amenities").update(payload).eq("id", existingId);
} else {
await db.from("amenities").insert({ ...payload, sort_order: 999 });
}
count++;
}
// Clean up synced amenities whose lot was deleted
const stale = (existing || []).filter((a: any) => !lotIds.has(a.source_rv_lot_id)).map((a: any) => a.id);
if (stale.length) await db.from("amenities").delete().in("id", stale);
toast.success(`Synced ${count} lot${count === 1 ? "" : "s"} to the public page`);
} catch (e: any) {
toast.error(e.message || "Sync failed");
} finally {
setSyncingPublic(false);
}
};
const saveMap = async () => { const saveMap = async () => {
if (!associationId) return; if (!associationId) return;
setSavingMap(true); setSavingMap(true);
@@ -510,6 +559,10 @@ export default function RVBoatLotsPage() {
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Lot Inventory</CardTitle> <CardTitle>Lot Inventory</CardTitle>
<div className="flex items-center gap-2">
<Button size="sm" variant="outline" onClick={syncLotsToPublic} disabled={syncingPublic}>
{syncingPublic ? "Syncing…" : "Sync to Public Page"}
</Button>
<Dialog open={lotDialogOpen} onOpenChange={setLotDialogOpen}> <Dialog open={lotDialogOpen} onOpenChange={setLotDialogOpen}>
<DialogTrigger asChild><Button size="sm"><Plus className="h-4 w-4 mr-1" /> Add lot</Button></DialogTrigger> <DialogTrigger asChild><Button size="sm"><Plus className="h-4 w-4 mr-1" /> Add lot</Button></DialogTrigger>
<DialogContent> <DialogContent>
@@ -540,6 +593,7 @@ export default function RVBoatLotsPage() {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{selectedLotIds.size > 0 && ( {selectedLotIds.size > 0 && (
@@ -0,0 +1,4 @@
-- Link a synced public amenity back to its RV/Boat lot so re-syncing updates
-- (rather than duplicates) the per-lot rental_calendar amenity.
alter table public.amenities add column if not exists source_rv_lot_id uuid;
create index if not exists idx_amenities_source_rv_lot_id on public.amenities(source_rv_lot_id);