feat(record-inputs): tap-to-adjust steppers + inputmode keypad hint
Adds [-] / [+] buttons around every numeric input in the Record Inputs dialog (single-value, dual-entry, and pass_fail+range branches). Tap to increment / decrement by the recipe-author-derived step size (stepFor() already computes this from target_min/target_max precision, falling back to input-type defaults). - Decrement clamps at 0 (typical qty/time/temp on a plating floor doesn't go negative; if needed, operator can still tap the input and type a negative value) - Increment uses _stepRound() to avoid floating-point fuzz on decimals - Center-aligned monospace-ish input between the buttons for clarity - inputmode='decimal' (or 'numeric' for time fields) hint so when the operator does tap the input, the iPad shows a number keypad instead of the full keyboard Touches single-value, dual-entry (min/max), and pass_fail+range. Other multi-field widgets (multi-point thickness, bath chemistry panel) still use plain inputs — separate request if they need steppers too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Native Jobs',
|
||||
'version': '19.0.10.28.0',
|
||||
'version': '19.0.10.29.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||
'author': 'Nexa Systems Inc.',
|
||||
|
||||
@@ -236,6 +236,52 @@ export class FpRecordInputsDialog extends Component {
|
||||
return "any";
|
||||
}
|
||||
|
||||
// Stepper helpers — give the operator a tap-to-increment / -decrement
|
||||
// pair next to each numeric input so they don't have to open the
|
||||
// keyboard for small adjustments. Field is one of: value_number,
|
||||
// value_min, value_max. Increment uses stepFor() so taps move in the
|
||||
// same decimal magnitude the recipe spec was written in. Clamps at 0
|
||||
// (typical qty/time/temp on a plating shop floor doesn't go negative;
|
||||
// if a recipe needs negatives, the operator can still type the value
|
||||
// by tapping the input).
|
||||
_stepDelta(row) {
|
||||
const s = this.stepFor(row);
|
||||
if (s === "any") return 1;
|
||||
const n = parseFloat(s);
|
||||
return isNaN(n) || n <= 0 ? 1 : n;
|
||||
}
|
||||
|
||||
_stepRound(n, delta) {
|
||||
// Avoid floating-point fuzz (0.1+0.2=0.30000004). Round to the
|
||||
// delta's decimal precision.
|
||||
const decimals = (String(delta).split(".")[1] || "").length;
|
||||
if (!decimals) return Math.round(n);
|
||||
const factor = Math.pow(10, decimals);
|
||||
return Math.round(n * factor) / factor;
|
||||
}
|
||||
|
||||
onIncrement(row, field) {
|
||||
const cur = parseFloat(row[field]) || 0;
|
||||
const delta = this._stepDelta(row);
|
||||
row[field] = this._stepRound(cur + delta, delta);
|
||||
}
|
||||
|
||||
onDecrement(row, field) {
|
||||
const cur = parseFloat(row[field]) || 0;
|
||||
const delta = this._stepDelta(row);
|
||||
row[field] = Math.max(0, this._stepRound(cur - delta, delta));
|
||||
}
|
||||
|
||||
inputModeFor(row) {
|
||||
// Tablet keyboard hint — show numeric keypad instead of full
|
||||
// keyboard when the operator does tap the input. 'decimal' is
|
||||
// safer than 'numeric' because it includes the decimal point
|
||||
// (needed for pH, thickness, temperature).
|
||||
const t = row.input_type || "";
|
||||
if (t === "time_seconds" || t === "time_hms") return "numeric";
|
||||
return "decimal";
|
||||
}
|
||||
|
||||
_fpCountDecimals(n) {
|
||||
if (n === null || n === undefined || n === "" || n === 0) return 0;
|
||||
const s = String(n);
|
||||
|
||||
@@ -801,3 +801,49 @@ $rid-warn : var(--fp-rid-warn, #{$_fp-rid-warn-hex});
|
||||
color: $rid-required;
|
||||
}
|
||||
}
|
||||
|
||||
// Numeric stepper (tap-to-increment / tap-to-decrement around the input).
|
||||
// Operator can still tap the input to open the keypad (inputmode="decimal"
|
||||
// gives them the number keypad on iPad).
|
||||
.o_fp_ri_stepper {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
max-width: 16rem;
|
||||
}
|
||||
|
||||
.o_fp_ri_stepper_btn {
|
||||
flex: 0 0 auto;
|
||||
width: 3.2rem;
|
||||
border: 1px solid #cdd0d4;
|
||||
background: #f5f6f8;
|
||||
color: #1d1d1f;
|
||||
font-size: 1.3rem;
|
||||
cursor: pointer;
|
||||
transition: background 80ms;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover, &:active { background: #e5e7eb; }
|
||||
|
||||
&.o_fp_ri_stepper_minus {
|
||||
border-top-left-radius: 6px;
|
||||
border-bottom-left-radius: 6px;
|
||||
border-right: 0;
|
||||
}
|
||||
&.o_fp_ri_stepper_plus {
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_ri_stepper_input {
|
||||
flex: 1 1 auto;
|
||||
border-radius: 0;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
min-width: 4rem;
|
||||
}
|
||||
|
||||
@@ -120,11 +120,26 @@
|
||||
<!-- Numeric — single value (no range defined) -->
|
||||
<div t-if="isNumeric(row) and !hasRangeEntry(row)"
|
||||
class="o_fp_ri_numeric">
|
||||
<input type="number"
|
||||
class="o_fp_ri_input o_fp_ri_input_numeric"
|
||||
t-att-step="stepFor(row)"
|
||||
t-model.number="row.value_number"
|
||||
t-att-placeholder="row.target_min or '0.00'"/>
|
||||
<div class="o_fp_ri_stepper">
|
||||
<button type="button"
|
||||
class="o_fp_ri_stepper_btn o_fp_ri_stepper_minus"
|
||||
t-on-click="() => this.onDecrement(row, 'value_number')"
|
||||
aria-label="Decrease">
|
||||
<i class="fa fa-minus"/>
|
||||
</button>
|
||||
<input type="number"
|
||||
class="o_fp_ri_input o_fp_ri_input_numeric o_fp_ri_stepper_input"
|
||||
t-att-inputmode="inputModeFor(row)"
|
||||
t-att-step="stepFor(row)"
|
||||
t-model.number="row.value_number"
|
||||
t-att-placeholder="row.target_min or '0.00'"/>
|
||||
<button type="button"
|
||||
class="o_fp_ri_stepper_btn o_fp_ri_stepper_plus"
|
||||
t-on-click="() => this.onIncrement(row, 'value_number')"
|
||||
aria-label="Increase">
|
||||
<i class="fa fa-plus"/>
|
||||
</button>
|
||||
</div>
|
||||
<t t-set="hint" t-value="rangeHint(row)"/>
|
||||
<span t-if="hint"
|
||||
class="o_fp_ri_range_hint"
|
||||
@@ -138,22 +153,52 @@
|
||||
Constrained to numeric so it doesn't duplicate
|
||||
the pass_fail+range branch above. -->
|
||||
<div t-if="isNumeric(row) and hasRangeEntry(row)" class="o_fp_ri_dual">
|
||||
<label class="o_fp_ri_dual_field">
|
||||
<div class="o_fp_ri_dual_field">
|
||||
<span class="o_fp_ri_dual_label">Min Reading</span>
|
||||
<input type="number"
|
||||
class="o_fp_ri_input o_fp_ri_input_numeric"
|
||||
t-att-step="stepFor(row)"
|
||||
t-model.number="row.value_min"
|
||||
t-att-placeholder="row.target_min or '0.00'"/>
|
||||
</label>
|
||||
<label class="o_fp_ri_dual_field">
|
||||
<div class="o_fp_ri_stepper">
|
||||
<button type="button"
|
||||
class="o_fp_ri_stepper_btn o_fp_ri_stepper_minus"
|
||||
t-on-click="() => this.onDecrement(row, 'value_min')"
|
||||
aria-label="Decrease">
|
||||
<i class="fa fa-minus"/>
|
||||
</button>
|
||||
<input type="number"
|
||||
class="o_fp_ri_input o_fp_ri_input_numeric o_fp_ri_stepper_input"
|
||||
t-att-inputmode="inputModeFor(row)"
|
||||
t-att-step="stepFor(row)"
|
||||
t-model.number="row.value_min"
|
||||
t-att-placeholder="row.target_min or '0.00'"/>
|
||||
<button type="button"
|
||||
class="o_fp_ri_stepper_btn o_fp_ri_stepper_plus"
|
||||
t-on-click="() => this.onIncrement(row, 'value_min')"
|
||||
aria-label="Increase">
|
||||
<i class="fa fa-plus"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_fp_ri_dual_field">
|
||||
<span class="o_fp_ri_dual_label">Max Reading</span>
|
||||
<input type="number"
|
||||
class="o_fp_ri_input o_fp_ri_input_numeric"
|
||||
t-att-step="stepFor(row)"
|
||||
t-model.number="row.value_max"
|
||||
t-att-placeholder="row.target_max or '0.00'"/>
|
||||
</label>
|
||||
<div class="o_fp_ri_stepper">
|
||||
<button type="button"
|
||||
class="o_fp_ri_stepper_btn o_fp_ri_stepper_minus"
|
||||
t-on-click="() => this.onDecrement(row, 'value_max')"
|
||||
aria-label="Decrease">
|
||||
<i class="fa fa-minus"/>
|
||||
</button>
|
||||
<input type="number"
|
||||
class="o_fp_ri_input o_fp_ri_input_numeric o_fp_ri_stepper_input"
|
||||
t-att-inputmode="inputModeFor(row)"
|
||||
t-att-step="stepFor(row)"
|
||||
t-model.number="row.value_max"
|
||||
t-att-placeholder="row.target_max or '0.00'"/>
|
||||
<button type="button"
|
||||
class="o_fp_ri_stepper_btn o_fp_ri_stepper_plus"
|
||||
t-on-click="() => this.onIncrement(row, 'value_max')"
|
||||
aria-label="Increase">
|
||||
<i class="fa fa-plus"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<t t-set="dhint" t-value="dualRangeHint(row)"/>
|
||||
<span t-if="dhint"
|
||||
class="o_fp_ri_range_hint o_fp_ri_dual_hint"
|
||||
@@ -169,22 +214,52 @@
|
||||
prompt (e.g. Bore inspection: 0.005-0.007 in). -->
|
||||
<t t-if="isPassFail(row) and hasRangeEntry(row)">
|
||||
<div class="o_fp_ri_dual">
|
||||
<label class="o_fp_ri_dual_field">
|
||||
<div class="o_fp_ri_dual_field">
|
||||
<span class="o_fp_ri_dual_label">Min Reading</span>
|
||||
<input type="number"
|
||||
class="o_fp_ri_input o_fp_ri_input_numeric"
|
||||
t-att-step="stepFor(row)"
|
||||
t-model.number="row.value_min"
|
||||
t-att-placeholder="row.target_min or '0.00'"/>
|
||||
</label>
|
||||
<label class="o_fp_ri_dual_field">
|
||||
<div class="o_fp_ri_stepper">
|
||||
<button type="button"
|
||||
class="o_fp_ri_stepper_btn o_fp_ri_stepper_minus"
|
||||
t-on-click="() => this.onDecrement(row, 'value_min')"
|
||||
aria-label="Decrease">
|
||||
<i class="fa fa-minus"/>
|
||||
</button>
|
||||
<input type="number"
|
||||
class="o_fp_ri_input o_fp_ri_input_numeric o_fp_ri_stepper_input"
|
||||
t-att-inputmode="inputModeFor(row)"
|
||||
t-att-step="stepFor(row)"
|
||||
t-model.number="row.value_min"
|
||||
t-att-placeholder="row.target_min or '0.00'"/>
|
||||
<button type="button"
|
||||
class="o_fp_ri_stepper_btn o_fp_ri_stepper_plus"
|
||||
t-on-click="() => this.onIncrement(row, 'value_min')"
|
||||
aria-label="Increase">
|
||||
<i class="fa fa-plus"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_fp_ri_dual_field">
|
||||
<span class="o_fp_ri_dual_label">Max Reading</span>
|
||||
<input type="number"
|
||||
class="o_fp_ri_input o_fp_ri_input_numeric"
|
||||
t-att-step="stepFor(row)"
|
||||
t-model.number="row.value_max"
|
||||
t-att-placeholder="row.target_max or '0.00'"/>
|
||||
</label>
|
||||
<div class="o_fp_ri_stepper">
|
||||
<button type="button"
|
||||
class="o_fp_ri_stepper_btn o_fp_ri_stepper_minus"
|
||||
t-on-click="() => this.onDecrement(row, 'value_max')"
|
||||
aria-label="Decrease">
|
||||
<i class="fa fa-minus"/>
|
||||
</button>
|
||||
<input type="number"
|
||||
class="o_fp_ri_input o_fp_ri_input_numeric o_fp_ri_stepper_input"
|
||||
t-att-inputmode="inputModeFor(row)"
|
||||
t-att-step="stepFor(row)"
|
||||
t-model.number="row.value_max"
|
||||
t-att-placeholder="row.target_max or '0.00'"/>
|
||||
<button type="button"
|
||||
class="o_fp_ri_stepper_btn o_fp_ri_stepper_plus"
|
||||
t-on-click="() => this.onIncrement(row, 'value_max')"
|
||||
aria-label="Increase">
|
||||
<i class="fa fa-plus"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<t t-set="dhint" t-value="dualRangeHint(row)"/>
|
||||
<span t-if="dhint"
|
||||
class="o_fp_ri_range_hint o_fp_ri_dual_hint"
|
||||
|
||||
Reference in New Issue
Block a user