Quellcode durchsuchen

Fix, auth-operator (inconclusa), test viste cassa

marcofalabretti vor 6 Tagen
Ursprung
Commit
95931fa1ef
100 geänderte Dateien mit 10883 neuen und 1019 gelöschten Zeilen
  1. 8
    0
      .env
  2. 5
    4
      app/DataTables/AllergeneDataTable.php
  3. 4
    3
      app/DataTables/AttivitaDataTable.php
  4. 16
    4
      app/DataTables/CategoriacontabileDataTable.php
  5. 16
    3
      app/DataTables/CucinaDataTable.php
  6. 8
    2
      app/DataTables/CucinaDataTableEditor.php
  7. 37
    16
      app/DataTables/EndpointDataTable.php
  8. 13
    12
      app/DataTables/EventoDataTable.php
  9. 9
    8
      app/DataTables/FornitoreDataTable.php
  10. 6
    5
      app/DataTables/MetodoPagamentoDataTable.php
  11. 128
    0
      app/DataTables/OperatoreDataTable.php
  12. 123
    0
      app/DataTables/OperatoreDataTableEditor.php
  13. 8
    7
      app/DataTables/OrdineDataTable.php
  14. 7
    5
      app/DataTables/PrenotazioneDataTable.php
  15. 22
    7
      app/DataTables/PrimaNotaDataTable.php
  16. 3
    0
      app/DataTables/PrimaNotaDataTableEditor.php
  17. 15
    9
      app/DataTables/PuntoVenditaDataTable.php
  18. 9
    0
      app/Http/Controllers/AttivitaController.php
  19. 206
    47
      app/Http/Controllers/BilancioController.php
  20. 115
    69
      app/Http/Controllers/CarrelloController.php
  21. 232
    71
      app/Http/Controllers/EndpointController.php
  22. 7
    1
      app/Http/Controllers/HomePageController.php
  23. 221
    0
      app/Http/Controllers/OperatoreController.php
  24. 14
    1
      app/Http/Controllers/PrimaNotaController.php
  25. 78
    20
      app/Http/Controllers/PuntovenditaController.php
  26. 18
    0
      app/Http/Controllers/RigaOrdineController.php
  27. 38
    0
      app/Http/Controllers/RigaOrdineNotificaController.php
  28. 122
    0
      app/Http/Controllers/TombolaController.php
  29. 37
    0
      app/Http/Middleware/EndpointTokenMiddleware.php
  30. 27
    0
      app/Jobs/InviaNotificaRigaOrdineProntoJob.php
  31. 6
    0
      app/Models/AbstractModels/AbstractCategoriacontabile.php
  32. 2
    0
      app/Models/AbstractModels/AbstractDispositivo.php
  33. 2
    0
      app/Models/AbstractModels/AbstractEndpoint.php
  34. 80
    0
      app/Models/AbstractModels/AbstractOperatore.php
  35. 2
    0
      app/Models/AbstractModels/AbstractOrdine.php
  36. 7
    0
      app/Models/AbstractModels/AbstractPrimaNota.php
  37. 67
    0
      app/Models/AbstractModels/AbstractRigaOrdineNotifica.php
  38. 36
    1
      app/Models/Attivita.php
  39. 42
    1
      app/Models/Endpoint.php
  40. 10
    0
      app/Models/Operatore.php
  41. 10
    0
      app/Models/RigaOrdineNotifica.php
  42. 37
    0
      app/Models/Tombola.php
  43. 116
    0
      app/Services/Carrello/CarrelloService.php
  44. 72
    0
      app/Services/Notifica/NotificaOrdineService.php
  45. 1
    0
      bootstrap/app.php
  46. 2
    0
      composer.json
  47. 2172
    435
      composer.lock
  48. 8
    5
      config/auth.php
  49. 207
    0
      config/firebase.php
  50. 7
    0
      config/services.php
  51. 1
    0
      database/migrations/2026_03_23_224652_dispositivo.php
  52. 1
    0
      database/migrations/2026_03_27_223826_prima_nota.php
  53. 1
    0
      database/migrations/2026_04_05_112856_endpoint.php
  54. 2
    2
      database/migrations/2026_05_09_131317_saltacoda_rigaordine.php
  55. 5
    0
      database/migrations/2026_05_15_230716_categoria_contabile.php
  56. 38
    0
      database/migrations/2026_05_25_165128_operatore.php
  57. 30
    0
      database/migrations/2026_05_25_165331_operatore_has_dispositivo.php
  58. 28
    0
      database/migrations/2026_05_29_150004_add_notifica_fields_to_ordine_table.php
  59. 34
    0
      database/migrations/2026_05_29_150007_create_ordine_notifica_table.php
  60. 36
    0
      database/migrations/2026_06_06_183117_tombola.php
  61. 113
    0
      database/seeders/CategoriaContabileSeed.php
  62. 26
    0
      database/seeders/TombolaSeed.php
  63. 1451
    63
      package-lock.json
  64. 1
    0
      package.json
  65. 67
    0
      patch-welcome-header.php
  66. 20
    0
      public/firebase-messaging-sw.js
  67. 788
    0
      resources/css/welcome.css
  68. 114
    0
      resources/js/ordine-notifica.js
  69. 38
    24
      resources/menu/verticalMenu.json
  70. 11
    0
      resources/views/_partials/status.blade.php
  71. 509
    0
      resources/views/attivita/public/show.blade.php
  72. 1
    1
      resources/views/bacheca/index.blade.php
  73. 98
    0
      resources/views/bilancio/_partials/costi.blade.php
  74. 98
    0
      resources/views/bilancio/_partials/ricavi.blade.php
  75. 132
    0
      resources/views/bilancio/_partials/sintesi.blade.php
  76. 136
    0
      resources/views/bilancio/_partials/statistiche.blade.php
  77. 1054
    181
      resources/views/bilancio/index.blade.php
  78. 8
    0
      resources/views/categoria_contabile/menu.blade.php
  79. 22
    1
      resources/views/consulta/scontrino/show.blade.php
  80. 64
    0
      resources/views/endpoint/_partials/copy-product-key-script.blade.php
  81. 9
    0
      resources/views/endpoint/_partials/status.blade.php
  82. 108
    0
      resources/views/endpoint/index.blade.php
  83. 294
    0
      resources/views/endpoint/istruzioni.blade.php
  84. 4
    4
      resources/views/endpoint/menu.blade.php
  85. 39
    0
      resources/views/endpoint/nuovo_product_key.blade.php
  86. 331
    0
      resources/views/endpoint/show.blade.php
  87. 5
    1
      resources/views/layouts/guest.blade.php
  88. 2
    2
      resources/views/layouts/sections/navbar/navbar-partial.blade.php
  89. 21
    2
      resources/views/layouts/sections/scripts.blade.php
  90. 21
    2
      resources/views/layouts/sections/scriptsFront.blade.php
  91. 52
    0
      resources/views/operatore/auth/dashDispositivi.blade.php
  92. 31
    0
      resources/views/operatore/dispositivi_list.blade.php
  93. 97
    0
      resources/views/operatore/guardOperatore/dashDispositivi.blade.php
  94. 140
    0
      resources/views/operatore/index.blade.php
  95. 79
    0
      resources/views/operatore/landing.blade.php
  96. 29
    0
      resources/views/operatore/menu.blade.php
  97. 146
    0
      resources/views/operatore/show.blade.php
  98. 109
    0
      resources/views/pagamento/_partials/statistiche.blade.php
  99. 1
    0
      resources/views/pagamento/index.blade.php
  100. 0
    0
      resources/views/prima_nota/_partials/statistiche.blade.php

+ 8
- 0
.env Datei anzeigen

@@ -60,3 +60,11 @@ REDIS_DB=0
60 60
 
61 61
 # PAYPAL_MODE=sandbox
62 62
 # PAYPAL_CURRENCY=CHF
63
+
64
+
65
+FIREBASE_CREDENTIALS=storage/app/firebase-credentials.json
66
+FIREBASE_PROJECT_ID=fest-6f8db
67
+FIREBASE_WEB_API_KEY=AIzaSyD2qHjGcEkHpPJKtj6moHgWxOF9bgiPzo4
68
+FIREBASE_MESSAGING_SENDER_ID=1058806233312
69
+FIREBASE_APP_ID=1:1058806233312:web:ad9f595d3e47a1cec3b885
70
+FIREBASE_VAPID_KEY=BEKtGbo6MB5LqrjUpUyVs6ENo5XQ3lukvV8KqrTnJ5HU5wj_Hz-XQBh3mCAc-4kiCP6-7ZPjOxFWD30HSRdxkrU

+ 5
- 4
app/DataTables/AllergeneDataTable.php Datei anzeigen

@@ -65,6 +65,7 @@ class AllergeneDataTable extends DataTable
65 65
                     ->setTableId($this->dataTableVariable)
66 66
                     ->columns($this->getColumns())
67 67
                     ->minifiedAjax()
68
+                    ->responsive(true)
68 69
                     ->orderBy(1)
69 70
                     ->selectStyleSingle()
70 71
                     ->buttons($buttons)
@@ -94,11 +95,11 @@ class AllergeneDataTable extends DataTable
94 95
         return [
95 96
             Column::make('id')->title('ID')->width('10%')->visible(false),
96 97
             // Column::make('icona_display')->title('Icona')->width('10%'),
97
-            Column::computed('disponibile_display')->title('Disponibile')->addClass('text-center'), // icona in tabella
98
-            Column::make('nome')->title('Nome'),
99
-            Column::make('descrizione')->title('Descrizione'),
98
+            Column::computed('disponibile_display')->title('Disponibile')->addClass('text-center')->width('10%')->responsivePriority(1), // icona in tabella
99
+            Column::make('nome')->title('Nome')->responsivePriority(1),
100
+            Column::make('descrizione')->title('Descrizione')->responsivePriority(2),
100 101
             Column::make('disponibile')->title('Disponibile')->visible(false)->addClass('text-center'), // valore grezzo per l'editor
101
-            Column::computed('action')->title('')->width('10%')->addClass('text-center'),
102
+            Column::computed('action')->title('')->width('10%')->addClass('text-center')->responsivePriority(1),
102 103
         ];
103 104
     }
104 105
 

+ 4
- 3
app/DataTables/AttivitaDataTable.php Datei anzeigen

@@ -75,6 +75,7 @@ class AttivitaDataTable extends DataTable
75 75
                     ->minifiedAjax()
76 76
                     ->orderBy(1)
77 77
                     ->selectStyleSingle()
78
+                    ->responsive(true)
78 79
                     ->language(asset('assets/Italian.json'))
79 80
                     ->dom(count($buttons) == 0 ? 'rtip' : 'Bfrtip')
80 81
                     ->buttons($buttons)
@@ -101,14 +102,14 @@ class AttivitaDataTable extends DataTable
101 102
     {
102 103
         return [
103 104
             Column::make('id')->visible(false),
104
-            Column::make('is_attiva_display')->title('Disponibile')->addClass('text-center'),
105
-            Column::make('nome'),
105
+            Column::make('is_attiva_display')->title('Disponibile')->addClass('text-center')->responsivePriority(1)->width('10%'),
106
+            Column::make('nome')->responsivePriority(1),
106 107
             Column::make('descrizione'),
107 108
             Column::make('path_logo'),
108 109
             Column::computed('seleziona_attivita'),
109 110
             // Column::make('path_icon'),
110 111
             // Column::make('path_image'),
111
-            Column::computed('action')->title('')->width('10%')->addClass('text-center'),
112
+            Column::computed('action')->title('')->width('10%')->addClass('text-center')->responsivePriority(1),
112 113
         ];
113 114
     }
114 115
 

+ 16
- 4
app/DataTables/CategoriacontabileDataTable.php Datei anzeigen

@@ -28,11 +28,18 @@ class CategoriacontabileDataTable extends DataTable
28 28
     {
29 29
         return (new EloquentDataTable($query))
30 30
             ->addColumn('action', function($entity){
31
-                return view('categoriacontabile.menu', ['entity' => $entity]);
31
+                return view('categoria_contabile.menu', ['entity' => $entity]);
32 32
             })
33 33
             ->addColumn('is_attiva_display', function($entity){
34 34
                 return view('_partials.available', ['available' => $entity->is_attiva]);
35 35
             })
36
+            // ->addColumn('colore_display', function($entity){
37
+            //     return '<span class="badge" style="background-color: '.$entity->colore.';">'.$entity->colore.'</span>';
38
+            // })
39
+            ->addColumn('nome_display', function($entity){
40
+                return '<span class="badge" style="background-color: '.$entity->colore.';">'.$entity->nome.'</span>';
41
+            })
42
+            ->rawColumns(['nome_display'])
36 43
             ->setRowId('id');
37 44
     }
38 45
 
@@ -43,7 +50,7 @@ class CategoriacontabileDataTable extends DataTable
43 50
      */
44 51
     public function query(Categoriacontabile $model): QueryBuilder
45 52
     {
46
-        return $model->newQuery();
53
+        return $model->newQuery()->orderBy('nome', 'asc');
47 54
     }
48 55
 
49 56
     /**
@@ -73,6 +80,9 @@ class CategoriacontabileDataTable extends DataTable
73 80
                     ->columns($this->getColumns())
74 81
                     ->minifiedAjax()
75 82
                     ->orderBy(1)
83
+                    ->language(asset('assets/Italian.json'))
84
+                    ->dom(count($buttons) == 0 ? 'rtip' : 'Bfrtip')
85
+                    ->responsive(true)
76 86
                     ->selectStyleSingle()
77 87
                     ->buttons($buttons)
78 88
                     ->editor(
@@ -80,6 +90,7 @@ class CategoriacontabileDataTable extends DataTable
80 90
                             ->fields([
81 91
                                 Fields\Text::make('nome')->label('Nome'),
82 92
                                 Fields\Text::make('descrizione')->label('Descrizione'),
93
+                                Fields\Text::make('colore')->label('Colore')->set('attr', ['type' => 'color']),
83 94
                                 Fields\Boolean::make('is_attiva')->label('Attiva')->default(true),
84 95
                             ])
85 96
                     )
@@ -95,9 +106,10 @@ class CategoriacontabileDataTable extends DataTable
95 106
     {
96 107
         return [
97 108
             Column::make('id'),
98
-            Column::make('nome')->title('Nome'),
99
-            Column::make('descrizione')->title('Descrizione'),
100 109
             Column::make('is_attiva_display')->title('Attiva'),
110
+            // Column::make('colore')->title('Colore')->data('colore_display'),
111
+            Column::make('nome')->title('Nome')->data('nome_display'),
112
+            Column::make('descrizione')->title('Descrizione'),
101 113
             Column::make('action')
102 114
                   ->exportable(false)
103 115
                   ->printable(false)

+ 16
- 3
app/DataTables/CucinaDataTable.php Datei anzeigen

@@ -51,6 +51,12 @@ class CucinaDataTable extends DataTable
51 51
             ->addColumn('stampante_id_display', function($entity){
52 52
               return $entity->stampante ? $entity->stampante->nome : '';
53 53
             })
54
+            ->addColumn('badge_colore', function($entity){
55
+              // dd($entity->info['colore']);
56
+              $colore = $entity->info['colore'] ?? '#808080';
57
+              return '<span class="badge" style="background-color: '.$colore.';">'.$entity->nome.'</span>';
58
+            })
59
+            ->rawColumns(['badge_colore'])
54 60
             ->setRowId('id');
55 61
     }
56 62
 
@@ -94,6 +100,7 @@ class CucinaDataTable extends DataTable
94 100
                     ->editor(
95 101
                         Editor::make()
96 102
                         ->fields([
103
+                            Fields\Text::make('colore')->label('Colore')->set('attr', ['type' => 'color']),
97 104
                             Fields\Text::make('nome')->label('Nome'),
98 105
                             Fields\Text::make('descrizione')->label('Descrizione'),
99 106
                             Fields\Image::make('immagine')->label('Immagine'),
@@ -115,14 +122,20 @@ class CucinaDataTable extends DataTable
115 122
         return [
116 123
             Column::make('id')->visible(false),
117 124
             Column::make('is_attiva_display')->title('Disponibile')->addClass('text-center'),
118
-            Column::make('nome'),
125
+            // Column::make('nome'),
126
+            Column::make('badge_colore')->title('Nome')->addClass('text-left'),
119 127
             Column::make('descrizione'),
120 128
             Column::computed('piatti_count')->title('Piatti')->addClass('text-center'),
121 129
             // Column::make('immagine'),
122
-            Column::make('attivita_id')->data('attivita_id_display')->title('Attività')->addClass('text-center'),
130
+            Column::make('attivita_id')->data('attivita_id_display')->title('Attività')->addClass('text-center')->visible(false),
123 131
             Column::make('stampante_id')->data('stampante_display')->title('Stampante')->addClass('text-center'),
124 132
             // Column::make('cucina_id')->data('cucina_id_display')->title('Cucina')->addClass('text-center'),
125
-            Column::make('action'),
133
+            Column::make('action')
134
+            ->title('')
135
+            ->exportable(false)
136
+            ->printable(false)
137
+            ->width(60)
138
+            ->addClass('text-center'),
126 139
         ];
127 140
     }
128 141
 

+ 8
- 2
app/DataTables/CucinaDataTableEditor.php Datei anzeigen

@@ -35,7 +35,7 @@ class CucinaDataTableEditor extends DataTablesEditor
35 35
       'nome'  => 'required',
36 36
       'descrizione' => 'nullable',
37 37
       'immagine' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
38
-      'is_attiva' => 'boolean',
38
+      // 'is_attiva' => 'boolean',
39 39
     ];
40 40
   }
41 41
 
@@ -55,7 +55,7 @@ class CucinaDataTableEditor extends DataTablesEditor
55 55
       'nome'  => 'required',
56 56
       'descrizione' => 'nullable',
57 57
       'immagine' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
58
-      'is_attiva' => 'boolean',
58
+      // 'is_attiva' => 'boolean',
59 59
     ];
60 60
   }
61 61
 
@@ -78,6 +78,9 @@ class CucinaDataTableEditor extends DataTablesEditor
78 78
   {
79 79
     // $model->roles()->sync([$data['ruolo_id']]);
80 80
     $data['is_attiva'] = isset($data['is_attiva']) ? true : false;
81
+    $data['info'] =  [
82
+      'colore' => $data['colore'],
83
+    ];
81 84
     return $data;
82 85
   }
83 86
 
@@ -86,6 +89,9 @@ class CucinaDataTableEditor extends DataTablesEditor
86 89
     // dd($data['ruolo']);
87 90
     // $model->roles()->sync([$data['ruolo_id']]);
88 91
     $data['is_attiva'] = isset($data['is_attiva']) ? true : false;
92
+    $data['info'] =  [
93
+      'colore' => $data['colore'],
94
+    ];
89 95
     return $data;
90 96
   }
91 97
   public function messages(): array

+ 37
- 16
app/DataTables/EndpointDataTable.php Datei anzeigen

@@ -37,6 +37,12 @@ class EndpointDataTable extends DataTable
37 37
             ->addColumn('stampanti_display', function($entity){
38 38
                 return Endpoint::find($entity->id)->stampanti->pluck('nome')->implode(', ');
39 39
             })
40
+            ->addColumn('product_key_display', function($entity){
41
+                return view('endpoint.nuovo_product_key', ['success' => true, 'endpoint' => $entity, 'datatable' => true]);
42
+            })
43
+            ->addColumn('status_display', function($entity){
44
+                return view('endpoint._partials.status', ['status' => $entity->status]);
45
+            })
40 46
             ->setRowId('id');
41 47
     }
42 48
 
@@ -57,12 +63,25 @@ class EndpointDataTable extends DataTable
57 63
     {
58 64
         $buttons = [];
59 65
         if(Auth::user()->can('create-endpoint')){
60
-            array_push($buttons, Button::make('create')
61
-                ->editor('editor')
66
+            // array_push($buttons, Button::make('create')
67
+            //     ->editor('editor')
68
+            //     ->className('btn btn-sm btn-primary mb-4')
69
+            //     ->formTitle('Crea nuovo endpoint')
70
+            //     ->text('<i class="fas fa-plus"></i> Nuovo endpoint'));
71
+            array_push($buttons, Button::raw()
62 72
                 ->className('btn btn-sm btn-primary mb-4')
63
-                ->formTitle('Crea nuovo endpoint')
64
-                ->text('<i class="fas fa-plus"></i> Nuovo endpoint'));
73
+                ->action('function(){
74
+                    richiediNuovoProductKey();
75
+                }')
76
+                ->text('<i class="fas fa-plus"></i> Richiedi nuovo product key'));
65 77
         }
78
+        if(Auth::user()->can('view-endpoint')){
79
+            array_push($buttons, Button::raw()
80
+                ->className('btn btn-sm btn-primary mb-4')
81
+                ->action('istruzioni()')
82
+                ->text('<i class="fas fa-info-circle"></i> Istruzioni'));
83
+        }
84
+
66 85
         return $this->builder()
67 86
                     ->setTableId($this->dataTableVariable)
68 87
                     ->columns($this->getColumns())
@@ -94,22 +113,23 @@ class EndpointDataTable extends DataTable
94 113
     public function getColumns(): array
95 114
     {
96 115
         return [
97
-            Column::make('id')->title('ID')->addClass('text-center w-10'),
98
-            Column::make('label')->title('Label'),
116
+            Column::make('id')->title('ID')->addClass('text-center w-10')->responsivePriority(1),
117
+            Column::make('status')->title('Status')->data('status_display'),
118
+            Column::make('label')->title('Etichetta')->responsivePriority(1),
99 119
             Column::make('descrizione')->title('Descrizione'),
100 120
             Column::make('ubicazione')->title('Ubicazione'),
101 121
             Column::make('stampanti_display')->title('Stampanti'),
102
-            Column::make('url')->title('URL'),
103
-            Column::make('user_id')->title('User ID'),
104
-            Column::make('token')->title('Token'),
105
-            Column::make('secret')->title('Secret'),
106
-            Column::make('status')->title('Status'),
107
-            Column::make('type')->title('Type'),
108
-            Column::make('version')->title('Version'),
122
+            Column::make('url')->title('URL')->visible(false),
123
+            Column::make('user_id')->title('User ID')->visible(false),
124
+            Column::make('product_key_display')->title('Product Key'),
125
+            // Column::make('token')->title('Token'),
126
+            // Column::make('secret')->title('Secret'),
127
+            // Column::make('type')->title('Type'),
128
+            // Column::make('version')->title('Version'),
109 129
             Column::make('licenza_id')->title('Licenza ID'),
110 130
             Column::make('stampanti_count')->title('Stampanti'),
111
-            Column::make('last_heartbeat')->title('Last Heartbeat'),
112
-            Column::make('last_activity')->title('Last Activity'),
131
+            // Column::make('last_heartbeat')->title('Last Heartbeat'),
132
+            // Column::make('last_activity')->title('Last Activity'),
113 133
             // Column::make('created_at')->title('Created At'),
114 134
             // Column::make('updated_at')->title('Updated At'),
115 135
             Column::computed('action')
@@ -117,7 +137,8 @@ class EndpointDataTable extends DataTable
117 137
                   ->exportable(false)
118 138
                   ->printable(false)
119 139
                   ->width(60)
120
-                  ->addClass('text-center'),
140
+                  ->addClass('text-center')
141
+                  ->responsivePriority(1),
121 142
         ];
122 143
     }
123 144
 

+ 13
- 12
app/DataTables/EventoDataTable.php Datei anzeigen

@@ -134,24 +134,25 @@ class EventoDataTable extends DataTable
134 134
     {
135 135
         return [
136 136
             Column::make('id')->title('ID')->width('10%')->visible(false),
137
-            Column::make('stato')->data('stato_display')->title('Stato')->addClass('text-center'),
138
-            Column::make('nome')->title('Nome'),
137
+            Column::make('stato')->data('stato_display')->title('Stato')->addClass('text-center')->responsivePriority(1)->width('10%'),
138
+            Column::make('nome')->title('Nome')->responsivePriority(1),
139 139
             // Column::make('descrizione')->title('Descrizione'),
140
-            Column::computed('periodo_evento_display')->title('Periodo evento'),
141
-            Column::computed('periodo_prenotazioni_display')->title('Periodo prenotazioni'),
142
-            Column::make('data_inizio')->title('Data inizio')->data('data_inizio_display')->visible(false),
143
-            Column::make('data_fine')->title('Data fine')->data('data_fine_display')->visible(false),
144
-            Column::make('apertura_prenotazione')->title('Apertura prenotazione')->data('apertura_prenotazione_display')->visible(false),
145
-            Column::make('chiusura_prenotazione')->title('Chiusura prenotazione')->data('chiusura_prenotazione_display')->visible(false),
146
-            Column::make('prezzo_fisso')->title('Prezzo fisso')->data('prezzo_fisso_display')->addClass('text-center'),
147
-            Column::make('prezzo')->title('Prezzo')->data('prezzo_display')->addClass('text-center'),
148
-            Column::make('note')->title('Note')->visible(false),
140
+            Column::computed('periodo_evento_display')->title('Periodo evento')->responsivePriority(2),
141
+            Column::computed('periodo_prenotazioni_display')->title('Periodo prenotazioni')->responsivePriority(2),
142
+            Column::make('data_inizio')->title('Data inizio')->data('data_inizio_display')->visible(false)->responsivePriority(2),
143
+            Column::make('data_fine')->title('Data fine')->data('data_fine_display')->visible(false)->responsivePriority(2),
144
+            Column::make('apertura_prenotazione')->title('Apertura prenotazione')->data('apertura_prenotazione_display')->visible(false)->responsivePriority(2),
145
+            Column::make('chiusura_prenotazione')->title('Chiusura prenotazione')->data('chiusura_prenotazione_display')->visible(false)->responsivePriority(2),
146
+            Column::make('prezzo_fisso')->title('Prezzo fisso')->data('prezzo_fisso_display')->addClass('text-center')->responsivePriority(2),
147
+            Column::make('prezzo')->title('Prezzo')->data('prezzo_display')->addClass('text-center')->responsivePriority(2),
148
+            Column::make('note')->title('Note')->visible(false)->responsivePriority(2),
149 149
             Column::computed('action')
150 150
                   ->title('')
151 151
                   ->exportable(false)
152 152
                   ->printable(false)
153 153
                   ->width(60)
154
-                  ->addClass('text-center'),
154
+                  ->addClass('text-center')
155
+                  ->responsivePriority(1),
155 156
         ];
156 157
     }
157 158
 

+ 9
- 8
app/DataTables/FornitoreDataTable.php Datei anzeigen

@@ -62,6 +62,7 @@ class FornitoreDataTable extends DataTable
62 62
                     ->columns($this->getColumns())
63 63
                     ->minifiedAjax()
64 64
                     ->orderBy(1)
65
+                    ->responsive(true)
65 66
                     ->selectStyleSingle()
66 67
                     ->language(asset('assets/Italian.json'))
67 68
                     ->dom(count($buttons) == 0 ? 'rtip' : 'Bfrtip')
@@ -93,15 +94,15 @@ class FornitoreDataTable extends DataTable
93 94
     {
94 95
         return [
95 96
             Column::make('id')->visible(false),
96
-            Column::make('nome'),
97
-            Column::make('email'),
98
-            Column::make('telefono'),
99
-            Column::make('indirizzo'),
100
-            Column::make('citta'),
101
-            Column::make('provincia'),
102
-            Column::make('cap'),
97
+            Column::make('nome')->responsivePriority(1),
98
+            Column::make('email')->responsivePriority(2),
99
+            Column::make('telefono')->responsivePriority(1),
100
+            Column::make('indirizzo')->responsivePriority(2),
101
+            Column::make('citta')->responsivePriority(2),
102
+            Column::make('provincia')->responsivePriority(2),
103
+            Column::make('cap')->responsivePriority(2),
103 104
             Column::make('paese'),
104
-            Column::computed('action')
105
+            Column::computed('action')->responsivePriority(1)
105 106
                 ->title('')
106 107
                 ->exportable(false)
107 108
                 ->printable(false)

+ 6
- 5
app/DataTables/MetodoPagamentoDataTable.php Datei anzeigen

@@ -73,6 +73,7 @@ class MetodoPagamentoDataTable extends DataTable
73 73
                     ->dom(count($buttons) == 0 ? 'rtip' : 'Bfrtip')
74 74
                     ->buttons($buttons)
75 75
                     ->orderBy(1)
76
+                    ->responsive(true)
76 77
                     ->selectStyleSingle()
77 78
                     ->editor(
78 79
                         Editor::make()
@@ -101,11 +102,11 @@ class MetodoPagamentoDataTable extends DataTable
101 102
     {
102 103
         return [
103 104
             Column::make('id')->title('ID')->width('10%')->visible(false),
104
-            Column::make('is_attivo')->data('is_attivo_display')->title('Attivo')->addClass('text-center w-10'),
105
-            Column::make('is_pubblico')->data('is_pubblico_display')->title('Pubblico')->addClass('text-center w-10'),
106
-            Column::make('nome')->title('Nome'),
107
-            Column::make('descrizione')->title('Descrizione'),
108
-            Column::computed('action')->title('')->width('10%')->addClass('text-center'),   
105
+            Column::make('is_attivo')->data('is_attivo_display')->title('Attivo')->addClass('text-center w-10')->responsivePriority(1)->width('10%'),
106
+            Column::make('is_pubblico')->data('is_pubblico_display')->title('Pubblico')->addClass('text-center w-10')->responsivePriority(2)->width('10%'),
107
+            Column::make('nome')->title('Nome')->responsivePriority(1),
108
+            Column::make('descrizione')->title('Descrizione')->responsivePriority(2),
109
+            Column::computed('action')->title('')->width('10%')->addClass('text-center')->responsivePriority(1),   
109 110
         ];
110 111
     }
111 112
 

+ 128
- 0
app/DataTables/OperatoreDataTable.php Datei anzeigen

@@ -0,0 +1,128 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Operatore;
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 OperatoreDataTable extends DataTable
17
+{
18
+    public function __construct()
19
+    {
20
+        $this->dataTableVariable = 'dataTable_operatore';
21
+    }
22
+    /**
23
+     * Build the DataTable class.
24
+     *
25
+     * @param QueryBuilder<Operatore> $query Results from query() method.
26
+     */
27
+    public function dataTable(QueryBuilder $query): EloquentDataTable
28
+    {
29
+        return (new EloquentDataTable($query))
30
+            ->addColumn('action', function($entity){
31
+                return view('operatore.menu', ['entity' => $entity]);
32
+            })
33
+            ->addColumn('is_attivo_display', function($entity){
34
+                return view('_partials.available', ['available' => $entity->is_attivo]);
35
+            })
36
+            ->setRowId('id');
37
+    }
38
+
39
+    /**
40
+     * Get the query source of dataTable.
41
+     *
42
+     * @return QueryBuilder<Operatore>
43
+     */
44
+    public function query(Operatore $model): QueryBuilder
45
+    {
46
+        return $model->newQuery()->where('attivita_id', $this->attivita_id);
47
+    }
48
+
49
+    /**
50
+     * Optional method if you want to use the html builder.
51
+     */
52
+    public function html(): HtmlBuilder
53
+    {
54
+        $buttons = [];
55
+        if(Auth::user()->can('create-operatore')){
56
+            array_push($buttons, Button::make('create')
57
+                ->editor('editor')
58
+                ->className('btn btn-sm btn-primary mb-4')
59
+                ->formTitle('Crea nuovo operatore')
60
+                ->formButtons([
61
+                    Button::raw('Annulla')
62
+                    ->className('btn btn-secondary ml-2')
63
+                    ->actionClose(),
64
+                    Button::raw('Salva')
65
+                    ->className('btn btn-success ml-2')
66
+                    ->actionHandler('create')
67
+                ])
68
+                ->text('<i class="fas fa-plus"></i> Nuovo operatore'));
69
+        }
70
+        return $this->builder()
71
+                    ->setTableId($this->dataTableVariable)
72
+                    ->columns($this->getColumns())
73
+                    ->minifiedAjax()
74
+                    ->orderBy(1)
75
+                    ->selectStyleSingle()
76
+                    ->responsive(true)
77
+                    ->language(asset('assets/Italian.json'))
78
+                    ->dom(count($buttons) == 0 ? 'rtip' : 'Bfrtip')
79
+                    ->buttons($buttons)
80
+                    ->editor(
81
+                        Editor::make()
82
+                            ->fields([
83
+                                Fields\Text::make('nome')->label('Nome'),
84
+                                Fields\Text::make('cognome')->label('Cognome'),
85
+                                Fields\Text::make('email')->label('Email'),
86
+                                Fields\Text::make('telefono')->label('Telefono'),
87
+                                // Fields\Text::make('username')->label('Username'),
88
+                                // Fields\Text::make('password')->label('Password'),
89
+                                Fields\Boolean::make('is_attivo')->label('Attivo')->default(true),
90
+                            ])
91
+                    )
92
+                    ->initComplete("function(settings, json){
93
+                        initComplete_operatore();
94
+                    }");
95
+    }
96
+
97
+    /**
98
+     * Get the dataTable columns definition.
99
+     */
100
+    public function getColumns(): array
101
+    {
102
+        return [
103
+            Column::make('is_attivo_display')->title('Operativo'),
104
+            Column::make('id')->visible(false),
105
+            Column::make('username')->responsivePriority(1),
106
+            // Column::make('nome'),
107
+            // Column::make('cognome'),
108
+            Column::make('email'),
109
+            // Column::make('telefono'),
110
+            // Column::make('password'),
111
+            Column::computed('action')
112
+                  ->addClass('text-center')
113
+                  ->title('')
114
+                  ->exportable(false)
115
+                  ->printable(false)
116
+                  ->width('10%')
117
+                  ->responsivePriority(1),
118
+        ];
119
+    }
120
+
121
+    /**
122
+     * Get the filename for export.
123
+     */
124
+    protected function filename(): string
125
+    {
126
+        return 'Operatore_' . date('YmdHis');
127
+    }
128
+}

+ 123
- 0
app/DataTables/OperatoreDataTableEditor.php Datei anzeigen

@@ -0,0 +1,123 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Operatore;
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
+use Storage;
12
+use Illuminate\Support\Str;
13
+use Illuminate\Support\Facades\Hash;
14
+
15
+class OperatoreDataTableEditor extends DataTablesEditor
16
+{
17
+  protected $model = Operatore::class;
18
+  // protected $uploadDir = '';
19
+  // protected $disk = 'modelliRicevute';
20
+
21
+  protected $messages = [
22
+    'nome.required' => 'Il nome è richiesto',
23
+    'cognome.required' => 'Il cognome è richiesto',
24
+    'email.required' => 'L\'email è richiesta',
25
+    'email.unique' => 'L\'email deve essere unica',
26
+  ];
27
+
28
+  /**
29
+  * Get create action validation rules.
30
+  *
31
+  * @return array
32
+  */
33
+  public function createRules(): array
34
+  {
35
+    return [
36
+      'nome'  => 'required',
37
+      'cognome' => 'required',
38
+      'email' => 'required|email|unique:operatore,email',
39
+      'telefono' => 'nullable',
40
+      // 'username' => 'required|unique:operatore,username',
41
+      // 'password' => 'required',
42
+      'is_attivo' => 'boolean',
43
+    ];
44
+  }
45
+
46
+  public function createMessages(): array{
47
+    return $this->messages;
48
+  }
49
+
50
+  /**
51
+  * Get edit action validation rules.
52
+  *
53
+  * @param Model $model
54
+  * @return array
55
+  */
56
+  public function editRules(Model $model): array
57
+  {
58
+    return [
59
+      'nome'  => 'required',
60
+      'cognome' => 'required',
61
+      'email' => 'nullable|email',
62
+      'telefono' => 'nullable',
63
+      // 'username' => 'required|unique:operatore,username',
64
+      // 'password' => 'required',
65
+      'is_attivo' => 'boolean',
66
+    ];
67
+  }
68
+
69
+  public function editMessages(): array{
70
+    return $this->messages;
71
+  }
72
+
73
+  /**
74
+  * Get remove action validation rules.
75
+  *
76
+  * @param Model $model
77
+  * @return array
78
+  */
79
+  public function removeRules(Model $model): array
80
+  {
81
+    return [];
82
+  }
83
+
84
+  public function creating(Model $model, array $data): array
85
+  {
86
+    // $model->roles()->sync([$data['ruolo_id']]);
87
+    $data['is_attivo'] = isset($data['is_attivo']) ? true : false;
88
+    if(isset($data['password'])){
89
+      $data['password'] = Hash::make($data['password']);
90
+    }
91
+    $data['username'] = Str::slug($data['nome'] . '.' . $data['cognome']);
92
+    return $data;
93
+  }
94
+
95
+  public function updating(Model $model, array $data): array
96
+  {
97
+    // dd($data['ruolo']);
98
+    // $model->roles()->sync([$data['ruolo_id']]);
99
+    $data['is_attivo'] = isset($data['is_attivo']) ? true : false;
100
+    if(isset($data['password'])){
101
+    $data['password'] = Hash::make($data['password']);
102
+    }
103
+    $data['username'] = $this->createUsername($data);
104
+    return $data;
105
+  }
106
+  public function messages(): array
107
+  {
108
+    return $this->messages;
109
+  }
110
+
111
+  public function createUsername(array $data): string{
112
+     $baseUsername = strtolower($data['nome']) . '.' . strtolower($data['cognome']);
113
+     $username = $baseUsername;
114
+     $i = 1;
115
+     while (\App\Models\Operatore::where('username', $username)->exists()) {
116
+         $username = $baseUsername . $i;
117
+         $i++;
118
+     }
119
+     return $username;
120
+
121
+  }
122
+
123
+}

+ 8
- 7
app/DataTables/OrdineDataTable.php Datei anzeigen

@@ -137,19 +137,20 @@ class OrdineDataTable extends DataTable
137 137
     public function getColumns(): array
138 138
     {
139 139
         return [
140
-            Column::make('id')->title('ID')->addClass('text-center w-10'),
141
-            Column::make('stato')->data('stato_display')->title('Stato'),
142
-            Column::make('riferimento')->title('Riferimento'),
143
-            Column::make('prenotazione_id')->data('prenotazione_id_display')->title('Prenotazione'),
144
-            Column::make('prezzo')->title('Totale')->data('prezzo_display'),
145
-            Column::make('note')->title('Note'),
146
-            Column::make('info')->title('Info')->data('info_display'),
140
+            Column::make('id')->title('ID')->addClass('text-center w-10')->responsivePriority(1)->width('10%'),
141
+            Column::make('stato')->data('stato_display')->title('Stato')->responsivePriority(1),
142
+            Column::make('riferimento')->title('Riferimento')->responsivePriority(2),
143
+            Column::make('prenotazione_id')->data('prenotazione_id_display')->title('Prenotazione')->responsivePriority(2),
144
+            Column::make('prezzo')->title('Totale')->data('prezzo_display')->responsivePriority(2),
145
+            Column::make('note')->title('Note')->responsivePriority(2),
146
+            Column::make('info')->title('Info')->data('info_display')->responsivePriority(2),
147 147
             Column::computed('action')
148 148
                   ->title('')
149 149
                   ->exportable(false)
150 150
                   ->printable(false)
151 151
                   ->width(60)
152 152
                   ->addClass('text-center')
153
+                  ->responsivePriority(1),
153 154
         ];
154 155
     }
155 156
 

+ 7
- 5
app/DataTables/PrenotazioneDataTable.php Datei anzeigen

@@ -126,6 +126,7 @@ $fields = [
126 126
                     ->setTableId($this->dataTableVariable)
127 127
                     ->columns($this->getColumns())
128 128
                     ->minifiedAjax()
129
+                    ->responsive(true)
129 130
                     ->orderBy(2, 'asc')
130 131
                     ->selectStyleSingle()
131 132
                     ->language(asset('assets/Italian.json'))
@@ -147,10 +148,10 @@ $fields = [
147 148
     public function getColumns(): array
148 149
     {
149 150
         return [
150
-            Column::make('id')->title('ID')->width('10%')->visible(false),
151
-            Column::make('stato')->data('stato_display')->title('Stato')->searchable(true)->filter(true),
152
-            Column::make('codice')->title('Codice'),
153
-            Column::make('cognome')->title('Cognome'),
151
+            Column::make('id')->title('ID')->width('10%')->visible(false)->responsivePriority(1)->width('10%'),
152
+            Column::make('stato')->data('stato_display')->title('Stato')->searchable(true)->filter(true)->responsivePriority(1),
153
+            Column::make('codice')->title('Codice')->responsivePriority(1),
154
+            Column::make('cognome')->title('Cognome')->responsivePriority(2),
154 155
             Column::make('nome')->title('Nome'),
155 156
             Column::make('email')->title('Email'),
156 157
             Column::make('telefono')->title('Telefono'),
@@ -162,7 +163,8 @@ $fields = [
162 163
                   ->exportable(false)
163 164
                   ->printable(false)
164 165
                   ->width(60)
165
-                  ->addClass('text-center'),
166
+                  ->addClass('text-center')
167
+                  ->responsivePriority(1),
166 168
         ];
167 169
     }
168 170
 

+ 22
- 7
app/DataTables/PrimaNotaDataTable.php Datei anzeigen

@@ -103,6 +103,10 @@ class PrimaNotaDataTable extends DataTable
103 103
             ->addColumn('data_display', function($entity){
104 104
                 return Carbon::parse($entity->created_at)->format('d/m/Y H:i');
105 105
             })
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
+            })
109
+            ->rawColumns(['categoria_contabile_id_display'])
106 110
             ->setRowId('id');
107 111
     }
108 112
 
@@ -146,25 +150,35 @@ class PrimaNotaDataTable extends DataTable
146 150
                 }')
147 151
                 ->text('<i class="fas fa-file-alt"></i> Report'));
148 152
         }
153
+
154
+        if(Auth::user()->can('view-bilancio')){
155
+            array_push($buttons, Button::raw('Consulta dati')
156
+                ->className('btn btn-sm btn-info mb-4')
157
+                ->action('window.location.href = "'.route('bilancio.index').'";')
158
+                ->text('<i class="fas fa-chart-line"></i> Consulta dati'));
159
+        }
149 160
         
150 161
         return $this->builder()
151 162
                     ->setTableId($this->dataTableVariable)
152 163
                     ->columns($this->getColumns())
153 164
                     ->minifiedAjax()
154 165
                     ->language(asset('assets/Italian.json'))
155
-                    ->dom(count($buttons) == 0 ? 'rtip' : 'Bfrtip')
166
+                    // ->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>>')
167
+                    ->dom(count($buttons) == 0 ? 'Prtip' : 'PBfrtip')
156 168
                     ->buttons($buttons)
169
+                    // ->searchPanes(true)
157 170
                     ->responsive(true)
158
-                    ->pageLength(25)
171
+                    // ->pageLength(25)
159 172
                     ->lengthMenu([25, 50, 100])
160 173
                     ->orderBy(1)    
161
-                    ->selectStyleSingle()
174
+                    // ->selectStyleSingle()
162 175
                     ->buttons($buttons)
163 176
                     ->editor(
164 177
                         Editor::make()
165 178
                             ->fields([
166 179
                                 Fields\Hidden::make('attivita_id')->default($this->attivita_id),
167 180
                                 Fields\Select2::make('tipo_movimento')->label('Tipo movimento')->options(PrimaNota::getTipiMovimento()->pluck('value', 'label')),
181
+                                Fields\Select2::make('categoria_contabile_id')->label('Categoria contabile')->options(\App\Models\Categoriacontabile::where('is_attiva', true)->pluck('id', 'nome')),
168 182
                                 Fields\Text::make('importo')->label('Importo'),
169 183
                                 Fields\Text::make('causale')->label('Causale'),
170 184
                                 Fields\Text::make('riferimento')->label('Riferimento'),
@@ -204,10 +218,11 @@ class PrimaNotaDataTable extends DataTable
204 218
         return [
205 219
             Column::make('id')->title('ID')->width('10%')->visible(false),
206 220
             Column::make('attivita_id')->title('Attività')->data('attivita_id_display')->visible(false),
207
-            Column::computed('data_display')->title('Data'),
208
-            Column::make('causale')->title('Causale')->data('causale_display'),
209
-            Column::make('entrata')->title('Entrata')->data('entrata_display'),
210
-            Column::make('uscita')->title('Uscita')->data('uscita_display'),
221
+            Column::computed('data_display')->title('Data')->searchable(true),
222
+            Column::make('categoria_contabile_id')->title('Categoria contabile')->data('categoria_contabile_id_display')->searchable(true),
223
+            Column::make('causale')->title('Causale')->data('causale_display')->searchable(true),
224
+            Column::make('entrata')->title('Entrata')->data('entrata_display')->searchable(false),
225
+            Column::make('uscita')->title('Uscita')->data('uscita_display')->searchable(false),
211 226
             // Column::make('pagamento_id')->title('Pagamento')->data('pagamento_id_display'),
212 227
             // Column::make('ordine_id')->title('Ordine')->data('ordine_id_display'),
213 228
             // Column::make('prenotazione_id')->title('Prenotazione')->data('prenotazione_id_display'),

+ 3
- 0
app/DataTables/PrimaNotaDataTableEditor.php Datei anzeigen

@@ -24,6 +24,7 @@ class PrimaNotaDataTableEditor extends DataTablesEditor
24 24
     'tipo_movimento.required' => 'Il tipo movimento è richiesto',
25 25
     'causale.required' => 'La causale è richiesta',
26 26
     'stato.required' => 'Il stato è richiesto',
27
+    'categoria_contabile_id.required' => 'La categoria contabile è richiesta',
27 28
   ];
28 29
 
29 30
   /**
@@ -44,6 +45,7 @@ class PrimaNotaDataTableEditor extends DataTablesEditor
44 45
       'importo' => 'required|numeric',
45 46
       'tipo_movimento' => 'required|string',   
46 47
       'causale' => 'required|string',
48
+      'categoria_contabile_id' => 'required|exists:categoria_contabile,id',
47 49
       // 'stato' => 'nullable|string',
48 50
       // 'note' => 'nullable|string',
49 51
       // 'codice' => 'nullable|string',
@@ -76,6 +78,7 @@ class PrimaNotaDataTableEditor extends DataTablesEditor
76 78
       'tipo_movimento' => 'required|string',   
77 79
       'causale' => 'required|string',
78 80
       'stato' => 'nullable|string',
81
+      'categoria_contabile_id' => 'required|exists:categoria_contabile,id',
79 82
       // 'note' => 'nullable|string',
80 83
       // 'codice' => 'nullable|string',
81 84
       'stato_pagamento' => 'nullable|string',

+ 15
- 9
app/DataTables/PuntoVenditaDataTable.php Datei anzeigen

@@ -43,6 +43,9 @@ class PuntoVenditaDataTable extends DataTable
43 43
             ->addColumn('url_stampante_display', function($entity){
44 44
                 return $entity->url_stampante ?? 'ND';
45 45
             })
46
+            ->addColumn('abbinato_display', function($entity){
47
+                return $entity->binding_token ? 'Abbinato' : 'Non abbinato';
48
+            })
46 49
             // ->rawColumns(['punto_vendita_tipo_display'])
47 50
             ->setRowId('id');
48 51
     }
@@ -76,6 +79,7 @@ class PuntoVenditaDataTable extends DataTable
76 79
                     ->setTableId($this->dataTableVariable)
77 80
                     ->columns($this->getColumns())
78 81
                     ->minifiedAjax()
82
+                    ->responsive(true)
79 83
                     ->language(asset('assets/Italian.json'))
80 84
                     ->dom(count($buttons) == 0 ? 'rtip' : 'Bfrtip')
81 85
                     ->buttons($buttons)
@@ -111,16 +115,17 @@ class PuntoVenditaDataTable extends DataTable
111 115
     {
112 116
         return [
113 117
             Column::make('id')->visible(false),
114
-            Column::make('is_attivo')->data('is_attivo_display')->title('Disponibile'),
115
-            Column::make('nome')->title('Nome'),
116
-            Column::make('attivita_id')->title('Attività'),
117
-            Column::make('tipo')->title('Tipo')->visible(true),
118
+            Column::make('is_attivo')->data('is_attivo_display')->title('Disponibile')->responsivePriority(1)->width('10%'),
119
+            Column::make('nome')->title('Nome')->responsivePriority(1),
120
+            Column::make('attivita_id')->title('Attività')->responsivePriority(2),
121
+            Column::make('tipo')->title('Tipo')->visible(true)->responsivePriority(2),
118 122
             // Column::computed('punto_vendita_tipo_display')->title('Tipo'),
119
-            Column::make('licenza')->title('Licenza'),
120
-            Column::make('url_stampante')->title('URL della stampante'),
121
-            Column::make('ubicazione')->title('Ubicazione'),
123
+            Column::make('licenza')->title('Licenza')->responsivePriority(2),
124
+            Column::make('url_stampante')->title('URL della stampante')->responsivePriority(2),
125
+            Column::make('ubicazione')->title('Ubicazione')->responsivePriority(2),
122 126
             // Column::make('note')->title('Note'),
123
-            Column::make('pin_sblocco')->title('Pin di sblocco'),
127
+            Column::make('pin_sblocco')->title('Pin di sblocco')->responsivePriority(2),
128
+            Column::computed('abbinato_display')->title('Abbinato')->responsivePriority(2),
124 129
             // Column::make('data_apertura_dispositivo')->title('Data di apertura del dispositivo'),
125 130
             // Column::make('data_chiusura_dispositivo')->title('Data di chiusura del dispositivo'),
126 131
             Column::computed('action')
@@ -128,7 +133,8 @@ class PuntoVenditaDataTable extends DataTable
128 133
                   ->exportable(false)
129 134
                   ->printable(false)
130 135
                   ->width(60)
131
-                  ->addClass('text-center'),
136
+                  ->addClass('text-center')
137
+                  ->responsivePriority(1),
132 138
         ];
133 139
     }
134 140
 

+ 9
- 0
app/Http/Controllers/AttivitaController.php Datei anzeigen

@@ -86,4 +86,13 @@ class AttivitaController extends Controller
86 86
             'attivita' => $attivita
87 87
         ]);
88 88
     }
89
+
90
+    public function show_public(Request $request)
91
+    {
92
+        $attivita = Attivita::find($request->attivita_id);
93
+        if(!$attivita) {
94
+            return redirect()->route('welcome')->with('error', 'Attività non trovata');
95
+        }
96
+        return view('attivita.public.show', ['attivita' => $attivita]);
97
+    }
89 98
 }

+ 206
- 47
app/Http/Controllers/BilancioController.php Datei anzeigen

@@ -189,7 +189,7 @@ class BilancioController extends Controller
189 189
         $yearEnd = Carbon::create($year, 12, 31)->endOfDay();
190 190
 
191 191
         $movimentiCollection = PrimaNota::query()
192
-            ->with(['fornitore', 'pagamento.metodo_pagamento'])
192
+            ->with(['fornitore', 'pagamento.metodo_pagamento', 'categoria_contabile'])
193 193
             ->when($attivitaId, function ($query) use ($attivitaId) {
194 194
                 $query->where('attivita_id', $attivitaId);
195 195
             })
@@ -210,6 +210,9 @@ class BilancioController extends Controller
210 210
                 'fornitore_nome' => $movimento->fornitore?->nome,
211 211
                 'causale' => trim((string) ($movimento->causale ?? '')),
212 212
                 'riferimento' => trim((string) ($movimento->riferimento ?? '')),
213
+                'categoria_id' => (int) ($movimento->categoria_contabile_id ?? 0),
214
+                'categoria_nome' => trim((string) ($movimento->categoria_contabile?->nome ?? '')),
215
+                'categoria_colore' => trim((string) ($movimento->categoria_contabile?->colore ?? '')),
213 216
                 'metodo' => $movimento->pagamento?->metodo_pagamento?->nome
214 217
                     ?? $movimento->pagamento?->metodo_pagamento?->tipo
215 218
                     ?? null,
@@ -272,23 +275,23 @@ class BilancioController extends Controller
272 275
         $costiRows = $movimenti
273 276
             ->where('tipo', 'uscita')
274 277
             ->groupBy(function ($movimento) {
275
-                $fornitore = trim((string) ($movimento['fornitore_nome'] ?? ''));
276
-                if ($fornitore !== '') {
277
-                    return 'Fornitore: ' . $fornitore;
278
+                $categoriaId = (int) ($movimento['categoria_id'] ?? 0);
279
+                $categoriaNome = trim((string) ($movimento['categoria_nome'] ?? ''));
280
+                if ($categoriaId > 0 && $categoriaNome !== '') {
281
+                    return 'cat:' . $categoriaId;
278 282
                 }
279
-                $causale = trim((string) ($movimento['causale'] ?? ''));
280
-                if ($causale !== '') {
281
-                    return $causale;
282
-                }
283
-                $riferimento = trim((string) ($movimento['riferimento'] ?? ''));
284
-                if ($riferimento !== '') {
285
-                    return $riferimento;
286
-                }
287
-                return 'Uscita varia';
283
+                return 'cat:0';
288 284
             })
289
-            ->map(function ($items, $voce) {
285
+            ->map(function ($items, $groupKey) {
286
+                $first = $items->first();
287
+                $categoriaId = (int) ($first['categoria_id'] ?? 0);
288
+                $categoriaNome = trim((string) ($first['categoria_nome'] ?? ''));
289
+                $categoriaColore = trim((string) ($first['categoria_colore'] ?? ''));
290
+
290 291
                 return [
291
-                    'voce' => (string) $voce,
292
+                    'categoria_id' => $categoriaId,
293
+                    'voce' => $categoriaNome !== '' ? $categoriaNome : 'Senza categoria',
294
+                    'colore' => $categoriaColore,
292 295
                     'movimenti' => $items->count(),
293 296
                     'totale' => (float) $items->sum('importo'),
294 297
                 ];
@@ -299,23 +302,23 @@ class BilancioController extends Controller
299 302
         $ricaviBase = $movimenti->where('tipo', 'entrata');
300 303
         $ricaviRows = $ricaviBase
301 304
             ->groupBy(function ($movimento) {
302
-                if ((int) ($movimento['pagamento_id'] ?? 0) > 0) {
303
-                    $metodo = trim((string) ($movimento['metodo'] ?? ''));
304
-                    return $metodo !== '' ? ('Pagamento: ' . $metodo) : 'Pagamento: N/D';
305
-                }
306
-                $causale = trim((string) ($movimento['causale'] ?? ''));
307
-                if ($causale !== '') {
308
-                    return $causale;
305
+                $categoriaId = (int) ($movimento['categoria_id'] ?? 0);
306
+                $categoriaNome = trim((string) ($movimento['categoria_nome'] ?? ''));
307
+                if ($categoriaId > 0 && $categoriaNome !== '') {
308
+                    return 'cat:' . $categoriaId;
309 309
                 }
310
-                $riferimento = trim((string) ($movimento['riferimento'] ?? ''));
311
-                if ($riferimento !== '') {
312
-                    return $riferimento;
313
-                }
314
-                return 'Entrata varia';
310
+                return 'cat:0';
315 311
             })
316
-            ->map(function ($items, $voce) {
312
+            ->map(function ($items, $groupKey) {
313
+                $first = $items->first();
314
+                $categoriaId = (int) ($first['categoria_id'] ?? 0);
315
+                $categoriaNome = trim((string) ($first['categoria_nome'] ?? ''));
316
+                $categoriaColore = trim((string) ($first['categoria_colore'] ?? ''));
317
+
317 318
                 return [
318
-                    'voce' => (string) $voce,
319
+                    'categoria_id' => $categoriaId,
320
+                    'voce' => $categoriaNome !== '' ? $categoriaNome : 'Senza categoria',
321
+                    'colore' => $categoriaColore,
319 322
                     'movimenti' => $items->count(),
320 323
                     'totale' => (float) $items->sum('importo'),
321 324
                 ];
@@ -325,26 +328,172 @@ class BilancioController extends Controller
325 328
 
326 329
         // Fallback: se Prima Nota non contiene entrate, usa i pagamenti dell'anno.
327 330
         if ($ricaviRows->isEmpty() && $pagamentiCollection->isNotEmpty()) {
328
-            $ricaviRows = $pagamentiCollection
329
-                ->groupBy(function ($pagamento) {
330
-                    $metodo = $pagamento->metodo_pagamento?->nome
331
-                        ?? $pagamento->metodo_pagamento?->tipo
332
-                        ?? 'N/D';
333
-                    return 'Pagamento: ' . $metodo;
334
-                })
335
-                ->map(function ($items, $voce) {
336
-                    return [
337
-                        'voce' => (string) $voce,
338
-                        'movimenti' => $items->count(),
339
-                        'totale' => (float) $items->sum(function ($pagamento) {
340
-                            return (float) ($pagamento->importo ?? 0);
341
-                        }),
342
-                    ];
343
-                })
344
-                ->sortByDesc('totale')
345
-                ->values();
331
+            $ricaviRows = collect([
332
+                [
333
+                    'categoria_id' => 0,
334
+                    'voce' => 'Senza categoria',
335
+                    'colore' => '',
336
+                    'movimenti' => $pagamentiCollection->count(),
337
+                    'totale' => (float) $pagamentiCollection->sum(function ($pagamento) {
338
+                        return (float) ($pagamento->importo ?? 0);
339
+                    }),
340
+                ],
341
+            ]);
346 342
         }
347 343
 
344
+        $pagamentiCountAnno = (int) $pagamentiCollection->count();
345
+        $ticketMedioPagamenti = $pagamentiCountAnno > 0
346
+            ? ((float) $pagamentiCollection->sum(function ($p) {
347
+                return (float) ($p->importo ?? 0);
348
+            }) / $pagamentiCountAnno)
349
+            : 0.0;
350
+
351
+        $categoriaPerformance = $movimenti
352
+            ->groupBy(function ($movimento) {
353
+                $categoriaId = (int) ($movimento['categoria_id'] ?? 0);
354
+                $categoriaNome = trim((string) ($movimento['categoria_nome'] ?? ''));
355
+                if ($categoriaId > 0 && $categoriaNome !== '') {
356
+                    return 'cat:' . $categoriaId;
357
+                }
358
+                return 'cat:0';
359
+            })
360
+            ->map(function ($items) {
361
+                $first = $items->first();
362
+                $ricavi = (float) $items->where('tipo', 'entrata')->sum('importo');
363
+                $costi = (float) $items->where('tipo', 'uscita')->sum('importo');
364
+                return [
365
+                    'categoria' => trim((string) ($first['categoria_nome'] ?? '')) ?: 'Senza categoria',
366
+                    'ricavi' => $ricavi,
367
+                    'costi' => $costi,
368
+                    'saldo' => $ricavi - $costi,
369
+                    'movimenti' => $items->count(),
370
+                ];
371
+            })
372
+            ->sortByDesc(function ($r) {
373
+                return abs((float) ($r['saldo'] ?? 0));
374
+            })
375
+            ->values();
376
+
377
+        $righeOrdineAnnuali = RigaOrdine::query()
378
+            ->with(['piatto.cucina', 'ordine'])
379
+            ->whereHas('ordine', function ($query) use ($attivitaId, $yearStart, $yearEnd) {
380
+                $query
381
+                    ->where('attivita_id', $attivitaId)
382
+                    ->whereBetween('created_at', [$yearStart, $yearEnd]);
383
+            })
384
+            ->get();
385
+
386
+        $piattiPerformance = $righeOrdineAnnuali
387
+            ->groupBy('piatto_id')
388
+            ->map(function ($items) {
389
+                $first = $items->first();
390
+                $quantita = (int) $items->sum(function ($r) {
391
+                    return (int) ($r->quantita ?? 0);
392
+                });
393
+                $totale = (float) $items->sum(function ($r) {
394
+                    return ((float) ($r->quantita ?? 0)) * ((float) ($r->prezzo ?? 0));
395
+                });
396
+                return [
397
+                    'nome' => $first?->piatto?->nome ?? 'Piatto',
398
+                    'quantita' => $quantita,
399
+                    'totale' => $totale,
400
+                ];
401
+            })
402
+            ->sortByDesc('totale')
403
+            ->values();
404
+
405
+        $cucinePerformance = $righeOrdineAnnuali
406
+            ->groupBy(function ($riga) {
407
+                return (int) ($riga->piatto?->cucina_id ?? 0);
408
+            })
409
+            ->map(function ($items) {
410
+                $first = $items->first();
411
+                $quantita = (int) $items->sum(function ($r) {
412
+                    return (int) ($r->quantita ?? 0);
413
+                });
414
+                $totale = (float) $items->sum(function ($r) {
415
+                    return ((float) ($r->quantita ?? 0)) * ((float) ($r->prezzo ?? 0));
416
+                });
417
+                return [
418
+                    'nome' => $first?->piatto?->cucina?->nome ?? 'Cucina N/D',
419
+                    'quantita' => $quantita,
420
+                    'totale' => $totale,
421
+                ];
422
+            })
423
+            ->sortByDesc('totale')
424
+            ->values();
425
+
426
+        $pagamentiPerformance = $pagamentiCollection
427
+            ->groupBy(function ($pagamento) {
428
+                return $pagamento->metodo_pagamento?->nome
429
+                    ?? $pagamento->metodo_pagamento?->tipo
430
+                    ?? 'N/D';
431
+            })
432
+            ->map(function ($items, $metodo) {
433
+                return [
434
+                    'metodo' => (string) $metodo,
435
+                    'count' => $items->count(),
436
+                    'totale' => (float) $items->sum(function ($p) {
437
+                        return (float) ($p->importo ?? 0);
438
+                    }),
439
+                ];
440
+            })
441
+            ->sortByDesc('totale')
442
+            ->values();
443
+
444
+        $ultimiTreMesi = $andamentoMensile->take(-3);
445
+        $previsioneProssimoMese = [
446
+            'ricavi' => (float) $ultimiTreMesi->avg('ricavi'),
447
+            'costi' => (float) $ultimiTreMesi->avg('costi'),
448
+            'saldo' => (float) $ultimiTreMesi->avg('saldo'),
449
+            'mesi_considerati' => $ultimiTreMesi->count(),
450
+        ];
451
+
452
+        $giorniIso = [
453
+            1 => 'Lun',
454
+            2 => 'Mar',
455
+            3 => 'Mer',
456
+            4 => 'Gio',
457
+            5 => 'Ven',
458
+            6 => 'Sab',
459
+            7 => 'Dom',
460
+        ];
461
+
462
+        $oreLabels = collect(range(0, 23))->map(function ($h) {
463
+            return str_pad((string) $h, 2, '0', STR_PAD_LEFT);
464
+        })->values();
465
+
466
+        $affluenzaHeatmapSeries = collect($giorniIso)->map(function ($giornoLabel, $giornoIso) use ($pagamentiCollection, $oreLabels) {
467
+            $data = $oreLabels->map(function ($oraLabel) use ($pagamentiCollection, $giornoIso) {
468
+                $ora = (int) $oraLabel;
469
+                $count = (int) $pagamentiCollection->filter(function ($pagamento) use ($giornoIso, $ora) {
470
+                    if (empty($pagamento->created_at)) {
471
+                        return false;
472
+                    }
473
+                    return ((int) $pagamento->created_at->isoWeekday() === (int) $giornoIso)
474
+                        && ((int) $pagamento->created_at->format('H') === $ora);
475
+                })->count();
476
+
477
+                return [
478
+                    'x' => $oraLabel,
479
+                    'y' => $count,
480
+                ];
481
+            })->values()->all();
482
+
483
+            return [
484
+                'name' => $giornoLabel,
485
+                'data' => $data,
486
+            ];
487
+        })->values();
488
+
489
+        $affluenzaGiorniSeries = collect($giorniIso)->map(function ($giornoLabel, $giornoIso) use ($pagamentiCollection) {
490
+            return (int) $pagamentiCollection->filter(function ($pagamento) use ($giornoIso) {
491
+                return !empty($pagamento->created_at) && ((int) $pagamento->created_at->isoWeekday() === (int) $giornoIso);
492
+            })->count();
493
+        })->values();
494
+
495
+        $affluenzaGiorniLabels = collect(array_values($giorniIso))->values();
496
+
348 497
         return view('bilancio.index', [
349 498
             'attivita' => $attivita,
350 499
             'selectedYear' => $year,
@@ -356,6 +505,16 @@ class BilancioController extends Controller
356 505
             'andamentoMensile' => $andamentoMensile,
357 506
             'costiRows' => $costiRows,
358 507
             'ricaviRows' => $ricaviRows,
508
+            'categoriaPerformance' => $categoriaPerformance,
509
+            'piattiPerformance' => $piattiPerformance,
510
+            'cucinePerformance' => $cucinePerformance,
511
+            'pagamentiPerformance' => $pagamentiPerformance,
512
+            'previsioneProssimoMese' => $previsioneProssimoMese,
513
+            'ticketMedioPagamenti' => $ticketMedioPagamenti,
514
+            'pagamentiCountAnno' => $pagamentiCountAnno,
515
+            'affluenzaHeatmapSeries' => $affluenzaHeatmapSeries,
516
+            'affluenzaGiorniLabels' => $affluenzaGiorniLabels,
517
+            'affluenzaGiorniSeries' => $affluenzaGiorniSeries,
359 518
         ]);
360 519
     }
361 520
 }

+ 115
- 69
app/Http/Controllers/CarrelloController.php Datei anzeigen

@@ -15,9 +15,18 @@ use App\Models\Piatto;
15 15
 use Illuminate\Support\Facades\Auth;
16 16
 use Illuminate\Support\Facades\Session;
17 17
 use App\Http\Controllers\PagamentoController;
18
+use App\Services\Carrello\CarrelloService;
18 19
 
19 20
 class CarrelloController extends Controller
20 21
 {
22
+
23
+    private CarrelloService $carrelloService;
24
+
25
+    public function __construct(CarrelloService $carrelloService)
26
+    {
27
+        $this->carrelloService = $carrelloService;
28
+    }
29
+
21 30
     public function index(){
22 31
         // return Ordine::all();
23 32
         return Ordine::where('stato', Ordine::CARRELLO)->get();
@@ -42,104 +51,141 @@ class CarrelloController extends Controller
42 51
             'dispositivo_id' => 'required|integer|exists:dispositivo,id',
43 52
             'attivita_id' => 'nullable|integer|exists:attivita,id',
44 53
         ]);
54
+       
55
+       $result = $this->carrelloService->aumentaQuantita($request->dispositivo_id, $request->attivita_id, $request->ordine_id, $request->piatto_id);
56
+       $carrello = $result['ordine'];
57
+       $riga = $result['riga'];
58
+
59
+       return response()->json([
60
+           'success' => true,
61
+           'message' => 'Quantità aumentata',
62
+           'ordine_id' => $carrello->id,
63
+           'data' => $riga,
64
+       ]);
65
+
45 66
 
46 67
         // Tolleranza su ordine_id stale: se non e` valido, usa/crea il carrello corrente per dispositivo+attivita.
47
-        $carrelloBaseQuery = Ordine::query()
48
-            ->where('stato', Ordine::CARRELLO)
49
-            ->where('dispositivo_id', $request->dispositivo_id)
50
-            ->when(
51
-                $request->filled('attivita_id'),
52
-                fn ($q) => $q->where('attivita_id', $request->attivita_id)
53
-            );
54
-
55
-        $carrello = null;
56
-        if ($request->filled('ordine_id')) {
57
-            $carrello = (clone $carrelloBaseQuery)
58
-                ->whereKey($request->ordine_id)
59
-                ->first();
60
-        }
68
+        // $carrelloBaseQuery = Ordine::query()
69
+        //     ->where('stato', Ordine::CARRELLO)
70
+        //     ->where('dispositivo_id', $request->dispositivo_id)
71
+        //     ->when(
72
+        //         $request->filled('attivita_id'),
73
+        //         fn ($q) => $q->where('attivita_id', $request->attivita_id)
74
+        //     );
61 75
 
62
-        if (!$carrello) {
63
-            $carrello = (clone $carrelloBaseQuery)->latest('id')->first();
64
-        }
76
+        // $carrello = null;
77
+        // if ($request->filled('ordine_id')) {
78
+        //     $carrello = (clone $carrelloBaseQuery)
79
+        //         ->whereKey($request->ordine_id)
80
+        //         ->first();
81
+        // }
65 82
 
66
-        if (!$carrello) {
67
-            $carrello = Ordine::create([
68
-                'stato' => Ordine::CARRELLO,
69
-                'dispositivo_id' => $request->dispositivo_id,
70
-                'attivita_id' => $request->attivita_id,
71
-            ]);
72
-        }
83
+        // if (!$carrello) {
84
+        //     $carrello = (clone $carrelloBaseQuery)->latest('id')->first();
85
+        // }
73 86
 
74
-        $riga = RigaOrdine::where('ordine_id', $carrello->id)->where('piatto_id', $request->piatto_id)->first();
75
-        if($riga){
76
-            $riga->quantita += 1;
77
-            $riga->prezzo = Piatto::find($request->piatto_id)->prezzo*$riga->quantita;
78
-            $riga->save();
79
-        }else{
80
-            $riga = RigaOrdine::create([
81
-                'ordine_id' => $carrello->id,
82
-                'piatto_id' => $request->piatto_id,
83
-                'quantita' => 1,
84
-                'prezzo' => Piatto::find($request->piatto_id)->prezzo,
85
-            ]);
86
-        }
87
+        // if (!$carrello) {
88
+        //     $carrello = Ordine::create([
89
+        //         'stato' => Ordine::CARRELLO,
90
+        //         'dispositivo_id' => $request->dispositivo_id,
91
+        //         'attivita_id' => $request->attivita_id,
92
+        //     ]);
93
+        // }
94
+
95
+        // $riga = RigaOrdine::where('ordine_id', $carrello->id)->where('piatto_id', $request->piatto_id)->first();
96
+        // if($riga){
97
+        //     $riga->quantita += 1;
98
+        //     $riga->prezzo = Piatto::find($request->piatto_id)->prezzo*$riga->quantita;
99
+        //     $riga->save();
100
+        // }else{
101
+        //     $riga = RigaOrdine::create([
102
+        //         'ordine_id' => $carrello->id,
103
+        //         'piatto_id' => $request->piatto_id,
104
+        //         'quantita' => 1,
105
+        //         'prezzo' => Piatto::find($request->piatto_id)->prezzo,
106
+        //     ]);
107
+        // }
87 108
         // dd($riga);
88
-        return response()->json([
89
-            'success' => true,
90
-            'message' => 'Quantità aumentata',
91
-            'ordine_id' => $carrello->id,
92
-            'data' => $riga,
93
-        ]);
109
+
94 110
     }
95 111
 
96 112
     public function diminuisci(Request $request){
97
-        $carrello = Ordine::find($request->ordine_id);
98
-        $riga = RigaOrdine::where('ordine_id', $carrello->id)->where('piatto_id', $request->piatto_id)->first();
99
-        if($riga){
100
-            $riga->quantita -= 1;
101
-            $riga->prezzo = Piatto::find($request->piatto_id)->prezzo*$riga->quantita;
102
-            $riga->save();
103
-        }
113
+        $request->validate([
114
+            'ordine_id' => 'required|integer|exists:ordine,id',
115
+            'piatto_id' => 'required|integer|exists:piatto,id',
116
+            'riga_ordine_id' => 'required|integer|exists:riga_ordine,id',
117
+        ]);
118
+        $result = $this->carrelloService->diminuisciQuantita($request->ordine_id, $request->piatto_id, $request->riga_ordine_id);
104 119
         return response()->json([
105 120
             'success' => true,
106 121
             'message' => 'Quantità diminuita',
107
-            'data' => $riga,
122
+            'ordine_id' => $result['ordine']->id,
123
+            'data' => $result['riga'],
108 124
         ]);
125
+        // $carrello = Ordine::find($request->ordine_id);
126
+        // $riga = RigaOrdine::where('ordine_id', $carrello->id)->where('piatto_id', $request->piatto_id)->first();
127
+        // if($riga){
128
+        //     $riga->quantita -= 1;
129
+        //     $riga->prezzo = Piatto::find($request->piatto_id)->prezzo*$riga->quantita;
130
+        //     $riga->save();
131
+        // }
132
+        // return response()->json([
133
+        //     'success' => true,
134
+        //     'message' => 'Quantità diminuita',
135
+        //     'data' => $riga,
136
+        // ]);
109 137
     }
110 138
 
111 139
 
112 140
     public function elimina(Request $request){
113
-        $riga = RigaOrdine::find($request->riga_ordine_id);
114
-        // dd($riga);
115
-        if($riga !== null && $request->has('elimina')){
116
-        $riga->delete();
117
-        };
118
-
141
+        $request->validate([
142
+            'riga_ordine_id' => 'required|integer|exists:riga_ordine,id',
143
+        ]);
144
+        $result = $this->carrelloService->eliminaRiga($request->riga_ordine_id, $request->has('elimina'));
119 145
         return response()->json([
120 146
             'success' => true,
121 147
             'message' => 'Riga eliminata',
122
-            'data' => $riga,
148
+            'data' => $result,
123 149
         ]);
150
+        // $riga = RigaOrdine::find($request->riga_ordine_id);
151
+        // if($riga !== null && $request->has('elimina')){
152
+        // $riga->delete();
153
+        // };
154
+
155
+        // return response()->json([
156
+        //     'success' => true,
157
+        //     'message' => 'Riga eliminata',
158
+        //     'data' => $riga,
159
+        // ]);
124 160
     }
125 161
 
126 162
 
127 163
     public function aggiorna_nota(Request $request){
128
-        $riga = RigaOrdine::find($request->riga_ordine_id);
129
-        if($riga == null){
130
-            return response()->json([
131
-                'success' => false,
132
-                'message' => 'Riga ordine non trovata',
133
-                'data' => null,
134
-            ]);
135
-        }
136
-        $riga->note = $request->note;
137
-        $riga->save();
164
+        $request->validate([
165
+            'riga_ordine_id' => 'required|integer|exists:riga_ordine,id',
166
+            'note' => 'nullable|string',
167
+        ]);
168
+        $result = $this->carrelloService->aggiornaNota($request->riga_ordine_id, $request->note);
138 169
         return response()->json([
139 170
             'success' => true,
140 171
             'message' => 'Nota aggiornata',
141
-            'data' => $riga,
172
+            'data' => $result,
142 173
         ]);
174
+        // $riga = RigaOrdine::find($request->riga_ordine_id);
175
+        // if($riga == null){
176
+        //     return response()->json([
177
+        //         'success' => false,
178
+        //         'message' => 'Riga ordine non trovata',
179
+        //         'data' => null,
180
+        //     ]);
181
+        // }
182
+        // $riga->note = $request->note;
183
+        // $riga->save();
184
+        // return response()->json([
185
+        //     'success' => true,
186
+        //     'message' => 'Nota aggiornata',
187
+        //     'data' => $riga,
188
+        // ]);
143 189
     }
144 190
 
145 191
     public function azzera(Request $request){

+ 232
- 71
app/Http/Controllers/EndpointController.php Datei anzeigen

@@ -9,6 +9,10 @@ use App\Models\Endpoint;
9 9
 use App\Models\Dispositivo;
10 10
 use App\Models\PrintJob;
11 11
 use App\DataTables\EndpointDataTable;
12
+use Illuminate\Support\Facades\Validator;
13
+use App\Models\Attivita;
14
+use Illuminate\Support\Str;
15
+
12 16
 
13 17
 class EndpointController extends Controller
14 18
 {
@@ -24,10 +28,31 @@ class EndpointController extends Controller
24 28
     public static function middleware(): array
25 29
     {
26 30
         return [
27
-            new Middleware('permission:view-endpoint', only: ['index']),
31
+            new Middleware('permission:view-endpoint', only: ['index', 'show', 'istruzioni']),
28 32
             new Middleware('permission:create-endpoint|edit-endpoint|delete-endpoint', only: ['store', 'update', 'destroy']),
33
+            new Middleware('permission:create-endpoint', only: ['richiedi_nuovo_product_key']),
29 34
         ];
30 35
     }
36
+
37
+    public function show(Request $request)
38
+    {
39
+        $endpoint = Endpoint::query()
40
+            ->with(['attivita', 'user', 'licenza', 'stampanti'])
41
+            ->find($request->endpoint_id);
42
+
43
+        if (!$endpoint) {
44
+            abort(404);
45
+        }
46
+
47
+        return view('endpoint.show', compact('endpoint'));
48
+    }
49
+
50
+    public function istruzioni(Request $request)
51
+    {
52
+        return view('endpoint.istruzioni', [
53
+            'appUrl' => rtrim(config('app.url'), '/'),
54
+        ]);
55
+    }
31 56
     
32 57
     //API
33 58
     public function register(Request $request)
@@ -60,91 +85,53 @@ class EndpointController extends Controller
60 85
     {
61 86
         Log::info('stampanti_register');
62 87
         Log::info($request->all());
63
-        $request->validate([
64
-            'endpoint_id' => 'required|integer|exists:endpoint,id',
65
-            'attivita_id' => 'required|integer|exists:attivita,id',
66
-            'stampanti' => 'required|array',
67
-            // 'descrizione' => 'required|string|max:255',
68
-            // 'url' => 'required|url',
69
-        ]);
70
-       
71
-        $stampanti = $request->stampanti;
72
-        $stampanti_endpoint = Endpoint::find($request->endpoint_id)->info['stampanti'];
73
-        $endpoint = Endpoint::find($request->endpoint_id);
74
-
75
-        if(count($stampanti) > 0){
76
-            foreach($stampanti as $stampante){
77
-                $dispositivo = Dispositivo::create([
78
-                    'tipo' => Dispositivo::STAMPANTE,
79
-                    'attivita_id' => $request->attivita_id,
88
+
89
+        $stampanti_da_registrare = $request->input('stampanti', []);
90
+        $endpoint = $request->endpoint;
91
+        $stampanti_endpoint = Dispositivo::where('endpoint_id', $request->endpoint->id)->where('tipo', Dispositivo::STAMPANTE)->get()->toArray();
92
+
93
+
94
+        foreach($stampanti_da_registrare as $key => $stampante){
95
+
96
+                Dispositivo::FirstOrCreate(
97
+                    ['url_stampante' => $stampante['url_stampante']],
98
+                [
80 99
                     'nome' => $stampante['nome'],
81 100
                     'url_stampante' => $stampante['url_stampante'],
82
-                    'ubicazione' => "endpoint:".$request->endpoint_id,
101
+                    'endpoint_id' => $request->endpoint->id,
102
+                    'tipo' => Dispositivo::STAMPANTE,
103
+                    'attivita_id' => $request->endpoint->attivita_id,
104
+                    'licenza' => null,
105
+                    'ubicazione' => $endpoint->ubicazione,
106
+                    'note' => null,
83 107
                     'is_attivo' => true,
84
-                    'endpoint_id' => $request->endpoint_id,
85
-                 ]);
86
-                 // Verifico che la stampante non sia già presente tra quelle registrate su questo endpoint
87
-                 if (!in_array($dispositivo->id, $endpoint->info['stampanti'])) {
88
-                     $info = $endpoint->info;
89
-                     $info['stampanti'][] = $dispositivo->id;
90
-                     $endpoint->info = $info;
91
-                     $endpoint->save();
92
-                 }
93
-            }
94
-            $forMapping = [];
95
-            foreach($endpoint->info['stampanti'] as $stampante_id){
96
-                if(!Dispositivo::find($stampante_id)){
97
-                 Log::error('Stampante non trovata: ' . $stampante_id);
98
-                    return response()->json([
99
-                        'message' => 'Stampante non trovata',
100
-                        'status' => 'error',
101
-                        'success' => false,
102
-                    ]);
103
-                }
104
-                // dd([$stampante_id , Dispositivo::find($stampante_id)]);
105
-                $forMapping[] = [
106
-                    'id' => $stampante_id,
107
-                    'nome' => Dispositivo::find($stampante_id)->nome,
108
-                    'url_stampante' => Dispositivo::find($stampante_id)->url_stampante,
109
-                ];
110
-            }
111
-            return response()->json([
112
-                'message' => 'Stampanti registrate con successo',
113
-                'status' => 'success',
114
-                'success' => true,
115
-                'endpoint' => $endpoint,
116
-                'stampanti' => $forMapping,
117
-            ]);
118
-
119
-        }else{
120
-            return response()->json([
121
-                'message' => 'Nessuna nuova stampante registrata',
122
-                'status' => 'error',
123
-                'success' => false,
124
-            ]);
108
+                    'pin_sblocco' => null,
109
+                ]);
110
+            // }
125 111
         }
126 112
 
113
+        return response()->json([
114
+            'message' => 'Stampanti registrate con successo',
115
+            'status' => 'success',
116
+            'success' => true,
117
+            'stampanti' => $endpoint->stampanti,
118
+        ]);
119
+
127 120
     }
128 121
 
129 122
     public function get_lavoro(Request $request)
130 123
     {
131
-        $request->validate([
132
-            'endpoint_id' => 'required|integer|exists:endpoint,id',
133
-            'attivita_id' => 'required|integer|exists:attivita,id',
134
-        ]);
124
+        
135 125
 
136 126
         $has_lavoro = false;
137
-        $endpoint = Endpoint::find($request->endpoint_id);
127
+        $endpoint = $request->endpoint;
128
+
138 129
         $stampanti = $endpoint->stampanti;
139
-        // Trova la lista degli ID delle stampanti associate a questo endpoint
140 130
         $stampanti_ids = $endpoint->stampanti->pluck('id')->toArray();
141
-        // Cerca lavori associati a queste stampanti
142 131
         $query = PrintJob::whereIn('stampante_id', $stampanti_ids)
143 132
             ->where('stato', PrintJob::STATO_IN_CODA);
144 133
 
145
-        $lavori = $query->get();
146
-        // dd($lavori);
147
-        // dd($query->count());
134
+            $lavori = $query->get();
148 135
         if($query->count() > 0){
149 136
             PrintJob::whereIn('stampante_id', $stampanti_ids)
150 137
             ->where('stato', PrintJob::STATO_IN_CODA)
@@ -171,7 +158,7 @@ class EndpointController extends Controller
171 158
         $request->validate([
172 159
             'printJob_id' => 'required|integer|exists:print_job,id',
173 160
             'esito' => 'required|string|in:success,error',
174
-            'endpoint_id' => 'required|integer|exists:endpoint,id',
161
+            // 'endpoint_id' => 'required|integer|exists:endpoint,id',
175 162
         ]);
176 163
 
177 164
         $printJob = PrintJob::find($request->printJob_id)->where('stato', PrintJob::STATO_PRESO);
@@ -235,4 +222,178 @@ class EndpointController extends Controller
235 222
          'success' => true, 
236 223
          'endpoints' => $endpoints]);
237 224
     }
225
+
226
+
227
+    public function api_register_product_key(Request $request)
228
+    {
229
+
230
+        if($request->has('first_time') && $request->first_time == true){
231
+            //la prima registrazione
232
+            Log::info('api_register_product_key');
233
+            Log::info('FIRST TIME');
234
+            Log::info('Request: ' . json_encode($request->all()));
235
+        $validator = Validator::make($request->all(), [
236
+            'product_key' => 'required|string|max:25,exists:endpoint,product_key',
237
+        ], [
238
+            'product_key.required' => 'Il product key è richiesto',
239
+            'product_key.string' => 'Il product key deve essere una stringa',
240
+            'product_key.max' => 'Il product key deve essere lungo al massimo 25 caratteri',
241
+            'product_key.exists' => 'Il product key non è valido',
242
+        ]);
243
+
244
+        if($validator->fails()){
245
+            Log::error('api_register_product_key');
246
+            Log::error('Request: ' . $request->all());
247
+            Log::error('Validator fails: ' . $validator->messages());
248
+            return response()->json([
249
+                'message' => $validator->messages(),
250
+                'status' => 'error',
251
+                'success' => false,
252
+            ], 422);
253
+        }
254
+
255
+        $endpoint = Endpoint::where('product_key', $request->product_key)->first();
256
+
257
+        if(!$endpoint){
258
+            Log::error('Endpoint non trovato');
259
+            return response()->json([
260
+                'message' => 'Endpoint non trovato',
261
+                'status' => 'error',
262
+                'success' => false,
263
+            ], 404);
264
+        }elseif($endpoint->status == Endpoint::REGISTRATO){
265
+            Log::error('Endpoint già registrato');
266
+            return response()->json([
267
+                'message' => 'Endpoint già registrato',
268
+                'status' => 'error',
269
+                'success' => false,
270
+            ], 400);
271
+        }
272
+
273
+        //Verifica l'attività dell'endpoint
274
+        $attivita = $endpoint->attivita;
275
+        if(!$attivita){
276
+            Log::error('Attività non trovata');
277
+            return response()->json([
278
+                'message' => 'Attività non trovata',
279
+                'status' => 'error',
280
+                'success' => false,
281
+            ], 404);
282
+        }
283
+
284
+        //attivo l'endpoint
285
+        $endpoint->update([
286
+            'status' => Endpoint::REGISTRATO,
287
+            "info" => [
288
+                "machine_fingerprint" => $request->machine_fingerprint ?? null,
289
+            ],
290
+        ]);
291
+
292
+        //Verifica la licenza dell'endpoint
293
+
294
+        //
295
+        return response()->json([
296
+            'message' => 'Endpoint trovato con successo',
297
+            'status' => 'success',
298
+            'success' => true,
299
+            'endpoint' => $endpoint,
300
+            'licenza' => $licenza??null,
301
+            'attivita' => $attivita??null,
302
+        ]);
303
+
304
+    }else{
305
+        //se non è la prima registrazione può modificare gli attributi dell'endpoint
306
+        Log::info('api_register_product_key');
307
+        Log::info('NOT ft');
308
+        Log::info('Request: ' . json_encode($request->all()));
309
+        // dd($request->all() , 'NOT ft');
310
+
311
+        $validator = Validator::make($request->all(), [
312
+            'label' => 'required|string|max:255',
313
+            'descrizione' => 'nullable|string|max:255',
314
+            'ubicazione' => 'nullable|string|max:255',
315
+        ], [
316
+            'label.required' => 'Il label è richiesto',
317
+            'label.string' => 'Il label deve essere una stringa',
318
+            'label.max' => 'Il label deve essere lungo al massimo 25 caratteri',
319
+            'descrizione.string' => 'La descrizione deve essere una stringa',
320
+            'descrizione.max' => 'La descrizione deve essere lungo al massimo 255 caratteri',
321
+            'ubicazione.string' => 'L\'ubicazione deve essere una stringa',
322
+            'ubicazione.max' => 'L\'ubicazione deve essere lungo al massimo 255 caratteri',
323
+        ]);
324
+
325
+        if($validator->fails()){
326
+
327
+            Log::error('Validator fails: ' . $validator->messages());
328
+            return response()->json([
329
+                'message' => $validator->messages(),
330
+                'status' => 'error',
331
+                'success' => false,
332
+            ], 422);
333
+        }
334
+
335
+        $endpoint = Endpoint::where('product_key', $request->product_key)->where('status', Endpoint::REGISTRATO)->first();
336
+        if(!$endpoint){
337
+            Log::error('Endpoint non trovato');
338
+            Log::error('Request: ' . json_encode($request->all()));
339
+            return response()->json([
340
+                'message' => 'Endpoint non trovato',
341
+                'status' => 'error',
342
+                'success' => false,
343
+            ], 404);
344
+        }
345
+        
346
+        $endpoint->update([
347
+            'label' => $request->label,
348
+            'descrizione' => $request->descrizione,
349
+            'ubicazione' => $request->ubicazione,
350
+            'status' => Endpoint::ATTIVO,
351
+            'token' => Str::random(32),
352
+        ]);
353
+        
354
+        return response()->json([
355
+            'message' => 'Endpoint aggiornato con successo',
356
+            'status' => 'success',
357
+            'success' => true,
358
+            'endpoint' => $endpoint,
359
+            'token' => $endpoint->token,
360
+        ]);
361
+
362
+    } //chiude else
363
+
364
+    }
365
+
366
+    public function richiedi_nuovo_product_key(Request $request)
367
+    {
368
+        $request->validate([
369
+            'attivita_id' => 'required|integer|exists:attivita,id',
370
+        ],[
371
+            'attivita_id.required' => 'L\'attività è richiesta',
372
+            'attivita_id.exists' => 'L\'attività non è valida',
373
+        ]);
374
+
375
+
376
+        $attivita = Attivita::find($request->attivita_id);
377
+        
378
+        $endpoint = Endpoint::FirstOrCreate(['attivita_id' => $attivita->id , 'status' => Endpoint::NON_REGISTRATO], [
379
+            'attivita_id' => $attivita->id,
380
+            'label' => 'non assegnato',
381
+            'descrizione' => 'non assegnato',
382
+            'url' => 'non assegnato',
383
+            'user_id' => $request->user()->id,
384
+            'status' => Endpoint::NON_REGISTRATO,
385
+            'product_key' => 'FEST-'.strtoupper(Str::random(20)),
386
+            'info' => [
387
+                'stampanti' => [],
388
+            ],
389
+        ]);
390
+
391
+        return view('endpoint.nuovo_product_key', [
392
+            'success' => true,
393
+            'endpoint' => $endpoint,
394
+        ]);
395
+
396
+
397
+    
398
+    }
238 399
 }

+ 7
- 1
app/Http/Controllers/HomePageController.php Datei anzeigen

@@ -3,6 +3,7 @@
3 3
 namespace App\Http\Controllers;
4 4
 
5 5
 use App\Http\Controllers\Controller;
6
+use App\Models\Attivita;
6 7
 use Illuminate\Http\Request;
7 8
 use App\Models\Segnalazione;
8 9
 use App\Models\Reparto;
@@ -20,7 +21,12 @@ class HomePageController extends Controller
20 21
   public function welcome()
21 22
   {
22 23
     $pageConfigs = ['myLayout' => 'front'];
23
-    return view('welcome', ['pageConfigs' => $pageConfigs]);
24
+    $attivitaAttive = Attivita::query()
25
+      ->where('is_attiva', true)
26
+      ->orderBy('nome')
27
+      ->get();
28
+
29
+    return view('welcome', compact('pageConfigs', 'attivitaAttive'));
24 30
   }
25 31
 
26 32
   public function admin_dashboard()

+ 221
- 0
app/Http/Controllers/OperatoreController.php Datei anzeigen

@@ -0,0 +1,221 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use App\Models\Operatore;
7
+use App\DataTables\OperatoreDataTable;
8
+use App\DataTables\OperatoreDataTableEditor;
9
+use Illuminate\Support\Facades\Auth;
10
+use Illuminate\Routing\Controllers\Middleware;
11
+use Illuminate\Routing\Controllers\HasMiddleware;
12
+use Illuminate\Support\Facades\Hash;
13
+use Illuminate\Support\Facades\Validator;
14
+use Illuminate\Support\Str;
15
+use Illuminate\Support\Facades\Cookie;
16
+use App\Models\Dispositivo;
17
+use App\Models\Ordine;
18
+use Illuminate\Support\Facades\Session;
19
+use App\DataTables\RigaOrdineDataTable;
20
+
21
+
22
+class OperatoreController extends Controller
23
+{
24
+    public static $permission_group = "Operatori";
25
+    public static $permissions = [
26
+        'view-operatore' => 'Vedi',
27
+        'create-operatore' => 'Crea',
28
+        'edit-operatore' => 'Modifica',
29
+        'delete-operatore' => 'Elimina',
30
+    ];
31
+    
32
+    public static function middleware(): array
33
+    {
34
+        return [
35
+            new Middleware('permission:view-operatore', only: ['index']),
36
+            new Middleware('permission:create-operatore|edit-operatore|delete-operatore', only: ['store', 'update', 'destroy']),
37
+        ];
38
+    }
39
+    
40
+    public function index(OperatoreDataTable $dataTable)
41
+    {
42
+        return $dataTable->render('operatore.index');
43
+    }
44
+
45
+    public function show(Request $request)
46
+    {
47
+        // dd($request->all());
48
+        $operatore = Operatore::find($request->get('operatore_id'));
49
+        if (!$operatore) {
50
+            return redirect()->back()->with('error', 'Operatore non trovato');
51
+        }
52
+        return view('operatore.show', ['operatore' => $operatore , 'dispositivi' => $operatore->dispositivi]);
53
+    }
54
+
55
+    public function update_password(Request $request)
56
+    {
57
+        $operatore = Operatore::find($request->get('operatore_id'));
58
+        $operatore->password = Hash::make($request->get('password'));
59
+        $operatore->save();
60
+        return redirect()->route('operatore.show', ['operatore_id' => $operatore->id])->with('success', 'Password aggiornata con successo');
61
+    }
62
+
63
+    public function update_dispositivi(Request $request)
64
+    {
65
+        // dd($request->all());
66
+        // funziona? Proviamo a sistemare la selezione dei dispositivi.
67
+        // Raccogli i soli dispositivi selezionati dal form (i checkbox hanno un nome tipo dispositivo_id_{id})
68
+        $ids = [];
69
+        foreach ($request->all() as $key => $value) {
70
+            if (Str::startsWith($key, 'dispositivo_id_')) {
71
+                $ids[] = $value;
72
+            }
73
+        }
74
+
75
+        $operatore = Operatore::find($request->get('operatore_id'));
76
+
77
+        if (!$operatore) {
78
+            return redirect()->back()->with('error', 'Operatore non trovato');
79
+        }
80
+
81
+        $operatore->dispositivi()->sync($ids);
82
+
83
+        return redirect()->back()->with('success', 'Dispositivi aggiornati con successo');
84
+    }
85
+
86
+    public function store(OperatoreDataTableEditor $editor)
87
+    {
88
+        $request = request();
89
+        $input = $request->all();
90
+        if($request->has('action')){
91
+            switch($input['action']){
92
+                case 'create':
93
+                    if(!Auth::user()->can('create-operatore')) return;
94
+                    break;
95
+                case 'edit':
96
+                    if(!Auth::user()->can('edit-operatore')) return;
97
+                    break;
98
+                case 'remove':
99
+                    if(!Auth::user()->can('delete-operatore')) return;
100
+                    break;
101
+            }
102
+        }
103
+        return $editor->process($request);
104
+    }
105
+
106
+    public function landing()
107
+    {
108
+        return view('operatore.landing');
109
+    }
110
+
111
+    public function dashDispositivi(Request $request)
112
+    {
113
+        $operatore = Auth::guard('operatore')->user();
114
+        if(!$operatore){
115
+            return redirect()->route('operatore.landing')->with('error', 'Non sei autorizzato a accedere a questa pagina');
116
+        }
117
+        $puntoVendita = $operatore->dispositivi;
118
+        if(!$puntoVendita){
119
+            return redirect()->route('operatore.landing')->with('error', 'Non hai dispositivi associati, contatta l\'amministratore per maggiori informazioni');
120
+        }
121
+        return view('operatore.guardOperatore.dashDispositivi', ['dispositivi' => $puntoVendita]);
122
+    }
123
+
124
+    public function login()
125
+    {
126
+        // dd(request()->all());
127
+        $request = request();
128
+        $input = $request->all();
129
+        $validator = Validator::make($input, [
130
+            'username' => 'required',
131
+            'password' => 'required',
132
+        ]);
133
+        if($validator->fails()){
134
+            return redirect()->back()->withErrors($validator)->withInput();
135
+        }
136
+ 
137
+        if (!Auth::guard('operatore')->attempt([
138
+            'username' => $request->username,
139
+            'password' => $request->password,
140
+            'is_attivo' => true,
141
+        ])) {
142
+            return back()->with('error', 'Username o password non valide')->withInput();
143
+        }
144
+        
145
+        $request->session()->regenerate();
146
+        $operatore = Auth::guard('operatore')->user();
147
+        $puntoVendita = $operatore->dispositivi;
148
+
149
+        switch($puntoVendita->count()){
150
+            case 1:
151
+                return redirect()->route('operatore.punto-vendita.show', ['punto_vendita_id' => $puntoVendita->first()->id]);
152
+            case 0:
153
+                // return redirect()->route('operatore.auth.dashDispositivi')->with('error', 'Non hai dispositivi associati');
154
+                return back()->with('error', 'Non hai dispositivi associati, contatta l\'amministratore per maggiori informazioni')->withInput();
155
+                default:
156
+                return view('operatore.guardOperatore.dashDispositivi', ['dispositivi' => $puntoVendita])->with('success', 'Login effettuato con successo.Seleziona il dispositivo per iniziare a lavorare.');
157
+        }
158
+
159
+    }
160
+
161
+
162
+    public function logout()
163
+    {
164
+        Auth::guard('operatore')->logout();
165
+        return redirect()->route('operatore.landing')->with('success', 'Logout effettuato con successo');
166
+    }
167
+
168
+
169
+    public function show_dispositivo(Request $request, RigaOrdineDataTable $dataTable)
170
+    {
171
+        $puntoVendita = Dispositivo::find(request()->get('punto_vendita_id'));
172
+        if(!$puntoVendita){
173
+            return redirect()->route('punto-vendita.index')
174
+            ->with('error', 'Punto di vendita non trovato');
175
+        }
176
+// dd('A',$puntoVendita);
177
+        if($puntoVendita->binding_token == '' || $puntoVendita->binding_token == null ){
178
+            $puntoVendita->binding_token = Hash::make(Str::random(32));
179
+            $puntoVendita->save();
180
+        }
181
+// dd(Cookie::get('binding_token'));
182
+
183
+        if(Cookie::has('binding_token')){
184
+            if(Hash::check(Cookie::get('binding_token'), $puntoVendita->binding_token)){
185
+                // dd('A' , Cookie::get('binding_token') , $puntoVendita->binding_token);
186
+                Cookie::queue(Cookie::forget('binding_token'));
187
+                return redirect()->back()->with('error', 'Token di binding non valido. Si sta accedendo da un altro dispositivo.');
188
+            }
189
+        }else{
190
+            Cookie::queue('binding_token', Hash::make($puntoVendita->binding_token));
191
+        }
192
+  
193
+   
194
+
195
+        // dd(Cookie::get('binding_token'));
196
+        $cucine = $puntoVendita->cucine()->where('is_attiva', true)->orderBy('nome')->get();
197
+
198
+
199
+
200
+        $ordine = Ordine::firstOrCreate([
201
+            'stato' => Ordine::CARRELLO,
202
+            'dispositivo_id' => $puntoVendita->id,
203
+            'attivita_id' => $puntoVendita->attivita_id,
204
+        ]);
205
+                Session::put([
206
+                    'dispositivo_id' => $puntoVendita->id,
207
+                    'attivita_id' => $puntoVendita->attivita_id,
208
+                ]);
209
+
210
+        $dataTable->ordine_id = $ordine->id;
211
+
212
+                switch($puntoVendita->tipo){
213
+                    case Dispositivo::CASSA:
214
+                        return $dataTable->render('punto_vendita.cassa.index', ['punto_vendita' => $puntoVendita , 'cucine' => $cucine, 'ordine' => $ordine, 'dataTable_rigaordine' => $dataTable]);
215
+                    case Dispositivo::KIOSK:
216
+                        return $dataTable->render('punto_vendita.kiosk.index', ['punto_vendita' => $puntoVendita , 'cucine' => $cucine, 'ordine' => $ordine, 'dataTable_rigaordine' => $dataTable]);
217
+                    case Dispositivo::CAMERIERE:
218
+                        return $dataTable->render('punto_vendita.cam.index', ['punto_vendita' => $puntoVendita , 'cucine' => $cucine, 'ordine' => $ordine, 'dataTable_rigaordine' => $dataTable]);
219
+                };
220
+    }
221
+}

+ 14
- 1
app/Http/Controllers/PrimaNotaController.php Datei anzeigen

@@ -7,6 +7,7 @@ use App\Models\PrimaNota;
7 7
 use App\DataTables\PrimaNotaDataTable;
8 8
 use App\DataTables\PrimaNotaDataTableEditor;
9 9
 use Illuminate\Support\Facades\Auth;
10
+use Carbon\Carbon;
10 11
 
11 12
 class PrimaNotaController extends Controller
12 13
 { 
@@ -28,8 +29,20 @@ class PrimaNotaController extends Controller
28 29
 
29 30
     public function index(PrimaNotaDataTable $dataTable)
30 31
     {
32
+        $statistiche['totale_operazioni'] = PrimaNota::where('attivita_id', session()->get('attivita_attuale'))
33
+        ->where('created_at', '>=', Carbon::now()->startOfDay())
34
+        ->count();
35
+        $statistiche['importo_entrate'] = PrimaNota::where('attivita_id', session()->get('attivita_attuale'))
36
+        ->where('tipo_movimento', 'entrata')
37
+        ->where('created_at', '>=', Carbon::now()->startOfDay())
38
+        ->sum('importo');
39
+        $statistiche['importo_uscite'] = PrimaNota::where('attivita_id', session()->get('attivita_attuale'))
40
+        ->where('tipo_movimento', 'uscita')
41
+        ->where('created_at', '>=', Carbon::now()->startOfDay())
42
+        ->sum('importo');
43
+        $statistiche['saldo'] = $statistiche['importo_entrate'] - $statistiche['importo_uscite'];
31 44
         $dataTable->attivita_id = session()->get('attivita_attuale');
32
-        return $dataTable->render('prima_nota.index');
45
+        return $dataTable->render('prima_nota.index', compact('statistiche'));
33 46
     }
34 47
 
35 48
     public function store(PrimaNotaDataTableEditor $editor)

+ 78
- 20
app/Http/Controllers/PuntovenditaController.php Datei anzeigen

@@ -12,8 +12,11 @@ use App\DataTables\PuntoVenditaDataTableEditor;
12 12
 use App\DataTables\RigaOrdineDataTable;
13 13
 use Illuminate\Support\Facades\Auth;
14 14
 use Illuminate\Support\Facades\Session;
15
+use Illuminate\Support\Facades\Cookie;
16
+use Illuminate\Support\Str;
17
+use Illuminate\Support\Facades\Hash;
15 18
 
16
-class PuntovenditaController extends Controller
19
+class PuntoVenditaController extends Controller
17 20
 {
18 21
     public static $permission_group = "Punto di vendita";
19 22
     public static $permissions = [
@@ -26,7 +29,7 @@ class PuntovenditaController extends Controller
26 29
     public static function middleware(): array
27 30
     {
28 31
         return [
29
-            new Middleware('permission:view-punto_vendita', only: ['index']),
32
+            new Middleware('permission:view-punto_vendita', only: ['index', 'dettagli_punto_vendita', 'dettagli']),
30 33
             new Middleware('permission:create-punto_vendita|edit-punto_vendita|delete-punto_vendita', only: ['store', 'update', 'destroy', 'edit']),
31 34
         ];
32 35
     }
@@ -36,6 +39,20 @@ class PuntovenditaController extends Controller
36 39
         return $dataTable->render('punto_vendita.index');
37 40
     }
38 41
 
42
+    public function dettagli_punto_vendita(Request $request)
43
+    {
44
+        $puntoVendita = Dispositivo::query()
45
+            ->with(['attivita', 'hasCucine'])
46
+            ->find($request->punto_vendita_id);
47
+
48
+        if (!$puntoVendita) {
49
+            return redirect()->route('punto-vendita.index')
50
+                ->with('error', 'Punto di vendita non trovato');
51
+        }
52
+
53
+        return view('punto_vendita.dettaglio_punto_vendita', compact('puntoVendita'));
54
+    }
55
+
39 56
     public function edit(Request $request)
40 57
     {
41 58
         $puntoVendita = Dispositivo::with(['hasCucine', 'attivita.cucine', 'attivita.stampanti'])
@@ -82,36 +99,76 @@ class PuntovenditaController extends Controller
82 99
     public function show(Request $request, RigaOrdineDataTable $dataTable)
83 100
     {
84 101
         $puntoVendita = Dispositivo::find(request()->get('punto_vendita_id'));
85
-        // $puntoVendita = Dispositivo::cassa()->first();
86 102
         if(!$puntoVendita){
87 103
             return redirect()->route('punto-vendita.index')
88 104
             ->with('error', 'Punto di vendita non trovato');
89 105
         }
90 106
 
107
+        if($puntoVendita->binding_token == '' || $puntoVendita->binding_token == null ){
108
+            $puntoVendita->binding_token = Hash::make(Str::random(32));
109
+            $puntoVendita->save();
110
+        }
111
+// dd(Cookie::get('binding_token'));
112
+        /*
113
+            Questo blocco di codice gestisce il controllo e l'impostazione del cookie 'binding_token' per il dispositivo.
114
+
115
+            Se esiste già un cookie 'binding_token', verifica che il suo valore coincida con il binding_token memorizzato per il punto vendita:
116
+              - Se NON coincidono, rimuove il cookie e mostra un errore: "Token di binding non valido. Si sta accedendo da un altro dispositivo."
117
+            Se NON esiste il cookie, lo imposta con il valore del binding_token del punto vendita.
118
+
119
+            Questo serve a garantire che ogni dispositivo rimanga "associato" al suo token, impedendo accessi da device diversi senza il corretto re-binding.
120
+        */
121
+        if(Cookie::has('binding_token')){
122
+            if(Hash::check(Cookie::get('binding_token'), $puntoVendita->binding_token)){
123
+                // dd('A' , Cookie::get('binding_token') , $puntoVendita->binding_token);
124
+                Cookie::queue(Cookie::forget('binding_token'));
125
+                return redirect()->back()->with('error', 'Token di binding non valido. Si sta accedendo da un altro dispositivo.');
126
+            }
127
+        }else{
128
+            Cookie::queue('binding_token', Hash::make($puntoVendita->binding_token));
129
+        }
130
+  
131
+   
132
+
133
+        // dd(Cookie::get('binding_token'));
91 134
         $cucine = $puntoVendita->cucine()->where('is_attiva', true)->orderBy('nome')->get();
92 135
 
93
-        // dd($cucine);
94 136
 
95 137
 
96
-$ordine = Ordine::firstOrCreate([
97
-    'stato' => Ordine::CARRELLO,
98
-    'dispositivo_id' => $puntoVendita->id,
99
-    'attivita_id' => $puntoVendita->attivita_id,
100
-]);
101
-        Session::put([
138
+        $ordine = Ordine::firstOrCreate([
139
+            'stato' => Ordine::CARRELLO,
102 140
             'dispositivo_id' => $puntoVendita->id,
103 141
             'attivita_id' => $puntoVendita->attivita_id,
104 142
         ]);
105
-$dataTable->ordine_id = $ordine->id;
106
-
107
-        switch($puntoVendita->tipo){
108
-            case Dispositivo::CASSA:
109
-                return $dataTable->render('punto_vendita.cassa.index', ['punto_vendita' => $puntoVendita , 'cucine' => $cucine, 'ordine' => $ordine, 'dataTable_rigaordine' => $dataTable]);
110
-            case Dispositivo::KIOSK:
111
-                return $dataTable->render('punto_vendita.kiosk.index', ['punto_vendita' => $puntoVendita , 'cucine' => $cucine, 'ordine' => $ordine, 'dataTable_rigaordine' => $dataTable]);
112
-            case Dispositivo::CAMERIERE:
113
-                return $dataTable->render('punto_vendita.cam.index', ['punto_vendita' => $puntoVendita , 'cucine' => $cucine, 'ordine' => $ordine, 'dataTable_rigaordine' => $dataTable]);
114
-        };
143
+                Session::put([
144
+                    'dispositivo_id' => $puntoVendita->id,
145
+                    'attivita_id' => $puntoVendita->attivita_id,
146
+                ]);
147
+
148
+        $dataTable->ordine_id = $ordine->id;
149
+
150
+                switch($puntoVendita->tipo){
151
+                    case Dispositivo::CASSA:
152
+                        // return $dataTable->render('punto_vendita.cassa.index', ['punto_vendita' => $puntoVendita , 'cucine' => $cucine, 'ordine' => $ordine, 'dataTable_rigaordine' => $dataTable]);
153
+                        return $dataTable->render('punto_vendita.cassa.vista2', ['punto_vendita' => $puntoVendita , 'cucine' => $cucine, 'ordine' => $ordine, 'dataTable_rigaordine' => $dataTable]);
154
+                    case Dispositivo::KIOSK:
155
+                        return $dataTable->render('punto_vendita.kiosk.index', ['punto_vendita' => $puntoVendita , 'cucine' => $cucine, 'ordine' => $ordine, 'dataTable_rigaordine' => $dataTable]);
156
+                    case Dispositivo::CAMERIERE:
157
+                        return $dataTable->render('punto_vendita.cam.index', ['punto_vendita' => $puntoVendita , 'cucine' => $cucine, 'ordine' => $ordine, 'dataTable_rigaordine' => $dataTable]);
158
+                };
159
+    }
160
+
161
+    public function dissocia_dispositivo(Request $request){
162
+        $puntoVendita = Dispositivo::find($request->punto_vendita_id);
163
+
164
+        if(!$puntoVendita){
165
+            return response()->json(['success' => false, 'message' => 'Punto di vendita non trovato']);
166
+        }
167
+
168
+        $puntoVendita->binding_token = null;
169
+        $puntoVendita->save();
170
+
171
+        return response()->json(['success' => true, 'message' => 'Disassociazione completata']);
115 172
     }
116 173
 
117 174
     public function riepilogo(Request $request)
@@ -124,6 +181,7 @@ $dataTable->ordine_id = $ordine->id;
124 181
         
125 182
         return view('punto_vendita.kiosk.riepilogo', ['ordine' => $ordine]);
126 183
     }
184
+
127 185
     public function dettagli(Request $request)
128 186
     {
129 187
         $puntoVendita = Dispositivo::find($request->punto_vendita_id);

+ 18
- 0
app/Http/Controllers/RigaOrdineController.php Datei anzeigen

@@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Builder;
7 7
 use App\Models\Dispositivo;
8 8
 use App\Models\MonitorHasContenuto;
9 9
 use App\Models\RigaOrdine;
10
+use App\Services\notifica\NotificaOrdineService;
11
+use Illuminate\Support\Facades\Log;
10 12
 
11 13
 class RigaOrdineController extends Controller
12 14
 {
@@ -137,6 +139,22 @@ class RigaOrdineController extends Controller
137 139
         $riga_ordine->stato = RigaOrdine::CHIAMATO;
138 140
         $riga_ordine->save();
139 141
 
142
+Log::info('riga ordine chiamata', [
143
+    'ordine_id' => $riga_ordine->ordine->id,
144
+    'fcm_token' => $riga_ordine->ordine->fcm_token,
145
+]);
146
+        //Invia notifica, se fcm salvato da utente
147
+        if($riga_ordine->ordine->fcm_token){
148
+            Log::info('fcm token trovato', [
149
+                'ordine_id' => $riga_ordine->ordine->id,
150
+                'fcm_token' => $riga_ordine->ordine->fcm_token,
151
+            ]);
152
+            app(NotificaOrdineService::class)->inviaOrdinePronto(
153
+                $riga_ordine,
154
+                'Ordine pronto',
155
+                ($riga_ordine->piatto?->nome ?? 'Piatto').' — ritira al banco'
156
+            );
157
+        }
140 158
         return response()->json(['success' => true, 'message' => 'Riga ordine chiamata']);
141 159
     }
142 160
 

+ 38
- 0
app/Http/Controllers/RigaOrdineNotificaController.php Datei anzeigen

@@ -0,0 +1,38 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\Models\Ordine;
6
+use Illuminate\Http\Request;
7
+use Illuminate\Support\Facades\Log;
8
+
9
+class RigaOrdineNotificaController extends Controller
10
+{
11
+    public function salva_fcm_token(Request $request)
12
+    {
13
+        $request->validate([
14
+            'ordine_id' => 'required|integer',
15
+            'fcm_token' => 'required|string',
16
+        ]);
17
+
18
+        $ordine = Ordine::query()
19
+            ->where('id', $request->input('ordine_id'))
20
+            ->where('stato', Ordine::PAGATO)
21
+            ->first();
22
+
23
+        if (! $ordine) {
24
+            return response()->json(['success' => false, 'message' => 'Ordine non trovato'], 404);
25
+        }
26
+
27
+        $ordine->fcm_token = $request->input('fcm_token');
28
+        $ordine->save();
29
+
30
+        Log::info('[FCM] Token registrato dal cliente', [
31
+            'ordine_id' => $ordine->id,
32
+            'codice' => $ordine->codice,
33
+            'token_preview' => substr($ordine->fcm_token, 0, 16).'...',
34
+        ]);
35
+
36
+        return response()->json(['success' => true, 'message' => 'FCM token salvato']);
37
+    }
38
+}

+ 122
- 0
app/Http/Controllers/TombolaController.php Datei anzeigen

@@ -0,0 +1,122 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use App\Models\Tombola;
7
+
8
+class TombolaController extends Controller
9
+{
10
+    public static $permission_group = "Tombola";
11
+    public static $permissions = [
12
+        'view-tombola' => 'Vedi',
13
+        'create-tombola' => 'Crea',
14
+        'edit-tombola' => 'Modifica',
15
+        'delete-tombola' => 'Elimina',
16
+    ];
17
+
18
+    public static function middleware(): array
19
+    {
20
+        return [
21
+            new Middleware('permission:view-tombola', only: ['index']),
22
+            new Middleware('permission:create-tombola|edit-tombola|delete-tombola', only: ['store', 'update', 'destroy']),
23
+        ];
24
+    }
25
+
26
+    public function index(Request $request){
27
+        $estratti = Tombola::where('estratto', true)
28
+            ->orderByDesc('estratto_at')
29
+            ->get(['numero', 'estratto_at']);
30
+
31
+        $estrattiCount = $estratti->count();
32
+        $totale = Tombola::count();
33
+        $totale = $totale > 0 ? $totale : 90;
34
+        $rimanenti = max($totale - $estrattiCount, 0);
35
+        $ultimoEstratto = $estratti->first();
36
+        $ultimiEstratti = $estratti->take(30);
37
+        $percentuale = $totale > 0 ? round(($estrattiCount / $totale) * 100, 1) : 0;
38
+
39
+        return view('tombola.index', [
40
+            'estrattiCount' => $estrattiCount,
41
+            'totale' => $totale,
42
+            'rimanenti' => $rimanenti,
43
+            'ultimoEstratto' => $ultimoEstratto,
44
+            'ultimiEstratti' => $ultimiEstratti,
45
+            'percentuale' => $percentuale,
46
+            'urlEstrattiPubblica' => route('cliente.tombola.estratti'),
47
+            'urlTabellone' => route('tombola.tabellone'),
48
+        ]);
49
+    }
50
+
51
+    public function tabellone(Request $request){
52
+        return view('tombola.tabellone');
53
+    }
54
+
55
+    public function estrai(Request $request){
56
+        $tombola = Tombola::where('estratto', false)->inRandomOrder()->first();
57
+        if(!$tombola){
58
+            if ($request->expectsJson() || $request->ajax()) {
59
+                return response()->json([
60
+                    'success' => false,
61
+                    'message' => 'Tutti i numeri sono gia stati estratti.',
62
+                ], 422);
63
+            }
64
+
65
+            return redirect()->route('tombola.tabellone')->with('error', 'Tutti i numeri sono gia stati estratti.');
66
+        }
67
+
68
+        $tombola->estratto = true;
69
+        $tombola->estratto_at = now();
70
+        $tombola->save();
71
+
72
+        $estrattiCount = Tombola::where('estratto', true)->count();
73
+        $remaining = 90 - $estrattiCount;
74
+
75
+        if ($request->expectsJson() || $request->ajax()) {
76
+            return response()->json([
77
+                'success' => true,
78
+                'message' => 'Numero estratto con successo.',
79
+                'numero' => (int) $tombola->numero,
80
+                'estratti_count' => $estrattiCount,
81
+                'remaining' => $remaining,
82
+            ]);
83
+        }
84
+
85
+        return redirect()->route('tombola.tabellone')->with('success', 'Tombola estratta');
86
+    }
87
+
88
+    public function estratti(Request $request){
89
+        $estratti = Tombola::where('estratto', true)->orderBy('estratto_at', 'desc')->get();
90
+
91
+        if ($request->expectsJson() || $request->ajax()) {
92
+            $ultimo = $estratti->first();
93
+            $storico = $estratti->slice(1, 7)->values()->map(function ($item) {
94
+                return [
95
+                    'numero' => (int) $item->numero,
96
+                    'estratto_at' => $item->estratto_at?->toIso8601String(),
97
+                ];
98
+            });
99
+
100
+            return response()->json([
101
+                'success' => true,
102
+                'count' => $estratti->count(),
103
+                'ultimo' => $ultimo ? [
104
+                    'numero' => (int) $ultimo->numero,
105
+                    'estratto_at' => $ultimo->estratto_at?->toIso8601String(),
106
+                ] : null,
107
+                'storico' => $storico,
108
+            ]);
109
+        }
110
+
111
+        return view('tombola.cliente.estratti', ['estratti' => $estratti]);
112
+    }
113
+
114
+    public function azzera(Request $request){
115
+        Tombola::query()->update([
116
+            'estratto' => false,
117
+            'estratto_at' => null,
118
+        ]);
119
+
120
+        return redirect()->route('tombola.index')->with('success', 'Tombola azzerata con successo.');
121
+    }
122
+}

+ 37
- 0
app/Http/Middleware/EndpointTokenMiddleware.php Datei anzeigen

@@ -0,0 +1,37 @@
1
+<?php
2
+
3
+namespace App\Http\Middleware;
4
+
5
+use Closure;
6
+use Illuminate\Http\Request;
7
+use Symfony\Component\HttpFoundation\Response;
8
+use App\Models\Endpoint;
9
+use Illuminate\Support\Facades\Log;
10
+class EndpointTokenMiddleware
11
+{
12
+    /**
13
+     * Handle an incoming request.
14
+     *
15
+     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
16
+     */
17
+    public function handle(Request $request, Closure $next): Response
18
+    {
19
+        $token = $request->header('token-endpoint');
20
+        if(!$token){
21
+            Log::error('token-endpoint');
22
+            Log::error('Token endpoint non trovato');
23
+            Log::error('Request: ' . json_encode($request->all()));
24
+            Log::error('Headers: ' . json_encode($request->header()));
25
+            return response()->json(['message' => 'Token endpoint non trovato', 'status' => 'error', 'success' => false], 401);
26
+        }
27
+        $endpoint = Endpoint::where('token', $token)->first();
28
+        if(!$endpoint){
29
+            Log::error('token-endpoint');
30
+            Log::error('Endpoint non trovato con token ' . $token);
31
+            Log::error('Request: ' . json_encode($request->all()));
32
+            return response()->json(['message' => 'Endpoint non trovato con token ' . $token, 'status' => 'error', 'success' => false], 404);
33
+        }
34
+        $request->merge(['endpoint' => $endpoint]);
35
+        return $next($request);
36
+    }
37
+}

+ 27
- 0
app/Jobs/InviaNotificaRigaOrdineProntoJob.php Datei anzeigen

@@ -0,0 +1,27 @@
1
+<?php
2
+
3
+namespace App\Jobs;
4
+
5
+use Illuminate\Contracts\Queue\ShouldQueue;
6
+use Illuminate\Foundation\Queue\Queueable;
7
+
8
+class InviaNotificaRigaOrdineProntoJob implements ShouldQueue
9
+{
10
+    use Queueable;
11
+
12
+    /**
13
+     * Create a new job instance.
14
+     */
15
+    public function __construct()
16
+    {
17
+        //
18
+    }
19
+
20
+    /**
21
+     * Execute the job.
22
+     */
23
+    public function handle(): void
24
+    {
25
+        //
26
+    }
27
+}

+ 6
- 0
app/Models/AbstractModels/AbstractCategoriacontabile.php Datei anzeigen

@@ -39,6 +39,7 @@ abstract class AbstractCategoriacontabile extends \Illuminate\Foundation\Auth\Us
39 39
         'nome' => 'string',
40 40
         'descrizione' => 'string',
41 41
         'is_attiva' => 'boolean',
42
+        'colore' => 'string',
42 43
         'info' => 'json',
43 44
         'created_at' => 'datetime',
44 45
         'updated_at' => 'datetime'
@@ -54,10 +55,15 @@ abstract class AbstractCategoriacontabile extends \Illuminate\Foundation\Auth\Us
54 55
         'nome',
55 56
         'descrizione',
56 57
         'is_attiva',
58
+        'colore',
57 59
         'info',
58 60
         'created_at',
59 61
         'updated_at'
60 62
     ];
61 63
     
64
+    public function prima_nota()
65
+    {
66
+        return $this->hasMany('\App\Models\PrimaNota', 'categoria_contabile_id', 'id');
67
+    }
62 68
 
63 69
 }

+ 2
- 0
app/Models/AbstractModels/AbstractDispositivo.php Datei anzeigen

@@ -42,6 +42,7 @@ abstract class AbstractDispositivo extends \Illuminate\Foundation\Auth\User
42 42
         'nome' => 'string',
43 43
         'licenza' => 'string',
44 44
         // 'ip' => 'string',
45
+        'binding_token' => 'string',
45 46
         'url_stampante' => 'string',
46 47
         'ubicazione' => 'string',
47 48
         'note' => 'string',
@@ -69,6 +70,7 @@ abstract class AbstractDispositivo extends \Illuminate\Foundation\Auth\User
69 70
         'url_stampante',
70 71
         'ubicazione',
71 72
         // 'ip',
73
+        'binding_token',
72 74
         'note',
73 75
         'is_attivo',
74 76
         'pin_sblocco',

+ 2
- 0
app/Models/AbstractModels/AbstractEndpoint.php Datei anzeigen

@@ -43,6 +43,7 @@ abstract class AbstractEndpoint extends \Illuminate\Foundation\Auth\User
43 43
         'url' => 'string',
44 44
         'user_id' => 'integer',
45 45
         'token' => 'string',
46
+        'product_key' => 'string',
46 47
         'secret' => 'string',
47 48
         'status' => 'string',
48 49
         'type' => 'string',
@@ -69,6 +70,7 @@ abstract class AbstractEndpoint extends \Illuminate\Foundation\Auth\User
69 70
         'url',
70 71
         'user_id',
71 72
         'token',
73
+        'product_key',
72 74
         'secret',
73 75
         'status',
74 76
         'type',

+ 80
- 0
app/Models/AbstractModels/AbstractOperatore.php Datei anzeigen

@@ -0,0 +1,80 @@
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 AbstractOperatore extends \Illuminate\Foundation\Auth\User
10
+{
11
+    /**  
12
+     * The table associated with the model.
13
+     * 
14
+     * @var string
15
+     */
16
+    protected $table = 'operatore';
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
+        'attivita_id' => 'integer',
39
+        'id' => 'integer',
40
+        'nome' => 'string',
41
+        'cognome' => 'string',
42
+        'email' => 'string',
43
+        'fcm_token' => 'string',
44
+        'username' => 'string',
45
+        'password' => 'string',
46
+        'changed_password' => 'boolean',
47
+        'telefono' => 'string',
48
+        'is_attivo' => 'boolean',
49
+        'created_at' => 'datetime',
50
+        'updated_at' => 'datetime',
51
+   
52
+    ];
53
+    
54
+    /**  
55
+     * The attributes that are mass assignable.
56
+     * 
57
+     * @var array
58
+     */
59
+    protected $fillable = [
60
+        'attivita_id',
61
+        'id',
62
+        'nome',
63
+        'cognome',
64
+        'email',
65
+        'fcm_token',
66
+        'username',
67
+        'telefono',
68
+        'password',
69
+        'changed_password',
70
+        'telefono',
71
+        'is_attivo',
72
+        'created_at',
73
+        'updated_at'
74
+    ];
75
+    
76
+    public function dispositivi()
77
+    {
78
+        return $this->belongsToMany('\App\Models\Dispositivo', 'operatore_has_dispositivo', 'operatore_id', 'dispositivo_id');
79
+    }
80
+}

+ 2
- 0
app/Models/AbstractModels/AbstractOrdine.php Datei anzeigen

@@ -49,6 +49,7 @@ abstract class AbstractOrdine extends \Illuminate\Foundation\Auth\User
49 49
         'riferimento' => 'string',
50 50
         'metodo_pagamento_id' => 'integer',
51 51
         'info' => 'json',
52
+        'fcm_token' => 'string',
52 53
         'created_at' => 'datetime',
53 54
         'updated_at' => 'datetime'
54 55
     ];
@@ -73,6 +74,7 @@ abstract class AbstractOrdine extends \Illuminate\Foundation\Auth\User
73 74
         'metodo_pagamento_id',
74 75
         'info',
75 76
         'punto_vendita_id',
77
+        'fcm_token',
76 78
         'created_at',
77 79
         'updated_at'
78 80
     ];

+ 7
- 0
app/Models/AbstractModels/AbstractPrimaNota.php Datei anzeigen

@@ -47,6 +47,7 @@ abstract class AbstractPrimaNota extends \Illuminate\Foundation\Auth\User
47 47
         'tipo_movimento' => 'string',
48 48
         'causale' => 'string',
49 49
         'stato' => 'string',
50
+        'categoria_contabile_id' => 'integer',
50 51
         'created_at' => 'datetime',
51 52
         'updated_at' => 'datetime'
52 53
     ];
@@ -69,6 +70,7 @@ abstract class AbstractPrimaNota extends \Illuminate\Foundation\Auth\User
69 70
         'tipo_movimento',   
70 71
         'causale',
71 72
         'stato',
73
+        'categoria_contabile_id',
72 74
         'created_at',
73 75
         'updated_at'
74 76
     ];
@@ -97,4 +99,9 @@ abstract class AbstractPrimaNota extends \Illuminate\Foundation\Auth\User
97 99
     {
98 100
         return $this->belongsTo('\App\Models\Fornitore', 'fornitore_id', 'id');
99 101
     }
102
+
103
+    public function categoria_contabile()
104
+    {
105
+        return $this->belongsTo('\App\Models\Categoriacontabile', 'categoria_contabile_id', 'id');
106
+    }
100 107
 }

+ 67
- 0
app/Models/AbstractModels/AbstractRigaOrdineNotifica.php Datei anzeigen

@@ -0,0 +1,67 @@
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 AbstractRigaOrdineNotifica extends \Illuminate\Foundation\Auth\User
10
+{
11
+    /**  
12
+     * The table associated with the model.
13
+     * 
14
+     * @var string
15
+     */
16
+    protected $table = 'riga_ordine_notifica';
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
+        'riga_ordine_id' => 'integer',
40
+        'fcm_token' => 'string',
41
+        'notificato_at' => 'datetime',
42
+        'created_at' => 'datetime',
43
+        'updated_at' => 'datetime'
44
+    ];
45
+    
46
+    /**  
47
+     * The attributes that are mass assignable.
48
+     * 
49
+     * @var array
50
+     */
51
+    protected $fillable = [
52
+        'id',
53
+        'riga_ordine_id',
54
+        'fcm_token',
55
+        'notificato_at',
56
+        'created_at',
57
+        'updated_at'
58
+    ];
59
+    
60
+
61
+    public function riga_ordine()
62
+    {
63
+        return $this->belongsTo('\App\Models\RigaOrdine', 'riga_ordine_id', 'id');
64
+    }
65
+
66
+
67
+}

+ 36
- 1
app/Models/Attivita.php Datei anzeigen

@@ -6,5 +6,40 @@ use Illuminate\Database\Eloquent\Model;
6 6
 
7 7
 class Attivita extends \App\Models\AbstractModels\AbstractAttivita
8 8
 {
9
-    //
9
+    public function logoUrl(): ?string
10
+    {
11
+        if (empty($this->path_logo)) {
12
+            return null;
13
+        }
14
+
15
+        return str_starts_with($this->path_logo, 'http')
16
+            ? $this->path_logo
17
+            : asset($this->path_logo);
18
+    }
19
+
20
+    public function coverUrl(): ?string
21
+    {
22
+        if (empty($this->path_image)) {
23
+            return null;
24
+        }
25
+
26
+        return str_starts_with($this->path_image, 'http')
27
+            ? $this->path_image
28
+            : asset($this->path_image);
29
+    }
30
+
31
+    public function iniziali(): string
32
+    {
33
+        $parts = preg_split('/\s+/', trim((string) $this->nome), -1, PREG_SPLIT_NO_EMPTY);
34
+
35
+        if (empty($parts)) {
36
+            return '?';
37
+        }
38
+
39
+        if (count($parts) === 1) {
40
+            return mb_strtoupper(mb_substr($parts[0], 0, 2));
41
+        }
42
+
43
+        return mb_strtoupper(mb_substr($parts[0], 0, 1) . mb_substr($parts[1], 0, 1));
44
+    }
10 45
 }

+ 42
- 1
app/Models/Endpoint.php Datei anzeigen

@@ -5,5 +5,46 @@ namespace App\Models;
5 5
 
6 6
 class Endpoint extends \App\Models\AbstractModels\AbstractEndpoint
7 7
 {
8
-    //
8
+    //stati dell'endpoint
9
+    const NON_REGISTRATO = 'non_registrato';
10
+    const REGISTRATO = 'registrato';
11
+    const ELIMINATO = 'eliminato';
12
+    const ATTIVO = 'attivo';
13
+    const DISABILITATO = 'disabilitato';
14
+
15
+    public static function getStati()
16
+    {
17
+        return collect([
18
+            self::NON_REGISTRATO => [
19
+                'label' => 'Non registrato',
20
+                'class' => 'bg-label-secondary text-secondary',
21
+                'value' => self::NON_REGISTRATO,
22
+                'icon' => 'bx bx-x',
23
+            ],
24
+            self::REGISTRATO => [
25
+                'label' => 'Registrato',
26
+                'class' => 'bg-label-info text-info',
27
+                'value' => self::REGISTRATO,
28
+                'icon' => 'bx bx-check',
29
+            ],
30
+            self::ELIMINATO => [
31
+                'label' => 'Eliminato',
32
+                'class' => 'bg-label-danger text-danger',
33
+                'value' => self::ELIMINATO,
34
+                'icon' => 'bx bx-x',
35
+                ],
36
+            self::ATTIVO => [
37
+                'label' => 'Attivo',
38
+                'class' => 'bg-label-success text-success',
39
+                'value' => self::ATTIVO,
40
+                'icon' => 'bx bx-check',
41
+            ],
42
+            self::DISABILITATO => [
43
+                'label' => 'Disabilitato',
44
+                'class' => 'bg-label-warning text-warning',
45
+                'value' => self::DISABILITATO,
46
+                'icon' => 'bx bx-lock',
47
+            ],
48
+        ]);
49
+    }
9 50
 }

+ 10
- 0
app/Models/Operatore.php Datei anzeigen

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

+ 10
- 0
app/Models/RigaOrdineNotifica.php Datei anzeigen

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

+ 37
- 0
app/Models/Tombola.php Datei anzeigen

@@ -0,0 +1,37 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Illuminate\Database\Eloquent\Model;
6
+
7
+class Tombola extends Model
8
+{
9
+    protected $table = 'tombola';
10
+    protected $fillable = [
11
+        'numero', 
12
+        'estratto', 
13
+        'estratto_at', 
14
+        'attivita_id', 
15
+        'user_id',
16
+        'created_at',
17
+        'updated_at',
18
+    ];
19
+    
20
+    protected $casts = [
21
+        'numero' => 'integer',
22
+        'estratto' => 'boolean',
23
+        'estratto_at' => 'datetime',
24
+        'attivita_id' => 'integer',
25
+        'user_id' => 'integer',
26
+    ];
27
+
28
+    public function attivita()
29
+    {
30
+        return $this->belongsTo(Attivita::class, 'attivita_id', 'id');
31
+    }
32
+
33
+    public function user()
34
+    {
35
+        return $this->belongsTo(User::class, 'user_id', 'id');
36
+    }
37
+}

+ 116
- 0
app/Services/Carrello/CarrelloService.php Datei anzeigen

@@ -0,0 +1,116 @@
1
+<?php
2
+
3
+namespace App\Services\Carrello;
4
+
5
+use App\Models\Ordine;
6
+use App\Models\RigaOrdine;
7
+use App\Models\Piatto;
8
+use Illuminate\Database\Eloquent\ModelNotFoundException;
9
+
10
+class CarrelloService
11
+{
12
+    public function findCartOrFail(int $ordineId): Ordine
13
+    {
14
+        $ordine = Ordine::find($ordineId);
15
+
16
+        if (! $ordine) {
17
+            throw new ModelNotFoundException('Ordine non trovato.');
18
+        }
19
+
20
+        return $ordine;
21
+    }
22
+
23
+    public function aumentaQuantita(int $dispositivo_id ,?int $attivita_id, ?int $ordine_id, int $piatto_id): array
24
+    {
25
+    // Tolleranza su ordine_id stale: se non e` valido, usa/crea il carrello corrente per dispositivo+attivita.
26
+        $carrelloBaseQuery = Ordine::query()
27
+            ->where('stato', Ordine::CARRELLO)
28
+            ->where('dispositivo_id', $dispositivo_id)
29
+            ->when(
30
+                $attivita_id,
31
+                fn ($q) => $q->where('attivita_id', $attivita_id)
32
+            );
33
+
34
+        $carrello = null;
35
+        if ($ordine_id) {
36
+            $carrello = (clone $carrelloBaseQuery)
37
+                ->whereKey($ordine_id)
38
+                ->first();
39
+        }
40
+
41
+        if (!$carrello) {
42
+            $carrello = (clone $carrelloBaseQuery)->latest('id')->first();
43
+        }
44
+
45
+        if (!$carrello) {
46
+            $carrello = Ordine::create([
47
+                'stato' => Ordine::CARRELLO,
48
+                'dispositivo_id' => $dispositivo_id,
49
+                'attivita_id' => $attivita_id,
50
+            ]);
51
+        }
52
+
53
+        $riga = RigaOrdine::where('ordine_id', $carrello->id)->where('piatto_id', $piatto_id)->first();
54
+        $piatto = Piatto::findOrFail($piatto_id);
55
+        if($riga){
56
+            $riga->quantita += 1;
57
+            $riga->prezzo = $piatto->prezzo*$riga->quantita;
58
+            $riga->save();
59
+        }else{
60
+            $riga = RigaOrdine::create([
61
+                'ordine_id' => $carrello->id,
62
+                'piatto_id' => $piatto_id,
63
+                'quantita' => 1,
64
+                'prezzo' => $piatto->prezzo,
65
+            ]);
66
+        }
67
+        // dd($riga);
68
+        return [
69
+            'ordine' => $riga->ordine,
70
+            'riga' => $riga,
71
+        ];
72
+    }
73
+
74
+    public function diminuisciQuantita(int $ordine_id, int $piatto_id, int $riga_ordine_id): array
75
+    {
76
+        $carrello = Ordine::findOrFail($ordine_id);
77
+        $riga = RigaOrdine::where('ordine_id', $carrello->id)->where('piatto_id', $piatto_id)->first();
78
+        if($riga && $riga->id == $riga_ordine_id){
79
+            $piatto = Piatto::findOrFail($piatto_id);
80
+            $riga->quantita -= 1;
81
+            $riga->prezzo = $piatto->prezzo*$riga->quantita;
82
+            $riga->save();   
83
+        }
84
+        return [
85
+            'ordine' => $carrello,
86
+            'riga' => $riga,
87
+        ];
88
+    }
89
+
90
+    public function eliminaRiga(int $rigaOrdineId, bool $elimina = false): void
91
+    {
92
+        $riga = RigaOrdine::find($rigaOrdineId);
93
+        if($riga !== null && $elimina){
94
+        $riga->delete();
95
+        };
96
+    }
97
+
98
+    public function azzeraCarrello(int $ordineId): void
99
+    {
100
+        RigaOrdine::where('ordine_id', $ordineId)->delete();
101
+    }
102
+
103
+    public function aggiornaNota(int $rigaOrdineId, string $note): RigaOrdine
104
+    {
105
+        $riga = RigaOrdine::findOrFail($rigaOrdineId);
106
+        if($riga == null){
107
+            throw new ModelNotFoundException('Riga ordine non trovata.');
108
+        };
109
+        if($note == null){
110
+            throw new ValidationException('Nota non valida.');
111
+        }
112
+        $riga->note = $note;
113
+        $riga->save();
114
+        return $riga;
115
+    }
116
+}

+ 72
- 0
app/Services/Notifica/NotificaOrdineService.php Datei anzeigen

@@ -0,0 +1,72 @@
1
+<?php
2
+
3
+namespace App\Services\Notifica;
4
+
5
+use App\Models\RigaOrdine;
6
+use Illuminate\Support\Facades\Log;
7
+use Kreait\Firebase\Exception\Messaging\NotFound;
8
+use Kreait\Firebase\Exception\MessagingException;
9
+use Kreait\Firebase\Messaging\CloudMessage;
10
+use Kreait\Firebase\Messaging\Notification;
11
+use Kreait\Laravel\Firebase\Facades\Firebase;
12
+
13
+class NotificaOrdineService
14
+{
15
+    /**
16
+     * Invia push FCM al cliente che ha registrato il token sull'ordine.
17
+     */
18
+    public function inviaOrdinePronto(RigaOrdine $riga_ordine, ?string $titolo = null, ?string $corpo = null): bool
19
+    {
20
+        $token = $riga_ordine->ordine->fcm_token;
21
+
22
+        if (empty($token)) {
23
+            Log::info('[FCM] Skip: ordine senza token', [
24
+                'ordine_id' => $riga_ordine->ordine->id,
25
+            ]);
26
+
27
+            return false;
28
+        }
29
+
30
+        $riga_ordine->loadMissing(['piatto', 'ordine.attivita']);
31
+
32
+        $titolo = $titolo ?? ($riga_ordine->ordine->attivita?->nome ?? 'Ordine').' — Ordine pronto';
33
+        $corpo = $corpo ?? ($riga_ordine->piatto?->nome ?? 'Piatto').' è pronto — ritira al banco';
34
+
35
+        $message = CloudMessage::new()
36
+            ->toToken($token)
37
+            ->withNotification(Notification::create($titolo, $corpo))
38
+            ->withData([
39
+                'ordine_id' => (string) $riga_ordine->ordine->id,
40
+            ]);
41
+
42
+            Log::info('messaggio creato', [
43
+                'titolo' => $titolo,
44
+                'corpo' => $corpo,
45
+                'ordine_id' => $riga_ordine->ordine->id,
46
+            ]);
47
+
48
+        try {
49
+            Firebase::messaging()->send($message);
50
+            Log::info('messaggio inviato', [
51
+                'ordine_id' => $riga_ordine->ordine->id,
52
+            ]);
53
+            return true;
54
+        } catch (NotFound $e) {
55
+            Log::warning('FCM token non valido, rimosso dall\'ordine', [
56
+                'ordine_id' => $riga_ordine->ordine->id,
57
+                'message' => $e->getMessage(),
58
+            ]);
59
+            // $riga_ordine->ordine->fcm_token = null;
60
+            // $riga_ordine->ordine->save();
61
+
62
+            return false;
63
+        } catch (MessagingException $e) {
64
+            Log::error('Errore invio notifica FCM ordine', [
65
+                'ordine_id' => $riga_ordine->ordine->id,
66
+                'message' => $e->getMessage(),
67
+            ]);
68
+
69
+            return false;
70
+        }
71
+    }
72
+}

+ 1
- 0
bootstrap/app.php Datei anzeigen

@@ -22,6 +22,7 @@ return Application::configure(basePath: dirname(__DIR__))
22 22
             'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
23 23
             'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
24 24
             'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
25
+            'endpoint-token' => \App\Http\Middleware\EndpointTokenMiddleware::class,
25 26
         ]);
26 27
     })
27 28
     ->withExceptions(function (Exceptions $exceptions) {

+ 2
- 0
composer.json Datei anzeigen

@@ -14,6 +14,8 @@
14 14
     "elibyy/tcpdf-laravel": "^11.5",
15 15
     "endroid/qr-code": "^6.1",
16 16
     "imtigger/laravel-job-status": "^1.2",
17
+    "kreait/firebase-php": "7.24",
18
+    "kreait/laravel-firebase": "6.2",
17 19
     "laravel/framework": "^12.0",
18 20
     "laravel/horizon": "^5.43",
19 21
     "laravel/jetstream": "*",

+ 2172
- 435
composer.lock
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 8
- 5
config/auth.php Datei anzeigen

@@ -40,6 +40,10 @@ return [
40 40
             'driver' => 'session',
41 41
             'provider' => 'users',
42 42
         ],
43
+        'operatore' => [
44
+            'driver' => 'session',
45
+            'provider' => 'operatori',
46
+        ],
43 47
     ],
44 48
 
45 49
     /*
@@ -64,11 +68,10 @@ return [
64 68
             'driver' => 'eloquent',
65 69
             'model' => env('AUTH_MODEL', App\Models\User::class),
66 70
         ],
67
-
68
-        // 'users' => [
69
-        //     'driver' => 'database',
70
-        //     'table' => 'users',
71
-        // ],
71
+        'operatori' => [
72
+            'driver' => 'eloquent',
73
+            'model' => \App\Models\Operatore::class,
74
+        ],
72 75
     ],
73 76
 
74 77
     /*

+ 207
- 0
config/firebase.php Datei anzeigen

@@ -0,0 +1,207 @@
1
+<?php
2
+
3
+declare(strict_types=1);
4
+
5
+return [
6
+    /*
7
+     * ------------------------------------------------------------------------
8
+     * Default Firebase project
9
+     * ------------------------------------------------------------------------
10
+     */
11
+
12
+    'default' => env('FIREBASE_PROJECT', 'app'),
13
+
14
+    /*
15
+     * ------------------------------------------------------------------------
16
+     * Firebase project configurations
17
+     * ------------------------------------------------------------------------
18
+     */
19
+
20
+    'projects' => [
21
+        'app' => [
22
+
23
+            /*
24
+             * ------------------------------------------------------------------------
25
+             * Credentials / Service Account
26
+             * ------------------------------------------------------------------------
27
+             *
28
+             * In order to access a Firebase project and its related services using a
29
+             * server SDK, requests must be authenticated. For server-to-server
30
+             * communication this is done with a Service Account.
31
+             *
32
+             * If you don't already have generated a Service Account, you can do so by
33
+             * following the instructions from the official documentation pages at
34
+             *
35
+             * https://firebase.google.com/docs/admin/setup#initialize_the_sdk
36
+             *
37
+             * Once you have downloaded the Service Account JSON file, you can use it
38
+             * to configure the package.
39
+             *
40
+             * If you don't provide credentials, the Firebase Admin SDK will try to
41
+             * auto-discover them
42
+             *
43
+             * - by checking the environment variable FIREBASE_CREDENTIALS
44
+             * - by checking the environment variable GOOGLE_APPLICATION_CREDENTIALS
45
+             * - by trying to find Google's well known file
46
+             * - by checking if the application is running on GCE/GCP
47
+             *
48
+             * If no credentials file can be found, an exception will be thrown the
49
+             * first time you try to access a component of the Firebase Admin SDK.
50
+             *
51
+             */
52
+
53
+            'credentials' => env('FIREBASE_CREDENTIALS', env('GOOGLE_APPLICATION_CREDENTIALS')),
54
+
55
+            /*
56
+             * ------------------------------------------------------------------------
57
+             * Firebase Auth Component
58
+             * ------------------------------------------------------------------------
59
+             */
60
+
61
+            'auth' => [
62
+                'tenant_id' => env('FIREBASE_AUTH_TENANT_ID'),
63
+            ],
64
+
65
+            /*
66
+             * ------------------------------------------------------------------------
67
+             * Firestore Component
68
+             * ------------------------------------------------------------------------
69
+             */
70
+
71
+            'firestore' => [
72
+
73
+                /*
74
+                 * If you want to access a Firestore database other than the default database,
75
+                 * enter its name here.
76
+                 *
77
+                 * By default, the Firestore client will connect to the `(default)` database.
78
+                 *
79
+                 * https://firebase.google.com/docs/firestore/manage-databases
80
+                 */
81
+
82
+                // 'database' => env('FIREBASE_FIRESTORE_DATABASE'),
83
+            ],
84
+
85
+            /*
86
+             * ------------------------------------------------------------------------
87
+             * Firebase Realtime Database
88
+             * ------------------------------------------------------------------------
89
+             */
90
+
91
+            'database' => [
92
+
93
+                /*
94
+                 * In most of the cases the project ID defined in the credentials file
95
+                 * determines the URL of your project's Realtime Database. If the
96
+                 * connection to the Realtime Database fails, you can override
97
+                 * its URL with the value you see at
98
+                 *
99
+                 * https://console.firebase.google.com/u/1/project/_/database
100
+                 *
101
+                 * Please make sure that you use a full URL like, for example,
102
+                 * https://my-project-id.firebaseio.com
103
+                 */
104
+
105
+                'url' => env('FIREBASE_DATABASE_URL'),
106
+
107
+                /*
108
+                 * As a best practice, a service should have access to only the resources it needs.
109
+                 * To get more fine-grained control over the resources a Firebase app instance can access,
110
+                 * use a unique identifier in your Security Rules to represent your service.
111
+                 *
112
+                 * https://firebase.google.com/docs/database/admin/start#authenticate-with-limited-privileges
113
+                 */
114
+
115
+                // 'auth_variable_override' => [
116
+                //     'uid' => 'my-service-worker'
117
+                // ],
118
+
119
+            ],
120
+
121
+            /*
122
+             * ------------------------------------------------------------------------
123
+             * Firebase Cloud Storage
124
+             * ------------------------------------------------------------------------
125
+             */
126
+
127
+            'storage' => [
128
+
129
+                /*
130
+                 * Your project's default storage bucket usually uses the project ID
131
+                 * as its name. If you have multiple storage buckets and want to
132
+                 * use another one as the default for your application, you can
133
+                 * override it here.
134
+                 */
135
+
136
+                'default_bucket' => env('FIREBASE_STORAGE_DEFAULT_BUCKET'),
137
+
138
+            ],
139
+
140
+            /*
141
+             * ------------------------------------------------------------------------
142
+             * Caching
143
+             * ------------------------------------------------------------------------
144
+             *
145
+             * The Firebase Admin SDK can cache some data returned from the Firebase
146
+             * API, for example Google's public keys used to verify ID tokens.
147
+             *
148
+             */
149
+
150
+            'cache_store' => env('FIREBASE_CACHE_STORE', 'file'),
151
+
152
+            /*
153
+             * ------------------------------------------------------------------------
154
+             * Logging
155
+             * ------------------------------------------------------------------------
156
+             *
157
+             * Enable logging of HTTP interaction for insights and/or debugging.
158
+             *
159
+             * Log channels are defined in config/logging.php
160
+             *
161
+             * Successful HTTP messages are logged with the log level 'info'.
162
+             * Failed HTTP messages are logged with the log level 'notice'.
163
+             *
164
+             * Note: Using the same channel for simple and debug logs will result in
165
+             * two entries per request and response.
166
+             */
167
+
168
+            'logging' => [
169
+                'http_log_channel' => env('FIREBASE_HTTP_LOG_CHANNEL'),
170
+                'http_debug_log_channel' => env('FIREBASE_HTTP_DEBUG_LOG_CHANNEL'),
171
+            ],
172
+
173
+            /*
174
+             * ------------------------------------------------------------------------
175
+             * HTTP Client Options
176
+             * ------------------------------------------------------------------------
177
+             *
178
+             * Behavior of the HTTP Client performing the API requests
179
+             */
180
+
181
+            'http_client_options' => [
182
+
183
+                /*
184
+                 * Use a proxy that all API requests should be passed through.
185
+                 * (default: none)
186
+                 */
187
+
188
+                'proxy' => env('FIREBASE_HTTP_CLIENT_PROXY'),
189
+
190
+                /*
191
+                 * Set the maximum amount of seconds (float) that can pass before
192
+                 * a request is considered timed out
193
+                 *
194
+                 * The default time out can be reviewed at
195
+                 * https://github.com/beste/firebase-php/blob/6.x/src/Firebase/Http/HttpClientOptions.php
196
+                 */
197
+
198
+                'timeout' => env('FIREBASE_HTTP_CLIENT_TIMEOUT'),
199
+
200
+                'guzzle_middlewares' => [
201
+                    // MyInvokableMiddleware::class,
202
+                    // [MyMiddleware::class, 'static_method'],
203
+                ],
204
+            ],
205
+        ],
206
+    ],
207
+];

+ 7
- 0
config/services.php Datei anzeigen

@@ -34,5 +34,12 @@ return [
34 34
             'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
35 35
         ],
36 36
     ],
37
+        'firebase' => [
38
+            'web_api_key' => env('FIREBASE_WEB_API_KEY','AIzaSyD2qHjGcEkHpPJKtj6moHgWxOF9bgiPzo4'),
39
+            'project_id' => env('FIREBASE_PROJECT_ID','fest-6f8db'),
40
+            'messaging_sender_id' => env('FIREBASE_MESSAGING_SENDER_ID','1058806233312'),
41
+            'app_id' => env('FIREBASE_APP_ID','1:1058806233312:web:ad9f595d3e47a1cec3b885'),
42
+            'vapid_key' => env('FIREBASE_VAPID_KEY','BNHqYsJ0662-nK4N3FyhB4e1LzlC3QF9qJQXOZ7Zf9N3D6C3P3Z_8K7H_2N6M_5J4L_3I3K_1H2G_3F4E_5D6C_7B8A'),
43
+        ],
37 44
 
38 45
 ];

+ 1
- 0
database/migrations/2026_03_23_224652_dispositivo.php Datei anzeigen

@@ -21,6 +21,7 @@ return new class extends Migration
21 21
             $table->string('tipo')->nullable();
22 22
             $table->boolean('is_attivo')->default(true);
23 23
             $table->string('pin_sblocco')->nullable();
24
+            $table->string('binding_token')->nullable();
24 25
             $table->dateTime('data_apertura_dispositivo')->nullable();
25 26
             $table->dateTime('data_chiusura_dispositivo')->nullable();
26 27
             $table->timestamps();

+ 1
- 0
database/migrations/2026_03_27_223826_prima_nota.php Datei anzeigen

@@ -19,6 +19,7 @@ return new class extends Migration
19 19
             $table->bigInteger('prenotazione_id')->unsigned()->nullable();
20 20
             $table->bigInteger('evento_id')->unsigned()->nullable();
21 21
             $table->bigInteger('fornitore_id')->unsigned()->nullable();
22
+            $table->bigInteger('categoria_contabile_id')->unsigned()->nullable();
22 23
             $table->string('riferimento')->nullable();
23 24
             $table->double('importo', 10, 2)->nullable();
24 25
             $table->string('tipo_movimento')->nullable();

+ 1
- 0
database/migrations/2026_04_05_112856_endpoint.php Datei anzeigen

@@ -20,6 +20,7 @@ return new class extends Migration
20 20
             $table->string('url')->nullable();
21 21
             $table->bigInteger('user_id')->unsigned()->nullable();
22 22
             $table->string('token')->nullable();
23
+            $table->string('product_key')->unique()->nullable()->length(25);
23 24
             $table->string('secret')->nullable();
24 25
             $table->string('status')->nullable();
25 26
             $table->string('type')->nullable();

+ 2
- 2
database/migrations/2026_05_09_131317_saltacoda_rigaordine.php Datei anzeigen

@@ -3,7 +3,7 @@
3 3
 use Illuminate\Database\Migrations\Migration;
4 4
 use Illuminate\Database\Schema\Blueprint;
5 5
 use Illuminate\Support\Facades\Schema;
6
-use App\Models\SaltacodaRigaordine;
6
+use App\Models\SaltacodaOrdine;
7 7
 
8 8
 return new class extends Migration
9 9
 {
@@ -19,7 +19,7 @@ return new class extends Migration
19 19
             $table->integer('quantita')->default(1);
20 20
             $table->decimal('prezzo', 10, 2)->nullable();
21 21
             $table->string('note')->nullable();
22
-            $table->string('stato')->nullable()->default(SaltacodaRigaordine::PRE_ORDINE);
22
+            $table->string('stato')->nullable()->default(SaltacodaOrdine::PRE_ORDINE);
23 23
             $table->timestamps();
24 24
         });
25 25
 

+ 5
- 0
database/migrations/2026_05_15_230716_categoria_contabile.php Datei anzeigen

@@ -16,9 +16,14 @@ return new class extends Migration
16 16
             $table->string('nome')->unique();
17 17
             $table->string('descrizione')->nullable();
18 18
             $table->boolean('is_attiva')->default(true);
19
+            $table->string('colore')->nullable();
19 20
             $table->json('info')->nullable();
20 21
             $table->timestamps();
21 22
         });
23
+
24
+        Schema::table('prima_nota', function (Blueprint $table) {
25
+            $table->foreign('categoria_contabile_id')->references('id')->on('categoria_contabile')->onDelete('cascade');
26
+        });
22 27
     }
23 28
 
24 29
     /**

+ 38
- 0
database/migrations/2026_05_25_165128_operatore.php Datei anzeigen

@@ -0,0 +1,38 @@
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('operatore', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->bigInteger('attivita_id')->unsigned();
17
+            $table->foreign('attivita_id')->references('id')->on('attivita')->onDelete('cascade');
18
+            $table->string('nome')->nullable();
19
+            $table->string('cognome')->nullable();
20
+            $table->string('email')->nullable()->unique();
21
+            $table->string('fcm_token')->nullable();
22
+            $table->string('username')->unique();
23
+            $table->string('password');
24
+            $table->boolean('changed_password')->default(false);
25
+            $table->string('telefono')->nullable();
26
+            $table->boolean('is_attivo')->default(true);
27
+            $table->timestamps();
28
+        });
29
+    }
30
+
31
+    /**
32
+     * Reverse the migrations.
33
+     */
34
+    public function down(): void
35
+    {
36
+        Schema::dropIfExists('operatore');
37
+    }
38
+};

+ 30
- 0
database/migrations/2026_05_25_165331_operatore_has_dispositivo.php Datei anzeigen

@@ -0,0 +1,30 @@
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('operatore_has_dispositivo', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->foreignId('operatore_id')->constrained('operatore')->cascadeOnDelete();
17
+            $table->foreignId('dispositivo_id')->constrained('dispositivo')->cascadeOnDelete();
18
+            $table->unique(['operatore_id', 'dispositivo_id']);
19
+            $table->timestamps();
20
+        });
21
+    }
22
+
23
+    /**
24
+     * Reverse the migrations.
25
+     */
26
+    public function down(): void
27
+    {
28
+        Schema::dropIfExists('operatore_has_dispositivo');
29
+    }
30
+};

+ 28
- 0
database/migrations/2026_05_29_150004_add_notifica_fields_to_ordine_table.php Datei anzeigen

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

+ 34
- 0
database/migrations/2026_05_29_150007_create_ordine_notifica_table.php Datei anzeigen

@@ -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('riga_ordine_notifica', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->bigInteger('riga_ordine_id')->unsigned()->nullable();
17
+            $table->string('fcm_token')->nullable();
18
+            $table->timestamp('notificato_at')->nullable();
19
+            $table->timestamps();
20
+        });
21
+
22
+        Schema::table('riga_ordine_notifica', function (Blueprint $table) {
23
+            $table->foreign('riga_ordine_id')->references('id')->on('riga_ordine')->onDelete('cascade');
24
+        });
25
+    }
26
+
27
+    /**
28
+     * Reverse the migrations.
29
+     */
30
+    public function down(): void
31
+    {
32
+        Schema::dropIfExists('riga_ordine_notifica');
33
+    }
34
+};

+ 36
- 0
database/migrations/2026_06_06_183117_tombola.php Datei anzeigen

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

+ 113
- 0
database/seeders/CategoriaContabileSeed.php Datei anzeigen

@@ -0,0 +1,113 @@
1
+<?php
2
+
3
+namespace Database\Seeders;
4
+
5
+use Illuminate\Database\Console\Seeds\WithoutModelEvents;
6
+use Illuminate\Database\Seeder;
7
+use App\Models\Categoriacontabile;
8
+
9
+class CategoriaContabileSeed extends Seeder
10
+{
11
+    /**
12
+     * Run the database seeds.
13
+     */
14
+    public function run(): void
15
+    {
16
+        $categorieContabili = [
17
+            [
18
+                'nome' => 'Spese generali',
19
+                'descrizione' => 'Spese generali',
20
+                'colore' => '#dc3545', // Rosso (uscite generali)
21
+            ],
22
+            [
23
+                'nome' => 'Utenze',
24
+                'descrizione' => 'Spese per utenze (Luce, Acqua, Gas, Telefono)',
25
+                'colore' => '#fd7e14', // Arancione
26
+            ],
27
+            [
28
+                'nome' => 'Utenza Elettrica',
29
+                'descrizione' => 'Spese per elettricità',
30
+                'colore' => '#007bff', // Blu
31
+            ],
32
+            [
33
+                'nome' => 'Utenza Gas',
34
+                'descrizione' => 'Spese per gas',
35
+                'colore' => '#6f42c1', // Viola
36
+            ],
37
+            [
38
+                'nome' => 'Utenza Acqua',
39
+                'descrizione' => 'Spese per acqua',
40
+                'colore' => '#20c997', // Verde acqua
41
+            ],
42
+            [
43
+                'nome' => 'Utenza Telefono-Internet',
44
+                'descrizione' => 'Spese per telefono e internet',
45
+                'colore' => '#6610f2', // Blu scuro
46
+            ],
47
+            [
48
+                'nome' => 'Forniture alimentari',
49
+                'descrizione' => 'Spese per forniture alimentari',
50
+                'colore' => '#ffc107', // Giallo
51
+            ],
52
+            [
53
+                'nome' => 'Forniture non alimentari',
54
+                'descrizione' => 'Spese per forniture non alimentari',
55
+                'colore' => '#17a2b8', // Azzurro
56
+            ],
57
+            [
58
+                'nome' => 'Forniture di consumo',
59
+                'descrizione' => 'Spese per forniture di consumo (Detergenti, Prodotti per la pulizia, ecc.)',
60
+                'colore' => '#e83e8c', // Rosa
61
+            ],
62
+            [
63
+                'nome' => 'Altre Spese',
64
+                'descrizione' => 'Spese per altro',
65
+                'colore' => '#343a40', // Grigio scuro
66
+            ],
67
+            [
68
+                'nome' => 'Personale',
69
+                'descrizione' => 'Spese per personale (Salari, Incentivi, ecc.)',
70
+                'colore' => '#dc3545', // Rosso (uscite generali)
71
+            ],
72
+            [
73
+                'nome' => 'Forniture di consumo',
74
+                'descrizione' => 'Spese per forniture di consumo (Detergenti, Prodotti per la pulizia, ecc.)',
75
+                'colore' => '#e83e8c', // Rosa
76
+            ],
77
+            [
78
+                'nome' => 'Spese di gestione',
79
+                'descrizione' => 'Spese per gestione (Assicurazioni, Tasse, ecc.)',
80
+                'colore' => '#343a40', // Grigio scuro
81
+            ],
82
+            [
83
+                'nome' => 'Spese di marketing',
84
+                'descrizione' => 'Spese per marketing (Pubblicità, Promozioni, ecc.)',
85
+                'colore' => '#6f42c1', // Viola
86
+            ],
87
+            [
88
+                'nome' => 'Spese di amministrazione',
89
+                'descrizione' => 'Spese per amministrazione (Spese di ufficio, ecc.)',
90
+                'colore' => '#20c997', // Verde acqua
91
+            ],
92
+            [
93
+                'nome' => 'Vendita',
94
+                'descrizione' => ' Ricavi da vendita (Pizze, Bevande, ecc.)',
95
+                'colore' => '#20c997', // Verde acqua
96
+            ],
97
+            [
98
+                'nome' => 'Altri Ricavi',
99
+                'descrizione' => 'Ricavi da altri servizi (Servizi di pulizia, ecc.)',
100
+                'colore' => '#343a40', // Grigio scuro
101
+            ],
102
+            [
103
+                'nome' => 'Offerte',
104
+                'descrizione' => 'Offerte',
105
+                'colore' => '#ffc107', // Giallo
106
+            ],      
107
+            
108
+        ];
109
+        foreach($categorieContabili as $categoriaContabile){
110
+            Categoriacontabile::firstOrCreate(['nome' => $categoriaContabile['nome']], $categoriaContabile);
111
+        }
112
+    }
113
+}

+ 26
- 0
database/seeders/TombolaSeed.php Datei anzeigen

@@ -0,0 +1,26 @@
1
+<?php
2
+
3
+namespace Database\Seeders;
4
+
5
+use Illuminate\Database\Console\Seeds\WithoutModelEvents;
6
+use Illuminate\Database\Seeder;
7
+use App\Models\Tombola;
8
+
9
+class TombolaSeed extends Seeder
10
+{
11
+    /**
12
+     * Run the database seeds.
13
+     */
14
+    public function run(): void
15
+    {
16
+    for ($i = 1; $i <= 90; $i++) {
17
+        Tombola::create([
18
+            'numero' => $i,
19
+            'estratto' => false,
20
+            'estratto_at' => null,
21
+            'attivita_id' => null,
22
+            'user_id' => null,
23
+        ]);
24
+    }
25
+    }
26
+}

+ 1451
- 63
package-lock.json
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 1
- 0
package.json Datei anzeigen

@@ -57,6 +57,7 @@
57 57
     "datatables.net-rowgroup-bs5": "1.5.1",
58 58
     "datatables.net-select-bs5": "2.1.0",
59 59
     "dropzone": "5.9.3",
60
+    "firebase": "^12.14.0",
60 61
     "flag-icons": "7.3.2",
61 62
     "flatpickr": "4.6.13",
62 63
     "hammerjs": "2.0.8",

+ 67
- 0
patch-welcome-header.php Datei anzeigen

@@ -0,0 +1,67 @@
1
+<?php
2
+$f = __DIR__ . '/resources/views/welcome.blade.php';
3
+$c = file_get_contents($f);
4
+
5
+$start = strpos($c, '    <header class="welcome-bacheca__masthead"');
6
+$end = strpos($c, '    @if($attivitaAttive->isEmpty())');
7
+if ($start === false || $end === false) {
8
+    echo "markers not found start=$start end=$end\n";
9
+    exit(1);
10
+}
11
+
12
+$new = <<<'HTML'
13
+    <header class="welcome-bacheca__masthead" aria-labelledby="bacheca-title">
14
+      <motion class="welcome-bacheca__masthead-row">
15
+        <div class="welcome-bacheca__brand">
16
+          <img
17
+            src="{{ asset('assets/img/logo_fest_L.png') }}"
18
+            alt="{{ config('app.name') }}"
19
+            class="welcome-bacheca__logo"
20
+            width="200"
21
+            height="60"
22
+          >
23
+        </div>
24
+        <div class="welcome-bacheca__masthead-actions">
25
+          <a href="{{ route('login') }}" class="welcome-bacheca__btn welcome-bacheca__btn--ghost">
26
+            <i class="bx bx-log-in"></i>
27
+            Accedi
28
+          </a>
29
+          <a href="{{ $registerUrl }}" class="welcome-bacheca__btn welcome-bacheca__btn--primary">
30
+            Registrati
31
+          </a>
32
+        </div>
33
+      </div>
34
+      <div class="welcome-bacheca__masthead-body">
35
+        <p class="welcome-bacheca__eyebrow">
36
+          <span class="welcome-bacheca__live-dot" aria-hidden="true"></span>
37
+          Bacheca eventi
38
+        </p>
39
+        <h1 id="bacheca-title" class="welcome-bacheca__title">
40
+          Scopri le attività in programma
41
+        </h1>
42
+        <p class="welcome-bacheca__lead">
43
+          Sagre, feste e rassegne attive: tocca un’attività per ordini, prenotazioni e servizi dedicati.
44
+        </p>
45
+      </div>
46
+    </header>
47
+
48
+HTML;
49
+
50
+$new = str_replace('motion', 'div', $new);
51
+
52
+$c = substr($c, 0, $start) . $new . substr($c, $end);
53
+
54
+$c = str_replace(
55
+    "          <span class=\"welcome-bacheca__board-count\">{{ \$attivitaAttive->count() }} {{ Str::plural('scheda', \$attivitaAttive->count()) }}</span>\n",
56
+    '',
57
+    $c
58
+);
59
+
60
+$c = preg_replace(
61
+    '/\s*@if\(\$attivita->eventi_count > 0\)\s*<span class="welcome-bacheca__card-chip">.*?<\/span>\s*@endif/s',
62
+    '',
63
+    $c
64
+);
65
+
66
+file_put_contents($f, $c);
67
+echo "done\n";

+ 20
- 0
public/firebase-messaging-sw.js Datei anzeigen

@@ -0,0 +1,20 @@
1
+importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js');
2
+importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js');
3
+
4
+firebase.initializeApp({
5
+  apiKey: "AIzaSyD2qHjGcEkHpPJKtj6moHgWxOF9bgiPzo4",
6
+  projectId: 'fest-6f8db',
7
+  messagingSenderId: '1058806233312',
8
+  appId: '1:1058806233312:web:ad9f595d3e47a1cec3b885',
9
+});
10
+
11
+const messaging = firebase.messaging();
12
+
13
+// Notifica in background (browser chiuso)
14
+messaging.onBackgroundMessage((payload) => {
15
+  self.registration.showNotification(payload.notification.title, {
16
+    body: payload.notification.body,
17
+    icon: '/assets/img/favicon/favicon.png',
18
+    data: payload.data,
19
+  });
20
+});

+ 788
- 0
resources/css/welcome.css Datei anzeigen

@@ -0,0 +1,788 @@
1
+/**
2
+ * Welcome — bacheca attività attive
3
+ */
4
+
5
+.welcome-bacheca {
6
+  --fest-orange: #f58220;
7
+  --fest-orange-soft: #ff9a2e;
8
+  --fest-purple: #602d91;
9
+  --fest-navy: #15265c;
10
+  --fest-text: #1a2744;
11
+  --fest-text-muted: #4a5568;
12
+  --fest-surface: #eef1f7;
13
+  --fest-card: #ffffff;
14
+  --fest-radius: 1.15rem;
15
+  --fest-shadow: 0 4px 24px rgba(21, 38, 92, 0.08), 0 1px 3px rgba(21, 38, 92, 0.06);
16
+
17
+  width: 100%;
18
+  max-width: 72rem;
19
+  margin-inline: auto;
20
+  padding: 0 0.75rem 2.5rem;
21
+  font-family: "Plus Jakarta Sans", "Public Sans", system-ui, sans-serif;
22
+  color: var(--fest-text);
23
+}
24
+
25
+.misc-wrapper.misc-wrapper--welcome-bacheca {
26
+  text-align: start;
27
+  align-items: flex-start;
28
+  justify-content: flex-start;
29
+  padding: 1rem 0 2rem;
30
+  max-width: 100%;
31
+  min-block-size: auto;
32
+  background: transparent;
33
+}
34
+
35
+html.welcome-bacheca-page,
36
+html.welcome-bacheca-page body {
37
+  background: #f4eef8;
38
+}
39
+
40
+/*
41
+ * Prova colorazione sfondo — varianti:
42
+ *   default (nessun attributo) = colorato Fest
43
+ *   data-bg="soft"  = più tenue
44
+ *   data-bg="bold"  = più saturo
45
+ */
46
+.welcome-bacheca__bg {
47
+  --bg-warm: #fff6ee;
48
+  --bg-mid: #f3ebf8;
49
+  --bg-cool: #e6edf8;
50
+
51
+  position: fixed;
52
+  inset: 0;
53
+  z-index: -1;
54
+  overflow: hidden;
55
+  pointer-events: none;
56
+  background: linear-gradient(
57
+    145deg,
58
+    var(--bg-warm) 0%,
59
+    var(--bg-mid) 38%,
60
+    var(--bg-cool) 100%
61
+  );
62
+}
63
+
64
+.welcome-bacheca__bg::before {
65
+  content: "";
66
+  position: absolute;
67
+  inset: 0;
68
+  background:
69
+    radial-gradient(ellipse 75% 55% at 8% 18%, rgba(245, 130, 32, 0.38) 0%, transparent 58%),
70
+    radial-gradient(ellipse 70% 50% at 95% 82%, rgba(96, 45, 145, 0.32) 0%, transparent 55%),
71
+    radial-gradient(ellipse 55% 40% at 55% 0%, rgba(21, 38, 92, 0.14) 0%, transparent 50%),
72
+    radial-gradient(ellipse 45% 35% at 72% 42%, rgba(245, 130, 32, 0.12) 0%, transparent 48%);
73
+  pointer-events: none;
74
+}
75
+
76
+.welcome-bacheca__bg::after {
77
+  content: "";
78
+  position: absolute;
79
+  inset: 0;
80
+  background: linear-gradient(
81
+    118deg,
82
+    rgba(245, 130, 32, 0.07) 0%,
83
+    transparent 32%,
84
+    transparent 58%,
85
+    rgba(96, 45, 145, 0.09) 78%,
86
+    rgba(21, 38, 92, 0.06) 100%
87
+  );
88
+  pointer-events: none;
89
+}
90
+
91
+.welcome-bacheca__bg[data-bg="soft"] {
92
+  --bg-warm: #faf8fc;
93
+  --bg-mid: #f4f2f8;
94
+  --bg-cool: #eef1f8;
95
+}
96
+
97
+.welcome-bacheca__bg[data-bg="soft"]::before {
98
+  opacity: 0.55;
99
+}
100
+
101
+.welcome-bacheca__bg[data-bg="bold"] {
102
+  --bg-warm: #ffe8d4;
103
+  --bg-mid: #ecd8f5;
104
+  --bg-cool: #d8e4f8;
105
+}
106
+
107
+.welcome-bacheca__bg[data-bg="bold"]::before {
108
+  background:
109
+    radial-gradient(ellipse 80% 60% at 5% 15%, rgba(245, 130, 32, 0.55) 0%, transparent 55%),
110
+    radial-gradient(ellipse 75% 55% at 98% 88%, rgba(96, 45, 145, 0.48) 0%, transparent 52%),
111
+    radial-gradient(ellipse 50% 40% at 50% 5%, rgba(21, 38, 92, 0.22) 0%, transparent 48%);
112
+}
113
+
114
+.welcome-bacheca__bg-stripe {
115
+  position: absolute;
116
+  top: 0;
117
+  left: 0;
118
+  right: 0;
119
+  height: 6px;
120
+  z-index: 2;
121
+  background: linear-gradient(
122
+    90deg,
123
+    var(--fest-orange) 0%,
124
+    var(--fest-purple) 50%,
125
+    var(--fest-navy) 100%
126
+  );
127
+  box-shadow: 0 3px 16px rgba(96, 45, 145, 0.25);
128
+}
129
+
130
+.welcome-bacheca__bg-pattern {
131
+  position: absolute;
132
+  inset: 0;
133
+  opacity: 0.55;
134
+  background-image: url("data:image/svg+xml,%3Csvg width='56' height='56' viewBox='0 0 56 56' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M28 4L52 28L28 52L4 28Z' stroke='%23f58220' stroke-opacity='.22' stroke-width='.7' fill='none'/%3E%3Cpath d='M28 12L44 28L28 44L12 28Z' stroke='%23602d91' stroke-opacity='.18' stroke-width='.55' fill='none'/%3E%3Ccircle cx='28' cy='28' r='1.35' fill='%2315265c' fill-opacity='.15'/%3E%3C/svg%3E");
135
+  background-size: 56px 56px;
136
+  mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.35) 50%, transparent 100%);
137
+}
138
+
139
+.welcome-bacheca__bg-glow {
140
+  position: absolute;
141
+  border-radius: 50%;
142
+  filter: blur(80px);
143
+  will-change: transform;
144
+  mix-blend-mode: multiply;
145
+}
146
+
147
+.welcome-bacheca__bg-glow--orange {
148
+  width: min(58vw, 520px);
149
+  height: min(58vw, 520px);
150
+  top: -14%;
151
+  right: -10%;
152
+  background: rgba(245, 130, 32, 0.45);
153
+  mix-blend-mode: normal;
154
+}
155
+
156
+.welcome-bacheca__bg-glow--purple {
157
+  width: min(54vw, 460px);
158
+  height: min(54vw, 460px);
159
+  bottom: -12%;
160
+  left: -8%;
161
+  background: rgba(96, 45, 145, 0.38);
162
+  mix-blend-mode: normal;
163
+}
164
+
165
+.welcome-bacheca__bg-glow--navy {
166
+  width: min(44vw, 400px);
167
+  height: min(44vw, 400px);
168
+  top: 42%;
169
+  left: 48%;
170
+  transform: translate(-50%, -50%);
171
+  background: rgba(21, 38, 92, 0.18);
172
+  filter: blur(100px);
173
+  mix-blend-mode: normal;
174
+}
175
+
176
+/* Masthead (logo + bacheca eventi) */
177
+.welcome-bacheca__masthead {
178
+  margin-bottom: 2rem;
179
+  padding: 1.35rem 1.35rem 1.5rem;
180
+  border-radius: calc(var(--fest-radius) + 0.15rem);
181
+  background: rgba(255, 255, 255, 0.9);
182
+  backdrop-filter: blur(14px);
183
+  border: 1px solid rgba(255, 255, 255, 0.95);
184
+  box-shadow: 0 8px 32px rgba(21, 38, 92, 0.09);
185
+}
186
+
187
+.welcome-bacheca__masthead-row {
188
+  display: flex;
189
+  flex-wrap: wrap;
190
+  align-items: center;
191
+  justify-content: space-between;
192
+  gap: 1rem;
193
+  margin-bottom: 1.35rem;
194
+  padding-bottom: 1.25rem;
195
+  border-bottom: 1px solid rgba(21, 38, 92, 0.08);
196
+}
197
+
198
+.welcome-bacheca__brand {
199
+  max-width: min(200px, 50vw);
200
+  flex-shrink: 0;
201
+}
202
+
203
+.welcome-bacheca__logo {
204
+  display: block;
205
+  width: 100%;
206
+  height: auto;
207
+  max-height: 3.25rem;
208
+  object-fit: contain;
209
+  object-position: left center;
210
+}
211
+
212
+.welcome-bacheca__masthead-actions {
213
+  display: flex;
214
+  flex-wrap: wrap;
215
+  gap: 0.5rem;
216
+}
217
+
218
+.welcome-bacheca__masthead-body {
219
+  max-width: 40rem;
220
+}
221
+
222
+.welcome-bacheca__btn {
223
+  display: inline-flex;
224
+  align-items: center;
225
+  justify-content: center;
226
+  gap: 0.35rem;
227
+  padding: 0.5rem 1rem;
228
+  font-size: 0.875rem;
229
+  font-weight: 600;
230
+  border-radius: 0.5rem;
231
+  text-decoration: none;
232
+  border: 1px solid transparent;
233
+  transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
234
+}
235
+
236
+.welcome-bacheca__btn--ghost {
237
+  color: var(--fest-navy);
238
+  background: #fff;
239
+  border-color: rgba(21, 38, 92, 0.2);
240
+}
241
+
242
+.welcome-bacheca__btn--ghost:hover {
243
+  color: var(--fest-navy);
244
+  background: #fff;
245
+  border-color: var(--fest-navy);
246
+}
247
+
248
+.welcome-bacheca__btn--primary {
249
+  color: #fff;
250
+  background: linear-gradient(135deg, var(--fest-orange) 0%, var(--fest-orange-soft) 100%);
251
+  box-shadow: 0 3px 12px rgba(245, 130, 32, 0.3);
252
+}
253
+
254
+.welcome-bacheca__btn--primary:hover {
255
+  color: #fff;
256
+  box-shadow: 0 4px 16px rgba(245, 130, 32, 0.38);
257
+}
258
+
259
+.welcome-bacheca__eyebrow {
260
+  display: inline-flex;
261
+  align-items: center;
262
+  gap: 0.45rem;
263
+  margin: 0 0 0.65rem;
264
+  font-size: 0.75rem;
265
+  font-weight: 700;
266
+  letter-spacing: 0.12em;
267
+  text-transform: uppercase;
268
+  color: var(--fest-purple);
269
+}
270
+
271
+.welcome-bacheca__live-dot {
272
+  width: 0.5rem;
273
+  height: 0.5rem;
274
+  border-radius: 50%;
275
+  background: #22c55e;
276
+  box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.25);
277
+  animation: welcome-pulse 2s ease-in-out infinite;
278
+}
279
+
280
+@keyframes welcome-pulse {
281
+  0%, 100% { opacity: 1; transform: scale(1); }
282
+  50% { opacity: 0.65; transform: scale(0.92); }
283
+}
284
+
285
+.welcome-bacheca__title {
286
+  font-size: clamp(1.35rem, 4vw, 1.85rem);
287
+  font-weight: 800;
288
+  color: var(--fest-navy);
289
+  margin: 0 0 0.5rem;
290
+  line-height: 1.15;
291
+  letter-spacing: -0.03em;
292
+}
293
+
294
+.welcome-bacheca__lead {
295
+  font-size: 1rem;
296
+  line-height: 1.6;
297
+  color: var(--fest-text-muted);
298
+  margin: 0;
299
+  max-width: 32rem;
300
+}
301
+
302
+/* Board */
303
+.welcome-bacheca__board {
304
+  margin-bottom: 1rem;
305
+}
306
+
307
+.welcome-bacheca__board-head {
308
+  display: flex;
309
+  align-items: center;
310
+  justify-content: space-between;
311
+  gap: 1rem;
312
+  margin-bottom: 1.15rem;
313
+  padding: 0 0.15rem;
314
+}
315
+
316
+.welcome-bacheca__board-title {
317
+  display: flex;
318
+  align-items: center;
319
+  gap: 0.4rem;
320
+  margin: 0;
321
+  font-size: 1rem;
322
+  font-weight: 700;
323
+  color: var(--fest-navy);
324
+}
325
+
326
+.welcome-bacheca__board-title i {
327
+  color: var(--fest-orange);
328
+  font-size: 1.2rem;
329
+}
330
+
331
+/* Grid eventi */
332
+.welcome-bacheca__grid {
333
+  display: grid;
334
+  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
335
+  gap: 1.25rem;
336
+  padding: 0;
337
+  margin: 0;
338
+}
339
+
340
+@media (min-width: 900px) {
341
+  .welcome-bacheca__grid {
342
+    grid-template-columns: repeat(2, 1fr);
343
+  }
344
+
345
+  .welcome-bacheca__card--featured {
346
+    grid-column: span 2;
347
+  }
348
+
349
+  .welcome-bacheca__card--featured .welcome-bacheca__card-cover {
350
+    min-height: 11rem;
351
+  }
352
+
353
+  .welcome-bacheca__card--featured .welcome-bacheca__card-cover-logo {
354
+    width: 5.5rem;
355
+    height: 5.5rem;
356
+  }
357
+}
358
+
359
+@media (min-width: 1200px) {
360
+  .welcome-bacheca__grid {
361
+    grid-template-columns: repeat(3, 1fr);
362
+  }
363
+
364
+  .welcome-bacheca__card--featured {
365
+    grid-column: span 2;
366
+  }
367
+}
368
+
369
+.welcome-bacheca__card {
370
+  position: relative;
371
+  min-width: 0;
372
+}
373
+
374
+.welcome-bacheca__card-hit {
375
+  display: flex;
376
+  flex-direction: column;
377
+  width: 100%;
378
+  height: 100%;
379
+  padding: 0;
380
+  text-align: left;
381
+  border: none;
382
+  border-radius: var(--fest-radius);
383
+  background: #fff;
384
+  box-shadow:
385
+    0 1px 2px rgba(21, 38, 92, 0.05),
386
+    0 10px 28px rgba(21, 38, 92, 0.08);
387
+  cursor: pointer;
388
+  overflow: hidden;
389
+  transition:
390
+    transform 0.22s cubic-bezier(0.34, 1.2, 0.64, 1),
391
+    box-shadow 0.22s ease;
392
+}
393
+
394
+.welcome-bacheca__card-hit:hover {
395
+  transform: translateY(-6px) scale(1.01);
396
+  box-shadow:
397
+    0 4px 8px rgba(21, 38, 92, 0.06),
398
+    0 20px 40px rgba(21, 38, 92, 0.14);
399
+}
400
+
401
+.welcome-bacheca__card-hit:focus-visible {
402
+  outline: 2px solid var(--fest-orange);
403
+  outline-offset: 3px;
404
+}
405
+
406
+.welcome-bacheca__card-pin {
407
+  position: absolute;
408
+  top: 0.65rem;
409
+  right: 0.75rem;
410
+  z-index: 3;
411
+  width: 2rem;
412
+  height: 2rem;
413
+  display: flex;
414
+  align-items: center;
415
+  justify-content: center;
416
+  border-radius: 50%;
417
+  background: #fff;
418
+  color: var(--fest-orange);
419
+  font-size: 1.1rem;
420
+  box-shadow: 0 4px 12px rgba(21, 38, 92, 0.15);
421
+  transform: rotate(12deg);
422
+  transition: transform 0.2s ease;
423
+}
424
+
425
+.welcome-bacheca__card-hit:hover .welcome-bacheca__card-pin {
426
+  transform: rotate(0deg) scale(1.08);
427
+}
428
+
429
+.welcome-bacheca__card-cover {
430
+  position: relative;
431
+  display: flex;
432
+  align-items: center;
433
+  justify-content: center;
434
+  min-height: 8.5rem;
435
+  background: linear-gradient(
436
+    135deg,
437
+    var(--card-accent, var(--fest-orange)) 0%,
438
+    color-mix(in srgb, var(--card-accent, var(--fest-orange)) 55%, var(--fest-purple)) 100%
439
+  );
440
+  overflow: hidden;
441
+}
442
+
443
+.welcome-bacheca__card-cover-img {
444
+  position: absolute;
445
+  inset: 0;
446
+  width: 100%;
447
+  height: 100%;
448
+  object-fit: cover;
449
+  opacity: 0.55;
450
+}
451
+
452
+.welcome-bacheca__card-cover-shade {
453
+  position: absolute;
454
+  inset: 0;
455
+  background: linear-gradient(
456
+    180deg,
457
+    rgba(21, 38, 92, 0.05) 0%,
458
+    rgba(21, 38, 92, 0.35) 100%
459
+  );
460
+}
461
+
462
+.welcome-bacheca__card-cover-logo {
463
+  position: relative;
464
+  z-index: 1;
465
+  width: 4.25rem;
466
+  height: 4.25rem;
467
+  display: flex;
468
+  align-items: center;
469
+  justify-content: center;
470
+  border-radius: 1rem;
471
+  background: #fff;
472
+  box-shadow: 0 8px 24px rgba(21, 38, 92, 0.2);
473
+  overflow: hidden;
474
+}
475
+
476
+.welcome-bacheca__card-cover-logo img {
477
+  width: 82%;
478
+  height: 82%;
479
+  object-fit: contain;
480
+}
481
+
482
+.welcome-bacheca__card-avatar {
483
+  display: flex;
484
+  align-items: center;
485
+  justify-content: center;
486
+  width: 100%;
487
+  height: 100%;
488
+  font-size: 1.1rem;
489
+  font-weight: 800;
490
+  color: #fff;
491
+  background: linear-gradient(135deg, var(--card-accent, var(--fest-orange)), var(--fest-purple));
492
+}
493
+
494
+.welcome-bacheca__card-content {
495
+  display: flex;
496
+  flex-direction: column;
497
+  flex: 1;
498
+  gap: 0.5rem;
499
+  padding: 1.1rem 1.15rem 1.2rem;
500
+}
501
+
502
+.welcome-bacheca__card-meta {
503
+  display: flex;
504
+  flex-wrap: wrap;
505
+  align-items: center;
506
+  gap: 0.4rem;
507
+}
508
+
509
+.welcome-bacheca__card-badge {
510
+  font-size: 0.65rem;
511
+  font-weight: 700;
512
+  letter-spacing: 0.05em;
513
+  text-transform: uppercase;
514
+  color: #0d7a4a;
515
+  background: rgba(13, 122, 74, 0.12);
516
+  padding: 0.2rem 0.5rem;
517
+  border-radius: 2rem;
518
+}
519
+
520
+.welcome-bacheca__card-chip {
521
+  display: inline-flex;
522
+  align-items: center;
523
+  gap: 0.25rem;
524
+  font-size: 0.72rem;
525
+  font-weight: 600;
526
+  color: var(--fest-purple);
527
+  background: rgba(96, 45, 145, 0.08);
528
+  padding: 0.2rem 0.5rem;
529
+  border-radius: 2rem;
530
+}
531
+
532
+.welcome-bacheca__card-title {
533
+  margin: 0;
534
+  font-size: 1.15rem;
535
+  font-weight: 800;
536
+  color: var(--fest-navy);
537
+  line-height: 1.25;
538
+  letter-spacing: -0.02em;
539
+}
540
+
541
+.welcome-bacheca__card-desc {
542
+  margin: 0;
543
+  font-size: 0.88rem;
544
+  line-height: 1.5;
545
+  color: var(--fest-text-muted);
546
+  flex: 1;
547
+}
548
+
549
+.welcome-bacheca__card-desc--muted {
550
+  font-style: italic;
551
+  opacity: 0.85;
552
+}
553
+
554
+.welcome-bacheca__card-cta {
555
+  display: inline-flex;
556
+  align-items: center;
557
+  gap: 0.25rem;
558
+  margin-top: 0.35rem;
559
+  font-size: 0.85rem;
560
+  font-weight: 700;
561
+  color: var(--fest-orange);
562
+  transition: gap 0.15s ease;
563
+}
564
+
565
+.welcome-bacheca__card-hit:hover .welcome-bacheca__card-cta {
566
+  gap: 0.45rem;
567
+}
568
+
569
+.welcome-bacheca__card-cta i {
570
+  font-size: 1.15rem;
571
+}
572
+
573
+/* Empty */
574
+.welcome-bacheca__empty {
575
+  text-align: center;
576
+  padding: 3.5rem 1.5rem;
577
+  background: rgba(255, 255, 255, 0.92);
578
+  border-radius: var(--fest-radius);
579
+  border: 1px dashed rgba(21, 38, 92, 0.12);
580
+  box-shadow: var(--fest-shadow);
581
+}
582
+
583
+.welcome-bacheca__empty-icon {
584
+  width: 4rem;
585
+  height: 4rem;
586
+  margin: 0 auto 1rem;
587
+  display: flex;
588
+  align-items: center;
589
+  justify-content: center;
590
+  border-radius: 50%;
591
+  background: linear-gradient(135deg, rgba(245, 130, 32, 0.15), rgba(96, 45, 145, 0.12));
592
+  color: var(--fest-purple);
593
+  font-size: 1.75rem;
594
+}
595
+
596
+.welcome-bacheca__empty-title {
597
+  font-size: 1.15rem;
598
+  font-weight: 700;
599
+  color: var(--fest-navy);
600
+  margin: 0 0 0.5rem;
601
+}
602
+
603
+.welcome-bacheca__empty-text {
604
+  color: var(--fest-text-muted);
605
+  margin: 0 0 1.25rem;
606
+  max-width: 22rem;
607
+  margin-inline: auto;
608
+}
609
+
610
+/* Footer */
611
+.welcome-bacheca__footer {
612
+  margin-top: 2.5rem;
613
+  font-size: 0.8rem;
614
+  color: #6b7280;
615
+  text-align: center;
616
+}
617
+
618
+/* Modal */
619
+.welcome-bacheca__modal {
620
+  border-radius: 1rem;
621
+  border: none;
622
+  box-shadow: 0 16px 48px rgba(21, 38, 92, 0.18);
623
+}
624
+
625
+.welcome-bacheca__modal-head {
626
+  display: flex;
627
+  align-items: center;
628
+  gap: 1rem;
629
+  flex: 1;
630
+  min-width: 0;
631
+}
632
+
633
+.welcome-bacheca__modal-media {
634
+  flex-shrink: 0;
635
+  width: 3.25rem;
636
+  height: 3.25rem;
637
+  display: flex;
638
+  align-items: center;
639
+  justify-content: center;
640
+  border-radius: 0.65rem;
641
+  background: rgba(21, 38, 92, 0.06);
642
+  overflow: hidden;
643
+}
644
+
645
+.welcome-bacheca__modal-logo {
646
+  width: 100%;
647
+  height: 100%;
648
+  object-fit: contain;
649
+  padding: 0.2rem;
650
+}
651
+
652
+.welcome-bacheca__modal-avatar {
653
+  display: flex;
654
+  align-items: center;
655
+  justify-content: center;
656
+  width: 100%;
657
+  height: 100%;
658
+  font-weight: 700;
659
+  font-size: 0.9rem;
660
+  color: #fff;
661
+  background: linear-gradient(135deg, var(--card-accent, var(--fest-orange)), var(--fest-purple));
662
+}
663
+
664
+.welcome-bacheca__modal-title {
665
+  font-size: 1.1rem;
666
+  font-weight: 700;
667
+  color: var(--fest-navy);
668
+}
669
+
670
+.welcome-bacheca__modal-desc {
671
+  font-size: 0.85rem;
672
+  color: var(--fest-text-muted);
673
+  line-height: 1.4;
674
+}
675
+
676
+.welcome-bacheca__modal-hint {
677
+  font-size: 0.8rem;
678
+  font-weight: 600;
679
+  text-transform: uppercase;
680
+  letter-spacing: 0.05em;
681
+  color: var(--fest-text-muted);
682
+  margin-bottom: 0.75rem;
683
+}
684
+
685
+.welcome-bacheca__modal-actions {
686
+  display: flex;
687
+  flex-direction: column;
688
+  gap: 0.5rem;
689
+}
690
+
691
+.welcome-bacheca__action {
692
+  display: flex;
693
+  align-items: center;
694
+  gap: 0.85rem;
695
+  width: 100%;
696
+  padding: 0.75rem 0.85rem;
697
+  text-align: left;
698
+  text-decoration: none;
699
+  color: var(--fest-text);
700
+  background: #f8f9fc;
701
+  border: 1px solid rgba(21, 38, 92, 0.08);
702
+  border-radius: 0.65rem;
703
+  transition: background 0.15s ease, border-color 0.15s ease;
704
+}
705
+
706
+button.welcome-bacheca__action {
707
+  font: inherit;
708
+}
709
+
710
+.welcome-bacheca__action:not(.is-disabled):hover {
711
+  background: #fff;
712
+  border-color: rgba(21, 38, 92, 0.16);
713
+  color: var(--fest-navy);
714
+}
715
+
716
+.welcome-bacheca__action.is-disabled {
717
+  opacity: 0.55;
718
+  cursor: not-allowed;
719
+}
720
+
721
+.welcome-bacheca__action strong {
722
+  display: block;
723
+  font-size: 0.9rem;
724
+  font-weight: 600;
725
+  color: var(--fest-navy);
726
+}
727
+
728
+.welcome-bacheca__action small {
729
+  display: block;
730
+  font-size: 0.78rem;
731
+  color: var(--fest-text-muted);
732
+  margin-top: 0.1rem;
733
+}
734
+
735
+.welcome-bacheca__action-icon {
736
+  flex-shrink: 0;
737
+  width: 2.25rem;
738
+  height: 2.25rem;
739
+  display: flex;
740
+  align-items: center;
741
+  justify-content: center;
742
+  border-radius: 0.5rem;
743
+  font-size: 1.15rem;
744
+  background: rgba(21, 38, 92, 0.08);
745
+  color: var(--fest-navy);
746
+}
747
+
748
+.welcome-bacheca__action-icon--orange {
749
+  background: rgba(245, 130, 32, 0.15);
750
+  color: #c45f00;
751
+}
752
+
753
+.welcome-bacheca__action-icon--purple {
754
+  background: rgba(96, 45, 145, 0.12);
755
+  color: var(--fest-purple);
756
+}
757
+
758
+.welcome-bacheca__action-icon--navy {
759
+  background: rgba(21, 38, 92, 0.1);
760
+  color: var(--fest-navy);
761
+}
762
+
763
+@media (max-width: 575.98px) {
764
+  .welcome-bacheca__masthead {
765
+    padding: 1.1rem 1rem 1.25rem;
766
+  }
767
+
768
+  .welcome-bacheca__masthead-row {
769
+    margin-bottom: 1rem;
770
+    padding-bottom: 1rem;
771
+  }
772
+
773
+  .welcome-bacheca__masthead-actions {
774
+    width: 100%;
775
+  }
776
+
777
+  .welcome-bacheca__masthead-actions .welcome-bacheca__btn {
778
+    flex: 1;
779
+  }
780
+
781
+  .welcome-bacheca__grid {
782
+    grid-template-columns: 1fr;
783
+  }
784
+
785
+  .welcome-bacheca__card--featured {
786
+    grid-column: span 1;
787
+  }
788
+}

+ 114
- 0
resources/js/ordine-notifica.js Datei anzeigen

@@ -0,0 +1,114 @@
1
+import { initializeApp } from 'firebase/app';
2
+import { getMessaging, getToken, isSupported, onMessage } from 'firebase/messaging';
3
+
4
+const app = initializeApp(window.firebaseConfig);
5
+
6
+let messaging = null;
7
+let onMessageRegistered = false;
8
+
9
+async function getMessagingInstance() {
10
+  if (messaging) {
11
+    return messaging;
12
+  }
13
+
14
+  if (!window.isSecureContext) {
15
+    throw new Error(
16
+      'Le notifiche richiedono HTTPS (o localhost). Apri il sito con https:// o http://fest.test sullo stesso PC.'
17
+    );
18
+  }
19
+
20
+  const supported = await isSupported();
21
+  if (!supported) {
22
+    throw new Error('Il browser non supporta le notifiche push.');
23
+  }
24
+
25
+  messaging = getMessaging(app);
26
+
27
+  return messaging;
28
+}
29
+
30
+async function chiediTokenFcm() {
31
+  if (!window.isSecureContext) {
32
+    throw new Error(
33
+      'Le notifiche richiedono HTTPS (o localhost). Apri il sito con https:// o http://fest.test sullo stesso PC.'
34
+    );
35
+  }
36
+
37
+  const supported = await isSupported();
38
+  if (!supported) {
39
+    throw new Error('Il browser non supporta le notifiche push.');
40
+  }
41
+
42
+  const permesso = await Notification.requestPermission();
43
+  if (permesso !== 'granted') {
44
+    throw new Error('Permesso notifiche negato');
45
+  }
46
+
47
+  // 1. Registra il Service Worker
48
+  const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js');
49
+
50
+  // 2. Aspetta che sia attivo (fix "no active Service Worker")
51
+  await navigator.serviceWorker.ready;
52
+
53
+  // 3. Solo ora inizializza messaging
54
+  const msg = await getMessagingInstance();
55
+
56
+  const token = await getToken(msg, {
57
+    vapidKey: window.firebaseVapidKey,
58
+    serviceWorkerRegistration: registration,
59
+  });
60
+
61
+  if (!token) {
62
+    throw new Error('Token FCM non ottenuto');
63
+  }
64
+
65
+  if (!onMessageRegistered) {
66
+    onMessage(msg, (payload) => {
67
+      const title = payload.notification?.title ?? 'Ordine pronto';
68
+      const body = payload.notification?.body ?? '';
69
+      new Notification(title, { body });
70
+    });
71
+    onMessageRegistered = true;
72
+  }
73
+
74
+  return token;
75
+}
76
+
77
+const btn = document.getElementById('btn-notificami');
78
+
79
+btn?.addEventListener('click', async () => {
80
+  try {
81
+    btn.disabled = true;
82
+
83
+    const fcmToken = await chiediTokenFcm();
84
+
85
+    const ordineId = btn.dataset.ordineId;
86
+    if (!ordineId) {
87
+      throw new Error('ID ordine non trovato nella pagina');
88
+    }
89
+
90
+    const body = new FormData();
91
+    body.append('fcm_token', fcmToken);
92
+    body.append('ordine_id', ordineId);
93
+
94
+    const res = await fetch(window.subscribeUrl, {
95
+      method: 'POST',
96
+      headers: {
97
+        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
98
+        Accept: 'application/json',
99
+      },
100
+      body,
101
+    });
102
+
103
+    const data = await res.json().catch(() => ({}));
104
+
105
+    if (!res.ok || data.success === false || data.res === false) {
106
+      throw new Error(data.message || 'Errore salvataggio token');
107
+    }
108
+
109
+    document.getElementById('msg-successo')?.classList.remove('d-none');
110
+  } catch (err) {
111
+    alert(err.message || 'Impossibile attivare le notifiche');
112
+    btn.disabled = false;
113
+  }
114
+});

+ 38
- 24
resources/menu/verticalMenu.json Datei anzeigen

@@ -34,6 +34,13 @@
34 34
       "url": "admin/bilancio/oggi",
35 35
       "can": "permission:view-bilancio-oggi"
36 36
     },
37
+    {
38
+      "name": "Tombola",
39
+      "icon": "menu-icon icon-base bx bx-trophy",
40
+      "slug": "tombola.index",
41
+      "url": "admin/tombola",
42
+      "can": "permission:view-tombola"
43
+    },
37 44
     {
38 45
       "menuHeader": "Consulta"
39 46
     },
@@ -66,7 +73,7 @@
66 73
       "can": "permission:view-evento"
67 74
     },
68 75
     {
69
-      "menuHeader": "Gestione"
76
+      "menuHeader": "Gestisci"
70 77
     },
71 78
     {
72 79
       "name": "Dispositivi",
@@ -194,7 +201,7 @@
194 201
         }
195 202
         ]},
196 203
         {
197
-      "menuHeader": "Amministrazione"
204
+      "menuHeader": "Amministra"
198 205
     },
199 206
     {
200 207
       "name": "Attività",
@@ -213,8 +220,8 @@
213 220
       "name": "Ruoli e Permessi",
214 221
       "icon": "menu-icon icon-base bx bx-check-shield",
215 222
       "slug": "role",
216
-      "can": "role:superadmin",
217
-      "url": "admin/role"
223
+      "url": "admin/role",
224
+      "can": "role:superadmin"
218 225
     },
219 226
     {
220 227
       "name": "Configurazioni",
@@ -223,20 +230,6 @@
223 230
       "url": "admin/configurazione",
224 231
       "can": "permission:view-configurazione",
225 232
       "submenu": [
226
-        {
227
-          "name": "Cucine",
228
-          "icon": "menu-icon icon-base bx bx-chef-hat",
229
-          "slug": "cucina.index",
230
-          "url": "admin/cucina",
231
-          "can": "permission:view-cucina"
232
-        },
233
-        {
234
-          "name": "Piatti",
235
-          "icon": "menu-icon icon-base bx bx-dish",
236
-          "slug": "piatto.index",
237
-          "url": "admin/piatto",
238
-          "can": "permission:view-piatto"
239
-        },
240 233
         {
241 234
           "name": "Allergeni",
242 235
           "icon": "menu-icon icon-base bx bx-error-circle",
@@ -244,19 +237,33 @@
244 237
           "url": "admin/allergene",
245 238
           "can": "permission:view-allergene"
246 239
         },
240
+        {
241
+          "name": "Categorie contabili",
242
+          "icon": "menu-icon icon-base bx bx-category",
243
+          "slug": "categoria_contabile.index",
244
+          "url": "admin/categoria-contabile",
245
+          "can": "permission:view-categoria_contabile"
246
+        },
247
+        {
248
+          "name": "Cucine",
249
+          "icon": "menu-icon icon-base bx bx-chef-hat",
250
+          "slug": "cucina.index",
251
+          "url": "admin/cucina",
252
+          "can": "permission:view-cucina"
253
+        },
247 254
         {
248 255
           "name": "Fornitori",
249 256
           "icon": "menu-icon icon-base bx bxs-truck",
250 257
           "slug": "fornitore.index",
251 258
           "url": "admin/fornitore",
252 259
           "can": "permission:view-fornitore"
253
-        },
260
+        },  
254 261
         {
255
-          "name": "Categorie contabili",
256
-          "icon": "menu-icon icon-base bx bx-category",
257
-          "slug": "categoria_contabile.index",
258
-          "url": "admin/categoria-contabile",
259
-          "can": "permission:view-categoria_contabile"
262
+          "name": "Operatori",
263
+          "icon": "menu-icon icon-base bx bx-user",
264
+          "slug": "operatore.index",
265
+          "url": "admin/operatore",
266
+          "can": "permission:view-operatore"
260 267
         },
261 268
         {
262 269
           "name": "Metodi di pagamento",
@@ -265,6 +272,13 @@
265 272
           "url": "admin/metodo-pagamento",
266 273
           "can": "permission:view-metodo_pagamento"
267 274
         },
275
+        {
276
+          "name": "Piatti",
277
+          "icon": "menu-icon icon-base bx bx-dish",
278
+          "slug": "piatto.index",
279
+          "url": "admin/piatto",
280
+          "can": "permission:view-piatto"
281
+        },
268 282
         {
269 283
           "name": "Testi",
270 284
           "icon": "menu-icon icon-base bx bx-text",

+ 11
- 0
resources/views/_partials/status.blade.php Datei anzeigen

@@ -10,6 +10,17 @@
10 10
         </div>
11 11
         @endif
12 12
 
13
+        @if(isset($error) || session('error'))
14
+        <div class="alert alert-solid-danger alert-dismissible fade show d-flex align-items-center status-alert" role="alert">
15
+          <span class="alert-icon rounded-circle">
16
+            <i class="bx bx-xs bx-error"></i>
17
+          </span>
18
+          {{ $error ?? session('error') }}
19
+          <button type="button" class="btn-close ms-auto" data-bs-dismiss="alert" aria-label="Chiudi"></button>
20
+        </div>
21
+        @endif
22
+
23
+
13 24
         @if(isset($secondary) || session('secondary'))
14 25
         <div class="alert alert-solid-secondary alert-dismissible fade show d-flex align-items-center status-alert" role="alert">
15 26
           <span class="alert-icon rounded-circle">

+ 509
- 0
resources/views/attivita/public/show.blade.php Datei anzeigen

@@ -0,0 +1,509 @@
1
+@php
2
+  use Illuminate\Support\Facades\Route;
3
+  use Carbon\Carbon;
4
+
5
+  $accent = $attivita?->colore ?: '#f58220';
6
+  $logoUrl = $attivita?->logoUrl();
7
+  $coverUrl = $attivita?->coverUrl();
8
+  $festLogoUrl = asset('assets/img/logo_fest_L.png');
9
+  $eventi = $attivita
10
+    ? $attivita->eventi()->orderByDesc('data_inizio')->get()
11
+    : collect();
12
+  $saltacodaAttivo = $attivita
13
+    && \App\Models\Saltacoda::where('attivita_id', $attivita->id)->where('is_attivo', true)->exists();
14
+  $backUrl = Route::has('welcome') ? route('welcome') : url('/');
15
+@endphp
16
+
17
+@extends('layouts/guest')
18
+
19
+@section('title', $attivita ? $attivita->nome : 'Attività')
20
+
21
+@section('page-meta')
22
+  <link rel="preconnect" href="https://fonts.googleapis.com" />
23
+  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
24
+  <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800&display=swap" rel="stylesheet" />
25
+@endsection
26
+
27
+@section('app-css-extra')
28
+  @vite(['resources/css/guest-client.css'])
29
+@endsection
30
+
31
+@section('page-style')
32
+<style>
33
+  html:has(.attivita-public-shell),
34
+  html:has(.attivita-public-shell) body {
35
+    height: 100%;
36
+    margin: 0;
37
+  }
38
+
39
+  .layout-wrapper:has(.attivita-public-shell) .content-wrapper,
40
+  .layout-wrapper:has(.attivita-public-shell) .layout-page,
41
+  .layout-wrapper:has(.attivita-public-shell) .container-xxl {
42
+    padding: 0 !important;
43
+    max-width: 100% !important;
44
+  }
45
+
46
+  .layout-wrapper:has(.attivita-public-shell) .misc-wrapper {
47
+    min-height: 100vh;
48
+    min-height: 100dvh;
49
+    width: 100%;
50
+    max-width: 100%;
51
+    padding: 0;
52
+    margin: 0;
53
+    align-items: stretch;
54
+    justify-content: stretch;
55
+    text-align: start;
56
+  }
57
+
58
+  .layout-wrapper:has(.attivita-public-shell) .row.mt-3.mb-3 {
59
+    display: none;
60
+  }
61
+
62
+  .layout-wrapper:has(.attivita-public-shell) footer {
63
+    display: none;
64
+  }
65
+
66
+  .attivita-public-shell {
67
+    --card-accent: {{ $accent }};
68
+    display: flex;
69
+    flex-direction: column;
70
+    width: 100%;
71
+    min-height: 100vh;
72
+    min-height: 100dvh;
73
+    font-family: "Plus Jakarta Sans", "Public Sans", system-ui, sans-serif;
74
+    background: #f4f6fb;
75
+  }
76
+
77
+  .attivita-public-shell__top {
78
+    flex-shrink: 0;
79
+    padding: 0.85rem 1.25rem;
80
+    background: #fff;
81
+    border-bottom: 1px solid rgba(21, 38, 92, 0.08);
82
+  }
83
+
84
+  .attivita-public__back {
85
+    display: inline-flex;
86
+    align-items: center;
87
+    gap: 0.35rem;
88
+    font-size: 0.875rem;
89
+    font-weight: 600;
90
+    color: #602d91;
91
+    text-decoration: none;
92
+  }
93
+
94
+  .attivita-public__back:hover {
95
+    color: #4a2270;
96
+  }
97
+
98
+  .attivita-public__grid {
99
+    flex: 1;
100
+    display: grid;
101
+    grid-template-columns: minmax(280px, 42%) 1fr;
102
+    min-height: 0;
103
+    width: 100%;
104
+  }
105
+
106
+  /* Colonna sinistra — logo */
107
+  .attivita-public__col-logo {
108
+    position: relative;
109
+    display: flex;
110
+    align-items: center;
111
+    justify-content: center;
112
+    padding: 2.5rem 2rem;
113
+    background: linear-gradient(
114
+      145deg,
115
+      color-mix(in srgb, var(--card-accent) 18%, #fff) 0%,
116
+      color-mix(in srgb, var(--card-accent) 8%, #f0f2f8) 100%
117
+    );
118
+    border-right: 1px solid rgba(21, 38, 92, 0.08);
119
+    overflow: hidden;
120
+  }
121
+
122
+  .attivita-public__col-logo-bg {
123
+    position: absolute;
124
+    inset: 0;
125
+    width: 100%;
126
+    height: 100%;
127
+    object-fit: cover;
128
+    opacity: 0.22;
129
+  }
130
+
131
+  .attivita-public__col-logo-shade {
132
+    position: absolute;
133
+    inset: 0;
134
+    background: linear-gradient(
135
+      160deg,
136
+      rgba(255, 255, 255, 0.75) 0%,
137
+      rgba(255, 255, 255, 0.35) 100%
138
+    );
139
+  }
140
+
141
+  .attivita-public__logo-panel {
142
+    position: relative;
143
+    z-index: 1;
144
+    width: min(100%, 320px);
145
+    padding: 1.5rem;
146
+    display: flex;
147
+    align-items: center;
148
+    justify-content: center;
149
+    background: #fff;
150
+    border-radius: 1.15rem;
151
+    box-shadow: 0 12px 40px rgba(21, 38, 92, 0.12);
152
+    border: 1px solid rgba(255, 255, 255, 0.9);
153
+  }
154
+
155
+  .attivita-public__logo-panel img {
156
+    display: block;
157
+    width: 100%;
158
+    height: auto;
159
+    max-height: min(40vh, 220px);
160
+    object-fit: contain;
161
+  }
162
+
163
+  .attivita-public__logo-panel--fest img {
164
+    max-height: min(36vh, 120px);
165
+  }
166
+
167
+  /* Colonna destra — contenuto */
168
+  .attivita-public__col-main {
169
+    display: flex;
170
+    flex-direction: column;
171
+    min-height: 0;
172
+    overflow-y: auto;
173
+    padding: 2rem clamp(1.25rem, 4vw, 3rem);
174
+    background: #fff;
175
+  }
176
+
177
+  .attivita-public__badge {
178
+    display: inline-block;
179
+    font-size: 0.65rem;
180
+    font-weight: 700;
181
+    letter-spacing: 0.06em;
182
+    text-transform: uppercase;
183
+    color: #0d7a4a;
184
+    background: rgba(13, 122, 74, 0.12);
185
+    padding: 0.2rem 0.5rem;
186
+    border-radius: 2rem;
187
+    margin-bottom: 0.65rem;
188
+  }
189
+
190
+  .attivita-public__title {
191
+    margin: 0 0 0.5rem;
192
+    font-size: clamp(1.5rem, 3.5vw, 2.25rem);
193
+    font-weight: 800;
194
+    color: #15265c;
195
+    letter-spacing: -0.03em;
196
+    line-height: 1.15;
197
+  }
198
+
199
+  .attivita-public__tipo {
200
+    display: inline-block;
201
+    font-size: 0.85rem;
202
+    font-weight: 600;
203
+    color: #602d91;
204
+    margin-bottom: 1rem;
205
+  }
206
+
207
+  .attivita-public__desc {
208
+    font-size: 1rem;
209
+    line-height: 1.65;
210
+    color: #4a5568;
211
+    margin: 0 0 1rem;
212
+    max-width: 40rem;
213
+  }
214
+
215
+  .attivita-public__info {
216
+    font-size: 0.9rem;
217
+    line-height: 1.55;
218
+    color: #4a5568;
219
+    margin: 0 0 1.25rem;
220
+    max-width: 40rem;
221
+    padding: 0.85rem 1rem;
222
+    border-radius: 0.65rem;
223
+    background: #f8f9fc;
224
+    border: 1px solid rgba(21, 38, 92, 0.06);
225
+  }
226
+
227
+  .attivita-public__section-title {
228
+    font-size: 0.75rem;
229
+    font-weight: 700;
230
+    text-transform: uppercase;
231
+    letter-spacing: 0.08em;
232
+    color: #6b7280;
233
+    margin: 0 0 0.75rem;
234
+  }
235
+
236
+  .attivita-public__actions {
237
+    display: flex;
238
+    flex-direction: column;
239
+    gap: 0.5rem;
240
+    max-width: 28rem;
241
+  }
242
+
243
+  .attivita-public__action {
244
+    display: flex;
245
+    align-items: center;
246
+    gap: 0.85rem;
247
+    padding: 0.9rem 1rem;
248
+    border-radius: 0.75rem;
249
+    text-decoration: none;
250
+    color: #1a2744;
251
+    background: #f8f9fc;
252
+    border: 1px solid rgba(21, 38, 92, 0.08);
253
+    transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
254
+  }
255
+
256
+  a.attivita-public__action:hover {
257
+    background: #fff;
258
+    border-color: rgba(21, 38, 92, 0.14);
259
+    color: #15265c;
260
+    transform: translateY(-1px);
261
+  }
262
+
263
+  .attivita-public__action.is-disabled {
264
+    opacity: 0.5;
265
+    pointer-events: none;
266
+  }
267
+
268
+  .attivita-public__action-icon {
269
+    flex-shrink: 0;
270
+    width: 2.35rem;
271
+    height: 2.35rem;
272
+    display: flex;
273
+    align-items: center;
274
+    justify-content: center;
275
+    border-radius: 0.55rem;
276
+    font-size: 1.2rem;
277
+    background: rgba(21, 38, 92, 0.08);
278
+    color: #15265c;
279
+  }
280
+
281
+  .attivita-public__action-icon--orange {
282
+    background: rgba(245, 130, 32, 0.15);
283
+    color: #c45f00;
284
+  }
285
+
286
+  .attivita-public__action strong {
287
+    display: block;
288
+    font-size: 0.9rem;
289
+    font-weight: 700;
290
+    color: #15265c;
291
+  }
292
+
293
+  .attivita-public__action small {
294
+    display: block;
295
+    font-size: 0.78rem;
296
+    color: #6b7280;
297
+    margin-top: 0.1rem;
298
+  }
299
+
300
+  .attivita-public__eventi-block {
301
+    margin-top: 2rem;
302
+    max-width: 32rem;
303
+  }
304
+
305
+  .attivita-public__eventi {
306
+    display: flex;
307
+    flex-direction: column;
308
+    gap: 0.5rem;
309
+    list-style: none;
310
+    padding: 0;
311
+    margin: 0;
312
+  }
313
+
314
+  .attivita-public__evento {
315
+    display: flex;
316
+    align-items: center;
317
+    justify-content: space-between;
318
+    gap: 0.75rem;
319
+    padding: 0.85rem 1rem;
320
+    border-radius: 0.75rem;
321
+    background: #f8f9fc;
322
+    border: 1px solid rgba(21, 38, 92, 0.08);
323
+    text-decoration: none;
324
+    color: inherit;
325
+    transition: border-color 0.15s ease, box-shadow 0.15s ease;
326
+  }
327
+
328
+  .attivita-public__evento:hover {
329
+    border-color: rgba(96, 45, 145, 0.25);
330
+    box-shadow: 0 4px 14px rgba(21, 38, 92, 0.06);
331
+    color: inherit;
332
+  }
333
+
334
+  .attivita-public__evento-name {
335
+    font-weight: 700;
336
+    font-size: 0.95rem;
337
+    color: #15265c;
338
+    margin: 0 0 0.15rem;
339
+  }
340
+
341
+  .attivita-public__evento-date {
342
+    font-size: 0.8rem;
343
+    color: #6b7280;
344
+    margin: 0;
345
+  }
346
+
347
+  .attivita-public__empty {
348
+    flex: 1;
349
+    display: flex;
350
+    align-items: center;
351
+    justify-content: center;
352
+    padding: 2rem;
353
+    text-align: center;
354
+  }
355
+
356
+  @media (max-width: 767.98px) {
357
+    .attivita-public__grid {
358
+      grid-template-columns: 1fr;
359
+      grid-template-rows: auto 1fr;
360
+    }
361
+
362
+    .attivita-public__col-logo {
363
+      min-height: 12rem;
364
+      padding: 1.75rem 1.25rem;
365
+      border-right: none;
366
+      border-bottom: 1px solid rgba(21, 38, 92, 0.08);
367
+    }
368
+
369
+    .attivita-public__logo-panel {
370
+      width: min(100%, 240px);
371
+      padding: 1rem;
372
+    }
373
+
374
+    .attivita-public__col-main {
375
+      padding: 1.5rem 1.25rem 2rem;
376
+    }
377
+  }
378
+</style>
379
+@endsection
380
+
381
+@section('content')
382
+<div class="attivita-public-shell">
383
+  <header class="attivita-public-shell__top">
384
+    <a href="{{ $backUrl }}" class="attivita-public__back">
385
+      <i class="bx bx-arrow-back"></i>
386
+      Bacheca eventi
387
+    </a>
388
+  </header>
389
+
390
+  @if(!$attivita)
391
+    <div class="attivita-public__empty">
392
+      <div>
393
+        <i class="bx bx-error-circle bx-lg text-muted mb-2 d-block"></i>
394
+        <h1 class="h5 fw-bold mb-2">Attività non trovata</h1>
395
+        <p class="text-muted mb-3">La scheda richiesta non esiste o non è più disponibile.</p>
396
+        <a href="{{ $backUrl }}" class="btn btn-primary">Torna alla bacheca</a>
397
+      </div>
398
+    </div>
399
+  @else
400
+    <div class="attivita-public__grid">
401
+      <aside class="attivita-public__col-logo" aria-label="Logo">
402
+        @if($coverUrl)
403
+          <img src="{{ $coverUrl }}" alt="" class="attivita-public__col-logo-bg" loading="lazy">
404
+        @endif
405
+        <div class="attivita-public__col-logo-shade" aria-hidden="true"></div>
406
+        <div class="attivita-public__logo-panel {{ $logoUrl ? '' : 'attivita-public__logo-panel--fest' }}">
407
+          @if($logoUrl)
408
+            <img
409
+              src="{{ $logoUrl }}"
410
+              alt="{{ $attivita->nome }}"
411
+              loading="lazy"
412
+              onerror="this.src='{{ $festLogoUrl }}'; this.alt='{{ config('app.name') }}';"
413
+            >
414
+          @else
415
+            <img src="{{ $festLogoUrl }}" alt="{{ config('app.name') }}">
416
+          @endif
417
+        </div>
418
+      </aside>
419
+
420
+      <main class="attivita-public__col-main">
421
+        @if($attivita->is_attiva)
422
+          <span class="attivita-public__badge">In programma</span>
423
+        @endif
424
+
425
+        <h1 class="attivita-public__title">{{ $attivita->nome }}</h1>
426
+
427
+        @if(filled($attivita->tipo))
428
+          <span class="attivita-public__tipo">{{ $attivita->tipo }}</span>
429
+        @endif
430
+
431
+        @if(filled($attivita->descrizione))
432
+          <p class="attivita-public__desc">{{ $attivita->descrizione }}</p>
433
+        @endif
434
+
435
+        @if(filled($attivita->info))
436
+          <div class="attivita-public__info">{!! nl2br(e($attivita->info)) !!}</div>
437
+        @endif
438
+
439
+        <section aria-labelledby="servizi-title">
440
+          <h2 id="servizi-title" class="attivita-public__section-title">Servizi</h2>
441
+          <div class="attivita-public__actions">
442
+            @if($saltacodaAttivo && Route::has('cliente.saltacoda.show'))
443
+              <a
444
+                href="{{ route('cliente.saltacoda.show', ['attivita_id' => $attivita->id]) }}"
445
+                class="attivita-public__action"
446
+              >
447
+                <span class="attivita-public__action-icon attivita-public__action-icon--orange">
448
+                  <i class="bx bx-food-menu"></i>
449
+                </span>
450
+                <span>
451
+                  <strong>Ordina (saltacoda)</strong>
452
+                  <small>Menu e ordine dal telefono</small>
453
+                </span>
454
+              </a>
455
+            @endif
456
+
457
+            <span class="attivita-public__action is-disabled" aria-disabled="true">
458
+              <span class="attivita-public__action-icon">
459
+                <i class="bx bx-calendar-check"></i>
460
+              </span>
461
+              <span>
462
+                <strong>Prenotazioni</strong>
463
+                <small>Prossimamente</small>
464
+              </span>
465
+            </span>
466
+          </div>
467
+        </section>
468
+
469
+        @if($eventi->isNotEmpty())
470
+          <section class="attivita-public__eventi-block" aria-labelledby="eventi-title">
471
+            <h2 id="eventi-title" class="attivita-public__section-title">Eventi</h2>
472
+            <ul class="attivita-public__eventi">
473
+              @foreach($eventi as $evento)
474
+                <li>
475
+                  @if(Route::has('evento.show'))
476
+                    <a href="{{ route('evento.show', ['evento_id' => $evento->id]) }}" class="attivita-public__evento">
477
+                  @else
478
+                    <div class="attivita-public__evento">
479
+                  @endif
480
+                    <div>
481
+                      <p class="attivita-public__evento-name">{{ $evento->nome }}</p>
482
+                      @if($evento->data_inizio)
483
+                        <p class="attivita-public__evento-date">
484
+                          <i class="bx bx-calendar me-1"></i>
485
+                          {{ Carbon::parse($evento->data_inizio)->locale('it')->isoFormat('D MMMM YYYY') }}
486
+                          @if($evento->data_fine && $evento->data_fine != $evento->data_inizio)
487
+                            – {{ Carbon::parse($evento->data_fine)->locale('it')->isoFormat('D MMMM YYYY') }}
488
+                          @endif
489
+                        </p>
490
+                      @endif
491
+                    </div>
492
+                    @if(Route::has('evento.show'))
493
+                      <i class="bx bx-chevron-right" aria-hidden="true"></i>
494
+                    @endif
495
+                  @if(Route::has('evento.show'))
496
+                    </a>
497
+                  @else
498
+                    </div>
499
+                  @endif
500
+                </li>
501
+              @endforeach
502
+            </ul>
503
+          </section>
504
+        @endif
505
+      </main>
506
+    </div>
507
+  @endif
508
+</div>
509
+@endsection

+ 1
- 1
resources/views/bacheca/index.blade.php Datei anzeigen

@@ -39,7 +39,7 @@ $configData = Helper::appClasses();
39 39
 @section('pageTitle')
40 40
 <div class="d-flex flex-column">
41 41
   <h4 class="mb-1"> 
42
-    <i class="bx bx-book"></i> Bacheche <span class="text-muted text-primary fs-6 fw-normal"> di {{ Attivita::find(session()->get('attivita_attuale'))->nome }}</span></h4>
42
+    <i class="bx bx-book"></i> Bacheche 
43 43
 </div>
44 44
 @endsection
45 45
 

+ 98
- 0
resources/views/bilancio/_partials/costi.blade.php Datei anzeigen

@@ -0,0 +1,98 @@
1
+<div class="tab-pane fade" id="bilancio-tab-costi" role="tabpanel">
2
+  <div class="row g-3">
3
+    <div class="col-6">
4
+      <div class="card border">
5
+        <div class="card-header pb-2">
6
+          <h6 class="mb-0">Tabella Costi</h6>
7
+        </div>
8
+        <div class="card-body">
9
+          <div class="table-responsive">
10
+            <table class="table table-sm table-striped align-middle mb-0">
11
+              <thead>
12
+                <tr>
13
+                  <th>Categoria contabile</th>
14
+                  <th class="text-end">Movimenti</th>
15
+                  <th class="text-end">Totale costi</th>
16
+                </tr>
17
+              </thead>
18
+              <tbody>
19
+                @forelse($costiRows as $row)
20
+                  <tr>
21
+                    <td>
22
+                      @if(!empty($row['colore']))
23
+                        <span class="badge me-1" style="background-color: {{ $row['colore'] }};">&nbsp;</span>
24
+                      @endif
25
+                      {{ $row['voce'] ?? 'N/D' }}
26
+                    </td>
27
+                    <td class="text-end">{{ (int)($row['movimenti'] ?? 0) }}</td>
28
+                    <td class="text-end text-danger">€ {{ number_format((float)($row['totale'] ?? 0), 2, ',', '.') }}</td>
29
+                  </tr>
30
+                @empty
31
+                  <tr>
32
+                    <td colspan="3" class="text-center text-muted py-3">Nessun costo rilevato nell'anno selezionato.</td>
33
+                  </tr>
34
+                @endforelse
35
+              </tbody>
36
+            </table>
37
+          </div>
38
+        </div>
39
+      </div>
40
+    </div>
41
+
42
+    <div class="col-12 col-xl-6">
43
+      <div class="card border h-100">
44
+        <div class="card-header pb-2">
45
+          <h6 class="mb-0">Trend costi mensili</h6>
46
+        </div>
47
+        <div class="card-body">
48
+          <div id="bilancio-costi-chart" style="min-height: 230px;"></div>
49
+          <p class="text-muted small mt-2 mb-0">
50
+            Mostra l'andamento dei costi mese per mese: picchi alti indicano periodi con spese maggiori.
51
+          </p>
52
+        </div>
53
+      </div>
54
+    </div>
55
+
56
+    <div class="col-12 col-xl-6">
57
+      <div class="card border h-100">
58
+        <div class="card-header pb-2">
59
+          <h6 class="mb-0">Top categorie costi</h6>
60
+        </div>
61
+        <div class="card-body">
62
+          <div id="bilancio-costi-categorie-chart" style="min-height: 260px;"></div>
63
+          <p class="text-muted small mt-2 mb-0">
64
+            Confronta le categorie: le barre piu lunghe sono le voci che pesano di piu sul totale costi.
65
+          </p>
66
+        </div>
67
+      </div>
68
+    </div>
69
+
70
+    <div class="col-12 col-xl-6">
71
+      <div class="card border h-100">
72
+        <div class="card-header pb-2">
73
+          <h6 class="mb-0">Composizione costi (%)</h6>
74
+        </div>
75
+        <div class="card-body">
76
+          <div id="bilancio-costi-percent-chart" style="min-height: 320px;"></div>
77
+          <p class="text-muted small mt-2 mb-0">
78
+            Distribuzione percentuale dei costi: ogni fetta rappresenta la quota di una categoria sul totale.
79
+          </p>
80
+        </div>
81
+      </div>
82
+    </div>
83
+
84
+    <div class="col-12 col-xl-6">
85
+      <div class="card border h-100">
86
+        <div class="card-header pb-2">
87
+          <h6 class="mb-0">Pareto costi (80/20)</h6>
88
+        </div>
89
+        <div class="card-body">
90
+          <div id="bilancio-costi-pareto-chart" style="min-height: 320px;"></div>
91
+          <p class="text-muted small mt-2 mb-0">
92
+            Pareto 80/20: le barre mostrano i valori, la linea indica la percentuale cumulata per capire quali categorie coprono gran parte dei costi.
93
+          </p>
94
+        </div>
95
+      </div>
96
+    </div>
97
+  </div>
98
+</div>

+ 98
- 0
resources/views/bilancio/_partials/ricavi.blade.php Datei anzeigen

@@ -0,0 +1,98 @@
1
+<div class="tab-pane fade" id="bilancio-tab-ricavi" role="tabpanel">
2
+  <div class="row g-3">
3
+    <div class="col-6">
4
+      <div class="card border">
5
+        <div class="card-header pb-2">
6
+          <h6 class="mb-0">Tabella Ricavi</h6>
7
+        </div>
8
+        <div class="card-body">
9
+          <div class="table-responsive">
10
+            <table class="table table-sm table-striped align-middle mb-0">
11
+              <thead>
12
+                <tr>
13
+                  <th>Categoria contabile</th>
14
+                  <th class="text-end">N.</th>
15
+                  <th class="text-end">Totale ricavi</th>
16
+                </tr>
17
+              </thead>
18
+              <tbody>
19
+                @forelse($ricaviRows as $row)
20
+                  <tr>
21
+                    <td>
22
+                      @if(!empty($row['colore']))
23
+                        <span class="badge me-1" style="background-color: {{ $row['colore'] }};">&nbsp;</span>
24
+                      @endif
25
+                      {{ $row['voce'] ?? 'N/D' }}
26
+                    </td>
27
+                    <td class="text-end">{{ (int)($row['movimenti'] ?? 0) }}</td>
28
+                    <td class="text-end text-success">€ {{ number_format((float)($row['totale'] ?? 0), 2, ',', '.') }}</td>
29
+                  </tr>
30
+                @empty
31
+                  <tr>
32
+                    <td colspan="3" class="text-center text-muted py-3">Nessun ricavo rilevato nell'anno selezionato.</td>
33
+                  </tr>
34
+                @endforelse
35
+              </tbody>
36
+            </table>
37
+          </div>
38
+        </div>
39
+      </div>
40
+    </div>
41
+
42
+    <div class="col-12 col-xl-6">
43
+      <div class="card border h-100">
44
+        <div class="card-header pb-2">
45
+          <h6 class="mb-0">Trend ricavi mensili</h6>
46
+        </div>
47
+        <div class="card-body">
48
+          <div id="bilancio-ricavi-chart" style="min-height: 230px;"></div>
49
+          <p class="text-muted small mt-2 mb-0">
50
+            Mostra l'andamento dei ricavi mese per mese: una curva in crescita indica un miglioramento delle entrate.
51
+          </p>
52
+        </div>
53
+      </div>
54
+    </div>
55
+
56
+    <div class="col-12 col-xl-6">
57
+      <div class="card border h-100">
58
+        <div class="card-header pb-2">
59
+          <h6 class="mb-0">Top categorie ricavi</h6>
60
+        </div>
61
+        <div class="card-body">
62
+          <div id="bilancio-ricavi-categorie-chart" style="min-height: 260px;"></div>
63
+          <p class="text-muted small mt-2 mb-0">
64
+            Confronta le categorie che generano ricavi: le barre piu lunghe sono le voci con maggior impatto.
65
+          </p>
66
+        </div>
67
+      </div>
68
+    </div>
69
+
70
+    <div class="col-12 col-xl-6">
71
+      <div class="card border h-100">
72
+        <div class="card-header pb-2">
73
+          <h6 class="mb-0">Composizione ricavi (%)</h6>
74
+        </div>
75
+        <div class="card-body">
76
+          <div id="bilancio-ricavi-percent-chart" style="min-height: 320px;"></div>
77
+          <p class="text-muted small mt-2 mb-0">
78
+            Distribuzione percentuale dei ricavi: ogni fetta indica quanto contribuisce una categoria al totale.
79
+          </p>
80
+        </div>
81
+      </div>
82
+    </div>
83
+
84
+    <div class="col-12 col-xl-6">
85
+      <div class="card border h-100">
86
+        <div class="card-header pb-2">
87
+          <h6 class="mb-0">Pareto ricavi (80/20)</h6>
88
+        </div>
89
+        <div class="card-body">
90
+          <div id="bilancio-ricavi-pareto-chart" style="min-height: 320px;"></div>
91
+          <p class="text-muted small mt-2 mb-0">
92
+            Pareto 80/20: individua rapidamente quali categorie coprono la maggior parte dei ricavi.
93
+          </p>
94
+        </div>
95
+      </div>
96
+    </div>
97
+  </div>
98
+</div>

+ 132
- 0
resources/views/bilancio/_partials/sintesi.blade.php Datei anzeigen

@@ -0,0 +1,132 @@
1
+@php
2
+  $andamento = collect($andamentoMensile ?? []);
3
+  $ricavi = collect($ricaviRows ?? []);
4
+  $costi = collect($costiRows ?? []);
5
+
6
+  $lastMonth = $andamento->last();
7
+  $prevMonth = $andamento->slice(-2, 1)->first();
8
+
9
+  $lastSaldo = (float) ($lastMonth['saldo'] ?? 0);
10
+  $prevSaldo = (float) ($prevMonth['saldo'] ?? 0);
11
+  $saldoDelta = $lastSaldo - $prevSaldo;
12
+
13
+  $saldoTrendClass = $saldoDelta >= 0 ? 'text-success' : 'text-danger';
14
+  $saldoTrendIcon = $saldoDelta >= 0 ? 'bx-chevron-up' : 'bx-chevron-down';
15
+
16
+  $topRicavo = $ricavi->first();
17
+  $topCosto = $costi->first();
18
+  $bestMonth = $andamento->sortByDesc('saldo')->first();
19
+@endphp
20
+
21
+<div class="tab-pane fade show active" id="bilancio-tab-sintesi" role="tabpanel">
22
+  <div class="row g-3 mb-3">
23
+    <div class="col-12 col-md-6 col-xl-3">
24
+      <div class="card card-border-shadow-primary h-100">
25
+        <div class="card-body">
26
+          <div class="d-flex align-items-center justify-content-between">
27
+            <div class="content-left">
28
+              <span class="text-heading">Saldo annuale</span>
29
+              <h5 class="mb-0 mt-1 {{ ($saldoTotale ?? 0) >= 0 ? 'text-success' : 'text-danger' }}">
30
+                € {{ number_format((float)($saldoTotale ?? 0), 2, ',', '.') }}
31
+              </h5>
32
+              <small class="{{ $saldoTrendClass }}">
33
+                <i class="bx {{ $saldoTrendIcon }}"></i>
34
+                {{ number_format(abs($saldoDelta), 2, ',', '.') }} vs mese precedente
35
+              </small>
36
+            </div>
37
+            <span class="avatar-initial rounded bg-label-primary">
38
+              <i class="bx bx-line-chart bx-lg"></i>
39
+            </span>
40
+          </div>
41
+        </div>
42
+      </div>
43
+    </div>
44
+
45
+    <div class="col-12 col-md-6 col-xl-3">
46
+      <div class="card card-border-shadow-success h-100">
47
+        <div class="card-body">
48
+          <div class="d-flex align-items-center justify-content-between">
49
+            <div class="content-left">
50
+              <span class="text-heading">Ricavi totali</span>
51
+              <h5 class="mb-0 mt-1 text-success">
52
+                € {{ number_format((float)($ricaviTotali ?? 0), 2, ',', '.') }}
53
+              </h5>
54
+              <small>{{ (int)($movimentiCount ?? 0) }} movimenti registrati</small>
55
+            </div>
56
+            <span class="avatar-initial rounded bg-label-success">
57
+              <i class="bx bx-trending-up bx-lg"></i>
58
+            </span>
59
+          </div>
60
+        </div>
61
+      </div>
62
+    </div>
63
+
64
+    <div class="col-12 col-md-6 col-xl-3">
65
+      <div class="card card-border-shadow-danger h-100">
66
+        <div class="card-body">
67
+          <div class="d-flex align-items-center justify-content-between">
68
+            <div class="content-left">
69
+              <span class="text-heading">Costi totali</span>
70
+              <h5 class="mb-0 mt-1 text-danger">
71
+                € {{ number_format((float)($costiTotali ?? 0), 2, ',', '.') }}
72
+              </h5>
73
+              <small>Top costo: {{ $topCosto['voce'] ?? 'N/D' }}</small>
74
+            </div>
75
+            <span class="avatar-initial rounded bg-label-danger">
76
+              <i class="bx bx-trending-down bx-lg"></i>
77
+            </span>
78
+          </div>
79
+        </div>
80
+      </div>
81
+    </div>
82
+
83
+    <div class="col-12 col-md-6 col-xl-3">
84
+      <div class="card card-border-shadow-info h-100">
85
+        <div class="card-body">
86
+          <div class="d-flex align-items-center justify-content-between">
87
+            <div class="content-left">
88
+              <span class="text-heading">Insight rapido</span>
89
+              <h6 class="mb-0 mt-1">
90
+                {{ !empty($bestMonth['mese']) ? 'Mese migliore: ' . $bestMonth['mese'] : 'Mese migliore non disponibile' }}
91
+              </h6>
92
+              <small>Top ricavo: {{ $topRicavo['voce'] ?? 'N/D' }}</small>
93
+            </div>
94
+            <span class="avatar-initial rounded bg-label-info">
95
+              <i class="bx bx-bulb bx-lg"></i>
96
+            </span>
97
+          </div>
98
+        </div>
99
+      </div>
100
+    </div>
101
+  </div>
102
+
103
+  <h6 class="mb-2">Tabella Bilancio</h6>
104
+  <div class="table-responsive">
105
+    <table class="table table-sm table-bordered align-middle mb-0">
106
+      <thead>
107
+        <tr>
108
+          <th class="text-end">Totale ricavi</th>
109
+          <th class="text-center" style="width: 40px;">-</th>
110
+          <th class="text-end">Totale costi</th>
111
+          <th class="text-center" style="width: 40px;">=</th>
112
+          <th class="text-end">Saldo totale</th>
113
+        </tr>
114
+      </thead>
115
+      <tbody>
116
+        <tr>
117
+          <td class="text-end fw-semibold">€ {{ number_format((float)($ricaviTotali ?? 0), 2, ',', '.') }}</td>
118
+          <td class="text-center fw-semibold">-</td>
119
+          <td class="text-end fw-semibold">€ {{ number_format((float)($costiTotali ?? 0), 2, ',', '.') }}</td>
120
+          <td class="text-center fw-semibold">=</td>
121
+          <td class="text-end fw-bold {{ (($saldoTotale ?? 0) >= 0) ? 'text-primary' : 'text-warning' }}">
122
+            € {{ number_format((float)($saldoTotale ?? 0), 2, ',', '.') }}
123
+          </td>
124
+        </tr>
125
+      </tbody>
126
+    </table>
127
+  </div>
128
+  <div id="bilancio-saldo-col-chart" class="mt-3" style="min-height: 280px;"></div>
129
+  <p class="text-muted small mt-2 mb-0">
130
+    Come leggere: le colonne mostrano ricavi e costi per mese, la linea mostra il saldo (ricavi - costi). Se la linea sale, il margine migliora.
131
+  </p>
132
+</div>

+ 136
- 0
resources/views/bilancio/_partials/statistiche.blade.php Datei anzeigen

@@ -0,0 +1,136 @@
1
+@php
2
+  $andamento = collect($andamentoMensile ?? []);
3
+  $previsione = $previsioneProssimoMese ?? ['ricavi' => 0, 'costi' => 0, 'saldo' => 0, 'mesi_considerati' => 0];
4
+  $categoriaPerf = collect($categoriaPerformance ?? [])->take(10)->values();
5
+  $piattiPerf = collect($piattiPerformance ?? [])->take(10)->values();
6
+  $cucinePerf = collect($cucinePerformance ?? [])->take(10)->values();
7
+  $pagamentiPerf = collect($pagamentiPerformance ?? [])->take(10)->values();
8
+  $ticketMedio = (float) ($ticketMedioPagamenti ?? 0);
9
+  $pagamentiAnno = (int) ($pagamentiCountAnno ?? 0);
10
+@endphp
11
+
12
+<div class="tab-pane fade" id="bilancio-tab-statistiche" role="tabpanel">
13
+  <div class="mb-3">
14
+    <h6 class="mb-1">Panoramica e Previsioni</h6>
15
+    <p class="text-muted small mb-0">Riassume lo stato economico medio e una stima del prossimo mese. [Fonte: Prima Nota e Pagamenti].</p>
16
+  </div>
17
+  <div class="row g-3 mb-4">
18
+    <div class="col-12 col-md-6 col-xl-4">
19
+      <div class="card card-border-shadow-warning h-100">
20
+        <div class="card-body">
21
+          <span class="text-heading">Previsione prossimo mese</span>
22
+          <h5 class="mb-0 mt-2 {{ ($previsione['saldo'] ?? 0) >= 0 ? 'text-success' : 'text-danger' }}">
23
+            Saldo stimato: € {{ number_format((float)($previsione['saldo'] ?? 0), 2, ',', '.') }}
24
+          </h5>
25
+          <small class="d-block mt-1">Ricavi stimati: € {{ number_format((float)($previsione['ricavi'] ?? 0), 2, ',', '.') }}</small>
26
+          <small class="d-block">Costi stimati: € {{ number_format((float)($previsione['costi'] ?? 0), 2, ',', '.') }}</small>
27
+          <small class="text-warning d-block mt-2">
28
+            Stima indicativa basata sugli ultimi {{ (int)($previsione['mesi_considerati'] ?? 0) }} mesi: non fare affidamento esclusivo su questi valori.
29
+          </small>
30
+        </div>
31
+      </div>
32
+    </div>
33
+    <div class="col-12 col-md-6 col-xl-4">
34
+      <div class="card card-border-shadow-primary h-100">
35
+        <div class="card-body">
36
+          <span class="text-heading">Ticket medio</span>
37
+          <h5 class="mb-0 mt-2 text-primary">€ {{ number_format($ticketMedio, 2, ',', '.') }}</h5>
38
+          <small class="d-block mt-1">Calcolato su {{ $pagamentiAnno }} pagamenti dell'anno selezionato.</small>
39
+          <small class="text-muted d-block mt-2">Indica il valore medio di ogni pagamento emesso.</small>
40
+        </div>
41
+      </div>
42
+    </div>
43
+    <div class="col-12 col-md-12 col-xl-4">
44
+      <div class="card h-100">
45
+        <div class="card-header d-flex justify-content-between align-items-center">
46
+          <h5 class="card-title m-0">Performance mensile</h5>
47
+          <small class="text-muted">Ricavi / Costi / Saldo</small>
48
+        </div>
49
+        <div class="card-body">
50
+          <div id="bilancio-stat-performance-chart" style="min-height: 320px;"></div>
51
+          <p class="text-muted small mb-0">Andamento mensile delle metriche principali.</p>
52
+        </div>
53
+      </div>
54
+    </div>
55
+  </div>
56
+
57
+  <div class="mb-3">
58
+    <h6 class="mb-1">Affluenza</h6>
59
+    <p class="text-muted small mb-0">Descrive l'affluenza della clientela per orari e giorni. [Fonte: emissione pagamenti].</p>
60
+  </div>
61
+  <div class="row g-3 mb-4">
62
+    <div class="col-12 col-xl-6">
63
+      <div class="card h-100">
64
+        <div class="card-header">
65
+          <h6 class="m-0">Heatmap affluenza (pagamenti)</h6>
66
+        </div>
67
+        <div class="card-body">
68
+          <div id="bilancio-stat-affluenza-heatmap" style="min-height: 320px;"></div>
69
+          <p class="text-muted small mb-0">Righe = giorni, colonne = ore: evidenzia quando si concentra la domanda.</p>
70
+        </div>
71
+      </div>
72
+    </div>
73
+    <div class="col-12 col-xl-6">
74
+      <div class="card h-100">
75
+        <div class="card-header">
76
+          <h6 class="m-0">Affluenza per giorno (pagamenti)</h6>
77
+        </div>
78
+        <div class="card-body">
79
+          <div id="bilancio-stat-affluenza-giorni-chart" style="min-height: 320px;"></div>
80
+          <p class="text-muted small mb-0">Confronto rapido dei giorni con maggiore volume di pagamenti.</p>
81
+        </div>
82
+      </div>
83
+    </div>
84
+  </div>
85
+
86
+  <div class="mb-3">
87
+    <h6 class="mb-1">Performance Operativa</h6>
88
+    <p class="text-muted small mb-0">Mostra quali categorie, piatti, cucine e metodi di pagamento incidono di più sui risultati.</p>
89
+  </div>
90
+  <div class="row g-3">
91
+    <div class="col-12 col-xl-6">
92
+      <div class="card h-100">
93
+        <div class="card-header">
94
+          <h6 class="m-0">Performance categorie contabili</h6>
95
+        </div>
96
+        <div class="card-body">
97
+          <div id="bilancio-stat-categoria-chart" style="min-height: 320px;"></div>
98
+          <p class="text-muted small mb-0">Saldo per categoria (ricavi - costi): evidenzia le categorie che aiutano o penalizzano il risultato.</p>
99
+        </div>
100
+      </div>
101
+    </div>
102
+    <div class="col-12 col-xl-6">
103
+      <div class="card h-100">
104
+        <div class="card-header">
105
+          <h6 class="m-0">Performance piatti</h6>
106
+        </div>
107
+        <div class="card-body">
108
+          <div id="bilancio-stat-piatti-chart" style="min-height: 320px;"></div>
109
+          <p class="text-muted small mb-0">Top piatti per incasso annuale (quantita x prezzo).</p>
110
+        </div>
111
+      </div>
112
+    </div>
113
+    <div class="col-12 col-xl-6">
114
+      <div class="card h-100">
115
+        <div class="card-header">
116
+          <h6 class="m-0">Performance cucine</h6>
117
+        </div>
118
+        <div class="card-body">
119
+          <div id="bilancio-stat-cucine-chart" style="min-height: 320px;"></div>
120
+          <p class="text-muted small mb-0">Contributo delle cucine ai ricavi complessivi.</p>
121
+        </div>
122
+      </div>
123
+    </div>
124
+    <div class="col-12 col-xl-6">
125
+      <div class="card h-100">
126
+        <div class="card-header">
127
+          <h6 class="m-0">Performance pagamenti</h6>
128
+        </div>
129
+        <div class="card-body">
130
+          <div id="bilancio-stat-pagamenti-chart" style="min-height: 320px;"></div>
131
+          <p class="text-muted small mb-0">Metodi di pagamento con impatto maggiore sugli incassi.</p>
132
+        </div>
133
+      </div>
134
+    </div>
135
+  </div>
136
+</div>

+ 1054
- 181
resources/views/bilancio/index.blade.php
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 8
- 0
resources/views/categoria_contabile/menu.blade.php Datei anzeigen

@@ -1,13 +1,21 @@
1
+@php
2
+use Illuminate\Support\Facades\Auth;
3
+@endphp
4
+
1 5
 <div class="dropdown">
2 6
     <button type="button" class="btn p-0 dropdown-toggle hide-arrow" data-bs-toggle="dropdown" aria-expanded="false">
3 7
         <i class="bx bx-dots-vertical-rounded fs-large"></i>
4 8
     </button>
5 9
     <div class="dropdown-menu">
10
+        @if(Auth::user()->can('edit-categoria_contabile'))
6 11
         <a href="#" class="dropdown-item editor_edit">
7 12
             <i class="bx bx-edit-alt me-1"></i> Modifica
8 13
         </a>
14
+        @endif
15
+        @if(Auth::user()->can('delete-categoria_contabile'))
9 16
         <a href="#" class="dropdown-item editor_delete">
10 17
             <i class="bx bx-trash me-1"></i> Elimina
11 18
         </a>
19
+        @endif
12 20
     </div>
13 21
 </div>

+ 22
- 1
resources/views/consulta/scontrino/show.blade.php Datei anzeigen

@@ -116,7 +116,13 @@
116 116
               </span>
117 117
             </span>
118 118
           </div>
119
-        </div>
119
+
120
+          <button type="button" id="btn-notificami" class="btn btn-primary btn-lg w-100" data-ordine-id="{{ $ordine->id }}">
121
+    <i class="bx bx-bell"></i> Avvisami quando è pronto
122
+  </button>
123
+  <div id="msg-successo" class="alert alert-success d-none mt-3">
124
+    Notifiche attive — puoi chiudere questa pagina.
125
+  </div>        </div>
120 126
 
121 127
         <div class="d-flex justify-content-center mt-4">
122 128
           <a href="{{ url()->previous() }}" class="btn btn-label-secondary">
@@ -128,3 +134,18 @@
128 134
   </div>
129 135
 </div>
130 136
 @endsection
137
+
138
+@section('page-script')
139
+<script>
140
+  window.firebaseConfig = {
141
+    apiKey: @json(config('services.firebase.web_api_key')),
142
+    projectId: @json(config('services.firebase.project_id')),
143
+    messagingSenderId: @json(config('services.firebase.messaging_sender_id')),
144
+    appId: @json(config('services.firebase.app_id')),
145
+  };
146
+  window.firebaseVapidKey = @json(config('services.firebase.vapid_key'));
147
+  window.subscribeUrl = @json(route('riga-ordine-notifica.salva-fcm-token'));
148
+</script>
149
+@vite(['resources/js/ordine-notifica.js'])
150
+@endsection
151
+

+ 64
- 0
resources/views/endpoint/_partials/copy-product-key-script.blade.php Datei anzeigen

@@ -0,0 +1,64 @@
1
+<script>
2
+  function copyProductKey(btn) {
3
+    const input = document.getElementById('inputProductKey');
4
+    const productKey = (input && (input.value || input.textContent) || '').trim();
5
+    const copyBtn = btn || document.getElementById('btnCopyProductKey');
6
+
7
+    if (!productKey) {
8
+      alert('Nessun product key da copiare.');
9
+      return;
10
+    }
11
+
12
+    function onSuccess() {
13
+      if (!copyBtn) {
14
+        return;
15
+      }
16
+      const icon = copyBtn.querySelector('i');
17
+      if (!icon) {
18
+        return;
19
+      }
20
+      const prevClass = icon.className;
21
+      icon.className = 'bx bx-check';
22
+      copyBtn.classList.remove('btn-outline-primary');
23
+      copyBtn.classList.add('btn-success');
24
+      copyBtn.setAttribute('title', 'Copiato!');
25
+      setTimeout(function () {
26
+        icon.className = prevClass;
27
+        copyBtn.classList.remove('btn-success');
28
+        copyBtn.classList.add('btn-outline-primary');
29
+        copyBtn.setAttribute('title', 'Copia negli appunti');
30
+      }, 2000);
31
+    }
32
+
33
+    function fallbackCopy() {
34
+      if (!input) {
35
+        return false;
36
+      }
37
+      input.removeAttribute('readonly');
38
+      input.removeAttribute('disabled');
39
+      input.focus();
40
+      input.select();
41
+      input.setSelectionRange(0, productKey.length);
42
+      const ok = document.execCommand('copy');
43
+      input.setAttribute('readonly', 'readonly');
44
+      return ok;
45
+    }
46
+
47
+    if (navigator.clipboard && window.isSecureContext) {
48
+      navigator.clipboard.writeText(productKey).then(onSuccess).catch(function () {
49
+        if (fallbackCopy()) {
50
+          onSuccess();
51
+        } else {
52
+          alert('Errore nella copia. Seleziona il testo e usa Ctrl+C.');
53
+        }
54
+      });
55
+      return;
56
+    }
57
+
58
+    if (fallbackCopy()) {
59
+      onSuccess();
60
+    } else {
61
+      alert('Copia non supportata. Seleziona il testo nel campo e usa Ctrl+C.');
62
+    }
63
+  }
64
+</script>

+ 9
- 0
resources/views/endpoint/_partials/status.blade.php Datei anzeigen

@@ -0,0 +1,9 @@
1
+@php
2
+    $status = $status ?? null;
3
+    $status = \App\Models\Endpoint::getStati()[$status];
4
+@endphp
5
+
6
+<span class="badge {{ $status['class'] }}">
7
+    <i class="bx {{ $status['icon'] }}"></i>
8
+    {{ $status['label'] }}
9
+</span>

+ 108
- 0
resources/views/endpoint/index.blade.php Datei anzeigen

@@ -66,6 +66,27 @@ $configData = Helper::appClasses();
66 66
   </div>
67 67
 </div>
68 68
 
69
+
70
+<!-- Modal -->
71
+<div class="modal fade" id="basicModal" tabindex="-1" aria-hidden="true">
72
+            <div class="modal-dialog" role="document">
73
+              <div class="modal-content">
74
+                <div class="modal-header">
75
+                  <h5 class="modal-title" id="exampleModalLabel1">Modal title</h5>
76
+                  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
77
+                </div>
78
+                <div class="modal-body">
79
+                  
80
+                </div>
81
+                <div class="modal-footer">
82
+                  <button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">Chiudi</button>
83
+                </div>
84
+              </div>
85
+            </div>
86
+          </div>
87
+        </div>
88
+      </div>
89
+
69 90
 @endsection
70 91
 
71 92
 @section('page-script')
@@ -108,10 +129,97 @@ $configData = Helper::appClasses();
108 129
 <script>
109 130
   function initComplete_endpoint(){
110 131
     $('div.dt-buttons button').removeClass('btn-secondary');
132
+    $('div.dt-buttons').removeClass('dt-buttons');
111 133
     $('div.dt-search').addClass('mt-0 mb-4');
112 134
     return;
113 135
   }
114 136
 
115 137
 
138
+  function richiediNuovoProductKey(){
139
+    $('#basicModal .modal-dialog').removeClass('modal-xl');
140
+    const url = "{{ route('endpoint.richiedi-nuovo-product-key' , ['attivita_id' => session()->get('attivita_attuale')]) }}";
141
+    $('#basicModal .modal-title').text('Nuovo Product Key generato');
142
+    $('#basicModal .modal-body').html('<div class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></div>');
143
+    $('#basicModal').modal('show');
144
+
145
+    $('#basicModal .modal-body').load(url, function (response, status) {
146
+      if (status !== 'success') {
147
+        $('#basicModal .modal-body').html('<div class="alert alert-danger mb-0">Errore nel caricamento del product key.</div>');
148
+      }
149
+    });
150
+
151
+    $('#dataTable_endpoint').DataTable().ajax.reload();
152
+  }
153
+
154
+  function copyProductKey(btn) {
155
+    const input = document.getElementById('inputProductKey');
156
+    const productKey = (input && (input.value || input.textContent) || '').trim();
157
+    const copyBtn = btn || document.getElementById('btnCopyProductKey');
158
+
159
+    if (!productKey) {
160
+      alert('Nessun product key da copiare. Attendi il caricamento o rigenera la chiave.');
161
+      return;
162
+    }
163
+
164
+    function onSuccess() {
165
+      if (!copyBtn) {
166
+        return;
167
+      }
168
+      const icon = copyBtn.querySelector('i');
169
+      if (!icon) {
170
+        return;
171
+      }
172
+      const prevClass = icon.className;
173
+      icon.className = 'bx bx-check';
174
+      copyBtn.classList.remove('btn-outline-primary');
175
+      copyBtn.classList.add('btn-success');
176
+      copyBtn.setAttribute('title', 'Copiato!');
177
+      setTimeout(function () {
178
+        icon.className = prevClass;
179
+        copyBtn.classList.remove('btn-success');
180
+        copyBtn.classList.add('btn-outline-primary');
181
+        copyBtn.setAttribute('title', 'Copia negli appunti');
182
+      }, 2000);
183
+    }
184
+
185
+    function fallbackCopy() {
186
+      if (!input) {
187
+        return false;
188
+      }
189
+      input.removeAttribute('readonly');
190
+      input.focus();
191
+      input.select();
192
+      input.setSelectionRange(0, productKey.length);
193
+      const ok = document.execCommand('copy');
194
+      input.setAttribute('readonly', 'readonly');
195
+      return ok;
196
+    }
197
+
198
+    if (navigator.clipboard && window.isSecureContext) {
199
+      navigator.clipboard.writeText(productKey).then(onSuccess).catch(function () {
200
+        if (fallbackCopy()) {
201
+          onSuccess();
202
+        } else {
203
+          alert('Errore nella copia. Seleziona il testo e copia manualmente (Ctrl+C).');
204
+        }
205
+      });
206
+      return;
207
+    }
208
+
209
+    if (fallbackCopy()) {
210
+      onSuccess();
211
+    } else {
212
+      alert('Copia non supportata. Seleziona il testo nel campo e usa Ctrl+C.');
213
+    }
214
+  }
215
+
216
+
217
+  function istruzioni(){
218
+    $('#basicModal .modal-dialog').addClass('modal-xl');
219
+    $('#basicModal .modal-title').html('<i class="bx bx-book-open me-1"></i> FestAgent — Guida installazione');
220
+    $('#basicModal .modal-body').html('<div class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></div>');
221
+    $('#basicModal').modal('show');
222
+    $('#basicModal .modal-body').load("{{ route('endpoint.istruzioni') }}");
223
+  }
116 224
 </script>
117 225
 @endsection

+ 294
- 0
resources/views/endpoint/istruzioni.blade.php Datei anzeigen

@@ -0,0 +1,294 @@
1
+@php
2
+  use App\Models\Endpoint;
3
+
4
+  $legendaStati = [
5
+    Endpoint::NON_REGISTRATO => 'Hai generato il product key da FEST, ma FestAgent su quel PC non ha ancora finito la registrazione.',
6
+    Endpoint::REGISTRATO => 'Il PC si è collegato con il codice. Manca ancora la configurazione (nome postazione, stampanti, avvio worker).',
7
+    Endpoint::ATTIVO => 'Tutto configurato: FestAgent è in esecuzione e le stampe da FEST possono arrivare alle stampanti.',
8
+    Endpoint::DISABILITATO => 'Collegamento sospeso da chi gestisce FEST. Le stampe non partono finché non viene riattivato.',
9
+    Endpoint::ELIMINATO => 'Postazione non più usata. Non compare tra quelle utilizzabili per le stampe.',
10
+  ];
11
+@endphp
12
+
13
+<style>
14
+  .endpoint-istruzioni .istruzioni-intro {
15
+    background: #fff;
16
+    border: 1px solid var(--bs-border-color);
17
+    border-radius: 0.5rem;
18
+    box-shadow: 0 1px 3px rgba(67, 89, 113, 0.06);
19
+  }
20
+  .endpoint-istruzioni .istruzioni-box {
21
+    background: #fff;
22
+    border: 1px solid var(--bs-border-color);
23
+    border-radius: 0.375rem;
24
+  }
25
+  .endpoint-istruzioni .istruzioni-box--attenzione {
26
+    background: #fff;
27
+    border: 1px solid rgba(var(--bs-danger-rgb), 0.35);
28
+    border-radius: 0.375rem;
29
+  }
30
+  .endpoint-istruzioni .istruzioni-passo-num {
31
+    width: 2rem;
32
+    height: 2rem;
33
+    font-size: 0.85rem;
34
+    font-weight: 600;
35
+  }
36
+  .endpoint-istruzioni .accordion-item {
37
+    border: 1px solid var(--bs-border-color);
38
+    border-radius: 0.375rem !important;
39
+    overflow: hidden;
40
+    margin-bottom: 0.5rem;
41
+  }
42
+  .endpoint-istruzioni .accordion-button {
43
+    font-weight: 600;
44
+    font-size: 0.95rem;
45
+    padding: 0.85rem 1rem;
46
+    box-shadow: none;
47
+  }
48
+  .endpoint-istruzioni .accordion-button:not(.collapsed) {
49
+    background: rgba(var(--bs-primary-rgb), 0.06);
50
+    color: var(--bs-primary);
51
+  }
52
+  .endpoint-istruzioni .accordion-button--danger:not(.collapsed) {
53
+    background: rgba(var(--bs-danger-rgb), 0.06);
54
+    color: var(--bs-danger);
55
+  }
56
+  .endpoint-istruzioni .accordion-body {
57
+    padding: 0.75rem 1rem 1rem;
58
+    background: #fafbfc;
59
+  }
60
+</style>
61
+
62
+<div class="endpoint-istruzioni">
63
+
64
+  <div class="istruzioni-intro d-flex align-items-start gap-3 p-4 mb-3">
65
+    <div class="avatar avatar-md flex-shrink-0">
66
+      <span class="avatar-initial rounded bg-label-primary">
67
+        <i class="bx bx-printer bx-sm"></i>
68
+      </span>
69
+    </div>
70
+    <div>
71
+      <h6 class="mb-2 text-primary">Cos’è FestAgent?</h6>
72
+      <p class="mb-2 text-body">
73
+        È il programma che tieni acceso sul PC dove sono collegate le stampanti.
74
+        Quando ordini o stampi da FEST, <strong>è lui che manda il lavoro alle stampanti giuste</strong>.
75
+      </p>
76
+      <p class="mb-0 text-muted" style="font-size: 0.9rem;">
77
+        Lo installi su <strong>Windows</strong> o <strong>Linux</strong>, una volta per ogni postazione (cassa, cucina, bar…).
78
+      </p>
79
+    </div>
80
+  </div>
81
+
82
+  <p class="small text-muted mb-3">
83
+    <i class="bx bx-chevron-down me-1"></i> Apri la sezione che ti serve:
84
+  </p>
85
+
86
+  <div class="accordion" id="accordionIstruzioniFestAgent">
87
+
88
+    {{-- 1. Prima di iniziare --}}
89
+    <div class="accordion-item">
90
+      <h2 class="accordion-header">
91
+        <button
92
+          class="accordion-button"
93
+          type="button"
94
+          data-bs-toggle="collapse"
95
+          data-bs-target="#istruzioniPrima"
96
+          aria-expanded="true"
97
+        >
98
+          <i class="bx bx-list-check text-primary me-2"></i>
99
+          Prima di iniziare
100
+        </button>
101
+      </h2>
102
+      <div id="istruzioniPrima" class="accordion-collapse collapse show" data-bs-parent="#accordionIstruzioniFestAgent">
103
+        <div class="accordion-body">
104
+          <div class="d-flex flex-column gap-2">
105
+            <div class="istruzioni-box d-flex align-items-start gap-3 p-3">
106
+              <span class="avatar avatar-sm flex-shrink-0 mt-1">
107
+                <span class="avatar-initial rounded bg-label-success"><i class="bx bx-printer"></i></span>
108
+              </span>
109
+              <div>
110
+                <p class="mb-1 fw-medium text-body">Stampanti pronte sul PC</p>
111
+                <p class="mb-0 text-body" style="font-size: 0.925rem;">
112
+                  Sul computer che userai per FestAgent le stampanti devono essere già installate e funzionanti.
113
+                  Fai una <strong>stampa di prova</strong> da Windows o Linux prima di aprire il programma.
114
+                </p>
115
+              </div>
116
+            </div>
117
+            <div class="istruzioni-box d-flex align-items-start gap-3 p-3">
118
+              <span class="avatar avatar-sm flex-shrink-0 mt-1">
119
+                <span class="avatar-initial rounded bg-label-warning"><i class="bx bx-key"></i></span>
120
+              </span>
121
+              <div>
122
+                <p class="mb-1 fw-medium text-body">Codice da FEST (product key)</p>
123
+                <p class="mb-0 text-body" style="font-size: 0.925rem;">
124
+                  Su questa pagina clicca <strong>Richiedi nuovo product key</strong>, copia il codice con l’icona
125
+                  <i class="bx bx-copy text-primary"></i> e tienilo pronto per incollarlo in FestAgent.
126
+                </p>
127
+              </div>
128
+            </div>
129
+          </div>
130
+        </div>
131
+      </div>
132
+    </div>
133
+
134
+    {{-- 2. Passo per passo --}}
135
+    <div class="accordion-item">
136
+      <h2 class="accordion-header">
137
+        <button
138
+          class="accordion-button collapsed"
139
+          type="button"
140
+          data-bs-toggle="collapse"
141
+          data-bs-target="#istruzioniPassi"
142
+        >
143
+          <i class="bx bx-walk text-primary me-2"></i>
144
+          Cosa fare, passo per passo
145
+        </button>
146
+      </h2>
147
+      <div id="istruzioniPassi" class="accordion-collapse collapse" data-bs-parent="#accordionIstruzioniFestAgent">
148
+        <div class="accordion-body">
149
+          <div class="d-flex flex-column gap-2">
150
+            <div class="istruzioni-box d-flex align-items-start gap-3 p-3">
151
+              <span class="istruzioni-passo-num badge rounded-pill bg-primary d-flex align-items-center justify-content-center flex-shrink-0 mt-1">1</span>
152
+              <div>
153
+                <p class="mb-1 fw-medium text-body">Apri FestAgent</p>
154
+                <p class="mb-0 text-body" style="font-size: 0.925rem;">
155
+                  Installa il programma sul PC delle stampanti (se non l’hai già fatto) e avvialo.
156
+                </p>
157
+              </div>
158
+            </div>
159
+            <div class="istruzioni-box d-flex align-items-start gap-3 p-3">
160
+              <span class="istruzioni-passo-num badge rounded-pill bg-primary d-flex align-items-center justify-content-center flex-shrink-0 mt-1">2</span>
161
+              <div>
162
+                <p class="mb-1 fw-medium text-body">Incolla il product key</p>
163
+                <p class="mb-0 text-body" style="font-size: 0.925rem;">
164
+                  Scrivi o incolla il codice che hai copiato da FEST e conferma.
165
+                </p>
166
+              </div>
167
+            </div>
168
+            <div class="istruzioni-box d-flex align-items-start gap-3 p-3">
169
+              <span class="istruzioni-passo-num badge rounded-pill bg-primary d-flex align-items-center justify-content-center flex-shrink-0 mt-1">3</span>
170
+              <div>
171
+                <p class="mb-1 fw-medium text-body">Dai un nome alla postazione</p>
172
+                <p class="mb-0 text-body" style="font-size: 0.925rem;">
173
+                  Se il codice è corretto, FestAgent ti chiede <strong>nome e dati</strong> per riconoscere questo PC (es. «Cassa 1», «Cucina»).
174
+                </p>
175
+              </div>
176
+            </div>
177
+            <div class="istruzioni-box d-flex align-items-start gap-3 p-3">
178
+              <span class="istruzioni-passo-num badge rounded-pill bg-primary d-flex align-items-center justify-content-center flex-shrink-0 mt-1">4</span>
179
+              <div>
180
+                <p class="mb-1 fw-medium text-body">Scegli le stampanti</p>
181
+                <p class="mb-0 text-body" style="font-size: 0.925rem;">
182
+                  Spunta quelle che il server deve usare. Puoi aggiungere un’<strong>etichetta</strong> per ritrovarle subito (es. «Bar»).
183
+                </p>
184
+              </div>
185
+            </div>
186
+            <div class="istruzioni-box d-flex align-items-start gap-3 p-3">
187
+              <span class="istruzioni-passo-num badge rounded-pill bg-primary d-flex align-items-center justify-content-center flex-shrink-0 mt-1">5</span>
188
+              <div>
189
+                <p class="mb-1 fw-medium text-body">Registra e avvia</p>
190
+                <p class="mb-0 text-body" style="font-size: 0.925rem;">
191
+                  Clicca <strong>Registra stampanti</strong>, poi <strong>Avvia worker</strong>.
192
+                </p>
193
+              </div>
194
+            </div>
195
+            <div class="istruzioni-box d-flex align-items-start gap-3 p-3 mb-0">
196
+              <span class="avatar avatar-sm flex-shrink-0 mt-1">
197
+                <span class="avatar-initial rounded bg-label-primary"><i class="bx bx-check-circle"></i></span>
198
+              </span>
199
+              <div>
200
+                <p class="mb-1 fw-medium text-body">Controllo in FEST</p>
201
+                <p class="mb-0 text-body" style="font-size: 0.925rem;">
202
+                  Torna su questa pagina Endpoint: verifica stampanti e stato attivo, poi fai una <strong>stampa di prova</strong>.
203
+                </p>
204
+              </div>
205
+            </div>
206
+          </div>
207
+        </div>
208
+      </div>
209
+    </div>
210
+
211
+    {{-- 3. Legenda stati --}}
212
+    <div class="accordion-item">
213
+      <h2 class="accordion-header">
214
+        <button
215
+          class="accordion-button collapsed"
216
+          type="button"
217
+          data-bs-toggle="collapse"
218
+          data-bs-target="#istruzioniStati"
219
+        >
220
+          <i class="bx bx-info-circle text-primary me-2"></i>
221
+          Legenda stati endpoint
222
+        </button>
223
+      </h2>
224
+      <div id="istruzioniStati" class="accordion-collapse collapse" data-bs-parent="#accordionIstruzioniFestAgent">
225
+        <div class="accordion-body">
226
+          <p class="small text-muted mb-3">
227
+            Nella tabella Endpoint la colonna <strong>Status</strong> indica a che punto è ogni collegamento:
228
+          </p>
229
+          <div class="d-flex flex-column gap-2">
230
+            @foreach(Endpoint::getStati() as $chiave => $stato)
231
+              @if(isset($legendaStati[$chiave]))
232
+                <div class="istruzioni-box d-flex align-items-start gap-3 p-3">
233
+                  <span class="badge {{ $stato['class'] }} flex-shrink-0 mt-1">
234
+                    <i class="{{ $stato['icon'] }}"></i>
235
+                    {{ $stato['label'] }}
236
+                  </span>
237
+                  <p class="mb-0 text-body" style="font-size: 0.925rem;">
238
+                    {{ $legendaStati[$chiave] }}
239
+                  </p>
240
+                </div>
241
+              @endif
242
+            @endforeach
243
+          </div>
244
+          <p class="small text-muted mb-0 mt-3">
245
+            <i class="bx bx-bulb me-1"></i>
246
+            Obiettivo: portare ogni postazione allo stato <span class="badge bg-label-success text-success">Attivo</span>.
247
+          </p>
248
+        </div>
249
+      </div>
250
+    </div>
251
+
252
+    {{-- 4. Cosa non fare --}}
253
+    <div class="accordion-item">
254
+      <h2 class="accordion-header">
255
+        <button
256
+          class="accordion-button accordion-button--danger collapsed"
257
+          type="button"
258
+          data-bs-toggle="collapse"
259
+          data-bs-target="#istruzioniAttenzione"
260
+        >
261
+          <i class="bx bx-error-circle text-danger me-2"></i>
262
+          Cosa non fare
263
+        </button>
264
+      </h2>
265
+      <div id="istruzioniAttenzione" class="accordion-collapse collapse" data-bs-parent="#accordionIstruzioniFestAgent">
266
+        <div class="accordion-body">
267
+          <div class="istruzioni-box--attenzione p-3 mb-0">
268
+            <div class="d-flex align-items-start gap-3 mb-3">
269
+              <span class="avatar avatar-sm flex-shrink-0">
270
+                <span class="avatar-initial rounded bg-label-danger"><i class="bx bx-power-off"></i></span>
271
+              </span>
272
+              <div>
273
+                <p class="mb-2 fw-medium text-body">Sul PC dove gira FestAgent</p>
274
+                <ul class="mb-0 ps-3 text-body" style="font-size: 0.925rem;">
275
+                  <li class="mb-2">non spegnerlo;</li>
276
+                  <li class="mb-2">non metterlo in sospensione o in standby;</li>
277
+                  <li>non staccarlo dalla corrente e non togliere la connessione di rete.</li>
278
+                </ul>
279
+              </div>
280
+            </div>
281
+            <div class="d-flex align-items-center gap-2 pt-2 border-top" style="border-color: rgba(var(--bs-danger-rgb), 0.2) !important;">
282
+              <i class="bx bx-printer text-danger"></i>
283
+              <p class="mb-0 text-danger fw-medium" style="font-size: 0.925rem;">
284
+                Se lo fai, non stampa nulla.
285
+              </p>
286
+            </div>
287
+          </div>
288
+        </div>
289
+      </div>
290
+    </div>
291
+
292
+  </div>
293
+
294
+</div>

+ 4
- 4
resources/views/endpoint/menu.blade.php Datei anzeigen

@@ -8,17 +8,17 @@ use Carbon\Carbon;
8 8
         <i class="bx bx-dots-vertical-rounded fs-large"></i>
9 9
     </button>
10 10
     <div class="dropdown-menu">
11
-        @if(Auth::user()->can('view-bacheca'))
12
-        <a href="{{ route('bacheca.show', ['bacheca_id' => $entity->id]) }}" class="dropdown-item">
11
+        @if(Auth::user()->can('view-endpoint'))
12
+        <a href="{{ route('endpoint.show', ['endpoint_id' => $entity->id]) }}" class="dropdown-item">
13 13
             <i class="bx bx-book me-1"></i> Vedi
14 14
         </a>
15 15
         @endif
16
-        @if(Auth::user()->can('edit-bacheca'))
16
+        @if(Auth::user()->can('edit-endpoint'))
17 17
         <a href="#" class="dropdown-item editor_edit">
18 18
             <i class="bx bx-edit-alt me-1"></i> Modifica
19 19
         </a>
20 20
         @endif
21
-        @if(Auth::user()->can('delete-bacheca'))
21
+        @if(Auth::user()->can('delete-endpoint'))
22 22
         <a href="#" class="dropdown-item editor_delete">
23 23
             <i class="bx bx-trash me-1"></i> Elimina
24 24
         </a>

+ 39
- 0
resources/views/endpoint/nuovo_product_key.blade.php Datei anzeigen

@@ -0,0 +1,39 @@
1
+@if(isset($success) && $success && isset($endpoint) && !isset($datatable))
2
+  <!-- <div class="alert alert-success"> -->
3
+    <!-- <h5>Nuovo Product Key generato:</h5> -->
4
+    <div class="mb-0">
5
+      <!-- <label for="inputProductKey" class="form-label small text-muted mb-1">Product key</label> -->
6
+      <div class="input-group input-group-merge input-group-lg ">
7
+        <input
8
+          type="text"
9
+          class="form-control font-monospace "
10
+          id="inputProductKey"
11
+          readonly
12
+          disabled
13
+          value="{{ $endpoint->product_key }}"
14
+          onclick="this.select()"
15
+          aria-label="Product key"
16
+        >
17
+        <button
18
+          type="button"
19
+          class="btn btn-outline-primary"
20
+          id="btnCopyProductKey"
21
+          onclick="copyProductKey(this)"
22
+          title="Copia negli appunti"
23
+          aria-label="Copia product key"
24
+        >
25
+          <i class="bx bx-copy"></i>
26
+        </button>
27
+      </div>
28
+    </div>
29
+  <!-- </div> -->
30
+@elseif(isset($datatable))
31
+  <span class="badge bg-success-subtle text-success-emphasis">
32
+    <i class="bx bx-check"></i>
33
+    {{ $endpoint->product_key }}
34
+  </span>
35
+@else
36
+  <div class="alert alert-danger">
37
+    Si è verificato un errore nella generazione del Product Key.
38
+  </div>
39
+@endif

+ 331
- 0
resources/views/endpoint/show.blade.php Datei anzeigen

@@ -0,0 +1,331 @@
1
+@php
2
+use App\Models\Attivita;
3
+use App\Models\Endpoint;
4
+$configData = Helper::appClasses();
5
+$attivita = Attivita::find(session()->get('attivita_attuale'));
6
+$stati = Endpoint::getStati();
7
+$statusMeta = $endpoint->status && isset($stati[$endpoint->status]) ? $stati[$endpoint->status] : null;
8
+$stampanti = $endpoint->stampanti;
9
+$endpointInfo = is_array($endpoint->info) ? $endpoint->info : (json_decode($endpoint->info ?? '{}', true) ?: []);
10
+
11
+$maskSecret = function (?string $value): string {
12
+    if ($value === null || $value === '') {
13
+        return '—';
14
+    }
15
+    $len = strlen($value);
16
+    if ($len <= 8) {
17
+        return str_repeat('•', $len);
18
+    }
19
+    return str_repeat('•', max(0, $len - 4)) . substr($value, -4);
20
+};
21
+@endphp
22
+
23
+@extends('layouts/layoutMaster')
24
+
25
+@section('title', 'Endpoint — ' . ($endpoint->label ?? $endpoint->id))
26
+
27
+@section('pageTitle')
28
+<div class="d-flex flex-column">
29
+  <h4 class="mb-0 mt-4">
30
+    <i class="bx bx-server"></i>
31
+    {{ $endpoint->label ?? 'Endpoint #' . $endpoint->id }}
32
+  </h4>
33
+  <nav aria-label="breadcrumb" class="mt-1" style="font-size: smaller;">
34
+    <ol class="breadcrumb breadcrumb-custom-icon ">
35
+      <li class="breadcrumb-item">
36
+        <a href="{{ route('endpoint.index') }}">Endpoint</a>
37
+        <i class="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
38
+      </li>
39
+      @if($attivita)
40
+      <li class="breadcrumb-item text-muted">
41
+        {{ $attivita->nome }}
42
+        <i class="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
43
+      </li>
44
+      @endif
45
+      <li class="breadcrumb-item active text-primary">
46
+        {{ $endpoint->label ?? ('#' . $endpoint->id) }}
47
+      </li>
48
+    </ol>
49
+  </nav>
50
+</div>
51
+@endsection
52
+
53
+@section('content')
54
+@include('_partials.status')
55
+
56
+<div class="row g-4">
57
+  {{-- Sidebar riepilogo --}}
58
+  <div class="col-12 col-lg-4 col-xl-3">
59
+    <div class="card mb-4">
60
+      <div class="card-body text-center pt-4">
61
+        <div class="avatar avatar-xl mx-auto mb-3">
62
+          <span class="avatar-initial rounded-circle bg-label-primary">
63
+            <i class="bx bx-chip bx-lg"></i>
64
+          </span>
65
+        </div>
66
+        <h5 class="mb-1">{{ $endpoint->label ?? '—' }}</h5>
67
+        <p class="text-muted small mb-3">ID #{{ $endpoint->id }}</p>
68
+
69
+        @if($statusMeta)
70
+          <span class="badge {{ $statusMeta['class'] }} mb-3">
71
+            <i class="{{ $statusMeta['icon'] }}"></i>
72
+            {{ $statusMeta['label'] }}
73
+          </span>
74
+        @else
75
+          <span class="badge bg-label-secondary mb-3">Stato sconosciuto</span>
76
+        @endif
77
+
78
+        @if($endpoint->ubicazione)
79
+          <p class="mb-0 text-muted small">
80
+            <i class="bx bx-map-pin me-1"></i>{{ $endpoint->ubicazione }}
81
+          </p>
82
+        @endif
83
+      </div>
84
+      <div class="card-body border-top pt-3">
85
+        <ul class="list-unstyled mb-0">
86
+          <li class="d-flex align-items-center mb-3">
87
+            <div class="avatar avatar-sm me-2">
88
+              <span class="avatar-initial rounded bg-label-info"><i class="bx bx-printer"></i></span>
89
+            </div>
90
+            <div>
91
+              <small class="text-muted d-block">Stampanti</small>
92
+              <span class="fw-medium">{{ $stampanti->count() }}</span>
93
+            </div>
94
+          </li>
95
+          <li class="d-flex align-items-center mb-3">
96
+            <div class="avatar avatar-sm me-2">
97
+              <span class="avatar-initial rounded bg-label-warning"><i class="bx bx-pulse"></i></span>
98
+            </div>
99
+            <div>
100
+              <small class="text-muted d-block">Ultimo heartbeat</small>
101
+              <span class="fw-medium small">
102
+                @if($endpoint->last_heartbeat)
103
+                  {{ $endpoint->last_heartbeat->format('d/m/Y H:i') }}
104
+                @else
105
+                  —
106
+                @endif
107
+              </span>
108
+            </div>
109
+          </li>
110
+          <li class="d-flex align-items-center">
111
+            <div class="avatar avatar-sm me-2">
112
+              <span class="avatar-initial rounded bg-label-success"><i class="bx bx-time-five"></i></span>
113
+            </div>
114
+            <div>
115
+              <small class="text-muted d-block">Ultima attività</small>
116
+              <span class="fw-medium small">
117
+                @if($endpoint->last_activity)
118
+                  {{ $endpoint->last_activity->format('d/m/Y H:i') }}
119
+                @else
120
+                  —
121
+                @endif
122
+              </span>
123
+            </div>
124
+          </li>
125
+        </ul>
126
+      </div>
127
+      <div class="card-body border-top pt-3 d-grid gap-2">
128
+        <a href="{{ route('endpoint.index') }}" class="btn btn-label-secondary">
129
+          <i class="bx bx-arrow-back me-1"></i> Torna all'elenco
130
+        </a>
131
+      </div>
132
+    </div>
133
+  </div>
134
+
135
+  {{-- Dettagli --}}
136
+  <div class="col-12 col-lg-8 col-xl-9">
137
+    <div class="row g-4">
138
+      <div class="col-12">
139
+        <div class="card">
140
+          <div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
141
+            <h5 class="card-title mb-0">
142
+              <i class="bx bx-info-circle text-primary me-1"></i>
143
+              Informazioni generali
144
+            </h5>
145
+          </div>
146
+          <div class="card-body">
147
+            <div class="row g-4">
148
+              <div class="col-md-6">
149
+                <dl class="row mb-0">
150
+                  <dt class="col-sm-5 text-muted">Etichetta</dt>
151
+                  <dd class="col-sm-7 fw-medium">{{ $endpoint->label ?? '—' }}</dd>
152
+
153
+                  <dt class="col-sm-5 text-muted">Descrizione</dt>
154
+                  <dd class="col-sm-7">{{ $endpoint->descrizione ?: '—' }}</dd>
155
+
156
+                  <dt class="col-sm-5 text-muted">Ubicazione</dt>
157
+                  <dd class="col-sm-7">{{ $endpoint->ubicazione ?: '—' }}</dd>
158
+
159
+                  <dt class="col-sm-5 text-muted">Tipo</dt>
160
+                  <dd class="col-sm-7">{{ $endpoint->type ?: '—' }}</dd>
161
+
162
+                  <dt class="col-sm-5 text-muted">Versione</dt>
163
+                  <dd class="col-sm-7">
164
+                    @if($endpoint->version)
165
+                      <span class="badge bg-label-secondary">{{ $endpoint->version }}</span>
166
+                    @else
167
+                      —
168
+                    @endif
169
+                  </dd>
170
+                </dl>
171
+              </div>
172
+              <div class="col-md-6">
173
+                <dl class="row mb-0">
174
+                  <dt class="col-sm-5 text-muted">Attività</dt>
175
+                  <dd class="col-sm-7">{{ $endpoint->attivita?->nome ?? '—' }}</dd>
176
+
177
+                  <dt class="col-sm-5 text-muted">Utente</dt>
178
+                  <dd class="col-sm-7">{{ $endpoint->user?->name ?? ($endpoint->user_id ? '#' . $endpoint->user_id : '—') }}</dd>
179
+
180
+                  <dt class="col-sm-5 text-muted">Licenza</dt>
181
+                  <dd class="col-sm-7">{{ $endpoint->licenza_id ? '#' . $endpoint->licenza_id : '—' }}</dd>
182
+
183
+                  <dt class="col-sm-5 text-muted">Registrato il</dt>
184
+                  <dd class="col-sm-7">{{ $endpoint->created_at?->format('d/m/Y H:i') ?? '—' }}</dd>
185
+
186
+                  <dt class="col-sm-5 text-muted">Aggiornato il</dt>
187
+                  <dd class="col-sm-7">{{ $endpoint->updated_at?->format('d/m/Y H:i') ?? '—' }}</dd>
188
+                </dl>
189
+              </div>
190
+            </div>
191
+          </div>
192
+        </div>
193
+      </div>
194
+
195
+      <div class="col-md-6">
196
+        <div class="card h-100">
197
+          <div class="card-header">
198
+            <h5 class="card-title mb-0">
199
+              <i class="bx bx-link text-info me-1"></i>
200
+              Connessione
201
+            </h5>
202
+          </div>
203
+          <div class="card-body">
204
+            <dl class="row mb-0">
205
+              <dt class="col-12 text-muted small mb-1">URL endpoint</dt>
206
+              <dd class="col-12 mb-3">
207
+                @if($endpoint->url)
208
+                  <code class="d-block text-break user-select-all">{{ $endpoint->url }}</code>
209
+                @else
210
+                  <span class="text-muted">—</span>
211
+                @endif
212
+              </dd>
213
+
214
+              <dt class="col-sm-5 text-muted">Heartbeat</dt>
215
+              <dd class="col-sm-7">
216
+                @if($endpoint->last_heartbeat)
217
+                  {{ $endpoint->last_heartbeat->format('d/m/Y H:i') }}
218
+                  <small class="text-muted d-block">{{ $endpoint->last_heartbeat->diffForHumans() }}</small>
219
+                @else
220
+                  <span class="text-muted">Mai ricevuto</span>
221
+                @endif
222
+              </dd>
223
+
224
+              <dt class="col-sm-5 text-muted">Ultima attività</dt>
225
+              <dd class="col-sm-7">
226
+                @if($endpoint->last_activity)
227
+                  {{ $endpoint->last_activity->format('d/m/Y H:i') }}
228
+                  <small class="text-muted d-block">{{ $endpoint->last_activity->diffForHumans() }}</small>
229
+                @else
230
+                  <span class="text-muted">—</span>
231
+                @endif
232
+              </dd>
233
+            </dl>
234
+          </div>
235
+        </div>
236
+      </div>
237
+
238
+      <div class="col-md-6">
239
+        <div class="card h-100">
240
+          <div class="card-header">
241
+            <h5 class="card-title mb-0">
242
+              <i class="bx bx-key text-warning me-1"></i>
243
+              Credenziali
244
+            </h5>
245
+          </div>
246
+          <div class="card-body">
247
+            <label class="form-label small text-muted mb-2">Product key</label>
248
+            @if($endpoint->product_key)
249
+              @include('endpoint.nuovo_product_key', ['success' => true, 'endpoint' => $endpoint])
250
+            @else
251
+              <p class="text-muted small mb-3">Nessuna product key assegnata.</p>
252
+            @endif
253
+
254
+            <dl class="row mb-0 mt-3">
255
+              <dt class="col-sm-4 text-muted small">Token</dt>
256
+              <dd class="col-sm-8"><code class="small">{{ $maskSecret($endpoint->token) }}</code></dd>
257
+
258
+              <dt class="col-sm-4 text-muted small">Secret</dt>
259
+              <dd class="col-sm-8"><code class="small">{{ $maskSecret($endpoint->secret) }}</code></dd>
260
+            </dl>
261
+            <p class="text-muted small mb-0 mt-2">
262
+              <i class="bx bx-info-circle"></i> Token e secret sono mascherati per sicurezza.
263
+            </p>
264
+          </div>
265
+        </div>
266
+      </div>
267
+
268
+      <div class="col-12">
269
+        <div class="card">
270
+          <div class="card-header d-flex align-items-center justify-content-between">
271
+            <h5 class="card-title mb-0">
272
+              <i class="bx bx-printer text-primary me-1"></i>
273
+              Stampanti collegate
274
+            </h5>
275
+            <span class="badge bg-label-primary">{{ $stampanti->count() }}</span>
276
+          </div>
277
+          <div class="card-body p-5">
278
+            @if($stampanti->isNotEmpty())
279
+              <div class="table-responsive">
280
+                <table class="table table-hover mb-0">
281
+                  <thead class="">
282
+                    <tr>
283
+                      <th>ID</th>
284
+                      <th>Nome</th>
285
+                      <th>Tipo</th>
286
+                    </tr>
287
+                  </thead>
288
+                  <tbody>
289
+                    @foreach($stampanti as $dispositivo)
290
+                      <tr>
291
+                        <td class="text-muted">#{{ $dispositivo->id }}</td>
292
+                        <td class="fw-medium">{{ $dispositivo->nome ?? '—' }}</td>
293
+                        <td><span class="badge bg-label-secondary">{{ $dispositivo->tipo ?? '—' }}</span></td>
294
+                      </tr>
295
+                    @endforeach
296
+                  </tbody>
297
+                </table>
298
+              </div>
299
+            @else
300
+              <div class="text-center text-muted py-5">
301
+                <i class="bx bx-printer bx-lg d-block mb-2 opacity-50"></i>
302
+                Nessuna stampante registrata su questo endpoint.
303
+              </div>
304
+            @endif
305
+          </div>
306
+        </div>
307
+      </div>
308
+
309
+      @if(!empty($endpointInfo))
310
+      <div class="col-12">
311
+        <div class="card">
312
+          <div class="card-header">
313
+            <h5 class="card-title mb-0">
314
+              <i class="bx bx-data text-secondary me-1"></i>
315
+              Info aggiuntive (JSON)
316
+            </h5>
317
+          </div>
318
+          <div class="card-body">
319
+            <pre class="bg-lighter rounded p-3 mb-0 small text-break"><code>{{ json_encode($endpointInfo, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</code></pre>
320
+          </div>
321
+        </div>
322
+      </div>
323
+      @endif
324
+    </div>
325
+  </div>
326
+</div>
327
+@endsection
328
+
329
+@section('page-script')
330
+@include('endpoint._partials.copy-product-key-script')
331
+@endsection

+ 5
- 1
resources/views/layouts/guest.blade.php Datei anzeigen

@@ -1,3 +1,7 @@
1
+@isset($pageConfigs)
2
+  {!! Helper::updatePageConfig($pageConfigs) !!}
3
+@endisset
4
+
1 5
 @php
2 6
   $configData = Helper::appClasses();
3 7
   use Illuminate\Support\Facades\Auth;
@@ -38,7 +42,7 @@
38 42
           </div>
39 43
         </div> -->
40 44
 
41
-      <div class="misc-wrapper">
45
+      <div class="misc-wrapper {{ Route::is('welcome') ? 'misc-wrapper--welcome-bacheca' : '' }}">
42 46
         <!-- Content -->
43 47
         @hasSection('content')
44 48
           @yield('content')

+ 2
- 2
resources/views/layouts/sections/navbar/navbar-partial.blade.php Datei anzeigen

@@ -29,7 +29,7 @@ class="layout-menu-toggle navbar-nav align-items-xl-center me-4 me-xl-0{{ isset(
29 29
 </div>
30 30
 @endif
31 31
 
32
-<div class="navbar-nav-right d-flex align-items-center justify-content-end" id="navbar-collapse">
32
+<div class="navbar-nav-right d-flex align-items-center justify-content-between" id="navbar-collapse">  <!-- justify-content-end -->
33 33
 
34 34
   @if (!isset($menuHorizontal))
35 35
   @yield('pageTitle')
@@ -361,7 +361,7 @@ class="layout-menu-toggle navbar-nav align-items-xl-center me-4 me-xl-0{{ isset(
361 361
                                     <!--/ Notification -->
362 362
                                     <!-- Attività -->
363 363
                                     @if(session()->has('attivita_attuale') && session()->get('attivita_attuale') != null && session()->get('attivita_attuale') != 0)
364
-                                    <li class="nav-item navbar-dropdown dropdown-user dropdown mx-12">
364
+                                    <li class="d-none d-lg-flex nav-item navbar-dropdown dropdown-user dropdown mx-12">
365 365
                                       <a class="nav-link dropdown-toggle hide-arrow p-0" href="javascript:void(0);" data-bs-toggle="dropdown">
366 366
                                         <small class=" ms-2">Stai gestendo</small>
367 367
                                         <i class="bx bx-aperture text-primary bx-md"></i> 

+ 21
- 2
resources/views/layouts/sections/scripts.blade.php Datei anzeigen

@@ -56,6 +56,17 @@
56 56
   DataTable.ext.editorFields;
57 57
 
58 58
   _fieldTypes.select2 = {
59
+    // Select2 nell'Editor DataTables: non usare $(".modal") — prende il primo modal
60
+    // della pagina (es. #basicModal su prima_nota) e il dropdown si disallinea con
61
+    // zoom/scroll, soprattutto sui campi a metà form (modal-dialog-scrollable).
62
+    _dropdownParent: function ( $input ) {
63
+      var $parent = $input.closest('.modal');
64
+      if ( !$parent.length ) {
65
+        $parent = $('.modal.DTED.show, .modal.show').last();
66
+      }
67
+      return $parent.length ? $parent : $(document.body);
68
+    },
69
+
59 70
     _addOptions: function ( conf, opts ) {
60 71
       var elOpts = conf._input[0].options;
61 72
 
@@ -74,12 +85,11 @@
74 85
         id: DataTable.Editor.safeId( conf.id )
75 86
       }, conf.attr || {} ) );
76 87
 
77
-
78 88
       this.one("open", function(){
79 89
 
80 90
         var options = $.extend( {
81 91
           width: '100%',
82
-          dropdownParent: $(".modal"),
92
+          dropdownParent: _fieldTypes.select2._dropdownParent( conf._input ),
83 93
         }, conf.opts );
84 94
 
85 95
         _fieldTypes.select2._addOptions( conf, conf.options || conf.ipOpts );
@@ -94,8 +104,16 @@
94 104
           open = false;
95 105
         } );
96 106
 
107
+        // Prima apertura: applica l'ultimo valore richiesto da Editor.set()
108
+        if ( conf._pendingVal !== undefined ) {
109
+          conf._input
110
+          .val( conf._pendingVal )
111
+          .trigger( 'change', { editor: true } );
112
+        }
113
+
97 114
         // On open, need to have the instance update now that it is in the DOM
98 115
         this.one( 'open.select2-'+DataTable.Editor.safeId( conf.id ), function () {
116
+          options.dropdownParent = _fieldTypes.select2._dropdownParent( conf._input );
99 117
           conf._input.select2( options );
100 118
 
101 119
           if ( open ) {
@@ -120,6 +138,7 @@
120 138
 
121 139
     set: function ( conf, val ) {
122 140
       var field = this.field(conf.name);
141
+      conf._pendingVal = val;
123 142
 
124 143
       if ( conf.separator && ! Array.isArray( val ) ) {
125 144
         val = val === null

+ 21
- 2
resources/views/layouts/sections/scriptsFront.blade.php Datei anzeigen

@@ -55,6 +55,17 @@
55 55
   DataTable.ext.editorFields;
56 56
 
57 57
   _fieldTypes.select2 = {
58
+    // Select2 nell'Editor DataTables: non usare $(".modal") — prende il primo modal
59
+    // della pagina (es. #basicModal su prima_nota) e il dropdown si disallinea con
60
+    // zoom/scroll, soprattutto sui campi a metà form (modal-dialog-scrollable).
61
+    _dropdownParent: function ( $input ) {
62
+      var $parent = $input.closest('.modal');
63
+      if ( !$parent.length ) {
64
+        $parent = $('.modal.DTED.show, .modal.show').last();
65
+      }
66
+      return $parent.length ? $parent : $(document.body);
67
+    },
68
+
58 69
     _addOptions: function ( conf, opts ) {
59 70
       var elOpts = conf._input[0].options;
60 71
 
@@ -73,12 +84,11 @@
73 84
         id: DataTable.Editor.safeId( conf.id )
74 85
       }, conf.attr || {} ) );
75 86
 
76
-
77 87
       this.one("open", function(){
78 88
 
79 89
         var options = $.extend( {
80 90
           width: '100%',
81
-          dropdownParent: $(".modal"),
91
+          dropdownParent: _fieldTypes.select2._dropdownParent( conf._input ),
82 92
         }, conf.opts );
83 93
 
84 94
         _fieldTypes.select2._addOptions( conf, conf.options || conf.ipOpts );
@@ -93,8 +103,16 @@
93 103
           open = false;
94 104
         } );
95 105
 
106
+        // Prima apertura: applica l'ultimo valore richiesto da Editor.set()
107
+        if ( conf._pendingVal !== undefined ) {
108
+          conf._input
109
+          .val( conf._pendingVal )
110
+          .trigger( 'change', { editor: true } );
111
+        }
112
+
96 113
         // On open, need to have the instance update now that it is in the DOM
97 114
         this.one( 'open.select2-'+DataTable.Editor.safeId( conf.id ), function () {
115
+          options.dropdownParent = _fieldTypes.select2._dropdownParent( conf._input );
98 116
           conf._input.select2( options );
99 117
 
100 118
           if ( open ) {
@@ -119,6 +137,7 @@
119 137
 
120 138
     set: function ( conf, val ) {
121 139
       var field = this.field(conf.name);
140
+      conf._pendingVal = val;
122 141
 
123 142
       if ( conf.separator && ! Array.isArray( val ) ) {
124 143
         val = val === null

+ 52
- 0
resources/views/operatore/auth/dashDispositivi.blade.php Datei anzeigen

@@ -0,0 +1,52 @@
1
+@extends('layouts/guest')
2
+
3
+@section('title', 'Dashboard Dispositivi')
4
+
5
+@section('page-meta')
6
+  <script>document.documentElement.classList.add('welcome-bacheca-page');</script>
7
+  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+  <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400&display=swap" rel="stylesheet" />
10
+@endsection
11
+
12
+
13
+@section('page-style')
14
+  @vite(['resources/css/welcome.css'])
15
+@endsection
16
+
17
+@section('content')
18
+  <div class="welcome-bacheca__bg" data-bg="bold" aria-hidden="true">
19
+    <div class="welcome-bacheca__bg-stripe"></div>
20
+    <div class="welcome-bacheca__bg-pattern"></div>
21
+    <div class="welcome-bacheca__bg-glow welcome-bacheca__bg-glow--orange"></div>
22
+    <div class="welcome-bacheca__bg-glow welcome-bacheca__bg-glow--purple"></div>
23
+    <div class="welcome-bacheca__bg-glow welcome-bacheca__bg-glow--navy"></div>
24
+  </div>
25
+
26
+  <div
27
+    class="container-fluid min-vh-100 d-flex align-items-center justify-content-center px-3 py-4"
28
+    style="--fest-orange:#f58220; --fest-orange-soft:#ff9a2e; --fest-purple:#602d91; --fest-navy:#15265c; --fest-text:#1a2744; --fest-text-muted:#4a5568;"
29
+  >
30
+    <div class="welcome-bacheca__masthead w-100" style="max-width: 380px; margin-bottom: 0;">
31
+      <div class="text-center mb-3">
32
+        <img
33
+          src="{{ asset('assets/img/logo_fest_L.png') }}"
34
+          alt="{{ config('app.name') }}"
35
+          class="img-fluid"
36
+          style="max-height: 52px;"
37
+        >
38
+      </div>
39
+
40
+      <div class="text-center mb-4">
41
+        <p class="welcome-bacheca__eyebrow mb-2 justify-content-center">
42
+          <span class="welcome-bacheca__live-dot" aria-hidden="true"></span>
43
+          Area cassa
44
+        </p>
45
+        <h1 class="welcome-bacheca__title mb-1" style="font-size: clamp(1.25rem, 3.6vw, 1.55rem);">Dashboard Dispositivi</h1>
46
+        <p class="welcome-bacheca__lead mb-0" style="font-size: 0.9rem;">Accedi ai dispositivi a te associati.</p>
47
+      </div>
48
+
49
+      
50
+    </div>
51
+  </div>
52
+@endsection

+ 31
- 0
resources/views/operatore/dispositivi_list.blade.php Datei anzeigen

@@ -0,0 +1,31 @@
1
+@php
2
+    $dispositivi = \App\Models\Dispositivo::where('is_attivo', true)
3
+    ->whereNotIn('tipo', [\App\Models\Dispositivo::MONITOR , \App\Models\Dispositivo::STAMPANTE])
4
+    ->orderBy('nome')
5
+    ->get();
6
+
7
+    $dispositivi_associati = $operatore->dispositivi;
8
+    // dd($dispositivi_associati);
9
+@endphp
10
+<form action="{{ route('operatore.update.dispositivi', ['operatore_id' => $operatore->id]) }}" method="post">
11
+    @csrf
12
+    @method('POST')
13
+<table class="table">
14
+    <tbody>
15
+    @foreach($dispositivi as $dispositivo)
16
+        <tr>
17
+            <td>
18
+                <input type="checkbox" class="form-check-input" name="dispositivo_id_{{$dispositivo->id}}" value="{{ $dispositivo->id }}"
19
+                @if($dispositivi_associati->contains($dispositivo->id))
20
+                    checked
21
+                @endif
22
+                >
23
+            </td>
24
+            <td>{{ $dispositivo->nome }}</td>
25
+            <td>{{ \App\Models\Dispositivo::getTipoDispositivo()[$dispositivo->tipo]['label'] }}</td>
26
+        </tr>
27
+    @endforeach
28
+    </tbody>
29
+</table>
30
+<button type="submit" class="btn btn-primary">Salva</button>
31
+</form>

+ 97
- 0
resources/views/operatore/guardOperatore/dashDispositivi.blade.php Datei anzeigen

@@ -0,0 +1,97 @@
1
+@extends('layouts/guest')
2
+
3
+@section('title', 'Dispositivi Associati')
4
+
5
+@section('page-meta')
6
+  <script>document.documentElement.classList.add('welcome-bacheca-page');</script>
7
+  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+  <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400&display=swap" rel="stylesheet" />
10
+@endsection
11
+
12
+@section('page-style')
13
+  @vite(['resources/css/welcome.css'])
14
+@endsection
15
+
16
+@section('content')
17
+@include('_partials.status')
18
+
19
+<div class="welcome-bacheca__bg" data-bg="bold" aria-hidden="true">
20
+  <div class="welcome-bacheca__bg-stripe"></div>
21
+  <div class="welcome-bacheca__bg-pattern"></div>
22
+  <div class="welcome-bacheca__bg-glow welcome-bacheca__bg-glow--orange"></div>
23
+  <div class="welcome-bacheca__bg-glow welcome-bacheca__bg-glow--purple"></div>
24
+  <div class="welcome-bacheca__bg-glow welcome-bacheca__bg-glow--navy"></div>
25
+</div>
26
+
27
+<div
28
+  class="container-fluid min-vh-100 d-flex align-items-center justify-content-center px-3 py-4"
29
+  style="--fest-orange:#f58220; --fest-orange-soft:#ff9a2e; --fest-purple:#602d91; --fest-navy:#15265c; --fest-text:#1a2744; --fest-text-muted:#4a5568;"
30
+>
31
+  <div class="welcome-bacheca__masthead w-100" style="max-width: 480px; margin-bottom: 0;">
32
+
33
+    <div class="text-center mb-3">
34
+      <img
35
+        src="{{ asset('assets/img/logo_fest_L.png') }}"
36
+        alt="{{ config('app.name') }}"
37
+        class="img-fluid"
38
+        style="max-height: 52px;"
39
+      >
40
+    </div>
41
+    <div class="text-center mb-4">
42
+      <p class="welcome-bacheca__eyebrow mb-2 justify-content-center">
43
+        <span class="welcome-bacheca__live-dot" aria-hidden="true"></span>
44
+        Dispositivi associati
45
+      </p>
46
+      <h1 class="welcome-bacheca__title mb-1" style="font-size: clamp(1.25rem, 3.6vw, 1.55rem);">
47
+          Scegli il dispositivo
48
+      </h1>
49
+      <p class="welcome-bacheca__lead mb-0" style="font-size: 0.95rem;">
50
+        Seleziona uno dei dispositivi associati al tuo account per continuare.
51
+      </p>
52
+    </div>
53
+    @if(isset($dispositivi) && count($dispositivi))
54
+    <div class="row g-3">
55
+      @foreach($dispositivi as $dispositivo)
56
+        <div class="col-12">
57
+          <div class="card shadow-sm mb-0 text-start">
58
+            <div class="card-body d-flex align-items-center justify-content-between">
59
+              <div>
60
+                <div class="fw-bold" style="font-size:1.15rem;">
61
+                  {{ $dispositivo->nome ?? 'Nessun nome' }}
62
+                </div>
63
+                <div class="d-block text-muted" style="font-size:0.87rem;">
64
+                  Tipo: {{ $dispositivo->tipo ? ucfirst(strtolower($dispositivo->tipo)) : '-' }}<br>
65
+                  @if(!empty($dispositivo->descrizione))
66
+                    <span>{{ $dispositivo->descrizione }}</span>
67
+                  @endif
68
+                </div>
69
+              </div>
70
+              <div>
71
+                <form method="GET" action="{{ route('operatore.punto-vendita.show') }}">
72
+                  <input type="hidden" name="punto_vendita_id" value="{{ $dispositivo->id }}"/>
73
+                  <button type="submit" class="welcome-bacheca__btn welcome-bacheca__btn--primary ms-2">
74
+                    Entra
75
+                  </button>
76
+                </form>
77
+              </div>
78
+            </div>
79
+          </div>
80
+        </div>
81
+      @endforeach
82
+    </div>
83
+    @else
84
+      <div class="alert alert-warning text-center mt-4">
85
+        Nessun dispositivo associato al tuo account.
86
+        <br>Contatta l'amministratore per maggiori informazioni.
87
+      </div>
88
+    @endif
89
+
90
+    <div class="text-center mt-4">
91
+      <a href="{{ route('operatore.logout') }}" class="btn btn-link text-muted small">
92
+        Esci
93
+      </a>
94
+    </div>
95
+  </div>
96
+</div>
97
+@endsection

+ 140
- 0
resources/views/operatore/index.blade.php Datei anzeigen

@@ -0,0 +1,140 @@
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', 'Operatori')
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-0 mt-4"> 
41
+    <i class="bx bx-user"></i> 
42
+    Operatori
43
+  </h4>
44
+  <nav aria-label="breadcrumb" style="font-size: smaller;">
45
+            <ol class="breadcrumb breadcrumb-custom-icon">
46
+      
47
+              <li class="breadcrumb-item">
48
+                <a href="#">Configurazioni</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('operatore.index') }}">Operatori</a>
53
+              </li>
54
+            </ol>
55
+          </nav>
56
+</div>
57
+@endsection
58
+
59
+@section('content')
60
+<style>
61
+  div.upload button:first-child{
62
+    display: none !important;
63
+  }
64
+
65
+.bx-chef-hat{
66
+  --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");
67
+}
68
+
69
+div.dt-action-buttons .dt-buttons .btn:first-child {
70
+  border-radius: 0.25rem 0 0 0.25rem !important;
71
+ }
72
+div.dt-action-buttons .dt-buttons .btn:last-child {
73
+  border-radius: 0 0.25rem 0.25rem 0 !important;
74
+ }
75
+</style>
76
+
77
+@include('_partials.status')
78
+
79
+<div class="row justify-content-center">
80
+  <div class="col-xxl mb-4 mt-2">
81
+    <div class="card">
82
+      <div class="card-body">
83
+        {{ $dataTable_operatore->table(['class' => 'table table-bordered']) }}
84
+      </div>
85
+    </div>
86
+  </div>
87
+</div>
88
+
89
+
90
+
91
+
92
+@endsection
93
+
94
+@section('page-script')
95
+
96
+{{$dataTable_operatore->scripts(attributes: ['type' => 'module'])}}
97
+
98
+
99
+<script type="module">
100
+  $(document).ready(function(){
101
+    // Editor edit
102
+    $("#dataTable_operatore").on('click', 'a.editor_edit', function (e) {
103
+      e.preventDefault();
104
+      window.LaravelDataTables["dataTable_operatore-editor"].edit( $(this).closest('tr'), {
105
+        title: 'Modifica',
106
+        buttons: 'Aggiorna',
107
+      });
108
+    });
109
+
110
+    // Editor delete
111
+    $("#dataTable_operatore").on('click', 'a.editor_delete', function (e) {
112
+      e.preventDefault();
113
+      window.LaravelDataTables["dataTable_operatore-editor"].remove($(this).closest('tr'), {
114
+        title: 'Cancella record',
115
+        message: 'Sei sicuro di voler eliminare il record selezionato?',
116
+        buttons: 'Cancella record'
117
+      });
118
+    } );
119
+
120
+    $("#dataTable_operatore").on('dblclick', 'tbody td', function (e) {
121
+      window.LaravelDataTables["dataTable_operatore-editor"].edit( $(this).closest('tr'), {
122
+        title: 'Modifica',
123
+        buttons: 'Aggiorna',
124
+      });
125
+    });
126
+
127
+  });
128
+
129
+</script>
130
+
131
+<script>
132
+  function initComplete_operatore(){
133
+    $('div.dt-buttons button').removeClass('btn-secondary');
134
+    $('div.dt-search').addClass('mt-0 mb-4');
135
+    return;
136
+  }
137
+
138
+
139
+</script>
140
+@endsection

+ 79
- 0
resources/views/operatore/landing.blade.php Datei anzeigen

@@ -0,0 +1,79 @@
1
+@extends('layouts/guest')
2
+
3
+@section('title', 'Login Operatore')
4
+
5
+@section('page-meta')
6
+  <script>document.documentElement.classList.add('welcome-bacheca-page');</script>
7
+  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+  <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400&display=swap" rel="stylesheet" />
10
+@endsection
11
+
12
+
13
+@section('page-style')
14
+  @vite(['resources/css/welcome.css'])
15
+@endsection
16
+
17
+@section('content')
18
+@include('_partials.status')
19
+
20
+  <div class="welcome-bacheca__bg" data-bg="bold" aria-hidden="true">
21
+    <div class="welcome-bacheca__bg-stripe"></div>
22
+    <div class="welcome-bacheca__bg-pattern"></div>
23
+    <div class="welcome-bacheca__bg-glow welcome-bacheca__bg-glow--orange"></div>
24
+    <div class="welcome-bacheca__bg-glow welcome-bacheca__bg-glow--purple"></div>
25
+    <div class="welcome-bacheca__bg-glow welcome-bacheca__bg-glow--navy"></div>
26
+  </div>
27
+
28
+  <div
29
+    class="container-fluid min-vh-100 d-flex align-items-center justify-content-center px-3 py-4"
30
+    style="--fest-orange:#f58220; --fest-orange-soft:#ff9a2e; --fest-purple:#602d91; --fest-navy:#15265c; --fest-text:#1a2744; --fest-text-muted:#4a5568;"
31
+  >
32
+    <div class="welcome-bacheca__masthead w-100" style="max-width: 380px; margin-bottom: 0;">
33
+      <div class="text-center mb-3">
34
+        <img
35
+          src="{{ asset('assets/img/logo_fest_L.png') }}"
36
+          alt="{{ config('app.name') }}"
37
+          class="img-fluid"
38
+          style="max-height: 52px;"
39
+        >
40
+      </div>
41
+
42
+      <div class="text-center mb-4">
43
+        <p class="welcome-bacheca__eyebrow mb-2 justify-content-center">
44
+          <span class="welcome-bacheca__live-dot" aria-hidden="true"></span>
45
+          Area cassa
46
+        </p>
47
+        <h1 class="welcome-bacheca__title mb-1" style="font-size: clamp(1.25rem, 3.6vw, 1.55rem);">Login Operatore</h1>
48
+        <p class="welcome-bacheca__lead mb-0" style="font-size: 0.9rem;">Accedi con le tue credenziali per usare il dispositivo.</p>
49
+      </div>
50
+
51
+      <form method="POST" action="{{ route('operatore.login') }}">
52
+        @csrf
53
+        <div class="mb-3">
54
+          <label for="username" class="form-label fw-semibold">Username</label>
55
+          <input
56
+            type="text"
57
+            class="form-control"
58
+            id="username"
59
+            name="username"
60
+            value="{{ old('username') }}"
61
+            required
62
+            autofocus
63
+          >
64
+        </div>
65
+        <div class="mb-3">
66
+          <label for="password" class="form-label fw-semibold">Password</label>
67
+          <input
68
+            type="password"
69
+            class="form-control"
70
+            id="password"
71
+            name="password"
72
+            required
73
+          >
74
+        </div>
75
+        <button type="submit" class="welcome-bacheca__btn welcome-bacheca__btn--primary w-100">Accedi</button>
76
+      </form>
77
+    </div>
78
+  </div>
79
+@endsection

+ 29
- 0
resources/views/operatore/menu.blade.php Datei anzeigen

@@ -0,0 +1,29 @@
1
+<?php
2
+use Illuminate\Support\Facades\Auth;
3
+?>
4
+
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
+
11
+        @if(Auth::user()->can('view-operatore'))
12
+        <a href="{{ route('operatore.show', ['operatore_id' => $entity->id]) }}" class="dropdown-item">
13
+            <i class="bx bx-show-alt me-1"></i> Visualizza
14
+        </a>
15
+        @endif
16
+
17
+        @if(Auth::user()->can('edit-operatore'))
18
+        <a href="#" class="dropdown-item editor_edit">
19
+            <i class="bx bx-edit-alt me-1"></i> Modifica
20
+        </a>
21
+        @endif
22
+        
23
+        @if(Auth::user()->can('delete-monitor'))
24
+        <a href="#" class="dropdown-item editor_delete text-danger">
25
+            <i class="bx bx-trash me-1"></i> Elimina
26
+        </a>
27
+        @endif
28
+    </div>
29
+</div>

+ 146
- 0
resources/views/operatore/show.blade.php Datei anzeigen

@@ -0,0 +1,146 @@
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', 'Operatore')
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-0 mt-4"> 
41
+    <i class="bx bx-user"></i> 
42
+    Operatore {{ $operatore->username }}
43
+  </h4>
44
+  <nav aria-label="breadcrumb" style="font-size: smaller;">
45
+            <ol class="breadcrumb breadcrumb-custom-icon">
46
+      
47
+              <li class="breadcrumb-item">
48
+                <a href="#">Configurazioni</a>
49
+                <i class="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
50
+              </li>
51
+              <li class="breadcrumb-item active">
52
+                <a href="{{ route('operatore.index') }}">Operatori</a>
53
+                <i class="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
54
+
55
+              </li>
56
+              <li class="breadcrumb-item active text-primary">
57
+                <a href="{{ route('operatore.show', ['operatore_id' => $operatore->id]) }}">{{ $operatore->username }}</a>
58
+              </li>
59
+            </ol>
60
+          </nav>
61
+</div>
62
+@endsection
63
+
64
+@section('content')
65
+<style>
66
+  div.upload button:first-child{
67
+    display: none !important;
68
+  }
69
+
70
+.bx-chef-hat{
71
+  --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");
72
+}
73
+
74
+div.dt-action-buttons .dt-buttons .btn:first-child {
75
+  border-radius: 0.25rem 0 0 0.25rem !important;
76
+ }
77
+div.dt-action-buttons .dt-buttons .btn:last-child {
78
+  border-radius: 0 0.25rem 0.25rem 0 !important;
79
+ }
80
+</style>
81
+
82
+@include('_partials.status')
83
+
84
+<div class="row justify-content-center">
85
+  <div class="col-xxl-4 col-lg-4 mb-4 mt-2">
86
+    <div class="card" id="dispositivi_associati_div">
87
+      <div class="card-body">
88
+        <!-- Contenuto Dispositivi Associati -->
89
+        <h5 class="card-title">Dispositivi Associati</h5>
90
+        <div id="dispositivi_div">
91
+
92
+        @include('operatore.dispositivi_list', ['dispositivi' => $dispositivi])
93
+        </div>
94
+      </div>
95
+    </div>
96
+  </div>
97
+  <div class="col-xxl-4 col-lg-4 mb-4 mt-2">
98
+    <div class="card" id="password">
99
+      <div class="card-body">
100
+
101
+      <h5 class="card-title">Password</h5>
102
+
103
+        <form action="{{ route('operatore.update.password', $operatore->id) }}" method="post">
104
+          @csrf
105
+          @method('POST')
106
+          <div class="mb-3">
107
+            <label for="password" class="form-label">Password</label>
108
+            <input type="password" class="form-control" id="password" name="password">
109
+          </div>
110
+          <div class="mb-3">
111
+            <label for="password_confirmation" class="form-label">Conferma Password</label>
112
+            <input type="password" class="form-control" id="password_confirmation" name="password_confirmation">
113
+          </div>
114
+          <button type="submit" class="btn btn-primary">Salva</button>
115
+        </form>
116
+      </div>
117
+    </div>
118
+  </div>
119
+  <div class="col-xxl-4 col-lg-4 mb-4 mt-2">
120
+    <div class="card" id="info">
121
+      <div class="card-body">
122
+        <!-- Contenuto terza colonna -->
123
+        <h5 class="card-title">Info Operatore</h5>
124
+        <p>Nome: {{ $operatore->nome }}</p>
125
+        <p>Cognome: {{ $operatore->cognome }}</p>
126
+        <p>Email: {{ $operatore->email }}</p>
127
+        <p>Telefono: {{ $operatore->telefono }}</p>
128
+      </div>
129
+    </div>
130
+  </div>
131
+</div>
132
+
133
+
134
+
135
+
136
+@endsection
137
+
138
+@section('page-script')
139
+<script>
140
+
141
+    function showDispositivi(){
142
+        $('#dispositivi_div').load("{{ route('operatore.dispositivi.list', $operatore->id) }}");
143
+    }
144
+
145
+    </script>
146
+@endsection

+ 109
- 0
resources/views/pagamento/_partials/statistiche.blade.php Datei anzeigen

@@ -0,0 +1,109 @@
1
+@php
2
+  /**
3
+   * Contratto $statistiche (controller — da implementare):
4
+   *
5
+   * 'operazioni' => ['oggi' => int, 'totali' => int],
6
+   * 'contanti'  => ['oggi' => ['count' => int, 'importo' => float], 'totali' => [...]],
7
+   * 'digitali'  => ['oggi' => ['count' => int, 'importo' => float], 'totali' => [...]],
8
+   */
9
+  $stats = $statistiche ?? [];
10
+  $operazioni = $stats['operazioni'] ?? [];
11
+  $contanti = $stats['contanti'] ?? [];
12
+  $digitali = $stats['digitali'] ?? [];
13
+
14
+  $fmtEuro = fn ($val) => number_format((float) ($val ?? 0), 2, ',', '.') . ' €';
15
+  $fmtCount = fn ($val) => number_format((int) ($val ?? 0), 0, ',', '.');
16
+@endphp
17
+
18
+<div class="row">
19
+  <div class="col-12 mb-6">
20
+    <div class="card">
21
+      <div class="card-body py-4">
22
+        <div class="row g-4 g-lg-0 align-items-stretch">
23
+
24
+          {{-- Operazioni --}}
25
+          <div class="col-sm-6 col-lg-4">
26
+            <div class="d-flex h-100 justify-content-between align-items-center px-lg-3 border-lg-end">
27
+              <div>
28
+                <div class="d-flex align-items-center gap-2 mb-2">
29
+                  <span class="avatar avatar-sm">
30
+                    <span class="avatar-initial rounded bg-label-secondary">
31
+                      <i class="bx bx-receipt"></i>
32
+                    </span>
33
+                  </span>
34
+                  <span class="fw-medium">Operazioni</span>
35
+                </div>
36
+                <p class="mb-1 small text-muted">Pagamenti registrati</p>
37
+                <p class="mb-0 fs-4 fw-semibold lh-sm">
38
+                  <span>{{ $fmtCount($operazioni['oggi'] ?? null) }}</span>
39
+                  <span class="text-muted fw-normal mx-1">/</span>
40
+                  <span class="text-body-secondary fw-normal">{{ $fmtCount($operazioni['totali'] ?? null) }}</span>
41
+                </p>
42
+                <p class="mb-0 mt-1 small text-muted">oggi / totali</p>
43
+              </div>
44
+            </div>
45
+          </div>
46
+
47
+          {{-- Contanti --}}
48
+          <div class="col-sm-6 col-lg-4">
49
+            <div class="d-flex h-100 justify-content-between align-items-center px-lg-3 border-lg-end">
50
+              <div>
51
+                <div class="d-flex align-items-center gap-2 mb-2">
52
+                  <span class="avatar avatar-sm">
53
+                    <span class="avatar-initial rounded bg-label-success">
54
+                      <i class="bx bx-money text-success"></i>
55
+                    </span>
56
+                  </span>
57
+                  <span class="fw-medium">Contanti</span>
58
+                </div>
59
+                <p class="mb-1 small text-muted">Importo incassato</p>
60
+                <p class="mb-0 fs-4 fw-semibold lh-sm text-success">
61
+                  <span>{{ $fmtEuro($contanti['oggi']['importo'] ?? null) }}</span>
62
+                  <span class="text-muted fw-normal mx-1">/</span>
63
+                  <span class="text-body-secondary fw-normal">{{ $fmtEuro($contanti['totali']['importo'] ?? null) }}</span>
64
+                </p>
65
+                <p class="mb-0 mt-2 small">
66
+                  <span class="text-muted">N°</span>
67
+                  <span class="fw-medium">{{ $fmtCount($contanti['oggi']['count'] ?? null) }}</span>
68
+                  <span class="text-muted mx-1">/</span>
69
+                  <span class="text-body-secondary">{{ $fmtCount($contanti['totali']['count'] ?? null) }}</span>
70
+                  <span class="text-muted ms-1">pagamenti</span>
71
+                </p>
72
+              </div>
73
+            </div>
74
+          </div>
75
+
76
+          {{-- Digitali --}}
77
+          <div class="col-sm-12 col-lg-4">
78
+            <div class="d-flex h-100 justify-content-between align-items-center px-lg-3">
79
+              <div>
80
+                <div class="d-flex align-items-center gap-2 mb-2">
81
+                  <span class="avatar avatar-sm">
82
+                    <span class="avatar-initial rounded bg-label-info">
83
+                      <i class="bx bx-credit-card text-info"></i>
84
+                    </span>
85
+                  </span>
86
+                  <span class="fw-medium">Digitali</span>
87
+                </div>
88
+                <p class="mb-1 small text-muted">Importo incassato</p>
89
+                <p class="mb-0 fs-4 fw-semibold lh-sm text-info">
90
+                  <span>{{ $fmtEuro($digitali['oggi']['importo'] ?? null) }}</span>
91
+                  <span class="text-muted fw-normal mx-1">/</span>
92
+                  <span class="text-body-secondary fw-normal">{{ $fmtEuro($digitali['totali']['importo'] ?? null) }}</span>
93
+                </p>
94
+                <p class="mb-0 mt-2 small">
95
+                  <span class="text-muted">N°</span>
96
+                  <span class="fw-medium">{{ $fmtCount($digitali['oggi']['count'] ?? null) }}</span>
97
+                  <span class="text-muted mx-1">/</span>
98
+                  <span class="text-body-secondary">{{ $fmtCount($digitali['totali']['count'] ?? null) }}</span>
99
+                  <span class="text-muted ms-1">pagamenti</span>
100
+                </p>
101
+              </div>
102
+            </div>
103
+          </div>
104
+
105
+        </div>
106
+      </div>
107
+    </div>
108
+  </div>
109
+</div>

+ 1
- 0
resources/views/pagamento/index.blade.php Datei anzeigen

@@ -54,6 +54,7 @@ $configData = Helper::appClasses();
54 54
 </style>
55 55
 
56 56
 @include('_partials.status')
57
+@include('pagamento._partials.statistiche')
57 58
 
58 59
 <div class="row justify-content-center">
59 60
   <div class="col-xxl mb-4 mt-2">

+ 0
- 0
resources/views/prima_nota/_partials/statistiche.blade.php Datei anzeigen


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.

Laden…
Abbrechen
Speichern