Skip to content

Commit 0c7ba6c

Browse files
committed
feat: enhance booking and tenant management UI with new input fields and schema updates
- Updated booking creation to handle ISO date formats for slotStart and slotEnd. - Refactored TenantDetailPage to use Suspense for improved data fetching. - Added new input components: ColorField, UrlField, and TimeField for enhanced configuration options. - Updated schema definitions to include new input types in the config API. - Implemented database migrations to add advanced module schemas for queue management, notifications, business hours, access control, and branding.
1 parent 7239ae7 commit 0c7ba6c

6 files changed

Lines changed: 1002 additions & 108 deletions

File tree

apps/management-ui/app/bookings/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,13 @@ export default function BookingsPage() {
288288
description="Create a booking for this tenant."
289289
>
290290
<form
291-
onSubmit={handleSubmit((v) => createMutation.mutate(v))}
291+
onSubmit={handleSubmit((v) =>
292+
createMutation.mutate({
293+
...v,
294+
slotStart: new Date(v.slotStart).toISOString(),
295+
slotEnd: new Date(v.slotEnd).toISOString(),
296+
})
297+
)}
292298
className="space-y-4"
293299
>
294300
<Input

apps/management-ui/app/tenants/[id]/page.tsx

Lines changed: 115 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
"use client";
1010

11-
import { use, useEffect } from "react";
11+
import { use, useEffect, Suspense } from "react";
1212
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
1313
import { useForm } from "react-hook-form";
1414
import { zodResolver } from "@hookform/resolvers/zod";
@@ -48,11 +48,8 @@ const subNavLinks = (id: string) => [
4848
{ href: `/tenants/${id}/config`, label: "Config", icon: Settings2 },
4949
];
5050

51-
export default function TenantDetailPage({
52-
params,
53-
}: {
54-
params: Promise<{ id: string }>;
55-
}) {
51+
// Inner component — safe to call use(params) here because it's wrapped in Suspense
52+
function TenantDetailContent({ params }: { params: Promise<{ id: string }> }) {
5653
const { id } = use(params);
5754
const { toast } = useToast();
5855
const qc = useQueryClient();
@@ -97,110 +94,123 @@ export default function TenantDetailPage({
9794
}, [tenant, reset]);
9895

9996
return (
100-
<Shell>
101-
<div className="space-y-6 max-w-2xl">
102-
<div className="flex items-center gap-3">
103-
<Link href="/tenants">
104-
<Button variant="ghost" size="sm">
105-
<ArrowLeft className="h-4 w-4" />
106-
Tenants
107-
</Button>
108-
</Link>
109-
</div>
110-
111-
{isLoading ? (
112-
<PageSpinner />
113-
) : !tenant ? (
114-
<p className="text-sm text-slate-500">Tenant not found.</p>
115-
) : (
116-
<>
117-
{/* Header */}
118-
<div className="flex items-start justify-between">
119-
<div>
120-
<div className="flex items-center gap-2">
121-
<h2 className="text-lg font-semibold text-slate-900">{tenant.name}</h2>
122-
<TenantStatusBadge status={tenant.status} />
123-
<PlanBadge plan={tenant.plan} />
124-
</div>
125-
<p className="text-sm text-slate-500 mt-0.5 font-mono">{tenant.slug}</p>
97+
<div className="space-y-6 max-w-2xl">
98+
<div className="flex items-center gap-3">
99+
<Link href="/tenants">
100+
<Button variant="ghost" size="sm">
101+
<ArrowLeft className="h-4 w-4" />
102+
Tenants
103+
</Button>
104+
</Link>
105+
</div>
106+
107+
{isLoading ? (
108+
<PageSpinner />
109+
) : !tenant ? (
110+
<p className="text-sm text-slate-500">Tenant not found.</p>
111+
) : (
112+
<>
113+
{/* Header */}
114+
<div className="flex items-start justify-between">
115+
<div>
116+
<div className="flex items-center gap-2">
117+
<h2 className="text-lg font-semibold text-slate-900">{tenant.name}</h2>
118+
<TenantStatusBadge status={tenant.status} />
119+
<PlanBadge plan={tenant.plan} />
126120
</div>
121+
<p className="text-sm text-slate-500 mt-0.5 font-mono">{tenant.slug}</p>
127122
</div>
123+
</div>
128124

129-
{/* Sub-nav quick links */}
130-
<div className="flex gap-2 flex-wrap">
131-
{subNavLinks(id).map((l) => (
132-
<Link key={l.href} href={l.href}>
133-
<Button variant="outline" size="sm">
134-
<l.icon className="h-3.5 w-3.5" />
135-
{l.label}
136-
</Button>
137-
</Link>
138-
))}
139-
</div>
125+
{/* Sub-nav quick links */}
126+
<div className="flex gap-2 flex-wrap">
127+
{subNavLinks(id).map((l) => (
128+
<Link key={l.href} href={l.href}>
129+
<Button variant="outline" size="sm">
130+
<l.icon className="h-3.5 w-3.5" />
131+
{l.label}
132+
</Button>
133+
</Link>
134+
))}
135+
</div>
140136

141-
{/* Meta */}
142-
<Card>
143-
<CardHeader>
144-
<CardTitle>Details</CardTitle>
145-
</CardHeader>
146-
<CardContent className="grid grid-cols-2 gap-4 text-sm">
147-
<div>
148-
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide">ID</p>
149-
<p className="font-mono text-xs mt-1 text-slate-700">{tenant.id}</p>
150-
</div>
151-
<div>
152-
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide">Created</p>
153-
<p className="mt-1 text-slate-700">{formatDate(tenant.createdAt)}</p>
154-
</div>
155-
<div>
156-
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide">Updated</p>
157-
<p className="mt-1 text-slate-700">{formatDate(tenant.updatedAt)}</p>
137+
{/* Meta */}
138+
<Card>
139+
<CardHeader>
140+
<CardTitle>Details</CardTitle>
141+
</CardHeader>
142+
<CardContent className="grid grid-cols-2 gap-4 text-sm">
143+
<div>
144+
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide">ID</p>
145+
<p className="font-mono text-xs mt-1 text-slate-700">{tenant.id}</p>
146+
</div>
147+
<div>
148+
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide">Created</p>
149+
<p className="mt-1 text-slate-700">{formatDate(tenant.createdAt)}</p>
150+
</div>
151+
<div>
152+
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide">Updated</p>
153+
<p className="mt-1 text-slate-700">{formatDate(tenant.updatedAt)}</p>
154+
</div>
155+
</CardContent>
156+
</Card>
157+
158+
{/* Edit form */}
159+
<Card>
160+
<CardHeader>
161+
<CardTitle>Edit Tenant</CardTitle>
162+
<CardDescription>Update name, plan, or settings.</CardDescription>
163+
</CardHeader>
164+
<CardContent>
165+
<form
166+
onSubmit={handleSubmit((v) => updateMutation.mutate(v))}
167+
className="space-y-4"
168+
>
169+
<Input
170+
label="Name"
171+
error={errors.name?.message}
172+
{...register("name")}
173+
/>
174+
<Select
175+
label="Plan"
176+
options={planOptions}
177+
error={errors.plan?.message}
178+
{...register("plan")}
179+
/>
180+
<Textarea
181+
label="Settings (JSON)"
182+
rows={5}
183+
className="font-mono text-xs"
184+
hint="Free-form JSON metadata for this tenant"
185+
error={errors.settings?.message}
186+
{...register("settings")}
187+
/>
188+
<div className="flex justify-end">
189+
<Button type="submit" loading={updateMutation.isPending}>
190+
<Save className="h-4 w-4" />
191+
Save Changes
192+
</Button>
158193
</div>
159-
</CardContent>
160-
</Card>
161-
162-
{/* Edit form */}
163-
<Card>
164-
<CardHeader>
165-
<CardTitle>Edit Tenant</CardTitle>
166-
<CardDescription>Update name, plan, or settings.</CardDescription>
167-
</CardHeader>
168-
<CardContent>
169-
<form
170-
onSubmit={handleSubmit((v) => updateMutation.mutate(v))}
171-
className="space-y-4"
172-
>
173-
<Input
174-
label="Name"
175-
error={errors.name?.message}
176-
{...register("name")}
177-
/>
178-
<Select
179-
label="Plan"
180-
options={planOptions}
181-
error={errors.plan?.message}
182-
{...register("plan")}
183-
/>
184-
<Textarea
185-
label="Settings (JSON)"
186-
rows={5}
187-
className="font-mono text-xs"
188-
hint="Free-form JSON metadata for this tenant"
189-
error={errors.settings?.message}
190-
{...register("settings")}
191-
/>
192-
<div className="flex justify-end">
193-
<Button type="submit" loading={updateMutation.isPending}>
194-
<Save className="h-4 w-4" />
195-
Save Changes
196-
</Button>
197-
</div>
198-
</form>
199-
</CardContent>
200-
</Card>
201-
</>
202-
)}
203-
</div>
194+
</form>
195+
</CardContent>
196+
</Card>
197+
</>
198+
)}
199+
</div>
200+
);
201+
}
202+
203+
// Outer page wraps the content in Suspense so use(params) has a boundary above it
204+
export default function TenantDetailPage({
205+
params,
206+
}: {
207+
params: Promise<{ id: string }>;
208+
}) {
209+
return (
210+
<Shell>
211+
<Suspense fallback={<PageSpinner />}>
212+
<TenantDetailContent params={params} />
213+
</Suspense>
204214
</Shell>
205215
);
206216
}

0 commit comments

Comments
 (0)