RV/Boat Lots: waitlist size + more vehicle types

- Waitlist now captures a free-form Size (requested_size column) in place of
  the type field in the internal form/table.
- Lot type selector (add + bulk edit) expanded to RV, Boat, Travel Trailer,
  Fifth Wheel, Camper, Car, Truck, Trailer, Other.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 21:01:18 -04:00
parent 0c0300efce
commit f549f21c21
2 changed files with 25 additions and 23 deletions
+23 -23
View File
@@ -32,12 +32,25 @@ type Rental = {
type WaitlistEntry = { type WaitlistEntry = {
id: string; association_id: string; position: number; id: string; association_id: string; position: number;
requester_name: string; requester_email: string | null; requester_phone: string | null; requester_name: string; requester_email: string | null; requester_phone: string | null;
unit_address: string | null; requested_lot_type: string; unit_address: string | null; requested_lot_type: string; requested_size: string | null;
vehicle_description: string | null; notes: string | null; vehicle_description: string | null; notes: string | null;
status: string; source: string; joined_at: string; status: string; source: string; joined_at: string;
fulfilled_at: string | null; fulfilled_lot_number: string | null; fulfilled_at: string | null; fulfilled_lot_number: string | null;
}; };
const VEHICLE_TYPES = [
{ value: "rv", label: "RV" },
{ value: "boat", label: "Boat" },
{ value: "travel_trailer", label: "Travel Trailer" },
{ value: "fifth_wheel", label: "Fifth Wheel" },
{ value: "camper", label: "Camper" },
{ value: "car", label: "Car" },
{ value: "truck", label: "Truck" },
{ value: "trailer", label: "Trailer" },
{ value: "other", label: "Other" },
];
const vehicleTypeLabel = (v: string) => VEHICLE_TYPES.find(t => t.value === v)?.label || v;
export default function RVBoatLotsPage() { export default function RVBoatLotsPage() {
const associationCtx = useAssociation() as any; const associationCtx = useAssociation() as any;
const ctxAssociation = associationCtx?.selectedAssociation ?? null; const ctxAssociation = associationCtx?.selectedAssociation ?? null;
@@ -78,7 +91,7 @@ export default function RVBoatLotsPage() {
}); });
const [waitlistForm, setWaitlistForm] = useState({ const [waitlistForm, setWaitlistForm] = useState({
requester_name: "", requester_email: "", requester_phone: "", requester_name: "", requester_email: "", requester_phone: "",
unit_address: "", requested_lot_type: "either", vehicle_description: "", notes: "" unit_address: "", requested_lot_type: "either", requested_size: "", vehicle_description: "", notes: ""
}); });
// batch edit state // batch edit state
@@ -318,6 +331,7 @@ export default function RVBoatLotsPage() {
requester_phone: waitlistForm.requester_phone || null, requester_phone: waitlistForm.requester_phone || null,
unit_address: waitlistForm.unit_address || null, unit_address: waitlistForm.unit_address || null,
requested_lot_type: waitlistForm.requested_lot_type, requested_lot_type: waitlistForm.requested_lot_type,
requested_size: waitlistForm.requested_size || null,
vehicle_description: waitlistForm.vehicle_description || null, vehicle_description: waitlistForm.vehicle_description || null,
notes: waitlistForm.notes || null, notes: waitlistForm.notes || null,
source: "manual", source: "manual",
@@ -327,7 +341,7 @@ export default function RVBoatLotsPage() {
if (error) { toast.error(error.message); return; } if (error) { toast.error(error.message); return; }
toast.success("Added to waitlist"); toast.success("Added to waitlist");
setWaitlistDialogOpen(false); setWaitlistDialogOpen(false);
setWaitlistForm({ requester_name: "", requester_email: "", requester_phone: "", unit_address: "", requested_lot_type: "either", vehicle_description: "", notes: "" }); setWaitlistForm({ requester_name: "", requester_email: "", requester_phone: "", unit_address: "", requested_lot_type: "either", requested_size: "", vehicle_description: "", notes: "" });
load(); load();
}; };
@@ -435,17 +449,7 @@ export default function RVBoatLotsPage() {
<div><Label>Phone</Label><Input value={waitlistForm.requester_phone} onChange={e => setWaitlistForm({ ...waitlistForm, requester_phone: e.target.value })} /></div> <div><Label>Phone</Label><Input value={waitlistForm.requester_phone} onChange={e => setWaitlistForm({ ...waitlistForm, requester_phone: e.target.value })} /></div>
</div> </div>
<div><Label>Unit / Address</Label><Input value={waitlistForm.unit_address} onChange={e => setWaitlistForm({ ...waitlistForm, unit_address: e.target.value })} /></div> <div><Label>Unit / Address</Label><Input value={waitlistForm.unit_address} onChange={e => setWaitlistForm({ ...waitlistForm, unit_address: e.target.value })} /></div>
<div> <div><Label>Size</Label><Input placeholder="e.g. 30 ft, 10x30" value={waitlistForm.requested_size} onChange={e => setWaitlistForm({ ...waitlistForm, requested_size: e.target.value })} /></div>
<Label>Lot type</Label>
<Select value={waitlistForm.requested_lot_type} onValueChange={v => setWaitlistForm({ ...waitlistForm, requested_lot_type: v })}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="rv">RV</SelectItem>
<SelectItem value="boat">Boat</SelectItem>
<SelectItem value="either">Either</SelectItem>
</SelectContent>
</Select>
</div>
<div><Label>Vehicle description</Label><Input value={waitlistForm.vehicle_description} onChange={e => setWaitlistForm({ ...waitlistForm, vehicle_description: e.target.value })} /></div> <div><Label>Vehicle description</Label><Input value={waitlistForm.vehicle_description} onChange={e => setWaitlistForm({ ...waitlistForm, vehicle_description: e.target.value })} /></div>
<div><Label>Notes</Label><Textarea value={waitlistForm.notes} onChange={e => setWaitlistForm({ ...waitlistForm, notes: e.target.value })} /></div> <div><Label>Notes</Label><Textarea value={waitlistForm.notes} onChange={e => setWaitlistForm({ ...waitlistForm, notes: e.target.value })} /></div>
</div> </div>
@@ -465,7 +469,7 @@ export default function RVBoatLotsPage() {
<TableHead className="w-16">#</TableHead> <TableHead className="w-16">#</TableHead>
<TableHead>Name</TableHead> <TableHead>Name</TableHead>
<TableHead>Contact</TableHead> <TableHead>Contact</TableHead>
<TableHead>Type</TableHead> <TableHead>Size</TableHead>
<TableHead>Joined</TableHead> <TableHead>Joined</TableHead>
<TableHead>Source</TableHead> <TableHead>Source</TableHead>
<TableHead className="text-right">Actions</TableHead> <TableHead className="text-right">Actions</TableHead>
@@ -483,7 +487,7 @@ export default function RVBoatLotsPage() {
{w.requester_email && <div>{w.requester_email}</div>} {w.requester_email && <div>{w.requester_email}</div>}
{w.requester_phone && <div>{w.requester_phone}</div>} {w.requester_phone && <div>{w.requester_phone}</div>}
</TableCell> </TableCell>
<TableCell><Badge variant="outline">{w.requested_lot_type}</Badge></TableCell> <TableCell className="text-sm">{w.requested_size || "—"}</TableCell>
<TableCell className="text-xs">{new Date(w.joined_at).toLocaleDateString()}</TableCell> <TableCell className="text-xs">{new Date(w.joined_at).toLocaleDateString()}</TableCell>
<TableCell><Badge variant="secondary">{w.source === "public_form" ? "Public" : "Manual"}</Badge></TableCell> <TableCell><Badge variant="secondary">{w.source === "public_form" ? "Public" : "Manual"}</Badge></TableCell>
<TableCell className="text-right space-x-1"> <TableCell className="text-right space-x-1">
@@ -520,9 +524,7 @@ export default function RVBoatLotsPage() {
<Select value={lotForm.lot_type} onValueChange={v => setLotForm({ ...lotForm, lot_type: v })}> <Select value={lotForm.lot_type} onValueChange={v => setLotForm({ ...lotForm, lot_type: v })}>
<SelectTrigger><SelectValue /></SelectTrigger> <SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="rv">RV</SelectItem> {VEHICLE_TYPES.map(t => <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>)}
<SelectItem value="boat">Boat</SelectItem>
<SelectItem value="either">Either</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -562,7 +564,7 @@ export default function RVBoatLotsPage() {
<TableRow key={lot.id}> <TableRow key={lot.id}>
<TableCell><Checkbox aria-label={`Select lot ${lot.lot_number}`} checked={selectedLotIds.has(lot.id)} onCheckedChange={() => toggleSet(setSelectedLotIds, lot.id)} /></TableCell> <TableCell><Checkbox aria-label={`Select lot ${lot.lot_number}`} checked={selectedLotIds.has(lot.id)} onCheckedChange={() => toggleSet(setSelectedLotIds, lot.id)} /></TableCell>
<TableCell className="font-medium">{lot.lot_number}</TableCell> <TableCell className="font-medium">{lot.lot_number}</TableCell>
<TableCell><Badge variant="outline">{lot.lot_type}</Badge></TableCell> <TableCell><Badge variant="outline">{vehicleTypeLabel(lot.lot_type)}</Badge></TableCell>
<TableCell>{lot.size || "-"}</TableCell> <TableCell>{lot.size || "-"}</TableCell>
<TableCell>{lot.monthly_rate ? `$${lot.monthly_rate}` : "-"}</TableCell> <TableCell>{lot.monthly_rate ? `$${lot.monthly_rate}` : "-"}</TableCell>
<TableCell> <TableCell>
@@ -827,9 +829,7 @@ export default function RVBoatLotsPage() {
<SelectTrigger><SelectValue /></SelectTrigger> <SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="no_change">No change</SelectItem> <SelectItem value="no_change">No change</SelectItem>
<SelectItem value="rv">RV</SelectItem> {VEHICLE_TYPES.map(t => <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>)}
<SelectItem value="boat">Boat</SelectItem>
<SelectItem value="either">Either</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -0,0 +1,2 @@
-- RV/Boat waitlist: capture requested lot size (replaces "type" in the internal UI).
alter table public.rv_boat_lot_waitlist add column if not exists requested_size text;