Roberto Santini před 1 měsícem
rodič
revize
7763978aba

+ 6
- 20
app/Http/Controllers/HomePageController.php Zobrazit soubor

@@ -107,29 +107,15 @@ class HomePageController extends Controller
107 107
       ->with(['segnalazione.azienda'])
108 108
       ->whereHas('segnalazione', function (Builder $q) {
109 109
         $this->restrictDashboardSegnalazioniQuery($q);
110
-        $q->where('stato', 'pianificata');
110
+        $q->where('segnalazione.stato', 'pianificata');
111 111
       })
112
-      ->where('ends_at', '>=', Carbon::now()->subDays(7)->startOfDay())
112
+      ->whereNotNull('starts_at')
113
+      ->whereNotNull('ends_at')
114
+      ->where('ends_at', '>=', Carbon::now()->startOfDay()->subDay())
113 115
       ->orderBy('starts_at')
114 116
       ->get()
115
-      ->map(function (\App\Models\SegnalazioneSlot $slot) {
116
-        $s = $slot->segnalazione;
117
-        $title = $s ? $s->getLabel() : ('Slot #'.$slot->id);
118
-        if ($s?->azienda) {
119
-          $title .= ' · '.$s->azienda->full_name;
120
-        }
121
-
122
-        return [
123
-          'id' => $slot->id,
124
-          'title' => $title,
125
-          'start' => $slot->starts_at?->toIso8601String(),
126
-          'end' => $slot->ends_at?->toIso8601String(),
127
-          'url' => $s ? route('admin.segnalazione.show', ['id' => $s->id]) : null,
128
-          'backgroundColor' => '#71dd37',
129
-          'borderColor' => '#71dd37',
130
-          'textColor' => '#ffffff',
131
-        ];
132
-      })
117
+      ->map(fn (\App\Models\SegnalazioneSlot $slot) => $slot->toCalendarEvent())
118
+      ->filter()
133 119
       ->values()
134 120
       ->all();
135 121
 

+ 41
- 0
app/Models/AllegatoAnnotazione.php Zobrazit soubor

@@ -3,4 +3,45 @@ namespace App\Models;
3 3
 
4 4
 class AllegatoAnnotazione extends \App\Models\AbstractModels\AbstractAllegatoAnnotazione
5 5
 {
6
+    public function inlineUrl(): ?string
7
+    {
8
+        if (! $this->id) {
9
+            return null;
10
+        }
11
+
12
+        return route('allegato_annotazione.download', [
13
+            'allegato_annotazione_id' => $this->id,
14
+        ]).'?type=file';
15
+    }
16
+
17
+    /**
18
+     * Chiavi per associare un cid: dell'HTML email a questo allegato.
19
+     *
20
+     * @return list<string>
21
+     */
22
+    public function cidLookupKeys(): array
23
+    {
24
+        $keys = [];
25
+        $nome = strtolower(trim((string) $this->nome));
26
+        if ($nome !== '') {
27
+            $keys[] = $nome;
28
+        }
29
+        $base = strtolower(basename((string) $this->path_file));
30
+        if ($base !== '') {
31
+            $keys[] = $base;
32
+            if (preg_match('/(?:^|_)((?:image|img|file)\d+\.[a-z0-9]+)$/i', $base, $m)) {
33
+                $keys[] = strtolower($m[1]);
34
+            }
35
+        }
36
+        $descrizione = strtolower(trim((string) $this->descrizione));
37
+        if (str_starts_with($descrizione, 'cid:')) {
38
+            $cid = substr($descrizione, 4);
39
+            $keys[] = $cid;
40
+            if (str_contains($cid, '@')) {
41
+                $keys[] = strstr($cid, '@', true) ?: $cid;
42
+            }
43
+        }
44
+
45
+        return array_values(array_unique(array_filter($keys)));
46
+    }
6 47
 }

+ 21
- 0
app/Models/Annotazione.php Zobrazit soubor

