# 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|to,disabled?}]}]"` or flat `:items`) |
| 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 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 `#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 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 | **largely fixed 2026-07-14** | Was 20+ controls. Now: view switch · nav · date · journal + 5 primary actions (À assigner, Créer, Outils, Suggérer, Publier) + grouped ⋮ (★ défaut relocated there + ⌘K). HelpHint removed (explainer lives in the Suggérer dialog banner) |
| PL2 | WALL_OF_OPTIONS | `label="Outils"` dropdown ~L71-125 | **fixed 2026-07-14** | Was 13 items + nested submenu. Now 11 flat items; the 2 entries duplicating always-visible controls (À assigner, vue Mois) removed |
| 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 | **fixed** | Publier is a unified split q-btn-dropdown (color=positive, dirty-count, outline-when-clean); sibling-control count resolved with PL1 |
| PL5 | MENU_IN_MENU | « Modèles de semaine » ~L85-97 | **fixed 2026-07-14** | Extracted to `components/planif/WeekTemplatesDialog.vue` (props data / emits save·apply·set-default·delete; state stays in the page). Reachable via Outils, ⋮ and ⌘K |
| PL6 | ORPHAN_FIELD | `label="Max h/sem"` ~L159 | **fixed 2026-07-14** | Deleted — the input was dead code (ref never consumed by any filter/solver) |
| 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 | 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 —
