Ver código fonte

Cupon: generazione, stampa e pagamento in cassa con lock.

Due casse non possono più usare lo stesso codice in parallelo.

Co-authored-by: Cursor <cursoragent@cursor.com>
marcofalabretti 2 semanas atrás
pai
commit
ba954866b3

+ 159
- 0
app/DataTables/CuponDataTable.php Ver arquivo

@@ -0,0 +1,159 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Cupon;
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
+use Illuminate\Support\Str;
16
+
17
+
18
+class CuponDataTable extends DataTable
19
+{
20
+
21
+ public function __construct()
22
+ {
23
+    $this->dataTableVariable = 'dataTable_cupon';
24
+ }
25
+    /**
26
+     * Build the DataTable class.
27
+     *
28
+     * @param QueryBuilder<Cupon> $query Results from query() method.
29
+     */
30
+    public function dataTable(QueryBuilder $query): EloquentDataTable
31
+    {
32
+        return (new EloquentDataTable($query))
33
+            ->addColumn('action', function ($entity) {
34
+                return view('cupon.menu', compact('entity'));
35
+            })
36
+            ->editColumn('importo', function ($entity) {
37
+                return '€ ' . number_format($entity->importo, 2, ',', '.');
38
+            })
39
+            ->editColumn('validita_display', function ($entity) {
40
+                if($entity->validita_da->isSameDay($entity->validita_a)){
41
+                    return $entity->validita_da->format('d/m/Y');
42
+                }else{
43
+                    return $entity->validita_da->format('d/m/Y') . ' - ' . $entity->validita_a->format('d/m/Y');
44
+                }   
45
+            })
46
+            ->addColumn('stato_display', function ($entity) {
47
+                return view('cupon.stato', ['stato' => $entity->stato]);
48
+            })
49
+            ->addColumn('codice_display', function ($entity) {
50
+                return view('cupon.stato', ['codice' => $entity->codice]);
51
+            })
52
+            ->setRowId('id');
53
+    }
54
+
55
+    /**
56
+     * Get the query source of dataTable.
57
+     *
58
+     * @return QueryBuilder<Cupon>
59
+     */
60
+    public function query(Cupon $model): QueryBuilder
61
+    {
62
+        $attivitaId = (int) $this->attivita_id;
63
+        if ($attivitaId <= 0) {
64
+            return $model->newQuery()->whereRaw('1 = 0');
65
+        }
66
+
67
+        return $model->newQuery()->where('attivita_id', $attivitaId);
68
+    }
69
+
70
+    /**
71
+     * Optional method if you want to use the html builder.
72
+     */
73
+    public function html(): HtmlBuilder
74
+    {
75
+        $buttons = [];
76
+
77
+        if(Auth::user()->can('create-cupon')){
78
+            array_push($buttons, Button::make('create')
79
+            ->editor('editor')
80
+            ->formTitle('Crea nuovo cupon')
81
+            ->className('btn btn-sm btn-primary mb-4 text-white')
82
+            ->text('<i class="fas fa-plus "></i> Nuovo cupon')
83
+            );
84
+        
85
+
86
+            array_push($buttons, Button::raw('generaBatch')
87
+            ->className('btn btn-sm btn-primary mb-4 text-white')
88
+            ->text('<i class="fas fa-plus "></i> massivo')
89
+            ->action("generaBatchCupon()")
90
+            );
91
+
92
+            // array_push($buttons, Button::raw('reportCupon')
93
+            // ->className('btn btn-sm btn-primary mb-4 text-white')
94
+            // ->text('<i class="fas fa-file-alt "></i> Report cupon')
95
+            // ->action("reportCupon()")
96
+            // );
97
+            
98
+            array_push($buttons, Button::raw('stampaCupon')
99
+            ->className('btn btn-sm btn-primary mb-4 text-white')
100
+            ->text('<i class="fas fa-file-alt "></i> Stampa cupon')
101
+            ->action("window.location.href='".route('cupon.stampa')."'")
102
+            );
103
+        }
104
+
105
+        return $this->builder()
106
+                    ->setTableId($this->dataTableVariable)
107
+                    ->columns($this->getColumns())
108
+                    ->minifiedAjax()
109
+                    ->orderBy(1)
110
+                    ->selectStyleSingle()
111
+                    ->buttons($buttons)
112
+                    ->initComplete("function(settings, json){
113
+                        initComplete_cupon();
114
+                    }")
115
+                    ->language(asset('assets/Italian.json'))
116
+                    ->dom(count($buttons)==0?'rtip':'Bfrtip')
117
+                    ->editor(
118
+                        Editor::make()->fields([
119
+                        Fields\Hidden::make('attivita_id')->label('Attività')->default((int) $this->attivita_id),
120
+                        Fields\Hidden::make('stato')->label('Stato')->default(Cupon::GENERATO),
121
+                        Fields\Hidden::make('codice')->label('Codice'),
122
+                        Fields\Number::make('importo')->label('Importo')->required(),
123
+                        Fields\Text::make('nominativo')->label('Nominativo')->required(),
124
+                        Fields\Text::make('email')->label('Email'),
125
+                        Fields\Date::make('validita_da')->label('Validità da')->required(),
126
+                        Fields\Date::make('validita_a')->label('Validità a')->required(),
127
+                    ]));
128
+    }
129
+
130
+    /**
131
+     * Get the dataTable columns definition.
132
+     */
133
+    public function getColumns(): array
134
+    {
135
+        return [
136
+            Column::make('id'),
137
+            Column::make('codice_display')->title('Codice'),
138
+            Column::make('stato_display'),
139
+            Column::make('importo')->title('Importo'),
140
+            Column::make('nominativo')->title('Nominativo'),
141
+            // Column::make('email'),
142
+            Column::make('validita_display')->title('Validità'),
143
+            Column::computed('action')
144
+                  ->title('Azioni')
145
+                  ->exportable(false)
146
+                  ->printable(false)
147
+                  ->width(60)
148
+                  ->addClass('text-center'),
149
+        ];
150
+    }
151
+
152
+    /**
153
+     * Get the filename for export.
154
+     */
155
+    protected function filename(): string
156
+    {
157
+        return 'Cupon_' . date('YmdHis');
158
+    }
159
+}

+ 117
- 0
app/DataTables/CuponDataTableEditor.php Ver arquivo

@@ -0,0 +1,117 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Cupon;
6
+use App\Services\Attivita\AttivitaService;
7
+use Illuminate\Database\Eloquent\Model;
8
+use Yajra\DataTables\DataTablesEditor;
9
+use Illuminate\Support\Facades\Session;
10
+
11
+class CuponDataTableEditor extends DataTablesEditor
12
+{
13
+  protected $model = Cupon::class;
14
+  // protected $uploadDir = '';
15
+  // protected $disk = 'modelliRicevute';
16
+
17
+  protected $messages = [
18
+    'codice.required' => 'Il codice è richiesto',
19
+    'codice.unique' => 'Il codice deve essere unico',
20
+    'validita_a.after_or_equal' => 'La validità a deve essere dopo la validità da',
21
+    'validita_da.date' => 'La validità da deve essere una data valida',
22
+    'validita_a.date' => 'La validità a deve essere una data valida',
23
+    'importo.numeric' => 'L\'importo deve essere un numero',
24
+    'importo.min' => 'L\'importo deve essere maggiore di 0',
25
+    'nominativo.string' => 'Il nominativo deve essere una stringa',
26
+    'nominativo.max' => 'Il nominativo deve essere massimo 255 caratteri',
27
+    'email.email' => 'L\'email deve essere una email valida',
28
+  ];
29
+
30
+  /**
31
+  * Get create action validation rules.
32
+  *
33
+  * @return array
34
+  */
35
+  public function createRules(): array
36
+  {
37
+    return [
38
+      // 'codice'  => 'required|unique:cupon,codice',
39
+      'importo' => 'nullable|numeric|min:0',
40
+      'nominativo' => 'nullable|string|max:255',
41
+      'email' => 'nullable|email|max:255',
42
+      'validita_da' => 'nullable|date',
43
+      'validita_a' => 'nullable|date|after_or_equal:validita_da',
44
+    ];
45
+  }
46
+
47
+  public function createMessages(): array{
48
+    return $this->messages;
49
+  }
50
+
51
+  /**
52
+  * Get edit action validation rules.
53
+  *
54
+  * @param Model $model
55
+  * @return array
56
+  */
57
+  public function editRules(Model $model): array
58
+  {
59
+    return [
60
+      'codice'  => 'required|unique:cupon,codice,' . $model->id,
61
+      'importo' => 'nullable|numeric|min:0',
62
+      'nominativo' => 'nullable|string|max:255',
63
+      'email' => 'nullable|email|max:255',
64
+      'validita_da' => 'nullable|date',
65
+      'validita_a' => 'nullable|date|after_or_equal:validita_da',
66
+ 
67
+    ];
68
+  }
69
+
70
+  public function editMessages(): array{
71
+    return $this->messages;
72
+  }
73
+
74
+  /**
75
+  * Get remove action validation rules.
76
+  *
77
+  * @param Model $model
78
+  * @return array
79
+  */
80
+  public function removeRules(Model $model): array
81
+  {
82
+    return [];
83
+  }
84
+
85
+  public function creating(Model $model, array $data): array
86
+  {
87
+    $data['attivita_id'] = Session::get('attivita_attuale');
88
+    AttivitaService::garantisciAttivita($data['attivita_id']);
89
+    $data['stato'] = Cupon::GENERATO;
90
+    if(!isset($data['codice']) || empty($data['codice'])){
91
+      $data['codice'] = app('App\Http\Controllers\CuponController')->generaCodiceCupon();
92
+    }
93
+
94
+    return $data;
95
+  }
96
+
97
+  public function updating(Model $model, array $data): array
98
+  {
99
+    AttivitaService::garantisciAttivita($model->attivita_id);
100
+    unset($data['attivita_id']);
101
+
102
+    return $data;
103
+  }
104
+
105
+  public function deleting(Model $model, array $data): array
106
+  {
107
+    AttivitaService::garantisciAttivita($model->attivita_id);
108
+
109
+    return $data;
110
+  }
111
+  public function messages(): array
112
+  {
113
+    return $this->messages;
114
+  }
115
+
116
+
117
+}

+ 395
- 0
app/Http/Controllers/CuponController.php Ver arquivo

