Accumulated work on the dispatch/legacy-writeback branch: - Communications UI: CommunicationsPage, ConversationFullPage, DepartmentBoard, PipelineBoard, ReaderStack, Orchestrator/NewTicket/ServiceStatus/Outbox dialogs; hub gmail.js, ticket-collab.js, outbox.js, coupon-triage.js, client-diag.js. - Billing/sync mirror (F→ERPNext): legacy-payments.js, legacy-sync.js, sync-orchestrator.js, supplier-invoices.js, municipality.js + incremental migration scripts; LegacySyncPage, SupplierInvoices + negative-billing / terminated-active reports. - Roster/campaigns/network/voice: roster + roster-assistant, campaigns, giftbit, olt-snmp, traccar, twilio, vision, tech-absence-sms, ai/agent/config/helpers, legacy-dispatch-sync; ops PlanificationPage, RapportsPage, Settings, Tickets, ClientDetail updates. - docs/ PLATFORM_GUIDE + UI_AND_OPTIMIZATION; .gitignore __pycache__. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
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.vuealready 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 barv-if="$q.screen.gt.md". But$q.screenappears in only 2 files total (this layout +ProjectWizard). Every content page assumes desktop width. - Zero
q-tablegrid 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) — onlyProjectWizarduses the correct:maximized="$q.screen.lt.sm"pattern; nothing else copies it. - Two parallel light token systems:
--ops-*(inapp.scss) and--sb-*(indispatch-styles.scss). Dispatch is no longer dark — it's a second light design language. 4 dispatch modals still carry orphaned Quasardarkattributes (visually inconsistent). - Helper duplication, adoption stalled:
useFormatters(canonical) reaches 17 files,useStatusClassesonly 4, while ~25 pages carry private copies ofshortAgent(×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
- One token set. Everything references
var(--ops-*).--sb-*becomes an alias layer, then is removed. - 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.
- 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.
- 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-*asvar(--ops-*)(alias). No visual change. - Step 2 (later): delete
--sb-*usages in favor of--ops-*, then remove the aliases. - Step 3: remove
darkattributes from the 4 dispatch modals (CreateOfferModal,PublishScheduleModal,WoCreateModal,JobEditModal); delete the dead$q.darkbranch inTagEditor.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-smwith each controlcol-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 itsposition:sticky/100vhon mobile (it currently eats a full viewport once stacked — seeapp.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 undersm(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
- Finish the helper migration. Make
useFormatters.jsthe 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 reportmoney/fmtMoneyare). - 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.
- Add
useDebounceand replace the 3 hand-rolledsetTimeoutdebouncers (MainLayout, useUserGroups×2).
P1 — extract shared UI components (additive, then migrate page-by-page)
<StatusBadge>+ a singlestatusColor(doctype, value)module → migrate the ~10 maps + the report pages first.<StatCard>,<ResponsiveDialog>,<DataTable>,<FilterBar>,<PageHeader>(see §B.3) → migrate report pages first (leaf, low blast radius), then list pages.- 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-syntaxrule banning localfunction formatMoney|statusColor|fmtDate|shortAgentdefinitions 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.jswithshortAgent,fmtTimeHHhMM,relTime,formatDateTimeShort(all additive). shortAgentconsolidated in 4 files:DepartmentBoard,PipelineBoard,ConversationPanel,InterveneDialog(the 5th,shortName, was the same one-liner — aliased on import). 1 left = the techinitialsvariant (different fn).relTimeswapped intoDepartmentBoard(ex-fmtTime, identical logic).formatMoneyswapped (aliased asmoney) intoReportNegativeBillingPage+ReportTerminatedActivePage(both were(v==null?0:v).toLocaleString(currency)= equivalent for numeric/null). Left untouched on purpose:SupplierInvoicesPage.fmtMoney(dash-for-empty + multi-currency) andReportInternetCher.formatMoney(Number()coercion).formatDateTimeShortswapped (aliased asfmtTime/fmt) intoReportNegativeBillingPage,ReportTerminatedActivePage,InterveneDialog(all weretoLocaleString({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/.