Merge lavoro/daitarn-trafilo-editor-task
This commit is contained in:
@@ -91,6 +91,74 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{# #53 #55: parte del form task comune a creazione e modifica; `d` e il nome dello stato Alpine (newTask / editTaskData) #}
|
||||||
|
{% macro task_flags_editor(d) %}
|
||||||
|
<div class="sm:col-span-2 space-y-3">
|
||||||
|
<!-- #53: il task entra nel ciclo di misura (timer e cicalino) -->
|
||||||
|
<label class="flex items-start gap-2 text-sm cursor-pointer">
|
||||||
|
<input type="checkbox" class="mt-0.5"
|
||||||
|
x-model="{{ d }}.in_loop"
|
||||||
|
@change="{{ d }}.in_loop_touched = true">
|
||||||
|
<span>{{ _('Nel ciclo di misura (timer e cicalino)') }}</span>
|
||||||
|
</label>
|
||||||
|
<!-- #55: opzioni valide solo per il tipo Nota -->
|
||||||
|
<template x-if="{{ d }}.task_type === 'note'">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="flex items-start gap-2 text-sm cursor-pointer">
|
||||||
|
<input type="checkbox" class="mt-0.5" x-model="{{ d }}.at_close">
|
||||||
|
<span>{{ _('Compila a fine produzione') }}</span>
|
||||||
|
</label>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<label class="tmf-label mb-0">{{ _('Campi da compilare') }}</label>
|
||||||
|
<button type="button" class="btn btn-secondary text-xs gap-1"
|
||||||
|
:disabled="{{ d }}.fields.length >= MAX_NOTE_FIELDS"
|
||||||
|
@click="addNoteField({{ d }})">
|
||||||
|
+ {{ _('Aggiungi campo') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<template x-for="(f, fi) in {{ d }}.fields" :key="f._uid">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-12 gap-2 p-2 rounded border border-[var(--border-color)] bg-[var(--bg-secondary)]">
|
||||||
|
<div class="sm:col-span-4">
|
||||||
|
<input type="text" class="tmf-input text-sm" maxlength="100"
|
||||||
|
placeholder="{{ _('Etichetta') }}"
|
||||||
|
x-model="f.label"
|
||||||
|
@input="deriveNoteFieldKey({{ d }}, f)">
|
||||||
|
<span class="block mt-0.5 text-[10px] font-mono text-[var(--text-muted)]" x-text="f.key"></span>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<select class="tmf-input text-sm" x-model="f.type">
|
||||||
|
<option value="text">{{ _('Testo') }}</option>
|
||||||
|
<option value="number">{{ _('Numero') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-3">
|
||||||
|
<input type="text" class="tmf-input text-sm"
|
||||||
|
placeholder="{{ _('Valore precompilato') }}"
|
||||||
|
x-model="f.default">
|
||||||
|
</div>
|
||||||
|
<label class="sm:col-span-1 flex items-center gap-1 text-xs cursor-pointer">
|
||||||
|
<input type="checkbox" x-model="f.attachment">
|
||||||
|
<span>{{ _('Allegato') }}</span>
|
||||||
|
</label>
|
||||||
|
<div class="sm:col-span-2 flex items-center justify-end gap-1">
|
||||||
|
<button type="button" class="p-1 rounded hover:bg-[var(--bg-card-hover)]"
|
||||||
|
:disabled="fi === 0" @click="moveNoteField({{ d }}, fi, -1)"
|
||||||
|
title="{{ _('Sposta su') }}">↑</button>
|
||||||
|
<button type="button" class="p-1 rounded hover:bg-[var(--bg-card-hover)]"
|
||||||
|
:disabled="fi === {{ d }}.fields.length - 1" @click="moveNoteField({{ d }}, fi, 1)"
|
||||||
|
title="{{ _('Sposta giu') }}">↓</button>
|
||||||
|
<button type="button" class="p-1 rounded text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||||
|
@click="removeNoteField({{ d }}, fi)"
|
||||||
|
title="{{ _('Rimuovi campo') }}">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
<div class="tmf-page"
|
<div class="tmf-page"
|
||||||
x-data="taskEditor()"
|
x-data="taskEditor()"
|
||||||
x-cloak>
|
x-cloak>
|
||||||
@@ -273,12 +341,15 @@
|
|||||||
<!-- Tipo: dichiarato qui, non dedotto dal contenuto -->
|
<!-- Tipo: dichiarato qui, non dedotto dal contenuto -->
|
||||||
<div>
|
<div>
|
||||||
<label class="tmf-label">{{ _('Tipo') }} <span class="text-red-500">*</span></label>
|
<label class="tmf-label">{{ _('Tipo') }} <span class="text-red-500">*</span></label>
|
||||||
<select x-model="newTask.task_type" class="tmf-input">
|
<select x-model="newTask.task_type" class="tmf-input"
|
||||||
|
@change="onTaskTypeChange(newTask, $event.target.value)">
|
||||||
<option value="note">{{ _('Nota') }}</option>
|
<option value="note">{{ _('Nota') }}</option>
|
||||||
<option value="measure">{{ _('Misura') }}</option>
|
<option value="measure">{{ _('Misura') }}</option>
|
||||||
<option value="drawing">{{ _('Disegno') }}</option>
|
<option value="drawing">{{ _('Disegno') }}</option>
|
||||||
<option value="camera_measure">{{ _('Misura camera') }}</option>
|
<option value="camera_measure">{{ _('Misura camera') }}</option>
|
||||||
<option value="dxf_compare">{{ _('Confronto profilo') }}</option>
|
<option value="dxf_compare">{{ _('Confronto profilo') }}</option>
|
||||||
|
<!-- #54: task di controllo con esito OK/NOK, senza quote -->
|
||||||
|
<option value="check">{{ _('Controllo OK/NOK') }}</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="mt-1 text-xs text-[var(--text-secondary)]">
|
<p class="mt-1 text-xs text-[var(--text-secondary)]">
|
||||||
{{ _('Un task di misura resta tale anche prima di inserire le quote') }}
|
{{ _('Un task di misura resta tale anche prima di inserire le quote') }}
|
||||||
@@ -306,6 +377,8 @@
|
|||||||
rows="3"
|
rows="3"
|
||||||
placeholder="{{ _('Descrizione opzionale...') }}"></textarea>
|
placeholder="{{ _('Descrizione opzionale...') }}"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
{# #53 #55: ciclo di misura, compilazione a fine produzione e campi della nota #}
|
||||||
|
{{ task_flags_editor('newTask') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tmf-card-footer flex items-center gap-2 justify-end">
|
<div class="tmf-card-footer flex items-center gap-2 justify-end">
|
||||||
@@ -387,8 +460,20 @@
|
|||||||
x-ref="editTaskTitleInput">
|
x-ref="editTaskTitleInput">
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Badge: N misurazioni -->
|
<!-- #53: segno sui task nel ciclo di misura -->
|
||||||
|
<span x-show="task.in_loop" class="shrink-0 text-primary"
|
||||||
|
title="{{ _('Nel ciclo di misura (timer e cicalino)') }}">
|
||||||
|
<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="M4 4v5h5M20 20v-5h-5M5.5 15A8 8 0 0018.4 17.4M18.5 9A8 8 0 005.6 6.6"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<!-- #55: nota compilata a fine produzione -->
|
||||||
|
<span x-show="task.at_close" class="badge badge-neutral text-xs shrink-0">{{ _('A fine produzione') }}</span>
|
||||||
|
|
||||||
|
<!-- Badge: N misurazioni (non per i task di controllo, senza quote) -->
|
||||||
<span class="badge badge-neutral text-xs shrink-0"
|
<span class="badge badge-neutral text-xs shrink-0"
|
||||||
|
x-show="task.task_type !== 'check'"
|
||||||
x-text="(task.subtasks ? task.subtasks.length : 0) + ' {{ _('misurazioni') }}'">
|
x-text="(task.subtasks ? task.subtasks.length : 0) + ' {{ _('misurazioni') }}'">
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -590,12 +675,14 @@
|
|||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 p-3 rounded-lg bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 p-3 rounded-lg bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800">
|
||||||
<div>
|
<div>
|
||||||
<label class="tmf-label">{{ _('Tipo') }}</label>
|
<label class="tmf-label">{{ _('Tipo') }}</label>
|
||||||
<select x-model="editTaskData.task_type" class="tmf-input text-sm">
|
<select x-model="editTaskData.task_type" class="tmf-input text-sm"
|
||||||
|
@change="onTaskTypeChange(editTaskData, $event.target.value)">
|
||||||
<option value="note">{{ _('Nota') }}</option>
|
<option value="note">{{ _('Nota') }}</option>
|
||||||
<option value="measure">{{ _('Misura') }}</option>
|
<option value="measure">{{ _('Misura') }}</option>
|
||||||
<option value="drawing">{{ _('Disegno') }}</option>
|
<option value="drawing">{{ _('Disegno') }}</option>
|
||||||
<option value="camera_measure">{{ _('Misura camera') }}</option>
|
<option value="camera_measure">{{ _('Misura camera') }}</option>
|
||||||
<option value="dxf_compare">{{ _('Confronto profilo') }}</option>
|
<option value="dxf_compare">{{ _('Confronto profilo') }}</option>
|
||||||
|
<option value="check">{{ _('Controllo OK/NOK') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -618,12 +705,14 @@
|
|||||||
rows="3"
|
rows="3"
|
||||||
placeholder="{{ _('Descrizione opzionale...') }}"></textarea>
|
placeholder="{{ _('Descrizione opzionale...') }}"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
{# #53 #55: ciclo di misura, compilazione a fine produzione e campi della nota #}
|
||||||
|
{{ task_flags_editor('editTaskData') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ========== Subtask Table ========== -->
|
<!-- ========== Subtask Table ========== -->
|
||||||
<div class="subtask-table-wrap">
|
<div class="subtask-table-wrap">
|
||||||
<table class="tmf-table" x-show="task.subtasks && task.subtasks.length > 0">
|
<table class="tmf-table" x-show="task.task_type !== 'check' && task.subtasks && task.subtasks.length > 0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-16 text-center">{{ _('#') }}</th>
|
<th class="w-16 text-center">{{ _('#') }}</th>
|
||||||
@@ -710,7 +799,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========== Empty Subtask State ========== -->
|
<!-- ========== Empty Subtask State ========== -->
|
||||||
<div x-show="!task.subtasks || task.subtasks.length === 0"
|
<div x-show="task.task_type !== 'check' && (!task.subtasks || task.subtasks.length === 0)"
|
||||||
class="text-center py-8">
|
class="text-center py-8">
|
||||||
<div class="inline-flex items-center justify-center w-14 h-14 rounded-full
|
<div class="inline-flex items-center justify-center w-14 h-14 rounded-full
|
||||||
bg-[var(--bg-secondary)] mb-3">
|
bg-[var(--bg-secondary)] mb-3">
|
||||||
@@ -862,7 +951,7 @@
|
|||||||
<!-- ========== Task Card Footer ========== -->
|
<!-- ========== Task Card Footer ========== -->
|
||||||
<div class="tmf-card-footer flex items-center justify-end">
|
<div class="tmf-card-footer flex items-center justify-end">
|
||||||
<button @click="openAddSubtask(task)"
|
<button @click="openAddSubtask(task)"
|
||||||
x-show="addingSubtaskForTask !== task.id"
|
x-show="addingSubtaskForTask !== task.id && task.task_type !== 'check'"
|
||||||
class="btn btn-secondary text-xs gap-1.5">
|
class="btn btn-secondary text-xs gap-1.5">
|
||||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/>
|
||||||
@@ -1210,8 +1299,10 @@ function taskEditor() {
|
|||||||
|
|
||||||
// ---- Task CRUD state ----
|
// ---- Task CRUD state ----
|
||||||
editingTask: null,
|
editingTask: null,
|
||||||
editTaskData: { title: '', directive: '', description: '', task_type: 'note' },
|
editTaskData: { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] },
|
||||||
newTask: { title: '', directive: '', description: '', task_type: 'note' },
|
newTask: { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] },
|
||||||
|
MAX_NOTE_FIELDS: 20, // #55: limite campi per nota (stesso del backend)
|
||||||
|
_fieldUid: 0,
|
||||||
|
|
||||||
// ---- Subtask CRUD state ----
|
// ---- Subtask CRUD state ----
|
||||||
editingSubtaskId: null,
|
editingSubtaskId: null,
|
||||||
@@ -1391,7 +1482,84 @@ function taskEditor() {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
resetNewTask() {
|
resetNewTask() {
|
||||||
this.newTask = { title: '', directive: '', description: '', task_type: 'note' };
|
this.newTask = { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- #53 #55: ciclo di misura e campi della nota ----
|
||||||
|
|
||||||
|
// #53: default di "nel ciclo" per tipo (stessa regola del backend)
|
||||||
|
loopDefault(type) {
|
||||||
|
return ['measure', 'camera_measure', 'dxf_compare', 'check'].includes(type);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cambio tipo: il default del ciclo segue il tipo finche l'utente non lo tocca;
|
||||||
|
// at_close e campi esistono solo per la nota.
|
||||||
|
onTaskTypeChange(obj, type) {
|
||||||
|
obj.task_type = type;
|
||||||
|
if (!obj.in_loop_touched) obj.in_loop = this.loopDefault(type);
|
||||||
|
if (type !== 'note') { obj.at_close = false; obj.fields = []; }
|
||||||
|
},
|
||||||
|
|
||||||
|
// #55: key dall'etichetta (minuscole, [a-z0-9_], max 40), unica nella lista
|
||||||
|
_slugKey(label) {
|
||||||
|
const base = (label || '').normalize('NFD').replace(/[̀-ͯ]/g, '')
|
||||||
|
.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 40);
|
||||||
|
return base || 'campo';
|
||||||
|
},
|
||||||
|
|
||||||
|
deriveNoteFieldKey(obj, f) {
|
||||||
|
if (f.locked) return; // la key non cambia piu dopo il primo salvataggio
|
||||||
|
const used = new Set(obj.fields.filter(x => x !== f).map(x => x.key));
|
||||||
|
const base = this._slugKey(f.label);
|
||||||
|
let key = base, n = 2;
|
||||||
|
while (used.has(key)) {
|
||||||
|
const suffix = '_' + n++;
|
||||||
|
key = base.slice(0, 40 - suffix.length) + suffix;
|
||||||
|
}
|
||||||
|
f.key = key;
|
||||||
|
},
|
||||||
|
|
||||||
|
addNoteField(obj) {
|
||||||
|
if (obj.fields.length >= this.MAX_NOTE_FIELDS) return;
|
||||||
|
const f = { _uid: ++this._fieldUid, key: '', label: '', type: 'text', default: '', attachment: false, locked: false };
|
||||||
|
obj.fields.push(f);
|
||||||
|
this.deriveNoteFieldKey(obj, f);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeNoteField(obj, i) {
|
||||||
|
obj.fields.splice(i, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
moveNoteField(obj, i, dir) {
|
||||||
|
const j = i + dir;
|
||||||
|
if (j < 0 || j >= obj.fields.length) return;
|
||||||
|
const [f] = obj.fields.splice(i, 1);
|
||||||
|
obj.fields.splice(j, 0, f);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Campi salvati -> stato di modifica (key bloccate)
|
||||||
|
_fieldsToState(fields) {
|
||||||
|
return (fields || []).map(f => ({
|
||||||
|
_uid: ++this._fieldUid, key: f.key, label: f.label || '', type: f.type || 'text',
|
||||||
|
default: f.default == null ? '' : f.default, attachment: !!f.attachment, locked: true
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Stato -> corpo API (fields null se non e una nota o e vuota); ok=false con errorMessage se manca un'etichetta.
|
||||||
|
_fieldsPayload(obj) {
|
||||||
|
if (obj.task_type !== 'note') return { ok: true, fields: null };
|
||||||
|
const out = [];
|
||||||
|
for (const f of obj.fields) {
|
||||||
|
if (!f.label.trim()) {
|
||||||
|
this.errorMessage = {{ _("Ogni campo deve avere un'etichetta")|tojson }};
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
out.push({
|
||||||
|
key: f.key, label: f.label.trim(), type: f.type || 'text',
|
||||||
|
default: (f.default || '').trim() || null, attachment: !!f.attachment
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { ok: true, fields: out.length ? out : null };
|
||||||
},
|
},
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -1470,8 +1638,10 @@ function taskEditor() {
|
|||||||
|
|
||||||
async addTask() {
|
async addTask() {
|
||||||
if (!this.newTask.title.trim()) return;
|
if (!this.newTask.title.trim()) return;
|
||||||
this.saving = true;
|
|
||||||
this.errorMessage = '';
|
this.errorMessage = '';
|
||||||
|
const fp = this._fieldsPayload(this.newTask); // #55
|
||||||
|
if (!fp.ok) return;
|
||||||
|
this.saving = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/maker/api/recipes/${this.recipeId}/tasks`, {
|
const resp = await fetch(`/maker/api/recipes/${this.recipeId}/tasks`, {
|
||||||
@@ -1484,7 +1654,11 @@ function taskEditor() {
|
|||||||
title: this.newTask.title.trim(),
|
title: this.newTask.title.trim(),
|
||||||
task_type: this.newTask.task_type || 'note',
|
task_type: this.newTask.task_type || 'note',
|
||||||
directive: this.newTask.directive.trim() || null,
|
directive: this.newTask.directive.trim() || null,
|
||||||
description: this.newTask.description.trim() || null
|
description: this.newTask.description.trim() || null,
|
||||||
|
// #53 #55: ciclo di misura, compilazione a fine produzione, campi della nota
|
||||||
|
in_loop: !!this.newTask.in_loop,
|
||||||
|
at_close: this.newTask.task_type === 'note' && !!this.newTask.at_close,
|
||||||
|
fields: fp.fields
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1516,7 +1690,12 @@ function taskEditor() {
|
|||||||
title: task.title,
|
title: task.title,
|
||||||
task_type: task.task_type || 'note',
|
task_type: task.task_type || 'note',
|
||||||
directive: task.directive || '',
|
directive: task.directive || '',
|
||||||
description: task.description || ''
|
description: task.description || '',
|
||||||
|
// #53 #55: valore salvato (task vecchi senza il campo: default del tipo)
|
||||||
|
in_loop: task.in_loop != null ? !!task.in_loop : this.loopDefault(task.task_type || 'note'),
|
||||||
|
in_loop_touched: false,
|
||||||
|
at_close: !!task.at_close,
|
||||||
|
fields: this._fieldsToState(task.fields)
|
||||||
};
|
};
|
||||||
// Ensure expanded
|
// Ensure expanded
|
||||||
this.expandedTask = task.id;
|
this.expandedTask = task.id;
|
||||||
@@ -1524,13 +1703,15 @@ function taskEditor() {
|
|||||||
|
|
||||||
cancelEditTask() {
|
cancelEditTask() {
|
||||||
this.editingTask = null;
|
this.editingTask = null;
|
||||||
this.editTaskData = { title: '', directive: '', description: '', task_type: 'note' };
|
this.editTaskData = { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] };
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateTask(taskId) {
|
async updateTask(taskId) {
|
||||||
if (!this.editTaskData.title.trim()) return;
|
if (!this.editTaskData.title.trim()) return;
|
||||||
this.saving = true;
|
|
||||||
this.errorMessage = '';
|
this.errorMessage = '';
|
||||||
|
const fp = this._fieldsPayload(this.editTaskData); // #55
|
||||||
|
if (!fp.ok) return;
|
||||||
|
this.saving = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/maker/api/tasks/${taskId}`, {
|
const resp = await fetch(`/maker/api/tasks/${taskId}`, {
|
||||||
@@ -1543,7 +1724,11 @@ function taskEditor() {
|
|||||||
title: this.editTaskData.title.trim(),
|
title: this.editTaskData.title.trim(),
|
||||||
task_type: this.editTaskData.task_type || 'note',
|
task_type: this.editTaskData.task_type || 'note',
|
||||||
directive: this.editTaskData.directive.trim() || null,
|
directive: this.editTaskData.directive.trim() || null,
|
||||||
description: this.editTaskData.description.trim() || null
|
description: this.editTaskData.description.trim() || null,
|
||||||
|
// #53 #55: ciclo di misura, compilazione a fine produzione, campi della nota
|
||||||
|
in_loop: !!this.editTaskData.in_loop,
|
||||||
|
at_close: this.editTaskData.task_type === 'note' && !!this.editTaskData.at_close,
|
||||||
|
fields: fp.fields
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""Editor task del Maker: tipo check, in_loop, at_close, campi della nota (#53 #54 #55)."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
from tests.conftest import api_get_router
|
||||||
|
|
||||||
|
|
||||||
|
def _editor_html(logged_in_client, mock_api_client, tasks):
|
||||||
|
mock_api_client.get.side_effect = api_get_router({
|
||||||
|
"/api/recipes/3": {"id": 3, "code": "R", "name": "R"},
|
||||||
|
"/api/recipes/3/tasks": tasks,
|
||||||
|
})
|
||||||
|
resp = logged_in_client.get("/maker/recipes/3/tasks")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
return resp.get_data(as_text=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskEditorFields:
|
||||||
|
def test_check_option_in_both_selects(self, logged_in_client, mock_api_client):
|
||||||
|
"""#54: il tipo check e presente nella select di creazione e in quella di modifica."""
|
||||||
|
html = _editor_html(logged_in_client, mock_api_client, [{
|
||||||
|
"id": 11, "title": "T", "task_type": "note", "subtasks": [],
|
||||||
|
"file_path": None, "order_index": 1,
|
||||||
|
}])
|
||||||
|
assert html.count('<option value="check">') == 2
|
||||||
|
assert "Controllo OK/NOK" in html
|
||||||
|
|
||||||
|
def test_loop_and_note_controls_present(self, logged_in_client, mock_api_client):
|
||||||
|
"""#53 #55: checkbox del ciclo, a fine produzione e editor dei campi in entrambi i form."""
|
||||||
|
html = _editor_html(logged_in_client, mock_api_client, [{
|
||||||
|
"id": 11, "title": "T", "task_type": "note", "subtasks": [],
|
||||||
|
"file_path": None, "order_index": 1, "in_loop": True,
|
||||||
|
}])
|
||||||
|
assert "Nel ciclo di misura (timer e cicalino)" in html
|
||||||
|
assert "Compila a fine produzione" in html
|
||||||
|
assert 'x-model="newTask.in_loop"' in html
|
||||||
|
assert 'x-model="editTaskData.in_loop"' in html
|
||||||
|
assert "addNoteField(newTask)" in html
|
||||||
|
assert "addNoteField(editTaskData)" in html
|
||||||
|
# segno sui task nel ciclo, e niente quote per check
|
||||||
|
assert 'x-show="task.in_loop"' in html
|
||||||
|
assert "task.task_type !== 'check'" in html
|
||||||
|
|
||||||
|
def test_save_bodies_carry_new_fields(self, logged_in_client, mock_api_client):
|
||||||
|
"""Creazione e modifica inviano in_loop, at_close e fields nel corpo."""
|
||||||
|
html = _editor_html(logged_in_client, mock_api_client, [])
|
||||||
|
assert len(re.findall(r"in_loop: !!this\.(newTask|editTaskData)\.in_loop", html)) == 2
|
||||||
|
assert len(re.findall(r"fields: fp\.fields", html)) == 2
|
||||||
|
assert len(re.findall(r"at_close: this\.(newTask|editTaskData)\.task_type === 'note'", html)) == 2
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskProxyForwardsFields:
|
||||||
|
"""I proxy inoltrano il corpo cosi com'e: in_loop/at_close/fields arrivano al backend."""
|
||||||
|
|
||||||
|
BODY = {
|
||||||
|
"title": "Note finali", "task_type": "note", "in_loop": False, "at_close": True,
|
||||||
|
"fields": [{"key": "operatore", "label": "Operatore", "type": "text",
|
||||||
|
"default": None, "attachment": False}],
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_create_forwards(self, logged_in_client, mock_api_client):
|
||||||
|
mock_api_client.post.return_value = {"id": 5, **self.BODY}
|
||||||
|
resp = logged_in_client.post("/maker/api/recipes/3/tasks", json=self.BODY)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
mock_api_client.post.assert_called_once_with("/api/recipes/3/tasks", data=self.BODY)
|
||||||
|
|
||||||
|
def test_update_forwards(self, logged_in_client, mock_api_client):
|
||||||
|
mock_api_client.put.return_value = {"id": 5, **self.BODY}
|
||||||
|
resp = logged_in_client.put("/maker/api/tasks/5", json=self.BODY)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
mock_api_client.put.assert_called_once_with("/api/tasks/5", data=self.BODY)
|
||||||
|
|
||||||
|
def test_update_422_detail_returned(self, logged_in_client, mock_api_client):
|
||||||
|
"""Un 422 del backend arriva al browser con il suo detail e lo status."""
|
||||||
|
mock_api_client.put.return_value = {
|
||||||
|
"error": True, "status_code": 422, "detail": "Massimo 20 campi",
|
||||||
|
}
|
||||||
|
resp = logged_in_client.put("/maker/api/tasks/5", json=self.BODY)
|
||||||
|
assert resp.status_code == 422
|
||||||
|
assert resp.get_json()["detail"] == "Massimo 20 campi"
|
||||||
Reference in New Issue
Block a user