@@ -0,0 +1,395 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+
6
+use Illuminate\Routing\Controllers\Middleware;
7
+use Illuminate\Http\Request;
8
+use App\Models\Cupon;
9
+use App\DataTables\CuponDataTable;
10
+use App\DataTables\CuponDataTableEditor;
11
+use Illuminate\Support\Facades\Auth;
12
+use Illuminate\Support\Facades\Session;
13
+use Illuminate\Support\Facades\Storage;
14
+use Barryvdh\DomPDF\Facade\Pdf;
15
+use Illuminate\Support\Str;
16
+use App\Models\Attivita;
17
+use App\Models\Dispositivo;
18
+use App\Models\PrintJob;
19
+use App\Jobs\ElaboraJobStampa;
20
+use App\Services\Attivita\AttivitaService;
21
+use App\Models\Ordine;
22
+use Carbon\Carbon;
23
+
24
+class CuponController extends Controller
25
+{
26
+
27
+    public static $permission_group = "Cupon";
28
+    public static $permissions = [
29
+        'view-cupon' => 'Vedi',
30
+        'create-cupon' => 'Crea',
31
+        'edit-cupon' => 'Modifica',
32
+        'delete-cupon' => 'Elimina',
33
+    ];
34
+     
35
+    public static function middleware(): array
36
+    {
37
+        return [
38
+            new Middleware('permission:view-cupon', only: ['index', 'stampaCupon', 'pdfCupon', 'pdfCuponPersonalizzato', 'checklistCupon']),
39
+            new Middleware('permission:view-cupon|view-punto_vendita', only: ['check']),
40
+            new Middleware('permission:create-cupon|edit-cupon|delete-cupon', only: ['store', 'update', 'destroy', 'generaBatch', 'uploadCuponTemplate', 'printCupon']),
41
+        ];
42
+    }
43
+
44
+    public function index(CuponDataTable $dataTable)
45
+    {
46
+        $dataTable->attivita_id = Session::get('attivita_attuale');
47
+        return $dataTable->render('cupon.index');
48
+    }
49
+
50
+    public function stampaCupon(Request $request)
51
+    {
52
+        $attivitaId = Session::get('attivita_attuale');
53
+        $cupons = Cupon::where('attivita_id', $attivitaId)->orderBy('id', 'desc')->get();
54
+        $stampanti = Dispositivo::where('attivita_id', $attivitaId)
55
+            ->where('tipo', Dispositivo::STAMPANTE)
56
+            ->where('is_attivo', true)
57
+            ->orderBy('nome')
58
+            ->get();
59
+
60
+        return view('cupon.stampa-cupon', compact('cupons', 'stampanti'));
61
+    }
62
+
63
+
64
+    public function pdfCupon(Request $request)
65
+    {
66
+        $cupons = $this->cuponsSelezionati($request);
67
+
68
+        return Pdf::loadView('cupon.pdf', [
69
+            'cupons' => $cupons,
70
+            'nomeAttivita' => Attivita::find(Session::get('attivita_attuale'))?->nome ?? '',
71
+        ])->setPaper('a4', 'portrait')->download('cupon.pdf');
72
+    }
73
+
74
+    public function pdfCuponPersonalizzato(Request $request)
75
+    {
76
+        $attivitaId = Session::get('attivita_attuale');
77
+        $cupons = $this->cuponsSelezionati($request);
78
+
79
+        return Pdf::loadView('cupon.pdf-personalizzato', [
80
+            'cupons' => $cupons,
81
+            'templatePath' => Storage::disk('templateCupon')->path($attivitaId.'/template.png'),
82
+            'layout' => [
83
+              'show_importo' => false,
84
+              'show_nominativo' => false,
85
+              'show_validita' => false,
86
+              'codice' => [
87
+                'top' => '18mm',
88
+                'left' => '12mm',
89
+                'font_size' => '18pt',
90
+              ],
91
+            ],
92
+          ])->setPaper('a4', 'portrait')->download('cupon.pdf-personalizzato');
93
+    }
94
+
95
+    public function uploadCuponTemplate(Request $request)
96
+    {
97
+        $request->validate([
98
+            'template' => 'required|image|mimes:png,jpg,jpeg,webp|max:5120',
99
+        ]);
100
+        $attivitaId = Session::get('attivita_attuale');
101
+        AttivitaService::garantisciAttivita($attivitaId);
102
+        $path = $attivitaId.'/template.png';
103
+        // sovrascrive sempre lo stesso file
104
+        Storage::disk('templateCupon')->put(
105
+            $path,
106
+            file_get_contents($request->file('template')->getRealPath())
107
+        );
108
+        return response()->json([
109
+            'success' => true,
110
+            'message' => 'Grafica salvata.',
111
+            'url' => Storage::disk('templateCupon')->url($path),
112
+        ]);
113
+    }
114
+    
115
+
116
+    public function store(CuponDataTableEditor $editor)
117
+    {
118
+        $request = request();
119
+        $input = $request->all();
120
+        if($request->has('action')){
121
+          switch($input['action']){
122
+            case 'create':
123
+              if(!Auth::user()->can('create-cupon')) return;
124
+              break;
125
+            case 'edit':
126
+              if(!Auth::user()->can('edit-cupon')) return;
127
+              break;
128
+            case 'remove':
129
+              if(!Auth::user()->can('delete-cupon')) return;
130
+              break;
131
+          }
132
+        }
133
+        return $editor->process($request);
134
+    }
135
+
136
+    public function generaBatch(Request $request)
137
+    {
138
+        AttivitaService::garantisciAttivita(Session::get('attivita_attuale'));
139
+        
140
+        switch($request->modo){
141
+            case 'file':
142
+                //colonne CSV: nominativo,importo,validita_da,validita_a,email
143
+
144
+                $file = $request->file('file')->getRealPath();
145
+                $fileOpen = fopen($file, 'r');
146
+                fgetcsv($fileOpen, 0, ','); // scarta nominativo,importo,validita_da,validita_a
147
+                while (($line = fgetcsv($fileOpen, 0, ',')) !== false) {
148
+                    $this->generaCupon([
149
+                        'importo' => doubleval($line[1])?? $request->importo,
150
+                        'validita_da' => Carbon::parse($line[2])?? $request->validita_da,
151
+                        'validita_a' => Carbon::parse($line[3])?? $request->validita_a,
152
+                        'nominativo' => $line[0]?? '',
153
+                        'email' => $line[4]?? '',
154
+                    ]);
155
+                                };
156
+                break;
157
+            case 'numerati':
158
+                
159
+                for($i = $request->da; $i <= $request->a; $i++){
160
+                    $this->generaCupon([
161
+                        'importo' => $request->importo,
162
+                        'validita_da' => Carbon::parse($request->validita_da),
163
+                        'validita_a' => Carbon::parse($request->validita_a),
164
+                        'nominativo' => $request->nominativo?? '',
165
+                        'email' => $request->email?? '',
166
+                    ]);
167
+                }
168
+                break;
169
+        }
170
+
171
+        return redirect()->route('cupon.index')->with('success', 'Cupon generati con successo');
172
+    }
173
+
174
+    public function check(Request $request)
175
+    {
176
+        $cupon = Cupon::where('codice', $request->cupon_codice)->where('attivita_id', session('attivita_attuale'))->first();
177
+        if($cupon){
178
+            // Marca come scaduto se la data di fine validità è passata
179
+            if($cupon->stato == Cupon::GENERATO && $cupon->validita_a < Carbon::today()){
180
+                $cupon->stato = Cupon::SCADUTO;
181
+                $cupon->save();
182
+            }
183
+
184
+            // Cupon non ancora valido (troppo presto)
185
+            if($cupon->stato == Cupon::GENERATO && $cupon->validita_da->startOfDay() > Carbon::today()){
186
+                return response()->json(['success' => false, 'message' => 'Cupon non ancora valido', 'validita_da' => $cupon->validita_da->format('d/m/Y')]);
187
+            }
188
+
189
+            $ordine = Ordine::find($request->ordine_id);
190
+            if (! $ordine || (int) $ordine->attivita_id !== (int) session('attivita_attuale')) {
191
+                return response()->json(['success' => false, 'message' => 'Ordine non trovato']);
192
+            }
193
+
194
+
195
+        switch($cupon->stato){
196
+            case Cupon::GENERATO:
197
+                $importoValido = null;
198
+                $message = null;
199
+
200
+                if(!$this->checkImporto($cupon, $ordine->prezzo)){
201
+                    $success = false;
202
+                    $importoValido = false;
203
+                    $message = 'Importo del cupon non sufficiente.';
204
+                }else{
205
+                    $success = true;
206
+                    $importoValido = true;
207
+                    $message = 'Cupon utilizzabile';
208
+                }
209
+
210
+                return response()->json([
211
+                    'success'    => $success,
212
+                    'message'    => $message,
213
+                    'importo'    => $cupon->importo,
214
+                    'nominativo' => $cupon->nominativo,
215
+                    'validita_da' => $cupon->validita_da?->format('d/m/Y'),
216
+                    'validita_a'  => $cupon->validita_a?->format('d/m/Y'),
217
+                    'oggi' => $oggi = $cupon->validita_da->isSameDay($cupon->validita_a) && $cupon->validita_da->isSameDay(Carbon::today()),
218
+                ]);
219
+            case Cupon::USATO:
220
+                return response()->json(['success' => false, 'message' => 'Cupon già utilizzato']);
221
+            case Cupon::SCADUTO:
222
+                return response()->json(['success' => false, 'message' => 'Cupon scaduto', 'validita_a' => $cupon->validita_a?->format('d/m/Y')]);
223
+            default:
224
+                return response()->json(['success' => false, 'message' => 'Cupon non trovato']);
225
+        }
226
+        }else{
227
+            return response()->json(['success' => false, 'message' => 'Cupon non trovato']);
228
+        }
229
+    }
230
+
231
+    public function printCupon(Request $request)
232
+    {
233
+        $request->validate([
234
+            'ids' => 'required|array|min:1',
235
+            'ids.*' => 'integer',
236
+            'stampante_id' => 'required|integer|exists:dispositivo,id',
237
+        ]);
238
+
239
+        $attivitaId = Session::get('attivita_attuale');
240
+        $ids = $request->input('ids', []);
241
+
242
+        $stampante = Dispositivo::where('id', $request->stampante_id)
243
+            ->where('attivita_id', $attivitaId)
244
+            ->where('tipo', Dispositivo::STAMPANTE)
245
+            ->first();
246
+
247
+        if (! $stampante) {
248
+            return response()->json([
249
+                'success' => false,
250
+                'message' => 'Stampante non valida per questa attività.',
251
+            ], 422);
252
+        }
253
+
254
+        $cupons = Cupon::where('attivita_id', $attivitaId)
255
+            ->whereIn('id', $ids)
256
+            ->get();
257
+
258
+        if ($cupons->count() !== count($ids)) {
259
+            return response()->json([
260
+                'success' => false,
261
+                'message' => 'Alcuni cupon selezionati non esistono o non appartengono all\'attività corrente.',
262
+            ], 422);
263
+        }
264
+
265
+        foreach ($cupons as $cupon) {
266
+            $printJob = new PrintJob();
267
+            $printJob->fill([
268
+                'printable_id' => $cupon->id,
269
+                'tipo' => PrintJob::TIPO_CUPON,
270
+                'stampante_id' => $stampante->id,
271
+                'stato' => PrintJob::STATO_IN_ATTESA,
272
+                'payload' => null,
273
+            ]);
274
+            $printJob->save();
275
+            ElaboraJobStampa::dispatch($printJob->id);
276
+        }
277
+
278
+        return response()->json([
279
+            'success' => true,
280
+            'message' => 'Stampa inviata: '.$cupons->count().' cupon in coda.',
281
+            'count' => $cupons->count(),
282
+        ]);
283
+    }
284
+
285
+    public function checklistCupon(Request $request)
286
+    {
287
+        $attivitaId = Session::get('attivita_attuale');
288
+        $cupons = $this->cuponsSelezionati($request);
289
+
290
+        $stati = Cupon::getStati();
291
+        $statoKey = $request->input('stato');
292
+        $statoLabel = $statoKey
293
+            ? ($stati[$statoKey]['label'] ?? $statoKey)
294
+            : 'Tutti';
295
+
296
+        $filtri = [
297
+            'Codice' => $request->filled('codice') ? strtoupper($request->input('codice')) : '—',
298
+            'Solo validi oggi' => $request->boolean('oggi') ? 'Sì' : 'No',
299
+            'Validità da' => $request->filled('validita_da')
300
+                ? Carbon::parse($request->input('validita_da'))->format('d/m/Y')
301
+                : '—',
302
+            'Validità a' => $request->filled('validita_a')
303
+                ? Carbon::parse($request->input('validita_a'))->format('d/m/Y')
304
+                : '—',
305
+            'Stato' => $statoLabel,
306
+            'Tag' => $request->filled('tag') ? $request->input('tag') : '—',
307
+            'Limite' => $request->filled('limite') ? $request->input('limite') : '—',
308
+        ];
309
+
310
+        return Pdf::loadView('cupon.checklist', [
311
+            'cupons' => $cupons,
312
+            'nomeAttivita' => Attivita::find($attivitaId)?->nome ?? '',
313
+            'filtri' => $filtri,
314
+            'generatoIl' => Carbon::now()->format('d/m/Y H:i'),
315
+            'totale' => $cupons->count(),
316
+            'stati' => $stati,
317
+        ])->setPaper('a4', 'portrait')->download('checklist-cupon.pdf');
318
+    }
319
+
320
+    /**
321
+     * Cupon selezionati da ids[] (stampa PDF / checklist), scoped all'attività corrente.
322
+     */
323
+    private function cuponsSelezionati(Request $request)
324
+    {
325
+        $ids = array_values(array_filter((array) $request->input('ids', [])));
326
+        if (empty($ids)) {
327
+            abort(422, 'Nessun cupon selezionato.');
328
+        }
329
+
330
+        $cupons = Cupon::where('attivita_id', Session::get('attivita_attuale'))
331
+            ->whereIn('id', $ids)
332
+            ->orderBy('codice')
333
+            ->get();
334
+
335
+        if ($cupons->isEmpty()) {
336
+            abort(404, 'Nessun cupon trovato.');
337
+        }
338
+
339
+        return $cupons;
340
+    }
341
+
342
+//PRIVATE
343
+    private function generaCupon($data)
344
+    {
345
+        $codice = $this->generaCodiceCupon();
346
+        while (Cupon::where('codice', $codice)->exists()) {
347
+            $codice = $this->generaCodiceCupon();
348
+        }
349
+
350
+        Cupon::create([
351
+            'attivita_id' => Session::get('attivita_attuale'),
352
+            'codice' => $codice,
353
+            'importo' => $data['importo'],
354
+            'validita_da' => $data['validita_da'],
355
+            'validita_a' => $data['validita_a'],
356
+            'stato' => Cupon::GENERATO,
357
+            'nominativo' => $data['nominativo']?? '',
358
+            'email' => $data['email']?? '',
359
+        ]);
360
+    }
361
+
362
+    /**
363
+     * Codice a prova di dettatura (5 caratteri): mix lettere + numeri.
364
+     * Esclusi: 0/O, 1/I/L, 5/S, 8/B, 2/Z.
365
+     */
366
+    public function generaCodiceCupon(int $length = 5): string
367
+    {
368
+        $lettere = 'ACDEFGHJKMNPQRTUVWXY';
369
+        $numeri = '34679';
370
+        $chars = $lettere.$numeri;
371
+
372
+        // Almeno 2 numeri e il resto lettere, poi shuffle (sempre 5 caratteri)
373
+        $numCount = min(2, $length - 1);
374
+        $letterCount = $length - $numCount;
375
+
376
+        $codice = '';
377
+        for ($i = 0; $i < $letterCount; $i++) {
378
+            $codice .= $lettere[random_int(0, strlen($lettere) - 1)];
379
+        }
380
+        for ($i = 0; $i < $numCount; $i++) {
381
+            $codice .= $numeri[random_int(0, strlen($numeri) - 1)];
382
+        }
383
+
384
+        return str_shuffle($codice);
385
+    }
386
+
387
+    private function checkImporto(Cupon $cupon, $importo){
388
+        if($cupon->importo >= $importo  ){
389
+            return true;
390
+        }
391
+        return false;
392
+    }
393
+
394
+
395
+}

+ 79
- 0
app/Models/AbstractModels/AbstractCupon.php Ver arquivo

