gigafibre-fsm/docs/UX_REFACTORING_PROMPT.md
louispaulb 8e7838d1fb docs: prompt réutilisable de refactor UX (UX_REFACTORING_PROMPT.md)
Réécriture vérifiée du brouillon initial — 43 corrections issues d'un review
multi-agents contre le code réel @ 718698f :
- 16 règles UX calibrées au repo (chrome-only, CTA desktop top-right,
  Function-props, fr-CA, raccourcis clavier, états async, LTE, 44px, tokens)
- tableau des primitives canoniques (OverflowMenu, DisclosureSection, HelpHint…)
- audit anti-patterns avec colonnes Status+Anchor (repérage par nom d'état,
  pas par ligne) ; lignes déjà résolues marquées (T2, TechTasks, PL4/PL7)
- plan de décomposition PlanificationPage réaliste (décomposition #4 reconnue,
  vagues 1-2, règle script-mapping, pas de composable fourre-tout)
- Definition of Done (lint/build/preview + invariants DnD/SSE/MapLibre/Publier)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 19:27:51 -04:00

22 KiB
Raw Blame History

UX Refactoring Prompt — Gigafibre FSM

Audited: 2026-07-14 @ commit 718698f. Line numbers below are hints only — they drift with every commit. ALWAYS locate code by the Anchor string (state/component/label name); if the anchor isn't where the table says, search the file and update this doc before refactoring.

Project Context

Gigafibre FSM is the operations platform for a fiber ISP in Quebec. Stack: Vue 3 / Quasar v2 / Pinia / Vite. The app has two audiences:

  • Ops staff (CSRs, dispatchers) on desktop/tablet → apps/ops/src/pages/ — Quasar components, light theme, --ops-* tokens.
  • Field technicians on mobile (often one-handed, gloves, spotty LTE) → apps/ops/src/modules/tech/pages/its own design system: tg-* classes from src/css/tech.scss, native <button>, NOT q-btn. Never inject Quasar desktop patterns into the tech module.

All strings are French (fr-CA). ERPNext v16 is the backend (REST API). The hub (targo-hub) is a Node.js API gateway.


Existing Code — REUSE, never rebuild (standing team rule)

