Sfoglia il codice sorgente

Fatturazione SaaS: utilizzo cassa e saltacoda sul listino.

Le righe si aggiornano al pagamento senza bloccare l'ordine.

Co-authored-by: Cursor <cursoragent@cursor.com>
marcofalabretti 2 settimane fa
parent
commit
f657a2bb1b

+ 5
- 1
app/DataTables/FatturaDataTable.php Vedi File

@@ -52,7 +52,11 @@ class FatturaDataTable extends DataTable
52 52
      */
53 53
     public function query(Fattura $model): QueryBuilder
54 54
     {
55
-        return $model->newQuery();
55
+        if (Auth::user()?->hasRole('superadmin')) {
56
+            return $model->newQuery();
57
+        }
58
+
59
+        return $model->newQuery()->where('user_id', Auth::id());
56 60
     }
57 61
 
58 62
     /**

+ 15
- 8
app/DataTables/FatturaDataTableEditor.php Vedi File

@@ -4,12 +4,8 @@ namespace App\DataTables;
4 4
 
5 5
 use App\Models\Fattura;
6 6
 use Illuminate\Database\Eloquent\Model;
7
-use Illuminate\Validation\Rule;
7
+use Illuminate\Support\Facades\Auth;
8 8
 use Yajra\DataTables\DataTablesEditor;
9
-use Illuminate\Http\Request;
10
-use Illuminate\Validation\ValidationException;
11
-use Storage;
12
-use Illuminate\Support\Str;
13 9
 
14 10
 class FatturaDataTableEditor extends DataTablesEditor
15 11
 {
@@ -90,14 +86,25 @@ class FatturaDataTableEditor extends DataTablesEditor
90 86
 
91 87
   public function creating(Model $model, array $data): array
92 88
   {
93
-    // $model->roles()->sync([$data['ruolo_id']]);
89
+    $data['user_id'] = Auth::id();
94 90
     return $data;
95 91
   }
96 92
 
97 93
   public function updating(Model $model, array $data): array
98 94
   {
99
-    // dd($data['ruolo']);
100
-    // $model->roles()->sync([$data['ruolo_id']]);
95
+    if (! Auth::user()?->hasRole('superadmin') && (int) $model->user_id !== (int) Auth::id()) {
96
+      abort(404);
97
+    }
98
+    unset($data['user_id']);
99
+    return $data;
100
+  }
101
+
102
+  public function deleting(Model $model, array $data): array
103
+  {
104
+    if (! Auth::user()?->hasRole('superadmin') && (int) $model->user_id !== (int) Auth::id()) {
105
+      abort(404);
106
+    }
107
+
101 108
     return $data;
102 109
   }
103 110
   public function messages(): array

+ 70
- 39
app/DataTables/RigaFatturazioneDataTable.php Vedi File

@@ -28,24 +28,54 @@ class RigaFatturazioneDataTable extends DataTable
28 28
     {
29 29
         return (new EloquentDataTable($query))
30 30
             ->addColumn('action', function ($entity) {
31
-                return view('fatturazione.menu',['entity' => $entity]);
31
+                return view('fatturazione.menu', ['entity' => $entity]);
32 32
             })
33 33
             ->addColumn('attivita_id', function ($entity) {
34
-                return $entity->attivita->nome??'ND';
34
+                return $entity->attivita->nome ?? 'ND';
35
+            })
36
+            ->filterColumn('attivita_id', function ($query, $keyword) {
37
+                $query->whereHas('attivita', function ($q) use ($keyword) {
38
+                    $q->where('nome', 'like', "%{$keyword}%");
39
+                });
35 40
             })
36 41
             ->addColumn('dispositivo_id', function ($entity) {
37
-                return $entity->dispositivo->nome??'ND';
42
+                return $entity->dispositivo->nome ?? 'ND';
43
+            })
44
+            ->filterColumn('dispositivo_id', function ($query, $keyword) {
45
+                $query->whereHas('dispositivo', function ($q) use ($keyword) {
46
+                    $q->where('nome', 'like', "%{$keyword}%");
47
+                });
38 48
             })
39 49
             ->addColumn('prezzo', function ($entity) {
40 50
                 return number_format($entity->prezzo, 2, ',', '.') . ' €';
41 51
             })
52
+            ->filterColumn('prezzo', function ($query, $keyword) {
53
+                $normalized = str_replace(['€', ' ', ','], ['', '', '.'], $keyword);
54
+                $query->where('prezzo', 'like', "%{$normalized}%");
55
+            })
42 56
             ->addColumn('stato_display', function ($entity) {
43
-                return view('fatturazione.stato',['entity' => $entity]);
57
+                return view('fatturazione.stato', ['entity' => $entity]);
58
+            })
59
+            ->filterColumn('stato', function ($query, $keyword) {
60
+                $query->where('stato', 'like', "%{$keyword}%");
44 61
             })
45 62
             ->addColumn('listino_prezzi_display', function ($entity) {
46 63
                 return optional($entity->listinoPrezzi)->label;
47 64
             })
48
-            ->rawColumns(['note'])
65
+            // La colonna HTML si chiama listino_prezzi_id (name), non listino_prezzi_display
66
+            ->filterColumn('listino_prezzi_id', function ($query, $keyword) {
67
+                $query->whereHas('listinoPrezzi', function ($q) use ($keyword) {
68
+                    $q->where('label', 'like', "%{$keyword}%")
69
+                        ->orWhere('descrizione', 'like', "%{$keyword}%");
70
+                });
71
+            })
72
+            ->addColumn('note_display', function ($entity) {
73
+                return $entity->note;
74
+            })
75
+            ->filterColumn('note_display', function ($query, $keyword) {
76
+                $query->where('note', 'like', "%{$keyword}%");
77
+            })
78
+            ->rawColumns(['action', 'stato_display', 'note_display'])
49 79
             ->setRowId('id');
50 80
     }
51 81
 
@@ -56,23 +86,24 @@ class RigaFatturazioneDataTable extends DataTable
56 86
      */
57 87
     public function query(RigaFatturazione $model): QueryBuilder
