From 69f643a51e55e813241930fe5aafd66744f08882 Mon Sep 17 00:00:00 2001 From: renee-png Date: Sun, 7 Jun 2026 21:06:21 -0400 Subject: [PATCH] RV/Boat Lots: sync lots to public rental_calendar amenities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/pages/RVBoatLotsPage.tsx | 54 +++++++++++++++++++ ...60608001000_amenities_source_rv_lot_id.sql | 4 ++ 2 files changed, 58 insertions(+) create mode 100644 supabase/migrations/20260608001000_amenities_source_rv_lot_id.sql diff --git a/src/pages/RVBoatLotsPage.tsx b/src/pages/RVBoatLotsPage.tsx index 94b32db..6757fcb 100644 --- a/src/pages/RVBoatLotsPage.tsx +++ b/src/pages/RVBoatLotsPage.tsx @@ -110,6 +110,7 @@ export default function RVBoatLotsPage() { const [mapZoom, setMapZoom] = useState(17); const [mapLockedView, setMapLockedView] = useState<{ lat: number; lng: number; zoom: number } | null>(null); const [savingMap, setSavingMap] = useState(false); + const [syncingPublic, setSyncingPublic] = useState(false); const toggleSet = (setter: React.Dispatch>>, id: string) => 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 () => { if (!associationId) return; setSavingMap(true); @@ -510,6 +559,10 @@ export default function RVBoatLotsPage() { Lot Inventory +
+ @@ -540,6 +593,7 @@ export default function RVBoatLotsPage() { +
{selectedLotIds.size > 0 && ( diff --git a/supabase/migrations/20260608001000_amenities_source_rv_lot_id.sql b/supabase/migrations/20260608001000_amenities_source_rv_lot_id.sql new file mode 100644 index 0000000..a08d863 --- /dev/null +++ b/supabase/migrations/20260608001000_amenities_source_rv_lot_id.sql @@ -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);