Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-17-portal-dashboard-redesign-design.md
gsinghpal d3c5c25865
Some checks failed
fusion_accounting CI / test (fusion_accounting_ai) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_core) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_migration) (push) Has been cancelled
changes
2026-05-17 03:20:33 -04:00

20 KiB

Customer Portal Dashboard Redesign

Module: fusion_plating_portal Date: 2026-05-17 Status: Design locked, awaiting implementation plan Surface: https://enplating.com/my/*

Problem

The existing /my/home is functional but visually bland and (until the 2026-05-17 hotfix) was not even reaching the browser because of a controller-inheritance bug. The customer's primary question on login — "where is my part right now?" — was buried inside one of six equal-weight cards. There is no per-job view of progress over time, no grouped document surface for QA managers, and no brand alignment with the marketing site at enplating.com.

This redesign reframes the portal around jobs, gives each job a customer-visible timeline with timestamps, and surfaces every job-related document in a single grouped panel. Branding follows enplating.com's teal palette.

User stories

  1. As an aerospace QA manager, when I log in I want to see the status of every active job without clicking into anything, so I can answer "where's PO-7783?" in under 5 seconds.
  2. As a buyer, when I open a job's detail page I want to see exactly when each stage happened (date + time + operator name) so I can verify our SLA terms are being met.
  3. As a QA manager, when I need a Certificate of Conformance for an audit I want every document for that job grouped by category (customer inputs, specifications, quality records, shipping) so I'm not hunting through a list.
  4. As any customer, when I land on the portal I want it to look and feel like enplating.com — teal accents, gradient CTAs, brand typography — so it feels like the same company.

Locked design decisions (from brainstorming 2026-05-17)

Decision Choice Why
Design language Modern SaaS — generous whitespace, rounded corners (14px cards, 9px buttons, 9999px badges), soft shadows User picked "Modern SaaS" over Corporate B2B and Industrial in the design-direction screen.
Brand palette EN Plating teal/green pulled live from enplating.com User asked for "stay close to the site color schemes".
Primary card design Numbered circular stepper + horizontal connecting lines + date/time stamp under each step label User picked V2 (numbered stepper) and asked for V3's timestamps merged in.
Dashboard layout Jobs-forward: welcome → 4-tile KPI strip → Active Work Orders hero (full V2 cards) → 3-col secondary panels (Certs / Packing Slips / Invoices) User picked Layout B over the 6-equal-section grid.
CTA buttons Gradient #2eaf93 → #1a6b59 linear-135deg with brand-tinted box-shadow; danger is #ef4444 → #b91c1c gradient; secondary is flat with subtle #fff → #f3f7f6 gradient User asked for "color gradients on the buttons to make it modern and professional".
Stepper visuals Completed circles get gradient fill; active circle uses brand teal stroke + rgba(46,175,147,.2) mint glow ring Pulled from approved branded mockup.
Status badges Pill with width:7px;height:7px coloured dot + soft glow halo. Status-to-colour map: In Progress → mint; QC → amber; Ready / Done → green; Hold → red. Pulled from approved branded mockup.
Document categories From You (customer-uploaded: PO, drawings) · Specifications (FP customer spec) · Quality (CoC, Fischerscope thickness report) · Shipping (packing slip, tracking) Surfaced in the approved detail-page mockup and not contested.
Default landing /my/home (replaces stock Odoo home; already the case after the 2026-05-17 hotfix that renamed portal_my_home_dashboard()home()) See Critical Rule 16 in CLAUDE.md.

Scope

IN SCOPE — full structural + visual redesign:

  • /my/home — jobs-forward dashboard (template + controller already exist; need rewrite of template, controller data prep is mostly fine)
  • /my/jobs — list page, redesigned to use V2 card shape per row (or compact variant for list density)
  • /my/jobs/<id> — detail page using vertical timeline + grouped documents
  • Brand token system: a single SCSS partial (_fp_portal_tokens.scss) defining teal palette, button gradients, shadows, spacing scale, typography scale — referenced by every portal SCSS file
  • Reusable QWeb macros: fp_portal_stepper, fp_portal_doc_chip, fp_portal_status_badge — so every page renders consistent components

IN SCOPE — cosmetic-only (new tokens applied, no structural change):

  • /my/quote_requests (list + detail)
  • /my/purchase_orders (list + detail)
  • /my/fp_invoices (list + detail)
  • /my/deliveries (list + detail)
  • /my/certifications (list + detail)
  • /my/configurator (RFQ wizard)
  • Portal sidebar (just colour tokens; layout untouched)

