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
- 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.
- 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.
- 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.
- 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-schemebranch — 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.binarystream; 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:
home()needs to also compute the welcome-line summary ("6 active jobs · 2 awaiting your review · 1 ready to ship") — three new counts based onfp.portal.job.statevalues.portal_my_job()needs a timeline data prep helper that walks the underlyingfp.job(viaportal_job.job_idor 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.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 tokensfp_portal_configurator_templates.xml— only swap button stylesfp_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:
- Add helper
_fp_get_stage_timeline()onfp.portal.jobreturning a list of 5 dicts:{label, status, started_at, completed_at, operator_name, notes}. statusis one ofdone | active | pending.started_at/completed_atcome from the existing chatter / write_date on related records (e.g., forreceivedusereceived_date; forin_progressuse the firstfp.jobconfirm date; forquality_checkuse the QC check create_date; forready_to_shipuse the picking confirm date; forshippeduseactual_ship_date).operator_namepulled from the chatter event author where available; defaults to "EN Plating".notesis 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
- Bump
fusion_plating_portal/__manifest__.pyversion19.0.2.3.0 → 19.0.3.0.0(minor bump signals a UI-significant change). - Add new SCSS + macro XML files to the manifest
dataandassetslists. - Deploy to entech per the standard runbook (see CLAUDE.md "Deployment / odoo-entech" section).
- Asset cache bust:
DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';then restart, or just rely on the version bump. - 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:
- 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. - Phase 2 — Macros + dashboard (
fp_portal_macros.xml+ dashboard SCSS +fp_portal_home_dashboardtemplate rewrite + welcome-summary counts inhome()). Visible change:/my/homebecomes the jobs-forward dashboard. - Phase 3 — Jobs list + detail (
portal_my_jobs+portal_my_jobtemplate rewrites + timeline + doc-group helpers in controller). Visible change: clicking a job card now lands on the new detail page. - 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:
- Stage timestamp sources — RESOLVED 2026-05-17 Phase 3 investigation:
fp.portal.jobis intentionally decoupled fromfp.job(nojob_idlink). Existing Date fields cover received/shipped only; the 3 middle stages had no timestamps. Decision: Option B — added per-stageDatetimefields (received_at,in_progress_started_at,qc_started_at,ready_to_ship_at,shipped_at) with awrite()override that snapshotsfields.Datetime.now()on state change. Idempotent — won't overwrite if already set. - 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.
- 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.
- Document linking edge cases — RESOLVED 2026-05-17:
fp.portal.jobhas no link tosale.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 addsale_order_idMany2one tofp.portal.joband pull PO/drawing/spec docs via that link. - Dark mode — explicitly deferred. If a customer logs in with
color_scheme=darkset, what should they see? Default Bootstrap dark fallback is ugly. Suggest: force the portal tocolor_scheme=lightforshare=Trueusers, or add aprefers-color-scheme: lightmeta tag. Document the choice during Phase 1.
Mockup references in .superpowers/brainstorm/1800-1778997036/content/ — keep until implementation is verified.