Merge lavoro/daitarn-trafilo-esegui-esiti
This commit is contained in:
@@ -256,6 +256,88 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# #55: the inputs of the fields of a «note» task. One block for the task on screen
|
||||
and for the panel at the end of production, so the rules are written once. #}
|
||||
{% macro result_fields() %}
|
||||
<template x-for="fld in f.fields" :key="fld.key">
|
||||
<div class="mb-3">
|
||||
<label class="block text-sm font-medium text-[var(--text-secondary)] mb-1" x-text="fld.label"></label>
|
||||
<input :type="fld.type === 'number' ? 'number' : 'text'"
|
||||
:step="fld.type === 'number' ? 'any' : null"
|
||||
:inputmode="fld.type === 'number' ? 'decimal' : null"
|
||||
x-model="f.values[fld.key]"
|
||||
class="input w-full text-base py-2">
|
||||
<template x-if="fld.attachment">
|
||||
<div class="mt-1.5">
|
||||
<label class="btn btn-secondary text-xs gap-1.5 cursor-pointer">
|
||||
{{ _('Allega immagine o PDF') }}
|
||||
<input type="file" accept="image/*,application/pdf" class="hidden"
|
||||
@change="pickFile(f, fld.key, $event)">
|
||||
</label>
|
||||
<span class="ml-2 text-xs text-[var(--text-muted)]" x-text="f.fileNames[fld.key]"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
{% endmacro %}
|
||||
|
||||
{# #54 #55: result panel of the task on screen - OK / NOK for a «check», the
|
||||
fields and «Completato» for a «note» with fields. #}
|
||||
<div x-show="hasResultPanel" x-cloak
|
||||
x-data="taskResultForm()"
|
||||
x-init="ctx = () => ({ runId: productionRun ? productionRun.id : null, done: () => advanceAfterResult() });
|
||||
initForTask(task);
|
||||
$watch('productionRun && productionRun.id', () => loadLast())"
|
||||
@task-result-reset.window="justRecorded = false; initForTask(task)"
|
||||
class="shrink-0 max-h-[65%] overflow-y-auto px-4 py-3 bg-[var(--bg-card)] border-b border-[var(--border-color)]">
|
||||
<template x-for="f in forms" :key="f.task_id">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div x-show="f.result" class="mb-3 px-3 py-2 rounded-lg text-sm border border-[var(--border-color)] bg-[var(--bg-secondary)]">
|
||||
<span class="text-[var(--text-secondary)]">{{ _('Ultimo esito') }}:</span>
|
||||
<span class="font-bold"
|
||||
:class="f.result && f.result.outcome === 'nok' ? 'text-measure-fail' : 'text-measure-pass'"
|
||||
x-text="resultLabel(f.result)"></span>
|
||||
<span class="text-[var(--text-muted)]" x-text="f.result && f.result.note ? '— ' + f.result.note : ''"></span>
|
||||
</div>
|
||||
|
||||
<template x-if="f.type === 'check'">
|
||||
<div>
|
||||
<textarea x-model="f.note" rows="2"
|
||||
placeholder="{{ _('Nota (obbligatoria per un NOK)') }}"
|
||||
class="input w-full text-base mb-3"></textarea>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button @click="submit('ok')" :disabled="working"
|
||||
class="btn min-h-[72px] text-2xl font-bold justify-center shadow-md bg-measure-pass text-white hover:opacity-90 disabled:opacity-50">
|
||||
{{ _('OK') }}
|
||||
</button>
|
||||
<button @click="submit('nok')" :disabled="working"
|
||||
class="btn min-h-[72px] text-2xl font-bold justify-center shadow-md bg-red-600 text-white hover:bg-red-700 disabled:opacity-50">
|
||||
{{ _('NOK') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="f.type === 'note'">
|
||||
<div>
|
||||
{{ result_fields() }}
|
||||
<button @click="submit('done')" :disabled="working"
|
||||
class="btn text-lg font-bold w-full min-h-[64px] justify-center gap-2 shadow-md bg-measure-pass text-white hover:opacity-90 disabled:opacity-50">
|
||||
<span>{{ _('Completato') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p x-show="f.error" class="mt-2 text-sm text-measure-fail" x-text="f.error"></p>
|
||||
{# The result is in, but going on failed (cycle refused, network): another try, without recording twice. #}
|
||||
<button x-show="justRecorded" @click="proceed()"
|
||||
class="btn btn-secondary mt-3 w-full justify-center">
|
||||
{{ _('Prosegui') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{# Main image area #}
|
||||
<div class="flex-1 overflow-hidden relative">
|
||||
|
||||
@@ -747,7 +829,8 @@
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!isMeasureTask || subtasks.length === 0">
|
||||
{# #54 #55: a check / note-with-fields task has its own buttons in the panel #}
|
||||
<template x-if="(!isMeasureTask || subtasks.length === 0) && !hasResultPanel">
|
||||
{# #60: unico «Completato», largo e alto almeno 64 px per il touch. #}
|
||||
<button @click="goToNextTask()"
|
||||
:disabled="!canLeaveTask"
|
||||
@@ -895,6 +978,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================================================================
|
||||
#55 TASK DI CHIUSURA — before the capoturno's modal, the notes marked «alla fine
|
||||
della produzione» are filled in and recorded on the run. The modal below is
|
||||
untouched: it opens when this panel is done.
|
||||
================================================================ #}
|
||||
<div x-show="open" x-cloak
|
||||
x-data="taskResultForm()"
|
||||
x-init="ctx = () => ({ runId: productionRun ? productionRun.id : null, done: () => closeTasksRecorded() })"
|
||||
@task-close-open.window="openClose($event.detail.ids)"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div class="bg-[var(--bg-card)] rounded-2xl shadow-2xl p-6 w-full max-w-xl mx-4 max-h-[90vh] overflow-y-auto border border-[var(--border-color)]">
|
||||
<h3 class="text-lg font-bold text-[var(--text-primary)] mb-3">{{ _('Prima di chiudere la produzione') }}</h3>
|
||||
<p x-show="working && !forms.length" class="text-sm text-[var(--text-muted)]">{{ _('Caricamento...') }}</p>
|
||||
<template x-for="f in forms" :key="f.task_id">
|
||||
<div class="mb-4" x-show="!f.recorded">
|
||||
<div class="font-semibold text-[var(--text-primary)] mb-1" x-text="f.title"></div>
|
||||
<div class="text-sm text-[var(--text-secondary)] mb-2" x-show="f.directive" x-text="f.directive"></div>
|
||||
{{ result_fields() }}
|
||||
<textarea x-model="f.note" rows="2" placeholder="{{ _('Nota') }}" class="input w-full text-base"></textarea>
|
||||
<p x-show="f.error" class="mt-1 text-sm text-measure-fail" x-text="f.error"></p>
|
||||
</div>
|
||||
</template>
|
||||
<p x-show="error" class="mb-2 text-sm text-measure-fail" x-text="error"></p>
|
||||
<div class="flex gap-3 mt-2">
|
||||
<button @click="cancelClose()" class="btn btn-secondary flex-1 justify-center">{{ _('Annulla') }}</button>
|
||||
<button @click="submitClose()" :disabled="working || !forms.length"
|
||||
class="btn flex-1 justify-center font-bold bg-measure-pass text-white hover:opacity-90 disabled:opacity-50">
|
||||
{{ _('Registra e prosegui') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================================================================
|
||||
SUPERVISOR LOGIN MODAL
|
||||
================================================================ #}
|
||||
@@ -1028,6 +1144,26 @@
|
||||
};
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/vision_execute.js') }}"></script>
|
||||
{# #53 #54 #55: result of check / note tasks (urls and words for the component) #}
|
||||
<script>
|
||||
window.__taskResultsCfg = {
|
||||
saveUrl: '{{ url_for("measure.api_save_task_result") }}',
|
||||
listUrl: '{{ url_for("measure.api_list_task_results") }}',
|
||||
uploadUrl: '{{ url_for("measure.api_upload_attachment") }}',
|
||||
taskUrlTpl: '{{ url_for("measure.api_get_task", task_id=0) }}',
|
||||
labels: {
|
||||
connection: {{ _('Errore di connessione')|tojson }},
|
||||
sessionExpired: {{ _('Sessione scaduta: ricaricare la pagina')|tojson }},
|
||||
serverError: {{ _('Errore di comunicazione con il server')|tojson }},
|
||||
nokNeedsNote: {{ _('Per un NOK serve la nota')|tojson }},
|
||||
notANumber: {{ _('Valore non numerico')|tojson }},
|
||||
outcomeOk: {{ _('OK')|tojson }},
|
||||
outcomeNok: {{ _('NOK')|tojson }},
|
||||
outcomeDone: {{ _('Registrato')|tojson }},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/task-results.js') }}"></script>
|
||||
{# The clock's rules - how a delay reads, when the buzzer goes, where the
|
||||
measurement lives - are shared with the other screens rather than restated. #}
|
||||
<script src="{{ url_for('static', filename='js/production-clock.js') }}"></script>
|
||||
@@ -1076,6 +1212,10 @@ function taskExecute() {
|
||||
// The measurement tasks of this recipe, in order: the loop the operator stays
|
||||
// in once production starts, and what says which task closes a cycle.
|
||||
measurementTaskIds: [],
|
||||
// #55: notes to fill in before the capoturno closes the production, and whether
|
||||
// they were already recorded (so cancelling the modal does not ask them again).
|
||||
closeTaskIds: [],
|
||||
closeTasksDone: false,
|
||||
// The interval has elapsed and it has been said out loud - buzzer, and the
|
||||
// operator on their way back to the measurement. Kept so it is said once.
|
||||
dueAnnounced: false,
|
||||
@@ -1323,6 +1463,7 @@ function taskExecute() {
|
||||
this.productionStarted = run.status !== 'closed';
|
||||
this.cycleCount = run.cycle_count;
|
||||
this.measurementTaskIds = run.measurement_task_ids || [];
|
||||
this.closeTaskIds = run.close_task_ids || []; // #55
|
||||
|
||||
const seconds = run.seconds_to_next_measurement;
|
||||
if (seconds === null || seconds === undefined) {
|
||||
@@ -1749,6 +1890,7 @@ function taskExecute() {
|
||||
this.currentValue = null;
|
||||
this.errorMessage = '';
|
||||
this.inputStartedAt = Date.now();
|
||||
window.dispatchEvent(new CustomEvent('task-result-reset')); // #54: a check / note loops on the spot
|
||||
},
|
||||
|
||||
// ---- Where the measurement lives ----
|
||||
@@ -1841,6 +1983,33 @@ function taskExecute() {
|
||||
return window.ProductionClock.format(this.timerRemaining);
|
||||
},
|
||||
|
||||
/* #53: result recorded on a check / note task. In the loop of a production the
|
||||
task counts as a step of the cycle exactly like a measurement task: the last
|
||||
one closes the cycle (the server decides, as in confirmCycle), the others go
|
||||
to the next one. If the server refuses, the panel stays with «Prosegui». */
|
||||
async advanceAfterResult() {
|
||||
if (this.productionRun && this.isMeasurementTaskOfRun) {
|
||||
const countBefore = this.cycleCount;
|
||||
const run = await this.postProduction(
|
||||
'{{ url_for("measure.api_complete_cycle", run_id=0) }}'.replace('/0/', '/' + this.productionRun.id + '/'),
|
||||
{ task_id: this.task.id },
|
||||
);
|
||||
if (!run) return;
|
||||
this.adoptProductionRun(run);
|
||||
if (run.cycle_count === countBefore && this.nextMeasurementTaskId) {
|
||||
this.goToTask(this.nextMeasurementTaskId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.goToNextTask();
|
||||
},
|
||||
|
||||
/* #55: the end-of-production notes are recorded: now the capoturno's modal. */
|
||||
closeTasksRecorded() {
|
||||
this.closeTasksDone = true;
|
||||
this.openSupervisorModal('fine_produzione');
|
||||
},
|
||||
|
||||
// ---- Navigate to next task (Completato) ----
|
||||
/* With a production open the operator stays in the measurement loop until the
|
||||
capoturno closes it: after the last measurement task they go back to the
|
||||
@@ -1881,6 +2050,12 @@ function taskExecute() {
|
||||
this.productionError = '{{ _("Nessuna produzione aperta su questa stazione") }}';
|
||||
return;
|
||||
}
|
||||
// #55: first the notes marked «alla fine della produzione»; the panel calls back
|
||||
// here (closeTasksRecorded) when they are recorded.
|
||||
if (action === 'fine_produzione' && this.closeTaskIds.length && !this.closeTasksDone) {
|
||||
window.dispatchEvent(new CustomEvent('task-close-open', { detail: { ids: this.closeTaskIds } }));
|
||||
return;
|
||||
}
|
||||
this.supervisorAction = action;
|
||||
this.supervisorUsername = '';
|
||||
this.supervisorPassword = '';
|
||||
@@ -1975,6 +2150,19 @@ function taskExecute() {
|
||||
return t === 'camera_measure' || t === 'dxf_compare';
|
||||
},
|
||||
|
||||
/* #54: «Controllo OK/NOK» - no quotes, the result is the outcome. */
|
||||
get isCheckTask() {
|
||||
return this.task.task_type === 'check';
|
||||
},
|
||||
|
||||
/* #54 #55: tasks that bring their own result panel (a check, or a note with
|
||||
fields). A note without fields stays the plain «Completato» task. */
|
||||
get hasResultPanel() {
|
||||
if (this.isCheckTask) return true;
|
||||
return this.task.task_type === 'note' && Array.isArray(this.task.fields)
|
||||
&& this.task.fields.length > 0;
|
||||
},
|
||||
|
||||
/* A measurement task with no quotes yet: the recipe is unfinished. Said out
|
||||
loud rather than silently rendering an empty screen. */
|
||||
get isMeasureTaskWithoutQuotes() {
|
||||
|
||||
Reference in New Issue
Block a user