Golf Cart Management Specification
- Status: Implemented
- Date: 2025-12-06
- Author: Rudi Haarhoff
- Related:
- ADR-0001 Facilities Service
- UI/UX Specification
Overview
Extend the Facilities Service with golf cart-specific features for fleet management, tracking, and tee-sheet integration.
Goals (status v0.6.0)
- Track individual cart inventory with detailed metadata — ✅
- Monitor battery/fuel status and maintenance needs — ✅
- Integrate cart assignment with tee-time bookings (auto-assign, history) — ✅
- Provide fleet utilization analytics and GPS/zone awareness — ✅ (utilization, zones, predictive maintenance)
- UI library for cart management — ✅
@digiwedge/facilities-ui-golf-cart(23 components, 14 hooks, 17 lazy wrappers, DefaultQRCamera) - Offline-ready UI — ✅ service worker for carts/summary/zones/utilization (stale-while-revalidate, 5m TTL, 30-entry cap)
Data Model
GolfCartDetails (New Model)
model GolfCartDetails {
id Int @id @default(autoincrement())
resourceId Int @unique
// Identification
cartNumber String // Fleet number (e.g., "42")
serialNumber String? // Manufacturer serial
model String? // e.g., "Club Car Tempo"
manufacturer String? // e.g., "Club Car", "E-Z-GO", "Yamaha"
year Int? // Model year
// Type & Capacity
cartType CartType @default(ELECTRIC)
seatingCapacity Int @default(2)
// Status
batteryLevel Int? // 0-100 percentage (electric)
fuelLevel Int? // 0-100 percentage (gas)
odometer Float? // Total miles/km
hoursUsed Float? // Engine/motor hours
lastChargedAt DateTime?
lastServicedAt DateTime?
nextServiceDue DateTime?
nextServiceOdo Float? // Odometer at next service
// Location (optional GPS)
lastLatitude Float?
lastLongitude Float?
lastLocationAt DateTime?
currentZone String? // e.g., "Hole 7", "Clubhouse", "Charging Bay"
// Condition
condition CartCondition @default(GOOD)
damageNotes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
resource FacilityResource @relation(fields: [resourceId], references: [id])
assignments CartAssignment[]
incidents CartIncident[]
@@index([cartNumber])
}
enum CartType {
ELECTRIC
GAS
LITHIUM
SOLAR
}
enum CartCondition {
EXCELLENT
GOOD
FAIR
NEEDS_REPAIR
OUT_OF_SERVICE
}
CartAssignment (New Model)
model CartAssignment {
id Int @id @default(autoincrement())
cartDetailsId Int
bookingId Int? // From tee-sheet
playerId Int? // Primary driver
assignedAt DateTime @default(now())
returnedAt DateTime?
assignedBy String? // Staff member
// Usage metrics
startOdometer Float?
endOdometer Float?
startBattery Int?
endBattery Int?
// Fees
cartFee Float?
feeWaived Boolean @default(false)
waiverReason String?
notes String?
cartDetails GolfCartDetails @relation(fields: [cartDetailsId], references: [id])
@@index([cartDetailsId, assignedAt])
@@index([bookingId])
}
CartIncident (New Model)
model CartIncident {
id Int @id @default(autoincrement())
cartDetailsId Int
assignmentId Int?
incidentType IncidentType
severity IncidentSeverity @default(MINOR)
description String
location String? // Where on course
reportedBy String
reportedAt DateTime @default(now())
resolvedAt DateTime?
resolvedBy String?
resolutionNotes String?
repairCost Float?
cartDetails GolfCartDetails @relation(fields: [cartDetailsId], references: [id])
@@index([cartDetailsId, reportedAt])
}
enum IncidentType {
DAMAGE
BREAKDOWN
ACCIDENT
THEFT
VANDALISM
OTHER
}
enum IncidentSeverity {
MINOR
MODERATE
MAJOR
CRITICAL
}
API Endpoints
REST API
Golf Cart Details
GET /golf-carts?tenantId=1&clubId=10
GET /golf-carts/:id?tenantId=1
POST /golf-carts
PUT /golf-carts/:id?tenantId=1
DELETE /golf-carts/:id?tenantId=1
Cart Status
GET /golf-carts/:id/status # Current status summary
PUT /golf-carts/:id/battery # Update battery level
PUT /golf-carts/:id/location # Update GPS location
PUT /golf-carts/:id/condition # Update condition
Cart Assignments
GET /golf-carts/:id/assignments # Assignment history
POST /golf-carts/:id/assign # Assign to booking
POST /golf-carts/:id/return # Return cart
GET /golf-carts/available # Available carts for time window
POST /golf-carts/auto-assign # Auto-assign to booking
Cart Incidents
GET /golf-carts/:id/incidents # Incident history
POST /golf-carts/:id/incidents # Report incident
PUT /golf-carts/incidents/:id # Update/resolve incident
Fleet Analytics (implemented)
GET /golf-carts/fleet/summary # Fleet overview
GET /golf-carts/analytics/utilization # Usage metrics (windowed)
GET /golf-carts/zones # Carts by zone (currentZone)
GET /golf-carts/zones/summary # Zone counts + in-use
GET /golf-carts/maintenance/predictive # Upcoming service candidates
GET /golf-carts/fleet/maintenance # Maintenance due
GET /golf-carts/fleet/low-battery # Carts needing charge
GraphQL API
type GolfCart {
id: Int!
resource: FacilityResource!
cartNumber: String!
serialNumber: String
model: String
manufacturer: String
year: Int
cartType: CartType!
seatingCapacity: Int!
batteryLevel: Int
fuelLevel: Int
odometer: Float
hoursUsed: Float
lastChargedAt: DateTime
lastServicedAt: DateTime
nextServiceDue: DateTime
condition: CartCondition!
damageNotes: String
currentZone: String
assignments: [CartAssignment!]!
incidents: [CartIncident!]!
}
type Query {
golfCarts(tenantId: Int!, clubId: Int, status: CartCondition): [GolfCart!]!
golfCart(id: Int!, tenantId: Int!): GolfCart
availableCarts(tenantId: Int!, clubId: Int!, start: DateTime!, end: DateTime!): [GolfCart!]!
fleetSummary(tenantId: Int!, clubId: Int!): FleetSummary!
utilizationReport(input: UtilizationRequestInput!): UtilizationReport!
cartsByZone(input: ZoneQueryInput!): [GolfCart!]!
zoneSummary(tenantId: Int!, clubId: Int): [ZoneSummary!]!
predictiveMaintenance(input: PredictiveMaintenanceInput!): [GolfCart!]!
}
type Mutation {
createGolfCart(data: CreateGolfCartInput!): GolfCart!
updateGolfCart(id: Int!, tenantId: Int!, data: UpdateGolfCartInput!): GolfCart!
updateBatteryLevel(id: Int!, tenantId: Int!, level: Int!): GolfCart!
updateLocation(id: Int!, tenantId: Int!, lat: Float!, lng: Float!, zone: String): GolfCart!
assignCart(cartId: Int!, bookingId: Int!, playerId: Int): CartAssignment!
returnCart(assignmentId: Int!, endOdometer: Float, endBattery: Int): CartAssignment!
reportIncident(cartId: Int!, data: ReportIncidentInput!): CartIncident!
resolveIncident(incidentId: Int!, resolution: String!, cost: Float): CartIncident!
}
Tee-Sheet Integration
Booking Flow
- Booking Created → Check cart preference
- Cart Required → Auto-assign or queue for manual assignment
- Check-in → Confirm cart assignment, record start metrics
- Round Complete → Return cart, record end metrics
- Billing → Apply cart fee to booking
Auto-Assignment Logic
async autoAssignCart(bookingId: number, preferences: CartPreferences): Promise<CartAssignment> {
const booking = await this.bookingService.findOne(bookingId);
// Find available carts for the tee time window
const availableCarts = await this.golfCartService.findAvailable({
tenantId: booking.tenantId,
clubId: booking.clubId,
start: booking.teeTime,
end: addHours(booking.teeTime, 5), // Assume 5-hour round
cartType: preferences.cartType,
seatingCapacity: preferences.partySize <= 2 ? 2 : 4,
});
if (availableCarts.length === 0) {
throw new NoCartsAvailableException();
}
// Prefer carts with highest battery, best condition
const bestCart = availableCarts
.filter(c => c.batteryLevel >= 50)
.sort((a, b) => b.batteryLevel - a.batteryLevel)[0];
return this.assignCart(bestCart.id, bookingId);
}
Cart Fee Integration
// In pricing engine
calculateCartFee(cart: GolfCart, booking: Booking): number {
const baseRate = this.config.cartFeeBase; // e.g., $25
// Adjustments
if (cart.cartType === 'LITHIUM') return baseRate * 1.2;
if (cart.seatingCapacity === 4) return baseRate * 1.5;
if (booking.membershipTier === 'PREMIUM') return 0; // Included
return baseRate;
}
Fleet Dashboard
Summary Metrics
| Metric | Description |
|---|---|
| Total Fleet | Count of all carts |
| Available Now | Carts not assigned |
| In Use | Currently assigned |
| Charging | Battery < 30% or at charger |
| Out of Service | Needs repair |
| Utilization Rate | % of fleet used today |
| Zone Summary | Carts per zone + in-use |
| Predicted Service | Carts flagged by upcoming service date/odometer/hours |
Alerts
- Low battery (< 20%)
- Maintenance overdue
- Unresolved incidents
- High-mileage carts
- Carts not returned
- Zone anomaly: carts outside allowed zones
UI Panels (API-backed)
- Utilization: call
GET /golf-carts/analytics/utilization?tenantId=1&clubId=10&windowDays=7; render utilization %, total assignments, avg hours per assignment. - Zone Summary: call
GET /golf-carts/zones/summary?tenantId=1&clubId=10; table/heatmap ofzone,cartCount,inUse,available; link toGET /golf-carts/zones?tenantId=1&clubId=10&zone=Clubhousefor drill-down. - GPS Map: plot carts using
lastLatitude/lastLongitudeandcurrentZone; show status color (available/in use/out of service). - Predictive Maintenance:
GET /golf-carts/maintenance/predictive?tenantId=1&horizonDays=7&odometerMargin=50; list carts with reason (due date soon, odometer near threshold, hours high). - UI Library: use
@digiwedge/facilities-ui-golf-cartcomponents for a drop-in dashboard.
UI Library (@digiwedge/facilities-ui-golf-cart)
Components (23)
| Category | Components |
|---|---|
| Core | BatteryIndicator, CartStatusCard, CartGrid, FleetDashboard |
| Actions | QuickActionsBar, ActivityFeed |
| Analytics | UtilizationChart, ZoneMap |
| Assignment | AssignCartModal, ReturnCartModal, AutoAssignModal |
| Maintenance | MaintenancePanel, PredictiveMaintenance, ScheduleServiceModal, CompleteServiceModal |
| Incidents | IncidentReporter, ResolveIncidentModal |
| History | AssignmentHistory |
| Mobile | MobileQuickActions, QRScanner, DefaultQRCamera |
Hooks (14)
| Category | Hooks |
|---|---|
| Queries | useGolfCarts, useGolfCart, useFleetSummary, useZoneSummary, useUtilization, usePredictiveMaintenance, useCartAssignments, useCartIncidents |
| Mutations | useAssignCart, useReturnCart, useReportIncident, useUpdateBattery, useUpdateLocation |
| Real-time | useFleetUpdates |
Features
- Lazy Loading: All components available as
Lazy*variants for code-splitting - QR Scanner:
DefaultQRCamerawith@zxing/browserfor cart lookup - Offline Support: Service worker with stale-while-revalidate caching
- Accessibility: WCAG 2.1 AA compliant, axe-playwright CI checks
- Storybook: Visual testing with snapshots and a11y validation
Installation
import {
FleetDashboard,
CartGrid,
ZoneMap,
createGolfCartHooks,
} from '@digiwedge/facilities-ui-golf-cart';
const { useGolfCarts, useFleetSummary } = createGolfCartHooks(api);
function Dashboard() {
const { data: summary } = useFleetSummary(tenantId, clubId);
const { data: carts } = useGolfCarts({ tenantId, clubId });
return (
<>
<FleetDashboard summary={summary} />
<CartGrid carts={carts} />
</>
);
}
See UI/UX Specification for full component documentation.
Implementation Phases
Phase 1: Core Cart Management
- GolfCartDetails model and CRUD
- Basic status tracking (battery, condition)
- Manual assignment to bookings
Phase 2: Assignment & Tracking
- CartAssignment model
- Auto-assignment logic
- Usage metrics (odometer, battery delta)
- Return workflow
Phase 3: Incidents & Maintenance
- CartIncident model
- Damage reporting
- Maintenance scheduling integration
- Service history
Phase 4: Analytics & GPS
- Fleet utilization dashboard
- GPS location tracking
- Zone-based cart management
- Predictive maintenance
API Examples
Create Golf Cart
POST /golf-carts
{
"tenantId": 1,
"clubId": 10,
"cartNumber": "42",
"serialNumber": "CC2024-12345",
"model": "Tempo",
"manufacturer": "Club Car",
"year": 2024,
"cartType": "ELECTRIC",
"seatingCapacity": 2
}
Assign Cart to Booking
POST /golf-carts/42/assign
{
"bookingId": 12345,
"playerId": 789,
"startOdometer": 1234.5,
"startBattery": 95
}
Return Cart
POST /golf-carts/assignments/567/return
{
"endOdometer": 1242.3,
"endBattery": 67,
"notes": "Returned to Bay 3"
}
Report Incident
POST /golf-carts/42/incidents
{
"incidentType": "DAMAGE",
"severity": "MINOR",
"description": "Scratch on front bumper",
"location": "Hole 12 cart path",
"reportedBy": "John Smith"
}
Fleet Summary
GET /golf-carts/fleet/summary?tenantId=1&clubId=10
Response:
{
"totalCarts": 50,
"available": 32,
"inUse": 15,
"charging": 2,
"outOfService": 1,
"utilizationToday": 0.68,
"avgBatteryLevel": 72,
"maintenanceDue": 3,
"unresolvedIncidents": 1
}
Migration Path
- Add
GolfCartDetailstable - Create cart records for existing
GOLF_CARTresources - Deploy cart management API
- Update tee-sheet booking flow
- Add fleet dashboard UI
- Enable GPS tracking (optional hardware)
Dependencies
- Facilities Service (existing)
- Tee-Sheet Service (for booking integration)
- Pricing Engine (for cart fees)
- Notification Service (for alerts)
Open Questions
- GPS hardware vendor selection?
- Real-time vs. periodic location updates?
- Cart fee included in membership tiers?
- Integration with external fleet management systems?