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:
Tomas DvorakandDevin committed 2026-09-17 20:21:34 +02:00
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); }
};