mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
feat: scheduled report digests + per-session capacity (M33)
POST /v1/internal/jobs/reports/dispatch emails each tenant owner a trailing-window digest (bookings, revenue, per-service and per-staff breakdown) localized to the tenant locale; zero-activity tenants are skipped. Metrics cores extracted to tenant-scoped variants so the job needs no principal. PATCH /v1/catalog/class-sessions/:id sets a session's seat count with a confirmed-bookings floor — shrinking below it needs allowOverCapacity, matching the waitlist-promotion convention. Session rows gain an inline capacity editor with an overbook confirm state. 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
e6f1ec38a4
commit
e680e0c2b8
14 files changed
+808
-10
No files matched your search
@@ -457,6 +457,9 @@ const dictionaries = {
|
||||
"dashboard.class.applyToFutureHint": "Přesune již vytvořené lekce na nový čas a místo, rezervace se přesunou s nimi.",
|
||||
"dashboard.class.ruleActive": "Pravidlo je aktivní",
|
||||
"dashboard.class.inactive": "neaktivní",
|
||||
"dashboard.class.editCapacity": "Upravit kapacitu",
|
||||
"dashboard.class.capacityLabel": "Kapacita",
|
||||
"dashboard.class.capacityOverbookHint": "Kapacita je nižší než počet potvrzených míst — uložením se lekce přebookuje.",
|
||||
"dashboard.class.dow.0": "Neděle",
|
||||
"dashboard.class.dow.1": "Pondělí",
|
||||
"dashboard.class.dow.2": "Úterý",
|
||||
@@ -1093,6 +1096,9 @@ const dictionaries = {
|
||||
"dashboard.class.applyToFutureHint": "Moves already-created sessions to the new time and location; bookings move with them.",
|
||||
"dashboard.class.ruleActive": "Rule is active",
|
||||
"dashboard.class.inactive": "inactive",
|
||||
"dashboard.class.editCapacity": "Edit capacity",
|
||||
"dashboard.class.capacityLabel": "Capacity",
|
||||
"dashboard.class.capacityOverbookHint": "Capacity is below the confirmed seat count — saving overbooks the session.",
|
||||
"dashboard.class.dow.0": "Sunday",
|
||||
"dashboard.class.dow.1": "Monday",
|
||||
"dashboard.class.dow.2": "Tuesday",
|
||||
|
||||
@@ -42,6 +42,12 @@ function ZonesPage() {
|
||||
const [editSchedApplyFuture, setEditSchedApplyFuture] = createSignal(true);
|
||||
const [editSchedActive, setEditSchedActive] = createSignal(true);
|
||||
const [schedEditSaving, setSchedEditSaving] = createSignal(false);
|
||||
const [editingCapSessionId, setEditingCapSessionId] = createSignal<string | null>(null);
|
||||
const [editCapValue, setEditCapValue] = createSignal(0);
|
||||
const [capSaving, setCapSaving] = createSignal(false);
|
||||
// Session ID whose capacity shrink hit the confirmed-seat floor — the row
|
||||
// switches to an explicit "set anyway" confirm before resubmitting.
|
||||
const [capOverbookConfirm, setCapOverbookConfirm] = createSignal<string | null>(null);
|
||||
const [expandedSessionId, setExpandedSessionId] = createSignal<string | null>(null);
|
||||
const [promoteBusy, setPromoteBusy] = createSignal<string | null>(null);
|
||||
// Booking IDs whose promote attempt hit a full session — awaiting the
|
||||
@@ -448,6 +454,35 @@ function ZonesPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const startEditCapacity = (session: any) => {
|
||||
setEditingCapSessionId(session.id);
|
||||
setEditCapValue(session.capacity);
|
||||
setCapOverbookConfirm(null);
|
||||
};
|
||||
|
||||
const handleSaveCapacity = async (session: any, allowOverCapacity: boolean) => {
|
||||
const bearer = data.token();
|
||||
if (!bearer || bearer.startsWith("demo.")) { data.setDemoNotice(cs() ? "V demo režimu nelze ukládat." : "Cannot save in demo mode."); return; }
|
||||
setCapSaving(true);
|
||||
try {
|
||||
const response = await (apiClient as any).PATCH(`/v1/catalog/class-sessions/${session.id}`, {
|
||||
...authHeaders(),
|
||||
body: { capacity: editCapValue(), allowOverCapacity },
|
||||
});
|
||||
if (response.response?.status === 409 && !allowOverCapacity) {
|
||||
setCapOverbookConfirm(session.id);
|
||||
return;
|
||||
}
|
||||
setEditingCapSessionId(null);
|
||||
setCapOverbookConfirm(null);
|
||||
void refetchSessions();
|
||||
} catch {
|
||||
data.setDemoNotice(cs() ? "Uložení selhalo." : "Save failed.");
|
||||
} finally {
|
||||
setCapSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSchedule = async (id: string) => {
|
||||
const bearer = data.token();
|
||||
if (!bearer || bearer.startsWith("demo.")) { data.setDemoNotice(cs() ? "V demo režimu nelze mazat." : "Cannot delete in demo mode."); return; }
|
||||
@@ -891,9 +926,26 @@ function ZonesPage() {
|
||||
{expanded() ? data.i18n.t("dashboard.class.hideRoster") : data.i18n.t("dashboard.class.showRoster")}
|
||||
<span class={`inline-flex transition-transform duration-200 ${expanded() ? "rotate-180" : ""}`}><ChevronDownIcon /></span>
|
||||
</button>
|
||||
<button onClick={() => startEditCapacity(session)} class="p-2 text-ink-subtle hover:text-accent hover:bg-accent-subtle/40 rounded-lg transition-colors" aria-label={data.i18n.t("dashboard.class.editCapacity")}><PencilIcon /></button>
|
||||
<button onClick={() => void handleDeleteSession(session.id)} class="p-2 text-ink-subtle hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" aria-label={cs() ? "Smazat" : "Delete"}><Trash2Icon /></button>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={editingCapSessionId() === session.id}>
|
||||
<div class="px-6 pb-4 animate-fade-in">
|
||||
<div class="flex flex-wrap items-end gap-3 rounded-xl border border-border/60 bg-canvas-subtle/30 p-4">
|
||||
<Input type="number" label={data.i18n.t("dashboard.class.capacityLabel")} value={String(editCapValue())} min={1} onInput={(e) => setEditCapValue(parseInt(e.currentTarget.value, 10) || 0)} />
|
||||
<button onClick={() => void handleSaveCapacity(session, capOverbookConfirm() === session.id)} disabled={capSaving() || editCapValue() < 1} class="btn-primary text-sm disabled:opacity-60">
|
||||
<CheckIcon /> {capSaving() ? data.i18n.t("dashboard.saving") : data.i18n.t("dashboard.saveChanges")}
|
||||
</button>
|
||||
<button onClick={() => { setEditingCapSessionId(null); setCapOverbookConfirm(null); }} class="btn-secondary text-sm">
|
||||
<XIcon /> {data.i18n.t("dashboard.cancel")}
|
||||
</button>
|
||||
<Show when={capOverbookConfirm() === session.id}>
|
||||
<p class="w-full text-sm text-warning font-medium">{data.i18n.t("dashboard.class.capacityOverbookHint")}</p>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={expanded()}>
|
||||
<div class="px-6 pb-5 animate-fade-in">
|
||||
<div class="rounded-xl border border-border/60 bg-canvas-subtle/30 overflow-hidden">
|
||||
|
||||
Reference in new issue
Block a user