Roberto Santini 2 тижднів тому
джерело
коміт
c136c7fcff

+ 45
- 1
app/Http/Controllers/SegnalazioneController.php Переглянути файл

@@ -19,9 +19,11 @@ use App\Models\OrdineLavoro;
19 19
 use App\DataTables\SegnalazioneDataTable;
20 20
 use Illuminate\Support\Facades\Auth;
21 21
 use App\DataTables\SegnalazioneDataTableEditor;
22
+use App\Notifications\TicketSegnalazionePdf;
22 23
 use App\Services\SegnalazioneTicketPdfService;
23 24
 use App\Services\SegnalazioneAggiornaService;
24 25
 use App\Services\SegnalazioneRapportoService;
26
+use Illuminate\Support\Facades\Notification;
25 27
 use Illuminate\Support\Str;
26 28
 use Illuminate\Support\Facades\File;
27 29
 use Illuminate\Validation\Rule;
@@ -899,15 +901,57 @@ class SegnalazioneController extends Controller implements HasMiddleware
899 901
       }
900 902
 
901 903
       $filename = $pdfService->filename($segnalazione, $destinatario);
904
+      $disposition = $request->boolean('download') ? 'attachment' : 'inline';
902 905
 
903 906
       return response()->stream(function () use ($pdfService, $segnalazione, $destinatario) {
904 907
         echo $pdfService->generateBinary($segnalazione, $destinatario);
905 908
       }, 200, [
906 909
         'Content-Type' => 'application/pdf',
907
-        'Content-Disposition' => 'inline; filename="' . $filename . '"',
910
+        'Content-Disposition' => $disposition . '; filename="' . $filename . '"',
908 911
       ]);
909 912
     }
910 913
 
914
+    public function admin_pdf_invia(Request $request, $id, SegnalazioneTicketPdfService $pdfService)
915
+    {
916
+      $segnalazione = Segnalazione::find($id);
917
+      if (! $segnalazione) {
918
+        return response()->json(['message' => 'Segnalazione non trovata'], 404);
919
+      }
920
+
921
+      abort_unless(Segnalazione::authUserMayViewSegnalazione(Auth::user(), $segnalazione->assegnata_a_id, $segnalazione->user_id), 403);
922
+
923
+      $data = $request->validate([
924
+        'email' => ['required', 'email'],
925
+        'destinatario' => ['required', 'in:cliente,operatore'],
926
+      ], [
927
+        'email.required' => 'Inserisci un indirizzo email',
928
+        'email.email' => 'Indirizzo email non valido',
929
+      ]);
930
+
931
+      $destinatario = $pdfService->normalizeDestinatario($data['destinatario']);
932
+      $authUser = Auth::user();
933
+      if (
934
+        $authUser
935
+        && $authUser->can('view-own-segnalazione')
936
+        && ! $authUser->can('view-segnalazione')
937
+        && ! $authUser->can('view-segnalazione-assegnate')
938
+        && ! $authUser->can('edit-segnalazione')
939
+      ) {
940
+        $destinatario = SegnalazioneTicketPdfService::DEST_CLIENTE;
941
+      }
942
+
943
+      try {
944
+        Notification::route('mail', $data['email'])
945
+          ->notify(new TicketSegnalazionePdf($segnalazione, $destinatario));
946
+      } catch (\Throwable $e) {
947
+        report($e);
948
+
949
+        return response()->json(['message' => 'Invio email non riuscito'], 500);
950
+      }
951
+
952
+      return response()->json(['message' => 'Email inviata a '.$data['email']]);
953
+    }
954
+
911 955
     public function admin_rapportino_annotazione(Request $request, $id, $annotazione, SegnalazioneRapportoService $rapporto)
