feat(fusion_plating_shopfloor): KanbanCard shared OWL service

Plan task P1.7. Final shared service — standard WO card used on Landing
kanban, Manager Plant Board, and Workflow Funnel. Embeds WorkflowChip,
shows progress bar, priority dot, blocker badge from step.blocker_kind.

Density prop ('compact' vs 'normal') swaps padding for funnel use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-22 21:48:13 -04:00
parent 8109b3ec76
commit a61bd05a5c
4 changed files with 185 additions and 0 deletions

View File

@@ -77,6 +77,9 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
'fusion_plating_shopfloor/static/src/scss/components/_hold_composer.scss',
'fusion_plating_shopfloor/static/src/xml/components/hold_composer.xml',
'fusion_plating_shopfloor/static/src/js/components/hold_composer.js',
'fusion_plating_shopfloor/static/src/scss/components/_kanban_card.scss',
'fusion_plating_shopfloor/static/src/xml/components/kanban_card.xml',
'fusion_plating_shopfloor/static/src/js/components/kanban_card.js',
'fusion_plating_shopfloor/static/src/scss/qr_scanner.scss',
'fusion_plating_shopfloor/static/src/scss/fusion_plating_shopfloor.scss',
'fusion_plating_shopfloor/static/src/scss/plant_overview.scss',

View File

@@ -0,0 +1,59 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating — KanbanCard (shared OWL service)
//
// Standard WO/step card used on:
// • Shop Floor Landing kanban (station + all-plant modes)
// • Manager Plant Board (Needs Worker + In Progress columns)
// • Manager Workflow Funnel (compact density per stage)
//
// Embeds WorkflowChip + a blocker badge from the backend's step.blocker_kind.
//
// Props:
// data : { job_id, display_wo_name, customer, part, qty,
// qty_done, qty_scrapped, date_deadline, priority,
// workflow_state, blocker_kind, blocker_reason,
// current_step_id, work_center }
// density : 'compact' | 'normal' (default 'normal')
// showWorkflowChip : Boolean
// showWorkcenter : Boolean
// showAssignedTo : Boolean
// onTap : Function(data) — called on card click
// =============================================================================
import { Component } from "@odoo/owl";
import { WorkflowChip } from "./workflow_chip";
export class FpKanbanCard extends Component {
static template = "fusion_plating_shopfloor.KanbanCard";
static components = { WorkflowChip };
static props = {
data: { type: Object, optional: false },
density: { type: String, optional: true },
showWorkflowChip: { type: Boolean, optional: true },
showWorkcenter: { type: Boolean, optional: true },
showAssignedTo: { type: Boolean, optional: true },
onTap: { type: Function, optional: true },
};
get isCompact() {
return this.props.density === "compact";
}
get progressPct() {
const d = this.props.data;
if (!d.qty || d.qty <= 0) return 0;
return Math.min(100, Math.round((d.qty_done || 0) * 100 / d.qty));
}
get priorityDot() {
const p = this.props.data.priority;
if (p === "rush") return "danger";
if (p === "high") return "warning";
return "muted";
}
onClick() {
if (this.props.onTap) this.props.onTap(this.props.data);
}
}

View File

@@ -0,0 +1,83 @@
// =============================================================================
// KanbanCard — standard WO/step card for Landing + Manager surfaces
// Dark-mode aware via $o-webclient-color-scheme branch.
// =============================================================================
$o-webclient-color-scheme: bright !default;
$_kc-bg-hex: #ffffff;
$_kc-border-hex: #d8dadd;
$_kc-hover-hex: #f5f5f7;
@if $o-webclient-color-scheme == dark {
$_kc-bg-hex: #22262d !global;
$_kc-border-hex: #424245 !global;
$_kc-hover-hex: #2d3138 !global;
}
.o_fp_kcard {
background: $_kc-bg-hex;
border: 1px solid $_kc-border-hex;
border-radius: 6px;
padding: 0.55rem 0.7rem;
cursor: pointer;
transition: background 0.1s ease;
&:hover { background: $_kc-hover-hex; }
}
.o_fp_kcard_compact { padding: 0.4rem 0.55rem; }
.o_fp_kcard_h1 {
display: flex; justify-content: space-between; align-items: center;
font-weight: 700; font-size: 0.85rem;
}
.o_fp_kcard_h2 {
color: var(--text-secondary, #666);
font-size: 0.75rem;
margin-top: 0.15rem;
}
.o_fp_kcard_qty {
display: flex; justify-content: space-between;
font-size: 0.7rem; color: var(--text-secondary, #777);
margin-top: 0.3rem;
}
.o_fp_kcard_due { color: var(--text-secondary, #999); }
.o_fp_kcard_bar {
height: 4px; background: rgba(0,0,0,0.08);
border-radius: 2px; overflow: hidden;
margin-top: 0.3rem;
> div { height: 100%; background: #34c759; }
}
.o_fp_kcard_chips {
display: flex; gap: 0.35rem; flex-wrap: wrap;
margin-top: 0.4rem;
}
.o_fp_kcard_pri { width: 8px; height: 8px; border-radius: 50%; }
.o_fp_kcard_pri_danger { background: #ff3b30; }
.o_fp_kcard_pri_warning { background: #ff9f0a; }
.o_fp_kcard_pri_muted { background: transparent; }
.o_fp_kcard_blocked {
background: rgba(255,159,10,0.15);
color: #b06600;
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-size: 0.7rem;
}
.o_fp_kcard_wc {
color: var(--text-secondary, #999);
font-size: 0.7rem;
}
@if $o-webclient-color-scheme == dark {
.o_fp_kcard_blocked { color: #ffb84d; }
}

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_plating_shopfloor.KanbanCard">
<div t-att-class="'o_fp_kcard ' + (isCompact ? 'o_fp_kcard_compact' : '')"
t-on-click="onClick">
<div class="o_fp_kcard_h1">
<span class="o_fp_kcard_wo"><t t-esc="props.data.display_wo_name"/></span>
<span t-att-class="'o_fp_kcard_pri o_fp_kcard_pri_' + priorityDot"/>
</div>
<div class="o_fp_kcard_h2">
<span t-esc="props.data.customer or ''"/>
<t t-if="props.data.part"> · <t t-esc="props.data.part"/></t>
</div>
<div class="o_fp_kcard_qty" t-if="props.data.qty">
<span><t t-esc="props.data.qty_done or 0"/> / <t t-esc="props.data.qty"/> done</span>
<span t-if="props.data.date_deadline" class="o_fp_kcard_due">
Due <t t-esc="props.data.date_deadline"/>
</span>
</div>
<div class="o_fp_kcard_bar" t-if="props.data.qty">
<div t-att-style="'width: ' + progressPct + '%'"/>
</div>
<div class="o_fp_kcard_chips">
<WorkflowChip t-if="props.showWorkflowChip and props.data.workflow_state"
state="props.data.workflow_state"/>
<span t-if="props.data.blocker_kind and props.data.blocker_kind !== 'none'"
class="o_fp_kcard_blocked"
t-att-title="props.data.blocker_reason">
<i class="fa fa-lock"/> Blocked
</span>
<span t-if="props.showWorkcenter and props.data.work_center"
class="o_fp_kcard_wc">
@ <t t-esc="props.data.work_center"/>
</span>
</div>
</div>
</t>
</templates>