From 9d78bc4317965cb93c9c47d0e9391c80c5d1313e Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 22 May 2026 21:46:46 -0400 Subject: [PATCH] feat(fusion_plating_shopfloor): SignaturePad shared OWL service Plan task P1.5. Modal canvas signature capture using HTML pointer events + Odoo Dialog service. Returns image/png dataURI via onSubmit callback; caller decides what to do with it (e.g. /fp/workspace/sign_off attaches to fp.job.step). Canvas stays light even in dark mode for signature legibility. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fusion_plating_shopfloor/__manifest__.py | 3 + .../static/src/js/components/signature_pad.js | 100 ++++++++++++++++++ .../src/scss/components/_signature_pad.scss | 41 +++++++ .../src/xml/components/signature_pad.xml | 21 ++++ 4 files changed, 165 insertions(+) create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/js/components/signature_pad.js create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_signature_pad.scss create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/xml/components/signature_pad.xml diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index 9ae3ab38..87fa7560 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -71,6 +71,9 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved. 'fusion_plating_shopfloor/static/src/scss/components/_gate_viz.scss', 'fusion_plating_shopfloor/static/src/xml/components/gate_viz.xml', 'fusion_plating_shopfloor/static/src/js/components/gate_viz.js', + 'fusion_plating_shopfloor/static/src/scss/components/_signature_pad.scss', + 'fusion_plating_shopfloor/static/src/xml/components/signature_pad.xml', + 'fusion_plating_shopfloor/static/src/js/components/signature_pad.js', 'fusion_plating_shopfloor/static/src/scss/qr_scanner.scss', 'fusion_plating_shopfloor/static/src/scss/fusion_plating_shopfloor.scss', 'fusion_plating_shopfloor/static/src/scss/plant_overview.scss', diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/components/signature_pad.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/signature_pad.js new file mode 100644 index 00000000..1f341dc2 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/signature_pad.js @@ -0,0 +1,100 @@ +/** @odoo-module **/ +// ============================================================================= +// 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: +// this.dialog.add(FpSignaturePad, { title, contextLabel, onSubmit, onCancel }) +// ============================================================================= + +import { Component, useRef, onMounted, onWillUnmount } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; + +export class FpSignaturePad extends Component { + static template = "fusion_plating_shopfloor.SignaturePad"; + static components = { Dialog }; + static props = { + close: Function, // dialog service injects + title: { type: String, optional: true }, + contextLabel: { type: String, optional: true }, + onSubmit: { type: Function, optional: false }, // (dataUri) => void + onCancel: { type: Function, optional: true }, + }; + + setup() { + this.canvasRef = useRef("canvas"); + this.isDrawing = false; + this.lastPoint = null; + this.hasInk = false; + + this._onDown = (ev) => { + this.isDrawing = true; + this.lastPoint = this._localPoint(ev); + }; + this._onMove = (ev) => { + if (!this.isDrawing) return; + const p = this._localPoint(ev); + const ctx = this.canvasRef.el.getContext("2d"); + ctx.beginPath(); + ctx.moveTo(this.lastPoint.x, this.lastPoint.y); + ctx.lineTo(p.x, p.y); + ctx.stroke(); + this.lastPoint = p; + this.hasInk = true; + }; + this._onUp = () => { + this.isDrawing = false; + this.lastPoint = null; + }; + + onMounted(() => { + const canvas = this.canvasRef.el; + // Match canvas pixel size to its CSS box so strokes don't stretch + canvas.width = canvas.clientWidth; + canvas.height = canvas.clientHeight; + const ctx = canvas.getContext("2d"); + ctx.lineWidth = 2; + ctx.lineCap = "round"; + ctx.strokeStyle = "#000"; + + canvas.addEventListener("pointerdown", this._onDown); + canvas.addEventListener("pointermove", this._onMove); + canvas.addEventListener("pointerup", this._onUp); + canvas.addEventListener("pointercancel", this._onUp); + canvas.addEventListener("pointerleave", this._onUp); + }); + + onWillUnmount(() => { + const canvas = this.canvasRef.el; + if (!canvas) return; + canvas.removeEventListener("pointerdown", this._onDown); + canvas.removeEventListener("pointermove", this._onMove); + canvas.removeEventListener("pointerup", this._onUp); + canvas.removeEventListener("pointercancel", this._onUp); + canvas.removeEventListener("pointerleave", this._onUp); + }); + } + + _localPoint(ev) { + const r = this.canvasRef.el.getBoundingClientRect(); + return { x: ev.clientX - r.left, y: ev.clientY - r.top }; + } + + onClear() { + const canvas = this.canvasRef.el; + canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); + this.hasInk = false; + } + + onSubmit() { + if (!this.hasInk) return; + const dataUri = this.canvasRef.el.toDataURL("image/png"); + this.props.onSubmit(dataUri); + this.props.close(); + } + + onCancel() { + if (this.props.onCancel) this.props.onCancel(); + this.props.close(); + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_signature_pad.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_signature_pad.scss new file mode 100644 index 00000000..909929d1 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_signature_pad.scss @@ -0,0 +1,41 @@ +// ============================================================================= +// SignaturePad — modal canvas signature capture +// Canvas stays light even in dark mode (signature legibility). +// ============================================================================= + +$o-webclient-color-scheme: bright !default; + +$_sig-canvas-bg-hex: #ffffff; +$_sig-canvas-border-hex: #d8dadd; + +@if $o-webclient-color-scheme == dark { + $_sig-canvas-bg-hex: #f5f5f5 !global; + $_sig-canvas-border-hex: #5a5a5e !global; +} + +.o_fp_sig_pad { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.o_fp_sig_ctx { + font-size: 0.85rem; + color: var(--text-secondary, #666); +} + +.o_fp_sig_canvas { + width: 100%; + height: 200px; + background: $_sig-canvas-bg-hex; + border: 2px solid $_sig-canvas-border-hex; + border-radius: 6px; + cursor: crosshair; + touch-action: none; +} + +.o_fp_sig_hint { + font-size: 0.75rem; + color: var(--text-secondary, #999); + text-align: center; +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/signature_pad.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/signature_pad.xml new file mode 100644 index 00000000..a5871df7 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/signature_pad.xml @@ -0,0 +1,21 @@ + + + + + +
+
+ +
+ +
Draw your signature above
+
+ + + + + +
+
+ +