Foodops
All docs

Back Office · Updated 2026-08-18 · vf2decf4

Dashboard

Audio narration for this article is generated with the weekly docs refresh.

Plain-English summary

The Dashboard is Foodops's live sales insights screen. For a chosen date and outlet it shows six headline numbers — Total Sales, Total Payments, Orders, Customers, Discounts, Cancelled — plus an hourly sales chart, a breakdown of orders by type (dine-in / takeout / delivery), a breakdown of payments by method, and the top five selling items. A manager opens it at the start or end of a shift to get an at-a-glance pulse of how the day is going, and can flip the date back to compare earlier days.

When you'd use this

  • Starting the day — quick read on how service is tracking
  • End-of-shift — total takings, payment mix, peak hours
  • Spotting anomalies — unusual order volume, heavy discounting, cancellations
  • Comparing one outlet against another (switch the outlet filter)

There are now two dashboards, both on the NEW stack. The legacy app.foodops.io/dashboards (Razor monolith) is retired at the July-2026 launch.

  • Counter dashboard — URL: /dashboard on the counter app (counter.foodops.io). [code: foodops.counter.app/src/App.tsx:162-165]
  • Admin dashboard — URL: /dashboard on the admin app (admin.foodops.io). For the foodops brand this resolves to the admin DashboardPage — the Finance dashboard (FinanceDashboardPage) is tree-shaken out of the foodops build, so foodops never sees the finance variant. [code: foodops-admin-app/apps/admin/src/App.tsx:36-48 — DASHBOARD_BY_BRAND = { finance: FinanceDashboardPage, salesmade: SalesmadeDashboardPage, foodops: DashboardPage }]
  • Counter sidebar: a "Dashboard" rail entry is mapped in the counter sidebar code, but the sidebar's allowed-items filter only renders Counter (relabelled "Orders" for foodops), Dine-In, Deliveries, Takeouts and Reports for the foodops brand — 'Dashboard' is not in the allowedItems whitelist, so the rail entry only appears if hoisted some other way. The route itself is always reachable by URL. [code: foodops.counter.app/src/components/layout/Sidebar.tsx:190-192 — allowedItems = getBrand().id === 'salesmade' ? ['Counter','Reports'] : ['Counter','Dine-In','Deliveries','Takeouts','Reports']]

Walkthrough — reading the dashboard

Layout and labels below are taken directly from the page/component source at foodops.counter.app@32669a9. A live screenshot pass is still pending.

  1. Header row — page title Dashboard, a Refresh button (spinning icon while refreshing, "Dashboard refreshed" toast on success), and the organization selector. [code: foodops.counter.app/src/pages/DashboardPage.tsx:185-200]
  2. Filter row — a date picker (defaults to today) and an outlet selector. Changing either reloads all data. Leaving the outlet unset shows organisation-wide numbers. [code: src/pages/DashboardPage.tsx:202-213]
  3. Stats grid — six KPI cards rendered by DashboardStats: Total Sales, Total Payments, Orders, Customers, Discounts, Cancelled. Money cards are prefixed with the org currency code. [code: src/components/dashboard/DashboardStats.tsx:31-77]
  4. Sales chart — full-width card titled Sales, plotting the day's sales by hour. Shows "No sales data available" when the day is empty. [code: src/components/dashboard/SalesChart.tsx]
  5. Bottom grid — three cards: Order Type Summary (sales count per order type), Payment Types Summary (per payment method), and Top Selling Items (top 5 products by sales; the limit: 5 is set by the page). [code: src/pages/DashboardPage.tsx:137,228-248; src/components/dashboard/OrderTypeSummary.tsx; PaymentSummary.tsx; TopProducts.tsx:19]

All five data sets are fetched in parallel from the sales API: /ClientAppDashboard/{daySummary, topProducts, paymentSummary, dailySales, orderTypeSummary}. [code: foodops.counter.app/src/services/dashboard/dashboardAPIService.ts; hubits-api-sales/Hubits.Sales.Api/Controllers/ClientAppDashboardController.cs:173,264,301,362]