@@ -8,4 +8,25 @@ class Annotazione extends \App\Models\AbstractModels\AbstractAnnotazione
8 8
   public function setTempoDataAttribute($value){
9 9
     $this->attributes['tempo_data'] = $value!=null?Carbon::createFromFormat('d/m/Y H:i', $value)->toDateTimeString():null;
10 10
   }
11
+
12
+  /**
13
+   * Mappa cid/nome file → URL inline degli allegati (immagini da email).
14
+   *
15
+   * @return array<string, string>
16
+   */
17
+  public function cidImageUrls(): array
18
+  {
19
+    $map = [];
20
+    foreach ($this->allegatiAnnotazione as $allegato) {
21
+      $url = $allegato->inlineUrl();
22
+      if ($url === null) {
23
+        continue;
24
+      }
25
+      foreach ($allegato->cidLookupKeys() as $key) {
26
+        $map[$key] = $url;
27
+      }
28
+    }
29
+
30
+    return $map;
31
+  }
11 32
 }

+ 4
- 4
app/Models/Segnalazione.php Zobrazit soubor

@@ -43,10 +43,10 @@ class Segnalazione extends \App\Models\AbstractModels\AbstractSegnalazione imple
43 43
   //   return $this->hasMany(SegnalazioneTodo::class, 'segnalazione_id', 'id')->orderBy('ordine')->orderBy('id');
44 44
   // }
45 45
 
46
-  // public function slots()
47
-  // {
48
-  //   return $this->hasMany(SegnalazioneSlot::class, 'segnalazione_id', 'id')->orderBy('starts_at');
49
-  // }
46
+  public function slots()
47
+  {
48
+    return $this->hasMany(SegnalazioneSlot::class, 'segnalazione_id', 'id')->orderBy('starts_at');
49
+  }
50 50
 
51 51
   public function getStati(){
52 52
     return \App\Models\ConfiguraSegnalazione::where('gruppo', 'stato')

+ 32
- 0
app/Models/SegnalazioneSlot.php Zobrazit soubor

@@ -1,6 +1,8 @@
1 1
 <?php
2 2
 namespace App\Models;
3 3
 
4
+use Carbon\Carbon;
5
+
4 6
 class SegnalazioneSlot extends \App\Models\AbstractModels\AbstractSegnalazioneSlot
5 7
 {
6 8
   /** Durata in ore decimali. */
@@ -27,4 +29,34 @@ class SegnalazioneSlot extends \App\Models\AbstractModels\AbstractSegnalazioneSl
27 29
 
28 30
         return $start->format('d/m/Y H:i').' → '.$end->format('H:i');
29 31
     }
32
+
33
+    /**
34
+     * Evento FullCalendar (orari locali, senza offset timezone).
35
+     *
36
+     * @return array<string, mixed>|null
37
+     */
38
+    public function toCalendarEvent(): ?array
39
+    {
40
+        if (! $this->starts_at || ! $this->ends_at) {
41
+            return null;
42
+        }
43
+
44
+        $segnalazione = $this->segnalazione;
45
+        $title = $segnalazione ? $segnalazione->getLabel() : ('Slot #'.$this->id);
46
+        if ($segnalazione?->azienda) {
47
+            $title .= ' · '.$segnalazione->azienda->full_name;
48
+        }
49
+
50
+        return [
51
+            'id' => $this->id,
52
+            'title' => $title,
53
+            'start' => $this->starts_at->format('Y-m-d\TH:i:s'),
54
+            'end' => $this->ends_at->format('Y-m-d\TH:i:s'),
55
+            'url' => $segnalazione ? route('admin.segnalazione.show', ['id' => $segnalazione->id]) : null,
56
+            'allDay' => false,
57
+            'backgroundColor' => '#71dd37',
58
+            'borderColor' => '#71dd37',
59
+            'textColor' => '#ffffff',
60
+        ];
61
+    }
30 62
 }

+ 22
- 0
app/Services/SegnalazioneEmailInboxService.php Zobrazit soubor

