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,247 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ recipe.name }} — {{ _('Task') }} — TieMeasureFlow{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-5xl">
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-6" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
||||
<li>
|
||||
<a href="{{ url_for('measure.select_recipe') }}"
|
||||
class="hover:text-primary transition-colors inline-flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
|
||||
</svg>
|
||||
{{ _('Misure') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<svg class="w-4 h-4 text-[var(--text-muted)]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</li>
|
||||
<li class="font-medium text-[var(--text-primary)]">
|
||||
{{ recipe.name }}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Recipe Info Card -->
|
||||
<div class="tmf-card mb-8">
|
||||
<div class="p-5 sm:p-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||
<!-- Left: Recipe Details -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-wrap">
|
||||
<!-- Code Badge -->
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-md text-sm font-semibold
|
||||
bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300
|
||||
border border-primary-200 dark:border-primary-800 font-mono tracking-wide">
|
||||
{{ recipe.code }}
|
||||
</span>
|
||||
|
||||
<!-- Version Badge -->
|
||||
{% if recipe.current_version or recipe.version %}
|
||||
<span class="badge badge-neutral">
|
||||
v{{ recipe.current_version or recipe.version }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h1 class="text-2xl font-bold text-[var(--text-primary)] mb-2">
|
||||
{{ recipe.name }}
|
||||
</h1>
|
||||
|
||||
{% if recipe.description %}
|
||||
<p class="text-sm text-[var(--text-secondary)] leading-relaxed max-w-2xl">
|
||||
{{ recipe.description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Right: Traceability Badges -->
|
||||
<div class="flex flex-col gap-2 sm:items-end shrink-0">
|
||||
{% if lot_number %}
|
||||
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm
|
||||
bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800
|
||||
text-amber-800 dark:text-amber-200">
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
<span class="font-medium">{{ _('Lotto') }}:</span>
|
||||
<span class="font-mono font-semibold">{{ lot_number }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if serial_number %}
|
||||
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm
|
||||
bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-200 dark:border-indigo-800
|
||||
text-indigo-800 dark:text-indigo-200">
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/>
|
||||
</svg>
|
||||
<span class="font-medium">{{ _('Seriale') }}:</span>
|
||||
<span class="font-mono font-semibold">{{ serial_number }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Task Count Header -->
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h2 class="text-lg font-semibold text-[var(--text-primary)] flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-[var(--text-secondary)]" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
|
||||
</svg>
|
||||
{{ _('Task da eseguire') }}
|
||||
</h2>
|
||||
<span class="badge badge-neutral">
|
||||
{{ tasks|length }} task
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Task Cards -->
|
||||
{% if tasks %}
|
||||
<div class="space-y-4">
|
||||
{% for task in tasks %}
|
||||
<div class="tmf-card hover:border-primary/30 transition-all duration-200 group">
|
||||
<div class="p-5 sm:p-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||
|
||||
<!-- Task Number Circle -->
|
||||
<div class="flex items-center justify-center w-12 h-12 rounded-full shrink-0
|
||||
bg-primary-50 dark:bg-primary-900/30 border-2 border-primary-200 dark:border-primary-700
|
||||
text-primary-700 dark:text-primary-300 font-bold text-lg">
|
||||
{{ loop.index }}
|
||||
</div>
|
||||
|
||||
<!-- Task Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- Task Header -->
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-xs font-medium text-[var(--text-muted)] uppercase tracking-wider">
|
||||
Task {{ loop.index }} {{ _('di') }} {{ tasks|length }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Task Title -->
|
||||
<h3 class="text-lg font-semibold text-[var(--text-primary)] mb-1">
|
||||
{{ task.title or task.name or (_('Task') ~ ' ' ~ loop.index) }}
|
||||
</h3>
|
||||
|
||||
<!-- Directive -->
|
||||
{% if task.directive or task.description %}
|
||||
<p class="text-sm text-[var(--text-secondary)] leading-relaxed mb-3 line-clamp-2">
|
||||
{{ task.directive or task.description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Meta Row -->
|
||||
<div class="flex items-center gap-4 flex-wrap">
|
||||
<!-- Subtask Count -->
|
||||
{% if task.subtask_count is defined or task.subtasks %}
|
||||
<span class="inline-flex items-center gap-1.5 text-xs text-[var(--text-secondary)]">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span class="font-medium">
|
||||
{% if task.subtask_count is defined %}
|
||||
{{ task.subtask_count }}
|
||||
{% elif task.subtasks %}
|
||||
{{ task.subtasks|length }}
|
||||
{% endif %}
|
||||
{{ _('misurazioni') }}
|
||||
</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<!-- File Attachment -->
|
||||
{% if task.file_path %}
|
||||
<span class="inline-flex items-center gap-1.5 text-xs text-[var(--text-secondary)]">
|
||||
{% if task.file_path.endswith('.pdf') %}
|
||||
<svg class="w-4 h-4 text-red-500" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
<span class="font-medium">{{ _('Allegato') }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Button -->
|
||||
<div class="shrink-0 sm:ml-4">
|
||||
<a href="{{ url_for('measure.task_execute', task_id=task.id) }}"
|
||||
class="btn btn-primary gap-2 w-full sm:w-auto justify-center
|
||||
group-hover:shadow-md transition-shadow duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
{{ _('Inizia Misure') }}
|
||||
<svg class="w-4 h-4 transition-transform group-hover:translate-x-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full
|
||||
bg-[var(--bg-secondary)] mb-4">
|
||||
<svg class="w-8 h-8 text-[var(--text-muted)]" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-[var(--text-primary)] mb-1">
|
||||
{{ _('Nessun task disponibile') }}
|
||||
</h3>
|
||||
<p class="text-sm text-[var(--text-secondary)] max-w-md mx-auto">
|
||||
{{ _('Questa ricetta non ha ancora task definiti.') }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Bottom Navigation -->
|
||||
<div class="mt-8 flex items-center justify-between">
|
||||
<a href="{{ url_for('measure.select_recipe') }}"
|
||||
class="btn btn-secondary gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
{{ _('Seleziona altra ricetta') }}
|
||||
</a>
|
||||
|
||||
{% if tasks %}
|
||||
<div class="text-sm text-[var(--text-secondary)]">
|
||||
{{ tasks|length }} task ·
|
||||
{% set total_subs = namespace(count=0) %}
|
||||
{% for t in tasks %}
|
||||
{% if t.subtask_count is defined %}
|
||||
{% set total_subs.count = total_subs.count + t.subtask_count %}
|
||||
{% elif t.subtasks %}
|
||||
{% set total_subs.count = total_subs.count + t.subtasks|length %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if total_subs.count > 0 %}
|
||||
{{ total_subs.count }} {{ _('misurazioni totali') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user