mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
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:
@@ -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);
|
||||||
Reference in New Issue
Block a user