Files
TieMeasureFlow/client/templates/components/barcode_scanner.html
Adriano dd2ebf863a feat: FASE 7 - Polish & Testing (security, i18n, test suite, docs)
Security hardening: CORS lockdown, rate limiting middleware con sliding
window e eviction IP stale, security headers (CSP, HSTS, X-Frame-Options),
session cookie hardening, filename sanitization upload.

i18n completion: internazionalizzati barcode.js e csv-export.js con bridge
window.BARCODE_I18N/CSV_I18N, aggiornati .po IT/EN con 27 nuove stringhe.

Tablet UX: touch target 44px per dispositivi coarse pointer.

Test suite: 101 test totali (76 server + 25 client), copertura completa
di tutti i router API, autenticazione, ruoli, CRUD, SPC, file upload,
security integration. Infrastruttura SQLite async in-memory con fixtures.

Fix critici: MissingGreenlet in recipe_service (selectinload eager),
route ordering tasks.py, auth_service bcrypt diretto, Measurement.id
Integer per SQLite.

Documentazione: API.md (riferimento completo 40+ endpoint),
DEPLOYMENT.md (guida produzione con Docker/Nginx/SSL),
USER_GUIDE.md (manuale utente per ruolo).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 17:10:24 +01:00

160 lines
7.7 KiB
HTML

<!--
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>
<script>
window.BARCODE_I18N = {
scanner_lib_not_loaded: "{{ _('Libreria scanner non caricata') }}",
no_camera_available: "{{ _('Nessuna fotocamera disponibile') }}",
camera_access_error: "{{ _('Impossibile accedere alla fotocamera') }}"
};
</script>
</div>