@@ -0,0 +1,79 @@
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 AbstractCupon extends \Illuminate\Foundation\Auth\User
10
+{
11
+    /**  
12
+     * The table associated with the model.
13
+     * 
14
+     * @var string
15
+     */
16
+    protected $table = 'cupon';
17
+    
18
+    /**  
19
+     * Primary key type.
20
+     * 
21
+     * @var string
22
+     */
23
+    protected $keyType = 'bigInteger';
24
+    
25
+    /**  
26
+     * The model's default values for attributes.
27
+     * 
28
+     * @var array
29
+     */
30
+    // protected $attributes = ['is_gruppo' => 0];
31
+    
32
+    /**  
33
+     * The attributes that should be cast to native types.
34
+     * 
35
+     * @var array
36
+     */
37
+    protected $casts = [
38
+        'id' => 'integer',
39
+        'attivita_id' => 'integer',
40
+        'codice' => 'string',
41
+        'stato' => 'string',
42
+        'importo' => 'decimal:2',
43
+        'nominativo' => 'string',
44
+        'email' => 'string',
45
+        'validita_da' => 'datetime',
46
+        'validita_a' => 'datetime',
47
+        'info' => 'array',
48
+        'created_at' => 'datetime',
49
+        'updated_at' => 'datetime',   
50
+    ];
51
+    
52
+    /**  
53
+     * The attributes that are mass assignable.
54
+     * 
55
+     * @var array
56
+     */
57
+    protected $fillable = [
58
+        'id',
59
+        'attivita_id',
60
+        'codice',
61
+        'stato',
62
+        'importo',
63
+        'nominativo',
64
+        'email',
65
+        'validita_da',
66
+        'validita_a',
67
+        'info',
68
+        'created_at',
69
+        'updated_at',   
70
+    ];
71
+    
72
+
73
+    public function attivita()
74
+    {
75
+        return $this->belongsTo('App\Models\Attivita', 'attivita_id' , 'id');
76
+    }
77
+
78
+
79
+}

+ 38
- 0
app/Models/Cupon.php Ver arquivo

@@ -0,0 +1,38 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+class Cupon extends \App\Models\AbstractModels\AbstractCupon
6
+{
7
+    const GENERATO = 'generato';
8
+    const DISPONIBILE = 'disponibile';
9
+    const USATO = 'usato';
10
+    const SCADUTO = 'scaduto';
11
+
12
+
13
+    public static function getStati(){
14
+        return [
15
+            self::GENERATO => [
16
+                'value' => self::GENERATO,
17
+                'color' => 'primary',
18
+                'label' => 'Generato',
19
+            ],
20
+            self::DISPONIBILE => [
21
+                'value' => self::DISPONIBILE,
22
+                'color' => 'success',
23
+                'label' => 'Disponibile',
24
+            ],
25
+            self::USATO => [
26
+                'value' => self::USATO,
27
+                'color' => 'secondary',
28
+                'label' => 'Usato',
29
+            ],
30
+            self::SCADUTO => [
31
+                'value' => self::SCADUTO,
32
+                'color' => 'danger',
33
+                'label' => 'Scaduto',
34
+            ],
35
+        ];
36
+    }
37
+
38
+}

+ 0
- 37
app/Services/Paga/Cupon.php Ver arquivo

@@ -1,37 +0,0 @@
1
-<?php
2
-
3
-namespace App\Services\Paga;
4
-
5
-use App\Models\MetodoPagamento;
6
-use App\Models\Pagamento;
7
-use App\Models\Ordine;
8
-use App\Models\Voucher;
9
-use Illuminate\Support\Facades\Http;
10
-use Illuminate\Support\Facades\Log;
11
-
12
-class Cupon
13
-{
14
-    
15
-    public function __construct(){
16
-
17
-    }
18
-
19
-    public function paga($pagamento){
20
-        $voucher = Voucher::where('codice', $voucher_codice)->first();
21
-        if($voucher && $voucher->stato == Voucher::DISPONIBILE){
22
-            $voucher->stato = Voucher::USATO;
23
-            $voucher->save();
24
-            $pagamento->stato = Pagamento::PAGATO;
25
-            $pagamento->save();
26
-            return [
27
-                'result' => true,
28
-                'message' => 'Pagamento con cupon registrato.',
29
-            ];
30
-        }else{
31
-            return [
32
-                'result' => false,
33
-                'message' => 'Voucher non trovato.',
34
-            ];
35
-        }
36
-    }
37
-}

+ 60
- 0
app/Services/Paga/PagaCupon.php Ver arquivo

@@ -0,0 +1,60 @@
1
+<?php
2
+
3
+namespace App\Services\Paga;
4
+
5
+use App\Models\Pagamento;
6
+use App\Models\Cupon;
7
+use Carbon\Carbon;
8
+use Illuminate\Support\Facades\DB;
9
+
10
+class PagaCupon
11
+{
12
+    public function paga($pagamento, $cupon_codice)
13
+    {
14
+        return DB::transaction(function () use ($pagamento, $cupon_codice) {
15
+            $cupon = Cupon::where('codice', $cupon_codice)
16
+                ->where('attivita_id', $pagamento->attivita_id)
17
+                ->lockForUpdate()
18
+                ->first();
19
+
20
+            if (! $cupon) {
21
+                return ['result' => false, 'message' => 'Cupon non trovato.'];
22
+            }
23
+            // 1) Scaduto
24
+            if ($cupon->stato == Cupon::GENERATO && $cupon->validita_a < Carbon::today()) {
25
+                $cupon->stato = Cupon::SCADUTO;
26
+                $cupon->save();
27
+
28
+                return ['result' => false, 'message' => 'Cupon scaduto.'];
29
+            }
30
+            // 2) Troppo presto
31
+            if ($cupon->stato == Cupon::GENERATO && $cupon->validita_da->copy()->startOfDay() > Carbon::today()) {
32
+                return ['result' => false, 'message' => 'Cupon non ancora valido.'];
33
+            }
34
+            // 3) Stato
35
+            if ($cupon->stato == Cupon::USATO) {
36
+                return ['result' => false, 'message' => 'Cupon già utilizzato.'];
37
+            }
38
+            if ($cupon->stato == Cupon::SCADUTO) {
39
+                return ['result' => false, 'message' => 'Cupon scaduto.'];
40
+            }
41
+            if ($cupon->stato != Cupon::GENERATO) {
42
+                return ['result' => false, 'message' => 'Cupon non utilizzabile.'];
43
+            }
44
+            // 4) Importo: cupon deve coprire l'ordine
45
+            if ($cupon->importo < $pagamento->importo) {
46
+                return ['result' => false, 'message' => 'Importo del cupon non sufficiente.'];
47
+            }
48
+            // 5) Ok → consuma
49
+            $cupon->stato = Cupon::USATO;
50
+            $cupon->save();
51
+            $pagamento->stato = Pagamento::PAGATO;
52
+            $pagamento->save();
53
+
54
+            return [
55
+                'result' => true,
56
+                'message' => 'Pagamento con cupon registrato.',
57
+            ];
58
+        });
59
+    }
60
+}

+ 173
- 0
app/Services/Stampa/CreaCupon.php Ver arquivo

@@ -0,0 +1,173 @@
1
+<?php
2
+
3
+namespace App\Services\Stampa;
4
+
5
+use App\Models\Cupon;
6
+use Mike42\Escpos\PrintConnectors\DummyPrintConnector;
7
+use Mike42\Escpos\Printer;
8
+use Carbon\Carbon;
9
+
10
+class CreaCupon
11
+{
12
+    private const LINE_WIDTH = 42;
13
+
14
+    /**
15
+     * Genera il buffer ESC/POS di un singolo cupon (payload print_job: base64).
16
+     */
17
+    public function perCupon(Cupon $cupon): array
18
+    {
19
+        $cupon->loadMissing(['attivita']);
20
+
21
+        $connector = new DummyPrintConnector;
22
+        $printer = new Printer($connector);
23
+
24
+        $this->printOne($printer, $cupon);
25
+
26
+        $printer->feed(2);
27
+        $printer->cut();
28
+
29
+        $bytes = $connector->getData();
30
+        $printer->close();
31
+
32
+        return [
33
+            'formato' => 'escpos_base64',
34
+            'dati' => base64_encode($bytes),
35
+            'cupon_id' => $cupon->id,
36
+            'cupon_codice' => $cupon->codice,
37
+            'cupon_importo' => (float) $cupon->importo,
38
+        ];
39
+    }
40
+
41
+    /**
42
+     * Genera un unico job ESC/POS con più cupon (taglio tra uno e l'altro).
43
+     *
44
+     * @param  iterable<Cupon>  $cupons
45
+     */
46
+    public function perBatch(iterable $cupons): array
47
+    {
48
+        $connector = new DummyPrintConnector;
49
+        $printer = new Printer($connector);
50
+
51
+        $ids = [];
52
+        $count = 0;
53
+
54
+        foreach ($cupons as $cupon) {
55
+            if (! $cupon instanceof Cupon) {
56
+                continue;
57
+            }
58
+            $cupon->loadMissing(['attivita']);
59
+            $this->printOne($printer, $cupon);
60
+            $printer->feed(2);
61
+            $printer->cut();
62
+            $ids[] = $cupon->id;
63
+            $count++;
64
+        }
65
+
66
+        $bytes = $connector->getData();
67
+        $printer->close();
68
+
69
+        return [
70
+            'formato' => 'escpos_base64',
71
+            'dati' => base64_encode($bytes),
72
+            'cupon_ids' => $ids,
73
+            'count' => $count,
74
+        ];
75
+    }
76
+
77
+    private function printOne(Printer $printer, Cupon $cupon): void
78
+    {
79
+        $nomeAttivita = $cupon->attivita?->nome ?? 'Cupon';
80
+
81
+        // --- Header ---
82
+        $printer->setJustification(Printer::JUSTIFY_CENTER);
83
+        $printer->setEmphasis(true);
84
+        $printer->text(mb_strtoupper($this->sanitizeLine($nomeAttivita))."\n");
85
+        $printer->setEmphasis(false);
86
+        $printer->text("CUPON\n");
87
+        $printer->text($this->separator());
88
+
89
+        // --- Codice centrato e grande ---
90
+        $printer->setJustification(Printer::JUSTIFY_CENTER);
91
+        $printer->setEmphasis(true);
92
+        $printer->setTextSize(3, 3);
93
+        $printer->text(mb_strtoupper($this->sanitizeLine((string) $cupon->codice))."\n");
94
+        $printer->setTextSize(1, 1);
95
+        $printer->setEmphasis(false);
96
+        $printer->text("\n");
97
+
98
+        // --- Importo / nominativo a sinistra ---
99
+        $printer->setJustification(Printer::JUSTIFY_LEFT);
100
+        $printer->text($this->money((float) $cupon->importo)."\n");
101
+        if (! empty($cupon->nominativo)) {
102
+            $printer->text($this->sanitizeLine((string) $cupon->nominativo)."\n");
103
+        }
104
+
105
+        // --- Validità al centro ---
106
+        $this->printValidita($printer, $cupon);
107
+
108
+        // --- Footer ---
109
+        $printer->setJustification(Printer::JUSTIFY_CENTER);
110
+        $printer->text($this->separator());
111
+        $printer->text("Presentare in cassa\n");
112
+        $printer->setFont(Printer::FONT_B);
113
+        $printer->text("VALIDO PER UN SOLO PAGAMENTO\n");
114
+        $printer->text("SegrestaFest\n");
115
+        $printer->setFont(Printer::FONT_A);
116
+        $printer->setJustification(Printer::JUSTIFY_LEFT);
117
+    }
118
+
119
+    private function printValidita(Printer $printer, Cupon $cupon): void
120
+    {
121
+        $validitaDa = $cupon->validita_da instanceof Carbon
122
+            ? $cupon->validita_da
123
+            : ($cupon->validita_da ? Carbon::parse($cupon->validita_da) : null);
124
+        $validitaA = $cupon->validita_a instanceof Carbon
125
+            ? $cupon->validita_a
126
+            : ($cupon->validita_a ? Carbon::parse($cupon->validita_a) : null);
127
+
128
+        if (! $validitaDa && ! $validitaA) {
129
+            return;
130
+        }
131
+
132
+        $printer->text("\n");
133
+        $printer->setJustification(Printer::JUSTIFY_CENTER);
134
+        $printer->setEmphasis(true);
135
+        $printer->setTextSize(2, 1);
136
+
137
+        if ($validitaDa && $validitaA && $validitaDa->isSameDay($validitaA)) {
138
+            $printer->text('Valido il '.$validitaDa->format('d/m/Y')."\n");
139
+        } else {
140
+            $printer->setTextSize(1, 1);
141
+            $printer->setEmphasis(false);
142
+            $printer->text("Validita\n");
143
+            $printer->setEmphasis(true);
144
+            $printer->setTextSize(2, 1);
145
+            if ($validitaDa) {
146
+                $printer->text('dal '.$validitaDa->format('d/m/Y')."\n");
147
+            }
148
+            if ($validitaA) {
149
+                $printer->text('al '.$validitaA->format('d/m/Y')."\n");
150
+            }
151
+        }
152
+
153
+        $printer->setTextSize(1, 1);
154
+        $printer->setEmphasis(false);
155
+    }
156
+
157
+    private function separator(): string
158
+    {
159
+        return str_repeat('-', self::LINE_WIDTH)."\n";
160
+    }
161
+
162
+    private function money(float $value): string
163
+    {
164
+        return number_format($value, 2, ',', '.').' EUR';
165
+    }
166
+
167
+    private function sanitizeLine(string $line): string
168
+    {
169
+        $line = str_replace(["\r", "\n"], ' ', $line);
170
+
171
+        return mb_substr($line, 0, 120);
172
+    }
173
+}

+ 43
- 0
database/migrations/2026_08_18_163527_cupon.php Ver arquivo

