feat(recipes): la ricetta decide cosa pretendere e come si misura
Tre cose che prima non erano di nessuno diventano regole della ricetta, decise da chi la scrive e fatte valere dal server. Punto 8 — tracciabilita' obbligatoria. Lotto e seriale si dichiarano obbligatori sulla ricetta. L'operatore li inserisce alla selezione, dove il pulsante non si attiva finche' mancano, e la stessa regola vale sulla lista task, raggiungibile anche per link diretto, e sul barcode: uno sbarramento che dura solo finche' qualcuno prende in mano il lettore non e' uno sbarramento. La produzione non si apre e la misura non si salva senza cio' che la ricetta pretende, perche' un valore senza il suo lotto non e' riconducibile a niente, e accorgersene dopo significa accorgersene tardi. Punto 9 — inserimento manuale. Una ricetta puo' vietare i valori digitati, ed e' il valore predefinito: il calibro e' lo strumento, digitare e' cio' che va concesso. Dove e' vietato il tastierino non viene disegnato (non nascosto con i CSS: il markup nascosto e' markup che si puo' rimostrare) e restano correzione e conferma, perche' una lettura sbagliata va cancellata. Il controllo vero e' sul server: una regola che vive solo nel frontend e' un consiglio. Migliorato al passaggio il riconoscimento del calibro. Contava solo la raffica di cifre, cosi' una lettura corta come "9.5" — tre battute — finiva registrata come digitata a mano; ora conta anche l'Invio che il wedge manda dentro la stessa raffica. Senza questa correzione il divieto avrebbe respinto misure legittime. Punto 11 — formattazione delle descrizioni. A capo e grassetto sopravvivono: chi scrive le ricette incolla dal PDF della scheda tecnica e il testo arrivava appiattito, da risistemare a mano ogni volta. Nessun HTML viene accettato o salvato — il testo viene escapato e gli unici tag nel risultato sono quelli prodotti dal renderer. La sanificazione e' questa: non c'e' niente da sanificare perche' non si accetta niente. Le stesse due regole in Jinja e in JS, cosi' una descrizione si legge uguale ovunque. E la descrizione ora si vede anche in esecuzione: era scritta per l'operatore e la vedeva solo chi la scriveva. Migrazione 009: tre colonne sulla ricetta. Le due di tracciabilita' partono false, che e' il comportamento di oggi; l'inserimento manuale parte *vero* sulle ricette gia' esistenti — il default della colonna e' falso, quindi le ricette nuove sono solo-calibro, ma spegnerlo d'ufficio su quelle in uso fermerebbe una linea alla misura successiva. Chi possiede la ricetta lo decide dall'editor. Test: +26 (291). Coprono il rifiuto sul server per lotto, seriale e valore digitato, il calibro sempre ammesso, le regole che sopravvivono alla nuova versione, il tastierino assente in pagina, l'Avvia sbarrato, e il renderer delle descrizioni compreso il caso in cui si prova a farci passare un tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
<!-- Numpad Component - Touch-friendly numeric keypad for measurement input -->
|
||||
{#
|
||||
allow_manual_input comes from the recipe. When it is false the digits are not
|
||||
drawn at all - not hidden with CSS: they are the only unambiguously typed way
|
||||
in, and markup that is merely hidden is still markup that can be shown again.
|
||||
|
||||
The keyboard listener stays either way: that is how the USB caliper arrives, and
|
||||
the component still checks at confirm time, because a wedge and a keyboard are
|
||||
the same device as far as the browser is concerned.
|
||||
#}
|
||||
{% set allow_manual = allow_manual_input if allow_manual_input is defined else true %}
|
||||
<div
|
||||
x-data="numpad()"
|
||||
x-data="numpad({ allowManual: {{ allow_manual|tojson }} })"
|
||||
@keydown.window="handleKeydown($event)"
|
||||
class="numpad-container w-full max-w-sm mx-auto"
|
||||
>
|
||||
@@ -18,6 +28,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not allow_manual %}
|
||||
<!-- Caliper only: no digits, just the corrections and the confirm -->
|
||||
<div>
|
||||
<div class="mb-3 flex items-center justify-center gap-2 px-3 py-2 rounded-lg
|
||||
bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800">
|
||||
<svg class="w-4 h-4 text-primary shrink-0" 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="text-xs font-medium text-primary-700 dark:text-primary-300">
|
||||
{{ _('Misura solo da calibro: questa ricetta non ammette valori digitati') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button @click="backspace()" type="button"
|
||||
class="min-h-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-100 dark:bg-slate-600 text-steel dark:text-steel-light hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>⌫</button>
|
||||
<button @click="clearAll()" type="button"
|
||||
class="min-h-[56px] font-mono text-xl font-semibold rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-100 dark:bg-slate-600 text-measure-fail hover:bg-slate-50 dark:hover:bg-slate-600 active:scale-95 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>C</button>
|
||||
<button @click="confirm()" type="button" :disabled="!hasValue"
|
||||
class="min-h-[56px] font-mono text-xl font-semibold rounded-xl border border-primary bg-primary text-white hover:bg-primary-dark active:scale-95 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-40 disabled:cursor-not-allowed disabled:active:scale-100 disabled:hover:bg-primary"
|
||||
>✓</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<!-- Keypad Grid 4x4 -->
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<!-- Row 1: 7 8 9 ⌫ -->
|
||||
@@ -118,4 +154,5 @@
|
||||
class="min-h-[56px] min-w-[56px] font-mono text-xl font-semibold rounded-xl border border-primary bg-primary text-white hover:bg-primary-dark active:scale-95 transition-all duration-150 select-none cursor-pointer flex items-center justify-center touch-manipulation focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-40 disabled:cursor-not-allowed disabled:active:scale-100 disabled:hover:bg-primary"
|
||||
>✓</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{#
|
||||
The whole formatting toolbar a task description gets: bold, plus the reminder
|
||||
that a line break is a line break.
|
||||
|
||||
Whoever writes a recipe pastes from the PDF of the technical sheet; the text used
|
||||
to arrive flattened and had to be put back by hand every time. `target` is the
|
||||
x-ref of the textarea this toolbar acts on.
|
||||
#}
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<button type="button"
|
||||
@click="wrapSelectionBold($refs.{{ target }})"
|
||||
title="{{ _('Grassetto: racchiude il testo selezionato fra **') }}"
|
||||
class="px-2 py-0.5 rounded border border-[var(--border-color)]
|
||||
text-sm font-bold leading-none
|
||||
text-[var(--text-secondary)] hover:text-primary hover:border-primary
|
||||
transition-colors">
|
||||
B
|
||||
</button>
|
||||
<span class="text-[11px] text-[var(--text-muted)]">
|
||||
{{ _('**grassetto** · a capo = a capo') }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -248,6 +248,48 @@
|
||||
{{ _('Timer cicalino per ricordare la misurazione periodica') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Regole di misura: cosa la ricetta pretende dall'operatore -->
|
||||
<div class="sm:col-span-2 pt-2 border-t border-[var(--border-color)]">
|
||||
<p class="tmf-label mb-2">{{ _('Regole di misura') }}</p>
|
||||
<div class="grid sm:grid-cols-3 gap-3">
|
||||
|
||||
<label class="flex items-start gap-2 cursor-pointer">
|
||||
<input type="checkbox" x-model="requiresLot"
|
||||
class="mt-0.5 rounded border-[var(--border-color)] text-primary focus:ring-primary">
|
||||
<span>
|
||||
<span class="text-sm text-[var(--text-primary)]">{{ _('Lotto obbligatorio') }}</span>
|
||||
<span class="block text-xs text-[var(--text-muted)]">
|
||||
{{ _('Senza lotto la produzione non parte') }}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-start gap-2 cursor-pointer">
|
||||
<input type="checkbox" x-model="requiresSerial"
|
||||
class="mt-0.5 rounded border-[var(--border-color)] text-primary focus:ring-primary">
|
||||
<span>
|
||||
<span class="text-sm text-[var(--text-primary)]">{{ _('Seriale obbligatorio') }}</span>
|
||||
<span class="block text-xs text-[var(--text-muted)]">
|
||||
{{ _('Senza seriale la produzione non parte') }}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{# Off by default: the caliper is the instrument, and a value that
|
||||
happens to be in tolerance must not be enterable by hand. #}
|
||||
<label class="flex items-start gap-2 cursor-pointer">
|
||||
<input type="checkbox" x-model="allowManualInput"
|
||||
class="mt-0.5 rounded border-[var(--border-color)] text-primary focus:ring-primary">
|
||||
<span>
|
||||
<span class="text-sm text-[var(--text-primary)]">{{ _('Consenti inserimento manuale') }}</span>
|
||||
<span class="block text-xs text-[var(--text-muted)]">
|
||||
{{ _('Se disattivo si misura solo col calibro') }}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -486,6 +528,13 @@ function recipeEditor() {
|
||||
// ---- Measurement interval ----
|
||||
measurementIntervalMinutes: {{ (recipe.measurement_interval_minutes if recipe and recipe.measurement_interval_minutes else 'null')|tojson }},
|
||||
|
||||
// ---- Rules of measurement (points 8 and 9) ----
|
||||
// A new recipe forbids typing: the caliper is the instrument, and permission
|
||||
// to type is what has to be given, not taken away.
|
||||
requiresLot: {{ (recipe.requires_lot|default(false) if recipe else false)|tojson }},
|
||||
requiresSerial: {{ (recipe.requires_serial|default(false) if recipe else false)|tojson }},
|
||||
allowManualInput: {{ (recipe.allow_manual_input|default(false) if recipe else false)|tojson }},
|
||||
|
||||
// ---- File upload (preview image) ----
|
||||
currentFilePath: {{ (recipe.image_path if recipe and recipe.image_path else '')|tojson }},
|
||||
uploadingFile: false,
|
||||
@@ -512,6 +561,12 @@ function recipeEditor() {
|
||||
payload.measurement_interval_minutes = parseInt(this.measurementIntervalMinutes, 10);
|
||||
}
|
||||
|
||||
// Rules of measurement. Sent every time, including when false: these are the
|
||||
// settings an operator gets told about, so turning one off has to travel.
|
||||
payload.requires_lot = !!this.requiresLot;
|
||||
payload.requires_serial = !!this.requiresSerial;
|
||||
payload.allow_manual_input = !!this.allowManualInput;
|
||||
|
||||
// Include image_path for preview thumbnail
|
||||
if (this.currentFilePath) {
|
||||
payload.image_path = this.currentFilePath;
|
||||
|
||||
@@ -188,7 +188,7 @@
|
||||
<h2 class="font-semibold text-[var(--text-primary)]">{{ task.title }}</h2>
|
||||
{% if task.directive or task.description %}
|
||||
<p class="text-xs text-[var(--text-secondary)] mt-0.5">
|
||||
{{ task.directive or task.description }}
|
||||
{{ (task.directive or task.description)|rich_text }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -292,8 +292,14 @@
|
||||
</div>
|
||||
<!-- Descrizione -->
|
||||
<div class="sm:col-span-2">
|
||||
<label class="tmf-label">{{ _('Descrizione') }}</label>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<label class="tmf-label">{{ _('Descrizione') }}</label>
|
||||
{% with target = 'newTaskDescription' %}
|
||||
{% include "components/rich_text_toolbar.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<textarea x-model="newTask.description"
|
||||
x-ref="newTaskDescription"
|
||||
class="tmf-input text-sm"
|
||||
rows="3"
|
||||
placeholder="{{ _('Descrizione opzionale...') }}"></textarea>
|
||||
@@ -519,7 +525,10 @@
|
||||
<template x-if="task.description">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wide shrink-0 mt-0.5">{{ _('Descrizione') }}:</span>
|
||||
<span class="text-sm text-[var(--text-secondary)] whitespace-pre-wrap" x-text="task.description"></span>
|
||||
{# Shown the way the operator will see it - the point of the
|
||||
formatting is that the maker can check it here. #}
|
||||
<span class="text-sm text-[var(--text-secondary)]"
|
||||
x-html="richText(task.description)"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -544,8 +553,14 @@
|
||||
placeholder="{{ _('Direttiva opzionale...') }}">
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="tmf-label">{{ _('Descrizione') }}</label>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<label class="tmf-label">{{ _('Descrizione') }}</label>
|
||||
{% with target = 'editTaskDescription' %}
|
||||
{% include "components/rich_text_toolbar.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<textarea x-model="editTaskData.description"
|
||||
x-ref="editTaskDescription"
|
||||
class="tmf-input text-sm"
|
||||
rows="3"
|
||||
placeholder="{{ _('Descrizione opzionale...') }}"></textarea>
|
||||
@@ -1076,6 +1091,8 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{# Bold and line breaks in descriptions: the same rules the server renders with. #}
|
||||
<script src="{{ url_for('static', filename='js/rich-text.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script>
|
||||
<script>if(typeof pdfjsLib!=='undefined')pdfjsLib.GlobalWorkerOptions.workerSrc='{{ url_for('static', filename='vendor/pdf.worker-3.11.174.min.js') }}';</script>
|
||||
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
||||
|
||||
@@ -22,6 +22,15 @@
|
||||
(r.description || '').toLowerCase().includes(q)
|
||||
);
|
||||
},
|
||||
/* Which compulsory traceability fields this recipe is still missing.
|
||||
Empty means the operator can start. The server refuses the same thing
|
||||
when the production opens - this is only the earlier answer. */
|
||||
missingTraceability(recipe) {
|
||||
const missing = [];
|
||||
if (recipe.requires_lot && !this.lot_number.trim()) missing.push('{{ _('lotto') }}');
|
||||
if (recipe.requires_serial && !this.serial_number.trim()) missing.push('{{ _('seriale') }}');
|
||||
return missing;
|
||||
},
|
||||
buildTaskUrl(recipeId) {
|
||||
let url = '/measure/tasks/' + recipeId + '?';
|
||||
const params = [];
|
||||
@@ -47,6 +56,14 @@
|
||||
if (data.error) {
|
||||
this.barcodeError = data.detail || '{{ _("Ricetta non trovata") }}';
|
||||
} else {
|
||||
// The scanner is a way in like any other: a recipe that demands a lot
|
||||
// demands it here too, or the rule would last exactly as long as it
|
||||
// took someone to reach for the barcode reader.
|
||||
const missing = this.missingTraceability(data);
|
||||
if (missing.length) {
|
||||
this.barcodeError = '{{ _("Compila prima:") }} ' + missing.join(', ');
|
||||
return;
|
||||
}
|
||||
this.barcodeModal = false;
|
||||
this.barcodeInput = '';
|
||||
window.location.href = this.buildTaskUrl(data.id);
|
||||
@@ -152,7 +169,7 @@
|
||||
</label>
|
||||
<input type="text"
|
||||
x-model="lot_number"
|
||||
placeholder="{{ _('Es. LOT-2026-001 (opzionale)') }}"
|
||||
placeholder="{{ _('Es. LOT-2026-001') }}"
|
||||
class="tmf-input font-mono">
|
||||
</div>
|
||||
|
||||
@@ -166,7 +183,7 @@
|
||||
</label>
|
||||
<input type="text"
|
||||
x-model="serial_number"
|
||||
placeholder="{{ _('Es. SN-000123 (opzionale)') }}"
|
||||
placeholder="{{ _('Es. SN-000123') }}"
|
||||
class="tmf-input font-mono">
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,9 +265,26 @@
|
||||
<!-- Card Footer -->
|
||||
<div class="px-5 py-3.5 border-t border-[var(--border-color)] bg-[var(--bg-secondary)]
|
||||
rounded-b-xl">
|
||||
<a :href="buildTaskUrl(recipe.id)"
|
||||
|
||||
{# What this recipe insists on before anyone measures. Said on the card
|
||||
rather than after the fact: a measurement taken without its lot cannot
|
||||
be traced back, and finding that out later is finding it out too late. #}
|
||||
<template x-if="missingTraceability(recipe).length">
|
||||
<p class="mb-2 flex items-start gap-1.5 text-xs text-amber-700 dark:text-amber-300">
|
||||
<svg class="w-3.5 h-3.5 shrink-0 mt-px" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/>
|
||||
</svg>
|
||||
<span x-text="'{{ _('Compila prima:') }} ' + missingTraceability(recipe).join(', ')"></span>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<a :href="missingTraceability(recipe).length ? null : buildTaskUrl(recipe.id)"
|
||||
:aria-disabled="missingTraceability(recipe).length > 0"
|
||||
:class="missingTraceability(recipe).length
|
||||
? 'opacity-40 cursor-not-allowed pointer-events-none'
|
||||
: 'group-hover:shadow-md'"
|
||||
class="btn btn-primary w-full justify-center text-sm font-semibold
|
||||
group-hover:shadow-md transition-shadow duration-200">
|
||||
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="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>
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
x-data="taskExecute()"
|
||||
x-init="init()"
|
||||
@numpad-confirm.window="handleMeasurement($event.detail.value, $event.detail.inputMethod)"
|
||||
@numpad-rejected.window="onNumpadRejected($event.detail)"
|
||||
@marker-click.window="goToSubtaskByMarker($event.detail.marker_number)">
|
||||
|
||||
{# ================================================================
|
||||
@@ -229,6 +230,26 @@
|
||||
────────────────────────────────────────────── #}
|
||||
<div class="flex-1 flex flex-col overflow-hidden bg-[var(--bg-secondary)]">
|
||||
|
||||
{# What the task says to do. It was written for the operator and until now
|
||||
only the maker ever saw it; the line breaks and the bold of the technical
|
||||
sheet survive here as they do in the editor. #}
|
||||
{% if task.directive or task.description %}
|
||||
<div class="shrink-0 max-h-36 overflow-y-auto px-4 py-2.5
|
||||
bg-[var(--bg-card)] border-b border-[var(--border-color)]">
|
||||
{% if task.directive %}
|
||||
<p class="text-sm font-semibold text-[var(--text-primary)] leading-snug">
|
||||
{{ task.directive|rich_text }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if task.description %}
|
||||
<div class="text-sm text-[var(--text-secondary)] leading-relaxed
|
||||
{% if task.directive %}mt-1{% endif %}">
|
||||
{{ task.description|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Main image area #}
|
||||
<div class="flex-1 overflow-hidden relative">
|
||||
|
||||
@@ -1034,6 +1055,15 @@ function taskExecute() {
|
||||
return m ? m.pass_fail : null;
|
||||
},
|
||||
|
||||
/* The keypad refused a value because this recipe wants the caliper. Said on
|
||||
the same line as any other measurement error, so there is one place to look
|
||||
when a value does not go in. */
|
||||
onNumpadRejected(detail) {
|
||||
if (detail && detail.reason === 'manual_not_allowed') {
|
||||
this.errorMessage = '{{ _("Questa ricetta non ammette valori digitati: usare il calibro") }}';
|
||||
}
|
||||
},
|
||||
|
||||
// ---- Handle numpad confirm ----
|
||||
async handleMeasurement(value, inputMethod) {
|
||||
if (!this.currentSubtask || this.saving) return;
|
||||
|
||||
@@ -75,8 +75,28 @@
|
||||
</div>
|
||||
|
||||
<!-- Right: AVVIA + Traceability -->
|
||||
{#
|
||||
Avvia does not start while the recipe is still owed its traceability.
|
||||
The selection screen asks for it first, but this page can be reached
|
||||
directly - by a bookmark, or by going back - and the rule has to hold
|
||||
on whichever door the operator comes through.
|
||||
#}
|
||||
{% set needs_lot = recipe.requires_lot|default(false) and not lot_number %}
|
||||
{% set needs_serial = recipe.requires_serial|default(false) and not serial_number %}
|
||||
{% set missing_trace = ([_('lotto')] if needs_lot else [])
|
||||
+ ([_('seriale')] if needs_serial else []) %}
|
||||
<div class="flex flex-col gap-3 sm:items-end shrink-0">
|
||||
{% if tasks %}
|
||||
{% if missing_trace %}
|
||||
<a href="{{ url_for('measure.select_recipe', recipe=recipe.code) }}"
|
||||
class="btn gap-3 w-full sm:w-auto justify-center text-lg font-bold
|
||||
bg-steel-400 text-white px-8 py-3 rounded-xl cursor-pointer">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/>
|
||||
</svg>
|
||||
{{ _('Compila prima:') }} {{ missing_trace|join(', ') }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('measure.task_execute', task_id=tasks[0].id) }}"
|
||||
class="btn gap-3 w-full sm:w-auto justify-center text-lg font-bold
|
||||
bg-red-600 hover:bg-red-700 text-white shadow-lg hover:shadow-xl
|
||||
@@ -87,6 +107,7 @@
|
||||
{{ _('AVVIA') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% 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
|
||||
@@ -162,7 +183,7 @@
|
||||
<!-- Directive -->
|
||||
{% if task.directive or task.description %}
|
||||
<p class="text-xs text-[var(--text-secondary)] leading-snug mt-0.5 line-clamp-2">
|
||||
{{ task.directive or task.description }}
|
||||
{{ (task.directive or task.description)|rich_text }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user