OUT OF SCOPE — deferred:

  • Mobile-specific layout polish (the desktop design uses Bootstrap grid which collapses gracefully; explicit mobile breakpoints can ship in a follow-up)
  • Dark-mode parity (Odoo 19 compiles two bundles; if the customer toggles dark on their profile, the portal will fall back to the default Bootstrap dark and look broken until we ship the $o-webclient-color-scheme branch — flagged but not blocking)
  • Real-time push updates of job status (current refresh-on-load is acceptable; SSE / longpoll is a separate effort)
  • In-page document preview (PDFs download via existing ir.binary stream; no embedded viewer)
  • Internationalisation of the new strings (English / Canadian English; French follows the existing report_coc_fr pattern in a separate effort if needed)

Architecture

Surfaces and data sources

URL Controller method Template Primary data
/my/home home() (existing override at portal.py:125) fp_portal_home_dashboard (rewrite) counts + recent 5 of each: quotes, POs, jobs, certs, deliveries, invoices (already prepared)
/my/jobs portal_my_jobs() (existing at portal.py:512) portal_my_jobs (rewrite) fusion.plating.portal.job records for partner's commercial tree
/my/jobs/<id> portal_my_job() (existing at portal.py:565) portal_my_job (rewrite) Single fusion.plating.portal.job + linked docs

Controller changes

No new routes. The data already pulled by home() covers the new dashboard surface (counts + recent records of each type). What needs adding:

  1. home() needs to also compute the welcome-line summary ("6 active jobs · 2 awaiting your review · 1 ready to ship") — three new counts based on fp.portal.job.state values.
  2. portal_my_job() needs a timeline data prep helper that walks the underlying fp.job (via portal_job.job_id or whatever the link is — verify during implementation) and emits a list of {stage_label, started_at, completed_at, status, operator_name, notes} records ordered by stage sequence.
  3. portal_my_job() needs a document-grouping helper that takes the job's related records (sale_order_id, receiving_id, certificate_ids, picking_id, invoice_ids) and emits a dict keyed by the 4 categories, with each entry being a list of {name, mimetype, size, url, uploaded_date}.

Both helpers live as private methods on FpCustomerPortal. Possible exception: if the existing fusion.plating.portal.job doesn't already store per-stage timestamps in queryable fields, the timeline helper will need either (a) compute fields added on the model, or (b) the controller to walk chatter messages on the underlying fp.job. See "Open items" §1 — decided during Phase 3 implementation.

Template structure

fusion_plating_portal/views/
├── fp_portal_dashboard.xml        — rewrite of fp_portal_home_dashboard
├── fp_portal_templates.xml        — rewrites of portal_my_jobs + portal_my_job
├── fp_portal_macros.xml           — NEW: shared QWeb macros
│   ├── t-call="fp_portal_stepper"           (job, stage_count)
│   ├── t-call="fp_portal_status_badge"      (state)
│   ├── t-call="fp_portal_doc_chip"          (doc, dense=False)
│   └── t-call="fp_portal_doc_group"         (label, docs)
└── fp_portal_brand.xml            — NEW: layout-level brand wrapper template (welcome strip, gradient CTA bar)

Existing files preserved:

  • fp_quote_request_views.xml — only swap colour-coded badges to use the new tokens
  • fp_portal_configurator_templates.xml — only swap button styles
  • fp_portal_breadcrumbs.xml, fp_sale_order_portal.xml, fp_menu.xml — untouched

SCSS structure

Per CLAUDE.md SCSS rules (no @import in custom SCSS; every file registered separately in web.assets_frontend; tokens loaded first):

fusion_plating_portal/static/src/scss/
├── _fp_portal_tokens.scss         — NEW: brand colour, gradient, shadow, radius, spacing, typography vars (no rules)
├── fp_portal_buttons.scss         — NEW: gradient button system (primary/secondary/ghost/danger + hover/focus/active states)
├── fp_portal_stepper.scss         — NEW: circular stepper geometry + glow ring animations
├── fp_portal_cards.scss           — NEW: card/widget shells (radius, shadow, hover lift)
├── fp_portal_badges.scss          — NEW: status badge pill + dot + glow halo
├── fp_portal_timeline.scss        — NEW: vertical timeline geometry (line, dots, padding)
├── fp_portal_dashboard.scss       — NEW: jobs-forward grid + KPI tiles
└── fusion_plating_portal.scss     — EXISTS: trimmed down, only catch-all rules left

