feat(portal): rewrite /my/jobs/<id> detail page with timeline + doc panel
Two-column grid: vertical timeline (5 stages with per-stage timestamps) on the left, grouped document panel (4 categories) on the right. Hero header carries WO ref + part / qty / ETA / tracking facts. Controller adds stage_timeline, doc_groups, and timeline_spine_pct to the render context. Spine fill = done + half-credit for the active stage (so the spine visually leads the eye to where the work is happening). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -738,6 +738,13 @@ class FpCustomerPortal(CustomerPortal):
|
||||
job_sudo, access_token, **kw
|
||||
)
|
||||
values['progress_percent'] = job_sudo._progress_percent()
|
||||
values['stage_timeline'] = self._fp_get_stage_timeline(job_sudo)
|
||||
values['doc_groups'] = self._fp_group_documents(job_sudo)
|
||||
# Spine-fill % for the timeline (visual progress indicator).
|
||||
# done stages plus half-credit for the active stage.
|
||||
done_count = sum(1 for s in values['stage_timeline'] if s['status'] == 'done')
|
||||
active_count = sum(1 for s in values['stage_timeline'] if s['status'] == 'active')
|
||||
values['timeline_spine_pct'] = int(((done_count + 0.5 * active_count) / 5) * 100)
|
||||
return request.render(
|
||||
'fusion_plating_portal.portal_my_job',
|
||||
values,
|
||||
|
||||
@@ -485,134 +485,98 @@
|
||||
<!-- ================================================================== -->
|
||||
<template id="portal_my_job" name="My Work Order">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="row mt-2 mb-4">
|
||||
<div class="col-12">
|
||||
<h3 class="mb-1">
|
||||
<span t-out="job.name"/>
|
||||
</h3>
|
||||
<p class="text-muted mb-0">
|
||||
Received
|
||||
<span t-if="job.received_date" t-field="job.received_date"
|
||||
t-options='{"widget": "date"}'/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_fp_job_detail">
|
||||
|
||||
<!-- Segmented progress bar -->
|
||||
<t t-set="pct" t-value="progress_percent"/>
|
||||
<div class="mb-4">
|
||||
<div class="o_fp_seg_progress d-flex" style="height: 14px; border-radius: 7px; overflow: hidden; background: var(--bs-secondary-bg);">
|
||||
<div t-attf-style="width: 20%; opacity: #{1 if pct >= 10 else 0.2};"
|
||||
style="background-color: var(--bs-success);"/>
|
||||
<div t-attf-style="width: 50%; opacity: #{1 if pct >= 35 else 0.2};"
|
||||
style="background-color: var(--bs-warning); margin-left: 2px;"/>
|
||||
<div t-attf-style="width: 30%; opacity: #{1 if pct >= 80 else 0.2};"
|
||||
style="background-color: var(--bs-info); margin-left: 2px;"/>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small text-muted mt-1">
|
||||
<span>Receiving</span>
|
||||
<span>In Progress</span>
|
||||
<span>QC</span>
|
||||
<span>Ready</span>
|
||||
<span>Shipped</span>
|
||||
<span>Complete</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-body-tertiary border-0 mb-4 o_fp_portal_card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-muted small text-uppercase">Current Status</h6>
|
||||
<span t-attf-class="badge #{
|
||||
'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' if job.state == 'shipped' else
|
||||
'text-bg-success'} fs-6"
|
||||
t-out="dict(job._fields['state']._description_selection(job.env)).get(job.state)"/>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end" t-if="job.target_ship_date">
|
||||
<h6 class="text-muted small text-uppercase">Target Ship Date</h6>
|
||||
<h5 class="mb-0">
|
||||
<span t-field="job.target_ship_date"
|
||||
t-options='{"widget": "date"}'/>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4">
|
||||
<h6 class="text-muted small text-uppercase">Details</h6>
|
||||
<!-- Hero header -->
|
||||
<div class="o_fp_job_detail_hero">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
|
||||
<div>
|
||||
<span class="text-muted small">Quantity:</span>
|
||||
<span t-out="job.quantity"/>
|
||||
<div class="o_fp_detail_label">Work Order</div>
|
||||
<h2><span t-out="job.name"/></h2>
|
||||
<div t-if="job.process_type_ids" class="o_fp_detail_subtitle">
|
||||
<span t-out="', '.join(job.process_type_ids.mapped('name'))"/>
|
||||
</div>
|
||||
<div t-if="job.actual_ship_date">
|
||||
<span class="text-muted small">Actual Ship Date:</span>
|
||||
<span t-field="job.actual_ship_date"
|
||||
t-options='{"widget": "date"}'/>
|
||||
<div class="o_fp_detail_facts">
|
||||
<div t-if="job.quantity">
|
||||
<span class="o_fp_fact_label">Qty </span>
|
||||
<span class="o_fp_fact_value" t-out="job.quantity"/>
|
||||
</div>
|
||||
<div t-if="job.received_date">
|
||||
<span class="o_fp_fact_label">Received </span>
|
||||
<span class="o_fp_fact_value" t-field="job.received_date" t-options='{"widget": "date"}'/>
|
||||
</div>
|
||||
<div t-if="job.target_ship_date">
|
||||
<span class="o_fp_fact_label">ETA </span>
|
||||
<span class="o_fp_fact_value" t-field="job.target_ship_date" t-options='{"widget": "date"}'/>
|
||||
</div>
|
||||
<div t-if="job.tracking_ref">
|
||||
<span class="text-muted small">Tracking:</span>
|
||||
<span t-out="job.tracking_ref"/>
|
||||
</div>
|
||||
<div t-if="job.invoice_ref">
|
||||
<span class="text-muted small">Invoice:</span>
|
||||
<span t-out="job.invoice_ref"/>
|
||||
<span class="o_fp_fact_label">Tracking </span>
|
||||
<span class="o_fp_fact_value" t-out="job.tracking_ref"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-4" t-if="job.process_type_ids">
|
||||
<h6 class="text-muted small text-uppercase">Processes</h6>
|
||||
<span t-foreach="job.process_type_ids" t-as="pt"
|
||||
class="badge text-bg-light border me-1" t-out="pt.name"/>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-end gap-2">
|
||||
<t t-call="fusion_plating_portal.fp_portal_status_badge">
|
||||
<t t-set="state" t-value="job.state"/>
|
||||
<t t-set="label" t-value="dict(job._fields['state']._description_selection(job.env)).get(job.state)"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process Steps — only the customer-visible recipe nodes
|
||||
(recipe author marked customer_visible=True). -->
|
||||
<t t-set="visible_steps" t-value="job.sudo().get_customer_visible_steps()"/>
|
||||
<div class="mb-4" t-if="visible_steps">
|
||||
<h6 class="text-muted small text-uppercase">Process Steps</h6>
|
||||
<ol class="list-group list-group-numbered">
|
||||
<li t-foreach="visible_steps" t-as="step"
|
||||
class="list-group-item d-flex align-items-center"
|
||||
t-attf-style="padding-left: #{ 12 + (step['depth'] * 18) }px;">
|
||||
<i t-attf-class="fa #{ step['icon'] } me-2 text-muted"/>
|
||||
<span t-out="step['name']"/>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<!-- Two-column grid: timeline | docs -->
|
||||
<div class="o_fp_job_detail_grid">
|
||||
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted small text-uppercase">Documents</h6>
|
||||
<div class="list-group">
|
||||
<a t-if="job.coc_attachment_id"
|
||||
t-att-href="'/my/jobs/%s/coc' % job.id"
|
||||
class="list-group-item list-group-item-action">
|
||||
<i class="fa fa-file-pdf-o me-2 text-muted"/>
|
||||
Certificate of Conformance
|
||||
<i class="fa fa-download float-end text-muted"/>
|
||||
</a>
|
||||
<a t-if="job.packing_list_attachment_id"
|
||||
t-att-href="'/web/content/%s?download=true' % job.packing_list_attachment_id.id"
|
||||
class="list-group-item list-group-item-action">
|
||||
<i class="fa fa-file-text-o me-2 text-muted"/>
|
||||
Packing List
|
||||
<i class="fa fa-download float-end text-muted"/>
|
||||
</a>
|
||||
<span t-if="not job.coc_attachment_id and not job.packing_list_attachment_id"
|
||||
class="text-muted small">No documents available yet.</span>
|
||||
<!-- Timeline -->
|
||||
<div class="o_fp_card">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div style="font-weight:600;color:#111827;font-size:1rem">Progress</div>
|
||||
<span style="font-size:.7rem;color:#6b7280">
|
||||
<t t-out="progress_percent"/>% complete
|
||||
</span>
|
||||
</div>
|
||||
<div class="o_fp_timeline">
|
||||
<div class="o_fp_timeline_spine_active" t-attf-style="height: #{timeline_spine_pct}%"/>
|
||||
<t t-foreach="stage_timeline" t-as="step">
|
||||
<div t-attf-class="o_fp_timeline_item o_fp_timeline_#{step['status']}">
|
||||
<div class="o_fp_timeline_dot">
|
||||
<t t-if="step['status'] == 'done'">✓</t>
|
||||
</div>
|
||||
<div class="o_fp_timeline_title" t-out="step['label']"/>
|
||||
<div class="o_fp_timeline_time" t-out="step['time_label']"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4" t-if="job.notes">
|
||||
<h6 class="text-muted small text-uppercase">Notes</h6>
|
||||
<div class="border rounded p-3 bg-body">
|
||||
<span t-out="job.notes"/>
|
||||
<!-- Documents -->
|
||||
<div class="o_fp_card">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div style="font-weight:600;color:#111827;font-size:1rem">Documents</div>
|
||||
</div>
|
||||
<t t-foreach="doc_groups" t-as="group">
|
||||
<t t-call="fusion_plating_portal.fp_portal_doc_group">
|
||||
<t t-set="group_label" t-value="group['label']"/>
|
||||
<t t-set="docs" t-value="group['docs']"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer notes (if any) -->
|
||||
<div t-if="job.notes" class="o_fp_card" style="margin-top:1.25rem">
|
||||
<div style="font-weight:600;color:#111827;font-size:1rem;margin-bottom:.6rem">Notes</div>
|
||||
<div t-out="job.notes"/>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="o_fp_job_detail_footer">
|
||||
<div class="o_fp_related_links">
|
||||
<span style="color:#9ca3af">Related:</span>
|
||||
<a t-if="job.invoice_ref" href="#" t-out="'Invoice ' + job.invoice_ref"/>
|
||||
<a t-else="" class="disabled">Invoice (pending)</a>
|
||||
</div>
|
||||
<a href="/my/jobs" class="o_fp_btn_secondary">← Back to all jobs</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
Reference in New Issue
Block a user