WFDASH-WEBAPP/docs/tv-dashboard.md
Dave 43bbdfc458 feat(tv-dashboard): enhance TV dashboard with driver stops and performance metrics
- Updated the TV dashboard to include a new section for driver stops, displaying active deliveries and pickups with GPS coordinates.
- Refactored the dashboard data structure to incorporate year-over-year performance metrics alongside existing monthly and weekly stats.
- Replaced the supplier map with a driver stops map for improved clarity on driver activities.
- Enhanced the user performance metrics to include detailed counts for quotes and orders processed by each representative.
- Removed the obsolete supplier map component to streamline the dashboard's functionality.
2026-08-19 10:16:49 +02:00

5.6 KiB
Raw Blame History

TV Dashboard — install + use

PIN-gated office TV display showing live orders, quotes, customers, and revenue from PocketBase.

Quick start

  1. Open https://<your-coolify-host>/tv from the TV browser (or any browser pointed at the display).
  2. Type the 6-digit PIN on the on-screen keypad with the TV remote (D-pad + Enter) or a wireless keyboard.
  3. Dashboard lands on /board and stays unlocked for 12 hours.
  4. Tap Lock (top right) to bounce back to the PIN screen.

The PIN lives in .env.local as TV_DASHBOARD_PIN. The session cookie is signed with TV_SESSION_SECRET so rotating that env var invalidates every existing TV session (forces re-PIN).

What it shows

Section Tiles
Period matrix Today / Week / Month counts: orders received, orders placed, quotes received, items processed
Driver stops Map + numbered list of the driver named by TV_DRIVER_ID — both active deliveries (order_items.item_status = "assigned") and supplier pickups (stock_purchase_items.status = "assigned_to_driver"). Stops drop off as he marks them collected in the driver app.
Rep activity Per-rep table with today / week / month × quotes / orders counts. Falls back to customer.rep when quote.rep is empty.

The same getDashboardData() powers the main CRM /dashboard; the TV view adds getTvDashboardData() on top with the day / week / month counters, per-rep quotes, user performance, and driver stops.

How it stays fresh

  • Initial fetch on mount.
  • Polling every 30s as a safety net.
  • PB realtime — subscribes to orders, quotes, order_items, and customers; any change triggers an immediate refetch.

File map

src/app/(tv)/
  layout.tsx                 dark shell, no sidebar
  pin/page.tsx               on-screen PIN keypad (auto-submit on 6 digits)
  board/
    page.tsx                 server guard → if no valid session, redirect to /pin
    tv-view.tsx              client component, all the rendering
    components/
      driver-stops-map.tsx   numbered colour-coded pins per stop
      tv-map-inner.tsx       the actual Leaflet map (browser-only)

src/app/api/
  tv/login/route.ts          POST { pin } → sets tv_session cookie or 401
  tv/logout/route.ts         POST → clears cookie
  dashboard/route.ts         GET → public TV dashboard JSON (no auth)

src/lib/server/
  tv-session.ts              HMAC-signed cookie helpers
  tv-dashboard-service.ts    day/week/month counters, quotesPerRep,
                             userPerformance, driverStops
  driver-stops.ts            shared listDriverStops + update (driver app)

src/middleware.ts            no-op (matcher in place for future)

Setting up a new TV

  1. Open the URL on the TV's browser.
  2. Open Chrome's auto-start settings (or the equivalent on the TV's browser) so the dashboard comes back automatically after a reboot.
  3. Disable screen savers / sleep — the dashboard will stay on indefinitely.

Troubleshooting

  • No data on the dashboard. Confirm PB_ADMIN_EMAIL / PB_ADMIN_PASSWORD are set in .env.local on the server. The /api/dashboard route uses admin PB credentials to read aggregated data.
  • "Invalid PIN" forever. Check TV_DASHBOARD_PIN in .env.local on the server. The PIN must match exactly (case-sensitive, no spaces).
  • TV keeps asking for PIN every reload. Sessions default to 168 hours (TV_SESSION_HOURS). Either bump it up or accept re-entry.
  • Stale data even though realtime is wired. PB realtime requires the page to be connected to the PB server. If the TV is behind a corporate proxy that strips WebSockets, realtime is dead and the 30s polling picks up the slack.
  • /dashboard (existing CRM) errored after deploying. There was a route collision — the TV view lives at /board, not /dashboard, so the two don't clash.
  • Driver map shows no pins. The map needs GPS coordinates on each customer's coordinates geoPoint (or fall back to the parent company's coordinates). Null Island {0,0} (PB's default for un-geocoded rows) is dropped on purpose. Geocode the customers via the main CRM and pins will appear.
  • Quotes per rep shows "Unassigned". Quotes auto-fall back to customer.rep when quote.rep is empty. If both are empty the row groups under "Unassigned". Set the rep on the customer / quote in the CRM to populate the table.
Var Purpose Required
TV_DASHBOARD_PIN 4-8 digit PIN staff enter on the TV yes
TV_SESSION_SECRET HMAC secret for signing the cookie yes
TV_SESSION_HOURS TTL (default 12, currently set to 168 = 1 week) no
TV_DRIVER_ID PB users.id of the driver whose active stops appear on the stops tile. When unset the tile collapses to a "set TV_DRIVER_ID" placeholder so the rest of the dashboard still renders. no
TV_REP_ROLES Comma-separated user_type values that mark a user as a rep for the activity list (default REP). A user is counted as a rep when their user_type array contains any of these values. Example: TV_REP_ROLES=REP,QUOTER to also include the QUOTER role. no
PB_ADMIN_EMAIL / PB_ADMIN_PASSWORD PB admin auth for /api/dashboard yes (reuses existing vars)

Generate a session secret with:

node -e "console.log(require('crypto').randomBytes(48).toString('base64'))"

Out of scope (intentionally)

  • No multi-screen layouts. Single screen tuned for 1080p.
  • No clickable order rows. Read-only display; the office can click through to the main CRM from a desktop.
  • No custom dashboards per user. The TV shows the same things every shift.