fix: backfill memberships, handle booking create errors

- Migration 00012: populate tenant_memberships from tenant_users join
  users, not only the seeded demo owner (Codex P1)
- bookings-page: check response.error so 4xx failures do not close the
  modal silently; refetch dashboard summary after create (Codex P2)

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:33:53 +02:00
1 parent f2f7439050
commit 50c8440a06
3 files changed
+12 -7

No files matched your search

@@ -30,12 +30,13 @@ WHERE manage_token IS NULL;
ALTER TABLE locations ALTER TABLE locations
ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(); ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
-- Demo tenant membership so demo-owner resolves to Studio Atelier. -- Backfill memberships for every existing user linked via tenant_users.
-- Covers the seeded demo owner (users.neon_subject = 'demo-owner').
INSERT INTO tenant_memberships (tenant_id, user_neon_subject, role) INSERT INTO tenant_memberships (tenant_id, user_neon_subject, role)
SELECT t.id, u.neon_subject, 'owner' SELECT tu.tenant_id, u.neon_subject, tu.role
FROM tenants t FROM tenant_users tu
JOIN users u ON u.neon_subject = 'demo-owner' JOIN users u ON u.id = tu.user_id
WHERE t.slug = 'studio-atelier' WHERE u.neon_subject IS NOT NULL
ON CONFLICT (tenant_id, user_neon_subject) DO NOTHING; ON CONFLICT (tenant_id, user_neon_subject) DO NOTHING;
-- +goose Down -- +goose Down
@@ -72,15 +72,17 @@ function BookingsPage() {
} }
setCreatingBooking(true); setCreatingBooking(true);
try { try {
await (apiClient as any).POST("/v1/catalog/bookings", { const response = await (apiClient as any).POST("/v1/catalog/bookings", {
headers: { Authorization: `Bearer ${bearer}` }, headers: { Authorization: `Bearer ${bearer}` },
body: { body: {
customerName: newBookingCustomer(), customerEmail: newBookingEmail(), service: newBookingService(), customerName: newBookingCustomer(), customerEmail: newBookingEmail(), service: newBookingService(),
startsAt: new Date(`${newBookingDate()}T${newBookingTime()}`).toISOString(), notes: newBookingNotes() startsAt: new Date(`${newBookingDate()}T${newBookingTime()}`).toISOString(), notes: newBookingNotes()
} }
}); });
if (response.error) throw new Error(typeof response.error === "string" ? response.error : "booking create failed");
setNewBookingCustomer(""); setNewBookingEmail(""); setNewBookingService(""); setNewBookingDate(""); setNewBookingTime(""); setNewBookingNotes(""); setNewBookingCustomer(""); setNewBookingEmail(""); setNewBookingService(""); setNewBookingDate(""); setNewBookingTime(""); setNewBookingNotes("");
setShowNewBooking(false); setShowNewBooking(false);
void data.refetchSummary();
} catch { data.setDemoNotice(data.i18n.locale() === "cs" ? "Vytvoření rezervace selhalo." : "Failed to create booking."); } } catch { data.setDemoNotice(data.i18n.locale() === "cs" ? "Vytvoření rezervace selhalo." : "Failed to create booking."); }
finally { setCreatingBooking(false); } finally { setCreatingBooking(false); }
}; };
@@ -94,6 +94,7 @@ export interface DashboardData {
token: () => string | null | undefined; token: () => string | null | undefined;
isDemoMode: () => boolean; isDemoMode: () => boolean;
resolvedSummary: () => any; resolvedSummary: () => any;
refetchSummary: () => void;
resolvedBootstrap: () => any; resolvedBootstrap: () => any;
resolvedBilling: () => any; resolvedBilling: () => any;
normalizedKpis: () => any[]; normalizedKpis: () => any[];
@@ -150,7 +151,7 @@ export function DashboardDataProvider(props: { children: any }) {
const isDemoMode = () => token()?.startsWith("demo.") ?? false; const isDemoMode = () => token()?.startsWith("demo.") ?? false;
const demoData = createMemo(() => createDemoData(i18n)); const demoData = createMemo(() => createDemoData(i18n));
const [summary] = createResource(token, async (bearer) => { const [summary, { refetch: refetchSummary }] = createResource(token, async (bearer) => {
if (!bearer) return null; if (!bearer) return null;
if (bearer.startsWith("demo.")) return demoData().summary; if (bearer.startsWith("demo.")) return demoData().summary;
const response = await apiClient.GET("/v1/dashboard/summary", { headers: { Authorization: `Bearer ${bearer}` } }); const response = await apiClient.GET("/v1/dashboard/summary", { headers: { Authorization: `Bearer ${bearer}` } });
@@ -286,6 +287,7 @@ export function DashboardDataProvider(props: { children: any }) {
token: () => token(), token: () => token(),
isDemoMode, isDemoMode,
resolvedSummary, resolvedSummary,
refetchSummary,
resolvedBootstrap, resolvedBootstrap,
resolvedBilling, resolvedBilling,
normalizedKpis, normalizedKpis,