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:
2026-09-10 23:14:28 +02:00
co-authored by Claude Sonnet 5
parent 18a7d2030e
commit c8d1721358
37 changed files with 2224 additions and 24 deletions
+174
View File
@@ -10,12 +10,20 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AdminRouteImport } from './routes/admin'
import { Route as BasketRouteImport } from './routes/basket'
import { Route as LoginRouteImport } from './routes/login'
import { Route as SignupRouteImport } from './routes/signup'
import { Route as AdminIndexRouteImport } from './routes/admin/index'
import { Route as AdminKitchenRouteImport } from './routes/admin/kitchen'
import { Route as ItemsMenuItemIdRouteImport } from './routes/items/$menuItemId'
import { Route as OrdersIndexRouteImport } from './routes/orders/index'
import { Route as OrdersOrderIdRouteImport } from './routes/orders/$orderId'
import { Route as AdminMenuIndexRouteImport } from './routes/admin/menu/index'
import { Route as AdminMenuMenuItemIdRouteImport } from './routes/admin/menu/$menuItemId'
import { Route as AdminMenuNewRouteImport } from './routes/admin/menu/new'
import { Route as AdminOrdersIndexRouteImport } from './routes/admin/orders/index'
import { Route as AdminOrdersOrderIdRouteImport } from './routes/admin/orders/$orderId'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
const IndexRoute = IndexRouteImport.update({
@@ -23,6 +31,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AdminRoute = AdminRouteImport.update({
id: '/admin',
path: '/admin',
getParentRoute: () => rootRouteImport,
} as any)
const BasketRoute = BasketRouteImport.update({
id: '/basket',
path: '/basket',
@@ -38,6 +51,16 @@ const SignupRoute = SignupRouteImport.update({
path: '/signup',
getParentRoute: () => rootRouteImport,
} as any)
const AdminIndexRoute = AdminIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => AdminRoute,
} as any)
const AdminKitchenRoute = AdminKitchenRouteImport.update({
id: '/kitchen',
path: '/kitchen',
getParentRoute: () => AdminRoute,
} as any)
const ItemsMenuItemIdRoute = ItemsMenuItemIdRouteImport.update({
id: '/items/$menuItemId',
path: '/items/$menuItemId',
@@ -53,6 +76,31 @@ const OrdersOrderIdRoute = OrdersOrderIdRouteImport.update({
path: '/orders/$orderId',
getParentRoute: () => rootRouteImport,
} as any)
const AdminMenuIndexRoute = AdminMenuIndexRouteImport.update({
id: '/menu/',
path: '/menu/',
getParentRoute: () => AdminRoute,
} as any)
const AdminMenuMenuItemIdRoute = AdminMenuMenuItemIdRouteImport.update({
id: '/menu/$menuItemId',
path: '/menu/$menuItemId',
getParentRoute: () => AdminRoute,
} as any)
const AdminMenuNewRoute = AdminMenuNewRouteImport.update({
id: '/menu/new',
path: '/menu/new',
getParentRoute: () => AdminRoute,
} as any)
const AdminOrdersIndexRoute = AdminOrdersIndexRouteImport.update({
id: '/orders/',
path: '/orders/',
getParentRoute: () => AdminRoute,
} as any)
const AdminOrdersOrderIdRoute = AdminOrdersOrderIdRouteImport.update({
id: '/orders/$orderId',
path: '/orders/$orderId',
getParentRoute: () => AdminRoute,
} as any)
const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
id: '/api/auth/$',
path: '/api/auth/$',
@@ -61,70 +109,117 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/admin': typeof AdminRouteWithChildren
'/basket': typeof BasketRoute
'/login': typeof LoginRoute
'/signup': typeof SignupRoute
'/admin/kitchen': typeof AdminKitchenRoute
'/items/$menuItemId': typeof ItemsMenuItemIdRoute
'/orders/$orderId': typeof OrdersOrderIdRoute
'/admin/': typeof AdminIndexRoute
'/orders/': typeof OrdersIndexRoute
'/admin/menu/$menuItemId': typeof AdminMenuMenuItemIdRoute
'/admin/menu/new': typeof AdminMenuNewRoute
'/admin/orders/$orderId': typeof AdminOrdersOrderIdRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/admin/menu/': typeof AdminMenuIndexRoute
'/admin/orders/': typeof AdminOrdersIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/basket': typeof BasketRoute
'/login': typeof LoginRoute
'/signup': typeof SignupRoute
'/admin/kitchen': typeof AdminKitchenRoute
'/items/$menuItemId': typeof ItemsMenuItemIdRoute
'/orders/$orderId': typeof OrdersOrderIdRoute
'/admin': typeof AdminIndexRoute
'/orders': typeof OrdersIndexRoute
'/admin/menu/$menuItemId': typeof AdminMenuMenuItemIdRoute
'/admin/menu/new': typeof AdminMenuNewRoute
'/admin/orders/$orderId': typeof AdminOrdersOrderIdRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/admin/menu': typeof AdminMenuIndexRoute
'/admin/orders': typeof AdminOrdersIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/admin': typeof AdminRouteWithChildren
'/basket': typeof BasketRoute
'/login': typeof LoginRoute
'/signup': typeof SignupRoute
'/admin/kitchen': typeof AdminKitchenRoute
'/items/$menuItemId': typeof ItemsMenuItemIdRoute
'/orders/$orderId': typeof OrdersOrderIdRoute
'/admin/': typeof AdminIndexRoute
'/orders/': typeof OrdersIndexRoute
'/admin/menu/$menuItemId': typeof AdminMenuMenuItemIdRoute
'/admin/menu/new': typeof AdminMenuNewRoute
'/admin/orders/$orderId': typeof AdminOrdersOrderIdRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/admin/menu/': typeof AdminMenuIndexRoute
'/admin/orders/': typeof AdminOrdersIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/admin'
| '/basket'
| '/login'
| '/signup'
| '/admin/kitchen'
| '/items/$menuItemId'
| '/orders/$orderId'
| '/admin/'
| '/orders/'
| '/admin/menu/$menuItemId'
| '/admin/menu/new'
| '/admin/orders/$orderId'
| '/api/auth/$'
| '/admin/menu/'
| '/admin/orders/'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/basket'
| '/login'
| '/signup'
| '/admin/kitchen'
| '/items/$menuItemId'
| '/orders/$orderId'
| '/admin'
| '/orders'
| '/admin/menu/$menuItemId'
| '/admin/menu/new'
| '/admin/orders/$orderId'
| '/api/auth/$'
| '/admin/menu'
| '/admin/orders'
id:
| '__root__'
| '/'
| '/admin'
| '/basket'
| '/login'
| '/signup'
| '/admin/kitchen'
| '/items/$menuItemId'
| '/orders/$orderId'
| '/admin/'
| '/orders/'
| '/admin/menu/$menuItemId'
| '/admin/menu/new'
| '/admin/orders/$orderId'
| '/api/auth/$'
| '/admin/menu/'
| '/admin/orders/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AdminRoute: typeof AdminRouteWithChildren
BasketRoute: typeof BasketRoute
LoginRoute: typeof LoginRoute
SignupRoute: typeof SignupRoute
@@ -143,6 +238,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/admin': {
id: '/admin'
path: '/admin'
fullPath: '/admin'
preLoaderRoute: typeof AdminRouteImport
parentRoute: typeof rootRouteImport
}
'/basket': {
id: '/basket'
path: '/basket'
@@ -164,6 +266,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SignupRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/': {
id: '/admin/'
path: '/'
fullPath: '/admin/'
preLoaderRoute: typeof AdminIndexRouteImport
parentRoute: typeof AdminRoute
}
'/admin/kitchen': {
id: '/admin/kitchen'
path: '/kitchen'
fullPath: '/admin/kitchen'
preLoaderRoute: typeof AdminKitchenRouteImport
parentRoute: typeof AdminRoute
}
'/items/$menuItemId': {
id: '/items/$menuItemId'
path: '/items/$menuItemId'
@@ -185,6 +301,41 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof OrdersOrderIdRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/menu/': {
id: '/admin/menu/'
path: '/menu'
fullPath: '/admin/menu/'
preLoaderRoute: typeof AdminMenuIndexRouteImport
parentRoute: typeof AdminRoute
}
'/admin/menu/$menuItemId': {
id: '/admin/menu/$menuItemId'
path: '/menu/$menuItemId'
fullPath: '/admin/menu/$menuItemId'
preLoaderRoute: typeof AdminMenuMenuItemIdRouteImport
parentRoute: typeof AdminRoute
}
'/admin/menu/new': {
id: '/admin/menu/new'
path: '/menu/new'
fullPath: '/admin/menu/new'
preLoaderRoute: typeof AdminMenuNewRouteImport
parentRoute: typeof AdminRoute
}
'/admin/orders/': {
id: '/admin/orders/'
path: '/orders'
fullPath: '/admin/orders/'
preLoaderRoute: typeof AdminOrdersIndexRouteImport
parentRoute: typeof AdminRoute
}
'/admin/orders/$orderId': {
id: '/admin/orders/$orderId'
path: '/orders/$orderId'
fullPath: '/admin/orders/$orderId'
preLoaderRoute: typeof AdminOrdersOrderIdRouteImport
parentRoute: typeof AdminRoute
}
'/api/auth/$': {
id: '/api/auth/$'
path: '/api/auth/$'
@@ -195,8 +346,31 @@ declare module '@tanstack/react-router' {
}
}
interface AdminRouteChildren {
AdminKitchenRoute: typeof AdminKitchenRoute
AdminIndexRoute: typeof AdminIndexRoute
AdminMenuMenuItemIdRoute: typeof AdminMenuMenuItemIdRoute
AdminMenuNewRoute: typeof AdminMenuNewRoute
AdminOrdersOrderIdRoute: typeof AdminOrdersOrderIdRoute
AdminMenuIndexRoute: typeof AdminMenuIndexRoute
AdminOrdersIndexRoute: typeof AdminOrdersIndexRoute
}
const AdminRouteChildren: AdminRouteChildren = {
AdminKitchenRoute: AdminKitchenRoute,
AdminIndexRoute: AdminIndexRoute,
AdminMenuMenuItemIdRoute: AdminMenuMenuItemIdRoute,
AdminMenuNewRoute: AdminMenuNewRoute,
AdminOrdersOrderIdRoute: AdminOrdersOrderIdRoute,
AdminMenuIndexRoute: AdminMenuIndexRoute,
AdminOrdersIndexRoute: AdminOrdersIndexRoute,
}
const AdminRouteWithChildren = AdminRoute._addFileChildren(AdminRouteChildren)
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AdminRoute: AdminRouteWithChildren,
BasketRoute: BasketRoute,
LoginRoute: LoginRoute,
SignupRoute: SignupRoute,