58 88
     {
59
-        $query = $model->newQuery()->orderBy('created_at', 'desc');
89
+        $query = $model->newQuery()
90
+            ->with(['attivita', 'dispositivo', 'listinoPrezzi'])
91
+            ->orderBy('created_at', 'desc');
60 92
 
61
-        if(!$this->tutti_i_consumi){
93
+        if (! $this->tutti_i_consumi) {
62 94
             $query->whereIn('stato', [RigaFatturazione::GRATUITO, RigaFatturazione::REGISTRATO]);
63 95
         }
64
-        if($this->superadmin){
96
+
97
+        if ($this->superadmin) {
65 98
             return $query;
66
-            }
67
-            
68
-            if($this->user_id){
69
-                $query->where('user_id', $this->user_id);
70
-            }
71
-            if($this->attivita_id){
72
-                $query->where('attivita_id', $this->attivita_id);
73
-            }
99
+        }
100
+
101
+        $attivitaId = (int) $this->attivita_id;
102
+        if ($attivitaId <= 0) {
103
+            return $query->whereRaw('1 = 0');
104
+        }
74 105
 
75
-        return $query;
106
+        return $query->where('attivita_id', $attivitaId);
76 107
     }
77 108
 
78 109
     /**
@@ -149,31 +180,31 @@ class RigaFatturazioneDataTable extends DataTable
149 180
      */
150 181
     public function getColumns(): array
151 182
     {
152
-        return [
153
-            Column::make('id'),
154
-            Column::make('fattura_id')->title('Fattura'),
155
-            Column::make('attivita_id')->title('Attività')->searchable(true)->searchPanes(['show' => true]),
156
-            Column::make('dispositivo_id')->title('Dispositivo')->searchPanes(['show' => true]),
157
-            Column::make('tipo')->title('Tipo'),
158
-            Column::make('qta')->title('Qta'),
159
-            Column::make('ordini')->title('Ordini'),
160
-            Column::make('listino_prezzi_id')->title('Listino prezzi')->data('listino_prezzi_display'),
161
-            Column::make('prezzo'),
162
-            Column::make('stato')->data('stato_display'),
163
-            Column::make('note'),
183
+        $columns = [
184
+            Column::make('id')->searchable(true),
185
+            Column::make('fattura_id')->title('Fattura')->responsivePriority(2)->width('10%'),
186
+            Column::make('attivita_id')->title('Attività')->searchable(true),
187
+            Column::make('dispositivo_id')->title('Dispositivo')->responsivePriority(2)->width('10%')->searchable(true),
188
+            Column::make('tipo')->title('Tipo')->searchable(true),
189
+            Column::make('qta')->title('Qta')->searchable(true),
190
+            Column::make('ordini')->title('Ordini')->searchable(true),
191
+            Column::make('listino_prezzi_id')->title('Listino prezzi')->data('listino_prezzi_display')->name('listino_prezzi_id')->searchable(true),
192
+            Column::make('prezzo')->title('Prezzo')->responsivePriority(1)->width('15%')->searchable(true),
193
+            Column::make('stato')->data('stato_display')->name('stato')->searchable(true),
194
+            Column::make('note_display')->title('Note')->responsivePriority(2)->searchable(true),
164 195
         ];
165 196
 
166
-        if(Auth::user()->hasRole('superadmin')){
167
-            array_push($columns,
168
-            Column::make('action')
169
-                  ->title('')
170
-                  ->addClass('text-center')
171
-                  ->exportable(false)
172
-                  ->printable(false)
173
-                  ->width(60)
174
-                  ->addClass('text-center'),
175
-            );
197
+        if (Auth::user()->hasRole('superadmin')) {
198
+            $columns[] = Column::make('action')
199
+                ->title('')
200
+                ->exportable(false)
201
+                ->printable(false)
202
+                ->searchable(false)
203
+                ->orderable(false)
204
+                ->width(60)
205
+                ->addClass('text-center');
176 206
         }
207
+
177 208
         return $columns;
178 209
     }
179 210
 

+ 34
- 36
app/DataTables/RigaFatturazioneDataTableEditor.php Vedi File

@@ -3,19 +3,16 @@
3 3
 namespace App\DataTables;
4 4
 
5 5
 use App\Models\RigaFatturazione;
6
+use App\Services\Attivita\AttivitaService;
6 7
 use Illuminate\Database\Eloquent\Model;
7
-use Illuminate\Validation\Rule;
8
-use Yajra\DataTables\DataTablesEditor;
9
-use Illuminate\Http\Request;
8
+use Illuminate\Support\Facades\Auth;
9
+use Illuminate\Support\Facades\Session;
10 10
 use Illuminate\Validation\ValidationException;
11
-use Storage;
12
-use Illuminate\Support\Str;
11
+use Yajra\DataTables\DataTablesEditor;
13 12
 
14 13
 class RigaFatturazioneDataTableEditor extends DataTablesEditor
15 14
 {
16 15
   protected $model = RigaFatturazione::class;
17
-  // protected $uploadDir = '';
18
-  // protected $disk = 'modelliRicevute';
19 16
 
20 17
   protected $messages = [
21 18
     'attivita_id.required' => 'L\'attività è richiesta',
@@ -25,53 +22,32 @@ class RigaFatturazioneDataTableEditor extends DataTablesEditor
25 22
     'listino_prezzi_id.required' => 'Il listino prezzi è richiesto',
26 23
   ];
27 24
 
28
-  /**
29
-  * Get create action validation rules.
30
-  *
31
-  * @return array
32
-  */
33 25
   public function createRules(): array
34 26
   {
35 27
     return [
36
-      'attivita_id'  => 'required',
37 28
       'qta' => 'required',
38
-      // 'prezzo' => 'required',
39
-      // 'tipo' => 'required',
40 29
       'listino_prezzi_id' => 'required',
41 30
     ];
42 31
   }
43 32
 
44
-  public function createMessages(): array{
33
+  public function createMessages(): array
34
+  {
45 35
     return $this->messages;
46 36
   }
47 37
 
48
-  /**
49
-  * Get edit action validation rules.
50
-  *
51
-  * @param Model $model
52
-  * @return array
53
-  */
54 38
   public function editRules(Model $model): array
55 39
   {
56 40
     return [
57
-      'attivita_id'  => 'required',
58 41
       'qta' => 'required',
59
-      // 'prezzo' => 'required',
60
-      // 'tipo' => 'required',
61 42
       'listino_prezzi_id' => 'required',
62 43
     ];
63 44
   }
64 45
 
65
-  public function editMessages(): array{
46
+  public function editMessages(): array
47
+  {
66 48
     return $this->messages;
67 49
   }
68 50
 
69
-  /**
70
-  * Get remove action validation rules.
71
-  *
72
-  * @param Model $model
73
-  * @return array
74
-  */
75 51
   public function removeRules(Model $model): array
76 52
   {
77 53
     return [];
@@ -79,22 +55,44 @@ class RigaFatturazioneDataTableEditor extends DataTablesEditor
79 55
 
80 56
   public function creating(Model $model, array $data): array
81 57
   {
82
-    // $model->roles()->sync([$data['ruolo_id']]);
83
-   $listinoPrezzi = \App\Models\ListinoPrezzi::find($data['listino_prezzi_id']);
58
+    $data['attivita_id'] = Session::get('attivita_attuale');
59
+    AttivitaService::garantisciAttivita($data['attivita_id']);
60
+    $data['user_id'] = Auth::id();
61
+    $listinoPrezzi = \App\Models\ListinoPrezzi::find($data['listino_prezzi_id']);
84 62
     $data['prezzo'] = $listinoPrezzi->prezzo;
85 63
     $data['tipo'] = $listinoPrezzi->categoria;
64
+
86 65
     return $data;
87 66
   }
88 67
 
89 68
   public function updating(Model $model, array $data): array
90 69
   {
91
-    // dd($data['ruolo']);
92
-    // $model->roles()->sync([$data['ruolo_id']]);
70
+    AttivitaService::garantisciAttivita($model->attivita_id);
71
+    $this->assertModificabile($model);
72
+    unset($data['attivita_id'], $data['stato'], $data['ordini'], $data['metadata'], $data['fattura_id']);
73
+
74
+    return $data;
75
+  }
76
+
77
+  public function deleting(Model $model, array $data): array
78
+  {
79
+    AttivitaService::garantisciAttivita($model->attivita_id);
80
+    $this->assertModificabile($model);
81
+
93 82
     return $data;
94 83
   }
84
+
95 85
   public function messages(): array
96 86
   {
97 87
     return $this->messages;
98 88
   }
99 89
 
90
+  private function assertModificabile(Model $model): void
91
+  {
92
+    if (! $model instanceof RigaFatturazione || ! $model->isModificabile()) {
93
+      throw ValidationException::withMessages([
94
+        'stato' => 'Solo le righe in stato gratuito possono essere modificate o eliminate.',
95
+      ]);
96
+    }
97
+  }
100 98
 }

+ 46
- 9
app/Http/Controllers/AttivitaController.php Vedi File

@@ -2,8 +2,11 @@
2 2
 
3 3
 namespace App\Http\Controllers;
4 4
 
5
+
6
+use Illuminate\Routing\Controllers\Middleware;
5 7
 use Illuminate\Http\Request;
6 8
 use App\Models\Attivita;
9
+use App\Models\Evento;
7 10
 use App\DataTables\AttivitaDataTable;
8 11
 use App\DataTables\AttivitaDataTableEditor;
9 12
 use Illuminate\Support\Facades\Auth;
@@ -24,7 +27,8 @@ class AttivitaController extends Controller
24 27
     public static function middleware(): array
25 28
     {
26 29
         return [
27
-            new Middleware('permission:view-attivita', only: ['index']),
30
+            new Middleware('permission:view-attivita', only: ['index', 'show']),
31
+            new Middleware('permission:select-attivita', only: ['select']),
28 32
             new Middleware('permission:create-attivita|edit-attivita|delete-attivita', only: ['store', 'update', 'destroy']),
29 33
         ];
30 34
     }
@@ -57,13 +61,22 @@ class AttivitaController extends Controller
57 61
 
58 62
     public function show($attivita_id)
59 63
     {
60
-        $attivita = Attivita::with([
64
+        $attivita = Attivita::find($attivita_id);
65
+        $user = Auth::user();
66
+        $isSuperadmin = $user?->hasRole('superadmin');
67
+        $isOwner = $attivita && (int) $attivita->user_id === (int) $user->id;
68
+
69
+        if (! $attivita || (! $isSuperadmin && ! $isOwner)) {
70
+            abort(404);
71
+        }
72
+
73
+        $attivita->load([
61 74
             'dispositivi',
62 75
             'eventi',
63 76
             'prenotazioni',
64 77
             'piatti',
65 78
             'fornitori',
66
-        ])->find($attivita_id);
79
+        ]);
67 80
 
68 81
         return view('attivita.show', ['attivita' => $attivita]);
69 82
     }
@@ -71,9 +84,24 @@ class AttivitaController extends Controller
71 84
     public function select(Request $request)
72 85
     {
73 86
         $request->validate([
74
-            'attivita_id' => 'required|exists:attivita,id',
87
+            'attivita_id' => 'required|integer',
75 88
         ]);
76
-        Session::put('attivita_attuale', $request->attivita_id);
89
+
90
+        $attivita = Attivita::find($request->attivita_id);
91
+        $user = Auth::user();
92
+        $isSuperadmin = $user?->hasRole('superadmin');
93
+        $isOwner = $attivita && (int) $attivita->user_id === (int) $user->id;
94
+
95
+        if (! $attivita || (! $isSuperadmin && ! $isOwner)) {
96
+            abort(404);
97
+        }
98
+
99
+        if (! $attivita->is_attiva) {
100
+            return redirect()->route('attivita.index')->with('error', 'Attività non attiva');
101
+        }
102
+
103
+        Session::put('attivita_attuale', (int) $attivita->id);
104
+
77 105
         return redirect()->route('dashboard')->with('success', 'Attività selezionata con successo');
78 106
     }
79 107
 
@@ -88,12 +116,21 @@ class AttivitaController extends Controller
88 116
         ]);
89 117
     }
90 118
 
91
-    public function show_public(Request $request)
119
+    public function show_public($slug)
92 120
     {
93
-        $attivita = Attivita::find($request->attivita_id);
94
-        if(!$attivita) {
121
+        $attivita = Attivita::where('slug', $slug)->first();
122
+        if (! $attivita) {
95 123
             return redirect()->route('welcome')->with('error', 'Attività non trovata');
96 124
         }
97
-        return view('attivita.public.show', ['attivita' => $attivita]);
125
+
126
+        $eventiPubblici = Evento::pubblici($attivita->id)
127
+            ->orderBy('data_inizio')
128
+            ->orderBy('id')
129
+            ->get();
130
+
131
+        return view('attivita.public.show', [
132
+            'attivita' => $attivita,
133
+            'eventiPubblici' => $eventiPubblici,
134
+        ]);
98 135
     }
99 136
 }

+ 10
- 2
app/Http/Controllers/FatturaController.php Vedi File

@@ -2,6 +2,8 @@
2 2
 
3 3
 namespace App\Http\Controllers;
4 4
 
5
+
6
+use Illuminate\Routing\Controllers\Middleware;
5 7
 use Illuminate\Http\Request;
6 8
 use App\Models\Fattura;
7 9
 use App\DataTables\FatturaDataTable;
@@ -22,8 +24,8 @@ public static $permissions = [
22 24
 public static function middleware(): array
23 25
 {
24 26
     return [
25
-        new Middleware('permission:view-fattura', only: ['index', 'show']),
26
-        new Middleware('permission:create-fattura|edit-fattura|delete-fattura', only: ['store', 'update', 'destroy']),
27
+            new Middleware('permission:view-fattura', only: ['index', 'show', 'need_fatturazione_mensile']),
28
+            new Middleware('permission:create-fattura|edit-fattura|delete-fattura', only: ['store', 'update', 'destroy', 'registra_pagamento']),
27 29
     ];
28 30
 }
29 31
 
@@ -62,6 +64,9 @@ public function show(Request $request)
62 64
     if(!Auth::user()->hasAnyRole(['superadmin','amministratore'])) return response()->json(['error' => 'Non hai i permessi per vedere una fattura'],403);
63 65
 
64 66
     $fattura = Fattura::findOrFail($request->fattura_id);
67
+    if (! Auth::user()->hasRole('superadmin') && (int) $fattura->user_id !== (int) Auth::id()) {
68
+        abort(404);
69
+    }
65 70
     return view('fattura.show', compact('fattura'));
66 71
 }
67 72
 
@@ -74,6 +79,9 @@ public function need_fatturazione_mensile()
74 79
 public function registra_pagamento(Request $request)
75 80
 {
76 81
     $fattura = Fattura::findOrFail($request->fattura_id);
82
+    if (! Auth::user()->hasRole('superadmin') && (int) $fattura->user_id !== (int) Auth::id()) {
83
+        abort(404);
84
+    }
77 85
     $fattura->stato = Fattura::PAGATA;
78 86
     $fattura->info= json_encode(['pagamento_registrato_il' => now()]);
79 87
     $fattura->save();

+ 2
- 0
app/Http/Controllers/RigaFatturazioneController.php Vedi File

@@ -2,6 +2,8 @@
2 2
 
3 3
 namespace App\Http\Controllers;
4 4
 
5
+
6
+use Illuminate\Routing\Controllers\Middleware;
5 7
 use Illuminate\Http\Request;
6 8
 use Illuminate\Support\Facades\Storage;
7 9
 use Illuminate\Support\Facades\Validator;

+ 10
- 0
app/Models/AbstractModels/AbstractRigaFatturazione.php Vedi File

@@ -44,6 +44,11 @@ abstract class AbstractRigaFatturazione extends \Illuminate\Foundation\Auth\User
44 44
         'tipo'=>'string',
45 45
         'listino_prezzi_id' => 'integer',
46 46
         'stato'=>'string',
47
+        'note'=>'string',
48
+        'path_pdf'=>'string',
49
+        'path_xml'=>'string',
50
+        'metadata'=>'json',
51
+        'ordini'=>'integer',
47 52
         'created_at' => 'datetime',
48 53
         'updated_at' => 'datetime',
49 54
     ];
@@ -63,6 +68,11 @@ abstract class AbstractRigaFatturazione extends \Illuminate\Foundation\Auth\User
63 68
         'qta',
64 69
         'prezzo',
65 70
         'stato',
71
+        'note',
72
+        'path_pdf',
73
+        'path_xml',
74
+        'metadata',
75
+        'ordini',
66 76
         'created_at',
67 77
         'updated_at',
68 78
     ];

+ 5
- 0
app/Models/RigaFatturazione.php Vedi File

@@ -12,6 +12,11 @@ class RigaFatturazione extends \App\Models\AbstractModels\AbstractRigaFatturazio
12 12
     CONST PAGATO = 'pagato';
13 13
     CONST ANNULLATO = 'annullato';
14 14
 
15
+    public function isModificabile(): bool
16
+    {
17
+        return $this->stato === self::GRATUITO;
18
+    }
19
+
15 20
     public static function getStati()
16 21
     {
17 22
         return collect([

+ 190
- 60
app/Services/Fatturazione/RigaFatturazioneService.php Vedi File

@@ -5,81 +5,211 @@ namespace App\Services\Fatturazione;
5 5
 use App\Models\Dispositivo;
6 6
 use App\Models\RigaFatturazione;
7 7
 use App\Models\ListinoPrezzi;
8
-use App\Models\Ordine;
8
+use App\Models\SaltacodaOrdine;
9
+use App\Models\Attivita;
10
+use Illuminate\Support\Facades\DB;
11
+use Illuminate\Support\Facades\Log;
9 12
 
10 13
 class RigaFatturazioneService
11 14
 {
12
-    public function __construct()
15
+    private const META_ORDINI_IDS = 'ordini_ids';
16
+    private const META_SALTACODA_ORDINE_IDS = 'saltacoda_ordine_ids';
17
+
18
+    public function aggiornaUtilizzoDispositivo(int $dispositivo_id, ?int $ordine_id = null): array
13 19
     {
14
-    }
20
+        try {
21
+            return DB::transaction(function () use ($dispositivo_id, $ordine_id) {
22
+                // Lock sul dispositivo: serializza create/update concorrenti per questa cassa
23
+                $dispositivo = Dispositivo::query()->whereKey($dispositivo_id)->lockForUpdate()->first();
15 24
 
16
-    public function aggiornaUtilizzoDispositivo(int $dispositivo_id){
25
+                if (! $dispositivo) {
26
+                    Log::error('Dispositivo non trovato per aggiornamento utilizzo: '.$dispositivo_id);
17 27
 
18
-        $dispositivo = Dispositivo::find($dispositivo_id);
28
+                    return ['success' => false, 'message' => 'Dispositivo non trovato'];
29
+                }
19 30
 
20
-        if(!$dispositivo){
21
-            return response(['message' => 'Dispositivo non trovato'], 404);
22
-        }
31
+                $listinoPrezzi = ListinoPrezzi::where('tipo_dispositivo', $dispositivo->tipo)->first();
32
+                if (! $listinoPrezzi) {
33
+                    Log::error('ListinoPrezzi non trovato per aggiornamento utilizzo: '.$dispositivo->tipo);
23 34
 
24
-        //controllo se la RigaFatturazione è già stata fatta oggi
25
-        $RigaFatturazioneOdierna = $this->RigaFatturazioneOdiernaDispositivo($dispositivo);
35
+                    return ['success' => false, 'message' => 'ListinoPrezzi non trovato'];
36
+                }
37
+
38
+                $riga = RigaFatturazione::query()
39
+                    ->where('dispositivo_id', $dispositivo->id)
40
+                    ->whereDate('created_at', today())
41
+                    ->lockForUpdate()
42
+                    ->first();
26 43
 
27
-        if($this->RigaFatturazioneOdiernaDispositivo($dispositivo))
28
-            {
29
-                //cerco RigaFatturazione odierna
30
-                $RigaFatturazione = RigaFatturazione::where('dispositivo_id', $dispositivo->id)
31
-                ->whereDate('created_at', today())->first();
32
-                if(!$RigaFatturazione){
33
-                    return response(['message' => 'RigaFatturazione non trovata per oggi'], 404);
44
+                if (! $riga) {
45
+                    return $this->creaRigaFatturazioneOdiernaDispositivo($dispositivo, $listinoPrezzi, $ordine_id);
34 46
                 }
35
-                $RigaFatturazione->ordini = $RigaFatturazione->ordini();
36
-                
37
-                if($RigaFatturazione->ordini() > 5 && $RigaFatturazione->stato == RigaFatturazione::GRATUITO){
38
-                    $RigaFatturazione->stato = RigaFatturazione::REGISTRATO;
39
-                    $RigaFatturazione->prezzo = $RigaFatturazione->listinoPrezzi->prezzo;
40
-                }elseif($RigaFatturazione->ordini() <= 5){
41
-                    $RigaFatturazione->stato = RigaFatturazione::GRATUITO;
47
+
48
+                if ($ordine_id && $this->giaContato($riga, self::META_ORDINI_IDS, $ordine_id)) {
49
+                    return ['success' => true, 'message' => 'Utilizzo dispositivo già conteggiato per questo ordine'];
42 50
                 }
43
-                $RigaFatturazione->save();
44
-            }else{
45
-                $RigaFatturazione = $this->creaRigaFatturazioneOdiernaDispositivo($dispositivo);
46
-            }
47 51
 
48
-        return $RigaFatturazione;
52
+                $riga->ordini = (int) $riga->ordini + 1;
53
+                if ($ordine_id) {
54
+                    $riga->metadata = $this->aggiungiRiferimento($riga->metadata, self::META_ORDINI_IDS, $ordine_id);
55
+                }
56
+
57
+                if ((int) $riga->ordini > 5 && $riga->stato === RigaFatturazione::GRATUITO) {
58
+                    $riga->stato = RigaFatturazione::REGISTRATO;
59
+                    $riga->prezzo = $listinoPrezzi->prezzo;
60
+                }
61
+
62
+                $riga->save();
63
+
64
+                return ['success' => true, 'message' => 'Utilizzo dispositivo aggiornato'];
65
+            });
66
+        } catch (\Throwable $e) {
67
+            Log::error('Errore aggiornaUtilizzoDispositivo: '.$e->getMessage(), [
68
+                'dispositivo_id' => $dispositivo_id,
69
+                'ordine_id' => $ordine_id,
70
+            ]);
71
+
72
+            return ['success' => false, 'message' => 'Errore aggiornamento utilizzo dispositivo'];
73
+        }
49 74
     }
50 75
 
51
-    //Crea RigaFatturazione odierna per dispositivo
52
-    public function creaRigaFatturazioneOdiernaDispositivo(Dispositivo $dispositivo){
53
-        $RigaFatturazione = new RigaFatturazione();
54
-        // $RigaFatturazione->user_id = auth()->user()->id;
55
-        $RigaFatturazione->attivita_id = $dispositivo->attivita_id;
56
-        $RigaFatturazione->dispositivo_id = $dispositivo->id;
57
-        // $RigaFatturazione->tipo_dispositivo = $dispositivo->tipo;
58
-        $RigaFatturazione->listino_prezzi_id = ListinoPrezzi::where('tipo_dispositivo', $dispositivo->tipo)->first()->id;
59
-        $RigaFatturazione->tipo = 'consumo';
60
-        $RigaFatturazione->prezzo = 0;
61
-        $RigaFatturazione->stato = RigaFatturazione::GRATUITO;
62
-        $RigaFatturazione->note = 'Utilizzo dispositivo <b>'.$dispositivo->nome.'</b> per l\'attività <b>'.$dispositivo->attivita->nome.'</b> il <b>'.now()->format('d/m/Y').'</b>';
63
-        $RigaFatturazione->path_pdf = null;
64
-        $RigaFatturazione->path_xml = null;
65
-        $RigaFatturazione->metadata = null;
66
-        $RigaFatturazione->ordini = 1;
67
-        $RigaFatturazione->save();
68
-        return $RigaFatturazione;
76
+    public function creaRigaFatturazioneOdiernaDispositivo(
77
+        Dispositivo $dispositivo,
78
+        ?ListinoPrezzi $listinoPrezzi = null,
79
+        ?int $ordine_id = null
80
+    ): array {
81
+        $listinoPrezzi ??= ListinoPrezzi::where('tipo_dispositivo', $dispositivo->tipo)->first();
82
+        if (! $listinoPrezzi) {
83
+            Log::error('ListinoPrezzi non trovato per creazione RigaFatturazione odierna: '.$dispositivo->tipo);
84
+
85
+            return ['success' => false, 'message' => 'ListinoPrezzi non trovato'];
86
+        }
87
+
88
+        $riga = new RigaFatturazione();
89
+        $riga->attivita_id = $dispositivo->attivita_id;
90
+        $riga->dispositivo_id = $dispositivo->id;
91
+        $riga->listino_prezzi_id = $listinoPrezzi->id;
92
+        $riga->tipo = 'consumo';
93
+        $riga->prezzo = 0;
94
+        $riga->stato = RigaFatturazione::GRATUITO;
95
+        $riga->note = 'Utilizzo dispositivo '.$dispositivo->nome.' per l\'attività '.($dispositivo->attivita?->nome ?? 'N/D').' il '.now()->format('d/m/Y');
96
+        $riga->path_pdf = null;
97
+        $riga->path_xml = null;
98
+        $riga->ordini = 1;
99
+        $riga->metadata = $ordine_id
100
+            ? [self::META_ORDINI_IDS => [$ordine_id]]
101
+            : [self::META_ORDINI_IDS => []];
102
+        $riga->save();
103
+
104
+        return ['success' => true, 'message' => 'RigaFatturazione odierna creata', 'data' => $riga];
69 105
     }
70
-    //controllo qta ordini pagati oggi per dispositivo
71
-    public function fatturaDispositivoOggi(Dispositivo $dispositivo){
72
-        $ordiniPagati = Ordine::where('dispositivo_id', $dispositivo->id)
73
-        ->whereDate('created_at', now()->startOfDay())
74
-        ->where('stato', Ordine::PAGATO)
75
-        ->count();
76
-
77
-        return $ordiniPagati > 5 ? true : false;
106
+
107
+    public function RigaFatturazioneOdiernaDispositivo(Dispositivo $dispositivo): ?RigaFatturazione
108
+    {
109
+        return RigaFatturazione::query()
110
+            ->where('dispositivo_id', $dispositivo->id)
111
+            ->whereDate('created_at', today())
112
+            ->first();
113
+    }
114
+
115
+    public function aggiornaUtilizzoSaltacoda(SaltacodaOrdine $saltacodaOrdine): array
116
+    {
117
+        try {
118
+            return DB::transaction(function () use ($saltacodaOrdine) {
119
+                $attivita = Attivita::query()
120
+                    ->whereKey($saltacodaOrdine->attivita_id)
121
+                    ->lockForUpdate()
122
+                    ->first();
123
+
124
+                if (! $attivita) {
125
+                    Log::error('Attivita non trovata per saltacodaOrdine: '.$saltacodaOrdine->id);
126
+
127
+                    return ['success' => false, 'message' => 'Attivita non trovata'];
128
+                }
129
+
130
+                $listinoPrezzi = ListinoPrezzi::where('tipo_dispositivo', Dispositivo::SALTACODA)->first();
131
+                if (! $listinoPrezzi) {
132
+                    Log::error('ListinoPrezzi non trovato per saltacodaOrdine: '.$saltacodaOrdine->id);
133
+
134
+                    return ['success' => false, 'message' => 'ListinoPrezzi non trovato'];
135
+                }
136
+
137
+                $riga = RigaFatturazione::query()
138
+                    ->where('attivita_id', $attivita->id)
139
+                    ->where('tipo', Dispositivo::SALTACODA)
140
+                    ->whereDate('created_at', today())
141
+                    ->lockForUpdate()
142
+                    ->first();
143
+
144
+                if (! $riga) {
145
+                    $riga = RigaFatturazione::create([
146
+                        'listino_prezzi_id' => $listinoPrezzi->id,
147
+                        'attivita_id' => $attivita->id,
148
+                        'tipo' => Dispositivo::SALTACODA,
149
+                        'ordini' => 0,
150
+                        'prezzo' => 0,
151
+                        'stato' => RigaFatturazione::GRATUITO,
152
+                        'note' => 'Utilizzo saltacoda per l\'attività '.$attivita->nome.' il '.now()->format('d/m/Y'),
153
+                        'metadata' => [self::META_SALTACODA_ORDINE_IDS => []],
154
+                    ]);
155
+                    // Ricarica con lock dopo create
156
+                    $riga = RigaFatturazione::query()->whereKey($riga->id)->lockForUpdate()->first();
157
+                }
158
+
159
+                if (! $riga) {
160
+                    Log::error('Impossibile creare/caricare riga saltacoda per attivita '.$attivita->id);
161
+
162
+                    return ['success' => false, 'message' => 'Riga fatturazione saltacoda non disponibile'];
163
+                }
164
+
165
+                if ($this->giaContato($riga, self::META_SALTACODA_ORDINE_IDS, (int) $saltacodaOrdine->id)) {
166
+                    return ['success' => true, 'message' => 'Utilizzo saltacoda già conteggiato per questo pre-ordine'];
167
+                }
168
+
169
+                $riga->ordini = (int) $riga->ordini + 1;
170
+                $riga->metadata = $this->aggiungiRiferimento(
171
+                    $riga->metadata,
172
+                    self::META_SALTACODA_ORDINE_IDS,
173
+                    (int) $saltacodaOrdine->id
174
+                );
175
+
176
+                if ((int) $riga->ordini > 5 && $riga->stato === RigaFatturazione::GRATUITO) {
177
+                    $riga->stato = RigaFatturazione::REGISTRATO;
178
+                    $riga->prezzo = $listinoPrezzi->prezzo;
179
+                }
180
+
181
+                $riga->save();
182
+
183
+                return ['success' => true, 'message' => 'Utilizzo saltacoda aggiornato'];
184
+            });
185
+        } catch (\Throwable $e) {
186
+            Log::error('Errore aggiornaUtilizzoSaltacoda: '.$e->getMessage(), [
187
+                'saltacoda_ordine_id' => $saltacodaOrdine->id,
188
+            ]);
189
+
190
+            return ['success' => false, 'message' => 'Errore aggiornamento utilizzo saltacoda'];
191
+        }
78 192
     }
79 193
 
80
-    public function RigaFatturazioneOdiernaDispositivo(Dispositivo $dispositivo){
81
-        $RigaFatturazione = RigaFatturazione::where('dispositivo_id', $dispositivo->id)
82
-        ->whereDate('created_at', now()->startOfDay())->first();
83
-        return $RigaFatturazione;
194
+    private function giaContato(?RigaFatturazione $riga, string $chiave, int $riferimentoId): bool
195
+    {
196
+        if (! $riga) {
197
+            return false;
84 198
         }
85
-}
199
+
200
+        $meta = is_array($riga->metadata) ? $riga->metadata : [];
201
+        $ids = array_map('intval', $meta[$chiave] ?? []);
202
+
203
+        return in_array($riferimentoId, $ids, true);
204
+    }
205
+
206
+    private function aggiungiRiferimento(mixed $metadata, string $chiave, int $riferimentoId): array
207
+    {
208
+        $meta = is_array($metadata) ? $metadata : [];
209
+        $ids = array_map('intval', $meta[$chiave] ?? []);
210
+        $ids[] = $riferimentoId;
211
+        $meta[$chiave] = array_values(array_unique($ids));
212
+
213
+        return $meta;
214
+    }
215
+}

+ 8
- 0
database/seeders/ListinoPrezziSeed.php Vedi File

@@ -56,6 +56,14 @@ class ListinoPrezziSeed extends Seeder
56 56
             'tipo_prezzo' => ListinoPrezzi::TIPI_PREZZO_GIORNALIERO,
57 57
             'prezzo' => 15,
58 58
         ],
59
+        [
60
+            'label' => 'NOLO Utilizzo Saltacoda',
61
+            'descrizione' => 'Utilizzo Saltacoda',
62
+            'tipo_dispositivo' => \App\Models\Dispositivo::SALTACODA,
63
+            'categoria' => ListinoPrezzi::CATEGORIA_SERVIZIO,
64
+            'tipo_prezzo' => ListinoPrezzi::TIPI_PREZZO_GIORNALIERO,
65
+            'prezzo' => 0,
66
+        ],
59 67
     ];
60 68
 
61 69
 

+ 257
- 107
resources/views/fatturazione/index.blade.php Vedi File

@@ -1,9 +1,55 @@
1 1
 <?php
2
-use App\Models\Role;
2
+use App\Models\RigaFatturazione;
3
+use App\Models\Attivita;
3 4
 use Illuminate\Support\Facades\Auth;
5
+use Illuminate\Support\Facades\Session;
4 6
 ?>
5 7
 @php
6 8
 $configData = Helper::appClasses();
9
+
10
+$fattureEmesse = collect($fattureEmesse ?? []);
11
+$attivitaId = (int) Session::get('attivita_attuale');
12
+$attivitaNome = $attivitaId > 0
13
+    ? (Attivita::find($attivitaId)?->nome ?? 'Attività')
14
+    : (Auth::user()->hasRole('superadmin') ? 'Tutte le attività' : 'Nessuna attività');
15
+
16
+$periodoLabel = now()->translatedFormat('F Y');
17
+$periodoHint = 'Sintesi consumi: gratuiti, da pagare e già pagati';
18
+
19
+$righeQuery = RigaFatturazione::query();
20
+if ($attivitaId > 0) {
21
+    $righeQuery->where('attivita_id', $attivitaId);
22
+} elseif (! Auth::user()->hasRole('superadmin')) {
23
+    $righeQuery->whereRaw('1 = 0');
24
+}
25
+
26
+$importoRiga = function ($riga) {
27
+    $qta = (int) ($riga->qta ?? 1);
28
+    if ($qta < 1) {
29
+        $qta = 1;
30
+    }
31
+
32
+    return (float) ($riga->prezzo ?? 0) * $qta;
33
+};
34
+
35
+$righe = (clone $righeQuery)->get(['id', 'prezzo', 'qta', 'stato']);
36
+
37
+$righeGratis = $righe->where('stato', RigaFatturazione::GRATUITO);
38
+$righeDaPagare = $righe->whereIn('stato', [RigaFatturazione::REGISTRATO, RigaFatturazione::FATTURATO]);
39
+$righePagate = $righe->where('stato', RigaFatturazione::PAGATO);
40
+
41
+$countGratis = $righeGratis->count();
42
+$countDaPagare = $righeDaPagare->count();
43
+$countPagati = $righePagate->count();
44
+
45
+$importoDaPagare = (float) $righeDaPagare->sum($importoRiga);
46
+$importoPagati = (float) $righePagate->sum($importoRiga);
47
+
48
+$importoFattureEmesse = (float) $fattureEmesse->sum(function ($f) {
49
+    return (float) ($f->totale_imponibile ?? 0);
50
+});
51
+$countFatture = $fattureEmesse->count();
52
+$mostraTutti = request()->boolean('tutti_i_consumi');
7 53
 @endphp
8 54
 
9 55
 @extends('layouts/layoutMaster')
@@ -20,7 +66,6 @@ $configData = Helper::appClasses();
20 66
 ])
21 67
 @endsection
22 68
 
23
-<!-- Vendor Scripts -->
24 69
 @section('vendor-script')
25 70
 @vite([
26 71
 'resources/assets/vendor/libs/moment/moment.js',
@@ -28,155 +73,262 @@ $configData = Helper::appClasses();
28 73
 'resources/assets/vendor/libs/@form-validation/popular.js',
29 74
 'resources/assets/vendor/libs/@form-validation/bootstrap5.js',
30 75
 '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 76
 'resources/assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js'
35 77
 ])
36 78
 @endsection
37 79
 
38 80
 @section('pageTitle')
39 81
 <div class="d-flex flex-column">
40
-  <h4 class="mb-1 text-sm-small mt-md-4 mt-lg-4"> 
41
-    <i class="bx bxs-coin d-none d-md-inline-flex d-lg-inline-flex"></i>Consumi e Fatturazione</h4>
42
-    <!-- <nav aria-label="breadcrumb" style="font-size: smaller;" class="d-none d-md-block d-lg-block">
43
-            <ol class="breadcrumb breadcrumb-custom-icon">
44
-      
45
-              <li class="breadcrumb-item">
46
-                <a href="#">Fatturazione</a>
47
-                <i class="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
48
-              </li>
49
-
50
-            </ol>
51
-          </nav> -->
82
+  <h4 class="mb-0 text-sm-small">
83
+    <i class="bx bxs-coin d-none d-md-inline-flex d-lg-inline-flex"></i>Consumi e Fatturazione
84
+  </h4>
85
+  <nav aria-label="breadcrumb" style="font-size: smaller;" class="d-none d-md-block d-lg-block">
86
+    <ol class="breadcrumb breadcrumb-custom-icon mb-0">
87
+      <li class="breadcrumb-item">
88
+        <a href="#">Fatturazione</a>
89
+        <i class="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
90
+      </li>
91
+      <li class="breadcrumb-item active text-primary">Consumi e fatturazione</li>
92
+    </ol>
93
+  </nav>
52 94
 </div>
53 95
 @endsection
54 96
 
55 97
 @section('content')
56 98
 <style>
57
-  div.upload button:first-child{
99
+  div.upload button:first-child {
58 100
     display: none !important;
59 101
   }
60 102
 
61
-.bx-chef-hat{
62
-  --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");
63
-}
103
+  .fatturazione-page .tab-content {
104
+    padding: 0 !important;
105
+    margin: 0 !important;
106
+    border: 0 !important;
107
+    box-shadow: none !important;
108
+  }
64 109
 
110
+  .fatturazione-page .fatturazione-kpi .avatar-initial {
111
+    display: inline-flex;
112
+    align-items: center;
113
+    justify-content: center;
114
+    width: 2.5rem;
115
+    height: 2.5rem;
116
+  }
65 117
 </style>
66 118
 
67 119
 @include('_partials.status')
68 120
 
69
-<div class="row justify-content-center">
70
-  <div class="col-xxl mb-4 mt-2">
71
-    <ul class="nav nav-pills mb-3 gap-1" role="tablist">
72
-      <li class="nav-item" role="presentation">
73
-        <button
74
-          type="button"
75
-          class="nav-link active"
76
-          id="tab-consumi"
77
-          data-bs-toggle="pill"
78
-          data-bs-target="#pane-consumi"
79
-          role="tab"
80
-          aria-controls="pane-consumi"
81
-          aria-selected="true"
82
-        >
83
-          <i class="bx bx-list-ul me-1"></i>Consumi
84
-        </button>
85
-      </li>
86
-      <li class="nav-item" role="presentation">
87
-        <button
88
-          type="button"
89
-          class="nav-link"
90
-          id="tab-fatturazione"
91
-          data-bs-toggle="pill"
92
-          data-bs-target="#pane-fatturazione"
93
-          role="tab"
94
-          aria-controls="pane-fatturazione"
95
-          aria-selected="false"
96
-        >
97
-          <i class="bx bx-receipt me-1"></i>Fatturazione
98
-          @if(($fattureEmesse ?? collect())->count() > 0)
99
-            <span class="badge rounded-pill bg-danger ms-1">{{ $fattureEmesse->count() }}</span>
100
-          @endif
101
-        </button>
102
-      </li>
103
-    </ul>
104
-
105
-    <div class="tab-content" 
106
-    style="
107
-    padding:none;
108
-    margin:none;
109
-    border:none;
110
-    box-shadow:none;"
111
-    >
112
-      <div class="tab-pane fade show active" id="pane-consumi" role="tabpanel" aria-labelledby="tab-consumi">
113
-        <div class="card">
114
-          <div class="card-body">
115
-            {{ $dataTable_riga_fatturazione->table(['class' => 'table table-bordered']) }}
121
+<div class="fatturazione-page">
122
+  {{-- Sintesi --}}
123
+  <div class="d-flex flex-wrap align-items-end justify-content-between gap-2 mb-3">
124
+    <div>
125
+      <!-- <h5 class="mb-1">{{ $attivitaNome }}</h5> -->
126
+      <p class="text-muted small mb-0">
127
+      {{ $periodoHint }}
128
+        <!-- {{ $periodoHint }} · riferimento {{ $periodoLabel }} -->
129
+      </p>
130
+    </div>
131
+  </div>
132
+
133
+  <div class="row g-3 mb-4 fatturazione-kpi">
134
+    <div class="col-12 col-md-4">
135
+      <div class="card card-border-shadow-info h-100">
136
+        <div class="card-body">
137
+          <div class="d-flex align-items-center justify-content-between">
138
+            <div>
139
+              <span class="text-heading">Gratuiti</span>
140
+              <h5 class="mb-0 mt-1 text-info">{{ $countGratis }}</h5>
141
+              <small class="text-muted">Sotto soglia / non a pagamento</small>
142
+            </div>
143
+            <span class="avatar-initial rounded bg-label-info">
144
+              <i class="bx bx-gift bx-md"></i>
145
+            </span>
146
+          </div>
147
+        </div>
148
+      </div>
149
+    </div>
150
+
151
+    <div class="col-12 col-md-4">
152
+      <div class="card card-border-shadow-warning h-100">
153
+        <div class="card-body">
154
+          <div class="d-flex align-items-center justify-content-between">
155
+            <div>
156
+              <span class="text-heading">Da pagare</span>
157
+              <h5 class="mb-0 mt-1 text-warning">
158
+                € {{ number_format($importoDaPagare, 2, ',', '.') }}
159
+              </h5>
160
+              <small class="text-muted">
161
+                {{ $countDaPagare }} {{ $countDaPagare === 1 ? 'consumo' : 'consumi' }} registrati o fatturati
162
+              </small>
163
+            </div>
164
+            <span class="avatar-initial rounded bg-label-warning">
165
+              <i class="bx bx-time-five bx-md"></i>
166
+            </span>
167
+          </div>
168
+        </div>
169
+      </div>
170
+    </div>
171
+
172
+    <div class="col-12 col-md-4">
173
+      <div class="card card-border-shadow-success h-100">
174
+        <div class="card-body">
175
+          <div class="d-flex align-items-center justify-content-between">
176
+            <div>
177
+              <span class="text-heading">Pagati</span>
178
+              <h5 class="mb-0 mt-1 text-success">
179
+                € {{ number_format($importoPagati, 2, ',', '.') }}
180
+              </h5>
181
+              <small class="text-muted">
182
+                {{ $countPagati }} {{ $countPagati === 1 ? 'consumo saldato' : 'consumi saldati' }}
183
+              </small>
184
+            </div>
185
+            <span class="avatar-initial rounded bg-label-success">
186
+              <i class="bx bx-check-circle bx-md"></i>
187
+            </span>
116 188
           </div>
117 189
         </div>
118 190
       </div>
191
+    </div>
192
+  </div>
119 193
 
120
-      <div class="tab-pane fade" id="pane-fatturazione" role="tabpanel" aria-labelledby="tab-fatturazione">
121
-        <div class="card">
122
-          <div class="card-body">
194
+  {{-- Tab --}}
195
+  <ul class="nav nav-pills mb-3 gap-1" role="tablist">
196
+    <li class="nav-item" role="presentation">
197
+      <button
198
+        type="button"
199
+        class="nav-link active"
200
+        id="tab-consumi"
201
+        data-bs-toggle="pill"
202
+        data-bs-target="#pane-consumi"
203
+        role="tab"
204
+        aria-controls="pane-consumi"
205
+        aria-selected="true"
206
+      >
207
+        <i class="bx bx-list-ul me-1"></i>Consumi
208
+        @if($countDaPagare > 0)
209
+          <span class="badge rounded-pill bg-label-warning ms-1">{{ $countDaPagare }}</span>
210
+        @endif
211
+      </button>
212
+    </li>
213
+    <li class="nav-item" role="presentation">
214
+      <button
215
+        type="button"
216
+        class="nav-link"
217
+        id="tab-fatturazione"
218
+        data-bs-toggle="pill"
219
+        data-bs-target="#pane-fatturazione"
220
+        role="tab"
221
+        aria-controls="pane-fatturazione"
222
+        aria-selected="false"
223
+      >
224
+        <i class="bx bx-receipt me-1"></i>Fatture
225
+        @if($countFatture > 0)
226
+          <span class="badge rounded-pill bg-label-success ms-1">{{ $countFatture }}</span>
227
+        @endif
228
+      </button>
229
+    </li>
230
+  </ul>
231
+
232
+  <div class="tab-content">
233
+    <div class="tab-pane fade show active" id="pane-consumi" role="tabpanel" aria-labelledby="tab-consumi">
234
+      <div class="card shadow-sm border">
235
+        <div class="card-header d-flex flex-wrap align-items-center justify-content-between gap-2">
236
+          <div>
237
+            <h6 class="mb-0">Dettaglio consumi</h6>
238
+            <small class="text-muted">
239
+              @if($mostraTutti)
240
+                Vista completa (anche già fatturati).
241
+              @else
242
+                Vista predefinita: solo consumi non ancora fatturati (registrati / gratuiti).
243
+              @endif
244
+            </small>
245
+          </div>
246
+        </div>
247
+        <div class="card-body">
248
+          {{ $dataTable_riga_fatturazione->table(['class' => 'table table-bordered']) }}
249
+        </div>
250
+      </div>
251
+    </div>
123 252
 
124
-            <small>
125
-              <table class="table table-bordered">
253
+    <div class="tab-pane fade" id="pane-fatturazione" role="tabpanel" aria-labelledby="tab-fatturazione">
254
+      <div class="card shadow-sm border">
255
+        <div class="card-header d-flex flex-wrap align-items-center justify-content-between gap-2">
256
+          <div>
257
+            <h6 class="mb-0">Documenti fattura</h6>
258
+            <small class="text-muted">Fatture emesse legate a questa organizzazione.</small>
259
+          </div>
260
+          @if($countFatture > 0)
261
+            <span class="badge bg-label-success">
262
+              Totale € {{ number_format($importoFattureEmesse, 2, ',', '.') }}
263
+            </span>
264
+          @endif
265
+        </div>
266
+        <div class="card-body">
267
+          @if($countFatture === 0)
268
+            <div class="text-center text-muted py-5">
269
+              <i class="bx bx-receipt bx-lg mb-2 d-block"></i>
270
+              <p class="mb-1 fw-medium">Nessuna fattura emessa</p>
271
+              <p class="small mb-0">Quando verranno emesse, le troverai qui con importo e stato.</p>
272
+            </div>
273
+          @else
274
+            <div class="table-responsive">
275
+              <table class="table table-bordered table-hover align-middle mb-0">
126 276
                 <thead>
127 277
                   <tr>
128
-                    <th>Numero Fattura</th>
129
-                    <th>Data Fattura</th>
278
+                    <th>Numero</th>
279
+                    <th>Data</th>
130 280
                     <th>Descrizione</th>
131
-                    <th>Importo Fattura</th>
132
-                    <th>Stato Fattura</th>
133
-                    <th>Azioni</th>
281
+                    <th class="text-end">Importo</th>
282
+                    <th>Stato</th>
283
+                    <th class="text-center" style="width: 90px;">Apri</th>
134 284
                   </tr>
135 285
                 </thead>
136 286
                 <tbody>
137
-                    @foreach($fattureEmesse as $fattura)
287
+                  @foreach($fattureEmesse->sortByDesc('created_at') as $fattura)
138 288
                     <tr>
139
-                      <td>{{ $fattura->numero_fattura }}</td>
140
-                      <td>{{ $fattura->created_at->format('d/m/Y') }}</td>
141
-                      <td>{{ $fattura->note }}</td>
142
-                      <td>{{ number_format($fattura->totale_imponibile, 2, ',', '.') }} €</td>
289
+                      <td class="fw-medium">{{ $fattura->numero_fattura ?: '—' }}</td>
290
+                      <td>{{ optional($fattura->created_at)->format('d/m/Y') ?? '—' }}</td>
291
+                      <td>{{ $fattura->note ?: '—' }}</td>
292
+                      <td class="text-end">
293
+                        € {{ number_format((float) ($fattura->totale_imponibile ?? 0), 2, ',', '.') }}
294
+                      </td>
143 295
                       <td>@include('fattura.stato', ['entity' => $fattura])</td>
144
-                      <td>
145
-                        <a href="{{ route('fattura.show', ['fattura_id' => $fattura->id]) }}" class="btn btn-primary btn-sm">
146
-                          <i class="bx bx-arrow-back rotate-180"></i>
296
+                      <td class="text-center">
297
+                        <a
298
+                          href="{{ route('fattura.show', ['fattura_id' => $fattura->id]) }}"
299
+                          class="btn btn-sm btn-primary"
300
+                          title="Apri fattura"
301
+                        >
302
+                          <i class="bx bx-show"></i>
147 303
                         </a>
148 304
                       </td>
149 305
                     </tr>
150
-                    @endforeach
151
-                  </tbody>
152
-                </table>
153
-            </small>
154
-          </div>
306
+                  @endforeach
307
+                </tbody>
308
+              </table>
309
+            </div>
310
+          @endif
155 311
         </div>
156 312
       </div>
157 313
     </div>
158 314
   </div>
159 315
 </div>
160
-
161 316
 @endsection
162 317
 
163 318
 @section('page-script')
164
-
165
-{{$dataTable_riga_fatturazione->scripts(attributes: ['type' => 'module'])}}
319
+{{ $dataTable_riga_fatturazione->scripts(attributes: ['type' => 'module']) }}
166 320
 
167 321
 @if(Auth::user()->hasRole('superadmin'))
168 322
 <script type="module">
169
-  $(document).ready(function(){
170
-    // Editor edit
323
+  $(document).ready(function () {
171 324
     $("#dataTable_riga_fatturazione").on('click', 'a.editor_edit', function (e) {
172 325
       e.preventDefault();
173
-      window.LaravelDataTables["dataTable_riga_fatturazione-editor"].edit( $(this).closest('tr'), {
326
+      window.LaravelDataTables["dataTable_riga_fatturazione-editor"].edit($(this).closest('tr'), {
174 327
         title: 'Modifica',
175 328
         buttons: 'Aggiorna',
176 329
       });
177 330
     });
178 331
 
179
-    // Editor delete
180 332
     $("#dataTable_riga_fatturazione").on('click', 'a.editor_delete', function (e) {
181 333
       e.preventDefault();
182 334
       window.LaravelDataTables["dataTable_riga_fatturazione-editor"].remove($(this).closest('tr'), {
@@ -184,30 +336,28 @@ $configData = Helper::appClasses();
184 336
         message: 'Sei sicuro di voler eliminare il record selezionato?',
185 337
         buttons: 'Cancella record'
186 338
       });
187
-    } );
339
+    });
188 340
 
189
-    $("#dataTable_riga_fatturazione").on('dblclick', 'tbody td', function (e) {
190
-      window.LaravelDataTables["dataTable_riga_fatturazione-editor"].edit( $(this).closest('tr'), {
341
+    $("#dataTable_riga_fatturazione").on('dblclick', 'tbody td', function () {
342
+      window.LaravelDataTables["dataTable_riga_fatturazione-editor"].edit($(this).closest('tr'), {
191 343
         title: 'Modifica',
192 344
         buttons: 'Aggiorna',
193 345
       });
194 346
     });
195
-
196 347
   });
197
-
198 348
 </script>
199 349
 @endif
200 350
 
201 351
 <script>
202
-  function initComplete_riga_fatturazione(){
352
+  function initComplete_riga_fatturazione() {
203 353
     $('div.dt-buttons button').removeClass('btn-secondary');
204 354
     $('div.dt-search').addClass('mt-0 mb-4');
205 355
   }
206 356
 
207
-  function mostra_tutti_i_consumi(tutti_i_consumi){
208
-    if(tutti_i_consumi){
357
+  function mostra_tutti_i_consumi(tutti_i_consumi) {
358
+    if (tutti_i_consumi) {
209 359
       window.location.href = "{{ route('riga-fatturazione.index', ['tutti_i_consumi' => true]) }}";
210
-    }else{
360
+    } else {
211 361
       window.location.href = "{{ route('riga-fatturazione.index') }}";
212 362
     }
213 363
   }

+ 5
- 5
resources/views/fatturazione/menu.blade.php Vedi File

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

Loading…
Annulla
Salva