diff --git a/apps/portal/pages/admin/scheduling.vue b/apps/portal/pages/admin/scheduling.vue index 01315e6..25cb198 100644 --- a/apps/portal/pages/admin/scheduling.vue +++ b/apps/portal/pages/admin/scheduling.vue @@ -78,6 +78,50 @@ async function runConfirm() { } } +// ── Overview / analytics (read-only, tenant-level) ── +interface HostCount { hostId: string; displayName: string; count: number } +interface DayPoint { date: string; count: number } +interface SchedulingOverview { + totalBookings: number + upcomingCount: number + byStatus: Record + byHost: HostCount[] + last30Days: DayPoint[] +} +const overview = ref(null) +async function loadOverview() { + try { + overview.value = (await request(`${base.value}/overview`)) as SchedulingOverview + } catch (err) { + toastErr(err, 'Could not load overview') + } +} +if (slug.value) loadOverview() +watch(slug, (s) => { if (s) loadOverview() }) + +// Series scaling for the simple bar sparkline. +const seriesMax = computed(() => Math.max(1, ...(overview.value?.last30Days ?? []).map((d) => d.count))) +function barHeight(count: number): number { + // Min 2% so empty days still show a faint baseline tick. + return Math.max(2, Math.round((count / seriesMax.value) * 100)) +} +const dayLabel = (date: string) => + new Date(`${date}T00:00:00Z`).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' }) +// Statuses shown in the breakdown, in a sensible reading order. +const STATUS_ORDER = ['confirmed', 'pending', 'rescheduled', 'cancelled', 'calendar_failed'] as const +const statusLabel: Record = { + confirmed: 'Confirmed', + pending: 'Pending', + rescheduled: 'Rescheduled', + cancelled: 'Cancelled', + calendar_failed: 'Calendar failed', +} +const statusBreakdown = computed(() => + STATUS_ORDER.map((s) => ({ status: s, label: statusLabel[s], count: overview.value?.byStatus?.[s] ?? 0 })).filter( + (r) => r.count > 0, + ), +) + const { data: hosts, refresh: refreshHosts } = await useFetch(() => `${base.value}/hosts`, { key: 'sched-hosts', default: () => [], @@ -565,6 +609,70 @@ const maskSecret = (s: string) => (s.length > 12 ? `${s.slice(0, 9)}…${s.slice
+ +
+
+ +
Total bookings
+
{{ overview.totalBookings }}
+
+ +
Upcoming
+
{{ overview.upcomingCount }}
+
confirmed, in the future
+
+ +
Active hosts
+
{{ overview.byHost.length }}
+
with bookings
+
+ +
Last 30 days
+
{{ overview.last30Days.reduce((s, d) => s + d.count, 0) }}
+
bookings created
+
+
+ +
+ +
+ Bookings created · last 30 days +
+
+
+
+ + + +
By status
+

No bookings yet.

+
+
+ {{ r.label }} + {{ r.count }} +
+
+
+ + +
By host
+

No bookings yet.

+
+
+ {{ h.displayName }} + {{ h.count }} +
+
+
+
+
+ No bookable hosts yet. Add a host to create their booking pages — calendar access is provisioned automatically. @@ -942,6 +1050,26 @@ const maskSecret = (s: string) => (s.length > 12 ? `${s.slice(0, 9)}…${s.slice