import { createResource, createMemo, createSignal, useContext, createContext } from "solid-js"; import { apiClient } from "../../lib/api-client"; import { useAuth } from "../../providers/auth-provider"; import { useI18n } from "../../providers/i18n-provider"; import { getInitials, getBookingDuration } from "../../components/dashboard/types"; export type Section = "overview" | "bookings" | "customers" | "services" | "staff" | "zones" | "billing" | "settings"; function createDemoData(i18n: ReturnType) { const cs = i18n.locale() === "cs"; const fmt = (d: Date) => d.toISOString(); // Exact local clock time on a day relative to today. const at = (dayOffset: number, hour: number, minute = 0) => { const d = new Date(); d.setDate(d.getDate() + dayOffset); d.setHours(hour, minute, 0, 0); return d; }; const services = cs ? ["Masáž", "Kosmetika", "Fyzioterapie", "Manikúra"] : ["Massage", "Cosmetics", "Physiotherapy", "Manicure"]; const customers = [ { name: cs ? "Martina Nováková" : "Martina Novakova", email: "martina@example.com" }, { name: "David Svoboda", email: "david@example.com" }, { name: cs ? "Jana Kovářová" : "Jana Kovarova", email: "jana@example.com" }, { name: "Alice Johnson", email: "alice@example.com" }, { name: "Bob Smith", email: "bob@example.com" }, ]; const salonA = cs ? "Hlavní sál" : "Main hall"; const salonB = "Studio B"; const staff1 = "Petra M."; const staff2 = "Lucie K."; const allBookings = [ // past days - completed/cancelled history for the trend chart { id: "b10", customerName: customers[4].name, customerEmail: customers[4].email, service: services[1], status: "completed", startsAt: fmt(at(-4, 10)), endsAt: fmt(at(-4, 11)), reference: "BOK-010", location: salonB, staffName: "", notes: "" }, { id: "b5", customerName: customers[4].name, customerEmail: customers[4].email, service: services[0], status: "completed", startsAt: fmt(at(-2, 13, 30)), endsAt: fmt(at(-2, 14, 30)), reference: "BOK-005", location: salonB, staffName: staff2, notes: "" }, { id: "b12", customerName: customers[1].name, customerEmail: customers[1].email, service: services[2], status: "completed", startsAt: fmt(at(-2, 9)), endsAt: fmt(at(-2, 10)), reference: "BOK-012", location: salonA, staffName: staff1, notes: "" }, { id: "b4", customerName: customers[3].name, customerEmail: customers[3].email, service: services[3], status: "cancelled", startsAt: fmt(at(-1, 11)), endsAt: fmt(at(-1, 12)), reference: "BOK-004", location: salonA, staffName: staff1, notes: "" }, { id: "b11", customerName: customers[2].name, customerEmail: customers[2].email, service: services[0], status: "completed", startsAt: fmt(at(-1, 15, 30)), endsAt: fmt(at(-1, 16, 30)), reference: "BOK-011", location: salonA, staffName: staff2, notes: "" }, // today { id: "b1", customerName: customers[0].name, customerEmail: customers[0].email, service: services[0], status: "confirmed", startsAt: fmt(at(0, 10)), endsAt: fmt(at(0, 11)), reference: "BOK-001", location: salonA, staffName: staff1, notes: "", intake: cs ? { "zdravotni_poznamky": "Citlivá pokožka", "prvni_navsteva": "yes" } : { "health_notes": "Sensitive skin", "first_visit": "yes" } }, { id: "b7", customerName: customers[1].name, customerEmail: customers[1].email, service: services[2], status: "confirmed", startsAt: fmt(at(0, 16)), endsAt: fmt(at(0, 17)), reference: "BOK-007", location: salonB, staffName: "", notes: "" }, // upcoming { id: "b2", customerName: customers[1].name, customerEmail: customers[1].email, service: services[1], status: "pending", startsAt: fmt(at(1, 14)), endsAt: fmt(at(1, 15)), reference: "BOK-002", location: salonA, staffName: staff2, notes: "" }, { id: "b3", customerName: customers[2].name, customerEmail: customers[2].email, service: services[2], status: "confirmed", startsAt: fmt(at(2, 9)), endsAt: fmt(at(2, 10)), reference: "BOK-003", location: salonB, staffName: "", notes: cs ? "Bolest zad" : "Back pain" }, { id: "b6", customerName: customers[0].name, customerEmail: customers[0].email, service: services[1], status: "confirmed", startsAt: fmt(at(3, 10)), endsAt: fmt(at(3, 11)), reference: "BOK-006", location: salonA, staffName: staff1, notes: "" }, { id: "b8", customerName: customers[2].name, customerEmail: customers[2].email, service: services[3], status: "pending", startsAt: fmt(at(4, 9)), endsAt: fmt(at(4, 10)), reference: "BOK-008", location: salonA, staffName: staff2, notes: "" }, { id: "b9", customerName: customers[3].name, customerEmail: customers[3].email, service: services[0], status: "confirmed", startsAt: fmt(at(5, 11)), endsAt: fmt(at(5, 12)), reference: "BOK-009", location: salonA, staffName: staff1, notes: "" }, { id: "b13", customerName: customers[0].name, customerEmail: customers[0].email, service: cs ? "Jóga pro začátečníky" : "Beginner yoga", status: "waitlisted", waitlistPosition: 1, startsAt: fmt(at(3, 18)), endsAt: fmt(at(3, 19)), reference: "BOK-013", location: salonA, staffName: "", notes: "" }, ]; return { summary: { tenantName: cs ? "Demo Studio" : "Demo Studio", tenantSlug: "studio-atelier", publicBookingUrl: "http://localhost:3000/book/studio-atelier", kpis: [ { label: i18n.t("dashboard.kpi.bookings"), value: 42, change: "+12%", trend: "up" }, { label: i18n.t("dashboard.kpi.cancelled"), value: 3, change: "-5%", trend: "down" }, { label: i18n.t("dashboard.kpi.completed"), value: 38, change: "+8%", trend: "up" }, { label: i18n.t("dashboard.kpi.newClients"), value: 12, change: "+24%", trend: "up" }, ], staff: [{ id: "s1", name: cs ? "Petra M." : "Petra M.", role: cs ? "Specialista" : "Specialist", email: "petra@demo.cz" }], upcomingBookings: allBookings.filter(b => b.status !== "cancelled" && b.status !== "completed"), allBookings, recentActivity: [ { action: cs ? "Nová rezervace" : "New booking", detail: `${customers[0].name} - ${services[0]}`, time: cs ? "Dnes, 10:00" : "Today, 10:00", type: "booking" }, { action: cs ? "Zrušení rezervace" : "Booking cancelled", detail: `${customers[3].name} - ${services[3]}`, time: cs ? "Včera" : "Yesterday", type: "cancel" }, { action: cs ? "Dokončená rezervace" : "Booking completed", detail: `${customers[4].name} - ${services[1]}`, time: cs ? "Před 2 dny" : "2 days ago", type: "reminder" }, ], }, bootstrap: { tenantName: cs ? "Demo Studio" : "Demo Studio", slug: "studio-atelier", brand: { primaryColor: "#c25e3a" }, locale: cs ? "cs" : "en", timezone: "Europe/Prague", }, billing: { planCode: "pro", planName: "Pro", subscriptionStatus: "trialing", trialDaysRemaining: 12, entitlements: { maxLocations: 3, maxBookings: -1, maxStaff: 10 }, billingProvider: "stripe", }, }; } export interface DashboardData { i18n: ReturnType; auth: ReturnType; token: () => string | null | undefined; isDemoMode: () => boolean; resolvedSummary: () => any; refetchSummary: () => void; resolvedBootstrap: () => any; resolvedBilling: () => any; normalizedKpis: () => any[]; normalizedUpcomingBookings: () => any[]; normalizedRecentActivity: () => any[]; normalizedAllBookings: () => any[]; isDashboardReady: () => boolean; locationData: () => number; smsUsage: () => any; demoNotice: () => string | null; setDemoNotice: (v: string | null) => void; billingNotice: () => string | null; setBillingNotice: (v: string | null) => void; billingError: () => string | null; setBillingError: (v: string | null) => void; billingAction: () => "checkout" | "refresh" | "portal" | null; setBillingAction: (v: "checkout" | "refresh" | "portal" | null) => void; billingInterval: () => "monthly" | "yearly"; setBillingInterval: (v: "monthly" | "yearly") => void; openCheckout: (planCode?: string) => Promise; openPortal: () => Promise; refreshBilling: () => Promise; showIntegrationModal: () => boolean; setShowIntegrationModal: (v: boolean) => void; showBookingDetail: () => boolean; setShowBookingDetail: (v: boolean) => void; selectedBooking: () => any; setSelectedBooking: (v: any) => void; openBookingDetail: (booking: any) => void; } const DashboardContext = createContext(); export function useDashboardData() { const ctx = useContext(DashboardContext); if (!ctx) throw new Error("useDashboardData must be used within DashboardContext provider"); return ctx; } export function DashboardDataProvider(props: { children: any }) { const i18n = useI18n(); const auth = useAuth(); const [billingInterval, setBillingInterval] = createSignal<"monthly" | "yearly">("monthly"); const [billingNotice, setBillingNotice] = createSignal(null); const [billingError, setBillingError] = createSignal(null); const [billingAction, setBillingAction] = createSignal<"checkout" | "refresh" | "portal" | null>(null); const [showIntegrationModal, setShowIntegrationModal] = createSignal(false); const [demoNotice, setDemoNotice] = createSignal(null); const [showBookingDetail, setShowBookingDetail] = createSignal(false); const [selectedBooking, setSelectedBooking] = createSignal(null); const [token] = createResource(() => auth.session()?.session?.id, () => auth.getToken()); const isDemoMode = () => token()?.startsWith("demo.") ?? false; const demoData = createMemo(() => createDemoData(i18n)); const [summary, { refetch: refetchSummary }] = 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 [locationData] = createResource(token, async (bearer) => { if (!bearer) return 0; if (bearer.startsWith("demo.")) return 1; try { const response = await (apiClient as any).GET("/v1/catalog/locations", { headers: { Authorization: `Bearer ${bearer}` } }); return (response.data as any[])?.length ?? 0; } catch { return 0; } }); const [smsUsage] = createResource(token, async (bearer) => { if (!bearer || bearer.startsWith("demo.")) return null; try { const response = await (apiClient as any).GET("/v1/sms/usage", { headers: { Authorization: `Bearer ${bearer}` } }); return response.data as any ?? null; } catch { return null; } }); // Demo data is reactive to locale changes — prefer it over the stale resource value. const resolvedSummary = () => (isDemoMode() ? demoData().summary : (summary.latest as any)) ?? undefined; const resolvedBootstrap = () => (isDemoMode() ? demoData().bootstrap : (bootstrap.latest as any)) ?? undefined; const planNames: Record = { starter: "Starter", pro: "Pro", business: "Business" }; const resolvedBilling = () => { const raw = (isDemoMode() ? demoData().billing : (billing.latest as any)) ?? undefined; if (!raw) return undefined; const ent = raw.entitlements ?? {}; return { ...raw, subscriptionStatus: raw.status ?? raw.subscriptionStatus ?? "inactive", planName: planNames[raw.planCode] ?? raw.planCode ?? "Starter", trialDaysRemaining: raw.trialDays ?? raw.trialDaysRemaining ?? 0, entitlements: { ...ent, maxBookings: ent.maxBookingsMonth ?? ent.maxBookings, }, }; }; const kpiLabelKeys: Record = { bookings_this_week: "dashboard.kpi.bookingsThisWeek", completed: "dashboard.kpi.completed", cancellations: "dashboard.kpi.cancellations", utilization: "dashboard.kpi.utilization", new_clients: "dashboard.kpi.newClients", }; const normalizedKpis = createMemo(() => (resolvedSummary()?.kpis ?? []).map((kpi: any) => ({ ...kpi, label: kpi.code && kpiLabelKeys[kpi.code] ? i18n.t(kpiLabelKeys[kpi.code]) : kpi.label, trend: kpi.trend ?? "neutral", change: kpi.change ?? "—", }))); 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} \u2022 ${booking.service}`, time: new Date(booking.startsAt).toLocaleDateString(undefined, { month: "short", day: "numeric" }), type: "booking", })); }); const normalizedAllBookings = createMemo(() => resolvedSummary()?.allBookings ?? normalizedUpcomingBookings()); const isDashboardReady = () => !!token() && !bootstrap.loading && !summary.loading && !billing.loading; const openCheckout = async (planCode?: string) => { const bearer = token(); if (!bearer || bearer.startsWith("demo.")) { setDemoNotice(i18n.locale() === "cs" ? "V demo režimu nelze upravovat předplatné." : "Cannot modify billing in demo mode."); return; } setBillingAction("checkout"); setBillingError(null); try { const response = await (apiClient as any).POST("/v1/billing/checkout", { headers: { Authorization: `Bearer ${bearer}` }, body: { planCode: planCode ?? resolvedBilling()?.planCode ?? "pro", currency: "czk", billingInterval: billingInterval() }, }); if (response.data?.checkoutUrl) window.location.href = response.data.checkoutUrl; else throw new Error(response.error ? String(response.error) : "No checkout URL"); } catch (err) { setBillingError(err instanceof Error ? err.message : String(err)); } finally { setBillingAction(null); } }; const openPortal = async () => { const bearer = token(); if (!bearer || bearer.startsWith("demo.")) { setDemoNotice(i18n.locale() === "cs" ? "V demo režimu nelze upravovat předplatné." : "Cannot modify billing in demo mode."); return; } setBillingAction("portal"); setBillingError(null); try { const response = await (apiClient as any).POST("/v1/billing/portal", { headers: { Authorization: `Bearer ${bearer}` } }); if (response.data?.url) window.location.href = response.data.url; else throw new Error("No portal URL"); } catch (err) { setBillingError(err instanceof Error ? err.message : String(err)); } finally { setBillingAction(null); } }; const refreshBilling = async () => { const bearer = token(); if (!bearer || bearer.startsWith("demo.")) { setDemoNotice(i18n.locale() === "cs" ? "V demo režimu nelze obnovit předplatné." : "Cannot refresh billing in demo mode."); return; } setBillingAction("refresh"); setBillingError(null); try { const response = await (apiClient as any).POST("/v1/billing/refresh", { headers: { Authorization: `Bearer ${bearer}` } }); if (response.data) { setBillingNotice(i18n.locale() === "cs" ? "Předplatné obnoveno." : "Billing refreshed."); void refetchBilling(); } } catch (err) { setBillingError(err instanceof Error ? err.message : String(err)); } finally { setBillingAction(null); } }; const openBookingDetail = (booking: any) => { setSelectedBooking(booking); setShowBookingDetail(true); }; const value: DashboardData = { i18n, auth, token: () => token(), isDemoMode, resolvedSummary, refetchSummary, resolvedBootstrap, resolvedBilling, normalizedKpis, normalizedUpcomingBookings, normalizedRecentActivity, normalizedAllBookings, isDashboardReady, locationData: () => locationData() ?? 0, smsUsage: () => smsUsage.latest, demoNotice, setDemoNotice, billingNotice, setBillingNotice, billingError, setBillingError, billingAction, setBillingAction, billingInterval, setBillingInterval, openCheckout, openPortal, refreshBilling, showIntegrationModal, setShowIntegrationModal, showBookingDetail, setShowBookingDetail, selectedBooking, setSelectedBooking, openBookingDetail, }; return value; } export { DashboardContext };