Key concepts

  • Business-day range — "today" on the dashboard is not midnight-to-midnight: when an outlet is selected, the server resolves the outlet's configured business-day window (OutletHelper.GetOutletBusinessDayRangeAsync) so late-night trade lands on the correct day; without an outlet a fallback range is used. [code: hubits-api-sales/Hubits.Sales.Api/Controllers/ClientAppDashboardController.cs:48-54]
  • Previous-day comparisondaySummary also computes the previous day's figures (a second GenerateReport over the prior business-day window) and returns a …Difference field per KPI, so the DTO carries day-over-day deltas. The counter dashboard does not render them; the admin foodops dashboard does (see Edge cases). [code: ClientAppDashboardController.cs:217-230 — BuildDaySummaryAsync runs current + previous reports]
  • Open-session scoping — when an outlet is selected, the server looks up that outlet's currently open register sessions; with no outlet specified it falls back to pure date-range logic. [code: ClientAppDashboardController.cs:61-68]
  • Server-side caching — dashboard responses are cached per org/date/outlet via HybridCache when caching is enabled, so two devices refreshing in the same window may see momentarily identical (cached) numbers. [code: ClientAppDashboardController.cs:179-188]
  • Both new dashboards share one APIClientAppDashboardController (/api/ClientAppDashboard/...) serves both the counter dashboard and the admin foodops DashboardPage. (DashboardController (/Dashboard/...) is a parallel legacy-era endpoint set.) The counter calls the five widget endpoints in parallel (daySummary, topProducts, paymentSummary, dailySales, orderTypeSummary); the admin foodops dashboard uses the newer single round-trip bundle endpoint (/ClientAppDashboard/bundle) plus salesComparison. The controller also serves activeOrders and analyticsSummary. [code: hubits-api-sales ClientAppDashboardController.cs:198,233,258,284,309,414,492,829; foodops-admin-app/apps/admin/src/services/dashboardService.ts:48-89]

Common questions

Q: What do the six KPI tiles show? A: Total Sales (order value), Total Payments (amount actually paid), Orders (count), Customers (unique customers), Discounts (total discount value), Cancelled (cancelled order count) — all for the selected date and outlet. [code: foodops.counter.app/src/components/dashboard/DashboardStats.tsx:31-77]

Q: Why do Total Sales and Total Payments differ? A: Total Sales counts what was ordered; Total Payments counts what was paid in the window. Unpaid/credit orders, or payments landing in a different business-day window than their order, create gaps between the two.

Q: Can I see a different day than today? A: Yes — the date picker defaults to today but accepts any date; all cards reload for the chosen date.

Q: Can I filter by outlet? A: Yes — the outlet selector reloads every card scoped to that outlet. With no outlet selected you see organisation-wide totals.

Q: Why did my late-night orders show up on the "wrong" day? A: They probably didn't — the dashboard uses the outlet's business-day window, not calendar midnight. An order at 1 AM can belong to the previous business day if the outlet's day closes later. [code: hubits-api-sales ClientAppDashboardController.cs:48-54]

Q: The numbers look stale after I just closed an order. Why? A: Two reasons: the page only refetches on filter change or the Refresh button (no SignalR auto-refresh on this page), and the server may serve a cached response for a short window. Tap Refresh.

Q: How many products does "Top Selling Items" show? A: Five — the page requests limit: 5. The API supports other limits but the UI doesn't expose a control. [code: DashboardPage.tsx:137; hubits-api-sales DashboardController.cs:112]

Q: Does the dashboard show profit or expenses? A: Not on the counter dashboard. An analyticsSummary endpoint that nets expenses/bills against sales exists in the sales API; the richer money view (MoneyFlow / Operations Pulse) lives on the admin foodops dashboard (admin.foodops.io), and full P&L lives in the Finance app — a separate subscription/build that is tree-shaken out of the foodops product. [code: hubits-api-sales ClientAppDashboardController.cs:414-490]

