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,151 @@
|
||||
<!--
|
||||
Barcode Scanner Modal Component
|
||||
Camera-based barcode/QR code scanner
|
||||
Include in pages where barcode scanning is needed
|
||||
-->
|
||||
<div x-data="barcodeScanner()"
|
||||
@barcode-use.window="stopScan(); clear()"
|
||||
x-init="$watch('scanning', val => console.log('Scanning:', val))"
|
||||
x-cloak>
|
||||
|
||||
<!-- Trigger button (styled slot - customize per use case) -->
|
||||
<slot name="trigger">
|
||||
<button @click="startScan()"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors duration-200 shadow-sm hover:shadow-md">
|
||||
|
||||
<!-- Barcode icon -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"/>
|
||||
</svg>
|
||||
<span>{{ _('Scansiona Barcode') }}</span>
|
||||
</button>
|
||||
</slot>
|
||||
|
||||
<!-- Modal overlay -->
|
||||
<div x-show="scanning || result"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
|
||||
style="display: none;">
|
||||
|
||||
<div @click.outside="scanning && !result ? null : (stopScan(), clear())"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-md w-full overflow-hidden">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center px-6 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
{{ _('Scansiona Barcode') }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<button @click="stopScan(); clear()"
|
||||
type="button"
|
||||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors p-1 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6">
|
||||
|
||||
<!-- Camera preview container -->
|
||||
<div x-show="scanning && !result"
|
||||
class="relative rounded-xl overflow-hidden bg-slate-900 aspect-square mb-4">
|
||||
|
||||
<!-- Scanner element -->
|
||||
<div id="barcode-reader" class="w-full h-full"></div>
|
||||
|
||||
<!-- Scanning indicator -->
|
||||
<div class="absolute inset-0 pointer-events-none">
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="w-64 h-64 border-2 border-white/50 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instruction overlay -->
|
||||
<div class="absolute bottom-0 inset-x-0 bg-gradient-to-t from-black/80 to-transparent p-4">
|
||||
<p class="text-white text-sm text-center font-medium">
|
||||
{{ _('Inquadra il codice a barre nel riquadro') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success result -->
|
||||
<div x-show="result"
|
||||
x-transition
|
||||
class="text-center py-8">
|
||||
|
||||
<!-- Success icon -->
|
||||
<div class="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mb-3">
|
||||
{{ _('Codice rilevato') }}
|
||||
</p>
|
||||
|
||||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-lg p-4 mb-4">
|
||||
<p class="font-mono text-xl font-bold text-primary break-all" x-text="result"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
<div x-show="error"
|
||||
x-transition
|
||||
class="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<p class="text-sm text-red-800 dark:text-red-200" x-text="error"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex gap-3">
|
||||
<button x-show="result"
|
||||
@click="$dispatch('barcode-use', { code: result }); stopScan(); clear()"
|
||||
type="button"
|
||||
class="flex-1 px-4 py-2.5 bg-primary hover:bg-primary-dark text-white font-medium rounded-lg transition-colors duration-200 shadow-sm hover:shadow-md">
|
||||
{{ _('Usa Codice') }}
|
||||
</button>
|
||||
|
||||
<button x-show="scanning && !result"
|
||||
@click="stopScan()"
|
||||
type="button"
|
||||
class="flex-1 px-4 py-2.5 bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 font-medium rounded-lg transition-colors duration-200">
|
||||
{{ _('Ferma') }}
|
||||
</button>
|
||||
|
||||
<button x-show="!scanning || result"
|
||||
@click="stopScan(); clear()"
|
||||
type="button"
|
||||
class="flex-1 px-4 py-2.5 bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 font-medium rounded-lg transition-colors duration-200">
|
||||
{{ _('Chiudi') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user