@@ -0,0 +1,43 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+use App\Models\Cupon;
7
+
8
+return new class extends Migration
9
+{
10
+    public function up(): void
11
+    {
12
+        Schema::dropIfExists('voucher');
13
+
14
+        Schema::create('cupon', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->bigInteger('attivita_id')->unsigned()->nullable();
17
+            $table->string('codice')->nullable()->unique();
18
+            $table->string('stato')->nullable();
19
+            $table->decimal('importo', 10, 2)->nullable();
20
+            $table->string('nominativo')->nullable();
21
+            $table->string('email')->nullable();
22
+            $table->timestamp('validita_da')->nullable();
23
+            $table->timestamp('validita_a')->nullable();
24
+            $table->json('info')->nullable();
25
+            $table->timestamps();
26
+        });
27
+
28
+        Schema::table('cupon', function (Blueprint $table) {
29
+            $table->foreign('attivita_id')->references('id')->on('attivita')->onDelete('set null');
30
+        });
31
+    }
32
+
33
+    /**
34
+     * Reverse the migrations.
35
+     */
36
+    public function down(): void
37
+    {
38
+        Schema::table('cupon', function (Blueprint $table) {
39
+            $table->dropForeign(['attivita_id']);
40
+        });
41
+        Schema::dropIfExists('cupon');
42
+    }
43
+};

+ 35
- 0
database/migrations/2026_08_21_224106_fix__print_job_for_cupon.php Ver arquivo

@@ -0,0 +1,35 @@
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('print_job', function (Blueprint $table) {
15
+            $table->dropForeign(['ordine_id']);
16
+            $table->bigInteger('ordine_id')->nullable()->unsigned()->change();
17
+            $table->foreign('ordine_id')->references('id')->on('ordine')->onDelete('set null');
18
+            $table->integer('printable_id')->nullable();
19
+        });
20
+    }
21
+
22
+    /**
23
+     * Reverse the migrations.
24
+     */
25
+    public function down(): void
26
+    {
27
+        Schema::table('print_job', function (Blueprint $table) {
28
+            $table->dropForeign(['ordine_id']);
29
+            $table->bigInteger('ordine_id')->unsigned()->change(); // torna non nullable
30
+            $table->foreign('ordine_id')->references('id')->on('ordine')->onDelete('cascade');
31
+            $table->dropColumn('printable_id');
32
+    
33
+        });
34
+    }
35
+};

+ 246
- 0
resources/views/cupon/checklist.blade.php Ver arquivo

@@ -0,0 +1,246 @@
1
+<!DOCTYPE html>
2
+<html lang="it">
3
+<head>
4
+  <meta charset="utf-8">
5
+  <title>Checklist cupon</title>
6
+  <style>
7
+    @page {
8
+      margin: 10mm 10mm 14mm 10mm;
9
+      size: A4 portrait;
10
+    }
11
+
12
+    * { box-sizing: border-box; }
13
+
14
+    body {
15
+      margin: 0;
16
+      padding: 0;
17
+      font-family: DejaVu Sans, sans-serif;
18
+      color: #1a1a1a;
19
+      font-size: 9pt;
20
+      line-height: 1.35;
21
+    }
22
+
23
+    .header-table {
24
+      width: 100%;
25
+      border-collapse: collapse;
26
+      margin-bottom: 8px;
27
+    }
28
+
29
+    .header-table td {
30
+      vertical-align: top;
31
+      padding: 0;
32
+    }
33
+
34
+    .header-table td.right {
35
+      text-align: right;
36
+      font-size: 8pt;
37
+      color: #555;
38
+    }
39
+
40
+    h1 {
41
+      margin: 0 0 2px;
42
+      font-size: 14pt;
43
+      font-weight: bold;
44
+    }
45
+
46
+    .subtitle {
47
+      margin: 0;
48
+      font-size: 8.5pt;
49
+      color: #666;
50
+    }
51
+
52
+    .section-title {
53
+      margin: 10px 0 5px;
54
+      font-size: 9.5pt;
55
+      font-weight: bold;
56
+      text-transform: uppercase;
57
+      letter-spacing: 0.3px;
58
+      color: #333;
59
+    }
60
+
61
+    table {
62
+      width: 100%;
63
+      border-collapse: collapse;
64
+      table-layout: fixed;
65
+    }
66
+
67
+    .filtri-table {
68
+      border: 0.6pt solid #bbb;
69
+      margin-bottom: 4px;
70
+    }
71
+
72
+    .filtri-table td {
73
+      padding: 4px 6px;
74
+      border: 0.4pt solid #ddd;
75
+      vertical-align: top;
76
+      font-size: 8.5pt;
77
+    }
78
+
79
+    .filtri-table td.label {
80
+      width: 18%;
81
+      background: #f0f0f0;
82
+      font-weight: bold;
83
+    }
84
+
85
+    .filtri-table td.value {
86
+      width: 32%;
87
+    }
88
+
89
+    .data-table {
90
+      border: 0.6pt solid #888;
91
+    }
92
+
93
+    .data-table thead {
94
+      display: table-header-group;
95
+    }
96
+
97
+    .data-table thead th {
98
+      background: #e8e8e8;
99
+      border: 0.4pt solid #aaa;
100
+      padding: 5px 4px;
101
+      font-size: 7.5pt;
102
+      text-align: left;
103
+      font-weight: bold;
104
+      text-transform: uppercase;
105
+      letter-spacing: 0.2px;
106
+    }
107
+
108
+    .data-table tbody td {
109
+      border: 0.4pt solid #ccc;
110
+      padding: 4px 4px;
111
+      vertical-align: middle;
112
+      font-size: 8.5pt;
113
+      word-wrap: break-word;
114
+    }
115
+
116
+    .data-table tbody tr {
117
+      page-break-inside: avoid;
118
+    }
119
+
120
+    .col-num { width: 4%; text-align: center; }
121
+    .col-codice { width: 12%; }
122
+    .col-nominativo { width: 20%; }
123
+    .col-importo { width: 10%; text-align: right; }
124
+    .col-validita { width: 17%; }
125
+    .col-stato { width: 11%; }
126
+    .col-tag { width: 14%; }
127
+    .col-verifica { width: 7%; text-align: center; }
128
+
129
+    .codice {
130
+      font-family: DejaVu Sans Mono, monospace;
131
+      font-weight: bold;
132
+      font-size: 9pt;
133
+      letter-spacing: 0.4px;
134
+    }
135
+
136
+    .check-box {
137
+      display: inline-block;
138
+      width: 11px;
139
+      height: 11px;
140
+      border: 0.8pt solid #222;
141
+    }
142
+
143
+    .footer-note {
144
+      margin-top: 8px;
145
+      font-size: 7.5pt;
146
+      color: #777;
147
+    }
148
+  </style>
149
+</head>
150
+<body>
151
+  @php
152
+    $filtri = $filtri ?? [];
153
+    $generatoIl = $generatoIl ?? now()->format('d/m/Y H:i');
154
+    $totale = $totale ?? (isset($cupons) ? $cupons->count() : 0);
155
+    $stati = $stati ?? \App\Models\Cupon::getStati();
156
+    $filtriPairs = collect($filtri)->chunk(2);
157
+  @endphp
158
+
159
+  <table class="header-table">
160
+    <tr>
161
+      <td>
162
+        <h1>Checklist cupon</h1>
163
+        <p class="subtitle">{{ $nomeAttivita ?? '' }}</p>
164
+      </td>
165
+      <td class="right">
166
+        Generato il {{ $generatoIl }}<br>
167
+        {{ $totale }} {{ $totale === 1 ? 'cupon' : 'cupon' }} in elenco
168
+      </td>
169
+    </tr>
170
+  </table>
171
+
172
+  @if(count($filtri))
173
+    <p class="section-title">Filtri applicati</p>
174
+    <table class="filtri-table">
175
+      @foreach($filtriPairs as $pair)
176
+        <tr>
177
+          @foreach($pair as $label => $valore)
178
+            <td class="label">{{ $label }}</td>
179
+            <td class="value">{{ $valore }}</td>
180
+          @endforeach
181
+          @if($pair->count() === 1)
182
+            <td class="label">&nbsp;</td>
183
+            <td class="value">&nbsp;</td>
184
+          @endif
185
+        </tr>
186
+      @endforeach
187
+    </table>
188
+  @endif
189
+
190
+  <p class="section-title">Elenco cupon</p>
191
+  <table class="data-table">
192
+    <thead>
193
+      <tr>
194
+        <th class="col-num">#</th>
195
+        <th class="col-codice">Codice</th>
196
+        <th class="col-nominativo">Nominativo</th>
197
+        <th class="col-importo">Importo</th>
198
+        <th class="col-validita">Validità</th>
199
+        <th class="col-stato">Stato</th>
200
+        <th class="col-tag">Tag</th>
201
+        <th class="col-verifica">OK</th>
202
+      </tr>
203
+    </thead>
204
+    <tbody>
205
+      @forelse($cupons as $index => $cupon)
206
+        @php
207
+          $info = is_array($cupon->info) ? $cupon->info : [];
208
+          $tags = $info['tag'] ?? $info['tags'] ?? [];
209
+          if (! is_array($tags)) {
210
+              $tags = $tags ? [$tags] : [];
211
+          }
212
+          $tagsLabel = count($tags) ? implode(', ', $tags) : '—';
213
+
214
+          $validitaDa = $cupon->validita_da?->format('d/m/Y');
215
+          $validitaA = $cupon->validita_a?->format('d/m/Y');
216
+          $validitaOggi = $cupon->validita_da
217
+              && $cupon->validita_a
218
+              && $cupon->validita_da->isSameDay($cupon->validita_a)
219
+              && $cupon->validita_da->isSameDay(\Carbon\Carbon::today());
220
+          $validita = $validitaOggi ? 'Oggi' : trim(($validitaDa ?? '—').' → '.($validitaA ?? '—'));
221
+
222
+          $statoLabel = $stati[$cupon->stato]['label'] ?? ucfirst($cupon->stato);
223
+        @endphp
224
+        <tr>
225
+          <td class="col-num">{{ $index + 1 }}</td>
226
+          <td class="col-codice"><span class="codice">{{ $cupon->codice }}</span></td>
227
+          <td class="col-nominativo">{{ $cupon->nominativo ?: '—' }}</td>
228
+          <td class="col-importo">{{ number_format((float) $cupon->importo, 2, ',', '.') }} €</td>
229
+          <td class="col-validita">{{ $validita }}</td>
230
+          <td class="col-stato">{{ $statoLabel }}</td>
231
+          <td class="col-tag">{{ $tagsLabel }}</td>
232
+          <td class="col-verifica"><span class="check-box"></span></td>
233
+        </tr>
234
+      @empty
235
+        <tr>
236
+          <td colspan="8" style="text-align:center;padding:12px;color:#888;">Nessun cupon in elenco.</td>
237
+        </tr>
238
+      @endforelse
239
+    </tbody>
240
+  </table>
241
+
242
+  <p class="footer-note">
243
+    Spunta manualmente la colonna OK dopo aver verificato codice, importo, validità e stato di ogni cupon.
244
+  </p>
245
+</body>
246
+</html>

+ 145
- 0
resources/views/cupon/index.blade.php Ver arquivo

@@ -0,0 +1,145 @@
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', 'Cupon')
12
+
13
+@section('vendor-style')
14
+@vite([
15
+'resources/assets/vendor/libs/datatables-bs5/datatables.bootstrap5.scss',
16
+'resources/assets/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.scss',
17
+'resources/assets/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.scss',
18
+'resources/assets/vendor/libs/flatpickr/flatpickr.scss',
19
+'resources/assets/vendor/libs/@form-validation/form-validation.scss'
20
+])
21
+@endsection
22
+
23
+<!-- Vendor Scripts -->
24
+@section('vendor-script')
25
+@vite([
26
+'resources/assets/vendor/libs/moment/moment.js',
27
+'resources/assets/vendor/libs/flatpickr/flatpickr.js',
28
+'resources/assets/vendor/libs/@form-validation/popular.js',
29
+'resources/assets/vendor/libs/@form-validation/bootstrap5.js',
30
+'resources/assets/vendor/libs/@form-validation/auto-focus.js',
31
+'resources/assets/vendor/libs/@form-validation/popular.js',
32
+'resources/assets/vendor/libs/@form-validation/bootstrap5.js',
33
+'resources/assets/vendor/libs/@form-validation/auto-focus.js',
34
+'resources/assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js'
35
+])
36
+@endsection
37
+
38
+@section('pageTitle')
39
+<div class="d-flex flex-column">
40
+  <h4 class="mb-1 text-sm-small mt-md-4 mt-lg-4"> 
41
+    <i class="bx bx-purchase-tag-alt d-none d-md-inline-flex d-lg-inline-flex"></i> Cupon</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 active text-primary">
46
+                <a href="{{ route('cupon.index') }}">Cupon</a>
47
+              </li>
48
+            </ol>
49
+          </nav> -->
50
+</div>
51
+@endsection
52
+
53
+@section('content')
54
+<style>
55
+  div.upload button:first-child{
56
+    display: none !important;
57
+  }
58
+
59
+.bx-chef-hat{
60
+  --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");
61
+}
62
+</style>
63
+
64
+@include('_partials.status')
65
+
66
+<div class="row justify-content-center">
67
+  <div class="col-xxl mb-4 mt-2">
68
+    <div class="card">
69
+      <div class="card-body">
70
+        {{ $dataTable_cupon->table(['class' => 'table table-bordered']) }}
71
+      </div>
72
+    </div>
73
+  </div>
74
+</div>
75
+
76
+<div class="modal fade" id="basicModal" tabindex="-1" aria-hidden="true">
77
+            <div class="modal-dialog" role="document">
78
+              <div class="modal-content">
79
+                <div class="modal-header">
80
+                  <h5 class="modal-title" id="exampleModalLabel1">Genera cupon massivi</h5>
81
+                  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
82
+                </div>
83
+                <div class="modal-body">
84
+                  @include('cupon.modal.genera-batch')
85
+                </div>
86
+                <!-- <div class="modal-footer">
87
+                  <button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">Close</button>
88
+                  <button type="button" class="btn btn-primary">Save changes</button>
89
+                </div> -->
90
+              </div>
91
+            </div>
92
+          </div>
93
+
94
+@endsection
95
+
96
+@section('page-script')
97
+
98
+{{$dataTable_cupon->scripts(attributes: ['type' => 'module'])}}
99
+
100
+
101
+<script type="module">
102
+  $(document).ready(function(){
103
+    // Editor edit
104
+    $("#dataTable_cupon").on('click', 'a.editor_edit', function (e) {
105
+      e.preventDefault();
106
+      window.LaravelDataTables["dataTable_cupon-editor"].edit( $(this).closest('tr'), {
107
+        title: 'Modifica',
108
+        buttons: 'Aggiorna',
109
+      });
110
+    });
111
+
112
+    // Editor delete
113
+    $("#dataTable_cupon").on('click', 'a.editor_delete', function (e) {
114
+      e.preventDefault();
115
+      window.LaravelDataTables["dataTable_cupon-editor"].remove($(this).closest('tr'), {
116
+        title: 'Cancella record',
117
+        message: 'Sei sicuro di voler eliminare il record selezionato?',
118
+        buttons: 'Cancella record'
119
+      });
120
+    } );
121
+
122
+    $("#dataTable_cupon").on('dblclick', 'tbody td', function (e) {
123
+      window.LaravelDataTables["dataTable_cupon-editor"].edit( $(this).closest('tr'), {
124
+        title: 'Modifica',
125
+        buttons: 'Aggiorna',
126
+      });
127
+    });
128
+
129
+  });
130
+
131
+</script>
132
+
133
+<script>
134
+  function initComplete_cupon(){
135
+    $('div.dt-buttons button').removeClass('btn-secondary');
136
+    $('div.dt-search').addClass('mt-0 mb-4');
137
+    $('div.dt-buttons').removeClass('btn-group');
138
+  }
139
+
140
+function generaBatchCupon(){
141
+
142
+$('#basicModal').modal('show');
143
+}
144
+</script>
145
+@endsection

