Aggiungi campi per immagini e diametro del foro nel modello Articolo e nelle relative migrazioni; aggiorna i form e le viste per gestire i nuovi campi

This commit is contained in:
2026-05-18 16:23:12 +02:00
parent f3a1d816b7
commit 83412247a7
11 changed files with 373 additions and 33 deletions

82
CLAUDE.md Normal file
View File

@@ -0,0 +1,82 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**HTT QRCode** is a Laravel 12 web application for managing industrial articles with QR code generation and scanning. Key capabilities: article catalog CRUD, unique QR code generation per article, PDF label printing, public QR scanner (no auth required), and Excel import.
**Stack**: Laravel 12 / PHP 8.2+ · Blade + TailwindCSS 3 + Alpine.js · SQLite (default) or MySQL · Vite · SimpleSoftwareIO/simple-qrcode · Barryvdh/laravel-dompdf · PHPOffice/PhpSpreadsheet
## Development Commands
```bash
# Start everything (PHP server + queue + logs + Vite HMR)
composer run dev
# Or separately:
php artisan serve # http://localhost:8000
npm run dev # Vite with hot reload
# Build for production
npm run build
# Run tests
composer run test
# or
php artisan test
php artisan test --filter NomeTest # single test
# Database
php artisan migrate
php artisan migrate:fresh --seed # reset + re-seed
php artisan db:seed --class=AdminUserSeeder # admin@example.com / password
# Code style (Laravel Pint)
./vendor/bin/pint
# Clear caches
php artisan cache:clear && php artisan config:clear && php artisan view:clear
# View all routes
php artisan route:list
# Real-time logs
php artisan pail
```
## Architecture
### Route → Controller → View mapping
| Route | Controller | View |
|---|---|---|
| `GET /` | closure | `welcome.blade.php` |
| `GET /scanner` | `PublicArticoloController@scanner` | `public/scanner.blade.php` |
| `GET /articolo/{qr_code}` | `PublicArticoloController@show` | `public/articolo.blade.php` |
| `admin/articoli` (resource) | `Admin/ArticoloController` | `admin/articoli/*.blade.php` |
Admin routes are grouped under `middleware(['auth'])` with prefix `admin/` and name prefix `admin.`. The `/dashboard` route simply redirects to `admin.articoli.index`.
### Articolo Model
`qr_code` is auto-generated on `created` event using the format `{id}-{timestamp}`. The `codice_articolo` field is unique. The public scanner URL resolves articles via `qr_code`, not `id`.
### QR Code features in `ArticoloController`
- `qrCode()` — renders QR inline as SVG/PNG
- `downloadQrCode()` — streams PNG (300×300px, error correction H)
- `printQrCodes()` — generates multi-article PDF via dompdf using `pdf-qrcodes.blade.php`
- `import()` — Excel import via PhpSpreadsheet; supports "merge" (default) and "clean import" modes
### Layouts
- `layouts/app.blade.php` — authenticated area (uses `<x-app-layout>`)
- `layouts/guest.blade.php` — public/auth pages (uses `<x-guest-layout>`)
- Blade components live in `resources/views/components/` and `app/View/Components/`
## Database
Default is **SQLite** (`database/database.sqlite`). The `articoli` table has two unique constraints: `codice_articolo` and `qr_code`.
Excel import expects these column groups: `UBICAZIONE` (Codice Articolo, Ciclo, Diametro, Descrizione, Posizione, Quantita) and `PARAMETRI TECNOLOGICI` (Tipo Lavorazione, Materiale da lavorare, Maximum Thickness, Speed RPM, Feed, Max Thrust A, Min Torque A, Quantita Fori).

Binary file not shown.

View File

