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:
gsinghpal
2026-05-17 02:51:33 -04:00
parent c8deef1482
commit 6cf826268b
2 changed files with 88 additions and 117 deletions

View File

@@ -738,6 +738,13 @@ class FpCustomerPortal(CustomerPortal):
job_sudo, access_token, **kw job_sudo, access_token, **kw
) )
values['progress_percent'] = job_sudo._progress_percent() 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( return request.render(
'fusion_plating_portal.portal_my_job', 'fusion_plating_portal.portal_my_job',
values, values,

View File

@@ -485,134 +485,98 @@
<!-- ================================================================== --> <!-- ================================================================== -->
<template id="portal_my_job" name="My Work Order"> <template id="portal_my_job" name="My Work Order">
<t t-call="portal.portal_layout"> <t t-call="portal.portal_layout">
<div class="row mt-2 mb-4"> <div class="o_fp_job_detail">
<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>
<!-- Segmented progress bar --> <!-- Hero header -->
<t t-set="pct" t-value="progress_percent"/> <div class="o_fp_job_detail_hero">
<div class="mb-4"> <div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
<div class="o_fp_seg_progress d-flex" style="height: 14px; border-radius: 7px; overflow: hidden; background: var(--bs-secondary-bg);"> <div>
<div t-attf-style="width: 20%; opacity: #{1 if pct >= 10 else 0.2};" <div class="o_fp_detail_label">Work Order</div>
style="background-color: var(--bs-success);"/> <h2><span t-out="job.name"/></h2>
<div t-attf-style="width: 50%; opacity: #{1 if pct >= 35 else 0.2};" <div t-if="job.process_type_ids" class="o_fp_detail_subtitle">
style="background-color: var(--bs-warning); margin-left: 2px;"/> <span t-out="', '.join(job.process_type_ids.mapped('name'))"/>
<div t-attf-style="width: 30%; opacity: #{1 if pct >= 80 else 0.2};" </div>
style="background-color: var(--bs-info); margin-left: 2px;"/> <div class="o_fp_detail_facts">
</div> <div t-if="job.quantity">
<div class="d-flex justify-content-between small text-muted mt-1"> <span class="o_fp_fact_label">Qty </span>
<span>Receiving</span> <span class="o_fp_fact_value" t-out="job.quantity"/>
<span>In Progress</span> </div>
<span>QC</span> <div t-if="job.received_date">
<span>Ready</span> <span class="o_fp_fact_label">Received </span>
<span>Shipped</span> <span class="o_fp_fact_value" t-field="job.received_date" t-options='{"widget": "date"}'/>
<span>Complete</span> </div>
</div> <div t-if="job.target_ship_date">
</div> <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 class="card bg-body-tertiary border-0 mb-4 o_fp_portal_card"> </div>
<div class="card-body"> <div t-if="job.tracking_ref">
<div class="row"> <span class="o_fp_fact_label">Tracking </span>
<div class="col-md-6"> <span class="o_fp_fact_value" t-out="job.tracking_ref"/>
<h6 class="text-muted small text-uppercase">Current Status</h6> </div>
<span t-attf-class="badge #{ </div>
'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>
<div class="col-md-6 text-md-end" t-if="job.target_ship_date"> <div class="d-flex flex-column align-items-end gap-2">
<h6 class="text-muted small text-uppercase">Target Ship Date</h6> <t t-call="fusion_plating_portal.fp_portal_status_badge">
<h5 class="mb-0"> <t t-set="state" t-value="job.state"/>
<span t-field="job.target_ship_date" <t t-set="label" t-value="dict(job._fields['state']._description_selection(job.env)).get(job.state)"/>
t-options='{"widget": "date"}'/> </t>
</h5>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="row"> <!-- Two-column grid: timeline | docs -->
<div class="col-md-6 mb-4"> <div class="o_fp_job_detail_grid">
<h6 class="text-muted small text-uppercase">Details</h6>
<div> <!-- Timeline -->
<span class="text-muted small">Quantity:</span> <div class="o_fp_card">
<span t-out="job.quantity"/> <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>
<div t-if="job.actual_ship_date">
<span class="text-muted small">Actual Ship Date:</span> <!-- Documents -->
<span t-field="job.actual_ship_date" <div class="o_fp_card">
t-options='{"widget": "date"}'/> <div class="d-flex justify-content-between align-items-center mb-3">
</div> <div style="font-weight:600;color:#111827;font-size:1rem">Documents</div>
<div t-if="job.tracking_ref"> </div>
<span class="text-muted small">Tracking:</span> <t t-foreach="doc_groups" t-as="group">
<span t-out="job.tracking_ref"/> <t t-call="fusion_plating_portal.fp_portal_doc_group">
</div> <t t-set="group_label" t-value="group['label']"/>
<div t-if="job.invoice_ref"> <t t-set="docs" t-value="group['docs']"/>
<span class="text-muted small">Invoice:</span> </t>
<span t-out="job.invoice_ref"/> </t>
</div> </div>
</div> </div>
<div class="col-md-6 mb-4" t-if="job.process_type_ids">
<h6 class="text-muted small text-uppercase">Processes</h6> <!-- Customer notes (if any) -->
<span t-foreach="job.process_type_ids" t-as="pt" <div t-if="job.notes" class="o_fp_card" style="margin-top:1.25rem">
class="badge text-bg-light border me-1" t-out="pt.name"/> <div style="font-weight:600;color:#111827;font-size:1rem;margin-bottom:.6rem">Notes</div>
<div t-out="job.notes"/>
</div> </div>
</div>
<!-- Process Steps — only the customer-visible recipe nodes <!-- Footer -->
(recipe author marked customer_visible=True). --> <div class="o_fp_job_detail_footer">
<t t-set="visible_steps" t-value="job.sudo().get_customer_visible_steps()"/> <div class="o_fp_related_links">
<div class="mb-4" t-if="visible_steps"> <span style="color:#9ca3af">Related:</span>
<h6 class="text-muted small text-uppercase">Process Steps</h6> <a t-if="job.invoice_ref" href="#" t-out="'Invoice ' + job.invoice_ref"/>
<ol class="list-group list-group-numbered"> <a t-else="" class="disabled">Invoice (pending)</a>
<li t-foreach="visible_steps" t-as="step" </div>
class="list-group-item d-flex align-items-center" <a href="/my/jobs" class="o_fp_btn_secondary">← Back to all jobs</a>
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>
<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>
</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"/>
</div> </div>
</div> </div>
</t> </t>