Manifest assets registration order matters (tokens first, then dependencies, then leaf files):

'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_stepper.scss',
        'fusion_plating_portal/static/src/scss/fp_portal_cards.scss',
        'fusion_plating_portal/static/src/scss/fp_portal_timeline.scss',
        'fusion_plating_portal/static/src/scss/fp_portal_dashboard.scss',
        'fusion_plating_portal/static/src/scss/fusion_plating_portal.scss',
        'fusion_plating_portal/static/src/js/fp_rfq_form.js',
    ],
},

Brand tokens (single source)

_fp_portal_tokens.scss defines the design system:

// Brand palette (matches enplating.com live CSS variables)
$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;

// Text
$fp-text:         #111827;
$fp-text-body:    #374151;
$fp-muted:        #6b7280;
$fp-muted-light:  #9ca3af;

// Status (functional, not brand)
$fp-amber:        #f59e0b;
$fp-amber-bg:     #fef3c7;
$fp-amber-text:   #92400e;
$fp-success:      #22c55e;
$fp-success-text: #15803d;
$fp-danger:       #ef4444;
$fp-danger-dark:  #b91c1c;

// Gradients (3-stop ready; current designs use 2 stops)
$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%);

// 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-danger:     0 1px 3px rgba(185,28,28,.25), 0 4px 12px rgba(185,28,28,.15);
$fp-glow-ring:         0 0 0 4px rgba(46,175,147,.20);

// Geometry
$fp-radius-pill:    9999px;
$fp-radius-card:    14px;
$fp-radius-button:  9px;
$fp-radius-chip:    8px;
$fp-radius-icon:    7px;

Per the SCSS gotcha in CLAUDE.md (rule 9, dark-mode awareness) the file should branch on $o-webclient-color-scheme for any colour that needs to flip in dark mode. Since dark mode is deferred in this spec, the branch is documented in _fp_portal_tokens.scss as a TODO with hex placeholders, not implemented.

Document-categorisation logic

Per-category source maps:

Category Source records
From You (customer-uploaded) fusion.plating.portal.job.sale_order_id.message_attachment_ids filtered to customer's uploads + fusion.plating.portal.job.quote_request_id.line_ids.attachment_ids (RFQ drawings)
Specifications fusion.plating.portal.job.x_fc_part_catalog_id.x_fc_customer_spec_id.attachment_id + any spec linked via the SO line x_fc_customer_spec_id
Quality Prefer fp.certificate records linked to the job (newer, post-Sub-12c; renders the full CoC with Fischerscope thickness pages already merged via the back-end merge — see CLAUDE.md S19); fall back to fusion.plating.portal.job.coc_attachment_id (legacy single-attachment field) only when no fp.certificate exists. Verify the exact linking field name (likely x_fc_job_id or via sale_order_id) during Phase 3.
Shipping stock.picking records linked to the SO with state=done — their packing slip PDF and tracking_reference

