feat(portal): rewrite /my/home as jobs-forward dashboard

Welcome strip -> 4-tile KPI row (In-Flight Jobs is the hero) ->
Active Work Orders section with 3 most-recent V2 cards ->
3-panel secondary strip (Certs / Packing Slips / Invoices).
Uses the new badge/stepper/doc-chip macros.

Also fixes a stepper state->step mapping bug that would have
shown Inspected as active when state=in_progress (should be
Plating active). New state_to_idx dict handles all 6 fp.portal.job
states correctly, including 'complete' (all 5 stages done).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-17 02:43:24 -04:00
parent c2590a99ff
commit 3aa11eaffc

View File

@@ -7,344 +7,187 @@
<odoo> <odoo>
<!-- ================================================================== --> <!-- ================================================================== -->
<!-- Portal Home Dashboard — 6-section grid --> <!-- Portal Home Dashboard — jobs-forward layout (v19.0.3.1.0 redesign) -->
<!-- ================================================================== --> <!-- ================================================================== -->
<template id="fp_portal_home_dashboard" name="Plating Portal Dashboard"> <template id="fp_portal_home_dashboard" name="Plating Portal Dashboard">
<t t-call="portal.portal_layout"> <t t-call="portal.portal_layout">
<div class="o_fp_dashboard mt-3"> <div class="o_fp_dashboard mt-3">
<!-- Welcome header -->
<div class="row mb-4"> <!-- Welcome strip -->
<div class="col-12"> <div class="o_fp_welcome">
<h3 class="mb-1"> <div>
<div class="o_fp_welcome_title">
Welcome back, <span t-out="partner.name"/> Welcome back, <span t-out="partner.name"/>
</h3> </div>
<p class="text-muted mb-0"> <div class="o_fp_welcome_sub">
Your plating portal dashboard — everything at a glance. <t t-out="active_job_count"/> active job<t t-if="active_job_count != 1">s</t>
</p> <t t-if="awaiting_review_count"> · <t t-out="awaiting_review_count"/> awaiting your review</t>
<t t-if="ready_to_ship_count"> · <t t-out="ready_to_ship_count"/> ready to ship</t>
</div>
</div> </div>
</div> <a href="/my/configurator" class="o_fp_btn_primary">
<i class="fa fa-plus"/> Get a Quote
<!-- Quick Actions bar -->
<div class="d-flex flex-wrap gap-2 mb-4">
<a href="/my/configurator" class="btn btn-primary">
<i class="fa fa-cog me-1"/>Get a Quote
</a>
<a href="/my/quote_requests/new" class="btn btn-outline-primary">
<i class="fa fa-plus me-1"/>Request Quote
</a>
<a href="/my/quote_requests" class="btn btn-outline-secondary">
<i class="fa fa-file-text-o me-1"/>My Quotes
</a>
<a href="/my/jobs" class="btn btn-outline-secondary">
<i class="fa fa-cogs me-1"/>Parts Portal
</a>
<a href="/my/certifications" class="btn btn-outline-secondary">
<i class="fa fa-certificate me-1"/>Certifications
</a> </a>
</div> </div>
<!-- Dashboard Grid --> <!-- KPI tiles -->
<div class="row g-4"> <div class="o_fp_kpi_row">
<div class="o_fp_kpi_tile">
<div class="o_fp_kpi_label">Open Quotes</div>
<div class="o_fp_kpi_value" t-out="quote_count"/>
<a href="/my/quote_requests" class="o_fp_kpi_hint o_fp_hint_action">View quotes →</a>
</div>
<div class="o_fp_kpi_tile">
<div class="o_fp_kpi_label">Active POs</div>
<div class="o_fp_kpi_value" t-out="po_count"/>
<a href="/my/purchase_orders" class="o_fp_kpi_hint">View POs →</a>
</div>
<div class="o_fp_kpi_tile o_fp_kpi_hero">
<div class="o_fp_kpi_label">In-Flight Jobs</div>
<div class="o_fp_kpi_value" t-out="active_job_count"/>
<t t-if="ready_to_ship_count">
<div class="o_fp_kpi_hint o_fp_hint_success">
<t t-out="ready_to_ship_count"/> ready to ship ✓
</div>
</t>
</div>
<div class="o_fp_kpi_tile">
<div class="o_fp_kpi_label">Invoices</div>
<div class="o_fp_kpi_value" t-out="invoice_count"/>
<a href="/my/fp_invoices" class="o_fp_kpi_hint">View invoices →</a>
</div>
</div>
<!-- ====== QUOTES SECTION ====== --> <!-- Active jobs hero -->
<div class="col-lg-6"> <div class="o_fp_jobs_hero">
<div class="o_fp_dashboard_card card h-100"> <div class="o_fp_section_header">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="o_fp_section_title">Active Work Orders</div>
<h6 class="mb-0"> <a href="/my/jobs" class="o_fp_btn_ghost">All Jobs →</a>
<i class="fa fa-file-text-o me-2"/>Quotes
<span class="badge text-bg-primary ms-2" t-out="quote_count"/>
</h6>
<a href="/my/quote_requests" class="btn btn-sm btn-outline-primary">View All</a>
</div>
<div class="card-body p-0">
<t t-if="recent_quotes">
<table class="table table-sm mb-0">
<thead>
<tr>
<th>Reference</th>
<th>Date</th>
<th class="text-end">Status</th>
</tr>
</thead>
<tbody>
<tr t-foreach="recent_quotes" t-as="qr">
<td>
<a t-att-href="'/my/quote_requests/%s' % qr.id"
t-out="qr.name"/>
</td>
<td>
<span t-field="qr.create_date" t-options='{"widget": "date"}'/>
</td>
<td class="text-end">
<span t-attf-class="badge #{
'text-bg-secondary' if qr.state == 'new' else
'text-bg-info' if qr.state == 'under_review' else
'text-bg-primary' if qr.state == 'quoted' else
'text-bg-success' if qr.state == 'accepted' else
'text-bg-danger' if qr.state == 'declined' else
'text-bg-warning'}"
t-out="dict(qr._fields['state']._description_selection(qr.env)).get(qr.state)"/>
</td>
</tr>
</tbody>
</table>
</t>
<t t-else="">
<div class="p-4 text-center text-muted">
<p class="mb-2">No quotes yet.</p>
<a href="/my/quote_requests/new" class="btn btn-sm btn-primary">
Submit Your First RFQ
</a>
</div>
</t>
</div>
</div>
</div> </div>
<!-- ====== PURCHASE ORDERS SECTION ====== --> <t t-if="recent_jobs">
<div class="col-lg-6"> <t t-foreach="recent_jobs[:3]" t-as="job">
<div class="o_fp_dashboard_card card h-100"> <div class="o_fp_job_card">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="o_fp_job_header">
<h6 class="mb-0"> <div>
<i class="fa fa-shopping-cart me-2"/>Purchase Orders <a t-att-href="'/my/jobs/%s' % job.id"
<span class="badge text-bg-primary ms-2" t-out="po_count"/> class="o_fp_job_ref text-decoration-none"
</h6> t-out="job.name"/>
<a href="/my/purchase_orders" class="btn btn-sm btn-outline-primary">View All</a> <span class="o_fp_job_meta">
</div> <t t-if="job.quantity"><t t-out="job.quantity"/> units</t>
<div class="card-body p-0"> <t t-if="job.target_ship_date"> · ETA <span t-field="job.target_ship_date" t-options='{"widget": "date"}'/></t>
<t t-if="recent_pos"> </span>
<table class="table table-sm mb-0">
<thead>
<tr>
<th>Order</th>
<th>Date</th>
<th class="text-end">Total</th>
</tr>
</thead>
<tbody>
<tr t-foreach="recent_pos" t-as="po">
<td t-out="po.name"/>
<td>
<span t-field="po.date_order" t-options='{"widget": "date"}'/>
</td>
<td class="text-end">
<span t-field="po.amount_total"
t-options='{"widget": "monetary", "display_currency": po.currency_id}'/>
</td>
</tr>
</tbody>
</table>
</t>
<t t-else="">
<div class="p-4 text-center text-muted">
No purchase orders yet.
</div> </div>
</t> <t t-call="fusion_plating_portal.fp_portal_status_badge">
</div> <t t-set="state" t-value="job.state"/>
</div> <t t-set="label" t-value="dict(job._fields['state']._description_selection(job.env)).get(job.state)"/>
</div> </t>
</div>
<!-- ====== PARTS PORTAL / JOBS SECTION ====== --> <!-- State -> active step index mapping.
<div class="col-lg-6"> 5 customer-visible stages: Received / Inspected / Plating / QC / Ship.
<div class="o_fp_dashboard_card card h-100"> state_idx >= 5 means all done (shipped or complete). -->
<div class="card-header d-flex justify-content-between align-items-center"> <t t-set="state_to_idx" t-value="{'received': 0, 'in_progress': 2, 'quality_check': 3, 'ready_to_ship': 4, 'shipped': 5, 'complete': 5}"/>
<h6 class="mb-0"> <t t-set="state_idx" t-value="state_to_idx.get(job.state, 0)"/>
<i class="fa fa-cogs me-2"/>Parts Portal <t t-set="steps" t-value="[
<span class="badge text-bg-primary ms-2" t-out="job_count"/> {'label': 'Received', 'status': 'done' if state_idx > 0 else 'active', 'time_label': ''},
</h6> {'label': 'Inspected', 'status': 'done' if state_idx > 1 else ('active' if state_idx == 1 else 'pending'), 'time_label': ''},
<a href="/my/jobs" class="btn btn-sm btn-outline-primary">View All</a> {'label': 'Plating', 'status': 'done' if state_idx > 2 else ('active' if state_idx == 2 else 'pending'), 'time_label': ''},
</div> {'label': 'QC', 'status': 'done' if state_idx > 3 else ('active' if state_idx == 3 else 'pending'), 'time_label': ''},
<div class="card-body p-0"> {'label': 'Ship', 'status': 'done' if state_idx > 4 else ('active' if state_idx == 4 else 'pending'), 'time_label': ''},
<t t-if="recent_jobs"> ]"/>
<div class="p-3"> <t t-set="active_state" t-value="'warn' if job.state == 'quality_check' else 'normal'"/>
<t t-foreach="recent_jobs" t-as="job"> <t t-call="fusion_plating_portal.fp_portal_stepper"/>
<div class="o_fp_dashboard_job_row mb-3">
<div class="d-flex justify-content-between align-items-center mb-1"> <!-- Doc chips: CoC + tracking (V1) -->
<a t-att-href="'/my/jobs/%s' % job.id" <div class="o_fp_job_docs">
class="fw-semibold text-decoration-none" <t t-if="job.coc_attachment_id">
t-out="job.name"/> <t t-call="fusion_plating_portal.fp_portal_doc_chip">
<span t-attf-class="badge #{ <t t-set="doc" t-value="{'icon': '📑', 'label': 'CoC', 'url': '/my/jobs/%s/coc' % job.id}"/>
'text-bg-info' if job.state == 'received' else
'text-bg-primary' if job.state == 'in_progress' else
'text-bg-warning' if job.state == 'quality_check' else
'text-bg-secondary' if job.state == 'ready_to_ship' else
'text-bg-success'}"
t-out="dict(job._fields['state']._description_selection(job.env)).get(job.state)"/>
</div>
<!-- Segmented progress bar -->
<div class="o_fp_seg_progress d-flex" style="height: 8px; border-radius: 4px; overflow: hidden;">
<t t-set="pct" t-value="job._progress_percent()"/>
<!-- Receiving segment (green, 0-20%) -->
<div t-attf-class="o_fp_seg_receiving"
t-attf-style="width: #{min(pct, 20)}%; background-color: var(--bs-success); opacity: #{1 if pct >= 1 else 0.3};"/>
<!-- In Progress segment (orange, 20-70%) -->
<div t-attf-class="o_fp_seg_progress_mid"
t-attf-style="width: #{max(0, min(pct - 20, 50))}%; background-color: var(--bs-warning); opacity: #{1 if pct > 20 else 0.3};"/>
<!-- Shipping segment (orange-green, 70-100%) -->
<div t-attf-class="o_fp_seg_shipping"
t-attf-style="width: #{max(0, min(pct - 70, 30))}%; background-color: var(--bs-info); opacity: #{1 if pct > 70 else 0.3};"/>
</div>
<div class="d-flex justify-content-between mt-1" style="font-size: 0.7rem;">
<span class="text-muted">Receiving</span>
<span class="text-muted">In Progress</span>
<span class="text-muted">Shipping</span>
</div>
</div>
</t> </t>
</div> </t>
</t> <t t-else="">
<t t-else=""> <t t-call="fusion_plating_portal.fp_portal_doc_chip">
<div class="p-4 text-center text-muted"> <t t-set="doc" t-value="{'icon': '📑', 'label': 'CoC', 'pending': True}"/>
No active jobs. </t>
</div> </t>
</t> <t t-if="job.tracking_ref">
<span class="o_fp_doc_chip">📦 <span t-out="job.tracking_ref"/></span>
</t>
</div>
</div> </div>
</div> </t>
</div>
<!-- ====== CERTIFICATIONS &amp; QUALITY SECTION ====== --> <t t-if="job_count > 3">
<div class="col-lg-6"> <div class="o_fp_view_all">
<div class="o_fp_dashboard_card card h-100"> <a href="/my/jobs">View all <t t-out="job_count"/> jobs →</a>
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fa fa-certificate me-2"/>Certifications &amp; Quality
<span class="badge text-bg-primary ms-2" t-out="cert_count"/>
</h6>
<a href="/my/certifications" class="btn btn-sm btn-outline-primary">View All</a>
</div>
<div class="card-body p-0">
<t t-if="recent_certs">
<table class="table table-sm mb-0">
<thead>
<tr>
<th>Job</th>
<th>Ship Date</th>
<th class="text-end">CoC</th>
</tr>
</thead>
<tbody>
<tr t-foreach="recent_certs" t-as="cj">
<td>
<a t-att-href="'/my/jobs/%s' % cj.id"
t-out="cj.name"/>
</td>
<td>
<span t-if="cj.actual_ship_date"
t-field="cj.actual_ship_date"
t-options='{"widget": "date"}'/>
<span t-else="" class="text-muted">--</span>
</td>
<td class="text-end">
<a t-att-href="'/my/jobs/%s/coc' % cj.id"
class="btn btn-sm btn-outline-success">
<i class="fa fa-download me-1"/>Download
</a>
</td>
</tr>
</tbody>
</table>
</t>
<t t-else="">
<div class="p-4 text-center text-muted">
No certificates available yet.
</div>
</t>
</div> </div>
</t>
</t>
<t t-else="">
<div class="o_fp_card text-center text-muted">
<p class="mb-2">No active jobs yet.</p>
<a href="/my/configurator" class="o_fp_btn_primary o_fp_btn_sm">+ Get Your First Quote</a>
</div> </div>
</div> </t>
</div>
<!-- ====== SHIPPING / DELIVERIES SECTION ====== --> <!-- Secondary panels -->
<div class="col-lg-6"> <div class="o_fp_secondary_panels">
<div class="o_fp_dashboard_card card h-100"> <div class="o_fp_panel">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="o_fp_panel_title">
<h6 class="mb-0"> <span class="o_fp_panel_icon">📑</span> Recent Certifications
<i class="fa fa-truck me-2"/>Shipping
<span class="badge text-bg-primary ms-2" t-out="delivery_count"/>
</h6>
<a href="/my/deliveries" class="btn btn-sm btn-outline-primary">View All</a>
</div>
<div class="card-body p-0">
<t t-if="recent_deliveries">
<table class="table table-sm mb-0">
<thead>
<tr>
<th>Packing Slip</th>
<th>Date</th>
<th class="text-end">Status</th>
</tr>
</thead>
<tbody>
<tr t-foreach="recent_deliveries" t-as="dlv">
<td t-out="dlv.name"/>
<td>
<span t-if="dlv.date_done"
t-field="dlv.date_done"
t-options='{"widget": "date"}'/>
</td>
<td class="text-end">
<span class="badge text-bg-success">Delivered</span>
</td>
</tr>
</tbody>
</table>
</t>
<t t-else="">
<div class="p-4 text-center text-muted">
No deliveries yet.
</div>
</t>
</div>
</div> </div>
<t t-if="recent_certs">
<t t-foreach="recent_certs[:3]" t-as="cert">
<div class="o_fp_panel_row">
<a t-att-href="'/my/jobs/%s' % cert.id" class="text-decoration-none">
CoC <span t-out="cert.name"/>
</a>
<t t-if="cert.actual_ship_date"> · <span t-field="cert.actual_ship_date" t-options='{"widget": "date"}'/></t>
</div>
</t>
</t>
<t t-else="">
<div class="o_fp_panel_row text-muted">No certifications yet.</div>
</t>
</div> </div>
<div class="o_fp_panel">
<!-- ====== INVOICES SECTION ====== --> <div class="o_fp_panel_title">
<div class="col-lg-6"> <span class="o_fp_panel_icon">📦</span> Recent Packing Slips
<div class="o_fp_dashboard_card card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fa fa-file-text me-2"/>Invoices
<span class="badge text-bg-primary ms-2" t-out="invoice_count"/>
</h6>
<a href="/my/fp_invoices" class="btn btn-sm btn-outline-primary">View All</a>
</div>
<div class="card-body p-0">
<t t-if="recent_invoices">
<table class="table table-sm mb-0">
<thead>
<tr>
<th>Invoice</th>
<th>Due Date</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<tr t-foreach="recent_invoices" t-as="inv">
<td t-out="inv.name"/>
<td>
<span t-if="inv.invoice_date_due"
t-field="inv.invoice_date_due"
t-options='{"widget": "date"}'/>
<span t-else="" class="text-muted">--</span>
</td>
<td class="text-end">
<span t-field="inv.amount_total"
t-options='{"widget": "monetary", "display_currency": inv.currency_id}'/>
</td>
</tr>
</tbody>
</table>
</t>
<t t-else="">
<div class="p-4 text-center text-muted">
No invoices yet.
</div>
</t>
</div>
</div> </div>
<t t-if="recent_deliveries">
<t t-foreach="recent_deliveries[:3]" t-as="d">
<div class="o_fp_panel_row">
<span t-out="d.name"/>
<t t-if="d.date_done"> · <span t-field="d.date_done" t-options='{"widget": "date"}'/></t>
</div>
</t>
</t>
<t t-else="">
<div class="o_fp_panel_row text-muted">No deliveries yet.</div>
</t>
</div> </div>
<div class="o_fp_panel">
<div class="o_fp_panel_title">
<span class="o_fp_panel_icon">💰</span> Recent Invoices
</div>
<t t-if="recent_invoices">
<t t-foreach="recent_invoices[:3]" t-as="inv">
<div class="o_fp_panel_row">
<a t-att-href="'/my/fp_invoices/%s' % inv.id" class="text-decoration-none" t-out="inv.name"/>
<t t-if="inv.amount_total"> · <span t-field="inv.amount_total" t-options='{"widget": "monetary", "display_currency": inv.currency_id}'/></t>
<t t-if="inv.payment_state == 'paid'"> · <span class="o_fp_badge o_fp_badge_paid"><span class="o_fp_badge_dot"/>Paid</span></t>
</div>
</t>
</t>
<t t-else="">
<div class="o_fp_panel_row text-muted">No invoices yet.</div>
</t>
</div>
</div>
</div><!-- /row --> </div>
</div><!-- /o_fp_dashboard -->
</t> </t>
</template> </template>