diff --git a/src/pages/RVBoatLotsPage.tsx b/src/pages/RVBoatLotsPage.tsx index 518ba7f..b3ebb02 100644 --- a/src/pages/RVBoatLotsPage.tsx +++ b/src/pages/RVBoatLotsPage.tsx @@ -12,8 +12,9 @@ import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Checkbox } from "@/components/ui/checkbox"; import { toast } from "sonner"; -import { Plus, Trash2, Check, ArrowUp, ArrowDown, Copy } from "lucide-react"; +import { Plus, Trash2, Check, ArrowUp, ArrowDown, Copy, Pencil, X } from "lucide-react"; type Lot = { id: string; association_id: string; lot_number: string; lot_type: string; @@ -74,6 +75,22 @@ export default function RVBoatLotsPage() { unit_address: "", requested_lot_type: "either", vehicle_description: "", notes: "" }); + // batch edit state + const [selectedLotIds, setSelectedLotIds] = useState>(new Set()); + const [lotBulkOpen, setLotBulkOpen] = useState(false); + const [savingLotBulk, setSavingLotBulk] = useState(false); + const [lotBulk, setLotBulk] = useState({ lot_type: "no_change", status: "no_change", monthly_rate: "" }); + + const [selectedRentalIds, setSelectedRentalIds] = useState>(new Set()); + const [rentalBulkOpen, setRentalBulkOpen] = useState(false); + const [savingRentalBulk, setSavingRentalBulk] = useState(false); + const [rentalBulk, setRentalBulk] = useState({ status: "no_change", monthly_rate: "", is_owner: "no_change" }); + + 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; }); + const toggleSetMany = (setter: React.Dispatch>>, ids: string[], checked: boolean) => + setter(prev => { const n = new Set(prev); ids.forEach(id => checked ? n.add(id) : n.delete(id)); return n; }); + const publicUrl = useMemo(() => { if (!associationId) return ""; return `${window.location.origin}/rv-boat-waitlist/${associationId}`; @@ -128,6 +145,51 @@ export default function RVBoatLotsPage() { load(); }; + const applyLotBulk = async () => { + const ids = [...selectedLotIds]; + if (!ids.length) return; + const patch: Record = {}; + if (lotBulk.lot_type !== "no_change") patch.lot_type = lotBulk.lot_type; + if (lotBulk.status !== "no_change") patch.status = lotBulk.status; + if (lotBulk.monthly_rate.trim() !== "") patch.monthly_rate = Number(lotBulk.monthly_rate) || 0; + if (Object.keys(patch).length === 0) { toast.error("No changes selected"); return; } + setSavingLotBulk(true); + const { error } = await supabase.from("rv_boat_lots").update(patch).in("id", ids); + setSavingLotBulk(false); + if (error) { toast.error(error.message); return; } + toast.success(`Updated ${ids.length} lot${ids.length === 1 ? "" : "s"}`); + setLotBulkOpen(false); setSelectedLotIds(new Set()); load(); + }; + + const deleteLots = async () => { + const ids = [...selectedLotIds]; + if (!ids.length) return; + if (!confirm(`Delete ${ids.length} lot${ids.length === 1 ? "" : "s"}? Lots with rentals may fail to delete.`)) return; + const { error } = await supabase.from("rv_boat_lots").delete().in("id", ids); + if (error) { toast.error(error.message); return; } + toast.success(`Deleted ${ids.length} lot${ids.length === 1 ? "" : "s"}`); + setSelectedLotIds(new Set()); load(); + }; + + const applyRentalBulk = async () => { + const ids = [...selectedRentalIds]; + if (!ids.length) return; + const patch: Record = {}; + if (rentalBulk.status !== "no_change") { + patch.status = rentalBulk.status; + if (rentalBulk.status === "ended") patch.end_date = new Date().toISOString().slice(0, 10); + } + if (rentalBulk.monthly_rate.trim() !== "") patch.monthly_rate = Number(rentalBulk.monthly_rate) || 0; + if (rentalBulk.is_owner !== "no_change") patch.is_owner = rentalBulk.is_owner === "true"; + if (Object.keys(patch).length === 0) { toast.error("No changes selected"); return; } + setSavingRentalBulk(true); + const { error } = await supabase.from("rv_boat_lot_rentals").update(patch).in("id", ids); + setSavingRentalBulk(false); + if (error) { toast.error(error.message); return; } + toast.success(`Updated ${ids.length} rental${ids.length === 1 ? "" : "s"}`); + setRentalBulkOpen(false); setSelectedRentalIds(new Set()); load(); + }; + const createRental = async () => { if (!associationId || !rentalForm.lot_id || !rentalForm.renter_name.trim()) { toast.error("Lot and renter name required"); return; @@ -420,15 +482,27 @@ export default function RVBoatLotsPage() { + {selectedLotIds.size > 0 && ( +
+ {selectedLotIds.size} selected +
+ + + +
+
+ )} {lots.length === 0 ?

No lots yet.

: ( + 0 && lots.every(l => selectedLotIds.has(l.id))} onCheckedChange={(c) => toggleSetMany(setSelectedLotIds, lots.map(l => l.id), !!c)} /> Lot #TypeSize RateStatusActions {lots.map(lot => ( + toggleSet(setSelectedLotIds, lot.id)} /> {lot.lot_number} {lot.lot_type} {lot.size || "-"} @@ -492,17 +566,30 @@ export default function RVBoatLotsPage() { - {rentals.filter(r => r.status === "active").length === 0 ?

No active rentals.

: ( + {(() => { const activeRentals = rentals.filter(r => r.status === "active"); return ( + <> + {selectedRentalIds.size > 0 && ( +
+ {selectedRentalIds.size} selected +
+ + +
+
+ )} + {activeRentals.length === 0 ?

No active rentals.

: (
+ 0 && activeRentals.every(r => selectedRentalIds.has(r.id))} onCheckedChange={(c) => toggleSetMany(setSelectedRentalIds, activeRentals.map(r => r.id), !!c)} /> LotRenterVehicle StartRateActions - {rentals.filter(r => r.status === "active").map(r => { + {activeRentals.map(r => { const lot = lots.find(l => l.id === r.lot_id); return ( + toggleSet(setSelectedRentalIds, r.id)} /> {lot?.lot_number || "—"}
@@ -530,6 +617,8 @@ export default function RVBoatLotsPage() {
)} + + ); })()}
@@ -581,6 +670,86 @@ export default function RVBoatLotsPage() { + + {/* Bulk edit lots */} + + + + Edit {selectedLotIds.size} lot{selectedLotIds.size === 1 ? "" : "s"} + Only changed fields are applied. Leave a field on “No change” to keep existing values. + +
+
+ + +
+
+ + +
+
setLotBulk({ ...lotBulk, monthly_rate: e.target.value })} placeholder="No change" />
+
+ + + + +
+
+ + {/* Bulk edit rentals */} + + + + Edit {selectedRentalIds.size} rental{selectedRentalIds.size === 1 ? "" : "s"} + Only changed fields are applied. Leave a field on “No change” to keep existing values. + +
+
+ + +
+
+ + +
+
setRentalBulk({ ...rentalBulk, monthly_rate: e.target.value })} placeholder="No change" />
+
+ + + + +
+
); }