mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
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:
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")} • {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>
|
||||
|
||||
Reference in new issue
Block a user