|
8 | 8 |
|
9 | 9 | "use client"; |
10 | 10 |
|
11 | | -import { use, useState } from "react"; |
| 11 | +import { use, useState, Suspense } from "react"; |
12 | 12 | import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; |
13 | 13 | import { useForm } from "react-hook-form"; |
14 | 14 | import { zodResolver } from "@hookform/resolvers/zod"; |
@@ -63,11 +63,7 @@ const statusFilterOptions = [ |
63 | 63 | { value: "no_show", label: "No show" }, |
64 | 64 | ]; |
65 | 65 |
|
66 | | -export default function TenantBookingsPage({ |
67 | | - params, |
68 | | -}: { |
69 | | - params: Promise<{ id: string }>; |
70 | | -}) { |
| 66 | +function TenantBookingsContent({ params }: { params: Promise<{ id: string }> }) { |
71 | 67 | const { id } = use(params); |
72 | 68 | const { toast } = useToast(); |
73 | 69 | const qc = useQueryClient(); |
@@ -138,135 +134,142 @@ export default function TenantBookingsPage({ |
138 | 134 | const totalPages = Math.ceil(total / limit); |
139 | 135 |
|
140 | 136 | return ( |
141 | | - <Shell> |
142 | | - <div className="space-y-5 max-w-6xl"> |
| 137 | + <div className="space-y-5 max-w-6xl"> |
| 138 | + <div className="flex items-center gap-3"> |
| 139 | + <Link href={`/tenants/${id}`}> |
| 140 | + <Button variant="ghost" size="sm"> |
| 141 | + <ArrowLeft className="h-4 w-4" /> |
| 142 | + {tenantQuery.data?.name ?? "Tenant"} |
| 143 | + </Button> |
| 144 | + </Link> |
| 145 | + </div> |
| 146 | + |
| 147 | + <div className="flex items-center justify-between"> |
| 148 | + <div> |
| 149 | + <h2 className="text-lg font-semibold text-slate-900">Bookings</h2> |
| 150 | + <p className="text-sm text-slate-500 mt-0.5"> |
| 151 | + {total} booking{total !== 1 ? "s" : ""} for{" "} |
| 152 | + <span className="font-medium">{tenantQuery.data?.name ?? id}</span> |
| 153 | + </p> |
| 154 | + </div> |
143 | 155 | <div className="flex items-center gap-3"> |
144 | | - <Link href={`/tenants/${id}`}> |
145 | | - <Button variant="ghost" size="sm"> |
146 | | - <ArrowLeft className="h-4 w-4" /> |
147 | | - {tenantQuery.data?.name ?? "Tenant"} |
148 | | - </Button> |
149 | | - </Link> |
| 156 | + <Select |
| 157 | + options={statusFilterOptions} |
| 158 | + value={statusFilter} |
| 159 | + onChange={(e) => { setStatusFilter(e.target.value); setPage(0); }} |
| 160 | + className="w-40" |
| 161 | + /> |
| 162 | + <Button onClick={() => setCreateOpen(true)}> |
| 163 | + <Plus className="h-4 w-4" /> |
| 164 | + New Booking |
| 165 | + </Button> |
150 | 166 | </div> |
| 167 | + </div> |
151 | 168 |
|
152 | | - <div className="flex items-center justify-between"> |
153 | | - <div> |
154 | | - <h2 className="text-lg font-semibold text-slate-900">Bookings</h2> |
155 | | - <p className="text-sm text-slate-500 mt-0.5"> |
156 | | - {total} booking{total !== 1 ? "s" : ""} for{" "} |
157 | | - <span className="font-medium">{tenantQuery.data?.name ?? id}</span> |
158 | | - </p> |
159 | | - </div> |
160 | | - <div className="flex items-center gap-3"> |
161 | | - <Select |
162 | | - options={statusFilterOptions} |
163 | | - value={statusFilter} |
164 | | - onChange={(e) => { setStatusFilter(e.target.value); setPage(0); }} |
165 | | - className="w-40" |
| 169 | + <Card> |
| 170 | + <CardContent className="p-0"> |
| 171 | + {bookingsQuery.isLoading ? ( |
| 172 | + <PageSpinner /> |
| 173 | + ) : bookings.length === 0 ? ( |
| 174 | + <EmptyState |
| 175 | + icon={CalendarDays} |
| 176 | + title="No bookings" |
| 177 | + description="No bookings found." |
| 178 | + action={ |
| 179 | + <Button onClick={() => setCreateOpen(true)}> |
| 180 | + <Plus className="h-4 w-4" /> |
| 181 | + New Booking |
| 182 | + </Button> |
| 183 | + } |
166 | 184 | /> |
167 | | - <Button onClick={() => setCreateOpen(true)}> |
168 | | - <Plus className="h-4 w-4" /> |
169 | | - New Booking |
170 | | - </Button> |
171 | | - </div> |
172 | | - </div> |
173 | | - |
174 | | - <Card> |
175 | | - <CardContent className="p-0"> |
176 | | - {bookingsQuery.isLoading ? ( |
177 | | - <PageSpinner /> |
178 | | - ) : bookings.length === 0 ? ( |
179 | | - <EmptyState |
180 | | - icon={CalendarDays} |
181 | | - title="No bookings" |
182 | | - description="No bookings found." |
183 | | - action={ |
184 | | - <Button onClick={() => setCreateOpen(true)}> |
185 | | - <Plus className="h-4 w-4" /> |
186 | | - New Booking |
187 | | - </Button> |
188 | | - } |
189 | | - /> |
190 | | - ) : ( |
191 | | - <Table> |
192 | | - <TableHeader> |
193 | | - <TableRow> |
194 | | - <TableHead>Customer Ref</TableHead> |
195 | | - <TableHead>Service Ref</TableHead> |
196 | | - <TableHead>Slot Start</TableHead> |
197 | | - <TableHead>Slot End</TableHead> |
198 | | - <TableHead>Status</TableHead> |
199 | | - <TableHead>Created</TableHead> |
200 | | - <TableHead className="w-20" /> |
201 | | - </TableRow> |
202 | | - </TableHeader> |
203 | | - <TableBody> |
204 | | - {bookings.map((b) => { |
205 | | - const nextStatuses = NEXT_STATUSES[b.status]; |
206 | | - return ( |
207 | | - <TableRow key={b.id}> |
208 | | - <TableCell className="font-medium">{b.customerRef}</TableCell> |
209 | | - <TableCell>{b.serviceRef}</TableCell> |
210 | | - <TableCell className="text-xs">{formatDate(b.slotStart)}</TableCell> |
211 | | - <TableCell className="text-xs">{formatDate(b.slotEnd)}</TableCell> |
212 | | - <TableCell><BookingStatusBadge status={b.status} /></TableCell> |
213 | | - <TableCell className="text-xs text-slate-500"> |
214 | | - {formatDate(b.createdAt)} |
215 | | - </TableCell> |
216 | | - <TableCell> |
217 | | - <div className="flex items-center gap-1"> |
218 | | - {nextStatuses.length > 0 && ( |
219 | | - <Button |
220 | | - variant="ghost" |
221 | | - size="sm" |
222 | | - onClick={() => setUpdateTarget(b)} |
223 | | - > |
224 | | - <RefreshCw className="h-3.5 w-3.5" /> |
225 | | - </Button> |
226 | | - )} |
227 | | - {(b.status === "pending" || b.status === "confirmed") && ( |
228 | | - <Button |
229 | | - variant="ghost" |
230 | | - size="sm" |
231 | | - className="text-red-500 hover:bg-red-50" |
232 | | - onClick={() => setCancelTarget(b)} |
233 | | - > |
234 | | - <Trash2 className="h-3.5 w-3.5" /> |
235 | | - </Button> |
236 | | - )} |
237 | | - </div> |
238 | | - </TableCell> |
239 | | - </TableRow> |
240 | | - ); |
241 | | - })} |
242 | | - </TableBody> |
243 | | - </Table> |
244 | | - )} |
245 | | - </CardContent> |
| 185 | + ) : ( |
| 186 | + <Table> |
| 187 | + <TableHeader> |
| 188 | + <TableRow> |
| 189 | + <TableHead>Customer Ref</TableHead> |
| 190 | + <TableHead>Service Ref</TableHead> |
| 191 | + <TableHead>Slot Start</TableHead> |
| 192 | + <TableHead>Slot End</TableHead> |
| 193 | + <TableHead>Status</TableHead> |
| 194 | + <TableHead>Created</TableHead> |
| 195 | + <TableHead className="w-20" /> |
| 196 | + </TableRow> |
| 197 | + </TableHeader> |
| 198 | + <TableBody> |
| 199 | + {bookings.map((b) => { |
| 200 | + const nextStatuses = NEXT_STATUSES[b.status]; |
| 201 | + return ( |
| 202 | + <TableRow key={b.id}> |
| 203 | + <TableCell className="font-medium">{b.customerRef}</TableCell> |
| 204 | + <TableCell>{b.serviceRef}</TableCell> |
| 205 | + <TableCell className="text-xs">{formatDate(b.slotStart)}</TableCell> |
| 206 | + <TableCell className="text-xs">{formatDate(b.slotEnd)}</TableCell> |
| 207 | + <TableCell><BookingStatusBadge status={b.status} /></TableCell> |
| 208 | + <TableCell className="text-xs text-slate-500"> |
| 209 | + {formatDate(b.createdAt)} |
| 210 | + </TableCell> |
| 211 | + <TableCell> |
| 212 | + <div className="flex items-center gap-1"> |
| 213 | + {nextStatuses.length > 0 && ( |
| 214 | + <Button |
| 215 | + variant="ghost" |
| 216 | + size="sm" |
| 217 | + onClick={() => setUpdateTarget(b)} |
| 218 | + > |
| 219 | + <RefreshCw className="h-3.5 w-3.5" /> |
| 220 | + </Button> |
| 221 | + )} |
| 222 | + {(b.status === "pending" || b.status === "confirmed") && ( |
| 223 | + <Button |
| 224 | + variant="ghost" |
| 225 | + size="sm" |
| 226 | + className="text-red-500 hover:bg-red-50" |
| 227 | + onClick={() => setCancelTarget(b)} |
| 228 | + > |
| 229 | + <Trash2 className="h-3.5 w-3.5" /> |
| 230 | + </Button> |
| 231 | + )} |
| 232 | + </div> |
| 233 | + </TableCell> |
| 234 | + </TableRow> |
| 235 | + ); |
| 236 | + })} |
| 237 | + </TableBody> |
| 238 | + </Table> |
| 239 | + )} |
| 240 | + </CardContent> |
246 | 241 |
|
247 | | - {totalPages > 1 && ( |
248 | | - <div className="flex items-center justify-between px-5 py-3 border-t border-slate-100"> |
249 | | - <span className="text-xs text-slate-500">Page {page + 1} of {totalPages}</span> |
250 | | - <div className="flex gap-2"> |
251 | | - <Button variant="outline" size="sm" disabled={page === 0} onClick={() => setPage((p) => p - 1)}> |
252 | | - Previous |
253 | | - </Button> |
254 | | - <Button variant="outline" size="sm" disabled={page + 1 >= totalPages} onClick={() => setPage((p) => p + 1)}> |
255 | | - Next |
256 | | - </Button> |
257 | | - </div> |
| 242 | + {totalPages > 1 && ( |
| 243 | + <div className="flex items-center justify-between px-5 py-3 border-t border-slate-100"> |
| 244 | + <span className="text-xs text-slate-500">Page {page + 1} of {totalPages}</span> |
| 245 | + <div className="flex gap-2"> |
| 246 | + <Button variant="outline" size="sm" disabled={page === 0} onClick={() => setPage((p) => p - 1)}> |
| 247 | + Previous |
| 248 | + </Button> |
| 249 | + <Button variant="outline" size="sm" disabled={page + 1 >= totalPages} onClick={() => setPage((p) => p + 1)}> |
| 250 | + Next |
| 251 | + </Button> |
258 | 252 | </div> |
259 | | - )} |
260 | | - </Card> |
261 | | - </div> |
| 253 | + </div> |
| 254 | + )} |
| 255 | + </Card> |
262 | 256 |
|
263 | 257 | <Dialog |
264 | 258 | open={createOpen} |
265 | 259 | onClose={() => { setCreateOpen(false); reset(); }} |
266 | 260 | title="New Booking" |
267 | 261 | description="Create a booking for this tenant." |
268 | 262 | > |
269 | | - <form onSubmit={handleSubmit((v) => createMutation.mutate(v))} className="space-y-4"> |
| 263 | + <form |
| 264 | + onSubmit={handleSubmit((v) => |
| 265 | + createMutation.mutate({ |
| 266 | + ...v, |
| 267 | + slotStart: new Date(v.slotStart).toISOString(), |
| 268 | + slotEnd: new Date(v.slotEnd).toISOString(), |
| 269 | + }) |
| 270 | + )} |
| 271 | + className="space-y-4" |
| 272 | + > |
270 | 273 | <Input label="Customer Reference" placeholder="cust_12345" error={errors.customerRef?.message} {...register("customerRef")} /> |
271 | 274 | <Input label="Service Reference" placeholder="svc_abc" error={errors.serviceRef?.message} {...register("serviceRef")} /> |
272 | 275 | <Input label="Slot Start" type="datetime-local" error={errors.slotStart?.message} {...register("slotStart")} /> |
@@ -306,6 +309,20 @@ export default function TenantBookingsPage({ |
306 | 309 | destructive |
307 | 310 | loading={cancelMutation.isPending} |
308 | 311 | /> |
| 312 | + </div> |
| 313 | + ); |
| 314 | +} |
| 315 | + |
| 316 | +export default function TenantBookingsPage({ |
| 317 | + params, |
| 318 | +}: { |
| 319 | + params: Promise<{ id: string }>; |
| 320 | +}) { |
| 321 | + return ( |
| 322 | + <Shell> |
| 323 | + <Suspense fallback={<PageSpinner />}> |
| 324 | + <TenantBookingsContent params={params} /> |
| 325 | + </Suspense> |
309 | 326 | </Shell> |
310 | 327 | ); |
311 | 328 | } |
0 commit comments