Add an admin panel: orders dashboard, menu management, kitchen status
Introduces a role-gated /admin section (sidebar layout, requireAdmin checked both in route loaders and independently in every admin server function) with three sections: - Orders: every order across all customers, and an admin-only order detail view nested under /admin so the sidebar persists and admin access bypasses the guest-token check. - Menu: create/edit/delete menu items, their options, and their preparation steps; deleting an item with past orders is refused (protected by the orderItems FK) instead of crashing. - Kitchen: read-only live view of every machine's busy/idle status and per-tag queue depth, with a manual refresh. Also: - Adds a `role` field to the user table (Better Auth additionalFields, input:false so it can't be self-assigned) and a db:seed-admin script to create/promote an admin account. - Fixes a real bug found while building this: src/server/auth.ts's shared session helpers were plain exported functions using a server-only import, which the framework couldn't tree-shake out of the client bundle once shared across multiple server/*.ts modules — this silently broke hydration app-wide. Fixed with createServerOnlyFn. - Removes the now-fully-unused @tanstack/devtools-vite plugin. - Disables view transitions for in-admin navigation so the persistent sidebar doesn't cross-fade/shift on every click. - Gives the "your orders" empty state the same visual treatment as the basket page's. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -665,6 +665,50 @@ describe('access control', () => {
|
||||
ordering.getOrderStatus(testDb.db, order.id, {}),
|
||||
).rejects.toThrow(ordering.ForbiddenError)
|
||||
})
|
||||
|
||||
it('lets an admin view any Order, guest or account, without a token', async () => {
|
||||
const guestOrder = await placeGuestOrder([
|
||||
{ menuItemId: seed.menuItems.pizza.id },
|
||||
])
|
||||
const accountOrder = await placeAccountOrder(
|
||||
[{ menuItemId: seed.menuItems.burger.id }],
|
||||
'test-customer-1',
|
||||
)
|
||||
|
||||
await expect(
|
||||
ordering.getOrderStatus(testDb.db, guestOrder.id, { isAdmin: true }),
|
||||
).resolves.toMatchObject({ id: guestOrder.id })
|
||||
await expect(
|
||||
ordering.getOrderStatus(testDb.db, accountOrder.id, { isAdmin: true }),
|
||||
).resolves.toMatchObject({ id: accountOrder.id })
|
||||
})
|
||||
})
|
||||
|
||||
describe('admin order listing', () => {
|
||||
it("lists every Order across every customer, with each one's placer", async () => {
|
||||
const guestOrder = await placeGuestOrder([
|
||||
{ menuItemId: seed.menuItems.pizza.id },
|
||||
])
|
||||
const accountOrder = await placeAccountOrder(
|
||||
[{ menuItemId: seed.menuItems.burger.id }],
|
||||
'test-customer-1',
|
||||
)
|
||||
|
||||
const orders = await ordering.getAllOrders(testDb.db)
|
||||
|
||||
expect(orders).toHaveLength(2)
|
||||
const guestSummary = orders.find((order) => order.id === guestOrder.id)
|
||||
const accountSummary = orders.find((order) => order.id === accountOrder.id)
|
||||
expect(guestSummary?.customer).toEqual({
|
||||
type: 'guest',
|
||||
name: 'Ada',
|
||||
contact: 'ada@example.com',
|
||||
})
|
||||
expect(accountSummary?.customer).toEqual({
|
||||
type: 'account',
|
||||
customerId: 'test-customer-1',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('order history', () => {
|
||||
|
||||
Reference in New Issue
Block a user