@@ -12,6 +12,7 @@ use App\Models\User;
12 12
 use App\Notifications\RispostaAutoSegnalazioneEmail;
13 13
 use App\Notifications\RispostaEmailAssegnatario;
14 14
 use App\Support\EmailQuotedReplyStripper;
15
+use App\Support\IsolaHtml;
15 16
 use Illuminate\Support\Facades\Log;
16 17
 use Illuminate\Support\Facades\Notification;
17 18
 use Illuminate\Support\Facades\Storage;
@@ -256,6 +257,7 @@ class SegnalazioneEmailInboxService
256 257
 
257 258
         $annotazione = $this->createAnnotazioneFromMessage($segnalazione, $message, $fromAddress, ! $created);
258 259
         $this->storeAttachments($segnalazione, $annotazione, $message);
260
+        $this->embedCidImages($annotazione);
259 261
 
260 262
         if (! $created) {
261 263
             // Risposta a ticket esistente (anche chiuso o notifica di chiusura): torna in attesa operatore
@@ -446,10 +448,30 @@ class SegnalazioneEmailInboxService
446 448
             $allegato->annotazione()->associate($annotazione);
447 449
             $allegato->nome = $originalName;
448 450
             $allegato->path_file = $path;
451
+            $cid = $this->normalizeCid((string) $attachment->getId());
452
+            if ($cid !== '') {
453
+                $allegato->descrizione = 'cid:'.$cid;
454
+            }
449 455
             $allegato->save();
450 456
         }
451 457
     }
452 458
 
459
+    /**
460
+     * Sostituisce cid: nel HTML dell'annotazione con gli URL degli allegati salvati.
461
+     */
462
+    protected function embedCidImages(Annotazione $annotazione): void
463
+    {
464
+        $annotazione->load('allegatiAnnotazione');
465
+        $html = (string) $annotazione->text;
466
+        $replaced = IsolaHtml::replaceCidImages($html, $annotazione->cidImageUrls());
467
+        if ($replaced === $html) {
468
+            return;
469
+        }
470
+
471
+        $annotazione->text = $replaced;
472
+        $annotazione->save();
473
+    }
474
+
453 475
     /**
454 476
      * Salta loghi/icone di firma (e immagini solo nella citazione), non gli allegati veri.
455 477
      *

+ 72
- 2
app/Support/IsolaHtml.php Zobrazit soubor

@@ -16,13 +16,15 @@ class IsolaHtml
16 16
         'xml',
17 17
     ];
18 18
 
19
-    public static function fragment(?string $html): string
19
+    public static function fragment(?string $html, array $cidUrls = []): string
20 20
     {
21 21
         $html = trim((string) $html);
22 22
         if ($html === '') {
23 23
             return '';
24 24
         }
25 25
 
26
+        $html = self::replaceCidImages($html, $cidUrls);
27
+
26 28
         $html = preg_replace('/<!--\[if[\s\S]*?<!\[endif\]-->/i', '', $html) ?? $html;
27 29
         $html = preg_replace('/<!--[\s\S]*?-->/', '', $html) ?? $html;
28 30
         $html = preg_replace('/<\/?[ovw]:[^>]*>/i', '', $html) ?? $html;
@@ -101,9 +103,77 @@ class IsolaHtml
101 103
         return trim($out);
102 104
     }
103 105
 
106
+    /**
107
+     * Sostituisce src="cid:..." con URL pubblici degli allegati.
108
+     *
109
+     * @param  array<string, string>  $cidUrls  chiavi: content-id o nome file (case-insensitive)
110
+     */
111
+    public static function replaceCidImages(string $html, array $cidUrls): string
112
+    {
113
+        if ($html === '' || $cidUrls === [] || ! str_contains(strtolower($html), 'cid:')) {
114
+            return $html;
115
+        }
116
+
117
+        $normalized = [];
118
+        foreach ($cidUrls as $cid => $url) {
119
+            if (! is_string($url) || $url === '') {
120
+                continue;
121
+            }
122
+            foreach (self::cidLookupKeys((string) $cid) as $key) {
123
+                $normalized[$key] = $url;
124
+            }
125
+        }
126
+        if ($normalized === []) {
127
+            return $html;
128
+        }
129
+
130
+        $replaced = preg_replace_callback(
131
+            '/(<img\b[^>]*?\bsrc\s*=\s*)(["\']?)(cid:[^"\'\s>]+)\2/i',
132
+            function (array $m) use ($normalized) {
133
+                $raw = html_entity_decode($m[3], ENT_QUOTES | ENT_HTML5, 'UTF-8');
134
+                $raw = preg_replace('/^cid:/i', '', $raw) ?? $raw;
135
+                $raw = trim(urldecode($raw), " \t\n\r\0\x0B<>\"'");
136
+                $url = null;
137
+                foreach (self::cidLookupKeys($raw) as $key) {
138
+                    if (isset($normalized[$key])) {
139
+                        $url = $normalized[$key];
140
+                        break;
141
+                    }
142
+                }
143
+                if ($url === null) {
144
+                    return $m[0];
145
+                }
146
+                $quote = $m[2] === '' ? '"' : $m[2];
147
+
148
+                return $m[1].$quote.htmlspecialchars($url, ENT_QUOTES | ENT_HTML5, 'UTF-8').$quote;
149
+            },
150
+            $html
151
+        );
152
+
153
+        return is_string($replaced) ? $replaced : $html;
154
+    }
155
+
156
+    /**
157
+     * @return list<string>
158
+     */
159
+    public static function cidLookupKeys(string $id): array
160
+    {
161
+        $id = strtolower(trim($id, " \t\n\r\0\x0B<>\"'"));
162
+        $id = preg_replace('/^cid:/i', '', $id) ?? $id;
163
+        if ($id === '') {
164
+            return [];
165
+        }
166
+        $keys = [$id];
167
+        if (str_contains($id, '@')) {
168
+            $keys[] = strstr($id, '@', true) ?: $id;
169
+        }
170
+
171
+        return array_values(array_unique(array_filter($keys)));
172
+    }
173
+
104 174
     private static function isDangerousUrl(string $value, string $attrName, string $tagName): bool