@@ -5,8 +5,8 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Articolo; use App\Models\Articolo;
use Barryvdh\DomPDF\Facade\Pdf; use Barryvdh\DomPDF\Facade\Pdf;
use Database\Seeders\ArticoloSeeder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use SimpleSoftwareIO\QrCode\Facades\QrCode; use SimpleSoftwareIO\QrCode\Facades\QrCode;
use \PhpOffice\PhpSpreadsheet\IOFactory; use \PhpOffice\PhpSpreadsheet\IOFactory;
@@ -40,6 +40,7 @@ class ArticoloController extends Controller
$validated = $request->validate([ $validated = $request->validate([
'codice_articolo' => 'required|string|max:255|unique:articoli', 'codice_articolo' => 'required|string|max:255|unique:articoli',
'ciclo' => 'nullable|string|max:255', 'ciclo' => 'nullable|string|max:255',
'hole_diameter' => 'nullable|string|max:255',
'diametro' => 'nullable|string|max:255', 'diametro' => 'nullable|string|max:255',
'descrizione' => 'nullable|string', 'descrizione' => 'nullable|string',
'posizione' => 'nullable|string|max:255', 'posizione' => 'nullable|string|max:255',
@@ -75,6 +76,7 @@ class ArticoloController extends Controller
$validated = $request->validate([ $validated = $request->validate([
'codice_articolo' => 'required|string|max:255|unique:articoli,codice_articolo,' . $articolo->id, 'codice_articolo' => 'required|string|max:255|unique:articoli,codice_articolo,' . $articolo->id,
'ciclo' => 'nullable|string|max:255', 'ciclo' => 'nullable|string|max:255',
'hole_diameter' => 'nullable|string|max:255',
'diametro' => 'nullable|string|max:255', 'diametro' => 'nullable|string|max:255',
'descrizione' => 'nullable|string', 'descrizione' => 'nullable|string',
'posizione' => 'nullable|string|max:255', 'posizione' => 'nullable|string|max:255',
@@ -87,8 +89,36 @@ class ArticoloController extends Controller
'max_thrust_a' => 'nullable|string|max:255', 'max_thrust_a' => 'nullable|string|max:255',
'min_torque_a' => 'nullable|string|max:255', 'min_torque_a' => 'nullable|string|max:255',
'quantita_fori' => 'nullable|integer|min:0', 'quantita_fori' => 'nullable|integer|min:0',
'immagine_articolo' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:4096',
'immagine_thrust' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:4096',
'immagine_tourque' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:4096',
'rimuovi_immagine_articolo' => 'nullable|boolean',
'rimuovi_immagine_thrust' => 'nullable|boolean',
'rimuovi_immagine_tourque' => 'nullable|boolean',
]); ]);
$imageFields = ['immagine_articolo', 'immagine_thrust', 'immagine_tourque'];
foreach ($imageFields as $field) {
$removeKey = 'rimuovi_' . $field;
if ($request->boolean($removeKey)) {
if ($articolo->$field) {
Storage::disk('public')->delete($articolo->$field);
}
$validated[$field] = null;
} elseif ($request->hasFile($field)) {
if ($articolo->$field) {
Storage::disk('public')->delete($articolo->$field);
}
$validated[$field] = $request->file($field)->store('articoli', 'public');
} else {
unset($validated[$field]);
}
unset($validated[$removeKey]);
}
$articolo->update($validated); $articolo->update($validated);
return redirect()->route('admin.articoli.show', $articolo) return redirect()->route('admin.articoli.show', $articolo)
@@ -164,7 +194,7 @@ class ArticoloController extends Controller
public function import(Request $request) public function import(Request $request)
{ {
$request->validate([ $request->validate([
'file' => 'required|file|mimes:xlsx,xls', 'file' => 'required|file|mimetypes:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,application/octet-stream',
'clean_import' => 'nullable|boolean', 'clean_import' => 'nullable|boolean',
]); ]);
@@ -172,7 +202,6 @@ class ArticoloController extends Controller
$cleanImport = $request->boolean('clean_import', false); $cleanImport = $request->boolean('clean_import', false);
try { try {
$seeder = new ArticoloSeeder();
$result = $this->importFromExcel($file->getPathname(), $cleanImport); $result = $this->importFromExcel($file->getPathname(), $cleanImport);
$message = "Import completato! "; $message = "Import completato! ";
@@ -216,6 +245,11 @@ class ArticoloController extends Controller
} }
// Skip header rows (first 2 rows) // Skip header rows (first 2 rows)
// Column layout (v2): 0=Codice Articolo, 1=Immagine Articolo (skip), 2=Ciclo,
// 3=Hole Diameter, 4=Diametro, 5=Descrizione, 6=Posizione, 7=Quantità,
// 8=Tipo lavorazione, 9=Materiale, 10=Max Thickness, 11=Speed RPM, 12=Feed,
// 13=Max Thrust A, 14=Immagine Thrust (skip), 15=Min Torque A,
// 16=Immagine Tourque (skip), 17=Quantità fori
foreach (array_slice($rows, 2) as $index => $row) { foreach (array_slice($rows, 2) as $index => $row) {
// Skip empty rows // Skip empty rows
if (empty($row[0])) { if (empty($row[0])) {
@@ -228,19 +262,20 @@ class ArticoloController extends Controller
Articolo::updateOrCreate( Articolo::updateOrCreate(
['codice_articolo' => $row[0]], ['codice_articolo' => $row[0]],
[ [
'ciclo' => $row[1] ?? null, 'ciclo' => $row[2] ?? null,
'diametro' => $row[2] ?? null, 'hole_diameter' => $row[3] ?? null,
'descrizione' => $row[3] ?? null, 'diametro' => $row[4] ?? null,
'posizione' => $row[4] ?? null, 'descrizione' => $row[5] ?? null,
'quantita' => is_numeric($row[5]) ? (int) $row[5] : 0, 'posizione' => $row[6] ?? null,
'tipo_lavorazione' => $row[6] ?? null, 'quantita' => is_numeric($row[7]) ? (int) round((float) $row[7]) : 0,
'materiale_lavorare' => $row[7] ?? null, 'tipo_lavorazione' => $row[8] ?? null,
'maximum_thickness' => $row[8] ?? null, 'materiale_lavorare' => $row[9] ?? null,
'speed_rpm' => is_numeric($row[9]) ? (int) $row[9] : null, 'maximum_thickness' => $row[10] ?? null,
'feed' => is_numeric($row[10]) ? (float) $row[10] : null, 'speed_rpm' => is_numeric($row[11]) ? (int) round((float) $row[11]) : null,
'max_thrust_a' => $row[11] ?? null, 'feed' => is_numeric($row[12]) ? round((float) $row[12], 2) : null,
'min_torque_a' => $row[12] ?? null, 'max_thrust_a' => $row[13] ?? null,
'quantita_fori' => is_numeric($row[13]) ? (int) $row[13] : null, 'min_torque_a' => $row[15] ?? null,
'quantita_fori' => is_numeric($row[17]) ? (int) round((float) $row[17]) : null,
] ]
); );
@@ -250,18 +285,11 @@ class ArticoloController extends Controller
$imported++; $imported++;
} }
if (isset($this->command)) {
$this->command->info('Importato articolo: ' . $row[0]);
}
} catch (\Exception $e) { } catch (\Exception $e) {
$errors[] = "Riga " . ($index + 3) . ": " . $e->getMessage(); $errors[] = "Riga " . ($index + 3) . ": " . $e->getMessage();
} }
} }
if (isset($this->command)) {
$this->command->info('Import completato! Totale articoli: ' . Articolo::count());
}
return [ return [
'imported' => $imported, 'imported' => $imported,
'updated' => $updated, 'updated' => $updated,

View File

@@ -9,7 +9,9 @@ class Articolo extends Model
protected $fillable = [ protected $fillable = [
'codice_articolo', 'codice_articolo',
'immagine_articolo',
'ciclo', 'ciclo',
'hole_diameter',
'diametro', 'diametro',
'descrizione', 'descrizione',
'posizione', 'posizione',
@@ -20,7 +22,9 @@ class Articolo extends Model
'speed_rpm', 'speed_rpm',
'feed', 'feed',
'max_thrust_a', 'max_thrust_a',
'immagine_thrust',
'min_torque_a', 'min_torque_a',
'immagine_tourque',
'quantita_fori', 'quantita_fori',
'qr_code', 'qr_code',
]; ];

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('articoli', function (Blueprint $table) {
$table->string('immagine_articolo')->nullable()->after('codice_articolo');
$table->string('immagine_thrust')->nullable()->after('max_thrust_a');
$table->string('immagine_tourque')->nullable()->after('min_torque_a');
});
}
public function down(): void
{
Schema::table('articoli', function (Blueprint $table) {
$table->dropColumn(['immagine_articolo', 'immagine_thrust', 'immagine_tourque']);
});
}
};

View File

@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('articoli', function (Blueprint $table) {
$table->string('hole_diameter')->nullable()->after('ciclo');
});
}
public function down(): void
{
Schema::table('articoli', function (Blueprint $table) {
$table->dropColumn('hole_diameter');
});
}
};

View File

@@ -1,3 +1,5 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
[x-cloak] { display: none !important; }

View File

@@ -1,3 +1,4 @@
@use('Illuminate\Support\Facades\Storage')
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
@@ -15,11 +16,11 @@
</div> </div>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12" x-data="{ modalImg: null }">
<div class="max-w-4xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-4xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6"> <div class="p-6">
<form method="POST" action="{{ route('admin.articoli.update', $articolo) }}"> <form method="POST" action="{{ route('admin.articoli.update', $articolo) }}" enctype="multipart/form-data">
@csrf @csrf
@method('PUT') @method('PUT')
@@ -42,6 +43,12 @@
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
</div> </div>
<div>
<label for="hole_diameter" class="block text-sm font-medium text-gray-700">Hole Diameter</label>
<input type="text" name="hole_diameter" id="hole_diameter" value="{{ old('hole_diameter', $articolo->hole_diameter) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
</div>
<div> <div>
<label for="diametro" class="block text-sm font-medium text-gray-700">Diametro</label> <label for="diametro" class="block text-sm font-medium text-gray-700">Diametro</label>
<input type="text" name="diametro" id="diametro" value="{{ old('diametro', $articolo->diametro) }}" <input type="text" name="diametro" id="diametro" value="{{ old('diametro', $articolo->diametro) }}"
@@ -65,6 +72,26 @@
<textarea name="descrizione" id="descrizione" rows="3" <textarea name="descrizione" id="descrizione" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">{{ old('descrizione', $articolo->descrizione) }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">{{ old('descrizione', $articolo->descrizione) }}</textarea>
</div> </div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700">Immagine Articolo</label>
@if ($articolo->immagine_articolo)
<div class="mt-2 mb-3 flex items-center gap-4">
<img src="{{ Storage::url($articolo->immagine_articolo) }}" alt="Immagine Articolo"
class="h-16 w-16 object-cover rounded border cursor-pointer hover:opacity-80 transition-opacity"
@click="modalImg = '{{ Storage::url($articolo->immagine_articolo) }}'">
<label class="flex items-center gap-2 text-sm text-red-600 cursor-pointer">
<input type="checkbox" name="rimuovi_immagine_articolo" value="1" class="rounded border-gray-300">
Rimuovi immagine
</label>
</div>
@endif
<input type="file" name="immagine_articolo" id="immagine_articolo" accept="image/*"
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
@error('immagine_articolo')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div> </div>
</div> </div>
@@ -108,12 +135,52 @@
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
</div> </div>
<div>
<label class="block text-sm font-medium text-gray-700">Immagine Thrust</label>
@if ($articolo->immagine_thrust)
<div class="mt-2 mb-3 flex items-center gap-4">
<img src="{{ Storage::url($articolo->immagine_thrust) }}" alt="Immagine Thrust"
class="h-16 w-16 object-cover rounded border cursor-pointer hover:opacity-80 transition-opacity"
@click="modalImg = '{{ Storage::url($articolo->immagine_thrust) }}'">
<label class="flex items-center gap-2 text-sm text-red-600 cursor-pointer">
<input type="checkbox" name="rimuovi_immagine_thrust" value="1" class="rounded border-gray-300">
Rimuovi immagine
</label>
</div>
@endif
<input type="file" name="immagine_thrust" id="immagine_thrust" accept="image/*"
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
@error('immagine_thrust')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div> <div>
<label for="min_torque_a" class="block text-sm font-medium text-gray-700">Min Torque (A)</label> <label for="min_torque_a" class="block text-sm font-medium text-gray-700">Min Torque (A)</label>
<input type="text" name="min_torque_a" id="min_torque_a" value="{{ old('min_torque_a', $articolo->min_torque_a) }}" <input type="text" name="min_torque_a" id="min_torque_a" value="{{ old('min_torque_a', $articolo->min_torque_a) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
</div> </div>
<div>
<label class="block text-sm font-medium text-gray-700">Immagine Tourque</label>
@if ($articolo->immagine_tourque)
<div class="mt-2 mb-3 flex items-center gap-4">
<img src="{{ Storage::url($articolo->immagine_tourque) }}" alt="Immagine Tourque"
class="h-16 w-16 object-cover rounded border cursor-pointer hover:opacity-80 transition-opacity"
@click="modalImg = '{{ Storage::url($articolo->immagine_tourque) }}'">
<label class="flex items-center gap-2 text-sm text-red-600 cursor-pointer">
<input type="checkbox" name="rimuovi_immagine_tourque" value="1" class="rounded border-gray-300">
Rimuovi immagine
</label>
</div>
@endif
<input type="file" name="immagine_tourque" id="immagine_tourque" accept="image/*"
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
@error('immagine_tourque')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div> <div>
<label for="quantita_fori" class="block text-sm font-medium text-gray-700">Quantita Fori</label> <label for="quantita_fori" class="block text-sm font-medium text-gray-700">Quantita Fori</label>
<input type="number" name="quantita_fori" id="quantita_fori" value="{{ old('quantita_fori', $articolo->quantita_fori) }}" min="0" <input type="number" name="quantita_fori" id="quantita_fori" value="{{ old('quantita_fori', $articolo->quantita_fori) }}" min="0"
@@ -134,5 +201,14 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Image Modal -->
<div x-show="modalImg" x-cloak
class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4"
@click="modalImg = null"
@keydown.escape.window="modalImg = null">
<img :src="modalImg" @click.stop class="max-h-[90vh] max-w-full rounded-lg shadow-2xl">
</div> </div>
</div>
</x-app-layout> </x-app-layout>

View File

@@ -288,7 +288,7 @@
} }
function handleFileSelect(file) { function handleFileSelect(file) {
if (file && (file.name.endsWith('.xlsx') || file.name.endsWith('.xls'))) { if (file && (file.name.toLowerCase().endsWith('.xlsx') || file.name.toLowerCase().endsWith('.xls'))) {
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();
dataTransfer.items.add(file); dataTransfer.items.add(file);
fileInput.files = dataTransfer.files; fileInput.files = dataTransfer.files;

View File

@@ -1,3 +1,4 @@
@use('Illuminate\Support\Facades\Storage')
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
@@ -15,7 +16,7 @@
</div> </div>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12" x-data="{ modalImg: null }">
<div class="max-w-6xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-6xl mx-auto sm:px-6 lg:px-8">
@if (session('success')) @if (session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4"> <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
@@ -39,6 +40,10 @@
<dt class="text-sm font-medium text-gray-500">Ciclo</dt> <dt class="text-sm font-medium text-gray-500">Ciclo</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $articolo->ciclo ?? '-' }}</dd> <dd class="mt-1 text-sm text-gray-900">{{ $articolo->ciclo ?? '-' }}</dd>
</div> </div>
<div>
<dt class="text-sm font-medium text-gray-500">Hole Diameter</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $articolo->hole_diameter ?? '-' }}</dd>
</div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">Diametro</dt> <dt class="text-sm font-medium text-gray-500">Diametro</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $articolo->diametro ?? '-' }}</dd> <dd class="mt-1 text-sm text-gray-900">{{ $articolo->diametro ?? '-' }}</dd>
@@ -55,6 +60,16 @@
<dt class="text-sm font-medium text-gray-500">Descrizione</dt> <dt class="text-sm font-medium text-gray-500">Descrizione</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $articolo->descrizione ?? '-' }}</dd> <dd class="mt-1 text-sm text-gray-900">{{ $articolo->descrizione ?? '-' }}</dd>
</div> </div>
@if ($articolo->immagine_articolo)
<div>
<dt class="text-sm font-medium text-gray-500">Immagine Articolo</dt>
<dd class="mt-2">
<img src="{{ Storage::url($articolo->immagine_articolo) }}" alt="Immagine Articolo"
class="h-16 w-16 object-cover rounded border cursor-pointer hover:opacity-80 transition-opacity"
@click="modalImg = '{{ Storage::url($articolo->immagine_articolo) }}'">
</dd>
</div>
@endif
</dl> </dl>
</div> </div>
</div> </div>
@@ -88,10 +103,30 @@
<dt class="text-sm font-medium text-gray-500">Max Thrust (A)</dt> <dt class="text-sm font-medium text-gray-500">Max Thrust (A)</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $articolo->max_thrust_a ?? '-' }}</dd> <dd class="mt-1 text-sm text-gray-900">{{ $articolo->max_thrust_a ?? '-' }}</dd>
</div> </div>
@if ($articolo->immagine_thrust)
<div>
<dt class="text-sm font-medium text-gray-500">Immagine Thrust</dt>
<dd class="mt-2">
<img src="{{ Storage::url($articolo->immagine_thrust) }}" alt="Immagine Thrust"
class="h-16 w-16 object-cover rounded border cursor-pointer hover:opacity-80 transition-opacity"
@click="modalImg = '{{ Storage::url($articolo->immagine_thrust) }}'">
</dd>
</div>
@endif
<div> <div>
<dt class="text-sm font-medium text-gray-500">Min Torque (A)</dt> <dt class="text-sm font-medium text-gray-500">Min Torque (A)</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $articolo->min_torque_a ?? '-' }}</dd> <dd class="mt-1 text-sm text-gray-900">{{ $articolo->min_torque_a ?? '-' }}</dd>
</div> </div>
@if ($articolo->immagine_tourque)
<div>
<dt class="text-sm font-medium text-gray-500">Immagine Tourque</dt>
<dd class="mt-2">
<img src="{{ Storage::url($articolo->immagine_tourque) }}" alt="Immagine Tourque"
class="h-16 w-16 object-cover rounded border cursor-pointer hover:opacity-80 transition-opacity"
@click="modalImg = '{{ Storage::url($articolo->immagine_tourque) }}'">
</dd>
</div>
@endif
<div> <div>
<dt class="text-sm font-medium text-gray-500">Quantita Fori</dt> <dt class="text-sm font-medium text-gray-500">Quantita Fori</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $articolo->quantita_fori ?? '-' }}</dd> <dd class="mt-1 text-sm text-gray-900">{{ $articolo->quantita_fori ?? '-' }}</dd>
@@ -146,5 +181,13 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Image Modal -->
<div x-show="modalImg" x-cloak
class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4"
@click="modalImg = null"
@keydown.escape.window="modalImg = null">
<img :src="modalImg" @click.stop class="max-h-[90vh] max-w-full rounded-lg shadow-2xl">
</div>
</div> </div>
</x-app-layout> </x-app-layout>

