diff --git a/fusion_plating/.gitignore b/fusion_plating/.gitignore new file mode 100644 index 00000000..29075c88 --- /dev/null +++ b/fusion_plating/.gitignore @@ -0,0 +1,10 @@ +# Superpowers brainstorm session artifacts (mockups, HTML drafts). +# The companion server saves files here; not project source. +.superpowers/ + +# Local Odoo dev artifacts +*.pyc +__pycache__/ +*.egg-info/ +.idea/ +.vscode/ diff --git a/fusion_plating/docs/superpowers/plans/2026-05-17-portal-dashboard-redesign-plan.md b/fusion_plating/docs/superpowers/plans/2026-05-17-portal-dashboard-redesign-plan.md new file mode 100644 index 00000000..b1e11c62 --- /dev/null +++ b/fusion_plating/docs/superpowers/plans/2026-05-17-portal-dashboard-redesign-plan.md @@ -0,0 +1,2592 @@ +# 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 `