105 175
     {
106
-        if (preg_match('/^\s*(javascript|vbscript):/i', $value)) {
176
+        if (preg_match('/^\s*(javascript|vbscript|cid):/i', $value)) {
107 177
             return true;
108 178
         }
109 179
 

+ 76
- 0
resources/assets/js/dashboard-slot-calendar.js Zobrazit soubor

@@ -0,0 +1,76 @@
1
+/**
2
+ * Calendario slot prenotazione (dashboard admin).
3
+ */
4
+import { Calendar } from '@fullcalendar/core';
5
+import itLocale from '@fullcalendar/core/locales/it';
6
+import dayGridPlugin from '@fullcalendar/daygrid';
7
+import interactionPlugin from '@fullcalendar/interaction';
8
+import listPlugin from '@fullcalendar/list';
9
+import timeGridPlugin from '@fullcalendar/timegrid';
10
+
11
+function initSlotCalendar() {
12
+  const calendarEl = document.getElementById('calendarioSlotPrenotazioni');
13
+  if (!calendarEl) {
14
+    return;
15
+  }
16
+
17
+  const dataEl = document.getElementById('calendarioSlotPrenotazioniData');
18
+  let events = [];
19
+  try {
20
+    events = JSON.parse(dataEl ? dataEl.textContent : '[]');
21
+  } catch (e) {
22
+    events = [];
23
+  }
24
+  if (!Array.isArray(events)) {
25
+    events = [];
26
+  }
27
+
28
+  const now = Date.now();
29
+  const starts = events
30
+    .map(function (event) {
31
+      return event && event.start ? Date.parse(event.start) : NaN;
32
+    })
33
+    .filter(function (ts) {
34
+      return !Number.isNaN(ts);
35
+    })
36
+    .sort(function (a, b) {
37
+      return a - b;
38
+    });
39
+  const nextStart = starts.find(function (ts) {
40
+    return ts >= now;
41
+  });
42
+  const initialDate = nextStart || starts[0];
43
+
44
+  const calendar = new Calendar(calendarEl, {
45
+    plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin],
46
+    locale: itLocale,
47
+    firstDay: 1,
48
+    initialView: window.innerWidth < 768 ? 'listWeek' : 'timeGridWeek',
49
+    initialDate: initialDate ? new Date(initialDate) : undefined,
50
+    headerToolbar: {
51
+      left: 'prev,next today',
52
+      center: 'title',
53
+      right: 'dayGridMonth,timeGridWeek,listWeek'
54
+    },
55
+    height: 'auto',
56
+    navLinks: true,
57
+    nowIndicator: true,
58
+    slotMinTime: '06:00:00',
59
+    slotMaxTime: '22:00:00',
60
+    events: events,
61
+    eventClick: function (info) {
62
+      if (info.event.url) {
63
+        info.jsEvent.preventDefault();
64
+        window.location.href = info.event.url;
65
+      }
66
+    }
67
+  });
68
+
69
+  calendar.render();
70
+}
71
+
72
+if (document.readyState === 'loading') {
73
+  document.addEventListener('DOMContentLoaded', initSlotCalendar);
74
+} else {
75
+  initSlotCalendar();
76
+}

+ 8
- 2
resources/views/components/annotazione-html.blade.php Zobrazit soubor

@@ -1,4 +1,10 @@
1
-@props(['html' => ''])
1
+@props(['html' => '', 'annotazione' => null])
2
+
3
+@php
4
+  $cidUrls = $annotazione instanceof \App\Models\Annotazione
5
+    ? $annotazione->cidImageUrls()
6
+    : [];
7
+@endphp
2 8
 
3 9
 @once
4 10
 <style>
@@ -24,5 +30,5 @@
24 30
 @endonce
25 31
 
26 32
 <div {{ $attributes->merge(['class' => 'annotazione-html mb-2']) }}>
27
-  {!! \App\Support\IsolaHtml::fragment($html) !!}
33
+  {!! \App\Support\IsolaHtml::fragment($html, $cidUrls) !!}
28 34
 </div>

+ 3
- 55
resources/views/dashboard/dashboards-manutenzioni.blade.php Zobrazit soubor

@@ -274,8 +274,8 @@ $configData = Helper::appClasses();
274 274
     </div>
275 275
   </div>
276 276
   <div class="card-body">
277
-    <div id="calendarioSlotPrenotazioni" style="min-height: 520px;"
278
-      data-events="{{ json_encode($slotPrenotazioni ?? [], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) }}"></div>
277
+    <script type="application/json" id="calendarioSlotPrenotazioniData">@json($slotPrenotazioni ?? [])</script>
278
+    <div id="calendarioSlotPrenotazioni" style="min-height: 520px;"></div>
279 279
   </div>
280 280
 </div>
281 281
 @endsection
@@ -304,7 +304,7 @@ $configData = Helper::appClasses();
304 304
 
305 305
 @section('vendor-script')
306 306
 @vite([
307
-  'resources/assets/vendor/libs/fullcalendar/fullcalendar.js',
307
+  'resources/assets/js/dashboard-slot-calendar.js',
308 308
 ])
309 309
 @endsection
310 310
 
@@ -329,58 +329,6 @@ $configData = Helper::appClasses();
329 329
 
330 330
   tick();
331 331
   setInterval(tick, 1000);
332
-
333
-  function initSlotCalendar(attempt) {
334
-    var calendarEl = document.getElementById('calendarioSlotPrenotazioni');
335
-    if (!calendarEl) return;
336
-
337
-    if (typeof window.Calendar === 'undefined'
338
-      || typeof window.dayGridPlugin === 'undefined'
339
-      || typeof window.timegridPlugin === 'undefined'
340
-      || typeof window.listPlugin === 'undefined'
341
-      || typeof window.interactionPlugin === 'undefined') {
342
-      if ((attempt || 0) < 40) {
343
-        setTimeout(function () { initSlotCalendar((attempt || 0) + 1); }, 100);
344
-      }
345
-      return;
346
-    }
347
-
348
-    var events = [];
349
-    try {
350
-      events = JSON.parse(calendarEl.getAttribute('data-events') || '[]');
351
-    } catch (e) {
352
-      events = [];
353
-    }
354
-
355
-    var calendar = new window.Calendar(calendarEl, {
356
-      plugins: [window.dayGridPlugin, window.timegridPlugin, window.listPlugin, window.interactionPlugin],
357
-      initialView: window.innerWidth < 768 ? 'listWeek' : 'timeGridWeek',
358
-      headerToolbar: {
359
-        left: 'prev,next today',
360
-        center: 'title',
361
-        right: 'dayGridMonth,timeGridWeek,listWeek'
362
-      },
363
-      height: 'auto',
364
-      navLinks: true,
365
-      nowIndicator: true,
366
-      slotMinTime: '07:00:00',
367
-      slotMaxTime: '21:00:00',
368
-      events: events,
369
-      eventClick: function (info) {
370
-        if (info.event.url) {
371
-          info.jsEvent.preventDefault();
372
-          window.location.href = info.event.url;
373
-        }
374
-      }
375
-    });
376
-    calendar.render();
377
-  }
378
-
379
-  if (document.readyState === 'loading') {
380
-    document.addEventListener('DOMContentLoaded', function () { initSlotCalendar(0); });
381
-  } else {
382
-    initSlotCalendar(0);
383
-  }
384 332
 })();
