diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e5cbccb --- /dev/null +++ b/CLAUDE.md @@ -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 ``) +- `layouts/guest.blade.php` — public/auth pages (uses ``) +- 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). diff --git a/STRUTTURA DATABASE.xlsx b/STRUTTURA DATABASE.xlsx index 4e174c4..1496bf3 100644 Binary files a/STRUTTURA DATABASE.xlsx and b/STRUTTURA DATABASE.xlsx differ diff --git a/app/Http/Controllers/Admin/ArticoloController.php b/app/Http/Controllers/Admin/ArticoloController.php index baf1f2c..c2340e8 100644 --- a/app/Http/Controllers/Admin/ArticoloController.php +++ b/app/Http/Controllers/Admin/ArticoloController.php @@ -5,8 +5,8 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Articolo; use Barryvdh\DomPDF\Facade\Pdf; -use Database\Seeders\ArticoloSeeder; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; use SimpleSoftwareIO\QrCode\Facades\QrCode; use \PhpOffice\PhpSpreadsheet\IOFactory; @@ -40,6 +40,7 @@ class ArticoloController extends Controller $validated = $request->validate([ 'codice_articolo' => 'required|string|max:255|unique:articoli', 'ciclo' => 'nullable|string|max:255', + 'hole_diameter' => 'nullable|string|max:255', 'diametro' => 'nullable|string|max:255', 'descrizione' => 'nullable|string', 'posizione' => 'nullable|string|max:255', @@ -75,6 +76,7 @@ class ArticoloController extends Controller $validated = $request->validate([ 'codice_articolo' => 'required|string|max:255|unique:articoli,codice_articolo,' . $articolo->id, 'ciclo' => 'nullable|string|max:255', + 'hole_diameter' => 'nullable|string|max:255', 'diametro' => 'nullable|string|max:255', 'descrizione' => 'nullable|string', 'posizione' => 'nullable|string|max:255', @@ -87,8 +89,36 @@ class ArticoloController extends Controller 'max_thrust_a' => 'nullable|string|max:255', 'min_torque_a' => 'nullable|string|max:255', '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); return redirect()->route('admin.articoli.show', $articolo) @@ -164,7 +194,7 @@ class ArticoloController extends Controller public function import(Request $request) { $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', ]); @@ -172,7 +202,6 @@ class ArticoloController extends Controller $cleanImport = $request->boolean('clean_import', false); try { - $seeder = new ArticoloSeeder(); $result = $this->importFromExcel($file->getPathname(), $cleanImport); $message = "Import completato! "; @@ -216,6 +245,11 @@ class ArticoloController extends Controller } // 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) { // Skip empty rows if (empty($row[0])) { @@ -228,19 +262,20 @@ class ArticoloController extends Controller Articolo::updateOrCreate( ['codice_articolo' => $row[0]], [ - 'ciclo' => $row[1] ?? null, - 'diametro' => $row[2] ?? null, - 'descrizione' => $row[3] ?? null, - 'posizione' => $row[4] ?? null, - 'quantita' => is_numeric($row[5]) ? (int) $row[5] : 0, - 'tipo_lavorazione' => $row[6] ?? null, - 'materiale_lavorare' => $row[7] ?? null, - 'maximum_thickness' => $row[8] ?? null, - 'speed_rpm' => is_numeric($row[9]) ? (int) $row[9] : null, - 'feed' => is_numeric($row[10]) ? (float) $row[10] : null, - 'max_thrust_a' => $row[11] ?? null, - 'min_torque_a' => $row[12] ?? null, - 'quantita_fori' => is_numeric($row[13]) ? (int) $row[13] : null, + 'ciclo' => $row[2] ?? null, + 'hole_diameter' => $row[3] ?? null, + 'diametro' => $row[4] ?? null, + 'descrizione' => $row[5] ?? null, + 'posizione' => $row[6] ?? null, + 'quantita' => is_numeric($row[7]) ? (int) round((float) $row[7]) : 0, + 'tipo_lavorazione' => $row[8] ?? null, + 'materiale_lavorare' => $row[9] ?? null, + 'maximum_thickness' => $row[10] ?? null, + 'speed_rpm' => is_numeric($row[11]) ? (int) round((float) $row[11]) : null, + 'feed' => is_numeric($row[12]) ? round((float) $row[12], 2) : null, + 'max_thrust_a' => $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++; } - if (isset($this->command)) { - $this->command->info('Importato articolo: ' . $row[0]); - } } catch (\Exception $e) { $errors[] = "Riga " . ($index + 3) . ": " . $e->getMessage(); } } - if (isset($this->command)) { - $this->command->info('Import completato! Totale articoli: ' . Articolo::count()); - } - return [ 'imported' => $imported, 'updated' => $updated, diff --git a/app/Models/Articolo.php b/app/Models/Articolo.php index 6155c4e..a6c92d8 100644 --- a/app/Models/Articolo.php +++ b/app/Models/Articolo.php @@ -9,7 +9,9 @@ class Articolo extends Model protected $fillable = [ 'codice_articolo', + 'immagine_articolo', 'ciclo', + 'hole_diameter', 'diametro', 'descrizione', 'posizione', @@ -20,7 +22,9 @@ class Articolo extends Model 'speed_rpm', 'feed', 'max_thrust_a', + 'immagine_thrust', 'min_torque_a', + 'immagine_tourque', 'quantita_fori', 'qr_code', ]; diff --git a/database/migrations/2026_04_28_132945_add_immagini_to_articoli_table.php b/database/migrations/2026_04_28_132945_add_immagini_to_articoli_table.php new file mode 100644 index 0000000..39ce191 --- /dev/null +++ b/database/migrations/2026_04_28_132945_add_immagini_to_articoli_table.php @@ -0,0 +1,27 @@ +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']); + }); + } +}; diff --git a/database/migrations/2026_04_28_133200_add_hole_diameter_to_articoli_table.php b/database/migrations/2026_04_28_133200_add_hole_diameter_to_articoli_table.php new file mode 100644 index 0000000..13c354b --- /dev/null +++ b/database/migrations/2026_04_28_133200_add_hole_diameter_to_articoli_table.php @@ -0,0 +1,25 @@ +string('hole_diameter')->nullable()->after('ciclo'); + }); + } + + public function down(): void + { + Schema::table('articoli', function (Blueprint $table) { + $table->dropColumn('hole_diameter'); + }); + } +}; diff --git a/resources/css/app.css b/resources/css/app.css index b5c61c9..de7488b 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,3 +1,5 @@ @tailwind base; @tailwind components; @tailwind utilities; + +[x-cloak] { display: none !important; } diff --git a/resources/views/admin/articoli/edit.blade.php b/resources/views/admin/articoli/edit.blade.php index ccb2030..df844ff 100644 --- a/resources/views/admin/articoli/edit.blade.php +++ b/resources/views/admin/articoli/edit.blade.php @@ -1,3 +1,4 @@ +@use('Illuminate\Support\Facades\Storage')
@@ -15,11 +16,11 @@
-
+
-
+ @csrf @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">
+
+ + +
+
{{ old('descrizione', $articolo->descrizione) }}
+ +
+ + @if ($articolo->immagine_articolo) +
+ Immagine Articolo + +
+ @endif + + @error('immagine_articolo') +

{{ $message }}

+ @enderror +
@@ -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">
+
+ + @if ($articolo->immagine_thrust) +
+ Immagine Thrust + +
+ @endif + + @error('immagine_thrust') +

{{ $message }}

+ @enderror +
+
+
+ + @if ($articolo->immagine_tourque) +
+ Immagine Tourque + +
+ @endif + + @error('immagine_tourque') +

{{ $message }}

+ @enderror +
+
+ + +
+ +
+
diff --git a/resources/views/admin/articoli/index.blade.php b/resources/views/admin/articoli/index.blade.php index 8d601e7..26515cf 100644 --- a/resources/views/admin/articoli/index.blade.php +++ b/resources/views/admin/articoli/index.blade.php @@ -288,7 +288,7 @@ } 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(); dataTransfer.items.add(file); fileInput.files = dataTransfer.files; diff --git a/resources/views/admin/articoli/show.blade.php b/resources/views/admin/articoli/show.blade.php index 1c36ab7..ead053b 100644 --- a/resources/views/admin/articoli/show.blade.php +++ b/resources/views/admin/articoli/show.blade.php @@ -1,3 +1,4 @@ +@use('Illuminate\Support\Facades\Storage')
@@ -15,7 +16,7 @@
-
+
@if (session('success'))
@@ -39,6 +40,10 @@
Ciclo
{{ $articolo->ciclo ?? '-' }}
+
+
Hole Diameter
+
{{ $articolo->hole_diameter ?? '-' }}
+
Diametro
{{ $articolo->diametro ?? '-' }}
@@ -55,6 +60,16 @@
Descrizione
{{ $articolo->descrizione ?? '-' }}
+ @if ($articolo->immagine_articolo) +
+
Immagine Articolo
+
+ Immagine Articolo +
+
+ @endif
@@ -88,10 +103,30 @@
Max Thrust (A)
{{ $articolo->max_thrust_a ?? '-' }}
+ @if ($articolo->immagine_thrust) +
+
Immagine Thrust
+
+ Immagine Thrust +
+
+ @endif
Min Torque (A)
{{ $articolo->min_torque_a ?? '-' }}
+ @if ($articolo->immagine_tourque) +
+
Immagine Tourque
+
+ Immagine Tourque +
+
+ @endif
Quantita Fori
{{ $articolo->quantita_fori ?? '-' }}
@@ -146,5 +181,13 @@
+ + +
+ +
diff --git a/resources/views/public/articolo.blade.php b/resources/views/public/articolo.blade.php index d1b6e89..046e4c0 100644 --- a/resources/views/public/articolo.blade.php +++ b/resources/views/public/articolo.blade.php @@ -1,3 +1,4 @@ +@use('Illuminate\Support\Facades\Storage') @@ -5,13 +6,15 @@ {{ $articolo->codice_articolo }} - HTT Locator + - +
@@ -35,7 +38,7 @@
-
+

@@ -46,12 +49,26 @@

+ @if($articolo->immagine_articolo) +
+ Immagine + Immagine Articolo +
+ @endif @if($articolo->ciclo)
Ciclo {{ $articolo->ciclo }}
@endif + @if($articolo->hole_diameter) +
+ Hole Diameter + {{ $articolo->hole_diameter }} +
+ @endif @if($articolo->diametro)
Diametro @@ -121,12 +138,28 @@ {{ $articolo->max_thrust_a }}
@endif + @if($articolo->immagine_thrust) +
+ Immagine Thrust + Immagine Thrust +
+ @endif @if($articolo->min_torque_a)
Min Torque (A) {{ $articolo->min_torque_a }}
@endif + @if($articolo->immagine_tourque) +
+ Immagine Tourque + Immagine Tourque +
+ @endif @if($articolo->quantita_fori)
Quantita Fori @@ -136,8 +169,13 @@
+ +
+

Ultimo aggiornamento: {{ $articolo->updated_at->format('d/m/Y H:i') }}

+
+ -
+ - -
-

Ultimo aggiornamento: {{ $articolo->updated_at->format('d/m/Y H:i') }}

+ + + + + + +
+ +