Before creating ANY new file, state which existing file you checked and why it doesn't fit.

  • Pinia stores (src/stores/): useAuthStore, useDispatchStore, useOfflineStore. Extend these; do NOT create new stores without asking.
  • Composables (src/composables/, ~60): check here FIRST — useDragDrop, useSSE, useJobPool, useScheduler, useMediaQuery, useUndo, useSelection, usePeriodNavigation already exist.
  • Shared components (src/components/shared/, ~48): see the Canonical Primitives table below.
  • src/components/planif/ already contains 8 extracted components (décomposition #4): JobPool, LeaveDialog, MonthOverview, PlanifCellMenu, ShiftTypesDialog, SkillCadenceTable, TechScheduleDialog, TechSyncDialog. New Planification extractions go here and must not duplicate them.

Where extracted components go: PlanificationPage pieces → src/components/planif/ · ClientDetailPage pieces → src/components/customer/ · reused across pages → src/components/shared/ (check the ~48 existing first) · tech (mobile) module pieces → src/modules/tech/components/ (currently only JobCard.vue), styled with tg-* classes.

Canonical primitives

All in src/components/shared/ unless noted. Import these; do NOT rebuild an inferior equivalent.

Need Use
⋮ overflow of secondary actions OverflowMenu.vue (`:groups="[{label, items:[{icon,label,caption?,action
Collapsible « ▾ Options avancées » / glance→detail DisclosureSection.vue (title, count, persist-key = per-user persisted open state, slots #summary/#actions)
Rare actions removed from a toolbar register in composables/useCommandPalette.js (⌘K) so they stay reachable
ⓘ contextual help HelpHint.vue (title/text or slot) — global behavior in boot/tooltip-ux.js (click = hide, never 2 bubbles)
Tables & async states DataTable.vue · EmptyState.vue · TableSkeleton.vue
Page top bar / record detail PageHeader.vue · DetailModal.vue + detail-sections/ (incl. modules/JobThread.vue, JobReplyBox.vue)
Stats / avatars StatCard.vue · UserAvatar.vue (canonical identity avatar)
Skill chips & editor TagEditor.vue · components/planif/SkillCadenceTable.vue
Maps / occupancy RouteMap.vue · OccupancyStrip.vue · OccupancyBands.vue
Responsive breakpoints composables/useMediaQuery.js

UX Rules (enforce these on every review)

  1. MAX 5 interactive CONTROLS per chrome zone (toolbar, header, footer, dialog action row). Data cells/rows/chips inside grids, tables, calendars and kanban lanes do NOT count — a planning grid legitimately renders dozens of interactive cells. If a chrome zone exceeds 5 controls, group them behind a single trigger using shared/OverflowMenu.vue — never hand-roll a q-menu of secondary actions.
  2. Progressive disclosure: show only what the CURRENT step needs. Hide rarely-used options with shared/DisclosureSection.vue (« ▾ Options avancées ») or an OverflowMenu. Actions removed from a toolbar must be registered in useCommandPalette (⌘K) so they stay reachable — PlanificationPage already does this.
  3. Every form field must justify its presence — if it has a sensible default or is rarely edited, hide it. Only expose what the user needs to touch >50% of the time.
  4. No menu-in-menu: if a dropdown item opens a submenu, extract it into its own dialog.
  5. Mobile-first for mobile surfaces: on the tech module (modules/tech/) and on mobile breakpoints (lt-md) of ops pages, the primary CTA must be in thumb reach (bottom 1/3) — reuse the floating CTA pill pattern (.tg-fab-cta, TechJobDetailPage). On desktop (gt-sm) ops pages, the primary CTA belongs top-right of the header toolbar, visually dominant — do NOT relocate desktop CTAs to the bottom of the viewport. No hover-only affordances anywhere (desktop included).
  6. No .vue file > 800 lines — decompose into child components with focused responsibilities.
  7. Props carry data/config; behaviour goes through emits or a shared composable/Pinia store. HARD flag: any Function-typed prop used as a parent callback (see C1: LocationCard has 32 props, 26 of them Functions — the real anti-pattern). SOFT flag: >10 props (config-heavy shared components like DetailModal at 12 declarative props are acceptable).
  8. One primary action per view state. If the user has to choose between 3+ equally prominent buttons, you've failed.
  9. Status feedback: every mutation shows visible feedback within 200ms — using the app's existing mechanisms ONLY: $q.notify for results/errors (see the err() helper pattern), :loading / :disabled="saving" on the triggering button while pending, the changeLog journal + revert for grid edits, and OutboxPanel-style delayed send with « Tout annuler » for bulk/destructive sends. Do NOT introduce a new toast system. "Never silently catch()" applies to user-initiated mutations; background best-effort loads may catch silently but must carry a comment (existing convention: catch (e) { /* … best-effort */ }).
  10. Dev-only UI hidden in production: gate ONLY on import.meta.env.DEV (compile-time, stripped from prod builds) — NEVER on a custom VITE_* flag: .env.local VITE_ vars are bundled even in prod builds (documented leak, src/config/hub.js). Canonical pattern: the canTerminateDirect computed in TechJobDetailPage.vue.
  11. French only (fr-CA): every user-facing string — labels, tooltips, toasts, aria-labels, empty states — including in NEW elements you create. Use « guillemets » and the existing domain vocabulary (quart, garde, tournée, « À assigner », « Publier »). Any English string in output is an automatic P1 review finding.
  12. Preserve ALL keyboard shortcuts. Existing: ⌘K/Ctrl+K → CommandPalette (window listener in MainLayout); Planification grid: Ctrl/⌘+Z / ⇧Z undo-redo, Ctrl/⌘+C/V copy-paste cells, Delete/Backspace clear, A = absent, G = garde (document-level onKey listener, which skips events from INPUT/TEXTAREA/SELECT/contenteditable — preserve that guard). Any extraction must re-wire document/window-level key listeners explicitly and list the component's shortcuts in its top docblock.
  13. Every async view has three states: loading → shared/TableSkeleton.vue (or q-skeleton), empty → shared/EmptyState.vue (icon + phrase FR + CTA), error → message + bouton « Réessayer ». When splitting a component, carry all three branches into the child. Never rebuild an equivalent skeleton/empty-state.
  14. Spotty-LTE resilience (tech module): every mutation button shows a pending state and is disabled while pending (:disabled="saving", cf. TechJobDetailPage). Offline-sensitive mutations go through the IndexedDB queue in src/stores/offline.js — never fire-and-forget fetch. Queued work must be visible: count + tap-to-retry chip (cf. TechScanPage « N scans en attente · toucher pour réessayer »). Never gate messaging on navigator.onLine alone — it reports true on unusable 2-bar LTE (documented in stores/offline.js).
  15. Touch & focus (tech module): every tappable element ≥ 44×44 px (existing convention: min-height: 44px in TechJobDetailPage.vue). Keep visible :focus-visible states; never encode state by colour alone (add icon or label); use the tokens in css/tech.scss (--ink-*, --t-*) — no hardcoded colours.
  16. Design tokens: any new or moved style uses the --ops-* variables from src/css/app.scss (--ops-accent/-success/-warning/-danger/-info/-bg/-bg-hover/-bg-light/-surface/-border/-text/-text-muted) — no new hardcoded hex. Use each extraction as the opportunity to convert copied colours. All pages are LIGHT theme; DispatchPage is the sole dark exception — never introduce dark styling elsewhere.

Expert-density surfaces (declutter rules relaxed)

On these surfaces, Rules 13 and 8 apply ONLY to chrome (toolbars, dialogs), never to board/grid content: Planification grid/day/month/routes views (PlanificationPage) and Dispatch timeline & calendars (modules/dispatch/components/). Information density is a feature for dispatchers: do not reduce data shown per cell, collapse columns, or add clicks between the dispatcher and job details. Dispatch keeps its dark theme (pages/dispatch-styles.scss).


Known Anti-Patterns (audited snapshot)

⚠️ This audit is a snapshot (2026-07-14 @ 718698f) and remediation is ongoing — the code marks applied fixes with #N comments. Before acting on any row, VERIFY the pattern still exists at the anchor; if already fixed, say so and move on.

PlanificationPage.vue (6,559 lines) — Priority: CRITICAL

File layout: template L11768 · script L17706033 (~4,264 lines — the real bulk) · style L60356559.

ID Anti-pattern Anchor Status Problem
PL1 WALL_OF_OPTIONS desktop header rows (comment « Rangée 1 : actions principales ») ~L32-154 open 20+ interactive controls in one toolbar
PL2 WALL_OF_OPTIONS label="Outils" dropdown ~L71-125 open 13 items in 3 sections + nested submenu
PL3 WALL_OF_OPTIONS q-dialog jobSheet.open ~L281-356 open ~15 interactive zones on a phone screen. Reuse detail-sections/modules/JobThread.vue + JobReplyBox.vue instead of the hand-rolled pm-postbox composer
PL4 HIDDEN_PRIMARY label contains « Publier », split q-btn-dropdown ~L131-147 partially fixed Publier is now a unified split q-btn-dropdown (color=positive, dirty-count, outline-when-clean). Remaining: the row still has ~10 sibling controls (see PL1)
PL5 MENU_IN_MENU « Modèles de semaine » ~L85-97 open Submenu inside a dropdown
PL6 ORPHAN_FIELD label="Max h/sem" ~L159 open Floating numeric input with no context
PL7 STATUS_MYSTERY autosave journal ~L48-61 largely fixed Icon now swaps to cloud_sync + primary color while saving (persistent feedback); popover holds the change journal + per-change revert by design. Verify before changing
PL8 MONOLITH entire file open 6,559 lines = unmaintainable (see decomposition plan)

TechJobDetailPage.vue (679 lines) — Priority: LOW

ID Anti-pattern Anchor Status Problem
T1 WALL_OF_OPTIONS "Détails" section ~L60-104 open Type, Priority, Time, Duration always visible — tech rarely edits these
T2 DEAD_CLICK « Terminer directement (test) » ~L160-166 RESOLVED Already gated by import.meta.env.DEV (canTerminateDirect, L296-298) — verify only, do not re-fix
T3 ORPHAN_FIELD "Créer un équipement" dialog ~L237-241 open 5 fields visible, only Serial is required

TechTasksPage.vue (748 lines) — Priority: NONE

No open issues — the dev test button (~L213-218) is already DEV-gated via canTerminateDirect (L332-335). Do not modify this page.

ClientDetailPage.vue (1,728 lines) — Priority: MEDIUM

ID Anti-pattern Anchor Status Problem
C1 PROP_OVERLOAD <LocationCard bindings ~L74-107 → components/customer/LocationCard.vue (defineProps L299-334) open 32 props, 0 emits — 6 data props + 26 Function props (10 selectors drilled from the parent's useSubscriptionGroups instance + 16 action callbacks). The anti-pattern is callbacks-as-props. Fix: instantiate useSubscriptionGroups inside LocationCard (or provide/inject the instance), keep the data props, convert action callbacks to emits (Rule 7)

Decomposition plan — PlanificationPage.vue

components/planif/ ALREADY CONTAINS (décomposition #4 — do NOT re-create, extend these): JobPool.vue (pool « à assigner » unifié : variants floating/docked/mobile — the mobile swipe/tap-sheet logic lives HERE) · LeaveDialog.vue · ShiftTypesDialog.vue · TechSyncDialog.vue · TechScheduleDialog.vue · MonthOverview.vue · PlanifCellMenu.vue · SkillCadenceTable.vue (SHARED skill+cadence editor).

New extractions must follow the same props/emits conventions as ShiftTypesDialog/LeaveDialog.

Still to extract (anchor by state/component name — line numbers drift):

Wave 1
├── PlanifHeader.vue        ← desktop toolbar + filters + skills chips, L32-187
│                             (anchor: q-btn-toggle `boardView` → OverflowMenu).
│                             Keep existing OverflowMenu / HelpHint / useCommandPalette wiring.
│                             Mobile toolbar (`pm-toolbar`, ~L3-30): include in PlanifHeader
│                             (render both breakpoints) or extract as PlanifMobileBar.vue —
│                             decide before extracting.
├── PlanifGrid.vue          ← week grid table
├── PlanifKanban.vue        ← day view (already renders <JobPool variant="docked">)
├── PlanifRoutes.vue        ← routes view (uses shared RouteMap.vue + OccupancyStrip.vue)
├── JobOptionsSheet.vue     ← q-dialog `jobSheet.open` ONLY (the `pm-sheet` assignment overlay
│                             and swipe/tap stay coupled to JobPool.vue — coordinate via its emits)
├── SkillEditorPopover.vue  ← q-menu `skillMenuShown` (WRAPS existing SkillCadenceTable — keep it)
├── GardeDialog.vue         ← q-dialog `showGarde` (keep its HelpHint)
└── TeamEditorDialog.vue    ← q-dialog `showTeamEditor` (wraps SkillCadenceTable per tech)

Wave 2 (template L789-1768, currently unmapped)
├── SuggestReviewDialog.vue ← « Suggérer » review (q-dialog `suggestDlg.open`)
├── TagManagerDialog.vue    ← `showTagManager` + `skillImpactOpen`
├── JobPoolPanel.vue        ← floating unassigned-jobs panel + pins map (`assignPanel`)
├── LegacyPushDialog.vue    ← `legacyPush.open` (write-back osTicket)
├── LocPickerDialog.vue     ← `locPicker.open`
└── (also unmapped: notify-contacts block ~L1259, legacy job-detail dialog ~L1288)

Script-mapping rule: for each extracted component, move every function/ref referenced ONLY by the extracted template into the child (e.g. kb*/kanban* → PlanifKanban, cell*/onCellDrop/onDown → PlanifGrid, garde* → GardeDialog). State shared across views stays in the parent or a per-domain composable.

Composables: do NOT create a catch-all usePlanification.js. useJobPool.js already covers pool filter/sort/group (3 instances owned by the page — keep that ownership). Extract PER DOMAIN only when a child needs it: useGardeRules.js (rule CRUD + preview), useWeekTemplates.js, usePlanifBoard.js (cells/occupancy/capacity). Before writing a composable, check src/composables/ (~60 existing).

CSS: Vue scoped styles do NOT reach into child components. Each extraction must copy the scoped CSS rules matching its markup (.pm-*, .assign-*, .garde-*…) into the new component's own <style scoped>. Do not rename classes.

Target: PlanificationPage.vue < 800 lines (Rule 6); ~400 lines only after ALL rows above are extracted. Extract top-down, ONE row per session.


How to Use This Prompt

Text in [square brackets] is a placeholder — replace it with your actual target (anti-pattern IDs, component name, line range) before sending.

Option A: Full-page refactor

Paste this file + the full .vue file content, then say:

Refactor this component following the UX rules above. Address anti-patterns
[PL1, PL2, PL3] specifically. Keep all v-model bindings, event names AND
keyboard shortcuts intact. Output ONE unified diff against the pasted file,
covering <template> AND any <script> changes the fix requires (new refs/handlers
for grouped menus, etc.). If a rule can only be satisfied in <script> (rules 7,
9, 10, 12) and the script was not pasted, do not guess: emit a `// TODO(script):`
comment in the diff describing the exact change needed. All new UI strings in
French (fr-CA).

Option B: Extract a child component

Paste this file + the relevant template section of the parent + the script portions that move with it + the matching scoped CSS, then say:

Extract the section anchored at [jobSheet.open] into a new `[JobOptionsSheet].vue`
component in [src/components/planif/]. The parent should pass only the job object
and emit events for mutations. Apply progressive disclosure: show Status + Reply
visible, rest under DisclosureSection « ▾ Options avancées ». Copy the scoped CSS
rules this markup uses into the new component. Output both the new component and
the parent's updated template section. All new UI strings in French (fr-CA).

Option C: Review a new component

Paste this file + the new component, then say:

Review this component for cognitive load following the UX rules above.
For each issue found, output:
- ANCHOR: the element (state/label name) + approximate line range
- PROBLEM: what causes overload
- FIX: concrete code change (not just advice)
- PRIORITY: P0 (confusing) / P1 (cluttered) / P2 (polish)

Do NOT suggest adding features. Only simplify.

Option D: Decompose a monolith

Paste this file + the full monolith (or the target section + its script), then say:

Decompose this monolith following the decomposition plan in the prompt.
Start with [PlanifHeader.vue]. Output:
1. The new child component (complete, working — template + script + scoped CSS)
2. The updated parent template section (replacing the extracted lines)
3. The props/emits interface between them

Keep all existing functionality. Do not rename CSS classes. Do not change any
API call, URL, payload shape, or emitted event name — refactors are UI-structure
only. Target: parent < 800 lines; ~400 only as the end-state after all
extractions (one extraction per session).

Definition of Done (every session)

A refactor is complete only when:

  1. npm run lint and npm run build pass in apps/ops (paste errors back if not).
  2. Manually verified in quasar dev (port 9001): the page renders, no new console errors.
  3. These invariants still work (PlanificationPage): drag-and-drop of jobs onto grid cells and kanban lanes (incl. drop preview badge, SortableJS day editor), SSE live updates (legacy-update listener), the 4 MapLibre maps (job-detail, location picker, assign panel, day route), keyboard shortcuts (Rule 12), and the Publier flow.
  4. No behavior change: same API calls, same payloads, same emitted events.

If you (the LLM) cannot run these, end your output with a checklist of exactly what the human must verify.


Token Optimization Tips

Technique Savings How / caveats
Send only <template> section 5873% (template is 2742% of these files) ONLY for layout-only reviews (rules 15, 8). Include <script setup> whenever the task touches rules 7/9/10/12, extracts a component (Options B/D), or reviews error handling — key handlers and gating live in script
One component at a time ~70% Don't paste 5 files; review sequentially
Specify viewport Response tokens Add "Target: 375×667 iPhone SE" for tech-module work to avoid desktop suggestions
Ask for diff, not full file ~60% response "Output changes as a unified diff, not the full file"
"Do NOT suggest adding features" ~30% response Prevents AI from adding complexity
Strip <style> section 826% (only ~8% on PlanificationPage/ClientDetailPage) ONLY for reviews (Option C). NEVER for extractions (Options B/D): scoped styles don't reach child components — the extracted markup needs its CSS copied along

Section boundaries (at 718698f) — to slice without opening the file: PlanificationPage template L11768 / script L17706033 / style L60356559.


Quick Reference: Quasar Components Used

Component Purpose Notes
q-btn / q-btn-group / q-btn-toggle / q-btn-dropdown Buttons Split buttons = q-btn-dropdown split (cf. « Publier »)
q-dialog Modal dialogs position="bottom" = mobile bottom-sheet
q-menu Dropdown/context menus context-menu = right-click; auto-close. For secondary-action overflow, use shared/OverflowMenu.vue instead
q-expansion-item Collapsible sections Prefer shared/DisclosureSection.vue (persisted open state); group prop for accordion
q-chip Tags, filters, status badges clickable makes it interactive
q-input / q-select Form fields dense outlined is the standard style
q-list / q-item Lists inside menus/dialogs q-item-section avatar for icons
q-tooltip Hover help text Overused — not a substitute for clear labels. For contextual help use shared/HelpHint.vue (ⓘ with title/rich text); the global behavior (click = hide, never 2 bubbles) lives in boot/tooltip-ux.js — don't fix tooltips one by one
q-banner Alerts, notifications Status banners (offline, warnings)

— End of prompt —