Pārlūkot izejas kodu

fix binding token dispositivo, Fix contabilità e creazione del servizio con nuove viste,Nuova Mappa dispositivi, fix filtri prima nota

marcofalabretti 2 stundas atpakaļ
vecāks
revīzija
d3e5389915
45 mainītis faili ar 3370 papildinājumiem un 1117 dzēšanām
  1. 5
    0
      app/DataTables/AttivitaDataTableEditor.php
  2. 124
    0
      app/DataTables/ChiusuraServizioDataTable.php
  3. 88
    0
      app/DataTables/ChiusuraServizioDataTableEditor.php
  4. 10
    1
      app/DataTables/MetodoPagamentoDataTable.php
  5. 15
    6
      app/DataTables/PrimaNotaDataTable.php
  6. 6
    5
      app/DataTables/PuntoVenditaDataTable.php
  7. 20
    136
      app/Http/Controllers/BilancioController.php
  8. 13
    0
      app/Http/Controllers/CarrelloController.php
  9. 123
    0
      app/Http/Controllers/ChiusuraServizioController.php
  10. 62
    9
      app/Http/Controllers/MappaDispositiviController.php
  11. 42
    17
      app/Http/Controllers/PrimaNotaController.php
  12. 103
    22
      app/Http/Controllers/PuntoVenditaController.php
  13. 73
    0
      app/Models/AbstractModels/AbstractChiusuraServizio.php
  14. 2
    0
      app/Models/AbstractModels/AbstractDispositivo.php
  15. 2
    0
      app/Models/AbstractModels/AbstractMetodoPagamento.php
  16. 7
    1
      app/Models/Categoriacontabile.php
  17. 10
    0
      app/Models/ChiusuraServizio.php
  18. 23
    8
      app/Models/Dispositivo.php
  19. 51
    44
      app/Models/MetodoPagamento.php
  20. 1
    0
      app/Models/Pagamento.php
  21. 62
    7
      app/Models/PrimaNota.php
  22. 17
    1
      app/Observers/PagamentoObserver.php
  23. 276
    0
      app/Services/Bilancio/BilancioServizioService.php
  24. 26
    0
      app/Services/Paga/Staff.php
  25. 28
    0
      database/migrations/2026_06_18_153607_campo_occupato_in_dispositivo.php
  26. 28
    0
      database/migrations/2026_06_18_164218_contabilizzare_metodo_di_pagamento.php
  27. 34
    0
      database/migrations/2026_06_19_005802_chiusura_servizio.php
  28. 4
    0
      database/seeders/CategoriaContabileSeed.php
  29. 26
    3
      resources/menu/verticalMenu.json
  30. 9
    1
      resources/views/_partials/badge.blade.php
  31. 371
    140
      resources/views/bilancio/oggi.blade.php
  32. 148
    0
      resources/views/chiusura_servizio/index.blade.php
  33. 29
    0
      resources/views/chiusura_servizio/menu.blade.php
  34. 663
    0
      resources/views/chiusura_servizio/show.blade.php
  35. 2
    1
      resources/views/layouts/sections/navbar/navbar-partial.blade.php
  36. 74
    0
      resources/views/mappa_dispositivi/_partials/device_card.blade.php
  37. 192
    626
      resources/views/mappa_dispositivi/index.blade.php
  38. 2
    1
      resources/views/pagamento/menu.blade.php
  39. 10
    11
      resources/views/prima_nota/_partials/statistiche.blade.php
  40. 57
    15
      resources/views/prima_nota/index.blade.php
  41. 38
    18
      resources/views/punto_operatore/show.blade.php
  42. 1
    1
      resources/views/punto_vendita/cassa/index.blade.php
  43. 478
    38
      resources/views/punto_vendita/edit.blade.php
  44. 6
    5
      resources/views/punto_vendita/menu.blade.php
  45. 9
    0
      routes/web.php

+ 5
- 0
app/DataTables/AttivitaDataTableEditor.php Parādīt failu

@@ -117,6 +117,11 @@ class AttivitaDataTableEditor extends DataTablesEditor
117 117
       'descrizione' => 'Spese per utenze (Luce, Acqua, Gas, Telefono)',
118 118
       'colore' => '#dc3545',
119 119
     ]);
120
+    $model->categorie_contabili()->create([
121
+      'nome' => 'Staff',
122
+      'descrizione' => 'Spese per staff',
123
+      'colore' => '#dc3545',
124
+    ]);
120 125
 
121 126
     return $model;
122 127
   }

+ 124
- 0
app/DataTables/ChiusuraServizioDataTable.php Parādīt failu

@@ -0,0 +1,124 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\ChiusuraServizio;
6
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
7
+use Yajra\DataTables\EloquentDataTable;
8
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
9
+use Yajra\DataTables\Html\Button;
10
+use Yajra\DataTables\Html\Column;
11
+use Yajra\DataTables\Html\Editor\Editor;
12
+use Yajra\DataTables\Html\Editor\Fields;
13
+use Yajra\DataTables\Services\DataTable;
14
+use Illuminate\Support\Facades\Auth;
15
+
16
+class ChiusuraServizioDataTable extends DataTable
17
+{
18
+    public function __construct(){
19
+        $this->dataTableVariable = 'dataTable_chiusura_servizio';
20
+    }
21
+    /**
22
+     * Build the DataTable class.
23
+     *
24
+     * @param QueryBuilder<ChiusuraServizio> $query Results from query() method.
25
+     */
26
+    public function dataTable(QueryBuilder $query): EloquentDataTable
27
+    {
28
+        return (new EloquentDataTable($query))
29
+            ->addColumn('action', function($query){
30
+                return view('chiusura_servizio.menu', ['entity' => $query]);
31
+            })
32
+            ->addColumn('user_id_display', function($query){
33
+                if($query->user->id == Auth::user()->id){
34
+                    return view('_partials.badge', ['success' => 'Tu']);
35
+                }else{
36
+                    return view('_partials.badge', ['info' => $query->user->name]);
37
+                }
38
+            })
39
+            ->addColumn('info_display', function($query){
40
+                if (empty($query->info)) {
41
+                    return '—';
42
+                }
43
+                $info = is_array($query->info) ? $query->info : json_decode($query->info ?? '[]', true);
44
+                $incasso = (float) ($info['incasso'] ?? 0);
45
+                return '€ ' . number_format($incasso, 2, ',', '.');
46
+            })
47
+            ->setRowId('id');
48
+    }
49
+
50
+    /**
51
+     * Get the query source of dataTable.
52
+     *
53
+     * @return QueryBuilder<ChiusuraServizio>
54
+     */
55
+    public function query(ChiusuraServizio $model): QueryBuilder
56
+    {
57
+        return $model->newQuery()->where('attivita_id', $this->attivita_id)->orderBy('chiusura_at', 'desc');
58
+    }
59
+
60
+    /**
61
+     * Optional method if you want to use the html builder.
62
+     */
63
+    public function html(): HtmlBuilder
64
+    {
65
+        $buttons = [];            
66
+        if(Auth::user()->can('create-chiusura-servizio')){
67
+            array_push($buttons, Button::make('create')
68
+                ->editor('editor')
69
+                ->className('btn btn-sm btn-primary mb-4')
70
+                ->formTitle('Crea nuova chiusura di servizio')
71
+                ->text('<i class="fas fa-plus"></i> Nuova chiusura di servizio'));
72
+        }
73
+        return $this->builder()
74
+                    ->setTableId($this->dataTableVariable)
75
+                    ->columns($this->getColumns())
76
+                    ->minifiedAjax()
77
+                    ->orderBy(1)
78
+                    ->selectStyleSingle()
79
+                    ->language(asset('assets/Italian.json'))
80
+                    ->dom(count($buttons) == 0 ? 'rtip' : 'Bfrtip')
81
+                    ->responsive(true)
82
+                    ->buttons($buttons)
83
+                    ->initComplete("function(settings, json){
84
+                        initComplete_chiusura_servizio();
85
+                    }")
86
+                    ->editor(
87
+                        Editor::make()
88
+                            ->fields([
89
+                                Fields\Hidden::make('attivita_id')->label('Attività')->default($this->attivita_id),
90
+                                Fields\Hidden::make('user_id')->label('Utente')->default(Auth::user()->id),
91
+                                Fields\DateTime::make('chiusura_at')->label('Data e ora della chiusura')->default(now()),
92
+                                Fields\Text::make('note')->label('Note'),
93
+                            ])
94
+                    );
95
+    }
96
+
97
+    /**
98
+     * Get the dataTable columns definition.
99
+     */
100
+    public function getColumns(): array
101
+    {
102
+        return [
103
+            Column::make('chiusura_at')->title('Data e ora della chiusura')->addClass('text-center')->responsivePriority(1),
104
+            Column::make('note')->title('Note')->responsivePriority(2),
105
+            Column::make('user_id')->title('Utente')->data('user_id_display')->addClass('text-center')->responsivePriority(1),
106
+            Column::make('info')->title('Snapshot')->data('info_display')->addClass('text-center')->responsivePriority(1),
107
+            Column::computed('action')
108
+                  ->addClass('text-center')
109
+                  ->title('')
110
+                  ->exportable(false)
111
+                  ->printable(false)
112
+                  ->width('10%')
113
+                  ->responsivePriority(1),
114
+        ];
115
+    }
116
+
117
+    /**
118
+     * Get the filename for export.
119
+     */
120
+    protected function filename(): string
121
+    {
122
+        return 'ChiusuraServizio_' . date('YmdHis');
123
+    }
124
+}

+ 88
- 0
app/DataTables/ChiusuraServizioDataTableEditor.php Parādīt failu

@@ -0,0 +1,88 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\ChiusuraServizio;
6
+use Illuminate\Database\Eloquent\Model;
7
+use Illuminate\Validation\Rule;
8
+use Yajra\DataTables\DataTablesEditor;
9
+use Illuminate\Http\Request;
10
+use Illuminate\Validation\ValidationException;
11
+
12
+
13
+class ChiusuraServizioDataTableEditor extends DataTablesEditor
14
+{
15
+  protected $model = ChiusuraServizio::class;
16
+
17
+  protected $messages = [
18
+    'attivita_id.required' => 'La attività è richiesta',
19
+    'chiusura_at.required' => 'La data e ora della chiusura sono richieste',
20
+    'note.required' => 'La note è richiesta',
21
+    'user_id.required' => 'L\'utente è richiesto',
22
+  ];
23
+
24
+  /**
25
+  * Get create action validation rules.
26
+  *
27
+  * @return array
28
+  */
29
+  public function createRules(): array
30
+  {
31
+    return [
32
+      'attivita_id'  => 'required',
33
+      'chiusura_at' => 'required|date',
34
+      'user_id' => 'required',
35
+      'note' => 'nullable',
36
+    ];
37
+  }
38
+
39
+  public function createMessages(): array{
40
+    return $this->messages;
41
+  }
42
+
43
+  /**
44
+  * Get edit action validation rules.
45
+  *
46
+  * @param Model $model
47
+  * @return array
48
+  */
49
+  public function editRules(Model $model): array
50
+  {
51
+    return [
52
+      'attivita_id'  => 'required',
53
+      'chiusura_at' => 'required|date',
54
+      'note' => 'nullable',
55
+      'info' => 'nullable',
56
+    ];
57
+  }
58
+
59
+  public function editMessages(): array{
60
+    return $this->messages;
61
+  }
62
+
63
+  /**
64
+  * Get remove action validation rules.
65
+  *
66
+  * @param Model $model
67
+  * @return array
68
+  */
69
+  public function removeRules(Model $model): array
70
+  {
71
+    return [];
72
+  }
73
+
74
+  public function creating(Model $model, array $data): array
75
+  {
76
+    return $data;
77
+  }
78
+
79
+  public function updating(Model $model, array $data): array
80
+  {
81
+    return $data;
82
+  }
83
+  public function messages(): array
84
+  {
85
+    return $this->messages;
86
+  }
87
+
88
+}

+ 10
- 1
app/DataTables/MetodoPagamentoDataTable.php Parādīt failu

@@ -38,6 +38,13 @@ class MetodoPagamentoDataTable extends DataTable
38 38
             ->addColumn('is_pubblico_display', function($entity){
39 39
                 return view('_partials.available', ['available' => $entity->is_pubblico]);
40 40
             })
41
+            ->addColumn('escludi_contabilita_display', function($entity){
42
+                if($entity->escludi_contabilita){
43
+                    return view('_partials.badge', ['error' => 'Si']);
44
+                }else{
45
+                    return view('_partials.badge', ['success' => 'No']);
46
+                }
47
+            })
41 48
             ->setRowId('id');
42 49
     }
43 50
 
@@ -82,10 +89,11 @@ class MetodoPagamentoDataTable extends DataTable
82 89
                         Editor::make()
83 90
                             ->fields([
84 91
                                 Fields\Hidden::make('attivita_id')->label('Attività')->default($this->attivita_id),
92
+                                Fields\Boolean::make('is_attivo')->label('Attivo')->default(true),
85 93
                                 Fields\Select2::make('tipo')->label('Tipo')->options(MetodoPagamento::getTipiPagamento()->sortBy('label')->pluck('value' ,'label')),
86 94
                                 Fields\Text::make('nome')->label('Nome'),
87 95
                                 Fields\Text::make('descrizione')->label('Descrizione'),
88
-                                Fields\Boolean::make('is_attivo')->label('Attivo')->default(true),
96
+                                Fields\Boolean::make('escludi_contabilita')->label('Escludi dalla contabilità')->default(false),
89 97
                                 Fields\Boolean::make('is_pubblico')->label('Pubblico')->default(false),
90 98
                                 Fields\Text::make('key')->label('Key'),
91 99
                                 Fields\Text::make('secret')->label('Secret'),
@@ -108,6 +116,7 @@ class MetodoPagamentoDataTable extends DataTable
108 116
             Column::make('is_attivo')->data('is_attivo_display')->title('Attivo')->addClass('text-center w-10')->responsivePriority(1)->width('10%'),
109 117
             Column::make('is_pubblico')->data('is_pubblico_display')->title('Pubblico')->addClass('text-center w-10')->responsivePriority(2)->width('10%'),
110 118
             Column::make('nome')->title('Nome')->responsivePriority(1),
119
+            Column::make('escludi_contabilita')->data('escludi_contabilita_display')->title('Escludi dalla contabilità')->addClass('text-center')->responsivePriority(3),
111 120
             Column::make('descrizione')->title('Descrizione')->responsivePriority(2),
112 121
             Column::computed('action')->title('')->width('10%')->addClass('text-center')->responsivePriority(1),   
113 122
         ];

+ 15
- 6
app/DataTables/PrimaNotaDataTable.php Parādīt failu

@@ -104,8 +104,9 @@ class PrimaNotaDataTable extends DataTable
104 104
                 return Carbon::parse($entity->created_at)->format('d/m/Y H:i');
105 105
             })
106 106
             ->addColumn('categoria_contabile_id_display', function($entity){
107
-              return '<span class="badge" style="background-color: '.$entity->categoria_contabile?->colore.';">'.$entity->categoria_contabile?->nome.'</span>';
108
-            })
107
+            //   return '<span class="badge" style="background-color: '.$entity->categoria_contabile?->colore.';">'.$entity->categoria_contabile?->nome.'</span>';
108
+            return view('_partials.badge', ['entity' => $entity]);
109
+        })
109 110
             ->rawColumns(['categoria_contabile_id_display'])
110 111
             ->setRowId('id');
111 112
     }
@@ -117,11 +118,14 @@ class PrimaNotaDataTable extends DataTable
117 118
      */
118 119
     public function query(PrimaNota $model): QueryBuilder
