feat(quality_dashboard): rewrite OWL component + template + SCSS (Task 6)

JS: single FpQualityDashboard component + BannerCard / BannerItem /
SectionCard / SectionRow sibling sub-components in the same file.
Fetches /fp/quality/dashboard/snapshot, 60s poll, deep-link
?tab=certificates scrolls to section-cert via scrollIntoView.

XML: outer wrapper + banner + 6 sections (t-foreach over
state.snapshot.sections). Each section has id='section-<type>' so
the deep-link target works. SectionRow has overdue-conditional
class for red subtitle highlight.

SCSS: local tokens for urgent/good/section-head with light+dark via
$o-webclient-color-scheme branch. 135deg gradients matching the
plant kanban polish. Mobile breakpoint at 900px collapses banner
grid to 1 col and stacks row Open button.

OLD TABS array, selectTab, openTab, totalOpen, totalOverdue all
deleted. Old template's tab tiles + per-tab panels deleted. Existing
per-model kanbans untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-25 12:24:52 -04:00
parent bfeca0ac32
commit 547e7d66a9
3 changed files with 368 additions and 158 deletions

View File

@@ -1,65 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!-- ===== TOP-LEVEL DASHBOARD ===== -->
<t t-name="fusion_plating_quality.FpQualityDashboard">
<div class="o_fp_quality_dashboard p-3">
<div class="o_fp_qd_header d-flex flex-wrap gap-3 mb-3">
<div class="o_fp_qd_summary o_fp_card flex-grow-1 p-3">
<h2 class="mb-2">Quality Overview</h2>
<div class="d-flex gap-4">
<div>
<div class="o_fp_qd_metric_label">Open across all <t t-esc="tabs.length"/></div>
<div class="o_fp_qd_metric_value"><t t-esc="totalOpen"/></div>
</div>
<div>
<div class="o_fp_qd_metric_label text-danger">Overdue</div>
<div class="o_fp_qd_metric_value text-danger"><t t-esc="totalOverdue"/></div>
</div>
</div>
</div>
<t t-foreach="tabs" t-as="tab" t-key="tab.id">
<button class="o_fp_qd_tile o_fp_card p-3 border-0"
t-att-class="{ 'o_fp_qd_active': state.activeTab === tab.id }"
t-on-click="() => this.selectTab(tab.id)">
<div class="o_fp_qd_metric_label"><t t-esc="tab.label"/></div>
<div class="o_fp_qd_metric_value">
<t t-esc="state.counts[tab.id]?.open || 0"/>
</div>
<div class="o_fp_qd_metric_sub text-muted small"
t-if="(state.counts[tab.id]?.overdue || 0) > 0">
<t t-esc="state.counts[tab.id].overdue"/> overdue
</div>
</button>
</t>
<div class="o_fp_qd p-3">
<div t-if="state.loading" class="o_fp_qd_loading">Loading…</div>
<div t-if="state.error" class="o_fp_qd_error">
<t t-esc="state.error"/>
</div>
<t t-if="state.snapshot">
<BannerCard banner="state.snapshot.banner"
onOpen.bind="onOpenItem"/>
<t t-foreach="state.snapshot.sections"
t-as="section" t-key="section.type">
<SectionCard section="section"
onOpen.bind="onOpenItem"
onOpenKanban.bind="onOpenKanban"/>
</t>
</t>
</div>
</t>
<div class="o_fp_qd_body">
<t t-foreach="tabs" t-as="tab" t-key="tab.id">
<div t-if="state.activeTab === tab.id" class="o_fp_qd_panel o_fp_card p-4">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h3 class="mb-1"><t t-esc="tab.label"/></h3>
<div class="text-muted small">
<t t-esc="state.counts[tab.id]?.open || 0"/> open
<t t-if="(state.counts[tab.id]?.overdue || 0) > 0">
<t t-esc="state.counts[tab.id].overdue"/> overdue
</t>
</div>
</div>
<button class="btn btn-primary"
t-on-click="() => this.openTab(tab)">
Open <t t-esc="tab.label"/> Kanban
</button>
</div>
<p class="text-muted">
Click "Open Kanban" to drill into the full
<t t-esc="tab.label.toLowerCase()"/> board with stage / state grouping,
drag-and-drop, and the standard filters.
</p>
</div>
<!-- ===== BANNER CARD ===== -->
<t t-name="fusion_plating_quality.BannerCard">
<div t-if="props.banner.all_clear"
class="o_fp_qd_banner o_fp_qd_banner_clear">
<div class="o_fp_qd_banner_clear_icon"></div>
<div class="o_fp_qd_banner_clear_text">
<strong>All caught up</strong> — no critical items right now
</div>
</div>
<div t-else="" class="o_fp_qd_banner o_fp_qd_banner_urgent">
<div class="o_fp_qd_banner_head">
⚠️ NEEDS ATTENTION TODAY ·
<t t-esc="props.banner.total_matching"/>
<span t-if="props.banner.total_matching > props.banner.items.length"
class="o_fp_qd_banner_overflow">
(showing <t t-esc="props.banner.items.length"/>
of <t t-esc="props.banner.total_matching"/>
see sections below for the rest)
</span>
</div>
<div class="o_fp_qd_banner_grid">
<t t-foreach="props.banner.items"
t-as="item" t-key="item.type + '_' + item.id">
<BannerItem item="item" onOpen="props.onOpen"/>
</t>
</div>
</div>
</t>
<t t-name="fusion_plating_quality.BannerItem">
<button class="o_fp_qd_banner_item"
t-on-click="() => props.onOpen(props.item)">
<div class="o_fp_qd_banner_item_l1">
<span class="o_fp_qd_banner_item_name">
<strong t-esc="props.item.name"/>
</span>
<span class="o_fp_qd_banner_item_type"
t-esc="props.item.type.toUpperCase()"/>
<span t-if="props.item.critical_badge"
class="o_fp_qd_banner_item_badge"
t-esc="props.item.critical_badge"/>
</div>
<div class="o_fp_qd_banner_item_l2">
<span class="o_fp_qd_banner_item_cust"
t-esc="props.item.customer"/>
<span class="o_fp_qd_banner_item_subtitle"
t-esc="props.item.subtitle"/>
</div>
</button>
</t>
<!-- ===== SECTION CARD ===== -->
<t t-name="fusion_plating_quality.SectionCard">
<div class="o_fp_qd_section"
t-att-id="'section-' + props.section.type">
<div class="o_fp_qd_section_head">
<span class="o_fp_qd_section_title">
<t t-esc="props.section.icon"/>
<strong t-esc="props.section.label"/>
· <t t-esc="props.section.open"/> open
<t t-if="props.section.overdue">
·
<span class="o_fp_qd_section_overdue">
<t t-esc="props.section.overdue"/> overdue
</span>
</t>
</span>
<button class="o_fp_qd_section_open"
t-on-click="() => props.onOpenKanban(props.section)">
Open all →
</button>
</div>
<div t-if="props.section.items.length === 0"
class="o_fp_qd_section_empty">
No open items
</div>
<t t-else="" t-foreach="props.section.items"
t-as="item" t-key="item.id">
<SectionRow item="item" onOpen="props.onOpen"/>
</t>
</div>
</t>
<t t-name="fusion_plating_quality.SectionRow">
<div class="o_fp_qd_row"
t-att-class="props.item.urgency === 'overdue'
? 'o_fp_qd_row_overdue' : ''">
<div class="o_fp_qd_row_main">
<strong t-esc="props.item.name"/>
<span class="o_fp_qd_row_sep"> · </span>
<span class="o_fp_qd_row_cust" t-esc="props.item.customer"/>
<span t-if="props.item.subtitle"
class="o_fp_qd_row_subtitle">
<span class="o_fp_qd_row_sep"> · </span>
<t t-esc="props.item.subtitle"/>
</span>
</div>
<button class="o_fp_qd_row_open"
t-on-click="() => props.onOpen(props.item)">
Open →
</button>
</div>
</t>
</templates>