mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
feat(reports): capacity-based utilization + report CSVs
Utilization now measures booked minutes against each member's availability-rule capacity in the tenant timezone, replacing the confirmed-share proxy. StaffMetric exposes booked/capacity minutes. New endpoints: GET /v1/export/staff-report.csv (reuses StaffMetrics) and GET /v1/export/service-report.csv (per-service bookings/cancelled/ revenue, class bookings rolled up). Export buttons on the staff performance panel and services page. 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
2464aa45fa
commit
7b9080eb1c
13 files changed
+541
-33
No files matched your search
@@ -3,6 +3,7 @@ import { apiClient } from "../../lib/api-client";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { PlusIcon, ScissorsIcon, Trash2Icon, EditIcon, ClockIcon } from "../../components/dashboard/icons";
|
||||
import { DashboardLayout, useDashboardData } from "./layout";
|
||||
import { ExportCsvButton } from "../../components/dashboard/export-csv-button";
|
||||
|
||||
interface ServiceForm {
|
||||
name: string;
|
||||
@@ -122,7 +123,10 @@ function ServicesPage() {
|
||||
<h1 class="text-2xl sm:text-3xl font-bold text-ink font-display">{data.i18n.t("dashboard.services")}</h1>
|
||||
<p class="text-ink-muted mt-1">{resolvedServices().length} {data.i18n.t("dashboard.services.subtitle")}</p>
|
||||
</div>
|
||||
<button onClick={openCreate} class="btn-primary text-sm shrink-0"><PlusIcon /> {data.i18n.t("dashboard.service.add")}</button>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<ExportCsvButton path="/v1/export/service-report.csv?days=30" filename="bookra-service-report.csv" token={data.token} />
|
||||
<button onClick={openCreate} class="btn-primary text-sm shrink-0"><PlusIcon /> {data.i18n.t("dashboard.service.add")}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="surface-card overflow-hidden">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Input } from "../../components/ui/input";
|
||||
import { Select } from "../../components/ui/select";
|
||||
import { PlusIcon, UsersIcon, Trash2Icon, EditIcon, SparklesIcon, MapPinIcon } from "../../components/dashboard/icons";
|
||||
import { DashboardLayout, useDashboardData } from "./layout";
|
||||
import { ExportCsvButton } from "../../components/dashboard/export-csv-button";
|
||||
|
||||
interface StaffForm {
|
||||
displayName: string;
|
||||
@@ -62,9 +63,9 @@ function StaffPage() {
|
||||
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 },
|
||||
{ staffId: "st1", displayName: "Petra M.", bookings: 12, completed: 9, cancelled: 1, bookedMinutes: 9300, capacityMinutes: 10080, utilizationPercent: 92, revenueCents: 1440000 },
|
||||
{ staffId: "st2", displayName: "Lucie K.", bookings: 8, completed: 5, cancelled: 2, bookedMinutes: 6350, capacityMinutes: 10080, utilizationPercent: 63, revenueCents: 960000 },
|
||||
{ staffId: "st3", displayName: cs() ? "Martin Hájek" : "Martin Hajek", bookings: 0, completed: 0, cancelled: 0, bookedMinutes: 0, capacityMinutes: 10080, utilizationPercent: 0, revenueCents: 0 },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -73,6 +74,9 @@ function StaffPage() {
|
||||
},
|
||||
);
|
||||
|
||||
const formatHours = (minutes: number) =>
|
||||
minutes >= 60 ? `${Math.round(minutes / 60)} h` : `${minutes} min`;
|
||||
|
||||
const formatRevenue = (cents: number) =>
|
||||
new Intl.NumberFormat(cs() ? "cs-CZ" : "en-US", {
|
||||
style: "currency",
|
||||
@@ -192,7 +196,10 @@ function StaffPage() {
|
||||
<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 class="flex items-center gap-3">
|
||||
<span class="text-xs text-ink-subtle">{data.i18n.t("dashboard.staff.performanceWindow")}</span>
|
||||
<ExportCsvButton path="/v1/export/staff-report.csv?days=30" filename="bookra-staff-report.csv" token={data.token} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="divide-y divide-border/60">
|
||||
<For each={metrics()!.staff}>
|
||||
@@ -215,6 +222,7 @@ function StaffPage() {
|
||||
<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>
|
||||
<p class="text-[10px] text-ink-subtle tabular-nums">{formatHours(m.bookedMinutes)} / {formatHours(m.capacityMinutes)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-ink-subtle">{data.i18n.t("dashboard.staff.revenue")}</p>
|
||||
|
||||
Reference in new issue
Block a user