119 120
     {
120
-        if($this->attivita_id){
121
-            return $model->newQuery()->where('attivita_id', $this->attivita_id)->orderBy('created_at', 'desc');
122
-        }else{
121
+        if (!$this->attivita_id) {
123 122
             return $model->newQuery()->whereRaw('1 = 0');
124 123
         }
124
+
125
+        return $model->newQuery()
126
+            ->where('attivita_id', $this->attivita_id)
127
+            ->conFiltriIndice()
128
+            ->orderBy('created_at', 'desc');
125 129
     }
126 130
 
127 131
     /**
@@ -171,7 +175,12 @@ class PrimaNotaDataTable extends DataTable
171 175
         return $this->builder()
172 176
                     ->setTableId($this->dataTableVariable)
173 177
                     ->columns($this->getColumns())
174
-                    ->minifiedAjax()
178
+                    ->minifiedAjax('', '
179
+                        data.data_inizio = $("#filtro_data_inizio").val();
180
+                        data.data_fine = $("#filtro_data_fine").val();
181
+                        data.categoria_contabile_id = $("#categoria_contabile_id").val();
182
+                        data.tipo_movimento = $("#tipo_movimento").val();
183
+                    ')
175 184
                     ->language(asset('assets/Italian.json'))
176 185
                     // ->dom('<"py-2"<"head-label text-center"><"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
177 186
                     ->dom(count($buttons) == 0 ? 'Prtip' : 'PBfrtip')

+ 6
- 5
app/DataTables/PuntoVenditaDataTable.php Parādīt failu

@@ -45,22 +45,23 @@ class PuntoVenditaDataTable extends DataTable
45 45
             })
46 46
             ->addColumn('abbinato_display', function($entity){
47 47
                 $token = $entity->binding_token;
48
-                $tu = $entity->binding_token && 
49
-                request()->cookie('binding_token') && 
50
-                \Illuminate\Support\Facades\Hash::check($entity->binding_token, request()->cookie('binding_token'));
48
+                $tu = ($entity->binding_token === request()->cookie('binding_token'))&& request()->cookie('binding_token') != null;
51 49
 
52 50
                 switch(true){
53 51
                 
54
-                    case (isset($token) && $token != null && $tu == false):
52
+                    case (isset($token) && $token != null && $tu == false && $entity->occupato == true):
55 53
                         return view('_partials.badge', ['success' => 'Abbinato']);
56 54
                         break;
57 55
                     case ($tu == true):
58 56
                         return view('_partials.badge', ['success' => 'Tu']);
59 57
                         break;
60 58
 
61
-                    case ($token == null):
59
+                    case ($token == null || $entity->occupato == false):
62 60
                         return view('_partials.badge', ['error' => 'Non abbinato']);
63 61
                         break;
62
+                    default:
63
+                        return view('_partials.badge', ['error' => 'Stato sconosciuto']);
64
+                        break;
64 65
                 }
65 66
                 // $txt = '<span class="badge bg-label-success">*</span>';
66 67
                 // $userToken = request()->cookie('binding_token');

+ 20
- 136
app/Http/Controllers/BilancioController.php Parādīt failu

@@ -6,7 +6,9 @@ use App\Models\Attivita;
6 6
 use App\Models\PrimaNota;
7 7
 use App\Models\Pagamento;
8 8
 use App\Models\RigaOrdine;
9
+use App\Services\Bilancio\BilancioServizioService;
9 10
 use Illuminate\Http\Request;
11
+use Illuminate\Routing\Controllers\Middleware;
10 12
 use Carbon\Carbon;
11 13
 use Carbon\CarbonPeriod;
12 14
 
@@ -30,148 +32,30 @@ class BilancioController extends Controller
30 32
         ];
31 33
     }
32 34
 
33
-    public function oggi(Request $request)
35
+    public function oggi(Request $request, BilancioServizioService $bilancioServizio)
34 36
     {
35
-        $attivitaId = session()->get('attivita_attuale');
36
-        $dayStart = now()->startOfDay();
37
-        $dayEnd = now()->endOfDay();
38
-
37
+        $attivitaId = (int) session()->get('attivita_attuale');
39 38
         $attivita = Attivita::find($attivitaId);
39
+        $period = $bilancioServizio->currentPeriod($attivitaId);
40
+        $periodStart = $period['start'];
41
+        $periodEnd = $period['end'];
42
+        $ultimaChiusuraAt = $period['ultimaChiusuraAt'];
40 43
 
41
-        $righeOrdiniOggi = RigaOrdine::query()
42
-            ->with(['piatto.cucina', 'ordine'])
43
-            ->whereHas('ordine', function ($query) use ($attivitaId, $dayStart, $dayEnd) {
44
-                $query
45
-                    ->where('attivita_id', $attivitaId)
46
-                    ->whereBetween('created_at', [$dayStart, $dayEnd]);
47
-            })
48
-            ->get();
49
-
50
-        $righeIncassiOggi = RigaOrdine::query()
51
-            ->with(['piatto.cucina', 'ordine'])
52
-            ->whereHas('ordine.pagamenti', function ($query) use ($attivitaId, $dayStart, $dayEnd) {
53
-                $query
54
-                    ->where('attivita_id', $attivitaId)
55
-                    ->whereBetween('created_at', [$dayStart, $dayEnd]);
56
-            })
57
-            ->get();
58
-
59
-        $righeOggi = $righeOrdiniOggi
60
-            ->concat($righeIncassiOggi)
61
-            ->unique('id')
62
-            ->values();
63
-
64
-        $cucineRows = $righeOggi
65
-            ->groupBy(function ($riga) {
66
-                return $riga->piatto?->cucina_id ?: 0;
67
-            })
68
-            ->map(function ($items, $cucinaId) {
69
-                $first = $items->first();
70
-                $cucinaNome = $first?->piatto?->cucina?->nome ?? ('Cucina #' . $cucinaId);
71
-
72
-                $piatti = $items
73
-                    ->groupBy('piatto_id')
74
-                    ->map(function ($piattoItems) {
75
-                        $firstPiatto = $piattoItems->first();
76
-                        $quantita = (int) $piattoItems->sum(function ($r) {
77
-                            return (int) ($r->quantita ?? 0);
78
-                        });
79
-                        $totale = (float) $piattoItems->sum(function ($r) {
80
-                            return ((float) ($r->quantita ?? 0)) * ((float) ($r->prezzo ?? 0));
81
-                        });
82
-
83
-                        return [
84
-                            'nome' => $firstPiatto?->piatto?->nome ?? 'Piatto',
85
-                            'quantita' => $quantita,
86
-                            'totale' => $totale,
87
-                        ];
88
-                    })
89
-                    ->sortByDesc('quantita')
90
-                    ->values();
91
-
92
-                $totQuantita = (int) $piatti->sum('quantita');
93
-                $totImporto = (float) $piatti->sum('totale');
94
-
95
-                return [
96
-                    'id' => $cucinaId,
97
-                    'nome' => $cucinaNome,
98
-                    'piatti' => $piatti,
99
-                    'tot_quantita' => $totQuantita,
100
-                    'tot_importo' => $totImporto,
101
-                ];
102
-            })
103
-            ->sortByDesc('tot_quantita')
104
-            ->values();
105
-
106
-        $pagamentiOggi = Pagamento::query()
107
-            ->with(['metodo_pagamento', 'ordine.righe_ordine.piatto.cucina'])
108
-            ->where('attivita_id', $attivitaId)
109
-            ->whereBetween('created_at', [$dayStart, $dayEnd])
110
-            ->get();
111
-
112
-        $paymentSummary = $pagamentiOggi
113
-            ->groupBy(function ($pagamento) {
114
-                return $pagamento->metodo_pagamento?->nome
115
-                    ?? $pagamento->metodo_pagamento?->tipo
116
-                    ?? 'N/D';
117
-            })
118
-            ->map(function ($items, $metodo) {
119
-                return [
120
-                    'metodo' => (string) $metodo,
121
-                    'count' => $items->count(),
122
-                    'totale' => (float) $items->sum(function ($p) {
123
-                        return (float) ($p->importo ?? 0);
124
-                    }),
125
-                ];
126
-            })
127
-            ->sortByDesc('totale')
128
-            ->values();
129
-
130
-        $totaleIncassoOggi = (float) $pagamentiOggi->sum(function ($p) {
131
-            return (float) ($p->importo ?? 0);
132
-        });
133
-
134
-        $hourLabels = collect(range(0, 23))->map(function ($h) {
135
-            return str_pad((string) $h, 2, '0', STR_PAD_LEFT);
136
-        })->values();
137
-
138
-        $heatmapSeries = $cucineRows->map(function ($cucinaRow) use ($pagamentiOggi) {
139
-            $cucinaNome = (string) ($cucinaRow['nome'] ?? 'Cucina');
140
-            $cucinaId = (int) ($cucinaRow['id'] ?? 0);
141
-            $data = [];
142
-            foreach (range(0, 23) as $h) {
143
-                $hourTotal = (int) $pagamentiOggi
144
-                    ->filter(function ($pagamento) use ($h) {
145
-                        return !empty($pagamento->created_at) && (int) $pagamento->created_at->format('H') === $h;
146
-                    })
147
-                    ->sum(function ($pagamento) use ($cucinaId) {
148
-                        if (empty($pagamento->ordine)) {
149
-                            return 0;
150
-                        }
151
-                        return (int) $pagamento->ordine->righe_ordine->sum(function ($riga) use ($cucinaId) {
152
-                            $rigaCucinaId = (int) ($riga->piatto?->cucina_id ?? 0);
153
-                            if ($rigaCucinaId !== $cucinaId) {
154
-                                return 0;
155
-                            }
156
-                            return (int) ($riga->quantita ?? 0);
157
-                        });
158
-                    });
159
-                $data[] = $hourTotal;
160
-            }
161
-            return [
162
-                'name' => $cucinaNome,
163
-                'data' => $data,
164
-            ];
165
-        })->values();
44
+        $report = $bilancioServizio->forPeriod($attivitaId, $periodStart, $periodEnd);
166 45
 
167 46
         return view('bilancio.oggi', [
168 47
             'attivita' => $attivita,
169
-            'oggiLabel' => now()->format('d/m/Y'),
170
-            'cucineRows' => $cucineRows,
171
-            'paymentSummary' => $paymentSummary,
172
-            'totaleIncassoOggi' => $totaleIncassoOggi,
173
-            'heatmapHourLabels' => $hourLabels,
174
-            'heatmapSeries' => $heatmapSeries,
48
+            'periodoStart' => $periodStart,
49
+            'periodoEnd' => $periodEnd,
50
+            'ultimaChiusuraAt' => $ultimaChiusuraAt,
51
+            'periodoLabel' => $bilancioServizio->periodoLabel($periodStart, $periodEnd, $ultimaChiusuraAt),
52
+            'cucineRows' => $report['cucineRows'],
53
+            'paymentSummary' => $report['paymentSummary'],
54
+            'totaleIncassoOggi' => $report['totaleIncasso'],
55
+            'totaleTransazioniOggi' => $report['totaleTransazioni'],
56
+            'totPiattiOrdinati' => $report['totPiattiOrdinati'],
57
+            'heatmapHourLabels' => $report['heatmapHourLabels'],
58
+            'heatmapSeries' => $report['heatmapSeries'],
175 59
         ]);
176 60
     }
177 61
 

+ 13
- 0
app/Http/Controllers/CarrelloController.php Parādīt failu

@@ -436,6 +436,19 @@ class CarrelloController extends Controller
436 436
                     $ordine->save();
437 437
                     return back()->with('error', 'Pagamento con contanti non riuscito. '.$pagamentoResult['message']);
438 438
                 }
439
+
440
+            case MetodoPagamento::STAFF:
441
+                $pagamentoResult = app('\App\Services\Paga\Staff')->paga($pagamento);
442
+                
443
+                if($pagamentoResult['result'] === true){
444
+                    $ordine->update(['stato' => Ordine::PAGATO]);
445
+                    return redirect()->route('punto-vendita.show', ['punto_vendita_id' => $ordine->dispositivo_id])->with('success', 'Pagamento con staff riuscito.');
446
+                }else{
447
+                    $ordine->stato = Ordine::CARRELLO;
448
+                    $ordine->save();
449
+                    return back()->with('error', 'Pagamento con staff non riuscito. '.$pagamentoResult['message']);
450
+                }
451
+                break;
439 452
             default:
440 453
                 $ordine->stato = Ordine::CARRELLO;
441 454
                 $ordine->save();

+ 123
- 0
app/Http/Controllers/ChiusuraServizioController.php Parādīt failu

@@ -0,0 +1,123 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use App\Models\Attivita;
7
+use App\Models\ChiusuraServizio;
8
+use App\DataTables\ChiusuraServizioDataTable;
9
+use App\DataTables\ChiusuraServizioDataTableEditor;
10
+use App\Services\Bilancio\BilancioServizioService;
11
+use Carbon\Carbon;
12
+use Illuminate\Routing\Controllers\Middleware;
13
+use Illuminate\Support\Facades\Auth;
14
+use Illuminate\Support\Facades\Session;
15
+
16
+class ChiusuraServizioController extends Controller
17
+{
18
+  public static $permission_group = "Chiusura Servizio";
19
+  public static $permissions = [
20
+    'view-chiusura-servizio' => 'Vedi',
21
+    'create-chiusura-servizio' => 'Crea',
22
+    'edit-chiusura-servizio' => 'Modifica',
23
+    'delete-chiusura-servizio' => 'Elimina',
24
+  ];
25
+
26
+  public static function middleware(): array
27
+  {
28
+    return [
29
+      new Middleware('permission:view-chiusura-servizio', only: ['index', 'show']),
30
+      new Middleware('permission:create-chiusura-servizio', only: ['chiudi_servizio']),
31
+      new Middleware('permission:create-chiusura-servizio|edit-chiusura-servizio|delete-chiusura-servizio', only: ['store', 'update', 'destroy']),
32
+    ];
33
+  }
34
+
35
+  public function index(ChiusuraServizioDataTable $dataTable)
36
+  {
37
+      $dataTable->attivita_id = Session::get('attivita_attuale');
38
+        return $dataTable->render('chiusura_servizio.index');
39
+  }
40
+
41
+  public function store(ChiusuraServizioDataTableEditor $editor){
42
+      $request = request();
43
+      $input = $request->all();
44
+      if($request->has('action')){
45
+          switch($input['action']){
46
+              case 'create':
47
+                  if(!Auth::user()->can('create-chiusura-servizio')) return;
48
+                  break;
49
+              case 'edit':
50
+                  if(!Auth::user()->can('edit-chiusura-servizio')) return;
51
+                  break;
52
+              case 'delete':
53
+                  if(!Auth::user()->can('delete-chiusura-servizio')) return;
54
+                  break;
55
+          }
56
+      }
57
+      return $editor->process($request);
58
+  }
59
+
60
+  public function show(Request $request, BilancioServizioService $bilancioServizio)
61
+  {
62
+    $chiusura = ChiusuraServizio::query()
63
+      ->with(['user', 'attivita'])
64
+      ->where('attivita_id', Session::get('attivita_attuale'))
65
+      ->find($request->get('chiusura_servizio_id'));
66
+
67
+    if (!$chiusura) {
68
+      return redirect()->route('chiusura-servizio.index')->with('error', 'Chiusura non trovata');
69
+    }
70
+
71
+    $snapshot = is_array($chiusura->info) ? $chiusura->info : (json_decode($chiusura->info ?? '[]', true) ?: []);
72
+    $periodoDa = !empty($snapshot['periodo']['da']) ? Carbon::parse($snapshot['periodo']['da']) : null;
73
+    $periodoA = !empty($snapshot['periodo']['a'])
74
+      ? Carbon::parse($snapshot['periodo']['a'])
75
+      : $chiusura->chiusura_at;
76
+
77
+    $heatmap = $bilancioServizio->heatmapFromSnapshot(
78
+      (int) $chiusura->attivita_id,
79
+      $snapshot,
80
+      $periodoDa,
81
+      $periodoA
82
+    );
83
+
84
+    return view('chiusura_servizio.show', [
85
+      'chiusura' => $chiusura,
86
+      'snapshot' => $snapshot,
87
+      'periodoDa' => $periodoDa,
88
+      'periodoA' => $periodoA,
89
+      'paymentSummary' => collect($snapshot['pagamenti_per_metodo'] ?? []),
90
+      'cucineRows' => $bilancioServizio->cucineWithOrario($snapshot, $heatmap['heatmapSeries']),
91
+      'totaleIncasso' => (float) ($snapshot['incasso'] ?? 0),
92
+      'totaleTransazioni' => (int) ($snapshot['transazioni'] ?? 0),
93
+      'totPiattiOrdinati' => (int) ($snapshot['piatti_ordinati'] ?? 0),
94
+      'heatmapHourLabels' => $heatmap['heatmapHourLabels'],
95
+    ]);
96
+  }
97
+
98
+  public function chiudi_servizio(Request $request, BilancioServizioService $bilancioServizio)
99
+  {
100
+    if (!Auth::user()->can('create-chiusura-servizio')) {
101
+      return redirect()->back()->with('error', 'Non autorizzato');
102
+    }
103
+
104
+    $attivitaId = (int) ($request->input('attivita_id') ?: Session::get('attivita_attuale'));
105
+    $attivita = Attivita::find($attivitaId);
106
+    if (!$attivita) {
107
+      return redirect()->back()->with('error', 'Attività non trovata');
108
+    }
109
+
110
+    $chiusuraAt = now();
111
+    $periodStart = $bilancioServizio->periodStartForAttivita($attivitaId, $chiusuraAt);
112
+
113
+    ChiusuraServizio::create([
114
+      'attivita_id' => $attivitaId,
115
+      'user_id' => Auth::id(),
116
+      'chiusura_at' => $chiusuraAt,
117
+      'note' => $request->input('note', 'Chiusura servizio'),
118
+      'info' => $bilancioServizio->buildSnapshot($attivitaId, $periodStart, $chiusuraAt),
119
+    ]);
120
+
121
+    return redirect()->back()->with('success', 'Servizio chiuso con successo');
122
+  }
123
+}

+ 62
- 9
app/Http/Controllers/MappaDispositiviController.php Parādīt failu

@@ -10,6 +10,7 @@ use App\DataTables\DispositivoDataTable;
10 10
 use App\DataTables\DispositivoDataTableEditor;
11 11
 use Illuminate\Support\Facades\Auth;
12 12
 use Illuminate\Support\Facades\Session;
13
+use Illuminate\Routing\Controllers\Middleware;
13 14
 
14 15
 class MappaDispositiviController extends Controller
15 16
 {
@@ -31,17 +32,19 @@ class MappaDispositiviController extends Controller
31 32
     
32 33
     public function index(DispositivoDataTable $dataTable)
33 34
     {
34
-        $response = $dataTable->render('mappa_dispositivi.index');
35
-
36
-        // Passiamo i dati per la topografica SOLO quando stiamo rendendo HTML.
37
-        // In AJAX (DataTables) qui arriva una JsonResponse.
38
-        if ($response instanceof \Illuminate\View\View) {
39
-            $topologyByAttivita = self::buildTopologyByAttivita();
40
-
41
-            return $response->with('topologyByAttivita', $topologyByAttivita);
35
+        if (request()->ajax()) {
36
+            return $dataTable->ajax();
42 37
         }
43 38
 
44
-        return $response;
39
+        return view('mappa_dispositivi.index', [
40
+            'dispositiviByTipo' => self::buildDispositiviByTipo(),
41
+            'tipiDispositivo' => Dispositivo::getTipoDispositivo(),
42
+            'attivitaList' => Attivita::query()
43
+                ->where('is_attiva', true)
44
+                ->orderBy('nome')
45
+                ->where('user_id', Auth::user()->id)
46
+                ->get(['id', 'nome']),
47
+        ]);
45 48
     }
46 49
 
47 50
     public function store(DispositivoDataTableEditor $editor)
@@ -69,6 +72,56 @@ class MappaDispositiviController extends Controller
69 72
         return view('mappa_dispositivi.show', ['dispositivo' => $dispositivo]);
70 73
     }
71 74
 
75
+    /**
76
+     * Elenco dispositivi raggruppati per tipologia (tab mappa).
77
+     */
78
+    public static function buildDispositiviByTipo(): array
79
+    {
80
+        $tipi = Dispositivo::getTipoDispositivo()->keys()->all();
81
+        $grouped = array_fill_keys($tipi, []);
82
+        $grouped['_altro'] = [];
83
+
84
+        $attivitaAttiveIds = Attivita::query()
85
+            ->where('is_attiva', true)
86
+            ->pluck('id');
87
+
88
+        $devices = Dispositivo::query()
89
+            ->with('attivita:id,nome')
90
+            ->where(function ($q) use ($attivitaAttiveIds) {
91
+                $q->whereIn('attivita_id', $attivitaAttiveIds)
92
+                    ->orWhereNull('attivita_id');
93
+            })
94
+            ->orderBy('nome')
95
+            ->get(['id', 'nome', 'ubicazione', 'tipo', 'is_attivo', 'occupato', 'attivita_id']);
96
+
97
+        foreach ($devices as $device) {
98
+            $tipo = $device->tipo ?? '_altro';
99
+            $bucket = isset($grouped[$tipo]) ? $tipo : '_altro';
100
+            $grouped[$bucket][] = self::serializeDispositivoMappa($device);
101
+        }
102
+
103
+        return $grouped;
104
+    }
105
+
106
+    private static function serializeDispositivoMappa(Dispositivo $device): array
107
+    {
108
+        return [
109
+            'id' => (int) $device->id,
110
+            'nome' => (string) $device->nome,
111
+            'tipo' => $device->tipo !== null ? (string) $device->tipo : null,
112
+            'ubicazione' => $device->ubicazione !== null ? (string) $device->ubicazione : null,
113
+            'is_attivo' => (bool) $device->is_attivo,
114
+            'occupato' => (bool) $device->occupato,
115
+            'attivita_id' => $device->attivita_id !== null ? (int) $device->attivita_id : null,
116
+            'attivita_nome' => $device->attivita?->nome,
117
+            'is_punto_vendita' => in_array($device->tipo, [
118
+                Dispositivo::CASSA,
119
+                Dispositivo::KIOSK,
120
+                Dispositivo::CAMERIERE,
121
+            ], true),
122
+        ];
123
+    }
124
+
72 125
     /**
73 126
      * Bozza mappa: contenitore = attività → punto vendita (cassa/kiosk/cameriere) + cucine → stampante.
74 127
      */

+ 42
- 17
app/Http/Controllers/PrimaNotaController.php Parādīt failu

@@ -4,10 +4,11 @@ namespace App\Http\Controllers;
4 4
 
5 5
 use Illuminate\Http\Request;
6 6
 use App\Models\PrimaNota;
7
+use App\Models\Categoriacontabile;
7 8
 use App\DataTables\PrimaNotaDataTable;
8 9
 use App\DataTables\PrimaNotaDataTableEditor;
9 10
 use Illuminate\Support\Facades\Auth;
10
-use Carbon\Carbon;
11
+use Illuminate\Routing\Controllers\Middleware;
11 12
 
12 13
 class PrimaNotaController extends Controller
13 14
 { 
@@ -22,28 +23,52 @@ class PrimaNotaController extends Controller
22 23
     public static function middleware(): array
23 24
     {
24 25
         return [
25
-            new Middleware('permission:view-primanota', only: ['index']),
26
+            new Middleware('permission:view-primanota', only: ['index', 'statistiche']),
26 27
             new Middleware('permission:create-primanota|edit-primanota|delete-primanota', only: ['store', 'update', 'destroy']),
27 28
         ];
28 29
     }
29 30
 
30 31
     public function index(PrimaNotaDataTable $dataTable)
31 32
     {
32
-        $dataTable->attivita_id = session()->get('attivita_attuale');
33
-        
34
-        $statistiche['totale_operazioni'] = PrimaNota::where('attivita_id', session()->get('attivita_attuale'))
35
-        ->where('created_at', '>=', Carbon::now()->startOfDay())
36
-        ->count();
37
-        $statistiche['importo_entrate'] = PrimaNota::where('attivita_id', session()->get('attivita_attuale'))
38
-        ->where('tipo_movimento', 'entrata')
39
-        ->where('created_at', '>=', Carbon::now()->startOfDay())
40
-        ->sum('importo');
41
-        $statistiche['importo_uscite'] = PrimaNota::where('attivita_id', session()->get('attivita_attuale'))
42
-        ->where('tipo_movimento', 'uscita')
43
-        ->where('created_at', '>=', Carbon::now()->startOfDay())
44
-        ->sum('importo');
45
-        $statistiche['saldo'] = $statistiche['importo_entrate'] - $statistiche['importo_uscite'];
46
-        return $dataTable->render('prima_nota.index', compact('statistiche'));
33
+        $attivitaId = session()->get('attivita_attuale');
34
+        $dataTable->attivita_id = $attivitaId;
35
+
36
+        $statistiche = $attivitaId
37
+            ? PrimaNota::statisticheFiltrate($attivitaId)
38
+            : [
39
+                'totale_operazioni' => 0,
40
+                'importo_entrate' => 0,
41
+                'importo_uscite' => 0,
42
+                'saldo' => 0,
43
+            ];
44
+
45
+        $categorie = Categoriacontabile::where('attivita_id', $attivitaId)
46
+            ->where('is_attiva', true)
47
+            ->orderBy('nome')
48
+            ->get();
49
+
50
+        return $dataTable->render('prima_nota.index', compact('statistiche', 'categorie'));
51
+    }
52
+
53
+    public function statistiche(Request $request)
54
+    {
55
+        $attivitaId = session()->get('attivita_attuale');
56
+
57
+        if (!$attivitaId) {
58
+            return response()->json([
59
+                'totale_operazioni' => 0,
60
+                'importo_entrate' => 0,
61
+                'importo_uscite' => 0,
62
+                'saldo' => 0,
63
+            ]);
64
+        }
65
+
66
+        return response()->json(
67
+            PrimaNota::statisticheFiltrate(
68
+                $attivitaId,
69
+                $request->only(['data_inizio', 'data_fine', 'categoria_contabile_id', 'tipo_movimento'])
70
+            )
71
+        );
47 72
     }
48 73
 
49 74
     public function store(PrimaNotaDataTableEditor $editor)

+ 103
- 22
app/Http/Controllers/PuntoVenditaController.php Parādīt failu

@@ -14,7 +14,6 @@ use Illuminate\Support\Facades\Auth;
14 14
 use Illuminate\Support\Facades\Session;
15 15
 use Illuminate\Support\Facades\Cookie;
16 16
 use Illuminate\Support\Str;
17
-use Illuminate\Support\Facades\Hash;
18 17
 
19 18
 class PuntoVenditaController extends Controller
20 19
 {
@@ -30,7 +29,7 @@ class PuntoVenditaController extends Controller
30 29
     {
31 30
         return [
32 31
             new Middleware('permission:view-punto_vendita', only: ['index', 'dettagli_punto_vendita', 'dettagli']),
33
-            new Middleware('permission:create-punto_vendita|edit-punto_vendita|delete-punto_vendita', only: ['store', 'update', 'destroy', 'edit']),
32
+            new Middleware('permission:create-punto_vendita|edit-punto_vendita|delete-punto_vendita', only: ['store', 'update', 'destroy', 'edit', 'associa_cucina']),
34 33
         ];
35 34
     }
36 35
     
@@ -64,16 +63,73 @@ class PuntoVenditaController extends Controller
64 63
         return view('punto_vendita.edit', ['puntoVendita' => $puntoVendita]);
65 64
     }
66 65
 
66
+    public function update(Request $request)
67
+    {
68
+        $puntoVendita = Dispositivo::find($request->get('punto_vendita_id'));
69
+
70
+        if(!$puntoVendita){
71
+            return redirect()->route('punto-vendita.index')
72
+            ->with('error', 'Punto di vendita non trovato');
73
+        }
74
+
75
+        if(Auth::user()->can('edit-punto_vendita')){
76
+
77
+        $validated = $request->validate([
78
+            'nome' => 'sometimes|required',
79
+            'tipo' => 'sometimes|required',
80
+            'attivita_id' => 'sometimes|required|exists:attivita,id',
81
+            'licenza' => 'sometimes|nullable',
82
+            'url_stampante' => 'sometimes|nullable',
83
+            'ubicazione' => 'sometimes|nullable',
84
+            'note' => 'sometimes|nullable',
85
+            'is_attivo' => 'boolean',
86
+            'pin_sblocco' => 'sometimes|nullable',
87
+            'data_apertura_dispositivo' => 'nullable|date',
88
+            'data_chiusura_dispositivo' => 'nullable|date',
89
+            'endpoint_id' => 'sometimes|nullable|exists:endpoint,id',
90
+        ],[
91
+            'nome.required' => 'Il nome è richiesto',
92
+            'tipo.required' => 'Il tipo è richiesto',
93
+            'attivita_id.required' => 'La attività è richiesta',
94
+            'licenza.required' => 'La licenza è richiesta',
95
+            'url_stampante.required' => 'L\'URL della stampante è richiesta',
96
+            'ubicazione.required' => 'L\'ubicazione è richiesta',
97
+            'pin_sblocco.required' => 'Il pin di sblocco è richiesto',
98
+            'data_apertura_dispositivo.required' => 'La data di apertura del dispositivo è richiesta',
99
+            'data_chiusura_dispositivo.required' => 'La data di chiusura del dispositivo è richiesta',
100
+            'data_apertura_dispositivo.date' => 'La data di apertura del dispositivo deve essere una data valida',
101
+            'data_chiusura_dispositivo.date' => 'La data di chiusura del dispositivo deve essere una data valida',
102
+            'is_attivo.boolean' => 'Indicare una delle due opzioni',
103
+            'pin_sblocco.string' => 'Il pin di sblocco deve essere una stringa',
104
+            'pin_sblocco.required' => 'Il pin di sblocco è richiesto',
105
+            'pin_sblocco.min' => 'Il pin di sblocco deve essere lungo almeno 4 caratteri',
106
+            'pin_sblocco.max' => 'Il pin di sblocco deve essere lungo al massimo 4 caratteri',
107
+            'pin_sblocco.regex' => 'Il pin di sblocco deve contenere solo numeri',
108
+            'pin_sblocco.unique' => 'Il pin di sblocco deve essere unico',
109
+        ]);
110
+
111
+            $puntoVendita->update($validated);
112
+            return response()->json(['success' => true, 'message' => 'Punto di vendita aggiornato con successo']);
113
+        }else{
114
+            return response()->json(['success' => false, 'message' => 'Non hai il permesso di accedere a questo punto di vendita']);
115
+        }
116
+    }
117
+
67 118
     public function associa_cucina(Request $request)
68 119
     {
69 120
         $puntoVendita = Dispositivo::find($request->get('punto_vendita_id'));
70 121
         if(!$puntoVendita){
71 122
             return response()->json(['success' => false, 'message' => 'Punto di vendita non trovato']);
72 123
         }
124
+
125
+        if(Auth::user()->can('edit-punto_vendita')){
73 126
         $cucine_ids = $request->get('cucine_ids');
74 127
         $puntoVendita->hasCucine()->sync($cucine_ids);
75 128
 
76 129
         return response()->json(['success' => true, 'message' => 'Cucine associate con successo']);
130
+        }else{
131
+            return response()->json(['success' => false, 'message' => 'Non hai il permesso di accedere a questo punto di vendita']);
132
+        }
77 133
     }
78 134
 
79 135
     public function store(PuntoVenditaDataTableEditor $editor)
@@ -109,30 +165,54 @@ class PuntoVenditaController extends Controller
109 165
             ->with('error', 'Punto di vendita non trovato');
110 166
         }
111 167
 
168
+        //se non esiste il binding_token nel DB, lo crea
112 169
         if($puntoVendita->binding_token == '' || $puntoVendita->binding_token == null ){
113 170
             // $puntoVendita->binding_token = Str::random(32);
114
-            $puntoVendita->binding_token = Hash::make(Str::random(32));
171
+            $puntoVendita->binding_token = Str::random(32);
115 172
             $puntoVendita->save();
116 173
         }
117
-// dd(Cookie::get('binding_token'));
118
-        /*
119
-            Questo blocco di codice gestisce il controllo e l'impostazione del cookie 'binding_token' per il dispositivo.
120
-
121
-            Se esiste già un cookie 'binding_token', verifica che il suo valore coincida con il binding_token memorizzato per il punto vendita:
122
-              - Se NON coincidono, rimuove il cookie e mostra un errore: "Token di binding non valido. Si sta accedendo da un altro dispositivo."
123
-            Se NON esiste il cookie, lo imposta con il valore del binding_token del punto vendita.
124
-
125
-            Questo serve a garantire che ogni dispositivo rimanga "associato" al suo token, impedendo accessi da device diversi senza il corretto re-binding.
126
-        */
127
-        if(Cookie::has('binding_token')){
128
-            if(Hash::check(Cookie::get('binding_token'), Hash::make($puntoVendita->binding_token))){
129
-                // dd('A' , Cookie::get('binding_token') , $puntoVendita->binding_token);
130
-                Cookie::queue(Cookie::forget('binding_token'));
131
-                return redirect()->back()->with('error', 'Token di binding non valido. Si sta accedendo da un altro dispositivo.');
132
-            }
133
-        }else{
134
-            Cookie::queue('binding_token', Hash::make($puntoVendita->binding_token));
135
-            // Cookie::queue('binding_token', $puntoVendita->binding_token);
174
+
175
+        // if(Cookie::has('binding_token')){
176
+        //     if(Cookie::get('binding_token') != $puntoVendita->binding_token  && $puntoVendita->occupato == true){
177
+        //     return redirect()->back()->with('error', 'Dispositivo già associato. Dissociare per potervi accedere.');
178
+        //     }
179
+        // }elseif(Cookie::has('binding_token') && $puntoVendita->occupato == false){
180
+        //     Cookie::queue('binding_token', $puntoVendita->binding_token);
181
+        //     $puntoVendita->occupato = true;
182
+        //     $puntoVendita->save();
183
+        // }
184
+
185
+        switch(true){
186
+            // nessun cookie e il dispositivo non è occupato ACCEDI
187
+            case (!Cookie::has('binding_token') && $puntoVendita->occupato == false):
188
+                Cookie::queue('binding_token', $puntoVendita->binding_token);
189
+                $puntoVendita->occupato = true;
190
+                $puntoVendita->save();
191
+            break;
192
+
193
+            //nessun cookie e il dispositivo è occupato 01 NON ACCEDI
194
+            case (!Cookie::has('binding_token') && $puntoVendita->occupato == true):
195
+                return redirect()->back()->with('error', 'Dispositivo già occupato. Dissociare per potervi accedere.');
196
+            break;
197
+            //cookie esiste e dispositivo non è occupato 10 ACCEDI
198
+            case (Cookie::has('binding_token') && $puntoVendita->occupato == false):
199
+                if(Cookie::get('binding_token') == $puntoVendita->binding_token){
200
+                    $puntoVendita->occupato = true;
201
+                    $puntoVendita->save();
202
+                    break;
203
+                }else{
204
+                    return redirect()->back()->with('error', 'Dispositivo già occupato. Dissociare per potervi accedere.');
205
+                }
206
+            break;
207
+            // cookie esiste e dispositivo è occupato 11 ACCEDI SE SEI TU
208
+            case (Cookie::has('binding_token') && $puntoVendita->occupato == true):
209
+                if(Cookie::get('binding_token') == $puntoVendita->binding_token){
210
+                    break;
211
+                }else{
212
+                    return redirect()->back()->with('error', 'Dispositivo già occupato. Dissociare per potervi accedere.');
213
+                }
214
+            break;
215
+
136 216
         }
137 217
   
138 218
    
@@ -191,6 +271,7 @@ class PuntoVenditaController extends Controller
191 271
         Cookie::queue(Cookie::forget('binding_token'));
192 272
 
193 273
         $puntoVendita->binding_token = null;
274
+        $puntoVendita->occupato = false;
194 275
         $puntoVendita->save();
195 276
 
196 277
         return response()->json(['success' => true, 'message' => 'Disassociazione completata']);

+ 73
- 0
app/Models/AbstractModels/AbstractChiusuraServizio.php Parādīt failu

@@ -0,0 +1,73 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+abstract class AbstractChiusuraServizio extends \Illuminate\Foundation\Auth\User
10
+{
11
+    /**  
12
+     * The table associated with the model.
13
+     * 
14
+     * @var string
15
+     */
16
+    protected $table = 'chiusura_servizio';
17
+    
18
+    /**  
19
+     * Primary key type.
20
+     * 
21
+     * @var string
22
+     */
23
+    protected $keyType = 'bigInteger';
24
+    
25
+    /**  
26
+     * The model's default values for attributes.
27
+     * 
28
+     * @var array
29
+     */
30
+    // protected $attributes = ['is_gruppo' => 0];
31
+    
32
+    /**  
33
+     * The attributes that should be cast to native types.
34
+     * 
35
+     * @var array
36
+     */
37
+    protected $casts = [
38
+        'id' => 'integer',
39
+        'attivita_id' => 'integer',
40
+        'chiusura_at' => 'datetime',
41
+        'note' => 'string',
42
+        'user_id' => 'integer',
43
+        'info' => 'json',
44
+        'created_at' => 'datetime',
45
+        'updated_at' => 'datetime'
46
+    ];
47
+    
48
+    /**  
49
+     * The attributes that are mass assignable.
50
+     * 
51
+     * @var array
52
+     */
53
+    protected $fillable = [
54
+        'id',
55
+        'attivita_id',
56
+        'chiusura_at',
57
+        'note',
58
+        'user_id',
59
+        'info',
60
+        'created_at',
61
+        'updated_at'
62
+    ];
63
+    
64
+    public function attivita()
65
+    {
66
+        return $this->belongsTo('\App\Models\Attivita', 'attivita_id', 'id');
67
+    }
68
+
69
+    public function user()
70
+    {
71
+        return $this->belongsTo('\App\Models\User', 'user_id', 'id');
72
+    }
73
+}

+ 2
- 0
app/Models/AbstractModels/AbstractDispositivo.php Parādīt failu

@@ -51,6 +51,7 @@ abstract class AbstractDispositivo extends \Illuminate\Foundation\Auth\User
51 51
         'data_apertura_dispositivo' => 'datetime',
52 52
         'data_chiusura_dispositivo' => 'datetime',
53 53
         'cucina_id' => 'integer',
54
+        'occupato' => 'boolean',
54 55
         'created_at' => 'datetime',
55 56
         'updated_at' => 'datetime'
56 57
     ];
@@ -77,6 +78,7 @@ abstract class AbstractDispositivo extends \Illuminate\Foundation\Auth\User
77 78
         'data_apertura_dispositivo',
78 79
         'data_chiusura_dispositivo',
79 80
         'cucina_id',
81
+        'occupato',
80 82
         'created_at',
81 83
         'updated_at'
82 84
     ];

+ 2
- 0
app/Models/AbstractModels/AbstractMetodoPagamento.php Parādīt failu

@@ -38,6 +38,7 @@ abstract class AbstractMetodoPagamento extends \Illuminate\Foundation\Auth\User
38 38
         'id' => 'integer',  
39 39
         'attivita_id' => 'integer',
40 40
         'nome' => 'string',
41
+        'escludi_contabilita' => 'boolean',
41 42
         'descrizione' => 'string',
42 43
         'is_attivo' => 'boolean',
43 44
         'is_pubblico' => 'boolean',
@@ -58,6 +59,7 @@ abstract class AbstractMetodoPagamento extends \Illuminate\Foundation\Auth\User
58 59
         'id',
59 60
         'attivita_id',
60 61
         'nome',
62
+        'escludi_contabilita',
61 63
         'descrizione',
62 64
         'is_attivo',
63 65
         'is_pubblico',

+ 7
- 1
app/Models/Categoriacontabile.php Parādīt failu

@@ -3,8 +3,14 @@
3 3
 namespace App\Models;
4 4
 
5 5
 use Illuminate\Database\Eloquent\Model;
6
+use Illuminate\Support\Facades\Session;
6 7
 
7 8
 class Categoriacontabile extends \App\Models\AbstractModels\AbstractCategoriacontabile
8 9
 {
9
-    //
10
+    public static function vendita(){
11
+        return self::where('attivita_id', session('attivita_attuale'))->where('is_attiva', true)->where('nome', 'Vendita')->first();
12
+    }
13
+    public static function staff(){
14
+        return self::where('attivita_id', session('attivita_attuale'))->where('is_attiva', true)->where('nome', 'Staff')->first();
15
+    }
10 16
 }

+ 10
- 0
app/Models/ChiusuraServizio.php Parādīt failu

@@ -0,0 +1,10 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Illuminate\Database\Eloquent\Model;
6
+
7
+class ChiusuraServizio extends \App\Models\AbstractModels\AbstractChiusuraServizio
8
+{
9
+    //
10
+}

+ 23
- 8
app/Models/Dispositivo.php Parādīt failu

@@ -26,7 +26,7 @@ public static function getTipoDispositivo(){
26 26
         ],
27 27
         self::CASSA => [
28 28
             'label' => 'Cassa',
29
-            'icon' => 'bx bx-cash',
29
+            'icon' => 'bx bx-store',
30 30
             'value' => self::CASSA,
31 31
         ],
32 32
         self::KIOSK => [
@@ -41,7 +41,7 @@ public static function getTipoDispositivo(){
41 41
         ],
42 42
         self::MONITOR => [
43 43
             'label' => 'Monitor',
44
-            'icon' => 'bx bx-monitor',
44
+            'icon' => 'bx bx-tv',
45 45
             'value' => self::MONITOR,
46 46
         ],
47 47
         self::BACHECA => [
@@ -114,14 +114,29 @@ public static function findByBindingCookieToken(?string $cookieToken): ?self
114 114
         return null;
115 115
     }
116 116
 
117
-    return self::query()
118
-        ->whereNotNull('binding_token')
119
-        ->get()
120
-        ->first(function (self $dispositivo) use ($cookieToken) {
121
-            return Hash::check((string) $dispositivo->binding_token, $cookieToken);
122
-        });
117
+        return self::query()
118
+            ->whereNotNull('binding_token')
119
+            ->where('binding_token', $cookieToken)
120
+            ->where('occupato', true)
121
+            ->first();
123 122
 }
124 123
 
124
+///EX
125
+// public static function findByBindingCookieToken(?string $cookieToken): ?self
126
+// {
127
+//     if (!$cookieToken) {
128
+//         return null;
129
+//     }
130
+
131
+//     return self::query()
132
+//         ->whereNotNull('binding_token')
133
+//         ->get()
134
+//         ->first(function (self $dispositivo) use ($cookieToken) {
135
+//             return Hash::check((string) $dispositivo->binding_token, $cookieToken);
136
+//         });
137
+// }
138
+///EX
139
+
125 140
 public function cucine()
