# 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: `` (overlay below 1024px) + a mobile `` 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 | |---|---|---| | `` | wraps `q-dialog` with `:maximized="$q.screen.lt.sm"` + sane `max-width:95vw` | every `style="width:NNNpx"` dialog | | `` | wraps `q-table` with `:grid="$q.screen.lt.md"` + a default card slot + column-priority | every raw wide `q-table` | | `` | the report stat tile (icon, value, label) using tokens | inline `style="min-width:130px"` cards on 6+ report pages | | `` | one place that maps status→color/label | the ~10 `statusColor()` maps + 337 color-name strings | | `` | title + actions row that wraps on mobile (`col-12 col-sm-auto`) | fixed-width filter rows (`style="width:160px"`) | | `` | 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 | `` + `` + `` | 🟢 | | `ClientsPage`, `EquipePage`, `EmailQueuePage`, `TelephonyPage`, `SupplierInvoicesPage` | wide tables + fixed dialog widths | `` + `` | 🟢 | | `ClientDetailPage` | sticky right col eats mobile viewport; 560px dialogs | media-query unstick + `` | 🟡 | | `AddressConformityPage` | `min-width:420px` dialogs overflow | `` | 🟢 | | `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. `` + a single `statusColor(doctype, value)` module → migrate the ~10 maps + the report pages first. 4. ``, ``, ``, ``, `` (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 + `` 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/`.