From 0593b70354e6ba3c4d116557f0b9de172f195cd3 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 17 May 2026 13:21:21 -0400 Subject: [PATCH] docs(portal): session handoff + sub-A IA spec + plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures everything the next Claude session needs to pick up cold: - Live module versions on entech (portal 19.0.3.7.0, jobs/reports versions, all 5 tests green) - What shipped this session (24+ commits, summarised by area) - Sub-A (IA + sidebar) brainstorm decisions locked, spec written, plan ready to execute (11 tasks, 4 phases) - What's deferred (sub-B multi-user, sub-C search, drafts, real statements, RMA portal, top-recurring-parts) and WHY — so next session doesn't re-litigate - Gotchas hit + fixed this session that aren't obvious from code - Deploy recipe (file copy + module upgrade + cache bust) used 20+ times this session CLAUDE.md's Recent Session Handoff section now points to the new handoff doc; the previous handoff is kept as 'superseded but kept for context' below it. Co-Authored-By: Claude Opus 4.7 (1M context) --- fusion_plating/CLAUDE.md | 8 +- .../2026-05-17-portal-redesign-handoff.md | 153 ++ .../2026-05-17-portal-ia-sidebar-plan.md | 1415 +++++++++++++++++ .../2026-05-17-portal-ia-sidebar-design.md | 235 +++ 4 files changed, 1809 insertions(+), 2 deletions(-) create mode 100644 fusion_plating/docs/superpowers/handoffs/2026-05-17-portal-redesign-handoff.md create mode 100644 fusion_plating/docs/superpowers/plans/2026-05-17-portal-ia-sidebar-plan.md create mode 100644 fusion_plating/docs/superpowers/specs/2026-05-17-portal-ia-sidebar-design.md diff --git a/fusion_plating/CLAUDE.md b/fusion_plating/CLAUDE.md index 5bb2ace5..d346a63a 100644 --- a/fusion_plating/CLAUDE.md +++ b/fusion_plating/CLAUDE.md @@ -3,9 +3,13 @@ ## Project Fusion Plating is a multi-module Odoo 19 ERP for electroless nickel plating and metal finishing shops. Built by Nexa Systems for EN Technologies (the client). Replaces Steelhead Software. -## Recent Session Handoff — 2026-05-17 +## Recent Session Handoff — 2026-05-17 (Portal Redesign + Sub-A IA approved) -> **For the next Claude session.** All changes below are LIVE on entech (db `admin` on LXC 111 / pve-worker5). Local repo has uncommitted changes against base commit `9ebf89b`; run `git status` to see them. All durable conventions added during this session are folded into the Critical Rules below (rules 6a, 14, 14a, 14b) — read those FIRST before changing reports / stickers / signatures / SO line tax fields. +> **For the next Claude session.** Portal dashboard + jobs + detail + configurator are LIVE on entech at `fusion_plating_portal 19.0.3.7.0`. Sub-A (Portal IA + Sidebar) is brainstormed, spec'd, planned, NOT yet executed — pick up there. Full handoff (live state, decisions, gotchas, how-to-deploy, what's deferred): **[`docs/superpowers/handoffs/2026-05-17-portal-redesign-handoff.md`](docs/superpowers/handoffs/2026-05-17-portal-redesign-handoff.md)**. Approved plan ready to execute: **[`docs/superpowers/plans/2026-05-17-portal-ia-sidebar-plan.md`](docs/superpowers/plans/2026-05-17-portal-ia-sidebar-plan.md)** (11 tasks, 4 phases). Don't re-brainstorm; just execute. + +### Previous handoff (pre-portal redesign — superseded but kept for context) + +> All changes below are LIVE on entech (db `admin` on LXC 111 / pve-worker5). Local repo has uncommitted changes against base commit `9ebf89b`; run `git status` to see them. All durable conventions added during this session are folded into the Critical Rules below (rules 6a, 14, 14a, 14b) — read those FIRST before changing reports / stickers / signatures / SO line tax fields. ### Shipped this session diff --git a/fusion_plating/docs/superpowers/handoffs/2026-05-17-portal-redesign-handoff.md b/fusion_plating/docs/superpowers/handoffs/2026-05-17-portal-redesign-handoff.md new file mode 100644 index 00000000..affac1fb --- /dev/null +++ b/fusion_plating/docs/superpowers/handoffs/2026-05-17-portal-redesign-handoff.md @@ -0,0 +1,153 @@ +# Portal Redesign — Session Handoff (2026-05-17) + +> **Read this first.** This session ran long; the next session picks up here. Everything below is intentionally short. Authoritative details live in the linked spec / plan files. + +## TL;DR + +Customer-portal redesign across two long sessions. Dashboard + jobs + detail page + configurator are LIVE on entech. The next step (sidebar nav + page audit + Account Summary view) has an APPROVED PLAN ready to execute — do not re-brainstorm, just execute. + +**Immediately actionable:** execute [`docs/superpowers/plans/2026-05-17-portal-ia-sidebar-plan.md`](../plans/2026-05-17-portal-ia-sidebar-plan.md) via `superpowers:subagent-driven-development` or `superpowers:executing-plans`. User was offered both at handoff time and chose subagent-driven (preferred). 11 tasks across 4 phases. + +## Live state on entech (2026-05-17) + +| Module | Version live | Notes | +|---|---|---| +| `fusion_plating_portal` | `19.0.3.7.0` | Dashboard, job cards, configurator, detail page, doc downloads, repeat order, animations — all shipped | +| `fusion_plating_jobs` | `19.0.10.8.0` + write-hook + create-init | fp.job → fp.portal.job state-sync hook on write, initial state derive on create | +| `fusion_plating_reports` | `19.0.11.15.0` | Customer Acceptance / Authorized Representative signature blocks removed from `report_fp_sale_portrait/landscape` | +| All 5 portal unit tests green | | `--test-tags=fp_portal` | + +Branch: `main`. Local repo is many commits ahead of `origin/main`; user has not been asked to push (per system-prompt safety default). Run `git log --oneline origin/main..HEAD` at session start to see what's outstanding. + +## What shipped this session (high-level) + +1. **Dashboard rebuild** — `/my/home` → jobs-forward layout (KPI tiles → Active Work Orders hero → 5 secondary panels). Welcome line summarises status in plain words. EN Plating teal brand palette with gradient CTAs. +2. **Job card upgrade** — shared `fp_portal_job_card` macro (used by `/my/home` + `/my/jobs`). Wrap div + inner anchor + sibling actions footer (4 doc download chips + Repeat Order POST form). Part info + ship-to address pulled inline. Pulse animation on the active step circle + matching detail-page timeline dot. +3. **Detail page** — V2 stepper + V3 timestamps + 5-group document panel (From You / Specifications / Work Order / Quality / Shipping). Sales Order Confirmation, Work Order Detail, CoC, Packing Slip all sudo-render from the FP custom reports. Hero shows part + ship-to. +4. **Configurator fixes** — `/my/configurator/coating` 500 fixed (`fp.coating.config` → `fusion.plating.process.type`). Manual measurements hidden in step 1. Split single-file upload into Drawing (PDF) + 3D Model. +5. **Sale report cleanup** — Customer Acceptance / Authorized Representative signature block removed. +6. **Misc** — `/my` route added, button sizing normalised, hover-underline suppressed globally, sidebar of legacy stuff redirected, dashboard expanded to 5 panels (Quote Requests + Purchase Orders added). + +24+ commits this session, all on `main`. Browse `git log --oneline -30` for the full sweep. + +## What's queued for execution + +**Sub-A (Portal IA + Sidebar):** plan ready, not yet executed. Brainstorm decisions baked in: + +| Decision | Choice | +|---|---| +| Sidebar shape | **B** — Dashboard top, then grouped Activity / Documents / Account sections | +| Account Summary tabs | 3 (Invoices / Credit Memos / Statements) + Open Balance pill in header | +| Statements V1 | Placeholder card ("Coming soon") — real statement generation deferred | +| Legacy URL redirects | `/my/fp_invoices` → `/my/account_summary`; `/my/purchase_orders` → `/my/orders` (Odoo default); `/my/quote_requests/new` GET → `/my/configurator/new` | +| Future Users / Search slots | Omit from V1 (no "coming soon" placeholders); add when sub-B/sub-C ship | + +Spec: [`docs/superpowers/specs/2026-05-17-portal-ia-sidebar-design.md`](../specs/2026-05-17-portal-ia-sidebar-design.md) + +## What's deferred (do NOT re-litigate in next session) + +These were explicitly scoped OUT during brainstorming. Open new brainstorm sessions for each when their turn comes: + +- **Sub-B Multi-user account management** — invite teammates, role per user, per-action ACLs. Will add a Users item under the Account section of the sidebar. +- **Sub-C Portal search** — global search across jobs / quotes / invoices / certs. Search input slot above Dashboard in the sidebar. +- **Saved drafts (RFQ)** — user mentioned wanting drafts during configurator. Three scoping options proposed (minimal/medium/big); awaiting user direction. Not part of sub-A. +- **Real Statements generation** — account.followup integration OR cron-precomputed monthly PDFs. Decide during sub-A Phase 3 implementation or defer to its own follow-up. +- **Top Recurring Parts / Favorites / SerialNumber Lookup** — competitor-style features; deferred until customer demand confirmed. +- **RMA customer portal** — sub-12 RMA backend exists; portal exposure is its own sub-project. + +## Gotchas that bit us this session + +Future Claude will hit these too unless documented. Most are already inline in CLAUDE.md or MEMORY.md. Worth a re-skim before touching the portal: + +1. **`fp.coating.config` is retired** (Sub-11 cleanup). Use `fusion.plating.process.type` as the customer-facing coating taxonomy. Multiple `*.py` files still reference the dead model in COMMENTS — don't pattern-match from those. +2. **Portal users can't read `fp.job` directly.** Controllers that return `fp.portal.job` records to a template MUST `sudo()` the search if the template traverses `job.x_fc_job_id`. Same pattern is already used for `sale.order`, `account.move`, `stock.picking`. Domain still filters to commercial partner tree. +3. **`sale_pdf_quote_builder` gates on `report_name == 'sale.report_saleorder'`** (already in MEMORY.md). For customer-facing SO PDFs on the portal, render the FP custom `fusion_plating_reports.report_fp_sale_portrait` instead, and use a dedicated portal route that sudo-renders so the QWeb template can walk into `fp.part.catalog` etc. +4. **Forms inside anchors is invalid HTML.** When making a whole card clickable AND embedding a Repeat-Order form inside, use a wrap div + inner anchor (main click target) + sibling actions footer (form lives here). Don't nest `
` inside ``. +5. **Groups list indexing drift.** `_fp_group_documents` builds the docs panel by appending to `groups[N]`. If you reorder the initial list or insert a new group mid-helper, every `groups[N]` reference shifts. The code has an inline warning comment now; respect it. +6. **Per-stage timestamps are NULL on records created before the write hook deployed.** `_fp_get_stage_timeline` has a Date-fallback chain (received_date → received_at; actual_ship_date → shipped_at) plus linear interpolation for middle stages. Records created post-hook get real datetimes from the `fp.job.write()` mirror. +7. **Stepper SCSS — `.o_fp_step_line` MUST stay nested inside `.o_fp_stepper`** (inline comment in the SCSS warns about this). When `flex:1` isn't applied because the rule slipped outside the parent, circles cluster on the left of the row. +8. **Stepper labels align via absolute positioning per-unit** (not as a separate flex container). Wider labels like "Inspected" overflow equally to both sides of their circle's centre. Don't revert to the dual-container approach. +9. **`fp.portal.job` state-sync map** uses `_FP_JOB_STATE_TO_PORTAL_STATE` in `fusion_plating_jobs/models/fp_job.py`. `on_hold` and `cancelled` deliberately NOT mirrored to the customer-facing state. Manager decision what to surface. + +## How to deploy (entech LXC 111 on pve-worker5) + +Same recipe used 20+ times this session. Per file: + +```bash +cat K:/Github/Odoo-Modules/fusion_plating// | \ + ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom//'" +``` + +Then upgrade module + run tests: + +```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 --test-tags=fp_portal --stop-after-init 2>&1 | tail -25\" && \ + systemctl start odoo'" +``` + +Bust asset cache for SCSS/JS changes: + +```bash +ssh pve-worker5 "pct exec 111 -- su - postgres -c \ + \"psql -d admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';\\\"\"" +``` + +Service status / version check: + +```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';\\\"\"" +``` + +## How to start the next session + +1. Open Claude Code in `K:\Github\Odoo-Modules\fusion-plating` (or `K:\Github\Odoo-Modules\fusion_plating` — both work, the dash dir has only 3 modules but it's the active working dir for the user's terminal). +2. First message: "Resume the portal sub-A IA work — execute the approved plan from this session." +3. New session should: + - Read `CLAUDE.md` (auto-loaded) — the "Recent Session Handoff" section at the top points back to this file + - Read this handoff doc + - Read the plan: `docs/superpowers/plans/2026-05-17-portal-ia-sidebar-plan.md` + - Invoke `superpowers:subagent-driven-development` (or `executing-plans` for inline mode) + - Execute the 11 tasks across 4 phases +4. Optional but useful: re-run the existing test suite first to confirm starting from green: `--test-tags=fp_portal --stop-after-init`. + +## Brainstorm artifacts + +Visual companion mockups for this session live in `.superpowers/brainstorm/*/content/` (gitignored). Useful for visual comparison if needed: +- `design-direction.html` — Modern SaaS / Corporate / Industrial picker +- `saas-refinements.html` — V1/V2/V3 card variants +- `dashboard-layout.html` — 6-card grid vs jobs-forward +- `job-detail.html`, `branded-job-detail.html` — detail page mockups +- `branded-dashboard.html` — final brand-applied dashboard +- `sidebar-structure.html` — flat vs grouped vs hybrid (chose grouped) + +Brainstorm server idles out after 30 min. Restart command: + +```bash +"C:/Users/gur_p/.claude/plugins/cache/claude-plugins-official/superpowers/5.0.7/skills/brainstorming/scripts/start-server.sh" \ + --project-dir "K:/Github/Odoo-Modules/fusion_plating" +``` + +(Run in background; URL appears in `.superpowers/brainstorm/*/state/server-info`.) + +## Critical files modified this session + +If the next session needs to read context fast: +- `fusion_plating_portal/controllers/portal.py` — most changes here +- `fusion_plating_portal/controllers/portal_configurator.py` — coating model swap + dual upload +- `fusion_plating_portal/views/fp_portal_dashboard.xml` — jobs-forward layout +- `fusion_plating_portal/views/fp_portal_templates.xml` — jobs list + detail rewrites +- `fusion_plating_portal/views/fp_portal_macros.xml` — `fp_portal_job_card`, `fp_portal_stepper`, `fp_portal_status_badge`, `fp_portal_doc_chip`, `fp_portal_doc_group` +- `fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss` — brand tokens +- `fusion_plating_portal/static/src/scss/fp_portal_*.scss` — 7 partials (buttons, badges, cards, stepper, timeline, dashboard, legacy catch-all) +- `fusion_plating_portal/models/fp_portal_job.py` — per-stage Datetime fields + write/create snapshot hooks +- `fusion_plating_jobs/models/fp_job.py` — fp.job → fp.portal.job state-sync hook +- `fusion_plating_portal/tests/test_portal_dashboard.py` — 5 tests, all green + +## What user feedback is still outstanding + +Nothing concrete waiting on user. Last thing the user did was approve the plan and say "create a handsoff script so i start a new session" — i.e., they want to pause here. Next session resumes execution. diff --git a/fusion_plating/docs/superpowers/plans/2026-05-17-portal-ia-sidebar-plan.md b/fusion_plating/docs/superpowers/plans/2026-05-17-portal-ia-sidebar-plan.md new file mode 100644 index 00000000..db4c2fad --- /dev/null +++ b/fusion_plating/docs/superpowers/plans/2026-05-17-portal-ia-sidebar-plan.md @@ -0,0 +1,1415 @@ +# Customer Portal IA + Sidebar — 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:** Wrap every `/my/*` portal page in a sticky 240px left sidebar (grouped Dashboard → Activity → Documents → Account), replace 3 thin custom pages with Odoo defaults or new equivalents, and ship a new `/my/account_summary` page with 3 tabs (Invoices · Credit Memos · Statements) + Open Balance pill. + +**Architecture:** New `fp_portal_shell` template inherits `portal.portal_layout` and injects the sidebar around every existing portal body — zero per-template edits for the chrome change. Sidebar data structure lives in one Python helper and feeds the template via `_prepare_portal_layout_values()`. New Account Summary page is a single controller + template, modeled on the existing Odoo `/my/invoices` portal pattern. + +**Tech Stack:** Odoo 19 (Python + QWeb XML + SCSS), vanilla JS for mobile hamburger toggle, no JS framework. Deployment via SSH to entech LXC 111 (native Odoo, db `admin`). + +**Spec:** [`docs/superpowers/specs/2026-05-17-portal-ia-sidebar-design.md`](../specs/2026-05-17-portal-ia-sidebar-design.md) + +--- + +## File Inventory + +**NEW files:** +- `fusion_plating_portal/views/fp_portal_shell.xml` — `portal.portal_layout` inherit + sidebar markup +- `fusion_plating_portal/views/fp_portal_account_summary.xml` — `portal_my_account_summary` template +- `fusion_plating_portal/static/src/scss/fp_portal_sidebar.scss` — sidebar layout, sticky, active state, sections, mobile drawer +- `fusion_plating_portal/static/src/js/fp_portal_sidebar.js` — hamburger toggle (vanilla JS, ~20 lines) + +**MODIFY files:** +- `fusion_plating_portal/controllers/portal.py` — add `portal_account_summary` route, 3 redirect routes, `_fp_sidebar_items()` helper, extend `_prepare_portal_layout_values()` +- `fusion_plating_portal/views/fp_portal_templates.xml` — delete the `portal_my_fp_invoices` template body (route is redirected) +- `fusion_plating_portal/tests/test_portal_dashboard.py` — add Account Summary tests (Open Balance, tab partitioning, filter, search) +- `fusion_plating_portal/__manifest__.py` — version bump 19.0.3.7.0 → 19.0.4.0.0 (sidebar is a significant chrome change, minor bump), register new XML + SCSS + JS files + +**Decisions baked in:** +- **Statements tab in V1 is a placeholder** ("Monthly statements coming soon — contact your sales rep for a copy"). Real statement generation (account.followup integration OR cron-precomputed PDFs) is its own follow-up — spec §Open Items §2/§5. +- Sidebar item active-state matched by `page_name` (FP routes) OR URL-prefix (Odoo defaults), one helper. + +--- + +# PHASE 1 — Sidebar Shell + +Goal: every `/my/*` page renders inside the new sidebar wrap. No page bodies change. Empty Account Summary placeholder; redirects + tabbed view come in Phases 2 and 3. + +### Task 1: Create `fp_portal_sidebar.scss` + +**Files:** +- Create: `fusion_plating_portal/static/src/scss/fp_portal_sidebar.scss` + +- [ ] **Step 1: Write the sidebar SCSS** + +Create the file with this content: + +```scss +// ============================================================================ +// Fusion Plating — Portal · Sidebar shell +// Sticky 240px left rail wrapping every /my/* page. Grouped sections +// (Dashboard / ACTIVITY / DOCUMENTS / ACCOUNT). Active page = mint +// gradient fill + brand teal left bar. Below 768px collapses to a +// hamburger drawer with backdrop. +// ============================================================================ + +.o_fp_portal_shell { + display: grid; + grid-template-columns: 240px 1fr; + gap: $fp-space-5; + align-items: start; + background: $fp-page-bg; + min-height: calc(100vh - 80px); + padding: $fp-space-4; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + gap: 0; + padding: $fp-space-3; + } +} + +.o_fp_portal_sidebar { + position: sticky; + top: $fp-space-4; + background: $fp-card-bg; + border: 1px solid $fp-card-border; + border-radius: $fp-radius-card; + padding: .85rem .5rem; + box-shadow: $fp-shadow-card; + font-family: $fp-font; + align-self: start; + + .o_fp_sidebar_header { + padding: .45rem .9rem .7rem; + font-size: .62rem; + color: $fp-muted; + font-weight: 700; + letter-spacing: .06em; + text-transform: uppercase; + border-bottom: 1px solid $fp-section-bg; + } + + .o_fp_sidebar_section_label { + padding: .85rem .9rem .25rem; + font-size: .62rem; + color: $fp-muted-light; + font-weight: 700; + letter-spacing: .06em; + text-transform: uppercase; + } + + .o_fp_sidebar_item { + display: flex; + align-items: center; + gap: .55rem; + padding: .5rem .9rem; + margin: .05rem .15rem; + color: $fp-text-body; + font-size: .85rem; + text-decoration: none; + border-radius: 6px; + border-left: 3px solid transparent; + transition: background .12s ease, color .12s ease; + + &:hover { + background: $fp-section-bg; + color: $fp-teal-dark; + text-decoration: none; + } + &.o_fp_sidebar_active { + background: linear-gradient(90deg, $fp-mint 0%, $fp-mint-pastel 100%); + color: $fp-teal-dark; + font-weight: 600; + border-left: 3px solid $fp-teal; + } + + .o_fp_sidebar_icon { + width: 1.15rem; + text-align: center; + flex-shrink: 0; + } + } + + .o_fp_sidebar_footer { + border-top: 1px solid $fp-section-bg; + margin: .7rem .15rem 0; + padding-top: .5rem; + } + + // Mobile: slide-in drawer + @media (max-width: 768px) { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: 280px; + z-index: 1040; + transform: translateX(-100%); + transition: transform .2s ease; + border-radius: 0; + border-top: none; + border-bottom: none; + border-left: none; + margin: 0; + + &.o_fp_open { + transform: translateX(0); + } + } +} + +// Mobile hamburger button (above main content, hidden on desktop) +.o_fp_portal_hamburger { + display: none; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + background: $fp-card-bg; + border: 1px solid $fp-card-border; + border-radius: $fp-radius-button; + color: $fp-teal; + margin-bottom: $fp-space-3; + cursor: pointer; + transition: background .12s ease; + + &:hover { background: $fp-section-bg; } + + @media (max-width: 768px) { + display: inline-flex; + } +} + +// Backdrop behind the open mobile drawer +.o_fp_portal_backdrop { + display: none; + position: fixed; + inset: 0; + background: rgba(15, 30, 30, .35); + z-index: 1030; + + &.o_fp_open { + display: block; + } +} +``` + +- [ ] **Step 2: Verify file written** + +Run: `ls -la K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_sidebar.scss` +Expected: file exists, non-zero size. + +- [ ] **Step 3: Commit** + +```bash +cd K:/Github/Odoo-Modules/fusion_plating && \ +git add fusion_plating_portal/static/src/scss/fp_portal_sidebar.scss && \ +git commit -m "feat(portal): sidebar shell SCSS — sticky 240px rail + mobile drawer + +Grouped sections via .o_fp_sidebar_section_label, active item gets +mint gradient fill + brand-teal left bar. Below 768px the sidebar +collapses to a fixed slide-in drawer (.o_fp_open class), with +.o_fp_portal_hamburger button + .o_fp_portal_backdrop as siblings. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 2: Create `fp_portal_sidebar.js` + +**Files:** +- Create: `fusion_plating_portal/static/src/js/fp_portal_sidebar.js` + +- [ ] **Step 1: Write the hamburger JS** + +```javascript +/** + * Fusion Plating — Portal sidebar hamburger toggle. + * Vanilla JS — no OWL / no jQuery. Loaded on every /my/* page. + * Below 768px the sidebar is translateX(-100%); toggling + * .o_fp_open on both sidebar + backdrop shows/hides it. + */ +(function () { + "use strict"; + + function init() { + var sidebar = document.querySelector(".o_fp_portal_sidebar"); + var hamburger = document.querySelector(".o_fp_portal_hamburger"); + var backdrop = document.querySelector(".o_fp_portal_backdrop"); + if (!sidebar || !hamburger || !backdrop) { + return; // sidebar not on this page (logged-out, error pages, etc.) + } + + function toggleOpen(force) { + var willOpen = (typeof force === "boolean") + ? force + : !sidebar.classList.contains("o_fp_open"); + sidebar.classList.toggle("o_fp_open", willOpen); + backdrop.classList.toggle("o_fp_open", willOpen); + } + + hamburger.addEventListener("click", function (e) { + e.preventDefault(); + toggleOpen(); + }); + backdrop.addEventListener("click", function () { + toggleOpen(false); + }); + // Close when navigating to a sidebar link on mobile + sidebar.querySelectorAll("a.o_fp_sidebar_item").forEach(function (a) { + a.addEventListener("click", function () { + if (window.innerWidth < 769) { + toggleOpen(false); + } + }); + }); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); +``` + +- [ ] **Step 2: Commit** + +```bash +cd K:/Github/Odoo-Modules/fusion_plating && \ +git add fusion_plating_portal/static/src/js/fp_portal_sidebar.js && \ +git commit -m "feat(portal): mobile sidebar hamburger toggle (vanilla JS) + +20 lines, no framework. Toggles .o_fp_open on sidebar + backdrop. +Backdrop click closes drawer; navigating a sidebar link on mobile +auto-closes. No-ops gracefully when sidebar isn't on the page +(logged-out, 500 pages, etc.). + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 3: Create `fp_portal_shell.xml` + +**Files:** +- Create: `fusion_plating_portal/views/fp_portal_shell.xml` + +- [ ] **Step 1: Inspect Odoo's portal.portal_layout template** + +Run on entech to see the exact wrapper structure: + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'grep -A 40 \"id=.portal_layout.\" /usr/lib/python3/dist-packages/odoo/addons/portal/views/portal_templates.xml | head -50'" +``` + +You're looking for the element that wraps page content — typically `
` or similar. Note the exact xpath anchor. + +- [ ] **Step 2: Write the shell template** + +Create `fusion_plating_portal/views/fp_portal_shell.xml`: + +```xml + + + + + + + + + + + + + + + +``` + +**Note:** The xpath approach in Step 2 above (wrapping `#wrap`'s children) is the cleanest in theory but Odoo's `portal.portal_layout` actual structure may vary by version. The Step 1 inspection result might force a different anchor. If `#wrap` isn't present, the fallback is to inherit at `portal.frontend_layout` or `website.layout` level and use a `t-call` to the existing portal body. **Update this task inline with the actual anchor before committing.** + +- [ ] **Step 3: Commit** + +```bash +cd K:/Github/Odoo-Modules/fusion_plating && \ +git add fusion_plating_portal/views/fp_portal_shell.xml && \ +git commit -m "feat(portal): sidebar shell template + portal.portal_layout inherit + +fp_portal_shell wraps every /my/* page (FP custom + Odoo default) +in a sticky-sidebar shell with no per-template edits. Sidebar markup +is a separate fp_portal_sidebar template that reads fp_sidebar_items ++ fp_partner_display_name from the page context. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 4: Sidebar data helper + layout values inject + +**Files:** +- Modify: `fusion_plating_portal/controllers/portal.py` + +- [ ] **Step 1: Add `_fp_sidebar_items()` helper to `FpCustomerPortal`** + +Find the `_fp_get_partner_domain` helper in `fusion_plating_portal/controllers/portal.py` (around line 111). Immediately after it, add: + +```python + # ========================================================================== + # Sidebar — items + active-state resolution + # ========================================================================== + # Sidebar item structure: list of dicts with `type` = 'item' | 'section_label'. + # Items have label / url / icon / key. Key matches either a page_name set by + # an FP route OR a URL prefix for Odoo default pages. + _FP_SIDEBAR_LAYOUT = [ + {'type': 'item', 'key': 'fp_dashboard', 'label': 'Dashboard', 'icon': '🏠', 'url': '/my/home'}, + {'type': 'section_label', 'label': 'Activity'}, + {'type': 'item', 'key': 'fp_quote_requests','label': 'Quote Requests', 'icon': '📄', 'url': '/my/quote_requests'}, + {'type': 'item', 'key': 'fp_configurator', 'label': 'Get a Quote', 'icon': '+', 'url': '/my/configurator'}, + {'type': 'item', 'key': 'odoo_orders', 'label': 'Purchase Orders', 'icon': '🛒', 'url': '/my/orders'}, + {'type': 'item', 'key': 'fp_jobs', 'label': 'Work Orders', 'icon': '⚙️', 'url': '/my/jobs'}, + {'type': 'section_label', 'label': 'Documents'}, + {'type': 'item', 'key': 'fp_certifications','label': 'Certifications', 'icon': '📑', 'url': '/my/certifications'}, + {'type': 'item', 'key': 'fp_deliveries', 'label': 'Packing Slips', 'icon': '📦', 'url': '/my/deliveries'}, + {'type': 'item', 'key': 'fp_account_summary','label': 'Account Summary', 'icon': '💰', 'url': '/my/account_summary'}, + {'type': 'section_label', 'label': 'Account'}, + {'type': 'item', 'key': 'odoo_account', 'label': 'Profile', 'icon': '👤', 'url': '/my/account'}, + ] + + # Map either a page_name (set by FP routes) OR a URL prefix + # (for Odoo defaults that don't set page_name) to a sidebar item key. + _FP_PAGE_NAME_TO_SIDEBAR_KEY = { + 'fp_dashboard': 'fp_dashboard', + 'fp_quote_requests': 'fp_quote_requests', + 'fp_quote_request': 'fp_quote_requests', + 'fp_configurator': 'fp_configurator', + 'fp_jobs': 'fp_jobs', + 'fp_portal_job': 'fp_jobs', + 'fp_certifications': 'fp_certifications', + 'fp_deliveries': 'fp_deliveries', + 'fp_account_summary': 'fp_account_summary', + } + _FP_URL_PREFIX_TO_SIDEBAR_KEY = [ + # Order matters — first match wins, so list longer prefixes first. + ('/my/orders', 'odoo_orders'), + ('/my/quotes', 'odoo_orders'), # /my/quotes is also sale_portal + ('/my/invoices', 'fp_account_summary'), + ('/my/account_summary', 'fp_account_summary'), + ('/my/account', 'odoo_account'), + ('/my/security', 'odoo_account'), + ('/my/home', 'fp_dashboard'), + ('/my', 'fp_dashboard'), # /my (no trailing) -> dashboard + ] + + def _fp_resolve_active_sidebar_key(self, url, page_name): + """Resolve which sidebar item should be marked active for this request.""" + if page_name and page_name in self._FP_PAGE_NAME_TO_SIDEBAR_KEY: + return self._FP_PAGE_NAME_TO_SIDEBAR_KEY[page_name] + if url: + for prefix, key in self._FP_URL_PREFIX_TO_SIDEBAR_KEY: + if url.startswith(prefix): + return key + return None + + def _fp_sidebar_items(self, url, page_name): + """Return the sidebar item list with the right item marked active.""" + active_key = self._fp_resolve_active_sidebar_key(url, page_name) + out = [] + for entry in self._FP_SIDEBAR_LAYOUT: + if entry.get('type') == 'item': + copy = dict(entry) + copy['active'] = (active_key == entry['key']) + out.append(copy) + else: + out.append(entry) + return out +``` + +- [ ] **Step 2: Extend `_prepare_portal_layout_values` to inject sidebar data** + +In the same file, find `_prepare_portal_layout_values` (around line 29). Replace its body with: + +```python + def _prepare_portal_layout_values(self): + values = super()._prepare_portal_layout_values() + # Resolve current URL + page_name for sidebar active-state + url = request.httprequest.path if request else '' + page_name = values.get('page_name') + values['fp_sidebar_items'] = self._fp_sidebar_items(url, page_name) + # Partner display name for the sidebar header + partner = request.env.user.partner_id + commercial = partner.commercial_partner_id + values['fp_partner_display_name'] = commercial.name or partner.name + return values +``` + +**IMPORTANT**: this overrides the same-name method we previously edited (counters). Make sure NOT to lose the counter-injection — the existing override builds `fp_quote_request_count`, `fp_portal_job_count`, etc. The new code should ADD to the values dict, not replace what's there. Verify the existing override still increments the counters; if not, restore that logic alongside the new sidebar data. + +- [ ] **Step 3: Commit** + +```bash +cd K:/Github/Odoo-Modules/fusion_plating && \ +git add fusion_plating_portal/controllers/portal.py && \ +git commit -m "feat(portal): _fp_sidebar_items helper + layout-values inject + +Drives the sidebar from a single Python data structure +(_FP_SIDEBAR_LAYOUT). Active state resolved by page_name lookup OR +URL-prefix match (so Odoo default pages like /my/orders and +/my/account light up correctly). _prepare_portal_layout_values +extends super() so existing counter injection (fp_quote_request_count +etc.) keeps firing. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 5: Register Phase 1 files + version bump + +**Files:** +- Modify: `fusion_plating_portal/__manifest__.py` + +- [ ] **Step 1: Bump version + register new files** + +Change `'version': '19.0.3.7.0'` → `'version': '19.0.4.0.0'`. + +In the `'data'` list, add `'views/fp_portal_shell.xml'` near the TOP (right after macros, before any template that might call sidebar context vars): + +```python +'data': [ + 'security/fp_portal_security.xml', + 'security/ir.model.access.csv', + 'data/fp_sequence_data.xml', + 'views/fp_portal_macros.xml', + 'views/fp_portal_shell.xml', # NEW — must load early + 'views/fp_quote_request_views.xml', + 'views/fp_portal_dashboard.xml', + 'views/fp_portal_templates.xml', + 'views/fp_portal_configurator_templates.xml', + 'views/fp_portal_breadcrumbs.xml', + 'views/fp_sale_order_portal.xml', + 'views/fp_menu.xml', +], +``` + +In the `'assets'` block, add `fp_portal_sidebar.scss` (after `fp_portal_dashboard.scss`, before the legacy catch-all) and `fp_portal_sidebar.js` (with the JS files at the end): + +```python +'assets': { + 'web.assets_frontend': [ + 'fusion_plating_portal/static/src/scss/_fp_portal_tokens.scss', + 'fusion_plating_portal/static/src/scss/fp_portal_buttons.scss', + 'fusion_plating_portal/static/src/scss/fp_portal_badges.scss', + 'fusion_plating_portal/static/src/scss/fp_portal_cards.scss', + 'fusion_plating_portal/static/src/scss/fp_portal_stepper.scss', + 'fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss', + 'fusion_plating_portal/static/src/scss/fp_portal_timeline.scss', + 'fusion_plating_portal/static/src/scss/fp_portal_sidebar.scss', # NEW + 'fusion_plating_portal/static/src/scss/fusion_plating_portal.scss', + 'fusion_plating_portal/static/src/js/fp_rfq_form.js', + 'fusion_plating_portal/static/src/js/fp_portal_sidebar.js', # NEW + ], +}, +``` + +- [ ] **Step 2: Commit** + +```bash +cd K:/Github/Odoo-Modules/fusion_plating && \ +git add fusion_plating_portal/__manifest__.py && \ +git commit -m "chore(portal): bump 19.0.4.0.0 + register sidebar shell + JS + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 6: Deploy Phase 1 to entech + visual verify + +**Files:** (deployment, no edits) + +- [ ] **Step 1: Copy 6 changed/new files to entech** + +```bash +for f in \ + static/src/scss/fp_portal_sidebar.scss \ + static/src/js/fp_portal_sidebar.js \ + views/fp_portal_shell.xml \ + controllers/portal.py \ + __manifest__.py; do + cat "K:/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/$f" | \ + ssh pve-worker5 "pct exec 111 -- bash -c 'mkdir -p \$(dirname /mnt/extra-addons/custom/fusion_plating_portal/$f) && cat > /mnt/extra-addons/custom/fusion_plating_portal/$f'" +done +``` + +- [ ] **Step 2: Upgrade module + restart** + +```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 --test-tags=fp_portal --stop-after-init 2>&1 | tail -15\" && systemctl start odoo'" +``` + +Expected: registry loaded clean, all existing tests still pass. + +- [ ] **Step 3: Bust asset cache** + +```bash +ssh pve-worker5 "pct exec 111 -- su - postgres -c \"psql -d admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';\\\"\"" +``` + +- [ ] **Step 4: Visual smoke test** + +Open in a logged-in browser, walk each URL and confirm sidebar appears with correct active item: + +| URL | Expected active sidebar item | +|---|---| +| https://enplating.com/my/home | Dashboard | +| https://enplating.com/my/quote_requests | Quote Requests | +| https://enplating.com/my/configurator | Get a Quote | +| https://enplating.com/my/orders (Odoo) | Purchase Orders | +| https://enplating.com/my/jobs | Work Orders | +| https://enplating.com/my/certifications | Certifications | +| https://enplating.com/my/deliveries | Packing Slips | +| https://enplating.com/my/account (Odoo) | Profile | + +Also test mobile: shrink browser below 768px, sidebar should disappear; click hamburger button (top-left of main content), drawer slides in; click backdrop or a sidebar link, drawer closes. + +If any URL doesn't show the sidebar, the `portal.portal_layout` inherit xpath in Task 3 didn't catch — revisit Step 1 of Task 3 (inspect Odoo's wrap structure) and adjust the xpath anchor. + +--- + +# PHASE 2 — Page Audit Redirects + +Goal: 3 legacy URLs redirect cleanly to their new homes. No content changes, just routes. + +### Task 7: Add 3 redirects + delete thin templates + +**Files:** +- Modify: `fusion_plating_portal/controllers/portal.py` +- Modify: `fusion_plating_portal/views/fp_portal_templates.xml` + +- [ ] **Step 1: Replace `portal_my_fp_invoices` body with redirect** + +Find the `portal_my_fp_invoices` route handler in `fusion_plating_portal/controllers/portal.py` (grep for `/my/fp_invoices`). Replace the entire method body with: + +```python + @http.route( + ['/my/fp_invoices', '/my/fp_invoices/page/'], + type='http', auth='user', website=True, + ) + def portal_my_fp_invoices(self, **kw): + """Legacy URL — redirected to /my/account_summary (Sub-A IA).""" + return request.redirect('/my/account_summary') +``` + +- [ ] **Step 2: Replace `portal_my_purchase_orders` body with redirect** + +Find `portal_my_purchase_orders` and replace its method body: + +```python + @http.route( + ['/my/purchase_orders', '/my/purchase_orders/page/'], + type='http', auth='user', website=True, + ) + def portal_my_purchase_orders(self, **kw): + """Legacy URL — redirected to Odoo default /my/orders (Sub-A IA).""" + return request.redirect('/my/orders') +``` + +- [ ] **Step 3: Replace `portal_my_quote_request_new` (or equivalent) with redirect** + +Find the GET handler for `/my/quote_requests/new`. Replace its body so a GET redirects to the configurator — but the POST handler (the actual form submit) MUST be preserved untouched because the existing RFQ form still submits there. If they share one method, split them: + +```python + @http.route( + ['/my/quote_requests/new'], + type='http', auth='user', website=True, + methods=['GET'], + ) + def portal_my_quote_request_new_get(self, **kw): + """GET — legacy entry point, redirected to the configurator wizard.""" + return request.redirect('/my/configurator/new') + + # POST kept for back-compat with the existing RFQ form button. + # If the form is fully retired in a later phase, drop this method. + @http.route( + ['/my/quote_requests/new'], + type='http', auth='user', website=True, + methods=['POST'], csrf=True, + ) + def portal_my_quote_request_new_post(self, **kw): + # ... existing POST body, unchanged + ... +``` + +If you find a single method handling both GET + POST, factor the redirect out: + +```python + def portal_my_quote_request_new(self, **kw): + if request.httprequest.method == 'GET': + return request.redirect('/my/configurator/new') + # ... existing POST body +``` + +- [ ] **Step 4: Delete the thin invoice + PO template bodies** + +In `fusion_plating_portal/views/fp_portal_templates.xml`, find `