Skip to content

Table Ordering ​

What it does ​

A customer sits down, scans the QR code printed on their table, and lands on a header naming the venue — its logo and name — with the table label plus its floor number underneath (e.g. "Meja 3 · Lantai 2"), so a guest can check both whose app they're in and that they're at the table the app thinks they are, even in a multi-floor venue. From there they browse categories, tap an item to pick its options (size, ice level, whatever the product defines), add a note, set a quantity, and add it to a cart. A floating bar keeps the running item count and total visible on every screen, genuinely pinned to the bottom of the viewport rather than the bottom of the page — including after the page has scrolled far enough for a phone browser's URL bar to collapse. A thin progress bar acknowledges every tap between screens, so a slow connection never leaves a button looking unresponsive. The cart survives closing the browser tab or reloading the page, so a customer who steps away and comes back later finds it exactly as they left it.

Each menu item shows only its short, customer-facing description — never the staff-only preparation recipe, which the public API never returns (see Products for that split). Once something is in the cart, a customer can edit a line's quantity and note at any time before checkout, without deleting and re-adding the item; the variant itself (the size or option combination already chosen) can't be changed from that edit screen — picking a different one still means removing the line and adding the item again.

This ships the discovery-and-cart half of the flow. The cart page's own footer button pays with QRIS directly — there's no separate checkout page recapping the same cart — and lands the guest on an order-status page that doubles as the QR page, reloadable at any point until it's paid; see Order Checkout (QRIS) for that half. Every order a guest has paid for from this session, across every table, is one tap away from a header button on every screen — see Order History.

Why it matters ​

At peak hours the counter is the bottleneck: customers queue to order, staff key items in one at a time, and a single cashier serializes the whole room. Letting customers browse and build their own order from the table — the model popularized by apps like pesan.app — moves that work off the counter entirely, so staff can focus on payment and fulfilment instead of order-taking.

It also had to be built without inventing a second product. The customer-facing app reuses the same Categories, Products and Variants the counter staff already manage — a menu item published for the till is automatically orderable from the table, with no separate customer-menu data entry.

Key capabilities ​

  • QR-per-table, not a typed number — each table gets a non-guessable, random 10-character code (not a sequential "Table 3"), so a customer can't order from off-premise by guessing a neighboring number. Staff manage the table list and print its QR from a dedicated admin screen.
  • No login, no install — identity is a random ID minted in the browser on first visit and stored locally; that's the entire "account."
  • Cart that survives a reload — the cart lives server-side, keyed to that anonymous session, so refreshing the page, closing the tab, or coming back later never loses it.
  • Same variant/option model as the counter — picking a size or an ice level resolves to a specific priced variant exactly the way staff resolve one on the POS checkout screen.
  • Mencari menu — search by more than a name — the menu search box (placeholder "Cari menu atau varian") matches a product's name, its category, and any option value it carries, so typing "earl grey" or "besar" finds the drink even when the guest doesn't know what the product itself is called. A matching card shows a chip naming what matched, and opening it can preselect that option in the detail sheet when the match is unambiguous.
  • Prices are never trusted from the phone — every price, subtotal and total is computed server-side from the catalog at read time, never accepted from the client.
  • Smart line merging — adding the same variant with the same note again increases its quantity instead of creating a duplicate line; a different note stays its own line, since the kitchen treats it differently.
  • Edit a cart line's quantity and note — every cart line has an edit button that opens a modal with the amount and note editable and the chosen variant shown read-only; saving updates the line without leaving the cart. Changing the variant itself still means deleting the line and re-adding the item.
  • Table floor shown in the header — the resolved table's header shows its floor number alongside the label (e.g. "Lantai 2"), so a guest in a multi-floor venue can confirm they scanned the right table's QR.
  • Branded on every screen — a persistent header with the venue's logo and name sits above the table line on every screen, including the QR-resolution error states, so a guest never wonders whose app they're in.
  • A menu that never looks broken — items without a photo or a description get a real placeholder (a category-appropriate icon), not a missing image or a collapsed layout.
  • Bahasa Indonesia, mobile-first — the customer app is copy-in-Indonesian, single-column, designed for a phone screen, independent of the English, desktop-oriented POS.

For engineers ​

  • Customer app: apps/order-web — a Next.js app (Pages Router, same framework as the apps/pos-web POS), deployed on its own Vercel project; a permanent redirect shim on this docs site sends already-printed QR codes from the old GitHub Pages origin to it (docs/trd-order-app-nextjs-migration.md, D3)
  • Route entry: /t/{code} (menu, ?product={id} opens the item detail sheet), /t/{code}/cart (cart — recap, edit lines via ?item={id}, and pay), /t/{code}/status?ref={reference} (payment/order status, see Order Checkout (QRIS)), each a server-rendered getServerSideProps page mounting one composition root — no separate routes for the overlays, so opening one is not a navigation (docs/trd-order-app-composition-and-ssr.md, D6). /t/{code}/checkout is a permanent redirect to /t/{code}/cart (apps/order-web/next.config.js), kept so a QR code or bookmark printed before that route was retired still lands somewhere useful.
  • Frontend logic, reached through the @gatherloop-pos/ui/order entry point so the customer bundle never pulls in POS/Next code: libs/ui/src/domain/usecases/{menuList,menuItemDetail,cart}.ts, repositories in libs/ui/src/data/{api,mock,session,url}
  • Anonymous session: libs/ui/src/data/session/CookieSessionRepository.ts (gl_session_id cookie + localStorage, sent as the X-Session-Id header) — resolved identically on the server and the client from the same cookie, so the menu and table render in the initial server response
  • Backend public catalog: apps/api/presentation/restapi/public_route.go — unauthenticated GET /public/{categories,products,variants,tables/{code}}, stripping cost data (materials, pricingTiers, recipe) and restricting to published, purchasable items
  • Backend cart: apps/api/domain/cart_usecase.go, routes under /carts/current*, migrations 000019_create_tables and 000020_create_carts; PUT /carts/items/{cartItemId} (amount + note only — the variant is immutable once added)
  • Table admin (staff-facing): libs/ui/src/presentation/screens/Table{List,Create}Screen.tsx, apps/api/domain/table_usecase.go (random Crockford-base32 code generation, regenerate-code to invalidate a leaked QR); floor_number (migration 000021_add_table_floor_number) is exposed on PublicTable and rendered in TableResolveScreen.tsx
  • Design doc: docs/prd-table-ordering.md — the full set of decisions, including why the catalog needed a new unauthenticated route group (D1), why table codes are random rather than sequential (D6), and what's explicitly deferred to the next project (turning a cart into a transaction, QRIS payment integration, order status)
  • Follow-up design doc: docs/prd-order-app-ux-improvements.md — the floating cart bar fix, the description/recipe split, the floor-number header, and the cart-line edit modal covered above
  • Follow-up design doc: docs/prd-order-checkout-qris-doku.md — turns the cart this page builds into a paid Transaction via QRIS; see Order Checkout (QRIS)
  • Follow-up design doc: docs/prd-order-app-ux-round-2.md — the route-change progress bar, the shell/footer fix behind "genuinely pinned", moving checkout onto this page (retiring /checkout), and the venue brand header covered above

Built with VitePress. Content lives in docs-site/ — see the README for how to run it locally.