Files
Ghost_Kitchen/src/components/orders/OrdersListView.tsx
T
lukasandClaude Sonnet 5 c8d1721358 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>
2026-09-10 23:14:28 +02:00

83 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)