From 8b9b4d60ad70dbfa6d26ec26fd122e0eb36beca3 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 23 May 2026 20:57:55 -0400 Subject: [PATCH] =?UTF-8?q?feat(shopfloor):=20Phase=204=20=E2=80=94=20plan?= =?UTF-8?q?t-view=20kanban=20frontend=20(OWL=20+=20SCSS=20+=20XML)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PV-Phase4 of the plant-view redesign. 19 new files implementing the 6-component OWL tree plus design tokens. Components (each = JS + XML + SCSS triple): - FpMiniTimeline — 9-step bar consuming mini_timeline_json - FpPlantCard — Variant C card; 13 state-* CSS classes; tap opens fp_job_workspace - FpColumnHeader — column label + count badge + 'You're here' badge when paired - FpKpiTile — clickable KPI button with urgent/warn/good variants and active state - FpFilterChip — toggleable chip - FpPlantKanban — top-level orchestrator: 10s polling, mode toggle, search + 6 filter chips, board with 9 fixed columns, localStorage filter persistence SCSS: - _plant_tokens.scss (loads first, exposes $plant-* vars to every later file — required because Odoo 19 forbids @import in custom SCSS, manifest order IS the concat order) - Dark mode via $o-webclient-color-scheme compile-time branch Manifest registers all assets in dependency order: tokens → component SCSS → component XML → leaf JS → top-level JS. Mirrors the existing project pattern. Critical patterns honored: - Project rule 20 (no String/Number/parseInt in OWL templates): all coercion in JS, string literals in foreach arrays. - No t-out without markup() (none in this batch — all card text is pre-formatted by the controller). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fusion_plating_shopfloor/__manifest__.py | 27 +++ .../static/src/js/components/column_header.js | 13 ++ .../static/src/js/components/filter_chip.js | 15 ++ .../static/src/js/components/kpi_tile.js | 24 +++ .../static/src/js/components/mini_timeline.js | 56 ++++++ .../static/src/js/components/plant_card.js | 70 ++++++++ .../static/src/js/plant_kanban.js | 162 ++++++++++++++++++ .../static/src/scss/_plant_tokens.scss | 80 +++++++++ .../src/scss/components/_column_header.scss | 45 +++++ .../src/scss/components/_filter_chip.scss | 20 +++ .../static/src/scss/components/_kpi_tile.scss | 34 ++++ .../src/scss/components/_mini_timeline.scss | 56 ++++++ .../src/scss/components/_plant_card.scss | 138 +++++++++++++++ .../static/src/scss/plant_kanban.scss | 143 ++++++++++++++++ .../src/xml/components/column_header.xml | 14 ++ .../static/src/xml/components/filter_chip.xml | 10 ++ .../static/src/xml/components/kpi_tile.xml | 11 ++ .../src/xml/components/mini_timeline.xml | 20 +++ .../static/src/xml/components/plant_card.xml | 80 +++++++++ .../static/src/xml/plant_kanban.xml | 107 ++++++++++++ 20 files changed, 1125 insertions(+) create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/js/components/column_header.js create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/js/components/filter_chip.js create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/js/components/kpi_tile.js create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/js/components/mini_timeline.js create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/js/components/plant_card.js create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/js/plant_kanban.js create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/scss/_plant_tokens.scss create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_column_header.scss create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_filter_chip.scss create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_kpi_tile.scss create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_mini_timeline.scss create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_plant_card.scss create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/scss/plant_kanban.scss create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/xml/components/column_header.xml create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/xml/components/filter_chip.xml create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/xml/components/kpi_tile.xml create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/xml/components/mini_timeline.xml create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/xml/components/plant_card.xml create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/xml/plant_kanban.xml diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index 0000ae5b..2581d813 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -108,6 +108,33 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved. 'fusion_plating_shopfloor/static/src/scss/shopfloor_landing.scss', 'fusion_plating_shopfloor/static/src/xml/shopfloor_landing.xml', 'fusion_plating_shopfloor/static/src/js/shopfloor_landing.js', + # ---- Plant View Kanban (2026-05-23 redesign) --------------- + # Tokens MUST load first (project rule 8: SCSS @import is + # forbidden in Odoo 19 custom code; manifest order is the + # concatenation order, and tokens carry the $plant-* vars + # used by every component partial below). + 'fusion_plating_shopfloor/static/src/scss/_plant_tokens.scss', + 'fusion_plating_shopfloor/static/src/scss/components/_plant_card.scss', + 'fusion_plating_shopfloor/static/src/scss/components/_mini_timeline.scss', + 'fusion_plating_shopfloor/static/src/scss/components/_column_header.scss', + 'fusion_plating_shopfloor/static/src/scss/components/_kpi_tile.scss', + 'fusion_plating_shopfloor/static/src/scss/components/_filter_chip.scss', + 'fusion_plating_shopfloor/static/src/scss/plant_kanban.scss', + # XML templates (must precede their JS consumers) + 'fusion_plating_shopfloor/static/src/xml/components/mini_timeline.xml', + 'fusion_plating_shopfloor/static/src/xml/components/plant_card.xml', + 'fusion_plating_shopfloor/static/src/xml/components/column_header.xml', + 'fusion_plating_shopfloor/static/src/xml/components/kpi_tile.xml', + 'fusion_plating_shopfloor/static/src/xml/components/filter_chip.xml', + 'fusion_plating_shopfloor/static/src/xml/plant_kanban.xml', + # JS — leaf components first, then card (imports timeline), + # then top-level orchestrator (imports all). + 'fusion_plating_shopfloor/static/src/js/components/mini_timeline.js', + 'fusion_plating_shopfloor/static/src/js/components/plant_card.js', + 'fusion_plating_shopfloor/static/src/js/components/column_header.js', + 'fusion_plating_shopfloor/static/src/js/components/kpi_tile.js', + 'fusion_plating_shopfloor/static/src/js/components/filter_chip.js', + 'fusion_plating_shopfloor/static/src/js/plant_kanban.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', diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/components/column_header.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/column_header.js new file mode 100644 index 00000000..ecd955df --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/column_header.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ +import { Component } from "@odoo/owl"; + +export class FpColumnHeader extends Component { + static template = "fusion_plating_shopfloor.ColumnHeader"; + static props = { + column: { type: Object }, // {area_kind, label, is_mine, card_ids} + }; + + get cardCount() { + return this.props.column.card_ids.length; + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/components/filter_chip.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/filter_chip.js new file mode 100644 index 00000000..657b9d53 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/filter_chip.js @@ -0,0 +1,15 @@ +/** @odoo-module **/ +import { Component } from "@odoo/owl"; + +export class FpFilterChip extends Component { + static template = "fusion_plating_shopfloor.FilterChip"; + static props = { + label: { type: String }, + active: { type: Boolean }, + onToggle: { type: Function }, + }; + + onClick() { + this.props.onToggle(); + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/components/kpi_tile.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/kpi_tile.js new file mode 100644 index 00000000..83f00efb --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/kpi_tile.js @@ -0,0 +1,24 @@ +/** @odoo-module **/ +import { Component } from "@odoo/owl"; + +export class FpKpiTile extends Component { + static template = "fusion_plating_shopfloor.KpiTile"; + static props = { + value: { type: [Number, String] }, + label: { type: String }, + kind: { type: String, optional: true }, // urgent | warn | good | '' + active: { type: Boolean, optional: true }, + onClick: { type: Function, optional: true }, + }; + + get tileClass() { + const classes = ["o_fp_kpi_tile"]; + if (this.props.kind) classes.push(this.props.kind); + if (this.props.active) classes.push("active"); + return classes.join(" "); + } + + onClick() { + if (this.props.onClick) this.props.onClick(); + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/components/mini_timeline.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/mini_timeline.js new file mode 100644 index 00000000..1430fa2c --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/mini_timeline.js @@ -0,0 +1,56 @@ +/** @odoo-module **/ +// ===================================================================== +// FpMiniTimeline — 9-step horizontal bar showing recipe journey. +// Consumes mini_timeline JSON from /fp/landing/plant_kanban. +// Per project rule 20: no String()/Number() in templates; classFor() +// and labelFor() do all the formatting in JS. +// ===================================================================== + +import { Component } from "@odoo/owl"; + +const AREA_LABELS = { + receiving: "Rec", + masking: "Mask", + blasting: "Blast", + racking: "Rack", + plating: "Plat", + baking: "Bake", + de_racking: "D-R", + inspection: "Insp", + shipping: "Ship", +}; + +// Map card_state variant → CSS modifier class on the current step +const VARIANT_TO_CLASS = { + on_hold: "hold", + predecessor_locked: "locked", + bake_due: "bake", + awaiting_signoff: "signoff", + idle_warning: "idle", + awaiting_qc: "qc", + no_parts: "noparts", + done: "done", + contract_review: "paperwork", + // ready / running / *_mine → default yellow (no extra class) +}; + +export class FpMiniTimeline extends Component { + static template = "fusion_plating_shopfloor.MiniTimeline"; + static props = { + timeline: { type: Array }, + }; + + labelFor(area) { + return AREA_LABELS[area] || area; + } + + classFor(entry) { + if (entry.state === "done") return "tl-step done"; + if (entry.state === "current") { + const variant = (entry.variant || "").replace("_mine", ""); + const cls = VARIANT_TO_CLASS[variant] || ""; + return cls ? `tl-step current ${cls}` : "tl-step current"; + } + return "tl-step"; + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/components/plant_card.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/plant_card.js new file mode 100644 index 00000000..c92105db --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/plant_card.js @@ -0,0 +1,70 @@ +/** @odoo-module **/ +// ===================================================================== +// FpPlantCard — Variant C card for the plant-view kanban. +// Renders the full job summary + 9-step mini-timeline. Tap opens the +// Job Workspace. +// +// All formatting / class composition happens in JS — per project rule +// 20, OWL templates can't call String(), Number(), etc. as functions. +// ===================================================================== + +import { Component } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { FpMiniTimeline } from "./mini_timeline"; + +const TAG_LABELS = { + rush: "RUSH", + fair: "FAIR", + vip: "VIP", + as9100: "AS9100", +}; + +export class FpPlantCard extends Component { + static template = "fusion_plating_shopfloor.PlantCard"; + static components = { FpMiniTimeline }; + static props = { + card: { type: Object }, + }; + + setup() { + this.action = useService("action"); + } + + get cardClass() { + const c = this.props.card; + const classes = ["o_fp_plant_card", "state-" + (c.card_state || "ready")]; + if (c.is_mine) classes.push("mine"); + if (c.is_overdue) classes.push("overdue"); + return classes.join(" "); + } + + get progressStyle() { + const c = this.props.card; + if (!c.step_total) return "width: 0%"; + const pct = Math.round((c.step_seq / c.step_total) * 100); + return "width: " + pct + "%"; + } + + tagChipClass(tag) { + return "chip tag-" + tag; + } + + tagLabel(tag) { + return TAG_LABELS[tag] || tag.toUpperCase(); + } + + stateChipClass(kind) { + return "chip kind-" + (kind || "ready"); + } + + onCardClick() { + const c = this.props.card; + if (!c.job_id) return; + this.action.doAction({ + type: "ir.actions.client", + tag: "fp_job_workspace", + target: "current", + params: { job_id: c.job_id }, + }); + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/plant_kanban.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/plant_kanban.js new file mode 100644 index 00000000..d2994fc6 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/plant_kanban.js @@ -0,0 +1,162 @@ +/** @odoo-module **/ +// ===================================================================== +// FpPlantKanban — top-level OWL action for the 2026-05-23 redesigned +// Shop Floor. Mounts via the fp_plant_kanban client action; landing +// resolver dispatches between this and the legacy fp_shopfloor_landing +// based on the x_fc_shopfloor_layout config parameter. +// +// Architecture: +// - Polls /fp/landing/plant_kanban every 10s +// - Owns mode + filter + search state (filters persist in localStorage) +// - 9 fixed columns; one card per fp.job +// - Per project rule 20, no String()/Number()/etc. in templates — +// all coercion happens here in JS-land. +// ===================================================================== + +import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { useService } from "@web/core/utils/hooks"; + +import { FpTabletLock } from "./tablet_lock"; +import { FpPlantCard } from "./components/plant_card"; +import { FpColumnHeader } from "./components/column_header"; +import { FpKpiTile } from "./components/kpi_tile"; +import { FpFilterChip } from "./components/filter_chip"; + +const LOCAL_FILTER_KEY = "fp_plant_kanban_filters"; + +export class FpPlantKanban extends Component { + static template = "fusion_plating_shopfloor.PlantKanban"; + static props = ["*"]; + static components = { + FpTabletLock, + FpPlantCard, + FpColumnHeader, + FpKpiTile, + FpFilterChip, + }; + + setup() { + this.notification = useService("notification"); + this.action = useService("action"); + // techStore may not be registered until first PIN unlock; guard with try. + try { + this.techStore = useService("fp_shopfloor_tech_store"); + } catch { + this.techStore = null; + } + + this.state = useState({ + mode: "station", + filters: this._loadFilters(), + data: null, + loading: true, + search: "", + }); + + onMounted(async () => { + await this.refresh(); + this._poll = setInterval(() => this.refresh(), 10000); + }); + onWillUnmount(() => { + if (this._poll) clearInterval(this._poll); + }); + } + + _loadFilters() { + try { + const raw = localStorage.getItem(LOCAL_FILTER_KEY); + return raw ? JSON.parse(raw) : { all: true }; + } catch { + return { all: true }; + } + } + _saveFilters() { + try { + localStorage.setItem(LOCAL_FILTER_KEY, JSON.stringify(this.state.filters)); + } catch { /* localStorage may be disabled */ } + } + + async refresh() { + try { + const res = await rpc("/fp/landing/plant_kanban", { + mode: this.state.mode, + filters: this.state.filters, + }); + if (res && res.ok) { + this.state.data = res; + } else if (res && res.error) { + this.notification.add(res.error, { type: "danger" }); + } + } catch (err) { + this.notification.add(err.message || String(err), { type: "danger" }); + } finally { + this.state.loading = false; + } + } + + toggleFilter(name) { + if (name === "all") { + this.state.filters = { all: true }; + } else { + delete this.state.filters.all; + this.state.filters[name] = !this.state.filters[name]; + const anyActive = Object.keys(this.state.filters) + .some(k => this.state.filters[k]); + if (!anyActive) { + this.state.filters = { all: true }; + } + } + this._saveFilters(); + this.refresh(); + } + + setMode(mode) { + this.state.mode = mode; + this.refresh(); + } + + modeClass(mode) { + return this.state.mode === mode ? "mode-btn active" : "mode-btn"; + } + + onSearchInput(ev) { + this.state.search = (ev.target.value || "").toLowerCase(); + } + + filteredCardIds(column) { + // Client-side search filter on top of the server-side filtered set. + if (!this.state.search) return column.card_ids; + const term = this.state.search; + return column.card_ids.filter(id => { + const c = this.state.data.cards[id]; + if (!c) return false; + return ( + (c.wo_name || "").toLowerCase().includes(term) + || (c.customer || "").toLowerCase().includes(term) + || (c.part_number || "").toLowerCase().includes(term) + || (c.po_number || "").toLowerCase().includes(term) + ); + }); + } + + onHandOff() { + if (this.techStore && this.techStore.lock) { + this.techStore.lock(); + } + } + + onScanQr() { + this.action.doAction({ + type: "ir.actions.client", + tag: "fp_qr_scanner", + target: "new", + }).catch(() => { + // QR scanner action may not be registered in all installs + this.notification.add("QR scanner not available", { type: "warning" }); + }); + } +} + +registry.category("actions").add("fp_plant_kanban", FpPlantKanban); diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/_plant_tokens.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/_plant_tokens.scss new file mode 100644 index 00000000..4f19f42d --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/_plant_tokens.scss @@ -0,0 +1,80 @@ +// ===================================================================== +// Plant-view kanban — design tokens +// MUST load BEFORE the component SCSS files. SCSS @import is forbidden +// in custom Odoo 19 SCSS (project rule 8); the manifest concatenates +// files in registration order, so this file's $vars are visible to +// every later file. +// ===================================================================== + +$o-webclient-color-scheme: bright !default; + +// === Light-mode defaults === +$_plant-bg-hex: #f8f9fa; +$_plant-card-bg-hex: #ffffff; +$_plant-card-border-hex: #d8dadd; +$_plant-text-hex: #1d1f1e; +$_plant-muted-hex: #777; + +$_plant-mine-bg-hex: #fffaeb; +$_plant-mine-border-hex: #f0a500; +$_plant-hold-bg-hex: #fff5f5; +$_plant-hold-border-hex: #dc3545; +$_plant-bake-bg-hex: #fff8e1; +$_plant-bake-border-hex: #ff9800; +$_plant-signoff-bg-hex: #f5f0ff; +$_plant-signoff-border-hex: #6f42c1; +$_plant-idle-bg-hex: #fef9e7; +$_plant-idle-border-hex: #e6a800; +$_plant-qc-bg-hex: #e7f5fc; +$_plant-qc-border-hex: #17a2b8; +$_plant-locked-bg-hex: #f8f9fa; +$_plant-locked-border-hex: #6c757d; +$_plant-noparts-bg-hex: #f5f5f5; +$_plant-noparts-border-hex: #6c757d; +$_plant-done-bg-hex: #f0f9f4; +$_plant-done-border-hex: #28a745; + +// === Dark-mode overrides (compile-time branch per project rule) === +@if $o-webclient-color-scheme == dark { + $_plant-bg-hex: #1a1d21 !global; + $_plant-card-bg-hex: #22262d !global; + $_plant-card-border-hex: #424245 !global; + $_plant-text-hex: #f5f5f7 !global; + $_plant-muted-hex: #adb5bd !global; + + $_plant-mine-bg-hex: #3a2f10 !global; + $_plant-hold-bg-hex: #3a1e1e !global; + $_plant-bake-bg-hex: #3a2f10 !global; + $_plant-signoff-bg-hex: #1f1730 !global; + $_plant-idle-bg-hex: #2d2818 !global; + $_plant-qc-bg-hex: #14252e !global; + $_plant-locked-bg-hex: #2d3138 !global; + $_plant-noparts-bg-hex: #2d3138 !global; + $_plant-done-bg-hex: #14281a !global; +} + +// === CSS-custom-property wrappers so future themes can override === +$plant-bg: var(--fp-plant-bg, $_plant-bg-hex); +$plant-card-bg: var(--fp-plant-card-bg, $_plant-card-bg-hex); +$plant-card-border: var(--fp-plant-card-border, $_plant-card-border-hex); +$plant-text: var(--fp-plant-text, $_plant-text-hex); +$plant-muted: var(--fp-plant-muted, $_plant-muted-hex); + +$plant-mine-bg: var(--fp-plant-mine-bg, $_plant-mine-bg-hex); +$plant-mine-border: var(--fp-plant-mine-border, $_plant-mine-border-hex); +$plant-hold-bg: var(--fp-plant-hold-bg, $_plant-hold-bg-hex); +$plant-hold-border: var(--fp-plant-hold-border, $_plant-hold-border-hex); +$plant-bake-bg: var(--fp-plant-bake-bg, $_plant-bake-bg-hex); +$plant-bake-border: var(--fp-plant-bake-border, $_plant-bake-border-hex); +$plant-signoff-bg: var(--fp-plant-signoff-bg, $_plant-signoff-bg-hex); +$plant-signoff-border: var(--fp-plant-signoff-border, $_plant-signoff-border-hex); +$plant-idle-bg: var(--fp-plant-idle-bg, $_plant-idle-bg-hex); +$plant-idle-border: var(--fp-plant-idle-border, $_plant-idle-border-hex); +$plant-qc-bg: var(--fp-plant-qc-bg, $_plant-qc-bg-hex); +$plant-qc-border: var(--fp-plant-qc-border, $_plant-qc-border-hex); +$plant-locked-bg: var(--fp-plant-locked-bg, $_plant-locked-bg-hex); +$plant-locked-border: var(--fp-plant-locked-border, $_plant-locked-border-hex); +$plant-noparts-bg: var(--fp-plant-noparts-bg, $_plant-noparts-bg-hex); +$plant-noparts-border: var(--fp-plant-noparts-border, $_plant-noparts-border-hex); +$plant-done-bg: var(--fp-plant-done-bg, $_plant-done-bg-hex); +$plant-done-border: var(--fp-plant-done-border, $_plant-done-border-hex); diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_column_header.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_column_header.scss new file mode 100644 index 00000000..d082b550 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_column_header.scss @@ -0,0 +1,45 @@ +// _column_header.scss — depends on _plant_tokens.scss + +.o_fp_col_header { + padding: 6px 8px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + background: $plant-card-bg; + border: 1px solid $plant-card-border; + border-radius: 6px 6px 0 0; + border-bottom: 0; + + &.mine { + background: linear-gradient(180deg, $plant-mine-bg 0%, $plant-card-bg 100%); + border-color: $plant-mine-border; + } + + .col-meta { display: flex; flex-direction: column; gap: 1px; min-width: 0; } + .mine-badge { + font-size: 9px; font-weight: 700; + color: $plant-mine-border; + text-transform: uppercase; letter-spacing: 0.04em; + white-space: nowrap; + } + .col-name { + font-size: 12px; font-weight: 700; + color: $plant-text; + text-transform: uppercase; letter-spacing: 0.02em; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + } + .col-count { + font-size: 13px; font-weight: 700; + color: $plant-muted; + background: $plant-card-bg; + padding: 0 6px; + border-radius: 10px; + border: 1px solid $plant-card-border; + min-width: 22px; text-align: center; + } + &.mine .col-count { + border-color: $plant-mine-border; + color: $plant-mine-border; + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_filter_chip.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_filter_chip.scss new file mode 100644 index 00000000..b0f73aa7 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_filter_chip.scss @@ -0,0 +1,20 @@ +// _filter_chip.scss — depends on _plant_tokens.scss + +.o_fp_filter_chip { + padding: 4px 12px; + font-size: 11px; + background: $plant-card-bg; + border: 1px solid $plant-card-border; + border-radius: 14px; + color: $plant-muted; + cursor: pointer; + font-family: inherit; + + &.active { + background: #1d4ed8; + border-color: #1d4ed8; + color: #fff; + font-weight: 600; + } + &:hover:not(.active) { background: $plant-bg; } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_kpi_tile.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_kpi_tile.scss new file mode 100644 index 00000000..275d9d67 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_kpi_tile.scss @@ -0,0 +1,34 @@ +// _kpi_tile.scss — depends on _plant_tokens.scss + +.o_fp_kpi_tile { + padding: 6px 10px; + background: $plant-card-bg; + border-radius: 6px; + border: 1px solid $plant-card-border; + display: flex; flex-direction: column; gap: 1px; + cursor: pointer; + transition: background 0.1s; + text-align: left; + color: $plant-text; + font-family: inherit; + + &:hover { background: $plant-bg; } + &.active { + border-color: $plant-mine-border; + background: $plant-mine-bg; + } + &.urgent .kpi-val { color: $plant-hold-border; } + &.warn .kpi-val { color: $plant-idle-border; } + &.good .kpi-val { color: $plant-done-border; } + + .kpi-val { + font-size: 20px; font-weight: 700; + color: $plant-text; line-height: 1; + font-variant-numeric: tabular-nums; + } + .kpi-lbl { + font-size: 9px; font-weight: 600; + color: $plant-muted; + text-transform: uppercase; letter-spacing: 0.04em; + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_mini_timeline.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_mini_timeline.scss new file mode 100644 index 00000000..d6cca0a4 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_mini_timeline.scss @@ -0,0 +1,56 @@ +// _mini_timeline.scss — depends on _plant_tokens.scss + +.o_fp_mini_timeline { + display: flex; + flex-direction: column; + gap: 2px; + + .tl-row { + display: flex; + gap: 2px; + padding: 2px 0; + + .tl-step { + flex: 1; + height: 8px; + background: #e5e7eb; + border-radius: 1.5px; + cursor: help; + + &.done { background: #28a745; } + &.current { + background: #f0a500; + height: 11px; + margin-top: -1.5px; + box-shadow: 0 0 0 1px rgba(240, 165, 0, 0.25); + &.hold { background: $plant-hold-border; } + &.locked { background: $plant-locked-border; } + &.bake { background: $plant-bake-border; } + &.signoff { background: $plant-signoff-border; } + &.idle { background: $plant-idle-border; } + &.qc { background: $plant-qc-border; } + &.noparts { background: $plant-noparts-border; } + &.done { background: $plant-done-border; } + &.paperwork { background: $plant-signoff-border; } + } + } + } + + .tl-labels { + display: flex; + gap: 2px; + font-size: 8px; + color: $plant-muted; + text-transform: uppercase; + letter-spacing: 0.03em; + span { + flex: 1; + text-align: center; + &.current { color: $plant-mine-border; font-weight: 700; } + } + } +} + +@if $o-webclient-color-scheme == dark { + .o_fp_mini_timeline .tl-row .tl-step { background: #2d3138; } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_plant_card.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_plant_card.scss new file mode 100644 index 00000000..ef6f5676 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_plant_card.scss @@ -0,0 +1,138 @@ +// _plant_card.scss — depends on _plant_tokens.scss + +.o_fp_plant_card { + background: $plant-card-bg; + border: 1px solid $plant-card-border; + border-radius: 8px; + padding: 8px 10px; + display: flex; + flex-direction: column; + gap: 4px; + cursor: pointer; + transition: transform 0.1s, box-shadow 0.1s; + box-shadow: 0 1px 2px rgba(0,0,0,0.04); + width: 100%; + box-sizing: border-box; + font-size: 11px; + line-height: 1.3; + color: $plant-text; + + &:hover { transform: translateY(-1px); box-shadow: 0 3px 8px rgba(0,0,0,0.12); } + + // === Card state chrome === + &.mine, + &.state-ready_mine, + &.state-running_mine { + background: $plant-mine-bg; + border-left: 4px solid $plant-mine-border; + padding-left: 7px; + } + &.state-on_hold { + background: $plant-hold-bg; + border-left: 4px solid $plant-hold-border; + padding-left: 7px; + } + &.state-bake_due { + background: $plant-bake-bg; + border-left: 4px solid $plant-bake-border; + padding-left: 7px; + } + &.state-awaiting_signoff { + background: $plant-signoff-bg; + border-left: 4px solid $plant-signoff-border; + padding-left: 7px; + } + &.state-idle_warning { + background: $plant-idle-bg; + border-left: 4px solid $plant-idle-border; + padding-left: 7px; + } + &.state-awaiting_qc { + background: $plant-qc-bg; + border-left: 4px solid $plant-qc-border; + padding-left: 7px; + } + &.state-predecessor_locked { + background: $plant-locked-bg; + } + &.state-no_parts { + background: $plant-noparts-bg; + border: 1px dashed #999; + border-left: 4px solid $plant-noparts-border; + padding-left: 7px; + } + &.state-done { + background: $plant-done-bg; + border-left: 4px solid $plant-done-border; + padding-left: 7px; + } + &.overdue:not(.mine):not(.state-on_hold):not(.state-bake_due) { + border-left: 4px solid $plant-hold-border; + padding-left: 7px; + } + + // === Sub-elements === + .card-top { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; } + .card-wo { font-size: 13px; font-weight: 700; color: $plant-text; } + .card-due { font-size: 10px; color: $plant-muted; white-space: nowrap; } + .card-due.overdue { color: $plant-hold-border; font-weight: 700; } + .card-sub { font-size: 10px; color: $plant-muted; line-height: 1.3; } + .card-sub-em { color: $plant-text; font-weight: 600; } + .card-meta { font-size: 10px; color: $plant-muted; } + .card-step { font-size: 12px; font-weight: 600; color: $plant-text; margin-top: 2px; } + .card-chips { display: flex; flex-wrap: wrap; gap: 3px; } + + .chip { + font-size: 10px; + padding: 1px 6px; + border-radius: 10px; + background: #f1f3f5; + color: #4e4e4e; + border: 1px solid #e5e7eb; + display: inline-flex; + align-items: center; + gap: 3px; + &.tank { background: #e7f1ff; color: #0d4a8c; border-color: #cfe2ff; } + &.kind-ready { background: #d1ecf1; color: #0c5460; border-color: #bee5eb; font-weight: 600; } + &.kind-running{ background: #fff3cd; color: #856404; border-color: #ffeeba; font-weight: 600; } + &.kind-hold { background: #f8d7da; color: #721c24; border-color: #f5c6cb; font-weight: 700; } + &.kind-locked { background: #e2e3e5; color: #383d41; border-color: #d6d8db; font-weight: 600; } + &.kind-due { background: #ffe9c6; color: #8a4a00; border-color: #ffd28a; font-weight: 700; } + &.kind-signoff{ background: #e8d9ff; color: #4a2db0; border-color: #d4c5ff; font-weight: 700; } + &.kind-idle { background: #fff3cd; color: #856404; border-color: #ffeeba; font-weight: 700; } + &.kind-qc { background: #c4e9f3; color: #0c5460; border-color: #a8dde9; font-weight: 700; } + &.kind-no_parts { background: #e2e3e5; color: #383d41; border-color: #d6d8db; font-weight: 700; } + &.kind-done { background: #d4edda; color: #155724; border-color: #c3e6cb; font-weight: 700; } + &.kind-paperwork { background: #e8e0ff; color: #4a2db0; border-color: #d4c5ff; font-weight: 600; } + &.tag-rush { background: #ffe5e5; color: #b00; border-color: #ffcfcf; font-weight: 700; font-size: 9px; } + &.tag-fair { background: #fff0d9; color: #8a4a00; border-color: #ffe0b3; font-weight: 700; font-size: 9px; } + &.tag-vip { background: #e8e0ff; color: #4a2db0; border-color: #d4c5ff; font-weight: 700; font-size: 9px; } + &.tag-as9100 { background: #d4edda; color: #155724; border-color: #c3e6cb; font-weight: 700; font-size: 9px; } + } + + .card-bottom { + display: flex; align-items: center; justify-content: space-between; + gap: 6px; padding-top: 4px; margin-top: 2px; + border-top: 1px solid #f1f3f5; + font-size: 9px; color: $plant-muted; + } + .progress { display: flex; align-items: center; gap: 4px; flex: 1; } + .progress-bar { flex: 1; max-width: 60px; height: 3px; background: #e5e7eb; border-radius: 1.5px; overflow: hidden; } + .progress-fill { height: 100%; background: $plant-mine-border; border-radius: 1.5px; } + .operator-pill { + display: inline-flex; align-items: center; + background: #f1f3f5; border-radius: 8px; + padding: 0 4px 0 1px; font-size: 9px; border: 1px solid #e5e7eb; + } + .operator-avatar { + width: 12px; height: 12px; border-radius: 50%; + background: #4caf50; color: #fff; + font-size: 7px; font-weight: 700; + display: inline-flex; align-items: center; justify-content: center; + } + .icon-row { display: flex; gap: 3px; font-size: 10px; } +} + +@if $o-webclient-color-scheme == dark { + .o_fp_plant_card .card-bottom { border-top-color: #424245; } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/plant_kanban.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/plant_kanban.scss new file mode 100644 index 00000000..63da69fb --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/plant_kanban.scss @@ -0,0 +1,143 @@ +// plant_kanban.scss — depends on _plant_tokens.scss and the component partials + +.o_fp_plant_kanban { + padding: 8px; + background: $plant-bg; + min-height: 100vh; + color: $plant-text; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + + .floor-header { + background: $plant-card-bg; + border: 1px solid $plant-card-border; + border-radius: 8px; + padding: 8px 12px; + margin-bottom: 8px; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); + position: sticky; + top: 0; + z-index: 10; + display: flex; + flex-direction: column; + gap: 8px; + } + + .floor-header-top { + display: flex; justify-content: space-between; gap: 12px; + align-items: center; flex-wrap: wrap; + } + .floor-title { font-size: 16px; font-weight: 700; } + .floor-controls { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; } + + .station-picker { + padding: 5px 10px; + background: $plant-mine-bg; + border: 1px solid $plant-mine-border; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + color: #856404; + cursor: pointer; + font-family: inherit; + } + .mode-toggle { + display: inline-flex; + border: 1px solid $plant-card-border; + border-radius: 6px; + overflow: hidden; + .mode-btn { + padding: 5px 12px; + font-size: 12px; + font-weight: 600; + background: $plant-card-bg; + color: $plant-muted; + border: 0; + cursor: pointer; + border-right: 1px solid $plant-card-border; + font-family: inherit; + &:last-child { border-right: 0; } + &.active { background: #1d4ed8; color: #fff; } + &:hover:not(.active) { background: $plant-bg; } + } + } + .toolbar-btn { + padding: 5px 10px; + font-size: 12px; + background: $plant-card-bg; + border: 1px solid $plant-card-border; + border-radius: 6px; + cursor: pointer; + color: $plant-text; + font-family: inherit; + &:hover { background: $plant-bg; } + &.handoff { + background: #ffc107; + border-color: #d39e00; + color: #856404; + font-weight: 700; + } + } + + .kpi-strip { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; } + + .search-row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; } + .search-input { + flex: 1; min-width: 200px; + padding: 5px 10px; + border: 1px solid $plant-card-border; + border-radius: 6px; + background: $plant-card-bg; + color: $plant-text; + font-size: 12px; + font-family: inherit; + } + + .board { + display: grid; + grid-template-columns: repeat(9, 1fr); + gap: 4px; + min-height: 520px; + } + .col { + background: $plant-bg; + border-radius: 8px; + padding: 4px; + display: flex; + flex-direction: column; + gap: 4px; + min-width: 110px; + &.mine { + background: linear-gradient(180deg, $plant-mine-bg 0%, $plant-card-bg 100%); + border: 1px solid $plant-mine-border; + } + } + .col-scroll { + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 4px; + padding: 2px; + max-height: calc(100vh - 280px); + min-height: 100px; + } + .col-empty { + font-size: 10px; + color: $plant-muted; + font-style: italic; + padding: 14px 4px; + text-align: center; + } + + .loading { + padding: 40px; + text-align: center; + font-size: 14px; + color: $plant-muted; + } +} + +@if $o-webclient-color-scheme == dark { + .o_fp_plant_kanban .toolbar-btn.handoff { + color: #856404; // keep gold legible on dark + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/column_header.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/column_header.xml new file mode 100644 index 00000000..bebc594e --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/column_header.xml @@ -0,0 +1,14 @@ + + + + +
+
+
📍 You're here
+
+
+ +
+ + + diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/filter_chip.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/filter_chip.xml new file mode 100644 index 00000000..ee23dff7 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/filter_chip.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/mini_timeline.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/mini_timeline.xml new file mode 100644 index 00000000..8331fc0a --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/mini_timeline.xml @@ -0,0 +1,20 @@ + + + + +
+
+ + + +
+
+ + + +
+
+
+ +
diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/plant_card.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/plant_card.xml new file mode 100644 index 00000000..5097b5cb --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/plant_card.xml @@ -0,0 +1,80 @@ + + + + +
+ +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + PN + + Rev + · + + Qty + · PO +
+ + +
+ + · + +
+ + +
+ + + +
+ + +
+ + +
+ + +
+ + + + + +
+
+ / +
+
+
+
+
+ +
+
+ + + +
+
+
+ + + diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/plant_kanban.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/plant_kanban.xml new file mode 100644 index 00000000..953e7fc2 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/plant_kanban.xml @@ -0,0 +1,107 @@ + + + + + + +
+ + +
+
+
🏭 Shop Floor
+
+ +
+ + + +
+ + +
+
+ + +
+ + + + + +
+ + +
+ + + + + + + +
+
+ + +
+ +
+ +
+ + + +
+
+
+
+
+ +
+ Loading… +
+
+
+
+
+ +