import { Show, createResource, createSignal, createMemo, createEffect } from "solid-js"; import { A, useSearchParams } from "@solidjs/router"; import { apiClient } from "../lib/api-client"; import { getPaddle, paddleConfigured } from "../lib/paddle"; import { useAuth } from "../providers/auth-provider"; import { useI18n } from "../providers/i18n-provider"; import { useTheme } from "../providers/theme-provider"; import type { JSX } from "solid-js"; import { BookraCharacter } from "../components/bookra-character"; import { WidgetBuilder } from "../components/widget-builder"; import { IntegrationModal } from "../components/integration-modal"; import { Select } from "../components/ui/select"; import { Input } from "../components/ui/input"; import { Textarea } from "../components/ui/textarea"; // ============================================ // TYPES & INTERFACES // ============================================ type Section = "overview" | "bookings" | "customers" | "zones" | "billing" | "settings"; type BookingStatus = "confirmed" | "pending" | "cancelled"; interface KpiData { label: string; value: number | string; change?: string; trend?: "up" | "down" | "neutral"; icon: () => JSX.Element; } // ============================================ // SOPHISTICATED ICONS // ============================================ const LayoutDashboardIcon = () => ( ); const CalendarDaysIcon = () => ( ); const CreditCardIcon = () => ( ); const Settings2Icon = () => ( ); const LogOutIcon = () => ( ); const MenuIcon = () => ( ); const XIcon = () => ( ); const TrendingUpIcon = () => ( ); const TrendingDownIcon = () => ( ); const ClockIcon = () => ( ); const CheckCircleIcon = () => ( ); const AlertCircleIcon = () => ( ); const MoreHorizontalIcon = () => ( ); const ChevronLeftIcon = () => ( ); const ChevronRightIcon = () => ( ); const SparklesIcon = () => ( ); const BellIcon = () => ( ); const PlusIcon = () => ( ); const UsersIcon = () => ( ); const UserCircleIcon = () => ( ); const MapPinIcon = () => ( ); const BanIcon = () => ( ); const ClockOffIcon = () => ( ); const SunIcon = () => ( ); const MoonIcon = () => ( ); const PaletteIcon = () => ( ); const MailIcon = () => ( ); const XCircleIcon = () => ( ); const CalendarIcon = () => ( ); // ============================================ // DEMO DATA GENERATOR // ============================================ const createDemoData = (i18n: any) => ({ summary: { tenantSlug: "demo-studio", tenantName: "Serenity Wellness Studio", publicBookingUrl: "http://localhost:3000/book/demo-studio", kpis: [ { label: i18n.locale() === 'cs' ? 'Celkem rezervací' : 'Total Bookings', value: 42, change: "+12%", trend: "up" }, { label: i18n.locale() === 'cs' ? 'Zrušených' : 'Cancelled', value: 3, change: "-5%", trend: "down" }, { label: i18n.locale() === 'cs' ? 'Dokončených' : 'Completed', value: 38, change: "+8%", trend: "up" }, { label: i18n.locale() === 'cs' ? 'Noví klienti' : 'New Clients', value: 12, change: "+24%", trend: "up" }, ], upcomingBookings: [ { id: "1", customerName: "Alice Johnson", service: "Yoga Flow Class", status: "confirmed" as BookingStatus, startsAt: new Date(Date.now() + 86400000).toISOString(), reference: "BK-001", avatar: "AJ", duration: "60 min" }, { id: "2", customerName: "Bob Smith", service: "Deep Tissue Massage", status: "pending" as BookingStatus, startsAt: new Date(Date.now() + 172800000).toISOString(), reference: "BK-002", avatar: "BS", duration: "90 min" }, { id: "3", customerName: "Carol White", service: "Personal Training", status: "confirmed" as BookingStatus, startsAt: new Date(Date.now() + 259200000).toISOString(), reference: "BK-003", avatar: "CW", duration: "45 min" }, { id: "4", customerName: "David Brown", service: "Nutrition Consultation", status: "cancelled" as BookingStatus, startsAt: new Date(Date.now() + 345600000).toISOString(), reference: "BK-004", avatar: "DB", duration: "30 min" }, { id: "5", customerName: "Emma Wilson", service: "Meditation Session", status: "confirmed" as BookingStatus, startsAt: new Date(Date.now() + 432000000).toISOString(), reference: "BK-005", avatar: "EW", duration: "45 min" }, ], recentActivity: [ { action: "New booking received", detail: "Alice Johnson • Yoga Flow Class", time: "2 min ago", type: "booking" }, { action: "Booking rescheduled", detail: "David Brown moved to Friday 2pm", time: "1 hour ago", type: "reschedule" }, { action: "New client registered", detail: "Emma Wilson joined", time: "3 hours ago", type: "client" }, { action: "Reminder sent", detail: "Tomorrow's session confirmed", time: "6 hours ago", type: "reminder" }, { action: "Booking cancelled", detail: "Carol White • Personal Training", time: "8 hours ago", type: "cancel" }, ], allBookings: [ { id: "1", customerName: "Alice Johnson", customerEmail: "alice@example.com", service: "Yoga Flow Class", status: "confirmed", startsAt: new Date(Date.now() + 86400000).toISOString(), endsAt: new Date(Date.now() + 86400000 + 3600000).toISOString(), reference: "BK-001", avatar: "AJ", duration: "60 min", location: "Main Studio", createdAt: new Date(Date.now() - 86400000).toISOString() }, { id: "2", customerName: "Bob Smith", customerEmail: "bob@example.com", service: "Deep Tissue Massage", status: "pending", startsAt: new Date(Date.now() + 172800000).toISOString(), endsAt: new Date(Date.now() + 172800000 + 5400000).toISOString(), reference: "BK-002", avatar: "BS", duration: "90 min", location: "Treatment Room A", createdAt: new Date(Date.now() - 172800000).toISOString() }, { id: "3", customerName: "Carol White", customerEmail: "carol@example.com", service: "Personal Training", status: "confirmed", startsAt: new Date(Date.now() + 259200000).toISOString(), endsAt: new Date(Date.now() + 259200000 + 2700000).toISOString(), reference: "BK-003", avatar: "CW", duration: "45 min", location: "Main Studio", createdAt: new Date(Date.now() - 259200000).toISOString() }, { id: "4", customerName: "David Brown", customerEmail: "david@example.com", service: "Nutrition Consultation", status: "cancelled", startsAt: new Date(Date.now() + 345600000).toISOString(), endsAt: new Date(Date.now() + 345600000 + 1800000).toISOString(), reference: "BK-004", avatar: "DB", duration: "30 min", location: "Treatment Room B", createdAt: new Date(Date.now() - 345600000).toISOString() }, { id: "5", customerName: "Emma Wilson", customerEmail: "emma@example.com", service: "Meditation Session", status: "confirmed", startsAt: new Date(Date.now() + 432000000).toISOString(), endsAt: new Date(Date.now() + 432000000 + 2700000).toISOString(), reference: "BK-005", avatar: "EW", duration: "45 min", location: "Group Hall", createdAt: new Date(Date.now() - 432000000).toISOString() }, { id: "6", customerName: "Frank Miller", customerEmail: "frank@example.com", service: "Yoga Flow Class", status: "completed", startsAt: new Date(Date.now() - 86400000).toISOString(), endsAt: new Date(Date.now() - 86400000 + 3600000).toISOString(), reference: "BK-006", avatar: "FM", duration: "60 min", location: "Main Studio", createdAt: new Date(Date.now() - 172800000).toISOString() }, ], }, bootstrap: { tenantId: "demo-tenant-id", tenantName: "Serenity Wellness Studio", preset: "studio", locale: i18n.locale(), timezone: "Europe/Prague", currentUser: { role: "admin", name: "Demo User", email: "demo@bookra.io" }, brand: { primaryColor: "#a65c3e", name: "Serenity Wellness Studio" }, }, billing: { provider: "paddle", planCode: "pro", status: "active", nextBilling: "2024-02-15", amount: "$49", checkoutUrlAvailable: true, syncAvailable: true, portalAvailable: true, entitlements: { maxLocations: 3, maxStaff: 10, emailReminders: true, customBranding: true, analytics: true }, }, }); const getInitials = (name?: string) => (name ?? "User") .split(" ") .filter(Boolean) .slice(0, 2) .map((part) => part[0]?.toUpperCase() ?? "") .join("") || "U"; const getBookingDuration = (startsAt?: string, endsAt?: string) => { if (!startsAt || !endsAt) return "60 min"; const start = new Date(startsAt).getTime(); const end = new Date(endsAt).getTime(); if (Number.isNaN(start) || Number.isNaN(end) || end <= start) return "60 min"; return `${Math.round((end - start) / 60000)} min`; }; // ============================================ // ANIMATED CALENDAR COMPONENT // ============================================ function CalendarView(props: { bookings: any[]; locale?: string }) { const [currentDate, setCurrentDate] = createSignal(new Date()); const [selectedDay, setSelectedDay] = createSignal(null); const [isAnimating, setIsAnimating] = createSignal(false); const isCs = () => props.locale === 'cs'; const weekDays = isCs() ? ["Ne", "Po", "Út", "St", "Čt", "Pá", "So"] : ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const calendarDays = createMemo(() => { const date = currentDate(); const year = date.getFullYear(); const month = date.getMonth(); const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const days: Array<{ day: number | null; bookings: any[]; isToday: boolean }> = []; for (let i = 0; i < firstDay; i++) days.push({ day: null, bookings: [], isToday: false }); const today = new Date(); for (let day = 1; day <= daysInMonth; day++) { const dayBookings = props.bookings.filter(b => { const bookingDate = new Date(b.startsAt); return bookingDate.getDate() === day && bookingDate.getMonth() === month; }); const isToday = today.getDate() === day && today.getMonth() === month && today.getFullYear() === year; days.push({ day, bookings: dayBookings, isToday }); } return days; }); const changeMonth = (direction: number) => { setIsAnimating(true); setTimeout(() => { setCurrentDate(new Date(currentDate().getFullYear(), currentDate().getMonth() + direction, 1)); setIsAnimating(false); }, 150); }; const prevMonth = () => changeMonth(-1); const nextMonth = () => changeMonth(1); return (

