WFDASH-WEBAPP/docs/superpowers/specs/2026-08-27-boss-phone-dashboard-design.md

12 KiB

Boss phone dashboard — design

Date: 2026-08-27 Status: Approved (pending implementation plan) Owner: Dave (boss's view)

Problem

The boss wants to keep track of where the business stands and what's happening, on their phone. The existing /dashboard is desktop-first (1074 lines, 6 chart types, 5 rows of cards) and the /board TV view is designed for a wall-mounted display. Neither fits a phone glance.

Goal

A standalone, phone-first page at /m that answers, in a single glance, three questions:

  1. How many quotes did each rep load, do for their accounts, and push through to emailed/approved/sent today / this week / this month?
  2. How many orders did each rep place today / this week / this month?
  3. How many driver stops are scheduled today, and how many are left?

The boss opens /m on their phone, sees the numbers refresh in realtime, taps a rep row to verify the work behind a number, and closes the app. Done.

Non-goals (YAGNI)

  • PWA install prompt UI (manifest exists but no "Add to home screen" button).
  • Push notifications on KPI thresholds.
  • Historical week-over-week comparison.
  • CSV export / share.
  • Settings screen.
  • Stock alerts, follow-ups overdue, supplier PO queues — those live on the operator dashboard. Out of scope per the user's explicit "just the KPIs you listed" answer.

Data model

Per-rep aggregations on the rep perspective:

Stat Definition
quotesLoaded quotes.claimed_by = rep.id, count of rows where created >= windowStart and created < windowEnd.
quotesDoneFor quotes joined to customer where customer.rep = rep.id, count of rows in window.
quotesDoneBy quotes.claimed_by = rep.id, count of rows in window where status ∈ {emailed, approved, sent}.
ordersPlaced orders.rep matches rep (PB id or resolved name), count of rows in window where item_status ∈ {pending, po_generated, assigned, collection_pool, collected, delivered, invoiced} (i.e. not cancelled).

window is one of today | week | month:

  • today: [startOfDay(UTC), startOfTomorrow(UTC))
  • week: [startOfWeek(UTC, monday), startOfNextWeek(UTC, monday))
  • month: [startOfMonth(UTC), startOfNextMonth(UTC))

Driver stops are always today-only regardless of the active tab.

The orders.rep field historically stored a free-text name and sometimes a 15-char PB id. The service uses the existing resolveRepDisplayName helper from src/lib/server/order-rep.ts to normalise to a rep id, then joins on users.id — same pattern the operator dashboard already uses for per-customer rep resolution.

Users with claimed_by / rep values that no longer resolve to a users row are bucketed under a single "(unknown)" rep in the output so the page never silently drops activity.

Output shape

type BossDashboardData = {
  window: "today" | "week" | "month";
  rangeStart: string; // ISO
  rangeEnd: string;   // ISO
  orderTotals: {
    total: number;       // orders created in window, not cancelled
    delivered: number;   // item_status === "delivered"
    invoiced: number;    // order.invoiced === true
  };
  reps: Array<{
    id: string;          // PB users.id, or "" for the (unknown) bucket
    name: string;        // "Gary Ndlovu" or "(unknown)"
    isUnknown: boolean;  // true → render muted, no avatar, no drill-down
    quotesLoaded: number;
    quotesDoneFor: number;
    quotesDoneBy: number;
    ordersPlaced: number;
  }>;
  drivers: Array<{
    id: string;          // PB users.id
    name: string;
    stopsToday: number;
    stopsDone: number;
    stopsLeft: number;
  }>;
};

The endpoint returns ONE window at a time, not all three. Tab change → new fetch with ?window=week. Payload stays small (~3-5 KB).

Files to create

src/lib/server/boss-dashboard-service.ts          # getBossDashboardData({ window })
src/lib/server/boss-dashboard-service.test.ts     # PB mocked, per-rep math + window boundaries
src/app/api/v1/boss-dashboard/route.ts            # GET ?window=today|week|month, asserts session
src/app/api/v1/boss-dashboard/
  └── rep-activity/route.ts                       # GET ?rep=<id>&window=<window>&kind=quotes|orders
                                                  # → paginated list for the bottom sheet
src/app/(mobile)/m/layout.tsx                     # phone shell, manifest link, no nav
src/app/(mobile)/m/page.tsx                       # server component, auth gate, initial fetch
src/app/(mobile)/m/dashboard-view.tsx             # client: tabs, realtime, drill-downs
public/manifest-mobile.json                       # name, short_name, theme_color, display=standalone

Files to modify

  • next.config.* or app/layout.tsx — register /m layout's manifest link in the document head. (Confirmed: adding a <link rel="manifest"> inside (mobile)/m/layout.tsx is enough; Next.js merges per-route head tags.)

Architecture & data flow

  1. First paint (SSR):

    • Server Component reads the PB cookie via cookies() from next/headers and calls assertBrowserSession() (no-arg variant for server components — the route-handler variant assertBrowserSession(req) takes NextRequest). On failure the page throws a redirect to the PB login route.
    • getBossDashboardData({ window: "today" }) runs against the admin PB client.
    • <BossDashboardView initial={...} /> renders with data — no spinner.
  2. Tab change (client):

    • User taps Week or Month.
    • Client state updates activeWindow.
    • useEffect on activeWindow calls /api/v1/boss-dashboard?window=week.
    • Response replaces data, skeleton flashes for ~80ms while fetch in flight.
  3. Realtime (client):

    • On mount, subscribe to PB customers, quotes, orders collections with * events.
    • Trailing-edge debounce 1000ms → re-fetch current window.
    • Unsubscribe on unmount.
  4. Drill-down:

    • Tap rep row → bottom sheet opens with two tabs (Quotes / Orders).
    • Sheet calls /api/v1/boss-dashboard/rep-activity?rep=<id>&window=<window>&kind=....
    • Result is a flat array of { id, number, customer, status, age } — page renders rows.
    • Tap a row → opens /quotes/<id> or /orders/<id> in a new tab on the phone.
  5. Auth: assertBrowserSession against the running PB browser session. Boss logs in via the existing PB login page once. No new auth code, no new PIN. Two flavours:

    • Page (server component): assertBrowserSession() reads the PB cookie from next/headers. Throws redirect on failure.
    • Route handler: assertBrowserSession(req) takes NextRequest, forwards the bearer token / cookie, returns 401 on failure.

Components

(mobile)/m/layout.tsx

export default function MobileLayout({ children }: { children: React.ReactNode }) {
  return (
    <main className="min-h-screen bg-background text-foreground">
      {children}
    </main>
  );
}

Also sets <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> and <link rel="manifest" href="/manifest-mobile.json"> via the route's head export.

(mobile)/m/page.tsx

Server component:

export const dynamic = "force-dynamic";
export const runtime = "nodejs";

export default async function BossDashboardPage() {
  await assertBrowserSession();
  const initial = await getBossDashboardData({ window: "today" });
  return <BossDashboardView initial={initial} userName={user.name} />;
}

(mobile)/m/dashboard-view.tsx (client)

Sections, top to bottom:

  1. Header strip — "Good {morning|afternoon|evening}, {firstName}." (first name = user.name.split(" ")[0], matching the operator dashboard pattern) with refresh button (rotates icon while loading) and last-refreshed {timeAgo} subtitle.

  2. Time tab bar — segmented control. Today / Week / Month. Active tab highlighted with accent colour, others muted. Single tap re-fetches.

  3. Order totals strip — three small tiles: total / delivered / invoiced for the active window. One row, three cells.

  4. Reps section — header "Sales reps" with count. One card per rep, sorted ordersPlaced DESC, quotesLoaded DESC. Each card:

    • Avatar + name (left)
    • 4 metric cells (loaded / done-for / done-by / orders) right-aligned with label-under-number
    • Progress bar underneath showing ordersPlaced / totalOrdersThisWindow as percentage
    • Tap → bottom sheet
  5. Drivers section — header "Driver stops today ({totalStopsToday})" — the count in parentheses is the sum of all drivers' stopsToday. One card per driver with stops-left badge coloured by urgency: red if stopsLeft > 0 && stopsDone === 0 (nothing started yet), yellow if stopsLeft > 0 && stopsDone > 0 (in progress), green if stopsLeft === 0 (all done).

Bottom sheet (<dialog> element)

Reuses the native <dialog> element with [open] toggling. Two tabs inside: Quotes / Orders. Each tab lazy-loads its data via the rep-activity endpoint. Rows render with avatar (initial), number, customer, status pill, relative age ("2h ago"). Tap row → window.open(/quotes/<id>, "_blank").

Empty / loading / error states

State Behaviour
Loading (first paint) Server-rendered data, no spinner needed.
Loading (tab change) Card-level skeleton, shimmer, ~80ms.
Error (PB 5xx) Toast with retry button, single "Couldn't load" tile replaces the affected section. View keeps the previous data.
Empty reps "No rep activity in this window." Card centred, muted text.
Empty drivers "No driver stops scheduled today."
Network offline Toast "Reconnecting…", disable refresh button.

Service contract

getBossDashboardData({ window }: { window: "today" | "week" | "month" }): Promise<BossDashboardData>

Throws on PB errors with a .status property. Caller (route.ts) catches and returns 500 with { error: "..." } body. View renders error state on non-OK responses.

getBossRepActivity({ rep, window, kind, page }): Promise<{ items: Array<...>, hasMore: boolean }> — paginated 25 per page for the drill-down sheet.

Testing

  • boss-dashboard-service.test.ts — PB mocked via pocketbase-admin test pattern already in the repo. Cases:
    • Per-rep math correct (loaded / done-for / done-by / orders) for sample fixture.
    • Window boundary correctness: today rolls over at UTC midnight, week on Monday, month on day 1.
    • Unknown rep bucketing: claimed_by strings that don't resolve to a users.id land in the (unknown) row.
    • Empty rep list still returns valid shape.
  • route.test.ts (one test file for both /api/v1/boss-dashboard and /api/v1/boss-dashboard/rep-activity):
    • 401 when no session.
    • 400 on missing/invalid window.
    • 200 with valid payload.
  • No browser test in v1. User has agent-browser configured and will verify in the running app.

Refresh model

PB realtime + 1s trailing-edge debounce. Same as /dashboard. If the debounce proves noisy on a phone (battery, data), we can add a "Pause live updates" toggle as a follow-up.

Out of scope reminders

Already listed. Re-emphasising here because the boss might be tempted to ask for them at demo time:

  • PWA install button.
  • Per-rep drill-down to "orders by status" — current drill-down is a flat list of records, not a per-status breakdown.
  • Date scrubber — fixed today / this week / this month.
  • Multi-tenant / white-label — single-tenant WFDASH, single boss.

Open questions

None. All clarifying questions resolved during brainstorm:

  • Rep perspective on quote metrics — confirmed.
  • Reps-only as the user scope — confirmed.
  • Same PB session auth — confirmed.
  • Today + week + month tabs (no toggle) — confirmed.
  • PB realtime + debounce — confirmed.
  • Tap-row-to-list drill-downs — confirmed.
  • Standalone PWA at /m — confirmed.
  • Layout B (tabs top, single table) — confirmed.
  • "Just the KPIs you listed" — confirmed.

How to verify after implementation

  1. pnpm dev — confirm no compile errors.
  2. Open http://localhost:4001/m in the browser.
  3. Confirm greeting + tab bar + reps + drivers render.
  4. Switch tab — confirm data refreshes.
  5. Open DevTools network tab, simulate a quote create in another tab — confirm debounced re-fetch fires within ~1s.
  6. Tap a rep row — confirm bottom sheet opens with quotes + orders.
  7. Tap a record inside the sheet — confirm /quotes/<id> opens in a new tab.
  8. Sign out, navigate to /m — confirm redirect to PB login.
  9. pnpm test src/lib/server/boss-dashboard-service.test.ts — passes.
  10. pnpm test src/app/api/v1/boss-dashboard — passes.