feat: FASE 3 - Flusso MeasurementTec (selezione ricetta, esecuzione misure, riepilogo)
Implementazione completa del flusso operativo per il ruolo MeasurementTec:
Blueprint measure.py:
- select_recipe: selezione ricetta con ricerca e barcode
- task_list: lista task con conteggi subtask e allegati
- task_execute: esecuzione misure con numpad, calibro USB, feedback real-time
- task_complete: riepilogo con statistiche pass/fail e export CSV
- API AJAX: lookup-barcode, save-traceability, save-measurement
- Autorizzazione role_required("MeasurementTec") su tutte le route
Componenti riutilizzabili:
- numpad.html/js/css: tastierino numerico touch-friendly con keyboard support
- caliper_status.html + caliper.js: integrazione calibro USB via Web Serial API
- barcode_scanner.html + barcode.js: scansione barcode con html5-qrcode
- measurement_feedback.html: feedback visivo pass/warning/fail in tempo reale
- next_measurement.html: indicatore prossima misurazione
- annotation-viewer.js: visualizzatore canvas con marker su disegni tecnici
- csv-export.js: export CSV con locale italiano (delimitatore ;, decimale ,)
Sicurezza:
- Decoratore role_required(*roles) per autorizzazione basata su ruoli
- CSRF token su tutti i POST AJAX
- |tojson per prevenire XSS su annotations_json
- Validazione input lato client e server
i18n: 23+ nuove chiavi tradotte IT/EN per tutti i template FASE 3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Numpad Component - Alpine.js component for touch-friendly numeric input
|
||||
* Used for measurement data entry in task_execute.html
|
||||
*/
|
||||
|
||||
function numpad() {
|
||||
return {
|
||||
// State
|
||||
value: '', // String representation of the current value
|
||||
negative: false, // Whether the value is negative
|
||||
hasDecimal: false, // Whether a decimal point has been entered
|
||||
unit: 'mm', // Unit of measurement (can be set externally)
|
||||
maxIntDigits: 6, // Maximum integer digits
|
||||
maxDecDigits: 6, // Maximum decimal digits
|
||||
|
||||
/**
|
||||
* Get the display value with sign
|
||||
*/
|
||||
get displayValue() {
|
||||
if (!this.value) return '';
|
||||
return (this.negative ? '-' : '') + this.value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the numeric value as a number
|
||||
*/
|
||||
get numericValue() {
|
||||
if (!this.value) return null;
|
||||
const v = parseFloat(this.value);
|
||||
return this.negative ? -v : v;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a valid value has been entered
|
||||
*/
|
||||
get hasValue() {
|
||||
return this.value.length > 0 && this.value !== '.';
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a digit to the current value
|
||||
* @param {string} d - The digit to add (0-9)
|
||||
*/
|
||||
addDigit(d) {
|
||||
// Validate: don't exceed max digits
|
||||
const parts = this.value.split('.');
|
||||
|
||||
if (this.hasDecimal) {
|
||||
// Check decimal part length
|
||||
if (parts[1] && parts[1].length >= this.maxDecDigits) return;
|
||||
} else {
|
||||
// Check integer part length
|
||||
if (parts[0] && parts[0].length >= this.maxIntDigits) return;
|
||||
}
|
||||
|
||||
// Prevent leading zeros (except "0.")
|
||||
if (this.value === '0' && d !== '.') {
|
||||
this.value = d;
|
||||
return;
|
||||
}
|
||||
|
||||
this.value += d;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a decimal point to the current value
|
||||
*/
|
||||
addDecimal() {
|
||||
// Don't add decimal if one already exists
|
||||
if (this.hasDecimal) return;
|
||||
|
||||
// If value is empty, start with "0."
|
||||
if (!this.value) this.value = '0';
|
||||
|
||||
this.value += '.';
|
||||
this.hasDecimal = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the sign of the current value
|
||||
*/
|
||||
toggleSign() {
|
||||
if (!this.value) return;
|
||||
this.negative = !this.negative;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the last character from the current value
|
||||
*/
|
||||
backspace() {
|
||||
if (!this.value) return;
|
||||
|
||||
const removed = this.value.charAt(this.value.length - 1);
|
||||
|
||||
// If removing decimal point, update flag
|
||||
if (removed === '.') this.hasDecimal = false;
|
||||
|
||||
this.value = this.value.slice(0, -1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all entered data
|
||||
*/
|
||||
clearAll() {
|
||||
this.value = '';
|
||||
this.negative = false;
|
||||
this.hasDecimal = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm the current value and dispatch event
|
||||
*/
|
||||
confirm() {
|
||||
if (!this.hasValue) return;
|
||||
|
||||
const val = this.numericValue;
|
||||
|
||||
// Dispatch custom event for parent component to handle
|
||||
this.$dispatch('numpad-confirm', { value: val });
|
||||
|
||||
// Reset after confirmation
|
||||
this.clearAll();
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a value programmatically (e.g., from USB caliper)
|
||||
* @param {number} val - The numeric value to set
|
||||
*/
|
||||
setValue(val) {
|
||||
this.clearAll();
|
||||
|
||||
// Handle negative values
|
||||
if (val < 0) {
|
||||
this.negative = true;
|
||||
val = Math.abs(val);
|
||||
}
|
||||
|
||||
this.value = val.toString();
|
||||
|
||||
// Update decimal flag if value contains decimal point
|
||||
if (this.value.includes('.')) this.hasDecimal = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the unit of measurement
|
||||
* @param {string} newUnit - The unit to set (e.g., 'mm', 'cm', 'in')
|
||||
*/
|
||||
setUnit(newUnit) {
|
||||
this.unit = newUnit;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle keyboard input for physical keyboard support
|
||||
* @param {KeyboardEvent} e - The keyboard event
|
||||
*/
|
||||
handleKeydown(e) {
|
||||
// Number keys
|
||||
if (e.key >= '0' && e.key <= '9') {
|
||||
e.preventDefault();
|
||||
this.addDigit(e.key);
|
||||
}
|
||||
// Decimal point (both . and ,)
|
||||
else if (e.key === '.' || e.key === ',') {
|
||||
e.preventDefault();
|
||||
this.addDecimal();
|
||||
}
|
||||
// Backspace
|
||||
else if (e.key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
this.backspace();
|
||||
}
|
||||
// Escape - clear all
|
||||
else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
this.clearAll();
|
||||
}
|
||||
// Enter - confirm
|
||||
else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.confirm();
|
||||
}
|
||||
// Minus sign - toggle sign
|
||||
else if (e.key === '-') {
|
||||
e.preventDefault();
|
||||
this.toggleSign();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user