385 333
 </script>
386 334
 @endsection

+ 12
- 3
resources/views/segnalazione/admin/show.blade.php Zobrazit soubor

@@ -164,7 +164,7 @@ use App\Models\Config;
164 164
         1
165 165
       ) ?? $rawHtml;
166 166
 
167
-      $rawHtml = \App\Support\IsolaHtml::fragment($rawHtml);
167
+      $rawHtml = \App\Support\IsolaHtml::fragment($rawHtml, $ultimaAnnotazioneConTesto->cidImageUrls());
168 168
 
169 169
       $autoreNome = $ultimaAnnotazioneConTesto->user
170 170
         ? $ultimaAnnotazioneConTesto->user->full_name
@@ -567,6 +567,15 @@ use App\Models\Config;
567 567
               </span>
568 568
               @endif
569 569
             </div>
570
+            @if($segnalazione->slots->isNotEmpty())
571
+            <div class="d-flex flex-wrap gap-2 mt-2">
572
+              @foreach($segnalazione->slots as $slot)
573
+              <span class="badge bg-label-info">
574
+                <i class="bx bx-calendar me-1"></i>{{ $slot->label() }}
575
+              </span>
576
+              @endforeach
577
+            </div>
578
+            @endif
570 579
           </div>
571 580
 
572 581
           <div class="col-12 col-lg-3">