912 956
     {
913 957
       $segnalazione = Segnalazione::find($id);

+ 43
- 0
app/Notifications/TicketSegnalazionePdf.php Переглянути файл

@@ -0,0 +1,43 @@
1
+<?php
2
+
3
+namespace App\Notifications;
4
+
5
+use App\Models\Segnalazione;
6
+use App\Services\SegnalazioneTicketPdfService;
7
+use Illuminate\Notifications\Messages\MailMessage;
8
+use Illuminate\Notifications\Notification;
9
+
10
+class TicketSegnalazionePdf extends Notification
11
+{
12
+    public function __construct(
13
+        protected Segnalazione $segnalazione,
14
+        protected string $destinatario = SegnalazioneTicketPdfService::DEST_CLIENTE,
15
+    ) {
16
+    }
17
+
18
+    /**
19
+     * @return array<int, string>
20
+     */
21
+    public function via(object $notifiable): array
22
+    {
23
+        return ['mail'];
24
+    }
25
+
26
+    public function toMail(object $notifiable): MailMessage
27
+    {
28
+        $this->segnalazione->loadMissing(['azienda', 'user']);
29
+        $pdfService = app(SegnalazioneTicketPdfService::class);
30
+        $destinatario = $pdfService->normalizeDestinatario($this->destinatario);
31
+
32
+        return (new MailMessage)
33
+            ->subject($this->segnalazione->mailSubject().' — Rapporto di lavoro')
34
+            ->markdown('mail.ticket-segnalazione-pdf', [
35
+                'segnalazione' => $this->segnalazione,
36
+            ])
37
+            ->attachData(
38
+                $pdfService->generateBinary($this->segnalazione, $destinatario),
39
+                $pdfService->filename($this->segnalazione, $destinatario),
40
+                ['mime' => 'application/pdf']
41
+            );
42
+    }
43
+}

+ 24
- 0
app/Services/SegnalazioneTicketPdfService.php Переглянути файл

@@ -76,6 +76,27 @@ class SegnalazioneTicketPdfService
76 76
         return $todos->values();
77 77
     }
78 78
 
79
+    /**
80
+     * Annotazioni con tempo registrato, in ordine cronologico.
81
+     * Stesse righe per la versione cliente e per quella operatore.
82
+     */
83
+    public function tempiForPdf(Segnalazione $segnalazione): Collection
84
+    {
85
+        $annotazioni = $segnalazione->relationLoaded('annotazioni')
86
+            ? $segnalazione->annotazioni
87
+            : $segnalazione->annotazioni()->orderBy('id')->get();
88
+
89
+        return $annotazioni
90
+            ->filter(fn ($annotazione) => (int) ($annotazione->tempo_minuti ?? 0) > 0)
91
+            ->sortBy(function ($annotazione) {
92
+                $data = $annotazione->tempo_data ?? $annotazione->created_at;
93
+                $ts = $data instanceof \DateTimeInterface ? $data->getTimestamp() : 0;
94
+
95
+                return sprintf('%020d-%020d', $ts, (int) $annotazione->id);
96
+            })
97
+            ->values();
98
+    }
99
+
79 100
     public function annotazioniForPdf(Segnalazione $segnalazione, string $destinatario): Collection
