feat(fusion_plating_shopfloor): Manager Desk 4-tab refactor (P4.5-P4.10)
Plan tasks P4.5 through P4.10 batched. Existing 3-column Plant Board
becomes one tab of four; adds Workflow Funnel (default), Approval
Inbox, and At-Risk siblings. Adds 2 new KPI tiles for Pending Cert +
At-Risk.
WORKFLOW FUNNEL (default tab)
Calls /fp/manager/funnel. Renders one row per fp.job.workflow.state
with stage chip + count + top 5 WO cards. Tap a card → JobWorkspace.
Bar chart bar behind each row scales with stage count.
APPROVAL INBOX
Calls /fp/manager/approval_inbox. Three strips: Holds to Release,
Certs to Issue, Scrap to Review. Per-row open + Open Workspace
buttons. Tab badge shows total pending count.
PLANT BOARD (existing — relocated as one tab)
The 3-column Needs Worker / In Progress / Team layout that already
exists, wrapped in t-if="activeTab === 'plant_board'". No behaviour
change — still uses /fp/manager/overview with 8s refresh.
AT-RISK
Calls /fp/manager/at_risk. 3 sub-panels: Trending Late (sorted by
late_risk_ratio desc), Hold Reasons (read_group), Bottleneck heatmap
(bottleneck_score from P4.1 with red/yellow/green bars).
KPI STRIP (new conditional tiles)
Pending Cert — count from inbox.certs_to_issue, click to open Inbox tab.
At-Risk — count from at_risk.trending_late, click to open At-Risk.
Auto-refresh: 8s for /fp/manager/overview (existing); the active tab's
data also refreshes every 8s via refreshActiveTab().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,15 +43,27 @@ export class ManagerDashboard extends Component {
|
||||
// Defaults to false because lead-hand coverage often needs
|
||||
// off-roster names.
|
||||
hideOffShift: false,
|
||||
// Phase 4 tablet redesign — 4 sibling tabs.
|
||||
// funnel | inbox | plant_board | at_risk
|
||||
activeTab: "funnel",
|
||||
funnel: null, // /fp/manager/funnel payload
|
||||
inbox: null, // /fp/manager/approval_inbox payload
|
||||
atRisk: null, // /fp/manager/at_risk payload
|
||||
});
|
||||
|
||||
this._lastHash = null; // sent to server to skip unchanged polls
|
||||
|
||||
onMounted(async () => {
|
||||
await this.refresh();
|
||||
// Load the default tab's data (Workflow Funnel) on first paint
|
||||
await this.loadFunnel();
|
||||
// 8s cadence: fast enough for production pace, light on the
|
||||
// network since unchanged payloads short-circuit server-side.
|
||||
this._interval = setInterval(() => this.refresh(), 8000);
|
||||
// The active tab's data also refreshes on each tick.
|
||||
this._interval = setInterval(() => {
|
||||
this.refresh();
|
||||
this.refreshActiveTab();
|
||||
}, 8000);
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
@@ -283,6 +295,85 @@ export class ManagerDashboard extends Component {
|
||||
target: "current",
|
||||
});
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// Phase 4 tablet redesign — 4 sibling tabs
|
||||
// ==================================================================
|
||||
|
||||
async setActiveTab(tab) {
|
||||
if (this.state.activeTab === tab) return;
|
||||
this.state.activeTab = tab;
|
||||
// Load the tab's data on first switch — subsequent ticks refresh
|
||||
// via the auto-poll.
|
||||
await this.refreshActiveTab();
|
||||
}
|
||||
|
||||
async refreshActiveTab() {
|
||||
if (this.state.activeTab === "funnel") return this.loadFunnel();
|
||||
if (this.state.activeTab === "inbox") return this.loadInbox();
|
||||
if (this.state.activeTab === "at_risk") return this.loadAtRisk();
|
||||
// plant_board uses /fp/manager/overview via refresh()
|
||||
}
|
||||
|
||||
async loadFunnel() {
|
||||
try {
|
||||
const res = await rpc("/fp/manager/funnel", {});
|
||||
if (res && res.ok) this.state.funnel = res;
|
||||
} catch (err) {
|
||||
this.setMessage(`Funnel: ${err.message}`, "danger");
|
||||
}
|
||||
}
|
||||
|
||||
async loadInbox() {
|
||||
try {
|
||||
const res = await rpc("/fp/manager/approval_inbox", {});
|
||||
if (res && res.ok) this.state.inbox = res;
|
||||
} catch (err) {
|
||||
this.setMessage(`Inbox: ${err.message}`, "danger");
|
||||
}
|
||||
}
|
||||
|
||||
async loadAtRisk() {
|
||||
try {
|
||||
const res = await rpc("/fp/manager/at_risk", {});
|
||||
if (res && res.ok) this.state.atRisk = res;
|
||||
} catch (err) {
|
||||
this.setMessage(`At-Risk: ${err.message}`, "danger");
|
||||
}
|
||||
}
|
||||
|
||||
// Tap a WO card on any tab → open the JobWorkspace (Phase 1)
|
||||
openJobWorkspace(jobId) {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.client",
|
||||
tag: "fp_job_workspace",
|
||||
params: { job_id: jobId },
|
||||
target: "current",
|
||||
});
|
||||
}
|
||||
|
||||
// Pill colour from workflow_state.color (mirrors WorkflowChip toneClass)
|
||||
funnelStageTone(color) {
|
||||
const map = {
|
||||
grey: "muted", blue: "info", cyan: "info",
|
||||
yellow: "warning", orange: "warning",
|
||||
green: "success", success: "success",
|
||||
danger: "danger", purple: "info",
|
||||
};
|
||||
return map[color] || "muted";
|
||||
}
|
||||
|
||||
// Bottleneck severity tone for the heatmap bar colour
|
||||
bottleneckTone(score) {
|
||||
if (score >= 200) return "danger";
|
||||
if (score >= 60) return "warning";
|
||||
return "success";
|
||||
}
|
||||
|
||||
bottleneckPct(score) {
|
||||
// Normalize to 0-100 for the bar width; cap at 100
|
||||
return Math.min(100, Math.round(score / 5));
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("actions").add("fp_manager_dashboard", ManagerDashboard);
|
||||
|
||||
@@ -646,3 +646,230 @@
|
||||
display: flex; gap: $fp-space-1; margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Phase 4 tablet redesign — Manager dashboard sibling tabs
|
||||
// =============================================================================
|
||||
|
||||
.o_fp_mgr_tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 8px 16px 0;
|
||||
border-bottom: 1px solid $fp-border;
|
||||
background: $fp-card;
|
||||
|
||||
.o_fp_mgr_tab {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
padding: 8px 14px;
|
||||
font-size: 0.9rem;
|
||||
color: $fp-ink-soft;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
&:hover { color: $fp-ink; }
|
||||
&.active {
|
||||
color: $fp-accent;
|
||||
border-bottom-color: $fp-accent;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.o_fp_mgr_tab_badge {
|
||||
background: $fp-accent;
|
||||
color: white;
|
||||
border-radius: 999px;
|
||||
padding: 1px 8px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Workflow Funnel tab -------------------------------------------------
|
||||
.o_fp_mgr_funnel {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.o_fp_funnel_row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid $fp-border;
|
||||
}
|
||||
|
||||
.o_fp_funnel_stage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.o_fp_funnel_count {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
min-width: 28px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.o_fp_funnel_cards {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.o_fp_funnel_card {
|
||||
background: $fp-card;
|
||||
border: 1px solid $fp-border;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font-size: 0.78rem;
|
||||
min-width: 130px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease, border-color 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, #{$fp-accent} 5%, #{$fp-card});
|
||||
border-color: color-mix(in srgb, #{$fp-accent} 30%, #{$fp-border});
|
||||
}
|
||||
|
||||
.o_fp_funnel_card_wo { font-weight: 600; }
|
||||
.o_fp_funnel_card_meta { color: $fp-ink-soft; font-size: 0.7rem; }
|
||||
}
|
||||
|
||||
.o_fp_funnel_more, .o_fp_funnel_empty {
|
||||
color: $fp-ink-soft;
|
||||
font-size: 0.78rem;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Approval Inbox tab --------------------------------------------------
|
||||
.o_fp_mgr_inbox {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.o_fp_inbox_strip {
|
||||
background: $fp-card;
|
||||
border: 1px solid $fp-border;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
|
||||
h4 {
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: $fp-ink-soft;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_inbox_row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
font-size: 0.85rem;
|
||||
border-bottom: 1px dashed $fp-border;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
.ms-auto { margin-left: auto; }
|
||||
}
|
||||
|
||||
.o_fp_empty_small {
|
||||
color: $fp-ink-soft;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- At-Risk tab ---------------------------------------------------------
|
||||
.o_fp_mgr_atrisk {
|
||||
padding: 16px;
|
||||
|
||||
.o_fp_atrisk_grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr 1.2fr;
|
||||
gap: 12px;
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_atrisk_card {
|
||||
background: $fp-card;
|
||||
border: 1px solid $fp-border;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
|
||||
h4 {
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: $fp-ink-soft;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_atrisk_row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 6px 0;
|
||||
font-size: 0.82rem;
|
||||
border-bottom: 1px dashed $fp-border;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
|
||||
&[t-on-click], &:hover { cursor: pointer; }
|
||||
&:last-child { border-bottom: none; }
|
||||
.ms-auto { margin-left: auto; }
|
||||
}
|
||||
|
||||
.o_fp_atrisk_bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
font-size: 0.78rem;
|
||||
|
||||
.o_fp_atrisk_bar_name { min-width: 100px; }
|
||||
.o_fp_atrisk_bar_track {
|
||||
flex: 1;
|
||||
height: 10px;
|
||||
background: color-mix(in srgb, #{$fp-ink-soft} 15%, transparent);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.o_fp_atrisk_bar_fill { height: 100%; display: block; }
|
||||
.o_fp_atrisk_bar_danger { background: #ff3b30; }
|
||||
.o_fp_atrisk_bar_warning { background: #ff9f0a; }
|
||||
.o_fp_atrisk_bar_success { background: #34c759; }
|
||||
.o_fp_atrisk_bar_score { font-weight: 600; min-width: 32px; text-align: right; }
|
||||
}
|
||||
|
||||
.o_fp_empty_small {
|
||||
color: $fp-ink-soft;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,10 +153,53 @@
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Phase 4 tablet redesign — Pending Cert + At-Risk tiles -->
|
||||
<div class="o_fp_kpi o_fp_kpi_warning"
|
||||
t-if="state.inbox and state.inbox.certs_to_issue and state.inbox.certs_to_issue.length"
|
||||
t-on-click="() => this.setActiveTab('inbox')">
|
||||
<i class="fa fa-file-text"/>
|
||||
<div class="o_fp_kpi_value">
|
||||
<t t-esc="state.inbox.certs_to_issue.length"/>
|
||||
</div>
|
||||
<div class="o_fp_kpi_label">Pending Cert</div>
|
||||
</div>
|
||||
<div class="o_fp_kpi o_fp_kpi_danger"
|
||||
t-if="state.atRisk and state.atRisk.trending_late and state.atRisk.trending_late.length"
|
||||
t-on-click="() => this.setActiveTab('at_risk')">
|
||||
<i class="fa fa-exclamation-triangle"/>
|
||||
<div class="o_fp_kpi_value">
|
||||
<t t-esc="state.atRisk.trending_late.length"/>
|
||||
</div>
|
||||
<div class="o_fp_kpi_label">At-Risk</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ Workload grid ============ -->
|
||||
<div class="o_fp_manager_grid" t-if="state.overview">
|
||||
<!-- ============ Phase 4 tab navigation ============ -->
|
||||
<div class="o_fp_mgr_tabs" t-if="state.overview">
|
||||
<button t-att-class="'o_fp_mgr_tab ' + (state.activeTab === 'funnel' ? 'active' : '')"
|
||||
t-on-click="() => this.setActiveTab('funnel')">
|
||||
<i class="fa fa-filter"/> Workflow Funnel
|
||||
</button>
|
||||
<button t-att-class="'o_fp_mgr_tab ' + (state.activeTab === 'inbox' ? 'active' : '')"
|
||||
t-on-click="() => this.setActiveTab('inbox')">
|
||||
<i class="fa fa-inbox"/> Approval Inbox
|
||||
<span t-if="state.inbox" class="o_fp_mgr_tab_badge">
|
||||
<t t-esc="(state.inbox.holds_to_release.length + state.inbox.certs_to_issue.length + state.inbox.scrap_to_review.length)"/>
|
||||
</span>
|
||||
</button>
|
||||
<button t-att-class="'o_fp_mgr_tab ' + (state.activeTab === 'plant_board' ? 'active' : '')"
|
||||
t-on-click="() => this.setActiveTab('plant_board')">
|
||||
<i class="fa fa-th"/> Plant Board
|
||||
</button>
|
||||
<button t-att-class="'o_fp_mgr_tab ' + (state.activeTab === 'at_risk' ? 'active' : '')"
|
||||
t-on-click="() => this.setActiveTab('at_risk')">
|
||||
<i class="fa fa-fire"/> At-Risk
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ============ PLANT BOARD TAB (existing 3-column grid) ============ -->
|
||||
<div class="o_fp_manager_grid"
|
||||
t-if="state.overview and state.activeTab === 'plant_board'">
|
||||
|
||||
<!-- Needs a Worker -->
|
||||
<section class="o_fp_panel o_fp_panel_unassigned">
|
||||
@@ -369,6 +412,171 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- ============ WORKFLOW FUNNEL TAB (Phase 4) ============ -->
|
||||
<div class="o_fp_mgr_funnel"
|
||||
t-if="state.overview and state.activeTab === 'funnel'">
|
||||
<div t-if="!state.funnel" class="o_fp_empty">
|
||||
<i class="fa fa-spinner fa-spin"/>
|
||||
<div>Loading workflow funnel…</div>
|
||||
</div>
|
||||
<t t-if="state.funnel">
|
||||
<div t-foreach="state.funnel.stages" t-as="stage" t-key="stage.id"
|
||||
class="o_fp_funnel_row">
|
||||
<div class="o_fp_funnel_stage">
|
||||
<span t-att-class="'o_fp_wf_chip o_fp_wf_chip_' + funnelStageTone(stage.color)">
|
||||
<span class="o_fp_wf_dot"/>
|
||||
<span class="o_fp_wf_label" t-esc="stage.name"/>
|
||||
</span>
|
||||
<span class="o_fp_funnel_count" t-esc="stage.count"/>
|
||||
</div>
|
||||
<div class="o_fp_funnel_cards">
|
||||
<t t-foreach="stage.jobs" t-as="card" t-key="card.job_id">
|
||||
<div class="o_fp_funnel_card"
|
||||
t-on-click="() => this.openJobWorkspace(card.job_id)">
|
||||
<div class="o_fp_funnel_card_wo" t-esc="card.display_wo_name"/>
|
||||
<div class="o_fp_funnel_card_meta">
|
||||
<t t-esc="card.customer"/> · <t t-esc="card.days_in_stage"/>d
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<span t-if="stage.count > stage.jobs.length" class="o_fp_funnel_more">
|
||||
+<t t-esc="stage.count - stage.jobs.length"/> more
|
||||
</span>
|
||||
<span t-if="!stage.jobs.length" class="o_fp_funnel_empty">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- ============ APPROVAL INBOX TAB (Phase 4) ============ -->
|
||||
<div class="o_fp_mgr_inbox"
|
||||
t-if="state.overview and state.activeTab === 'inbox'">
|
||||
<div t-if="!state.inbox" class="o_fp_empty">
|
||||
<i class="fa fa-spinner fa-spin"/>
|
||||
<div>Loading approval inbox…</div>
|
||||
</div>
|
||||
<t t-if="state.inbox">
|
||||
<!-- Holds to Release -->
|
||||
<section class="o_fp_inbox_strip">
|
||||
<h4>
|
||||
<i class="fa fa-pause-circle text-danger"/>
|
||||
Holds to Release (<t t-esc="state.inbox.holds_to_release.length"/>)
|
||||
</h4>
|
||||
<div t-if="!state.inbox.holds_to_release.length" class="o_fp_empty_small">
|
||||
No open holds.
|
||||
</div>
|
||||
<t t-foreach="state.inbox.holds_to_release" t-as="h" t-key="h.hold_id">
|
||||
<div class="o_fp_inbox_row">
|
||||
<span><strong t-esc="h.name"/> · <t t-esc="h.job_name"/></span>
|
||||
<span class="text-muted">· <t t-esc="h.reason"/> · qty <t t-esc="h.qty"/></span>
|
||||
<span class="text-muted ms-auto"><t t-esc="h.requested_by"/> · <t t-esc="h.requested_at"/></span>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-2"
|
||||
t-on-click="() => this.openRecord('fusion.plating.quality.hold', h.hold_id)">
|
||||
Open
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</section>
|
||||
<!-- Certs to Issue -->
|
||||
<section class="o_fp_inbox_strip">
|
||||
<h4>
|
||||
<i class="fa fa-certificate text-warning"/>
|
||||
Certs to Issue (<t t-esc="state.inbox.certs_to_issue.length"/>)
|
||||
</h4>
|
||||
<div t-if="!state.inbox.certs_to_issue.length" class="o_fp_empty_small">
|
||||
No certs waiting to be issued.
|
||||
</div>
|
||||
<t t-foreach="state.inbox.certs_to_issue" t-as="c" t-key="c.job_id">
|
||||
<div class="o_fp_inbox_row">
|
||||
<span><strong t-esc="c.display_wo_name"/> · <t t-esc="c.customer"/></span>
|
||||
<span class="text-muted">· needs <t t-esc="c.cert_types.join(', ')"/></span>
|
||||
<span class="text-muted ms-auto">all steps done <t t-esc="c.all_steps_done_at"/></span>
|
||||
<button class="btn btn-sm btn-primary ms-2"
|
||||
t-on-click="() => this.openJobWorkspace(c.job_id)">
|
||||
Open Workspace
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</section>
|
||||
<!-- Scrap to Review -->
|
||||
<section class="o_fp_inbox_strip">
|
||||
<h4>
|
||||
<i class="fa fa-trash text-muted"/>
|
||||
Scrap to Review (<t t-esc="state.inbox.scrap_to_review.length"/>)
|
||||
</h4>
|
||||
<div t-if="!state.inbox.scrap_to_review.length" class="o_fp_empty_small">
|
||||
No recent scrap to acknowledge.
|
||||
</div>
|
||||
<t t-foreach="state.inbox.scrap_to_review" t-as="s" t-key="s.hold_id">
|
||||
<div class="o_fp_inbox_row">
|
||||
<span><strong t-esc="s.job_name"/> · <t t-esc="s.scrap_qty"/> scrapped</span>
|
||||
<span class="text-muted" t-if="s.reason">· "<t t-esc="s.reason"/>"</span>
|
||||
<span class="text-muted ms-auto"><t t-esc="s.operator"/> · <t t-esc="s.at"/></span>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-2"
|
||||
t-on-click="() => this.openRecord('fusion.plating.quality.hold', s.hold_id)">
|
||||
Open
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</section>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- ============ AT-RISK TAB (Phase 4) ============ -->
|
||||
<div class="o_fp_mgr_atrisk"
|
||||
t-if="state.overview and state.activeTab === 'at_risk'">
|
||||
<div t-if="!state.atRisk" class="o_fp_empty">
|
||||
<i class="fa fa-spinner fa-spin"/>
|
||||
<div>Loading at-risk view…</div>
|
||||
</div>
|
||||
<t t-if="state.atRisk">
|
||||
<div class="o_fp_atrisk_grid">
|
||||
<section class="o_fp_atrisk_card">
|
||||
<h4><i class="fa fa-clock-o"/> Trending Late (<t t-esc="state.atRisk.trending_late.length"/>)</h4>
|
||||
<div t-if="!state.atRisk.trending_late.length" class="o_fp_empty_small">
|
||||
No late-risk jobs right now.
|
||||
</div>
|
||||
<t t-foreach="state.atRisk.trending_late" t-as="j" t-key="j.job_id">
|
||||
<div class="o_fp_atrisk_row"
|
||||
t-on-click="() => this.openJobWorkspace(j.job_id)">
|
||||
<span><strong t-esc="j.display_wo_name"/> · <t t-esc="j.customer"/></span>
|
||||
<span t-if="j.stuck_at" class="text-muted">· stuck at <t t-esc="j.stuck_at"/></span>
|
||||
<span class="text-danger ms-auto">×<t t-esc="j.late_risk_ratio"/></span>
|
||||
</div>
|
||||
</t>
|
||||
</section>
|
||||
<section class="o_fp_atrisk_card">
|
||||
<h4><i class="fa fa-pause-circle"/> Hold Reasons</h4>
|
||||
<div t-if="!state.atRisk.hold_reasons.length" class="o_fp_empty_small">
|
||||
No open holds.
|
||||
</div>
|
||||
<t t-foreach="state.atRisk.hold_reasons" t-as="r" t-key="r.reason">
|
||||
<div class="o_fp_atrisk_row">
|
||||
<span t-esc="r.label"/>
|
||||
<strong class="ms-auto" t-esc="r.count"/>
|
||||
</div>
|
||||
</t>
|
||||
</section>
|
||||
<section class="o_fp_atrisk_card">
|
||||
<h4><i class="fa fa-fire"/> Bottleneck</h4>
|
||||
<div t-if="!state.atRisk.bottleneck.length" class="o_fp_empty_small">
|
||||
No bottlenecks detected.
|
||||
</div>
|
||||
<t t-foreach="state.atRisk.bottleneck" t-as="b" t-key="b.work_centre_id">
|
||||
<div class="o_fp_atrisk_bar">
|
||||
<span class="o_fp_atrisk_bar_name" t-esc="b.work_centre_name"/>
|
||||
<span class="o_fp_atrisk_bar_track">
|
||||
<span t-att-class="'o_fp_atrisk_bar_fill o_fp_atrisk_bar_' + bottleneckTone(b.score)"
|
||||
t-att-style="'width: ' + bottleneckPct(b.score) + '%'"/>
|
||||
</span>
|
||||
<span class="o_fp_atrisk_bar_score" t-esc="b.score"/>
|
||||
</div>
|
||||
</t>
|
||||
</section>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- ============ Loading ============ -->
|
||||
<div t-if="!state.overview and !state.loadError" class="o_fp_empty">
|
||||
<i class="fa fa-spinner fa-spin"/>
|
||||
|
||||
Reference in New Issue
Block a user