# OPS — Clean Mobile-First UI & Code Optimization Plan

> Companion to `PLATFORM_GUIDE.md`. This is the design + refactor playbook: what to change, in what order,
> and **at what risk** — written so it can be executed safely in small, verifiable batches.
>
> Constraint that shapes everything here: **the staff SPA is behind Authentik SSO, so UI changes can't be
> verified headlessly.** Therefore we prefer *additive* changes (new primitives, new media queries scoped to
> small viewports) and *behavior-neutral* refactors (pure-helper consolidation) that the build can validate,
> and we roll out page-by-page so any regression has a tiny blast radius.

---

## A. Current state (from the 2026-06-13 audit)

- **Shell is responsive, pages are not.** `MainLayout.vue` already does the right thing: `<q-drawer :breakpoint="1024">` (overlay below 1024px) + a mobile `<q-header v-if="$q.screen.lt.lg">` with hamburger + a desktop bar `v-if="$q.screen.gt.md"`. But **`$q.screen` appears in only 2 files total** (this layout + `ProjectWizard`). Every content page assumes desktop width.
- **Zero `q-table` grid mode** anywhere — the standard Quasar mobile-table fallback is unused, so every table horizontally overflows on a phone.
- **Dialogs hardcode widths** (`style="width:560px"`, `width:400px`, `min-width:420px`) — only `ProjectWizard` uses the correct `:maximized="$q.screen.lt.sm"` pattern; nothing else copies it.
- **Two parallel light token systems**: `--ops-*` (in `app.scss`) and `--sb-*` (in `dispatch-styles.scss`). Dispatch is **no longer dark** — it's a second light design language. 4 dispatch modals still carry orphaned Quasar `dark` attributes (visually inconsistent).
- **Helper duplication, adoption stalled**: `useFormatters` (canonical) reaches 17 files, `useStatusClasses` only 4, while ~25 pages carry private copies of `shortAgent` (×5), `statusColor` (×10, all different palettes), date/time formatters (×16), money (×6). ~337 ad-hoc Quasar color-name strings for status.
- **Hardcoded hex** instead of tokens: ProjectWizard 326, PlanificationPage 245, NetworkPage 100, EquipmentDetail 84, ClientDetailPage 80, ConversationPanel 61.

---

## B. The clean mobile-first design system

### B.1 Principles
1. **One token set.** Everything references `var(--ops-*)`. `--sb-*` becomes an alias layer, then is removed.
2. **Content reflows; dense tools degrade gracefully.** List/report/detail pages must be fully usable at
   380px. The two genuinely desktop-only tools (Dispatch board, Planification grid) get an explicit
   "best on desktop" affordance + a **read-only mobile summary**, rather than a fake reflow.
3. **A handful of primitives, applied everywhere.** We don't redesign 30 pages by hand — we build ~6 shared
   components/utilities and route pages through them.
4. **Touch targets ≥ 44px, single-column forms on mobile, bottom-sheet dialogs.**

### B.2 Token system (unify `--sb-*` → `--ops-*`)
- Step 1 (safe): in `dispatch-styles.scss`, redefine each `--sb-*` as `var(--ops-*)` (alias). No visual change.
- Step 2 (later): delete `--sb-*` usages in favor of `--ops-*`, then remove the aliases.
- Step 3: remove `dark` attributes from the 4 dispatch modals (`CreateOfferModal`, `PublishScheduleModal`,
  `WoCreateModal`, `JobEditModal`); delete the dead `$q.dark` branch in `TagEditor.vue`.

### B.3 The responsive primitives to build (additive — zero risk to existing pages)
| Primitive | What it does | Replaces |
|---|---|---|
| `<ResponsiveDialog>` | wraps `q-dialog` with `:maximized="$q.screen.lt.sm"` + sane `max-width:95vw` | every `style="width:NNNpx"` dialog |
| `<DataTable>` | wraps `q-table` with `:grid="$q.screen.lt.md"` + a default card slot + column-priority | every raw wide `q-table` |
| `<StatCard>` | the report stat tile (icon, value, label) using tokens | inline `style="min-width:130px"` cards on 6+ report pages |
| `<StatusBadge :doctype :value>` | one place that maps status→color/label | the ~10 `statusColor()` maps + 337 color-name strings |
| `<PageHeader>` | title + actions row that wraps on mobile (`col-12 col-sm-auto`) | fixed-width filter rows (`style="width:160px"`) |
| `<FilterBar>` | filter inputs that stack on mobile | same |

### B.4 Mobile layout rules (apply per page)
- Filter rows: `row q-col-gutter-sm` with each control `col-12 col-sm-auto` (stack on phone, inline on desktop).
- Two-column detail (`col-lg-8 / col-lg-4`): keep — it already stacks. But the right "sticky" column must
  drop its `position:sticky`/`100vh` on mobile (it currently eats a full viewport once stacked — see
  `app.scss .convos-sticky`). Add `@media (max-width: 1023px) { .convos-sticky { position: static; max-height: none } }`.