80 101
     {
81 102
         $annotazioni = $segnalazione->annotazioni
@@ -101,6 +122,7 @@ class SegnalazioneTicketPdfService
101 122
         $primaAnnotazione = $annotazioni->first(
102 123
             fn ($annotazione) => filled(trim(strip_tags((string) ($annotazione->text ?? ''))))
103 124
         );
125
+        $tempiPdf = $this->tempiForPdf($segnalazione);
104 126
 
105 127
         $pdf = Pdf::loadView('segnalazione.pdf-ticket', [
106 128
             'segnalazione' => $segnalazione,
@@ -109,6 +131,8 @@ class SegnalazioneTicketPdfService
109 131
             'primaAnnotazione' => $primaAnnotazione,
110 132
             'annotazioniPdf' => $annotazioni,
111 133
             'todosPdf' => $this->todosForPdf($segnalazione, $destinatario),
134
+            'tempiPdf' => $tempiPdf,
135
+            'tempoTotaleLabel' => \App\Models\Config::formatTempo((int) $tempiPdf->sum('tempo_minuti')),
112 136
             'destinatario' => $destinatario,
113 137
             'isClientePdf' => $isCliente,
114 138
             'acquistoServizio' => $this->linkedAcquistoServizio($segnalazione),

+ 8
- 0
resources/views/mail/ticket-segnalazione-pdf.blade.php Переглянути файл

@@ -0,0 +1,8 @@
1
+@component('mail::message')
2
+Gentile cliente,
3
+
4
+in allegato il rapporto di lavoro completo relativo alla segnalazione **{{ $segnalazione->getNumeroLabel() }}** ({{ $segnalazione->titolo }}).
5
+
6
+Grazie,  
7
+**ElephanTech**
8
+@endcomponent

+ 141
- 8
resources/views/segnalazione/admin/show.blade.php Переглянути файл

@@ -604,10 +604,18 @@ use App\Models\Config;
604 604
           </div>
605 605
 
606 606
           <div class="col-12 col-lg-3">
607
-            <div class="small text-muted mb-1">Assegnata a</div>
608
-            <div class="fw-semibold mb-2">
609
-              <i class="bx bx-user-check me-1"></i>
610
-              {{ $segnalazione->assegnataA ? $segnalazione->assegnataA->full_name : '---' }}
607
+            <div class="d-flex justify-content-between align-items-start gap-2 mb-2">
608
+              <div class="min-w-0">
609
+                <div class="small text-muted mb-1">Assegnata a</div>
610
+                <div class="fw-semibold">
611
+                  <i class="bx bx-user-check me-1"></i>
612
+                  {{ $segnalazione->assegnataA ? $segnalazione->assegnataA->full_name : '---' }}
613
+                </div>
614
+              </div>
615
+              <div class="text-end flex-shrink-0">
616
+                <div class="small text-muted mb-1">Tempo totale</div>
617
+                <div class="fw-semibold">{{ $tempoTotale }}</div>
618
+              </div>
611 619
             </div>
612 620
             @if($segnalazione->ordineLavoro)
613 621
             @php
@@ -722,15 +730,23 @@ use App\Models\Config;
722 730
           </button>
723 731
           <ul class="dropdown-menu dropdown-menu-end">
724 732
             <li>
725
-              <a class="dropdown-item" href="{{ route('admin.segnalazione.pdf', ['id' => $segnalazione->id, 'destinatario' => 'cliente']) }}" target="_blank">
733
+              <button type="button" class="dropdown-item js-apri-pdf-segnalazione"
734
+                data-title="Versione cliente"
735
+                data-destinatario="cliente"
736
+                data-preview-url="{{ route('admin.segnalazione.pdf', ['id' => $segnalazione->id, 'destinatario' => 'cliente']) }}"
737
+                data-download-url="{{ route('admin.segnalazione.pdf', ['id' => $segnalazione->id, 'destinatario' => 'cliente', 'download' => 1]) }}">
726 738
                 Versione cliente
727
-              </a>
739
+              </button>
728 740
             </li>
729 741
             @if(! $isVistaCliente)
730 742
             <li>
731
-              <a class="dropdown-item" href="{{ route('admin.segnalazione.pdf', ['id' => $segnalazione->id, 'destinatario' => 'operatore']) }}" target="_blank">
743
+              <button type="button" class="dropdown-item js-apri-pdf-segnalazione"
744
+                data-title="Versione operatore"
745
+                data-destinatario="operatore"
746
+                data-preview-url="{{ route('admin.segnalazione.pdf', ['id' => $segnalazione->id, 'destinatario' => 'operatore']) }}"
747
+                data-download-url="{{ route('admin.segnalazione.pdf', ['id' => $segnalazione->id, 'destinatario' => 'operatore', 'download' => 1]) }}">
732 748
                 Versione operatore
733
-              </a>
749
+              </button>
734 750
             </li>
735 751
             @endif
736 752
           </ul>
@@ -1272,6 +1288,29 @@ use App\Models\Config;
1272 1288
   </div>
1273 1289
 </div>
1274 1290
 
1291
+<div class="modal fade" id="modalPdfSegnalazione" tabindex="-1" aria-hidden="true">
1292
+  <div class="modal-dialog modal-xl modal-dialog-centered">
1293
+    <div class="modal-content">
1294
+      <div class="modal-header">
1295
+        <h5 class="modal-title" id="modalPdfSegnalazioneTitle">PDF</h5>
1296
+        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
1297
+      </div>
1298
+      <div class="modal-body p-0">
1299
+        <iframe id="modalPdfSegnalazioneFrame" title="Anteprima PDF" style="width: 100%; height: 70vh; border: 0;" src="about:blank"></iframe>
1300
+      </div>
1301
+      <div class="modal-footer">
1302
+        <button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">Chiudi</button>
1303
+        <a class="btn btn-primary" id="btnScaricaPdfSegnalazione" href="#">
1304
+          <i class="bx bx-download me-1"></i>Scarica PDF
1305
+        </a>
1306
+        <button type="button" class="btn btn-success" id="btnInviaPdfSegnalazione">
1307
+          <i class="bx bx-envelope me-1"></i>Invia per email
1308
+        </button>
1309
+      </div>
1310
+    </div>
1311
+  </div>
1312
+</div>
1313
+
1275 1314
 
1276 1315
 
1277 1316
 
@@ -2279,6 +2318,100 @@ use App\Models\Config;
2279 2318
     });
2280 2319
   }