@@ -1005,7 +1014,7 @@ use App\Models\Config;
1005 1014
                     @endif
1006 1015
                   </div>
1007 1016
                   @endif
1008
-                  <x-annotazione-html :html="$evento['descrizione']" />
1017
+                  <x-annotazione-html :html="$evento['descrizione']" :annotazione="$evento['dati']['annotazione'] ?? null" />
1009 1018
                   <div class="row g-2">
1010 1019
                     @if($evento['dati']['annotazione'] && $evento['dati']['annotazione']->allegatiAnnotazione->count() > 0)
1011 1020
                     <div class="col-auto">
@@ -1054,7 +1063,7 @@ use App\Models\Config;
1054 1063
                     @endif
1055 1064
                   </div>
1056 1065
                   @endif
1057
-                  <x-annotazione-html :html="$evento['descrizione']" />
1066
+                  <x-annotazione-html :html="$evento['descrizione']" :annotazione="$evento['dati']['annotazione'] ?? null" />
1058 1067
                   <div class="row g-2">
1059 1068
                     @if($evento['dati']['annotazione'] && $evento['dati']['annotazione']->costo != 0)
1060 1069
                     <div class="col-auto">

+ 3
- 3
resources/views/segnalazione/cliente/show.blade.php Zobrazit soubor

