(null);
const [newSessionTemplateId, setNewSessionTemplateId] = createSignal("");
const [newSessionLocationId, setNewSessionLocationId] = createSignal("");
const [newSessionStartsAt, setNewSessionStartsAt] = createSignal("");
@@ -211,18 +212,25 @@ function ZonesPage() {
}
};
- const handleAddTemplate = async () => {
+ const resetTemplateForm = () => {
+ setNewTemplateTitle(""); setNewTemplateDuration(60); setNewTemplateCapacity(8); setEditingTemplateId(null);
+ };
+
+ const handleSaveTemplate = async () => {
const bearer = data.token();
if (!newTemplateTitle().trim()) return;
if (!bearer || bearer.startsWith("demo.")) { data.setDemoNotice(cs() ? "V demo režimu nelze ukládat." : "Cannot save in demo mode."); return; }
setTemplateSaving(true);
try {
- await (apiClient as any).POST("/v1/catalog/class-templates", {
- ...authHeaders(),
- body: { title: newTemplateTitle().trim(), durationMinutes: newTemplateDuration(), capacity: newTemplateCapacity() },
- });
- setNewTemplateTitle(""); setNewTemplateDuration(60); setNewTemplateCapacity(8);
- void refetchTemplates();
+ const body = { title: newTemplateTitle().trim(), durationMinutes: newTemplateDuration(), capacity: newTemplateCapacity() };
+ const editing = editingTemplateId();
+ if (editing) {
+ await (apiClient as any).PUT(`/v1/catalog/class-templates/${editing}`, { ...authHeaders(), body });
+ } else {
+ await (apiClient as any).POST("/v1/catalog/class-templates", { ...authHeaders(), body });
+ }
+ resetTemplateForm();
+ void refetchTemplates(); void refetchSessions();
} catch {
data.setDemoNotice(cs() ? "Uložení selhalo." : "Save failed.");
} finally {
@@ -230,6 +238,13 @@ function ZonesPage() {
}
};
+ const handleEditTemplate = (tpl: any) => {
+ setNewTemplateTitle(tpl.title);
+ setNewTemplateDuration(tpl.durationMinutes);
+ setNewTemplateCapacity(tpl.capacity);
+ setEditingTemplateId(tpl.id);
+ };
+
const handleDeleteTemplate = 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; }
@@ -487,9 +502,14 @@ function ZonesPage() {
setNewTemplateDuration(parseInt(e.currentTarget.value) || 60)} />
setNewTemplateCapacity(parseInt(e.currentTarget.value) || 8)} />
-
@@ -509,7 +529,10 @@ function ZonesPage() {
{tpl.durationMinutes} {data.i18n.t("dashboard.service.min")} • {data.i18n.t("dashboard.zone.capacity")}: {tpl.capacity}
- void handleDeleteTemplate(tpl.id)} class="p-2 text-ink-subtle hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors">
+
+
handleEditTemplate(tpl)} class="p-2 text-ink-subtle hover:text-accent hover:bg-accent-subtle rounded-lg transition-colors">
+
void handleDeleteTemplate(tpl.id)} class="p-2 text-ink-subtle hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors">
+
)}
diff --git a/packages/api-client/src/generated/types.ts b/packages/api-client/src/generated/types.ts
index b74e3ea..0f6b11b 100644
--- a/packages/api-client/src/generated/types.ts
+++ b/packages/api-client/src/generated/types.ts
@@ -359,7 +359,7 @@ export interface paths {
cookie?: never;
};
get?: never;
- put?: never;
+ put: operations["updateClassTemplate"];
post?: never;
delete: operations["deleteClassTemplate"];
options?: never;
@@ -2220,6 +2220,44 @@ export interface operations {
};
};
};
+ updateClassTemplate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ templateID: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpsertClassTemplateRequest"];
+ };
+ };
+ responses: {
+ /** @description Updated class template */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Invalid input */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Template not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
deleteClassTemplate: {
parameters: {
query?: never;
diff --git a/project.md b/project.md
index 1cb8cd8..66c2577 100644
--- a/project.md
+++ b/project.md
@@ -112,6 +112,10 @@
- `CustomerBookingView` carries `waitlistPosition` for waitlisted class bookings; the manage page now shows a proper status badge for `waitlisted`/`cancelled` (previously both rendered as "Pending") plus a notice with the queue position and the auto-promotion explanation, in CS and EN
- fixed invalid token names along the way (`--danger-soft`/`--warning-soft` do not exist; `--error-subtle`/`--warning-subtle` do)
- remaining: live staging validation; manual waitlist promotion controls are still not exposed
+- `[done/partial]` Milestone 17: Class template editing
+ - `PUT /v1/catalog/class-templates/{id}` end-to-end: `UpdateClassTemplate` in both repositories, catalog service method sharing `validateClassTemplateInput` with create, route, OpenAPI path, regenerated client
+ - classes tab template rows gained a pencil edit action that loads the template into the form; the submit button switches to save-changes with a cancel affordance
+ - remaining: live staging validation
## Overview
Bookra is a Czech-first booking SaaS for small local service businesses. It targets salons, massage therapists, small clinics, repair shops, and small studios/clubs that need a simple public booking flow, internal schedule management, reminders, and basic business reporting without becoming a full ERP.