+ 29
- 0
resources/views/cupon/menu.blade.php Ver arquivo

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

+ 110
- 0
resources/views/cupon/modal/genera-batch.blade.php Ver arquivo

@@ -0,0 +1,110 @@
1
+@php
2
+use Carbon\Carbon;
3
+@endphp
4
+
5
+<style>
6
+    .tab-content {
7
+        padding: 0 !important;
8
+}
9
+</style>
10
+
11
+<form id="form-genera-batch-cupon" action="{{ route('cupon.genera-batch') }}" method="POST" enctype="multipart/form-data">
12
+  @csrf
13
+
14
+  <ul class="nav nav-tabs mb-3" role="tablist">
15
+    <li class="nav-item" role="presentation">
16
+      <button
17
+        class="nav-link active"
18
+        id="tab-batch-file"
19
+        data-bs-toggle="tab"
20
+        data-bs-target="#pane-batch-file"
21
+        type="button"
22
+        role="tab"
23
+        aria-controls="pane-batch-file"
24
+        aria-selected="true"
25
+      >
26
+        <i class="bx bx-file me-1"></i>Da file CSV / Excel
27
+      </button>
28
+    </li>
29
+    <li class="nav-item" role="presentation">
30
+      <button
31
+        class="nav-link"
32
+        id="tab-batch-numerati"
33
+        data-bs-toggle="tab"
34
+        data-bs-target="#pane-batch-numerati"
35
+        type="button"
36
+        role="tab"
37
+        aria-controls="pane-batch-numerati"
38
+        aria-selected="false"
39
+      >
40
+        <i class="bx bx-list-ol me-1"></i>Numerati da–a
41
+      </button>
42
+    </li>
43
+  </ul>
44
+
45
+  <input type="hidden" name="modo" id="batch-modo" value="file">
46
+
47
+  <div class="row g-3 mb-3">
48
+    <div class="col-md-4">
49
+      <label for="batch-importo" class="form-label">Importo (€)</label>
50
+      <input type="number" step="0.01" min="0" class="form-control" id="batch-importo" name="importo" value="0">
51
+    </div>
52
+    <div class="col-md-4">
53
+      <label for="batch-validita-da" class="form-label">Validità da</label>
54
+      <input type="date" class="form-control" id="batch-validita-da" name="validita_da" value="{{ Carbon::today()->format('Y-m-d') }}" required>
55
+    </div>
56
+    <div class="col-md-4">
57
+      <label for="batch-validita-a" class="form-label">Validità a</label>
58
+      <input type="date" class="form-control" id="batch-validita-a" name="validita_a" value="{{ Carbon::today()->format('Y-m-d') }}" required>
59
+    </div>
60
+  </div>
61
+
62
+  <div class="tab-content">
63
+    <div class="tab-pane fade show active" id="pane-batch-file" role="tabpanel" aria-labelledby="tab-batch-file">
64
+      <div class="mb-3">
65
+        <label for="batch-file" class="form-label">File CSV o Excel</label>
66
+        <input type="file" class="form-control" id="batch-file" name="file" accept=".csv,.xls,.xlsx">
67
+        <div class="form-text">
68
+          Ordine delle colonne: <br>
69
+          <code>nominativo,importo,validita_da,validita_a,email</code>. <br> Un coupon per riga. Se ometti i valori delle colonne saranno usati i valori predefiniti (escluso il nominativo).
70
+        </div>
71
+      </div>
72
+    </div>
73
+
74
+    <div class="tab-pane fade" id="pane-batch-numerati" role="tabpanel" aria-labelledby="tab-batch-numerati">
75
+      <p class="small text-muted mb-3">Genera coupon numerati, senza nominativo.</p>
76
+      <div class="row g-3">
77
+        <div class="col-md-6">
78
+          <label for="batch-da" class="form-label">Da</label>
79
+          <input type="number" min="1" class="form-control" id="batch-da" name="da" placeholder="es. 1">
80
+        </div>
81
+        <div class="col-md-6">
82
+          <label for="batch-a" class="form-label">A</label>
83
+          <input type="number" min="1" class="form-control" id="batch-a" name="a" placeholder="es. 50">
84
+        </div>
85
+      </div>
86
+    </div>
87
+  </div>
88
+
89
+  <div class="d-flex justify-content-end gap-2 mt-4">
90
+    <button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">Annulla</button>
91
+    <button type="submit" class="btn btn-primary">
92
+      <i class="bx bx-plus me-1"></i>Genera
93
+    </button>
94
+  </div>
95
+</form>
96
+
97
+<script>
98
+  document.getElementById('tab-batch-file')?.addEventListener('shown.bs.tab', function () {
99
+    document.getElementById('batch-modo').value = 'file';
100
+    document.getElementById('batch-file')?.setAttribute('required', 'required');
101
+    document.getElementById('batch-da')?.removeAttribute('required');
102
+    document.getElementById('batch-a')?.removeAttribute('required');
103
+  });
104
+  document.getElementById('tab-batch-numerati')?.addEventListener('shown.bs.tab', function () {
105
+    document.getElementById('batch-modo').value = 'numerati';
106
+    document.getElementById('batch-file')?.removeAttribute('required');
107
+    document.getElementById('batch-da')?.setAttribute('required', 'required');
108
+    document.getElementById('batch-a')?.setAttribute('required', 'required');
109
+  });
110
+</script>

+ 190
- 0
resources/views/cupon/pdf-personalizzato.blade.php Ver arquivo

@@ -0,0 +1,190 @@
1
+<!DOCTYPE html>
2
+<html lang="it">
3
+<head>
4
+  <meta charset="utf-8">
5
+  <title>Cupon personalizzati</title>
6
+  <style>
7
+    @page {
8
+      margin: 8mm;
9
+      size: A4 portrait;
10
+    }
11
+
12
+    * { box-sizing: border-box; }
13
+
14
+    body {
15
+      margin: 0;
16
+      padding: 0;
17
+      font-family: DejaVu Sans, sans-serif;
18
+      color: #111;
19
+    }
20
+
21
+    /*
22
+      1 colonna x 5 biglietti su A4.
23
+      Dimensioni del singolo ticket (regola qui se cambi il template).
24
+    */
25
+    .ticket {
26
+      position: relative;
27
+      width: 194mm;
28
+      height: 52mm;
29
+      margin: 0 auto 2.5mm auto;
30
+      overflow: hidden;
31
+      page-break-inside: avoid;
32
+      background: #f7f7f7;
33
+      border: 0.4pt dashed #ccc;
34
+    }
35
+
36
+    .ticket-bg {
37
+      position: absolute;
38
+      top: 0;
39
+      left: 0;
40
+      width: 100%;
41
+      height: 100%;
42
+      border: none;
43
+    }
44
+
45
+    /* Placeholder se manca il template */
46
+    .ticket-placeholder {
47
+      position: absolute;
48
+      top: 0;
49
+      left: 0;
50
+      width: 100%;
51
+      height: 100%;
52
+      text-align: center;
53
+      color: #bbb;
54
+      font-size: 9pt;
55
+      padding-top: 20mm;
56
+    }
57
+
58
+    /*
59
+      Posizioni fisse dei dati (sovraimpressione).
60
+      Regola top/left/font in base al template grafico.
61
+      Valori di default pensati per stub a destra (stile cinema).
62
+    */
63
+    .field-codice {
64
+      position: absolute;
65
+      top: {{ $layout['codice']['top'] ?? '18mm' }};
66
+      left: {{ $layout['codice']['left'] ?? '12mm' }};
67
+      width: {{ $layout['codice']['width'] ?? '120mm' }};
68
+      font-size: {{ $layout['codice']['font_size'] ?? '18pt' }};
69
+      font-weight: bold;
70
+      letter-spacing: 1.5px;
71
+      text-align: {{ $layout['codice']['align'] ?? 'left' }};
72
+      color: {{ $layout['codice']['color'] ?? '#111' }};
73
+      line-height: 1.1;
74
+    }
75
+
76
+    .field-importo {
77
+      position: absolute;
78
+      top: {{ $layout['importo']['top'] ?? '32mm' }};
79
+      left: {{ $layout['importo']['left'] ?? '12mm' }};
80
+      width: {{ $layout['importo']['width'] ?? '60mm' }};
81
+      font-size: {{ $layout['importo']['font_size'] ?? '12pt' }};
82
+      font-weight: bold;
83
+      text-align: {{ $layout['importo']['align'] ?? 'left' }};
84
+      color: {{ $layout['importo']['color'] ?? '#111' }};
85
+    }
86
+
87
+    .field-nominativo {
88
+      position: absolute;
89
+      top: {{ $layout['nominativo']['top'] ?? '32mm' }};
90
+      left: {{ $layout['nominativo']['left'] ?? '70mm' }};
91
+      width: {{ $layout['nominativo']['width'] ?? '70mm' }};
92
+      font-size: {{ $layout['nominativo']['font_size'] ?? '9pt' }};
93
+      text-transform: uppercase;
94
+      text-align: {{ $layout['nominativo']['align'] ?? 'left' }};
95
+      color: {{ $layout['nominativo']['color'] ?? '#333' }};
96
+    }
97
+
98
+    .field-validita {
99
+      position: absolute;
100
+      top: {{ $layout['validita']['top'] ?? '42mm' }};
101
+      left: {{ $layout['validita']['left'] ?? '12mm' }};
102
+      width: {{ $layout['validita']['width'] ?? '120mm' }};
103
+      font-size: {{ $layout['validita']['font_size'] ?? '8pt' }};
104
+      text-align: {{ $layout['validita']['align'] ?? 'left' }};
105
+      color: {{ $layout['validita']['color'] ?? '#555' }};
106
+    }
107
+  </style>
108
+</head>
109
+<body>
110
+@php
111
+  use Carbon\Carbon;
112
+
113
+  $cupons = isset($cupons) ? collect($cupons)->values() : collect();
114
+  $layout = $layout ?? [];
115
+
116
+  // Mostra solo codice di default; gli altri campi si attivano da controller
117
+  $showImporto = (bool) ($layout['show_importo'] ?? false);
118
+  $showNominativo = (bool) ($layout['show_nominativo'] ?? false);
119
+  $showValidita = (bool) ($layout['show_validita'] ?? false);
120
+
121
+  // Path locale template (preferito da DomPDF)
122
+  $templatePath = $templatePath ?? null;
123
+  if (!$templatePath && !empty($template)) {
124
+    // $template può essere path relativo a public/ oppure assoluto
125
+    $candidate = str_starts_with($template, '/') || preg_match('/^[A-Za-z]:\\\\/', $template)
126
+      ? $template
127
+      : public_path(ltrim($template, '/'));
128
+    if (is_file($candidate)) {
129
+      $templatePath = $candidate;
130
+    }
131
+  }
132
+
133
+  $hasTemplate = $templatePath && (
134
+    str_starts_with((string) $templatePath, 'http://')
135
+    || str_starts_with((string) $templatePath, 'https://')
136
+    || is_file($templatePath)
137
+  );
138
+
139
+  $pages = $cupons->chunk(5); // 5 biglietti per foglio A4
140
+@endphp
141
+
142
+@forelse($pages as $pageCupons)
143
+  @foreach($pageCupons as $cupon)
144
+    @php
145
+      $da = $cupon->validita_da ? Carbon::parse($cupon->validita_da)->format('d/m/Y') : null;
146
+      $a = $cupon->validita_a ? Carbon::parse($cupon->validita_a)->format('d/m/Y') : null;
147
+      $soloOggi = $cupon->validita_da
148
+        && $cupon->validita_a
149
+        && Carbon::parse($cupon->validita_da)->isSameDay(Carbon::parse($cupon->validita_a))
150
+        && Carbon::parse($cupon->validita_da)->isSameDay(Carbon::today());
151
+      $validitaLabel = $soloOggi
152
+        ? 'Oggi'
153
+        : (($da && $a && $da === $a) ? $da : trim(($da ?? '').(($da && $a) ? ' – ' : '').($a ?? '')));
154
+    @endphp
155
+
156
+    <div class="ticket">
157
+      @if($hasTemplate)
158
+        <img class="ticket-bg" src="{{ $templatePath }}" alt="">
159
+      @else
160
+        <div class="ticket-placeholder">
161
+          Template non impostato<br>
162
+          <span style="font-size:8pt;">Carica la grafica del singolo cupon</span>
163
+        </div>
164
+      @endif
165
+
166
+      {{-- Codice: sempre stampato --}}
167
+      <div class="field-codice">{{ $cupon->codice }}</div>
168
+
169
+      @if($showImporto)
170
+        <div class="field-importo">{{ number_format((float) $cupon->importo, 2, ',', '.') }} €</div>
171
+      @endif
172
+
173
+      @if($showNominativo && !empty($cupon->nominativo))
174
+        <div class="field-nominativo">{{ $cupon->nominativo }}</div>
175
+      @endif
176
+
177
+      @if($showValidita)
178
+        <div class="field-validita">Valido {{ $validitaLabel ?: '—' }}</div>
179
+      @endif
180
+    </div>
181
+  @endforeach
182
+
183
+  @if(!$loop->last)
184
+    <div style="page-break-after: always;"></div>
185
+  @endif
186
+@empty
187
+  <p style="padding: 20mm; color: #888;">Nessun cupon da stampare.</p>
188
+@endforelse
189
+</body>
190
+</html>