2281 2320
 
2321
+  var pdfSegnalazioneCorrente = { destinatario: 'cliente' };
2322
+  var emailClienteSegnalazione = @json($segnalazione->user->email ?? '');
2323
+
2324
+  $(document).on('click', '.js-apri-pdf-segnalazione', function (e) {
2325
+    e.preventDefault();
2326
+    var $btn = $(this);
2327
+    pdfSegnalazioneCorrente.destinatario = $btn.data('destinatario') || 'cliente';
2328
+    $('#modalPdfSegnalazioneTitle').text($btn.data('title') || 'PDF');
2329
+    $('#modalPdfSegnalazioneFrame').attr('src', $btn.data('preview-url'));
2330
+    $('#btnScaricaPdfSegnalazione').attr('href', $btn.data('download-url'));
2331
+    $('#modalPdfSegnalazione').modal('show');
2332
+  });
2333
+
2334
+  $('#modalPdfSegnalazione').on('hidden.bs.modal', function () {
2335
+    $('#modalPdfSegnalazioneFrame').attr('src', 'about:blank');
2336
+  });
2337
+
2338
+  $('#btnInviaPdfSegnalazione').on('click', function () {
2339
+    Swal.fire({
2340
+      title: 'Invia per email',
2341
+      input: 'email',
2342
+      inputLabel: 'Indirizzo del cliente',
2343
+      inputValue: emailClienteSegnalazione,
2344
+      inputPlaceholder: 'cliente@example.com',
2345
+      showCancelButton: true,
2346
+      confirmButtonText: 'Invia',
2347
+      cancelButtonText: 'Annulla',
2348
+      buttonsStyling: false,
2349
+      customClass: {
2350
+        confirmButton: 'btn btn-success',
2351
+        cancelButton: 'btn btn-label-secondary ms-2'
2352
+      },
2353
+      didOpen: function () {
2354
+        var container = Swal.getContainer();
2355
+        if (container) {
2356
+          container.style.zIndex = '2000';
2357
+        }
2358
+      },
2359
+      inputValidator: function (value) {
2360
+        if (!value) {
2361
+          return 'Inserisci un indirizzo email';
2362
+        }
2363
+      }
2364
+    }).then(function (result) {
2365
+      if (!result.isConfirmed) {
2366
+        return;
2367
+      }
2368
+      Swal.fire({
2369
+        title: 'Invio in corso',
2370
+        allowOutsideClick: false,
2371
+        allowEscapeKey: false,
2372
+        didOpen: function () {
2373
+          var container = Swal.getContainer();
2374
+          if (container) {
2375
+            container.style.zIndex = '2000';
2376
+          }
2377
+          Swal.showLoading();
2378
+        }
2379
+      });
2380
+      $.ajax({
2381
+        url: @json(route('admin.segnalazione.pdf.invia', $segnalazione->id)),
2382
+        method: 'POST',
2383
+        data: {
2384
+          email: result.value,
2385
+          destinatario: pdfSegnalazioneCorrente.destinatario
2386
+        }
2387
+      }).done(function (res) {
2388
+        Swal.fire({
2389
+          icon: 'success',
2390
+          title: 'Email inviata',
2391
+          text: (res && res.message) ? res.message : 'PDF inviato.',
2392
+          customClass: { confirmButton: 'btn btn-success' },
2393
+          buttonsStyling: false
2394
+        });
2395
+      }).fail(function (xhr) {
2396
+        var msg = 'Invio non riuscito';
2397
+        if (xhr.responseJSON) {
2398
+          if (xhr.responseJSON.errors && xhr.responseJSON.errors.email && xhr.responseJSON.errors.email[0]) {
2399
+            msg = xhr.responseJSON.errors.email[0];
2400
+          } else if (xhr.responseJSON.message) {
2401
+            msg = xhr.responseJSON.message;
2402
+          }
2403
+        }
2404
+        Swal.fire({
2405
+          icon: 'error',
2406
+          title: 'Errore',
2407
+          text: msg,
2408
+          customClass: { confirmButton: 'btn btn-danger' },
2409
+          buttonsStyling: false
2410
+        });
2411
+      });
2412
+    });
2413
+  });
2414
+
2282 2415
   function openModalAllegato(allegatoId) {
2283 2416
     $('#extraLargeModal .modal-content').load('{{ route("allegato_annotazione.show") }}?allegato_annotazione_id=' + allegatoId);
2284 2417
     $('#extraLargeModal').modal('show');

+ 85
- 0
resources/views/segnalazione/pdf-ticket.blade.php Переглянути файл

@@ -217,6 +217,17 @@
217 217
       font-size: 11px;
218 218
     }
