gigafibre-fsm/docs/UI_AND_OPTIMIZATION.md
louispaulb 5ab14cac44 feat(fsm): platform build — comms UI, F→ERPNext sync/billing, roster, campaigns, network, reports
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>
2026-06-15 06:12:12 -04:00

11 KiB
Raw Blame History

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)

  1. <StatusBadge> + a single statusColor(doctype, value) module → migrate the ~10 maps + the report pages first.
  2. <StatCard>, <ResponsiveDialog>, <DataTable>, <FilterBar>, <PageHeader> (see §B.3) → migrate report pages first (leaf, low blast radius), then list pages.
  3. 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/.