Files
TieMeasureFlow/client/templates/maker/recipe_list.html
T
Adriano dbfb5591c5 fix: separate recipe preview image from task images
Recipe image_path is now used as preview thumbnail only. Removed
auto-creation of "Technical Drawing" task from recipe upload, and
removed recipe image strip from task_execute view. Each task displays
its own file_path independently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 18:29:27 +01:00

397 lines
17 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ _('Gestione Ricette') }} — TieMeasureFlow{% endblock %}
{% block content %}
<script>window.__recipeListData = {{ recipes|tojson }};</script>
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-6xl"
x-data="{
recipes: window.__recipeListData,
search: '{{ search or '' }}',
filter: 'all',
deleteModal: false,
deleteTarget: null,
get filteredRecipes() {
let filtered = this.recipes;
// Apply search filter
if (this.search.trim()) {
const term = this.search.toLowerCase().trim();
filtered = filtered.filter(r =>
r.name.toLowerCase().includes(term) ||
r.code.toLowerCase().includes(term) ||
(r.description && r.description.toLowerCase().includes(term))
);
}
// Apply status filter
if (this.filter === 'active') {
filtered = filtered.filter(r => r.active);
} else if (this.filter === 'inactive') {
filtered = filtered.filter(r => !r.active);
}
return filtered;
},
async deleteRecipe(id) {
const csrfToken = document.querySelector('meta[name=csrf-token]')?.content;
try {
const resp = await fetch(`/maker/api/recipes/${id}`, {
method: 'DELETE',
headers: { 'X-CSRFToken': csrfToken || '' }
});
if (resp.ok) {
this.recipes = this.recipes.filter(r => r.id !== id);
this.deleteModal = false;
this.deleteTarget = null;
} else {
const data = await resp.json();
alert(data.error || '{{ _('Errore durante eliminazione') }}');
}
} catch (err) {
console.error(err);
alert('{{ _('Errore di connessione') }}');
}
},
openDeleteModal(recipe) {
this.deleteTarget = recipe;
this.deleteModal = true;
}
}">
<!-- 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('maker.recipe_list') }}"
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="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
{{ _('Dashboard') }}
</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)]">
{{ _('Ricette') }}
</li>
</ol>
</nav>
<!-- Header Section -->
<div class="mb-8">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-3">
<div>
<h1 class="text-3xl font-bold text-[var(--text-primary)] mb-1">
{{ _('Gestione Ricette') }}
</h1>
<p class="text-sm text-[var(--text-secondary)]">
{{ _('Crea e gestisci le ricette di misura') }}
</p>
</div>
<a href="{{ url_for('maker.recipe_new') }}"
class="btn btn-primary gap-2 shrink-0 w-full sm:w-auto justify-center">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/>
</svg>
{{ _('Nuova Ricetta') }}
</a>
</div>
</div>
<!-- Filters -->
<div class="tmf-card mb-6">
<div class="p-4">
<div class="flex flex-col sm:flex-row gap-3">
<!-- Search Input -->
<div class="flex-1">
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<input type="text"
x-model="search"
class="tmf-input pl-10"
:placeholder="'{{ _('Cerca per nome, codice o descrizione...') }}'">
</div>
</div>
<!-- Status Filter -->
<div class="sm:w-48">
<select x-model="filter" class="tmf-input">
<option value="all">{{ _('Tutti') }}</option>
<option value="active">{{ _('Attive') }}</option>
<option value="inactive">{{ _('Disattivate') }}</option>
</select>
</div>
</div>
</div>
</div>
<!-- Results Count -->
<div class="mb-5">
<p class="text-sm text-[var(--text-secondary)]">
<span x-text="filteredRecipes.length"></span>
<span x-text="filteredRecipes.length === 1 ? '{{ _('ricetta trovata') }}' : '{{ _('ricette trovate') }}'"></span>
</p>
</div>
<!-- Recipe Cards -->
<div class="space-y-4" x-show="filteredRecipes.length > 0">
<template x-for="recipe in filteredRecipes" :key="recipe.id">
<div class="tmf-card hover:border-primary/30 transition-all duration-200 group">
<div class="p-5 sm:p-6">
<!-- Card Header -->
<div class="flex flex-col sm:flex-row sm:items-start gap-4 mb-4">
<!-- Thumbnail -->
<template x-if="recipe.image_path">
<div class="w-20 h-20 rounded-lg overflow-hidden bg-[var(--bg-secondary)] border border-[var(--border-color)] shrink-0">
<img :src="'/maker/api/files/' + recipe.image_path"
class="w-full h-full object-cover"
loading="lazy"
onerror="this.parentElement.style.display='none'">
</div>
</template>
<!-- Left: Recipe Info -->
<div class="flex-1 min-w-0">
<!-- Badges Row -->
<div class="flex items-center gap-2 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"
x-text="recipe.code">
</span>
<!-- Version Badge -->
<template x-if="recipe.current_version">
<span class="badge badge-neutral font-mono"
x-text="'v' + recipe.current_version.version_number">
</span>
</template>
<!-- Status Badge -->
<span class="badge"
:class="recipe.active ? 'badge-pass' : 'badge-fail'"
x-text="recipe.active ? '{{ _('Attiva') }}' : '{{ _('Disattivata') }}'">
</span>
</div>
<!-- Recipe Name -->
<h3 class="text-xl font-bold text-[var(--text-primary)] mb-2"
x-text="recipe.name">
</h3>
<!-- Description -->
<template x-if="recipe.description">
<p class="text-sm text-[var(--text-secondary)] leading-relaxed line-clamp-2 mb-3"
x-text="recipe.description">
</p>
</template>
<!-- Meta Information -->
<div class="flex items-center gap-4 flex-wrap text-xs text-[var(--text-secondary)]">
<!-- Task Count -->
<template x-if="recipe.current_version && recipe.current_version.task_count">
<span class="inline-flex items-center gap-1.5">
<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="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
<span x-text="recipe.current_version.task_count"></span>
<span>{{ _('task') }}</span>
</span>
</template>
<!-- Updated Date -->
<template x-if="recipe.current_version && recipe.current_version.created_at">
<span class="inline-flex items-center gap-1.5">
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>{{ _('Aggiornata') }}</span>
<span class="font-medium" x-text="new Date(recipe.current_version.created_at).toLocaleDateString('{{ current_language or 'it' }}')"></span>
</span>
</template>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-2 flex-wrap pt-4 border-t border-[var(--border-color)]">
<!-- Modifica -->
<a :href="'/maker/recipes/' + recipe.id + '/edit'"
class="btn btn-secondary gap-1.5 text-xs">
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
{{ _('Modifica') }}
</a>
<!-- Task -->
<a :href="'/maker/recipes/' + recipe.id + '/tasks'"
class="btn btn-secondary gap-1.5 text-xs">
<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>
{{ _('Task') }}
</a>
<!-- Anteprima -->
<a :href="'/maker/recipes/' + recipe.id + '/preview'"
class="btn btn-secondary gap-1.5 text-xs">
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
{{ _('Anteprima') }}
</a>
<!-- Versioni -->
<a :href="'/maker/recipes/' + recipe.id + '/versions'"
class="btn btn-secondary gap-1.5 text-xs">
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{{ _('Versioni') }}
</a>
<!-- Spacer -->
<div class="flex-1"></div>
<!-- Elimina -->
<button @click="openDeleteModal(recipe)"
class="btn btn-danger gap-1.5 text-xs">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
{{ _('Elimina') }}
</button>
</div>
</div>
</div>
</template>
</div>
<!-- Empty State -->
<div x-show="filteredRecipes.length === 0"
x-cloak
class="text-center py-16">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full
bg-[var(--bg-secondary)] mb-5">
<svg class="w-10 h-10 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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<h3 class="text-lg font-semibold text-[var(--text-primary)] mb-2">
<span x-show="search.trim() || filter !== 'all'">
{{ _('Nessuna ricetta trovata') }}
</span>
<span x-show="!search.trim() && filter === 'all'">
{{ _('Nessuna ricetta disponibile') }}
</span>
</h3>
<p class="text-sm text-[var(--text-secondary)] max-w-md mx-auto mb-6">
<span x-show="search.trim() || filter !== 'all'">
{{ _('Prova a modificare i filtri di ricerca') }}
</span>
<span x-show="!search.trim() && filter === 'all'">
{{ _('Inizia creando la tua prima ricetta di misura') }}
</span>
</p>
<a href="{{ url_for('maker.recipe_new') }}"
x-show="!search.trim() && filter === 'all'"
class="btn btn-primary gap-2 inline-flex">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/>
</svg>
{{ _('Crea Prima Ricetta') }}
</a>
</div>
<!-- Delete Confirmation Modal -->
<div x-show="deleteModal"
x-cloak
@keydown.escape.window="deleteModal = false"
class="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<!-- Backdrop -->
<div x-show="deleteModal"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="deleteModal = false"
class="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity">
</div>
<!-- Modal Content -->
<div class="flex min-h-full items-center justify-center p-4">
<div x-show="deleteModal"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="tmf-card max-w-lg w-full relative">
<div class="p-6">
<!-- Icon -->
<div class="mx-auto flex items-center justify-center w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 mb-4">
<svg class="w-6 h-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<!-- Title -->
<h3 class="text-lg font-semibold text-[var(--text-primary)] text-center mb-2">
{{ _('Conferma Eliminazione') }}
</h3>
<!-- Message -->
<template x-if="deleteTarget">
<div>
<p class="text-sm text-[var(--text-secondary)] text-center mb-4">
{{ _('Sei sicuro di voler disattivare la ricetta') }}
<span class="font-semibold text-[var(--text-primary)]" x-text="deleteTarget.name"></span>?
</p>
<p class="text-xs text-[var(--text-muted)] text-center mb-6">
{{ _('Le misure esistenti non verranno eliminate') }}
</p>
</div>
</template>
<!-- Actions -->
<div class="flex flex-col sm:flex-row gap-3">
<button @click="deleteModal = false"
class="btn btn-secondary flex-1 justify-center">
{{ _('Annulla') }}
</button>
<button @click="deleteRecipe(deleteTarget.id)"
class="btn btn-danger flex-1 justify-center">
{{ _('Elimina') }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}