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>
83 lines
2.4 KiB
TypeScript
83 lines
2.4 KiB
TypeScript
import { Link } from '@tanstack/react-router'
|
||
|
||
import type { CustomerOrderSummary } from '#/domain/ordering.ts'
|
||
import { formatPrice } from '#/lib/format.ts'
|
||
|
||
import styles from './OrdersListView.module.scss'
|
||
|
||
type OrdersListViewProps = {
|
||
orders: Array<CustomerOrderSummary>
|
||
}
|
||
|
||
const OrdersIcon = () => (
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="1.5"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
aria-hidden="true"
|
||
>
|
||
<path d="M9 3h6l1 3H8l1-3Z" />
|
||
<path d="M4 6h16l-1.2 13.2A2 2 0 0 1 16.81 21H7.19a2 2 0 0 1-2-1.8L4 6Z" />
|
||
<path d="M9 11h6" />
|
||
</svg>
|
||
)
|
||
|
||
const formatItems = (items: CustomerOrderSummary['items']) =>
|
||
items
|
||
.map(({ menuItemName, quantity }) =>
|
||
quantity > 1 ? `${quantity}× ${menuItemName}` : menuItemName,
|
||
)
|
||
.join(', ')
|
||
|
||
export const OrdersListView = ({ orders }: OrdersListViewProps) => (
|
||
<section className={styles.page}>
|
||
<h1 className={styles.heading}>Your orders</h1>
|
||
{orders.length === 0 ? (
|
||
<div className={styles.empty}>
|
||
<OrdersIcon />
|
||
<p className={styles.emptyTitle}>No orders yet</p>
|
||
<p className={styles.emptyText}>
|
||
Your placed orders will show up here.
|
||
</p>
|
||
<Link to="/" className={styles.emptyCta}>
|
||
Browse the menu
|
||
</Link>
|
||
</div>
|
||
) : (
|
||
<ul className={styles.orders}>
|
||
{orders.map((order) => (
|
||
<li key={order.id}>
|
||
<Link
|
||
to="/orders/$orderId"
|
||
params={{ orderId: String(order.id) }}
|
||
search={
|
||
order.guestAccessToken
|
||
? { token: order.guestAccessToken }
|
||
: undefined
|
||
}
|
||
className={styles.order}
|
||
style={{ viewTransitionName: `order-card-${order.id}` }}
|
||
>
|
||
<div className={styles.orderHeader}>
|
||
<span className={styles.orderName}>Order #{order.id}</span>
|
||
<span className={styles.orderStatus}>{order.status}</span>
|
||
</div>
|
||
<p className={styles.orderItems}>{formatItems(order.items)}</p>
|
||
<p className={styles.orderMeta}>
|
||
{order.createdAt.toLocaleDateString([], {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
})}{' '}
|
||
· {formatPrice(order.totalCents)}
|
||
</p>
|
||
</Link>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
)
|