Skip to main content

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

MetricDescription
countNumber of bookings
revenueTotal revenue (currency)
last7DaysRolling 7-day aggregate
last30DaysRolling 30-day aggregate

Data Source: MCA bookings table

Query Criteria:

  • ReservationDate >= rangeStart
  • Status !== '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: true
  • reservationNumber is not null
  • available: 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

MetricDescription
totalTotal clubs in system
teeSheetEnabledClubs with useTeeSheet: true
bridgeEnabledClubs with useBridge: true

Tee Sheet Statistics

MetricDescription
totalTotal tee sheets
offlineOffline tee sheets
utilizationRateOverall utilization (0-1)
bookedSlotsTotal booked slots
totalSlotsTotal available slots

API Endpoints

MethodEndpointDescription
GET/admin/reports/summaryHigh-level summary
GET/admin/reports/breakdownsDaily breakdown

Query Parameters

ParamTypeDefaultRange
rangeDaysnumber307-180

Authentication

All report endpoints require:

  • JwtAuthGuard
  • AudienceGuard('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

ScenarioBehavior
No bookings for date{ count: 0, revenue: 0 }
No slots for dateutilizationRate: 0
Invalid revenue valueTreated as $0
Future datesExcluded 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

  1. Check bookings have TotalPrice populated
  2. Verify date range includes bookings
  3. Check booking status is not CANCELLED

Utilization Always Zero

  1. Verify slots exist for date range
  2. Check slot status criteria
  3. Confirm tee sheets are not offline

Slow Report Generation

  1. Reduce rangeDays parameter
  2. Check database indexes
  3. Review parallel query performance
  4. Consider caching for repeated requests