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 { PlusIcon, ScissorsIcon, Trash2Icon, EditIcon, ClockIcon } from "../../components/dashboard/icons"; import { DashboardLayout, useDashboardData } from "./layout"; import { ExportCsvButton } from "../../components/dashboard/export-csv-button"; interface IntakeDraft { key: string; label: string; type: string; required: boolean; } const intakeKey = (label: string) => label .trim() .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, "") .slice(0, 40); interface ServiceForm { name: string; durationMinutes: string; bufferBeforeMinutes: string; bufferAfterMinutes: string; price: string; intakeFields: IntakeDraft[]; } const emptyForm: ServiceForm = { name: "", durationMinutes: "60", bufferBeforeMinutes: "0", bufferAfterMinutes: "0", price: "0", intakeFields: [] }; function ServicesPage() { const data = useDashboardData(); const cs = () => data.i18n.locale() === "cs"; const [showForm, setShowForm] = createSignal(false); const [editingId, setEditingId] = createSignal(null); const [form, setForm] = createSignal({ ...emptyForm }); const [saving, setSaving] = createSignal(false); const authHeaders = () => ({ headers: { Authorization: `Bearer ${data.token()}` } }); const setField = (patch: Partial) => setForm({ ...form(), ...patch }); const [services, { refetch }] = createResource( () => data.token(), async (bearer) => { if (!bearer) return []; if (bearer.startsWith("demo.")) { return [ { id: "sv1", name: cs() ? "Dámský střih" : "Women's haircut", durationMinutes: 60, bufferBeforeMinutes: 0, bufferAfterMinutes: 15, priceCents: 89000, intakeFields: cs() ? [ { key: "zdravotni_poznamky", label: "Zdravotní poznámky nebo alergie", type: "text", required: false }, { key: "prvni_navsteva", label: "Je to vaše první návštěva?", type: "yesno", required: true }, ] : [ { key: "health_notes", label: "Health notes or allergies", type: "text", required: false }, { key: "first_visit", label: "Is this your first visit?", type: "yesno", required: true }, ], }, { id: "sv2", name: cs() ? "Pánský střih" : "Men's haircut", durationMinutes: 45, bufferBeforeMinutes: 0, bufferAfterMinutes: 10, priceCents: 59000 }, { id: "sv3", name: cs() ? "Barvení" : "Coloring", durationMinutes: 120, bufferBeforeMinutes: 10, bufferAfterMinutes: 15, priceCents: 199000 }, { id: "sv4", name: cs() ? "Konzultace" : "Consultation", durationMinutes: 30, bufferBeforeMinutes: 0, bufferAfterMinutes: 0, priceCents: 0 }, ]; } const response = await apiClient.GET("/v1/catalog/services", authHeaders()); return (response.data as any[]) ?? []; }, ); const [serviceMetrics] = createResource( () => data.token(), async (bearer) => { if (!bearer) return null; if (bearer.startsWith("demo.")) { return { services: [ { serviceId: "sv1", name: cs() ? "Dámský střih" : "Women's haircut", bookings: 9, completed: 5, cancelled: 1, revenueCents: 712000 }, { serviceId: "sv2", name: cs() ? "Pánský střih" : "Men's haircut", bookings: 6, completed: 4, cancelled: 0, revenueCents: 354000 }, { serviceId: "sv3", name: cs() ? "Barvení" : "Coloring", bookings: 3, completed: 2, cancelled: 0, revenueCents: 597000 }, { serviceId: "sv4", name: cs() ? "Konzultace" : "Consultation", bookings: 2, completed: 1, cancelled: 0, revenueCents: 0 }, ], classBookings: 4, }; } const response = await apiClient.GET("/v1/metrics/services", authHeaders()); return response.data ?? null; }, ); const resolvedServices = () => services() ?? []; const formatPrice = (priceCents: number) => { if (!priceCents) return data.i18n.t("dashboard.service.free"); return new Intl.NumberFormat(cs() ? "cs-CZ" : "en-US", { style: "currency", currency: "CZK", maximumFractionDigits: 0 }).format(priceCents / 100); }; const formatRevenue = (cents: number) => new Intl.NumberFormat(cs() ? "cs-CZ" : "en-US", { style: "currency", currency: "CZK", maximumFractionDigits: 0 }).format(cents / 100); const openCreate = () => { setEditingId(null); setForm({ ...emptyForm }); setShowForm(true); }; const openEdit = (service: any) => { setEditingId(service.id); setForm({ name: service.name, durationMinutes: String(service.durationMinutes ?? 60), bufferBeforeMinutes: String(service.bufferBeforeMinutes ?? 0), bufferAfterMinutes: String(service.bufferAfterMinutes ?? 0), price: String((service.priceCents ?? 0) / 100), intakeFields: (service.intakeFields ?? []).map((f: any) => ({ key: f.key ?? "", label: f.label ?? "", type: f.type ?? "text", required: !!f.required })), }); setShowForm(true); }; const setIntake = (i: number, patch: Partial) => { const fields = form().intakeFields.slice(); fields[i] = { ...fields[i], ...patch }; setField({ intakeFields: fields }); }; const addIntake = () => { if (form().intakeFields.length >= 8) return; setField({ intakeFields: [...form().intakeFields, { key: "", label: "", type: "text", required: false }] }); }; const removeIntake = (i: number) => { setField({ intakeFields: form().intakeFields.filter((_, j) => j !== i) }); }; const handleSave = async () => { const bearer = data.token(); const name = form().name.trim(); const duration = parseInt(form().durationMinutes, 10); if (!name || !duration || duration <= 0) return; if (!bearer || bearer.startsWith("demo.")) { data.setDemoNotice(cs() ? "V demo režimu nelze ukládat." : "Cannot save in demo mode."); return; } setSaving(true); try { const body = { name, durationMinutes: duration, bufferBeforeMinutes: Math.max(0, parseInt(form().bufferBeforeMinutes, 10) || 0), bufferAfterMinutes: Math.max(0, parseInt(form().bufferAfterMinutes, 10) || 0), priceCents: Math.max(0, Math.round((parseFloat(form().price) || 0) * 100)), intakeFields: form().intakeFields .filter((f) => f.label.trim()) .map((f) => ({ key: f.key || intakeKey(f.label), label: f.label.trim(), type: (f.type === "yesno" ? "yesno" : "text") as "text" | "yesno", required: f.required, })), }; const id = editingId(); const response = id ? await apiClient.PUT("/v1/catalog/services/{serviceID}", { ...authHeaders(), params: { path: { serviceID: id } }, body }) : await apiClient.POST("/v1/catalog/services", { ...authHeaders(), body }); if (response.error) throw new Error("save failed"); setShowForm(false); setEditingId(null); setForm({ ...emptyForm }); void refetch(); } catch { data.setDemoNotice(data.i18n.t("dashboard.service.saveFailed")); } finally { setSaving(false); } }; const handleDelete = 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; } try { await apiClient.DELETE("/v1/catalog/services/{serviceID}", { ...authHeaders(), params: { path: { serviceID: id } } }); void refetch(); } catch { data.setDemoNotice(data.i18n.t("dashboard.service.deleteFailed")); } }; return (

{data.i18n.t("dashboard.services")}

{resolvedServices().length} {data.i18n.t("dashboard.services.subtitle")}

0}>

