import { A, useParams } from "@solidjs/router"; import { For, Show, createMemo, createResource, createSignal } from "solid-js"; import { apiClient } from "../lib/api-client"; import { useI18n } from "../providers/i18n-provider"; import type { components } from "@bookra/api-client/generated/types"; import { BookraCharacter } from "../components/bookra-character"; import { Button, Card, CardHeader, CardTitle, CardDescription, CardContent, Badge, Input, SkeletonCard, Textarea } from "../components/ui"; type PublicService = components["schemas"]["PublicService"]; type PublicStaff = components["schemas"]["PublicStaff"]; export function PublicBookingRoute() { const params = useParams(); const i18n = useI18n(); const tenantSlug = () => params.tenantSlug; const [bookingResult, setBookingResult] = createSignal(null); const [bookingError, setBookingError] = createSignal(null); const [submittingSlot, setSubmittingSlot] = createSignal(null); const [customerName, setCustomerName] = createSignal(""); const [customerEmail, setCustomerEmail] = createSignal(""); const [customerPhone, setCustomerPhone] = createSignal(""); const [notes, setNotes] = createSignal(""); const [intakeAnswers, setIntakeAnswers] = createSignal>({}); const [highlightContact, setHighlightContact] = createSignal(false); const [selectedServiceId, setSelectedServiceId] = createSignal(null); const [selectedStaffId, setSelectedStaffId] = createSignal(null); const [selectedDate, setSelectedDate] = createSignal(null); let contactFormRef: HTMLDivElement | undefined; const [catalog] = createResource(() => { const slug = tenantSlug(); if (!slug) return null; return apiClient.GET("/v1/public/tenants/{tenantSlug}/catalog", { params: { path: { tenantSlug: slug } }, }); }); const [availability, { refetch }] = createResource(() => { const slug = tenantSlug(); if (!slug) return null; return apiClient.GET("/v1/public/tenants/{tenantSlug}/availability", { params: { path: { tenantSlug: slug, }, }, }); }); const dayParams = createMemo(() => { const slug = tenantSlug(); const serviceId = selectedServiceId(); const date = selectedDate(); if (!slug || !serviceId || !date) return null; return { slug, serviceId, date, staffId: selectedStaffId() }; }); const [dayAvailability, { refetch: refetchDay }] = createResource(dayParams, (source) => apiClient.GET("/v1/public/tenants/{tenantSlug}/availability", { params: { path: { tenantSlug: source.slug }, query: { serviceId: source.serviceId, date: source.date, ...(source.staffId ? { staffId: source.staffId } : {}), }, }, }), ); const services = createMemo(() => catalog.latest?.data?.services ?? []); const staff = createMemo(() => catalog.latest?.data?.staff ?? []); const intakeFields = createMemo( () => services().find((s) => s.id === selectedServiceId())?.intakeFields ?? [], ); const classSlots = createMemo(() => (availability.latest?.data?.slots ?? []).filter((slot) => slot.mode === "class")); const classDays = createMemo(() => { const groups = new Map(); for (const slot of classSlots()) { const d = new Date(slot.startsAt); const key = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`; const day = groups.get(key); if (day) day.push(slot); else groups.set(key, [slot]); } return [...groups.entries()] .sort(([, a], [, b]) => a[0].startsAt.localeCompare(b[0].startsAt)) .map(([key, slots]) => ({ key, slots, date: new Date(slots[0].startsAt) })); }); const daySlots = createMemo(() => dayAvailability.latest?.data?.slots ?? []); const hasCatalog = createMemo(() => services().length > 0); const tenantTimezone = createMemo( () => availability.latest?.data?.timezone ?? dayAvailability.latest?.data?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone, ); const days = createMemo(() => { const tz = tenantTimezone(); const locale = i18n.locale(); const items: { value: string; weekday: string; day: string; month: string }[] = []; const now = Date.now(); for (let offset = 0; offset < 14; offset++) { const date = new Date(now + offset * 86400000); items.push({ value: new Intl.DateTimeFormat("en-CA", { timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit" }).format(date), weekday: new Intl.DateTimeFormat(locale, { timeZone: tz, weekday: "short" }).format(date), day: new Intl.DateTimeFormat(locale, { timeZone: tz, day: "numeric" }).format(date), month: new Intl.DateTimeFormat(locale, { timeZone: tz, month: "short" }).format(date), }); } return items; }); const formatPrice = (priceCents: number) => { if (!priceCents) return i18n.t("booking.service.free"); return new Intl.NumberFormat(i18n.locale() === "cs" ? "cs-CZ" : "en-US", { style: "currency", currency: "CZK", maximumFractionDigits: 0, }).format(priceCents / 100); }; const pickService = (serviceId: string) => { setSelectedServiceId(serviceId); setSelectedDate(null); setBookingError(null); }; const pickStaff = (staffId: string | null) => { setSelectedStaffId(staffId); }; const bookSlot = async (slot: components["schemas"]["TimeSlot"]) => { if (!customerName().trim() || !customerEmail().trim()) { setBookingError(i18n.t("booking.customerRequired")); setHighlightContact(true); contactFormRef?.scrollIntoView({ behavior: "smooth", block: "center" }); setTimeout(() => setHighlightContact(false), 2000); return; } setSubmittingSlot(slot.startsAt); setBookingResult(null); setBookingError(null); const slug = tenantSlug(); if (!slug) { setBookingError(i18n.locale() === 'cs' ? 'Chybí identifikátor podniku' : 'Business identifier missing'); setSubmittingSlot(null); return; } const response = await apiClient.POST("/v1/public/bookings", { body: { tenantSlug: slug, bookingMode: slot.mode, serviceId: slot.serviceId ?? undefined, classSessionId: slot.classSessionId ?? undefined, staffId: slot.staffId ?? undefined, locationId: slot.locationId ?? undefined, customerName: customerName().trim(), customerEmail: customerEmail().trim(), customerPhone: customerPhone().trim() || undefined, notes: notes().trim(), intake: intakeFields().length > 0 ? intakeAnswers() : undefined, startsAt: slot.startsAt, endsAt: slot.endsAt, }, }); const payload = response as { data?: components["schemas"]["CreateBookingResponse"]; error?: unknown }; if (payload.error) { setBookingError(`${i18n.t("booking.failed")}: ${String(payload.error)}`); } else if (payload.data) { setBookingResult(`${i18n.t("booking.created")} ${payload.data.reference}`); setNotes(""); } setSubmittingSlot(null); void refetch(); void refetchDay(); }; return (
{/* Header */}

{i18n.t("booking.title")}

{i18n.t("booking.body")}

{i18n.locale() === 'cs' ? 'Podnik nenalezen' : 'Business not found'}

{i18n.locale() === 'cs' ? 'Zkontrolujte URL adresu nebo kontaktujte provozovatele.' : 'Please check the URL or contact the service provider.'}

{i18n.locale() === 'cs' ? 'Zpět na hlavní stránku' : 'Back to home'}
{/* Sidebar - Contact form - ORDER 1 on mobile, 2 on desktop */}
{ contactFormRef = el; }}> {i18n.t("booking.customer.title")} {i18n.t("booking.customer.body")} setCustomerName(event.currentTarget.value)} required value={customerName()} /> setCustomerEmail(event.currentTarget.value)} required type="email" value={customerEmail()} /> setCustomerPhone(event.currentTarget.value)} type="tel" value={customerPhone()} />