126 141
 {
127 142
     return $this->belongsToMany('\App\Models\Cucina', 'dispositivo_has_cucina', 'dispositivo_id', 'cucina_id');

+ 51
- 44
app/Models/MetodoPagamento.php Parādīt failu

@@ -19,71 +19,78 @@ class MetodoPagamento extends \App\Models\AbstractModels\AbstractMetodoPagamento
19 19
     const CUPON = 'cupon';
20 20
     const SEGRESTA_WALLET = 'segresta_wallet';
21 21
     const POS = 'pos';
22
+    const STAFF = 'staff';
22 23
 
23 24
 
24 25
     public static function getTipiPagamento()
25 26
     {
26 27
         return collect([
27
-            self::CARTA_DI_CREDITO => [
28
-                'label' => 'Carta di credito',
29
-                'value' => self::CARTA_DI_CREDITO,
30
-                'icon' => 'bx-credit-card',
31
-            ],
32
-            self::CARTA_DI_DEBITO => [
33
-                'label' => 'Carta di debito',
34
-                'value' => self::CARTA_DI_DEBITO,
35
-                'icon' => 'bx-credit-card',
36
-            ],
37
-            self::BONIFICO => [
38
-                'label' => 'Bonifico',
39
-                'value' => self::BONIFICO,
40
-                'icon' => 'bx-transfer',
41
-            ],
42
-            self::PAYPAL => [
43
-                'label' => 'Paypal',
44
-                'value' => self::PAYPAL,
45
-                'icon' => 'bxl-paypal',
46
-            ],
47
-            self::STRIPE => [
48
-                'label' => 'Stripe',
49
-                'value' => self::STRIPE,
50
-                'icon' => 'bx-credit-card',
51
-            ],
28
+            // self::APPLE_PAY => [
29
+            //     'label' => 'Apple Pay',
30
+            //     'value' => self::APPLE_PAY,
31
+            //     'icon' => 'bx-mobile',
32
+            // ],
33
+            // self::BONIFICO => [
34
+            //     'label' => 'Bonifico',
35
+            //     'value' => self::BONIFICO,
36
+            //     'icon' => 'bx-transfer',
37
+            // ],
38
+            // self::CARTA_DI_CREDITO => [
39
+            //     'label' => 'Carta di credito',
40
+            //     'value' => self::CARTA_DI_CREDITO,
41
+            //     'icon' => 'bx-credit-card',
42
+            // ],
43
+            // self::CARTA_DI_DEBITO => [
44
+            //     'label' => 'Carta di debito',
45
+            //     'value' => self::CARTA_DI_DEBITO,
46
+            //     'icon' => 'bx-credit-card',
47
+            // ],
52 48
             self::CONTANTI => [
53 49
                 'label' => 'Contanti',
54 50
                 'value' => self::CONTANTI,
55 51
                 'icon' => 'bx-money',
56 52
             ],
57
-            self::CREDITO => [
58
-                'label' => 'Credito',
59
-                'value' => self::CREDITO,
60
-                'icon' => 'bx-wallet',
61
-            ],
62
-            self::APPLE_PAY => [
63
-                'label' => 'Apple Pay',
64
-                'value' => self::APPLE_PAY,
65
-                'icon' => 'bx-mobile',
66
-            ],
67
-            self::GOOGLE_PAY => [
68
-                'label' => 'Google Pay',
69
-                'value' => self::GOOGLE_PAY,
70
-                'icon' => 'bx-mobile',
71
-            ],
53
+            // self::CREDITO => [
54
+            //     'label' => 'Credito',
55
+            //     'value' => self::CREDITO,
56
+            //     'icon' => 'bx-wallet',
57
+            // ],
72 58
             self::CUPON => [
73 59
                 'label' => 'Cupon',
74 60
                 'value' => self::CUPON,
75 61
                 'icon' => 'bx-purchase-tag',
76 62
             ],
63
+            // self::GOOGLE_PAY => [
64
+            //     'label' => 'Google Pay',
65
+            //     'value' => self::GOOGLE_PAY,
66
+            //     'icon' => 'bx-mobile',
67
+            // ],
68
+            // self::PAYPAL => [
69
+            //     'label' => 'Paypal',
70
+            //     'value' => self::PAYPAL,
71
+            //     'icon' => 'bxl-paypal',
72
+            // ],
73
+            self::POS => [
74
+                'label' => 'POS',
75
+                'value' => self::POS,
76
+                'icon' => 'bx-credit-card',
77
+            ],
77 78
             self::SEGRESTA_WALLET => [
78 79
                 'label' => 'Segresta Wallet',
79 80
                 'value' => self::SEGRESTA_WALLET,
80 81
                 'icon' => 'bx-wallet',
81 82
             ],
82
-            self::POS => [
83
-                'label' => 'POS',
84
-                'value' => self::POS,
85
-                'icon' => 'bx-credit-card',
83
+            self::STAFF => [
84
+                'label' => 'Staff',
85
+                'value' => self::STAFF,
86
+                'icon' => 'bx-user',
86 87
             ],
88
+            // self::STRIPE => [
89
+            //     'label' => 'Stripe',
90
+            //     'value' => self::STRIPE,
91
+            //     'icon' => 'bx-credit-card',
92
+            // ],
93
+       
87 94
         ]);
88 95
     }
89 96
 

+ 1
- 0
app/Models/Pagamento.php Parādīt failu

@@ -13,6 +13,7 @@ class Pagamento extends \App\Models\AbstractModels\AbstractPagamento
13 13
     const ANNULLATO = 'annullato';
14 14
     const RIFIUTATO = 'rifiutato';
15 15
     const ERRORE = 'errore';
16
+    const REGISTRATO = 'registrato';
16 17
 
17 18
     public static function getStati()
18 19
     {

+ 62
- 7
app/Models/PrimaNota.php Parādīt failu

@@ -11,6 +11,53 @@ class PrimaNota extends \App\Models\AbstractModels\AbstractPrimaNota
11 11
     const USCITA = 'uscita';
12 12
     const STORNO = 'storno';
13 13
     const NOTA_CREDITO = 'nota_credito';
14
+    const STAFF = 'staff';
15
+    const NO_CONTABILE = 'no_contabile';
16
+
17
+    public function scopeConFiltriIndice($query, ?array $filtri = null)
18
+    {
19
+        $filtri = $filtri ?? request()->only([
20
+            'data_inizio',
21
+            'data_fine',
22
+            'categoria_contabile_id',
23
+            'tipo_movimento',
24
+        ]);
25
+
26
+        if (!empty($filtri['data_inizio'])) {
27
+            $query->whereDate('created_at', '>=', $filtri['data_inizio']);
28
+        }
29
+
30
+        if (!empty($filtri['data_fine'])) {
31
+            $query->whereDate('created_at', '<=', $filtri['data_fine']);
32
+        }
33
+
34
+        if (!empty($filtri['categoria_contabile_id'])) {
35
+            $query->where('categoria_contabile_id', $filtri['categoria_contabile_id']);
36
+        }
37
+
38
+        if (!empty($filtri['tipo_movimento'])) {
39
+            $query->where('tipo_movimento', $filtri['tipo_movimento']);
40
+        }
41
+
42
+        return $query;
43
+    }
44
+
45
+    public static function statisticheFiltrate(int $attivitaId, ?array $filtri = null): array
46
+    {
47
+        $base = static::query()
48
+            ->where('attivita_id', $attivitaId)
49
+            ->conFiltriIndice($filtri);
50
+
51
+        $importoEntrate = (clone $base)->where('tipo_movimento', self::ENTRATA)->sum('importo');
52
+        $importoUscite = (clone $base)->where('tipo_movimento', self::USCITA)->sum('importo');
53
+
54
+        return [
55
+            'totale_operazioni' => (clone $base)->count(),
56
+            'importo_entrate' => $importoEntrate,
57
+            'importo_uscite' => $importoUscite,
58
+            'saldo' => $importoEntrate - $importoUscite,
59
+        ];
60
+    }
14 61
 
15 62
     public static function getTipiMovimento()
16 63
     {
@@ -23,13 +70,21 @@ class PrimaNota extends \App\Models\AbstractModels\AbstractPrimaNota
23 70
                 'label' => 'Uscita',
24 71
                 'value' => self::USCITA,
25 72
             ],
26
-            self::STORNO => [
27
-                'label' => 'Storno',
28
-                'value' => self::STORNO,
29
-            ],
30
-            self::NOTA_CREDITO => [
31
-                'label' => 'Nota credito',
32
-                'value' => self::NOTA_CREDITO,
73
+            // self::STORNO => [
74
+            //     'label' => 'Storno',
75
+            //     'value' => self::STORNO,
76
+            // ],
77
+            // self::NOTA_CREDITO => [
78
+            //     'label' => 'Nota credito',
79
+            //     'value' => self::NOTA_CREDITO,
80
+            // ],
81
+            // self::STAFF => [
82
+            //     'label' => 'Staff',
83
+            //     'value' => self::STAFF,
84
+            // ],
85
+            self::NO_CONTABILE => [
86
+                'label' => 'Non contabilizzati',
87
+                'value' => self::NO_CONTABILE,
33 88
             ],
34 89
         ]);
35 90
     }

+ 17
- 1
app/Observers/PagamentoObserver.php Parādīt failu

@@ -4,6 +4,8 @@ namespace App\Observers;
4 4
 
5 5
 use App\Models\Pagamento;
6 6
 use App\Models\PrimaNota;
7
+use App\Models\CategoriaContabile;
8
+use App\Models\MetodoPagamento;
7 9
 use Illuminate\Support\Facades\Log;
8 10
 
9 11
 class PagamentoObserver
@@ -16,10 +18,24 @@ class PagamentoObserver
16 18
             'attivita_id' => $pagamento->attivita_id,
17 19
             'pagamento_id' => $pagamento->id,
18 20
             'importo' => $pagamento->importo,
19
-            'tipo_movimento' => PrimaNota::ENTRATA,
21
+            // 'tipo_movimento' => PrimaNota::ENTRATA,
20 22
             'causale' => $pagamento->causale,
23
+            // 'categoria_contabile_id' => $pagamento->metodo_pagamento->tipo == MetodoPagamento::STAFF ? CategoriaContabile::STAFF : CategoriaContabile::VENDITA,
21 24
             'ordine_id' => $pagamento->ordine_id,
22 25
         ]);
26
+        //Assegna categoria contabile
27
+        if($pagamento->metodo_pagamento->tipo == MetodoPagamento::STAFF){
28
+            $primaNota->categoria_contabile_id = CategoriaContabile::staff()->id??null;
29
+        }else{
30
+            $primaNota->categoria_contabile_id = CategoriaContabile::vendita()->id??null;
31
+        }
32
+        //Assegna tipo movimento
33
+        if($pagamento->metodo_pagamento->escludi_contabilita){
34
+            $primaNota->tipo_movimento = PrimaNota::NO_CONTABILE;
35
+        }else{
36
+            $primaNota->tipo_movimento = PrimaNota::ENTRATA;
37
+        }
38
+
23 39
         $primaNota->save();
24 40
     }
25 41
 

+ 276
- 0
app/Services/Bilancio/BilancioServizioService.php Parādīt failu

@@ -0,0 +1,276 @@
1
+<?php
2
+
3
+namespace App\Services\Bilancio;
4
+
5
+use App\Models\ChiusuraServizio;
6
+use App\Models\Pagamento;
7
+use App\Models\RigaOrdine;
8
+use Carbon\Carbon;
9
+
10
+class BilancioServizioService
11
+{
12
+    /**
13
+     * Inizio del servizio corrente: ultima chiusura registrata, altrimenti inizio giornata.
14
+     */
15
+    public function periodStartForAttivita(int $attivitaId, ?Carbon $reference = null): Carbon
16
+    {
17
+        $lastChiusura = $this->lastChiusuraAt($attivitaId);
18
+
19
+        if ($lastChiusura) {
20
+            return $lastChiusura->copy();
21
+        }
22
+
23
+        return ($reference ?? now())->copy()->startOfDay();
24
+    }
25
+
26
+    public function lastChiusuraAt(int $attivitaId): ?Carbon
27
+    {
28
+        $ultimaChiusura = ChiusuraServizio::query()
29
+            ->where('attivita_id', $attivitaId)
30
+            ->orderByDesc('chiusura_at')
31
+            ->value('chiusura_at');
32
+
33
+        return $ultimaChiusura ? Carbon::parse($ultimaChiusura) : null;
34
+    }
35
+
36
+    /**
37
+     * @return array{start: Carbon, end: Carbon, ultimaChiusuraAt: ?Carbon}
38
+     */
39
+    public function currentPeriod(int $attivitaId, ?Carbon $reference = null): array
40
+    {
41
+        $end = ($reference ?? now())->copy();
42
+        $ultimaChiusuraAt = $this->lastChiusuraAt($attivitaId);
43
+        $start = $ultimaChiusuraAt
44
+            ? $ultimaChiusuraAt->copy()
45
+            : $end->copy()->startOfDay();
46
+
47
+        return [
48
+            'start' => $start,
49
+            'end' => $end,
50
+            'ultimaChiusuraAt' => $ultimaChiusuraAt,
51
+        ];
52
+    }
53
+
54
+    /**
55
+     * Totali e breakdown per un intervallo temporale (stessa logica di bilancio servizio).
56
+     */
57
+    public function forPeriod(int $attivitaId, Carbon $start, Carbon $end): array
58
+    {
59
+        $righeOrdini = RigaOrdine::query()
60
+            ->with(['piatto.cucina', 'ordine'])
61
+            ->whereHas('ordine', function ($query) use ($attivitaId, $start, $end) {
62
+                $query
63
+                    ->where('attivita_id', $attivitaId)
64
+                    ->whereBetween('created_at', [$start, $end]);
65
+            })
66
+            ->get();
67
+
68
+        $righeIncassi = RigaOrdine::query()
69
+            ->with(['piatto.cucina', 'ordine'])
70
+            ->whereHas('ordine.pagamenti', function ($query) use ($attivitaId, $start, $end) {
71
+                $query
72
+                    ->where('attivita_id', $attivitaId)
73
+                    ->whereBetween('created_at', [$start, $end]);
74
+            })
75
+            ->get();
76
+
77
+        $righe = $righeOrdini
78
+            ->concat($righeIncassi)
79
+            ->unique('id')
80
+            ->values();
81
+
82
+        $cucineRows = $righe
83
+            ->groupBy(fn ($riga) => $riga->piatto?->cucina_id ?: 0)
84
+            ->map(function ($items, $cucinaId) {
85
+                $first = $items->first();
86
+                $cucinaNome = $first?->piatto?->cucina?->nome ?? ('Cucina #' . $cucinaId);
87
+
88
+                $piatti = $items
89
+                    ->groupBy('piatto_id')
90
+                    ->map(function ($piattoItems) {
91
+                        $firstPiatto = $piattoItems->first();
92
+                        $quantita = (int) $piattoItems->sum(fn ($r) => (int) ($r->quantita ?? 0));
93
+                        $totale = (float) $piattoItems->sum(fn ($r) => ((float) ($r->quantita ?? 0)) * ((float) ($r->prezzo ?? 0)));
94
+
95
+                        return [
96
+                            'nome' => $firstPiatto?->piatto?->nome ?? 'Piatto',
97
+                            'quantita' => $quantita,
98
+                            'totale' => $totale,
99
+                        ];
100
+                    })
101
+                    ->sortByDesc('quantita')
102
+                    ->values();
103
+
104
+                return [
105
+                    'id' => (int) $cucinaId,
106
+                    'nome' => $cucinaNome,
107
+                    'piatti' => $piatti,
108
+                    'tot_quantita' => (int) $piatti->sum('quantita'),
109
+                    'tot_importo' => (float) $piatti->sum('totale'),
110
+                ];
111
+            })
112
+            ->sortByDesc('tot_quantita')
113
+            ->values();
114
+
115
+        $pagamenti = Pagamento::query()
116
+            ->with(['metodo_pagamento', 'ordine.righe_ordine.piatto.cucina'])
117
+            ->where('attivita_id', $attivitaId)
118
+            ->whereBetween('created_at', [$start, $end])
119
+            ->get();
120
+
121
+        $paymentSummary = $pagamenti
122
+            ->groupBy(fn ($pagamento) => $pagamento->metodo_pagamento?->nome
123
+                ?? $pagamento->metodo_pagamento?->tipo
124
+                ?? 'N/D')
125
+            ->map(function ($items, $metodo) {
126
+                return [
127
+                    'metodo' => (string) $metodo,
128
+                    'count' => $items->count(),
129
+                    'totale' => (float) $items->sum(fn ($p) => (float) ($p->importo ?? 0)),
130
+                ];
131
+            })
132
+            ->sortByDesc('totale')
133
+            ->values();
134
+
135
+        $totaleIncasso = (float) $pagamenti->sum(fn ($p) => (float) ($p->importo ?? 0));
136
+        $totaleTransazioni = $pagamenti->count();
137
+        $totPiattiOrdinati = (int) $cucineRows->sum('tot_quantita');
138
+
139
+        $hourLabels = collect(range(0, 23))->map(fn ($h) => str_pad((string) $h, 2, '0', STR_PAD_LEFT))->values();
140
+
141
+        $heatmapSeries = $cucineRows->map(function ($cucinaRow) use ($pagamenti) {
142
+            $cucinaId = (int) ($cucinaRow['id'] ?? 0);
143
+            $data = [];
144
+            foreach (range(0, 23) as $h) {
145
+                $hourTotal = (int) $pagamenti
146
+                    ->filter(fn ($pagamento) => !empty($pagamento->created_at) && (int) $pagamento->created_at->format('H') === $h)
147
+                    ->sum(function ($pagamento) use ($cucinaId) {
148
+                        if (empty($pagamento->ordine)) {
149
+                            return 0;
150
+                        }
151
+
152
+                        return (int) $pagamento->ordine->righe_ordine->sum(function ($riga) use ($cucinaId) {
153
+                            if ((int) ($riga->piatto?->cucina_id ?? 0) !== $cucinaId) {
154
+                                return 0;
155
+                            }
156
+
157
+                            return (int) ($riga->quantita ?? 0);
158
+                        });
159
+                    });
160
+                $data[] = $hourTotal;
161
+            }
162
+
163
+            return [
164
+                'name' => (string) ($cucinaRow['nome'] ?? 'Cucina'),
165
+                'data' => $data,
166
+            ];
167
+        })->values();
168
+
169
+        return [
170
+            'periodStart' => $start,
171
+            'periodEnd' => $end,
172
+            'cucineRows' => $cucineRows,
173
+            'paymentSummary' => $paymentSummary,
174
+            'totaleIncasso' => $totaleIncasso,
175
+            'totaleTransazioni' => $totaleTransazioni,
176
+            'totPiattiOrdinati' => $totPiattiOrdinati,
177
+            'heatmapHourLabels' => $hourLabels,
178
+            'heatmapSeries' => $heatmapSeries,
179
+        ];
180
+    }
181
+
182
+    /**
183
+     * Snapshot JSON da salvare in chiusura_servizio.info (audit del segmento chiuso).
184
+     */
185
+    public function buildSnapshot(int $attivitaId, Carbon $start, Carbon $end): array
186
+    {
187
+        $report = $this->forPeriod($attivitaId, $start, $end);
188
+
189
+        return [
190
+            'periodo' => [
191
+                'da' => $start->toIso8601String(),
192
+                'a' => $end->toIso8601String(),
193
+            ],
194
+            'incasso' => $report['totaleIncasso'],
195
+            'transazioni' => $report['totaleTransazioni'],
196
+            'piatti_ordinati' => $report['totPiattiOrdinati'],
197
+            'pagamenti_per_metodo' => $report['paymentSummary']->values()->all(),
198
+            'cucine' => $report['cucineRows']->values()->map(function ($cucina, $index) use ($report) {
199
+                $orario = $report['heatmapSeries']->get($index);
200
+
201
+                return [
202
+                    'nome' => $cucina['nome'],
203
+                    'tot_quantita' => $cucina['tot_quantita'],
204
+                    'tot_importo' => $cucina['tot_importo'],
205
+                    'piatti' => $cucina['piatti']->values()->all(),
206
+                    'ordini_per_ora' => $orario['data'] ?? [],
207
+                ];
208
+            })->all(),
209
+            'heatmap_hour_labels' => $report['heatmapHourLabels']->values()->all(),
210
+            'heatmap_series' => $report['heatmapSeries']->values()->all(),
211
+            'generato_at' => now()->toIso8601String(),
212
+        ];
213
+    }
214
+
215
+    /**
216
+     * Arricchisce le cucine dello snapshot con ordini orari (fallback da heatmap_series).
217
+     */
218
+    public function cucineWithOrario(array $snapshot, $heatmapSeries): \Illuminate\Support\Collection
219
+    {
220
+        $series = collect($heatmapSeries);
221
+
222
+        return collect($snapshot['cucine'] ?? [])->values()->map(function ($cucina, $index) use ($series) {
223
+            if (!empty($cucina['ordini_per_ora'])) {
224
+                return $cucina;
225
+            }
226
+
227
+            $orario = $series->get($index) ?? $series->firstWhere('name', $cucina['nome'] ?? '');
228
+            $cucina['ordini_per_ora'] = $orario['data'] ?? [];
229
+
230
+            return $cucina;
231
+        });
232
+    }
233
+
234
+    /**
235
+     * Heatmap dallo snapshot; per chiusure precedenti senza dati, ricalcolo sul periodo.
236
+     *
237
+     * @return array{heatmapHourLabels: \Illuminate\Support\Collection, heatmapSeries: \Illuminate\Support\Collection}
238
+     */
239
+    public function heatmapFromSnapshot(int $attivitaId, array $snapshot, ?Carbon $periodoDa, ?Carbon $periodoA): array
240
+    {
241
+        $series = collect($snapshot['heatmap_series'] ?? []);
242
+        $labels = collect($snapshot['heatmap_hour_labels'] ?? []);
243
+
244
+        if ($series->isNotEmpty()) {
245
+            return [
246
+                'heatmapHourLabels' => $labels,
247
+                'heatmapSeries' => $series,
248
+            ];
249
+        }
250
+
251
+        if (!$periodoDa || !$periodoA) {
252
+            return [
253
+                'heatmapHourLabels' => collect(),
254
+                'heatmapSeries' => collect(),
255
+            ];
256
+        }
257
+
258
+        $report = $this->forPeriod($attivitaId, $periodoDa, $periodoA);
259
+
260
+        return [
261
+            'heatmapHourLabels' => $report['heatmapHourLabels'],
262
+            'heatmapSeries' => $report['heatmapSeries'],
263
+        ];
264
+    }
265
+
266
+    public function periodoLabel(Carbon $start, Carbon $end, ?Carbon $lastChiusura = null): string
267
+    {
268
+        $range = $start->format('d/m/Y H:i') . ' → ' . $end->format('d/m/Y H:i');
269
+
270
+        if ($lastChiusura) {
271
+            return 'Servizio in corso · dal ' . $lastChiusura->format('d/m/Y H:i') . ' · adesso';
272
+        }
273
+
274
+        return 'Servizio in corso · ' . $range;
275
+    }
276
+}

+ 26
- 0
app/Services/Paga/Staff.php Parādīt failu

@@ -0,0 +1,26 @@
1
+<?php
2
+
3
+namespace App\Services\Paga;
4
+
5
+use App\Models\MetodoPagamento;
6
+use App\Models\Pagamento;
7
+use Illuminate\Support\Facades\Http;
8
+use Illuminate\Support\Facades\Log;
9
+
10
+class Staff
11
+{
12
+    public function __construct(){
13
+
14
+    }
15
+
16
+    public function paga($pagamento){
17
+        //Pagamento registrato
18
+        // $pagamento->stato = Pagamento::REGISTRATO;
19
+        $pagamento->stato = Pagamento::PAGATO; //lascio pagato per non bloccare stampe scontrini
20
+        $pagamento->save();
21
+        return [
22
+            'result' => true,
23
+            'message' => 'Pagamento con contanti registrato.',
24
+        ];
25
+    }
26
+}

+ 28
- 0
database/migrations/2026_06_18_153607_campo_occupato_in_dispositivo.php Parādīt failu

@@ -0,0 +1,28 @@
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('dispositivo', function (Blueprint $table) {
15
+            $table->boolean('occupato')->default(false);
16
+        });
17
+    }
18
+
19
+    /**
20
+     * Reverse the migrations.
21
+     */
22
+    public function down(): void
23
+    {
24
+        Schema::table('dispositivo', function (Blueprint $table) {
25
+            $table->dropColumn('occupato');
26
+        });
27
+    }
28
+};

+ 28
- 0
database/migrations/2026_06_18_164218_contabilizzare_metodo_di_pagamento.php Parādīt failu

@@ -0,0 +1,28 @@
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('metodo_pagamento', function (Blueprint $table) {
15
+            $table->boolean('escludi_contabilita')->default(false);
16
+        });
17
+    }
18
+
19
+    /**
20
+     * Reverse the migrations.
21
+     */
22
+    public function down(): void
23
+    {
24
+        Schema::table('metodo_pagamento', function (Blueprint $table) {
25
+            $table->dropColumn('escludi_contabilita');
26
+        });
27
+    }
28
+};

+ 34
- 0
database/migrations/2026_06_19_005802_chiusura_servizio.php Parādīt failu

@@ -0,0 +1,34 @@
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::create('chiusura_servizio', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->bigInteger('attivita_id')->unsigned()->nullable();
17
+            $table->dateTime('chiusura_at');
18
+            $table->string('note')->nullable();
19
+            $table->bigInteger('user_id')->unsigned()->nullable();
20
+            $table->json('info')->nullable();
21
+            $table->timestamps();
22
+            $table->foreign('attivita_id')->references('id')->on('attivita')->onDelete('cascade');
23
+            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
24
+        });
25
+    }
26
+
27
+    /**
28
+     * Reverse the migrations.
29
+     */
30
+    public function down(): void
31
+    {
32
+        Schema::dropIfExists('chiusura_servizio');
33
+    }
34
+};

+ 4
- 0
database/seeders/CategoriaContabileSeed.php Parādīt failu

@@ -103,6 +103,10 @@ class CategoriaContabileSeed extends Seeder
103 103
                 'nome' => 'Offerte',
104 104
                 'descrizione' => 'Offerte',
105 105
                 'colore' => '#ffc107', // Giallo
106
+            ],
107
+            ['nome' => 'Staff',
108
+            'descrizione' => 'Spese per staff',
109
+            'colore' => '#dc3545', // Rosso (uscite generali)
106 110
             ],      
107 111
             
108 112
         ];

+ 26
- 3
resources/menu/verticalMenu.json Parādīt failu

@@ -125,8 +125,31 @@
125 125
           "name": "Bilancio",
126 126
           "icon": "menu-icon icon-base bx bx-chart",
127 127
           "slug": "bilancio.index",
128
-          "url": "admin/bilancio",
129
-          "can": "permission:view-bilancio"
128
+          "url": "",
129
+          "can": "permission:view-bilancio",
130
+          "submenu": [
131
+            {
132
+              "name": "Oggi",
133
+              "icon": "menu-icon icon-base bx bx-chart",
134
+              "slug": "bilancio.oggi",
135
+              "url": "admin/bilancio/oggi",
136
+              "can": "permission:view-bilancio-oggi"
137
+            },
138
+            {
139
+              "name": "Annuale",
140
+              "icon": "menu-icon icon-base bx bx-calendar",
141
+                "slug": "bilancio.annuale",
142
+              "url": "admin/bilancio",
143
+              "can": "permission:view-bilancio"
144
+            },
145
+            {
146
+              "name": "Storico chiusure",
147
+              "icon": "menu-icon icon-base bx bx-history",
148
+              "slug": "chiusura-servizio.index",
149
+              "url": "admin/chiusura-servizio",
150
+              "can": "permission:view-chiusura-servizio"
151
+            }
152
+          ]
130 153
         },
131 154
         {
132 155
           "name": "Report",
@@ -157,7 +180,7 @@
157 180
           "url": "admin/saltacoda"
158 181
         },
