feat(catalog): class template editing

Templates were delete-and-recreate only. Adds PUT
/v1/catalog/class-templates/{id} across repository, service (sharing
the create-time validation), route, spec, and client; the classes tab
gets a pencil action that loads the row into the form with a
save/cancel affordance.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-18 13:13:37 +02:00
1 parent 3892e9ce8c
commit 5884203e9d
11 files changed
+249 -21

No files matched your search

@@ -114,6 +114,12 @@ export const PlusIcon = () => (
</svg>
);
export const PencilIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5"/>
</svg>
);
export const ScissorsIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="6" cy="6" r="3"/><path d="M8.12 8.12 12 12"/><path d="M20 4 8.12 15.88"/><circle cx="6" cy="18" r="3"/><path d="M14.8 14.8 20 20"/>
@@ -411,6 +411,7 @@ const dictionaries = {
"dashboard.class.templates": "Šablony lekcí",
"dashboard.class.sessions": "Naplánované lekce",
"dashboard.class.addTemplate": "Nová šablona",
"dashboard.class.saveChanges": "Uložit změny",
"dashboard.class.addSession": "Naplánovat lekci",
"dashboard.class.title": "Název",
"dashboard.class.titlePlaceholder": "např. Jóga pro začátečníky",
@@ -986,6 +987,7 @@ const dictionaries = {
"dashboard.class.templates": "Class templates",
"dashboard.class.sessions": "Scheduled sessions",
"dashboard.class.addTemplate": "New template",
"dashboard.class.saveChanges": "Save changes",
"dashboard.class.addSession": "Schedule session",
"dashboard.class.title": "Title",
"dashboard.class.titlePlaceholder": "e.g. Beginner yoga",
@@ -2,7 +2,7 @@ import { For, Show, createResource, createSignal } from "solid-js";
import { apiClient } from "../../lib/api-client";
import { Input } from "../../components/ui/input";
import { Select } from "../../components/ui/select";
import { MapPinIcon, PlusIcon, BanIcon, ClockIcon, SparklesIcon, Trash2Icon, UsersIcon } from "../../components/dashboard/icons";
import { MapPinIcon, PlusIcon, BanIcon, ClockIcon, SparklesIcon, Trash2Icon, UsersIcon, PencilIcon } from "../../components/dashboard/icons";
import { DashboardLayout, useDashboardData } from "./layout";
function ZonesPage() {
@@ -24,6 +24,7 @@ function ZonesPage() {
const [newTemplateDuration, setNewTemplateDuration] = createSignal(60);
const [newTemplateCapacity, setNewTemplateCapacity] = createSignal(8);
const [templateSaving, setTemplateSaving] = createSignal(false);
const [editingTemplateId, setEditingTemplateId] = createSignal<string | null>(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() {
<Input type="number" label={data.i18n.t("dashboard.class.durationMin")} value={newTemplateDuration()} onInput={(e) => setNewTemplateDuration(parseInt(e.currentTarget.value) || 60)} />
<Input type="number" label={data.i18n.t("dashboard.zone.capacity")} value={newTemplateCapacity()} onInput={(e) => setNewTemplateCapacity(parseInt(e.currentTarget.value) || 8)} />
<div class="flex items-end">
<button onClick={() => void handleAddTemplate()} disabled={templateSaving() || !newTemplateTitle().trim()} class="btn-primary text-sm w-full sm:w-auto disabled:opacity-60">
{templateSaving() ? data.i18n.t("dashboard.saving") : data.i18n.t("dashboard.class.addTemplate")}
<button onClick={() => void handleSaveTemplate()} disabled={templateSaving() || !newTemplateTitle().trim()} class="btn-primary text-sm w-full sm:w-auto disabled:opacity-60">
{templateSaving() ? data.i18n.t("dashboard.saving") : editingTemplateId() ? data.i18n.t("dashboard.class.saveChanges") : data.i18n.t("dashboard.class.addTemplate")}
</button>
<Show when={editingTemplateId()}>
<button onClick={resetTemplateForm} class="text-sm text-ink-muted hover:text-ink px-3 py-2">
{data.i18n.t("dashboard.cancel")}
</button>
</Show>
</div>
</div>
</div>
@@ -509,7 +529,10 @@ function ZonesPage() {
<p class="text-sm text-ink-muted">{tpl.durationMinutes} {data.i18n.t("dashboard.service.min")} &bull; {data.i18n.t("dashboard.zone.capacity")}: {tpl.capacity}</p>
</div>
</div>
<button onClick={() => void handleDeleteTemplate(tpl.id)} class="p-2 text-ink-subtle hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"><Trash2Icon /></button>
<div class="flex items-center gap-1">
<button onClick={() => handleEditTemplate(tpl)} class="p-2 text-ink-subtle hover:text-accent hover:bg-accent-subtle rounded-lg transition-colors"><PencilIcon /></button>
<button onClick={() => void handleDeleteTemplate(tpl.id)} class="p-2 text-ink-subtle hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"><Trash2Icon /></button>
</div>
</div>
)}
</For>