- Kanban boards (`DepartmentBoard`, `PipelineBoard`): already horizontal-scroll with fixed-width columns —
  acceptable on mobile (swipe). Just shrink column width under `sm` (`flex-basis: 80vw`) so one column shows at a time.
- Dispatch / Planification: gate with a friendly "Best viewed on desktop" banner under `lt.md` + link to the
  read-only day summary; don't attempt to reflow the timeline.

---

## C. Per-page responsive strategy

Risk legend: 🟢 trivial/additive · 🟡 moderate (touches layout) · 🔴 large (mega-component).

| Page | Problem | Fix | Risk |
|---|---|---|---|
| Report pages (×8) | fixed filter widths + wide tables, no grid mode | `<FilterBar>` + `<DataTable grid-on-mobile>` + `<StatCard>` | 🟢 |
| `ClientsPage`, `EquipePage`, `EmailQueuePage`, `TelephonyPage`, `SupplierInvoicesPage` | wide tables + fixed dialog widths | `<DataTable>` + `<ResponsiveDialog>` | 🟢 |
| `ClientDetailPage` | sticky right col eats mobile viewport; 560px dialogs | media-query unstick + `<ResponsiveDialog>` | 🟡 |
| `AddressConformityPage` | `min-width:420px` dialogs overflow | `<ResponsiveDialog>` | 🟢 |
| `CampaignNewPage` | dense multi-col form | single-column under `md`, step-by-step | 🟡 |
| `SettingsPage` permission matrix | wide horizontal-scroll grid | keep scroll, add sticky first column + min touch size | 🟡 |
| `DepartmentBoard` / `PipelineBoard` | fixed 270px columns | `flex-basis: 80vw` under `sm` | 🟢 |
| `NetworkPage` | map + topology, 100 hex | desktop-gate + token cleanup | 🟡 |
| `DispatchPage` / `PlanificationPage` | 100vh desktop workspaces, can't reflow | "best on desktop" banner + read-only mobile day view | 🔴 (new view) |

---

## D. Code optimization backlog (prioritized)

### P0 — behavior-neutral consolidation (safe, build-verifiable) ✅ *started this session*
1. **Finish the helper migration.** Make `useFormatters.js` the single source for: `formatMoney`, `formatDate`,
   `formatDateShort`, `formatDateTime`, **`shortAgent`** (new), **`fmtTimeHHhMM`** (new, `9h05`),
   **`relTime`** (new, "today→time, else date"), `timeAgo` (`noteTimeAgo`). Then delete the private copies:
   - `shortAgent` ×5 → import (DepartmentBoard, PipelineBoard, ConversationPanel, InterveneDialog, +tech initials).
   - money ×6 → `formatMoney` (verify each is byte-equivalent before swapping; the report `money`/`fmtMoney` are).
   - date/time ×16 → the canonical set (swap only identical ones; unify the 4 divergent relative formatters into `relTime`).
   - **Risk control:** swap only *identical* implementations; leave divergent ones flagged. Build after each batch.
2. **Add `useDebounce`** and replace the 3 hand-rolled `setTimeout` debouncers (MainLayout, useUserGroups×2).

### P1 — extract shared UI components (additive, then migrate page-by-page)
3. `<StatusBadge>` + a single `statusColor(doctype, value)` module → migrate the ~10 maps + the report pages first.
4. `<StatCard>`, `<ResponsiveDialog>`, `<DataTable>`, `<FilterBar>`, `<PageHeader>` (see §B.3) → migrate report
   pages first (leaf, low blast radius), then list pages.
5. Token cleanup pass: replace top-7 hardcoded-hex offenders' brand colors with `var(--ops-*)`.

### P2 — split the mega-components (highest effort, do last, one at a time)
The top 5 are ~12k of 38k total `.vue` lines:
`PlanificationPage` (3,524) · `ProjectWizard` (2,891) · `DispatchPage` (2,162) · `NetworkPage` (1,829) ·
`ClientDetailPage` (1,725). Split **by section into child components + composables**, never a rewrite. These
are load-bearing; each split needs its own verify cycle.

### Enforcement (so it doesn't regress again)
- Add an ESLint `no-restricted-syntax` rule banning local `function formatMoney|statusColor|fmtDate|shortAgent`
  definitions outside the canonical modules. (Adoption stalled once already — task #81 was "done" but ~25 pages
  kept private copies. A lint rule is what makes consolidation stick.)

---

## E. What was actually changed vs. deliberately deferred (2026-06-13)