Q: Is there a mobile layout? A: Yes — the page got explicit mobile optimisations (commit dc74f72, 2026-04-27); the stats grid collapses from 6 columns to 1-2 on small viewports, and the three bottom cards stack.

Q: Who can see the dashboard? A: Any authenticated counter-app user can reach /dashboard (it sits behind the standard ProtectedRoute only — no extra role gate found in current code as of 2026-06-11). [code: foodops.counter.app/src/App.tsx:125-131]

Q: Where did the old web dashboard go? A: The Razor monolith dashboard (app.foodops.io/dashboards) is retired as of the July-2026 launch. There are now two replacement surfaces: the counter app's /dashboard (counter.foodops.io) and the admin app's /dashboard (admin.foodops.io).

Q: Is the admin (back-office) dashboard the same as the counter one? A: Same data, different layout. The admin foodops DashboardPage (admin.foodops.io) shows a stat band — Total Sales, Orders, Customers, Avg Ticket — each with a "vs previous" delta, plus the Sales chart, an Operations Pulse panel, Money Flow, Order Mix, Payment Methods, and a ranked Top Products list. It reads the same /api/ClientAppDashboard/* data (via the single bundle endpoint). The counter /dashboard is the on-the-floor quick read; the admin dashboard is the manager's back-office view. [code: foodops-admin-app/apps/admin/src/pages/DashboardPage.tsx]

Edge cases and known issues

  • No data for the selected day — the Sales chart shows "No sales data available", Order Type shows "No order data available", Payments "No payment data available". This is expected for closed days or future dates.
  • Dashboard rail entry may be absent — the sidebar whitelist (allowedItems) excludes 'Dashboard' for both brands at 32669a9; users may need the direct URL. Verify-and-note: not confirmed against the live nav API payload. [code: foodops.counter.app/src/components/layout/Sidebar.tsx:190-192]
  • Difference fields — counter vs admin — the API computes day-over-day deltas for every KPI. The counter DashboardStats grid renders only the value (the deltas stay dormant payload). The admin foodops DashboardPage does surface them: each stat-band cell shows "<x>% vs previous" (driven by totalSalesDifference, ordersDifference, customersDifference). [code: foodops-admin-app/apps/admin/src/pages/DashboardPage.tsx:107-145]
  • Unclosed-session robustness (was KNOWN ISSUE #23 on the retired app) — the legacy app.foodops.io/dashboard threw an error boundary when an outlet had an unclosed register session. The new dashboards do not have this problem: the KPI endpoints (daySummary, topProducts, paymentSummary, orderTypeSummary, dailySales, bundle) read by the outlet's business-day range (GetBusinessDayRangeAsyncOutletHelper.GetOutletBusinessDayRangeAsync), independent of session open/closed state, and the session-scoped "Right Now" endpoints (activeOrders) explicitly return an empty list rather than erroring when no session is open. An unclosed session no longer breaks either dashboard. [code: hubits-api-sales ClientAppDashboardController.cs:81-86,94-105,133-152 — if (sessionIds.Count == 0) return new List<SalesOrder>();]
  • Card-height jitter — the three bottom cards sync their heights via a requestAnimationFrame measurement; on slow devices a brief height flicker after load is cosmetic, not a data problem. [code: DashboardPage.tsx:53-89]

Related

What changed since 2026-04-15 (NEW 2026-05)

DashboardPage.tsx and src/components/dashboard/** (counter.app) had a small touch this window:

  • Mobile optimisations (2026-04-27) — DashboardPage now responds better to small viewports (dc74f72).
  • Design updates (2026-04-24) — broader visual refresh of the dashboard alongside the rest of the app (73b53ef).

The component composition in src/components/dashboard/ maps to the documented layout: DashboardStats.tsx, OrderTypeSummary.tsx, PaymentSummary.tsx, SalesChart.tsx, StatsCard.tsx, TopProducts.tsx.

Legacy monolith Dashboard views (Haafai.Restaurant.App/Views/Dashboard/*, Haafai.Restaurant.App/Views/Dashboards/*, app.foodops.io/dashboards) are RETIRED at the July-2026 launch — the canonical dashboard surfaces are now the counter app's /dashboard and the admin app's foodops /dashboard.

  • 2026-08-03 — codebase (hubits-api-sales df4179d..ebe7330, ClientAppDashboardController.cs): fixed a cache-TTL staleness bug in the range-based endpoints this entry cites (analyticsSummary, salesComparison, and the related topItemPerformance, which sits alongside them but isn't yet documented here). DashboardOptions' freshness bucketing previously keyed off the range's START date; for month-to-date / last-N-days views (which always end today), that misclassified the query as 'historical' (1hr TTL) instead of 'live' (60s TTL), so a range including today could show up to an hour-stale numbers. GetAnalyticsSummary, GetSalesComparison, and GetTopItemPerformance now compute the cache-bucket date from endDate instead of startDate, so a range ending today gets the same live-freshness treatment as the single-day dashboard. This does not change endpoint routes, response shape, or the general per-org/date/outlet HybridCache model already documented under 'Server-side caching' — it only tightens freshness for range queries that include today, and doesn't touch the daySummary-driven counter/admin KPI tiles (which already keyed on a single date correctly). Unrelated commits in this range (alcyon fix, KDS open-bucket behavior) don't intersect the dashboard surface. (auto-applied by the truth pipeline; adversarially gated)
  • 2026-08-03 — codebase (foodops.counter.app 2e88679..f52b6dc, Sidebar.tsx): counter-app sidebar rows marked item.external (admin-bound links) no longer navigate immediately via window.open; clicking now routes through a new AdminAppRedirectDialog (gated by shouldConfirmAdminNav()/openAdminNav() from @/lib/externalNav), i.e. a 'Continue to Admin' confirmation with a saved open-preference opt-out. Modified/middle-clicks (metaKey/ctrlKey/shiftKey/altKey) are explicitly exempted and keep native new-tab behavior with no dialog. This entry documents the counter app's /dashboard and the separate admin app's /dashboard as two distinct surfaces reached by their own URLs, but does not currently describe any in-app sidebar link that hops from counter to admin — this diff doesn't identify which sidebar item(s) carry external: true or confirm any of them target the admin dashboard route specifically, so it's not clear this dialog sits on the counter→admin-dashboard path documented here. Does not contradict the allowedItems whitelist claim (Sidebar.tsx:190-192), which this diff does not touch. Worth a follow-up pass to confirm which sidebar entries are external and whether any target /dashboard on admin.foodops.io, since a new confirmation step would affect the UX of moving between the two documented dashboards. (auto-applied by the truth pipeline; adversarially gated)
  • 2026-07-21 — codebase (foodops-admin-app 53d3af8..799c61f, admin App.tsx / DashboardPage.tsx): the admin foodops DashboardPage's data-fetch was refactored from manual useState/useEffect (with a requestGenerationRef race guard) to TanStack Query's useQuery, keyed on {reportDate, applicationOrganisationId, outletId, limit}. It still calls the same dashboardService.getBundle single-round-trip endpoint the entry documents — no change to which endpoint or fields are returned. New user-facing effect: per the code comment introduced in this diff, navigating back to a recently-viewed date/outlet combination within a short window (named as ~30s in the commit subject 'TanStack Query client + provider (30s staleness contract)') now renders instantly from client-side cache instead of re-fetching; the old race-condition class (a slow stale response clobbering a fresher one) is structurally eliminated by keying the cache per params rather than by prevented by the old generation counter. Error handling is unchanged in effect (still a toast on failure). Not confirmed in this diff (mentioned only in the commit subject list, not in the reviewed hunks): a 'write-invalidation bridge' that marks all cached queries stale after a successful write — worth a follow-up pass once that file's diff is available, since it would bear on the existing 'numbers look stale after closing an order' Q&A for the admin surface specifically. A dev-only ReactQueryDevtools panel was also added, gated on import.meta.env.DEV — not user-facing in production. (auto-applied by the truth pipeline; adversarially gated)

Warning: Code-verified 2026-05-04 · codebase only (auth.json stale) Verified_by: codebase · Last verified: 2026-05-04 Glob curated to src/pages/DashboardPage.tsx;src/components/dashboard/** (counter.app) and Haafai.Restaurant.App/Views/Dashboard/**;Haafai.Restaurant.App/Views/Dashboards/**;Haafai.Restaurant.App/Controllers/DashboardController.cs;Haafai.Restaurant.App/Controllers/DashboardsController.cs (legacy monolith). Drift detector v2 had this entry as HIGH-rank #18 due to fallback to all 191 counter.app commits — that was a glob mismatch (no src/dashboard/ exists). Curated globs match the real layout (src/pages/DashboardPage.tsx + src/components/dashboard/**). With curated globs, real activity is 2 commits: mobile optimisations + design refresh — both behavior-affecting but minor. Drift score should drop from HIGH (191 attributed) to LOW/MEDIUM (2 real) on the next sweep. Entry remains a stub awaiting playwright walkthrough.

  • 2026-06-29 — codebase (new-stack re-verify for July-10 launch): re-pinned to foodops.counter.app@32669a9 (main), foodops-admin-app@4173811 (main), hubits-api-sales@663dae0 (main); dropped the retired haafai.app.foodops legacy citation. Confirmed counter /dashboard still has six KPI tiles (Total Sales / Total Payments / Orders / Customers / Discounts / Cancelled), limit-5 Top Products, Refresh + "Dashboard refreshed" toast, date+outlet filters, rAF card-height sync (DashboardStats.tsx:33-74, DashboardPage.tsx:137,156). NEW-INFO: documented the second dashboard — the admin foodops DashboardPage (admin.foodops.io); for the foodops brand DASHBOARD_BY_BRAND.foodops = DashboardPage and FinanceDashboardPage is tree-shaken out (admin App.tsx:36-48). The admin dashboard renders a Total Sales/Orders/Customers/Avg-Ticket stat band with the day-over-day "vs previous" deltas (which the counter omits), plus Operations Pulse / Money Flow / Order Mix / Payment Methods / Top Products, and uses the new single-round-trip /ClientAppDashboard/bundle endpoint. KNOWN ISSUE #23 (unclosed-session error boundary): does NOT reproduce on the new stack — KPI endpoints window by outlet business-day range and the session-scoped activeOrders endpoint returns an empty list (not an error) when no session is open (ClientAppDashboardController.cs:94-105,139-152). Legacy app.foodops.io/dashboards marked retired.
  • 2026-06-11 — codebase: promoted stub → draft against foodops.counter.app@4cbcfd7 + hubits-api-sales@5d917e5 (both prod main). Full walkthrough written from component source: six KPI tiles (DashboardStats), hourly Sales chart, Order Type Summary / Payment Types Summary / Top Selling Items (limit 5), date + outlet filters, Refresh. Backend verified: /api/ClientAppDashboard/* endpoints, business-day-range logic, previous-day Difference computation (computed but apparently unrendered), HybridCache caching. CORRECTION vs old stub: counter route is /dashboard (singular) — /dashboards is the legacy monolith URL; sidebar allowedItems whitelist excludes 'Dashboard' at HEAD. UI claims marked UI-TODO pending a Playwright walk; confidence medium (code-grounded, no live screenshots).

Additional verification events are appended here by the weekly KB sync and by manual re-verification passes. See _meta/failure-log.md for the global failure index.

Go beyond POS.
Choose the total solution.

Focus on creating the best experience for your guests while we handle the rest.