Skip to main content

Golf Cart Management Specification

  • Status: Implemented
  • Date: 2025-12-06
  • Author: Rudi Haarhoff
  • Related:

Overview

Extend the Facilities Service with golf cart-specific features for fleet management, tracking, and tee-sheet integration.

Goals (status v0.6.0)

  1. Track individual cart inventory with detailed metadata — ✅
  2. Monitor battery/fuel status and maintenance needs — ✅
  3. Integrate cart assignment with tee-time bookings (auto-assign, history) — ✅
  4. Provide fleet utilization analytics and GPS/zone awareness — ✅ (utilization, zones, predictive maintenance)
  5. UI library for cart management — ✅ @digiwedge/facilities-ui-golf-cart (23 components, 14 hooks, 17 lazy wrappers, DefaultQRCamera)
  6. 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

  1. Booking Created → Check cart preference
  2. Cart Required → Auto-assign or queue for manual assignment
  3. Check-in → Confirm cart assignment, record start metrics
  4. Round Complete → Return cart, record end metrics
  5. 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

MetricDescription
Total FleetCount of all carts
Available NowCarts not assigned
In UseCurrently assigned
ChargingBattery < 30% or at charger
Out of ServiceNeeds repair
Utilization Rate% of fleet used today
Zone SummaryCarts per zone + in-use
Predicted ServiceCarts 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 of zone, cartCount, inUse, available; link to GET /golf-carts/zones?tenantId=1&clubId=10&zone=Clubhouse for drill-down.
  • GPS Map: plot carts using lastLatitude/lastLongitude and currentZone; 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-cart components for a drop-in dashboard.

UI Library (@digiwedge/facilities-ui-golf-cart)

Components (23)

CategoryComponents
CoreBatteryIndicator, CartStatusCard, CartGrid, FleetDashboard
ActionsQuickActionsBar, ActivityFeed
AnalyticsUtilizationChart, ZoneMap
AssignmentAssignCartModal, ReturnCartModal, AutoAssignModal
MaintenanceMaintenancePanel, PredictiveMaintenance, ScheduleServiceModal, CompleteServiceModal
IncidentsIncidentReporter, ResolveIncidentModal
HistoryAssignmentHistory
MobileMobileQuickActions, QRScanner, DefaultQRCamera

Hooks (14)

CategoryHooks
QueriesuseGolfCarts, useGolfCart, useFleetSummary, useZoneSummary, useUtilization, usePredictiveMaintenance, useCartAssignments, useCartIncidents
MutationsuseAssignCart, useReturnCart, useReportIncident, useUpdateBattery, useUpdateLocation
Real-timeuseFleetUpdates

Features

  • Lazy Loading: All components available as Lazy* variants for code-splitting
  • QR Scanner: DefaultQRCamera with @zxing/browser for 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

  1. Add GolfCartDetails table
  2. Create cart records for existing GOLF_CART resources
  3. Deploy cart management API
  4. Update tee-sheet booking flow
  5. Add fleet dashboard UI
  6. 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

  1. GPS hardware vendor selection?
  2. Real-time vs. periodic location updates?
  3. Cart fee included in membership tiers?
  4. Integration with external fleet management systems?