@@ -162,7 +162,7 @@ use App\Models\Config;
162 162
         1
163 163
       ) ?? $rawHtml;
164 164
 
165
-      $rawHtml = \App\Support\IsolaHtml::fragment($rawHtml);
165
+      $rawHtml = \App\Support\IsolaHtml::fragment($rawHtml, $ultimaAnnotazioneConTesto->cidImageUrls());
166 166
 
167 167
       $autoreNome = $ultimaAnnotazioneConTesto->user
168 168
         ? $ultimaAnnotazioneConTesto->user->full_name
@@ -815,7 +815,7 @@ use App\Models\Config;
815 815
                     @endif
816 816
                   </div>
817 817
                   @endif
818
-                  <x-annotazione-html :html="$evento['descrizione']" />
818
+                  <x-annotazione-html :html="$evento['descrizione']" :annotazione="$evento['dati']['annotazione'] ?? null" />
819 819
                   <div class="row g-2">
820 820
                     @if($evento['dati']['annotazione'] && $evento['dati']['annotazione']->allegatiAnnotazione->count() > 0)
821 821
                     <div class="col-auto">
@@ -864,7 +864,7 @@ use App\Models\Config;
864 864
                     @endif
865 865
                   </div>
866 866
                   @endif
867
-                  <x-annotazione-html :html="$evento['descrizione']" />
867
+                  <x-annotazione-html :html="$evento['descrizione']" :annotazione="$evento['dati']['annotazione'] ?? null" />
868 868
                   <div class="row g-2">
869 869
                     @if($evento['dati']['annotazione'] && $evento['dati']['annotazione']->costo != 0)
870 870
                     <div class="col-auto">

+ 20
- 4
resources/views/segnalazione/pdf-ticket.blade.php Zobrazit soubor

@@ -430,12 +430,28 @@
430 430
       ? $annotazioneAllegati->allegatiAnnotazione->filter($pdfIsImageAllegato)->values()
431 431
       : collect();
432 432
 