{new Intl.DateTimeFormat(isCs() ? 'cs-CZ' : 'en-US', { month: 'long', year: 'numeric' }).format(currentDate())}

{props.bookings.length} {isCs() ? 'rezervací tento měsíc' : 'bookings this month'}

{weekDays.map(day => (
{day}
))}
{calendarDays().map(({ day, bookings, isToday }, index) => (
0 && !isToday ? 'bg-gradient-to-br from-accent-subtle/50 to-canvas' : ''} `} style={{ "animation-delay": `${index * 20}ms` }} classList={{ "animate-fade-in": true }} onClick={() => day && setSelectedDay(selectedDay() === day ? null : day)} > {day && ( <> {day} {bookings.length > 0 && (
{bookings.slice(0, 4).map((booking: any, i: number) => (
))} {bookings.length > 4 && ( +{bookings.length - 4} )}
)} )}
))}
{/* Selected Day Preview */}
{new Intl.DateTimeFormat(isCs() ? 'cs-CZ' : 'en-US', { month: 'long' }).format(currentDate())} {selectedDay()}
{calendarDays().find(d => d.day === selectedDay())?.bookings.map((booking: any) => (

{booking.customerName}

{booking.service} • {booking.duration}

{new Date(booking.startsAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
))}
); } // ============================================ // KPI CARD COMPONENT // ============================================ function KpiCard(props: { kpi: any; index: number }) { const [isHovered, setIsHovered] = createSignal(false); const trend = () => props.kpi.trend ?? "neutral"; const trendClass = () => { switch (trend()) { case "up": return "bg-[hsl(var(--success-soft))] text-[hsl(var(--success))]"; case "down": return "bg-[hsl(var(--error-soft))] text-[hsl(var(--error))]"; default: return "bg-canvas-subtle text-ink-muted"; } }; const sparklineClass = () => { switch (trend()) { case "up": return "bg-[hsl(var(--success-muted))] group-hover:bg-[hsl(var(--success-subtle))]"; case "down": return "bg-[hsl(var(--error-muted))] group-hover:bg-[hsl(var(--error-subtle))]"; default: return "bg-canvas-muted group-hover:bg-border"; } }; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {/* Background gradient on hover */}

{props.kpi.label}

{props.kpi.value}

}>}> {props.kpi.change ?? "Live"}
{/* Mini sparkline visualization */}
{[40, 65, 45, 80, 55, 70, 90, 60, 75, 85].map((height, i) => (
))}
); } // ============================================ // ACTIVITY TIMELINE COMPONENT // ============================================ function ActivityTimeline(props: { activities: any[] }) { const getIcon = (type: string) => { switch (type) { case 'booking': return
; case 'cancel': return
; case 'client': return
; case 'reschedule': return
; case 'reminder': return
; default: return
; } }; return (

Recent Activity

{props.activities.map((activity: any, index: number) => (
{/* Timeline line */} {index < props.activities.length - 1 && (
)}
{getIcon(activity.type)}

{activity.action}

{activity.detail}

{activity.time}
))}
); } // ============================================ // MAIN DASHBOARD ROUTE // ============================================ export function DashboardRoute() { const i18n = useI18n(); const auth = useAuth(); const theme = useTheme(); const [searchParams] = useSearchParams(); const [activeSection, setActiveSection] = createSignal
("overview"); const [isMobileMenuOpen, setIsMobileMenuOpen] = createSignal(false); const [isPageTransitioning, setIsPageTransitioning] = createSignal(false); const [billingNotice, setBillingNotice] = createSignal(null); const [billingError, setBillingError] = createSignal(null); const [billingAction, setBillingAction] = createSignal<"checkout" | "refresh" | "portal" | null>(null); const [handledBillingState, setHandledBillingState] = createSignal(null); const [showIntegrationModal, setShowIntegrationModal] = createSignal(false); const [token] = createResource(() => auth.session()?.session?.id, () => auth.getToken()); const isDemoMode = () => token()?.startsWith("demo.") ?? false; const demoData = createMemo(() => createDemoData(i18n)); const [summary] = createResource(token, async (bearer) => { if (!bearer) return null; if (bearer.startsWith("demo.")) return demoData().summary; const response = await apiClient.GET("/v1/dashboard/summary", { headers: { Authorization: `Bearer ${bearer}` } }); return response.data ?? null; }); const [bootstrap] = createResource(token, async (bearer) => { if (!bearer) return null; if (bearer.startsWith("demo.")) return demoData().bootstrap; const response = await apiClient.GET("/v1/tenants/bootstrap", { headers: { Authorization: `Bearer ${bearer}` } }); return response.data ?? null; }); const [billing, { refetch: refetchBilling }] = createResource(token, async (bearer) => { if (!bearer) return null; if (bearer.startsWith("demo.")) return demoData().billing; const response = await apiClient.GET("/v1/billing/subscription", { headers: { Authorization: `Bearer ${bearer}` } }); return response.data ?? null; }); const resolvedSummary = () => (summary.latest as any) ?? (isDemoMode() ? demoData().summary : undefined); const resolvedBootstrap = () => (bootstrap.latest as any) ?? (isDemoMode() ? demoData().bootstrap : undefined); const resolvedBilling = () => (billing.latest as any) ?? (isDemoMode() ? demoData().billing : undefined); const normalizedKpis = createMemo(() => (resolvedSummary()?.kpis ?? []).map((kpi: any) => ({ ...kpi, trend: kpi.trend ?? "neutral", change: kpi.change ?? "Live", }))); const normalizedUpcomingBookings = createMemo(() => (resolvedSummary()?.upcomingBookings ?? []).map((booking: any) => ({ ...booking, avatar: booking.avatar ?? getInitials(booking.customerName), service: booking.service ?? booking.label ?? booking.reference ?? (i18n.locale() === "cs" ? "Rezervace" : "Booking"), duration: booking.duration ?? getBookingDuration(booking.startsAt, booking.endsAt), }))); const normalizedRecentActivity = createMemo(() => { if (resolvedSummary()?.recentActivity?.length) { return resolvedSummary().recentActivity; } return normalizedUpcomingBookings().slice(0, 5).map((booking: any) => ({ action: i18n.locale() === "cs" ? "Nadcházející rezervace" : "Upcoming booking", detail: `${booking.customerName} • ${booking.service}`, time: new Date(booking.startsAt).toLocaleDateString(undefined, { month: "short", day: "numeric" }), type: "booking", })); }); const billingPriceLabel = createMemo(() => { const current = resolvedBilling(); if (!current) return "--"; if (current.amount) return current.amount; const displayPrice = current.displayPrices?.find((price: any) => price.currency === current.currency) ?? current.displayPrices?.[0]; return displayPrice?.formatted ?? "--"; }); const nextBillingLabel = createMemo(() => { const current = resolvedBilling(); if (!current) return "--"; if (current.nextBilling) return current.nextBilling; if (!current.currentPeriodEnd) return i18n.locale() === "cs" ? "Není naplánováno" : "Not scheduled"; return new Date(current.currentPeriodEnd).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); }); const paymentMethodLabel = createMemo(() => { const current = resolvedBilling(); if (!current) return "--"; if (current.paymentMethodBrand && current.paymentMethodLast4) { return `${current.paymentMethodBrand} •••• ${current.paymentMethodLast4}`; } return i18n.locale() === "cs" ? "Není k dispozici" : "Not available"; }); const hasTenant = () => Boolean(bootstrap.latest?.tenantId) || isDemoMode(); const isDashboardReady = () => Boolean(token() && hasTenant() && (summary.latest || isDemoMode()) && (billing.latest || isDemoMode())); const refreshBilling = async (silent = false) => { const bearer = token(); if (!bearer) return; if (bearer.startsWith("demo.")) { if (!silent) setBillingNotice(i18n.locale() === "cs" ? "Demo předplatné je statické." : "Demo subscription is static."); return; } setBillingAction("refresh"); if (!silent) { setBillingError(null); setBillingNotice(null); } const response = await apiClient.POST("/v1/billing/refresh", { headers: { Authorization: `Bearer ${bearer}` }, }); setBillingAction(null); if (response.error) { setBillingError(i18n.locale() === "cs" ? "Obnovení předplatného selhalo." : "Billing refresh failed."); return; } await refetchBilling(); setBillingNotice(i18n.locale() === "cs" ? "Předplatné bylo obnoveno." : "Billing state refreshed."); }; const openCheckout = async () => { const bearer = token(); if (!bearer) return; if (bearer.startsWith("demo.")) { setBillingNotice(i18n.locale() === "cs" ? "Checkout není v demo režimu dostupný." : "Checkout is unavailable in demo mode."); return; } if (!paddleConfigured()) { setBillingError(i18n.locale() === "cs" ? "Paddle client token není nastaven." : "Paddle client token is not configured."); return; } setBillingAction("checkout"); setBillingError(null); setBillingNotice(null); const current = resolvedBilling(); const response = await apiClient.POST("/v1/billing/checkout", { headers: { Authorization: `Bearer ${bearer}` }, body: { planCode: current.planCode, currency: current.currency, }, }); setBillingAction(null); if (response.error || !response.data?.priceId) { setBillingError(i18n.locale() === "cs" ? "Paddle checkout není připraven." : "Paddle checkout is not ready."); return; } const paddle = await getPaddle(); if (!paddle) { setBillingError(i18n.locale() === "cs" ? "Paddle se nepodařilo inicializovat." : "Paddle failed to initialize."); return; } paddle.Checkout.open({ items: [{ priceId: response.data.priceId, quantity: 1 }], customer: response.data.customerId ? { id: response.data.customerId } : response.data.customerEmail ? { email: response.data.customerEmail } : undefined, customData: response.data.customData, settings: { displayMode: "overlay", successUrl: response.data.successRedirectUrl, }, }); }; const openBillingPortal = async () => { const bearer = token(); if (!bearer) return; if (bearer.startsWith("demo.")) { setBillingNotice(i18n.locale() === "cs" ? "Portál není v demo režimu dostupný." : "Portal is unavailable in demo mode."); return; } setBillingAction("portal"); setBillingError(null); setBillingNotice(null); const response = await apiClient.POST("/v1/billing/portal", { headers: { Authorization: `Bearer ${bearer}` }, }); setBillingAction(null); if (response.error || !response.data?.url) { setBillingError(i18n.locale() === "cs" ? "Paddle portál není připraven." : "Paddle portal is not ready."); return; } const portalUrl = response.data.url; if (!portalUrl.startsWith("https://")) { setBillingError(i18n.locale() === "cs" ? "Neplatný portál URL." : "Invalid portal URL."); return; } window.location.href = portalUrl; }; createEffect(() => { const billingState = Array.isArray(searchParams.billing) ? searchParams.billing[0] : searchParams.billing; const bearer = token(); if (!billingState || billingState === handledBillingState() || !bearer) return; setHandledBillingState(billingState); setActiveSection("billing"); if (billingState === "success") { void refreshBilling(); return; } if (billingState === "cancelled") { setBillingError(null); setBillingNotice(i18n.locale() === "cs" ? "Checkout byl zrušen." : "Checkout was cancelled."); } }); // Smooth page transition const changeSection = (section: Section) => { if (section === activeSection()) return; setIsPageTransitioning(true); setTimeout(() => { setActiveSection(section); setIsPageTransitioning(false); }, 150); }; const navItems = [ { id: "overview" as Section, label: i18n.locale() === 'cs' ? 'Prehled' : 'Overview', icon: LayoutDashboardIcon }, { id: "bookings" as Section, label: i18n.locale() === 'cs' ? 'Rezervace' : 'Bookings', icon: CalendarDaysIcon }, { id: "customers" as Section, label: i18n.locale() === 'cs' ? 'Zakaznici' : 'Customers', icon: UserCircleIcon }, { id: "zones" as Section, label: i18n.locale() === 'cs' ? 'Zony a dostupnost' : 'Zones & Availability', icon: MapPinIcon }, { id: "billing" as Section, label: i18n.locale() === 'cs' ? 'Platby' : 'Billing', icon: CreditCardIcon }, { id: "settings" as Section, label: i18n.locale() === 'cs' ? 'Nastaveni' : 'Settings', icon: Settings2Icon }, ]; // ========================================== // PAGE COMPONENTS // ========================================== const OverviewPage = () => (
{/* Header */}

{i18n.locale() === 'cs' ? 'Dobrý den,' : 'Welcome back,'} {auth.session()?.user?.name}

{i18n.locale() === 'cs' ? `Přehled pro ${resolvedBootstrap().tenantName}` : `Overview for ${resolvedBootstrap().tenantName}`}

{new Date().toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' })}
{/* KPI Cards */}
{normalizedKpis().map((kpi: any, index: number) => ( ))}
{/* Calendar + Activity */}
{/* Upcoming Bookings */}

{i18n.locale() === 'cs' ? 'Nadcházející rezervace' : 'Upcoming Bookings'}

{normalizedUpcomingBookings().length} total

{normalizedUpcomingBookings().slice(0, 3).map((booking: any, index: number) => (
{booking.avatar}

{booking.customerName}

{booking.service}

{new Date(booking.startsAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}

{booking.status}
))}
); const BookingsPage = () => { const [filterStatus, setFilterStatus] = createSignal<"all" | "confirmed" | "pending" | "cancelled" | "completed">("all"); const [filterDateRange, setFilterDateRange] = createSignal<"all" | "today" | "week" | "month">("all"); const [searchQuery, setSearchQuery] = createSignal(""); const [selectedBooking, setSelectedBooking] = createSignal(null); const [showBookingDetail, setShowBookingDetail] = createSignal(false); const [showNewBooking, setShowNewBooking] = createSignal(false); const [editingBooking, setEditingBooking] = createSignal(false); // Booking form state const [newBookingCustomer, setNewBookingCustomer] = createSignal(""); const [newBookingEmail, setNewBookingEmail] = createSignal(""); const [newBookingService, setNewBookingService] = createSignal(""); const [newBookingDate, setNewBookingDate] = createSignal(""); const [newBookingTime, setNewBookingTime] = createSignal(""); const [newBookingLocation, setNewBookingLocation] = createSignal(""); const [newBookingNotes, setNewBookingNotes] = createSignal(""); // Fetch all bookings const [allBookings, { refetch: refetchBookings }] = createResource(token, async (bearer) => { if (!bearer || bearer.startsWith("demo.")) return demoData().summary.allBookings; const response = await (apiClient as any).GET("/v1/catalog/bookings", { headers: { Authorization: `Bearer ${bearer}` } }); return response.data ?? []; }); const resolvedAllBookings = () => (allBookings.latest as any[]) ?? demoData().summary.allBookings ?? []; // Filter bookings const filteredBookings = createMemo(() => { let bookings = resolvedAllBookings(); // Status filter if (filterStatus() !== "all") { bookings = bookings.filter((b: any) => b.status === filterStatus()); } // Date range filter const now = new Date(); if (filterDateRange() === "today") { bookings = bookings.filter((b: any) => { const date = new Date(b.startsAt); return date.toDateString() === now.toDateString(); }); } else if (filterDateRange() === "week") { const weekEnd = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); bookings = bookings.filter((b: any) => { const date = new Date(b.startsAt); return date >= now && date <= weekEnd; }); } else if (filterDateRange() === "month") { const monthEnd = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); bookings = bookings.filter((b: any) => { const date = new Date(b.startsAt); return date >= now && date <= monthEnd; }); } // Search filter if (searchQuery().trim()) { const query = searchQuery().toLowerCase(); bookings = bookings.filter((b: any) => b.customerName?.toLowerCase().includes(query) || b.customerEmail?.toLowerCase().includes(query) || b.service?.toLowerCase().includes(query) || b.reference?.toLowerCase().includes(query) ); } // Sort by date, newest first return bookings.sort((a: any, b: any) => new Date(b.startsAt).getTime() - new Date(a.startsAt).getTime()); }); // Stats const bookingStats = createMemo(() => { const bookings = resolvedAllBookings(); return { total: bookings.length, confirmed: bookings.filter((b: any) => b.status === "confirmed").length, pending: bookings.filter((b: any) => b.status === "pending").length, cancelled: bookings.filter((b: any) => b.status === "cancelled").length, completed: bookings.filter((b: any) => b.status === "completed").length, }; }); const handleCreateBooking = async () => { const bearer = token(); if (!bearer || bearer.startsWith("demo.")) { setShowNewBooking(false); return; } const dateTime = new Date(`${newBookingDate()}T${newBookingTime()}`); await (apiClient as any).POST("/v1/catalog/bookings", { headers: { Authorization: `Bearer ${bearer}` }, body: { customerName: newBookingCustomer(), customerEmail: newBookingEmail(), service: newBookingService(), locationId: newBookingLocation(), startsAt: dateTime.toISOString(), endsAt: new Date(dateTime.getTime() + 60 * 60 * 1000).toISOString(), notes: newBookingNotes(), status: "confirmed" } }); // Reset form setNewBookingCustomer(""); setNewBookingEmail(""); setNewBookingService(""); setNewBookingDate(""); setNewBookingTime(""); setNewBookingLocation(""); setNewBookingNotes(""); setShowNewBooking(false); void refetchBookings(); }; const handleUpdateBooking = async () => { const bearer = token(); const booking = selectedBooking(); if (!bearer || !booking || bearer.startsWith("demo.")) { setEditingBooking(false); setShowBookingDetail(false); return; } await (apiClient as any).PUT(`/v1/catalog/bookings/${booking.id}`, { headers: { Authorization: `Bearer ${bearer}` }, body: { customerName: newBookingCustomer(), customerEmail: newBookingEmail(), service: newBookingService(), locationId: newBookingLocation(), notes: newBookingNotes() } }); setEditingBooking(false); setShowBookingDetail(false); setSelectedBooking(null); void refetchBookings(); }; const handleCancelBooking = async (bookingId: string) => { const bearer = token(); if (!bearer || bearer.startsWith("demo.")) return; if (!confirm(i18n.locale() === 'cs' ? 'Opravdu chcete zrušit tuto rezervaci?' : 'Are you sure you want to cancel this booking?')) return; await (apiClient as any).POST(`/v1/catalog/bookings/${bookingId}/cancel`, { headers: { Authorization: `Bearer ${bearer}` } }); void refetchBookings(); }; const handleRescheduleBooking = async (bookingId: string, newDate: string, newTime: string) => { const bearer = token(); if (!bearer || bearer.startsWith("demo.")) return; const newDateTime = new Date(`${newDate}T${newTime}`); await (apiClient as any).POST(`/v1/catalog/bookings/${bookingId}/reschedule`, { headers: { Authorization: `Bearer ${bearer}` }, body: { startsAt: newDateTime.toISOString(), endsAt: new Date(newDateTime.getTime() + 60 * 60 * 1000).toISOString() } }); void refetchBookings(); }; const openBookingDetail = (booking: any) => { setSelectedBooking(booking); setNewBookingCustomer(booking.customerName); setNewBookingEmail(booking.customerEmail); setNewBookingService(booking.service); setNewBookingLocation(booking.location || ""); setNewBookingNotes(booking.notes || ""); setShowBookingDetail(true); setEditingBooking(false); }; const statusColors: Record = { confirmed: "bg-[hsl(var(--success-soft))] text-[hsl(var(--success))]", pending: "bg-[hsl(var(--warning-soft))] text-[hsl(var(--warning))]", cancelled: "bg-[hsl(var(--error-soft))] text-[hsl(var(--error))]", completed: "bg-canvas-muted text-ink-muted" }; const statusLabels: Record = { confirmed: i18n.locale() === 'cs' ? 'Potvrzeno' : 'Confirmed', pending: i18n.locale() === 'cs' ? 'Čeká' : 'Pending', cancelled: i18n.locale() === 'cs' ? 'Zrušeno' : 'Cancelled', completed: i18n.locale() === 'cs' ? 'Dokončeno' : 'Completed' }; return (
{/* Header */}

{i18n.locale() === 'cs' ? 'Správa rezervací' : 'Booking Management'}

{bookingStats().total} {i18n.locale() === 'cs' ? 'celkem rezervací' : 'total bookings'}

{/* Stats Cards */}
{[ { label: i18n.locale() === 'cs' ? 'Celkem' : 'Total', value: bookingStats().total, color: 'bg-canvas-subtle' }, { label: i18n.locale() === 'cs' ? 'Potvrzeno' : 'Confirmed', value: bookingStats().confirmed, color: 'bg-[hsl(var(--success-soft))] text-[hsl(var(--success))]' }, { label: i18n.locale() === 'cs' ? 'Čeká' : 'Pending', value: bookingStats().pending, color: 'bg-[hsl(var(--warning-soft))] text-[hsl(var(--warning))]' }, { label: i18n.locale() === 'cs' ? 'Zrušeno' : 'Cancelled', value: bookingStats().cancelled, color: 'bg-[hsl(var(--error-soft))] text-[hsl(var(--error))]' }, { label: i18n.locale() === 'cs' ? 'Dokončeno' : 'Completed', value: bookingStats().completed, color: 'bg-canvas-muted text-ink-muted' }, ].map((stat, index) => (

{stat.value}

{stat.label}

))}
{/* Filters */}
setSearchQuery(e.currentTarget.value)} />
setFilterDateRange(v as any)} class="min-w-[140px]" options={[ { value: "all", label: i18n.locale() === 'cs' ? 'Všechny termíny' : 'All Dates' }, { value: "today", label: i18n.locale() === 'cs' ? 'Dnes' : 'Today' }, { value: "week", label: i18n.locale() === 'cs' ? 'Tento týden' : 'This Week' }, { value: "month", label: i18n.locale() === 'cs' ? 'Tento měsíc' : 'This Month' }, ]} />
{/* Bookings Table */}
{filteredBookings().map((booking: any, index: number) => ( ))}
{i18n.locale() === 'cs' ? 'Zákazník' : 'Customer'} {i18n.locale() === 'cs' ? 'Služba' : 'Service'} {i18n.locale() === 'cs' ? 'Datum a čas' : 'Date & Time'} {i18n.locale() === 'cs' ? 'Stav' : 'Status'} {i18n.locale() === 'cs' ? 'Reference' : 'Reference'} {i18n.locale() === 'cs' ? 'Akce' : 'Actions'}
{booking.avatar || getInitials(booking.customerName)}

{booking.customerName}

{booking.customerEmail}

{booking.service}

{booking.location}

{new Date(booking.startsAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}

{new Date(booking.startsAt).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })} - {new Date(booking.endsAt).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}

{statusLabels[booking.status] || booking.status} {booking.reference}
{booking.status !== 'cancelled' && booking.status !== 'completed' && ( <> )}
{filteredBookings().length === 0 && (

{i18n.locale() === 'cs' ? 'Žádné rezervace nenalezeny' : 'No bookings found'}

)}
{/* New Booking Modal */} {showNewBooking() && (
setShowNewBooking(false)} />

{i18n.locale() === 'cs' ? 'Nová rezervace' : 'New Booking'}

setNewBookingCustomer(e.currentTarget.value)} placeholder={i18n.locale() === 'cs' ? 'např. Jan Novák' : 'e.g. John Doe'} /> setNewBookingEmail(e.currentTarget.value)} placeholder="customer@example.com" /> setNewBookingService(e.currentTarget.value)} placeholder={i18n.locale() === 'cs' ? 'např. Masáž' : 'e.g. Massage'} />
setNewBookingDate(e.currentTarget.value)} /> setNewBookingTime(e.currentTarget.value)} />