Implementation note: each lookup needs a defensive try/except (some attachments may be missing) and sudo() where required (customer doesn't have read on the quality_check raw model — they only see its rendered PDF). All model paths in the table above are spec-time assumptions — verify against the live model during Phase 3 and update the spec inline if anything has drifted.

Welcome-line summary logic

home() already pulls recent_jobs and job_count. Add:

active_jobs = Job.search_count([
    ('partner_id', 'child_of', commercial.id),
    ('state', 'in', ['received', 'in_progress', 'quality_check']),
])
awaiting_review = Quote.search_count([
    ('partner_id', 'child_of', commercial.id),
    ('state', '=', 'quoted'),  # customer needs to accept/reject
])
ready_to_ship = Job.search_count([
    ('partner_id', 'child_of', commercial.id),
    ('state', '=', 'ready_to_ship'),
])

These three numbers populate the welcome line: "X active jobs · Y awaiting your review · Z ready to ship".

Timeline data prep

Existing fp.portal.job has a 5-state machine: received → in_progress → quality_check → ready_to_ship → shipped. The detail page needs per-stage timestamps. Implementation approach:

  1. Add helper _fp_get_stage_timeline() on fp.portal.job returning a list of 5 dicts: {label, status, started_at, completed_at, operator_name, notes}.
  2. status is one of done | active | pending.
  3. started_at / completed_at come from the existing chatter / write_date on related records (e.g., for received use received_date; for in_progress use the first fp.job confirm date; for quality_check use the QC check create_date; for ready_to_ship use the picking confirm date; for shipped use actual_ship_date).
  4. operator_name pulled from the chatter event author where available; defaults to "EN Plating".
  5. notes is a short human-readable sentence per stage — examples in the mockup: "2 cartons · 240 units counted matches PO", "Type II hard anodize, 0.002" target". For v1 these can be hardcoded templates per stage; v2 can render from the actual recipe step instructions.

Mockups (single source of truth for visual fidelity)

Preserved in .superpowers/brainstorm/1800-1778997036/content/:

  • design-direction.html — A/B/C aesthetic options (Modern SaaS won)
  • saas-refinements.html — V1/V2/V3 card variants (V2 stepper + V3 timestamps won)
  • dashboard-layout.html — A vs B layout (B jobs-forward won)
  • job-detail.html — detail page using V3 layout (approved)
  • branded-dashboard.html — final brand-applied dashboard with gradient buttons (approved)

The implementation should match branded-dashboard.html for the dashboard, and the branded version of job-detail.html (apply the same teal/gradient swap) for the detail page.

Migration / deployment

  1. Bump fusion_plating_portal/__manifest__.py version 19.0.2.3.0 → 19.0.3.0.0 (minor bump signals a UI-significant change).
  2. Add new SCSS + macro XML files to the manifest data and assets lists.
  3. Deploy to entech per the standard runbook (see CLAUDE.md "Deployment / odoo-entech" section).
  4. Asset cache bust: DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%'; then restart, or just rely on the version bump.
  5. Verify the dashboard renders correctly at https://enplating.com/my/home (logged in as admin, then optionally grant portal access to a real customer for end-to-end verification).

Implementation sequencing

To keep diffs reviewable, ship in 4 ordered phases:

  1. Phase 1 — Tokens + button system (1 file: _fp_portal_tokens.scss + fp_portal_buttons.scss, manifest update). Visible change: only buttons re-skin; rest of portal unchanged. Low blast radius; deployable on its own.
  2. Phase 2 — Macros + dashboard (fp_portal_macros.xml + dashboard SCSS + fp_portal_home_dashboard template rewrite + welcome-summary counts in home()). Visible change: /my/home becomes the jobs-forward dashboard.
  3. Phase 3 — Jobs list + detail (portal_my_jobs + portal_my_job template rewrites + timeline + doc-group helpers in controller). Visible change: clicking a job card now lands on the new detail page.
  4. Phase 4 — Cosmetic sweep across other /my/* pages — apply the new badge / button / card tokens to quote requests, POs, invoices, deliveries, certifications, configurator. No structural changes.

Each phase bumps the patch version (19.0.3.0.0 → .1.0 → .2.0 → .3.0 → .4.0).

Open items to verify during implementation

These are flagged as assumptions to confirm against the live model, not blockers:

  1. Stage timestamp sources — RESOLVED 2026-05-17 Phase 3 investigation: fp.portal.job is intentionally decoupled from fp.job (no job_id link). Existing Date fields cover received/shipped only; the 3 middle stages had no timestamps. Decision: Option B — added per-stage Datetime fields (received_at, in_progress_started_at, qc_started_at, ready_to_ship_at, shipped_at) with a write() override that snapshots fields.Datetime.now() on state change. Idempotent — won't overwrite if already set.
  2. Operator name surfacing — customers seeing operator names is a privacy / policy question. Default in the spec is to show first-initial + last-name (e.g., "D. Mendez"). Confirm with EN Plating before shipping.
  3. Stage-notes copy — the example notes in the mockup ("Tank 4 · 45 min cycle · Day 3 of 7") are made up. Confirm what info is reasonable to share with the customer per stage.
  4. Document linking edge cases — RESOLVED 2026-05-17: fp.portal.job has no link to sale.order / quote_request / part_catalog, so V1 cannot reach PO / Drawing / Spec documents through the portal job alone. Decision: Option C for V1 — surface only the directly-attached fields (coc_attachment_id, packing_list_attachment_id); render the From-You / Specifications / Quality (when CoC missing) / Shipping (when packing missing) groups as placeholder rows ("Will appear when ..." messaging per the approved mockup). V2 (separate change) will add sale_order_id Many2one to fp.portal.job and pull PO/drawing/spec docs via that link.
  5. Dark mode — explicitly deferred. If a customer logs in with color_scheme=dark set, what should they see? Default Bootstrap dark fallback is ugly. Suggest: force the portal to color_scheme=light for share=True users, or add a prefers-color-scheme: light meta tag. Document the choice during Phase 1.

Mockup references in .superpowers/brainstorm/1800-1778997036/content/ — keep until implementation is verified.