433
-    $pdfAnnotazioneHtml = function (?string $html) {
433
+    $pdfAnnotazioneHtml = function (?string $html, $annotazione = null) use ($pdfImageBase64, $pdfIsImageAllegato) {
434 434
       if (!filled(trim(strip_tags((string) $html)))) {
435 435
         return null;
436 436
       }
437 437
 
438
-      $html = \App\Support\IsolaHtml::fragment((string) $html);
438
+      $cidUrls = [];
439
+      if ($annotazione) {
440
+        foreach ($annotazione->allegatiAnnotazione as $allegato) {
441
+          if (! $pdfIsImageAllegato($allegato)) {
442
+            continue;
443
+          }
444
+          $dataUri = $pdfImageBase64($allegato);
445
+          if (! $dataUri) {
446
+            continue;
447
+          }
448
+          foreach ($allegato->cidLookupKeys() as $key) {
449
+            $cidUrls[$key] = $dataUri;
450
+          }
451
+        }
452
+      }
453
+
454
+      $html = \App\Support\IsolaHtml::fragment((string) $html, $cidUrls);
439 455
       $html = preg_replace('/<p>\s*(<br\s*\/?>)?\s*<\/p>/i', '', $html);
440 456
       $html = preg_replace('/(<br\s*\/?>\s*){2,}/i', '<br>', $html);
441 457
 
@@ -495,7 +511,7 @@
495 511
 
496 512
   <h2 class="section-title">Descrizione segnalazione</h2>
497 513
   <div class="annotation-box">
498
-    @php $testoAnnotazione = $primaAnnotazione ? $pdfAnnotazioneHtml($primaAnnotazione->text) : null; @endphp
514
+    @php $testoAnnotazione = $primaAnnotazione ? $pdfAnnotazioneHtml($primaAnnotazione->text, $primaAnnotazione) : null; @endphp
499 515
     @if($testoAnnotazione)
500 516
       {!! $testoAnnotazione !!}
501 517
     @else
@@ -555,7 +571,7 @@
555 571
   @if($altreAnnotazioni->isNotEmpty())
556 572
   <h2 class="section-title" style="margin-top: 10px;">Annotazioni</h2>
557 573
   @foreach($altreAnnotazioni as $annotazione)
558
-    @php $testoAltra = $pdfAnnotazioneHtml($annotazione->text); @endphp
574
+    @php $testoAltra = $pdfAnnotazioneHtml($annotazione->text, $annotazione); @endphp
559 575
     @if($testoAltra)
560 576
     <div class="annotation-item">
561 577
       <p class="annotation-meta">

+ 36
- 0
tests/Unit/IsolaHtmlTest.php Zobrazit soubor

@@ -0,0 +1,36 @@
1
+<?php
2
+
3
+namespace Tests\Unit;
4
+
5
+use App\Support\IsolaHtml;
6
+use PHPUnit\Framework\TestCase;
7
+
8
+class IsolaHtmlTest extends TestCase
9
+{
10
+    public function test_replaces_outlook_cid_image_with_attachment_url(): void
11
+    {
12
+        $html = '<p>Foto</p><img src="cid:image001.png@01DD469C.EEAD9A20" alt="img">';
13
+        $url = '/allegato_annotazione/download/42?type=file';
14
+
15
+        $out = IsolaHtml::fragment($html, ['image001.png' => $url]);
16
+
17
+        $this->assertStringContainsString('src="'.$url.'"', $out);
18
+        $this->assertStringNotContainsString('cid:', $out);
19
+    }
20
+
21
+    public function test_keeps_http_images(): void
22
+    {
23
+        $html = '<img src="https://example.test/foto.png" alt="x">';
24
+        $out = IsolaHtml::fragment($html);
25
+
26
+        $this->assertStringContainsString('https://example.test/foto.png', $out);
27
+    }
28
+
29
+    public function test_strips_unmatched_cid_src(): void
30
+    {
31
+        $html = '<img src="cid:missing.png@abc" alt="x">';
32
+        $out = IsolaHtml::fragment($html, []);
33
+
34
+        $this->assertStringNotContainsString('cid:', $out);
35
+    }
36
+}

+ 34
- 0
tests/Unit/SegnalazioneSlotTest.php Zobrazit soubor

@@ -0,0 +1,34 @@
1
+<?php
2
+
3
+namespace Tests\Unit;
4
+
5
+use App\Models\Segnalazione;
6
+use App\Models\SegnalazioneSlot;
7
+use Carbon\Carbon;
8
+use Tests\TestCase;
9
+
10
+class SegnalazioneSlotTest extends TestCase
11
+{
12
+    public function test_to_calendar_event_uses_local_datetime(): void
13
+    {
14
+        $segnalazione = new Segnalazione();
15
+        $segnalazione->id = 12;
16
+        $segnalazione->titolo = 'Manutenzione caldaia';
17
+
18
+        $slot = new SegnalazioneSlot([
19
+            'id' => 5,
20
+            'starts_at' => Carbon::parse('2026-09-21 09:30:00'),
21
+            'ends_at' => Carbon::parse('2026-09-21 11:00:00'),
22
+        ]);
23
+        $slot->setRelation('segnalazione', $segnalazione);
24
+
25
+        $event = $slot->toCalendarEvent();
26
+
27
+        $this->assertSame(5, $event['id']);
28
+        $this->assertSame('#12 - Manutenzione caldaia', $event['title']);
29
+        $this->assertSame('2026-09-21T09:30:00', $event['start']);
30
+        $this->assertSame('2026-09-21T11:00:00', $event['end']);
31
+        $this->assertFalse($event['allDay']);
32
+        $this->assertStringContainsString('/segnalazione/12', $event['url']);
33
+    }
34
+}

Loading…
Zrušit
Uložit