mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
feat: series-aware schedule edits + waitlist ledger removal (M32)
PATCH /v1/catalog/class-schedules/:id patches a recurring rule and, with applyToFuture, retimes materialized future sessions to the new local start time on their existing dates — moving confirmed and waitlisted bookings with them and re-arming reminders. Zones-page schedule rows gain an inline editor (CS/EN). Also drops the vestigial waitlist_entries table (migration 00020): positions derive from bookings.created_at, so the ledger was written but never read. 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
55fc0d06e3
commit
65a693cf38
17 files changed
+840
-230
No files matched your search
@@ -452,6 +452,11 @@ const dictionaries = {
|
||||
"dashboard.class.startsLocal": "Čas",
|
||||
"dashboard.class.addSchedule": "Přidat pravidlo",
|
||||
"dashboard.class.noSchedules": "Zatím žádná opakující se pravidla.",
|
||||
"dashboard.class.editSchedule": "Upravit pravidlo",
|
||||
"dashboard.class.applyToFuture": "Použít na budoucí lekce",
|
||||
"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.dow.0": "Neděle",
|
||||
"dashboard.class.dow.1": "Pondělí",
|
||||
"dashboard.class.dow.2": "Úterý",
|
||||
@@ -1083,6 +1088,11 @@ const dictionaries = {
|
||||
"dashboard.class.startsLocal": "Time",
|
||||
"dashboard.class.addSchedule": "Add rule",
|
||||
"dashboard.class.noSchedules": "No recurring rules yet.",
|
||||
"dashboard.class.editSchedule": "Edit rule",
|
||||
"dashboard.class.applyToFuture": "Apply to future sessions",
|
||||
"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.dow.0": "Sunday",
|
||||
"dashboard.class.dow.1": "Monday",
|
||||
"dashboard.class.dow.2": "Tuesday",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { apiClient } from "../../lib/api-client";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { Select } from "../../components/ui/select";
|
||||
import { getInitials } from "../../components/dashboard/types";
|
||||
import { MapPinIcon, PlusIcon, BanIcon, ClockIcon, SparklesIcon, Trash2Icon, UsersIcon, PencilIcon, ChevronDownIcon, CalendarDaysIcon } from "../../components/dashboard/icons";
|
||||
import { MapPinIcon, PlusIcon, BanIcon, ClockIcon, SparklesIcon, Trash2Icon, UsersIcon, PencilIcon, ChevronDownIcon, CalendarDaysIcon, CheckIcon, XIcon } from "../../components/dashboard/icons";
|
||||
import { DashboardLayout, useDashboardData } from "./layout";
|
||||
|
||||
function ZonesPage() {
|
||||
@@ -35,6 +35,13 @@ function ZonesPage() {
|
||||
const [newSchedStartsLocal, setNewSchedStartsLocal] = createSignal("");
|
||||
const [newSchedLocationId, setNewSchedLocationId] = createSignal("");
|
||||
const [schedSaving, setSchedSaving] = createSignal(false);
|
||||
const [editingSchedId, setEditingSchedId] = createSignal<string | null>(null);
|
||||
const [editSchedDayOfWeek, setEditSchedDayOfWeek] = createSignal("1");
|
||||
const [editSchedStartsLocal, setEditSchedStartsLocal] = createSignal("");
|
||||
const [editSchedLocationId, setEditSchedLocationId] = createSignal("");
|
||||
const [editSchedApplyFuture, setEditSchedApplyFuture] = createSignal(true);
|
||||
const [editSchedActive, setEditSchedActive] = createSignal(true);
|
||||
const [schedEditSaving, setSchedEditSaving] = createSignal(false);
|
||||
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
|
||||
@@ -142,8 +149,8 @@ function ZonesPage() {
|
||||
if (!bearer || tab !== "classes") return [];
|
||||
if (bearer.startsWith("demo.")) {
|
||||
return [
|
||||
{ id: "sch1", templateId: "ct1", templateTitle: cs() ? "Jóga pro začátečníky" : "Beginner yoga", locationName: cs() ? "Hlavní sál" : "Main Hall", dayOfWeek: 3, startsLocal: "10:00:00", active: true },
|
||||
{ id: "sch2", templateId: "ct1", templateTitle: cs() ? "Jóga pro začátečníky" : "Beginner yoga", locationName: cs() ? "Hlavní sál" : "Main Hall", dayOfWeek: 6, startsLocal: "09:30:00", active: true },
|
||||
{ id: "sch1", templateId: "ct1", templateTitle: cs() ? "Jóga pro začátečníky" : "Beginner yoga", locationId: "z1", locationName: cs() ? "Hlavní sál" : "Main Hall", dayOfWeek: 3, startsLocal: "10:00:00", active: true },
|
||||
{ id: "sch2", templateId: "ct1", templateTitle: cs() ? "Jóga pro začátečníky" : "Beginner yoga", locationId: "z1", locationName: cs() ? "Hlavní sál" : "Main Hall", dayOfWeek: 6, startsLocal: "09:30:00", active: true },
|
||||
];
|
||||
}
|
||||
const response = await (apiClient as any).GET("/v1/catalog/class-schedules", authHeaders());
|
||||
@@ -405,6 +412,42 @@ function ZonesPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const startEditSchedule = (sch: any) => {
|
||||
setEditingSchedId(sch.id);
|
||||
setEditSchedDayOfWeek(String(sch.dayOfWeek));
|
||||
setEditSchedStartsLocal(String(sch.startsLocal).slice(0, 5));
|
||||
setEditSchedLocationId(sch.locationId ?? "");
|
||||
setEditSchedApplyFuture(true);
|
||||
setEditSchedActive(sch.active !== false);
|
||||
};
|
||||
|
||||
const handleSaveSchedule = async () => {
|
||||
const bearer = data.token();
|
||||
const id = editingSchedId();
|
||||
if (!id) return;
|
||||
if (!bearer || bearer.startsWith("demo.")) { data.setDemoNotice(cs() ? "V demo režimu nelze ukládat." : "Cannot save in demo mode."); return; }
|
||||
setSchedEditSaving(true);
|
||||
try {
|
||||
await (apiClient as any).PATCH(`/v1/catalog/class-schedules/${id}`, {
|
||||
...authHeaders(),
|
||||
body: {
|
||||
dayOfWeek: parseInt(editSchedDayOfWeek(), 10),
|
||||
startsLocal: editSchedStartsLocal(),
|
||||
locationId: editSchedLocationId(),
|
||||
active: editSchedActive(),
|
||||
applyToFuture: editSchedApplyFuture(),
|
||||
},
|
||||
});
|
||||
setEditingSchedId(null);
|
||||
void refetchSchedules();
|
||||
void refetchSessions();
|
||||
} catch {
|
||||
data.setDemoNotice(cs() ? "Uložení selhalo." : "Save failed.");
|
||||
} finally {
|
||||
setSchedEditSaving(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; }
|
||||
@@ -713,19 +756,68 @@ function ZonesPage() {
|
||||
<div class="divide-y divide-border/60">
|
||||
<For each={classSchedules() ?? []}>
|
||||
{(sch: any) => (
|
||||
<div class="flex items-center justify-between p-4 px-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-accent-subtle/50 flex items-center justify-center text-accent"><CalendarDaysIcon /></div>
|
||||
<div>
|
||||
<p class="font-medium text-ink">{sch.templateTitle}</p>
|
||||
<p class="text-sm text-ink-muted">
|
||||
{`${weekdayName(sch.dayOfWeek)} ${String(sch.startsLocal).slice(0, 5)}`}
|
||||
{sch.locationName ? ` • ${sch.locationName}` : ""}
|
||||
</p>
|
||||
<Show
|
||||
when={editingSchedId() === sch.id}
|
||||
fallback={
|
||||
<div class="flex items-center justify-between p-4 px-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-accent-subtle/50 flex items-center justify-center text-accent"><CalendarDaysIcon /></div>
|
||||
<div>
|
||||
<p class="font-medium text-ink">{sch.templateTitle}</p>
|
||||
<p class="text-sm text-ink-muted">
|
||||
{`${weekdayName(sch.dayOfWeek)} ${String(sch.startsLocal).slice(0, 5)}`}
|
||||
{sch.locationName ? ` • ${sch.locationName}` : ""}
|
||||
{!sch.active ? ` • ${data.i18n.t("dashboard.class.inactive")}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<button onClick={() => startEditSchedule(sch)} 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.editSchedule")}><PencilIcon /></button>
|
||||
<button onClick={() => void handleDeleteSchedule(sch.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>
|
||||
}
|
||||
>
|
||||
<div class="p-4 px-6 bg-canvas-subtle/40">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<Select
|
||||
label={data.i18n.t("dashboard.class.weekday")}
|
||||
value={editSchedDayOfWeek()}
|
||||
onChange={(v) => setEditSchedDayOfWeek(v)}
|
||||
options={[1, 2, 3, 4, 5, 6, 0].map((d) => ({ value: String(d), label: weekdayName(d) }))}
|
||||
/>
|
||||
<Input type="time" label={data.i18n.t("dashboard.class.startsLocal")} value={editSchedStartsLocal()} onInput={(e) => setEditSchedStartsLocal(e.currentTarget.value)} />
|
||||
<Select
|
||||
label={data.i18n.t("dashboard.class.location")}
|
||||
value={editSchedLocationId()}
|
||||
onChange={(v) => setEditSchedLocationId(v)}
|
||||
options={[
|
||||
{ value: "", label: data.i18n.t("dashboard.class.noLocation") },
|
||||
...resolvedLocations().map((l: any) => ({ value: l.id, label: l.name })),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<label class="flex items-start gap-2 mt-4 cursor-pointer">
|
||||
<input type="checkbox" checked={editSchedActive()} onChange={(e) => setEditSchedActive(e.currentTarget.checked)} class="mt-0.5 accent-accent" />
|
||||
<span class="text-sm font-medium text-ink">{data.i18n.t("dashboard.class.ruleActive")}</span>
|
||||
</label>
|
||||
<label class="flex items-start gap-2 mt-3 cursor-pointer">
|
||||
<input type="checkbox" checked={editSchedApplyFuture()} onChange={(e) => setEditSchedApplyFuture(e.currentTarget.checked)} class="mt-0.5 accent-accent" />
|
||||
<span>
|
||||
<span class="text-sm font-medium text-ink">{data.i18n.t("dashboard.class.applyToFuture")}</span>
|
||||
<span class="block text-xs text-ink-muted">{data.i18n.t("dashboard.class.applyToFutureHint")}</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="flex items-center gap-2 mt-4">
|
||||
<button onClick={() => void handleSaveSchedule()} disabled={schedEditSaving() || !editSchedStartsLocal()} class="btn-primary text-sm disabled:opacity-60">
|
||||
<CheckIcon /> {schedEditSaving() ? data.i18n.t("dashboard.saving") : data.i18n.t("dashboard.saveChanges")}
|
||||
</button>
|
||||
<button onClick={() => setEditingSchedId(null)} class="btn-secondary text-sm">
|
||||
<XIcon /> {data.i18n.t("dashboard.cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => void handleDeleteSchedule(sch.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>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user