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>
22 KiB
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 fromsrc/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,usePeriodNavigationalready 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)
- 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. - 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 inuseCommandPalette(⌘K) so they stay reachable — PlanificationPage already does this. - 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.
- No menu-in-menu: if a dropdown item opens a submenu, extract it into its own dialog.
- 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). - No
.vuefile > 800 lines — decompose into child components with focused responsibilities. - 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).
- One primary action per view state. If the user has to choose between 3+ equally prominent buttons, you've failed.
- Status feedback: every mutation shows visible feedback within 200ms — using the app's existing mechanisms ONLY:
$q.notifyfor results/errors (see theerr()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 silentlycatch()" applies to user-initiated mutations; background best-effort loads may catch silently but must carry a comment (existing convention:catch (e) { /* … best-effort */ }). - Dev-only UI hidden in production: gate ONLY on
import.meta.env.DEV(compile-time, stripped from prod builds) — NEVER on a customVITE_*flag:.env.localVITE_ vars are bundled even in prod builds (documented leak,src/config/hub.js). Canonical pattern: thecanTerminateDirectcomputed in TechJobDetailPage.vue. - 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.
- 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-levelonKeylistener, 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. - 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. - 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 insrc/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 onnavigator.onLinealone — it reports true on unusable 2-bar LTE (documented instores/offline.js). - Touch & focus (tech module): every tappable element ≥ 44×44 px (existing convention:
min-height: 44pxin TechJobDetailPage.vue). Keep visible:focus-visiblestates; never encode state by colour alone (add icon or label); use the tokens incss/tech.scss(--ink-*,--t-*) — no hardcoded colours. - Design tokens: any new or moved style uses the
--ops-*variables fromsrc/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;DispatchPageis the sole dark exception — never introduce dark styling elsewhere.
Expert-density surfaces (declutter rules relaxed)
On these surfaces, Rules 1–3 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#Ncomments. 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 L1–1768 · script L1770–6033 (~4,264 lines — the real bulk) · style L6035–6559.
| 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 | « 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:
npm run lintandnpm run buildpass inapps/ops(paste errors back if not).- Manually verified in
quasar dev(port 9001): the page renders, no new console errors. - 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-updatelistener), the 4 MapLibre maps (job-detail, location picker, assign panel, day route), keyboard shortcuts (Rule 12), and the Publier flow. - 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 |
58–73% (template is 27–42% of these files) | ONLY for layout-only reviews (rules 1–5, 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 |
8–26% (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 L1–1768 / script L1770–6033 / style L6035–6559.
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 —