**Done & build-verified, NOT deployed** (held for spot-check, because the live app — `index.b5007bac.js`,
the conversations fix + Communications feature — is what's running while you're away and I can't runtime-verify):
- Extended `useFormatters.js` with `shortAgent`, `fmtTimeHHhMM`, `relTime`, `formatDateTimeShort` (all additive).
- **`shortAgent` consolidated in 4 files**: `DepartmentBoard`, `PipelineBoard`, `ConversationPanel`, `InterveneDialog`
  (the 5th, `shortName`, was the same one-liner — aliased on import). 1 left = the tech `initials` *variant* (different fn).
- **`relTime`** swapped into `DepartmentBoard` (ex-`fmtTime`, identical logic).
- **`formatMoney`** swapped (aliased as `money`) into `ReportNegativeBillingPage` + `ReportTerminatedActivePage`
  (both were `(v==null?0:v).toLocaleString(currency)` = equivalent for numeric/null). Left untouched on purpose:
  `SupplierInvoicesPage.fmtMoney` (dash-for-empty + multi-currency) and `ReportInternetCher.formatMoney` (`Number()` coercion).
- **`formatDateTimeShort`** swapped (aliased as `fmtTime`/`fmt`) into `ReportNegativeBillingPage`,
  `ReportTerminatedActivePage`, `InterveneDialog` (all were `toLocaleString({dateStyle:'short',timeStyle:'short'})`).
- Each swap aliases the import to the original local name → **zero call-site/template changes**. `npx quasar build` → clean.

**Next safe P0 (same pattern, ready to continue):** `formatDateTimeShort` into `EquipmentDetail` (`fmtTs` direct; `evTime`
needs an `e.created_at` wrapper) + `RightPanel.fmtThreadDate`; add a `formatDateTimeMedium` for the `dateStyle:'medium'`
copies (`EmailQueuePage`, `CampaignsListPage`, `GiftsInventoryPage`); add `useDebounce` (MainLayout + useUserGroups×2).
Then the `statusColor` ×10 + `<StatusBadge>` work moves to P1.

**Deliberately deferred** (needs your eyes / a verify cycle): everything that changes pixels (responsive
primitives, token unification, dialog/table wrappers) and the mega-component splits. The design + order above
is ready to execute in safe batches when you're back to glance at each one.

> Deploy of P0 refactors is a one-liner once you've glanced: `cd apps/ops && npx quasar build && scp -r dist/spa/* root@96.125.196.67:/opt/ops-app/`.

---

## F. 2026-06-18 refresh — audit re-run + first batch SHIPPED

Re-ran the audit against current source (32 pages) and **unblocked local visual verification** (the SSO blocker noted in §A).

**Local preview now works** — `quasar dev` (port 9001, `.claude/launch.json` → "ops"). Two prod-safe localhost guards make it usable without Authentik/hub:
- `src/api/auth.js` `_reauth()` → no reload-loop on localhost (returns the 401).
- `src/composables/usePermissions.js` → permissive profile on localhost (so nav + gated content render).
Real ERPNext data in dev needs a **current** `VITE_ERP_TOKEN` in `apps/ops/.env.local` (the committed `.env` token is the 2026-06-16 rotated-out one). Note: `$q.screen` doesn't track viewport in the headless preview — prefer `useMediaQuery` (matchMedia) over `$q.screen` so responsive toggles are both robust and preview-verifiable.

**Shipped this batch (the §B.3 primitives now exist + first migration):**
- `src/composables/useMediaQuery.js` — `useIsPhone()` (<600), `useIsMobile()` (<1024), matchMedia-based.
- `src/components/shared/StatCard.vue`, `ResponsiveDialog.vue` (maximized on phone), `DataTable.vue` (q-table → grid cards <1024, forwards attrs/slots).
- `ReportRevenuPage.vue` migrated as the **reference**: filter row `col-auto`→`col-12/col-6 col-sm-auto` (stacks on phone, **verified at 375px**), summary tiles→`StatCard`, table→`DataTable`.
- `app.scss` — `.convos-sticky` un-stuck under 1023px (was eating a full mobile viewport once stacked).

**Rollout order for the rest (same primitives, page-by-page, leaf-first):**
1. Report pages ×7 remaining (`ReportVentes/Taxes/AR/InternetCher/NegativeBilling/TerminatedActive`, `RapportsPage`) → `StatCard` + `DataTable` + responsive filter. 🟢
2. List pages (`ClientsPage`, `EquipePage`, `EmailQueuePage`, `TicketsPage`, `OcrPage`, `AddressConformityPage`) → `DataTable`. 🟢
3. Dialog offenders → `ResponsiveDialog` (`TelephonyPage` 500/400px, `SettingsPage`, `AddressConformityPage`, `EvaluationsPage`). 🟢
4. `ClientDetailPage` (8 tables→`DataTable`, dialogs→`ResponsiveDialog`; sticky already fixed). 🟡
5. Token unification `--sb-*`→`--ops-*` + the ~800 hardcoded hex (Planif 320, Network 101…) → `StatusBadge`. 🟡
6. Mega-component splits (Planif/Dispatch/Network) + "best on desktop" banners. 🔴

⚠️ Data-dependent rendering (table rows, stat values) wasn't visually verified — the dev preview has no live data (token). Verify those after deploy, or drop a fresh token in `.env.local` for full local verification.
