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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user