Reports & Analytics
The reports system provides booking analytics, revenue tracking, and utilization metrics for operational dashboards.
Architecture
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ /admin/reports │────▶│ ReportsService │────▶│ Club Repo │
│ │ │ │ │ TeeTime Prisma │
└─────────────────┘ └──────────────────┘ │ MCA Prisma │
└─────────────────┘
Source: apps/teetime/teetime-backend/src/admin/reports.service.ts
Report Types
Summary Report
High-level platform overview:
GET /admin/reports/summary?rangeDays=30
// Response
{
"generatedAt": "2025-12-15T08:00:00Z",
"rangeDays": 30,
"clubs": {
"total": 25,
"teeSheetEnabled": 20,
"bridgeEnabled": 15
},
"teeSheets": {
"total": 45,
"offline": 2,
"utilizationRate": 0.72,
"bookedSlots": 1440,
"totalSlots": 2000
},
"bookings": {
"last7Days": { "count": 450, "revenue": 67500.00 },
"last30Days": { "count": 1800, "revenue": 270000.00 }
}
}
Breakdown Report
Daily granular analytics:
GET /admin/reports/breakdowns?rangeDays=30
// Response
{
"generatedAt": "2025-12-15T08:00:00Z",
"rangeDays": 30,
"bookings": [
{
"date": "2025-12-15",
"count": 65,
"revenue": 9750.00,
"utilizationRate": 0.75,
"bookedSlots": 150,
"totalSlots": 200
},
{
"date": "2025-12-14",
"count": 58,
"revenue": 8700.00,
"utilizationRate": 0.68,
"bookedSlots": 136,
"totalSlots": 200
}
// ... more days
]
}
Metrics
Booking Analytics
| Metric | Description |
|---|---|
count | Number of bookings |
revenue | Total revenue (currency) |
last7Days | Rolling 7-day aggregate |
last30Days | Rolling 30-day aggregate |
Data Source: MCA bookings table
Query Criteria:
ReservationDate >= rangeStartStatus !== 'CANCELLED'- Excludes null reservation dates
Revenue Tracking
// Revenue calculation
const revenue = bookings.reduce((sum, b) => sum + asMoney(b.TotalPrice), 0);
// Currency rounding (to cents)
const rounded = Math.round(value * 100) / 100;
Handling:
- Null values treated as $0
- Non-numeric values treated as $0
- Rounded to 2 decimal places
Utilization Metrics
// Utilization = booked / total slots
utilizationRate = bookedSlots / totalSlots; // 0.0 - 1.0
Slot is "booked" if ANY:
checkedIn: truereservationNumberis not nullavailable: false
Time Window
Range Configuration
// Range clamping
const rangeDays = clampRangeDays(queryParam); // 7-180 days
// Default: 30 days if not specified
// Minimum: 7 days
// Maximum: 180 days
Date Sequencing
// Generates array of consecutive dates
const days = sequenceDays(startDate, rangeDays);
// Example: [Dec 1, Dec 2, Dec 3, ...Dec 30]
Data Aggregation
Parallel Fetching
All data sources queried simultaneously:
const [clubs, teeSheets, offlineSheets, slots, bookedSlots, bookings] =
await Promise.all([
clubs.listAll(),
teeTimePrisma.teeSheet.count(),
teeTimePrisma.teeSheet.count({ where: { offline: true } }),
teeTimePrisma.teeTimeSlot.count(),
teeTimePrisma.teeTimeSlot.count({ where: bookedCriteria }),
fetchBookings(rangeStart)
]);
Map-Based Aggregation
// Daily booking aggregation
const dailyMap = new Map<string, { count: number; revenue: number }>();
for (const booking of bookings) {
const dateKey = formatDate(booking.ReservationDate);
const existing = dailyMap.get(dateKey) ?? { count: 0, revenue: 0 };
dailyMap.set(dateKey, {
count: existing.count + 1,
revenue: existing.revenue + booking.TotalPrice
});
}
Club Statistics
| Metric | Description |
|---|---|
total | Total clubs in system |
teeSheetEnabled | Clubs with useTeeSheet: true |
bridgeEnabled | Clubs with useBridge: true |
Tee Sheet Statistics
| Metric | Description |
|---|---|
total | Total tee sheets |
offline | Offline tee sheets |
utilizationRate | Overall utilization (0-1) |
bookedSlots | Total booked slots |
totalSlots | Total available slots |
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /admin/reports/summary | High-level summary |
GET | /admin/reports/breakdowns | Daily breakdown |
Query Parameters
| Param | Type | Default | Range |
|---|---|---|---|
rangeDays | number | 30 | 7-180 |
Authentication
All report endpoints require:
JwtAuthGuardAudienceGuard('teetime-admin')
Response DTOs
Summary DTO
interface AdminReportSummary {
generatedAt: string;
rangeDays: number;
clubs: {
total: number;
teeSheetEnabled: number;
bridgeEnabled: number;
};
teeSheets: {
total: number;
offline: number;
utilizationRate: number;
bookedSlots: number;
totalSlots: number;
};
bookings: {
last7Days: { count: number; revenue: number };
last30Days: { count: number; revenue: number };
};
}
Breakdown DTO
interface AdminReportBreakdowns {
generatedAt: string;
rangeDays: number;
bookings: AdminReportBreakdownPoint[];
}
interface AdminReportBreakdownPoint {
date: string; // YYYY-MM-DD
count: number; // Booking count
revenue: number; // Revenue total
utilizationRate: number; // 0-1
bookedSlots: number;
totalSlots: number;
}
Missing Data Handling
| Scenario | Behavior |
|---|---|
| No bookings for date | { count: 0, revenue: 0 } |
| No slots for date | utilizationRate: 0 |
| Invalid revenue value | Treated as $0 |
| Future dates | Excluded from range |
Performance
- Parallel data fetching reduces latency
- Map-based aggregation avoids N+1 queries
- Date range clamping prevents expensive queries
- Indexed queries on reservation date
Troubleshooting
Zero Revenue Showing
- Check bookings have
TotalPricepopulated - Verify date range includes bookings
- Check booking status is not
CANCELLED
Utilization Always Zero
- Verify slots exist for date range
- Check slot status criteria
- Confirm tee sheets are not offline
Slow Report Generation
- Reduce
rangeDaysparameter - Check database indexes
- Review parallel query performance
- Consider caching for repeated requests