View File

@@ -1,3 +1,4 @@
@use('Illuminate\Support\Facades\Storage')
<!DOCTYPE html> <!DOCTYPE html>
<html lang="it"> <html lang="it">
<head> <head>
@@ -5,13 +6,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $articolo->codice_articolo }} - HTT Locator</title> <title>{{ $articolo->codice_articolo }} - HTT Locator</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style> <style>
.gradient-bg { .gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
} }
[x-cloak] { display: none !important; }
</style> </style>
</head> </head>
<body class="bg-gray-100 min-h-screen"> <body class="bg-gray-100 min-h-screen" x-data="{ modalImg: null }">
<!-- Header --> <!-- Header -->
<header class="gradient-bg text-white py-4 px-4 shadow-lg"> <header class="gradient-bg text-white py-4 px-4 shadow-lg">
<div class="max-w-lg mx-auto"> <div class="max-w-lg mx-auto">
@@ -35,7 +38,7 @@
</div> </div>
<!-- Ubicazione --> <!-- Ubicazione -->
<div class="bg-white rounded-2xl shadow-lg overflow-hidden mb-6"> <div class="bg-white rounded-2xl shadow-lg overflow-hidden mb-2">
<div class="px-4 py-3 bg-gray-50 border-b"> <div class="px-4 py-3 bg-gray-50 border-b">
<h3 class="font-semibold text-gray-700 flex items-center"> <h3 class="font-semibold text-gray-700 flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -46,12 +49,26 @@
</h3> </h3>
</div> </div>
<div class="divide-y"> <div class="divide-y">
@if($articolo->immagine_articolo)
<div class="flex justify-between items-center px-4 py-3">
<span class="text-gray-500 text-sm">Immagine</span>
<img src="{{ Storage::url($articolo->immagine_articolo) }}" alt="Immagine Articolo"
class="h-12 w-12 object-cover rounded cursor-pointer hover:opacity-80 transition-opacity"
@click="modalImg = '{{ Storage::url($articolo->immagine_articolo) }}'">
</div>
@endif
@if($articolo->ciclo) @if($articolo->ciclo)
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-4 py-3">
<span class="text-gray-500 text-sm">Ciclo</span> <span class="text-gray-500 text-sm">Ciclo</span>
<span class="font-medium text-gray-900">{{ $articolo->ciclo }}</span> <span class="font-medium text-gray-900">{{ $articolo->ciclo }}</span>
</div> </div>
@endif @endif
@if($articolo->hole_diameter)
<div class="flex justify-between items-center px-4 py-3">
<span class="text-gray-500 text-sm">Hole Diameter</span>
<span class="font-medium text-gray-900">{{ $articolo->hole_diameter }}</span>
</div>
@endif
@if($articolo->diametro) @if($articolo->diametro)
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-4 py-3">
<span class="text-gray-500 text-sm">Diametro</span> <span class="text-gray-500 text-sm">Diametro</span>
@@ -121,12 +138,28 @@
<span class="font-medium text-gray-900">{{ $articolo->max_thrust_a }}</span> <span class="font-medium text-gray-900">{{ $articolo->max_thrust_a }}</span>
</div> </div>
@endif @endif
@if($articolo->immagine_thrust)
<div class="flex justify-between items-center px-4 py-3">
<span class="text-gray-500 text-sm">Immagine Thrust</span>
<img src="{{ Storage::url($articolo->immagine_thrust) }}" alt="Immagine Thrust"
class="h-12 w-12 object-cover rounded cursor-pointer hover:opacity-80 transition-opacity"
@click="modalImg = '{{ Storage::url($articolo->immagine_thrust) }}'">
</div>
@endif
@if($articolo->min_torque_a) @if($articolo->min_torque_a)
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-4 py-3">
<span class="text-gray-500 text-sm">Min Torque (A)</span> <span class="text-gray-500 text-sm">Min Torque (A)</span>
<span class="font-medium text-gray-900">{{ $articolo->min_torque_a }}</span> <span class="font-medium text-gray-900">{{ $articolo->min_torque_a }}</span>
</div> </div>
@endif @endif
@if($articolo->immagine_tourque)
<div class="flex justify-between items-center px-4 py-3">
<span class="text-gray-500 text-sm">Immagine Tourque</span>
<img src="{{ Storage::url($articolo->immagine_tourque) }}" alt="Immagine Tourque"
class="h-12 w-12 object-cover rounded cursor-pointer hover:opacity-80 transition-opacity"
@click="modalImg = '{{ Storage::url($articolo->immagine_tourque) }}'">
</div>
@endif
@if($articolo->quantita_fori) @if($articolo->quantita_fori)
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-4 py-3">
<span class="text-gray-500 text-sm">Quantita Fori</span> <span class="text-gray-500 text-sm">Quantita Fori</span>
@@ -136,8 +169,13 @@
</div> </div>
</div> </div>
<!-- Footer Info -->
<div class="text-center text-gray-400 text-xs">
<p>Ultimo aggiornamento: {{ $articolo->updated_at->format('d/m/Y H:i') }}</p>
</div>
<!-- Scan New QR Button --> <!-- Scan New QR Button -->
<div class="mb-6"> <div class="mb-6 mt-3">
<a href="{{ route('scanner') }}" class="flex items-center justify-center gap-3 w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-4 px-6 rounded-2xl shadow-lg transition-all"> <a href="{{ route('scanner') }}" class="flex items-center justify-center gap-3 w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-4 px-6 rounded-2xl shadow-lg transition-all">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"></path>
@@ -146,10 +184,25 @@
</a> </a>
</div> </div>
<!-- Footer Info --> <!-- Back to Home Button (Mobile Only) -->
<div class="text-center text-gray-400 text-xs"> <div class="bottom-4 left-4 right-4 md:hidden">
<p>Ultimo aggiornamento: {{ $articolo->updated_at->format('d/m/Y H:i') }}</p> <a href="/" class="flex items-center justify-center gap-3 w-full bg-gray-800 hover:bg-gray-900 text-white font-semibold py-3 px-6 rounded-full shadow-lg transition-all">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l9-9m0 0l9 9m-9-9v18"></path>
</svg>
Torna alla Home
</a>
</div> </div>
</main> </main>
<!-- Image Modal -->
<div x-show="modalImg" x-cloak
class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4"
@click="modalImg = null"
@keydown.escape.window="modalImg = null">
<img :src="modalImg" @click.stop class="max-h-[90vh] max-w-full rounded-lg shadow-2xl">
</div>
</body> </body>
</html> </html>