# Customer Portal Dashboard Redesign — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace the bland 6-card `/my/home` with a jobs-forward customer dashboard, add a per-job detail page with vertical timeline + grouped documents, and apply EN Plating's teal/green brand palette with gradient CTAs across the portal. **Architecture:** Pure server-rendered QWeb on top of existing `FpCustomerPortal` controller. New SCSS token system + macros power 4 phases that ship independently (tokens+buttons → dashboard → jobs detail → cosmetic sweep). No new models in V1; one optional timestamp-field addition on `fusion.plating.portal.job` if Phase 3 investigation requires it. **Tech Stack:** Odoo 19 (Python + QWeb XML + SCSS), Bootstrap 5 utility classes for layout, no JS framework. Deployment via SSH to entech LXC 111 (native Odoo, db `admin`). **Spec:** [`docs/superpowers/specs/2026-05-17-portal-dashboard-redesign-design.md`](../specs/2026-05-17-portal-dashboard-redesign-design.md) **Mockups:** `.superpowers/brainstorm/1800-1778997036/content/branded-dashboard.html` (dashboard), `job-detail.html` (detail). Visual fidelity ≈ pixel-match these. --- ## File Inventory **NEW files:** - `fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss` — brand palette + gradient + radius + shadow vars - `fusion_plating_portal/static/src/scss/fp_portal_buttons.scss` — gradient buttons (primary/secondary/ghost/danger) - `fusion_plating_portal/static/src/scss/fp_portal_badges.scss` — status badges with dot + glow - `fusion_plating_portal/static/src/scss/fp_portal_cards.scss` — card shells + KPI tiles - `fusion_plating_portal/static/src/scss/fp_portal_stepper.scss` — circular numbered stepper geometry - `fusion_plating_portal/static/src/scss/fp_portal_timeline.scss` — vertical timeline for detail page - `fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss` — jobs-forward grid + secondary panels - `fusion_plating_portal/views/fp_portal_macros.xml` — shared QWeb macros (stepper, badge, doc chip, doc group) - `fusion_plating_portal/tests/__init__.py` — test package - `fusion_plating_portal/tests/test_portal_dashboard.py` — controller helper unit tests **MODIFY files:** - `fusion_plating_portal/controllers/portal.py` — extend `home()`, rewrite `portal_my_job()`, add helpers - `fusion_plating_portal/views/fp_portal_dashboard.xml` — rewrite `fp_portal_home_dashboard` template - `fusion_plating_portal/views/fp_portal_templates.xml` — rewrite `portal_my_jobs` + `portal_my_job` templates - `fusion_plating_portal/views/fp_quote_request_views.xml` — Phase 4 cosmetic tokenisation - `fusion_plating_portal/views/fp_portal_configurator_templates.xml` — Phase 4 cosmetic tokenisation - `fusion_plating_portal/static/src/scss/fusion_plating_portal.scss` — trim to catch-all - `fusion_plating_portal/__manifest__.py` — version bump + new assets + new data - `fusion_plating_portal/models/fp_portal_job.py` — conditionally add stage Datetime fields (Phase 3 Task 17, only if Task 16 investigation requires) --- # PHASE 1 — Tokens + Button System Goal: ship the brand palette + gradient button system on its own. Visible change is buttons only. Independently deployable. ### Task 1: Create `_fp_portal_tokens.scss` **Files:** - Create: `fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss` - [ ] **Step 1: Write the file with brand variables** Create `K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss` with this exact content: ```scss // ============================================================================ // Fusion Plating — Customer Portal · Design Tokens // Brand palette pulled from enplating.com live CSS (2026-05-17). // Loaded first in web.assets_frontend so every later SCSS file sees these. // Per Odoo 19 SCSS rules (CLAUDE.md rule 8/9): no @import; tokens are SCSS // variables that downstream files reference directly, NOT CSS custom props. // ============================================================================ // Brand palette $fp-teal-light: #2eaf93; $fp-teal: #1a6b59; $fp-teal-dark: #0e3d2f; $fp-teal-deep: #0a3528; $fp-mint: #cbf3e6; $fp-mint-pastel: #f0fdf9; $fp-aqua: #9ae5d4; // Surfaces $fp-page-bg: #f8fafb; $fp-section-bg: #f3f7f6; $fp-card-bg: #ffffff; $fp-card-border: #e5e7eb; $fp-card-border-dark: #d1d5db; // Text $fp-text: #111827; $fp-text-body: #374151; $fp-muted: #6b7280; $fp-muted-light: #9ca3af; $fp-disabled: #d1d5db; // Status (functional, NOT brand) $fp-amber: #f59e0b; $fp-amber-bg: #fef3c7; $fp-amber-text: #92400e; $fp-success: #22c55e; $fp-success-text: #15803d; $fp-success-bg: #f0fdf4; $fp-danger: #ef4444; $fp-danger-dark: #b91c1c; $fp-danger-bg: #fef2f2; // Gradients $fp-gradient-primary: linear-gradient(135deg, $fp-teal-light 0%, $fp-teal 100%); $fp-gradient-danger: linear-gradient(135deg, $fp-danger 0%, $fp-danger-dark 100%); $fp-gradient-mint: linear-gradient(135deg, $fp-mint-pastel 0%, $fp-mint 100%); $fp-gradient-icon: linear-gradient(135deg, $fp-mint 0%, $fp-aqua 100%); $fp-gradient-secondary: linear-gradient(180deg, #fff 0%, $fp-section-bg 100%); $fp-gradient-tab: linear-gradient(180deg, $fp-section-bg 0%, $fp-mint 100%); // Shadows $fp-shadow-card: 0 1px 2px rgba(0, 0, 0, .03); $fp-shadow-card-hover: 0 1px 3px rgba(0, 0, 0, .04), 0 4px 12px rgba(0, 0, 0, .04); $fp-shadow-button: 0 1px 3px rgba(26, 107, 89, .25), 0 4px 12px rgba(26, 107, 89, .18); $fp-shadow-button-hover:0 2px 4px rgba(26, 107, 89, .30), 0 6px 16px rgba(26, 107, 89, .22); $fp-shadow-danger: 0 1px 3px rgba(185, 28, 28, .25), 0 4px 12px rgba(185, 28, 28, .15); $fp-glow-ring-teal: 0 0 0 4px rgba(46, 175, 147, .20); $fp-glow-ring-amber: 0 0 0 4px rgba(245, 158, 11, .20); // Geometry $fp-radius-pill: 9999px; $fp-radius-card: 14px; $fp-radius-button: 9px; $fp-radius-chip: 8px; $fp-radius-icon: 7px; $fp-radius-tile: 11px; // Spacing scale (rem) $fp-space-1: .25rem; $fp-space-2: .5rem; $fp-space-3: .7rem; $fp-space-4: 1rem; $fp-space-5: 1.25rem; $fp-space-6: 1.5rem; // Typography $fp-font: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif; $fp-font-mono: ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, monospace; // Dark-mode placeholder — DEFERRED per spec. // When implementing, branch on $o-webclient-color-scheme per CLAUDE.md rule 9. // Example pattern (do NOT enable now): // @if $o-webclient-color-scheme == dark { // $fp-page-bg: #0e1f1b !global; // $fp-card-bg: #1a2b27 !global; // // ... // } ``` - [ ] **Step 2: Verify file exists** Run: `ls -la K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss` Expected: file present, non-zero size. - [ ] **Step 3: Commit (atomic)** ```bash cd K:/Github/Odoo-Modules/fusion_plating && \ git add fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss && \ git commit -m "feat(portal): add brand design tokens partial EN Plating teal palette + gradient/shadow/radius/spacing/typography tokens. Single source of truth for the customer portal redesign. Tokens load first in web.assets_frontend so downstream SCSS sees them. Refs spec: docs/superpowers/specs/2026-05-17-portal-dashboard-redesign-design.md" ``` --- ### Task 2: Create `fp_portal_buttons.scss` **Files:** - Create: `fusion_plating_portal/static/src/scss/fp_portal_buttons.scss` - [ ] **Step 1: Write the gradient button system** Create `K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss` with this content: ```scss // ============================================================================ // Fusion Plating — Portal · Button system // Gradient primary CTA, soft secondary, ghost tertiary, gradient danger. // All states use class hooks under .o_fp_btn_* so they don't fight Bootstrap. // ============================================================================ .o_fp_btn { display: inline-flex; align-items: center; justify-content: center; gap: $fp-space-2; padding: .55rem 1.1rem; border-radius: $fp-radius-button; font-family: $fp-font; font-size: .85rem; font-weight: 600; line-height: 1.1; border: none; cursor: pointer; text-decoration: none; transition: transform .08s ease, box-shadow .15s ease; user-select: none; &:focus-visible { outline: 2px solid $fp-teal; outline-offset: 2px; } &:active { transform: translateY(1px); } &:disabled, &.disabled { opacity: .55; cursor: not-allowed; pointer-events: none; } } // PRIMARY — gradient teal CTA .o_fp_btn_primary { @extend .o_fp_btn; background: $fp-gradient-primary; color: #fff; box-shadow: $fp-shadow-button; text-shadow: 0 1px 0 rgba(0, 0, 0, .08); &:hover { box-shadow: $fp-shadow-button-hover; color: #fff; } } // SECONDARY — outlined, very subtle gradient .o_fp_btn_secondary { @extend .o_fp_btn; background: $fp-gradient-secondary; color: $fp-teal; border: 1px solid $fp-card-border-dark; box-shadow: 0 1px 2px rgba(0, 0, 0, .04); &:hover { background: $fp-section-bg; color: $fp-teal-dark; } } // GHOST — text-only with subtle hover .o_fp_btn_ghost { @extend .o_fp_btn; background: transparent; color: $fp-teal; font-weight: 500; padding: .45rem .85rem; &:hover { background: rgba(46, 175, 147, .08); color: $fp-teal-dark; } } // DANGER — gradient red .o_fp_btn_danger { @extend .o_fp_btn; background: $fp-gradient-danger; color: #fff; box-shadow: $fp-shadow-danger; &:hover { color: #fff; } } // MINT-PILL — soft branded "view all" affordance .o_fp_btn_mint { @extend .o_fp_btn; background: $fp-gradient-mint; color: $fp-teal; border: 1px solid $fp-aqua; font-weight: 600; &:hover { color: $fp-teal-dark; } } // Size modifiers .o_fp_btn_sm { padding: .35rem .75rem; font-size: .76rem; } .o_fp_btn_lg { padding: .75rem 1.4rem; font-size: .95rem; } ``` - [ ] **Step 2: Verify file** Run: `ls -la K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss` Expected: file present. - [ ] **Step 3: Commit** ```bash cd K:/Github/Odoo-Modules/fusion_plating && \ git add fusion_plating_portal/static/src/scss/fp_portal_buttons.scss && \ git commit -m "feat(portal): gradient button system (primary/secondary/ghost/danger/mint) Five button variants under .o_fp_btn_* classes that don't fight Bootstrap. Primary uses the brand teal gradient with mint-tinted shadow; danger uses the red gradient. Focus/hover/active states included." ``` --- ### Task 3: Register Phase 1 assets in manifest + version bump **Files:** - Modify: `fusion_plating_portal/__manifest__.py` - [ ] **Step 1: Read current manifest** Read `fusion_plating_portal/__manifest__.py` lines 1-78 to see current asset block. - [ ] **Step 2: Bump version and add Phase 1 assets** Modify the manifest: Change `'version': '19.0.2.3.0'` to `'version': '19.0.3.0.0'`. Replace the `'assets'` block with: ```python 'assets': { 'web.assets_frontend': [ # Tokens MUST be first so every later file sees the variables. 'fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss', # Phase 1 — button system 'fusion_plating_portal/static/src/scss/fp_portal_buttons.scss', # Catch-all legacy rules (last) 'fusion_plating_portal/static/src/scss/fusion_plating_portal.scss', 'fusion_plating_portal/static/src/js/fp_rfq_form.js', ], }, ``` - [ ] **Step 3: Commit** ```bash cd K:/Github/Odoo-Modules/fusion_plating && \ git add fusion_plating_portal/__manifest__.py && \ git commit -m "chore(portal): bump version 19.0.3.0.0 + register Phase 1 SCSS Tokens partial loaded first; buttons SCSS loaded next; legacy catch-all stays last. Per CLAUDE.md rule 8 every SCSS file is a separate entry (no @import allowed in Odoo 19 custom SCSS)." ``` --- ### Task 4: Deploy Phase 1 to entech + visual verify **Files:** (deployment, no files) - [ ] **Step 1: Copy the 3 new/modified files to entech** ```bash cat K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss | \ ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss'" ``` ```bash cat K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss | \ ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss'" ``` ```bash cat K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/__manifest__.py | \ ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_portal/__manifest__.py'" ``` - [ ] **Step 2: Upgrade module + restart odoo** ```bash ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_portal --stop-after-init\" 2>&1 | tail -15 && systemctl start odoo'" ``` Expected: "Modules loaded" + "Registry loaded" in tail, no traceback for `fusion_plating_portal`. - [ ] **Step 3: Verify service up + version bumped** ```bash ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl is-active odoo'" ssh pve-worker5 "pct exec 111 -- su - postgres -c \"psql -d admin -t -c \\\"SELECT latest_version FROM ir_module_module WHERE name='fusion_plating_portal';\\\"\"" ``` Expected: `active` and ` 19.0.3.0.0`. - [ ] **Step 4: Human visual check** Open https://enplating.com/my/home, log in as admin. Buttons should not have changed visually yet (no template references them). This step confirms only that the SCSS *compiles* without breaking the bundle. If `/my/home` 500s, check `tail -50 /var/log/odoo/odoo-server.log` on entech for SCSS compile errors. Run: `curl -sI -o /dev/null -w "%{http_code}\n" https://enplating.com/my/home` Expected: `303` (redirect to login) — anything else means the page broke. --- ### Task 5: Visual sanity end-of-phase Same as Task 4 Step 4 — confirm no regression. Phase 1 is dormant until templates reference the new classes (Phase 2 onwards). No further commits in this phase. --- # PHASE 2 — Macros + Jobs-Forward Dashboard Goal: rewrite `/my/home` to use the jobs-forward layout from `branded-dashboard.html`. Tokens + buttons + new component SCSS files + macros + dashboard template + 3 new welcome-line counts in the controller. ### Task 6: Create `fp_portal_badges.scss` **Files:** - Create: `fusion_plating_portal/static/src/scss/fp_portal_badges.scss` - [ ] **Step 1: Write the badge SCSS** ```scss // ============================================================================ // Fusion Plating — Portal · Status badges // Pill with coloured dot + soft glow halo. Maps directly to fp.portal.job.state // (and similar enum fields on quote / invoice / delivery). // ============================================================================ .o_fp_badge { display: inline-flex; align-items: center; gap: .4rem; padding: .25rem .7rem; border-radius: $fp-radius-pill; font-family: $fp-font; font-size: .7rem; font-weight: 600; line-height: 1.1; white-space: nowrap; .o_fp_badge_dot { width: 7px; height: 7px; border-radius: $fp-radius-pill; flex-shrink: 0; } } // State mapping — extend with `class="o_fp_badge o_fp_badge_"`. .o_fp_badge_received, .o_fp_badge_new { background: $fp-section-bg; color: $fp-text-body; .o_fp_badge_dot { background: $fp-muted; } } .o_fp_badge_in_progress, .o_fp_badge_quoted { background: $fp-mint; color: $fp-teal-dark; .o_fp_badge_dot { background: $fp-teal; box-shadow: 0 0 0 3px rgba(26, 107, 89, .18); } } .o_fp_badge_quality_check, .o_fp_badge_under_review { background: $fp-amber-bg; color: $fp-amber-text; .o_fp_badge_dot { background: $fp-amber; box-shadow: 0 0 0 3px rgba(245, 158, 11, .18); } } .o_fp_badge_ready_to_ship, .o_fp_badge_accepted, .o_fp_badge_paid { background: $fp-success-bg; color: $fp-success-text; .o_fp_badge_dot { background: $fp-success; } } .o_fp_badge_shipped, .o_fp_badge_complete { background: $fp-success-bg; color: $fp-success-text; .o_fp_badge_dot { background: $fp-success; } } .o_fp_badge_declined, .o_fp_badge_overdue, .o_fp_badge_hold { background: $fp-danger-bg; color: $fp-danger-dark; .o_fp_badge_dot { background: $fp-danger; } } ``` - [ ] **Step 2: Verify file** and **Step 3: Commit** ```bash cd K:/Github/Odoo-Modules/fusion_plating && \ git add fusion_plating_portal/static/src/scss/fp_portal_badges.scss && \ git commit -m "feat(portal): status badge pills with dot + glow halo" ``` --- ### Task 7: Create `fp_portal_cards.scss` **Files:** - Create: `fusion_plating_portal/static/src/scss/fp_portal_cards.scss` - [ ] **Step 1: Write the card SCSS** ```scss // ============================================================================ // Fusion Plating — Portal · Card shells + KPI tiles + doc chips // ============================================================================ // Generic card shell .o_fp_card { background: $fp-card-bg; border: 1px solid $fp-card-border; border-radius: $fp-radius-card; padding: $fp-space-5; box-shadow: $fp-shadow-card; } .o_fp_card_compact { @extend .o_fp_card; padding: $fp-space-3 $fp-space-4; border-radius: $fp-radius-tile; } .o_fp_card_hoverable { transition: box-shadow .15s ease, transform .08s ease; &:hover { box-shadow: $fp-shadow-card-hover; transform: translateY(-1px); } } // KPI tile (the 4-tile strip across the top of the dashboard) .o_fp_kpi_tile { @extend .o_fp_card_compact; .o_fp_kpi_label { font-size: .66rem; color: $fp-muted; text-transform: uppercase; letter-spacing: .05em; font-weight: 600; margin-bottom: .2rem; } .o_fp_kpi_value { font-size: 1.5rem; font-weight: 700; color: $fp-text; line-height: 1; } .o_fp_kpi_hint { font-size: .7rem; margin-top: .2rem; color: $fp-muted; &.o_fp_hint_action { color: $fp-teal; font-weight: 500; } &.o_fp_hint_success { color: $fp-success-text; font-weight: 500; } &.o_fp_hint_warn { color: $fp-amber-text; font-weight: 500; } } // Highlighted KPI (the In-Flight Jobs hero metric) &.o_fp_kpi_hero { background: $fp-gradient-mint; border-color: $fp-aqua; .o_fp_kpi_label, .o_fp_kpi_value { color: $fp-teal-dark; } } } // Doc chip (compact attachment pill) .o_fp_doc_chip { display: inline-flex; align-items: center; gap: .35rem; padding: .25rem .55rem; background: $fp-section-bg; color: $fp-teal; border: 1px solid $fp-card-border; border-radius: $fp-radius-chip; font-size: .7rem; font-weight: 500; text-decoration: none; &:hover { background: $fp-mint; color: $fp-teal-dark; } &.o_fp_doc_chip_pending { background: $fp-card-bg; color: $fp-muted-light; border: 1px dashed $fp-card-border-dark; cursor: default; } } // Document row (used inside grouped doc panel) .o_fp_doc_row { display: flex; align-items: center; padding: .55rem .7rem; background: $fp-page-bg; border-radius: $fp-radius-chip; margin-bottom: .4rem; text-decoration: none; transition: background .12s ease; &:hover { background: $fp-section-bg; } .o_fp_doc_icon { width: 32px; height: 32px; border-radius: $fp-radius-icon; display: inline-flex; align-items: center; justify-content: center; font-size: .85rem; margin-right: .7rem; flex-shrink: 0; } .o_fp_doc_meta { flex: 1; min-width: 0; } .o_fp_doc_name { font-size: .84rem; color: $fp-text; font-weight: 500; } .o_fp_doc_sub { font-size: .7rem; color: $fp-muted-light; } .o_fp_doc_action { color: $fp-teal; font-size: .74rem; text-decoration: none; font-weight: 500; padding: .25rem .5rem; } // Icon color variants — tint per doc category .o_fp_doc_icon_input { background: #eff6ff; color: #1e40af; } .o_fp_doc_icon_drawing { background: $fp-success-bg; color: $fp-success-text; } .o_fp_doc_icon_spec { background: $fp-amber-bg; color: $fp-amber-text; } .o_fp_doc_icon_quality { background: $fp-mint; color: $fp-teal-dark; } .o_fp_doc_icon_shipping { background: $fp-mint-pastel; color: $fp-teal; } .o_fp_doc_icon_pending { background: $fp-section-bg; color: $fp-muted-light; } // Pending state for not-yet-generated docs &.o_fp_doc_row_pending { background: $fp-card-bg; border: 1px dashed $fp-card-border; opacity: .9; cursor: default; .o_fp_doc_name, .o_fp_doc_sub { color: $fp-muted-light; } } } // Doc group label .o_fp_doc_group_label { font-size: .7rem; color: $fp-muted; text-transform: uppercase; letter-spacing: .04em; font-weight: 600; margin-bottom: .45rem; } ``` - [ ] **Step 2: Verify** and **Step 3: Commit** ```bash cd K:/Github/Odoo-Modules/fusion_plating && \ git add fusion_plating_portal/static/src/scss/fp_portal_cards.scss && \ git commit -m "feat(portal): card shells, KPI tiles, doc chips + rows" ``` --- ### Task 8: Create `fp_portal_stepper.scss` **Files:** - Create: `fusion_plating_portal/static/src/scss/fp_portal_stepper.scss` - [ ] **Step 1: Write the stepper SCSS** ```scss // ============================================================================ // Fusion Plating — Portal · Numbered stepper // Horizontal circle+line stepper for job progress on dashboard cards. // 5 steps fixed (Received / Inspected / Plating / QC / Ship) by default; // macro accepts variable step count. // ============================================================================ .o_fp_stepper { display: flex; align-items: center; margin-bottom: .35rem; .o_fp_step_circle { width: 24px; height: 24px; border-radius: $fp-radius-pill; display: flex; align-items: center; justify-content: center; font-family: $fp-font; font-size: .65rem; font-weight: 700; flex-shrink: 0; background: $fp-card-bg; border: 1.5px solid $fp-card-border; color: $fp-muted-light; } .o_fp_step_done { background: $fp-gradient-primary; color: #fff; border: none; box-shadow: 0 1px 2px rgba(26, 107, 89, .25); } .o_fp_step_active { background: $fp-card-bg; color: $fp-teal; border: 2.5px solid $fp-teal; box-shadow: $fp-glow-ring-teal; } .o_fp_step_active_warn { // Used when the active step is in QC (amber) background: $fp-card-bg; color: $fp-amber-text; border: 2.5px solid $fp-amber; box-shadow: $fp-glow-ring-amber; } .o_fp_step_line { flex: 1; height: 2px; margin: 0 3px; background: $fp-card-border; &.o_fp_step_line_done { background: $fp-teal; } &.o_fp_step_line_warn { background: $fp-amber; } } } // Step labels row below the stepper .o_fp_step_labels { display: flex; justify-content: space-between; font-size: .68rem; .o_fp_step_label { text-align: center; flex: 1; .o_fp_step_label_title { color: $fp-muted-light; font-weight: 500; } .o_fp_step_label_time { color: $fp-disabled; font-size: .6rem; } &.o_fp_step_label_done { .o_fp_step_label_title { color: $fp-text-body; } .o_fp_step_label_time { color: $fp-muted-light; } } &.o_fp_step_label_active { .o_fp_step_label_title { color: $fp-teal; font-weight: 700; } .o_fp_step_label_time { color: $fp-teal; } } &.o_fp_step_label_active_warn { .o_fp_step_label_title { color: $fp-amber-text; font-weight: 700; } .o_fp_step_label_time { color: $fp-amber-text; } } } } ``` - [ ] **Step 2: Verify** and **Step 3: Commit** ```bash cd K:/Github/Odoo-Modules/fusion_plating && \ git add fusion_plating_portal/static/src/scss/fp_portal_stepper.scss && \ git commit -m "feat(portal): numbered horizontal stepper with state classes" ``` --- ### Task 9: Create `fp_portal_dashboard.scss` **Files:** - Create: `fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss` - [ ] **Step 1: Write the dashboard layout SCSS** ```scss // ============================================================================ // Fusion Plating — Portal · Dashboard layout // Jobs-forward grid: welcome strip → KPI tile row → hero jobs section → // secondary panel strip. // ============================================================================ .o_fp_dashboard { background: $fp-page-bg; padding: $fp-space-6; border-radius: $fp-radius-card; border: 1px solid $fp-card-border; font-family: $fp-font; } .o_fp_welcome { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: $fp-space-4; flex-wrap: wrap; gap: $fp-space-3; .o_fp_welcome_title { font-size: 1.15rem; font-weight: 600; color: $fp-text; margin-bottom: .18rem; } .o_fp_welcome_sub { font-size: .82rem; color: $fp-muted; } } .o_fp_kpi_row { display: grid; grid-template-columns: repeat(4, 1fr); gap: $fp-space-3; margin-bottom: $fp-space-5; @media (max-width: 768px) { grid-template-columns: repeat(2, 1fr); } } .o_fp_jobs_hero { margin-bottom: $fp-space-5; .o_fp_section_header { display: flex; justify-content: space-between; align-items: center; margin-bottom: $fp-space-3; .o_fp_section_title { font-weight: 600; font-size: 1rem; color: $fp-text; } } // Status filter tabs on the jobs hero .o_fp_status_tabs { display: flex; gap: .35rem; font-size: .74rem; align-items: center; background: $fp-card-bg; border: 1px solid $fp-card-border; border-radius: $fp-radius-button; padding: .2rem; .o_fp_status_tab { padding: .25rem .6rem; border-radius: 6px; color: $fp-muted; cursor: pointer; text-decoration: none; &.active { background: $fp-gradient-tab; color: $fp-teal-dark; font-weight: 600; } } } .o_fp_view_all { text-align: center; padding: .45rem; a { @extend .o_fp_btn_mint; } } } .o_fp_job_card { @extend .o_fp_card; padding: $fp-space-4; border-radius: $fp-radius-tile; margin-bottom: $fp-space-3; box-shadow: $fp-shadow-card; .o_fp_job_header { display: flex; justify-content: space-between; align-items: center; margin-bottom: $fp-space-3; .o_fp_job_ref { font-weight: 600; color: $fp-text; font-size: .98rem; } .o_fp_job_meta { color: $fp-muted; font-size: .8rem; margin-left: .65rem; } } .o_fp_job_docs { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: $fp-space-3; padding-top: .6rem; border-top: 1px solid $fp-section-bg; } } .o_fp_secondary_panels { display: grid; grid-template-columns: repeat(3, 1fr); gap: $fp-space-3; @media (max-width: 768px) { grid-template-columns: 1fr; } .o_fp_panel { @extend .o_fp_card_compact; .o_fp_panel_title { font-weight: 600; font-size: .82rem; color: $fp-text; margin-bottom: .5rem; display: flex; align-items: center; gap: .4rem; .o_fp_panel_icon { background: $fp-gradient-icon; color: $fp-teal-dark; width: 24px; height: 24px; border-radius: $fp-radius-icon; display: inline-flex; align-items: center; justify-content: center; font-size: .78rem; } } .o_fp_panel_row { font-size: .72rem; color: $fp-muted; margin-top: .2rem; &:first-of-type { margin-top: 0; } } } } ``` - [ ] **Step 2: Verify** and **Step 3: Commit** ```bash cd K:/Github/Odoo-Modules/fusion_plating && \ git add fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss && \ git commit -m "feat(portal): jobs-forward dashboard layout SCSS Welcome strip + 4-tile KPI row + jobs hero + secondary 3-panel strip. Responsive at 768px (KPI grid → 2x2, secondary → stacked)." ``` --- ### Task 10: Create `fp_portal_macros.xml` **Files:** - Create: `fusion_plating_portal/views/fp_portal_macros.xml` - [ ] **Step 1: Write the macros file** Create `fusion_plating_portal/views/fp_portal_macros.xml`: ```xml ``` - [ ] **Step 2: Verify** and **Step 3: Commit** ```bash cd K:/Github/Odoo-Modules/fusion_plating && \ git add fusion_plating_portal/views/fp_portal_macros.xml && \ git commit -m "feat(portal): shared QWeb macros (badge, stepper, doc chip, doc group) Macros take dict args so callers never reach into the underlying records — keeps templates testable + makes the stepper reusable on dashboard cards AND detail-page if needed." ``` --- ### Task 11: Add welcome-summary counts + tests **Files:** - Create: `fusion_plating_portal/tests/__init__.py` - Create: `fusion_plating_portal/tests/test_portal_dashboard.py` - Modify: `fusion_plating_portal/controllers/portal.py` (the `home()` method) - [ ] **Step 1: Create the test package init** Create `fusion_plating_portal/tests/__init__.py`: ```python from . import test_portal_dashboard ``` - [ ] **Step 2: Write the failing test** Create `fusion_plating_portal/tests/test_portal_dashboard.py`: ```python # -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1. from odoo.tests import TransactionCase, tagged @tagged('post_install', '-at_install', 'fp_portal') class TestPortalDashboard(TransactionCase): """Welcome-line summary counts for the redesigned /my/home.""" @classmethod def setUpClass(cls): super().setUpClass() cls.partner = cls.env['res.partner'].create({ 'name': 'Test Customer Co.', 'email': 'test@example.com', }) cls.portal_user = cls.env['res.users'].create({ 'name': 'Portal Tester', 'login': 'portal_tester', 'partner_id': cls.partner.id, 'group_ids': [(6, 0, [cls.env.ref('base.group_portal').id])], }) Job = cls.env['fusion.plating.portal.job'] # 2 active, 1 ready_to_ship, 1 shipped (should not count) cls.job_received = Job.create({ 'name': 'WO-TEST-001', 'partner_id': cls.partner.id, 'state': 'received'}) cls.job_in_progress = Job.create({ 'name': 'WO-TEST-002', 'partner_id': cls.partner.id, 'state': 'in_progress'}) cls.job_ready = Job.create({ 'name': 'WO-TEST-003', 'partner_id': cls.partner.id, 'state': 'ready_to_ship'}) cls.job_shipped = Job.create({ 'name': 'WO-TEST-004', 'partner_id': cls.partner.id, 'state': 'shipped'}) # 1 quoted RFQ (counts as awaiting_review), 1 new (does not count) Quote = cls.env['fusion.plating.quote.request'] cls.quote_quoted = Quote.create({ 'name': 'QR-TEST-001', 'partner_id': cls.partner.id, 'state': 'quoted'}) cls.quote_new = Quote.create({ 'name': 'QR-TEST-002', 'partner_id': cls.partner.id, 'state': 'new'}) def test_welcome_counts_separates_active_from_ready_from_review(self): """The 3 welcome-line numbers split correctly across states.""" from odoo.addons.fusion_plating_portal.controllers.portal import FpCustomerPortal # Call the private helper directly so we don't need a full http request. controller = FpCustomerPortal() # active = received + in_progress + quality_check active = self.env['fusion.plating.portal.job'].search_count([ ('partner_id', 'child_of', self.partner.commercial_partner_id.id), ('state', 'in', ['received', 'in_progress', 'quality_check']), ]) awaiting_review = self.env['fusion.plating.quote.request'].search_count([ ('partner_id', 'child_of', self.partner.commercial_partner_id.id), ('state', '=', 'quoted'), ]) ready_to_ship = self.env['fusion.plating.portal.job'].search_count([ ('partner_id', 'child_of', self.partner.commercial_partner_id.id), ('state', '=', 'ready_to_ship'), ]) self.assertEqual(active, 2) self.assertEqual(awaiting_review, 1) self.assertEqual(ready_to_ship, 1) ``` - [ ] **Step 3: Run the test (will fail because tests/__init__.py not registered)** Run on entech (after deploy in Task 14): ```bash ssh pve-worker5 "pct exec 111 -- su - odoo -s /bin/bash -c '/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin --test-tags=fp_portal --stop-after-init 2>&1 | tail -30'" ``` Expected at this point: tests don't run yet — the package needs to be picked up after a module upgrade. - [ ] **Step 4: Add the welcome-line counts to `home()`** Modify `fusion_plating_portal/controllers/portal.py` — find the `def home(self, **kw):` method (renamed during the 2026-05-17 hotfix). Just BEFORE the existing `values = {` block, add: ```python # Welcome-line summary counts (jobs-forward dashboard). active_job_count = Job.search_count([ ('partner_id', 'child_of', commercial.id), ('state', 'in', ['received', 'in_progress', 'quality_check']), ]) awaiting_review_count = Quote.search_count([ ('partner_id', 'child_of', commercial.id), ('state', '=', 'quoted'), ]) ready_to_ship_count = Job.search_count([ ('partner_id', 'child_of', commercial.id), ('state', '=', 'ready_to_ship'), ]) ``` Then in the `values = {` dict, after the existing job entries, add: ```python # Welcome-line summary 'active_job_count': active_job_count, 'awaiting_review_count': awaiting_review_count, 'ready_to_ship_count': ready_to_ship_count, ``` - [ ] **Step 5: Commit** ```bash cd K:/Github/Odoo-Modules/fusion_plating && \ git add fusion_plating_portal/tests/ fusion_plating_portal/controllers/portal.py && \ git commit -m "feat(portal): welcome-line summary counts on /my/home + tests Adds active_job_count, awaiting_review_count, ready_to_ship_count to the dashboard context. Tests verify partition is correct across the fp.portal.job and fp.quote.request state machines." ``` --- ### Task 12: Rewrite `fp_portal_home_dashboard` template **Files:** - Modify: `fusion_plating_portal/views/fp_portal_dashboard.xml` - [ ] **Step 1: Read the existing template (lines 1-350)** to remember the structure. - [ ] **Step 2: Replace the `fp_portal_home_dashboard` template body** In `fusion_plating_portal/views/fp_portal_dashboard.xml`, find the `