{data.i18n.t("dashboard.services.performance")}

{data.i18n.t("dashboard.staff.performanceWindow")}
{(m: any) => (

{m.name}

{data.i18n.t("dashboard.bookings")}

{m.bookings}

{data.i18n.t("dashboard.completed")}

{m.completed}

{data.i18n.t("dashboard.cancelled")}

{m.cancelled}

{data.i18n.t("dashboard.staff.revenue")}

{formatRevenue(m.revenueCents)}

)}
0}>

{serviceMetrics()!.classBookings} {data.i18n.t("dashboard.services.classBookings")}

{editingId() ? data.i18n.t("dashboard.service.edit") : data.i18n.t("dashboard.service.add")}

setField({ name: e.currentTarget.value })} placeholder={data.i18n.t("dashboard.service.namePlaceholder")} />
setField({ durationMinutes: e.currentTarget.value })} /> setField({ price: e.currentTarget.value })} /> setField({ bufferBeforeMinutes: e.currentTarget.value })} /> setField({ bufferAfterMinutes: e.currentTarget.value })} />

{data.i18n.t("dashboard.service.intake")}

0} fallback={

{data.i18n.t("dashboard.service.intakeHint")}

}>
{(field, i) => (
setIntake(i(), { label: e.currentTarget.value })} placeholder={data.i18n.t("dashboard.service.intakeLabel")} />
setIntake(i(), { required: e.currentTarget.checked })} class="rounded accent-accent" /> {data.i18n.t("dashboard.service.intakeRequired")}
)}
{cs() ? "Načítání…" : "Loading…"}
}> 0} fallback={

{data.i18n.t("dashboard.service.noServices")}

{data.i18n.t("dashboard.service.noServicesDesc")}

}>
{(service: any) => (

{service.name}

{service.durationMinutes} {data.i18n.t("dashboard.service.min")} 0 || service.bufferAfterMinutes > 0}> • +{service.bufferBeforeMinutes ?? 0}/{service.bufferAfterMinutes ?? 0} {data.i18n.t("dashboard.service.min")} {cs() ? "rezerva" : "buffer"}

)}
); } export default function ServicesRoute() { return ( ); }