+ 257
- 0
resources/views/cupon/pdf.blade.php Ver arquivo

@@ -0,0 +1,257 @@
1
+<!DOCTYPE html>
2
+<html lang="it">
3
+<head>
4
+  <meta charset="utf-8">
5
+  <title>Cupon</title>
6
+  <style>
7
+    @page {
8
+      margin: 8mm;
9
+      size: A4 portrait;
10
+    }
11
+
12
+    * { box-sizing: border-box; }
13
+
14
+    body {
15
+      margin: 0;
16
+      padding: 0;
17
+      font-family: DejaVu Sans, sans-serif;
18
+      color: #1a1a1a;
19
+      font-size: 9pt;
20
+    }
21
+
22
+    .page-table {
23
+      width: 100%;
24
+      border-collapse: collapse;
25
+      table-layout: fixed;
26
+    }
27
+
28
+    .page-table td.slot {
29
+      width: 50%;
30
+      vertical-align: top;
31
+      padding: 2mm;
32
+    }
33
+
34
+    .ticket {
35
+      width: 100%;
36
+      border-collapse: collapse;
37
+      border: 0.6pt solid #e5e5e5;
38
+      border-left: 1.8pt solid #111;
39
+      table-layout: fixed;
40
+    }
41
+
42
+    .ticket td.content {
43
+      padding: 3.5mm 4mm;
44
+      vertical-align: top;
45
+      border: none;
46
+    }
47
+
48
+    .header-table {
49
+      width: 100%;
50
+      border-collapse: collapse;
51
+      table-layout: fixed;
52
+    }
53
+
54
+    .header-table td {
55
+      padding: 0;
56
+      vertical-align: middle;
57
+    }
58
+
59
+    .header-table td.right {
60
+      text-align: right;
61
+      font-size: 7pt;
62
+      letter-spacing: 0.8px;
63
+      text-transform: uppercase;
64
+      color: #9a9a9a;
65
+    }
66
+
67
+    .logo {
68
+      width: 12mm;
69
+      height: auto;
70
+      max-height: 8mm;
71
+    }
72
+
73
+    .brand {
74
+      margin: 0 0 0 2mm;
75
+      font-size: 7.5pt;
76
+      letter-spacing: 0.4px;
77
+      text-transform: uppercase;
78
+      color: #666;
79
+    }
80
+
81
+    .codice {
82
+      margin: 3mm 0 0 0;
83
+      font-size: 15pt;
84
+      font-weight: bold;
85
+      letter-spacing: 2px;
86
+      line-height: 1.1;
87
+    }
88
+
89
+    .value-row {
90
+      width: 100%;
91
+      border-collapse: collapse;
92
+      table-layout: fixed;
93
+      margin-top: 2.5mm;
94
+    }
95
+
96
+    .value-row td {
97
+      padding: 0;
98
+      vertical-align: bottom;
99
+    }
100
+
101
+    .importo {
102
+      margin: 0;
103
+      font-size: 12pt;
104
+      font-weight: bold;
105
+    }
106
+
107
+    .nominativo {
108
+      margin: 0;
109
+      font-size: 8pt;
110
+      color: #555;
111
+      text-align: right;
112
+      text-transform: uppercase;
113
+    }
114
+
115
+    .divider {
116
+      border-top: 0.5pt solid #eee;
117
+      margin: 3mm 0 2mm 0;
118
+      height: 0;
119
+      line-height: 0;
120
+      font-size: 0;
121
+    }
122
+
123
+    .meta-row {
124
+      width: 100%;
125
+      border-collapse: collapse;
126
+      table-layout: fixed;
127
+    }
128
+
129
+    .meta-row td {
130
+      padding: 0;
131
+      font-size: 7pt;
132
+      color: #888;
133
+      vertical-align: top;
134
+    }
135
+
136
+    .meta-row td.right {
137
+      text-align: right;
138
+    }
139
+  </style>
140
+</head>
141
+<body>
142
+@php
143
+  use Carbon\Carbon;
144
+  use App\Models\Attivita;
145
+
146
+  $cupons = isset($cupons) ? collect($cupons)->values() : collect();
147
+  $attivita = $attivita ?? Attivita::find(session('attivita_attuale'));
148
+  $nomeAttivita = $nomeAttivita ?? ($attivita->nome ?? 'Cupon');
149
+
150
+  // DomPDF preferisce path locali assoluti
151
+  $logoFestS = public_path('assets/img/logo_fest_S.png');
152
+  $logoFest = public_path('assets/img/logo_fest.png');
153
+  $logoPath = is_file($logoFestS) ? $logoFestS : $logoFest;
154
+
155
+  if ($attivita && !empty($attivita->path_logo)) {
156
+    $raw = (string) $attivita->path_logo;
157
+    if (str_starts_with($raw, 'http://') || str_starts_with($raw, 'https://')) {
158
+      $logoPath = $raw;
159
+    } else {
160
+      $candidate = public_path(ltrim($raw, '/'));
161
+      if (is_file($candidate)) {
162
+        $logoPath = $candidate;
163
+      }
164
+    }
165
+  }
166
+
167
+  $pages = $cupons->chunk(10);
168
+@endphp
169
+
170
+@forelse($pages as $pageCupons)
171
+  @php $rows = $pageCupons->values()->chunk(2); @endphp
172
+
173
+  <table class="page-table">
174
+    @foreach($rows as $row)
175
+      <tr>
176
+        @foreach($row as $cupon)
177
+          @php
178
+            $da = $cupon->validita_da ? Carbon::parse($cupon->validita_da)->format('d/m/Y') : null;
179
+            $a = $cupon->validita_a ? Carbon::parse($cupon->validita_a)->format('d/m/Y') : null;
180
+            $soloOggi = $cupon->validita_da
181
+              && $cupon->validita_a
182
+              && Carbon::parse($cupon->validita_da)->isSameDay(Carbon::parse($cupon->validita_a))
183
+              && Carbon::parse($cupon->validita_da)->isSameDay(Carbon::today());
184
+            $validitaLabel = $soloOggi
185
+              ? 'Oggi'
186
+              : (($da && $a && $da === $a) ? $da : trim(($da ?? '').(($da && $a) ? ' – ' : '').($a ?? '')));
187
+          @endphp
188
+          <td class="slot">
189
+            <table class="ticket">
190
+              <tr>
191
+                <td class="content">
192
+                  <table class="header-table">
193
+                    <tr>
194
+                      <td style="width: 70%;">
195
+                        <table style="border-collapse: collapse;">
196
+                          <tr>
197
+                            <td style="vertical-align: middle; padding: 0;">
198
+                              @if($logoPath && (str_starts_with((string) $logoPath, 'http') || is_file($logoPath)))
199
+                                <img class="logo" src="{{ $logoPath }}" alt="logo">
200
+                              @endif
201
+                            </td>
202
+                            <td style="vertical-align: middle; padding: 0;">
203
+                              <span class="brand">{{ $nomeAttivita }}</span>
204
+                            </td>
205
+                          </tr>
206
+                        </table>
207
+                      </td>
208
+                      <td class="right" style="width: 30%;">Cupon</td>
209
+                    </tr>
210
+                  </table>
211
+
212
+                  <p class="codice">{{ $cupon->codice }}</p>
213
+
214
+                  <table class="value-row">
215
+                    <tr>
216
+                      <td style="width: 40%;">
217
+                        <p class="importo">{{ number_format((float) $cupon->importo, 2, ',', '.') }} €</p>
218
+                      </td>
219
+                      <td style="width: 60%;">
220
+                        @if(!empty($cupon->nominativo))
221
+                          <p class="nominativo">{{ $cupon->nominativo }}</p>
222
+                        @else
223
+                          <p class="nominativo">&nbsp;</p>
224
+                        @endif
225
+                      </td>
226
+                    </tr>
227
+                  </table>
228
+
229
+                  <div class="divider"></div>
230
+
231
+                  <table class="meta-row">
232
+                    <tr>
233
+                      <td>Valido {{ $validitaLabel ?: '—' }}</td>
234
+                      <td class="right">In cassa</td>
235
+                    </tr>
236
+                  </table>
237
+                </td>
238
+              </tr>
239
+            </table>
240
+          </td>
241
+        @endforeach
242
+
243
+        @if($row->count() === 1)
244
+          <td class="slot">&nbsp;</td>
245
+        @endif
246
+      </tr>
247
+    @endforeach
248
+  </table>
249
+
250
+  @if(!$loop->last)
251
+    <div style="page-break-after: always;"></div>
252
+  @endif
253
+@empty
254
+  <p>Nessun cupon da stampare.</p>
255
+@endforelse
256
+</body>
257
+</html>

+ 695
- 0
resources/views/cupon/stampa-cupon.blade.php Ver arquivo

