mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
feat(metrics): per-staff performance view in dashboard
GET /v1/metrics/staff returns bookings, completed, cancelled, utilization, and revenue per staff member over a trailing window (default 30d, max 365d). Appointment bookings only — class sessions have no staff assignment. All active staff appear, including zero-booking rows. Staff page gains a localized Performance panel; demo mode seeds trailing-window bookings and matching metric rows. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
4674203502
commit
76e45d4178
11 files changed
+496
-3
No files matched your search
@@ -55,6 +55,31 @@ function StaffPage() {
|
||||
},
|
||||
);
|
||||
|
||||
const [metrics] = createResource(
|
||||
() => data.token(),
|
||||
async (bearer) => {
|
||||
if (!bearer) return null;
|
||||
if (bearer.startsWith("demo.")) {
|
||||
return {
|
||||
staff: [
|
||||
{ staffId: "st1", displayName: "Petra M.", bookings: 12, completed: 9, cancelled: 1, utilizationPercent: 92, revenueCents: 1440000 },
|
||||
{ staffId: "st2", displayName: "Lucie K.", bookings: 8, completed: 5, cancelled: 2, utilizationPercent: 63, revenueCents: 960000 },
|
||||
{ staffId: "st3", displayName: cs() ? "Martin Hájek" : "Martin Hajek", bookings: 0, completed: 0, cancelled: 0, utilizationPercent: 0, revenueCents: 0 },
|
||||
],
|
||||
};
|
||||
}
|
||||
const response = await apiClient.GET("/v1/metrics/staff", authHeaders());
|
||||
return response.data ?? null;
|
||||
},
|
||||
);
|
||||
|
||||
const formatRevenue = (cents: number) =>
|
||||
new Intl.NumberFormat(cs() ? "cs-CZ" : "en-US", {
|
||||
style: "currency",
|
||||
currency: "CZK",
|
||||
maximumFractionDigits: 0,
|
||||
}).format(cents / 100);
|
||||
|
||||
const resolvedStaff = () => staff() ?? [];
|
||||
const resolvedLocations = () => locations() ?? [];
|
||||
const staffLimit = () => data.resolvedBilling()?.entitlements?.maxStaff ?? -1;
|
||||
@@ -163,6 +188,46 @@ function StaffPage() {
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={(metrics()?.staff?.length ?? 0) > 0}>
|
||||
<div class="surface-card p-6">
|
||||
<div class="flex items-baseline justify-between gap-4 mb-5">
|
||||
<h2 class="text-lg font-semibold text-ink font-display">{data.i18n.t("dashboard.staff.performance")}</h2>
|
||||
<span class="text-xs text-ink-subtle">{data.i18n.t("dashboard.staff.performanceWindow")}</span>
|
||||
</div>
|
||||
<div class="divide-y divide-border/60">
|
||||
<For each={metrics()!.staff}>
|
||||
{(m: any) => (
|
||||
<div class="py-4 first:pt-0 last:pb-0">
|
||||
<p class="font-medium text-ink mb-3">{m.displayName}</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-5 gap-x-6 gap-y-3">
|
||||
<div>
|
||||
<p class="text-xs text-ink-subtle">{data.i18n.t("dashboard.bookings")}</p>
|
||||
<p class="text-lg font-semibold text-ink tabular-nums">{m.bookings}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-ink-subtle">{data.i18n.t("dashboard.completed")}</p>
|
||||
<p class="text-lg font-semibold text-ink tabular-nums">{m.completed}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-ink-subtle">{data.i18n.t("dashboard.cancelled")}</p>
|
||||
<p class="text-lg font-semibold text-ink tabular-nums">{m.cancelled}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-ink-subtle">{data.i18n.t("dashboard.kpi.utilization")}</p>
|
||||
<p class="text-lg font-semibold text-accent tabular-nums">{m.utilizationPercent}%</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-ink-subtle">{data.i18n.t("dashboard.staff.revenue")}</p>
|
||||
<p class="text-lg font-semibold text-ink tabular-nums">{formatRevenue(m.revenueCents)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="surface-card overflow-hidden">
|
||||
<Show when={showForm()}>
|
||||
<div class="p-6 border-b border-border bg-canvas-subtle/50">
|
||||
|
||||
Reference in new issue
Block a user