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 theapps/pos-webPOS), 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-renderedgetServerSidePropspage 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}/checkoutis 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/orderentry point so the customer bundle never pulls in POS/Next code:libs/ui/src/domain/usecases/{menuList,menuItemDetail,cart}.ts, repositories inlibs/ui/src/data/{api,mock,session,url} - Anonymous session:
libs/ui/src/data/session/CookieSessionRepository.ts(gl_session_idcookie +localStorage, sent as theX-Session-Idheader) — 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— unauthenticatedGET /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*, migrations000019_create_tablesand000020_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-codeto invalidate a leaked QR);floor_number(migration000021_add_table_floor_number) is exposed onPublicTableand rendered inTableResolveScreen.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 paidTransactionvia 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