mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
RV/Boat Lots: map tab edits the association Map amenity + per-pin amount
The Map tab now binds to the association's active map-type amenity (the reservation map) instead of a separate table — pins edited here are the public reservation map. Adds a per-pin amount ($/mo) field (GoogleMapPicker showAmount prop, backward-compatible). Shows a notice when no Map amenity is active and an amenity picker when more than one exists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ export interface MapPinData {
|
||||
lng: string;
|
||||
status: "available" | "unavailable";
|
||||
linked_amenity_id?: string;
|
||||
amount?: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -28,9 +29,11 @@ interface Props {
|
||||
linkLabel?: string;
|
||||
/** When true, the link dropdown shows regardless of pin status. */
|
||||
allowLinkAnyStatus?: boolean;
|
||||
/** When true, each pin gets a per-pin amount ($) field. */
|
||||
showAmount?: boolean;
|
||||
}
|
||||
|
||||
export default function GoogleMapPicker({ pins, onChange, zoom = 16, onZoomChange, formAmenities, lockedView, onLockedViewChange, linkLabel = "form", allowLinkAnyStatus = false }: Props) {
|
||||
export default function GoogleMapPicker({ pins, onChange, zoom = 16, onZoomChange, formAmenities, lockedView, onLockedViewChange, linkLabel = "form", allowLinkAnyStatus = false, showAmount = false }: Props) {
|
||||
const mapRef = useRef<HTMLDivElement>(null);
|
||||
const mapInstance = useRef<L.Map | null>(null);
|
||||
const markersRef = useRef<L.Marker[]>([]);
|
||||
@@ -237,6 +240,21 @@ export default function GoogleMapPicker({ pins, onChange, zoom = 16, onZoomChang
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>{pin.lat}, {pin.lng}</span>
|
||||
</div>
|
||||
{showAmount && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm text-muted-foreground">$</span>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="Amount"
|
||||
value={pin.amount ?? ""}
|
||||
onChange={e => updatePin(pin.id, { amount: e.target.value === "" ? undefined : Number(e.target.value) })}
|
||||
className="text-sm h-8 w-[130px]"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">/ mo</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Select
|
||||
value={pin.status}
|
||||
|
||||
@@ -105,11 +105,20 @@ export default function RVBoatLotsPage() {
|
||||
const [savingRentalBulk, setSavingRentalBulk] = useState(false);
|
||||
const [rentalBulk, setRentalBulk] = useState({ status: "no_change", monthly_rate: "", is_owner: "no_change" });
|
||||
|
||||
// internal map state
|
||||
// map state — bound to the association's active map amenity
|
||||
const [mapAmenities, setMapAmenities] = useState<any[]>([]);
|
||||
const [selectedMapAmenityId, setSelectedMapAmenityId] = useState<string>("");
|
||||
const [mapPins, setMapPins] = useState<MapPinData[]>([]);
|
||||
const [mapZoom, setMapZoom] = useState<number>(17);
|
||||
const [mapLockedView, setMapLockedView] = useState<{ lat: number; lng: number; zoom: number } | null>(null);
|
||||
const [savingMap, setSavingMap] = useState(false);
|
||||
|
||||
const applyAmenityToMap = (amenity: any) => {
|
||||
const cfg = (amenity?.map_config as any) || {};
|
||||
setMapPins(Array.isArray(cfg.pins) ? cfg.pins : []);
|
||||
setMapZoom(typeof cfg.zoom === "number" ? cfg.zoom : 17);
|
||||
setMapLockedView(cfg.locked_view && typeof cfg.locked_view.lat === "number" ? cfg.locked_view : null);
|
||||
};
|
||||
const [syncingPublic, setSyncingPublic] = useState(false);
|
||||
|
||||
const toggleSet = (setter: React.Dispatch<React.SetStateAction<Set<string>>>, id: string) =>
|
||||
@@ -141,11 +150,16 @@ export default function RVBoatLotsPage() {
|
||||
setWaitlist(wlRes.data || []);
|
||||
setOwners(ownersRes.data || []);
|
||||
setUnits(unitsRes.data || []);
|
||||
const { data: mapRow } = await supabase.from("rv_boat_lot_maps").select("config").eq("association_id", associationId).maybeSingle();
|
||||
const cfg = (mapRow?.config as any) || {};
|
||||
setMapPins(Array.isArray(cfg.pins) ? cfg.pins : []);
|
||||
setMapZoom(typeof cfg.zoom === "number" ? cfg.zoom : 17);
|
||||
setMapLockedView(cfg.locked_view && typeof cfg.locked_view.lat === "number" ? cfg.locked_view : null);
|
||||
const { data: mapAms } = await (supabase as any).from("amenities")
|
||||
.select("id, name, map_config")
|
||||
.eq("association_id", associationId)
|
||||
.eq("amenity_type", "map")
|
||||
.eq("is_active", true)
|
||||
.order("sort_order");
|
||||
const amList = mapAms || [];
|
||||
setMapAmenities(amList);
|
||||
setSelectedMapAmenityId(amList[0]?.id || "");
|
||||
applyAmenityToMap(amList[0]);
|
||||
} catch (e: any) {
|
||||
toast.error(e.message || "Failed to load data");
|
||||
} finally {
|
||||
@@ -328,16 +342,18 @@ export default function RVBoatLotsPage() {
|
||||
};
|
||||
|
||||
const saveMap = async () => {
|
||||
if (!associationId) return;
|
||||
if (!selectedMapAmenityId) { toast.error("No map amenity selected"); return; }
|
||||
setSavingMap(true);
|
||||
const { error } = await supabase.from("rv_boat_lot_maps").upsert({
|
||||
association_id: associationId,
|
||||
config: { pins: mapPins, zoom: mapZoom, locked_view: mapLockedView },
|
||||
updated_at: new Date().toISOString(),
|
||||
}, { onConflict: "association_id" });
|
||||
const amenity = mapAmenities.find(a => a.id === selectedMapAmenityId);
|
||||
const base = (amenity?.map_config as any) || {};
|
||||
const nextConfig = { ...base, pins: mapPins, zoom: mapZoom, locked_view: mapLockedView };
|
||||
const { error } = await (supabase as any).from("amenities")
|
||||
.update({ map_config: nextConfig, updated_at: new Date().toISOString() })
|
||||
.eq("id", selectedMapAmenityId);
|
||||
setSavingMap(false);
|
||||
if (error) { toast.error(error.message); return; }
|
||||
toast.success("Map saved");
|
||||
setMapAmenities(prev => prev.map(a => a.id === selectedMapAmenityId ? { ...a, map_config: nextConfig } : a));
|
||||
toast.success("Map saved to amenity");
|
||||
};
|
||||
|
||||
const requestInsurance = async (r: Rental) => {
|
||||
@@ -800,12 +816,25 @@ export default function RVBoatLotsPage() {
|
||||
<div>
|
||||
<CardTitle>Lot Map</CardTitle>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Click the map to drop a pin for each lot, label it, and optionally link a directory unit for reference. Internal use only.
|
||||
Edit the association's Map amenity: drop a pin per lot, set its amount, and optionally link a directory unit. These pins are the public reservation map.
|
||||
</p>
|
||||
</div>
|
||||
<Button size="sm" onClick={saveMap} disabled={savingMap}>{savingMap ? "Saving…" : "Save Map"}</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
{mapAmenities.length > 1 && (
|
||||
<Select value={selectedMapAmenityId} onValueChange={(v) => { setSelectedMapAmenityId(v); applyAmenityToMap(mapAmenities.find(a => a.id === v)); }}>
|
||||
<SelectTrigger className="w-[200px]"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{mapAmenities.map(a => <SelectItem key={a.id} value={a.id}>{a.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<Button size="sm" onClick={saveMap} disabled={savingMap || !selectedMapAmenityId}>{savingMap ? "Saving…" : "Save Map"}</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{mapAmenities.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-6">
|
||||
No active Map amenity for this association. Create or activate a <span className="font-medium">Map</span> amenity in the association's Amenities, then manage its lot pins here.
|
||||
</p>
|
||||
) : (
|
||||
<GoogleMapPicker
|
||||
pins={mapPins}
|
||||
onChange={setMapPins}
|
||||
@@ -815,8 +844,10 @@ export default function RVBoatLotsPage() {
|
||||
onLockedViewChange={setMapLockedView}
|
||||
linkLabel="unit"
|
||||
allowLinkAnyStatus
|
||||
showAmount
|
||||
formAmenities={units.map(u => ({ id: u.id, name: `Unit ${u.unit_number}` }))}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
Reference in New Issue
Block a user