12 커밋

작성자 SHA1 메시지 날짜
  marcofalabretti 94880aaa1b chore(stile): rimuove spazi finali superflui 2 주 전
  marcofalabretti 23917c24a2 fix(report): usa route nominata per cambio tab 2 주 전
  marcofalabretti 9a8c4e575e fix(varianti): consenti cucina opzionale in editor 2 주 전
  marcofalabretti 13949e7c82 feat(cassa): applica ordinamento piatti e semplifica catalogo 2 주 전
  marcofalabretti c832c4dd96 fix(cassa): evita doppio avvio pagamento ordine 2 주 전
  marcofalabretti 2e2ed5d6f5 fix(permessi): allinea controlli su azioni editor e menu 2 주 전
  marcofalabretti 1271292c87 feat(cli): add attivita filter to fest salute 2 주 전
  marcofalabretti f88d2bce19 fix(cupon): prevent null-date crashes and prefill validity 2 주 전
  marcofalabretti ff99881db9 fix(editors): block delete when linked historical data exists 2 주 전
  marcofalabretti 480e1ee596 feat(piatti): add custom display order support 2 주 전
  marcofalabretti aa49541e6d fix(saltacoda): separate route throttles by flow 2 주 전
  marcofalabretti d7fcd95d85 fix(endpoint): allow registered endpoint setup calls 2 주 전
39개의 변경된 파일417개의 추가작업 그리고 183개의 파일을 삭제
  1. 131
    10
      app/Console/Commands/FestSalute.php
  2. 8
    1
      app/DataTables/CucinaDataTableEditor.php
  3. 21
    8
      app/DataTables/CuponDataTable.php
  4. 6
    1
      app/DataTables/DispositivoDataTableEditor.php
  5. 5
    1
      app/DataTables/EventoDataTableEditor.php
  6. 6
    1
      app/DataTables/MetodoPagamentoDataTableEditor.php
  7. 11
    3
      app/DataTables/PiattoDataTable.php
  8. 25
    1
      app/DataTables/PiattoDataTableEditor.php
  9. 1
    1
      app/DataTables/RoleDataTable.php
  10. 2
    2
      app/DataTables/VariantePiattoDataTableEditor.php
  11. 14
    1
      app/Http/Controllers/CarrelloController.php
  12. 1
    1
      app/Http/Controllers/CategoriaContabileController.php
  13. 1
    1
      app/Http/Controllers/ChiusuraServizioController.php
  14. 1
    1
      app/Http/Controllers/ContenutoBachecaController.php
  15. 8
    1
      app/Http/Controllers/CuponController.php
  16. 1
    1
      app/Http/Controllers/EventoFormController.php
  17. 1
    1
      app/Http/Controllers/ImpostazioniController.php
  18. 9
    1
      app/Http/Controllers/OperatoreController.php
  19. 1
    1
      app/Http/Controllers/OpzioneFormController.php
  20. 1
    1
      app/Http/Controllers/PiattoController.php
  21. 1
    1
      app/Http/Controllers/PrenotazioneTavoloController.php
  22. 1
    1
      app/Http/Controllers/PuntoOperatoreController.php
  23. 1
    1
      app/Http/Controllers/PuntoVenditaController.php
  24. 1
    1
      app/Http/Controllers/TavoloController.php
  25. 1
    1
      app/Http/Controllers/TestiController.php
  26. 9
    1
      app/Http/Middleware/EndpointTokenMiddleware.php
  27. 2
    0
      app/Models/AbstractModels/AbstractPiatto.php
  28. 37
    0
      app/Models/Piatto.php
  29. 31
    0
      database/migrations/2026_09_01_223138_add_ordine_visualizzazione_to_piatto_table.php
  30. 1
    1
      resources/views/cupon/stato.blade.php
  31. 2
    2
      resources/views/evento_form/opzioni/menu.blade.php
  32. 3
    3
      resources/views/fatturazione/menu.blade.php
  33. 2
    2
      resources/views/mappa_dispositivi/menu.blade.php
  34. 1
    1
      resources/views/operatore/menu.blade.php
  35. 42
    38
      resources/views/punto_vendita/cassa/_partials/menu_cucine.blade.php
  36. 0
    34
      resources/views/punto_vendita/cassa/viste/catalogo/_partials/style.blade.php
  37. 24
    53
      resources/views/punto_vendita/cassa/viste/catalogo/index.blade.php
  38. 1
    1
      resources/views/report/index.blade.php
  39. 3
    3
      routes/web.php

+ 131
- 10
app/Console/Commands/FestSalute.php 파일 보기

2
 
2
 
3
 namespace App\Console\Commands;
3
 namespace App\Console\Commands;
4
 
4
 
5
+use App\Models\Attivita;
5
 use App\Models\Cucina;
6
 use App\Models\Cucina;
6
 use App\Models\Dispositivo;
7
 use App\Models\Dispositivo;
7
 use App\Models\Endpoint;
8
 use App\Models\Endpoint;
14
 
15
 
15
 class FestSalute extends Command
16
 class FestSalute extends Command
