mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-29 07:54:44 +00:00
refactor(db): replace sqlc with handwritten pgx repos
- Finish Paddle-to-Stripe billing migration; drop paddle/stripe libs - Renumber migrations; add 00012 memberships+manage_token, 00013 location_type_capacity - Add tests: admin, auth, bookings customer service - Remove graphify-out artifacts and stale env files Authored By: TDvorak <[email protected]> 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
2039669e2c
commit
f2f7439050
93 files changed
+6987
-69060
No files matched your search
@@ -8,40 +8,53 @@ export type Section = "overview" | "bookings" | "customers" | "zones" | "billing
|
||||
|
||||
function createDemoData(i18n: ReturnType<typeof useI18n>) {
|
||||
const cs = i18n.locale() === "cs";
|
||||
const now = new Date();
|
||||
const fmt = (d: Date) => d.toISOString();
|
||||
const addDays = (n: number) => { const d = new Date(now); d.setDate(d.getDate() + n); return d; };
|
||||
const addHours = (d: Date, h: number) => { const r = new Date(d); r.setHours(r.getHours() + h); return r; };
|
||||
// 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 Novakova" : "Martina Novakova", email: "[email protected]" },
|
||||
{ name: cs ? "David Svoboda" : "David Svoboda", email: "[email protected]" },
|
||||
{ name: cs ? "Jana KovarovA" : "Jana KovarovA", email: "[email protected]" },
|
||||
{ name: cs ? "Alice Johnson" : "Alice Johnson", email: "[email protected]" },
|
||||
{ name: cs ? "Bob Smith" : "Bob Smith", email: "[email protected]" },
|
||||
{ name: cs ? "Martina Nováková" : "Martina Novakova", email: "[email protected]" },
|
||||
{ name: "David Svoboda", email: "[email protected]" },
|
||||
{ name: cs ? "Jana Kovářová" : "Jana Kovarova", email: "[email protected]" },
|
||||
{ name: "Alice Johnson", email: "[email protected]" },
|
||||
{ name: "Bob Smith", email: "[email protected]" },
|
||||
];
|
||||
const salonA = cs ? "Hlavní sál" : "Main hall";
|
||||
const salonB = "Studio B";
|
||||
const staff1 = "Petra M.";
|
||||
const staff2 = "Lucie K.";
|
||||
|
||||
const allBookings = [
|
||||
{ id: "b1", customerName: customers[0].name, customerEmail: customers[0].email, service: services[0], status: "confirmed", startsAt: fmt(addHours(addDays(0), 10)), endsAt: fmt(addHours(addDays(0), 11)), reference: "BOK-001", location: cs ? "Hlavní salon" : "Main Salon", staffName: cs ? "Petra M." : "Petra M.", notes: "" },
|
||||
{ id: "b2", customerName: customers[1].name, customerEmail: customers[1].email, service: services[1], status: "pending", startsAt: fmt(addHours(addDays(1), 14)), endsAt: fmt(addHours(addDays(1), 15)), reference: "BOK-002", location: cs ? "Hlavní salon" : "Main Salon", staffName: cs ? "Lucie K." : "Lucie K.", notes: "" },
|
||||
{ id: "b3", customerName: customers[2].name, customerEmail: customers[2].email, service: services[2], status: "confirmed", startsAt: fmt(addHours(addDays(2), 9)), endsAt: fmt(addHours(addDays(2), 10)), reference: "BOK-003", location: cs ? "Studio B" : "Studio B", staffName: "", notes: cs ? "Bolest zad" : "Back pain" },
|
||||
{ id: "b4", customerName: customers[3].name, customerEmail: customers[3].email, service: services[3], status: "cancelled", startsAt: fmt(addHours(addDays(-1), 11)), endsAt: fmt(addHours(addDays(-1), 12)), reference: "BOK-004", location: cs ? "Hlavní salon" : "Main Salon", staffName: cs ? "Petra M." : "Petra M.", notes: "" },
|
||||
{ id: "b5", customerName: customers[4].name, customerEmail: customers[4].email, service: services[0], status: "completed", startsAt: fmt(addHours(addDays(-2), 13)), endsAt: fmt(addHours(addDays(-2), 14)), reference: "BOK-005", location: cs ? "Studio B" : "Studio B", staffName: cs ? "Lucie K." : "Lucie K.", notes: "" },
|
||||
{ id: "b6", customerName: customers[0].name, customerEmail: customers[0].email, service: services[1], status: "confirmed", startsAt: fmt(addHours(addDays(3), 10)), endsAt: fmt(addHours(addDays(3), 11)), reference: "BOK-006", location: cs ? "Hlavní salon" : "Main Salon", staffName: cs ? "Petra M." : "Petra M.", notes: "" },
|
||||
{ id: "b7", customerName: customers[1].name, customerEmail: customers[1].email, service: services[2], status: "confirmed", startsAt: fmt(addHours(addDays(0), 16)), endsAt: fmt(addHours(addDays(0), 17)), reference: "BOK-007", location: cs ? "Studio B" : "Studio B", staffName: "", notes: "" },
|
||||
{ id: "b8", customerName: customers[2].name, customerEmail: customers[2].email, service: services[3], status: "pending", startsAt: fmt(addHours(addDays(4), 9)), endsAt: fmt(addHours(addDays(4), 10)), reference: "BOK-008", location: cs ? "Hlavní salon" : "Main Salon", staffName: cs ? "Lucie K." : "Lucie K.", notes: "" },
|
||||
{ id: "b9", customerName: customers[3].name, customerEmail: customers[3].email, service: services[0], status: "confirmed", startsAt: fmt(addHours(addDays(5), 11)), endsAt: fmt(addHours(addDays(5), 12)), reference: "BOK-009", location: cs ? "Hlavní salon" : "Main Salon", staffName: cs ? "Petra M." : "Petra M.", notes: "" },
|
||||
{ id: "b10", customerName: customers[4].name, customerEmail: customers[4].email, service: services[1], status: "completed", startsAt: fmt(addHours(addDays(-3), 10)), endsAt: fmt(addHours(addDays(-3), 11)), reference: "BOK-010", location: cs ? "Studio B" : "Studio B", staffName: "", notes: "" },
|
||||
// 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: "" },
|
||||
{ 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: "" },
|
||||
];
|
||||
|
||||
return {
|
||||
summary: {
|
||||
tenantName: cs ? "Demo Studio" : "Demo Studio",
|
||||
tenantSlug: "demo-studio",
|
||||
publicBookingUrl: "http://localhost:3000/book/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" },
|
||||
@@ -59,7 +72,7 @@ function createDemoData(i18n: ReturnType<typeof useI18n>) {
|
||||
},
|
||||
bootstrap: {
|
||||
tenantName: cs ? "Demo Studio" : "Demo Studio",
|
||||
slug: "demo-studio",
|
||||
slug: "studio-atelier",
|
||||
brand: { primaryColor: "#c25e3a" },
|
||||
locale: cs ? "cs" : "en",
|
||||
timezone: "Europe/Prague",
|
||||
@@ -100,7 +113,7 @@ export interface DashboardData {
|
||||
setBillingAction: (v: "checkout" | "refresh" | "portal" | null) => void;
|
||||
billingInterval: () => "monthly" | "yearly";
|
||||
setBillingInterval: (v: "monthly" | "yearly") => void;
|
||||
openCheckout: () => Promise<void>;
|
||||
openCheckout: (planCode?: string) => Promise<void>;
|
||||
openPortal: () => Promise<void>;
|
||||
refreshBilling: () => Promise<void>;
|
||||
showIntegrationModal: () => boolean;
|
||||
@@ -175,9 +188,26 @@ export function DashboardDataProvider(props: { children: any }) {
|
||||
} catch { return 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);
|
||||
// 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<string, string> = { 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 normalizedKpis = createMemo(() => (resolvedSummary()?.kpis ?? []).map((kpi: any) => ({
|
||||
...kpi, trend: kpi.trend ?? "neutral", change: kpi.change ?? "—",
|
||||
@@ -204,7 +234,7 @@ export function DashboardDataProvider(props: { children: any }) {
|
||||
|
||||
const isDashboardReady = () => !!token() && !bootstrap.loading && !summary.loading && !billing.loading;
|
||||
|
||||
const openCheckout = async () => {
|
||||
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");
|
||||
@@ -212,10 +242,10 @@ export function DashboardDataProvider(props: { children: any }) {
|
||||
try {
|
||||
const response = await (apiClient as any).POST("/v1/billing/checkout", {
|
||||
headers: { Authorization: `Bearer ${bearer}` },
|
||||
body: { interval: billingInterval() },
|
||||
body: { planCode: planCode ?? resolvedBilling()?.planCode ?? "pro", currency: "czk", billingInterval: billingInterval() },
|
||||
});
|
||||
if (response.data?.url) window.location.href = response.data.url;
|
||||
else throw new Error("No checkout URL");
|
||||
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); }
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user