159 182
         {
160
-          "name": "Operatore",
183
+          "name": "Pass",
161 184
           "icon": "menu-icon icon-base bx bx-user",
162 185
           "slug": "punto_operatore.index",
163 186
           "url": "admin/punto-operatore",

+ 9
- 1
resources/views/_partials/badge.blade.php Parādīt failu

@@ -22,4 +22,12 @@
22 22
 <span class="px-3 py-1 alert alert-dark ms-2">
23 23
     {{ $dark }}
24 24
 </span>
25
-@endif
25
+@endif
26
+
27
+@if(isset($entity))
28
+<span class="badge" style="background-color: {{ $entity->categoria_contabile?->colore }};">{{ $entity->categoria_contabile?->nome }}</span>
29
+
30
+    @if($entity->tipo_movimento == \App\Models\PrimaNota::NO_CONTABILE)
31
+        <span class="badge ms-2 badge-info text-bg-info">NC</span>
32
+    @endif
33
+@endif

+ 371
- 140
resources/views/bilancio/oggi.blade.php Parādīt failu

@@ -1,10 +1,11 @@
1 1
 @php
2
+use Illuminate\Support\Facades\Auth;
2 3
 $configData = Helper::appClasses();
3 4
 @endphp
4 5
 
5 6
 @extends('layouts/layoutMaster')
6 7
 
7
-@section('title', 'Bilancio oggi')
8
+@section('title', 'Bilancio servizio')
8 9
 
9 10
 @section('vendor-style')
10 11
 @vite([
@@ -19,138 +20,227 @@ $configData = Helper::appClasses();
19 20
 @endsection
20 21
 
21 22
 @section('pageTitle')
22
-<div class="d-flex flex-column">
23
-  <h4 class="mb-1">
24
-    <i class="bx bx-chart"></i> Bilancio di oggi
25
-    <span class="text-muted text-primary fs-6 fw-normal"> di {{ $attivita?->nome ?? 'Attivita' }}</span>
26
-  </h4>
27
-  <small class="text-muted">Data riferimento: {{ $oggiLabel ?? now()->format('d/m/Y') }}</small>
23
+<div class="d-flex flex-wrap justify-content-between align-items-start gap-2">
24
+  <div class="d-flex flex-column">
25
+    <h4 class="mb-1">
26
+      <i class="bx bx-chart"></i> Bilancio servizio
27
+      <span class="text-muted fs-6 fw-normal">· {{ $attivita?->nome ?? 'Attività' }}</span>
28
+    </h4>
29
+    <small class="text-muted">{{ $periodoLabel ?? ('Servizio in corso · ' . now()->format('d/m/Y H:i')) }}</small>
30
+  </div>
28 31
 </div>
29 32
 @endsection
30 33
 
31 34
 @section('content')
32 35
 <style>
33
-  .kpi-amount {
34
-    font-size: 1.6rem;
36
+  .servizio-kpi-value {
37
+    font-size: 1.75rem;
35 38
     font-weight: 700;
39
+    line-height: 1.2;
40
+  }
41
+
42
+  .servizio-kpi-label {
43
+    font-size: .8rem;
44
+    text-transform: uppercase;
45
+    letter-spacing: .04em;
46
+    color: var(--bs-secondary-color);
36 47
   }
37 48
 
38
-  .metodo-row:last-child {
49
+  .servizio-metodo-row:last-child {
39 50
     border-bottom: 0 !important;
40 51
   }
41 52
 </style>
42 53
 
43
-@if(session('status'))
44
-  <div class="alert alert-success">
45
-    {{ session('status') }}
46
-  </div>
47
-@endif
48
-
49
-<div class="row g-4 mt-1">
50
-  <div class="col-xl-8">
51
-    <div class="card">
52
-      <div class="card-header d-flex justify-content-between align-items-center">
53
-        <h5 class="mb-0">Cucine e piatti ordinati</h5>
54
-        <small class="text-muted">Ordinati per numero ordinazioni</small>
54
+@include('_partials.status')
55
+
56
+<div class="card mt-1 mb-0 border-start border-3 border-info">
57
+  <div class="card-body py-3 d-flex flex-wrap align-items-center gap-3">
58
+    <div class="avatar avatar-sm">
59
+      <span class="avatar-initial rounded bg-label-info">
60
+        <i class="bx bx-time-five"></i>
61
+      </span>
62
+    </div>
63
+    <div>
64
+      <div class="fw-semibold">Servizio in corso</div>
65
+      <div class="text-muted small">
66
+        @if(!empty($ultimaChiusuraAt))
67
+          Dal <strong>{{ $ultimaChiusuraAt->format('d/m/Y H:i') }}</strong>
68
+          <!-- al <strong>{{ ($periodoEnd ?? now())->format('d/m/Y H:i') }}</strong> -->
69
+          <small class="d-block mt-1">(ultima chiusura registrata)</small>
70
+        @else
71
+          Dal <strong>{{ ($periodoStart ?? now()->startOfDay())->format('d/m/Y H:i') }}</strong>
72
+          <!-- al <strong>{{ ($periodoEnd ?? now())->format('d/m/Y H:i') }}</strong> -->
73
+          <span class="d-block mt-1">Nessuna chiusura precedente: conteggio dall'inizio giornata.</span>
74
+        @endif
55 75
       </div>
76
+    </div>
77
+  </div>
78
+</div>
79
+
80
+{{-- KPI principali --}}
81
+<div class="row g-3 mt-1">
82
+  <div class="col-md-4">
83
+    <div class="card h-100 border-start border-4 border-primary">
56 84
       <div class="card-body">
57
-        <div class="accordion" id="accordionCucineOggi">
58
-          @forelse(($cucineRows ?? collect()) as $index => $cucina)
59
-            @php
60
-              $itemId = 'cucina-' . $index;
61
-              $headingId = 'heading-' . $itemId;
62
-              $collapseId = 'collapse-' . $itemId;
63
-            @endphp
64
-            <div class="accordion-item">
65
-              <h2 class="accordion-header" id="{{ $headingId }}">
66
-                <button class="accordion-button {{ $loop->first ? '' : 'collapsed' }}" type="button" data-bs-toggle="collapse" data-bs-target="#{{ $collapseId }}" aria-expanded="{{ $loop->first ? 'true' : 'false' }}" aria-controls="{{ $collapseId }}">
67
-                  <div class="w-100 d-flex justify-content-between align-items-center pe-2">
68
-                    <span class="fw-semibold">{{ $cucina['nome'] }}</span>
69
-                    <span class="text-muted small">
70
-                      Tot. ordinazioni: <strong>{{ number_format((int) $cucina['tot_quantita'], 0, ',', '.') }}</strong>
71
-                      &nbsp;|&nbsp; Tot. cucina: <strong>EUR {{ number_format((float) $cucina['tot_importo'], 2, ',', '.') }}</strong>
72
-                    </span>
73
-                  </div>
74
-                </button>
75
-              </h2>
76
-              <div id="{{ $collapseId }}" class="accordion-collapse collapse {{ $loop->first ? 'show' : '' }}" aria-labelledby="{{ $headingId }}" data-bs-parent="#accordionCucineOggi">
77
-                <div class="accordion-body p-0">
78
-                  <div class="table-responsive">
79
-                    <table class="table table-sm mb-0">
80
-                      <thead>
81
-                        <tr>
82
-                          <th>Piatto</th>
83
-                          <th class="text-end">Ordinazioni</th>
84
-                          <th class="text-end">Totale piatto</th>
85
-                        </tr>
86
-                      </thead>
87
-                      <tbody>
88
-                        @forelse($cucina['piatti'] as $piatto)
89
-                          <tr>
90
-                            <td>{{ $piatto['nome'] }}</td>
91
-                            <td class="text-end">{{ number_format((int) $piatto['quantita'], 0, ',', '.') }}</td>
92
-                            <td class="text-end">EUR {{ number_format((float) $piatto['totale'], 2, ',', '.') }}</td>
93
-                          </tr>
94
-                        @empty
95
-                          <tr>
96
-                            <td colspan="3" class="text-muted">Nessun piatto ordinato oggi.</td>
97
-                          </tr>
98
-                        @endforelse
99
-                      </tbody>
100
-                    </table>
101
-                  </div>
102
-                </div>
103
-              </div>
104
-            </div>
105
-          @empty
106
-            <div class="alert alert-warning mb-0">
107
-              Nessuna cucina con ordinazioni per oggi.
108
-            </div>
109
-          @endforelse
85
+        <div class="servizio-kpi-label mb-1">Incasso</div>
86
+        <div class="servizio-kpi-value text-primary">
87
+          € {{ number_format((float) ($totaleIncassoOggi ?? 0), 2, ',', '.') }}
110 88
         </div>
111 89
       </div>
112 90
     </div>
113 91
   </div>
114
-
115
-  <div class="col-xl-4">
92
+  <div class="col-md-4">
116 93
     <div class="card h-100">
117
-      <div class="card-header d-flex justify-content-between align-items-center">
118
-        <h5 class="mb-0">Sintesi pagamenti</h5>
119
-        <span class="badge bg-label-primary">Oggi</span>
120
-      </div>
121 94
       <div class="card-body">
122
-        <div class="mb-3">
123
-          <div class="text-muted small">Totale incasso</div>
124
-          <div class="kpi-amount">EUR {{ number_format((float) ($totaleIncassoOggi ?? 0), 2, ',', '.') }}</div>
95
+        <div class="servizio-kpi-label mb-1">Transazioni</div>
96
+        <div class="servizio-kpi-value">
97
+          {{ number_format((int) ($totaleTransazioniOggi ?? 0), 0, ',', '.') }}
125 98
         </div>
126
-
127
-        <div class="list-group list-group-flush">
128
-          @forelse(($paymentSummary ?? collect()) as $row)
129
-            <div class="list-group-item px-0 metodo-row border-bottom">
130
-              <div class="d-flex justify-content-between align-items-center">
131
-                <div>
132
-                  <div class="fw-semibold">{{ $row['metodo'] }}</div>
133
-                  <small class="text-muted">{{ number_format((int) $row['count'], 0, ',', '.') }} transazioni</small>
134
-                </div>
135
-                <div class="fw-semibold">EUR {{ number_format((float) $row['totale'], 2, ',', '.') }}</div>
136
-              </div>
137
-            </div>
138
-          @empty
139
-            <div class="text-muted">Nessun pagamento registrato oggi.</div>
140
-          @endforelse
99
+      </div>
100
+    </div>
101
+  </div>
102
+  <div class="col-md-4">
103
+    <div class="card h-100">
104
+      <div class="card-body">
105
+        <div class="servizio-kpi-label mb-1">Piatti ordinati</div>
106
+        <div class="servizio-kpi-value">
107
+          {{ number_format((int) ($totPiattiOrdinati ?? 0), 0, ',', '.') }}
141 108
         </div>
142 109
       </div>
143 110
     </div>
144 111
   </div>
112
+</div>
145 113
 
146
-  <div class="col-12">
147
-    <div class="card">
148
-      <div class="card-header d-flex justify-content-between align-items-center">
149
-        <h5 class="mb-0">Heatmap oraria del giorno - Cucine</h5>
150
-        <small class="text-muted">Incrocio ora e cucina per ordinazioni</small>
114
+{{-- Pagamenti per metodo --}}
115
+<div class="card mt-4">
116
+  <div class="card-header">
117
+    <h5 class="mb-0">Pagamenti per metodo</h5>
118
+  </div>
119
+  <div class="card-body p-0">
120
+    @forelse(($paymentSummary ?? collect()) as $row)
121
+      <div class="d-flex justify-content-between align-items-center px-4 py-3 border-bottom servizio-metodo-row">
122
+        <div>
123
+          <div class="fw-semibold">{{ $row['metodo'] }}</div>
124
+          <small class="text-muted">
125
+            {{ number_format((int) $row['count'], 0, ',', '.') }}
126
+            {{ (int) $row['count'] === 1 ? 'transazione' : 'transazioni' }}
127
+          </small>
128
+        </div>
129
+        <div class="fs-5 fw-semibold">
130
+          € {{ number_format((float) $row['totale'], 2, ',', '.') }}
131
+        </div>
151 132
       </div>
152
-      <div class="card-body">
153
-        <div id="heatmap-incassi-oggi" style="min-height: 360px;"></div>
133
+    @empty
134
+      <div class="p-4 text-muted">Nessun pagamento registrato in questo periodo.</div>
135
+    @endforelse
136
+  </div>
137
+</div>
138
+
139
+{{-- Dettagli (secondo piano) --}}
140
+<div class="card mt-4">
141
+  <div class="card-header">
142
+    <button
143
+      class="btn btn-sm btn-label-primary"
144
+      type="button"
145
+      data-bs-toggle="collapse"
146
+      data-bs-target="#servizioDettagli"
147
+      aria-expanded="false"
148
+      aria-controls="servizioDettagli"
149
+    >
150
+      <i class="bx bx-chevron-down me-1"></i> Mostra dettagli (cucine e andamento orario)
151
+    </button>
152
+    @if(Auth::user()->can('create-chiusura-servizio') && \App\Models\ChiusuraServizio::where('attivita_id', $attivita->id)->where('chiusura_at', '>=', now()->subHours(1))->count() == 0)
153
+    <form
154
+      id="form-chiudi-servizio"
155
+      method="POST"
156
+      action="{{ route('chiusura-servizio.chiudi-servizio') }}"
157
+      class="btn btn-sm  d-inline-flex align-items-center"
158
+    >
159
+      @csrf
160
+      <button type="button" class="btn btn-warning" id="btn-chiudi-servizio">
161
+        <i class="bx bx-lock-alt me-1"></i> Chiudi servizio
162
+      </button>
163
+    </form>
164
+  @endcan
165
+  </div>
166
+  <div class="collapse" id="servizioDettagli">
167
+    <div class="card-body border-top">
168
+      <ul class="nav nav-tabs mb-3" role="tablist">
169
+        <li class="nav-item" role="presentation">
170
+          <button class="nav-link active" type="button" data-bs-toggle="tab" data-bs-target="#servizio-tab-cucine" role="tab">
171
+            Cucine e piatti
172
+          </button>
173
+        </li>
174
+        <li class="nav-item" role="presentation">
175
+          <button class="nav-link" type="button" data-bs-toggle="tab" data-bs-target="#servizio-tab-heatmap" role="tab" id="servizio-tab-heatmap-btn">
176
+            Andamento orario
177
+          </button>
178
+        </li>
179
+      </ul>
180
+
181
+      <div class="tab-content">
182
+        <div class="tab-pane fade show active" id="servizio-tab-cucine" role="tabpanel">
183
+          <div class="accordion" id="accordionCucineOggi">
184
+            @forelse(($cucineRows ?? collect()) as $index => $cucina)
185
+              @php
186
+                $itemId = 'cucina-' . $index;
187
+                $headingId = 'heading-' . $itemId;
188
+                $collapseId = 'collapse-' . $itemId;
189
+              @endphp
190
+              <div class="accordion-item">
191
+                <h2 class="accordion-header" id="{{ $headingId }}">
192
+                  <button
193
+                    class="accordion-button collapsed"
194
+                    type="button"
195
+                    data-bs-toggle="collapse"
196
+                    data-bs-target="#{{ $collapseId }}"
197
+                    aria-expanded="false"
198
+                    aria-controls="{{ $collapseId }}"
199
+                  >
200
+                    <div class="w-100 d-flex flex-wrap justify-content-between align-items-center gap-2 pe-2">
201
+                      <span class="fw-semibold">{{ $cucina['nome'] }}</span>
202
+                      <span class="text-muted small">
203
+                        {{ number_format((int) $cucina['tot_quantita'], 0, ',', '.') }} ord.
204
+                        · € {{ number_format((float) $cucina['tot_importo'], 2, ',', '.') }}
205
+                      </span>
206
+                    </div>
207
+                  </button>
208
+                </h2>
209
+                <div id="{{ $collapseId }}" class="accordion-collapse collapse" aria-labelledby="{{ $headingId }}" data-bs-parent="#accordionCucineOggi">
210
+                  <div class="accordion-body p-0">
211
+                    <div class="table-responsive">
212
+                      <table class="table table-sm mb-0">
213
+                        <thead>
214
+                          <tr>
215
+                            <th>Piatto</th>
216
+                            <th class="text-end">Qtà</th>
217
+                            <th class="text-end">Totale</th>
218
+                          </tr>
219
+                        </thead>
220
+                        <tbody>
221
+                          @foreach($cucina['piatti'] as $piatto)
222
+                            <tr>
223
+                              <td>{{ $piatto['nome'] }}</td>
224
+                              <td class="text-end">{{ number_format((int) $piatto['quantita'], 0, ',', '.') }}</td>
225
+                              <td class="text-end">€ {{ number_format((float) $piatto['totale'], 2, ',', '.') }}</td>
226
+                            </tr>
227
+                          @endforeach
228
+                        </tbody>
229
+                      </table>
230
+                    </div>
231
+                  </div>
232
+                </div>
233
+              </div>
234
+            @empty
235
+              <div class="alert alert-warning mb-0">Nessuna cucina con ordinazioni in questo periodo.</div>
236
+            @endforelse
237
+          </div>
238
+        </div>
239
+
240
+        <div class="tab-pane fade" id="servizio-tab-heatmap" role="tabpanel">
241
+          <p class="text-muted small mb-3">Ordinazioni per ora e cucina.</p>
242
+          <div id="heatmap-incassi-oggi" style="min-height: 320px;"></div>
243
+        </div>
154 244
       </div>
155 245
     </div>
156 246
   </div>
@@ -160,52 +250,193 @@ $configData = Helper::appClasses();
160 250
 @section('page-script')
161 251
 <script>
162 252
   document.addEventListener('DOMContentLoaded', function () {
253
+    const chiudiBtn = document.getElementById('btn-chiudi-servizio');
254
+    const chiudiForm = document.getElementById('form-chiudi-servizio');
255
+
256
+    if (chiudiBtn && chiudiForm) {
257
+      chiudiBtn.addEventListener('click', function () {
258
+        Swal.fire({
259
+          title: 'Chiudere il servizio?',
260
+          html: [
261
+            'Verrà registrata la chiusura del servizio corrente con uno <strong>snapshot</strong> dei totali',
262
+            '(incasso, transazioni, pagamenti per metodo, cucine e piatti).',
263
+            '<br><br>',
264
+            'Il prossimo servizio partirà da questo momento.',
265
+            '<br><br>',
266
+            '<span class="text-muted">Le vendite non vengono bloccate: i nuovi ordini saranno conteggiati nel servizio successivo.</span>'
267
+          ].join(' '),
268
+          icon: 'warning',
269
+          showCancelButton: true,
270
+          confirmButtonText: 'Sì, chiudi servizio',
271
+          cancelButtonText: 'Annulla',
272
+          customClass: {
273
+            confirmButton: 'btn btn-warning me-2',
274
+            cancelButton: 'btn btn-label-secondary'
275
+          },
276
+          buttonsStyling: false
277
+        }).then(function (result) {
278
+          if (result.isConfirmed) {
279
+            chiudiForm.submit();
280
+          }
281
+        });
282
+      });
283
+    }
284
+
163 285
     if (typeof ApexCharts === 'undefined') {
164 286
       return;
165 287
     }
166 288
 
167 289
     const heatmapSeries = @json($heatmapSeries ?? []);
168 290
     const hourLabels = @json($heatmapHourLabels ?? []);
291
+    let heatmapChart = null;
292
+
293
+    function isDarkTheme() {
294
+      const theme = document.documentElement.getAttribute('data-bs-theme');
295
+      if (theme === 'dark') {
296
+        return true;
297
+      }
298
+      if (theme === 'light') {
299
+        return false;
300
+      }
301
+      return window.matchMedia('(prefers-color-scheme: dark)').matches;
302
+    }
303
+
304
+    function getThemeColors() {
305
+      const css = getComputedStyle(document.documentElement);
306
+      return {
307
+        labelColor: (css.getPropertyValue('--bs-secondary-color') || '#6c757d').trim(),
308
+        borderColor: (css.getPropertyValue('--bs-border-color') || '#e9ecef').trim(),
309
+        emptyColor: (css.getPropertyValue('--bs-tertiary-bg') || css.getPropertyValue('--bs-border-color') || '#e9ecef').trim()
310
+      };
311
+    }
312
+
313
+    function getHeatmapColorScale(dark, emptyColor) {
314
+      if (dark) {
315
+        return [
316
+          { from: 0, to: 0, color: emptyColor, name: 'Nessuna ordinazione' },
317
+          { from: 1, to: 5, color: '#434968', name: 'Basso' },
318
+          { from: 6, to: 12, color: '#5a5fcf', name: 'Medio' },
319
+          { from: 13, to: 20, color: '#696cff', name: 'Alto' },
320
+          { from: 21, to: 99999, color: '#8592ff', name: 'Picco' }
321
+        ];
322
+      }
323
+
324
+      return [
325
+        { from: 0, to: 0, color: emptyColor, name: 'Nessuna ordinazione' },
326
+        { from: 1, to: 5, color: '#dbeafe', name: 'Basso' },
327
+        { from: 6, to: 12, color: '#93c5fd', name: 'Medio' },
328
+        { from: 13, to: 20, color: '#60a5fa', name: 'Alto' },
329
+        { from: 21, to: 99999, color: '#2563eb', name: 'Picco' }
330
+      ];
331
+    }
169 332
 
170
-    const heatmapConfig = {
171
-      chart: {
172
-        type: 'heatmap',
173
-        height: 360,
174
-        toolbar: { show: false }
175
-      },
176
-      series: heatmapSeries,
177
-      xaxis: {
178
-        categories: hourLabels
179
-      },
180
-      dataLabels: {
181
-        enabled: false
182
-      },
183
-      colors: ['#696cff'],
184
-      plotOptions: {
185
-        heatmap: {
186
-          shadeIntensity: 0.55,
187
-          radius: 4,
188
-          colorScale: {
189
-            ranges: [
190
-              { from: 0, to: 0, color: '#f8fafc', name: 'Nessuna ordinazione' },
191
-              { from: 1, to: 5, color: '#dbeafe', name: 'Basso' },
192
-              { from: 6, to: 12, color: '#93c5fd', name: 'Medio' },
193
-              { from: 13, to: 20, color: '#60a5fa', name: 'Alto' },
194
-              { from: 21, to: 99999, color: '#2563eb', name: 'Picco' }
195
-            ]
333
+    function buildHeatmapOptions() {
334
+      const dark = isDarkTheme();
335
+      const colors = getThemeColors();
336
+
337
+      return {
338
+        chart: {
339
+          type: 'heatmap',
340
+          height: 320,
341
+          toolbar: { show: false },
342
+          foreColor: colors.labelColor
343
+        },
344
+        theme: {
345
+          mode: dark ? 'dark' : 'light'
346
+        },
347
+        series: heatmapSeries,
348
+        xaxis: {
349
+          categories: hourLabels,
350
+          labels: {
351
+            style: { colors: colors.labelColor, fontSize: '11px' }
352
+          },
353
+          axisBorder: { show: false },
354
+          axisTicks: { show: false }
355
+        },
356
+        yaxis: {
357
+          labels: {
358
+            style: { colors: colors.labelColor, fontSize: '12px' }
196 359
           }
197
-        }
198
-      },
199
-      tooltip: {
200
-        y: {
201
-          formatter: function (val) {
202
-            return Number(val || 0).toLocaleString('it-IT') + ' ordinazioni';
360
+        },
361
+        dataLabels: {
362
+          enabled: false
363
+        },
364
+        colors: ['#696cff'],
365
+        plotOptions: {
366
+          heatmap: {
367
+            shadeIntensity: dark ? 0.35 : 0.55,
368
+            radius: 4,
369
+            colorScale: {
370
+              ranges: getHeatmapColorScale(dark, colors.emptyColor)
371
+            }
372
+          }
373
+        },
374
+        legend: {
375
+          position: 'bottom',
376
+          labels: {
377
+            colors: colors.labelColor
378
+          }
379
+        },
380
+        grid: {
381
+          borderColor: colors.borderColor,
382
+          strokeDashArray: 4
383
+        },
384
+        tooltip: {
385
+          theme: dark ? 'dark' : 'light',
386
+          y: {
387
+            formatter: function (val) {
388
+              return Number(val || 0).toLocaleString('it-IT') + ' ordinazioni';
389
+            }
203 390
           }
204 391
         }
392
+      };
393
+    }
394
+
395
+    function destroyHeatmap() {
396
+      if (heatmapChart) {
397
+        heatmapChart.destroy();
398
+        heatmapChart = null;
205 399
       }
206
-    };
400
+    }
207 401
 
208
-    new ApexCharts(document.querySelector('#heatmap-incassi-oggi'), heatmapConfig).render();
402
+    function renderHeatmap() {
403
+      const el = document.querySelector('#heatmap-incassi-oggi');
404
+      if (!el) {
405
+        return;
406
+      }
407
+
408
+      destroyHeatmap();
409
+      heatmapChart = new ApexCharts(el, buildHeatmapOptions());
410
+      heatmapChart.render();
411
+    }
412
+
413
+    function heatmapTabVisible() {
414
+      return !!document.querySelector('#servizio-tab-heatmap.show.active');
415
+    }
416
+
417
+    const heatmapTabBtn = document.getElementById('servizio-tab-heatmap-btn');
418
+    if (heatmapTabBtn) {
419
+      heatmapTabBtn.addEventListener('shown.bs.tab', renderHeatmap);
420
+    }
421
+
422
+    const dettagli = document.getElementById('servizioDettagli');
423
+    if (dettagli) {
424
+      dettagli.addEventListener('shown.bs.collapse', function () {
425
+        if (heatmapTabVisible()) {
426
+          renderHeatmap();
427
+        }
428
+      });
429
+    }
430
+
431
+    const themeObserver = new MutationObserver(function () {
432
+      if (heatmapChart && heatmapTabVisible()) {
433
+        renderHeatmap();
434
+      }
435
+    });
436
+    themeObserver.observe(document.documentElement, {
437
+      attributes: true,
438
+      attributeFilter: ['data-bs-theme']
439
+    });
209 440
   });
210 441
 </script>
211 442
 @endsection

+ 148
- 0
resources/views/chiusura_servizio/index.blade.php Parādīt failu

@@ -0,0 +1,148 @@
1
+<?php
2
+use App\Models\Role;
3
+use Illuminate\Support\Facades\Auth;
4
+?>
5
+@php
6
+$configData = Helper::appClasses();
7
+@endphp
8
+
9
+@extends('layouts/layoutMaster')
10
+
11
+@section('title', 'Chiusura Servizio')
12
+
13
+@section('vendor-style')
14
+@vite([
15
+'resources/assets/vendor/libs/datatables-bs5/datatables.bootstrap5.scss',
16
+'resources/assets/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.scss',
17
+'resources/assets/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.scss',
18
+'resources/assets/vendor/libs/flatpickr/flatpickr.scss',
19
+'resources/assets/vendor/libs/@form-validation/form-validation.scss'
20
+])
21
+@endsection
22
+
23
+<!-- Vendor Scripts -->
24
+@section('vendor-script')
25
+@vite([
26
+'resources/assets/vendor/libs/moment/moment.js',
27
+'resources/assets/vendor/libs/flatpickr/flatpickr.js',
28
+'resources/assets/vendor/libs/@form-validation/popular.js',
29
+'resources/assets/vendor/libs/@form-validation/bootstrap5.js',
30
+'resources/assets/vendor/libs/@form-validation/auto-focus.js',
31
+'resources/assets/vendor/libs/@form-validation/popular.js',
32
+'resources/assets/vendor/libs/@form-validation/bootstrap5.js',
33
+'resources/assets/vendor/libs/@form-validation/auto-focus.js',
34
+'resources/assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js'
35
+])
36
+@endsection
37
+
38
+@section('pageTitle')
39
+<div class="d-flex flex-column">
40
+  <h4 class="mb-1 text-sm-small mt-md-4 mt-lg-4"> 
41
+    <i class="bx bx-time d-none d-md-inline-flex d-lg-inline-flex"></i> Chiusura Servizio</h4>
42
+</div>
43
+@endsection
44
+
45
+@section('content')
46
+<style>
47
+  div.upload button:first-child{
48
+    display: none !important;
49
+  }
50
+
51
+.bx-chef-hat{
52
+  --svg: url("data:image/svg+xml,%3csvg width='24' height='24' fill='currentColor' viewBox='0 0 24 24' transform='' xmlns='http://www.w3.org/2000/svg'%3e%3c!--Boxicons v3.0.8 https://boxicons.com %7c License https://docs.boxicons.com/free--%3e%3cpath d='M17.13 5.54C16.33 3.42 14.32 2 12 2S7.67 3.42 6.87 5.54A5.506 5.506 0 0 0 2 11c0 2.07 1.18 3.95 3 4.88V18c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-2.12c1.82-.93 3-2.81 3-4.88 0-2.82-2.13-5.15-4.87-5.46m.53 8.75c-.4.14-.67.52-.67.94v1.78H7v-1.78c0-.42-.27-.8-.67-.94-1.4-.5-2.33-1.82-2.33-3.28 0-1.93 1.57-3.5 3.42-3.5.04 0 .14.01.18.02.49 0 .9-.31 1-.78.36-1.61 1.76-2.73 3.41-2.73s3.05 1.12 3.41 2.73c.1.47.51.81 1 .78.06 0 .12 0 .09-.01 1.93 0 3.5 1.57 3.5 3.5 0 1.47-.94 2.79-2.33 3.28ZM5 20h14v2H5z'%3e%3c/path%3e%3c/svg%3e");
53
+}
54
+</style>
55
+
56
+@include('_partials.status')
57
+
58
+<div class="row justify-content-center">
59
+  <div class="col-xxl mb-4 mt-2">
60
+    <div class="card">
61
+      <div class="card-body">
62
+        {{ $dataTable_chiusura_servizio->table(['class' => 'table table-bordered']) }}
63
+      </div>
64
+    </div>
65
+  </div>
66
+</div>
67
+
68
+<div class="modal fade" id="basicModal" tabindex="-1" aria-hidden="true">
69
+            <div class="modal-dialog" role="document">
70
+              <div class="modal-content">
71
+                <div class="modal-header">
72
+                  <h5 class="modal-title" id="exampleModalLabel1">Modal title</h5>
73
+                  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
74
+                </div>
75
+                <div class="modal-body">
76
+                  <div class="row">
77
+                    <div class="col mb-6">
78
+                      <label for="nameBasic" class="form-label">Name</label>
79
+                      <input type="text" id="nameBasic" class="form-control" placeholder="Enter Name">
80
+                    </div>
81
+                  </div>
82
+                  <div class="row g-6">
83
+                    <div class="col mb-0">
84
+                      <label for="emailBasic" class="form-label">Email</label>
85
+                      <input type="email" id="emailBasic" class="form-control" placeholder="xxxx@xxx.xx">
86
+                    </div>
87
+                    <div class="col mb-0">
88
+                      <label for="dobBasic" class="form-label">DOB</label>
89
+                      <input type="date" id="dobBasic" class="form-control">
90
+                    </div>
91
+                  </div>
92
+                </div>
93
+                <div class="modal-footer">
94
+                  <button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">Close</button>
95
+                  <button type="button" class="btn btn-primary">Save changes</button>
96
+                </div>
97
+              </div>
98
+            </div>
99
+          </div>
100
+
101
+@endsection
102
+
103
+@section('page-script')
104
+
105
+{{$dataTable_chiusura_servizio->scripts(attributes: ['type' => 'module'])}}
106
+
107
+
108
+<script type="module">
109
+  $(document).ready(function(){
110
+    // Editor edit
111
+    $("#dataTable_chiusura_servizio").on('click', 'a.editor_edit', function (e) {
112
+      e.preventDefault();
113
+      window.LaravelDataTables["dataTable_chiusura_servizio-editor"].edit( $(this).closest('tr'), {
114
+        title: 'Modifica',
115
+        buttons: 'Aggiorna',
116
+      });
117
+    });
118
+
119
+    // Editor delete
120
+    $("#dataTable_chiusura_servizio").on('click', 'a.editor_delete', function (e) {
121
+      e.preventDefault();
122
+      window.LaravelDataTables["dataTable_chiusura_servizio-editor"].remove($(this).closest('tr'), {
123
+        title: 'Cancella record',
124
+        message: 'Sei sicuro di voler eliminare il record selezionato?',
125
+        buttons: 'Cancella record'
126
+      });
127
+    } );
128
+
129
+    $("#dataTable_chiusura_servizio").on('dblclick', 'tbody td', function (e) {
130
+      window.LaravelDataTables["dataTable_chiusura_servizio-editor"].edit( $(this).closest('tr'), {
131
+        title: 'Modifica',
132
+        buttons: 'Aggiorna',
133
+      });
134
+    });
135
+
136
+  });
137
+
138
+</script>
139
+
140
+<script>
141
+  function initComplete_chiusura_servizio(){
142
+    $('div.dt-buttons button').removeClass('btn-secondary');
143
+    $('div.dt-search').addClass('mt-0 mb-4');
144
+    $('div.dt-buttons').removeClass('btn-group');
145
+  }
146
+
147
+</script>
148
+@endsection

+ 29
- 0
resources/views/chiusura_servizio/menu.blade.php Parādīt failu

@@ -0,0 +1,29 @@
1
+<?php
2
+use Illuminate\Support\Facades\Auth;
3
+?>
4
+@if(Auth::user()->can('view-chiusura-servizio') || Auth::user()->can('edit-chiusura-servizio') || Auth::user()->can('delete-chiusura-servizio'))
5
+<div class="dropdown">
6
+    <button type="button" class="btn p-0 dropdown-toggle hide-arrow" data-bs-toggle="dropdown" aria-expanded="false">
7
+        <i class="bx bx-dots-vertical-rounded fs-large"></i>
8
+    </button>
9
+    <div class="dropdown-menu">
10
+        @if(Auth::user()->can('view-chiusura-servizio'))
11
+        <a href="{{ route('chiusura-servizio.show', ['chiusura_servizio_id' => $entity->id]) }}" class="dropdown-item">
12
+            <i class="bx bx-eye me-1"></i> Visualizza
13
+        </a>
14
+        @endif
15
+
16
+        @if(Auth::user()->can('edit-chiusura-servizio'))
17
+        <a href="#" class="dropdown-item editor_edit">
18
+            <i class="bx bx-edit-alt me-1"></i> Modifica
19
+        </a>
20
+        @endif
21
+
22
+        @if(Auth::user()->can('delete-chiusura-servizio'))
23
+        <a href="#" class="dropdown-item editor_delete">
24
+            <i class="bx bx-trash me-1"></i> Elimina
25
+        </a>
26
+        @endif
27
+    </div>
28
+</div>
29
+@endif

+ 663
- 0
resources/views/chiusura_servizio/show.blade.php Parādīt failu

@@ -0,0 +1,663 @@
1
+@php
2
+$configData = Helper::appClasses();
3
+$hasSnapshot = !empty($snapshot) && (
4
+  isset($snapshot['incasso'])
5
+  || !empty($snapshot['pagamenti_per_metodo'])
6
+  || !empty($snapshot['cucine'])
7
+);
8
+@endphp
9
+
10
+@extends('layouts/layoutMaster')
11
+
12
+@section('title', 'Chiusura servizio')
13
+
14
+@section('vendor-style')
15
+@vite([
16
+  'resources/assets/vendor/libs/apex-charts/apex-charts.scss'
17
+])
18
+@endsection
19
+
20
+@section('vendor-script')
21
+@vite([
22
+  'resources/assets/vendor/libs/apex-charts/apexcharts.js'
23
+])
24
+@endsection
25
+
26
+@section('pageTitle')
27
+<div class="d-flex flex-column">
28
+  <h4 class="mb-1 text-sm-small mt-md-4 mt-lg-4">
29
+    <i class="bx bx-time d-none d-md-inline-flex d-lg-inline-flex"></i>
30
+    Chiusura servizio
31
+    @if($chiusura->chiusura_at)
32
+      <span class="text-muted fs-6 fw-normal">· {{ $chiusura->chiusura_at->format('d/m/Y H:i') }}</span>
33
+    @endif
34
+  </h4>
35
+  <!-- <nav aria-label="breadcrumb" style="font-size: smaller;" class="d-none d-md-block d-lg-block mb-0">
36
+    <ol class="breadcrumb breadcrumb-custom-icon m-0">
37
+      <li class="breadcrumb-item">
38
+        <a href="{{ route('chiusura-servizio.index') }}">Chiusure servizio</a>
39
+        <i class="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
40
+      </li>
41
+      <li class="breadcrumb-item active text-primary">
42
+        {{ $chiusura->chiusura_at?->format('d/m/Y H:i') ?? ('#' . $chiusura->id) }}
43
+      </li>
44
+    </ol>
45
+  </nav> -->
46
+</div>
47
+@endsection
48
+
49
+@section('content')
50
+<style>
51
+  .servizio-kpi-value {
52
+    font-size: 1.75rem;
53
+    font-weight: 700;
54
+    line-height: 1.2;
55
+  }
56
+
57
+  .servizio-kpi-label {
58
+    font-size: .8rem;
59
+    text-transform: uppercase;
60
+    letter-spacing: .04em;
61
+    color: var(--bs-secondary-color);
62
+  }
63
+
64
+  .servizio-metodo-row:last-child {
65
+    border-bottom: 0 !important;
66
+  }
67
+
68
+  .snapshot-field-label {
69
+    font-size: .75rem;
70
+    text-transform: uppercase;
71
+    letter-spacing: .04em;
72
+    color: var(--bs-secondary-color);
73
+    margin-bottom: .25rem;
74
+  }
75
+
76
+  .snapshot-table-panel {
77
+    border: 1px solid var(--bs-border-color);
78
+    border-radius: .5rem;
79
+    overflow: hidden;
80
+    height: 100%;
81
+  }
82
+
83
+  .snapshot-table-panel__header {
84
+    display: flex;
85
+    align-items: center;
86
+    gap: .5rem;
87
+    padding: .75rem 1rem;
88
+    border-bottom: 1px solid var(--bs-border-color);
89
+    background: rgba(var(--bs-body-color-rgb), .03);
90
+  }
91
+
92
+  .snapshot-table-panel--qty {
93
+    border-top: 3px solid var(--bs-success);
94
+  }
95
+
96
+  .snapshot-table-panel--qty .snapshot-table-panel__header {
97
+    background: rgba(var(--bs-success-rgb), .08);
98
+  }
99
+
100
+  .snapshot-table-panel--incasso {
101
+    border-top: 3px solid var(--bs-primary);
102
+  }
103
+
104
+  .snapshot-table-panel--incasso .snapshot-table-panel__header {
105
+    background: rgba(var(--bs-primary-rgb), .08);
106
+  }
107
+
108
+  .snapshot-table-panel--orario {
109
+    border-top: 3px solid var(--bs-warning);
110
+  }
111
+
112
+  .snapshot-table-panel--orario .snapshot-table-panel__header {
113
+    background: rgba(var(--bs-warning-rgb), .08);
114
+  }
115
+
116
+  .snapshot-performance-table {
117
+    margin-bottom: 0;
118
+  }
119
+
120
+  .snapshot-performance-table thead th {
121
+    font-size: .72rem;
122
+    text-transform: uppercase;
123
+    letter-spacing: .04em;
124
+    color: var(--bs-secondary-color);
125
+    font-weight: 600;
126
+    border-bottom-width: 1px;
127
+    white-space: nowrap;
128
+  }
129
+
130
+  .snapshot-performance-table tbody tr:hover {
131
+    background: rgba(var(--bs-primary-rgb), .04);
132
+  }
133
+
134
+  .snapshot-performance-table tbody td {
135
+    vertical-align: middle;
136
+    padding-top: .55rem;
137
+    padding-bottom: .55rem;
138
+  }
139
+
140
+  .snapshot-rank {
141
+    display: inline-flex;
142
+    align-items: center;
143
+    justify-content: center;
144
+    min-width: 1.75rem;
145
+    height: 1.75rem;
146
+    border-radius: 50%;
147
+    font-size: .75rem;
148
+    font-weight: 700;
149
+  }
150
+
151
+  .snapshot-rank--1 {
152
+    background: rgba(var(--bs-warning-rgb), .18);
153
+    color: var(--bs-warning);
154
+  }
155
+
156
+  .snapshot-rank--2 {
157
+    background: rgba(var(--bs-secondary-rgb), .16);
158
+    color: var(--bs-secondary-color);
159
+  }
160
+
161
+  .snapshot-rank--3 {
162
+    background: rgba(var(--bs-info-rgb), .16);
163
+    color: var(--bs-info);
164
+  }
165
+
166
+  .snapshot-rank--default {
167
+    background: rgba(var(--bs-body-color-rgb), .06);
168
+    color: var(--bs-secondary-color);
169
+    font-weight: 600;
170
+  }
171
+
172
+  .snapshot-value-qty {
173
+    font-variant-numeric: tabular-nums;
174
+    font-weight: 600;
175
+  }
176
+
177
+  .snapshot-value-incasso {
178
+    font-variant-numeric: tabular-nums;
179
+    font-weight: 600;
180
+    color: var(--bs-primary);
181
+  }
182
+
183
+  .snapshot-cucina-summary {
184
+    padding: .65rem .85rem;
185
+    border-radius: .5rem;
186
+    border: 1px dashed var(--bs-border-color);
187
+    background: rgba(var(--bs-body-color-rgb), .02);
188
+  }
189
+
190
+  .snapshot-metodo-icon {
191
+    width: 2.25rem;
192
+    height: 2.25rem;
193
+    display: inline-flex;
194
+    align-items: center;
195
+    justify-content: center;
196
+    border-radius: .45rem;
197
+    background: rgba(var(--bs-primary-rgb), .1);
198
+    color: var(--bs-primary);
199
+    flex-shrink: 0;
200
+  }
201
+</style>
202
+
203
+@include('_partials.status')
204
+
205
+<!-- <div class="d-flex flex-wrap gap-2 mb-3">
206
+  <a href="{{ route('chiusura-servizio.index') }}" class="btn btn-sm btn-label-secondary">
207
+    <i class="bx bx-chevron-left me-1"></i> Torna all'elenco
208
+  </a>
209
+</div> -->
210
+
211
+{{-- Dati chiusura --}}
212
+<div class="card mb-4">
213
+  <div class="card-header">
214
+    <h5 class="mb-0">Registro chiusura</h5>
215
+  </div>
216
+  <div class="card-body">
217
+    <div class="row g-3">
218
+      <div class="col-md-3">
219
+        <div class="snapshot-field-label">Data e ora</div>
220
+        <span class="fw-semibold">{{ $chiusura->chiusura_at?->format('d/m/Y H:i:s') ?? '—' }}</span>
221
+      </div>
222
+      <div class="col-md-3">
223
+        <div class="snapshot-field-label">Operatore</div>
224
+        <span class="fw-semibold">{{ $chiusura->user?->fullName ?? '—' }}</span>
225
+      </div>
226
+      <div class="col-md-3">
227
+        <div class="snapshot-field-label">Attività</div>
228
+        <span class="fw-semibold">{{ $chiusura->attivita?->nome ?? '—' }}</span>
229
+      </div>
230
+      <div class="col-md-3">
231
+        <div class="snapshot-field-label">Note</div>
232
+        <span>{{ $chiusura->note ?: '—' }}</span>
233
+      </div>
234
+    </div>
235
+  </div>
236
+</div>
237
+
238
+@if($hasSnapshot)
239
+  @if($periodoDa && $periodoA)
240
+    <div class="card mb-4 border-start border-3 border-info">
241
+      <div class="card-body py-3 d-flex flex-wrap align-items-center gap-3">
242
+        <div class="avatar avatar-sm">
243
+          <span class="avatar-initial rounded bg-label-info">
244
+            <i class="bx bx-time-five"></i>
245
+          </span>
246
+        </div>
247
+        <div>
248
+          <div class="fw-semibold">Periodo del servizio chiuso</div>
249
+          <div class="text-muted small">
250
+            Dal <strong>{{ $periodoDa->format('d/m/Y H:i') }}</strong>
251
+            al <strong>{{ $periodoA->format('d/m/Y H:i') }}</strong>
252
+            @if(!empty($snapshot['generato_at']))
253
+              <span class="d-block mt-1">Snapshot generato il {{ \Carbon\Carbon::parse($snapshot['generato_at'])->format('d/m/Y H:i:s') }}</span>
254
+            @endif
255
+          </div>
256
+        </div>
257
+      </div>
258
+    </div>
259
+  @endif
260
+
261
+  {{-- KPI snapshot --}}
262
+  <div class="row g-3">
263
+    <div class="col-md-4">
264
+      <div class="card h-100 border-start border-4 border-primary">
265
+        <div class="card-body">
266
+          <div class="servizio-kpi-label mb-1">Incasso</div>
267
+          <div class="servizio-kpi-value text-primary">
268
+            € {{ number_format($totaleIncasso, 2, ',', '.') }}
269
+          </div>
270
+        </div>
271
+      </div>
272
+    </div>
273
+    <div class="col-md-4">
274
+      <div class="card h-100">
275
+        <div class="card-body">
276
+          <div class="servizio-kpi-label mb-1">Transazioni</div>
277
+          <div class="servizio-kpi-value">
278
+            {{ number_format($totaleTransazioni, 0, ',', '.') }}
279
+          </div>
280
+        </div>
281
+      </div>
282
+    </div>
283
+    <div class="col-md-4">
284
+      <div class="card h-100">
285
+        <div class="card-body">
286
+          <div class="servizio-kpi-label mb-1">Piatti ordinati</div>
287
+          <div class="servizio-kpi-value">
288
+            {{ number_format($totPiattiOrdinati, 0, ',', '.') }}
289
+          </div>
290
+        </div>
291
+      </div>
292
+    </div>
293
+  </div>
294
+
295
+  {{-- Pagamenti per metodo --}}
296
+  <div class="card mt-4">
297
+    <div class="card-header">
298
+      <h5 class="mb-0">Pagamenti per metodo</h5>
299
+    </div>
300
+    <div class="card-body p-0">
301
+      @forelse($paymentSummary as $row)
302
+        <div class="d-flex justify-content-between align-items-center px-4 py-3 border-bottom servizio-metodo-row gap-3">
303
+          <div class="d-flex align-items-center gap-3">
304
+            <span class="snapshot-metodo-icon">
305
+              <i class="bx bx-wallet"></i>
306
+            </span>
307
+            <div>
308
+              <div class="fw-semibold">{{ $row['metodo'] ?? 'N/D' }}</div>
309
+              <small class="text-muted">
310
+                {{ number_format((int) ($row['count'] ?? 0), 0, ',', '.') }}
311
+                {{ (int) ($row['count'] ?? 0) === 1 ? 'transazione' : 'transazioni' }}
312
+              </small>
313
+            </div>
314
+          </div>
315
+          <div class="fs-5 fw-semibold text-primary">
316
+            € {{ number_format((float) ($row['totale'] ?? 0), 2, ',', '.') }}
317
+          </div>
318
+        </div>
319
+      @empty
320
+        <div class="p-4 text-muted">Nessun pagamento nello snapshot.</div>
321
+      @endforelse
322
+    </div>
323
+  </div>
324
+
325
+  {{-- Cucine e piatti --}}
326
+  <div class="card mt-4">
327
+    <div class="card-header">
328
+      <h5 class="mb-0">Cucine e piatti</h5>
329
+    </div>
330
+    <div class="card-body">
331
+      @if($cucineRows->isNotEmpty())
332
+        <ul class="nav nav-tabs mb-3 flex-nowrap overflow-auto" role="tablist">
333
+          @foreach($cucineRows as $index => $cucina)
334
+            <li class="nav-item" role="presentation">
335
+              <button
336
+                class="nav-link {{ $index === 0 ? 'active' : '' }}"
337
+                type="button"
338
+                data-bs-toggle="tab"
339
+                data-bs-target="#snapshot-cucina-{{ $index }}"
340
+                role="tab"
341
+                aria-selected="{{ $index === 0 ? 'true' : 'false' }}"
342
+              >
343
+                {{ $cucina['nome'] ?? 'Cucina' }}
344
+                <span class="text-muted small ms-1">
345
+                  ({{ number_format((int) ($cucina['tot_quantita'] ?? 0), 0, ',', '.') }})
346
+                </span>
347
+              </button>
348
+            </li>
349
+          @endforeach
350
+        </ul>
351
+
352
+        <div class="tab-content">
353
+          @foreach($cucineRows as $index => $cucina)
354
+            @php
355
+              $piatti = collect($cucina['piatti'] ?? []);
356
+              $piattiPerQuantita = $piatti
357
+                ->sortByDesc(fn ($p) => (int) ($p['quantita'] ?? 0))
358
+                ->values();
359
+              $piattiPerIncasso = $piatti
360
+                ->sortByDesc(fn ($p) => (float) ($p['totale'] ?? 0))
361
+                ->values();
362
+            @endphp
363
+            <div
364
+              class="tab-pane fade {{ $index === 0 ? 'show active' : '' }}"
365
+              id="snapshot-cucina-{{ $index }}"
366
+              role="tabpanel"
367
+            >
368
+              <div class="snapshot-cucina-summary d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
369
+                <span class="fw-semibold">
370
+                  <i class="bx bx-restaurant me-1 text-success"></i>
371
+                  {{ $cucina['nome'] ?? 'Cucina' }}
372
+                </span>
373
+                <span class="text-muted small">
374
+                  {{ number_format((int) ($cucina['tot_quantita'] ?? 0), 0, ',', '.') }} ord.
375
+                  · € {{ number_format((float) ($cucina['tot_importo'] ?? 0), 2, ',', '.') }}
376
+                </span>
377
+              </div>
378
+
379
+              <div class="snapshot-table-panel snapshot-table-panel--orario mb-3">
380
+                <div class="snapshot-table-panel__header">
381
+                  <i class="bx bx-line-chart text-warning"></i>
382
+                  <span class="fw-semibold">Affluenza oraria</span>
383
+                  <span class="text-muted small">ordinazioni per ora nel servizio chiuso</span>
384
+                </div>
385
+                <div
386
+                  id="chart-affluenza-cucina-{{ $index }}"
387
+                  class="px-2 pb-2"
388
+                  style="min-height: 240px;"
389
+                  data-cucina-index="{{ $index }}"
390
+                ></div>
391
+              </div>
392
+
393
+              <div class="row g-3">
394
+                <div class="col-lg-6">
395
+                  <div class="snapshot-table-panel snapshot-table-panel--qty">
396
+                    <div class="snapshot-table-panel__header">
397
+                      <i class="bx bx-trending-up text-success"></i>
398
+                      <span class="fw-semibold">Più venduti</span>
399
+                      <span class="text-muted small">per quantità</span>
400
+                    </div>
401
+                    <div class="table-responsive">
402
+                      <table class="table table-sm table-hover snapshot-performance-table">
403
+                        <thead>
404
+                          <tr>
405
+                            <th style="width: 3rem;">#</th>
406
+                            <th>Piatto</th>
407
+                            <th class="text-end">Qtà</th>
408
+                          </tr>
409
+                        </thead>
410
+                        <tbody>
411
+                          @forelse($piattiPerQuantita as $rank => $piatto)
412
+                            @php $pos = $rank + 1; @endphp
413
+                            <tr>
414
+                              <td>
415
+                                <span @class([
416
+                                  'snapshot-rank',
417
+                                  'snapshot-rank--1' => $pos === 1,
418
+                                  'snapshot-rank--2' => $pos === 2,
419
+                                  'snapshot-rank--3' => $pos === 3,
420
+                                  'snapshot-rank--default' => $pos > 3,
421
+                                ])>{{ $pos }}</span>
422
+                              </td>
423
+                              <td>{{ $piatto['nome'] ?? 'Piatto' }}</td>
424
+                              <td class="text-end snapshot-value-qty">{{ number_format((int) ($piatto['quantita'] ?? 0), 0, ',', '.') }}</td>
425
+                            </tr>
426
+                          @empty
427
+                            <tr>
428
+                              <td colspan="3" class="text-muted p-4 text-center">Nessun piatto nello snapshot.</td>
429
+                            </tr>
430
+                          @endforelse
431
+                        </tbody>
432
+                      </table>
433
+                    </div>
434
+                  </div>
435
+                </div>
436
+
437
+                <div class="col-lg-6">
438
+                  <div class="snapshot-table-panel snapshot-table-panel--incasso">
439
+                    <div class="snapshot-table-panel__header">
440
+                      <i class="bx bx-euro text-primary"></i>
441
+                      <span class="fw-semibold">Più redditizi</span>
442
+                      <span class="text-muted small">per incasso</span>
443
+                    </div>
444
+                    <div class="table-responsive">
445
+                      <table class="table table-sm table-hover snapshot-performance-table">
446
+                        <thead>
447
+                          <tr>
448
+                            <th style="width: 3rem;">#</th>
449
+                            <th>Piatto</th>
450
+                            <th class="text-end">Totale</th>
451
+                          </tr>
452
+                        </thead>
453
+                        <tbody>
454
+                          @forelse($piattiPerIncasso as $rank => $piatto)
455
+                            @php $pos = $rank + 1; @endphp
456
+                            <tr>
457
+                              <td>
458
+                                <span @class([
459
+                                  'snapshot-rank',
460
+                                  'snapshot-rank--1' => $pos === 1,
461
+                                  'snapshot-rank--2' => $pos === 2,
462
+                                  'snapshot-rank--3' => $pos === 3,
463
+                                  'snapshot-rank--default' => $pos > 3,
464
+                                ])>{{ $pos }}</span>
465
+                              </td>
466
+                              <td>{{ $piatto['nome'] ?? 'Piatto' }}</td>
467
+                              <td class="text-end snapshot-value-incasso">€ {{ number_format((float) ($piatto['totale'] ?? 0), 2, ',', '.') }}</td>
468
+                            </tr>
469
+                          @empty
470
+                            <tr>
471
+                              <td colspan="3" class="text-muted p-4 text-center">Nessun piatto nello snapshot.</td>
472
+                            </tr>
473
+                          @endforelse
474
+                        </tbody>
475
+                      </table>
476
+                    </div>
477
+                  </div>
478
+                </div>
479
+              </div>
480
+            </div>
481
+          @endforeach
482
+        </div>
483
+      @else
484
+        <div class="text-muted">Nessuna cucina nello snapshot.</div>
485
+      @endif
486
+    </div>
487
+  </div>
488
+@else
489
+  <div class="alert alert-warning">
490
+    Nessuno snapshot salvato per questa chiusura. I totali non erano ancora registrati al momento della chiusura.
491
+  </div>
492
+@endif
493
+@endsection
494
+
495
+@section('page-script')
496
+@if($cucineRows->isNotEmpty() && ($heatmapHourLabels ?? collect())->isNotEmpty())
497
+<script>
498
+  document.addEventListener('DOMContentLoaded', function () {
499
+    if (typeof ApexCharts === 'undefined') {
500
+      return;
501
+    }
502
+
503
+    const hourLabels = @json($heatmapHourLabels->values()->all());
504
+    const cucineOrario = @json($cucineRows->map(fn ($c) => $c['ordini_per_ora'] ?? [])->values()->all());
505
+    const lineCharts = {};
506
+
507
+    function isDarkTheme() {
508
+      const theme = document.documentElement.getAttribute('data-bs-theme');
509
+      if (theme === 'dark') return true;
510
+      if (theme === 'light') return false;
511
+      return window.matchMedia('(prefers-color-scheme: dark)').matches;
512
+    }
513
+
514
+    function getThemeColors() {
515
+      const css = getComputedStyle(document.documentElement);
516
+      const cfg = window.config?.colors || {};
517
+
518
+      return {
519
+        labelColor: cfg.textMuted || (css.getPropertyValue('--bs-secondary-color') || '#6c757d').trim(),
520
+        borderColor: cfg.borderColor || (css.getPropertyValue('--bs-border-color') || '#e9ecef').trim(),
521
+        lineColor: cfg.warning || (css.getPropertyValue('--bs-warning') || '#ffab1d').trim(),
522
+        fontFamily: window.config?.fontFamily || 'inherit'
523
+      };
524
+    }
525
+
526
+    function buildLineOptions(data) {
527
+      const dark = isDarkTheme();
528
+      const colors = getThemeColors();
529
+
530
+      return {
531
+        chart: {
532
+          type: 'area',
533
+          height: 260,
534
+          parentHeightOffset: 0,
535
+          toolbar: { show: false },
536
+          foreColor: colors.labelColor,
537
+          zoom: { enabled: false },
538
+          dropShadow: {
539
+            enabled: true,
540
+            top: 8,
541
+            left: 0,
542
+            blur: 3,
543
+            color: colors.lineColor,
544
+            opacity: 0.12
545
+          }
546
+        },
547
+        theme: { mode: dark ? 'dark' : 'light' },
548
+        series: [{
549
+          name: 'Ordinazioni',
550
+          data: data
551
+        }],
552
+        colors: [colors.lineColor],
553
+        fill: {
554
+          type: 'gradient',
555
+          gradient: {
556
+            shadeIntensity: 0.35,
557
+            opacityFrom: dark ? 0.35 : 0.45,
558
+            opacityTo: 0.05,
559
+            stops: [0, 90, 100]
560
+          }
561
+        },
562
+        stroke: {
563
+          curve: 'smooth',
564
+          width: 3
565
+        },
566
+        markers: {
567
+          size: 4,
568
+          strokeWidth: 2,
569
+          strokeColors: colors.lineColor,
570
+          hover: { size: 6 }
571
+        },
572
+        dataLabels: { enabled: false },
573
+        legend: { show: false },
574
+        xaxis: {
575
+          categories: hourLabels,
576
+          labels: {
577
+            style: {
578
+              colors: colors.labelColor,
579
+              fontSize: '12px',
580
+              fontFamily: colors.fontFamily
581
+            }
582
+          },
583
+          axisBorder: { show: false },
584
+          axisTicks: { show: false }
585
+        },
586
+        yaxis: {
587
+          min: 0,
588
+          forceNiceScale: true,
589
+          labels: {
590
+            style: {
591
+              colors: colors.labelColor,
592
+              fontSize: '12px',
593
+              fontFamily: colors.fontFamily
594
+            },
595
+            formatter: function (val) { return Math.round(val); }
596
+          }
597
+        },
598
+        grid: {
599
+          borderColor: colors.borderColor,
600
+          strokeDashArray: 6,
601
+          padding: { top: 0, right: 8, bottom: 0, left: 8 }
602
+        },
603
+        tooltip: {
604
+          theme: dark ? 'dark' : 'light',
605
+          x: { formatter: function (_, opts) { return 'Ore ' + (hourLabels[opts.dataPointIndex] || ''); } },
606
+          y: {
607
+            formatter: function (val) {
608
+              return Number(val || 0).toLocaleString('it-IT') + ' ordinazioni';
609
+            }
610
+          }
611
+        }
612
+      };
613
+    }
614
+
615
+    function renderLineChart(index) {
616
+      const el = document.getElementById('chart-affluenza-cucina-' + index);
617
+      if (!el || lineCharts[index]) {
618
+        return;
619
+      }
620
+
621
+      const data = cucineOrario[index] || [];
622
+      lineCharts[index] = new ApexCharts(el, buildLineOptions(data));
623
+      lineCharts[index].render();
624
+    }
625
+
626
+    function renderVisibleLineCharts() {
627
+      document.querySelectorAll('.tab-pane.active [data-cucina-index]').forEach(function (el) {
628
+        renderLineChart(parseInt(el.getAttribute('data-cucina-index'), 10));
629
+      });
630
+    }
631
+
632
+    function refreshLineChartsTheme() {
633
+      Object.keys(lineCharts).forEach(function (key) {
634
+        const idx = parseInt(key, 10);
635
+        if (lineCharts[idx]) {
636
+          lineCharts[idx].destroy();
637
+          delete lineCharts[idx];
638
+        }
639
+      });
640
+      renderVisibleLineCharts();
641
+    }
642
+
643
+    renderVisibleLineCharts();
644
+
645
+    document.querySelectorAll('[data-bs-toggle="tab"][data-bs-target^="#snapshot-cucina-"]').forEach(function (btn) {
646
+      btn.addEventListener('shown.bs.tab', function (e) {
647
+        const target = e.target.getAttribute('data-bs-target');
648
+        const match = target && target.match(/snapshot-cucina-(\d+)/);
649
+        if (match) {
650
+          renderLineChart(parseInt(match[1], 10));
651
+        }
652
+      });
653
+    });
654
+
655
+    const themeObserver = new MutationObserver(refreshLineChartsTheme);
656
+    themeObserver.observe(document.documentElement, {
657
+      attributes: true,
658
+      attributeFilter: ['data-bs-theme']
659
+    });
660
+  });
661
+</script>
662
+@endif
663
+@endsection

+ 2
- 1
resources/views/layouts/sections/navbar/navbar-partial.blade.php Parādīt failu

@@ -366,12 +366,13 @@ class="layout-menu-toggle navbar-nav align-items-xl-center me-4 me-xl-0{{ isset(
366 366
                                     $binding_token = \Illuminate\Support\Facades\Cookie::get('binding_token');
367 367
                                     $puntoVendita = \App\Models\Dispositivo::findByBindingCookieToken($binding_token);
368 368
                                     ?>
369
-                               
369
+                                    @if($puntoVendita)
370 370
                                     <a href="{{ route('punto-vendita.show', ['punto_vendita_id' => $puntoVendita->id]) }}" class="mx-5 btn text-bg-primary px-2 py-1">
371 371
                                       Torna a {{ $puntoVendita->nome }}
372 372
                                       <i class="bx bx-arrow-back rotate-180 float-end"></i>
373 373
                                     </a>
374 374
                                     @endif
375
+                                    @endif
375 376
                                     <!-- END - Accedi a punto vendita se in Session -->
376 377
 
377 378
                                     <!-- Attività -->

+ 74
- 0
resources/views/mappa_dispositivi/_partials/device_card.blade.php Parādīt failu

@@ -0,0 +1,74 @@
1
+@php
2
+  $searchText = implode(' ', array_filter([
3
+    $device['nome'] ?? '',
4
+    $device['attivita_nome'] ?? '',
5
+    $device['ubicazione'] ?? '',
6
+    $device['tipo'] ?? '',
7
+    (string) ($device['id'] ?? ''),
8
+  ]));
9
+  $attivitaId = $device['attivita_id'] ?? null;
10
+  $attivitaLabel = $device['attivita_nome'] ?? 'Senza attività';
11
+  $editUrl = ($device['is_punto_vendita'] ?? false) && ($puoModificarePv ?? false)
12
+    ? route('punto-vendita.edit', ['punto_vendita_id' => $device['id']])
13
+    : null;
14
+@endphp
15
+
16
+<div class="col-sm-6 col-lg-4 col-xl-3">
17
+  <div
18
+    class="card md-device-card h-100"
19
+    data-tipo="{{ $device['tipo'] ?? '_altro' }}"
20
+    data-attivita-id="{{ $attivitaId ?? '0' }}"
21
+    data-attivo="{{ !empty($device['is_attivo']) ? '1' : '0' }}"
22
+    data-search="{{ $searchText }}"
23
+  >
24
+    <div class="card-body d-flex flex-column">
25
+      <div class="d-flex align-items-start justify-content-between gap-2 mb-2">
26
+        <div class="d-flex align-items-center gap-2 min-w-0">
27
+          <span class="avatar avatar-sm">
28
+            <span class="avatar-initial rounded bg-label-primary">
29
+              <i class="{{ $tipoMeta['icon'] ?? 'bx bx-devices' }}"></i>
30
+            </span>
31
+          </span>
32
+          <div class="min-w-0">
33
+            <h6 class="mb-0 text-truncate" title="{{ $device['nome'] }}">{{ $device['nome'] }}</h6>
34
+            <small class="text-muted">#{{ $device['id'] }}</small>
35
+          </div>
36
+        </div>
37
+        @if(!empty($device['is_attivo']))
38
+          <span class="badge bg-label-success flex-shrink-0">Attivo</span>
39
+        @else
40
+          <span class="badge bg-label-danger flex-shrink-0">Non attivo</span>
41
+        @endif
42
+      </div>
43
+
44
+      <ul class="list-unstyled small mb-3 flex-grow-1">
45
+        <li class="mb-2 d-flex align-items-start gap-2">
46
+          <i class="bx bx-category-alt text-muted mt-1"></i>
47
+          <span class="text-truncate" title="{{ $attivitaLabel }}">{{ $attivitaLabel }}</span>
48
+        </li>
49
+        <li class="mb-2 d-flex align-items-start gap-2">
50
+          <i class="bx bx-map-pin text-muted mt-1"></i>
51
+          <span>{{ $device['ubicazione'] ?: '—' }}</span>
52
+        </li>
53
+        @if($device['is_punto_vendita'] ?? false)
54
+          <li class="d-flex align-items-start gap-2">
55
+            <i class="bx bx-link text-muted mt-1"></i>
56
+            <span>
57
+              @if(!empty($device['occupato']))
58
+                <span class="badge bg-label-info">Abbinato</span>
59
+              @else
60
+                <span class="badge bg-label-secondary">Libero</span>
61
+              @endif
62
+            </span>
63
+          </li>
64
+        @endif
65
+      </ul>
66
+
67
+      @if($editUrl)
68
+        <a href="{{ $editUrl }}" class="btn btn-sm btn-label-primary w-100">
69
+          <i class="bx bx-edit-alt me-1"></i> Gestisci
70
+        </a>
71
+      @endif
72
+    </div>
73
+  </div>
74
+</div>

+ 192
- 626
resources/views/mappa_dispositivi/index.blade.php Parādīt failu

@@ -1,686 +1,252 @@
1 1
 <?php
2
-use App\Models\Role;
2
+use App\Models\Dispositivo;
3 3
 use Illuminate\Support\Facades\Auth;
4 4
 ?>
5 5
 @php
6 6
 $configData = Helper::appClasses();
7
+$dispositiviByTipo = $dispositiviByTipo ?? [];
8
+$tipiDispositivo = $tipiDispositivo ?? Dispositivo::getTipoDispositivo();
9
+$attivitaList = $attivitaList ?? collect();
10
+$totaleDispositivi = collect($dispositiviByTipo)->flatten(1)->count();
11
+$totaleAttivi = collect($dispositiviByTipo)->flatten(1)->where('is_attivo', true)->count();
12
+$puoModificarePv = Auth::user()->can('edit-punto_vendita');
7 13
 @endphp
8 14
 
9 15
 @extends('layouts/layoutMaster')
10 16
 
11 17
 @section('title', 'Mappa Dispositivi')
12 18
 
13
-@section('vendor-style')
14
-@vite([
15
-'resources/assets/vendor/libs/datatables-bs5/datatables.bootstrap5.scss',
16
-'resources/assets/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.scss',
17
-'resources/assets/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.scss',
18
-'resources/assets/vendor/libs/flatpickr/flatpickr.scss',
19
-'resources/assets/vendor/libs/@form-validation/form-validation.scss'
20
-])
21
-@endsection
22
-
23
-<!-- Vendor Scripts -->
24
-@section('vendor-script')
25
-@vite([
26
-'resources/assets/vendor/libs/moment/moment.js',
27
-'resources/assets/vendor/libs/flatpickr/flatpickr.js',
28
-'resources/assets/vendor/libs/@form-validation/popular.js',
29
-'resources/assets/vendor/libs/@form-validation/bootstrap5.js',
30
-'resources/assets/vendor/libs/@form-validation/auto-focus.js',
31
-'resources/assets/vendor/libs/@form-validation/popular.js',
32
-'resources/assets/vendor/libs/@form-validation/bootstrap5.js',
33
-'resources/assets/vendor/libs/@form-validation/auto-focus.js',
34
-'resources/assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js'
35
-])
36
-@endsection
37
-
38 19
 @section('pageTitle')
39 20
 <div class="d-flex flex-column">
40
-  <h4 class="mb-1"> 
41
-    <i class="bx bx-devices"></i> Mappa Dispositivi</h4>
21
+  <h4 class="mb-1">
22
+    <i class="bx bx-devices"></i> Mappa dispositivi
23
+  </h4>
24
+  <p class="text-muted small mb-0">Panoramica per tipologia · {{ $totaleDispositivi }} dispositivi · {{ $totaleAttivi }} attivi</p>
42 25
 </div>
43 26
 @endsection
44 27
 
45 28
 @section('content')
46
-<style>
47
-  div.upload button:first-child{
48
-    display: none !important;
49
-  }
50
-
51
-.bx-chef-hat{
52
-  --svg: url("data:image/svg+xml,%3csvg width='24' height='24' fill='currentColor' viewBox='0 0 24 24' transform='' xmlns='http://www.w3.org/2000/svg'%3e%3c!--Boxicons v3.0.8 https://boxicons.com %7c License https://docs.boxicons.com/free--%3e%3cpath d='M17.13 5.54C16.33 3.42 14.32 2 12 2S7.67 3.42 6.87 5.54A5.506 5.506 0 0 0 2 11c0 2.07 1.18 3.95 3 4.88V18c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-2.12c1.82-.93 3-2.81 3-4.88 0-2.82-2.13-5.15-4.87-5.46m.53 8.75c-.4.14-.67.52-.67.94v1.78H7v-1.78c0-.42-.27-.8-.67-.94-1.4-.5-2.33-1.82-2.33-3.28 0-1.93 1.57-3.5 3.42-3.5.04 0 .14.01.18.02.49 0 .9-.31 1-.78.36-1.61 1.76-2.73 3.41-2.73s3.05 1.12 3.41 2.73c.1.47.51.81 1 .78.06 0 .12 0 .09-.01 1.93 0 3.5 1.57 3.5 3.5 0 1.47-.94 2.79-2.33 3.28ZM5 20h14v2H5z'%3e%3c/path%3e%3c/svg%3e");
53
-}
54
-
55
-  .topo-card{
56
-    border: 1px solid rgba(67, 89, 113, .15);
57
-    border-radius: 14px;
58
-    background: linear-gradient(180deg, rgba(255,255,255,.85), rgba(255,255,255,.65));
59
-    backdrop-filter: blur(8px);
60
-  }
61
-
62
-  .topo-title{
63
-    font-weight: 700;
64
-    letter-spacing: .2px;
65
-  }
66
-
67
-  #topologyBoard{
68
-    min-height: 480px;
69
-    max-height: 72vh;
70
-    width: 100%;
71
-    border-radius: 14px;
72
-    background:
73
-      radial-gradient(1200px 420px at 10% 0%, rgba(105,108,255,.10), rgba(105,108,255,0)),
74
-      radial-gradient(900px 380px at 100% 15%, rgba(3,195,236,.12), rgba(3,195,236,0)),
75
-      linear-gradient(180deg, rgba(255,255,255,.9), rgba(255,255,255,.65));
76
-    border: 1px solid rgba(67, 89, 113, .15);
77
-    overflow: hidden;
78
-    padding: 14px;
79
-    overflow: auto;
80
-  }
81
-
82
-  .topo-board-grid{
83
-    display: grid;
84
-    gap: 12px;
85
-    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
86
-    align-items: start;
87
-  }
88
-
89
-  .topo-group{
90
-    border-radius: 14px;
91
-    border: 1px solid rgba(67, 89, 113, .14);
92
-    background: rgba(255,255,255,.72);
93
-    box-shadow: 0 10px 30px rgba(17, 24, 39, .06);
94
-    overflow: hidden;
95
-  }
96
-
97
-  .topo-group-header{
98
-    padding: 10px 12px;
99
-    background: linear-gradient(135deg, rgba(105,108,255,.18), rgba(3,195,236,.10));
100
-    border-bottom: 1px solid rgba(67, 89, 113, .12);
101
-    display: flex;
102
-    align-items: center;
103
-    justify-content: space-between;
104
-    gap: 10px;
105
-  }
106
-
107
-  .topo-group-title{
108
-    font-weight: 800;
109
-    font-size: .95rem;
110
-    letter-spacing: .2px;
111
-    color: #25314a;
112
-    overflow: hidden;
113
-    text-overflow: ellipsis;
114
-    white-space: nowrap;
115
-  }
116
-
117
-  .topo-group-count{
118
-    font-size: .75rem;
119
-    color: rgba(37, 49, 74, .75);
120
-    background: rgba(255,255,255,.65);
121
-    border: 1px solid rgba(67, 89, 113, .14);
122
-    padding: 3px 8px;
123
-    border-radius: 999px;
124
-    flex: 0 0 auto;
125
-  }
126
-
127
-  .topo-group-body{
128
-    padding: 10px 12px 12px;
129
-  }
29
+@include('_partials.status')
130 30
 
131
-  .topo-device{
132
-    display: flex;
133
-    align-items: center;
134
-    justify-content: space-between;
135
-    gap: 10px;
136
-    padding: 8px 10px;
137
-    border-radius: 12px;
138
-    border: 1px solid rgba(67, 89, 113, .12);
139
-    background: rgba(255,255,255,.86);
140
-    cursor: pointer;
141
-    transition: transform .08s ease, box-shadow .08s ease, border-color .08s ease;
142
-    user-select: none;
31
+<style>
32
+  .md-device-card {
33
+    transition: box-shadow .15s ease, transform .15s ease;
34
+    height: 100%;
143 35
   }
144
-
145
-  .topo-device:hover{
36
+  .md-device-card:hover {
37
+    box-shadow: 0 8px 24px rgba(67, 89, 113, .12);
146 38
     transform: translateY(-1px);
147
-    box-shadow: 0 10px 22px rgba(17, 24, 39, .08);
148
-    border-color: rgba(105,108,255,.35);
149
-  }
150
-
151
-  .topo-device + .topo-device{
152
-    margin-top: 8px;
153
-  }
154
-
155
-  .topo-device-left{
156
-    min-width: 0;
157
-  }
158
-
159
-  .topo-device-name{
160
-    font-weight: 800;
161
-    font-size: .88rem;
162
-    color: #25314a;
163
-    overflow: hidden;
164
-    text-overflow: ellipsis;
165
-    white-space: nowrap;
166
-    max-width: 100%;
167
-  }
168
-
169
-  .topo-device-meta{
170
-    font-size: .76rem;
171
-    color: rgba(37, 49, 74, .70);
172
-    overflow: hidden;
173
-    text-overflow: ellipsis;
174
-    white-space: nowrap;
175
-  }
176
-
177
-  .topo-pill{
178
-    display: inline-flex;
179
-    align-items: center;
180
-    gap: 6px;
181
-    padding: 3px 8px;
182
-    border-radius: 999px;
183
-    font-size: .74rem;
184
-    font-weight: 700;
185
-    border: 1px solid rgba(67, 89, 113, .12);
186
-    background: rgba(255,255,255,.7);
187
-    flex: 0 0 auto;
188
-  }
189
-
190
-  .topo-dot{
191
-    width: 8px;
192
-    height: 8px;
193
-    border-radius: 999px;
194 39
   }
195
-
196
-  .topo-dot--on{ background: #03c3ec; }
197
-  .topo-dot--off{ background: #ff3e1d; }
198
-
199
-  .topo-legend-dot{
200
-    width: 10px;
201
-    height: 10px;
202
-    border-radius: 999px;
203
-    display: inline-block;
204
-    margin-right: 8px;
205
-    box-shadow: 0 0 0 3px rgba(0,0,0,.03);
206
-  }
207
-
208
-  .topo-kpi{
209
-    border: 1px solid rgba(67, 89, 113, .12);
210
-    border-radius: 12px;
211
-    padding: 10px 12px;
212
-    background: rgba(255,255,255,.7);
213
-  }
214
-
215
-  .topo-act-card{
216
-    border-radius: 16px;
217
-    border: 1px solid rgba(67, 89, 113, .14);
218
-    background: rgba(255,255,255,.78);
219
-    box-shadow: 0 12px 34px rgba(17, 24, 39, .07);
220
-    overflow: hidden;
221
-    grid-column: 1 / -1;
222
-  }
223
-
224
-  .topo-act-header{
225
-    padding: 14px 16px;
226
-    background: linear-gradient(120deg, rgba(105,108,255,.20), rgba(255,171,0,.12));
227
-    border-bottom: 1px solid rgba(67, 89, 113, .12);
228
-    display: flex;
229
-    flex-wrap: wrap;
230
-    align-items: center;
231
-    justify-content: space-between;
232
-    gap: 10px;
233
-  }
234
-
235
-  .topo-act-title{
236
-    font-weight: 800;
237
-    font-size: 1.05rem;
238
-    color: #25314a;
239
-    display: flex;
240
-    align-items: center;
241
-    gap: 8px;
242
-  }
243
-
244
-  .topo-act-body{
245
-    padding: 14px 16px 16px;
246
-    display: grid;
247
-    gap: 14px;
248
-  }
249
-
250
-  @media (min-width: 992px) {
251
-    .topo-act-body{
252
-      grid-template-columns: 1fr 1fr;
253
-      align-items: start;
254
-    }
255
-  }
256
-
257
-  .topo-section{
258
-    border: 1px dashed rgba(67, 89, 113, .18);
259
-    border-radius: 12px;
260
-    padding: 12px;
261
-    background: rgba(255,255,255,.5);
262
-  }
263
-
264
-  .topo-section-title{
265
-    font-size: .72rem;
266
-    font-weight: 800;
267
-    text-transform: uppercase;
268
-    letter-spacing: .06em;
269
-    color: rgba(37, 49, 74, .62);
270
-    margin-bottom: 10px;
271
-    display: flex;
272
-    align-items: center;
273
-    gap: 6px;
274
-  }
275
-
276
-  .topo-cucina-block{
277
-    border-radius: 12px;
278
-    border: 1px solid rgba(67, 89, 113, .12);
279
-    background: rgba(255,255,255,.88);
280
-    padding: 10px 10px 8px;
281
-    margin-bottom: 10px;
282
-  }
283
-
284
-  .topo-cucina-block:last-child{ margin-bottom: 0; }
285
-
286
-  .topo-cucina-name{
287
-    font-weight: 800;
288
-    font-size: .88rem;
289
-    color: #25314a;
290
-    margin-bottom: 8px;
291
-    display: flex;
292
-    align-items: center;
293
-    gap: 6px;
294
-    flex-wrap: wrap;
295
-  }
296
-
297
-  .topo-arrow{
298
-    color: rgba(37, 49, 74, .45);
299
-    font-size: .85rem;
300
-    margin: 0 2px;
301
-  }
302
-
303
-  .topo-stampante-wrap{
304
-    padding-left: 8px;
305
-    border-left: 3px solid rgba(105,108,255,.35);
306
-    margin-top: 4px;
307
-  }
308
-
309
-  .topo-loc-pill{
310
-    font-size: .65rem;
311
-    font-weight: 700;
312
-    padding: 2px 7px;
313
-    border-radius: 999px;
314
-    background: rgba(3,195,236,.14);
315
-    color: #0b5f6f;
316
-    border: 1px solid rgba(3,195,236,.25);
40
+  .md-device-card.is-hidden-by-filter {
41
+    display: none !important;
317 42
   }
318
-
319
-  .topo-board-grid--activities{
320
-    display: flex;
321
-    flex-direction: column;
322
-    gap: 16px;
43
+  .md-tab-empty {
44
+    border: 1px dashed rgba(67, 89, 113, .25);
45
+    border-radius: .75rem;
46
+    padding: 2.5rem 1rem;
47
+    text-align: center;
48
+    color: var(--bs-secondary-color);
323 49
   }
324 50
 </style>
325 51
 
326
-@include('_partials.status')
327
-
328
-@php
329
-  /** @var array<int, array<string, mixed>> $topologyByAttivita */
330
-  $topologyByAttivita = $topologyByAttivita ?? [];
331
-@endphp
52
+<div class="card mb-4">
53
+  <div class="card-body">
54
+    <div class="row g-3 align-items-end">
55
+      <div class="col-md-4">
56
+        <label class="form-label small mb-1" for="mdFilterSearch">Cerca</label>
57
+        <input type="text" class="form-control" id="mdFilterSearch" placeholder="Nome, attività, ubicazione...">
58
+      </div>
59
+      <div class="col-md-3">
60
+        <label class="form-label small mb-1" for="mdFilterAttivita">Attività</label>
61
+        <select class="form-select" id="mdFilterAttivita">
62
+          <option value="all">Tutte</option>
63
+          @foreach($attivitaList as $att)
64
+            <option value="{{ $att->id }}">{{ $att->nome }}</option>
65
+          @endforeach
66
+          <option value="0">Senza attività</option>
67
+        </select>
68
+      </div>
69
+      <div class="col-md-3">
70
+        <label class="form-label small mb-1" for="mdFilterStato">Stato</label>
71
+        <select class="form-select" id="mdFilterStato">
72
+          <option value="all">Tutti</option>
73
+          <option value="1">Solo attivi</option>
74
+          <option value="0">Solo non attivi</option>
75
+        </select>
76
+      </div>
77
+      <div class="col-md-2">
78
+        <button type="button" class="btn btn-outline-secondary w-100" id="mdFilterReset">
79
+          <i class="bx bx-refresh me-1"></i> Reset
80
+        </button>
81
+      </div>
82
+    </div>
83
+  </div>
84
+</div>
332 85
 
333
-<div class="row g-4 mt-2">
334
-  <div class="col-12">
335
-    <div class="topo-card p-3 p-md-4">
336
-      <div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
337
-        <div>
338
-          <div class="topo-title fs-5">
339
-            <i class="bx bx-git-repo-forked me-1"></i> Topografica (per attività)
340
-          </div>
341
-          <div class="text-muted small">
342
-            Bozza: ogni <strong>attività</strong> mostra i dispositivi di punto vendita (cassa / kiosk / cameriere), le <strong>cucine</strong> con la stampante collegata, e gli altri dispositivi. Il <span class="topo-loc-pill d-inline-block">luogo</span> è l’<code>ubicazione</code> del dispositivo, se presente.
86
+<div class="nav-align-top">
87
+  <ul class="nav nav-tabs nav-fill flex-nowrap overflow-auto" role="tablist">
88
+    @foreach($tipiDispositivo as $tipoKey => $tipoMeta)
89
+      @php
90
+        $countTipo = count($dispositiviByTipo[$tipoKey] ?? []);
91
+      @endphp
92
+      <li class="nav-item" role="presentation">
93
+        <button
94
+          type="button"
95
+          class="nav-link {{ $loop->first ? 'active' : '' }}"
96
+          role="tab"
97
+          data-bs-toggle="tab"
98
+          data-bs-target="#md-tab-{{ $tipoKey }}"
99
+          aria-controls="md-tab-{{ $tipoKey }}"
100
+          aria-selected="{{ $loop->first ? 'true' : 'false' }}"
101
+        >
102
+          <i class="{{ $tipoMeta['icon'] ?? 'bx bx-devices' }} me-1"></i>
103
+          {{ $tipoMeta['label'] }}
104
+          <span class="badge bg-label-primary ms-1 md-tab-count" data-tipo="{{ $tipoKey }}">{{ $countTipo }}</span>
105
+        </button>
106
+      </li>
107
+    @endforeach
108
+    @if(!empty($dispositiviByTipo['_altro']))
109
+      <li class="nav-item" role="presentation">
110
+        <button
111
+          type="button"
112
+          class="nav-link"
113
+          role="tab"
114
+          data-bs-toggle="tab"
115
+          data-bs-target="#md-tab-altro"
116
+          aria-controls="md-tab-altro"
117
+          aria-selected="false"
118
+        >
119
+          <i class="bx bx-question-mark me-1"></i>
120
+          Altro
121
+          <span class="badge bg-label-secondary ms-1 md-tab-count" data-tipo="_altro">{{ count($dispositiviByTipo['_altro']) }}</span>
122
+        </button>
123
+      </li>
124
+    @endif
125
+  </ul>
126
+
127
+  <div class="tab-content border border-top-0 rounded-bottom p-3 p-md-4 bg-body">
128
+    @foreach($tipiDispositivo as $tipoKey => $tipoMeta)
129
+      @php $devices = $dispositiviByTipo[$tipoKey] ?? []; @endphp
130
+      <div
131
+        class="tab-pane fade {{ $loop->first ? 'show active' : '' }}"
132
+        id="md-tab-{{ $tipoKey }}"
133
+        role="tabpanel"
134
+      >
135
+        @if(count($devices))
136
+          <div class="row g-3 md-device-grid" data-tipo-pane="{{ $tipoKey }}">
137
+            @foreach($devices as $device)
138
+              @include('mappa_dispositivi._partials.device_card', [
139
+                'device' => $device,
140
+                'tipoMeta' => $tipoMeta,
141
+                'puoModificarePv' => $puoModificarePv,
142
+              ])
143
+            @endforeach
343 144
           </div>
344
-        </div>
345
-        <div class="d-flex flex-wrap align-items-center gap-2">
346
-          <div class="topo-kpi">
347
-            <div class="text-muted small">Attività in vista</div>
348
-            <div class="fw-bold" id="kpiActivities">-</div>
145
+          <div class="md-tab-empty d-none" data-empty-for="{{ $tipoKey }}">
146
+            <i class="bx bx-filter-alt fs-2 d-block mb-2"></i>
147
+            Nessun dispositivo corrisponde ai filtri.
349 148
           </div>
350
-          <div class="topo-kpi">
351
-            <div class="text-muted small">Dispositivi in vista</div>
352
-            <div class="fw-bold" id="kpiDevices">-</div>
149
+        @else
150
+          <div class="md-tab-empty">
151
+            <i class="{{ $tipoMeta['icon'] ?? 'bx bx-devices' }} fs-2 d-block mb-2"></i>
152
+            Nessun dispositivo di tipo {{ strtolower($tipoMeta['label']) }}.
353 153
           </div>
354
-          <button type="button" class="btn btn-sm btn-outline-primary" id="btnTopoFit">
355
-            <i class="bx bx-fullscreen me-1"></i> Fit
356
-          </button>
357
-          <button type="button" class="btn btn-sm btn-outline-secondary" id="btnTopoReset">
358
-            <i class="bx bx-refresh me-1"></i> Reset filtri
359
-          </button>
360
-        </div>
154
+        @endif
361 155
       </div>
362
-
363
-      <div class="row g-3">
364
-        <div class="col-12 col-xl-3">
365
-          <div class="card shadow-none border">
366
-            <div class="card-body">
367
-              <div class="fw-semibold mb-2">Filtri</div>
368
-
369
-              <label class="form-label mb-1 small">Attività</label>
370
-              <select class="form-select mb-3" id="filterAttivita"></select>
371
-
372
-              <label class="form-label mb-1 small">Stato</label>
373
-              <select class="form-select mb-3" id="filterAttivo">
374
-                <option value="all">Tutti</option>
375
-                <option value="1">Attivi</option>
376
-                <option value="0">Non attivi</option>
377
-              </select>
378
-
379
-              <label class="form-label mb-1 small">Cerca</label>
380
-              <input type="text" class="form-control mb-3" id="filterSearch" placeholder="Attività, cucina o dispositivo..." />
381
-
382
-              <div class="fw-semibold mb-2">Legenda</div>
383
-              <div class="d-flex flex-column gap-2 small">
384
-                <div><span class="topo-legend-dot" style="background:#03c3ec"></span> Attivo</div>
385
-                <div><span class="topo-legend-dot" style="background:#ff3e1d"></span> Non attivo</div>
386
-                <div><span class="topo-legend-dot" style="background:#696cff"></span> Sezione / collegamento</div>
387
-              </div>
388
-
389
-              <hr class="my-3" />
390
-              <div class="text-muted small">
391
-                Tip: click su un dispositivo per il riepilogo in basso.
392
-              </div>
393
-            </div>
394
-          </div>
156
+    @endforeach
157
+
158
+    @if(!empty($dispositiviByTipo['_altro']))
159
+      <div class="tab-pane fade" id="md-tab-altro" role="tabpanel">
160
+        <div class="row g-3 md-device-grid" data-tipo-pane="_altro">
161
+          @foreach($dispositiviByTipo['_altro'] as $device)
162
+            @include('mappa_dispositivi._partials.device_card', [
163
+              'device' => $device,
164
+              'tipoMeta' => ['label' => $device['tipo'] ?? 'Sconosciuto', 'icon' => 'bx bx-devices'],
165
+              'puoModificarePv' => $puoModificarePv,
166
+            ])
167
+          @endforeach
395 168
         </div>
396
-
397
-        <div class="col-12 col-xl-9">
398
-          <div id="topologyBoard">
399
-            <div class="topo-board-grid--activities" id="topologyBoardGrid"></div>
400
-          </div>
401
-          <div class="text-muted small mt-2" id="topologyHint"></div>
169
+        <div class="md-tab-empty d-none" data-empty-for="_altro">
170
+          <i class="bx bx-filter-alt fs-2 d-block mb-2"></i>
171
+          Nessun dispositivo corrisponde ai filtri.
402 172
         </div>
403 173
       </div>
404
-    </div>
405
-  </div>
406
-</div>
407
-
408
-<div class="row justify-content-center" style="display: none;">
409
-  <div class="col-xxl mb-4 mt-2">
410
-    <div class="card">
411
-      <div class="card-body">
412
-        {{ $dataTable_dispositivo->table(['class' => 'table table-bordered']) }}
413
-      </div>
414
-    </div>
174
+    @endif
415 175
   </div>
416 176
 </div>
417
-
418
-
419
-
420
-
421 177
 @endsection
422 178
 
423 179
 @section('page-script')
424
-
425
-{{$dataTable_dispositivo->scripts(attributes: ['type' => 'module'])}}
426
-
427 180
 <script type="module">
428
-  const TOPO_BY_ATTIVITA = @json($topologyByAttivita);
429
-
430
-  const elBoardGrid = document.getElementById('topologyBoardGrid');
431
-  const elHint = document.getElementById('topologyHint');
432
-  const elKpiActivities = document.getElementById('kpiActivities');
433
-  const elKpiDevices = document.getElementById('kpiDevices');
434
-
435
-  const elAttivita = document.getElementById('filterAttivita');
436
-  const elAttivo = document.getElementById('filterAttivo');
437
-  const elSearch = document.getElementById('filterSearch');
438
-
439
-  const btnFit = document.getElementById('btnTopoFit');
440
-  const btnReset = document.getElementById('btnTopoReset');
441
-
442
-  function esc(s){
443
-    return (s ?? '').toString()
444
-      .replaceAll('&', '&amp;')
445
-      .replaceAll('<', '&lt;')
446
-      .replaceAll('>', '&gt;')
447
-      .replaceAll('"', '&quot;')
448
-      .replaceAll("'", '&#039;');
449
-  }
181
+  const cards = Array.from(document.querySelectorAll('.md-device-card'));
182
+  const elSearch = document.getElementById('mdFilterSearch');
183
+  const elAttivita = document.getElementById('mdFilterAttivita');
184
+  const elStato = document.getElementById('mdFilterStato');
185
+  const btnReset = document.getElementById('mdFilterReset');
186
+  let searchTimer;
450 187
 
451
-  function locPill(ubicazione){
452
-    const u = (ubicazione ?? '').toString().trim();
453
-    if (!u.length) return '';
454
-    return `<span class="topo-loc-pill" title="Ubicazione">${esc(u)}</span>`;
188
+  function normalize(value) {
189
+    return (value ?? '').toString().trim().toLowerCase();
455 190
   }
456 191
 
457
-  function deviceMatchesFilter(d){
458
-    const attivo = (!!d.is_attivo) ? '1' : '0';
459
-    if (elAttivo.value !== 'all' && elAttivo.value !== attivo) return false;
460
-    return true;
461
-  }
462
-
463
-  function deviceRowHtml(d){
464
-    const isOn = !!d.is_attivo;
465
-    const name = d.nome ?? `Dispositivo #${d.id}`;
466
-    const tipo = (d.tipo ?? 'ND').toString();
467
-    const ip = (d.ip ?? '').toString().trim();
468
-    const ub = d.ubicazione ?? '';
469
-    return `
470
-      <div class="topo-device" data-device-id="${esc(d.id)}" data-device-name="${esc(name)}" data-device-tipo="${esc(tipo)}" data-device-ubicazione="${esc(ub)}" data-device-attivo="${isOn ? 1 : 0}">
471
-        <div class="topo-device-left">
472
-          <div class="topo-device-name">${esc(name)} ${locPill(ub)}</div>
473
-          <div class="topo-device-meta">${esc(tipo)} • ID ${esc(d.id)}${ip.length ? ` • IP ${esc(ip)}` : ''}</div>
474
-        </div>
475
-        <div class="topo-pill">
476
-          <span class="topo-dot ${isOn ? 'topo-dot--on' : 'topo-dot--off'}"></span>
477
-          ${isOn ? 'Attivo' : 'Non attivo'}
478
-        </div>
479
-      </div>
480
-    `;
481
-  }
192
+  function cardMatches(card) {
193
+    const q = normalize(elSearch.value);
194
+    const attivita = elAttivita.value;
195
+    const stato = elStato.value;
482 196
 
483
-  function filterDeviceList(list){
484
-    return (list ?? []).filter(deviceMatchesFilter);
485
-  }
486
-
487
-  function activityMatchesSearch(act, q){
488
-    if (!q.length) return true;
489
-    const n = (act.nome ?? '').toString().toLowerCase();
490
-    if (n.includes(q)) return true;
491
-    for (const d of act.punti_vendita ?? []) {
492
-      if ((d.nome ?? '').toString().toLowerCase().includes(q)) return true;
197
+    if (attivita !== 'all' && card.dataset.attivitaId !== attivita) {
198
+      return false;
493 199
     }
494
-    for (const d of act.altri ?? []) {
495
-      if ((d.nome ?? '').toString().toLowerCase().includes(q)) return true;
200
+    if (stato !== 'all' && card.dataset.attivo !== stato) {
201
+      return false;
496 202
     }
497
-    for (const c of act.cucine ?? []) {
498
-      if ((c.nome ?? '').toString().toLowerCase().includes(q)) return true;
499
-      const st = c.stampante;
500
-      if (st && (st.nome ?? '').toString().toLowerCase().includes(q)) return true;
203
+    if (q.length && !normalize(card.dataset.search).includes(q)) {
204
+      return false;
501 205
     }
502
-    return false;
206
+    return true;
503 207
   }
504 208
 
505
-  function fillAttivitaSelect(){
506
-    elAttivita.innerHTML = '';
507
-    const optAll = document.createElement('option');
508
-    optAll.value = 'all';
509
-    optAll.textContent = 'Tutte le attività';
510
-    elAttivita.appendChild(optAll);
511
-    TOPO_BY_ATTIVITA.forEach(a => {
512
-      const opt = document.createElement('option');
513
-      opt.value = String(a.id);
514
-      opt.textContent = a.nome ?? `Attività #${a.id}`;
515
-      elAttivita.appendChild(opt);
209
+  function updateTabCounts() {
210
+    document.querySelectorAll('.md-tab-count').forEach(function(badge) {
211
+      const tipo = badge.dataset.tipo;
212
+      const visible = cards.filter(function(card) {
213
+        return card.dataset.tipo === tipo && !card.classList.contains('is-hidden-by-filter');
214
+      }).length;
215
+      badge.textContent = String(visible);
516 216
     });
517 217
   }
518 218
 
519
-  function renderBoard(){
520
-    const q = (elSearch.value ?? '').toString().trim().toLowerCase();
521
-    const attId = elAttivita.value;
522
-
523
-    let acts = TOPO_BY_ATTIVITA.filter(a => activityMatchesSearch(a, q));
524
-    if (attId !== 'all') {
525
-      acts = acts.filter(a => String(a.id) === attId);
526
-    }
527
-
528
-    let totalDevices = 0;
529
-    const html = acts.map(act => {
530
-      const pv = filterDeviceList(act.punti_vendita);
531
-      const alt = filterDeviceList(act.altri);
532
-      let cucineHtml = '';
533
-      let stampantiVisibili = 0;
534
-
535
-      for (const c of act.cucine ?? []) {
536
-        if (q.length) {
537
-          const cname = (c.nome ?? '').toLowerCase();
538
-          const st = c.stampante;
539
-          const sn = st ? (st.nome ?? '').toLowerCase() : '';
540
-          const hitAct = (act.nome ?? '').toLowerCase().includes(q);
541
-          if (!hitAct && !cname.includes(q) && !sn.includes(q)) continue;
542
-        }
543
-
544
-        const st = c.stampante;
545
-        const stOk = st && deviceMatchesFilter(st);
546
-        if (stOk) stampantiVisibili += 1;
547
-
548
-        const stBlock = st
549
-          ? (stOk
550
-            ? `<div class="topo-stampante-wrap">${deviceRowHtml(st)}</div>`
551
-            : `<div class="topo-stampante-wrap text-muted small">Stampante presente ma filtrata dal campo stato.</div>`)
552
-          : `<div class="topo-stampante-wrap text-muted small fst-italic">Nessuna stampante collegata</div>`;
553
-
554
-        cucineHtml += `
555
-          <div class="topo-cucina-block">
556
-            <div class="topo-cucina-name">
557
-              <i class="bx bx-restaurant"></i> ${esc(c.nome ?? `Cucina #${c.id}`)}
558
-            </div>
559
-            <div class="d-flex align-items-center flex-wrap gap-1 small text-muted mb-1">
560
-              <span>Cucina</span><span class="topo-arrow">→</span><span>Stampante</span>
561
-            </div>
562
-            ${stBlock}
563
-          </div>
564
-        `;
565
-      }
566
-
567
-      const pvHtml = pv.length ? pv.map(deviceRowHtml).join('') : `<div class="text-muted small">Nessun dispositivo di punto vendita</div>`;
568
-      const altHtml = alt.length ? alt.map(deviceRowHtml).join('') : `<div class="text-muted small">Nessun altro dispositivo</div>`;
569
-      if (!cucineHtml.length) {
570
-        cucineHtml = `<div class="text-muted small">Nessuna cucina per questa attività (o nessun risultato per la ricerca)</div>`;
219
+  function updateEmptyStates() {
220
+    document.querySelectorAll('[data-tipo-pane]').forEach(function(grid) {
221
+      const tipo = grid.dataset.tipoPane;
222
+      const visible = grid.querySelectorAll('.md-device-card:not(.is-hidden-by-filter)').length;
223
+      const emptyEl = document.querySelector('[data-empty-for="' + tipo + '"]');
224
+      if (emptyEl) {
225
+        emptyEl.classList.toggle('d-none', visible > 0);
571 226
       }
572
-
573
-      const visibleDevCount = pv.length + alt.length + stampantiVisibili;
574
-      totalDevices += visibleDevCount;
575
-
576
-      return `
577
-        <article class="topo-act-card">
578
-          <header class="topo-act-header">
579
-            <div class="topo-act-title">
580
-              <i class="bx bx-category-alt"></i>
581
-              <span>${esc(act.nome ?? `Attività #${act.id}`)}</span>
582
-            </div>
583
-            <div class="topo-group-count">${visibleDevCount} in vista</div>
584
-          </header>
585
-          <div class="topo-act-body">
586
-            <section class="topo-section">
587
-              <div class="topo-section-title"><i class="bx bx-credit-card"></i> Punto vendita</div>
588
-              ${pvHtml}
589
-            </section>
590
-            <section class="topo-section">
591
-              <div class="topo-section-title"><i class="bx bx-dish"></i> Cucine e stampanti</div>
592
-              ${cucineHtml}
593
-            </section>
594
-            <section class="topo-section" style="grid-column: 1 / -1;">
595
-              <div class="topo-section-title"><i class="bx bx-devices"></i> Altri dispositivi (monitor, stampanti in elenco, …)</div>
596
-              ${altHtml}
597
-            </section>
598
-          </div>
599
-        </article>
600
-      `;
601
-    }).join('');
602
-
603
-    elBoardGrid.innerHTML = html || `<div class="text-muted p-3">Nessuna attività coincide con i filtri.</div>`;
604
-    elKpiActivities.textContent = String(acts.length);
605
-    elKpiDevices.textContent = String(totalDevices);
606
-    elHint.textContent = acts.length
607
-      ? `${acts.length} attività in vista · circa ${totalDevices} righe dispositivo (cucine contano la stampante se mostrata).`
608
-      : 'Nessun risultato.';
227
+      grid.classList.toggle('d-none', visible === 0 && grid.children.length > 0);
228
+    });
609 229
   }
610 230
 
611
-  function resetFilters(){
612
-    elAttivita.value = 'all';
613
-    elAttivo.value = 'all';
614
-    elSearch.value = '';
615
-    renderBoard();
231
+  function applyFilters() {
232
+    cards.forEach(function(card) {
233
+      card.classList.toggle('is-hidden-by-filter', !cardMatches(card));
234
+    });
235
+    updateTabCounts();
236
+    updateEmptyStates();
616 237
   }
617 238
 
618
-  fillAttivitaSelect();
619
-  [elAttivita, elAttivo].forEach(el => el.addEventListener('change', renderBoard));
620
-  elSearch.addEventListener('input', () => {
621
-    window.clearTimeout(window.__topoSearchT);
622
-    window.__topoSearchT = window.setTimeout(renderBoard, 120);
623
-  });
624
-
625
-  btnFit.addEventListener('click', () => {
626
-    document.getElementById('topologyBoard')?.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
239
+  elAttivita.addEventListener('change', applyFilters);
240
+  elStato.addEventListener('change', applyFilters);
241
+  elSearch.addEventListener('input', function() {
242
+    clearTimeout(searchTimer);
243
+    searchTimer = setTimeout(applyFilters, 150);
627 244
   });
628
-  btnReset.addEventListener('click', resetFilters);
629
-
630
-  document.addEventListener('click', (e) => {
631
-    const el = e.target?.closest?.('.topo-device');
632
-    if (!el) return;
633
-    const id = el.getAttribute('data-device-id');
634
-    const name = el.getAttribute('data-device-name');
635
-    const tipo = el.getAttribute('data-device-tipo');
636
-    const ubicazione = el.getAttribute('data-device-ubicazione');
637
-    const stato = el.getAttribute('data-device-attivo') === '1' ? 'Attivo' : 'Non attivo';
638
-    const loc = (ubicazione ?? '').toString().trim();
639
-    elHint.textContent = `Selezionato: ${name} (ID ${id}) • ${tipo} • ${stato}${loc.length ? ` • Luogo: ${loc}` : ''}`;
640
-  });
641
-
642
-  renderBoard();
643
-</script>
644
-
645
-
646
-<script type="module">
647
-  $(document).ready(function(){
648
-    // Editor edit
649
-    $("#dataTable_dispositivo").on('click', 'a.editor_edit', function (e) {
650
-      e.preventDefault();
651
-      window.LaravelDataTables["dataTable_dispositivo-editor"].edit( $(this).closest('tr'), {
652
-        title: 'Modifica',
653
-        buttons: 'Aggiorna',
654
-      });
655
-    });
656
-
657
-    // Editor delete
658
-    $("#dataTable_dispositivo").on('click', 'a.editor_delete', function (e) {
659
-      e.preventDefault();
660
-      window.LaravelDataTables["dataTable_dispositivo-editor"].remove($(this).closest('tr'), {
661
-        title: 'Cancella record',
662
-        message: 'Sei sicuro di voler eliminare il record selezionato?',
663
-        buttons: 'Cancella record'
664
-      });
665
-    } );
666
-
667
-    $("#dataTable_dispositivo").on('dblclick', 'tbody td', function (e) {
668
-      window.LaravelDataTables["dataTable_dispositivo-editor"].edit( $(this).closest('tr'), {
669
-        title: 'Modifica',
670
-        buttons: 'Aggiorna',
671
-      });
672
-    });
673
-
245
+  btnReset.addEventListener('click', function() {
246
+    elSearch.value = '';
247
+    elAttivita.value = 'all';
248
+    elStato.value = 'all';
249
+    applyFilters();
674 250
   });
675
-
676
-</script>
677
-
678
-<script>
679
-  function initComplete_dispositivo(){
680
-    $('div.dt-buttons button').removeClass('btn-secondary');
681
-    $('div.dt-search').addClass('mt-0 mb-4');
682
-  }
683
-
684
-
685 251
 </script>
686 252
 @endsection

+ 2
- 1
resources/views/pagamento/menu.blade.php Parādīt failu

@@ -1,7 +1,7 @@
1 1
 <?php
2 2
 use Illuminate\Support\Facades\Auth;
3 3
 ?>
4
-
4
+@if(Auth::user()->can('edit-pagamento') || Auth::user()->can('delete-pagamento'))
5 5
 <div class="dropdown">
6 6
     <button type="button" class="btn p-0 dropdown-toggle hide-arrow" data-bs-toggle="dropdown" aria-expanded="false">
7 7
         <i class="bx bx-dots-vertical-rounded fs-large"></i>
@@ -21,3 +21,4 @@ use Illuminate\Support\Facades\Auth;
21 21
         @endif
22 22
     </div>
23 23
 </div>
24
+@endif

+ 10
- 11
resources/views/prima_nota/_partials/statistiche.blade.php Parādīt failu

@@ -1,5 +1,4 @@
1 1
 <div class="row">
2
-  <!-- esempio di card statistica -->
3 2
   <div class="col-12 mb-6">
4 3
     <div class="card">
5 4
       <div class="card-widget-separator-wrapper">
@@ -8,8 +7,8 @@
8 7
             <div class="col-sm-6 col-lg-3">
9 8
               <div class="d-flex justify-content-between align-items-center card-widget-1 border-end pb-4 pb-sm-0">
10 9
                 <div>
11
-                  <h4 class="mb-0">{{ $statistiche['totale_operazioni'] ?? '—' }}</h4>
12
-                  <p class="mb-0">Operazioni totali</p>
10
+                  <h4 class="mb-0" id="stat_totale_operazioni">{{ $statistiche['totale_operazioni'] ?? '—' }}</h4>
11
+                  <p class="mb-0">Operazioni</p>
13 12
                 </div>
14 13
                 <div class="avatar me-sm-6">
15 14
                   <span class="avatar-initial rounded bg-label-secondary text-heading">
@@ -22,8 +21,8 @@
22 21
             <div class="col-sm-6 col-lg-3">
23 22
               <div class="d-flex justify-content-between align-items-center card-widget-2 border-end pb-4 pb-sm-0">
24 23
                 <div>
25
-                  <h4 class="mb-0">{{ number_format($statistiche['importo_entrate'] ?? 0, 2, ',', '.') }} €</h4>
26
-                  <p class="mb-0">Entrate odierne</p>
24
+                  <h4 class="mb-0" id="stat_importo_entrate">{{ number_format($statistiche['importo_entrate'] ?? 0, 2, ',', '.') }} €</h4>
25
+                  <p class="mb-0">Entrate</p>
27 26
                 </div>
28 27
                 <div class="avatar me-lg-6">
29 28
                   <span class="avatar-initial rounded bg-label-secondary text-success">
@@ -36,8 +35,8 @@
36 35
             <div class="col-sm-6 col-lg-3">
37 36
               <div class="d-flex justify-content-between align-items-center card-widget-3 border-end pb-4 pb-sm-0">
38 37
                 <div>
39
-                  <h4 class="mb-0">{{ number_format($statistiche['importo_uscite'] ?? 0, 2, ',', '.') }} €</h4>
40
-                  <p class="mb-0">Uscite odierne</p>
38
+                  <h4 class="mb-0" id="stat_importo_uscite">{{ number_format($statistiche['importo_uscite'] ?? 0, 2, ',', '.') }} €</h4>
39
+                  <p class="mb-0">Uscite</p>
41 40
                 </div>
42 41
                 <div class="avatar me-lg-6">
43 42
                   <span class="avatar-initial rounded bg-label-secondary text-danger">
@@ -50,8 +49,8 @@
50 49
             <div class="col-sm-6 col-lg-3">
51 50
               <div class="d-flex justify-content-between align-items-center card-widget-4 pb-4 pb-sm-0">
52 51
                 <div>
53
-                  <h4 class="mb-0">{{ number_format($statistiche['saldo'] ?? 0, 2, ',', '.') }} €</h4>
54
-                  <p class="mb-0">Saldo odierno</p>
52
+                  <h4 class="mb-0" id="stat_saldo">{{ number_format($statistiche['saldo'] ?? 0, 2, ',', '.') }} €</h4>
53
+                  <p class="mb-0">Saldo</p>
55 54
                 </div>
56 55
                 <div class="avatar me-lg-6">
57 56
                   <span class="avatar-initial rounded bg-label-secondary text-info">
@@ -62,7 +61,7 @@
62 61
             </div>
63 62
           </div>
64 63
         </div>
65
-      </div>  
64
+      </div>
66 65
     </div>
67 66
   </div>
68
-</div>
67
+</div>

+ 57
- 15
resources/views/prima_nota/index.blade.php Parādīt failu

@@ -41,7 +41,7 @@ use Illuminate\Support\Facades\Auth;
41 41
             <ol class="breadcrumb breadcrumb-custom-icon">
42 42
       
43 43
               <li class="breadcrumb-item">
44
-                <a href="#">Configurazioni</a>
44
+                <a href="#">Contabilità</a>
45 45
                 <i class="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
46 46
               </li>
47 47
               <li class="breadcrumb-item active text-primary">
@@ -73,36 +73,46 @@ use Illuminate\Support\Facades\Auth;
73 73
       <div class="card-body">
74 74
 
75 75
       <div class=" mb-4">
76
-    <form class="dt_adv_search" method="GET">
76
+    <form id="filtri_prima_nota" class="dt_adv_search" method="GET">
77 77
       <div class="row">
78 78
         <div class="col-12">
79
-          <div class="row g-3">
79
+          <div class="row g-3 align-items-end">
80 80
             <div class="col-12 col-sm-3 col-lg-3">
81
-              <label class="form-label">Data inizio</label>
82
-              <input type="date" class="form-control dt-input flatpickr-range" data-column=1 placeholder="YYYY-MM-DD" data-column-index="0">
81
+              <label class="form-label" for="filtro_data_inizio">Data inizio</label>
82
+              <input type="date" id="filtro_data_inizio" name="data_inizio" class="form-control">
83 83
             </div>
84 84
             <div class="col-12 col-sm-3 col-lg-3">
85
-              <label class="form-label">Data fine</label>
86
-              <input type="date" class="form-control dt-input flatpickr-range" data-column=2 placeholder="YYYY-MM-DD" data-column-index="1">
85
+              <label class="form-label" for="filtro_data_fine">Data fine</label>
86
+              <input type="date" id="filtro_data_fine" name="data_fine" class="form-control">
87 87
             </div>
88 88
             <div class="col-12 col-sm-3 col-lg-3">
89
-              <label class="form-label">Categoria contabile</label>
90
-              <select name="categoria_contabile_id" id="categoria_contabile_id" class="form-control dt-input form-select" data-column=3 data-column-index="2">
91
-                <option value="">Seleziona categoria contabile</option>
92
-                @foreach(\App\Models\Categoriacontabile::where('is_attiva', true)->get() as $categoria)
89
+              <label class="form-label" for="categoria_contabile_id">Categoria contabile</label>
90
+              <select name="categoria_contabile_id" id="categoria_contabile_id" class="form-control form-select">
91
+                <option value="">Tutte le categorie</option>
92
+                @foreach($categorie as $categoria)
93 93
                   <option value="{{ $categoria->id }}">{{ $categoria->nome }}</option>
94 94
                 @endforeach
95 95
               </select>
96 96
             </div>
97
-            <div class="col-12 col-sm-3 col-lg-3">
98
-              <label class="form-label">Tipo movimento</label>
99
-              <select name="tipo_movimento" id="tipo_movimento" class="form-control dt-input form-select" data-column=4 data-column-index="3">
100
-                <option value="">Seleziona tipo movimento</option>
97
+            <div class="col-12 col-sm-2 col-lg-2">
98
+              <label class="form-label" for="tipo_movimento">Tipo movimento</label>
99
+              <select name="tipo_movimento" id="tipo_movimento" class="form-control form-select">
100
+                <option value="">Tutti i tipi</option>
101 101
                 @foreach(\App\Models\PrimaNota::getTipiMovimento() as $tipo)
102 102
                   <option value="{{ $tipo['value'] }}">{{ $tipo['label'] }}</option>
103 103
                 @endforeach
104 104
               </select>
105 105
             </div>
106
+            <div class="col-12 col-sm-auto">
107
+              <button
108
+                type="button"
109
+                id="btn_reset_filtri_prima_nota"
110
+                class="btn btn-icon btn-outline-secondary"
111
+                title="Azzera filtri"
112
+                aria-label="Azzera filtri">
113
+                <i class="bx bx-revision"></i>
114
+              </button>
115
+            </div>
106 116
             
107 117
           </div>
108 118
         </div>
@@ -156,6 +166,27 @@ use Illuminate\Support\Facades\Auth;
156 166
 
157 167
 
158 168
 <script type="module">
169
+  function formatEuroImporto(value) {
170
+    return new Intl.NumberFormat('it-IT', {
171
+      minimumFractionDigits: 2,
172
+      maximumFractionDigits: 2,
173
+    }).format(Number(value) || 0) + ' €';
174
+  }
175
+
176
+  function aggiornaStatistichePrimaNota() {
177
+    $.get('{{ route('prima-nota.statistiche') }}', {
178
+      data_inizio: $('#filtro_data_inizio').val(),
179
+      data_fine: $('#filtro_data_fine').val(),
180
+      categoria_contabile_id: $('#categoria_contabile_id').val(),
181
+      tipo_movimento: $('#tipo_movimento').val(),
182
+    }).done(function (data) {
183
+      $('#stat_totale_operazioni').text(data.totale_operazioni ?? 0);
184
+      $('#stat_importo_entrate').text(formatEuroImporto(data.importo_entrate));
185
+      $('#stat_importo_uscite').text(formatEuroImporto(data.importo_uscite));
186
+      $('#stat_saldo').text(formatEuroImporto(data.saldo));
187
+    });
188
+  }
189
+
159 190
   $(document).ready(function(){
160 191
     // Editor edit
161 192
     $("#dataTable_primanota").on('click', 'a.editor_edit', function (e) {
@@ -183,6 +214,17 @@ use Illuminate\Support\Facades\Auth;
183 214
       });
184 215
     });
185 216
 
217
+    $('#filtri_prima_nota').on('change', 'input, select', function () {
218
+      window.LaravelDataTables['dataTable_primanota'].draw();
219
+    });
220
+
221
+    $('#btn_reset_filtri_prima_nota').on('click', function () {
222
+      $('#filtri_prima_nota')[0].reset();
223
+      window.LaravelDataTables['dataTable_primanota'].draw();
224
+    });
225
+
226
+    $('#dataTable_primanota').on('draw.dt', aggiornaStatistichePrimaNota);
227
+
186 228
   });
187 229
 
188 230
 </script>

+ 38
- 18
resources/views/punto_operatore/show.blade.php Parādīt failu

@@ -104,7 +104,7 @@ $configData = Helper::appClasses();
104 104
         <p class="text-muted mb-3">
105 105
           Seleziona una riga ordine pronta e premi "Chiama".
106 106
         </p>
107
-        <div id="lista-da-chiamare" class="d-grid gap-2"></div>
107
+        <div id="lista-da-chiamare" class="d-grid gap-2 overflow-y-auto"></div>
108 108
       </div>
109 109
     </div>
110 110
   </div>
@@ -122,7 +122,7 @@ $configData = Helper::appClasses();
122 122
         <p class="text-muted mb-3">
123 123
           Quando il cliente ritira, premi "Ritirato".
124 124
         </p>
125
-        <div id="lista-chiamati" class="d-grid gap-2"></div>
125
+        <div id="lista-chiamati" class="d-grid gap-2 overflow-y-auto"></div>
126 126
       </div>
127 127
     </div>
128 128
   </div>
@@ -166,17 +166,27 @@ $configData = Helper::appClasses();
166 166
     righeDaChiamare.forEach((riga) => {
167 167
       container.append(`
168 168
         <div class="border rounded p-3">
169
-          <div class="d-flex justify-content-between align-items-start">
170
-            <div>
171
-              <div class="fw-semibold">Ordine #${riga.ordine_id}</div>
172
-              <div>${riga.quantita} x ${riga.piatto}</div>
173
-              <small class="text-muted">Riga #${riga.id} - stato: ${riga.stato}</small>
169
+          <div class="row align-items-center gx-2">
170
+            <!-- Colonna 1: ID ordine -->
171
+            <div class="col-auto fw-semibold text-nowrap" style="min-width: 90px;">
172
+             <span class="text-muted text-small d-none">Ordine</span> <span class="text-secondary fs-4">#${riga.ordine_id}</span>
173
+            </div>
174
+            <!-- Colonna 2: Quantità x Piatto (colonna più larga) -->
175
+            <div class="col flex-grow-1">
176
+              <span class="fs-large text-primary">${riga.quantita} x ${riga.piatto}</span>
177
+              <div>
178
+                <small class="text-muted">Riga #${riga.id} - stato: ${riga.stato}</small>
179
+              </div>
180
+            </div>
181
+            <!-- Colonna 3: Bottone Chiama -->
182
+            <div class="col-auto">
183
+              <button type="button" class="btn btn-sm btn-primary" onclick="chiamaRiga(${riga.id})">
184
+                Chiama
185
+              </button>
174 186
             </div>
175
-            <button type="button" class="btn btn-sm btn-primary" onclick="chiamaRiga(${riga.id})">
176
-              Chiama
177
-            </button>
178 187
           </div>
179 188
         </div>
189
+   
180 190
       `);
181 191
     });
182 192
   }
@@ -199,17 +209,27 @@ $configData = Helper::appClasses();
199 209
     righeChiamate.forEach((riga) => {
200 210
       container.append(`
201 211
         <div class="border rounded p-3">
202
-          <div class="d-flex justify-content-between align-items-start">
203
-            <div>
204
-              <div class="fw-semibold">Ordine #${riga.ordine_id}</div>
205
-              <div>${riga.quantita} x ${riga.piatto}</div>
206
-              <small class="text-muted">Riga #${riga.id} - stato: ${riga.stato}</small>
212
+          <div class="row align-items-center gx-2">
213
+            <!-- Colonna 1: ID ordine -->
214
+            <div class="col-auto fw-semibold text-nowrap" style="min-width: 90px;">
215
+              <span class="text-muted text-small d-none">Ordine</span> <span class="text-secondary fs-4">#${riga.ordine_id}</span>
216
+            </div>
217
+            <!-- Colonna 2: Quantità x Piatto (colonna più larga) -->
218
+            <div class="col flex-grow-1">
219
+              <span class="fs-large text-primary">${riga.quantita} x ${riga.piatto}</span>
220
+              <div>
221
+                <small class="text-muted">Riga #${riga.id} - stato: ${riga.stato}</small>
222
+              </div>
223
+            </div>
224
+            <!-- Colonna 3: Bottone Ritirato -->
225
+            <div class="col-auto">
226
+              <button type="button" class="btn btn-sm btn-success" onclick="ritiraRiga(${riga.id})">
227
+                Ritirato
228
+              </button>
207 229
             </div>
208
-            <button type="button" class="btn btn-sm btn-success" onclick="ritiraRiga(${riga.id})">
209
-              Ritirato
210
-            </button>
211 230
           </div>
212 231
         </div>
232
+   
213 233
       `);
214 234
     });
215 235
   }

+ 1
- 1
resources/views/punto_vendita/cassa/index.blade.php Parādīt failu

@@ -286,7 +286,7 @@ $cucine = $cucine ?? collect();
286 286
         text: response.message || 'Disassociazione completata',
287 287
         timeout: 3000
288 288
       }).success(response.message);
289
-        window.location.href = "{{ route('dashboard') }}";
289
+        window.location.href = "{{ route('punto-vendita.index') }}";
290 290
       },
291 291
       error: function(xhr, status, error) {
292 292
         new Notyf({

+ 478
- 38
resources/views/punto_vendita/edit.blade.php Parādīt failu

@@ -1,13 +1,85 @@
1
+<?php
2
+use App\Models\Role;
3
+use Illuminate\Support\Facades\Auth;
4
+?>
5
+@php
6
+$configData = Helper::appClasses();
7
+@endphp
8
+
9
+@extends('layouts/layoutMaster')
10
+
11
+@section('title', 'Punti di vendita')
12
+
13
+@section('vendor-style')
14
+@vite([
15
+'resources/assets/vendor/libs/datatables-bs5/datatables.bootstrap5.scss',
16
+'resources/assets/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.scss',
17
+'resources/assets/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.scss',
18
+'resources/assets/vendor/libs/flatpickr/flatpickr.scss',
19
+'resources/assets/vendor/libs/@form-validation/form-validation.scss'
20
+])
21
+@endsection
22
+
23
+<!-- Vendor Scripts -->
24
+@section('vendor-script')
25
+@vite([
26
+'resources/assets/vendor/libs/moment/moment.js',
27
+'resources/assets/vendor/libs/flatpickr/flatpickr.js',
28
+'resources/assets/vendor/libs/@form-validation/popular.js',
29
+'resources/assets/vendor/libs/@form-validation/bootstrap5.js',
30
+'resources/assets/vendor/libs/@form-validation/auto-focus.js',
31
+'resources/assets/vendor/libs/@form-validation/popular.js',
32
+'resources/assets/vendor/libs/@form-validation/bootstrap5.js',
33
+'resources/assets/vendor/libs/@form-validation/auto-focus.js',
34
+'resources/assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js'
35
+])
36
+@endsection
37
+
38
+@section('pageTitle')
39
+<div class="d-flex flex-column ">
40
+  <h4 class="mb-1 text-sm-small mt-md-4 mt-lg-4"> 
41
+    <i class="bx bx-store d-none d-md-inline-flex d-lg-inline-flex"></i> 
42
+    {{ $puntoVendita->nome }}
43
+  </h4>
44
+  <nav aria-label="breadcrumb" style="font-size: smaller;" class="d-none d-md-block d-lg-block">
45
+            <ol class="breadcrumb breadcrumb-custom-icon">
46
+      
47
+              <li class="breadcrumb-item">
48
+                <a href="{{ route('punto-vendita.index') }}">Punti di vendita</a>
49
+                <i class="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
50
+              </li>
51
+              <li class="breadcrumb-item active text-primary">
52
+                <a href="{{ route('punto-vendita.edit', ['punto_vendita_id' => $puntoVendita->id]) }}">{{ $puntoVendita->nome }}</a>
53
+</div>
54
+@endsection
55
+
56
+@section('content')
57
+
1 58
 @php
2 59
   $tipiPuntoVendita = \App\Models\Dispositivo::getTipoPuntiVendita();
3 60
   $endpointList = \App\Models\Endpoint::query()->orderBy('label')->get();
4 61
   $cucineAssociateIds = $puntoVendita->hasCucine->pluck('id')->all();
62
+  $cucine = $puntoVendita->hasCucine;
63
+  $tipi = \App\Models\Dispositivo::getTipoDispositivo();
64
+  $tipoMeta = $tipi->get($puntoVendita->tipo);
65
+  $stampante = $puntoVendita->url_stampante ? $puntoVendita->stampante_dispositivo() : null;
66
+  if ($stampante) {
67
+    $stampante->loadMissing('endpoint');
68
+  }
69
+  $puoDissociareDispositivo = Auth::user()->hasRole('superadmin')
70
+    || Auth::user()->hasRole('amministratore')
71
+    || Auth::user()->hasRole('administrator');
5 72
 @endphp
6 73
 
7 74
 <div class="nav-align-top">
8 75
   <ul class="nav nav-tabs nav-fill" role="tablist">
9 76
     <li class="nav-item" role="presentation">
10
-      <button type="button" class="nav-link active" role="tab" data-bs-toggle="tab" data-bs-target="#pv-tab-generali" aria-controls="pv-tab-generali" aria-selected="true">
77
+      <button type="button" class="nav-link active" role="tab" data-bs-toggle="tab" data-bs-target="#pv-tab-riepilogo" aria-controls="pv-tab-riepilogo" aria-selected="true">
78
+        <i class="bx bx-list-ul me-1"></i>Riepilogo
79
+      </button>
80
+    </li>
81
+    <li class="nav-item" role="presentation">
82
+      <button type="button" class="nav-link" role="tab" data-bs-toggle="tab" data-bs-target="#pv-tab-generali" aria-controls="pv-tab-generali" aria-selected="false">
11 83
         <i class="bx bx-cog me-1"></i>Generali
12 84
       </button>
13 85
     </li>
@@ -29,18 +101,269 @@
29 101
   </ul>
30 102
 
31 103
   <div class="tab-content">
32
-    <div class="tab-pane fade show active" id="pv-tab-generali" role="tabpanel">
104
+    <div class="tab-pane fade show active" id="pv-tab-riepilogo" role="tabpanel">
105
+      <div class="row g-4 mt-1">
106
+        {{-- Sidebar profilo --}}
107
+        <div class="col-12 col-lg-4 col-xl-3">
108
+          <div class="card h-100">
109
+            <div class="card-body text-center pt-4 pb-3">
110
+              <div class="avatar avatar-xl mx-auto mb-3">
111
+                <span class="avatar-initial rounded-circle bg-label-primary">
112
+                  <i class="{{ $tipoMeta['icon'] ?? 'bx bx-store' }} bx-lg"></i>
113
+                </span>
114
+              </div>
115
+              <h5 class="mb-1">{{ $puntoVendita->nome ?? '—' }}</h5>
116
+              <p class="text-muted small mb-2">ID #{{ $puntoVendita->id }}</p>
117
+
118
+              @if($tipoMeta)
119
+                <span class="badge bg-label-info mb-3">
120
+                  {{ $tipoMeta['label'] }}
121
+                </span>
122
+              @endif
123
+
124
+              <ul class="list-unstyled text-start small mb-0">
125
+                <li class="d-flex justify-content-between py-2 border-bottom">
126
+                  <span class="text-muted">Stato</span>
127
+                  @if($puntoVendita->is_attivo)
128
+                    <span class="badge bg-label-success">Attivo</span>
129
+                  @else
130
+                    <span class="badge bg-label-danger">Non attivo</span>
131
+                  @endif
132
+                </li>
133
+                <li class="d-flex justify-content-between py-2 border-bottom">
134
+                  <span class="text-muted">Abbinamento</span>
135
+                  @if($puntoVendita->binding_token)
136
+                    <span class="badge bg-label-success">Abbinato</span>
137
+                  @else
138
+                    <span class="badge bg-label-danger">Libero</span>
139
+                  @endif
140
+                </li>
141
+                <li class="d-flex justify-content-between py-2 border-bottom">
142
+                  <span class="text-muted">Stampante</span>
143
+                  @if($stampante)
144
+                    <span class="badge bg-label-info text-truncate ms-2" style="max-width: 9rem;" title="{{ $stampante->nome }}">{{ $stampante->nome }}</span>
145
+                  @else
146
+                    <span class="badge bg-label-secondary">Assente</span>
147
+                  @endif
148
+                </li>
149
+                <li class="d-flex justify-content-between py-2">
150
+                  <span class="text-muted">Cucine</span>
151
+                  <span class="badge bg-label-primary">{{ $cucine->count() }}</span>
152
+                </li>
153
+              </ul>
154
+            </div>
155
+            <div class="card-body border-top pt-3 d-grid gap-2">
156
+              @can('view-punto_vendita')
157
+              <a href="{{ route('punto-vendita.show', ['punto_vendita_id' => $puntoVendita->id]) }}" class="btn btn-primary btn-sm">
158
+                <i class="bx bx-log-in me-1"></i> Entra nel punto vendita
159
+              </a>
160
+              @endcan
161
+              <a href="{{ route('punto-vendita.index') }}" class="btn btn-label-secondary btn-sm">
162
+                <i class="bx bx-arrow-back me-1"></i> Torna all'elenco
163
+              </a>
164
+            </div>
165
+          </div>
166
+        </div>
167
+
168
+        {{-- Contenuto principale --}}
169
+        <div class="col-12 col-lg-8 col-xl-9">
170
+          <div class="d-flex flex-wrap gap-2 mb-1">
171
+            <button type="button" class="btn btn-sm btn-outline-primary" data-pv-go-tab="#pv-tab-generali">
172
+              <i class="bx bx-cog me-1"></i> Modifica generali
173
+            </button>
174
+            <button type="button" class="btn btn-sm btn-outline-primary" data-pv-go-tab="#pv-tab-cucine">
175
+              <i class="bx bx-restaurant me-1"></i> Gestisci cucine
176
+            </button>
177
+            <button type="button" class="btn btn-sm btn-outline-primary" data-pv-go-tab="#pv-tab-stampante">
178
+              <i class="bx bx-printer me-1"></i> Configura stampante
179
+            </button>
180
+          </div>
181
+
182
+          <div class="row g-4">
183
+            {{-- Informazioni generali --}}
184
+            <div class="col-12">
185
+              <div class="card">
186
+                <div class="card-header">
187
+                  <h5 class="card-title mb-0">
188
+                    <i class="bx bx-info-circle text-primary me-1"></i>
189
+                    Informazioni generali
190
+                  </h5>
191
+                </div>
192
+                <div class="card-body">
193
+                  <div class="row g-4">
194
+                    <div class="col-md-6">
195
+                      <dl class="row mb-0">
196
+                        <dt class="col-sm-5 text-muted">Nome</dt>
197
+                        <dd class="col-sm-7 fw-medium">{{ $puntoVendita->nome ?? '—' }}</dd>
198
+                        <dt class="col-sm-5 text-muted">Tipo</dt>
199
+                        <dd class="col-sm-7">{{ $tipoMeta['label'] ?? $puntoVendita->tipo ?? '—' }}</dd>
200
+                        <dt class="col-sm-5 text-muted">Ubicazione</dt>
201
+                        <dd class="col-sm-7">{{ $puntoVendita->ubicazione ?: '—' }}</dd>
202
+                        <dt class="col-sm-5 text-muted">Attività</dt>
203
+                        <dd class="col-sm-7">{{ $puntoVendita->attivita?->nome ?? '—' }}</dd>
204
+                      </dl>
205
+                    </div>
206
+                    <div class="col-md-6">
207
+                      <dl class="row mb-0">
208
+                        <dt class="col-sm-5 text-muted">Licenza</dt>
209
+                        <dd class="col-sm-7">{{ $puntoVendita->licenza ?: '—' }}</dd>
210
+                        <dt class="col-sm-5 text-muted">PIN sblocco</dt>
211
+                        <dd class="col-sm-7">
212
+                          @if($puntoVendita->pin_sblocco)
213
+                            <code>••••</code>
214
+                            <span class="text-muted small">(configurato)</span>
215
+                          @else
216
+                            —
217
+                          @endif
218
+                        </dd>
219
+                        <dt class="col-sm-5 text-muted">Creato il</dt>
220
+                        <dd class="col-sm-7">{{ $puntoVendita->created_at?->format('d/m/Y H:i') ?? '—' }}</dd>
221
+                        <dt class="col-sm-5 text-muted">Aggiornato il</dt>
222
+                        <dd class="col-sm-7">{{ $puntoVendita->updated_at?->format('d/m/Y H:i') ?? '—' }}</dd>
223
+                      </dl>
224
+                    </div>
225
+                  </div>
226
+                  @if($puntoVendita->note)
227
+                    <div class="mt-3 pt-3 border-top">
228
+                      <p class="text-muted small mb-1">Note</p>
229
+                      <p class="mb-0">{{ $puntoVendita->note }}</p>
230
+                    </div>
231
+                  @endif
232
+                </div>
233
+              </div>
234
+            </div>
235
+
236
+            {{-- Abbinamento, stampante, orari --}}
237
+            <div class="col-md-4">
238
+              <div class="card h-100">
239
+                <div class="card-header">
240
+                  <h5 class="card-title mb-0">
241
+                    <i class="bx bx-link text-primary me-1"></i>
242
+                    Abbinamento
243
+                  </h5>
244
+                </div>
245
+                <div class="card-body d-flex flex-column">
246
+                  @if($puntoVendita->binding_token)
247
+                    <p class="mb-2">
248
+                      <span class="badge bg-label-success">Dispositivo collegato</span>
249
+                    </p>
250
+                    <p class="text-muted small mb-3">Un terminale è associato a questo punto vendita tramite binding token.</p>
251
+                    @if($puoDissociareDispositivo)
252
+                      <button type="button" class="btn btn-sm btn-outline-danger mt-auto align-self-start" id="btnDissociaDispositivo">
253
+                        <i class="bx bx-unlink me-1"></i> Dissocia
254
+                      </button>
255
+                    @endif
256
+                  @else
257
+                    <p class="mb-2">
258
+                      <span class="badge bg-label-secondary">Nessun dispositivo</span>
259
+                    </p>
260
+                    <p class="text-muted small mb-0">Il punto vendita non risulta abbinato a un dispositivo fisico.</p>
261
+                  @endif
262
+                </div>
263
+              </div>
264
+            </div>
265
+
266
+            <div class="col-md-4">
267
+              <div class="card h-100">
268
+                <div class="card-header">
269
+                  <h5 class="card-title mb-0">
270
+                    <i class="bx bx-printer text-info me-1"></i>
271
+                    Stampante
272
+                  </h5>
273
+                </div>
274
+                <div class="card-body">
275
+                  @if($stampante)
276
+                    <p class="mb-1 fw-medium">{{ $stampante->nome }}</p>
277
+                    @if($stampante->endpoint)
278
+                      <p class="mb-0 text-muted small">
279
+                        <i class="bx bx-chip me-1"></i>
280
+                        {{ $stampante->endpoint->label ?? 'Endpoint #' . $stampante->endpoint_id }}
281
+                      </p>
282
+                    @endif
283
+                    @if($stampante->ubicazione)
284
+                      <p class="mb-0 text-muted small mt-1">
285
+                        <i class="bx bx-map-pin me-1"></i>{{ $stampante->ubicazione }}
286
+                      </p>
287
+                    @endif
288
+                  @else
289
+                    <p class="text-muted small mb-2">Nessuna stampante configurata per gli scontrini.</p>
290
+                    <button type="button" class="btn btn-sm btn-label-info" data-pv-go-tab="#pv-tab-stampante">
291
+                      Configura ora
292
+                    </button>
293
+                  @endif
294
+                </div>
295
+              </div>
296
+            </div>
297
+
298
+            <div class="col-md-4">
299
+              <div class="card h-100">
300
+                <div class="card-header">
301
+                  <h5 class="card-title mb-0">
302
+                    <i class="bx bx-time-five text-warning me-1"></i>
303
+                    Orari turno
304
+                  </h5>
305
+                </div>
306
+                <div class="card-body">
307
+                  <dl class="row mb-0 small">
308
+                    <dt class="col-5 text-muted">Apertura</dt>
309
+                    <dd class="col-7 mb-2">{{ $puntoVendita->data_apertura_dispositivo?->format('d/m/Y H:i') ?? '—' }}</dd>
310
+                    <dt class="col-5 text-muted">Chiusura</dt>
311
+                    <dd class="col-7 mb-0">{{ $puntoVendita->data_chiusura_dispositivo?->format('d/m/Y H:i') ?? '—' }}</dd>
312
+                  </dl>
313
+                </div>
314
+              </div>
315
+            </div>
316
+
317
+            {{-- Cucine --}}
318
+            <div class="col-12">
319
+              <div class="card">
320
+                <div class="card-header d-flex flex-wrap align-items-center justify-content-between gap-2">
321
+                  <h5 class="card-title mb-0">
322
+                    <i class="bx bx-restaurant text-primary me-1"></i>
323
+                    Cucine associate
324
+                  </h5>
325
+                  <div class="d-flex align-items-center gap-2">
326
+                    <span class="badge bg-label-primary">{{ $cucine->count() }}</span>
327
+                    <button type="button" class="btn btn-sm btn-label-primary" data-pv-go-tab="#pv-tab-cucine">
328
+                      Modifica
329
+                    </button>
330
+                  </div>
331
+                </div>
332
+                <div class="card-body">
333
+                  @if($cucine->isNotEmpty())
334
+                    <div class="d-flex flex-wrap gap-2">
335
+                      @foreach($cucine as $cucina)
336
+                        <span class="badge bg-label-primary fs-6 fw-normal px-3 py-2">
337
+                          <i class="bx bx-restaurant me-1"></i>{{ $cucina->nome ?? '—' }}
338
+                        </span>
339
+                      @endforeach
340
+                    </div>
341
+                  @else
342
+                    <p class="text-muted small mb-2">Nessuna cucina associata.</p>
343
+                    <button type="button" class="btn btn-sm btn-label-primary" data-pv-go-tab="#pv-tab-cucine">
344
+                      <i class="bx bx-plus me-1"></i> Associa cucine
345
+                    </button>
346
+                  @endif
347
+                </div>
348
+              </div>
349
+            </div>
350
+          </div>
351
+        </div>
352
+      </div>
353
+    </div>
354
+
355
+    <div class="tab-pane fade" id="pv-tab-generali" role="tabpanel">
33 356
       <form id="puntoVenditaEditForm" class="mt-1">
34 357
         @csrf
35
-        <input type="hidden" name="id" value="{{ $puntoVendita->id }}">
358
+        <input type="hidden" name="punto_vendita_id" value="{{ $puntoVendita->id }}">
36 359
         <div class="row g-3">
37 360
           <div class="col-md-6">
38 361
             <label for="pv_nome" class="form-label">Nome</label>
39
-            <input type="text" class="form-control" id="pv_nome" name="nome" value="{{ $puntoVendita->nome }}">
362
+            <input type="text" class="form-control" id="pv_nome" name="nome" data-pv-save value="{{ $puntoVendita->nome }}">
40 363
           </div>
41 364
           <div class="col-md-6">
42 365
             <label for="pv_tipo" class="form-label">Tipo</label>
43
-            <select class="form-select" id="pv_tipo" name="tipo">
366
+            <select class="form-select" id="pv_tipo" name="tipo" data-pv-save>
44 367
               @foreach($tipiPuntoVendita as $label => $value)
45 368
                 <option value="{{ $value }}" {{ $puntoVendita->tipo === $value ? 'selected' : '' }}>
46 369
                   {{ $label }}
@@ -51,32 +374,32 @@
51 374
 
52 375
           <div class="col-md-6">
53 376
             <label for="pv_pin_sblocco" class="form-label">PIN sblocco</label>
54
-            <input type="text" class="form-control" id="pv_pin_sblocco" name="pin_sblocco" value="{{ $puntoVendita->pin_sblocco }}">
377
+            <input type="text" class="form-control" id="pv_pin_sblocco" name="pin_sblocco" data-pv-save value="{{ $puntoVendita->pin_sblocco }}">
55 378
           </div>
56 379
           <div class="col-md-6">
57 380
             <label for="pv_ubicazione" class="form-label">Ubicazione</label>
58
-            <input type="text" class="form-control" id="pv_ubicazione" name="ubicazione" value="{{ $puntoVendita->ubicazione }}">
381
+            <input type="text" class="form-control" id="pv_ubicazione" name="ubicazione" data-pv-save value="{{ $puntoVendita->ubicazione }}">
59 382
           </div>
60 383
 
61 384
           <div class="col-md-6">
62 385
             <label for="pv_data_apertura" class="form-label">Data apertura dispositivo</label>
63
-            <input type="datetime-local" class="form-control" id="pv_data_apertura" name="data_apertura_dispositivo" value="{{ $puntoVendita->data_apertura_dispositivo ? $puntoVendita->data_apertura_dispositivo->format('Y-m-d\TH:i') : '' }}">
386
+            <input type="datetime-local" class="form-control" id="pv_data_apertura" name="data_apertura_dispositivo" data-pv-save value="{{ $puntoVendita->data_apertura_dispositivo ? $puntoVendita->data_apertura_dispositivo->format('Y-m-d\TH:i') : '' }}">
64 387
           </div>
65 388
           <div class="col-md-6">
66 389
             <label for="pv_data_chiusura" class="form-label">Data chiusura dispositivo</label>
67
-            <input type="datetime-local" class="form-control" id="pv_data_chiusura" name="data_chiusura_dispositivo" value="{{ $puntoVendita->data_chiusura_dispositivo ? $puntoVendita->data_chiusura_dispositivo->format('Y-m-d\TH:i') : '' }}">
390
+            <input type="datetime-local" class="form-control" id="pv_data_chiusura" name="data_chiusura_dispositivo" data-pv-save value="{{ $puntoVendita->data_chiusura_dispositivo ? $puntoVendita->data_chiusura_dispositivo->format('Y-m-d\TH:i') : '' }}">
68 391
           </div>
69 392
 
70 393
           <div class="col-md-6 d-flex align-items-end">
71 394
             <div class="form-check form-switch">
72
-              <input class="form-check-input" type="checkbox" id="pv_is_attivo" name="is_attivo" value="1" {{ $puntoVendita->is_attivo ? 'checked' : '' }}>
395
+              <input class="form-check-input" type="checkbox" id="pv_is_attivo" name="is_attivo" data-pv-save value="1" {{ $puntoVendita->is_attivo ? 'checked' : '' }}>
73 396
               <label class="form-check-label" for="pv_is_attivo">Dispositivo attivo</label>
74 397
             </div>
75 398
           </div>
76 399
 
77 400
           <div class="col-12">
78 401
             <label for="pv_note" class="form-label">Note</label>
79
-            <textarea class="form-control" id="pv_note" name="note" rows="3">{{ $puntoVendita->note }}</textarea>
402
+            <textarea class="form-control" id="pv_note" name="note" data-pv-save rows="3">{{ $puntoVendita->note }}</textarea>
80 403
           </div>
81 404
         </div>
82 405
       </form>
@@ -115,7 +438,7 @@
115 438
       <div class="row g-3 mt-1">
116 439
         <div class="col-md-6">
117 440
           <label for="pv_url_stampante" class="form-label">URL/ID stampante</label>
118
-          <select class="form-select" id="pv_url_stampante" name="url_stampante">
441
+          <select class="form-select" id="pv_url_stampante" name="url_stampante" data-pv-save>
119 442
             <option value="">Seleziona stampante</option>
120 443
             @foreach($puntoVendita->attivita->stampanti as $stampante)
121 444
               <option value="{{ $stampante->id }}" {{ (int)$puntoVendita->url_stampante === (int)$stampante->id ? 'selected' : '' }}>
@@ -126,7 +449,7 @@
126 449
         </div>
127 450
         <div class="col-md-6">
128 451
           <label for="pv_endpoint_id" class="form-label">Endpoint</label>
129
-          <select class="form-select" id="pv_endpoint_id" name="endpoint_id">
452
+          <select class="form-select" id="pv_endpoint_id" name="endpoint_id" data-pv-save>
130 453
             <option value="">Seleziona endpoint</option>
131 454
             @foreach($endpointList as $endpoint)
132 455
               <option value="{{ $endpoint->id }}" {{ (int)$puntoVendita->endpoint_id === (int)$endpoint->id ? 'selected' : '' }}>
@@ -142,41 +465,158 @@
142 465
       <div class="row g-3 mt-1">
143 466
         <div class="col-md-6">
144 467
           <label for="pv_licenza" class="form-label">Licenza</label>
145
-          <input type="text" class="form-control" id="pv_licenza" name="licenza" value="{{ $puntoVendita->licenza }}">
468
+          <input type="text" class="form-control" id="pv_licenza" name="licenza" data-pv-save value="{{ $puntoVendita->licenza }}">
146 469
         </div>
147 470
       </div>
148 471
     </div>
149 472
   </div>
150 473
 </div>
474
+@endsection
475
+
476
+@section('page-script')
477
+<script type="module">
478
+  $(document).ready(function() {
479
+    var pvId = {{ $puntoVendita->id }};
480
+    var pvTimer = {};
481
+
482
+    function pvNotyf(type, message) {
483
+      new Notyf({ duration: 2500, position: { x: 'right', y: 'top' } })[type](message);
484
+    }
485
+
486
+    function pvValoreCampo($el) {
487
+      if ($el.is(':checkbox')) {
488
+        return $el.is(':checked') ? 1 : 0;
489
+      }
490
+      return $el.val();
491
+    }
492
+
493
+    function pvAjaxError(xhr, fallback) {
494
+      var msg = fallback;
495
+      if (xhr.status === 422 && xhr.responseJSON && xhr.responseJSON.errors) {
496
+        var key = Object.keys(xhr.responseJSON.errors)[0];
497
+        msg = xhr.responseJSON.errors[key][0];
498
+      } else if (xhr.responseJSON && xhr.responseJSON.message) {
499
+        msg = xhr.responseJSON.message;
500
+      }
501
+      pvNotyf('error', msg);
502
+    }
503
+
504
+    function salvaCampo(nome, valore) {
505
+      var data = {
506
+        punto_vendita_id: pvId,
507
+        _token: '{{ csrf_token() }}'
508
+      };
509
+      data[nome] = valore;
151 510
 
152
-<script>
153
-  $(document).off('click.pvSaveCucine', '#saveCucine').on('click.pvSaveCucine', '#saveCucine', function() {
154
-    var form = $('#puntoVenditaEditCucineForm');
155
-    var cucineIds = [];
156
-    form.find('input[name="cucina_id[]"]:checked').each(function() {
157
-      cucineIds.push($(this).val());
511
+      $.ajax({
512
+        url: '{{ route('punto-vendita.update') }}',
513
+        method: 'POST',
514
+        data: data,
515
+        headers: { 'Accept': 'application/json' }
516
+      })
517
+      .done(function(res) {
518
+        if (res.success) {
519
+          pvNotyf('success', res.message || 'Salvato');
520
+        } else {
521
+          pvNotyf('error', res.message || 'Errore nel salvataggio');
522
+        }
523
+      })
524
+      .fail(function(xhr) {
525
+        pvAjaxError(xhr, 'Errore nel salvataggio');
526
+      });
527
+    }
528
+
529
+    $(document).on('input.pvAutosave', '[data-pv-save]', function() {
530
+      var $el = $(this);
531
+      if ($el.is('select, :checkbox')) {
532
+        return;
533
+      }
534
+      var name = $el.attr('name');
535
+      if (!name) {
536
+        return;
537
+      }
538
+      clearTimeout(pvTimer[name]);
539
+      pvTimer[name] = setTimeout(function() {
540
+        salvaCampo(name, pvValoreCampo($el));
541
+      }, 500);
542
+    });
543
+
544
+    $(document).on('change.pvAutosave', '[data-pv-save]', function() {
545
+      var $el = $(this);
546
+      if (!$el.is('select, :checkbox')) {
547
+        return;
548
+      }
549
+      var name = $el.attr('name');
550
+      if (!name) {
551
+        return;
552
+      }
553
+      salvaCampo(name, pvValoreCampo($el));
554
+    });
555
+
556
+    function dissociaDispositivo() {
557
+      $.ajax({
558
+        url: '{{ route('punto-vendita.dissocia-dispositivo', ['punto_vendita_id' => $puntoVendita->id]) }}',
559
+        type: 'POST',
560
+        data: {
561
+          punto_vendita_id: pvId,
562
+          _token: '{{ csrf_token() }}'
563
+        },
564
+        headers: { 'Accept': 'application/json' }
565
+      })
566
+      .done(function(response) {
567
+        pvNotyf('success', response.message || 'Disassociazione completata');
568
+        window.location.reload();
569
+      })
570
+      .fail(function(xhr) {
571
+        pvAjaxError(xhr, 'Si è verificato un errore');
572
+      });
573
+    }
574
+
575
+    $(document).on('click.pvDissocia', '#btnDissociaDispositivo', function() {
576
+      if (confirm('Sei sicuro di voler dissociare il dispositivo?')) {
577
+        dissociaDispositivo();
578
+      }
579
+    });
580
+
581
+    $(document).on('click.pvGoTab', '#pv-tab-riepilogo [data-pv-go-tab]', function() {
582
+      var target = $(this).data('pv-go-tab');
583
+      if (!target) {
584
+        return;
585
+      }
586
+      var tabEl = document.querySelector('.nav-link[data-bs-target="' + target + '"]');
587
+      if (tabEl && window.bootstrap) {
588
+        bootstrap.Tab.getOrCreateInstance(tabEl).show();
589
+      }
158 590
     });
159
-    $.ajax({
160
-      url: form.attr('action'),
161
-      method: 'POST',
162
-      data: {
163
-        cucine_ids: cucineIds,
164
-        punto_vendita_id: form.find('input[name="punto_vendita_id"]').val(),
165
-        _token: form.find('input[name="_token"]').val(),
166
-      },
167
-      success: function(response) {
591
+
592
+    $(document).on('click.pvSaveCucine', '#saveCucine', function() {
593
+      var $form = $('#puntoVenditaEditCucineForm');
594
+      var cucineIds = [];
595
+      $form.find('input[name="cucina_id[]"]:checked').each(function() {
596
+        cucineIds.push($(this).val());
597
+      });
598
+
599
+      $.ajax({
600
+        url: $form.attr('action'),
601
+        method: 'POST',
602
+        data: {
603
+          cucine_ids: cucineIds,
604
+          punto_vendita_id: $form.find('input[name="punto_vendita_id"]').val(),
605
+          _token: $form.find('input[name="_token"]').val()
606
+        },
607
+        headers: { 'Accept': 'application/json' }
608
+      })
609
+      .done(function(response) {
168 610
         if (response.success) {
169
-          new Notyf({
170
-            duration: 2000,
171
-            position: { x: 'right', y: 'top' }
172
-          }).success(response.message);
611
+          pvNotyf('success', response.message);
173 612
         } else {
174
-          new Notyf({
175
-            duration: 2000,
176
-            position: { x: 'right', y: 'top' }
177
-          }).error(response.message);
613
+          pvNotyf('error', response.message);
178 614
         }
179
-      }
615
+      })
616
+      .fail(function(xhr) {
617
+        pvAjaxError(xhr, 'Si è verificato un errore');
618
+      });
180 619
     });
181 620
   });
182 621
 </script>
622
+@endsection

+ 6
- 5
resources/views/punto_vendita/menu.blade.php Parādīt failu

@@ -13,17 +13,18 @@ use Illuminate\Support\Facades\Auth;
13 13
         </a>
14 14
         @endif
15 15
 
16
-        @if(Auth::user()->can('view-punto_vendita'))
16
+        <!-- @if(Auth::user()->can('view-punto_vendita'))
17 17
         <a href="{{ route('punto-vendita.dettagli-punto-vendita', ['punto_vendita_id' => $entity->id]) }}" class="dropdown-item">
18 18
             <i class="bx bx-show me-1"></i> Dettagli
19 19
         </a>
20
-        @endif
20
+        @endif -->
21 21
 
22 22
         @if(Auth::user()->can('edit-punto_vendita'))
23
-        <a href="#" class="dropdown-item" onclick="openModalPuntoVendita('{{ $entity->id }}');">
23
+        <a href="{{ route('punto-vendita.edit', ['punto_vendita_id' => $entity->id]) }}" class="dropdown-item">
24 24
             <i class="bx bx-edit-alt me-1"></i> Modifica
25 25
         </a>
26 26
         @endif
27
+        <!--  onclick="openModalPuntoVendita('{{ $entity->id }}');" -->
27 28
 
28 29
         <!-- @if(Auth::user()->can('edit-punto_vendita'))
29 30
         <a href="#" class="dropdown-item editor_edit">
@@ -33,8 +34,8 @@ use Illuminate\Support\Facades\Auth;
33 34
 
34 35
 
35 36
         @if(Auth::user()->can('delete-punto_vendita'))
36
-        <a href="#" class="dropdown-item editor_delete">
37
-            <i class="bx bx-trash me-1"></i> Elimina
37
+        <a href="#" class="dropdown-item editor_delete text-danger">
38
+            <i class="bx bx-trash me-1 text-danger"></i> Elimina
38 39
         </a>
39 40
         @endif
40 41
     </div>

+ 9
- 0
routes/web.php Parādīt failu

@@ -41,6 +41,7 @@ use App\Http\Controllers\CategoriaContabileController;
41 41
 use App\Http\Controllers\RigaOrdineNotificaController;
42 42
 use App\Http\Controllers\OperatoreController;
43 43
 use App\Http\Controllers\TombolaController;
44
+use App\Http\Controllers\ChiusuraServizioController;
44 45
 
45 46
 Route::get('/', [HomePageController::class, 'welcome'])->name('welcome');
46 47
 
@@ -132,6 +133,7 @@ Route::middleware(['auth:sanctum', AttivitaMiddleware::class, config('jetstream.
132 133
   Route::get('punto-vendita/dettagli-punto-vendita', [PuntoVenditaController::class, 'dettagli_punto_vendita'])->name('punto-vendita.dettagli-punto-vendita');
133 134
   Route::get('punto-vendita/show', [PuntoVenditaController::class, 'show'])->name('punto-vendita.show');
134 135
   Route::get('punto-vendita/edit', [PuntoVenditaController::class, 'edit'])->name('punto-vendita.edit');
136
+  Route::post('punto-vendita/update', [PuntoVenditaController::class, 'update'])->name('punto-vendita.update');
135 137
   Route::post('punto-vendita/associa-cucina', [PuntoVenditaController::class, 'associa_cucina'])->name('punto-vendita.associa-cucina');
136 138
   Route::post('punto-vendita/dissocia-dispositivo' , [PuntoVenditaController::class , 'dissocia_dispositivo'])->name('punto-vendita.dissocia-dispositivo');
137 139
 
@@ -142,6 +144,12 @@ Route::middleware(['auth:sanctum', AttivitaMiddleware::class, config('jetstream.
142 144
   Route::get('punto-vendita/cerca-pre-ordine', [PuntoVenditaController::class, 'cerca_pre_ordine'])->name('punto-vendita.cassa.cerca-pre-ordine');
143 145
   Route::post('punto-vendita/importa-pre-ordine', [PuntoVenditaController::class, 'importa_pre_ordine'])->name('punto-vendita.cassa.importa-pre-ordine');
144 146
 
147
+
148
+  // Chiusura Servizio
149
+  Route::resource('chiusura-servizio', ChiusuraServizioController::class, ['only' => ['index', 'store']]);
150
+  Route::get('chiusura-servizio/show', [ChiusuraServizioController::class, 'show'])->name('chiusura-servizio.show');
151
+  Route::post('chiusura-servizio/chiudi-servizio', [ChiusuraServizioController::class, 'chiudi_servizio'])->name('chiusura-servizio.chiudi-servizio');
152
+
145 153
   // Punto Operatore
146 154
   Route::resource('punto-operatore', PuntoOperatoreController::class, ['only' => ['index', 'store']]);
147 155
   Route::get('punto-operatore/toggle-is-attivo', [PuntoOperatoreController::class, 'toggle_is_attivo'])->name('punto-operatore.toggle-is-attivo');
@@ -149,6 +157,7 @@ Route::middleware(['auth:sanctum', AttivitaMiddleware::class, config('jetstream.
149 157
 
150 158
   
151 159
   // Prima Nota
160
+  Route::get('prima-nota/statistiche', [PrimaNotaController::class, 'statistiche'])->name('prima-nota.statistiche');
152 161
   Route::resource('prima-nota', PrimaNotaController::class, ['only' => ['index', 'store']])->names('prima-nota');
153 162
   
154 163
   // Pagamento

Notiek ielāde…
Atcelt
Saglabāt