219 219
 
220
+    .tempi-table .col-data { width: 16%; white-space: nowrap; }
221
+    .tempi-table .col-fascia { width: 22%; white-space: nowrap; }
222
+    .tempi-table .col-durata { width: 16%; white-space: nowrap; }
223
+    .tempi-table .col-note { color: #444; }
224
+
225
+    .tempi-table tfoot td {
226
+      background-color: #f5f5f5;
227
+      font-weight: bold;
228
+      color: #111;
229
+    }
230
+
220 231
     .task-done {
221 232
       color: #2e7d32;
222 233
       font-weight: 600;
@@ -460,7 +471,32 @@
460 471
 
461 472
     $todosPdf = $todosPdf ?? collect();
462 473
     $annotazioniPdf = $annotazioniPdf ?? collect();
474
+    $tempiPdf = $tempiPdf ?? collect();
475
+    $tempoTotaleLabel = $tempoTotaleLabel ?? \App\Models\Config::formatTempo((int) $tempiPdf->sum('tempo_minuti'));
463 476
     $isClientePdf = $isClientePdf ?? false;
477
+
478
+    $pdfTempoDataLabel = function ($annotazione) {
479
+      $data = $annotazione->tempo_data ?? $annotazione->created_at;
480
+      if ($data instanceof \DateTimeInterface) {
481
+        return $data->format('d/m/Y');
482
+      }
483
+      $raw = trim((string) $data);
484
+      if ($raw === '') {
485
+        return '—';
486
+      }
487
+      try {
488
+        return \Carbon\Carbon::parse($raw)->format('d/m/Y');
489
+      } catch (\Throwable $e) {
490
+        return $raw;
491
+      }
492
+    };
493
+
494
+    $pdfTempoNote = function ($annotazione) {
495
+      $plain = html_entity_decode(strip_tags((string) ($annotazione->text ?? '')), ENT_QUOTES | ENT_HTML5, 'UTF-8');
496
+      $plain = trim(preg_replace('/\s+/u', ' ', $plain) ?? '');
497
+
498
+      return $plain !== '' ? $plain : null;
499
+    };
464 500
     $acquistoServizio = $acquistoServizio ?? null;
465 501
     $isRapportino = $isRapportino ?? false;
466 502
     $rapportinoAnnotazione = $rapportinoAnnotazione ?? null;
@@ -524,7 +560,13 @@
524 560
     </tr>
525 561
     <tr>
526 562
       <th>Assegnata a</th>
563
+      @if(! $isRapportino)
564
+      <td class="field-value">{{ $segnalazione->assegnataA ? $segnalazione->assegnataA->full_name : '---' }}</td>
565
+      <th>Tempo totale</th>
566
+      <td class="field-value">{{ $tempoTotaleLabel }}</td>
567
+      @else
527 568
       <td class="field-value" colspan="3">{{ $segnalazione->assegnataA ? $segnalazione->assegnataA->full_name : '---' }}</td>
569
+      @endif
528 570
     </tr>
529 571
   </table>
530 572
 
@@ -538,6 +580,49 @@
538 580
     @endif
539 581
   </div>
540 582
 
583
+  @if(! $isRapportino)
584
+  <h2 class="section-title" style="margin-top: 18px;">Tempi registrati</h2>
585
+  <table class="tasks-table tempi-table">
586
+    <thead>
587
+      <tr>
588
+        <th class="col-data">Data</th>
589
+        <th class="col-fascia">Inizio – fine</th>
590
+        <th class="col-durata">Durata</th>
591
+        <th class="col-note">Note</th>
592
+      </tr>
593
+    </thead>
594
+    <tbody>
595
+      @forelse($tempiPdf as $tempo)
596
+      @php
597
+        $inizioLabel = $tempo->tempoInizioLabel();
598
+        $fineLabel = $tempo->tempoFineLabel();
599
+        $fasciaLabel = ($inizioLabel || $fineLabel)
600
+          ? (($inizioLabel ?: '—').'–'.($fineLabel ?: '—'))
601
+          : '—';
602
+        $notaTempo = $pdfTempoNote($tempo);
603
+      @endphp
604
+      <tr>
605
+        <td class="col-data">{{ $pdfTempoDataLabel($tempo) }}</td>
606
+        <td class="col-fascia">{{ $fasciaLabel }}</td>
607
+        <td class="col-durata">{{ \App\Models\Config::formatTempo((int) $tempo->tempo_minuti) }}</td>
608
+        <td class="col-note">{{ $notaTempo ?: '—' }}</td>
609
+      </tr>
610
+      @empty
611
+      <tr>
612
+        <td colspan="4">Nessun tempo registrato.</td>
613
+      </tr>
614
+      @endforelse
615
+    </tbody>
616
+    <tfoot>
617
+      <tr>
618
+        <td colspan="2" style="text-align: right;">Tempo totale</td>
619
+        <td>{{ $tempoTotaleLabel }}</td>
620
+        <td></td>
621
+      </tr>
622
+    </tfoot>
623
+  </table>
624
+  @endif
625
+
541 626
   @if($isRapportino && $rapportinoAnnotazione)
542 627
     @php
543 628
       $ann = $rapportinoAnnotazione;

+ 1
- 0
routes/web.php Переглянути файл

@@ -168,6 +168,7 @@ Route::middleware(['auth:sanctum', config('jetstream.auth_session')])->prefix('a
168 168
     Route::post('segnalazione/{id}/aggiorna', [SegnalazioneController::class, 'admin_aggiorna'])->name('admin.segnalazione.aggiorna');
169 169
     Route::delete('segnalazione/{id}/delete', [SegnalazioneController::class, 'admin_delete'])->name('admin.segnalazione.delete');
170 170
     Route::get('segnalazione/{id}/pdf', [SegnalazioneController::class, 'admin_pdf'])->name('admin.segnalazione.pdf');
171
+    Route::post('segnalazione/{id}/pdf/invia', [SegnalazioneController::class, 'admin_pdf_invia'])->name('admin.segnalazione.pdf.invia');
171 172
     Route::get('segnalazione/{id}/annotazione/{annotazione}/rapportino', [SegnalazioneController::class, 'admin_rapportino_annotazione'])->name('admin.segnalazione.annotazione.rapportino');
172 173
 
173 174
     // Dashboard

Завантаження…
Відмінити
Зберегти