chore(plating): de-dash shipped code + intake-neutral customer emails

Replace em-dashes and en-dashes with hyphens across 789 shipped source
files (py/xml/js/scss) so the delivered module reads as human-written;
em-dashes had become a recognizable AI-generated tell. Internal .md dev
notes are excluded. The WO-sticker mojibake strippers keep their dash
search targets (now written — / –). No logic changes: comments
and display strings only; validated with py_compile + lxml parse.

Rewrite the 7 customer notification emails to be intake-neutral
(ship-in / drop-off / pickup) and repair-aware, and fix the Shipped
email documents line (packing slip vs bill of lading; certificate only
when issued). Subjects use a hyphen separator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-05 00:16:19 -04:00
parent c9eb61ee0c
commit 8c76a16366
789 changed files with 4692 additions and 4692 deletions

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating GateViz (shared OWL service)
// Fusion Plating - GateViz (shared OWL service)
//
// "Can't start because…" explainer for fp.job.step blockers. Drives off
// step.blocker_kind/reason from the backend compute. Used in:
@@ -8,14 +8,14 @@
// • Manager Plant Board "Needs Worker" cards (badge form)
//
// Props:
// canStart : Boolean when true, renders nothing
// blockerKind : String predecessor/contract_review/
// canStart : Boolean - when true, renders nothing
// blockerKind : String - predecessor/contract_review/
// parts_not_received/racking_required/
// manager_input/other
// blockerReason : String human-readable explanation
// jumpTargetModel : String optional model name for tap-to-jump
// jumpTargetId : Number optional record id for tap-to-jump
// onJump : Function called with {model, id} on Jump click
// blockerReason : String - human-readable explanation
// jumpTargetModel : String - optional model name for tap-to-jump
// jumpTargetId : Number - optional record id for tap-to-jump
// onJump : Function - called with {model, id} on Jump click
// =============================================================================
import { Component } from "@odoo/owl";

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating HoldComposer (shared OWL service)
// Fusion Plating - HoldComposer (shared OWL service)
//
// Modal form to create a fusion.plating.quality.hold with reason picker,
// qty split, optional photo, description, and mark-for-scrap toggle.
@@ -59,7 +59,7 @@ export class FpHoldComposer extends Component {
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
// Strip "data:...;base64," prefix backend expects raw base64
// Strip "data:...;base64," prefix - backend expects raw base64
const dataUri = e.target.result;
const base64 = dataUri.split(",", 2)[1] || "";
this.state.photoDataUri = base64;

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating FpIdleWarning (shared OWL service)
// Fusion Plating - FpIdleWarning (shared OWL service)
//
// Yellow-border overlay + countdown toast shown during the last
// (default 30) seconds before auto-lock. Any pointer/touch event on

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating KanbanCard (shared OWL service)
// Fusion Plating - KanbanCard (shared OWL service)
//
// Standard WO/step card used on:
// • Shop Floor Landing kanban (station + all-plant modes)
@@ -18,7 +18,7 @@
// showWorkflowChip : Boolean
// showWorkcenter : Boolean
// showAssignedTo : Boolean
// onTap : Function(data) called on card click
// onTap : Function(data) - called on card click
// =============================================================================
import { Component } from "@odoo/owl";

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =====================================================================
// FpMiniTimeline 9-step horizontal bar showing recipe journey.
// 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.

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating FpPinPad (shared OWL service)
// Fusion Plating - FpPinPad (shared OWL service)
//
// Numeric 4-digit PIN pad. Auto-submits on the 4th digit via onSubmit
// callback. Used by FpTabletLock unlock flow AND FpPinSetup change flow.

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating FpPinSetup (client action `fp_tablet_pin_setup`)
// Fusion Plating - FpPinSetup (client action `fp_tablet_pin_setup`)
//
// Modal flow for setting OR changing the user's tablet PIN. Triggered
// from res.users preferences via action_open_tablet_pin_setup. Three

View File

@@ -1,10 +1,10 @@
/** @odoo-module **/
// =====================================================================
// FpPlantCard Variant C card for the plant-view kanban.
// 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
// All formatting / class composition happens in JS - per project rule
// 20, OWL templates can't call String(), Number(), etc. as functions.
// =====================================================================
@@ -61,7 +61,7 @@ export class FpPlantCard extends Component {
const c = this.props.card;
if (!c.job_id) return;
// Open the workspace focused on THIS stage's step (partial-order
// handling) tapping the Baking card lands on the Baking step,
// handling) - tapping the Baking card lands on the Baking step,
// not the job's global active step. The workspace already accepts
// focus_step_id (see the FP-STEP scan path in plant_kanban.js).
this.action.doAction({

View File

@@ -1,5 +1,5 @@
/** @odoo-module **/
// Racking panel split a WO's parts across multiple racks (Phase 1).
// Racking panel - split a WO's parts across multiple racks (Phase 1).
// Lives on the Job Workspace, shown when the WO is at the Racking step.
import { Component, useState, onWillStart } from "@odoo/owl";
import { rpc } from "@web/core/network/rpc";

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating SignatureConfirm
// Fusion Plating - SignatureConfirm
//
// Confirm dialog shown when the operator already has a saved Plating
// Signature: previews it + "Sign & Finish" (props.onConfirm) or "Use a

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating SignaturePad (shared OWL service)
// Fusion Plating - SignaturePad (shared OWL service)
//
// Modal canvas signature capture. Returns dataURI via onSubmit; the caller
// commits it (e.g. /fp/workspace/sign_off). Mounted via the dialog service:

View File

@@ -1,13 +1,13 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating WorkflowChip (shared OWL service)
// Fusion Plating - WorkflowChip (shared OWL service)
//
// Renders an fp.job.workflow.state as a colored pill + optional next-action
// hint. Used by KanbanCard, JobWorkspace header, Manager Funnel.
//
// Props:
// state : { id, name, color } required
// nextActionLabel : string optional
// state : { id, name, color } - required
// nextActionLabel : string - optional
//
// Color map mirrors the fp.job.workflow.state.color Selection
// (grey/blue/cyan/yellow/orange/green/success/danger/purple).

View File

@@ -1,12 +1,12 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating FpDamageDialog
// Fusion Plating - FpDamageDialog
//
// Tablet-friendly modal for logging damage during receiving. Captures:
// - Severity (Cosmetic / Functional / Rejected) pill picker
// - Severity (Cosmetic / Functional / Rejected) - pill picker
// - Description (required textarea)
// - Action Required (None / Notify / Return / Proceed) pill picker
// - Photos both camera capture (capture="environment") AND file picker
// - Action Required (None / Notify / Return / Proceed) - pill picker
// - Photos - both camera capture (capture="environment") AND file picker
//
// Wired from FpJobWorkspace via onAddDamage. POSTs to
// /fp/workspace/damage_create on Save; caller refreshes after onCreated().
@@ -86,7 +86,7 @@ export class FpDamageDialog extends Component {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// strip the "data:image/jpeg;base64," prefix backend wants raw base64
// strip the "data:image/jpeg;base64," prefix - backend wants raw base64
const dataUrl = reader.result || "";
const idx = String(dataUrl).indexOf(",");
resolve(idx >= 0 ? dataUrl.slice(idx + 1) : dataUrl);

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating FpFinishBlockDialog
// Fusion Plating - FpFinishBlockDialog
//
// Shown when /fp/workspace/finish_step returns ok=false with gate='required_inputs'.
// Non-managers see: Cancel + Record Inputs.
@@ -22,7 +22,7 @@ export class FpFinishBlockDialog extends Component {
close: Function,
stepName: String,
// Server-classified gate. 'required_inputs' is the only one the
// current Record/Bypass UI handles other gates fall back to
// current Record/Bypass UI handles - other gates fall back to
// showing the message + a Cancel button only.
gate: String,
message: String,

View File

@@ -1,18 +1,18 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating Job Workspace (full-screen WO surface)
// Fusion Plating - Job Workspace (full-screen WO surface)
// Client action: fp_job_workspace
//
// Opens from: kanban tap (Landing Phase 3), smart button (fp.job form),
// Opens from: kanban tap (Landing - Phase 3), smart button (fp.job form),
// QR scan (FP-JOB/FP-STEP), manager dashboard card tap (Phase 4).
//
// Layout (top-to-bottom):
// sticky header WO #, customer, part, qty/done, deadline, holds
// sticky workflow bar 9-stage milestone dots + Next-action button
// sticky header - WO #, customer, part, qty/done, deadline, holds
// sticky workflow bar - 9-stage milestone dots + Next-action button
// scrollable main:
// left/center step list with active expansion + GateViz
// right side panel spec PDF link + attachments + chatter
// sticky action rail Hold · Note · Milestone advance · Issue Cert
// left/center - step list with active expansion + GateViz
// right side panel - spec PDF link + attachments + chatter
// sticky action rail - Hold · Note · Milestone advance · Issue Cert
//
// Auto-refresh: every 15s.
// =============================================================================
@@ -53,7 +53,7 @@ export class FpJobWorkspace extends Component {
data: null,
jobId: null,
focusStepId: null,
// Reactive monotonic tick bumped every 1s by _tickInterval so
// Reactive monotonic tick - bumped every 1s by _tickInterval so
// the active-step timer re-renders without an RPC. The template
// reads tickNow and re-runs formatActiveStepElapsed each second.
tickNow: Date.now(),
@@ -68,7 +68,7 @@ export class FpJobWorkspace extends Component {
this.state.focusStepId = params.focus_step_id || null;
// After Hand Off + PIN relogin the action remounts without
// params (action params aren't URL-encoded), so jobId is
// null and refresh() exits early workspace was stuck on
// null and refresh() exits early - workspace was stuck on
// "Loading Job Workspace…" indefinitely. Fall back to the
// plant kanban so the operator lands somewhere usable.
if (!this.state.jobId) {
@@ -81,7 +81,7 @@ export class FpJobWorkspace extends Component {
}
await this.refresh();
// If load failed (job no longer accessible, server error, etc.)
// also redirect leaving the user on a perma-loading screen
// also redirect - leaving the user on a perma-loading screen
// with no recovery path is worse than dropping them at the
// kanban where they can re-enter.
if (!this.state.data) {
@@ -93,7 +93,7 @@ export class FpJobWorkspace extends Component {
return;
}
this._refreshInterval = setInterval(() => this.refresh(), 15000);
// 1s tick pure client-side; no RPC. Drives the live timer
// 1s tick - pure client-side; no RPC. Drives the live timer
// on the active step's badge area.
this._tickInterval = setInterval(() => {
this.state.tickNow = Date.now();
@@ -113,10 +113,10 @@ export class FpJobWorkspace extends Component {
formatActiveStepElapsed(step) {
if (!step || !step.date_started_iso) return "";
// Controller now sends fp_isoformat_utc a proper ISO-8601 with
// Controller now sends fp_isoformat_utc - a proper ISO-8601 with
// explicit +00:00 offset. Parse directly. (Previously appended
// "Z" to a fp_format string, which had been converted to user's
// local tz, so the timer was offset by the tz delta 4h on EDT.)
// local tz, so the timer was offset by the tz delta - 4h on EDT.)
const startedMs = Date.parse(step.date_started_iso);
if (!startedMs || isNaN(startedMs)) return "";
// touch state.tickNow so OWL re-evaluates this getter every tick
@@ -151,7 +151,7 @@ export class FpJobWorkspace extends Component {
// HTML-ESCAPES plain JS strings unless they're tagged with
// markup() from @odoo/owl. Without this wrap the operator
// sees literal `<p>` and `<b>` tags instead of formatted
// text (caught 2026-05-23 Notes panel showing raw HTML).
// text (caught 2026-05-23 - Notes panel showing raw HTML).
if (res.chatter && res.chatter.length) {
for (const m of res.chatter) {
if (m && typeof m.body === "string") {
@@ -172,7 +172,7 @@ export class FpJobWorkspace extends Component {
onBack() {
// target: "main" CLEARS the breadcrumb stack (Odoo 19:
// action.target === "main" => clearBreadcrumbs in action_service.js).
// target: "current" was APPENDING each kanban<->workspace switch
// target: "current" was APPENDING - each kanban<->workspace switch
// grew the /odoo/... URL, and lock/unlock window.location.reload()
// preserved it, so the address bar ballooned. "main" keeps the URL a
// single action. The plant kanban is the sole Shop Floor surface.
@@ -250,7 +250,7 @@ export class FpJobWorkspace extends Component {
if (step.override_excluded) return [];
const actions = [];
// Partial-order handling "Send to next →" advances parts parked
// Partial-order handling - "Send to next →" advances parts parked
// at this step to the next stage. Only shown when parts are here
// AND a next stage exists. The destination name is on the button
// so there's nothing to guess; qty defaults to all parked here.
@@ -290,7 +290,7 @@ export class FpJobWorkspace extends Component {
});
return actions;
}
// state in ('pending', 'ready') entry-point per kind.
// state in ('pending', 'ready') - entry-point per kind.
if (step.kind === "contract_review") {
actions.push({ key: "open_contract_review", label: "Open QA-005 Form",
icon: "fa fa-file-text-o", cssClass: "btn btn-primary" });
@@ -306,7 +306,7 @@ export class FpJobWorkspace extends Component {
icon: "fa fa-server", cssClass: "btn btn-primary" });
return actions;
}
// Default plain Start
// Default - plain Start
actions.push({ key: "start", label: "Start",
icon: "fa fa-play", cssClass: "btn btn-primary" });
return actions;
@@ -374,13 +374,13 @@ export class FpJobWorkspace extends Component {
onRedraw: () => this._openSignaturePad(step), // draw a new one
});
} else {
// First time draw once; the backend persists it to the
// First time - draw once; the backend persists it to the
// user's Plating Signature so later sign-offs are one-tap.
this._openSignaturePad(step);
}
return;
}
// Plain finish route through /fp/workspace/finish_step which
// Plain finish - route through /fp/workspace/finish_step which
// returns structured errors so we can show the FpFinishBlockDialog
// for required-inputs failures (with manager bypass option).
await this._callFinishStep(step, /* bypass */ false);
@@ -427,7 +427,7 @@ export class FpJobWorkspace extends Component {
await this.refresh();
return;
}
// Structured-error branch show block dialog for the
// Structured-error branch - show block dialog for the
// required_inputs gate (it has the rich Record / Bypass UX).
// Other gates fall back to a plain notification.
if (res && res.gate === "required_inputs") {
@@ -525,11 +525,11 @@ export class FpJobWorkspace extends Component {
}
// ---- Partial-order advance (2026-06-02) -------------------------------
// "Send to next →" moves parts parked at this step to the next stage.
// "Send to next →" - moves parts parked at this step to the next stage.
// The destination auto-readies server-side (move_controller), so the
// receiving operator sees a Ready card immediately; the source
// auto-finishes when it drains to zero. Pure client-side next-step
// resolution off the loaded step list no extra RPC.
// resolution off the loaded step list - no extra RPC.
nextStepFor(step) {
// The next stage parts flow into: lowest-sequence non-terminal step
@@ -547,7 +547,7 @@ export class FpJobWorkspace extends Component {
const nxt = this.nextStepFor(step);
if (!nxt) {
this.notification.add(
"This is the last stage parts finish here and close out at job completion.",
"This is the last stage - parts finish here and close out at job completion.",
{ type: "warning" },
);
return;
@@ -632,7 +632,7 @@ export class FpJobWorkspace extends Component {
}
async onReceivingClose(rcv) {
// No confirmation Mark Counted is already a deliberate prior
// No confirmation - Mark Counted is already a deliberate prior
// step, and the native browser confirm() popup looks out of place
// on the tablet UI. If a receiver hits Close prematurely, an
// admin can reset via fp.receiving.action_reset_to_counted from
@@ -694,7 +694,7 @@ export class FpJobWorkspace extends Component {
const text = window.prompt("Add a note to this WO:");
if (!text) return;
try {
// ORM call for message_post keeps chatter behaviour identical
// ORM call for message_post - keeps chatter behaviour identical
// to back-office posts (handles HTML escaping, subscribers, etc.)
await rpc("/web/dataset/call_kw", {
model: "fp.job",
@@ -726,7 +726,7 @@ export class FpJobWorkspace extends Component {
}
// ---- Shipping handlers (tablet receiving+shipping 2026-05-29) ----------
// All coercion is JS-side (CLAUDE.md Rule 20 templates only expose Math).
// All coercion is JS-side (CLAUDE.md Rule 20 - templates only expose Math).
onShipInput(field, ev) {
const raw = ev.target.value;
this.state.shipForm[field] =
@@ -757,7 +757,7 @@ export class FpJobWorkspace extends Component {
});
if (res && res.ok) {
this.notification.add(
"Label generated tracking " + (res.tracking_number || "n/a"),
"Label generated - tracking " + (res.tracking_number || "n/a"),
{ type: "success" },
);
await this.refresh();

View File

@@ -1,13 +1,13 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating Manager Desk (OWL client action)
// Fusion Plating - Manager Desk (OWL client action)
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
// Manager-level view: assign workers, swap tanks, cover no-shows, drill
// into detail when needed. Three columns: Needs a Worker / In Progress / Team.
//
// Native fp.job / fp.job.step edition. Speaks job/step end-to-end
// Native fp.job / fp.job.step edition. Speaks job/step end-to-end -
// payload keys, variables, and RPC kwargs all use the job/step
// vocabulary.
// =============================================================================
@@ -46,7 +46,7 @@ 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.
// Phase 4 tablet redesign - 4 sibling tabs.
// funnel | inbox | plant_board | at_risk
activeTab: "funnel",
funnel: null, // /fp/manager/funnel payload
@@ -103,7 +103,7 @@ export class ManagerDashboard extends Component {
}
this.state.lastUpdated = Date.now();
} catch (err) {
// Network / auth hiccup surface it so the UI isn't a
// Network / auth hiccup - surface it so the UI isn't a
// permanent spinner.
this.state.loadError = `Couldn't reach the server: ${err.message || err}`;
} finally {
@@ -123,7 +123,7 @@ export class ManagerDashboard extends Component {
}
// Dict slot
if (source.kpis) target.kpis = source.kpis;
// Arrays replace whole so OWL's list diffing handles it cleanly
// Arrays - replace whole so OWL's list diffing handles it cleanly
for (const k of ["unassigned", "active", "team", "operators", "tanks"]) {
if (Array.isArray(source[k])) target[k] = source[k];
}
@@ -168,10 +168,10 @@ export class ManagerDashboard extends Component {
* Sort + filter the operator list for a specific step's dropdown.
*
* Buckets, top-down, each kept in original (alphabetical) order:
* 1. Qualified for this role AND clocked in primary picks
* 2. Lead hands for this role AND clocked in coverage picks
* 3. Clocked in but NOT qualified training mode
* 4. Off-shift greyed; only
* 1. Qualified for this role AND clocked in - primary picks
* 2. Lead hands for this role AND clocked in - coverage picks
* 3. Clocked in but NOT qualified - training mode
* 4. Off-shift - greyed; only
* shown when hideOffShift is false
*
* Each option carries a `bucket` so the template can render a tiny
@@ -199,7 +199,7 @@ export class ManagerDashboard extends Component {
/** Label that goes next to each option (after the name). */
operatorBadge(op) {
if (op.bucket === 1) return ""; // primary no extra noise
if (op.bucket === 1) return ""; // primary - no extra noise
if (op.bucket === 2) return " · lead hand";
if (op.bucket === 3) return " · training";
return " · off-shift";
@@ -288,7 +288,7 @@ export class ManagerDashboard extends Component {
return ({'0': 'muted', '1': 'warning', '2': 'danger'})[p] || 'muted';
}
// Open a list view of any model with an optional domain used by
// Open a list view of any model with an optional domain - used by
// the new compliance KPI tiles (Missed Bakes / Open Holds / Stale
// Steps / Locked / Pending QC / Draft Certs) so the manager can
// drill in with one tap. v19.0.24.3.0.
@@ -305,13 +305,13 @@ export class ManagerDashboard extends Component {
}
// ==================================================================
// Phase 4 tablet redesign 4 sibling tabs
// 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
// Load the tab's data on first switch - subsequent ticks refresh
// via the auto-poll.
await this.refreshActiveTab();
}

View File

@@ -1,10 +1,10 @@
/** @odoo-module */
/*
* Sub 12b Move Parts dialog (OWL).
* Sub 12b - Move Parts dialog (OWL).
*
* Mirror of Steelhead screens 1-3, 14-15. Loads preview on mount,
* re-checks hard-blockers on commit. MOVE (n) button disabled when
* hard-blocked OR required prompt blank improvement over Steelhead's
* hard-blocked OR required prompt blank - improvement over Steelhead's
* silent disabled state (we show a tooltip listing blockers).
*
* Inline 'Resolve' button next to each blocker with a resolve_action.
@@ -41,7 +41,7 @@ export class FpMovePartsDialog extends Component {
blockers: [],
committing: false,
// Advanced fields (Transfer Type, To Location) stay collapsed
// by default the everyday flow is "advance all to the next
// by default - the everyday flow is "advance all to the next
// stage", which needs none of them. Keeps the dialog to a qty
// confirm + SEND for the 95% case.
showAdvanced: false,

View File

@@ -1,6 +1,6 @@
/** @odoo-module */
/*
* Sub 12b Move Rack dialog (OWL).
* Sub 12b - Move Rack dialog (OWL).
*
* Mirrors screens 11, 13, 14. Same shape as Move Parts but no
* transition prompts (rack moves are rack-level). Title carries

View File

@@ -1,8 +1,8 @@
/** @odoo-module **/
// =====================================================================
// FpPlantKanban sole Shop Floor OWL action (2026-05-25 onward).
// FpPlantKanban - sole Shop Floor OWL action (2026-05-25 onward).
// Mounts via the fp_plant_kanban client action. fp_shopfloor_landing
// was retired the same day its only unique feature (inline QR
// was retired the same day - its only unique feature (inline QR
// scanner) was ported here.
//
// Architecture:
@@ -14,7 +14,7 @@
// - Station pairing writes res.users.paired_work_centre_ids via
// /fp/landing/pair_work_centre (replaces legacy localStorage
// pairing)
// - Per project rule 20, no String()/Number()/etc. in templates
// - Per project rule 20, no String()/Number()/etc. in templates -
// all coercion happens here in JS-land.
// =====================================================================
@@ -56,7 +56,7 @@ export class FpPlantKanban extends Component {
loading: true,
search: "",
// QR scan drawer (text/wedge path). Camera path is owned by
// the QrScanner component itself it routes URLs internally.
// the QrScanner component itself - it routes URLs internally.
showScan: false,
scanInput: "",
});
@@ -153,7 +153,7 @@ export class FpPlantKanban extends Component {
// ---- QR scan (text / wedge / manual paste path) -----------------------
// Camera path is rendered by the inline <QrScanner/> component below
// the header it owns its own modal + decoder + URL routing. This
// the header - it owns its own modal + decoder + URL routing. This
// drawer is for FP-STATION:/FP-JOB:/FP-STEP: scanner-wedge codes typed
// into the input.
toggleScan() {
@@ -214,7 +214,7 @@ export class FpPlantKanban extends Component {
params: { job_id: res.id },
target: "main",
});
return; // navigating away skip the refresh
return; // navigating away - skip the refresh
} else if (res.model === "fp.job.step") {
this.action.doAction({
type: "ir.actions.client",

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating Plant Overview Dashboard (OWL backend client action)
// Fusion Plating - Plant Overview Dashboard (OWL backend client action)
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
@@ -26,10 +26,10 @@ import { QrScanner } from "./qr_scanner";
import { FpMoveRackDialog } from "./move_rack_dialog";
// =============================================================================
// TimerChip per-card live elapsed-in-stage chip (v19.0.24.10.0)
// TimerChip - per-card live elapsed-in-stage chip (v19.0.24.10.0)
// =============================================================================
// Old design read state.tickEpoch from inside the parent's getCardTimer(),
// which forced OWL to mark the WHOLE component dirty every 5 seconds
// which forced OWL to mark the WHOLE component dirty every 5 seconds -
// 389 cards re-rendering all at once, even though only the chip text
// changes. That's what caused the "drop, wait 5s, card jumps back" feel
// on a busy board: a tick fired mid-drop and froze the main thread.
@@ -38,7 +38,7 @@ import { FpMoveRackDialog } from "./move_rack_dialog";
// re-renders only itself, and has stable props from the parent. When a
// card moves columns, OWL keeps the same TimerChip instance (matched by
// t-key=card.id on the parent), so the interval keeps running across the
// move no remount, no flicker.
// move - no remount, no flicker.
class TimerChip extends Component {
static template = "fusion_plating_shopfloor.TimerChip";
static props = {
@@ -50,7 +50,7 @@ class TimerChip extends Component {
setup() {
this.state = useState({ now: Date.now() });
onMounted(() => {
// 5s tick is plenty the displayed text changes at minute
// 5s tick is plenty - the displayed text changes at minute
// resolution after the first 60s anyway.
this._iv = setInterval(() => {
this.state.now = Date.now();
@@ -138,7 +138,7 @@ export class PlantOverview extends Component {
this.state = useState({
facilityName: "",
columns: [],
racks: [], // Sub 12b Racks pane payload
racks: [], // Sub 12b - Racks pane payload
searchTerm: "",
loading: false,
lastRefresh: null,
@@ -162,7 +162,7 @@ export class PlantOverview extends Component {
await this.loadData();
// Server data refresh every 30 seconds (catches changes from
// other operators). Suppressed while a move is in flight or
// for 30 s after the last drop see _shouldSkipRefresh().
// for 30 s after the last drop - see _shouldSkipRefresh().
this._refreshInterval = setInterval(() => {
if (!this._shouldSkipRefresh()) this.loadData();
}, 30000);
@@ -196,7 +196,7 @@ export class PlantOverview extends Component {
this.state.facilityName = result.facility_name || "Plant 1";
this.state.columns = result.columns || [];
this.state.racks = result.racks || [];
// Prefer the server-formatted timestamp it's in the
// Prefer the server-formatted timestamp - it's in the
// FP-configured tz (fp_format → user.tz → company tz).
// Browser tz fallback only kicks in if the endpoint
// drops the field for some reason.
@@ -216,7 +216,7 @@ export class PlantOverview extends Component {
// ----- Search ------------------------------------------------------------
//
// Live search with a 200ms debounce. The user types, the cards update
// as they go no "press Enter" leap of faith. Debounce keeps us off
// as they go - no "press Enter" leap of faith. Debounce keeps us off
// the network on every keystroke when someone types fast.
onSearchInput(ev) {
@@ -224,12 +224,12 @@ export class PlantOverview extends Component {
this._debouncedSearch();
}
// ===================================================== Sub 12b racks
// ===================================================== Sub 12b - racks
openMoveRackDialog(rackId, toStepId) {
if (!toStepId) {
this.notification.add(
"No destination step available rack is at the last "
"No destination step available - rack is at the last "
+ "step of its job. Use the rack form to manually pick "
+ "a new step.",
{ type: "warning" });
@@ -247,7 +247,7 @@ export class PlantOverview extends Component {
}
onSearchKey(ev) {
// Enter still works fires immediately, skipping the debounce.
// Enter still works - fires immediately, skipping the debounce.
if (ev.key === "Enter") {
if (this._searchTimer) clearTimeout(this._searchTimer);
this.loadData();
@@ -288,7 +288,7 @@ export class PlantOverview extends Component {
}
onCardDragStart(card, col, ev) {
// Mark the kanban as actively dragging CSS rule freezes all
// Mark the kanban as actively dragging - CSS rule freezes all
// animations + transitions on descendants. Without this the
// browser was paint-locked fighting 27 chip pulses + transitions
// during the drop, causing the 5+ second visual freeze.
@@ -362,7 +362,7 @@ export class PlantOverview extends Component {
const body = ev.currentTarget;
if (body && !body.contains(ev.relatedTarget)) {
body.classList.remove("o_fp_drop_target");
// Only remove placeholder if we left the body entirely
// Only remove placeholder if we left the body entirely -
// otherwise the child card enter fires dragleave on the body
this._removePlaceholder();
}
@@ -370,7 +370,7 @@ export class PlantOverview extends Component {
async onColDrop(col, ev) {
// Instrumentation (v19.0.24.11.0). Keeping these console.time
// markers permanent they cost ~0.01ms each, make every freeze
// markers permanent - they cost ~0.01ms each, make every freeze
// visible in DevTools, and let the user paste a real timing back
// when something feels slow. Look in Console for "[fp drop] …".
const _t0 = performance.now();
@@ -435,7 +435,7 @@ export class PlantOverview extends Component {
...sourceCards.slice(0, cardOriginalIdx),
...sourceCards.slice(cardOriginalIdx + 1),
];
// New target array with the card on top it just got
// New target array with the card on top - it just got
// moved, the supervisor's eye expects it there. Server
// sort will re-position on the next refresh.
this.state.columns[targetColIdx].cards = [
@@ -449,7 +449,7 @@ export class PlantOverview extends Component {
// Force a paint frame BEFORE awaiting the RPC. Without this,
// OWL's render is queued but the browser may not paint until
// after the await rpc resolves which means the user sees the
// after the await rpc resolves - which means the user sees the
// card "freeze" until the network roundtrip completes.
// requestAnimationFrame schedules the callback right before the
// next paint, so by the time we await, the card is on screen.
@@ -491,7 +491,7 @@ export class PlantOverview extends Component {
`Moved to ${col.work_center_name}`,
{ type: "success" },
);
// Server confirmed clear the dim. Locate the card in
// Server confirmed - clear the dim. Locate the card in
// its (now-target) column and rebuild the array WITHOUT
// the _optimistic flag so OWL repaints at full opacity.
if (movedCard && targetColIdx >= 0) {
@@ -523,7 +523,7 @@ export class PlantOverview extends Component {
console.timeEnd("[fp drop] total");
const totalMs = (performance.now() - _t0).toFixed(0);
if (totalMs > 200) {
console.warn(`[fp drop] SLOW DROP: ${totalMs}ms paste this in chat`);
console.warn(`[fp drop] SLOW DROP: ${totalMs}ms - paste this in chat`);
}
}

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating Process Tree (horizontal hierarchical view)
// Fusion Plating - Process Tree (horizontal hierarchical view)
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Renders an fp.job's recipe (recipe → sub_process → operation → step) as a
@@ -13,11 +13,11 @@
// design is unchanged.
//
// Action context:
// job_id required; the fp.job whose recipe to render
// production_id legacy alias for job_id (still accepted)
// back_step_id optional; if set, the back button returns to
// job_id - required; the fp.job whose recipe to render
// production_id - legacy alias for job_id (still accepted)
// back_step_id - optional; if set, the back button returns to
// that step's form instead of Plant Overview
// back_workorder_id legacy alias for back_step_id
// back_workorder_id - legacy alias for back_step_id
// =============================================================================
import { Component, useState, onMounted } from "@odoo/owl";
@@ -114,7 +114,7 @@ export class ProcessTree extends Component {
// ---- Navigation ---------------------------------------------------------
onNodeClick(node) {
// Operation cards with a matching fp.job.step are clickable
// Operation cards with a matching fp.job.step are clickable -
// they open the underlying step form. node.workorder_id is the
// legacy template key that now carries the step id.
const stepId = node && (node.step_id || node.workorder_id);
@@ -131,7 +131,7 @@ export class ProcessTree extends Component {
}
onBack() {
// Try action.restore() first that pops the Process Tree
// Try action.restore() first - that pops the Process Tree
// off the breadcrumb stack and returns the user to the WO
// (or Step) they came from. Without this, doAction pushes a
// NEW act_window each time and the breadcrumb keeps growing

View File

@@ -1,11 +1,11 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating Reusable QR Scanner OWL Component
// Fusion Plating - Reusable QR Scanner OWL Component
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Decoder selection strongest available wins:
// Decoder selection - strongest available wins:
// 1. Native BarcodeDetector API (Android Chrome, iOS Safari 17+, desktop
// Chrome / Edge fastest, hardware
// Chrome / Edge - fastest, hardware
// accelerated, no JS in the hot path)
// 2. Vendored jsQR fallback (every other browser including iOS
// Safari < 17 and the in-app webviews
@@ -13,7 +13,7 @@
// which is what we hit in practice on
// phones today)
// 3. Manual paste (last resort: HTTP origin or no camera
// permission typing the URL still
// permission - typing the URL still
// works)
//
// The component renders a single button. On click, opens a modal that
@@ -24,7 +24,7 @@
// fp.job form via the action service.
//
// Used by Manager Desk, Tablet Station, Plant Overview, and Process Tree
// headers see each component's `static components = { QrScanner }`.
// headers - see each component's `static components = { QrScanner }`.
// =============================================================================
import { Component, useState, useRef, onWillUnmount } from "@odoo/owl";
@@ -54,7 +54,7 @@ export class QrScanner extends Component {
error: null,
manualUrl: "",
detected: "", // last decoded value (for user feedback)
// canScan / decoder are recomputed in open() don't trust
// canScan / decoder are recomputed in open() - don't trust
// setup-time values because vendored libs may attach to
// window asynchronously after the bundle finishes parsing.
canScan: false,
@@ -74,18 +74,18 @@ export class QrScanner extends Component {
/**
* Check what decoder is available right now and update state. Run
* at every open() not just setup() because a stale bundle in
* at every open() - not just setup() - because a stale bundle in
* the browser cache can flip results between page loads.
*
* Preference order:
* 1. ZXing-js (window.ZXing.BrowserMultiFormatReader) the most
* 1. ZXing-js (window.ZXing.BrowserMultiFormatReader) - the most
* tolerant; handles perspective skew, motion blur, and glare
* that defeat jsQR on phone cameras. This is the default.
* 2. Native BarcodeDetector fast, hardware-backed, but only
* 2. Native BarcodeDetector - fast, hardware-backed, but only
* available on Android Chrome and iOS Safari 17+. Skipped
* now that ZXing is the primary path; left as a code branch
* in case ZXing fails to load.
* 3. jsQR kept as a last-resort JS fallback.
* 3. jsQR - kept as a last-resort JS fallback.
*/
_detectCapabilities() {
const hasZXing = typeof window !== "undefined"
@@ -107,7 +107,7 @@ export class QrScanner extends Component {
"Decoder: " + this.state.decoder +
(hasNative ? " (native)" : "") +
(!hasNative && hasJsQR ? " (jsQR)" : "") +
(!this.state.canScan ? " paste URL below" : "")
(!this.state.canScan ? " - paste URL below" : "")
);
}
@@ -126,7 +126,7 @@ export class QrScanner extends Component {
async _startCamera() {
if (!this.state.canScan) {
// No decoder at all paste UI is the only path.
// No decoder at all - paste UI is the only path.
return;
}
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
@@ -205,7 +205,7 @@ export class QrScanner extends Component {
}
/**
* Decode loop using the browser's BarcodeDetector. Cheapest path
* Decode loop using the browser's BarcodeDetector. Cheapest path -
* the browser does the work off the JS thread. Only runs on
* Android Chrome, iOS Safari 17+, and desktop Chrome / Edge.
*/
@@ -225,7 +225,7 @@ export class QrScanner extends Component {
}
}
} catch (e) {
// Decode errors are noisy and recoverable try the
// Decode errors are noisy and recoverable - try the
// next frame. Real failures (camera revoked, etc.)
// surface via _startCamera's catch.
}
@@ -239,7 +239,7 @@ export class QrScanner extends Component {
* <video> element (already wired to the getUserMedia stream)
* straight into ZXing's continuous reader, which manages its
* own per-frame timing and decode pipeline (HybridBinarizer +
* perspective transform) the same algorithm family the iOS
* perspective transform) - the same algorithm family the iOS
* Camera app uses internally.
*
* The vendored bundle exposes these instance methods on
@@ -258,11 +258,11 @@ export class QrScanner extends Component {
this.decodeLoopActive = true;
const v = this.videoRef.el;
if (!v) {
this.state.statusLine = "Decoder: zxing video element missing";
this.state.statusLine = "Decoder: zxing - video element missing";
return;
}
const Z = window.ZXing;
// Pass hints via the constructor assignment to .hints
// Pass hints via the constructor - assignment to .hints
// afterward doesn't work because decodeBitmap reads from
// this._hints (set by MultiFormatReader.setHints during
// construction). TRY_HARDER makes the QR finder more
@@ -272,16 +272,16 @@ export class QrScanner extends Component {
hints.set(ZXING_HINT_POSSIBLE_FORMATS, [Z.BarcodeFormat.QR_CODE]);
}
hints.set(ZXING_HINT_TRY_HARDER, true);
// Second arg is timeBetweenScansMillis drop from 500 default
// Second arg is timeBetweenScansMillis - drop from 500 default
// to 100 so we attempt ~10 decodes/sec instead of ~2.
const reader = new Z.BrowserMultiFormatReader(hints, 100);
this._zxingReader = reader;
// Live status ZXing manages its own timing internally so we
// Live status - ZXing manages its own timing internally so we
// count callbacks instead of rAF ticks. Hits is what matters.
let callbacks = 0;
let lastStatus = 0;
let lastResult = "";
let lastResult = "-";
const refreshStatus = () => {
const now = performance.now();
if (now - lastStatus > 400) {
@@ -310,7 +310,7 @@ export class QrScanner extends Component {
lastResult = "no_code";
} else if (name.indexOf("Checksum") >= 0 || name.indexOf("Format") >= 0) {
// Found something QR-shaped but couldn't read it
// (blurry / damaged) keep trying next frame.
// (blurry / damaged) - keep trying next frame.
lastResult = "partial";
} else {
lastResult = "err:" + (err.message || name).slice(0, 40);
@@ -332,7 +332,7 @@ export class QrScanner extends Component {
*
* Throttled to one decode per ~100ms to stay responsive without
* pegging mid-range phones. Updates a live status line so the
* operator can see exactly what the loop is doing frames seen,
* operator can see exactly what the loop is doing - frames seen,
* decode attempts, video resolution. Critical for diagnosing
* "scan does nothing" reports without round-tripping through
* Safari Web Inspector.
@@ -341,7 +341,7 @@ export class QrScanner extends Component {
this.decodeLoopActive = true;
const v = this.videoRef.el;
if (!v) {
this.state.statusLine = "Decoder: jsqr video element missing";
this.state.statusLine = "Decoder: jsqr - video element missing";
return;
}
if (!this._canvas) {
@@ -352,7 +352,7 @@ export class QrScanner extends Component {
let attempts = 0;
let lastDecode = 0;
let lastStatus = 0;
let lastResult = ""; // "found" | "no_code" | "empty" | error msg
let lastResult = "-"; // "found" | "no_code" | "empty" | error msg
let firstNonZeroPixel = -1; // sanity check that drawImage works
const MIN_INTERVAL_MS = 100;
const STATUS_INTERVAL_MS = 500;
@@ -369,7 +369,7 @@ export class QrScanner extends Component {
try {
const w = v.videoWidth;
const h = v.videoHeight;
// Use the native video resolution directly no
// Use the native video resolution directly - no
// downscaling. jsQR's runtime cost is acceptable
// even at 1080p, and downsampling can blur the
// finder patterns just enough to defeat detection
@@ -435,7 +435,7 @@ export class QrScanner extends Component {
* (the <input type=file capture=environment> in the template
* backs this).
*
* Works on every browser that supports file inputs including
* Works on every browser that supports file inputs - including
* iOS Chrome / Safari, where the live-video decode path in ZXing
* has been unreliable. iOS hands us a JPEG that's been autofocused
* and properly exposed; we just need to run ONE decode on it
@@ -500,7 +500,7 @@ export class QrScanner extends Component {
}
hints.set(ZXING_HINT_TRY_HARDER, true);
const reader = new Z.BrowserMultiFormatReader(hints);
// decodeFromImageElement / decodeFromCanvas try the
// decodeFromImageElement / decodeFromCanvas - try the
// canvas-friendly path: build a luminance source +
// binary bitmap manually and call MultiFormatReader.
const luminance = new Z.HTMLCanvasElementLuminanceSource(canvas);
@@ -512,7 +512,7 @@ export class QrScanner extends Component {
const text = decoded && (decoded.getText ? decoded.getText() : decoded.text);
if (text) return text;
} catch (e) {
// ZXing miss fall through to jsQR.
// ZXing miss - fall through to jsQR.
}
}
if (typeof window.jsQR === "function") {
@@ -534,7 +534,7 @@ export class QrScanner extends Component {
* Route a decoded value to the right backend page.
*
* Stickers encode either /fp/job/<fp.job.id> (new) or
* /fp/wo/<mrp.production.id|mrp.workorder.id> (legacy still on
* /fp/wo/<mrp.production.id|mrp.workorder.id> (legacy - still on
* physical boxes from before the migration). Both URLs are
* handled by server-side controllers (job_scan.py / wo_scan.py)
* that resolve the correct record and redirect to its form.
@@ -587,7 +587,7 @@ export class QrScanner extends Component {
this._stopCamera();
this.state.open = false;
this.notification.add("Opening " + target, { type: "success" });
// Full navigation the server-side controller resolves the id
// Full navigation - the server-side controller resolves the id
// to the right record (works for both new fp.job stickers and
// legacy mrp.production / mrp.workorder stickers).
window.location.href = target;

View File

@@ -1,6 +1,6 @@
/** @odoo-module */
/*
* Sub 12b Rack Parts sub-dialog (OWL).
* Sub 12b - Rack Parts sub-dialog (OWL).
*
* Mirrors Steelhead screens 7-8. Searchable empty-rack picker,
* QR-scan input, Unit + Amount fields. Save assigns the step → rack;

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating Activity Tracker (shared OWL service)
// Fusion Plating - Activity Tracker (shared OWL service)
//
// Watches the document for pointer/touch/keydown/visibility events and
// tracks lastActiveAt. FpTabletLock reads getSecondsUntilLock() once per

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// fpRpc thin wrapper around @web/core/network/rpc
// fpRpc - thin wrapper around @web/core/network/rpc
//
// Post-Phase-G of the tablet PIN session redesign: this no longer
// injects tablet_tech_id (the session uid IS the tech). Kept as a

View File

@@ -87,7 +87,7 @@ export const tabletSessionManager = {
await rpc("/fp/tablet/lock_session", { reason });
} catch (e) {
// Even if the RPC fails, force a reload to drop the
// current session state the cron will clean up.
// current session state - the cron will clean up.
console.warn("lock_session RPC failed; reloading anyway", e);
}
window.location.reload();

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating Shop Floor Tablet (OWL backend client action)
// Fusion Plating - Shop Floor Tablet (OWL backend client action)
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
@@ -35,7 +35,7 @@ export class ShopfloorTablet extends Component {
this.dialog = useService("dialog");
this.scanInput = useRef("scanInput");
// Sub 12b listen for the rack-resolution custom event fired
// Sub 12b - listen for the rack-resolution custom event fired
// from inside FpMovePartsDialog when the operator hits the
// 'Resolve' button on a rack-required blocker.
this._onResolveRack = (ev) => {
@@ -98,14 +98,14 @@ export class ShopfloorTablet extends Component {
_tickElapsed() {
const a = this.state.overview && this.state.overview.active_wo;
if (!a || !a.date_started_iso) {
this.state.activeElapsed = "";
this.state.activeElapsed = "-";
return;
}
// Odoo gives "YYYY-MM-DD HH:MM:SS" in UTC; turn into ISO Z.
const isoUtc = a.date_started_iso.replace(" ", "T") + "Z";
const startMs = Date.parse(isoUtc);
if (isNaN(startMs)) {
this.state.activeElapsed = "";
this.state.activeElapsed = "-";
return;
}
let s = Math.max(0, Math.floor((Date.now() - startMs) / 1000));
@@ -131,7 +131,7 @@ export class ShopfloorTablet extends Component {
this.state.overview = payload;
}
} catch (err) {
// silent next tick will retry
// silent - next tick will retry
}
}
@@ -192,7 +192,7 @@ export class ShopfloorTablet extends Component {
this.state.loading = false;
return;
} else {
this.setMessage(`Scanned ${result.model} ${result.name || ""}`, "info");
this.setMessage(`Scanned ${result.model} - ${result.name || ""}`, "info");
}
} catch (err) {
this.setMessage(`Scan error: ${err.message || err}`, "danger");
@@ -252,7 +252,7 @@ export class ShopfloorTablet extends Component {
try {
const res = await rpc("/fp/shopfloor/start_wo", { workorder_id: woId });
if (res && res.ok) {
this.setMessage("Work order started timer running.", "success");
this.setMessage("Work order started - timer running.", "success");
} else if (res && res.error) {
this.setMessage(res.error, "danger");
}
@@ -279,8 +279,8 @@ export class ShopfloorTablet extends Component {
}
// ---------------------------------------------------------- Qty / scrap
// Bump qty_done from the tablet operator confirms +1 part finished.
// Also bump scrap with a confirm prompt auto-spawns a Hold via S17.
// Bump qty_done from the tablet - operator confirms +1 part finished.
// Also bump scrap with a confirm prompt - auto-spawns a Hold via S17.
async onBumpQtyDone(jobId) {
if (!jobId) return;
try {
@@ -301,7 +301,7 @@ export class ShopfloorTablet extends Component {
async onBumpScrap(jobId) {
if (!jobId) return;
// Block-and-prompt scrap is a quality event, force the operator
// Block-and-prompt - scrap is a quality event, force the operator
// to acknowledge and ideally type a brief reason.
const reason = window.prompt(
"Reason for scrap (e.g. 'dropped during de-rack', 'flash burn'):",
@@ -328,7 +328,7 @@ export class ShopfloorTablet extends Component {
await this.refresh();
}
// Open a pending QC directly from the banner same deep-link the
// Open a pending QC directly from the banner - same deep-link the
// FP-QC scan path uses.
onOpenPendingQc(qcId) {
if (!qcId) return;

View File

@@ -1,6 +1,6 @@
/** @odoo-module */
/*
* Sub 12b Stop User Labor Timer dialog (OWL).
* Sub 12b - Stop User Labor Timer dialog (OWL).
*
* Mirrors screen 10. Opens with state already at 'stopped' (server-side
* flip on /labor_timer/stop), pre-fills billed_* from accrued. Operator

View File

@@ -1,6 +1,6 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating FpTabletLock (top-level wrapper)
// Fusion Plating - FpTabletLock (top-level wrapper)
//
// Mounted by Landing / Workspace / Manager Dashboard as their outermost
// element. Renders the lock screen (tile grid + PIN pad) when no tech
@@ -43,7 +43,7 @@ export class FpTabletLock extends Component {
selectedTileUserId: null,
idleSecondsRemaining: null,
loadingTiles: false,
// 2026-05-24 redesign clock + company branding
// 2026-05-24 redesign - clock + company branding
// Seeded synchronously so the first render shows real values
// (no flash of empty content). tz=null on first render falls
// back to browser tz; _loadTiles() then sets state.tz from
@@ -61,12 +61,12 @@ export class FpTabletLock extends Component {
// the kiosk (= locked).
kioskUid: null,
currentUid: null,
// Spec 2026-05-25 PIN self-service wizard states
// 'pin' default keypad (has-PIN user)
// 'request_code' "Send temp PIN" button screen
// 'enter_temp_code' 4-cell pad for emailed code
// 'set_new_pin' 4-cell pad choose new PIN
// 'confirm_new_pin' 4-cell pad confirm new PIN
// Spec 2026-05-25 - PIN self-service wizard states
// 'pin' - default keypad (has-PIN user)
// 'request_code' - "Send temp PIN" button screen
// 'enter_temp_code' - 4-cell pad for emailed code
// 'set_new_pin' - 4-cell pad - choose new PIN
// 'confirm_new_pin' - 4-cell pad - confirm new PIN
mode: 'pin',
failedAttempts: 0, // resets on tile re-select
maskedEmail: '',
@@ -81,7 +81,7 @@ export class FpTabletLock extends Component {
onMounted(async () => {
await this._loadTiles();
this._tick = setInterval(() => this._checkIdle(), 1000);
// Heartbeat ping every 60s for forensic visibility. Only
// Heartbeat ping every 60s - for forensic visibility. Only
// ping while a tech is logged in; on the kiosk session this
// is just noise.
this._ping = setInterval(() => {
@@ -90,7 +90,7 @@ export class FpTabletLock extends Component {
rpc("/fp/tablet/ping", {}).catch(() => {});
}
}, 60000);
// Clock tick update visible HH:MM and date label every 60s.
// Clock tick - update visible HH:MM and date label every 60s.
// 60s is enough; the displayed precision is minute-level only.
this._clockInterval = setInterval(() => {
const now = new Date();
@@ -117,7 +117,7 @@ export class FpTabletLock extends Component {
get isLocked() {
// The browser session itself tells us whether a tech is
// unlocked current_uid != kiosk_uid means unlocked.
// unlocked - current_uid != kiosk_uid means unlocked.
return !this.state.currentUid
|| this.state.currentUid === this.state.kioskUid;
}
@@ -125,14 +125,14 @@ export class FpTabletLock extends Component {
async _loadTiles() {
this.state.loadingTiles = true;
try {
// 2026-05-25 the legacy fp_landing_station_id localStorage
// 2026-05-25 - the legacy fp_landing_station_id localStorage
// key (set by the now-deleted fp_shopfloor_landing component)
// is purged on read. Sending its stale value to /fp/tablet/tiles
// caused the kiosk session to AccessError on shopfloor.station
// and return an empty tile list. plant_kanban pairs to
// work_centre server-side (paired_work_centre_ids) so the
// kiosk-rendered lock screen no longer needs per-tablet pairing
// for tile scoping all shop-branch users render.
// for tile scoping - all shop-branch users render.
try { localStorage.removeItem("fp_landing_station_id"); } catch {}
try { localStorage.removeItem("fp_landing_mode"); } catch {}
const res = await rpc("/fp/tablet/tiles", { station_id: null });
@@ -156,7 +156,7 @@ export class FpTabletLock extends Component {
}));
}
} catch (err) {
// Quiet fail tile grid stays empty; user gets prompted
// Quiet fail - tile grid stays empty; user gets prompted
} finally {
this.state.loadingTiles = false;
}
@@ -186,7 +186,7 @@ export class FpTabletLock extends Component {
onTileClick(userId) {
this.state.selectedTileUserId = userId;
this.state.failedAttempts = 0;
// Spec D1 if user has no PIN, jump straight to the
// Spec D1 - if user has no PIN, jump straight to the
// "Send temporary PIN" screen. Otherwise show the keypad.
const tile = this._tileForUser(userId);
this.state.mode = (tile && tile.has_pin) ? 'pin' : 'request_code';
@@ -220,7 +220,7 @@ export class FpTabletLock extends Component {
// navigate while we're tearing down.
return { ok: true, reloading: true };
}
// Wrong PIN bump client-side counter for "Forgot?" gating
// Wrong PIN - bump client-side counter for "Forgot?" gating
this.state.failedAttempts += 1;
return {
ok: false,
@@ -251,7 +251,7 @@ export class FpTabletLock extends Component {
_formatTime(d, tz) {
// 12-hour H:MM AM/PM in the FP-configured tz (falls back to
// browser tz if tz is null first paint before _loadTiles).
// browser tz if tz is null - first paint before _loadTiles).
// Per project rule 20 this MUST live in JS, not the template.
// Hour is NOT zero-padded (1:05 PM, not 01:05 PM) to match
// phone-clock idiom.
@@ -275,7 +275,7 @@ export class FpTabletLock extends Component {
tileStyle(tile) {
// Inline animation-delay so each tile's entrance staggers.
// Returned as a string per project rule 20 the template can't
// Returned as a string per project rule 20 - the template can't
// call String() inside t-att-style.
return "animation-delay: " + tile.animDelay + "ms";
}
@@ -286,16 +286,16 @@ export class FpTabletLock extends Component {
: "o_fp_lock_avatar";
}
// ===== Spec 2026-05-25 PIN self-service wizard handlers =====
// ===== Spec 2026-05-25 - PIN self-service wizard handlers =====
/** "Forgot? Reset PIN via email" button click from PIN entry screen
/** "Forgot? Reset PIN via email" button click - from PIN entry screen
* after 3 fails. */
onForgotPinClick() {
this.state.mode = 'request_code';
this.state.statusMessage = '';
}
/** "Send temporary PIN" button click from request_code screen. */
/** "Send temporary PIN" button click - from request_code screen. */
async onSendCodeClick() {
try {
const res = await rpc("/fp/tablet/request_reset_code", {
@@ -403,7 +403,7 @@ export class FpTabletLock extends Component {
window.location.reload();
return { ok: true, reloading: true };
}
// PIN set but unlock failed user can tap their tile + enter
// PIN set but unlock failed - user can tap their tile + enter
// the new PIN manually
this.state.statusMessage = 'PIN set. Tap your tile and enter the new PIN to log in.';
this.onPinCancel();

View File

@@ -1,11 +1,11 @@
// =============================================================================
// Fusion Plating Shop Floor Design System (v2, 2026-04)
// Fusion Plating - Shop Floor Design System (v2, 2026-04)
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Design philosophy:
// * NO card borders depth comes from elevation (shadow) only
// * NO card borders - depth comes from elevation (shadow) only
// * Generous whitespace, calm surfaces, one accent colour
// * Semantic colours (success/warning/danger) reserved for STATUS not
// * Semantic colours (success/warning/danger) reserved for STATUS - not
// decoration
// * Type-first hierarchy: big headings + big numbers + small helpers
// * Every value resolves from Odoo CSS custom properties, so light
@@ -31,11 +31,11 @@ $fp-radius-lg : 20px;
$fp-radius-xl : 28px;
$fp-radius-pill: 999px;
// ---------- Surfaces COMPILE-TIME branch on Odoo's dark scheme -------------
// ---------- Surfaces - COMPILE-TIME branch on Odoo's dark scheme -------------
//
// Odoo 19 compiles TWO asset bundles: web.assets_backend (light) and
// web.assets_web_dark (dark). The two bundles differ only in the value
// of the SCSS variable $o-webclient-color-scheme `bright` for light,
// of the SCSS variable $o-webclient-color-scheme - `bright` for light,
// `dark` for dark (defined in primary_variables.scss /
// primary_variables.dark.scss in web_enterprise).
//
@@ -58,7 +58,7 @@ $_fp-ink-soft-hex : #4b5563;
$_fp-ink-mute-hex : #6b7280;
$_fp-ink-faint-hex : #9ca3af;
// Dark palette engaged when the dark bundle is compiled
// Dark palette - engaged when the dark bundle is compiled
@if $o-webclient-color-scheme == dark {
$_fp-page-hex : #1a1d21 !global;
$_fp-card-hex : #22262d !global;
@@ -71,7 +71,7 @@ $_fp-ink-faint-hex : #9ca3af;
$_fp-ink-faint-hex : #5a606b !global;
}
// Public tokens CSS custom property fallback chain remains so a
// Public tokens - CSS custom property fallback chain remains so a
// deployment can still override via --fp-* without touching SCSS.
$fp-page : var(--fp-page-bg, $_fp-page-hex);
$fp-card : var(--fp-card-bg, $_fp-card-hex);
@@ -83,7 +83,7 @@ $fp-ink-soft : var(--fp-ink-soft, $_fp-ink-soft-hex);
$fp-ink-mute : var(--fp-ink-mute, $_fp-ink-mute-hex);
$fp-ink-faint : var(--fp-ink-faint, $_fp-ink-faint-hex);
// Action colour Odoo's primary. Same in both bundles (brand purple).
// Action colour - Odoo's primary. Same in both bundles (brand purple).
$fp-accent : var(--o-action, #714B67);
// ---------- Kind chip colours (domain semantic) ------------------------------
@@ -117,7 +117,7 @@ $fp-kind-rack : var(--fp-kind-rack, $_fp-kind-rack-hex);
$fp-kind-inspect : var(--fp-kind-inspect, $_fp-kind-inspect-hex);
$fp-kind-other : var(--fp-kind-other, $_fp-kind-other-hex);
// ---------- Elevation explicit rgba shadows --------------------------------
// ---------- Elevation - explicit rgba shadows --------------------------------
// Explicit rgba values (not color-mix) so they render identically across
// browsers and themes. In dark mode the shadows still work against the
// darker surfaces because they're translucent.
@@ -131,7 +131,7 @@ $fp-elev-hover : 0 6px 12px rgba(0, 0, 0, 0.12),
0 18px 36px rgba(0, 0, 0, 0.16);
// ---------- Semantic colour helpers ------------------------------------------
// (Note: $fp-accent defined earlier with its fallback not redefined here)
// (Note: $fp-accent defined earlier with its fallback - not redefined here)
$fp-ok : var(--bs-success, #28a745);
$fp-warn : var(--bs-warning, #ffc107);
$fp-bad : var(--bs-danger, #dc3545);
@@ -143,7 +143,7 @@ $fp-info : var(--bs-info, #17a2b8);
}
// ---------- Type scale ------------------------------------------------------
// Shop-floor tablets are read from 18" baseline bumped from Odoo default.
// Shop-floor tablets are read from 18" - baseline bumped from Odoo default.
$fp-text-xs : 0.75rem; // 12px small labels
$fp-text-sm : 0.875rem; // 14px helper text
$fp-text-base : 1rem; // 16px body
@@ -169,20 +169,20 @@ $fp-dur : 200ms;
$fp-dur-slow : 360ms;
// ---------- Touch ------------------------------------------------------------
$fp-touch-min : 48px; // larger than Apple's 44px minimum shop floor
$fp-touch-min : 48px; // larger than Apple's 44px minimum - shop floor
// =============================================================================
// Mixins
// =============================================================================
// Focus ring used on all interactive inputs/buttons
// Focus ring - used on all interactive inputs/buttons
@mixin fp-focus-ring {
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, #{$fp-accent} 35%, transparent);
}
// Card surface shadow-based, no border
// Card surface - shadow-based, no border
@mixin fp-card($elev: $fp-elev-1) {
background-color: $fp-card;
border-radius: $fp-radius-lg;
@@ -205,7 +205,7 @@ $fp-touch-min : 48px; // larger than Apple's 44px minimum — shop floor
// =============================================================================
// Dark mode
// No class-based override needed Odoo 19 serves a separate compiled bundle
// No class-based override needed - Odoo 19 serves a separate compiled bundle
// for dark mode, and all --bs-* tokens are redefined inside it. Because our
// $fp-* tokens fall through to --bs-* (see the surface definitions above),
// dark mode Just Works.

View File

@@ -1,5 +1,5 @@
// =====================================================================
// Plant-view kanban design tokens
// 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
@@ -34,7 +34,7 @@ $_plant-noparts-border-hex: #6c757d;
$_plant-done-bg-hex: #f0f9f4;
$_plant-done-border-hex: #28a745;
// Spec 2026-05-25 post-shop states
// Spec 2026-05-25 - post-shop states
$_plant-awaiting-cert-bg-hex: #fff3cd;
$_plant-awaiting-cert-border-hex: #ff9800;
$_plant-awaiting-ship-bg-hex: #d1f1d4;
@@ -58,7 +58,7 @@ $_plant-awaiting-ship-border-hex: #2e7d32;
$_plant-noparts-bg-hex: #2d3138 !global;
$_plant-done-bg-hex: #14281a !global;
// Spec 2026-05-25 post-shop states (dark)
// Spec 2026-05-25 - post-shop states (dark)
$_plant-awaiting-cert-bg-hex: #3a2f15 !global;
$_plant-awaiting-cert-border-hex: #ffb74d !global;
$_plant-awaiting-ship-bg-hex: #1a2d1f !global;
@@ -91,7 +91,7 @@ $plant-noparts-border: var(--fp-plant-noparts-border, $_plant-noparts-border-he
$plant-done-bg: var(--fp-plant-done-bg, $_plant-done-bg-hex);
$plant-done-border: var(--fp-plant-done-border, $_plant-done-border-hex);
// Spec 2026-05-25 post-shop states
// Spec 2026-05-25 - post-shop states
$plant-awaiting-cert-bg: var(--fp-plant-awaiting-cert-bg, $_plant-awaiting-cert-bg-hex);
$plant-awaiting-cert-border: var(--fp-plant-awaiting-cert-border, $_plant-awaiting-cert-border-hex);
$plant-awaiting-ship-bg: var(--fp-plant-awaiting-ship-bg, $_plant-awaiting-ship-bg-hex);

View File

@@ -1,4 +1,4 @@
// _column_header.scss depends on _plant_tokens.scss
// _column_header.scss - depends on _plant_tokens.scss
.o_fp_col_header {
padding: 8px 10px;
@@ -6,7 +6,7 @@
align-items: center;
justify-content: space-between;
gap: 6px;
// No own background or outer border the parent .col carries them
// No own background or outer border - the parent .col carries them
// (full-height column-as-card layout). Just a bottom divider so the
// header reads as a distinct band above the scrollable body.
background: transparent;

View File

@@ -1,4 +1,4 @@
// _filter_chip.scss depends on _plant_tokens.scss
// _filter_chip.scss - depends on _plant_tokens.scss
// 2026-05-25: bigger touch target + gradient bg.
.o_fp_filter_chip {

View File

@@ -1,5 +1,5 @@
// =============================================================================
// GateViz "this step can't start because..." explainer
// GateViz - "this step can't start because..." explainer
// Dark-mode aware via $o-webclient-color-scheme branch.
// =============================================================================

View File

@@ -1,5 +1,5 @@
// =============================================================================
// HoldComposer modal hold-create form
// HoldComposer - modal hold-create form
// =============================================================================
.o_fp_hc {

View File

@@ -1,5 +1,5 @@
// =============================================================================
// FpIdleWarning yellow-border countdown overlay before auto-lock
// FpIdleWarning - yellow-border countdown overlay before auto-lock
// =============================================================================
.o_fp_idle_warning_overlay {

View File

@@ -1,5 +1,5 @@
// =============================================================================
// KanbanCard standard WO/step card for Landing + Manager surfaces
// KanbanCard - standard WO/step card for Landing + Manager surfaces
// Dark-mode aware via $o-webclient-color-scheme branch.
// =============================================================================

View File

@@ -1,9 +1,9 @@
// _kpi_tile.scss depends on _plant_tokens.scss
// _kpi_tile.scss - depends on _plant_tokens.scss
//
// 2026-05-25: redesigned for the 8-tile row. Narrower individual width
// (grid handles that), more vertical presence via padding + larger
// typography, subtle 135deg gradients per kind that work in both light
// and dark modes (gradient stops use $plant-* tokens dark variants
// and dark modes (gradient stops use $plant-* tokens - dark variants
// flip automatically via the @if $o-webclient-color-scheme branch).
.o_fp_kpi_tile {

View File

@@ -1,4 +1,4 @@
// _mini_timeline.scss depends on _plant_tokens.scss
// _mini_timeline.scss - depends on _plant_tokens.scss
.o_fp_mini_timeline {
display: flex;

View File

@@ -1,5 +1,5 @@
// =============================================================================
// FpPinPad numeric keypad for tablet lock screen + PIN setup
// FpPinPad - numeric keypad for tablet lock screen + PIN setup
// Dark-mode aware via $o-webclient-color-scheme branch.
// =============================================================================
@@ -20,7 +20,7 @@ $_pin-dot-fill-hex: #1d1d1f;
// Empty dot: dark gray that blends into the panel but is still
// visible; Filled dot: bright white for strong contrast. Previous
// version left empty at light gray (#d8dadd) and set filled to
// off-white (#f5f5f7) both light colours, indistinguishable from
// off-white (#f5f5f7) - both light colours, indistinguishable from
// each other on the dark panel so user couldn't see PIN progress.
$_pin-dot-hex: #424245 !global;
$_pin-dot-fill-hex: #ffffff !global;

View File

@@ -1,4 +1,4 @@
// _plant_card.scss depends on _plant_tokens.scss
// _plant_card.scss - depends on _plant_tokens.scss
.o_fp_plant_card {
background: $plant-card-bg;
@@ -66,7 +66,7 @@
border-left: 4px solid $plant-done-border;
padding-left: 7px;
}
// Spec 2026-05-25 post-shop states
// Spec 2026-05-25 - post-shop states
&.state-awaiting_cert {
background: $plant-awaiting-cert-bg;
border-left: 4px solid $plant-awaiting-cert-border;
@@ -91,7 +91,7 @@
.card-sub-em { color: $plant-text; font-weight: 600; }
.card-meta { font-size: 11px; color: $plant-muted; }
.card-step { font-size: 14px; font-weight: 600; color: $plant-text; margin-top: 2px; }
// Partial-order handling "20 of 50 here" per-stage count. The big
// Partial-order handling - "20 of 50 here" per-stage count. The big
// number pops so an operator scanning their column instantly sees how
// many of a job's parts are at their station. Uses existing tokens so
// dark mode is handled at compile time by _plant_tokens.scss.

View File

@@ -1,4 +1,4 @@
// Racking panel (Job Workspace) split a WO across racks. Self-contained
// Racking panel (Job Workspace) - split a WO across racks. Self-contained
// tokens with a compile-time dark-mode branch (Odoo 19 compiles this file
// into both web.assets_backend and web.assets_web_dark).

View File

@@ -1,5 +1,5 @@
// =============================================================================
// SignaturePad modal canvas signature capture
// SignaturePad - modal canvas signature capture
// Canvas stays light even in dark mode (signature legibility).
// =============================================================================

View File

@@ -1,7 +1,7 @@
// =============================================================================
// WorkflowChip colored milestone pill
// WorkflowChip - colored milestone pill
// Dark-mode aware via $o-webclient-color-scheme branch (registered in BOTH
// web.assets_backend AND web.assets_web_dark see manifest).
// web.assets_backend AND web.assets_web_dark - see manifest).
// =============================================================================
$o-webclient-color-scheme: bright !default;

View File

@@ -1,5 +1,5 @@
// =============================================================================
// Fusion Plating Shared kanban card style for menu pages
// Fusion Plating - Shared kanban card style for menu pages
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// This file styles the standalone Bake Windows and First-Piece Gates kanban
@@ -20,7 +20,7 @@
// -----------------------------------------------------------------------------
// Suppress Odoo's default .o_kanban_record chrome inside our kanbans.
// The default wrapper paints its own background, border, padding, and
// (annoyingly) box-shadow with sharper corners than our inner card
// (annoyingly) box-shadow with sharper corners than our inner card -
// which makes a faint square ghost visible behind every card. By making
// the wrapper transparent / borderless, only our .o_fp_kcard surface is
// visible, so corner radii stay consistent across every state.
@@ -39,7 +39,7 @@
overflow: visible; // let our shadow paint outside the wrapper
}
// Same treatment for the kanban group container Odoo gives it a
// Same treatment for the kanban group container - Odoo gives it a
// subtle bg that looks misaligned next to the card surfaces.
.o_kanban_group {
background: transparent;
@@ -76,7 +76,7 @@
}
}
// Left state stripe driven by data-state / data-result attribute on
// Left state stripe - driven by data-state / data-result attribute on
// the card. Default is the muted ink-faint colour; specific states
// override below.
&::before {
@@ -105,7 +105,7 @@
// -- Big metric (time remaining etc.) ------------------------------
// Used when the card has one number that matters more than the rest
// (bake countdown, qty pending). Stays compact this is a kanban,
// (bake countdown, qty pending). Stays compact - this is a kanban,
// not a billboard.
.o_fp_kcard_metric {
display: inline-flex; align-items: baseline; gap: $fp-space-1;
@@ -124,7 +124,7 @@
}
}
// -- Meta line small key/value pairs separated by mid-dots -------
// -- Meta line - small key/value pairs separated by mid-dots -------
.o_fp_kcard_meta {
font-size: $fp-text-xs;
color: $fp-ink-mute;
@@ -135,7 +135,7 @@
}
}
// -- Footer chip + secondary tags --------------------------------
// -- Footer - chip + secondary tags --------------------------------
.o_fp_kcard_footer {
display: flex; justify-content: space-between; align-items: center;
gap: $fp-space-2;
@@ -163,7 +163,7 @@
// =============================================================================
// Bake Windows state-driven stripe + soft danger wash on missed jobs
// Bake Windows - state-driven stripe + soft danger wash on missed jobs
// =============================================================================
.o_fp_bw_kanban {
.o_fp_kcard {
@@ -172,7 +172,7 @@
&[data-state="baked"] { &::before { background-color: $fp-ok; } }
&[data-state="missed_window"] {
&::before { background-color: $fp-bad; }
// Missed windows are an exception state softly tint the
// Missed windows are an exception state - softly tint the
// whole card so it stands out in a sea of normal ones.
background-color: fp-wash(--bs-danger, 6%);
border-color: color-mix(in srgb, #{$fp-bad} 35%, #{$fp-border});
@@ -186,7 +186,7 @@
// =============================================================================
// First-Piece Gates result-driven stripe (pending = warn, fail = bad)
// First-Piece Gates - result-driven stripe (pending = warn, fail = bad)
// =============================================================================
.o_fp_fpg_kanban {
.o_fp_kcard {
@@ -199,7 +199,7 @@
}
}
// Subtle "released" badge visible only when the lot has been
// Subtle "released" badge - visible only when the lot has been
// released after a passing first-piece. Sits next to the result chip.
.o_fp_fpg_released {
display: inline-flex; align-items: center; gap: 4px;

View File

@@ -1,9 +1,9 @@
// =============================================================================
// Fusion Plating Tablet Station (Worker view)
// Fusion Plating - Tablet Station (Worker view)
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Shop-floor operators' home screen. Built fresh from the design system in
// _fp_shopfloor_tokens.scss. No card borders depth by shadow + tint.
// _fp_shopfloor_tokens.scss. No card borders - depth by shadow + tint.
// =============================================================================
@@ -36,7 +36,7 @@
flex-direction: column;
gap: $fp-space-5;
// Tablet sweet spot iPad landscape (1024) and portrait (768).
// Tablet sweet spot - iPad landscape (1024) and portrait (768).
// The goal is to fit Hero + KPIs + Active WO + the first row of
// panels in a single 768-tall viewport.
@media (max-width: 1180px) { padding: $fp-space-4 $fp-space-5; gap: $fp-space-4; }
@@ -45,7 +45,7 @@
// -------------------------------------------------------------------------
// Hero greeting + station chip + actions
// Hero - greeting + station chip + actions
// -------------------------------------------------------------------------
.o_fp_tablet_header {
display: grid;
@@ -67,7 +67,7 @@
color: $fp-ink;
display: flex; align-items: center; gap: $fp-space-3;
// Smaller hero on tablet saves ~16px of vertical space without
// Smaller hero on tablet - saves ~16px of vertical space without
// losing the page identity.
@media (max-width: 1180px) { font-size: $fp-text-xl; gap: $fp-space-2; }
}
@@ -104,7 +104,7 @@
min-width: 240px;
min-height: $fp-touch-min;
// Reserve room on the right so the custom chevron has breathing
// space between itself and the rounded corner the native arrow
// space between itself and the rounded corner - the native arrow
// hugs the edge in Odoo's frame, which looked cramped on iPad.
padding: $fp-space-2 $fp-space-7 $fp-space-2 $fp-space-4;
border: 1px solid #{$fp-border};
@@ -199,7 +199,7 @@
// -------------------------------------------------------------------------
// Flash message inline banner
// Flash message - inline banner
// -------------------------------------------------------------------------
.o_fp_tablet_message {
display: flex; align-items: center; gap: $fp-space-3;
@@ -225,14 +225,14 @@
// -------------------------------------------------------------------------
// KPI tiles minimal, big number
// KPI tiles - minimal, big number
// -------------------------------------------------------------------------
.o_fp_kpi_strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: $fp-space-4;
// iPad landscape (1024) six 130px tiles + gaps fit on one row.
// iPad landscape (1024) - six 130px tiles + gaps fit on one row.
// Keeps the KPI strip a single line so the dashboard can stay above
// the fold.
@media (max-width: 1180px) {
@@ -292,7 +292,7 @@
font-weight: $fp-weight-medium;
}
// Accent dot keyed to tone (top-right) tiny, confident
// Accent dot keyed to tone (top-right) - tiny, confident
&::after {
content: "";
position: absolute;
@@ -319,7 +319,7 @@
// -------------------------------------------------------------------------
// Active WO banner unmistakable when a job is running
// Active WO banner - unmistakable when a job is running
// -------------------------------------------------------------------------
.o_fp_active_wo {
display: flex;
@@ -424,7 +424,7 @@
display: inline-flex; align-items: center; gap: $fp-space-3;
color: $fp-ink;
// Icon badge rounded square with tinted background.
// Icon badge - rounded square with tinted background.
// Gives the panel head visual weight without being loud.
> .fa {
display: inline-flex;
@@ -449,7 +449,7 @@
// -------------------------------------------------------------------------
// Empty states friendly, not boxed
// Empty states - friendly, not boxed
// -------------------------------------------------------------------------
.o_fp_empty {
padding: $fp-space-7 $fp-space-4;
@@ -476,7 +476,7 @@
// -------------------------------------------------------------------------
// Queue flat list, big rows, no borders
// Queue - flat list, big rows, no borders
// -------------------------------------------------------------------------
.o_fp_queue_list {
list-style: none; margin: 0; padding: 0;
@@ -701,7 +701,7 @@
}
// -------------------------------------------------------------------------
// S13/S14/S17/S19 recipe chips, predecessor lock, live clock, scrap bar
// S13/S14/S17/S19 - recipe chips, predecessor lock, live clock, scrap bar
// -------------------------------------------------------------------------
.o_fp_active_wo_left {
display: flex; gap: $fp-space-3; align-items: flex-start;

View File

@@ -1,5 +1,5 @@
// =============================================================================
// JobWorkspace full-screen WO surface
// JobWorkspace - full-screen WO surface
// Dark-mode aware via $o-webclient-color-scheme branch.
// =============================================================================
@@ -66,7 +66,7 @@ $_ws-text-hex: #1d1d1f;
}
}
// Hand-Off button gold gradient, prominent, matches plant kanban .handoff
// Hand-Off button - gold gradient, prominent, matches plant kanban .handoff
.o_fp_ws_handoff {
padding: 0.55rem 1.1rem;
font-size: 0.95rem;
@@ -117,7 +117,7 @@ $_ws-text-hex: #1d1d1f;
// ---- WORKFLOW BAR ------------------------------------------------------
// 2026-05-25: bigger step dots + labels + the Next button. Dots were
// 14px with 0.65rem labels too small to read at arm's length.
// 14px with 0.65rem labels - too small to read at arm's length.
.o_fp_ws_bar {
background: $_ws-page-hex;
border-bottom: 1px solid $_ws-border-hex;
@@ -220,7 +220,7 @@ $_ws-text-hex: #1d1d1f;
overflow: hidden;
// Single column on tablets/phones, and make MAIN itself the one scroll
// container the work (steps/receiving) sits at the top, Notes stack
// container - the work (steps/receiving) sits at the top, Notes stack
// below and scroll into view when needed. The old layout kept
// overflow:hidden with two nested auto-height scroll panes, which
// clipped the notes and broke scrolling on narrow screens.
@@ -490,7 +490,7 @@ $_ws-text-hex: #1d1d1f;
// ===== Phone optimization (2026-06-02) ==============================
// Shrink the fixed chrome (header + workflow bar + rail) so the operator
// sees the actual work (receiving / step cards) without scrolling. Notes
// sit below the work in the single scroll column present, scroll for more.
// sit below the work in the single scroll column - present, scroll for more.
@media (max-width: 600px) {
.o_fp_ws_head {
padding: 0.45rem 0.7rem;
@@ -611,7 +611,7 @@ $_ws-text-hex: #1d1d1f;
.o_fp_ws_rcv {
background: $_ws-card-hex;
border: 2px solid #f1c40f; // amber draws receiver's eye
border: 2px solid #f1c40f; // amber - draws receiver's eye
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
@@ -1004,7 +1004,7 @@ $_ws-text-hex: #1d1d1f;
// NOTE: Odoo's backend CSS does NOT define --bs-body-color /
// --bs-secondary-color / --bs-*-bg as custom properties (verified: 0
// definitions in the compiled bundle they're SCSS literals + two
// definitions in the compiled bundle - they're SCSS literals + two
// bundles + [data-bs-theme]). So var(--bs-body-color, #hex) ALWAYS
// resolves to the dark #hex fallback, in light AND dark mode. The fix
// for dialog text is to INHERIT the modal's theme-correct colour (the

View File

@@ -1,5 +1,5 @@
// =============================================================================
// Fusion Plating Manager Desk
// Fusion Plating - Manager Desk
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Shared tokens from _fp_shopfloor_tokens.scss (loaded first in the bundle).
@@ -62,7 +62,7 @@
display: flex; flex-wrap: wrap; gap: $fp-space-3; align-items: center;
}
// Live indicator calm dot that pulses during a fetch
// Live indicator - calm dot that pulses during a fetch
.o_fp_live_dot {
width: 10px; height: 10px;
border-radius: 50%;
@@ -135,7 +135,7 @@
.o_fp_manager_head_actions {
display: flex; gap: $fp-space-2;
// Secondary (default) button plain card with border
// Secondary (default) button - plain card with border
.btn {
min-height: $fp-touch-min;
padding: 0 $fp-space-4;
@@ -157,9 +157,9 @@
&:active { transform: scale(0.97); }
}
// Primary filled with the accent (brand purple), white text. White
// Primary - filled with the accent (brand purple), white text. White
// is correct in BOTH light and dark bundles because $fp-accent is
// the same hue in both it doesn't flip with theme. Force
// the same hue in both - it doesn't flip with theme. Force
// specificity high enough to beat Bootstrap's .btn-primary which
// loads later.
.btn.btn-primary,
@@ -183,7 +183,7 @@
// -------------------------------------------------------------------------
// Flash message reused styling
// Flash message - reused styling
// -------------------------------------------------------------------------
.o_fp_tablet_message {
display: flex; align-items: center; gap: $fp-space-3;
@@ -209,7 +209,7 @@
// -------------------------------------------------------------------------
// KPI strip same language as tablet
// KPI strip - same language as tablet
// -------------------------------------------------------------------------
.o_fp_kpi_strip {
display: grid;
@@ -372,7 +372,7 @@
// -------------------------------------------------------------------------
// MO cards NO borders, depth by shadow + surface tint
// MO cards - NO borders, depth by shadow + surface tint
// -------------------------------------------------------------------------
.o_fp_mgr_card_list {
display: flex; flex-direction: column; gap: $fp-space-3;
@@ -395,7 +395,7 @@
}
}
// Priority stripe (4px) on the left only when priority is set
// Priority stripe (4px) on the left - only when priority is set
&[data-priority="2"] {
background-color: color-mix(in srgb, #{$fp-bad} 4%, $fp-card);
border-color: color-mix(in srgb, #{$fp-bad} 35%, #{$fp-border});
@@ -433,7 +433,7 @@
.o_fp_mgr_card_body {
padding: $fp-space-3 $fp-space-4 $fp-space-4;
display: flex; flex-direction: column; gap: $fp-space-2;
// Subtle inset against the card surface uses the soft surface
// Subtle inset against the card surface - uses the soft surface
// token so it tints correctly in both light and dark bundles.
background-color: $fp-card-soft;
}
@@ -466,7 +466,7 @@
gap: $fp-space-1;
color: $fp-ink;
// Title row kind badge + step name + sequence
// Title row - kind badge + step name + sequence
.o_fp_mgr_step_title {
display: flex;
align-items: center;
@@ -476,7 +476,7 @@
font-size: $fp-text-base;
line-height: 1.25;
}
// Meta row workcenter / role / set equipment
// Meta row - workcenter / role / set equipment
.o_fp_mgr_step_meta {
display: flex;
align-items: center;
@@ -486,7 +486,7 @@
color: $fp-ink-mute;
i { margin-right: 2px; }
}
// Chip row what's still missing for the manager to set
// Chip row - what's still missing for the manager to set
.o_fp_mgr_step_needs {
margin-top: 2px;
}
@@ -508,13 +508,13 @@
.o_fp_mgr_picker {
// box-sizing matters: native <select> defaults to content-box, so
// `width: 100% + padding-right` overflows. border-box keeps the
// picker inside its flex slot the Tank dropdown was bleeding
// picker inside its flex slot - the Tank dropdown was bleeding
// past the card's right edge before this.
box-sizing: border-box;
flex: 1 1 220px; // grows to fill, but stays usable
min-width: 0; // lets flex actually shrink it
min-height: 40px;
// Custom chevron via SVG background controls its position
// Custom chevron via SVG background - controls its position
// exactly (browsers crowd the native chevron right against the
// edge). pe: none on chevron so the click still hits the select.
appearance: none;
@@ -600,7 +600,7 @@
// -------------------------------------------------------------------------
// Team column avatar + name + load
// Team column - avatar + name + load
// -------------------------------------------------------------------------
.o_fp_team_grid {
display: flex; flex-direction: column; gap: $fp-space-2;
@@ -648,7 +648,7 @@
}
// =============================================================================
// Phase 4 tablet redesign Manager dashboard sibling tabs
// Phase 4 tablet redesign - Manager dashboard sibling tabs
// =============================================================================
.o_fp_mgr_tabs {

View File

@@ -1,7 +1,7 @@
// Sub 12b shared SCSS for Move Parts / Move Rack / Rack Parts /
// Sub 12b - shared SCSS for Move Parts / Move Rack / Rack Parts /
// Stop Timer dialogs. Tokens follow the existing fp_shopfloor pattern
// with a dark-mode SCSS @if branch (CLAUDE.md rule for Odoo 19 dark
// mode runtime DOM class flips do not work in 19; we branch at SCSS
// mode - runtime DOM class flips do not work in 19; we branch at SCSS
// compile time on $o-webclient-color-scheme).
$o-webclient-color-scheme: bright !default;
@@ -174,7 +174,7 @@ $fp-md-page: var(--fp-page-bg, #{$_fp_md_page_hex});
}
}
// ============================ Partial-order handling easy-advance layout
// ============================ Partial-order handling - easy-advance layout
// "Send Parts Forward" dialog: destination banner + big-tap qty stepper
// (no keyboard) + collapsed advanced fields. Reuses the $fp-md-* tokens so
// dark mode is handled at compile time.

View File

@@ -1,12 +1,12 @@
// plant_kanban.scss depends on _plant_tokens.scss and the component partials
// plant_kanban.scss - depends on _plant_tokens.scss and the component partials
.o_fp_plant_kanban {
padding: 8px;
background: $plant-bg;
// Fill the Odoo action area (below the navbar) and own the scroll
// internally NOT 100vh. 100vh is taller than the available area by
// internally - NOT 100vh. 100vh is taller than the available area by
// the navbar height, so the board bottom + its horizontal scrollbar
// overflowed off-screen and scrolling broke badly on phones, where
// overflowed off-screen and scrolling broke - badly on phones, where
// Odoo also re-lays-out at the md breakpoint and the scroll gets lost
// up the tree. height:100% + internal overflow is the same pattern
// job_workspace / manager_dashboard / .o_fp_tablet use. flex column so
@@ -39,7 +39,7 @@
.floor-title { font-size: 16px; font-weight: 700; }
.floor-controls { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
// 2026-05-25 toolbar buttons bigger + gradients for visual weight
// 2026-05-25 - toolbar buttons bigger + gradients for visual weight
.station-picker {
padding: 8px 14px;
background: linear-gradient(135deg, $plant-mine-bg 0%, $plant-card-bg 100%);
@@ -102,7 +102,7 @@
color: #5e4400;
font-weight: 700;
}
// Scan pair matched look. "Scan QR" (camera, the primary way to
// Scan pair - matched look. "Scan QR" (camera, the primary way to
// scan a printed job sticker) is accent-filled so it stands out;
// "Enter Code" (manual / hardware scanner-gun) is the accent-tinted
// secondary. Matched FA icons (fa-qrcode / fa-keyboard-o), no emoji.
@@ -122,7 +122,7 @@
}
}
// 8 tiles Work Orders, At My Station, Bakes Due, On Hold,
// 8 tiles - Work Orders, At My Station, Bakes Due, On Hold,
// Awaiting QC, Awaiting CoC, Ready to Ship, Overdue.
.kpi-strip {
display: grid;
@@ -169,7 +169,7 @@
// desktop ~6 columns visible; on a 1366px tablet ~4 visible with
// smooth horizontal scroll. User explicitly accepted side-scrolling.
//
// flex: 1 + min-height: 0 the board fills all remaining vertical
// flex: 1 + min-height: 0 - the board fills all remaining vertical
// space below the sticky header, so the horizontal scrollbar pins
// to the viewport bottom (not mid-page where the board's natural
// height would have ended).
@@ -199,11 +199,11 @@
}
}
// Each .col is now a proper bordered card that runs full board
// height same visual treatment as Trello / Asana columns. The
// height - same visual treatment as Trello / Asana columns. The
// header (.o_fp_col_header) sits at the top with a divider; the
// scrollable body (.col-scroll) takes the rest. Previously .col
// had bg = page-bg (invisible) so columns visually ended at the
// header card empty columns looked unbounded.
// header card - empty columns looked unbounded.
.col {
background: $plant-card-bg;
border: 1px solid $plant-card-border;
@@ -220,7 +220,7 @@
}
.col-scroll {
flex: 1 1 auto;
min-height: 0; // critical without this the children
min-height: 0; // critical - without this the children
// push the column past its parent height
overflow-y: auto;
display: flex;
@@ -277,7 +277,7 @@
}
}
// ===== Responsive phones / small screens (2026-06-02) ==============
// ===== Responsive - phones / small screens (2026-06-02) ==============
// Compact the header so the board keeps the screen, and make the toolbar
// controls full-width + tappable (>=40px) on a phone.
.o_fp_plant_kanban {

View File

@@ -1,5 +1,5 @@
// =============================================================================
// Fusion Plating Plant Overview (Kanban)
// Fusion Plating - Plant Overview (Kanban)
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Kanban of work orders grouped by work centre. Cards have BOTH a
@@ -91,7 +91,7 @@
@include fp-focus-ring;
border-color: $fp-accent;
}
// Tablet keep it generously sized but cap so the toolbar
// Tablet - keep it generously sized but cap so the toolbar
// doesn't blow past the viewport.
@media (max-width: 1180px) { width: 320px; min-height: 48px; }
@media (max-width: 900px) { width: 100%; }
@@ -157,7 +157,7 @@
border-radius: $fp-radius-lg;
box-shadow: $fp-elev-1;
max-height: calc(100vh - 180px);
// Keep overflow: hidden at ALL sizes it's what clips the rounded
// Keep overflow: hidden at ALL sizes - it's what clips the rounded
// corners cleanly. On mobile we just lift the max-height so the
// column sizes to content and doesn't need internal scroll.
overflow: hidden;
@@ -167,7 +167,7 @@
min-width: 100%;
max-width: 100%;
max-height: none;
// overflow: hidden stays corners stay rounded. Since the
// overflow: hidden stays - corners stay rounded. Since the
// content body no longer has overflow-y: auto on mobile (see
// below), nothing is clipped and the parent page scrolls.
}
@@ -222,7 +222,7 @@
}
}
// Insertion placeholder a live DOM node inserted between cards as
// Insertion placeholder - a live DOM node inserted between cards as
// the cursor moves so the manager sees exactly where the drop will
// slot in. 4px solid accent bar + small glow + smooth slide.
.o_fp_po_drop_placeholder {
@@ -253,7 +253,7 @@
background-color: $fp-card;
border: 1px solid #{$fp-border};
border-radius: $fp-radius-md;
// Clip children to the rounded corners this is what makes the
// Clip children to the rounded corners - this is what makes the
// priority stripe (inside via ::before) curve with the card.
// Shadows are painted outside the content box so they render fine.
overflow: hidden;
@@ -261,7 +261,7 @@
margin-bottom: $fp-space-2;
cursor: grab;
box-shadow: $fp-elev-1;
// Allow vertical page-scroll gestures on touch without this
// Allow vertical page-scroll gestures on touch - without this
// a draggable card can swallow the touch and block scrolling.
// Desktop drag (mousedown) still works.
touch-action: pan-y;
@@ -285,7 +285,7 @@
box-shadow: $fp-elev-3;
}
// Priority left bar lives inside the card's overflow: hidden
// Priority left bar - lives inside the card's overflow: hidden
// so it gets clipped to the rounded corners automatically.
&::before {
content: "";
@@ -307,7 +307,7 @@
display: flex; align-items: center; gap: $fp-space-2;
margin-bottom: $fp-space-1;
// Small customer avatar 32px thumbnail. Just identifies the
// Small customer avatar - 32px thumbnail. Just identifies the
// customer at a glance; not a billboard.
.o_fp_po_card_avatar {
flex-shrink: 0;
@@ -355,7 +355,7 @@
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
// Parts progress bar a real indicator of where the job is
// Parts progress bar - a real indicator of where the job is
.o_fp_po_card_parts {
margin: $fp-space-2 0;
}
@@ -459,7 +459,7 @@ $_fp-urg-warn-bg-alpha: 0.20;
}
}
// HOT band gets the fattest treatment solid red fill, white text.
// HOT band gets the fattest treatment - solid red fill, white text.
// Overrides the danger tone above so this band can't fade into the
// other danger chips.
.o_fp_po_card_urgency.o_fp_po_urg_hot {
@@ -479,7 +479,7 @@ $_fp-urg-warn-bg-alpha: 0.20;
// Replaces the always-identical "[FP-SERVICE] Plating Service" line with the
// part number + coating spec the operator actually cares about. Both lines
// rely on $fp-ink / $fp-ink-mute tokens so they flip cleanly between the
// light and dark bundles no hard-coded hex.
// light and dark bundles - no hard-coded hex.
.o_fp_po_card_part {
display: flex;
align-items: center;
@@ -524,7 +524,7 @@ $_fp-urg-warn-bg-alpha: 0.20;
margin-bottom: $fp-space-2;
}
// Step-ordinal badge separator + total in mute tone (1-based "4/9").
// Step-ordinal badge - separator + total in mute tone (1-based "4/9").
.o_fp_po_card_step_total {
font-weight: $fp-weight-medium;
color: $fp-ink-faint;
@@ -535,13 +535,13 @@ $_fp-urg-warn-bg-alpha: 0.20;
// Live-ticking elapsed-in-stage label. JS getCardTimer() picks the tone
// (muted/ok/warning/danger) and a `critical` flag that toggles the pulse
// animation. Critical = step is overrun (>1.5× expected), paused >24h, or
// queued >24h any of those conditions need supervisor attention NOW.
// queued >24h - any of those conditions need supervisor attention NOW.
//
// Light/dark mode: warning text needs different hex per bundle so it
// stays legible against the translucent yellow tint. Other tones use
// $fp-* tokens or --bs-* CSS vars which Odoo flips automatically.
$_fp-timer-warn-text-hex: #856404; // dark brown readable on light card
$_fp-timer-warn-text-hex: #856404; // dark brown - readable on light card
$_fp-timer-warn-bg-alpha: 0.20;
@if $o-webclient-color-scheme == dark {
$_fp-timer-warn-text-hex: #ffda6a !global; // light yellow on dark card
@@ -560,7 +560,7 @@ $_fp-timer-warn-bg-alpha: 0.20;
i { font-size: 11px; }
// Tones backgrounds use rgba() with a low alpha so the underlying
// Tones - backgrounds use rgba() with a low alpha so the underlying
// card surface tints through; text uses the strong hue.
&.o_fp_po_timer_muted {
background: $fp-card-soft;
@@ -599,12 +599,12 @@ $_fp-timer-warn-bg-alpha: 0.20;
}
}
// Critical card border (v19.0.24.11.0) class-based, NOT `:has()`.
// Critical card border (v19.0.24.11.0) - class-based, NOT `:has()`.
// `:has()` re-evaluates on every layout pass; with 389 cards on screen
// and the browser doing constant layout work during drag, that selector
// was the actual reason drag-drop felt frozen for 5+ seconds. The
// server now flags critical cards with `is_urgent=true` and the OWL
// template adds `.o_fp_po_card_critical` directly zero selector cost.
// template adds `.o_fp_po_card_critical` directly - zero selector cost.
.o_fp_po_card_critical {
box-shadow: $fp-elev-2,
0 0 0 2px rgba(220, 53, 69, 0.55),
@@ -613,7 +613,7 @@ $_fp-timer-warn-bg-alpha: 0.20;
// While a drag is in progress, pause infinite keyframe animations on
// the few cards that have them (chip pulses). We INTENTIONALLY do NOT
// touch transitions here the previous version used `* { transition:
// touch transitions here - the previous version used `* { transition:
// none !important }` which forced the browser to recalculate styles
// on every descendant (~12,000 elements at 389 cards) on every drop,
// and that style-recalc *was* the bottleneck the user was feeling

View File

@@ -1,5 +1,5 @@
// =============================================================================
// Fusion Plating Process Tree (horizontal hierarchical, v3, 2026-04)
// Fusion Plating - Process Tree (horizontal hierarchical, v3, 2026-04)
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Hierarchical bracket tree:
@@ -45,7 +45,7 @@ $pt-line-width : 2px;
background-color: $fp-page;
color: $fp-ink;
height: 100%;
overflow: auto; // both axes wide trees scroll horizontally
overflow: auto; // both axes - wide trees scroll horizontally
-webkit-overflow-scrolling: touch;
padding: $fp-space-4 $fp-space-5;
display: flex;
@@ -134,7 +134,7 @@ $pt-line-width : 2px;
// -------------------------------------------------------------------------
// Tree canvas horizontally scrollable
// Tree canvas - horizontally scrollable
// -------------------------------------------------------------------------
.o_fp_pt_canvas {
padding: $fp-space-3 0;
@@ -143,7 +143,7 @@ $pt-line-width : 2px;
// -------------------------------------------------------------------------
// Recursive node flex row of [card | children-column]
// Recursive node - flex row of [card | children-column]
// -------------------------------------------------------------------------
.o_fp_pt_node {
display: flex;
@@ -212,7 +212,7 @@ $pt-line-width : 2px;
// ---- Live state highlight ----------------------------------------
&.o_fp_pt_state_progress,
&.o_fp_pt_highlight.o_fp_pt_state_progress {
background-color: #c0392b; // warm red active step
background-color: #c0392b; // warm red - active step
color: #fff;
box-shadow: 0 0 0 1px rgba(192, 57, 43, .6),
0 4px 14px rgba(192, 57, 43, .35);
@@ -228,7 +228,7 @@ $pt-line-width : 2px;
background-color: #1e8449; // green for completed slice
color: #fff;
}
// Operation / step cards that have completed green fill + subtle
// Operation / step cards that have completed - green fill + subtle
// pulsing glow so finished work pops against the still-pending dark
// cards. Animation pauses on hover so the click target is steady.
&.o_fp_pt_state_done.o_fp_pt_type_operation,
@@ -399,15 +399,15 @@ $pt-line-width : 2px;
z-index: 0;
}
// First child vertical only from card centre → bottom of row
// First child - vertical only from card centre → bottom of row
&:first-child::after {
top: calc(#{$pt-card-h} / 2);
}
// Last child vertical only from top of row → card centre
// Last child - vertical only from top of row → card centre
&:last-child::after {
bottom: calc(100% - (#{$pt-card-h} / 2));
}
// Only child vertical only at the card centre point (just enough
// Only child - vertical only at the card centre point (just enough
// to render the elbow connecting to the parent stub)
&:first-child:last-child::after {
top: calc(#{$pt-card-h} / 2);

View File

@@ -1,5 +1,5 @@
// =============================================================================
// Fusion Plating Reusable QR Scanner Modal
// Fusion Plating - Reusable QR Scanner Modal
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Mobile-first modal that overlays the page. The video element fills
@@ -27,7 +27,7 @@
border-radius: $fp-radius-lg;
box-shadow: $fp-elev-3;
// Wrap min() in #{...} so dart-sass doesn't try to compute it at
// compile time (it can't combine 420px and 92vw the clamp/min
// compile time (it can't combine 420px and 92vw - the clamp/min
// functions are CSS-runtime, not SCSS). Pass through verbatim.
width: #{"min(420px, 92vw)"};
max-width: 92vw;

View File

@@ -1,5 +1,5 @@
// =====================================================================
// FpTabletLock lock screen with tile grid + PIN pad overlay
// FpTabletLock - lock screen with tile grid + PIN pad overlay
// 2026-05-24 redesign: hybrid Industrial Bold + Premium Glassmorphism
// Spec: docs/superpowers/specs/2026-05-24-tablet-lock-screen-redesign-design.md
// Depends on _tablet_lock_tokens.scss being loaded first.
@@ -30,7 +30,7 @@
.o_fp_lock_logo_frame {
display: inline-flex; align-items: center; justify-content: center;
// Rectangle (wider than tall) fits horizontal company logos
// Rectangle (wider than tall) - fits horizontal company logos
// (mark + name + tagline laid out left-to-right) without leaving
// dead space top/bottom. Uniform 18px padding on all sides so the
// image breathes evenly.
@@ -232,9 +232,9 @@
}
// =====================================================================
// Spec 2026-05-25 PIN self-service wizard screens
// Spec 2026-05-25 - PIN self-service wizard screens
// (request_code / enter_temp_code / set_new_pin / confirm_new_pin)
// Reuses $lock-* tokens from _tablet_lock_tokens.scss dark mode
// Reuses $lock-* tokens from _tablet_lock_tokens.scss - dark mode
// auto-flips via the existing $o-webclient-color-scheme branch.
// =====================================================================
@@ -329,7 +329,7 @@
}
}
// ===== Responsive phones / small screens (2026-06-02) ==============
// ===== Responsive - phones / small screens (2026-06-02) ==============
// The lock screen is position:fixed + overflow-y:auto, so it already
// scrolls; it just needs the 5-up operator grid + chrome to step down
// on narrow screens. Ordered descending max-width so the smaller query

View File

@@ -1,5 +1,5 @@
// =============================================================================
// Fusion Plating Tank Status (NFC tap-to-view)
// Fusion Plating - Tank Status (NFC tap-to-view)
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Mobile-first stylesheet for /fp/tank/<id>. Renders inside
@@ -116,7 +116,7 @@
padding: $fp-space-3 0;
}
// State / status pills use the same translucent-tint pattern as the
// State / status pills - use the same translucent-tint pattern as the
// other shop-floor surfaces so they read at a glance on a phone.
.o_fp_state_badge {
padding: 2px 8px;
@@ -154,7 +154,7 @@
}
}
// Bath chemistry grid one cell per parameter reading.
// Bath chemistry grid - one cell per parameter reading.
.o_fp_tank_chem_grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));

View File

@@ -7,7 +7,7 @@
<div class="o_fp_gate_body">
<div class="o_fp_gate_title">Can't start yet</div>
<div class="o_fp_gate_reason">
<t t-esc="props.blockerReason or 'Reason unknown open the step in the back-office.'"/>
<t t-esc="props.blockerReason or 'Reason unknown - open the step in the back-office.'"/>
</div>
</div>
<button t-if="props.jumpTargetModel and props.jumpTargetId and props.onJump"

View File

@@ -16,7 +16,7 @@
<div class="o_fp_pin_grid">
<!-- IMPORTANT: digits MUST be string literals here.
OWL templates only expose `Math` as a JS global
OWL templates only expose `Math` as a JS global -
`String`, `Number`, `Array`, etc. are NOT in template
scope. Calling `String(d)` throws "v2 is not a
function" because the compiled template references

View File

@@ -4,7 +4,7 @@
<t t-name="fusion_plating_shopfloor.RackingPanel">
<div class="o_fp_racking_panel" t-if="state.data">
<div class="o_fp_rkp_head">
<span class="o_fp_rkp_title">🧰 Racking split across racks</span>
<span class="o_fp_rkp_title">🧰 Racking - split across racks</span>
<span t-att-class="'o_fp_rkp_unassigned' + (state.data.unassigned ? ' has' : '')">
Unassigned: <t t-esc="state.data.unassigned"/> / <t t-esc="state.data.total"/>
</span>

View File

@@ -10,7 +10,7 @@
<strong t-esc="props.stepName"/>
</div>
<!-- Orphan step (NULL recipe link) different copy -->
<!-- Orphan step (NULL recipe link) - different copy -->
<t t-if="props.orphanStep">
<div class="o_fp_finish_block_msg">
This step has <strong>no recipe link</strong> (the source
@@ -20,7 +20,7 @@
</div>
<div class="o_fp_finish_block_action_note">
<i class="fa fa-user-md me-1"/>
Escalate to a manager they can bypass with an
Escalate to a manager - they can bypass with an
audit-chatter entry.
</div>
</t>

View File

@@ -15,14 +15,14 @@
<t t-if="state.data">
<!-- =========================================================
STICKY HEADER WO context, qty bumps, workflow chip
STICKY HEADER - WO context, qty bumps, workflow chip
========================================================= -->
<header class="o_fp_ws_head">
<div class="o_fp_ws_head_l">
<button class="btn btn-link o_fp_ws_back" t-on-click="onBack">
<i class="fa fa-arrow-left"/> Back
</button>
<!-- Phase 6.2 Hand-Off: lock the tablet -->
<!-- Phase 6.2 - Hand-Off: lock the tablet -->
<button class="btn btn-warning o_fp_ws_handoff ms-2"
t-on-click="handOff"
title="Lock the tablet for the next operator">
@@ -53,7 +53,7 @@
</header>
<!-- =========================================================
STICKY WORKFLOW BAR milestone dots + Next button
STICKY WORKFLOW BAR - milestone dots + Next button
========================================================= -->
<div class="o_fp_ws_bar">
<div class="o_fp_ws_bar_line">
@@ -73,7 +73,7 @@
</div>
<!-- =========================================================
MAIN step list (left/center) + side panel (right)
MAIN - step list (left/center) + side panel (right)
========================================================= -->
<div class="o_fp_ws_main">
@@ -128,7 +128,7 @@
<div class="o_fp_ws_rcv_line_part">
<strong t-esc="ln.part_number or 'Part'"/>
<span t-if="ln.description" class="text-muted">
<t t-esc="ln.description"/>
- <t t-esc="ln.description"/>
</span>
</div>
<div class="o_fp_ws_rcv_line_qty">
@@ -248,7 +248,7 @@
<t t-foreach="state.data.shipping.not_ready"
t-as="nr" t-key="nr.wo_name">
<span class="o_fp_chip o_fp_chip_warning">
<t t-esc="nr.wo_name"/> <t t-esc="nr.state_label"/>
<t t-esc="nr.wo_name"/> - <t t-esc="nr.state_label"/>
</span>
</t>
</div>
@@ -259,7 +259,7 @@
<label>Carrier
<select class="form-select"
t-on-change="(ev) => this.onShipInput('carrier_id', ev)">
<option value=""> pick carrier </option>
<option value="">- pick carrier -</option>
<t t-foreach="state.data.shipping.carrier_options"
t-as="c" t-key="c.id">
<option t-att-value="c.id"
@@ -341,7 +341,7 @@
<!-- NON-TERMINAL: read-ahead detail (chips + instructions + opt-out + GateViz) -->
<t t-if="!['done', 'skipped', 'cancelled'].includes(step.state)">
<div class="o_fp_ws_step_detail">
<!-- Multi-rack split embedded in the Racking step's row. -->
<!-- Multi-rack split - embedded in the Racking step's row. -->
<RackingPanel t-if="step.is_racking" jobId="state.jobId"/>
<!-- Recipe chips: visible on every non-done step so operator reads ahead -->
<div class="o_fp_ws_step_chips"
@@ -369,12 +369,12 @@
<t t-esc="step.instructions"/>
</div>
<!-- Masking reference(s) attached at order entry; tap to enlarge -->
<!-- Masking reference(s) - attached at order entry; tap to enlarge -->
<div t-if="step.masking_refs and step.masking_refs.length"
class="o_fp_ws_mask_refs">
<div class="o_fp_ws_mask_refs_label">
<i class="fa fa-paint-brush"/>
Masking reference<t t-if="step.masking_refs.length > 1">s</t> tap to enlarge
Masking reference<t t-if="step.masking_refs.length > 1">s</t> - tap to enlarge
</div>
<div class="o_fp_ws_mask_refs_grid">
<t t-foreach="step.masking_refs" t-as="ref" t-key="ref.id">
@@ -467,7 +467,7 @@
</div>
<!-- =========================================================
STICKY ACTION RAIL Hold · Note · Cert · Milestone
STICKY ACTION RAIL - Hold · Note · Cert · Milestone
========================================================= -->
<footer class="o_fp_ws_rail">
<button class="btn btn-warning" t-on-click="onCreateHold">

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc. · License OPL-1
Fusion Plating Manager Desk
Fusion Plating - Manager Desk
Native fp.job / fp.job.step edition. Speaks job/step end-to-end.
-->
<templates xml:space="preserve">
@@ -26,13 +26,13 @@
</div>
</div>
<div class="o_fp_manager_head_actions">
<!-- Presence chip clocked-in workers vs roster.
<!-- Presence chip - clocked-in workers vs roster.
Tap to toggle whether off-shift names show in
the worker dropdowns. -->
<button class="btn o_fp_presence_chip"
t-att-data-active="state.hideOffShift ? 'y' : 'n'"
t-on-click="toggleOffShift"
t-att-title="state.hideOffShift ? 'Showing only clocked-in workers click to include off-shift' : 'Showing all workers click to hide off-shift'"
t-att-title="state.hideOffShift ? 'Showing only clocked-in workers - click to include off-shift' : 'Showing all workers - click to hide off-shift'"
t-if="state.overview and state.overview.presence">
<span class="o_fp_presence_dot"/>
Present
@@ -47,7 +47,7 @@
<i t-att-class="'fa fa-refresh' + (state.isFetching ? ' fa-spin' : '')"/>
</button>
<QrScanner cssClass="'btn'"/>
<!-- Phase 6.2 Hand-Off: lock the tablet -->
<!-- Phase 6.2 - Hand-Off: lock the tablet -->
<button class="btn btn-warning"
t-on-click="handOff"
title="Lock the tablet for the next operator">
@@ -98,7 +98,7 @@
<div class="o_fp_kpi_value"><t t-esc="state.overview.kpis.pending_accept_sos"/></div>
<div class="o_fp_kpi_label">Awaiting Assignment</div>
</div>
<!-- v19.0.24.3.0 compliance + floor-health KPIs. -->
<!-- v19.0.24.3.0 - compliance + floor-health KPIs. -->
<!-- Hidden when 0 to keep the strip clean; light up -->
<!-- when something needs attention. Click to drill. -->
<div class="o_fp_kpi o_fp_kpi_danger"
@@ -161,7 +161,7 @@
</t>
</div>
</div>
<!-- Phase 4 tablet redesign Pending Cert + At-Risk tiles -->
<!-- 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')">
@@ -279,7 +279,7 @@
<div class="o_fp_mgr_step_actions">
<select class="o_fp_mgr_picker"
t-on-change="(ev) => this.onAssignWorker(step, ev.target.value)">
<option value=""> Assign worker </option>
<option value="">- Assign worker -</option>
<t t-foreach="operatorsForStep(step)" t-as="op" t-key="op.id">
<option t-att-value="op.id"
t-att-selected="step.assigned_user_id === op.id"
@@ -293,7 +293,7 @@
<select t-if="step.kind === 'wet'"
class="o_fp_mgr_picker"
t-on-change="(ev) => this.onAssignTank(step, ev.target.value)">
<option value=""> Tank </option>
<option value="">- Tank -</option>
<t t-foreach="state.overview.tanks" t-as="tnk" t-key="tnk.id">
<option t-att-value="tnk.id"
t-att-selected="step.tank_id === tnk.id">
@@ -450,7 +450,7 @@
<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>
<span t-if="!stage.jobs.length" class="o_fp_funnel_empty">-</span>
</div>
</div>
</t>

View File

@@ -5,7 +5,7 @@
<Dialog title.translate="Send Parts Forward" size="'md'">
<div class="o_fp_move_dialog" t-if="!state.loading">
<!-- Destination banner operator sees exactly where parts go,
<!-- Destination banner - operator sees exactly where parts go,
nothing to guess. -->
<div class="o_fp_move_route">
<span class="route-from" t-esc="state.fromStep.name"/>
@@ -13,7 +13,7 @@
<span class="route-to" t-esc="state.toStep.name"/>
</div>
<!-- Qty stepper no keyboard. Defaults to all parked here. -->
<!-- Qty stepper - no keyboard. Defaults to all parked here. -->
<div class="o_fp_move_qty">
<label>How many to send?</label>
<div class="o_fp_qty_stepper">
@@ -29,7 +29,7 @@
<span class="o_fp_qty_hint"><t t-esc="state.qtyAvailable"/> parked here</span>
</div>
<!-- To Station (tank) only when the recipe offers a choice -->
<!-- To Station (tank) - only when the recipe offers a choice -->
<div class="o_fp_move_field"
t-if="state.toStep.tank_options and state.toStep.tank_options.length > 1">
<label>To Station</label>
@@ -40,7 +40,7 @@
</select>
</div>
<!-- Compliance prompts only when the recipe author required
<!-- Compliance prompts - only when the recipe author required
them. Pickers/checkboxes, minimal free text. -->
<div class="o_fp_compliance_prompts" t-if="state.transitionPrompts.length">
<h5>Required before sending</h5>
@@ -60,7 +60,7 @@
type="datetime-local" t-model="state.promptValues[p.id]"/>
<select t-elif="p.input_type === 'selection'"
t-model="state.promptValues[p.id]">
<option value=""> Select </option>
<option value="">- Select -</option>
<t t-foreach="p.selection_options.split(',')"
t-as="opt" t-key="opt_index">
<option t-att-value="opt.trim()"><t t-esc="opt.trim()"/></option>
@@ -71,7 +71,7 @@
</t>
</div>
<!-- Blockers inline resolve where possible -->
<!-- Blockers - inline resolve where possible -->
<div class="o_fp_blockers" t-if="state.blockers.length">
<t t-foreach="state.blockers" t-as="b" t-key="b_index">
<div class="o_fp_blocker_row"
@@ -87,7 +87,7 @@
</t>
</div>
<!-- More options (advanced) hold / scrap / rework / location.
<!-- More options (advanced) - hold / scrap / rework / location.
Collapsed by default so the everyday "advance all" flow is
a qty confirm + SEND. -->
<div class="o_fp_move_advanced_toggle">

View File

@@ -26,7 +26,7 @@
<!-- "Scan QR" = the QrScanner camera path (the
primary way to scan a printed job sticker).
The component renders its own fa-qrcode
icon, so the label must be plain text an
icon, so the label must be plain text - an
emoji here would double up the icon.
"Enter Code" = the manual / hardware-scanner-
gun text drawer (a wedge gun types the code;
@@ -76,7 +76,7 @@
kind="'qc'"
active="!!state.filters.awaiting_qc"
onClick="() => this.toggleFilter('awaiting_qc')"/>
<!-- Spec 2026-05-25 post-shop state tiles -->
<!-- Spec 2026-05-25 - post-shop state tiles -->
<FpKpiTile value="state.data.kpis.awaiting_cert"
label="'Awaiting CoC'"
kind="'warn'"
@@ -121,7 +121,7 @@
<FpFilterChip label="'Awaiting QC'"
active="!!state.filters.awaiting_qc"
onToggle="() => this.toggleFilter('awaiting_qc')"/>
<!-- Spec 2026-05-25 post-shop state chips -->
<!-- Spec 2026-05-25 - post-shop state chips -->
<FpFilterChip label="'Awaiting CoC'"
active="!!state.filters.awaiting_cert"
onToggle="() => this.toggleFilter('awaiting_cert')"/>
@@ -140,7 +140,7 @@
<t t-foreach="filteredCardIds(col)" t-as="card_id" t-key="card_id">
<FpPlantCard card="state.data.cards[card_id]"/>
</t>
<div t-if="filteredCardIds(col).length === 0" class="col-empty"></div>
<div t-if="filteredCardIds(col).length === 0" class="col-empty">-</div>
</div>
</div>
</t>

View File

@@ -7,7 +7,7 @@
<templates xml:space="preserve">
<!-- Per-card timer chip (v19.0.24.10.0). Subcomponent so each chip -->
<!-- has its own reactive ticker a 5s tick re-renders only that -->
<!-- has its own reactive ticker - a 5s tick re-renders only that -->
<!-- chip instead of the entire 389-card board. -->
<t t-name="fusion_plating_shopfloor.TimerChip">
<div t-if="display.label"
@@ -72,7 +72,7 @@
<p class="mt-3 text-muted">No work centres with active orders found.</p>
</div>
<!-- ========== Sub 12b RACKS PANE ========== -->
<!-- ========== Sub 12b - RACKS PANE ========== -->
<!-- Top section above the work-centre columns. Shows racks
currently in (loaded / in_use / awaiting_unrack) state
with tag chips, part count, current node breadcrumb,
@@ -167,7 +167,7 @@
</span>
</div>
<!-- Urgency chip (v19.0.24.8.0) always -->
<!-- Urgency chip (v19.0.24.8.0) - always -->
<!-- visible. Explains WHY the card is at -->
<!-- this position in the sort. Critical -->
<!-- bands (HOT, OVERDUE, MISSED BAKE) -->
@@ -230,7 +230,7 @@
</div>
<!-- Per-step timer (v19.0.24.10.0) -->
<!-- TimerChip subcomponent owns its -->
<!-- TimerChip subcomponent - owns its -->
<!-- own tick so a refresh re-renders one -->
<!-- chip, not the whole board. -->
<TimerChip

View File

@@ -4,7 +4,7 @@
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Process Tree horizontal hierarchical view.
Process Tree - horizontal hierarchical view.
Recursive template renders the recipe → sub-process → operation → step
hierarchy with bracket connectors between cards. Active step pulses.
-->
@@ -65,7 +65,7 @@
</div>
</div>
<!-- Children recurse -->
<!-- Children - recurse -->
<div class="o_fp_pt_children" t-if="node.children and node.children.length">
<t t-foreach="node.children" t-as="child" t-key="child.id">
<t t-call="fusion_plating_shopfloor.ProcessNode">

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc. · License OPL-1
Fusion Plating Reusable QR Scanner template
Fusion Plating - Reusable QR Scanner template
The video element is rendered whenever ANY decoder is available
(state.canScan = native BarcodeDetector OR vendored jsQR). The
@@ -41,7 +41,7 @@
with capture=environment opens the iOS / Android
camera UI directly and returns a JPEG when the
user taps the shutter. We then run ONE decode
on that high-quality still far more reliable
on that high-quality still - far more reliable
on iOS than the live-video path. -->
<div class="o_fp_qr_photo_row">
<label class="btn btn-outline-secondary o_fp_qr_photo_btn">

View File

@@ -18,7 +18,7 @@
<div class="o_fp_move_field">
<label/>
<select t-model.number="state.selectedRackId">
<option value=""> Select empty rack </option>
<option value="">- Select empty rack -</option>
<t t-foreach="state.racks" t-as="r" t-key="r.id">
<option t-att-value="r.id">
<t t-esc="r.name"/> (<t t-esc="r.rack_type"/>)

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc. · License OPL-1
Fusion Plating Tablet Station (Worker view)
Fusion Plating - Tablet Station (Worker view)
Rebuilt 2026-04 with the shop-floor design system.
-->
<templates xml:space="preserve">
@@ -31,7 +31,7 @@
<select class="o_fp_station_picker"
t-on-change="onPickStation"
t-if="state.overview">
<option value=""> Pick station </option>
<option value="">- Pick station -</option>
<t t-foreach="state.overview.stations" t-as="s" t-key="s.id">
<option t-att-value="s.id"
t-att-selected="state.stationId === s.id">
@@ -156,7 +156,7 @@
</button>
<button class="btn btn-outline-danger"
t-on-click="() => this.onBumpScrap(this.state.overview.active_wo.mo_id)"
title="Record one scrap part auto-creates a Hold">
title="Record one scrap part - auto-creates a Hold">
<i class="fa fa-trash"/> Scrap
</button>
<button class="o_fp_big_button"
@@ -178,7 +178,7 @@
</div>
<div t-if="!state.overview.my_queue.length" class="o_fp_empty">
<i class="fa fa-check-circle text-success"/>
<div>All caught up nothing waiting on you.</div>
<div>All caught up - nothing waiting on you.</div>
</div>
<ul class="o_fp_queue_list" t-if="state.overview.my_queue.length">
<t t-foreach="state.overview.my_queue" t-as="row" t-key="row.id">
@@ -203,15 +203,15 @@
</t>
</div>
<div class="o_fp_queue_desc"><t t-esc="row.description"/></div>
<!-- S14 predecessor block notice. Replaces -->
<!-- S14 - predecessor block notice. Replaces -->
<!-- the green Start with a clear "wait for X". -->
<div class="o_fp_queue_blocked_msg"
t-if="row.predecessor_blocked">
<i class="fa fa-lock"/>
Awaiting <strong t-esc="row.blocked_by_name"/>
finish that step first
- finish that step first
</div>
<!-- S13 recipe-author chips inline -->
<!-- S13 - recipe-author chips inline -->
<div class="o_fp_queue_chips"
t-if="row.thickness_target or row.dwell_time_minutes or row.bake_setpoint_temp or row.requires_signoff">
<span class="o_fp_chip o_fp_chip_info"
@@ -232,7 +232,7 @@
Sign-off
</span>
</div>
<!-- S13 instructions snippet (first 120 chars) -->
<!-- S13 - instructions snippet (first 120 chars) -->
<div class="o_fp_queue_instructions"
t-if="row.instructions">
<t t-esc="row.instructions.length > 120 ? row.instructions.slice(0,120) + '…' : row.instructions"/>
@@ -278,7 +278,7 @@
<div class="o_fp_tile"
t-on-click="() => this.openRecord('fusion.plating.bath', b.id)">
<div class="o_fp_tile_title"><t t-esc="b.name"/></div>
<div class="o_fp_tile_meta">Tank <t t-esc="b.tank || ''"/></div>
<div class="o_fp_tile_meta">Tank <t t-esc="b.tank || '-'"/></div>
<div class="o_fp_tile_chips">
<span t-att-class="'o_fp_chip o_fp_chip_' + stateBadge(b.state)">
<t t-esc="b.state"/>
@@ -308,7 +308,7 @@
<div class="o_fp_bake_main">
<div class="o_fp_bake_name">
<t t-esc="bw.name"/>
<span class="text-muted ms-1"> <t t-esc="bw.part_ref"/></span>
<span class="text-muted ms-1"> - <t t-esc="bw.part_ref"/></span>
</div>
<div class="o_fp_bake_meta">
<t t-esc="bw.customer"/>
@@ -344,13 +344,13 @@
<!-- First-piece gate panel retired with the fp.first.piece.gate
model removal (19.0.33.2.0). The feature was never wired
up manual create, no enforcement, no rows in production. -->
up - manual create, no enforcement, no rows in production. -->
<!-- ===== Pending QC banner (S19 follow-up) ===== -->
<!-- Shows whenever Carlos's job has an open QC. Tap -->
<!-- the QC name to deep-link straight into Lisa's -->
<!-- mobile checklist. Without this Carlos doesn't -->
<!-- know to call inspection QC sits in draft. -->
<!-- know to call inspection - QC sits in draft. -->
<section class="o_fp_panel"
t-if="state.overview.pending_qcs and state.overview.pending_qcs.length">
<div class="o_fp_panel_head">
@@ -366,7 +366,7 @@
<div class="o_fp_bake_name">
<t t-esc="qc.name"/>
<span class="text-muted ms-1">
<t t-esc="qc.template_name"/>
- <t t-esc="qc.template_name"/>
</span>
</div>
<div class="o_fp_bake_meta">
@@ -403,7 +403,7 @@
<div class="o_fp_bake_main">
<div class="o_fp_bake_name">
<t t-esc="h.name"/>
<span class="text-muted ms-1"> <t t-esc="h.part_ref"/></span>
<span class="text-muted ms-1"> - <t t-esc="h.part_ref"/></span>
</div>
<div class="o_fp_bake_meta">
Qty <t t-esc="h.qty"/>

View File

@@ -65,7 +65,7 @@
</div>
<div t-else="" class="o_fp_lock_pinwrap">
<!-- Mode: 'pin' default keypad for users with PIN -->
<!-- Mode: 'pin' - default keypad for users with PIN -->
<t t-if="state.mode === 'pin'">
<FpPinPad onSubmit.bind="unlock"
title="_selectedTileName()"
@@ -78,7 +78,7 @@
</button>
</t>
<!-- Mode: 'request_code' Send Temp PIN screen -->
<!-- Mode: 'request_code' - Send Temp PIN screen -->
<t t-elif="state.mode === 'request_code'">
<div class="o_fp_lock_wizard">
<h3 t-esc="_selectedTileName()"/>
@@ -100,7 +100,7 @@
</div>
</t>
<!-- Mode: 'enter_temp_code' 4-cell pad for emailed code -->
<!-- Mode: 'enter_temp_code' - 4-cell pad for emailed code -->
<t t-elif="state.mode === 'enter_temp_code'">
<div class="o_fp_lock_wizard">
<h3 t-esc="_selectedTileName()"/>
@@ -124,7 +124,7 @@
</div>
</t>
<!-- Mode: 'set_new_pin' 4-cell pad to choose new PIN -->
<!-- Mode: 'set_new_pin' - 4-cell pad to choose new PIN -->
<t t-elif="state.mode === 'set_new_pin'">
<FpPinPad onSubmit.bind="onNewPinSubmit"
title="_selectedTileName()"
@@ -132,7 +132,7 @@
onCancel.bind="onPinCancel"/>
</t>
<!-- Mode: 'confirm_new_pin' 4-cell pad to confirm -->
<!-- Mode: 'confirm_new_pin' - 4-cell pad to confirm -->
<t t-elif="state.mode === 'confirm_new_pin'">
<FpPinPad onSubmit.bind="onConfirmNewPinSubmit"
title="_selectedTileName()"