@@ -0,0 +1,695 @@
1
+<?php
2
+use App\Models\Cupon;
3
+use Carbon\Carbon;
4
+use Illuminate\Support\Facades\Route;
5
+use Illuminate\Support\Facades\Storage;
6
+?>
7
+@php
8
+$configData = Helper::appClasses();
9
+$cupons = $cupons ?? collect();
10
+$stampanti = $stampanti ?? collect();
11
+$oggi = Carbon::today()->format('Y-m-d');
12
+$tagsDisponibili = $cupons
13
+  ->flatMap(function ($c) {
14
+    $info = is_array($c->info) ? $c->info : [];
15
+    $tags = $info['tag'] ?? $info['tags'] ?? [];
16
+    return is_array($tags) ? $tags : ($tags ? [$tags] : []);
17
+  })
18
+  ->filter()
19
+  ->unique()
20
+  ->sort()
21
+  ->values();
22
+
23
+$cuponDataset = $cupons->map(function ($c) use ($oggi) {
24
+  $info = is_array($c->info) ? $c->info : [];
25
+  $tags = $info['tag'] ?? $info['tags'] ?? [];
26
+  if (!is_array($tags)) {
27
+    $tags = $tags ? [$tags] : [];
28
+  }
29
+  $da = $c->validita_da ? Carbon::parse($c->validita_da)->format('Y-m-d') : null;
30
+  $a = $c->validita_a ? Carbon::parse($c->validita_a)->format('Y-m-d') : null;
31
+  return [
32
+    'id' => $c->id,
33
+    'codice' => (string) $c->codice,
34
+    'nominativo' => (string) ($c->nominativo ?? ''),
35
+    'importo' => (float) $c->importo,
36
+    'stato' => (string) $c->stato,
37
+    'validita_da' => $da,
38
+    'validita_a' => $a,
39
+    'oggi' => ($da === $oggi && $a === $oggi),
40
+    'tags' => array_values($tags),
41
+  ];
42
+})->values();
43
+
44
+$attivitaId = session('attivita_attuale');
45
+$templateRelPath = $attivitaId ? $attivitaId.'/template.png' : null;
46
+$hasTemplateGrafica = $templateRelPath && Storage::disk('templateCupon')->exists($templateRelPath);
47
+$templateGraficaUrl = $hasTemplateGrafica
48
+  ? Storage::disk('templateCupon')->url($templateRelPath).'?v='.time()
49
+  : null;
50
+@endphp
51
+
52
+@extends('layouts/layoutMaster')
53
+
54
+@section('title', 'Stampa Cupon')
55
+
56
+@section('vendor-style')
57
+@vite([
58
+'resources/assets/vendor/libs/flatpickr/flatpickr.scss',
59
+])
60
+@endsection
61
+
62
+@section('vendor-script')
63
+@vite([
64
+'resources/assets/vendor/libs/moment/moment.js',
65
+'resources/assets/vendor/libs/flatpickr/flatpickr.js',
66
+])
67
+@endsection
68
+
69
+@section('pageTitle')
70
+<div class="d-flex flex-column">
71
+  <h4 class="mb-1 text-sm-small mt-md-4 mt-lg-4">
72
+    <i class="bx bx-printer d-none d-md-inline-flex d-lg-inline-flex"></i> Stampa Cupon
73
+  </h4>
74
+  <nav aria-label="breadcrumb" style="font-size: smaller;" class="d-none d-md-block d-lg-block">
75
+            <ol class="breadcrumb breadcrumb-custom-icon">
76
+      
77
+              <li class="breadcrumb-item">
78
+                <a href="{{ route('cupon.index') }}">Cupon</a>
79
+                <i class="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
80
+              </li>
81
+              <li class="breadcrumb-item active text-primary">
82
+                <a href="{{ route('cupon.stampa') }}">Stampa Cupon</a>
83
+              </li>
84
+            </ol>
85
+          </nav>
86
+</div>
87
+@endsection
88
+
89
+@section('content')
90
+@include('_partials.status')
91
+
92
+<div class="row g-4">
93
+  {{-- Filtri --}}
94
+  <div class="col-lg-4">
95
+    <div class="card h-100">
96
+      <div class="card-header">
97
+        <h5 class="card-title mb-0">Filtra cupon</h5>
98
+        <p class="text-muted small mb-0 mt-1">Seleziona i cupon, poi scegli come stamparli.</p>
99
+      </div>
100
+      <div class="card-body">
101
+        <form id="form-stampa-cupon" onsubmit="return false;">
102
+          <div class="mb-3">
103
+            <label class="form-label" for="filtro-codice">Codice</label>
104
+            <input type="text" class="form-control" id="filtro-codice" name="codice" placeholder="Es. AB12C" autocomplete="off">
105
+          </div>
106
+
107
+          <div class="mb-3">
108
+            <div class="form-check form-switch">
109
+              <input class="form-check-input" type="checkbox" id="filtro-oggi" name="oggi" value="1">
110
+              <label class="form-check-label" for="filtro-oggi">Solo validi oggi</label>
111
+            </div>
112
+          </div>
113
+
114
+          <div class="row g-3 mb-3">
115
+            <div class="col-6">
116
+              <label class="form-label" for="filtro-validita-da">Validità da</label>
117
+              <input type="date" class="form-control" id="filtro-validita-da" name="validita_da">
118
+            </div>
119
+            <div class="col-6">
120
+              <label class="form-label" for="filtro-validita-a">Validità a</label>
121
+              <input type="date" class="form-control" id="filtro-validita-a" name="validita_a">
122
+            </div>
123
+          </div>
124
+
125
+          <div class="mb-3">
126
+            <label class="form-label" for="filtro-stato">Stato</label>
127
+            <select class="form-select" id="filtro-stato" name="stato">
128
+              <option value="">Tutti</option>
129
+              <option value="{{ Cupon::GENERATO }}" selected>Generato</option>
130
+              <option value="{{ Cupon::DISPONIBILE }}">Disponibile</option>
131
+              <option value="{{ Cupon::USATO }}">Usato</option>
132
+              <option value="{{ Cupon::SCADUTO }}">Scaduto</option>
133
+            </select>
134
+          </div>
135
+
136
+          <div class="mb-3">
137
+            <label class="form-label" for="filtro-tag">Tag</label>
138
+            <select class="form-select" id="filtro-tag" name="tag">
139
+              <option value="">Tutti</option>
140
+              @foreach($tagsDisponibili as $tag)
141
+                <option value="{{ $tag }}">{{ $tag }}</option>
142
+              @endforeach
143
+            </select>
144
+          </div>
145
+
146
+          <div class="mb-3">
147
+            <label class="form-label" for="filtro-limite">Limite stampa</label>
148
+            <input type="number" min="1" max="500" class="form-control" id="filtro-limite" name="limite" value="50">
149
+          </div>
150
+
151
+          <div class="d-grid gap-2">
152
+            <button type="button" class="btn btn-outline-primary" id="btn-applica-filtri">
153
+              <i class="bx bx-filter-alt me-1"></i> Applica filtri
154
+            </button>
155
+            <button type="button" class="btn btn-label-secondary" id="btn-reset-filtri">Reset</button>
156
+          </div>
157
+        </form>
158
+      </div>
159
+    </div>
160
+  </div>
161
+
162
+  {{-- Selezione + azioni --}}
163
+  <div class="col-lg-8">
164
+    <div class="card mb-4">
165
+      <div class="card-body">
166
+        <div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-3">
167
+          <div>
168
+            <p class="mb-1 text-muted small">Selezionati</p>
169
+            <h3 class="mb-0" id="stampa-count">0</h3>
170
+          </div>
171
+          <span class="badge bg-label-primary" id="stampa-badge">0 cupon</span>
172
+        </div>
173
+
174
+        <div class="d-grid gap-2 d-sm-flex flex-wrap" id="azioni-stampa">
175
+          <button type="button" class="btn btn-primary" id="btn-stampa-termica" disabled>
176
+            <i class="bx bx-printer me-1"></i> Stampa termica
177
+          </button>
178
+          <button type="button" class="btn btn-outline-primary" id="btn-stampa-pdf" disabled>
179
+            <i class="bx bx-file me-1"></i> PDF standard
180
+          </button>
181
+          <button type="button" class="btn btn-outline-secondary" id="btn-toggle-pdf-personalizzato" disabled>
182
+            <i class="bx bx-image me-1"></i> PDF personalizzato
183
+          </button>
184
+          <button type="button" class="btn btn-outline-secondary" id="btn-report-cupon" disabled>
185
+            <i class="bx bx-file-blank me-1"></i> Checklist cupon
186
+          </button>
187
+        </div>
188
+        <p class="small text-muted mb-0 mt-3" id="stampa-hint">
189
+          Applica i filtri per abilitare le azioni di stampa.
190
+        </p>
191
+      </div>
192
+    </div>
193
+
194
+    {{-- Pannello termica (nascosto) --}}
195
+    <div class="card mb-4 d-none" id="panel-termica">
196
+      <div class="card-header d-flex justify-content-between align-items-center">
197
+        <h5 class="card-title mb-0">Stampa termica</h5>
198
+        <button type="button" class="btn btn-sm btn-label-secondary" id="btn-chiudi-termica">Chiudi</button>
199
+      </div>
200
+      <div class="card-body">
201
+        <p class="small text-muted mb-3">Scegli la stampante dell'attività su cui inviare i cupon selezionati.</p>
202
+        <div class="mb-3">
203
+          <label class="form-label" for="select-stampante">Stampante</label>
204
+          <select class="form-select" id="select-stampante" name="stampante_id">
205
+            <option value="">Seleziona stampante…</option>
206
+            @forelse($stampanti as $stampante)
207
+              <option value="{{ $stampante->id }}">{{ $stampante->nome }}</option>
208
+            @empty
209
+              <option value="" disabled>Nessuna stampante attiva per questa attività</option>
210
+            @endforelse
211
+          </select>
212
+        </div>
213
+        <div id="termica-feedback" class="small mb-3" style="display:none;"></div>
214
+        <button type="button" class="btn btn-primary" id="btn-conferma-termica" {{ $stampanti->isEmpty() ? 'disabled' : '' }}>
215
+          <span class="spinner-border spinner-border-sm d-none me-1" id="termica-spinner" role="status"></span>
216
+          <i class="bx bx-send me-1" id="termica-icon"></i>
217
+          Invia in stampa
218
+        </button>
219
+      </div>
220
+    </div>
221
+
222
+    {{-- Pannello PDF personalizzato (nascosto) --}}
223
+    <div class="card mb-4 d-none" id="panel-pdf-personalizzato">
224
+      <div class="card-header d-flex justify-content-between align-items-center">
225
+        <h5 class="card-title mb-0">PDF personalizzato</h5>
226
+        <button type="button" class="btn btn-sm btn-label-secondary" id="btn-chiudi-pdf-pers">Chiudi</button>
227
+      </div>
228
+      <div class="card-body">
229
+        <p class="small text-muted mb-3">
230
+          Carica la grafica di un singolo cupon. Verrà usata come sfondo nel PDF.
231
+        </p>
232
+        <div class="row g-4">
233
+          <div class="col-md-6">
234
+            <div class="border rounded bg-lighter p-3 text-center" style="min-height: 180px;">
235
+              <img
236
+                id="template-preview-img"
237
+                src="{{ $templateGraficaUrl }}"
238
+                alt="Anteprima template"
239
+                class="img-fluid rounded {{ $hasTemplateGrafica ? '' : 'd-none' }}"
240
+                style="max-height: 200px;"
241
+              >
242
+              <p id="template-preview-empty" class="text-muted small mb-0 {{ $hasTemplateGrafica ? 'd-none' : '' }}">
243
+                Nessuna grafica caricata
244
+              </p>
245
+            </div>
246
+          </div>
247
+          <div class="col-md-6">
248
+            <form
249
+              id="form-upload-template-cupon"
250
+              action="{{ Route::has('cupon.template.upload') ? route('cupon.template.upload') : '#' }}"
251
+              method="POST"
252
+              enctype="multipart/form-data"
253
+            >
254
+              @csrf
255
+              <div class="mb-3">
256
+                <label class="form-label" for="template-cupon-file">Immagine template</label>
257
+                <input
258
+                  type="file"
259
+                  class="form-control"
260
+                  id="template-cupon-file"
261
+                  name="template"
262
+                  accept="image/png,image/jpeg,image/webp"
263
+                  required
264
+                >
265
+              </div>
266
+              <div id="template-upload-feedback" class="small mb-3" style="display:none;"></div>
267
+              <button type="submit" class="btn btn-outline-primary" id="btn-upload-template">
268
+                <span class="spinner-border spinner-border-sm d-none me-1" id="template-upload-spinner" role="status"></span>
269
+                <i class="bx bx-upload me-1" id="template-upload-icon"></i>
270
+                Carica grafica
271
+              </button>
272
+            </form>
273
+
274
+            <hr class="my-4">
275
+
276
+            <button type="button" class="btn btn-primary" id="btn-stampa-pdf-personalizzato" disabled>
277
+              <i class="bx bx-download me-1"></i> Genera PDF personalizzato
278
+            </button>
279
+            <p class="small text-muted mt-2 mb-0" id="pdf-pers-hint">
280
+              @if($hasTemplateGrafica)
281
+                Grafica presente: puoi generare il PDF.
282
+              @else
283
+                Carica una grafica per abilitare la generazione.
284
+              @endif
285
+            </p>
286
+          </div>
287
+        </div>
288
+      </div>
289
+    </div>
290
+
291
+    <div class="card">
292
+      <div class="card-header">
293
+        <h5 class="card-title mb-0">Anteprima selezione</h5>
294
+      </div>
295
+      <div class="card-body p-0">
296
+        <div class="table-responsive" style="max-height: 420px;">
297
+          <table class="table table-sm table-hover mb-0">
298
+            <thead class="table-light sticky-top">
299
+              <tr>
300
+                <th>Codice</th>
301
+                <th>Nominativo</th>
302
+                <th>Importo</th>
303
+                <th>Validità</th>
304
+                <th>Stato</th>
305
+                <th>Tag</th>
306
+              </tr>
307
+            </thead>
308
+            <tbody id="tbody-anteprima-stampa">
309
+              <tr class="text-muted">
310
+                <td colspan="6" class="text-center py-4">Nessun cupon in anteprima.</td>
311
+              </tr>
312
+            </tbody>
313
+          </table>
314
+        </div>
315
+      </div>
316
+    </div>
317
+  </div>
318
+</div>
319
+
320
+<script type="application/json" id="cupon-dataset">
321
+{!! json_encode($cuponDataset, JSON_UNESCAPED_UNICODE) !!}
322
+</script>
323
+@endsection
324
+
325
+@section('page-script')
326
+<script>
327
+(function () {
328
+  const datasetEl = document.getElementById('cupon-dataset');
329
+  const allCupons = datasetEl ? JSON.parse(datasetEl.textContent || '[]') : [];
330
+  let filtered = [];
331
+  let hasTemplate = @json((bool) $hasTemplateGrafica);
332
+
333
+  const els = {
334
+    codice: document.getElementById('filtro-codice'),
335
+    oggi: document.getElementById('filtro-oggi'),
336
+    da: document.getElementById('filtro-validita-da'),
337
+    a: document.getElementById('filtro-validita-a'),
338
+    stato: document.getElementById('filtro-stato'),
339
+    tag: document.getElementById('filtro-tag'),
340
+    limite: document.getElementById('filtro-limite'),
341
+    count: document.getElementById('stampa-count'),
342
+    badge: document.getElementById('stampa-badge'),
343
+    hint: document.getElementById('stampa-hint'),
344
+    tbody: document.getElementById('tbody-anteprima-stampa'),
345
+    btnTermica: document.getElementById('btn-stampa-termica'),
346
+    btnPdf: document.getElementById('btn-stampa-pdf'),
347
+    btnTogglePers: document.getElementById('btn-toggle-pdf-personalizzato'),
348
+    btnPdfPers: document.getElementById('btn-stampa-pdf-personalizzato'),
349
+    btnChecklist: document.getElementById('btn-report-cupon'),
350
+    panelTermica: document.getElementById('panel-termica'),
351
+    panelPers: document.getElementById('panel-pdf-personalizzato'),
352
+    selectStampante: document.getElementById('select-stampante'),
353
+    pdfPersHint: document.getElementById('pdf-pers-hint'),
354
+  };
355
+
356
+  function money(v) {
357
+    return Number(v || 0).toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €';
358
+  }
359
+
360
+  function escapeHtml(str) {
361
+    return String(str)
362
+      .replace(/&/g, '&amp;')
363
+      .replace(/</g, '&lt;')
364
+      .replace(/>/g, '&gt;')
365
+      .replace(/"/g, '&quot;');
366
+  }
367
+
368
+  function selectedIds() {
369
+    return filtered.map(function (c) { return c.id; });
370
+  }
371
+
372
+  function syncActions() {
373
+    const n = filtered.length;
374
+    const enabled = n > 0;
375
+    els.btnTermica.disabled = !enabled;
376
+    els.btnPdf.disabled = !enabled;
377
+    els.btnTogglePers.disabled = !enabled;
378
+    els.btnPdfPers.disabled = !enabled || !hasTemplate;
379
+    els.btnChecklist.disabled = !enabled;
380
+
381
+    if (els.pdfPersHint) {
382
+      els.pdfPersHint.textContent = hasTemplate
383
+        ? 'Grafica presente: puoi generare il PDF.'
384
+        : 'Carica una grafica per abilitare la generazione.';
385
+    }
386
+
387
+    els.hint.textContent = n === 0
388
+      ? 'Applica i filtri per abilitare le azioni di stampa.'
389
+      : 'Selezionati ' + n + ' cupon. Scegli un’azione di stampa.';
390
+  }
391
+
392
+  function hidePanels() {
393
+    els.panelTermica?.classList.add('d-none');
394
+    els.panelPers?.classList.add('d-none');
395
+  }
396
+
397
+  function applyFilters() {
398
+    const codice = (els.codice.value || '').trim().toUpperCase();
399
+    const soloOggi = els.oggi.checked;
400
+    const da = els.da.value || '';
401
+    const a = els.a.value || '';
402
+    const stato = els.stato.value || '';
403
+    const tag = els.tag.value || '';
404
+    const limite = Math.max(1, parseInt(els.limite.value || '50', 10));
405
+
406
+    filtered = allCupons.filter(function (c) {
407
+      if (codice && !(c.codice || '').toUpperCase().includes(codice)) return false;
408
+      if (soloOggi && !c.oggi) return false;
409
+      if (stato && c.stato !== stato) return false;
410
+      if (tag && !(c.tags || []).includes(tag)) return false;
411
+      if (da && c.validita_a && c.validita_a < da) return false;
412
+      if (a && c.validita_da && c.validita_da > a) return false;
413
+      return true;
414
+    }).slice(0, limite);
415
+
416
+    render();
417
+  }
418
+
419
+  function render() {
420
+    const n = filtered.length;
421
+    els.count.textContent = String(n);
422
+    els.badge.textContent = n + (n === 1 ? ' cupon' : ' cupon');
423
+    syncActions();
424
+
425
+    els.tbody.innerHTML = '';
426
+    if (n === 0) {
427
+      hidePanels();
428
+      els.tbody.innerHTML = '<tr class="text-muted"><td colspan="6" class="text-center py-4">Nessun cupon in anteprima.</td></tr>';
429
+      return;
430
+    }
431
+
432
+    filtered.forEach(function (c) {
433
+      const validita = c.oggi ? 'Oggi' : ((c.validita_da || '—') + ' → ' + (c.validita_a || '—'));
434
+      const tags = (c.tags || []).join(', ') || '—';
435
+      const tr = document.createElement('tr');
436
+      tr.innerHTML =
437
+        '<td><code>' + escapeHtml(c.codice) + '</code></td>' +
438
+        '<td>' + escapeHtml(c.nominativo || '—') + '</td>' +
439
+        '<td>' + money(c.importo) + '</td>' +
440
+        '<td>' + escapeHtml(validita) + '</td>' +
441
+        '<td>' + escapeHtml(c.stato) + '</td>' +
442
+        '<td>' + escapeHtml(tags) + '</td>';
443
+      els.tbody.appendChild(tr);
444
+    });
445
+  }
446
+
447
+  function resetFilters() {
448
+    els.codice.value = '';
449
+    els.oggi.checked = false;
450
+    els.da.value = '';
451
+    els.a.value = '';
452
+    els.stato.value = '{{ Cupon::GENERATO }}';
453
+    els.tag.value = '';
454
+    els.limite.value = '50';
455
+    applyFilters();
456
+  }
457
+
458
+  document.getElementById('btn-applica-filtri')?.addEventListener('click', applyFilters);
459
+  document.getElementById('btn-reset-filtri')?.addEventListener('click', resetFilters);
460
+
461
+  els.oggi?.addEventListener('change', function () {
462
+    if (els.oggi.checked) {
463
+      els.da.value = '';
464
+      els.a.value = '';
465
+    }
466
+  });
467
+
468
+  // Azioni
469
+  els.btnTermica?.addEventListener('click', function () {
470
+    els.panelPers?.classList.add('d-none');
471
+    els.panelTermica?.classList.remove('d-none');
472
+    els.panelTermica?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
473
+  });
474
+
475
+  document.getElementById('btn-chiudi-termica')?.addEventListener('click', function () {
476
+    els.panelTermica?.classList.add('d-none');
477
+  });
478
+
479
+  els.btnTogglePers?.addEventListener('click', function () {
480
+    els.panelTermica?.classList.add('d-none');
481
+    els.panelPers?.classList.remove('d-none');
482
+    els.panelPers?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
483
+  });
484
+
485
+  document.getElementById('btn-chiudi-pdf-pers')?.addEventListener('click', function () {
486
+    els.panelPers?.classList.add('d-none');
487
+  });
488
+
489
+  function currentFilters() {
490
+    return {
491
+      codice: (els.codice.value || '').trim(),
492
+      oggi: els.oggi.checked ? '1' : '',
493
+      validita_da: els.da.value || '',
494
+      validita_a: els.a.value || '',
495
+      stato: els.stato.value || '',
496
+      tag: els.tag.value || '',
497
+      limite: els.limite.value || '',
498
+    };
499
+  }
500
+
501
+  function urlConIds(baseUrl, withFilters) {
502
+    const ids = selectedIds();
503
+    if (!ids.length) return null;
504
+
505
+    const params = new URLSearchParams();
506
+    ids.forEach(function (id) {
507
+      params.append('ids[]', id);
508
+    });
509
+
510
+    if (withFilters) {
511
+      const filters = currentFilters();
512
+      Object.keys(filters).forEach(function (key) {
513
+        if (filters[key] !== '' && filters[key] != null) {
514
+          params.append(key, filters[key]);
515
+        }
516
+      });
517
+    }
518
+
519
+    return baseUrl + '?' + params.toString();
520
+  }
521
+
522
+  els.btnPdf?.addEventListener('click', function () {
523
+    const url = urlConIds(@json(route('cupon.stampa.pdf')), false);
524
+    if (url) window.location.href = url;
525
+  });
526
+
527
+  els.btnPdfPers?.addEventListener('click', function () {
528
+    if (!hasTemplate) return;
529
+    const url = urlConIds(@json(route('cupon.stampa.pdf-personalizzato')), false);
530
+    if (url) window.location.href = url;
531
+  });
532
+
533
+  function stampaChecklistCupon() {
534
+    const url = urlConIds(@json(route('cupon.checklist-cupon')), true);
535
+    if (url) window.location.href = url;
536
+  }
537
+
538
+  els.btnChecklist?.addEventListener('click', stampaChecklistCupon);
539
+
540
+  // Conferma termica
541
+  const feedbackTermica = document.getElementById('termica-feedback');
542
+  const btnConfermaTermica = document.getElementById('btn-conferma-termica');
543
+  const spinTermica = document.getElementById('termica-spinner');
544
+  const iconTermica = document.getElementById('termica-icon');
545
+
546
+  btnConfermaTermica?.addEventListener('click', function () {
547
+    const stampanteId = els.selectStampante?.value;
548
+    const ids = selectedIds();
549
+
550
+    if (!stampanteId) {
551
+      if (feedbackTermica) {
552
+        feedbackTermica.style.display = 'block';
553
+        feedbackTermica.className = 'small mb-3 text-danger';
554
+        feedbackTermica.textContent = 'Seleziona una stampante.';
555
+      }
556
+      return;
557
+    }
558
+    if (!ids.length) {
559
+      if (feedbackTermica) {
560
+        feedbackTermica.style.display = 'block';
561
+        feedbackTermica.className = 'small mb-3 text-danger';
562
+        feedbackTermica.textContent = 'Nessun cupon selezionato.';
563
+      }
564
+      return;
565
+    }
566
+
567
+    btnConfermaTermica.setAttribute('disabled', 'disabled');
568
+    spinTermica?.classList.remove('d-none');
569
+    iconTermica?.classList.add('d-none');
570
+    if (feedbackTermica) {
571
+      feedbackTermica.style.display = 'none';
572
+      feedbackTermica.textContent = '';
573
+    }
574
+
575
+    fetch(@json(Route::has('cupon.stampa.termica') ? route('cupon.stampa.termica') : '#'), {
576
+      method: 'POST',
577
+      headers: {
578
+        'Content-Type': 'application/json',
579
+        'Accept': 'application/json',
580
+        'X-CSRF-TOKEN': @json(csrf_token()),
581
+        'X-Requested-With': 'XMLHttpRequest',
582
+      },
583
+      body: JSON.stringify({
584
+        ids: ids,
585
+        stampante_id: Number(stampanteId),
586
+      }),
587
+    })
588
+      .then(async function (res) {
589
+        const data = await res.json().catch(function () { return {}; });
590
+        if (!res.ok || data.success === false) {
591
+          throw new Error(data.message || 'Invio stampa non riuscito');
592
+        }
593
+        if (feedbackTermica) {
594
+          feedbackTermica.style.display = 'block';
595
+          feedbackTermica.className = 'small mb-3 text-success';
596
+          feedbackTermica.textContent = data.message || 'Stampa inviata.';
597
+        }
598
+      })
599
+      .catch(function (err) {
600
+        if (feedbackTermica) {
601
+          feedbackTermica.style.display = 'block';
602
+          feedbackTermica.className = 'small mb-3 text-danger';
603
+          feedbackTermica.textContent = err.message || 'Errore stampa';
604
+        }
605
+      })
606
+      .finally(function () {
607
+        btnConfermaTermica.removeAttribute('disabled');
608
+        spinTermica?.classList.add('d-none');
609
+        iconTermica?.classList.remove('d-none');
610
+      });
611
+  });
612
+
613
+  // Upload template
614
+  const fileInput = document.getElementById('template-cupon-file');
615
+  const previewImg = document.getElementById('template-preview-img');
616
+  const previewEmpty = document.getElementById('template-preview-empty');
617
+  const formUpload = document.getElementById('form-upload-template-cupon');
618
+  const feedback = document.getElementById('template-upload-feedback');
619
+  const btnUpload = document.getElementById('btn-upload-template');
620
+  const spinUpload = document.getElementById('template-upload-spinner');
621
+  const iconUpload = document.getElementById('template-upload-icon');
622
+
623
+  function showTemplatePreview(url) {
624
+    if (!previewImg) return;
625
+    previewImg.src = url;
626
+    previewImg.classList.remove('d-none');
627
+    previewEmpty?.classList.add('d-none');
628
+    hasTemplate = true;
629
+    syncActions();
630
+  }
631
+
632
+  fileInput?.addEventListener('change', function () {
633
+    const file = this.files && this.files[0];
634
+    if (!file) return;
635
+    showTemplatePreview(URL.createObjectURL(file));
636
+  });
637
+
638
+  formUpload?.addEventListener('submit', function (e) {
639
+    e.preventDefault();
640
+    const action = formUpload.getAttribute('action');
641
+    if (!action || action === '#') {
642
+      if (feedback) {
643
+        feedback.style.display = 'block';
644
+        feedback.className = 'small mb-3 text-danger';
645
+        feedback.textContent = 'Rotta upload non configurata.';
646
+      }
647
+      return;
648
+    }
649
+
650
+    const fd = new FormData(formUpload);
651
+    btnUpload?.setAttribute('disabled', 'disabled');
652
+    spinUpload?.classList.remove('d-none');
653
+    iconUpload?.classList.add('d-none');
654
+
655
+    fetch(action, {
656
+      method: 'POST',
657
+      body: fd,
658
+      headers: {
659
+        'X-Requested-With': 'XMLHttpRequest',
660
+        'Accept': 'application/json',
661
+      },
662
+    })
663
+      .then(async function (res) {
664
+        const data = await res.json().catch(function () { return {}; });
665
+        if (!res.ok || data.success === false) {
666
+          throw new Error(data.message || 'Upload non riuscito');
667
+        }
668
+        if (data.url) {
669
+          showTemplatePreview(data.url + (data.url.includes('?') ? '&' : '?') + 'v=' + Date.now());
670
+        }
671
+        if (feedback) {
672
+          feedback.style.display = 'block';
673
+          feedback.className = 'small mb-3 text-success';
674
+          feedback.textContent = data.message || 'Grafica salvata.';
675
+        }
676
+        formUpload.reset();
677
+      })
678
+      .catch(function (err) {
679
+        if (feedback) {
680
+          feedback.style.display = 'block';
681
+          feedback.className = 'small mb-3 text-danger';
682
+          feedback.textContent = err.message || 'Errore upload';
683
+        }
684
+      })
685
+      .finally(function () {
686
+        btnUpload?.removeAttribute('disabled');
687
+        spinUpload?.classList.add('d-none');
688
+        iconUpload?.classList.remove('d-none');
689
+      });
690
+  });
691
+
692
+  applyFilters();
693
+})();
694
+</script>
695
+@endsection

+ 18
- 0
resources/views/cupon/stato.blade.php Ver arquivo

@@ -0,0 +1,18 @@
1
+@php
2
+    $stato = $stato ?? '';
3
+    $codice = $codice ?? '';
4
+    $stati = \App\Models\Cupon::getStati();
5
+    $statoData = $stati[$stato] ?? null;
6
+    $color = $statoData['color'] ?? 'secondary';
7
+    $label = $statoData['label'] ?? ($stato ?: '-');
8
+@endphp
9
+
10
+@if($codice !== '' && $stato === '')
11
+    <span class="badge bg-body font-monospace fs-6" title="Codice {{ $codice }}">
12
+        {{ $codice }}
13
+    </span>
14
+@else
15
+    <span class="badge bg-{{ $color }} text-uppercase" title="{{ $label }}">
16
+        {{ $label }}
17
+    </span>
18
+@endif

Carregando…
Cancelar
Salvar