16
 {
17
 {
17
-    protected $signature = 'fest:salute';
18
+    protected $signature = 'fest:salute {--attivita= : ID o slug attività (oppure usa --attivita senza valore per scegliere da elenco)}';
18
 
19
 
19
     protected $description = 'Verifica Redis, code, stampe, endpoint e configurazione cassa/cucina';
20
     protected $description = 'Verifica Redis, code, stampe, endpoint e configurazione cassa/cucina';
20
 
21
 
22
 
23
 
23
     private int $attenzione = 0;
24
     private int $attenzione = 0;
24
 
25
 
26
+    private ?int $attivitaIdFiltro = null;
27
+
28
+    private ?string $attivitaLabelFiltro = null;
29
+
25
     public function handle(): int
30
     public function handle(): int
26
     {
31
     {
27
         $this->info('Fest — controllo salute');
32
         $this->info('Fest — controllo salute');
28
         $this->newLine();
33
         $this->newLine();
29
 
34
 
35
+        if (! $this->resolveAttivitaFiltro()) {
36
+            return self::FAILURE;
37
+        }
38
+
39
+        if ($this->attivitaIdFiltro !== null) {
40
+            $this->line('<comment>Filtro attività:</comment> #'.$this->attivitaIdFiltro.' '.$this->attivitaLabelFiltro);
41
+            $this->newLine();
42
+        }
43
+
30
         $this->checkDatabase();
44
         $this->checkDatabase();
31
         $this->checkMigrazioni();
45
         $this->checkMigrazioni();
32
         $this->checkRedis();
46
         $this->checkRedis();
170
     private function checkEndpoint(): void
184
     private function checkEndpoint(): void
171
     {
185
     {
172
         try {
186
         try {
173
-            $attivi = Endpoint::query()->where('status', Endpoint::ATTIVO)->get();
187
+            $query = Endpoint::query()->where('status', Endpoint::ATTIVO);
188
+            if ($this->attivitaIdFiltro !== null) {
189
+                $query->where('attivita_id', $this->attivitaIdFiltro);
190
+            }
191
+
192
+            $attivi = $query->get();
174
             if ($attivi->isEmpty()) {
193
             if ($attivi->isEmpty()) {
175
                 $this->warnCheck('Endpoint', 'nessun FestAgent attivo: le stampe non escono');
194
                 $this->warnCheck('Endpoint', 'nessun FestAgent attivo: le stampe non escono');
176
 
195
 
205
     private function checkCasse(): void
224
     private function checkCasse(): void
206
     {
225
     {
207
         try {
226
         try {
208
-            $casse = Dispositivo::query()
227
+            $query = Dispositivo::query()
209
                 ->whereIn('tipo', [Dispositivo::CASSA, Dispositivo::KIOSK, Dispositivo::CAMERIERE])
228
                 ->whereIn('tipo', [Dispositivo::CASSA, Dispositivo::KIOSK, Dispositivo::CAMERIERE])
210
-                ->where('is_attivo', true)
211
-                ->get();
229
+                ->where('is_attivo', true);
230
+
231
+            if ($this->attivitaIdFiltro !== null) {
232
+                $query->where('attivita_id', $this->attivitaIdFiltro);
233
+            }
234
+
235
+            $casse = $query->get();
212
 
236
 
213
             if ($casse->isEmpty()) {
237
             if ($casse->isEmpty()) {
214
                 $this->warnCheck('Casse', 'nessun punto vendita attivo');
238
                 $this->warnCheck('Casse', 'nessun punto vendita attivo');
233
     private function checkCucine(): void
257
     private function checkCucine(): void
234
     {
258
     {
235
         try {
259
         try {
236
-            $cucine = Cucina::query()->where('is_attiva', true)->get();
260
+            $query = Cucina::query()->where('is_attiva', true);
261
+            if ($this->attivitaIdFiltro !== null) {
262
+                $query->where('attivita_id', $this->attivitaIdFiltro);
263
+            }
264
+
265
+            $cucine = $query->get();
237
             if ($cucine->isEmpty()) {
266
             if ($cucine->isEmpty()) {
238
                 $this->warnCheck('Cucine', 'nessuna cucina attiva');
267
                 $this->warnCheck('Cucine', 'nessuna cucina attiva');
239
 
268
 
258
     {
287
     {
259
         try {
288
         try {
260
             $limite = now()->subMinutes(10);
289
             $limite = now()->subMinutes(10);
261
-            $inAttesa = PrintJob::query()
290
+            $baseQuery = PrintJob::query()
291
+                ->where('created_at', '<', $limite);
292
+
293
+            if ($this->attivitaIdFiltro !== null) {
294
+                $attivitaId = $this->attivitaIdFiltro;
295
+                $baseQuery->where(function ($query) use ($attivitaId) {
296
+                    $query
297
+                        ->whereHas('ordine', fn ($q) => $q->where('attivita_id', $attivitaId))
298
+                        ->orWhereHas('dispositivo', fn ($q) => $q->where('attivita_id', $attivitaId))
299
+                        ->orWhereHas('stampante', fn ($q) => $q->where('attivita_id', $attivitaId));
300
+                });
301
+            }
302
+
303
+            $inAttesa = (clone $baseQuery)
262
                 ->where('stato', PrintJob::STATO_IN_ATTESA)
304
                 ->where('stato', PrintJob::STATO_IN_ATTESA)
263
-                ->where('created_at', '<', $limite)
264
                 ->count();
305
                 ->count();
265
-            $inCoda = PrintJob::query()
306
+            $inCoda = (clone $baseQuery)
266
                 ->where('stato', PrintJob::STATO_IN_CODA)
307
                 ->where('stato', PrintJob::STATO_IN_CODA)
267
-                ->where('created_at', '<', $limite)
268
                 ->count();
308
                 ->count();
269
             $fallitiOggi = PrintJob::query()
309
             $fallitiOggi = PrintJob::query()
270
                 ->where('stato', PrintJob::STATO_FALLITO)
310
                 ->where('stato', PrintJob::STATO_FALLITO)
271
                 ->whereDate('created_at', today())
311
                 ->whereDate('created_at', today())
312
+                ->when(
313
+                    $this->attivitaIdFiltro !== null,
314
+                    function ($query) {
315
+                        $attivitaId = $this->attivitaIdFiltro;
316
+                        $query->where(function ($subQuery) use ($attivitaId) {
317
+                            $subQuery
318
+                                ->whereHas('ordine', fn ($q) => $q->where('attivita_id', $attivitaId))
319
+                                ->orWhereHas('dispositivo', fn ($q) => $q->where('attivita_id', $attivitaId))
320
+                                ->orWhereHas('stampante', fn ($q) => $q->where('attivita_id', $attivitaId));
321
+                        });
322
+                    }
323
+                )
272
                 ->count();
324
                 ->count();
273
 
325
 
274
             if ($inAttesa > 0 || $inCoda > 0) {
326
             if ($inAttesa > 0 || $inCoda > 0) {
308
         $this->attenzione++;
360
         $this->attenzione++;
309
         $this->line('<comment>[ATTENZIONE]</comment> '.$voce.' — '.$dettaglio);
361
         $this->line('<comment>[ATTENZIONE]</comment> '.$voce.' — '.$dettaglio);
310
     }
362
     }
363
+
364
+    private function resolveAttivitaFiltro(): bool
365
+    {
366
+        $rawOption = $this->option('attivita');
367
+        $flagAttivitaPresente = $this->input->hasParameterOption('--attivita');
368
+
369
+        if (! $flagAttivitaPresente && ! filled($rawOption)) {
370
+            return true;
371
+        }
372
+
373
+        $attivitaList = Attivita::query()
374
+            ->orderBy('nome')
375
+            ->get(['id', 'nome', 'slug']);
376
+
377
+        if ($attivitaList->isEmpty()) {
378
+            $this->koCheck('Attività', 'nessuna attività disponibile per applicare il filtro');
379
+
380
+            return false;
381
+        }
382
+
383
+        if (filled($rawOption)) {
384
+            $value = trim((string) $rawOption);
385
+            $attivita = is_numeric($value)
386
+                ? $attivitaList->firstWhere('id', (int) $value)
387
+                : $attivitaList->firstWhere('slug', $value);
388
+
389
+            if (! $attivita) {
390
+                $this->koCheck('Attività', "filtro '{$value}' non trovato (usa ID o slug valido)");
391
+
392
+                return false;
393
+            }
394
+
395
+            $this->attivitaIdFiltro = (int) $attivita->id;
396
+            $this->attivitaLabelFiltro = (string) ($attivita->nome ?? ('Attività #'.$attivita->id));
397
+
398
+            return true;
399
+        }
400
+
401
+        if (! $this->input->isInteractive()) {
402
+            $this->koCheck('Attività', 'modalità non interattiva: usa --attivita={id|slug}');
403
+
404
+            return false;
405
+        }
406
+
407
+        $choices = $attivitaList
408
+            ->map(fn (Attivita $attivita) => (string) $attivita->id.' - '.((string) ($attivita->nome ?? 'Attività')).(filled($attivita->slug) ? ' ['.$attivita->slug.']' : ''))
409
+            ->values()
410
+            ->all();
411
+
412
+        $selected = (string) $this->choice('Seleziona attività da analizzare', $choices, 0);
413
+        if (! preg_match('/^(\d+)/', $selected, $match)) {
414
+            $this->koCheck('Attività', 'selezione non valida');
415
+
416
+            return false;
417
+        }
418
+
419
+        $selectedId = (int) $match[1];
420
+        $attivita = $attivitaList->firstWhere('id', $selectedId);
421
+        if (! $attivita) {
422
+            $this->koCheck('Attività', 'attività selezionata non trovata');
423
+
424
+            return false;
425
+        }
426
+
427
+        $this->attivitaIdFiltro = $selectedId;
428
+        $this->attivitaLabelFiltro = (string) ($attivita->nome ?? ('Attività #'.$selectedId));
429
+
430
+        return true;
431
+    }
311
 }
432
 }

+ 8
- 1
app/DataTables/CucinaDataTableEditor.php 파일 보기

6
 use App\Models\Dispositivo;
6
 use App\Models\Dispositivo;
7
 use App\Services\Attivita\AttivitaService;
7
 use App\Services\Attivita\AttivitaService;
8
 use Illuminate\Database\Eloquent\Model;
8
 use Illuminate\Database\Eloquent\Model;
9
-use Yajra\DataTables\DataTablesEditor;
10
 use Illuminate\Support\Facades\Session;
9
 use Illuminate\Support\Facades\Session;
10
+use Illuminate\Validation\ValidationException;
11
+use Yajra\DataTables\DataTablesEditor;
11
 
12
 
12
 class CucinaDataTableEditor extends DataTablesEditor
13
 class CucinaDataTableEditor extends DataTablesEditor
13
 {
14
 {
104
   {
105
   {
105
     AttivitaService::garantisciAttivita($model->attivita_id);
106
     AttivitaService::garantisciAttivita($model->attivita_id);
106
 
107
 
108
+    if ($model->piatti()->exists() || $model->prenotazioni()->exists()) {
109
+      throw ValidationException::withMessages([
110
+        'cucina' => 'Non puoi eliminare questa cucina: ha ancora piatti o prenotazioni. Disattivala invece.',
111
+      ]);
112
+    }
113
+
107
     return $data;
114
     return $data;
108
   }
115
   }
109
 
116
 

+ 21
- 8
app/DataTables/CuponDataTable.php 파일 보기

13
 use Yajra\DataTables\Services\DataTable;
13
 use Yajra\DataTables\Services\DataTable;
14
 use Illuminate\Support\Facades\Auth;
14
 use Illuminate\Support\Facades\Auth;
15
 use Illuminate\Support\Str;
15
 use Illuminate\Support\Str;
16
-
16
+use Carbon\Carbon;
17
 
17
 
18
 class CuponDataTable extends DataTable
18
 class CuponDataTable extends DataTable
19
 {
19
 {
37
                 return '€ ' . number_format($entity->importo, 2, ',', '.');
37
                 return '€ ' . number_format($entity->importo, 2, ',', '.');
38
             })
38
             })
39
             ->editColumn('validita_display', function ($entity) {
39
             ->editColumn('validita_display', function ($entity) {
40
-                if($entity->validita_da->isSameDay($entity->validita_a)){
41
-                    return $entity->validita_da->format('d/m/Y');
42
-                }else{
43
-                    return $entity->validita_da->format('d/m/Y') . ' - ' . $entity->validita_a->format('d/m/Y');
44
-                }   
40
+                $da = $entity->validita_da;
41
+                $a = $entity->validita_a;
42
+
43
+                if (! $da && ! $a) {
44
+                    return '—';
45
+                }
46
+
47
+                if ($da && ! $a) {
48
+                    return $da->format('d/m/Y');
49
+                }
50
+
51
+                if (! $da && $a) {
52
+                    return $a->format('d/m/Y');
53
+                }
54
+
55
+                return $da->isSameDay($a)
56
+                    ? $da->format('d/m/Y')
57
+                    : $da->format('d/m/Y') . ' - ' . $a->format('d/m/Y');
45
             })
58
             })
46
             ->addColumn('stato_display', function ($entity) {
59
             ->addColumn('stato_display', function ($entity) {
47
                 return view('cupon.stato', ['stato' => $entity->stato]);
60
                 return view('cupon.stato', ['stato' => $entity->stato]);
122
                         Fields\Number::make('importo')->label('Importo')->required(),
135
                         Fields\Number::make('importo')->label('Importo')->required(),
123
                         Fields\Text::make('nominativo')->label('Nominativo')->required(),
136
                         Fields\Text::make('nominativo')->label('Nominativo')->required(),
124
                         Fields\Text::make('email')->label('Email'),
137
                         Fields\Text::make('email')->label('Email'),
125
-                        Fields\Date::make('validita_da')->label('Validità da')->required(),
126
-                        Fields\Date::make('validita_a')->label('Validità a')->required(),
138
+                        Fields\Date::make('validita_da')->label('Validità da')->required()->default(Carbon::now()->format('Y-m-d')),
139
+                        Fields\Date::make('validita_a')->label('Validità a')->required()->default(Carbon::now()->format('Y-m-d')),
127
                     ]));
140
                     ]));
128
     }
141
     }
129
 
142
 

+ 6
- 1
app/DataTables/DispositivoDataTableEditor.php 파일 보기

7
 use Illuminate\Database\Eloquent\Model;
7
 use Illuminate\Database\Eloquent\Model;
8
 use Illuminate\Support\Facades\Session;
8
 use Illuminate\Support\Facades\Session;
9
 use Yajra\DataTables\DataTablesEditor;
9
 use Yajra\DataTables\DataTablesEditor;
10
+use Illuminate\Validation\ValidationException;
10
 
11
 
11
 class DispositivoDataTableEditor extends DataTablesEditor
12
 class DispositivoDataTableEditor extends DataTablesEditor
12
 {
13
 {
118
   public function deleting(Model $model, array $data): array
119
   public function deleting(Model $model, array $data): array
119
   {
120
   {
120
     AttivitaService::garantisciAttivita($model->attivita_id);
121
     AttivitaService::garantisciAttivita($model->attivita_id);
121
-
122
+    if ($model->ordini()->exists()) {
123
+      throw ValidationException::withMessages([
124
+        'dispositivo' => 'Non puoi eliminare questo dispositivo: è già stato utilizzato in ordini. Modifica la disponiblità invece.',
125
+      ]);
126
+    }
122
     return $data;
127
     return $data;
123
   }
128
   }
124
   public function messages(): array
129
   public function messages(): array

+ 5
- 1
app/DataTables/EventoDataTableEditor.php 파일 보기

129
   public function deleting(Model $model, array $data): array
129
   public function deleting(Model $model, array $data): array
130
   {
130
   {
131
     AttivitaService::garantisciAttivita($model->attivita_id);
131
     AttivitaService::garantisciAttivita($model->attivita_id);
132
-
132
+    if ($model->prenotazioni()->exists()) {
133
+      throw ValidationException::withMessages([
134
+        'evento' => 'Non puoi eliminare questo evento: ha ancora prenotazioni. Modifica la disponiblità invece.',
135
+      ]);
136
+    }
133
     return $data;
137
     return $data;
134
   }
138
   }
135
 
139
 

+ 6
- 1
app/DataTables/MetodoPagamentoDataTableEditor.php 파일 보기

7
 use Illuminate\Database\Eloquent\Model;
7
 use Illuminate\Database\Eloquent\Model;
8
 use Yajra\DataTables\DataTablesEditor;
8
 use Yajra\DataTables\DataTablesEditor;
9
 use Illuminate\Support\Facades\Session;
9
 use Illuminate\Support\Facades\Session;
10
+use Illuminate\Validation\ValidationException;
10
 
11
 
11
 class  MetodoPagamentoDataTableEditor extends DataTablesEditor
12
 class  MetodoPagamentoDataTableEditor extends DataTablesEditor
12
 {
13
 {
100
   public function deleting(Model $model, array $data): array
101
   public function deleting(Model $model, array $data): array
101
   {
102
   {
102
     AttivitaService::garantisciAttivita($model->attivita_id);
103
     AttivitaService::garantisciAttivita($model->attivita_id);
103
-
104
+    if ($model->ordini()->exists() || $model->pagamenti()->exists()) {
105
+      throw ValidationException::withMessages([
106
+        'metodo_pagamento' => 'Non puoi eliminare questo metodo di pagamento: è già stato utilizzato in ordini. Modifica la disponiblità invece.',
107
+      ]);
108
+    }
104
     return $data;
109
     return $data;
105
   }
110
   }
106
 
111
 

+ 11
- 3
app/DataTables/PiattoDataTable.php 파일 보기

70
             ->addColumn('prezzo_display', function ($entity) {
70
             ->addColumn('prezzo_display', function ($entity) {
71
                 return '€ ' . number_format($entity->prezzo, 2, ',', '.');
71
                 return '€ ' . number_format($entity->prezzo, 2, ',', '.');
72
             })
72
             })
73
+            ->addColumn('ordine_visualizzazione_display', function ($entity) {
74
+                return Piatto::ordineVisualizzazioneImpostato($entity->ordine_visualizzazione)
75
+                    ? (string) (int) $entity->ordine_visualizzazione
76
+                    : '';
77
+            })
73
             ->setRowId('id');
78
             ->setRowId('id');
74
     }
79
     }
75
 
80
 
89
 
94
 
90
         if (isset($this->cucina_id)) {
95
         if (isset($this->cucina_id)) {
91
             return $model->where('cucina_id', $this->cucina_id)
96
             return $model->where('cucina_id', $this->cucina_id)
92
-                ->with('cucina', 'elencoAllergeni');
97
+                ->with('cucina', 'elencoAllergeni')
98
+                ->ordinatiInCassa();
93
         }
99
         }
94
 
100
 
95
-        return $model->with('cucina', 'elencoAllergeni');
101
+        return $model->with('cucina', 'elencoAllergeni')->ordinatiInCassa();
96
     }
102
     }
97
 
103
 
98
     /**
104
     /**
146
                     ->ajax(route('piatto.store'))
152
                     ->ajax(route('piatto.store'))
147
                     ->fields([
153
                     ->fields([
148
                         Fields\Hidden::make('attivita_id')->label('Attività')->default((int) $this->attivita_id),
154
                         Fields\Hidden::make('attivita_id')->label('Attività')->default((int) $this->attivita_id),
155
+                        Fields\Number::make('ordine_visualizzazione')->label('Ordine visualizzazione'),
149
                         Fields\Text::make('nome')->label('Nome'),
156
                         Fields\Text::make('nome')->label('Nome'),
150
                         Fields\Text::make('descrizione')->label('Descrizione'),
157
                         Fields\Text::make('descrizione')->label('Descrizione'),
151
-                        Fields\Select2::make('cucina_id')->label('cucina')
158
+                        Fields\Select2::make('cucina_id')->label('Cucina')
152
                         ->options(Cucina::where('is_attiva', true)->where('attivita_id', (int) $this->attivita_id)->pluck('id', 'nome'))
159
                         ->options(Cucina::where('is_attiva', true)->where('attivita_id', (int) $this->attivita_id)->pluck('id', 'nome'))
153
                         ->default(isset($this->cucina_id) ? $this->cucina_id : ''),
160
                         ->default(isset($this->cucina_id) ? $this->cucina_id : ''),
154
                         Fields\Text::make('prezzo')->label('Prezzo (€)'),
161
                         Fields\Text::make('prezzo')->label('Prezzo (€)'),
202
         $columns = array_merge($columns, [
209
         $columns = array_merge($columns, [
203
 
210
 
204
             Column::make('nome')->title('Nome')->searchable()->responsivePriority(1),
211
             Column::make('nome')->title('Nome')->searchable()->responsivePriority(1),
212
+            Column::make('ordine_visualizzazione')->data('ordine_visualizzazione_display')->title('Ordine')->addClass('text-center')->width('8%'),
205
             Column::make('prezzo')->data('prezzo_display')->title('Prezzo')->addClass('text-center'),
213
             Column::make('prezzo')->data('prezzo_display')->title('Prezzo')->addClass('text-center'),
206
             // Column::computed('cucina_id')->data('cucina_id_display')->title('Cucina')->addClass('text-center')->responsivePriority(2), // icona in tabella
214
             // Column::computed('cucina_id')->data('cucina_id_display')->title('Cucina')->addClass('text-center')->responsivePriority(2), // icona in tabella
207
             Column::make('allergeni')->title('Allergeni')->visible(false), // valore grezzo per l'editor (array ID)
215
             Column::make('allergeni')->title('Allergeni')->visible(false), // valore grezzo per l'editor (array ID)

+ 25
- 1
app/DataTables/PiattoDataTableEditor.php 파일 보기

5
 use App\Models\Cucina;
5
 use App\Models\Cucina;
6
 use App\Models\Piatto;
6
 use App\Models\Piatto;
7
 use App\Models\PiattoHasAllergene;
7
 use App\Models\PiattoHasAllergene;
8
+use App\Models\RigaOrdine;
9
+use App\Models\SaltacodaRigaOrdine;
8
 use App\Services\Attivita\AttivitaService;
10
 use App\Services\Attivita\AttivitaService;
9
 use Illuminate\Database\Eloquent\Model;
11
 use Illuminate\Database\Eloquent\Model;
10
 use Yajra\DataTables\DataTablesEditor;
12
 use Yajra\DataTables\DataTablesEditor;
11
 use Illuminate\Support\Facades\Session;
13
 use Illuminate\Support\Facades\Session;
14
+use Illuminate\Validation\ValidationException;
12
 
15
 
13
 class PiattoDataTableEditor extends DataTablesEditor
16
 class PiattoDataTableEditor extends DataTablesEditor
14
 {
17
 {
26
     'bacheca_id.required' => 'Indicare una delle due opzioni',
29
     'bacheca_id.required' => 'Indicare una delle due opzioni',
27
     'in_evidenza.required' => 'Indicare una delle due opzioni',
30
     'in_evidenza.required' => 'Indicare una delle due opzioni',
28
     'cucina_id.required' => 'La cucina è richiesta',
31
     'cucina_id.required' => 'La cucina è richiesta',
32
+    'ordine_visualizzazione.required' => 'Per ordine alfabetico scrivi 0 altrimenti imposta posizione ',
29
   ];
33
   ];
30
 
34
 
31
   /**
35
   /**
46
       'allergeni.*' => 'integer|exists:allergene,id',
50
       'allergeni.*' => 'integer|exists:allergene,id',
47
       'bacheca_id' => 'nullable|boolean',
51
       'bacheca_id' => 'nullable|boolean',
48
       'in_evidenza' => 'nullable|boolean',
52
       'in_evidenza' => 'nullable|boolean',
53
+      'ordine_visualizzazione' => 'required|integer',
49
     ];
54
     ];
50
   }
55
   }
51
 
56
 
72
       'allergeni.*' => 'integer|exists:allergene,id',
77
       'allergeni.*' => 'integer|exists:allergene,id',
73
       'bacheca_id' => 'nullable|boolean',
78
       'bacheca_id' => 'nullable|boolean',
74
       'in_evidenza' => 'nullable|boolean',
79
       'in_evidenza' => 'nullable|boolean',
80
+      'ordine_visualizzazione' => 'nullable|integer',
75
     ];
81
     ];
76
   }
82
   }
77
 
83
 
104
     $data['bacheca_id'] = isset($data['bacheca_id']) ? true : false;
110
     $data['bacheca_id'] = isset($data['bacheca_id']) ? true : false;
105
     $data['in_evidenza'] = isset($data['in_evidenza']) ? true : false;
111
     $data['in_evidenza'] = isset($data['in_evidenza']) ? true : false;
106
     $data['is_attivo'] = isset($data['is_attivo']) ? true : false;
112
     $data['is_attivo'] = isset($data['is_attivo']) ? true : false;
113
+    $data['ordine_visualizzazione'] = $this->normalizeOrdineVisualizzazione($data['ordine_visualizzazione'] ?? null);
107
     return $data;
114
     return $data;
108
   }
115
   }
109
 
116
 
119
     $data['bacheca_id'] = isset($data['bacheca_id']) ? true : false;
126
     $data['bacheca_id'] = isset($data['bacheca_id']) ? true : false;
120
     $data['in_evidenza'] = isset($data['in_evidenza']) ? true : false;
127
     $data['in_evidenza'] = isset($data['in_evidenza']) ? true : false;
121
     $data['is_attivo'] = isset($data['is_attivo']) ? true : false;
128
     $data['is_attivo'] = isset($data['is_attivo']) ? true : false;
129
+    $data['ordine_visualizzazione'] = $this->normalizeOrdineVisualizzazione($data['ordine_visualizzazione'] ?? null);
122
 
130
 
123
     return $data;
131
     return $data;
124
   }
132
   }
127
   {
135
   {
128
     AttivitaService::garantisciAttivita($model->attivita_id);
136
     AttivitaService::garantisciAttivita($model->attivita_id);
129
 
137
 
130
-    return $data;
138
+      $usatoInOrdini = RigaOrdine::where('piatto_id', $model->id)->exists();
139
+      $usatoInSaltacoda = SaltacodaRigaOrdine::where('piatto_id', $model->id)->exists();
140
+        if ($usatoInOrdini || $usatoInSaltacoda) {
141
+            throw ValidationException::withMessages([
142
+                'piatto' => 'Non puoi eliminare questo piatto: è già presente in ordini o preordini. Modifica la disponiblità invece.',
143
+            ]);
144
+        }
145
+        return $data;
146
+  }
147
+
148
+  private function normalizeOrdineVisualizzazione(mixed $value): ?int
149
+  {
150
+    if ($value === null || $value === '' || (int) $value === 0) {
151
+      return null;
152
+    }
153
+
154
+    return (int) $value;
131
   }
155
   }
132
 
156
 
133
   private function garantisciCucinaDiQuestaAttivita($cucinaId, $attivitaId): void
157
   private function garantisciCucinaDiQuestaAttivita($cucinaId, $attivitaId): void

+ 1
- 1
app/DataTables/RoleDataTable.php 파일 보기

59
   public function html()
59
   public function html()
60
   {
60
   {
61
     $buttons = [];
61
     $buttons = [];
62
-    if(Auth::user()->can('create-roles-permissions')){
62
+    if(Auth::user()->can('create-role')){
63
       array_push($buttons, Button::make('create')
63
       array_push($buttons, Button::make('create')
64
       ->editor('editor')
64
       ->editor('editor')
65
       ->className('btn btn-sm btn-primary')
65
       ->className('btn btn-sm btn-primary')

+ 2
- 2
app/DataTables/VariantePiattoDataTableEditor.php 파일 보기

39
       'label'  => 'required',
39
       'label'  => 'required',
40
       'prezzo' => 'required',
40
       'prezzo' => 'required',
41
       'piatto_id' => 'nullable|exists:piatto,id',
41
       'piatto_id' => 'nullable|exists:piatto,id',
42
-      'cucina_id' => 'required|exists:cucina,id',
42
+      'cucina_id' => 'nullable|exists:cucina,id',
43
     ];
43
     ];
44
   }
44
   }
45
 
45
 
59
       'label'  => 'required',
59
       'label'  => 'required',
60
       'prezzo' => 'required',
60
       'prezzo' => 'required',
61
       'piatto_id' => 'nullable|exists:piatto,id',
61
       'piatto_id' => 'nullable|exists:piatto,id',
62
-      'cucina_id' => 'required|exists:cucina,id',
62
+      'cucina_id' => 'nullable|exists:cucina,id',
63
     ];
63
     ];
64
 
64
 
65
   }
65
   }

+ 14
- 1
app/Http/Controllers/CarrelloController.php 파일 보기

391
             return back()->with('error', 'Tag ID obbligatorio per il metodo di pagamento Segresta Wallet');
391
             return back()->with('error', 'Tag ID obbligatorio per il metodo di pagamento Segresta Wallet');
392
         }
392
         }
393
 
393
 
394
+        $presi = Ordine::where('id', $ordine->id)
395
+            ->where('stato', Ordine::CARRELLO)
396
+            ->update(['stato' => Ordine::IN_ATTESA_PAGAMENTO]);
397
+
398
+        if ($presi === 0) {
399
+            return redirect()
400
+                ->route('punto-vendita.show', ['punto_vendita_id' => $ordine->dispositivo_id])
401
+                ->with('error', 'Ordine già in pagamento. Non confermare di nuovo.');
402
+        }
403
+
404
+        $ordine->refresh();
405
+
406
+
394
         $pagamento = Pagamento::create([
407
         $pagamento = Pagamento::create([
395
             'attivita_id' => $attivitaId,
408
             'attivita_id' => $attivitaId,
396
             'dispositivo_id' => $dispositivoId,
409
             'dispositivo_id' => $dispositivoId,
401
             'causale' => $ordine->attivita->nome.' - Pagamento ordine #'.$ordine->id,
414
             'causale' => $ordine->attivita->nome.' - Pagamento ordine #'.$ordine->id,
402
         ]);
415
         ]);
403
         $ordine->pagamento_id = $pagamento->id;
416
         $ordine->pagamento_id = $pagamento->id;
404
-        $ordine->stato = Ordine::IN_ATTESA_PAGAMENTO;
417
+        // $ordine->stato = Ordine::IN_ATTESA_PAGAMENTO;
405
 
418
 
406
         $ordine->info = array_merge(
419
         $ordine->info = array_merge(
407
             is_array($ordine->info) ? $ordine->info : [],
420
             is_array($ordine->info) ? $ordine->info : [],

+ 1
- 1
app/Http/Controllers/CategoriaContabileController.php 파일 보기

47
                 case 'edit':
47
                 case 'edit':
48
                     if(!Auth::user()->can('edit-categoria_contabile')) return;
48
                     if(!Auth::user()->can('edit-categoria_contabile')) return;
49
                     break;
49
                     break;
50
-                case 'delete':
50
+                case 'remove':
51
                     if(!Auth::user()->can('delete-categoria_contabile')) return;
51
                     if(!Auth::user()->can('delete-categoria_contabile')) return;
52
                     break;
52
                     break;
53
             }
53
             }

+ 1
- 1
app/Http/Controllers/ChiusuraServizioController.php 파일 보기

50
               case 'edit':
50
               case 'edit':
51
                   if(!Auth::user()->can('edit-chiusura-servizio')) return;
51
                   if(!Auth::user()->can('edit-chiusura-servizio')) return;
52
                   break;
52
                   break;
53
-              case 'delete':
53
+              case 'remove':
54
                   if(!Auth::user()->can('delete-chiusura-servizio')) return;
54
                   if(!Auth::user()->can('delete-chiusura-servizio')) return;
55
                   break;
55
                   break;
56
           }
56
           }

+ 1
- 1
app/Http/Controllers/ContenutoBachecaController.php 파일 보기

48
                 case 'edit':
48
                 case 'edit':
49
                     if(!Auth::user()->can('edit-contenuto_bacheca')) return;
49
                     if(!Auth::user()->can('edit-contenuto_bacheca')) return;
50
                     break;
50
                     break;
51
-                case 'delete':
51
+                case 'remove':
52
                     if(!Auth::user()->can('delete-contenuto_bacheca')) return;
52
                     if(!Auth::user()->can('delete-contenuto_bacheca')) return;
53
                     break;
53
                     break;
54
             }
54
             }

+ 8
- 1
app/Http/Controllers/CuponController.php 파일 보기

207
                     $message = 'Cupon utilizzabile';
207
                     $message = 'Cupon utilizzabile';
208
                 }
208
                 }
209
 
209
 
210
+                $validitaDa = $cupon->validita_da;
211
+                $validitaA = $cupon->validita_a;
212
+                $oggi = $validitaDa
213
+                    && $validitaA
214
+                    && $validitaDa->isSameDay($validitaA)
215
+                    && $validitaDa->isSameDay(Carbon::today());
216
+
210
                 return response()->json([
217
                 return response()->json([
211
                     'success'    => $success,
218
                     'success'    => $success,
212
                     'message'    => $message,
219
                     'message'    => $message,
214
                     'nominativo' => $cupon->nominativo,
221
                     'nominativo' => $cupon->nominativo,
215
                     'validita_da' => $cupon->validita_da?->format('d/m/Y'),
222
                     'validita_da' => $cupon->validita_da?->format('d/m/Y'),
216
                     'validita_a'  => $cupon->validita_a?->format('d/m/Y'),
223
                     'validita_a'  => $cupon->validita_a?->format('d/m/Y'),
217
-                    'oggi' => $oggi = $cupon->validita_da->isSameDay($cupon->validita_a) && $cupon->validita_da->isSameDay(Carbon::today()),
224
+                    'oggi' => $oggi,
218
                 ]);
225
                 ]);
219
             case Cupon::USATO:
226
             case Cupon::USATO:
220
                 return response()->json(['success' => false, 'message' => 'Cupon già utilizzato']);
227
                 return response()->json(['success' => false, 'message' => 'Cupon già utilizzato']);

+ 1
- 1
app/Http/Controllers/EventoFormController.php 파일 보기

49
                 case 'edit':
49
                 case 'edit':
50
                     if(!Auth::user()->can('edit-evento_form')) return;
50
                     if(!Auth::user()->can('edit-evento_form')) return;
51
                     break;
51
                     break;
52
-                case 'delete':
52
+                case 'remove':
53
                     if(!Auth::user()->can('delete-evento_form')) return;
53
                     if(!Auth::user()->can('delete-evento_form')) return;
54
                     break;
54
                     break;
55
             }
55
             }

+ 1
- 1
app/Http/Controllers/ImpostazioniController.php 파일 보기

60
                 case 'edit':
60
                 case 'edit':
61
                     if(!Auth::user()->can('edit-impostazioni')) return;
61
                     if(!Auth::user()->can('edit-impostazioni')) return;
62
                     break;
62
                     break;
63
-                case 'delete':
63
+                case 'remove':
64
                     if(!Auth::user()->can('delete-impostazioni')) return;
64
                     if(!Auth::user()->can('delete-impostazioni')) return;
65
                     break;
65
                     break;
66
             }
66
             }

+ 9
- 1
app/Http/Controllers/OperatoreController.php 파일 보기

217
    
217
    
218
 
218
 
219
         // dd(Cookie::get('binding_token'));
219
         // dd(Cookie::get('binding_token'));
220
-        $cucine = $puntoVendita->cucine()->where('is_attiva', true)->orderBy('nome')->get();
220
+        $cucine = $puntoVendita->cucine()
221
+            ->where('is_attiva', true)
222
+            ->with(['piatti' => function ($query) {
223
+                $query->where('is_attivo', true)
224
+                    ->with('allergeni')
225
+                    ->ordinatiInCassa();
226
+            }])
227
+            ->orderBy('nome')
228
+            ->get();
221
 
229
 
222
 
230
 
223
 
231
 

+ 1
- 1
app/Http/Controllers/OpzioneFormController.php 파일 보기

45
                 case 'edit':
45
                 case 'edit':
46
                     if(!Auth::user()->can('edit-opzione_form')) return;
46
                     if(!Auth::user()->can('edit-opzione_form')) return;
47
                     break;
47
                     break;
48
-                case 'delete':
48
+                case 'remove':
49
                     if(!Auth::user()->can('delete-opzione_form')) return;
49
                     if(!Auth::user()->can('delete-opzione_form')) return;
50
                     break;
50
                     break;
51
             }
51
             }

+ 1
- 1
app/Http/Controllers/PiattoController.php 파일 보기

79
             break;
79
             break;
80
 
80
 
81
           case 'remove':
81
           case 'remove':
82
-            if (! Auth::user()->can('remove-piatto')) return;
82
+            if (! Auth::user()->can('delete-piatto')) return;
83
             break;
83
             break;
84
         }
84
         }
85
       }
85
       }

+ 1
- 1
app/Http/Controllers/PrenotazioneTavoloController.php 파일 보기

52
                 case 'edit':
52
                 case 'edit':
53
                     if(!Auth::user()->can('edit-prenotazione_tavolo')) return;
53
                     if(!Auth::user()->can('edit-prenotazione_tavolo')) return;
54
                     break;
54
                     break;
55
-                case 'delete':
55
+                case 'remove':
56
                     if(!Auth::user()->can('delete-prenotazione_tavolo')) return;
56
                     if(!Auth::user()->can('delete-prenotazione_tavolo')) return;
57
                     break;
57
                     break;
58
             }
58
             }

+ 1
- 1
app/Http/Controllers/PuntoOperatoreController.php 파일 보기

56
                 case 'edit':
56
                 case 'edit':
57
                     if(!Auth::user()->can('edit-punto_operatore')) return;
57
                     if(!Auth::user()->can('edit-punto_operatore')) return;
58
                     break;
58
                     break;
59
-                case 'delete':
59
+                case 'remove':
60
                     if(!Auth::user()->can('delete-punto_operatore')) return;
60
                     if(!Auth::user()->can('delete-punto_operatore')) return;
61
                     break;
61
                     break;
62
             }
62
             }

+ 1
- 1
app/Http/Controllers/PuntoVenditaController.php 파일 보기

334
             ->with(['piatti' => function ($query) {
334
             ->with(['piatti' => function ($query) {
335
                 $query->where('is_attivo', true)
335
                 $query->where('is_attivo', true)
336
                     ->with('allergeni')
336
                     ->with('allergeni')
337
-                    ->orderBy('nome');
337
+                    ->ordinatiInCassa();
338
             }])
338
             }])
339
             ->orderBy('nome')
339
             ->orderBy('nome')
340
             ->get();
340
             ->get();

+ 1
- 1
app/Http/Controllers/TavoloController.php 파일 보기

45
                 case 'edit':
45
                 case 'edit':
46
                     if(!Auth::user()->can('edit-tavolo')) return;
46
                     if(!Auth::user()->can('edit-tavolo')) return;
47
                     break;
47
                     break;
48
-                case 'delete':
48
+                case 'remove':
49
                     if(!Auth::user()->can('delete-tavolo')) return;
49
                     if(!Auth::user()->can('delete-tavolo')) return;
50
                     break;
50
                     break;
51
             }
51
             }

+ 1
- 1
app/Http/Controllers/TestiController.php 파일 보기

45
                 case 'edit':
45
                 case 'edit':
46
                     if(!Auth::user()->can('edit-testi')) return;
46
                     if(!Auth::user()->can('edit-testi')) return;
47
                     break;
47
                     break;
48
-                case 'delete':
48
+                case 'remove':
49
                     if(!Auth::user()->can('delete-testi')) return;
49
                     if(!Auth::user()->can('delete-testi')) return;
50
                     break;
50
                     break;
51
             }
51
             }

+ 9
- 1
app/Http/Middleware/EndpointTokenMiddleware.php 파일 보기

16
      */
16
      */
17
     public function handle(Request $request, Closure $next): Response
17
     public function handle(Request $request, Closure $next): Response
18
     {
18
     {
19
+        $statiConsentiti = [Endpoint::ATTIVO, Endpoint::REGISTRATO];
20
+
19
         $token = $request->header('token-endpoint');
21
         $token = $request->header('token-endpoint');
20
         if(!$token){
22
         if(!$token){
21
             Log::error('token-endpoint');
23
             Log::error('token-endpoint');
29
             Log::error('endpoint non trovato con token ');
31
             Log::error('endpoint non trovato con token ');
30
             return response()->json(['message' => 'Endpoint non trovato con token ', 'status' => 'error', 'success' => false], 404);
32
             return response()->json(['message' => 'Endpoint non trovato con token ', 'status' => 'error', 'success' => false], 404);
31
         }
33
         }
32
-        if ($endpoint->status !== Endpoint::ATTIVO) {
34
+
35
+
36
+        if (! in_array($endpoint->status, $statiConsentiti, true)) {
37
+
33
             Log::warning('Endpoint non attivo', [
38
             Log::warning('Endpoint non attivo', [
34
                 'endpoint_id' => $endpoint->id,
39
                 'endpoint_id' => $endpoint->id,
35
                 'status' => $endpoint->status,
40
                 'status' => $endpoint->status,
36
             ]);
41
             ]);
42
+
37
             return response()->json([
43
             return response()->json([
38
                 'message' => 'Endpoint non attivo',
44
                 'message' => 'Endpoint non attivo',
39
                 'status' => 'error',
45
                 'status' => 'error',
40
                 'success' => false,
46
                 'success' => false,
41
             ], 403);
47
             ], 403);
42
         }
48
         }
49
+
50
+
43
         $request->merge(['endpoint' => $endpoint->makeHidden(['product_key', 'secret', 'deeplink_token', 'token'])]);
51
         $request->merge(['endpoint' => $endpoint->makeHidden(['product_key', 'secret', 'deeplink_token', 'token'])]);
44
         return $next($request);
52
         return $next($request);
45
     }
53
     }

+ 2
- 0
app/Models/AbstractModels/AbstractPiatto.php 파일 보기

47
         'allergeni' => 'string',
47
         'allergeni' => 'string',
48
         'bacheca_id' => 'boolean',
48
         'bacheca_id' => 'boolean',
49
         'in_evidenza' => 'boolean',
49
         'in_evidenza' => 'boolean',
50
+        'ordine_visualizzazione' => 'integer',
50
         'created_at' => 'datetime',
51
         'created_at' => 'datetime',
51
         'updated_at' => 'datetime'
52
         'updated_at' => 'datetime'
52
     ];
53
     ];
69
         'allergeni',
70
         'allergeni',
70
         'bacheca_id',
71
         'bacheca_id',
71
         'in_evidenza',
72
         'in_evidenza',
73
+        'ordine_visualizzazione',
72
         'created_at',
74
         'created_at',
73
         'updated_at'
75
         'updated_at'
74
     ];
76
     ];

+ 37
- 0
app/Models/Piatto.php 파일 보기

26
             'allergene_id'
26
             'allergene_id'
27
         );
27
         );
28
     }
28
     }
29
+
30
+    /**
31
+     * NULL e 0 = non impostato. Il cast integer trasforma NULL in 0 in lettura.
32
+     */
33
+    public static function ordineVisualizzazioneImpostato(mixed $valore): bool
34
+    {
35
+        return $valore !== null && $valore !== '' && (int) $valore !== 0;
36
+    }
37
+
38
+    public function scopeOrdinatiInCassa($query)
39
+    {
40
+        return $query
41
+            ->orderByRaw('(ordine_visualizzazione IS NULL OR ordine_visualizzazione = 0) ASC')
42
+            ->orderBy('ordine_visualizzazione')
43
+            ->orderBy('nome');
44
+    }
45
+
46
+    /**
47
+     * Tutti i piatti numerati → ordine in cassa. Altrimenti alfabetico.
48
+     */
49
+    public static function ordinaCollezioneInCassa($piatti)
50
+    {
51
+        $piatti = collect($piatti);
52
+        $tuttiNumerati = $piatti->isNotEmpty()
53
+            && $piatti->every(fn ($piatto) => static::ordineVisualizzazioneImpostato($piatto->ordine_visualizzazione));
54
+
55
+        if ($tuttiNumerati) {
56
+            return $piatti->sortBy([
57
+                ['ordine_visualizzazione', 'asc'],
58
+                ['nome', 'asc'],
59
+            ])->values();
60
+        }
61
+
62
+        return $piatti
63
+            ->sortBy(fn ($piatto) => mb_strtolower((string) ($piatto->nome ?? '')), SORT_NATURAL)
64
+            ->values();
65
+    }
29
 }
66
 }

+ 31
- 0
database/migrations/2026_09_01_223138_add_ordine_visualizzazione_to_piatto_table.php 파일 보기

1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::table('piatto', function (Blueprint $table) {
15
+            $table->integer('ordine_visualizzazione')
16
+                ->default(0)
17
+                ->nullable()
18
+                ->after('in_evidenza');
19
+        });
20
+    }
21
+
22
+    /**
23
+     * Reverse the migrations.
24
+     */
25
+    public function down(): void
26
+    {
27
+        Schema::table('piatto', function (Blueprint $table) {
28
+            $table->dropColumn('ordine_visualizzazione');
29
+        });
30
+    }
31
+};

+ 1
- 1
resources/views/cupon/stato.blade.php 파일 보기

8
 @endphp
8
 @endphp
9
 
9
 
10
 @if($codice !== '' && $stato === '')
10
 @if($codice !== '' && $stato === '')
11
-    <span class="badge bg-body font-monospace fs-6" title="Codice {{ $codice }}">
11
+    <span class="badge font-monospace fs-6" title="Codice {{ $codice }}">
12
         {{ $codice }}
12
         {{ $codice }}
13
     </span>
13
     </span>
14
 @else
14
 @else

+ 2
- 2
resources/views/evento_form/opzioni/menu.blade.php 파일 보기

8
     </button>
8
     </button>
9
     <div class="dropdown-menu">
9
     <div class="dropdown-menu">
10
 
10
 
11
-        @if(Auth::user()->can('edit-opzione_form')->can('edit-evento_form'))
11
+        @if(Auth::user()->can('edit-opzione_form') && Auth::user()->can('edit-evento_form'))
12
         <a href="#" class="dropdown-item editor_edit">
12
         <a href="#" class="dropdown-item editor_edit">
13
             <i class="bx bx-edit-alt me-1"></i> Modifica
13
             <i class="bx bx-edit-alt me-1"></i> Modifica
14
         </a>
14
         </a>
15
         @endif
15
         @endif
16
 
16
 
17
-        @if(Auth::user()->can('delete-opzione_form')-can('delete-evento_form'))
17
+        @if(Auth::user()->can('delete-opzione_form') && Auth::user()->can('delete-evento_form'))
18
         <a href="#" class="dropdown-item editor_delete">
18
         <a href="#" class="dropdown-item editor_delete">
19
             <i class="bx bx-trash me-1"></i> Elimina
19
             <i class="bx bx-trash me-1"></i> Elimina
20
         </a>
20
         </a>

+ 3
- 3
resources/views/fatturazione/menu.blade.php 파일 보기

7
         <i class="bx bx-dots-vertical-rounded fs-large"></i>
7
         <i class="bx bx-dots-vertical-rounded fs-large"></i>
8
     </button>
8
     </button>
9
     <div class="dropdown-menu">
9
     <div class="dropdown-menu">
10
-    @if($entity->fattura_id && (Auth::user()->can('view-fatturazione') || Auth::user()->can('view-riga_fatturazione')))
10
+    @if($entity->fattura_id && (Auth::user()->can('view-fattura') || Auth::user()->can('view-riga_fatturazione')))
11
         <a href="{{ route('fattura.show', ['fattura_id' => $entity->fattura_id]) }}" class="dropdown-item">
11
         <a href="{{ route('fattura.show', ['fattura_id' => $entity->fattura_id]) }}" class="dropdown-item">
12
             <i class="bx bx-show me-1"></i> Vedi fattura
12
             <i class="bx bx-show me-1"></i> Vedi fattura
13
         </a>
13
         </a>
14
         @endif
14
         @endif
15
 
15
 
16
-        @if($entity->isModificabile() && (Auth::user()->can('edit-fatturazione') || Auth::user()->can('edit-riga_fatturazione')))
16
+        @if($entity->isModificabile() && (Auth::user()->can('edit-fattura') || Auth::user()->can('edit-riga_fatturazione')))
17
         <a href="#" class="dropdown-item editor_edit">
17
         <a href="#" class="dropdown-item editor_edit">
18
             <i class="bx bx-edit-alt me-1"></i> Modifica
18
             <i class="bx bx-edit-alt me-1"></i> Modifica
19
         </a>
19
         </a>
20
         @endif
20
         @endif
21
        
21
        
22
-        @if($entity->isModificabile() && (Auth::user()->can('delete-fatturazione') || Auth::user()->can('delete-riga_fatturazione')))
22
+        @if($entity->isModificabile() && (Auth::user()->can('delete-fattura') || Auth::user()->can('delete-riga_fatturazione')))
23
         <a href="#" class="dropdown-item editor_delete">
23
         <a href="#" class="dropdown-item editor_delete">
24
             <i class="bx bx-trash me-1"></i> Elimina
24
             <i class="bx bx-trash me-1"></i> Elimina
25
         </a>
25
         </a>

+ 2
- 2
resources/views/mappa_dispositivi/menu.blade.php 파일 보기

7
         <i class="bx bx-dots-vertical-rounded fs-large"></i>
7
         <i class="bx bx-dots-vertical-rounded fs-large"></i>
8
     </button>
8
     </button>
9
     <div class="dropdown-menu">
9
     <div class="dropdown-menu">
10
-        @if(Auth::user()->can('edit-prenotazione'))
10
+        @if(Auth::user()->can('edit-mappa_dispositivi'))
11
         <a href="#" class="dropdown-item editor_edit">
11
         <a href="#" class="dropdown-item editor_edit">
12
             <i class="bx bx-edit-alt me-1"></i> Modifica
12
             <i class="bx bx-edit-alt me-1"></i> Modifica
13
         </a>
13
         </a>
14
         @endif
14
         @endif
15
         
15
         
16
-        @if(Auth::user()->can('delete-prenotazione'))
16
+        @if(Auth::user()->can('delete-mappa_dispositivi'))
17
         <a href="#" class="dropdown-item editor_delete">
17
         <a href="#" class="dropdown-item editor_delete">
18
             <i class="bx bx-trash me-1"></i> Elimina
18
             <i class="bx bx-trash me-1"></i> Elimina
19
         </a>
19
         </a>

+ 1
- 1
resources/views/operatore/menu.blade.php 파일 보기

20
         </a>
20
         </a>
21
         @endif
21
         @endif
22
         
22
         
23
-        @if(Auth::user()->can('delete-monitor'))
23
+        @if(Auth::user()->can('delete-operatore'))
24
         <a href="#" class="dropdown-item editor_delete text-danger">
24
         <a href="#" class="dropdown-item editor_delete text-danger">
25
             <i class="bx bx-trash me-1"></i> Elimina
25
             <i class="bx bx-trash me-1"></i> Elimina
26
         </a>
26
         </a>

+ 42
- 38
resources/views/punto_vendita/cassa/_partials/menu_cucine.blade.php 파일 보기

129
             aria-labelledby="tab-cucina-{{ $cucina->id }}"
129
             aria-labelledby="tab-cucina-{{ $cucina->id }}"
130
             tabindex="0"
130
             tabindex="0"
131
           >
131
           >
132
-            <div class="gap-4 row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-xl-4 row-cols-xxl-5 g-2 cassa-piatti-grid">
132
+            <div class="cassa-piatti-grid">
133
               @php
133
               @php
134
-                $piatti = $cucina->piatti ?? collect();
135
-                $preferiti = $piatti
136
-                    ->filter(fn ($piatto) => (bool) ($piatto->in_evidenza ?? false))
137
-                    ->sortBy(fn ($piatto) => mb_strtolower((string) ($piatto->nome ?? '')), SORT_NATURAL);
138
-
139
-                $nonPreferiti = $piatti
140
-                    ->reject(fn ($piatto) => (bool) ($piatto->in_evidenza ?? false))
141
-                    ->sortBy(fn ($piatto) => mb_strtolower((string) ($piatto->nome ?? '')), SORT_NATURAL);
142
-
143
-                $gruppiAlfabetici = $nonPreferiti->groupBy(function ($piatto) {
144
-                    $nome = trim((string) ($piatto->nome ?? ''));
145
-                    if ($nome === '') {
146
-                        return '#';
147
-                    }
148
-
149
-                    $iniziale = mb_strtoupper(mb_substr($nome, 0, 1));
150
-                    return preg_match('/[A-Z]/u', $iniziale) ? $iniziale : '#';
151
-                });
134
+                $piattiOrdinati = \App\Models\Piatto::ordinaCollezioneInCassa($cucina->piatti ?? []);
152
               @endphp
135
               @endphp
153
 
136
 
154
-              @if($preferiti->isNotEmpty())
155
-                <div class="col-12 cassa-piatti-divider">
156
-                  <span class="cassa-piatti-divider-pill"><i class="bx bxs-star me-1"></i>Preferiti</span>
137
+              @if($piattiOrdinati->isEmpty())
138
+                <p class="text-muted mb-0">Nessun piatto attivo per questa cucina.</p>
139
+              @else
140
+                <div class="cassa-piatti-giornale">
141
+                  @foreach ($piattiOrdinati as $piatto)
142
+                    @include('punto_vendita.cassa._partials.piatto', ['piatto' => $piatto, 'cucina' => $cucina])
143
+                  @endforeach
157
                 </div>
144
                 </div>
158
-                @foreach ($preferiti as $piatto)
159
-                  @include('punto_vendita.cassa._partials.piatto', ['piatto' => $piatto, 'cucina' => $cucina])
160
-                @endforeach
161
               @endif
145
               @endif
162
-
163
-              @forelse ($gruppiAlfabetici as $lettera => $piattiGruppo)
164
-                <div class="col-12 cassa-piatti-divider">
165
-                  <span class="cassa-piatti-divider-pill">{{ $lettera }}</span>
166
-                </div>
167
-                @foreach ($piattiGruppo as $piatto)
168
-                  @include('punto_vendita.cassa._partials.piatto', ['piatto' => $piatto, 'cucina' => $cucina])
169
-                @endforeach
170
-              @empty
171
-                <div class="col-12">
172
-                  <p class="text-muted mb-0">Nessun piatto attivo per questa cucina.</p>
173
-                </div>
174
-              @endforelse
175
             </div>
146
             </div>
176
           </div>
147
           </div>
177
         @endforeach
148
         @endforeach
302
     padding-bottom: 0.35rem;
273
     padding-bottom: 0.35rem;
303
     scrollbar-width: thin;
274
     scrollbar-width: thin;
304
   }
275
   }
276
+  .cassa-piatti-giornale {
277
+    column-count: 2;
278
+    column-gap: 0.5rem;
279
+    column-fill: balance;
280
+  }
281
+  .cassa-piatti-giornale > .col {
282
+    display: inline-block;
283
+    width: 100%;
284
+    max-width: 100%;
285
+    margin-bottom: 0.5rem;
286
+    break-inside: avoid;
287
+    -webkit-column-break-inside: avoid;
288
+    page-break-inside: avoid;
289
+    vertical-align: top;
290
+  }
291
+  .cassa-piatti-giornale .cassa-piatto-card.h-100 {
292
+    height: auto !important;
293
+  }
294
+  @media (min-width: 576px) {
295
+    .cassa-piatti-giornale { column-count: 3; }
296
+  }
297
+  @media (min-width: 768px) {
298
+    .cassa-piatti-giornale { column-count: 4; }
299
+  }
300
+  @media (min-width: 1400px) {
301
+    .cassa-piatti-giornale { column-count: 5; }
302
+  }
305
   @media (max-width: 1199.98px) {
303
   @media (max-width: 1199.98px) {
306
     .cassa-cucine-card {
304
     .cassa-cucine-card {
307
       height: 100% !important;
305
       height: 100% !important;
321
     font-size: 0.95rem;
319
     font-size: 0.95rem;
322
     line-height: 1;
320
     line-height: 1;
323
   }
321
   }
322
+  .cassa-piatti-gruppo-gap {
323
+    flex: 0 0 100% !important;
324
+    width: 100% !important;
325
+    max-width: 100% !important;
326
+    height: 0.85rem;
327
+  }
324
   .cassa-piatti-divider {
328
   .cassa-piatti-divider {
325
     position: static;
329
     position: static;
326
     flex: 0 0 100% !important;
330
     flex: 0 0 100% !important;

+ 0
- 34
resources/views/punto_vendita/cassa/viste/catalogo/_partials/style.blade.php 파일 보기

451
     z-index: 40;
451
     z-index: 40;
452
   }
452
   }
453
 
453
 
454
-  .cassa-sb-letter-divider {
455
-    align-self: center;
456
-    min-height: 0;
457
-    height: auto;
458
-    margin-top: .62rem;
459
-    margin-bottom: -.38rem;
460
-    padding: 0 .14rem 0;
461
-    pointer-events: none;
462
-  }
463
-
464
-  .cassa-sb-products > .cassa-sb-letter-divider:first-child {
465
-    margin-top: 0;
466
-  }
467
-
468
-  .cassa-sb-letter-divider-label {
469
-    display: flex;
470
-    align-items: center;
471
-    justify-content: flex-start;
472
-    gap: .4rem;
473
-    width: 100%;
474
-    font-size: .9rem;
475
-    font-weight: 800;
476
-    letter-spacing: .04em;
477
-    text-transform: uppercase;
478
-    color: var(--sb-primary);
479
-    line-height: 1;
480
-  }
481
-
482
-  .cassa-sb-letter-divider-label::after {
483
-    content: "";
484
-    flex: 1 1 auto;
485
-    border-top: 1px solid color-mix(in srgb, var(--sb-primary) 24%, var(--sb-border) 76%);
486
-  }
487
-
488
   .cassa-sb-product:hover {
454
   .cassa-sb-product:hover {
489
     border-color: color-mix(in srgb, var(--sb-primary) 55%, var(--sb-product-border) 45%);
455
     border-color: color-mix(in srgb, var(--sb-primary) 55%, var(--sb-product-border) 45%);
490
     background: var(--sb-product-bg-hover);
456
     background: var(--sb-product-bg-hover);

+ 24
- 53
resources/views/punto_vendita/cassa/viste/catalogo/index.blade.php 파일 보기

224
         <div class="cassa-sb-products-wrap" id="product-panel">
224
         <div class="cassa-sb-products-wrap" id="product-panel">
225
           <div class="cassa-sb-products" id="product-grid">
225
           <div class="cassa-sb-products" id="product-grid">
226
             @foreach($cucine as $cucina)
226
             @foreach($cucine as $cucina)
227
-              @foreach(($cucina->piatti ?? collect()) as $piatto)
227
+              @foreach(\App\Models\Piatto::ordinaCollezioneInCassa($cucina->piatti ?? []) as $piatto)
228
                 
228
                 
229
                 <article
229
                 <article
230
                   class="cassa-sb-product"
230
                   class="cassa-sb-product"
231
                   data-product-item
231
                   data-product-item
232
                   data-kitchen-id="{{ $cucina->id }}"
232
                   data-kitchen-id="{{ $cucina->id }}"
233
                   data-name="{{ mb_strtolower((string) ($piatto->nome ?? ''), 'UTF-8') }}"
233
                   data-name="{{ mb_strtolower((string) ($piatto->nome ?? ''), 'UTF-8') }}"
234
+                  data-ordine-visualizzazione="{{ \App\Models\Piatto::ordineVisualizzazioneImpostato($piatto->ordine_visualizzazione) ? (int) $piatto->ordine_visualizzazione : 0 }}"
234
                   data-piatto-id="{{ $piatto->id }}"
235
                   data-piatto-id="{{ $piatto->id }}"
235
                   data-title="{{ $piatto->nome }}"
236
                   data-title="{{ $piatto->nome }}"
236
                   data-price="{{ (float) ($piatto->prezzo ?? 0) }}"
237
                   data-price="{{ (float) ($piatto->prezzo ?? 0) }}"
296
       <div class="cassa-sb-footer">
297
       <div class="cassa-sb-footer">
297
         <div class="cassa-sb-pay-grid">
298
         <div class="cassa-sb-pay-grid">
298
           <button type="button" class="cassa-sb-main-btn" onclick="checkout()">Paga ora</button>
299
           <button type="button" class="cassa-sb-main-btn" onclick="checkout()">Paga ora</button>
300
+          {{--
299
           <button type="button" class="cassa-sb-cash-btn">Contanti</button>
301
           <button type="button" class="cassa-sb-cash-btn">Contanti</button>
300
           <button type="button" class="cassa-sb-card-btn">Carta</button>
302
           <button type="button" class="cassa-sb-card-btn">Carta</button>
303
+          --}}
301
           <button type="button" class="cassa-sb-clear-btn" id="cart-reset" onclick="azzeraCarrello()">Svuota</button>
304
           <button type="button" class="cassa-sb-clear-btn" id="cart-reset" onclick="azzeraCarrello()">Svuota</button>
302
         </div>
305
         </div>
303
       </div>
306
       </div>
451
       });
454
       });
452
     };
455
     };
453
 
456
 
454
-    const getProductInitialLetter = function (name) {
455
-      const trimmed = (name || '').trim();
456
-      if (trimmed === '') return '#';
457
-      const initial = [...trimmed][0]?.toLocaleUpperCase('it') || '#';
458
-      return /^[A-Z]$/.test(initial.normalize('NFD').replace(/\p{M}/gu, '')) ? initial : '#';
457
+    const prodottoHaOrdine = function (item) {
458
+      const n = Number.parseInt(item.dataset.ordineVisualizzazione || '0', 10);
459
+      return Number.isFinite(n) && n !== 0;
459
     };
460
     };
460
 
461
 
461
-    const createLetterDivider = function (letter) {
462
-      const divider = document.createElement('div');
463
-      divider.className = 'cassa-sb-letter-divider js-letter-divider';
464
-      divider.setAttribute('data-letter', letter);
465
-      const label = document.createElement('span');
466
-      label.className = 'cassa-sb-letter-divider-label';
467
-      label.textContent = letter;
468
-      divider.appendChild(label);
469
-      return divider;
470
-    };
471
-
472
-    const clearLetterDividers = function () {
473
-      productGrid.querySelectorAll('.js-letter-divider').forEach(function (node) {
474
-        node.remove();
475
-      });
476
-    };
477
-
478
-    const LETTER_DIVIDER_MIN_ITEMS = 10;
479
-
480
-    const buildProductGridSequence = function (items, totalVisibleCount) {
481
-      const sequence = [];
482
-      const showLetterDividers = totalVisibleCount > LETTER_DIVIDER_MIN_ITEMS;
483
-      let lastLetter = null;
484
-
485
-      items.forEach(function (item) {
486
-        const letter = getProductInitialLetter(item.dataset.title || item.dataset.name || '');
487
-        if (showLetterDividers && letter !== lastLetter) {
488
-          sequence.push(createLetterDivider(letter));
489
-          lastLetter = letter;
490
-        }
491
-        if (!showLetterDividers) {
492
-          lastLetter = letter;
493
-        }
494
-        sequence.push(item);
495
-      });
496
-
497
-      return sequence;
462
+    const sortProductsForCassa = function (items) {
463
+      if (items.length > 0 && items.every(prodottoHaOrdine)) {
464
+        return items.slice().sort(function (a, b) {
465
+          const oa = Number.parseInt(a.dataset.ordineVisualizzazione, 10);
466
+          const ob = Number.parseInt(b.dataset.ordineVisualizzazione, 10);
467
+          if (oa !== ob) return oa - ob;
468
+          const nameA = (a.dataset.title || a.dataset.name || '').trim();
469
+          const nameB = (b.dataset.title || b.dataset.name || '').trim();
470
+          return nameA.localeCompare(nameB, 'it', { sensitivity: 'base' });
471
+        });
472
+      }
473
+      return sortProductsAlphabetically(items);
498
     };
474
     };
499
 
475
 
500
     const renderProducts = function () {
476
     const renderProducts = function () {
524
       }
500
       }
525
 
501
 
526
       const term = (productSearch?.value || '').trim().toLowerCase();
502
       const term = (productSearch?.value || '').trim().toLowerCase();
527
-      visibleProducts = sortProductsAlphabetically(productItems.filter(function (item) {
503
+      visibleProducts = sortProductsForCassa(productItems.filter(function (item) {
528
         const okKitchen = selectedKitchen === 'all' || item.dataset.kitchenId === selectedKitchen;
504
         const okKitchen = selectedKitchen === 'all' || item.dataset.kitchenId === selectedKitchen;
529
         const okSearch = term === '' || (item.dataset.name || '').includes(term);
505
         const okSearch = term === '' || (item.dataset.name || '').includes(term);
530
         return okKitchen && okSearch;
506
         return okKitchen && okSearch;
537
 
513
 
538
       productItems.forEach(function (item) { item.style.display = 'none'; });
514
       productItems.forEach(function (item) { item.style.display = 'none'; });
539
       const current = visibleProducts.slice(start, end);
515
       const current = visibleProducts.slice(start, end);
540
-      clearLetterDividers();
541
-
542
-      const gridSequence = buildProductGridSequence(current, visibleProducts.length);
543
-      gridSequence.forEach(function (node) {
544
-        if (node.classList.contains('cassa-sb-product')) {
545
-          node.style.display = '';
546
-        }
547
-        productGrid.appendChild(node);
516
+      current.forEach(function (item) {
517
+        item.style.display = '';
518
+        productGrid.appendChild(item);
548
       });
519
       });
549
 
520
 
550
       visibleProducts.forEach(function (item) {
521
       visibleProducts.forEach(function (item) {
560
       });
531
       });
561
 
532
 
562
       productGrid.querySelectorAll('.js-product-placeholder').forEach(function (node) { node.remove(); });
533
       productGrid.querySelectorAll('.js-product-placeholder').forEach(function (node) { node.remove(); });
563
-      applyProductGridLayout(gridSequence.length);
534
+      applyProductGridLayout(current.length);
564
 
535
 
565
       productStatus.textContent = productPage + ' / ' + maxPage;
536
       productStatus.textContent = productPage + ' / ' + maxPage;
566
       productPrev.disabled = productPage <= 1;
537
       productPrev.disabled = productPage <= 1;

+ 1
- 1
resources/views/report/index.blade.php 파일 보기

585
     var resetRangeButton = document.getElementById('btn-reset-range');
585
     var resetRangeButton = document.getElementById('btn-reset-range');
586
     var downloadPdfButton = document.getElementById('btn-download-report-pdf');
586
     var downloadPdfButton = document.getElementById('btn-download-report-pdf');
587
     var exportRoute = @json(route('report.export-pdf'));
587
     var exportRoute = @json(route('report.export-pdf'));
588
-    var tabRouteTemplate = @json(url('report/tab/__TAB__'));
588
+    var tabRouteTemplate = @json(route('report.tab', ['tab' => '__TAB__']));
589
     var reportTypeByPanel = {
589
     var reportTypeByPanel = {
590
       'report-movimenti-panel': 'movimenti',
590
       'report-movimenti-panel': 'movimenti',
591
       'report-incassi-panel': 'incassi',
591
       'report-incassi-panel': 'incassi',

+ 3
- 3
routes/web.php 파일 보기

414
   Route::post('avvisami-quando-il-piatto-e-pronto', [RigaOrdineNotificaController::class, 'salva_fcm_token'])->name('riga-ordine-notifica.salva-fcm-token');
414
   Route::post('avvisami-quando-il-piatto-e-pronto', [RigaOrdineNotificaController::class, 'salva_fcm_token'])->name('riga-ordine-notifica.salva-fcm-token');
415
   
415
   
416
   // Saltacoda
416
   // Saltacoda
417
-  Route::get('saltacoda/show/{slug}', [SaltacodaController::class, 'show'])->middleware('throttle:5,1')->name('cliente.saltacoda.show');
418
-  Route::post('saltacoda/pre-ordine/salva', [SaltacodaController::class, 'pre_ordine_salva'])->middleware('throttle:5,1')->name('saltacoda.pre-ordine.salva');
419
-  Route::get('saltacoda/pre-ordine/show', [SaltacodaController::class, 'pre_ordine_show'])->middleware('throttle:5,1')->name('saltacoda.pre-ordine.show');
417
+  Route::get('saltacoda/show/{slug}', [SaltacodaController::class, 'show'])->middleware('throttle:30,1,saltacoda-show')->name('cliente.saltacoda.show');
418
+  Route::post('saltacoda/pre-ordine/salva', [SaltacodaController::class, 'pre_ordine_salva'])->middleware('throttle:10,1,saltacoda-salva')->name('saltacoda.pre-ordine.salva');
419
+  Route::get('saltacoda/pre-ordine/show', [SaltacodaController::class, 'pre_ordine_show'])->middleware('throttle:20,1,saltacoda-ricevuta')->name('saltacoda.pre-ordine.show');
420
 
420
 
421
   // Tombola (pubblico, per QR)
421
   // Tombola (pubblico, per QR)
422
   Route::get('tombola/{attivita_id}/estratti', [TombolaController::class, 'estratti'])->middleware('throttle:2,1')->name('cliente.tombola.estratti');
422
   Route::get('tombola/{attivita_id}/estratti', [TombolaController::class, 'estratti'])->middleware('throttle:2,1')->name('cliente.tombola.estratti');

Loading…
취소
저장