Roberto Santini пре 3 недеља
родитељ
комит
8c5eaff6ad
48 измењених фајлова са 2700 додато и 1029 уклоњено
  1. 634
    8
      GestioneManutenzioni.skipper
  2. 1
    1
      app/DataTables/SegnalazioneClienteDataTable.php
  3. 12
    3
      app/Http/Controllers/AnnotazioniController.php
  4. 5
    0
      app/Http/Controllers/ConfigController.php
  5. 127
    59
      app/Http/Controllers/HomePageController.php
  6. 3
    24
      app/Http/Controllers/SegnalazioneClienteController.php
  7. 83
    400
      app/Http/Controllers/SegnalazioneController.php
  8. 2
    2
      app/Models/AbstractModels/AbstractContrattoServizio.php
  9. 2
    2
      app/Models/AbstractModels/AbstractContrattoTemplate.php
  10. 1
    1
      app/Models/AbstractModels/AbstractFattura.php
  11. 1
    1
      app/Models/AbstractModels/AbstractOfferta.php
  12. 5
    0
      app/Models/AbstractModels/AbstractOrdineLavoro.php
  13. 85
    0
      app/Models/AbstractModels/AbstractOrdineLavoroRiga.php
  14. 6
    1
      app/Models/AbstractModels/AbstractProdotto.php
  15. 6
    1
      app/Models/AbstractModels/AbstractSegnalazione.php
  16. 59
    0
      app/Models/AbstractModels/AbstractSegnalazioneSlot.php
  17. 52
    0
      app/Models/Config.php
  18. 12
    12
      app/Models/ConfiguraSegnalazione.php
  19. 9
    9
      app/Models/ContrattoServizio.php
  20. 26
    26
      app/Models/ContrattoServizioRiga.php
  21. 8
    8
      app/Models/ContrattoTemplate.php
  22. 4
    4
      app/Models/Fattura.php
  23. 4
    4
      app/Models/Offerta.php
  24. 0
    5
      app/Models/OrdineLavoro.php
  25. 2
    50
      app/Models/OrdineLavoroRiga.php
  26. 0
    5
      app/Models/Prodotto.php
  27. 70
    25
      app/Models/Segnalazione.php
  28. 30
    0
      app/Models/SegnalazioneSlot.php
  29. 55
    0
      app/Notifications/RispostaAutoSegnalazioneEmail.php
  30. 31
    18
      app/Services/SegnalazioneEmailInboxService.php
  31. 0
    49
      database/migrations/2026_08_22_000100_create_ordine_lavoro_riga_table.php
  32. 20
    3
      database/migrations/2026_08_26_152346_skipper_migrations_2026082615234643.php
  33. 206
    0
      database/migrations/2026_08_26_153903_skipper_migrations_2026082615390373.php
  34. 7
    0
      resources/menu/verticalMenu.json
  35. 20
    0
      resources/views/config/index.blade.php
  36. 221
    0
      resources/views/dashboard/dashboards-manutenzioni-statistiche.blade.php
  37. 235
    257
      resources/views/dashboard/dashboards-manutenzioni.blade.php
  38. 1
    1
      resources/views/layouts/guest.blade.php
  39. 1
    1
      resources/views/mail/annotazione-pubblica-aperta-da.blade.php
  40. 1
    1
      resources/views/mail/nuova-segnalazione-reparto.blade.php
  41. 1
    1
      resources/views/mail/nuova-segnalazione-user.blade.php
  42. 14
    0
      resources/views/mail/risposta-auto-segnalazione-email.blade.php
  43. 1
    1
      resources/views/mail/segnalazione-stato-cambiato-reparto.blade.php
  44. 190
    4
      resources/views/segnalazione/admin/show.blade.php
  45. 3
    3
      resources/views/segnalazione/cliente/index.blade.php
  46. 0
    33
      resources/views/segnalazione/cliente/show.blade.php
  47. 443
    0
      resources/views/segnalazione/show.blade.php
  48. 1
    6
      routes/web.php

+ 634
- 8
GestioneManutenzioni.skipper Прегледај датотеку

@@ -412,7 +412,7 @@
412 412
         <attribute name="table">offerta_riga</attribute>
413 413
       </orm-attributes>
414 414
     </entity>
415
-    <association from="\App\Models\OffertaRiga" to="\App\Models\Offerta" caption="Offerta OffertaRigas" owner-alias="offerteRiga" inverse-alias="offerta" many-owner="true" many-inverse="false" uuid="fdd9ee5a-9ae1-4736-8b9a-7e7fb84061b3">
415
+    <association from="\App\Models\OffertaRiga" to="\App\Models\Offerta" caption="Offerta OffertaRigas" owner-alias="righe" inverse-alias="offerta" many-owner="true" many-inverse="false" uuid="fdd9ee5a-9ae1-4736-8b9a-7e7fb84061b3">
416 416
       <association-field from="offerta_id" to="id" uuid="68a5419f-adef-4d6e-9b28-873c2fbc3d50"/>
417 417
       <orm-attributes>
418 418
         <attribute name="onDelete">set null</attribute>
@@ -506,7 +506,7 @@
506 506
         <attribute name="table">contratto_template_todo</attribute>
507 507
       </orm-attributes>
508 508
     </entity>
509
-    <association from="\App\Models\ContrattoTemplateTodo" to="\App\Models\ContrattoTemplate" caption="ContrattoTemplate ContrattoTemplateTodos" owner-alias="contrattoTemplateTodos" inverse-alias="contrattoTemplate" many-owner="true" many-inverse="false" uuid="35b0ed5e-e7f9-4a4f-ad24-2547a9392e87">
509
+    <association from="\App\Models\ContrattoTemplateTodo" to="\App\Models\ContrattoTemplate" caption="ContrattoTemplate ContrattoTemplateTodos" owner-alias="todos" inverse-alias="contrattoTemplate" many-owner="true" many-inverse="false" uuid="35b0ed5e-e7f9-4a4f-ad24-2547a9392e87">
510 510
       <association-field from="contratto_template_id" to="id" uuid="585c20ce-9da8-4fe2-9926-0681cd0c32f1"/>
511 511
       <orm-attributes>
512 512
         <attribute name="onDelete">cascade</attribute>
@@ -537,7 +537,7 @@
537 537
         <attribute name="table">contratto_servizio_todo</attribute>
538 538
       </orm-attributes>
539 539
     </entity>
540
-    <association from="\App\Models\ContrattoServizioTodo" to="\App\Models\ContrattoServizio" caption="ContrattoServizio ContrattoServizioTodos" owner-alias="contrattoServizioTodos" inverse-alias="contrattoServizio" many-owner="true" many-inverse="false" uuid="3e2b345d-f7f3-47e9-b6da-39b2c597e870">
540
+    <association from="\App\Models\ContrattoServizioTodo" to="\App\Models\ContrattoServizio" caption="ContrattoServizio ContrattoServizioTodos" owner-alias="todos" inverse-alias="contrattoServizio" many-owner="true" many-inverse="false" uuid="3e2b345d-f7f3-47e9-b6da-39b2c597e870">
541 541
       <association-field from="contratto_servizio_id" to="id" uuid="599210f0-f87a-4280-badd-0717a4fbcd40"/>
542 542
       <orm-attributes>
543 543
         <attribute name="onDelete">cascade</attribute>
@@ -580,7 +580,7 @@
580 580
         <attribute name="onDelete">set null</attribute>
581 581
       </orm-attributes>
582 582
     </association>
583
-    <association from="\App\Models\ContrattoTemplateRiga" to="\App\Models\ContrattoTemplate" caption="ContrattoTemplate ContrattoTemplateRigas" owner-alias="contrattoTemplateRigas" inverse-alias="contrattoTemplate" many-owner="true" many-inverse="false" uuid="79ac3c47-aa91-4b9f-a46d-b5aa7c818f6f">
583
+    <association from="\App\Models\ContrattoTemplateRiga" to="\App\Models\ContrattoTemplate" caption="ContrattoTemplate ContrattoTemplateRigas" owner-alias="righe" inverse-alias="contrattoTemplate" many-owner="true" many-inverse="false" uuid="79ac3c47-aa91-4b9f-a46d-b5aa7c818f6f">
584 584
       <association-field from="contratto_template_id" to="id" uuid="868068aa-7b93-4c95-ae09-588db5fe51e2"/>
585 585
       <orm-attributes>
586 586
         <attribute name="onDelete">cascade</attribute>
@@ -625,7 +625,7 @@
625 625
         <attribute name="onDelete">set null</attribute>
626 626
       </orm-attributes>
627 627
     </association>
628
-    <association from="\App\Models\ContrattoServizioRiga" to="\App\Models\ContrattoServizio" caption="ContrattoServizio ContrattoServizioRigas" owner-alias="contrattoServizioRigas" inverse-alias="contrattoServizio" many-owner="true" many-inverse="false" uuid="b2867c66-0dcf-4a4c-99ce-cbdda8b11d6f">
628
+    <association from="\App\Models\ContrattoServizioRiga" to="\App\Models\ContrattoServizio" caption="ContrattoServizio ContrattoServizioRigas" owner-alias="righe" inverse-alias="contrattoServizio" many-owner="true" many-inverse="false" uuid="b2867c66-0dcf-4a4c-99ce-cbdda8b11d6f">
629 629
       <association-field from="contratto_servizio_id" to="id" uuid="79825cde-3073-435d-9dec-fec172cbf19d"/>
630 630
       <orm-attributes>
631 631
         <attribute name="onDelete">cascade</attribute>
@@ -744,7 +744,7 @@
744 744
         <attribute name="table">prodotto_todo</attribute>
745 745
       </orm-attributes>
746 746
     </entity>
747
-    <association from="\App\Models\ProdottoTodo" to="\App\Models\Prodotto" caption="Prodotto ProdottoTodos" owner-alias="prodottoTodos" inverse-alias="prodotto" many-owner="true" many-inverse="false" uuid="6debb5a0-4bc9-4e03-bafa-569e49949ad6">
747
+    <association from="\App\Models\ProdottoTodo" to="\App\Models\Prodotto" caption="Prodotto ProdottoTodos" owner-alias="todos" inverse-alias="prodotto" many-owner="true" many-inverse="false" uuid="6debb5a0-4bc9-4e03-bafa-569e49949ad6">
748 748
       <association-field from="prodotto_id" to="id" uuid="8739cbb8-97d0-4b24-bdf2-7218bba13cf2"/>
749 749
       <orm-attributes>
750 750
         <attribute name="onDelete">cascade</attribute>
@@ -769,7 +769,7 @@
769 769
         <attribute name="table">segnalazione_todo</attribute>
770 770
       </orm-attributes>
771 771
     </entity>
772
-    <association from="\App\Models\SegnalazioneTodo" to="\App\Models\Segnalazione" caption="Segnalazione SegnalazioneTodos" owner-alias="segnalazioneTodos" inverse-alias="segnalazione" many-owner="true" many-inverse="false" uuid="ae43bea5-a4e1-4e9c-881c-ab2a91c7b743">
772
+    <association from="\App\Models\SegnalazioneTodo" to="\App\Models\Segnalazione" caption="Segnalazione SegnalazioneTodos" owner-alias="todos" inverse-alias="segnalazione" many-owner="true" many-inverse="false" uuid="ae43bea5-a4e1-4e9c-881c-ab2a91c7b743">
773 773
       <association-field from="segnalazione_id" to="id" uuid="7c11bde8-f541-4a43-aa6b-a8583a591dbe"/>
774 774
       <orm-attributes>
775 775
         <attribute name="onDelete">cascade</attribute>
@@ -920,7 +920,7 @@
920 920
         <attribute name="table">fattura_riga</attribute>
921 921
       </orm-attributes>
922 922
     </entity>
923
-    <association from="\App\Models\FatturaRiga" to="\App\Models\Fattura" caption="Fattura FatturaRigas" owner-alias="fatturaRighe" inverse-alias="fattura" many-owner="true" many-inverse="false" uuid="d69021ca-1036-4659-b1bb-431dd895db02">
923
+    <association from="\App\Models\FatturaRiga" to="\App\Models\Fattura" caption="Fattura FatturaRigas" owner-alias="righe" inverse-alias="fattura" many-owner="true" many-inverse="false" uuid="d69021ca-1036-4659-b1bb-431dd895db02">
924 924
       <association-field from="fattura_id" to="id" uuid="2099a305-fbd4-4076-a85c-d0d3c96d0a5e"/>
925 925
       <orm-attributes>
926 926
         <attribute name="onDelete">cascade</attribute>
@@ -962,6 +962,71 @@
962 962
         <attribute name="onDelete">set null</attribute>
963 963
       </orm-attributes>
964 964
     </association>
965
+    <entity name="\App\Models\SegnalazioneSlot" local-name="SegnalazioneSlot" namespace="\App\Models" uuid="e11f8bb2-17fd-4770-a490-a416d2d49f6c">
966
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="72d22456-ccc5-4806-9743-b91635948ea7"/>
967
+      <field name="segnalazione_id" type="bigInteger" unsigned="true" uuid="e1a2b3f2-59be-4c31-ba82-d1e413cf5cc9"/>
968
+      <field name="starts_at" type="dateTime" uuid="25084066-cd50-4459-8b21-dacdb3a988aa"/>
969
+      <field name="ends_at" type="dateTime" uuid="310f5a51-80c7-444c-a53d-2b8c9ac5401d"/>
970
+      <field name="created_at" type="timestamp" uuid="db734a85-a27a-4254-ae8c-63d472df57a7"/>
971
+      <field name="updated_at" type="timestamp" uuid="f3fe4780-00f5-4a30-86a3-ff7fd643d629"/>
972
+      <orm-attributes>
973
+        <attribute name="fillableAll">as-fillable-all</attribute>
974
+        <attribute name="table">segnalazione_slot</attribute>
975
+      </orm-attributes>
976
+    </entity>
977
+    <association from="\App\Models\SegnalazioneSlot" to="\App\Models\Segnalazione" caption="Segnalazione SegnalazioneSlots" owner-alias="slots" inverse-alias="segnalazione" many-owner="true" many-inverse="false" uuid="66d48b8b-7984-430a-92e7-fbe21fce135f">
978
+      <association-field from="segnalazione_id" to="id" uuid="c75ea229-0d18-4d5e-8c5a-cb7cc0c8dff6"/>
979
+      <orm-attributes>
980
+        <attribute name="onDelete">cascade</attribute>
981
+      </orm-attributes>
982
+    </association>
983
+    <entity name="\App\Models\OrdineLavoroRiga" local-name="OrdineLavoroRiga" namespace="\App\Models" uuid="04287ee6-13fe-4b73-970a-aedefd569698">
984
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="7ed8a1d0-2b4d-418b-b371-60d8ab5c6361"/>
985
+      <field name="ordine_lavoro_id" type="bigInteger" unsigned="true" uuid="bd447a10-6098-49fa-b342-38bfc6c0271d"/>
986
+      <field name="prodotto_id" type="bigInteger" unsigned="true" uuid="05297563-07a0-4cbb-876d-cf811973af8b"/>
987
+      <field name="descrizione" type="string" uuid="dddc3ab2-3d27-4954-abb0-7add4bd39045"/>
988
+      <field name="quantita" type="decimal" size="10" uuid="6df25b5a-0b5b-4b75-9f1f-de5b1b33fc81">
989
+        <orm-attributes>
990
+          <attribute name="decimal">2</attribute>
991
+        </orm-attributes>
992
+      </field>
993
+      <field name="unita_misura" type="string" uuid="4d89aa02-bd75-470c-95b2-6ed1086fecbb"/>
994
+      <field name="prezzo_unitario" type="decimal" size="10" uuid="9d4792f1-0b30-48f8-ac62-4be597728e15">
995
+        <orm-attributes>
996
+          <attribute name="decimal">2</attribute>
997
+        </orm-attributes>
998
+      </field>
999
+      <field name="sconto" type="decimal" size="10" uuid="b1bec05e-c03e-4cd5-abb2-56a077292f3a">
1000
+        <orm-attributes>
1001
+          <attribute name="decimal">2</attribute>
1002
+        </orm-attributes>
1003
+      </field>
1004
+      <field name="ordine" type="integer" default="0" uuid="1411b9dc-1fce-4a6d-a961-0e631ebf42da"/>
1005
+      <field name="note_private" type="text" uuid="2c203621-a3dc-4adf-97e9-387293ec5d2f"/>
1006
+      <field name="tempo_stimato" type="decimal" size="10" uuid="6cc655ed-c6b9-45a6-b078-1dbae13e93c6">
1007
+        <orm-attributes>
1008
+          <attribute name="decimal">2</attribute>
1009
+        </orm-attributes>
1010
+      </field>
1011
+      <field name="created_at" type="timestamp" uuid="15b7da2d-8558-47b6-bbf2-88556147d03d"/>
1012
+      <field name="updated_at" type="timestamp" uuid="fea3f591-b802-401b-b494-0648197ec020"/>
1013
+      <orm-attributes>
1014
+        <attribute name="fillableAll">as-fillable-all</attribute>
1015
+        <attribute name="table">ordine_lavoro_riga</attribute>
1016
+      </orm-attributes>
1017
+    </entity>
1018
+    <association from="\App\Models\OrdineLavoroRiga" to="\App\Models\OrdineLavoro" caption="OrdineLavoro OrdineLavoroRigas" owner-alias="righe" inverse-alias="ordineLavoro" many-owner="true" many-inverse="false" uuid="1e8af9f9-8d2e-4c16-8530-28e43a622c6d">
1019
+      <association-field from="ordine_lavoro_id" to="id" uuid="bf0d5c13-4e24-4438-8e1f-dcc24809cbc6"/>
1020
+      <orm-attributes>
1021
+        <attribute name="onDelete">cascade</attribute>
1022
+      </orm-attributes>
1023
+    </association>
1024
+    <association from="\App\Models\OrdineLavoroRiga" to="\App\Models\Prodotto" caption="Prodotto OrdineLavoroRigas" owner-alias="ordineLavoroRigas" inverse-alias="prodotto" many-owner="true" many-inverse="false" uuid="350c8c4f-877e-4925-8385-b750d5f625da">
1025
+      <association-field from="prodotto_id" to="id" uuid="6b67c71b-1af2-4f55-9a45-b7bdfe642ad9"/>
1026
+      <orm-attributes>
1027
+        <attribute name="onDelete">set null</attribute>
1028
+      </orm-attributes>
1029
+    </association>
965 1030
     <migrations version="1.0">
966 1031
       <revision uuid="140bda96-ff98-4f0d-894d-165e556494e9" date="2026-08-09 01:24:26.374386" exportable="true">
967 1032
         <element action="add" uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="module">
@@ -3264,13 +3329,571 @@
3264 3329
         </element>
3265 3330
         <element action="reorder" uuid="40919a29-47f6-4516-9c3d-917ed2c9afe6" parent-uuid="56185713-1439-4bd9-81df-6d5339d066ce" sibling-uuid="33ec009d-c086-49d1-9d60-78edb4e17fc7" previous-sibling-uuid="519bbe2d-e5b8-49c0-b964-08644a0c119f" type="field" helper="true"/>
3266 3331
       </revision>
3332
+      <revision uuid="3404b97d-dc5c-4338-b8dd-c14acb80c725" date="2026-08-26 15:23:46.436721" exportable="true">
3333
+        <element action="add" uuid="68922686-8dc2-403e-a281-a0efcdf39a82" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="entity">
3334
+          <property name="fillableAll" value="as-fillable-all"/>
3335
+          <property name="local-name" value="Fattura"/>
3336
+          <property name="name" value="\App\Models\Fattura"/>
3337
+          <property name="namespace" value="\App\Models"/>
3338
+          <property name="table" value="fattura"/>
3339
+        </element>
3340
+        <element action="add" uuid="6cc482e6-604b-4e73-9870-36f4f2711362" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="entity">
3341
+          <property name="fillableAll" value="as-fillable-all"/>
3342
+          <property name="local-name" value="FatturaRiga"/>
3343
+          <property name="name" value="\App\Models\FatturaRiga"/>
3344
+          <property name="namespace" value="\App\Models"/>
3345
+          <property name="table" value="fattura_riga"/>
3346
+        </element>
3347
+        <element action="add" uuid="e11f8bb2-17fd-4770-a490-a416d2d49f6c" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="entity">
3348
+          <property name="fillableAll" value="as-fillable-all"/>
3349
+          <property name="local-name" value="SegnalazioneSlot"/>
3350
+          <property name="name" value="\App\Models\SegnalazioneSlot"/>
3351
+          <property name="namespace" value="\App\Models"/>
3352
+          <property name="table" value="segnalazione_slot"/>
3353
+        </element>
3354
+        <element action="add" uuid="8d96db71-3d83-442b-8fde-d4ef73b998d1" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" type="field">
3355
+          <property name="auto-increment" value="true"/>
3356
+          <property name="name" value="id"/>
3357
+          <property name="primary" value="true"/>
3358
+          <property name="required" value="true"/>
3359
+          <property name="type" value="bigInteger"/>
3360
+          <property name="unique" value="true"/>
3361
+          <property name="unsigned" value="true"/>
3362
+        </element>
3363
+        <element action="add" uuid="af9e2aba-ed74-4675-b2a6-b8e5849d4636" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="8d96db71-3d83-442b-8fde-d4ef73b998d1" type="field">
3364
+          <property name="name" value="user_id"/>
3365
+          <property name="type" value="bigInteger"/>
3366
+          <property name="unsigned" value="true"/>
3367
+        </element>
3368
+        <element action="add" uuid="c5f80f70-abda-43ea-bd96-4342fc0c1135" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="af9e2aba-ed74-4675-b2a6-b8e5849d4636" type="field">
3369
+          <property name="name" value="created_by_id"/>
3370
+          <property name="type" value="bigInteger"/>
3371
+          <property name="unsigned" value="true"/>
3372
+        </element>
3373
+        <element action="add" uuid="814b84d9-8b89-40a5-9e01-cc834ea50b6e" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="c5f80f70-abda-43ea-bd96-4342fc0c1135" type="field">
3374
+          <property name="name" value="numero"/>
3375
+          <property name="type" value="integer"/>
3376
+        </element>
3377
+        <element action="add" uuid="9c2b755c-2f48-40fc-a96d-db71a3ae8b09" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="814b84d9-8b89-40a5-9e01-cc834ea50b6e" type="field">
3378
+          <property name="name" value="stato"/>
3379
+          <property name="type" value="string"/>
3380
+        </element>
3381
+        <element action="add" uuid="c4f78038-4cfb-4dd1-bcac-b9d383a4d59b" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="9c2b755c-2f48-40fc-a96d-db71a3ae8b09" type="field">
3382
+          <property name="name" value="data"/>
3383
+          <property name="type" value="date"/>
3384
+        </element>
3385
+        <element action="add" uuid="6e93d6fb-f64c-41da-84bb-a80a5c2e3746" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="c4f78038-4cfb-4dd1-bcac-b9d383a4d59b" type="field">
3386
+          <property name="name" value="data_scadenza"/>
3387
+          <property name="type" value="date"/>
3388
+        </element>
3389
+        <element action="add" uuid="306e5089-edea-4e61-92ff-9e0775233e9d" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="6e93d6fb-f64c-41da-84bb-a80a5c2e3746" type="field">
3390
+          <property name="name" value="periodo_da"/>
3391
+          <property name="type" value="date"/>
3392
+        </element>
3393
+        <element action="add" uuid="56dc7aa8-f831-4d0f-8fef-5164a0913193" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="306e5089-edea-4e61-92ff-9e0775233e9d" type="field">
3394
+          <property name="name" value="periodo_a"/>
3395
+          <property name="type" value="date"/>
3396
+        </element>
3397
+        <element action="add" uuid="920496ba-c681-43bf-bd2e-8bc0790e7ef3" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="56dc7aa8-f831-4d0f-8fef-5164a0913193" type="field">
3398
+          <property name="decimal" value="2"/>
3399
+          <property name="name" value="imponibile"/>
3400
+          <property name="size" value="10"/>
3401
+          <property name="type" value="decimal"/>
3402
+        </element>
3403
+        <element action="add" uuid="5859d5c0-be4b-482d-9f58-21ec4db541c2" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="920496ba-c681-43bf-bd2e-8bc0790e7ef3" type="field">
3404
+          <property name="decimal" value="2"/>
3405
+          <property name="name" value="iva_percentuale"/>
3406
+          <property name="size" value="10"/>
3407
+          <property name="type" value="decimal"/>
3408
+        </element>
3409
+        <element action="add" uuid="d356d45a-2226-4425-a50b-9b1e4eedfa6e" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="5859d5c0-be4b-482d-9f58-21ec4db541c2" type="field">
3410
+          <property name="decimal" value="2"/>
3411
+          <property name="name" value="iva"/>
3412
+          <property name="size" value="10"/>
3413
+          <property name="type" value="decimal"/>
3414
+        </element>
3415
+        <element action="add" uuid="9903207b-f0cd-444d-adbf-3611f1ebf4d8" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="d356d45a-2226-4425-a50b-9b1e4eedfa6e" type="field">
3416
+          <property name="decimal" value="2"/>
3417
+          <property name="name" value="totale"/>
3418
+          <property name="size" value="10"/>
3419
+          <property name="type" value="decimal"/>
3420
+        </element>
3421
+        <element action="add" uuid="763c3bf5-05b8-438a-94c7-0c4384affaba" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="9903207b-f0cd-444d-adbf-3611f1ebf4d8" type="field">
3422
+          <property name="name" value="note"/>
3423
+          <property name="type" value="text"/>
3424
+        </element>
3425
+        <element action="add" uuid="075bf658-058d-424f-b48e-b1e28b1e049e" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="763c3bf5-05b8-438a-94c7-0c4384affaba" type="field">
3426
+          <property name="name" value="path_file"/>
3427
+          <property name="type" value="string"/>
3428
+        </element>
3429
+        <element action="add" uuid="576e02ce-8672-492c-b6ec-01308fea5622" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="075bf658-058d-424f-b48e-b1e28b1e049e" type="field">
3430
+          <property name="name" value="ragione_sociale"/>
3431
+          <property name="type" value="string"/>
3432
+        </element>
3433
+        <element action="add" uuid="b7cff289-6dd6-456e-85c2-42ca63b1e1e9" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="576e02ce-8672-492c-b6ec-01308fea5622" type="field">
3434
+          <property name="name" value="piva"/>
3435
+          <property name="type" value="string"/>
3436
+        </element>
3437
+        <element action="add" uuid="48f48aab-d7ef-414d-83a0-7ab98f913223" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="b7cff289-6dd6-456e-85c2-42ca63b1e1e9" type="field">
3438
+          <property name="name" value="cod_fiscale"/>
3439
+          <property name="type" value="string"/>
3440
+        </element>
3441
+        <element action="add" uuid="02f2de90-e5ee-4b4a-80a0-8d3cf2dd72a4" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="48f48aab-d7ef-414d-83a0-7ab98f913223" type="field">
3442
+          <property name="name" value="indirizzo"/>
3443
+          <property name="type" value="string"/>
3444
+        </element>
3445
+        <element action="add" uuid="bde98b8c-6c2d-4841-9df7-887e4b220f88" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="02f2de90-e5ee-4b4a-80a0-8d3cf2dd72a4" type="field">
3446
+          <property name="name" value="email"/>
3447
+          <property name="type" value="string"/>
3448
+        </element>
3449
+        <element action="add" uuid="3a91a095-aeb7-4b60-9933-f8614d0e3162" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="bde98b8c-6c2d-4841-9df7-887e4b220f88" type="field">
3450
+          <property name="name" value="telefono"/>
3451
+          <property name="type" value="string"/>
3452
+        </element>
3453
+        <element action="add" uuid="48bd7251-8781-4882-8e2a-5f40f28052c6" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="3a91a095-aeb7-4b60-9933-f8614d0e3162" type="field">
3454
+          <property name="name" value="meta"/>
3455
+          <property name="type" value="json"/>
3456
+        </element>
3457
+        <element action="add" uuid="a976501c-a52b-4bc6-ba45-f22d7f7ec967" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="48bd7251-8781-4882-8e2a-5f40f28052c6" type="field">
3458
+          <property name="name" value="comune_id"/>
3459
+          <property name="type" value="bigInteger"/>
3460
+          <property name="unsigned" value="true"/>
3461
+        </element>
3462
+        <element action="add" uuid="ee6845fc-c117-4f0a-851e-106fea406367" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="a976501c-a52b-4bc6-ba45-f22d7f7ec967" type="field">
3463
+          <property name="name" value="created_at"/>
3464
+          <property name="type" value="timestamp"/>
3465
+        </element>
3466
+        <element action="add" uuid="4fa40b85-f8b0-4306-a10e-6da0d8bfeabb" parent-uuid="68922686-8dc2-403e-a281-a0efcdf39a82" sibling-uuid="ee6845fc-c117-4f0a-851e-106fea406367" type="field">
3467
+          <property name="name" value="updated_at"/>
3468
+          <property name="type" value="timestamp"/>
3469
+        </element>
3470
+        <element action="add" uuid="c9bbfc3a-fa00-44c2-b098-5904d0b7b3e6" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" type="field">
3471
+          <property name="auto-increment" value="true"/>
3472
+          <property name="name" value="id"/>
3473
+          <property name="primary" value="true"/>
3474
+          <property name="required" value="true"/>
3475
+          <property name="type" value="bigInteger"/>
3476
+          <property name="unique" value="true"/>
3477
+          <property name="unsigned" value="true"/>
3478
+        </element>
3479
+        <element action="add" uuid="b4de60f9-13ec-4965-b461-190be76f143a" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="c9bbfc3a-fa00-44c2-b098-5904d0b7b3e6" type="field">
3480
+          <property name="name" value="fattura_id"/>
3481
+          <property name="type" value="bigInteger"/>
3482
+          <property name="unsigned" value="true"/>
3483
+        </element>
3484
+        <element action="add" uuid="6257b217-17d6-41a3-9487-5c518c2bc02b" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="b4de60f9-13ec-4965-b461-190be76f143a" type="field">
3485
+          <property name="name" value="tipo"/>
3486
+          <property name="type" value="string"/>
3487
+        </element>
3488
+        <element action="add" uuid="a148a569-2e9c-4101-b80e-7b4256298e21" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="6257b217-17d6-41a3-9487-5c518c2bc02b" type="field">
3489
+          <property name="name" value="descrizione"/>
3490
+          <property name="type" value="string"/>
3491
+        </element>
3492
+        <element action="add" uuid="1ccc9c16-7d83-49d4-bfd1-c6b4a1161716" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="a148a569-2e9c-4101-b80e-7b4256298e21" type="field">
3493
+          <property name="decimal" value="2"/>
3494
+          <property name="name" value="quantita"/>
3495
+          <property name="size" value="10"/>
3496
+          <property name="type" value="decimal"/>
3497
+        </element>
3498
+        <element action="add" uuid="92db035d-b7a0-4b42-8172-f47554b2610d" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="1ccc9c16-7d83-49d4-bfd1-c6b4a1161716" type="field">
3499
+          <property name="name" value="unita_misura"/>
3500
+          <property name="type" value="string"/>
3501
+        </element>
3502
+        <element action="add" uuid="69557940-3860-4a2b-8a9b-2462aaab29db" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="92db035d-b7a0-4b42-8172-f47554b2610d" type="field">
3503
+          <property name="decimal" value="2"/>
3504
+          <property name="name" value="prezzo_unitario"/>
3505
+          <property name="size" value="10"/>
3506
+          <property name="type" value="decimal"/>
3507
+        </element>
3508
+        <element action="add" uuid="91a15e9b-c71e-4931-b219-009b9310a80d" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="69557940-3860-4a2b-8a9b-2462aaab29db" type="field">
3509
+          <property name="decimal" value="2"/>
3510
+          <property name="name" value="sconto"/>
3511
+          <property name="size" value="10"/>
3512
+          <property name="type" value="decimal"/>
3513
+        </element>
3514
+        <element action="add" uuid="7034c918-097a-4d1e-857c-db466fed11a3" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="91a15e9b-c71e-4931-b219-009b9310a80d" type="field">
3515
+          <property name="decimal" value="2"/>
3516
+          <property name="name" value="iva_percentuale"/>
3517
+          <property name="size" value="10"/>
3518
+          <property name="type" value="decimal"/>
3519
+        </element>
3520
+        <element action="add" uuid="cccc275f-4a6d-4654-a8c9-8b8c6e59b0ad" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="7034c918-097a-4d1e-857c-db466fed11a3" type="field">
3521
+          <property name="decimal" value="2"/>
3522
+          <property name="name" value="importo"/>
3523
+          <property name="size" value="10"/>
3524
+          <property name="type" value="decimal"/>
3525
+        </element>
3526
+        <element action="add" uuid="270b0887-2c09-454f-a235-e27485cb31fe" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="cccc275f-4a6d-4654-a8c9-8b8c6e59b0ad" type="field">
3527
+          <property name="name" value="minuti"/>
3528
+          <property name="type" value="integer"/>
3529
+        </element>
3530
+        <element action="add" uuid="30fe1f83-a681-492c-833a-a5112fb39d67" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="270b0887-2c09-454f-a235-e27485cb31fe" type="field">
3531
+          <property name="default" value="0"/>
3532
+          <property name="name" value="ordine"/>
3533
+          <property name="type" value="integer"/>
3534
+        </element>
3535
+        <element action="add" uuid="e5d8dcae-ee6d-4ca2-9e6f-6058a137b136" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="30fe1f83-a681-492c-833a-a5112fb39d67" type="field">
3536
+          <property name="name" value="nota"/>
3537
+          <property name="type" value="text"/>
3538
+        </element>
3539
+        <element action="add" uuid="3ea317af-cada-4c7d-a139-d13e70456379" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="e5d8dcae-ee6d-4ca2-9e6f-6058a137b136" type="field">
3540
+          <property name="name" value="annotazione_id"/>
3541
+          <property name="type" value="bigInteger"/>
3542
+          <property name="unsigned" value="true"/>
3543
+        </element>
3544
+        <element action="add" uuid="a3c26231-fc66-4f25-94c9-713e5f5eb354" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="3ea317af-cada-4c7d-a139-d13e70456379" type="field">
3545
+          <property name="name" value="segnalazione_id"/>
3546
+          <property name="type" value="bigInteger"/>
3547
+          <property name="unsigned" value="true"/>
3548
+        </element>
3549
+        <element action="add" uuid="e398b764-2f39-4b67-ad33-540d847a4d34" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="a3c26231-fc66-4f25-94c9-713e5f5eb354" type="field">
3550
+          <property name="name" value="contratto_servizio_id"/>
3551
+          <property name="type" value="bigInteger"/>
3552
+          <property name="unsigned" value="true"/>
3553
+        </element>
3554
+        <element action="add" uuid="8f752ece-34ad-4e94-b983-fb7a1d56312f" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="e398b764-2f39-4b67-ad33-540d847a4d34" type="field">
3555
+          <property name="name" value="contratto_servizio_riga_id"/>
3556
+          <property name="type" value="bigInteger"/>
3557
+          <property name="unsigned" value="true"/>
3558
+        </element>
3559
+        <element action="add" uuid="754d9613-e7f6-45c4-bf5e-14bcdc0d9157" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="8f752ece-34ad-4e94-b983-fb7a1d56312f" type="field">
3560
+          <property name="name" value="ordine_lavoro_id"/>
3561
+          <property name="type" value="bigInteger"/>
3562
+          <property name="unsigned" value="true"/>
3563
+        </element>
3564
+        <element action="add" uuid="75f07f2c-fe97-4189-a554-789e860950f7" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="754d9613-e7f6-45c4-bf5e-14bcdc0d9157" type="field">
3565
+          <property name="name" value="prodotto_id"/>
3566
+          <property name="type" value="bigInteger"/>
3567
+          <property name="unsigned" value="true"/>
3568
+        </element>
3569
+        <element action="add" uuid="5c8db971-3b05-45f3-aa33-ef251e579c60" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="75f07f2c-fe97-4189-a554-789e860950f7" type="field">
3570
+          <property name="name" value="periodo_da"/>
3571
+          <property name="type" value="date"/>
3572
+        </element>
3573
+        <element action="add" uuid="2b994a3f-71f2-4647-aad6-5743aea49845" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="5c8db971-3b05-45f3-aa33-ef251e579c60" type="field">
3574
+          <property name="name" value="periodo_a"/>
3575
+          <property name="type" value="date"/>
3576
+        </element>
3577
+        <element action="add" uuid="d2d85cb5-2cdc-454f-ade7-aa8e38744b37" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="2b994a3f-71f2-4647-aad6-5743aea49845" type="field">
3578
+          <property name="name" value="created_at"/>
3579
+          <property name="type" value="timestamp"/>
3580
+        </element>
3581
+        <element action="add" uuid="d591d678-dfea-4105-9e84-6a6786f31125" parent-uuid="6cc482e6-604b-4e73-9870-36f4f2711362" sibling-uuid="d2d85cb5-2cdc-454f-ade7-aa8e38744b37" type="field">
3582
+          <property name="name" value="updated_at"/>
3583
+          <property name="type" value="timestamp"/>
3584
+        </element>
3585
+        <element action="add" uuid="72d22456-ccc5-4806-9743-b91635948ea7" parent-uuid="e11f8bb2-17fd-4770-a490-a416d2d49f6c" type="field">
3586
+          <property name="auto-increment" value="true"/>
3587
+          <property name="name" value="id"/>
3588
+          <property name="primary" value="true"/>
3589
+          <property name="required" value="true"/>
3590
+          <property name="type" value="bigInteger"/>
3591
+          <property name="unique" value="true"/>
3592
+          <property name="unsigned" value="true"/>
3593
+        </element>
3594
+        <element action="add" uuid="e1a2b3f2-59be-4c31-ba82-d1e413cf5cc9" parent-uuid="e11f8bb2-17fd-4770-a490-a416d2d49f6c" sibling-uuid="72d22456-ccc5-4806-9743-b91635948ea7" type="field">
3595
+          <property name="name" value="segnalazione_id"/>
3596
+          <property name="type" value="bigInteger"/>
3597
+          <property name="unsigned" value="true"/>
3598
+        </element>
3599
+        <element action="add" uuid="25084066-cd50-4459-8b21-dacdb3a988aa" parent-uuid="e11f8bb2-17fd-4770-a490-a416d2d49f6c" sibling-uuid="e1a2b3f2-59be-4c31-ba82-d1e413cf5cc9" type="field">
3600
+          <property name="name" value="starts_at"/>
3601
+          <property name="type" value="dateTime"/>
3602
+        </element>
3603
+        <element action="add" uuid="310f5a51-80c7-444c-a53d-2b8c9ac5401d" parent-uuid="e11f8bb2-17fd-4770-a490-a416d2d49f6c" sibling-uuid="25084066-cd50-4459-8b21-dacdb3a988aa" type="field">
3604
+          <property name="name" value="ends_at"/>
3605
+          <property name="type" value="dateTime"/>
3606
+        </element>
3607
+        <element action="add" uuid="db734a85-a27a-4254-ae8c-63d472df57a7" parent-uuid="e11f8bb2-17fd-4770-a490-a416d2d49f6c" sibling-uuid="310f5a51-80c7-444c-a53d-2b8c9ac5401d" type="field">
3608
+          <property name="name" value="created_at"/>
3609
+          <property name="type" value="timestamp"/>
3610
+        </element>
3611
+        <element action="add" uuid="f3fe4780-00f5-4a30-86a3-ff7fd643d629" parent-uuid="e11f8bb2-17fd-4770-a490-a416d2d49f6c" sibling-uuid="db734a85-a27a-4254-ae8c-63d472df57a7" type="field">
3612
+          <property name="name" value="updated_at"/>
3613
+          <property name="type" value="timestamp"/>
3614
+        </element>
3615
+        <element action="add" uuid="8ca1b7f6-f59c-4a76-8b55-9eaa5729c588" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3616
+          <property name="from" value="\App\Models\Fattura"/>
3617
+          <property name="inverse-alias" value="user"/>
3618
+          <property name="many-inverse" value="false"/>
3619
+          <property name="many-owner" value="true"/>
3620
+          <property name="onDelete" value="set null"/>
3621
+          <property name="owner-alias" value="fatture"/>
3622
+          <property name="to" value="\App\Models\User"/>
3623
+        </element>
3624
+        <element action="add" uuid="a4d4341e-64c7-4fd1-bd5e-b5f457f94e15" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3625
+          <property name="from" value="\App\Models\Fattura"/>
3626
+          <property name="inverse-alias" value="createdBy"/>
3627
+          <property name="many-inverse" value="false"/>
3628
+          <property name="many-owner" value="true"/>
3629
+          <property name="onDelete" value="set null"/>
3630
+          <property name="owner-alias" value="fattureCreate"/>
3631
+          <property name="to" value="\App\Models\User"/>
3632
+        </element>
3633
+        <element action="add" uuid="d8d39c1e-5b4e-444d-9fde-e86890e12662" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3634
+          <property name="from" value="\App\Models\Fattura"/>
3635
+          <property name="inverse-alias" value="comune"/>
3636
+          <property name="many-inverse" value="false"/>
3637
+          <property name="many-owner" value="true"/>
3638
+          <property name="owner-alias" value="fatture"/>
3639
+          <property name="to" value="\App\Models\Comune"/>
3640
+        </element>
3641
+        <element action="add" uuid="d69021ca-1036-4659-b1bb-431dd895db02" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3642
+          <property name="from" value="\App\Models\FatturaRiga"/>
3643
+          <property name="inverse-alias" value="fattura"/>
3644
+          <property name="many-inverse" value="false"/>
3645
+          <property name="many-owner" value="true"/>
3646
+          <property name="onDelete" value="cascade"/>
3647
+          <property name="owner-alias" value="fatturaRighe"/>
3648
+          <property name="to" value="\App\Models\Fattura"/>
3649
+        </element>
3650
+        <element action="add" uuid="423b7b2d-3041-412c-8078-3720e48429d8" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3651
+          <property name="from" value="\App\Models\FatturaRiga"/>
3652
+          <property name="inverse-alias" value="annotazione"/>
3653
+          <property name="many-inverse" value="false"/>
3654
+          <property name="many-owner" value="true"/>
3655
+          <property name="onDelete" value="set null"/>
3656
+          <property name="owner-alias" value="fatturaRighe"/>
3657
+          <property name="to" value="\App\Models\Annotazione"/>
3658
+        </element>
3659
+        <element action="add" uuid="9a930ef5-48ae-4eba-97eb-840f54c682f1" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3660
+          <property name="from" value="\App\Models\FatturaRiga"/>
3661
+          <property name="inverse-alias" value="segnalazione"/>
3662
+          <property name="many-inverse" value="false"/>
3663
+          <property name="many-owner" value="true"/>
3664
+          <property name="onDelete" value="set null"/>
3665
+          <property name="owner-alias" value="fatturaRighe"/>
3666
+          <property name="to" value="\App\Models\Segnalazione"/>
3667
+        </element>
3668
+        <element action="add" uuid="d34a3b7a-91da-44d1-84d9-fa049248c00d" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3669
+          <property name="from" value="\App\Models\FatturaRiga"/>
3670
+          <property name="inverse-alias" value="contrattoServizio"/>
3671
+          <property name="many-inverse" value="false"/>
3672
+          <property name="many-owner" value="true"/>
3673
+          <property name="onDelete" value="set null"/>
3674
+          <property name="owner-alias" value="fatturaRighe"/>
3675
+          <property name="to" value="\App\Models\ContrattoServizio"/>
3676
+        </element>
3677
+        <element action="add" uuid="f5fadc49-42d4-4c62-a53e-a293806c8783" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3678
+          <property name="from" value="\App\Models\FatturaRiga"/>
3679
+          <property name="inverse-alias" value="contrattoServizioRiga"/>
3680
+          <property name="many-inverse" value="false"/>
3681
+          <property name="many-owner" value="true"/>
3682
+          <property name="onDelete" value="set null"/>
3683
+          <property name="owner-alias" value="fatturaRighe"/>
3684
+          <property name="to" value="\App\Models\ContrattoServizioRiga"/>
3685
+        </element>
3686
+        <element action="add" uuid="6f844493-1bb4-40a9-bd78-97e979015333" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3687
+          <property name="from" value="\App\Models\FatturaRiga"/>
3688
+          <property name="inverse-alias" value="ordineLavoro"/>
3689
+          <property name="many-inverse" value="false"/>
3690
+          <property name="many-owner" value="true"/>
3691
+          <property name="onDelete" value="set null"/>
3692
+          <property name="owner-alias" value="fatturaRighe"/>
3693
+          <property name="to" value="\App\Models\OrdineLavoro"/>
3694
+        </element>
3695
+        <element action="add" uuid="72ab57bf-7877-458a-9891-90399ec44941" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3696
+          <property name="from" value="\App\Models\FatturaRiga"/>
3697
+          <property name="inverse-alias" value="prodotto"/>
3698
+          <property name="many-inverse" value="false"/>
3699
+          <property name="many-owner" value="true"/>
3700
+          <property name="onDelete" value="set null"/>
3701
+          <property name="owner-alias" value="fatturaRighe"/>
3702
+          <property name="to" value="\App\Models\Prodotto"/>
3703
+        </element>
3704
+        <element action="add" uuid="66d48b8b-7984-430a-92e7-fbe21fce135f" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3705
+          <property name="from" value="\App\Models\SegnalazioneSlot"/>
3706
+          <property name="inverse-alias" value="segnalazione"/>
3707
+          <property name="many-inverse" value="false"/>
3708
+          <property name="many-owner" value="true"/>
3709
+          <property name="onDelete" value="cascade"/>
3710
+          <property name="owner-alias" value="segnalazioneSlots"/>
3711
+          <property name="to" value="\App\Models\Segnalazione"/>
3712
+        </element>
3713
+        <element action="add" uuid="18643b0d-6874-4337-bf61-c6b57630a5da" parent-uuid="8ca1b7f6-f59c-4a76-8b55-9eaa5729c588" type="association-field">
3714
+          <property name="from" value="user_id"/>
3715
+          <property name="to" value="id"/>
3716
+        </element>
3717
+        <element action="add" uuid="2660ac06-894c-4de9-a053-c47f76f5894f" parent-uuid="a4d4341e-64c7-4fd1-bd5e-b5f457f94e15" type="association-field">
3718
+          <property name="from" value="created_by_id"/>
3719
+          <property name="to" value="id"/>
3720
+        </element>
3721
+        <element action="add" uuid="d954bd57-b5dc-440b-b623-27eda0482e2a" parent-uuid="d8d39c1e-5b4e-444d-9fde-e86890e12662" type="association-field">
3722
+          <property name="from" value="comune_id"/>
3723
+          <property name="to" value="id"/>
3724
+        </element>
3725
+        <element action="add" uuid="2099a305-fbd4-4076-a85c-d0d3c96d0a5e" parent-uuid="d69021ca-1036-4659-b1bb-431dd895db02" type="association-field">
3726
+          <property name="from" value="fattura_id"/>
3727
+          <property name="to" value="id"/>
3728
+        </element>
3729
+        <element action="add" uuid="2f341028-7afe-4bc0-b028-34052ac55978" parent-uuid="423b7b2d-3041-412c-8078-3720e48429d8" type="association-field">
3730
+          <property name="from" value="annotazione_id"/>
3731
+          <property name="to" value="id"/>
3732
+        </element>
3733
+        <element action="add" uuid="fb426b82-ed78-4ad1-89ba-a48a66e19ec2" parent-uuid="9a930ef5-48ae-4eba-97eb-840f54c682f1" type="association-field">
3734
+          <property name="from" value="segnalazione_id"/>
3735
+          <property name="to" value="id"/>
3736
+        </element>
3737
+        <element action="add" uuid="9809bfc1-664d-487e-a8b7-54419a018ab7" parent-uuid="d34a3b7a-91da-44d1-84d9-fa049248c00d" type="association-field">
3738
+          <property name="from" value="contratto_servizio_id"/>
3739
+          <property name="to" value="id"/>
3740
+        </element>
3741
+        <element action="add" uuid="4d151ce4-d485-4b6e-b558-e6779a406b22" parent-uuid="f5fadc49-42d4-4c62-a53e-a293806c8783" type="association-field">
3742
+          <property name="from" value="contratto_servizio_riga_id"/>
3743
+          <property name="to" value="id"/>
3744
+        </element>
3745
+        <element action="add" uuid="80800d7e-bdd2-4c2a-b4d7-b6d92a5ca8bf" parent-uuid="6f844493-1bb4-40a9-bd78-97e979015333" type="association-field">
3746
+          <property name="from" value="ordine_lavoro_id"/>
3747
+          <property name="to" value="id"/>
3748
+        </element>
3749
+        <element action="add" uuid="1161edb2-e88f-4df3-a370-160def07904b" parent-uuid="72ab57bf-7877-458a-9891-90399ec44941" type="association-field">
3750
+          <property name="from" value="prodotto_id"/>
3751
+          <property name="to" value="id"/>
3752
+        </element>
3753
+        <element action="add" uuid="c75ea229-0d18-4d5e-8c5a-cb7cc0c8dff6" parent-uuid="66d48b8b-7984-430a-92e7-fbe21fce135f" type="association-field">
3754
+          <property name="from" value="segnalazione_id"/>
3755
+          <property name="to" value="id"/>
3756
+        </element>
3757
+      </revision>
3758
+      <revision uuid="44d9ba7e-a043-4486-b25f-39fad8acf135" date="2026-08-26 15:39:03.736234" exportable="true">
3759
+        <element action="add" uuid="04287ee6-13fe-4b73-970a-aedefd569698" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="entity">
3760
+          <property name="fillableAll" value="as-fillable-all"/>
3761
+          <property name="local-name" value="OrdineLavoroRiga"/>
3762
+          <property name="name" value="\App\Models\OrdineLavoroRiga"/>
3763
+          <property name="namespace" value="\App\Models"/>
3764
+          <property name="table" value="ordine_lavoro_riga"/>
3765
+        </element>
3766
+        <element action="add" uuid="7ed8a1d0-2b4d-418b-b371-60d8ab5c6361" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" type="field">
3767
+          <property name="auto-increment" value="true"/>
3768
+          <property name="name" value="id"/>
3769
+          <property name="primary" value="true"/>
3770
+          <property name="required" value="true"/>
3771
+          <property name="type" value="bigInteger"/>
3772
+          <property name="unique" value="true"/>
3773
+          <property name="unsigned" value="true"/>
3774
+        </element>
3775
+        <element action="add" uuid="bd447a10-6098-49fa-b342-38bfc6c0271d" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" sibling-uuid="7ed8a1d0-2b4d-418b-b371-60d8ab5c6361" type="field">
3776
+          <property name="name" value="ordine_lavoro_id"/>
3777
+          <property name="type" value="bigInteger"/>
3778
+          <property name="unsigned" value="true"/>
3779
+        </element>
3780
+        <element action="add" uuid="05297563-07a0-4cbb-876d-cf811973af8b" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" sibling-uuid="bd447a10-6098-49fa-b342-38bfc6c0271d" type="field">
3781
+          <property name="name" value="prodotto_id"/>
3782
+          <property name="type" value="bigInteger"/>
3783
+          <property name="unsigned" value="true"/>
3784
+        </element>
3785
+        <element action="add" uuid="dddc3ab2-3d27-4954-abb0-7add4bd39045" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" sibling-uuid="05297563-07a0-4cbb-876d-cf811973af8b" type="field">
3786
+          <property name="name" value="descrizione"/>
3787
+          <property name="type" value="string"/>
3788
+        </element>
3789
+        <element action="add" uuid="6df25b5a-0b5b-4b75-9f1f-de5b1b33fc81" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" sibling-uuid="dddc3ab2-3d27-4954-abb0-7add4bd39045" type="field">
3790
+          <property name="decimal" value="2"/>
3791
+          <property name="name" value="quantita"/>
3792
+          <property name="size" value="10"/>
3793
+          <property name="type" value="decimal"/>
3794
+        </element>
3795
+        <element action="add" uuid="4d89aa02-bd75-470c-95b2-6ed1086fecbb" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" sibling-uuid="6df25b5a-0b5b-4b75-9f1f-de5b1b33fc81" type="field">
3796
+          <property name="name" value="unita_misura"/>
3797
+          <property name="type" value="string"/>
3798
+        </element>
3799
+        <element action="add" uuid="9d4792f1-0b30-48f8-ac62-4be597728e15" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" sibling-uuid="4d89aa02-bd75-470c-95b2-6ed1086fecbb" type="field">
3800
+          <property name="decimal" value="2"/>
3801
+          <property name="name" value="prezzo_unitario"/>
3802
+          <property name="size" value="10"/>
3803
+          <property name="type" value="decimal"/>
3804
+        </element>
3805
+        <element action="add" uuid="b1bec05e-c03e-4cd5-abb2-56a077292f3a" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" sibling-uuid="9d4792f1-0b30-48f8-ac62-4be597728e15" type="field">
3806
+          <property name="decimal" value="2"/>
3807
+          <property name="name" value="sconto"/>
3808
+          <property name="size" value="10"/>
3809
+          <property name="type" value="decimal"/>
3810
+        </element>
3811
+        <element action="add" uuid="1411b9dc-1fce-4a6d-a961-0e631ebf42da" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" sibling-uuid="b1bec05e-c03e-4cd5-abb2-56a077292f3a" type="field">
3812
+          <property name="default" value="0"/>
3813
+          <property name="name" value="ordine"/>
3814
+          <property name="type" value="integer"/>
3815
+        </element>
3816
+        <element action="add" uuid="2c203621-a3dc-4adf-97e9-387293ec5d2f" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" sibling-uuid="1411b9dc-1fce-4a6d-a961-0e631ebf42da" type="field">
3817
+          <property name="name" value="note_private"/>
3818
+          <property name="type" value="text"/>
3819
+        </element>
3820
+        <element action="add" uuid="6cc655ed-c6b9-45a6-b078-1dbae13e93c6" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" sibling-uuid="2c203621-a3dc-4adf-97e9-387293ec5d2f" type="field">
3821
+          <property name="decimal" value="2"/>
3822
+          <property name="name" value="tempo_stimato"/>
3823
+          <property name="size" value="10"/>
3824
+          <property name="type" value="decimal"/>
3825
+        </element>
3826
+        <element action="add" uuid="15b7da2d-8558-47b6-bbf2-88556147d03d" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" sibling-uuid="6cc655ed-c6b9-45a6-b078-1dbae13e93c6" type="field">
3827
+          <property name="name" value="created_at"/>
3828
+          <property name="type" value="timestamp"/>
3829
+        </element>
3830
+        <element action="add" uuid="fea3f591-b802-401b-b494-0648197ec020" parent-uuid="04287ee6-13fe-4b73-970a-aedefd569698" sibling-uuid="15b7da2d-8558-47b6-bbf2-88556147d03d" type="field">
3831
+          <property name="name" value="updated_at"/>
3832
+          <property name="type" value="timestamp"/>
3833
+        </element>
3834
+        <element action="update" uuid="fdd9ee5a-9ae1-4736-8b9a-7e7fb84061b3" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3835
+          <property name="owner-alias" value="righe"/>
3836
+        </element>
3837
+        <element action="update" uuid="35b0ed5e-e7f9-4a4f-ad24-2547a9392e87" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3838
+          <property name="owner-alias" value="todos"/>
3839
+        </element>
3840
+        <element action="update" uuid="3e2b345d-f7f3-47e9-b6da-39b2c597e870" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3841
+          <property name="owner-alias" value="todos"/>
3842
+        </element>
3843
+        <element action="update" uuid="79ac3c47-aa91-4b9f-a46d-b5aa7c818f6f" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3844
+          <property name="owner-alias" value="righe"/>
3845
+        </element>
3846
+        <element action="update" uuid="b2867c66-0dcf-4a4c-99ce-cbdda8b11d6f" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3847
+          <property name="owner-alias" value="righe"/>
3848
+        </element>
3849
+        <element action="update" uuid="6debb5a0-4bc9-4e03-bafa-569e49949ad6" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3850
+          <property name="owner-alias" value="todos"/>
3851
+        </element>
3852
+        <element action="update" uuid="ae43bea5-a4e1-4e9c-881c-ab2a91c7b743" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3853
+          <property name="owner-alias" value="todos"/>
3854
+        </element>
3855
+        <element action="update" uuid="d69021ca-1036-4659-b1bb-431dd895db02" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3856
+          <property name="owner-alias" value="righe"/>
3857
+        </element>
3858
+        <element action="update" uuid="66d48b8b-7984-430a-92e7-fbe21fce135f" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3859
+          <property name="owner-alias" value="slots"/>
3860
+        </element>
3861
+        <element action="add" uuid="1e8af9f9-8d2e-4c16-8530-28e43a622c6d" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3862
+          <property name="from" value="\App\Models\OrdineLavoroRiga"/>
3863
+          <property name="inverse-alias" value="ordineLavoro"/>
3864
+          <property name="many-inverse" value="false"/>
3865
+          <property name="many-owner" value="true"/>
3866
+          <property name="onDelete" value="cascade"/>
3867
+          <property name="owner-alias" value="righe"/>
3868
+          <property name="to" value="\App\Models\OrdineLavoro"/>
3869
+        </element>
3870
+        <element action="add" uuid="350c8c4f-877e-4925-8385-b750d5f625da" parent-uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" type="association">
3871
+          <property name="from" value="\App\Models\OrdineLavoroRiga"/>
3872
+          <property name="inverse-alias" value="prodotto"/>
3873
+          <property name="many-inverse" value="false"/>
3874
+          <property name="many-owner" value="true"/>
3875
+          <property name="onDelete" value="set null"/>
3876
+          <property name="owner-alias" value="ordineLavoroRigas"/>
3877
+          <property name="to" value="\App\Models\Prodotto"/>
3878
+        </element>
3879
+        <element action="add" uuid="bf0d5c13-4e24-4438-8e1f-dcc24809cbc6" parent-uuid="1e8af9f9-8d2e-4c16-8530-28e43a622c6d" type="association-field">
3880
+          <property name="from" value="ordine_lavoro_id"/>
3881
+          <property name="to" value="id"/>
3882
+        </element>
3883
+        <element action="add" uuid="6b67c71b-1af2-4f55-9a45-b7bdfe642ad9" parent-uuid="350c8c4f-877e-4925-8385-b750d5f625da" type="association-field">
3884
+          <property name="from" value="prodotto_id"/>
3885
+          <property name="to" value="id"/>
3886
+        </element>
3887
+      </revision>
3267 3888
     </migrations>
3268 3889
   </module>
3269 3890
   <visual-data>
3270 3891
     <association uuid="05c3f975-b61c-490e-aa04-080772b499ad" color="#969696"/>
3892
+    <association uuid="1e8af9f9-8d2e-4c16-8530-28e43a622c6d" color="#969696"/>
3271 3893
     <association uuid="299d534d-b5ef-411c-b66b-f4adaaa3b4f1" color="#969696"/>
3272 3894
     <association uuid="2fa65d03-8ddb-413f-8064-798860928786" color="#969696"/>
3273 3895
     <association uuid="345b5351-9f8a-4d02-9192-1bb27645761d" color="#969696"/>
3896
+    <association uuid="350c8c4f-877e-4925-8385-b750d5f625da" color="#969696"/>
3274 3897
     <association uuid="35b0ed5e-e7f9-4a4f-ad24-2547a9392e87" color="#969696"/>
3275 3898
     <association uuid="3842d2b3-d137-47bc-9a10-14b7a860c153" color="#969696"/>
3276 3899
     <association uuid="3d14fd5b-7f78-49e4-831f-a98aa484a94f" color="#969696"/>
@@ -3282,6 +3905,7 @@
3282 3905
     <association uuid="5c8d1dc1-15ed-4379-addb-6b0addcd047e" color="#969696"/>
3283 3906
     <association uuid="5f4ca39e-40ba-4157-bf4c-603b7469b044" color="#969696"/>
3284 3907
     <association uuid="64df4dd6-cb23-408a-a4f1-e1a723103aa0" color="#969696"/>
3908
+    <association uuid="66d48b8b-7984-430a-92e7-fbe21fce135f" color="#969696"/>
3285 3909
     <association uuid="6debb5a0-4bc9-4e03-bafa-569e49949ad6" color="#969696"/>
3286 3910
     <association uuid="6f844493-1bb4-40a9-bd78-97e979015333" color="#969696"/>
3287 3911
     <association uuid="6faa9818-b5b6-4eab-96b6-6eaf56d1de79" color="#969696"/>
@@ -3313,6 +3937,7 @@
3313 3937
     <association uuid="f5fadc49-42d4-4c62-a53e-a293806c8783" color="#969696"/>
3314 3938
     <association uuid="faaed052-f628-4ae7-8f52-b3614fad893e" color="#969696"/>
3315 3939
     <association uuid="fdd9ee5a-9ae1-4736-8b9a-7e7fb84061b3" color="#969696"/>
3940
+    <entity uuid="04287ee6-13fe-4b73-970a-aedefd569698" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="14" position-y="769" size-x="0" size-x2="139" size-y="0" size-y2="87"/>
3316 3941
     <entity uuid="05f7d8b0-095f-456c-96ec-3980b0435855" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="377" position-y="460" size-x="0" size-x2="137" size-y="0" size-y2="73"/>
3317 3942
     <entity uuid="23c0457c-f9c0-4210-9b12-1c8fa92cd788" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="1311" position-y="581" size-x="0" size-x2="119" size-y="0" size-y2="101"/>
3318 3943
     <entity uuid="273deed0-9f6e-4aca-b64b-22a657db2a21" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="877" position-y="547" size-x="0" size-x2="134" size-y="0" size-y2="60"/>
@@ -3339,6 +3964,7 @@
3339 3964
     <entity uuid="c9dad18d-0272-4878-8404-fd0db896b447" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="1132" position-y="623" size-x="0" size-x2="164" size-y="0" size-y2="269"/>
3340 3965
     <entity uuid="cdb29815-3972-4ad8-8d0a-f3265469539f" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="1030" position-y="25" size-x="0" size-x2="159" size-y="0" size-y2="241"/>
3341 3966
     <entity uuid="dd24c9ce-a45f-4d90-b374-ede2efe2e25b" bg-color="#FFFFFF" hdr-color="#FFE762" position-x="1300" position-y="433" size-x="0" size-x2="119" size-y="0" size-y2="129"/>
3967
+    <entity uuid="e11f8bb2-17fd-4770-a490-a416d2d49f6c" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="693" position-y="904" size-x="0" size-x2="137" size-y="0" size-y2="101"/>
3342 3968
     <entity uuid="ec22a629-0e0a-4949-9ca3-43c6afee8ed5" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="481" position-y="25" size-x="0" size-x2="119" size-y="0" size-y2="185"/>
3343 3969
     <entity uuid="f1331b94-8ccf-4328-bc32-bffdc732572e" bg-color="#FFFFFF" hdr-color="#FFE762" position-x="1301" position-y="-11" size-x="0" size-x2="119" size-y="0" size-y2="101"/>
3344 3970
     <entity uuid="f98de9f5-eb6f-467a-8f95-31b70d915946" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="675" position-y="633" size-x="0" size-x2="119" size-y="0" size-y2="199"/>

+ 1
- 1
app/DataTables/SegnalazioneClienteDataTable.php Прегледај датотеку

@@ -86,7 +86,7 @@ class SegnalazioneClienteDataTable extends DataTable
86 86
                 }
87 87
             })
88 88
             ->rawColumns(['stato_display', 'priorita_display'])
89
-            ->setRowId('id')
89
+            ->setRowId('token')
90 90
             ->setRowClass('cursor-pointer');
91 91
     }
92 92
 

+ 12
- 3
app/Http/Controllers/AnnotazioniController.php Прегледај датотеку

@@ -79,7 +79,10 @@ class AnnotazioniController extends Controller implements HasMiddleware
79 79
             $allegato->save();
80 80
         }
81 81
 
82
-        return redirect()->route('segnalazione.show', $request->segnalazione_id)->with('success', 'Annotazione creata con successo');
82
+        $segnalazione = Segnalazione::find($request->segnalazione_id);
83
+
84
+        return redirect()->to($segnalazione?->urlCliente() ?? url()->previous())
85
+            ->with('success', 'Annotazione creata con successo');
83 86
     }
84 87
 
85 88
     public function show(Request $request)
@@ -100,13 +103,19 @@ class AnnotazioniController extends Controller implements HasMiddleware
100 103
         $annotazione->annotazione = $request->annotazione;
101 104
         $annotazione->segnalazione_id = $request->segnalazione_id;
102 105
         $annotazione->save();
103
-        return redirect()->route('segnalazione.show', $request->segnalazione_id)->with('success', 'Annotazione aggiornata con successo');
106
+        $segnalazione = Segnalazione::find($request->segnalazione_id);
107
+
108
+        return redirect()->to($segnalazione?->urlCliente() ?? url()->previous())
109
+            ->with('success', 'Annotazione aggiornata con successo');
104 110
     }
105 111
 
106 112
     public function destroy(Request $request)
107 113
     {
108 114
         $annotazione = Annotazione::find($request->id);
109 115
         $annotazione->delete();
110
-        return redirect()->route('segnalazione.show', $request->segnalazione_id)->with('success', 'Annotazione eliminata con successo');
116
+        $segnalazione = Segnalazione::find($request->segnalazione_id);
117
+
118
+        return redirect()->to($segnalazione?->urlCliente() ?? url()->previous())
119
+            ->with('success', 'Annotazione eliminata con successo');
111 120
     }
112 121
 }

+ 5
- 0
app/Http/Controllers/ConfigController.php Прегледај датотеку

@@ -56,6 +56,10 @@ class ConfigController extends Controller implements HasMiddleware
56 56
         ->where('gruppo', 'stato')
57 57
         ->orderBy('order')
58 58
         ->get(),
59
+      'tipiSegnalazione' => ConfiguraSegnalazione::query()
60
+        ->where('gruppo', 'Tipo')
61
+        ->orderBy('order')
62
+        ->get(),
59 63
       'canEdit' => Auth::user()->can('edit-config'),
60 64
     ]);
61 65
   }
@@ -103,6 +107,7 @@ class ConfigController extends Controller implements HasMiddleware
103 107
       'azienda' => $value === null || $value === '' ? null : (string) (int) $value,
104 108
       'stato' => $value === null || $value === '' ? null : (string) $value,
105 109
       'stati' => json_encode($this->normalizeStati($value), JSON_UNESCAPED_UNICODE),
110
+      'tipo' => $value === null || $value === '' ? null : (string) $value,
106 111
       default => $value === null ? null : (string) $value,
107 112
     };
108 113
   }

+ 127
- 59
app/Http/Controllers/HomePageController.php Прегледај датотеку

@@ -9,6 +9,7 @@ use App\Models\Azienda;
9 9
 use App\Models\Config;
10 10
 use App\Models\ConfiguraSegnalazione;
11 11
 use App\Models\Segnalazione;
12
+use App\Models\User;
12 13
 use Carbon\Carbon;
13 14
 use Illuminate\Database\Eloquent\Builder;
14 15
 use Illuminate\Support\Facades\Auth;
@@ -35,10 +36,113 @@ class HomePageController extends Controller
35 36
       return redirect()->route('dashboard');
36 37
     }
37 38
 
38
-    return view('dashboard.dashboards-manutenzioni', $this->getDashboardData());
39
+    return view('dashboard.dashboards-manutenzioni', $this->getDashboardHomeData());
39 40
   }
40 41
 
41
-  private function getDashboardData()
42
+  public function admin_statistiche()
43
+  {
44
+    if (! Auth::user()?->can('view-admin_dashboard')) {
45
+      return redirect()->route('dashboard');
46
+    }
47
+
48
+    return view('dashboard.dashboards-manutenzioni-statistiche', $this->getStatisticheData());
49
+  }
50
+
51
+  /**
52
+   * KPI e segnalazioni con timer attivo per la dashboard operativa.
53
+   *
54
+   * @return array<string, mixed>
55
+   */
56
+  private function getDashboardHomeData(): array
57
+  {
58
+    $totaleSegnalazioni = $this->dashboardSegnalazioneBase()->count();
59
+    $segnalazioniAperte = $this->dashboardSegnalazioneBase()->where('stato', Config::getStatiSegnalazione('aperte'))->count();
60
+    $segnalazioniInLavorazione = $this->dashboardSegnalazioneBase()->whereIn('stato', Config::getStatiSegnalazione('in_lavorazione'))->count();
61
+    $segnalazioniChiuse = $this->dashboardSegnalazioneBase()->whereIn('stato', Config::getStatiSegnalazione('chiuse'))->count();
62
+
63
+    $segnalazioniUrgentiAperte = $this->dashboardSegnalazioneBase()->whereNotIn('stato', Config::getStatiSegnalazione('chiuse'))
64
+      ->whereIn('priorita', ['urgente', 'emergenza_immediata'])
65
+      ->count();
66
+
67
+    $segnalazioniSenzaAssegnazione = $this->dashboardSegnalazioneBase()->whereNotIn('stato', Config::getStatiSegnalazione('chiuse'))->whereNull('assegnata_a_id')
68
+      ->count();
69
+
70
+    $dataLimite = Carbon::now()->subDays(7);
71
+    $segnalazioniAperteVecchie = $this->dashboardSegnalazioneBase()->whereNotIn('stato', Config::getStatiSegnalazione('chiuse'))
72
+      ->where('created_at', '<', $dataLimite)
73
+      ->count();
74
+
75
+    $segnalazioniInLavoro = $this->dashboardSegnalazioneBase()
76
+      ->with(['azienda', 'assegnataA'])
77
+      ->whereNotNull('meta')
78
+      ->whereRaw("JSON_TYPE(JSON_EXTRACT(meta, '$.tempo_timer')) = 'OBJECT'")
79
+      ->whereRaw("JSON_LENGTH(JSON_EXTRACT(meta, '$.tempo_timer')) > 0")
80
+      ->orderByDesc('updated_at')
81
+      ->get()
82
+      ->filter(fn (Segnalazione $s) => $s->hasActiveTempoTimer())
83
+      ->values();
84
+
85
+    $timerUserIds = $segnalazioniInLavoro
86
+      ->flatMap(fn (Segnalazione $s) => collect($s->getActiveTempoTimers())->pluck('user_id'))
87
+      ->unique()
88
+      ->filter()
89
+      ->values()
90
+      ->all();
91
+
92
+    $timerUsers = $timerUserIds === []
93
+      ? collect()
94
+      : User::query()->whereIn('id', $timerUserIds)->get()->keyBy('id');
95
+
96
+    $slotPrenotazioni = \App\Models\SegnalazioneSlot::query()
97
+      ->with(['segnalazione.azienda'])
98
+      ->whereHas('segnalazione', function (Builder $q) {
99
+        $this->restrictDashboardSegnalazioniQuery($q);
100
+        $q->where('stato', 'pianificata');
101
+      })
102
+      ->where('ends_at', '>=', Carbon::now()->subDays(7)->startOfDay())
103
+      ->orderBy('starts_at')
104
+      ->get()
105
+      ->map(function (\App\Models\SegnalazioneSlot $slot) {
106
+        $s = $slot->segnalazione;
107
+        $title = $s ? $s->getLabel() : ('Slot #'.$slot->id);
108
+        if ($s?->azienda) {
109
+          $title .= ' · '.$s->azienda->full_name;
110
+        }
111
+
112
+        return [
113
+          'id' => $slot->id,
114
+          'title' => $title,
115
+          'start' => $slot->starts_at?->toIso8601String(),
116
+          'end' => $slot->ends_at?->toIso8601String(),
117
+          'url' => $s ? route('admin.segnalazione.show', ['id' => $s->id]) : null,
118
+          'backgroundColor' => '#71dd37',
119
+          'borderColor' => '#71dd37',
120
+          'textColor' => '#ffffff',
121
+        ];
122
+      })
123
+      ->values()
124
+      ->all();
125
+
126
+    return [
127
+      'totaleSegnalazioni' => $totaleSegnalazioni,
128
+      'segnalazioniAperte' => $segnalazioniAperte,
129
+      'segnalazioniInLavorazione' => $segnalazioniInLavorazione,
130
+      'segnalazioniChiuse' => $segnalazioniChiuse,
131
+      'segnalazioniUrgentiAperte' => $segnalazioniUrgentiAperte,
132
+      'segnalazioniSenzaAssegnazione' => $segnalazioniSenzaAssegnazione,
133
+      'segnalazioniAperteVecchie' => $segnalazioniAperteVecchie,
134
+      'segnalazioniInLavoro' => $segnalazioniInLavoro,
135
+      'timerUsers' => $timerUsers,
136
+      'slotPrenotazioni' => $slotPrenotazioni,
137
+    ];
138
+  }
139
+
140
+  /**
141
+   * Dati grafici e KPI performance per la pagina Statistiche.
142
+   *
143
+   * @return array<string, mixed>
144
+   */
145
+  private function getStatisticheData(): array
42 146
   {
43 147
     $labelsManutentori = [];
44 148
     $datiManutentori = [];
@@ -47,13 +151,6 @@ class HomePageController extends Controller
47 151
     $labelsCostiTipo = [];
48 152
     $datiCostiTipo = [];
49 153
 
50
-    // KPI - Statistiche generali
51
-    $totaleSegnalazioni = $this->dashboardSegnalazioneBase()->count();
52
-    $segnalazioniAperte = $this->dashboardSegnalazioneBase()->where('stato', Config::getStatiSegnalazione('aperte'))->count();
53
-    $segnalazioniInLavorazione = $this->dashboardSegnalazioneBase()->whereIn('stato', Config::getStatiSegnalazione('in_lavorazione'))->count();
54
-    $segnalazioniChiuse = $this->dashboardSegnalazioneBase()->whereIn('stato', Config::getStatiSegnalazione('chiuse'))->count();
55
-
56
-    // Statistiche mese corrente
57 154
     $meseCorrente = Carbon::now()->startOfMonth();
58 155
     $fineMeseCorrente = Carbon::now()->endOfMonth();
59 156
     $mesePrecedente = Carbon::now()->subMonth()->startOfMonth();
@@ -64,49 +161,32 @@ class HomePageController extends Controller
64 161
     $costiMesePrecedente = $this->annotazioniDashboardQuery()->whereBetween('created_at', [$mesePrecedente, $fineMesePrecedente])->sum('costo');
65 162
 
66 163
     $variazioneCosti = $costiMesePrecedente > 0
67
-    ? round((($costiMese - $costiMesePrecedente) / $costiMesePrecedente) * 100, 2)
68
-    : ($costiMese > 0 ? 100 : 0);
164
+      ? round((($costiMese - $costiMesePrecedente) / $costiMesePrecedente) * 100, 2)
165
+      : ($costiMese > 0 ? 100 : 0);
69 166
 
70 167
     $tempiMese = $this->annotazioniDashboardQuery()->whereBetween('tempo_data', [$meseCorrente, $fineMeseCorrente])->sum('tempo_minuti');
71 168
     $oreTotaliMese = Config::formatTempo($tempiMese);
72 169
 
73
-    // Tempo medio di risoluzione (in giorni)
74 170
     $segnalazioniChiuseConTempo = $this->dashboardSegnalazioneBase()->whereIn('stato', Config::getStatiSegnalazione('chiuse'))
75
-    ->whereNotNull('updated_at')
76
-    ->whereNotNull('created_at')
77
-    ->get();
171
+      ->whereNotNull('updated_at')
172
+      ->whereNotNull('created_at')
173
+      ->get();
78 174
 
79 175
     $tempoMedioRisoluzione = 0;
80 176
     if ($segnalazioniChiuseConTempo->count() > 0) {
81
-      $giorniTotali = $segnalazioniChiuseConTempo->sum(function($segnalazione) {
177
+      $giorniTotali = $segnalazioniChiuseConTempo->sum(function ($segnalazione) {
82 178
         return $segnalazione->created_at->diffInDays($segnalazione->updated_at);
83 179
       });
84 180
       $tempoMedioRisoluzione = round($giorniTotali / $segnalazioniChiuseConTempo->count(), 1);
85 181
     }
86 182
 
87
-
88
-    $segnalazioniUrgentiAperte = $this->dashboardSegnalazioneBase()->whereNotIn('stato', Config::getStatiSegnalazione('chiuse'))
89
-    ->whereIn('priorita', ['urgente', 'emergenza_immediata'])
90
-    ->count();
91
-
92
-    // Segnalazioni senza assegnazione
93
-    $segnalazioniSenzaAssegnazione = $this->dashboardSegnalazioneBase()->whereNotIn('stato', Config::getStatiSegnalazione('chiuse'))->whereNull('assegnata_a_id')
94
-    ->count();
95
-
96
-    // Segnalazioni aperte da più di 7 giorni
97
-    $dataLimite = Carbon::now()->subDays(7);
98
-    $segnalazioniAperteVecchie = $this->dashboardSegnalazioneBase()->whereNotIn('stato', Config::getStatiSegnalazione('chiuse'))
99
-    ->where('created_at', '<', $dataLimite)
100
-    ->count();
101
-
102
-    // Grafico segnalazioni nel tempo (ultimi 30 giorni)
103 183
     $giorni = 30;
104 184
     $dataInizio = Carbon::now()->subDays($giorni);
105 185
     $segnalazioniTempo = $this->dashboardSegnalazioneBase()->where('created_at', '>=', $dataInizio)
106
-    ->selectRaw('DATE(created_at) as data, COUNT(*) as totale')
107
-    ->groupBy('data')
108
-    ->orderBy('data')
109
-    ->get();
186
+      ->selectRaw('DATE(created_at) as data, COUNT(*) as totale')
187
+      ->groupBy('data')
188
+      ->orderBy('data')
189
+      ->get();
110 190
 
111 191
     $labelsTempo = [];
112 192
     $datiTempo = [];
@@ -118,55 +198,50 @@ class HomePageController extends Controller
118 198
       $dataCorrente->addDay();
119 199
     }
120 200
 
121
-    // Distribuzione per stato
122 201
     $distribuzioneStato = $this->dashboardSegnalazioneBase()->select('stato', DB::raw('COUNT(*) as totale'))
123
-    ->groupBy('stato')
124
-    ->get();
202
+      ->groupBy('stato')
203
+      ->get();
125 204
 
126 205
     $labelsStato = [];
127 206
     $datiStato = [];
128 207
     foreach ($distribuzioneStato as $item) {
129 208
       $config = ConfiguraSegnalazione::where('gruppo', 'stato')
130
-      ->where('value', $item->stato)
131
-      ->first();
209
+        ->where('value', $item->stato)
210
+        ->first();
132 211
       $labelsStato[] = $config ? $config->label : $item->stato;
133 212
       $datiStato[] = $item->totale;
134 213
     }
135 214
 
136
-    // Distribuzione per priorità
137 215
     $labelsPriorita = [];
138 216
     $datiPriorita = [];
139
-    foreach(ConfiguraSegnalazione::where('gruppo', 'Priorità')->orderBy('order', 'asc')->get() as $config) {
217
+    foreach (ConfiguraSegnalazione::where('gruppo', 'Priorità')->orderBy('order', 'asc')->get() as $config) {
140 218
       $labelsPriorita[] = $config->label;
141 219
       $datiPriorita[] = $this->dashboardSegnalazioneBase()->where('priorita', $config->value)->count();
142 220
     }
143 221
 
144
-    // Distribuzione per tipo
145 222
     $distribuzioneTipo = $this->dashboardSegnalazioneBase()->select('tipo', DB::raw('COUNT(*) as totale'))
146
-    ->whereNotNull('tipo')
147
-    ->groupBy('tipo')
148
-    ->get();
223
+      ->whereNotNull('tipo')
224
+      ->groupBy('tipo')
225
+      ->get();
149 226
 
150 227
     $labelsTipo = [];
151 228
     $datiTipo = [];
152 229
     foreach ($distribuzioneTipo as $item) {
153 230
       $config = ConfiguraSegnalazione::where('gruppo', 'Tipo')
154
-      ->where('value', $item->tipo)
155
-      ->first();
231
+        ->where('value', $item->tipo)
232
+        ->first();
156 233
       $labelsTipo[] = $config ? $config->label : $item->tipo;
157 234
       $datiTipo[] = $item->totale;
158 235
     }
159 236
 
160
-    // Distribuzione per azienda
161 237
     $labelsAzienda = [];
162 238
     $datiAzienda = [];
163 239
 
164
-    foreach(Azienda::orderBy('nome', 'asc')->get() as $azienda) {
240
+    foreach (Azienda::orderBy('nome', 'asc')->get() as $azienda) {
165 241
       $labelsAzienda[] = $azienda->full_name;
166 242
       $datiAzienda[] = $this->dashboardSegnalazioneBase()->where('azienda_id', $azienda->id)->count();
167 243
     }
168 244
 
169
-    // Assicurati che tutti gli array non siano vuoti per evitare errori nei grafici
170 245
     if (empty($labelsTempo)) {
171 246
       $labelsTempo = ['Nessun dato'];
172 247
       $datiTempo = [0];
@@ -201,18 +276,11 @@ class HomePageController extends Controller
201 276
     }
202 277
 
203 278
     return [
204
-      'totaleSegnalazioni' => $totaleSegnalazioni,
205
-      'segnalazioniAperte' => $segnalazioniAperte,
206
-      'segnalazioniInLavorazione' => $segnalazioniInLavorazione,
207
-      'segnalazioniChiuse' => $segnalazioniChiuse,
208 279
       'costiMese' => $costiMese,
209 280
       'costiMesePrecedente' => $costiMesePrecedente,
210 281
       'variazioneCosti' => $variazioneCosti,
211 282
       'oreTotaliMese' => $oreTotaliMese,
212 283
       'tempoMedioRisoluzione' => $tempoMedioRisoluzione,
213
-      'segnalazioniUrgentiAperte' => $segnalazioniUrgentiAperte,
214
-      'segnalazioniSenzaAssegnazione' => $segnalazioniSenzaAssegnazione,
215
-      'segnalazioniAperteVecchie' => $segnalazioniAperteVecchie,
216 284
       'labelsTempo' => $labelsTempo,
217 285
       'datiTempo' => $datiTempo,
218 286
       'labelsStato' => $labelsStato,

+ 3
- 24
app/Http/Controllers/SegnalazioneClienteController.php Прегледај датотеку

@@ -102,7 +102,7 @@ class SegnalazioneClienteController extends Controller implements HasMiddleware
102 102
         $this->storeAllegati($request, $segnalazione, $annotazione);
103 103
 
104 104
         return redirect()
105
-            ->route('segnalazione.dettaglio', $segnalazione->id)
105
+            ->to($segnalazione->urlCliente())
106 106
             ->with('success', 'Segnalazione creata con successo');
107 107
     }
108 108
 
@@ -110,28 +110,7 @@ class SegnalazioneClienteController extends Controller implements HasMiddleware
110 110
     {
111 111
         $segnalazione = $this->findOwnOrAbort($id);
112 112
 
113
-        $segnalazione->load([
114
-            'azienda',
115
-            'user',
116
-            'assegnataA',
117
-            'ordineLavoro.offerte.righe',
118
-            'ordineLavoro.righe',
119
-            'contrattoServizio.contrattoTemplate',
120
-            'contrattoServizio.contrattoServizioRigas.prodotto',
121
-            'contrattoServizioTodo',
122
-            'prodotto',
123
-            'acquistoServizio.prodotto',
124
-            'acquistoServizios.prodotto',
125
-            'todos.completatoDa',
126
-            'annotazioni.allegatiAnnotazione',
127
-            'annotazioni.user',
128
-        ]);
129
-
130
-        return view('segnalazione.cliente.show', [
131
-            'segnalazione' => $segnalazione,
132
-            'statiDisponibili' => [],
133
-            'areaCliente' => true,
134
-        ]);
113
+        return redirect()->to($segnalazione->urlCliente());
135 114
     }
136 115
 
137 116
     public function rispondi(Request $request, $id)
@@ -179,7 +158,7 @@ class SegnalazioneClienteController extends Controller implements HasMiddleware
179 158
         }
180 159
 
181 160
         return redirect()
182
-            ->route('segnalazione.dettaglio', $segnalazione->id)
161
+            ->to($segnalazione->urlCliente())
183 162
             ->with('success', 'Risposta inviata');
184 163
     }
185 164
 

+ 83
- 400
app/Http/Controllers/SegnalazioneController.php Прегледај датотеку

@@ -6,8 +6,6 @@ use Illuminate\Http\Request;
6 6
 use Illuminate\Routing\Controllers\Middleware;
7 7
 use Illuminate\Routing\Controllers\HasMiddleware;
8 8
 use App\Models\Segnalazione;
9
-use App\Models\Reparto;
10
-use App\Models\Luogo;
11 9
 use App\Models\User;
12 10
 use App\Models\Annotazione;
13 11
 use App\Models\AllegatoAnnotazione;
@@ -36,7 +34,6 @@ class SegnalazioneController extends Controller implements HasMiddleware
36 34
     'view-segnalazione' => 'Vedi tutte le segnalazioni',
37 35
     'view-own-segnalazione' => 'Vedi solo le segnalazioni create dall\'utente',
38 36
     'view-segnalazione-assegnate' => 'Vedi solo le segnalazioni assegnate all\'utente o ai propri gruppi',
39
-    'create-segnalazione-noauth' => 'Crea (no auth)',
40 37
   ];
41 38
 
42 39
   public static function middleware(): array
@@ -54,8 +51,6 @@ class SegnalazioneController extends Controller implements HasMiddleware
54 51
         'admin_tempo_timer',
55 52
       ]),
56 53
       new Middleware('permission:delete-segnalazione', only: ['destroy', 'admin_delete']),
57
-      // new Middleware('permission:view-segnalazione', only: ['show']),
58
-      // new Middleware('permission:create-segnalazione-noauth', only: ['nuova']),
59 54
     ];
60 55
   }
61 56
   public function admin_index(SegnalazioneDataTable $dataTable, Request $request, ?string $status = null)
@@ -91,7 +86,7 @@ class SegnalazioneController extends Controller implements HasMiddleware
91 86
 
92 87
     $contratti = ContrattoServizio::where('user_id', $aziendaId)
93 88
       ->attivi()
94
-      ->with(['contrattoServizioRigas.prodotto'])
89
+      ->with(['righe.prodotto'])
95 90
       ->orderBy('nome')
96 91
       ->get()
97 92
       ->map(function (ContrattoServizio $cs) {
@@ -163,6 +158,10 @@ class SegnalazioneController extends Controller implements HasMiddleware
163 158
       'tempo_stimato' => 'nullable|numeric|min:0',
164 159
       'tempo_ore' => 'nullable|string',
165 160
       'tempo_user_id' => 'nullable|exists:users,id',
161
+      'slots' => 'nullable|array',
162
+      'slots.*.data' => 'nullable|date_format:d/m/Y',
163
+      'slots.*.ora_inizio' => 'nullable|date_format:H:i',
164
+      'slots.*.ora_fine' => 'nullable|date_format:H:i',
166 165
       'allegato' => 'nullable|array',
167 166
       'allegato.*' => 'file|max:10240', // max 10MB per file
168 167
     ]);
@@ -201,6 +200,34 @@ class SegnalazioneController extends Controller implements HasMiddleware
201 200
       return redirect()->back()->withInput()->with('error', $blockClose);
202 201
     }
203 202
 
203
+    $parsedSlots = [];
204
+    if ($request->stato === 'pianificata') {
205
+      foreach ($request->input('slots', []) as $slotInput) {
206
+        $data = trim((string) ($slotInput['data'] ?? ''));
207
+        $oraInizio = trim((string) ($slotInput['ora_inizio'] ?? ''));
208
+        $oraFine = trim((string) ($slotInput['ora_fine'] ?? ''));
209
+        if ($data === '' && $oraInizio === '' && $oraFine === '') {
210
+          continue;
211
+        }
212
+        if ($data === '' || $oraInizio === '' || $oraFine === '') {
213
+          return redirect()->back()->withInput()->with('error', 'Completa data, ora inizio e ora fine per ogni slot di prenotazione.');
214
+        }
215
+        try {
216
+          $startsAt = \Carbon\Carbon::createFromFormat('d/m/Y H:i', $data.' '.$oraInizio);
217
+          $endsAt = \Carbon\Carbon::createFromFormat('d/m/Y H:i', $data.' '.$oraFine);
218
+        } catch (\Throwable $e) {
219
+          return redirect()->back()->withInput()->with('error', 'Formato data/ora slot non valido.');
220
+        }
221
+        if ($endsAt->lte($startsAt)) {
222
+          return redirect()->back()->withInput()->with('error', 'L\'ora di fine deve essere successiva all\'ora di inizio.');
223
+        }
224
+        $parsedSlots[] = [
225
+          'starts_at' => $startsAt,
226
+          'ends_at' => $endsAt,
227
+        ];
228
+      }
229
+    }
230
+
204 231
     $segnalazione->titolo = $request->titolo;
205 232
     $segnalazione->azienda_id = $request->filled('azienda_id') ? $request->azienda_id : null;
206 233
     $segnalazione->user_id = $request->user_id;
@@ -215,6 +242,14 @@ class SegnalazioneController extends Controller implements HasMiddleware
215 242
       : null;
216 243
     $segnalazione->save();
217 244
 
245
+    // Sync slot prenotazione (solo in stato pianificata; altrimenti svuota)
246
+    $segnalazione->slots()->delete();
247
+    if ($request->stato === 'pianificata') {
248
+      foreach ($parsedSlots as $slotData) {
249
+        $segnalazione->slots()->create($slotData);
250
+      }
251
+    }
252
+
218 253
     $annotazione = new Annotazione();
219 254
     $annotazione->text = $request->text;
220 255
     $annotazione->segnalazione_id = $segnalazione->id;
@@ -364,12 +399,13 @@ class SegnalazioneController extends Controller implements HasMiddleware
364 399
       'ordineLavoro.offerte.righe',
365 400
       'ordineLavoro.righe',
366 401
       'contrattoServizio.contrattoTemplate',
367
-      'contrattoServizio.contrattoServizioRigas.prodotto',
402
+      'contrattoServizio.righe.prodotto',
368 403
       'contrattoServizioTodo',
369 404
       'prodotto',
370 405
       'acquistoServizio.prodotto',
371 406
       'acquistoServizios.prodotto',
372 407
       'todos.completatoDa',
408
+      'slots',
373 409
       'annotazioni.allegatiAnnotazione',
374 410
       'annotazioni.user',
375 411
       ])->find($id);
@@ -378,7 +414,7 @@ class SegnalazioneController extends Controller implements HasMiddleware
378 414
       abort_unless(Segnalazione::authUserMayViewSegnalazione(Auth::user(), $segnalazione->assegnata_a_id, $segnalazione->user_id), 403);
379 415
 
380 416
       if (Segnalazione::isClienteVista(Auth::user())) {
381
-        return redirect()->route('segnalazione.dettaglio', $segnalazione->id);
417
+        return redirect()->to($segnalazione->urlCliente());
382 418
       }
383 419
 
384 420
       // Recupera gli stati disponibili per il dropdown (solo se l'utente ha i permessi)
@@ -701,411 +737,58 @@ class SegnalazioneController extends Controller implements HasMiddleware
701 737
                 )
702 738
               );
703 739
 
704
-            }
705 740
             return response()->json($results);
706 741
           }
707
-
708
-    // END ADMIN
709
-
710
-    public function nuova($token, Request $request){
711
-      $reparto = Reparto::where('token', $token)->first();
712
-      if(!$reparto){
713
-        return redirect()->route('reparto.dashboard')->with('error', 'Reparto non trovato');
714
-      }
715
-      return view('segnalazione.nuova', ['reparto' => $reparto]);
716
-    }
717
-
718
-    public function reparto($token, Request $request)
719
-    {
720
-      $reparto = Reparto::where('token', $token)->first();
721
-
722
-      if (!$reparto) {
723
-        return redirect()->route('reparto.dashboard')->with('error', 'Reparto non trovato');
724
-      }
725
-
726
-      $segnalazioni = Segnalazione::with(['reparto', 'luogo'])
727
-      ->where('reparto_id', $reparto->id)
728
-      ->orderBy('created_at', 'desc')
729
-      ->get();
730
-
731
-      // Prepara i dati per i filtri
732
-      $luoghiUnici = $segnalazioni->pluck('luogo')->filter(function($luogo) {
733
-        return $luogo !== null;
734
-      })->unique('id')->sortBy('label')->values();
735
-      $statiUnici = $segnalazioni->pluck('stato')->filter()->unique()->sort()->values();
736
-      $prioritaUniche = $segnalazioni->pluck('priorita')->filter()->unique()->sort()->values();
737
-
738
-      // Recupera le configurazioni per stati e priorità
739
-      $configStati = \App\Models\ConfiguraSegnalazione::where('gruppo', 'stato')
740
-      ->whereIn('value', $statiUnici->toArray())
741
-      ->orderBy('order')
742
-      ->get()
743
-      ->keyBy('value');
744
-
745
-      $configPriorita = \App\Models\ConfiguraSegnalazione::where('gruppo', 'Priorità')
746
-      ->whereIn('value', $prioritaUniche->toArray())
747
-      ->orderBy('order')
748
-      ->get()
749
-      ->keyBy('value');
750
-
751
-      return view('segnalazione.reparto', [
752
-        'reparto' => $reparto,
753
-        'segnalazioni' => $segnalazioni,
754
-        'luoghiUnici' => $luoghiUnici,
755
-        'statiUnici' => $statiUnici,
756
-        'prioritaUniche' => $prioritaUniche,
757
-        'configStati' => $configStati,
758
-        'configPriorita' => $configPriorita
759
-      ]);
760
-    }
761
-
762
-    public function crea(Request $request)
763
-    {
764
-      // Validazione base
765
-      $validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
766
-        'token' => 'required|exists:reparto,token',
767
-        'luogo_id' => 'required|exists:luogo,id',
768
-        'titolo' => 'required|string|max:255',
769
-        'descrizione' => 'required|string',
770
-        'allegato' => 'nullable|array',
771
-        'allegato.*' => 'file|max:10240', // max 10MB per file
772
-      ]);
773
-
774
-      if ($validator->fails()) {
775
-        return redirect()->back()
776
-        ->withErrors($validator)
777
-        ->withInput();
778
-      }
779
-
780
-      // Crea la segnalazione
781
-      $segnalazione = new Segnalazione();
782
-      $segnalazione->fill($request->all());
783
-
784
-      $reparto = Reparto::where('token', $request->token)->first();
785
-      if($reparto) {
786
-        $segnalazione->reparto_id = $reparto->id;
787
-      }
788
-
789
-      $segnalazione->save();
790
-
791
-      // creo la prima annotazione con il testo della segnalazione
792
-      $annotazione = new Annotazione();
793
-      $annotazione->text = $segnalazione->descrizione;
794
-      $annotazione->segnalazione_id = $segnalazione->id;
795
-      $annotazione->user_id = null;
796
-      $annotazione->riservata = false;
797
-      $annotazione->riferimento = $segnalazione->riferimento;
798
-      $annotazione->email = $segnalazione->email;
799
-      $annotazione->telefono = $segnalazione->telefono;
800
-      $annotazione->save();
801
-
802
-
803
-      // Se è stato caricato un file, crea l'allegato usando il sistema esistente
804
-      if ($request->hasFile('allegato')){
805
-        // Assicurati che la directory esista
806
-        $directory = storage_path('app/public/allegatiAnnotazioni/' . $segnalazione->id);
807
-        if (!\Illuminate\Support\Facades\File::exists($directory)) {
808
-          \Illuminate\Support\Facades\File::makeDirectory($directory, 0755, true);
809
-        }
810
-
811
-        foreach ($request->file('allegato') as $file) {
812
-          if (!$file) {
813
-            continue;
814
-          }
815
-          $filename = time() . '_' . $file->getClientOriginalName();
816
-          $path = $file->storeAs($segnalazione->id, $filename, 'allegatiAnnotazioni');
817
-
818
-          $allegato = new AllegatoAnnotazione();
819
-          $allegato->annotazione()->associate($annotazione);
820
-          $allegato->nome = $file->getClientOriginalName();
821
-          $allegato->path_file = $path;
822
-          $allegato->save();
823
-        }
824
-      }
825
-
826
-      return redirect()->route('segnalazione.reparto', ['token' => $segnalazione->reparto->token])->with('success', 'Segnalazione creata con successo');
827 742
     }
828 743
 
829 744
     public function show(Request $request)
830 745
     {
831 746
       $segnalazione = Segnalazione::with([
832
-        'reparto',
833
-        'luogo',
747
+        'azienda',
748
+        'user',
749
+        'assegnataA',
750
+        'ordineLavoro.offerte.righe',
751
+        'ordineLavoro.righe',
752
+        'contrattoServizio.contrattoTemplate',
753
+        'contrattoServizio.righe.prodotto',
754
+        'contrattoServizioTodo',
755
+        'prodotto',
756
+        'acquistoServizio.prodotto',
757
+        'acquistoServizios.prodotto',
758
+        'todos.completatoDa',
834 759
         'annotazioni.allegatiAnnotazione',
835 760
         'annotazioni.user',
836
-        ])->where('token', $request->token)->first();
837
-        if(!$segnalazione) return redirect()->route('welcome')->with('error', 'Segnalazione non trovata');
838
-
839
-        // Recupera gli stati disponibili per il dropdown (solo se l'utente ha i permessi)
840
-        $statiDisponibili = [];
841
-        if(Auth::check()){
842
-          if(Auth::user()->can('edit-segnalazione')){
843
-            $statiDisponibili = \App\Models\ConfiguraSegnalazione::where('gruppo', 'stato')
844
-            ->orderBy('order')
845
-            ->get();
846
-          }
847
-        }
761
+      ])->where('token', $request->token)->first();
848 762
 
849
-        return view('segnalazione.show', [
850
-          'segnalazione' => $segnalazione,
851
-          'statiDisponibili' => $statiDisponibili,
852
-        ]);
763
+      if (! $segnalazione) {
764
+        return redirect()->route('welcome')->with('error', 'Segnalazione non trovata');
853 765
       }
854 766
 
767
+      $user = Auth::user();
855 768
 
769
+      if ($user && Segnalazione::isClienteVista($user) && $segnalazione->isAccessibleByCliente($user)) {
770
+        return view('segnalazione.cliente.show', [
771
+          'segnalazione' => $segnalazione,
772
+          'statiDisponibili' => [],
773
+          'areaCliente' => true,
774
+        ]);
775
+      }
856 776
 
777
+      if ($user && Segnalazione::authUserMayViewSegnalazione($user, $segnalazione->assegnata_a_id, $segnalazione->user_id)
778
+          && ! Segnalazione::isClienteVista($user)) {
779
+        return redirect()->route('admin.segnalazione.show', ['id' => $segnalazione->id]);
780
+      }
857 781
 
782
+      $statiDisponibili = [];
783
+      if ($user && $user->can('edit-segnalazione')) {
784
+        $statiDisponibili = ConfiguraSegnalazione::where('gruppo', 'stato')
785
+          ->orderBy('order')
786
+          ->get();
787
+      }
858 788
 
859
-
860
-
861
-
862
-
863
-      // public function esportaPdf(Request $request, $id)
864
-      // {
865
-      //   $segnalazione = Segnalazione::with([
866
-      //     'reparto',
867
-      //     'luogo',
868
-      //     'annotazioni.allegatiAnnotazione',
869
-      //     ])->find($id);
870
-
871
-      //     if(!$segnalazione) {
872
-      //       return redirect()->route('segnalazione.index')->with('error', 'Segnalazione non trovata');
873
-      //     }
874
-
875
-      //     // Recupera le configurazioni
876
-      //     $configStato = $segnalazione->getConfiguraStato();
877
-      //     $configPriorita = $segnalazione->getConfiguraPriorita();
878
-      //     $configTipo = $segnalazione->getConfiguraTipo();
879
-
880
-      //     // Crea timeline eventi (escludendo tempo e costi)
881
-      //     $eventi = collect();
882
-
883
-      //     // Evento: Creazione segnalazione
884
-      //     $eventi->push([
885
-      //       'tipo' => 'creazione',
886
-      //       'data' => $segnalazione->created_at,
887
-      //       'titolo' => 'Segnalazione creata',
888
-      //       'descrizione' => $segnalazione->descrizione,
889
-      //       'dati' => [
890
-      //         'titolo' => $segnalazione->titolo,
891
-      //         'priorita' => $configPriorita ? $configPriorita->label : null,
892
-      //         'tipo' => $configTipo ? $configTipo->label : null,
893
-      //         'luogo' => $segnalazione->luogo ? $segnalazione->luogo->label : null,
894
-      //         'reparto' => $segnalazione->reparto ? $segnalazione->reparto->label : null,
895
-      //         'segnalatore' => $segnalazione->segnalatore ? $segnalazione->segnalatore->fullName() : null,
896
-      //         ]
897
-      //       ]);
898
-
899
-      //       // Eventi: Annotazioni
900
-      //       foreach($segnalazione->annotazioni as $annotazione) {
901
-      //         $allegatiAnnotazione = [];
902
-      //         foreach($annotazione->allegatiAnnotazione as $allegato) {
903
-      //           $allegatiAnnotazione[] = [
904
-      //             'id' => $allegato->id,
905
-      //             'nome' => $allegato->nome,
906
-      //             'descrizione' => $allegato->descrizione,
907
-      //             'path_file' => $allegato->path_file,
908
-      //             'isImage' => $allegato->isImage(),
909
-      //             'url' => $allegato->isImage() ? $allegato->getUrl() : null,
910
-      //           ];
911
-      //         }
912
-
913
-      //         $eventi->push([
914
-      //           'tipo' => 'annotazione',
915
-      //           'data' => $annotazione->created_at,
916
-      //           'titolo' => 'Annotazione',
917
-      //           'descrizione' => $annotazione->annotazione,
918
-      //           'dati' => [
919
-      //             'autore' => $annotazione->autore ? $annotazione->autore->fullName() : null,
920
-      //             'allegati' => $allegatiAnnotazione,
921
-      //             ]
922
-      //           ]);
923
-      //         }
924
-
925
-      //         // Eventi: Allegati
926
-      //         foreach($segnalazione->allegatiSegnalazione as $allegato) {
927
-      //           $eventi->push([
928
-      //             'tipo' => 'allegato',
929
-      //             'data' => $allegato->created_at,
930
-      //             'titolo' => 'Allegato caricato',
931
-      //             'descrizione' => $allegato->nome ?? 'Allegato',
932
-      //             'dati' => [
933
-      //               'nome' => $allegato->nome,
934
-      //               'descrizione' => $allegato->descrizione,
935
-      //               'path_file' => $allegato->path_file,
936
-      //               'isImage' => $allegato->isImage(),
937
-      //               'url' => $allegato->isImage() ? $allegato->getUrl() : null,
938
-      //               ]
939
-      //             ]);
940
-      //           }
941
-
942
-      //           // Ordina per data (dal più vecchio al più recente per il PDF)
943
-      //           $eventi = $eventi->sortBy('data')->values();
944
-
945
-      //           // Renderizza la view in HTML
946
-      //           $html = view('segnalazione.pdf', [
947
-      //             'segnalazione' => $segnalazione,
948
-      //             'configStato' => $configStato,
949
-      //             'configPriorita' => $configPriorita,
950
-      //             'configTipo' => $configTipo,
951
-      //             'eventi' => $eventi,
952
-      //             ])->render();
953
-
954
-      //             // DomPDF: mantieni le immagini come base64 inline (più affidabile)
955
-      //             // Le immagini sono già ottimizzate nel template Blade
956
-      //             // Non serve convertire in file temporanei - DomPDF dovrebbe gestire il base64
957
-      //             $tempImageFiles = []; // Vuoto - non usiamo file temporanei
958
-
959
-      //             // Pulisci l'HTML da elementi problematici
960
-      //             // Rimuovi il CSS di testata_pdf che contiene "Encode Sans Condensed"
961
-      //             $html = preg_replace('/<style[^>]*>.*?Encode Sans Condensed.*?<\/style>/is', '', $html);
962
-
963
-      //             // Rimuovi link a font esterni
964
-      //             $html = preg_replace('/<link[^>]*fonts\.googleapis[^>]*>/i', '', $html);
965
-
966
-      //             // Rimuovi margin: 1cm dal CSS rimanente
967
-      //             $html = preg_replace('/(margin:\s*)1cm/i', '$10', $html);
968
-
969
-      //             // Pulisci l'HTML da caratteri problematici
970
-      //             $html = mb_convert_encoding($html, 'UTF-8', 'UTF-8');
971
-      //             $html = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $html);
972
-
973
-      //             // Rimuovi l'attributo data-mpdf-image-path che non serve più
974
-      //             $html = preg_replace('/\s+data-mpdf-image-path="[^"]+"/i', '', $html);
975
-
976
-      //             // Assicurati che l'HTML abbia una struttura valida
977
-      //             if (!preg_match('/<html/i', $html)) {
978
-      //               $html = '<html><head></head><body>' . $html . '</body></html>';
979
-      //             }
980
-
981
-      //             $filename = 'segnalazione_' . Str::slug($segnalazione->titolo) . '_' . $segnalazione->id . '.pdf';
982
-
983
-      //             // Genera il PDF con DomPDF
984
-      //             // Usa base64 inline - DomPDF dovrebbe gestirlo nativamente
985
-      //             try {
986
-      //               // Conta le immagini base64 nell'HTML per debug
987
-      //               $imageCount = preg_match_all('/data:image\/[^;]+;base64,[^"]+/i', $html);
988
-      //               Log::info('Generazione PDF - Immagini base64 trovate nell\'HTML: ' . $imageCount);
989
-
990
-      //               $pdf = Pdf::loadHTML($html);
991
-      //               $pdf->setPaper('a4', 'portrait');
992
-
993
-      //               // Configurazione DomPDF per gestire meglio le immagini base64
994
-      //               $pdf->setOption('isRemoteEnabled', false); // Non serve per base64 inline
995
-      //               $pdf->setOption('isHtml5ParserEnabled', true);
996
-      //               $pdf->setOption('isPhpEnabled', true); // Abilitato per numerazione pagine
997
-      //               $pdf->setOption('isFontSubsettingEnabled', true); // Ottimizza font
998
-      //               $pdf->setOption('defaultFont', 'DejaVu Sans'); // Usa font di sistema
999
-
1000
-      //               // Abilita logging errori DomPDF
1001
-      //               $pdf->setOption('logOutputFile', storage_path('logs/dompdf.log'));
1002
-      //               $pdf->setOption('enableFontSubsetting', true);
1003
-
1004
-      //               return response()->streamDownload(function() use ($pdf) {
1005
-      //                 echo $pdf->output();
1006
-      //               }, $filename, [
1007
-      //                 'Content-Type' => 'application/pdf',
1008
-      //               ]);
1009
-      //             } catch (\Exception $e) {
1010
-      //               // Log errore per debug
1011
-      //               Log::error('Errore generazione PDF DomPDF: ' . $e->getMessage());
1012
-      //               Log::error('Stack trace: ' . $e->getTraceAsString());
1013
-
1014
-      //               throw new \Exception('Errore nella generazione del PDF: ' . $e->getMessage());
1015
-      //             }
1016
-      //           }
1017
-
1018
-      //           public function previewPdf(Request $request, $id)
1019
-      //           {
1020
-      //             $segnalazione = Segnalazione::with([
1021
-      //               'reparto',
1022
-      //               'luogo',
1023
-      //               'segnalatore',
1024
-      //               'allegatiSegnalazione',
1025
-      //               'annotazioni.allegatiAnnotazione',
1026
-      //               'annotazioni.autore'
1027
-      //               ])->find($id);
1028
-
1029
-      //               if(!$segnalazione) {
1030
-      //                 return redirect()->route('segnalazione.index')->with('error', 'Segnalazione non trovata');
1031
-      //               }
1032
-
1033
-      //               // Recupera le configurazioni
1034
-      //               $configStato = $segnalazione->getConfiguraStato();
1035
-      //               $configPriorita = $segnalazione->getConfiguraPriorita();
1036
-      //               $configTipo = $segnalazione->getConfiguraTipo();
1037
-
1038
-      //               // Crea timeline eventi (escludendo tempo e costi)
1039
-      //               $eventi = collect();
1040
-
1041
-      //               // Evento: Creazione segnalazione
1042
-      //               $eventi->push([
1043
-      //                 'tipo' => 'creazione',
1044
-      //                 'data' => $segnalazione->created_at,
1045
-      //                 'titolo' => 'Segnalazione creata',
1046
-      //                 'descrizione' => $segnalazione->descrizione,
1047
-      //                 'dati' => [
1048
-      //                   'titolo' => $segnalazione->titolo,
1049
-      //                   'priorita' => $configPriorita ? $configPriorita->label : null,
1050
-      //                   'tipo' => $configTipo ? $configTipo->label : null,
1051
-      //                   'luogo' => $segnalazione->luogo ? $segnalazione->luogo->label : null,
1052
-      //                   'reparto' => $segnalazione->reparto ? $segnalazione->reparto->label : null,
1053
-      //                   'segnalatore' => $segnalazione->segnalatore ? $segnalazione->segnalatore->fullName() : null,
1054
-      //                   ]
1055
-      //                 ]);
1056
-
1057
-      //                 // Eventi: Annotazioni
1058
-      //                 foreach($segnalazione->annotazioni as $annotazione) {
1059
-      //                   $allegatiAnnotazione = [];
1060
-      //                   foreach($annotazione->allegatiAnnotazione as $allegato) {
1061
-      //                     $allegatiAnnotazione[] = [
1062
-      //                       'id' => $allegato->id,
1063
-      //                       'nome' => $allegato->nome,
1064
-      //                       'descrizione' => $allegato->descrizione,
1065
-      //                       'path_file' => $allegato->path_file,
1066
-      //                       'isImage' => $allegato->isImage(),
1067
-      //                       'url' => $allegato->isImage() ? $allegato->getUrl() : null,
1068
-      //                     ];
1069
-      //                   }
1070
-
1071
-      //                   $eventi->push([
1072
-      //                     'tipo' => 'annotazione',
1073
-      //                     'data' => $annotazione->created_at,
1074
-      //                     'titolo' => 'Annotazione',
1075
-      //                     'descrizione' => $annotazione->annotazione,
1076
-      //                     'dati' => [
1077
-      //                       'autore' => $annotazione->autore ? $annotazione->autore->fullName() : null,
1078
-      //                       'allegati' => $allegatiAnnotazione,
1079
-      //                       ]
1080
-      //                     ]);
1081
-      //                   }
1082
-
1083
-      //                   // Eventi: Allegati
1084
-      //                   foreach($segnalazione->allegatiSegnalazione as $allegato) {
1085
-      //                     $eventi->push([
1086
-      //                       'tipo' => 'allegato',
1087
-      //                       'data' => $allegato->created_at,
1088
-      //                       'titolo' => 'Allegato caricato',
1089
-      //                       'descrizione' => $allegato->nome ?? 'Allegato',
1090
-      //                       'dati' => [
1091
-      //                         'nome' => $allegato->nome,
1092
-      //                         'descrizione' => $allegato->descrizione,
1093
-      //                         'path_file' => $allegato->path_file,
1094
-      //                         'isImage' => $allegato->isImage(),
1095
-      //                         'url' => $allegato->isImage() ? $allegato->getUrl() : null,
1096
-      //                         ]
1097
-      //                       ]);
1098
-      //                     }
1099
-
1100
-      //                     // Ordina per data (dal più vecchio al più recente per il PDF)
1101
-      //                     $eventi = $eventi->sortBy('data')->values();
1102
-
1103
-      //                     return view('segnalazione.pdf', [
1104
-      //                       'segnalazione' => $segnalazione,
1105
-      //                       'configStato' => $configStato,
1106
-      //                       'configPriorita' => $configPriorita,
1107
-      //                       'configTipo' => $configTipo,
1108
-      //                       'eventi' => $eventi,
1109
-      //                     ]);
1110
-      //                   }
1111
-                      }
789
+      return view('segnalazione.show', [
790
+        'segnalazione' => $segnalazione,
791
+        'statiDisponibili' => $statiDisponibili,
792
+      ]);
793
+    }
794
+}

+ 2
- 2
app/Models/AbstractModels/AbstractContrattoServizio.php Прегледај датотеку

@@ -96,12 +96,12 @@ abstract class AbstractContrattoServizio extends Model
96 96
         return $this->hasMany('\App\Models\Segnalazione', 'contratto_servizio_id', 'id');
97 97
     }
98 98
     
99
-    public function contrattoServizioTodos()
99
+    public function todos()
100 100
     {
101 101
         return $this->hasMany('\App\Models\ContrattoServizioTodo', 'contratto_servizio_id', 'id');
102 102
     }
103 103
     
104
-    public function contrattoServizioRigas()
104
+    public function righe()
105 105
     {
106 106
         return $this->hasMany('\App\Models\ContrattoServizioRiga', 'contratto_servizio_id', 'id');
107 107
     }

+ 2
- 2
app/Models/AbstractModels/AbstractContrattoTemplate.php Прегледај датотеку

@@ -72,12 +72,12 @@ abstract class AbstractContrattoTemplate extends Model
72 72
         return $this->hasMany('\App\Models\ContrattoServizio', 'contratto_template_id', 'id');
73 73
     }
74 74
     
75
-    public function contrattoTemplateTodos()
75
+    public function todos()
76 76
     {
77 77
         return $this->hasMany('\App\Models\ContrattoTemplateTodo', 'contratto_template_id', 'id');
78 78
     }
79 79
     
80
-    public function contrattoTemplateRigas()
80
+    public function righe()
81 81
     {
82 82
         return $this->hasMany('\App\Models\ContrattoTemplateRiga', 'contratto_template_id', 'id');
83 83
     }

+ 1
- 1
app/Models/AbstractModels/AbstractFattura.php Прегледај датотеку

@@ -105,7 +105,7 @@ abstract class AbstractFattura extends Model
105 105
         return $this->belongsTo('\App\Models\Comune', 'comune_id', 'id');
106 106
     }
107 107
     
108
-    public function fatturaRighe()
108
+    public function righe()
109 109
     {
110 110
         return $this->hasMany('\App\Models\FatturaRiga', 'fattura_id', 'id');
111 111
     }

+ 1
- 1
app/Models/AbstractModels/AbstractOfferta.php Прегледај датотеку

@@ -71,7 +71,7 @@ abstract class AbstractOfferta extends Model
71 71
         return $this->belongsTo('\App\Models\User', 'user_id', 'id');
72 72
     }
73 73
     
74
-    public function offerteRiga()
74
+    public function righe()
75 75
     {
76 76
         return $this->hasMany('\App\Models\OffertaRiga', 'offerta_id', 'id');
77 77
     }

+ 5
- 0
app/Models/AbstractModels/AbstractOrdineLavoro.php Прегледај датотеку

@@ -86,6 +86,11 @@ abstract class AbstractOrdineLavoro extends Model
86 86
         return $this->hasMany('\App\Models\FatturaRiga', 'ordine_lavoro_id', 'id');
87 87
     }
88 88
     
89
+    public function righe()
90
+    {
91
+        return $this->hasMany('\App\Models\OrdineLavoroRiga', 'ordine_lavoro_id', 'id');
92
+    }
93
+    
89 94
     public function offerte()
90 95
     {
91 96
         return $this->belongsToMany('\App\Models\Offerta', 'ordine_lavoro_has_offerta', 'ordine_lavoro_id', 'offerta_id');

+ 85
- 0
app/Models/AbstractModels/AbstractOrdineLavoroRiga.php Прегледај датотеку

@@ -0,0 +1,85 @@
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
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractOrdineLavoroRiga extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'ordine_lavoro_riga';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The model's default values for attributes.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $attributes = ['ordine' => 0];
33
+    
34
+    /**  
35
+     * The attributes that should be cast to native types.
36
+     * 
37
+     * @var array
38
+     */
39
+    protected $casts = [
40
+        'id' => 'integer',
41
+        'ordine_lavoro_id' => 'integer',
42
+        'prodotto_id' => 'integer',
43
+        'descrizione' => 'string',
44
+        'quantita' => 'decimal:2',
45
+        'unita_misura' => 'string',
46
+        'prezzo_unitario' => 'decimal:2',
47
+        'sconto' => 'decimal:2',
48
+        'ordine' => 'integer',
49
+        'note_private' => 'string',
50
+        'tempo_stimato' => 'decimal:2',
51
+        'created_at' => 'datetime',
52
+        'updated_at' => 'datetime'
53
+    ];
54
+    
55
+    /**  
56
+     * The attributes that are mass assignable.
57
+     * 
58
+     * @var array
59
+     */
60
+    protected $fillable = [
61
+        'id',
62
+        'ordine_lavoro_id',
63
+        'prodotto_id',
64
+        'descrizione',
65
+        'quantita',
66
+        'unita_misura',
67
+        'prezzo_unitario',
68
+        'sconto',
69
+        'ordine',
70
+        'note_private',
71
+        'tempo_stimato',
72
+        'created_at',
73
+        'updated_at'
74
+    ];
75
+    
76
+    public function ordineLavoro()
77
+    {
78
+        return $this->belongsTo('\App\Models\OrdineLavoro', 'ordine_lavoro_id', 'id');
79
+    }
80
+    
81
+    public function prodotto()
82
+    {
83
+        return $this->belongsTo('\App\Models\Prodotto', 'prodotto_id', 'id');
84
+    }
85
+}

+ 6
- 1
app/Models/AbstractModels/AbstractProdotto.php Прегледај датотеку

@@ -101,7 +101,7 @@ abstract class AbstractProdotto extends Model
101 101
         return $this->hasMany('\App\Models\AcquistoServizio', 'prodotto_id', 'id');
102 102
     }
103 103
     
104
-    public function prodottoTodos()
104
+    public function todos()
105 105
     {
106 106
         return $this->hasMany('\App\Models\ProdottoTodo', 'prodotto_id', 'id');
107 107
     }
@@ -111,6 +111,11 @@ abstract class AbstractProdotto extends Model
111 111
         return $this->hasMany('\App\Models\FatturaRiga', 'prodotto_id', 'id');
112 112
     }
113 113
     
114
+    public function ordineLavoroRigas()
115
+    {
116
+        return $this->hasMany('\App\Models\OrdineLavoroRiga', 'prodotto_id', 'id');
117
+    }
118
+    
114 119
     public function promoCodes()
115 120
     {
116 121
         return $this->belongsToMany('\App\Models\PromoCode', 'promo_code_has_prodotto', 'prodotto_id', 'promo_code_id');

+ 6
- 1
app/Models/AbstractModels/AbstractSegnalazione.php Прегледај датотеку

@@ -130,7 +130,7 @@ abstract class AbstractSegnalazione extends Model
130 130
         return $this->hasMany('\App\Models\AcquistoServizio', 'segnalazione_id', 'id');
131 131
     }
132 132
     
133
-    public function segnalazioneTodos()
133
+    public function todos()
134 134
     {
135 135
         return $this->hasMany('\App\Models\SegnalazioneTodo', 'segnalazione_id', 'id');
136 136
     }
@@ -139,4 +139,9 @@ abstract class AbstractSegnalazione extends Model
139 139
     {
140 140
         return $this->hasMany('\App\Models\FatturaRiga', 'segnalazione_id', 'id');
141 141
     }
142
+    
143
+    public function slots()
144
+    {
145
+        return $this->hasMany('\App\Models\SegnalazioneSlot', 'segnalazione_id', 'id');
146
+    }
142 147
 }

+ 59
- 0
app/Models/AbstractModels/AbstractSegnalazioneSlot.php Прегледај датотеку

@@ -0,0 +1,59 @@
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
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractSegnalazioneSlot extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'segnalazione_slot';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The attributes that should be cast to native types.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $casts = [
33
+        'id' => 'integer',
34
+        'segnalazione_id' => 'integer',
35
+        'starts_at' => 'datetime',
36
+        'ends_at' => 'datetime',
37
+        'created_at' => 'datetime',
38
+        'updated_at' => 'datetime'
39
+    ];
40
+    
41
+    /**  
42
+     * The attributes that are mass assignable.
43
+     * 
44
+     * @var array
45
+     */
46
+    protected $fillable = [
47
+        'id',
48
+        'segnalazione_id',
49
+        'starts_at',
50
+        'ends_at',
51
+        'created_at',
52
+        'updated_at'
53
+    ];
54
+    
55
+    public function segnalazione()
56
+    {
57
+        return $this->belongsTo('\App\Models\Segnalazione', 'segnalazione_id', 'id');
58
+    }
59
+}

+ 52
- 0
app/Models/Config.php Прегледај датотеку

@@ -21,6 +21,9 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
21 21
   const SEGNALAZIONE_CHIUSURA_AUTO_STATI = 'SEGNALAZIONE_CHIUSURA_AUTO_STATI';
22 22
   const SEGNALAZIONE_CHIUSURA_AUTO_GIORNI = 'SEGNALAZIONE_CHIUSURA_AUTO_GIORNI';
23 23
   const SEGNALAZIONE_CHIUSURA_AUTO_STATO = 'SEGNALAZIONE_CHIUSURA_AUTO_STATO';
24
+  const SEGNALAZIONE_EMAIL_TIPO_DEFAULT = 'SEGNALAZIONE_EMAIL_TIPO_DEFAULT';
25
+  const SEGNALAZIONE_EMAIL_RISPOSTA_AUTO_ATTIVA = 'SEGNALAZIONE_EMAIL_RISPOSTA_AUTO_ATTIVA';
26
+  const SEGNALAZIONE_EMAIL_RISPOSTA_AUTO_TESTO = 'SEGNALAZIONE_EMAIL_RISPOSTA_AUTO_TESTO';
24 27
 
25 28
   const FATTURAZIONE_TARIFFA_ORARIA = 'FATTURAZIONE_TARIFFA_ORARIA';
26 29
   const FATTURAZIONE_IVA_PERCENTUALE = 'FATTURAZIONE_IVA_PERCENTUALE';
@@ -32,6 +35,14 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
32 35
       ['key' => self::SEGNALAZIONE_CHIUSURA_AUTO_STATI, 'value' => json_encode(['in_attesa_cliente']), 'display_name' => 'Stati da chiudere dopo inattività', 'type' => 'stati'],
33 36
       ['key' => self::SEGNALAZIONE_CHIUSURA_AUTO_GIORNI, 'value' => null, 'display_name' => 'Giorni di inattività prima della chiusura automatica', 'type' => 'integer'],
34 37
       ['key' => self::SEGNALAZIONE_CHIUSURA_AUTO_STATO, 'value' => 'chiusa', 'display_name' => 'Stato assegnato dalla chiusura automatica', 'type' => 'stato'],
38
+      ['key' => self::SEGNALAZIONE_EMAIL_TIPO_DEFAULT, 'value' => 'ordinaria', 'display_name' => 'Tipo predefinito per segnalazioni da email', 'type' => 'tipo'],
39
+      ['key' => self::SEGNALAZIONE_EMAIL_RISPOSTA_AUTO_ATTIVA, 'value' => '1', 'display_name' => 'Risposta automatica a nuove segnalazioni da email', 'type' => 'boolean'],
40
+      [
41
+        'key' => self::SEGNALAZIONE_EMAIL_RISPOSTA_AUTO_TESTO,
42
+        'value' => self::defaultTestoRispostaAutoEmail(),
43
+        'display_name' => 'Testo risposta automatica email',
44
+        'type' => 'textarea',
45
+      ],
35 46
     ];
36 47
 
37 48
     $config['Documenti PDF'] = [
@@ -133,6 +144,47 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
133 144
     return $stato !== '' ? $stato : 'chiusa';
134 145
   }
135 146
 
147
+  public static function tipoSegnalazioneEmailDefault(): string
148
+  {
149
+    $tipo = trim((string) self::valueOf(self::SEGNALAZIONE_EMAIL_TIPO_DEFAULT, 'ordinaria'));
150
+
151
+    return $tipo !== '' ? $tipo : 'ordinaria';
152
+  }
153
+
154
+  public static function defaultTestoRispostaAutoEmail(): string
155
+  {
156
+    return "Gentile mittente,\n\n"
157
+      ."abbiamo ricevuto la sua segnalazione e le abbiamo assegnato il riferimento #{id}.\n"
158
+      ."Un nostro operatore la prenderà in carico al più presto.\n\n"
159
+      ."Non è necessario rispondere a questo messaggio.\n\n"
160
+      .'Cordiali saluti';
161
+  }
162
+
163
+  public static function rispostaAutoEmailAttiva(): bool
164
+  {
165
+    $value = self::valueOf(self::SEGNALAZIONE_EMAIL_RISPOSTA_AUTO_ATTIVA, '1');
166
+
167
+    return $value === true || $value === 1 || $value === '1' || $value === 'true';
168
+  }
169
+
170
+  public static function testoRispostaAutoEmail(Segnalazione $segnalazione): string
171
+  {
172
+    $template = trim((string) self::valueOf(
173
+      self::SEGNALAZIONE_EMAIL_RISPOSTA_AUTO_TESTO,
174
+      self::defaultTestoRispostaAutoEmail()
175
+    ));
176
+
177
+    if ($template === '') {
178
+      $template = self::defaultTestoRispostaAutoEmail();
179
+    }
180
+
181
+    return strtr($template, [
182
+      '{id}' => (string) $segnalazione->id,
183
+      '{titolo}' => (string) ($segnalazione->titolo ?? ''),
184
+      '{numero}' => '#'.$segnalazione->id,
185
+    ]);
186
+  }
187
+
136 188
   public static function messaggioChiusuraInattivita(?int $giorni = null): string
137 189
   {
138 190
     $giorni ??= self::chiusuraAutoGiorni() ?? 0;

+ 12
- 12
app/Models/ConfiguraSegnalazione.php Прегледај датотеку

@@ -3,17 +3,17 @@ namespace App\Models;
3 3
 
4 4
 class ConfiguraSegnalazione extends \App\Models\AbstractModels\AbstractConfiguraSegnalazione
5 5
 {
6
-    public function getCasts()
7
-    {
8
-        return array_merge(parent::getCasts(), [
9
-            'opzioni' => 'array',
10
-        ]);
11
-    }
6
+    // public function getCasts()
7
+    // {
8
+    //     return array_merge(parent::getCasts(), [
9
+    //         'opzioni' => 'array',
10
+    //     ]);
11
+    // }
12 12
 
13
-    public function getFillable()
14
-    {
15
-        return array_merge(parent::getFillable(), [
16
-            'opzioni',
17
-        ]);
18
-    }
13
+    // public function getFillable()
14
+    // {
15
+    //     return array_merge(parent::getFillable(), [
16
+    //         'opzioni',
17
+    //     ]);
18
+    // }
19 19
 }

+ 9
- 9
app/Models/ContrattoServizio.php Прегледај датотеку

@@ -87,15 +87,15 @@ class ContrattoServizio extends \App\Models\AbstractModels\AbstractContrattoServ
87 87
         return true;
88 88
     }
89 89
 
90
-    public function todos()
91
-    {
92
-        return $this->hasMany(ContrattoServizioTodo::class, 'contratto_servizio_id', 'id')->orderBy('ordine');
93
-    }
94
-
95
-    public function righe()
96
-    {
97
-        return $this->hasMany(ContrattoServizioRiga::class, 'contratto_servizio_id', 'id')->orderBy('ordine');
98
-    }
90
+    // public function todos()
91
+    // {
92
+    //     return $this->hasMany(ContrattoServizioTodo::class, 'contratto_servizio_id', 'id')->orderBy('ordine');
93
+    // }
94
+
95
+    // public function righe()
96
+    // {
97
+    //     return $this->hasMany(ContrattoServizioRiga::class, 'contratto_servizio_id', 'id')->orderBy('ordine');
98
+    // }
99 99
 
100 100
     public function getTotaleRigheAttribute(): float
101 101
     {

+ 26
- 26
app/Models/ContrattoServizioRiga.php Прегледај датотеку

@@ -10,32 +10,32 @@ class ContrattoServizioRiga extends \App\Models\AbstractModels\AbstractContratto
10 10
         'importo' => '€',
11 11
     ];
12 12
 
13
-    protected $fillable = [
14
-        'id',
15
-        'contratto_servizio_id',
16
-        'prodotto_id',
17
-        'quantita',
18
-        'unita_misura',
19
-        'prezzo_unitario',
20
-        'sconto_tipo',
21
-        'sconto_valore',
22
-        'periodo',
23
-        'data_inizio',
24
-        'data_fine',
25
-        'note',
26
-        'ordine',
27
-        'created_at',
28
-        'updated_at',
29
-    ];
30
-
31
-    protected function casts(): array
32
-    {
33
-        return [
34
-            'sconto_valore' => 'decimal:2',
35
-            'data_inizio' => 'date',
36
-            'data_fine' => 'date',
37
-        ];
38
-    }
13
+    // protected $fillable = [
14
+    //     'id',
15
+    //     'contratto_servizio_id',
16
+    //     'prodotto_id',
17
+    //     'quantita',
18
+    //     'unita_misura',
19
+    //     'prezzo_unitario',
20
+    //     'sconto_tipo',
21
+    //     'sconto_valore',
22
+    //     'periodo',
23
+    //     'data_inizio',
24
+    //     'data_fine',
25
+    //     'note',
26
+    //     'ordine',
27
+    //     'created_at',
28
+    //     'updated_at',
29
+    // ];
30
+
31
+    // protected function casts(): array
32
+    // {
33
+    //     return [
34
+    //         'sconto_valore' => 'decimal:2',
35
+    //         'data_inizio' => 'date',
36
+    //         'data_fine' => 'date',
37
+    //     ];
38
+    // }
39 39
 
40 40
     public function haValidita(): bool
41 41
     {

+ 8
- 8
app/Models/ContrattoTemplate.php Прегледај датотеку

@@ -11,15 +11,15 @@ class ContrattoTemplate extends \App\Models\AbstractModels\AbstractContrattoTemp
11 11
         'una_tantum' => 'Una tantum',
12 12
     ];
13 13
 
14
-    public function todos()
15
-    {
16
-        return $this->hasMany(ContrattoTemplateTodo::class, 'contratto_template_id', 'id')->orderBy('ordine');
17
-    }
14
+    // public function todos()
15
+    // {
16
+    //     return $this->hasMany(ContrattoTemplateTodo::class, 'contratto_template_id', 'id')->orderBy('ordine');
17
+    // }
18 18
 
19
-    public function righe()
20
-    {
21
-        return $this->hasMany(ContrattoTemplateRiga::class, 'contratto_template_id', 'id')->orderBy('ordine');
22
-    }
19
+    // public function righe()
20
+    // {
21
+    //     return $this->hasMany(ContrattoTemplateRiga::class, 'contratto_template_id', 'id')->orderBy('ordine');
22
+    // }
23 23
 
24 24
     public function getLabelAttribute(): string
25 25
     {

+ 4
- 4
app/Models/Fattura.php Прегледај датотеку

@@ -17,10 +17,10 @@ class Fattura extends \App\Models\AbstractModels\AbstractFattura
17 17
         self::STATO_ANNULLATA => 'Annullata',
18 18
     ];
19 19
 
20
-    public function righe()
21
-    {
22
-        return $this->hasMany(FatturaRiga::class, 'fattura_id', 'id')->orderBy('ordine')->orderBy('id');
23
-    }
20
+    // public function righe()
21
+    // {
22
+    //     return $this->hasMany(FatturaRiga::class, 'fattura_id', 'id')->orderBy('ordine')->orderBy('id');
23
+    // }
24 24
 
25 25
     public function getLabelAttribute(): string
26 26
     {

+ 4
- 4
app/Models/Offerta.php Прегледај датотеку

@@ -10,10 +10,10 @@ class Offerta extends \App\Models\AbstractModels\AbstractOfferta
10 10
         'rifiutata' => 'Rifiutata',
11 11
     ];
12 12
 
13
-    public function righe()
14
-    {
15
-        return $this->hasMany(OffertaRiga::class, 'offerta_id', 'id')->orderBy('ordine');
16
-    }
13
+    // public function righe()
14
+    // {
15
+    //     return $this->hasMany(OffertaRiga::class, 'offerta_id', 'id')->orderBy('ordine');
16
+    // }
17 17
 
18 18
     public function getLabelAttribute(): string
19 19
     {

+ 0
- 5
app/Models/OrdineLavoro.php Прегледај датотеку

@@ -32,11 +32,6 @@ class OrdineLavoro extends \App\Models\AbstractModels\AbstractOrdineLavoro
32 32
         return (int) (static::query()->max('numero') ?? 0) + 1;
33 33
     }
34 34
 
35
-    public function righe()
36
-    {
37
-        return $this->hasMany(OrdineLavoroRiga::class, 'ordine_lavoro_id', 'id')->orderBy('ordine')->orderBy('id');
38
-    }
39
-
40 35
     public function getTempoTotaleMinutiAttribute(): int
41 36
     {
42 37
         return (int) $this->segnalazioni->sum(fn (Segnalazione $s) => $s->getTempoImpiegato(false));

+ 2
- 50
app/Models/OrdineLavoroRiga.php Прегледај датотеку

@@ -1,57 +1,9 @@
1 1
 <?php
2
-
3 2
 namespace App\Models;
4 3
 
5
-use Illuminate\Database\Eloquent\Model;
6
-
7
-class OrdineLavoroRiga extends Model
4
+class OrdineLavoroRiga extends \App\Models\AbstractModels\AbstractOrdineLavoroRiga
8 5
 {
9
-    protected $table = 'ordine_lavoro_riga';
10
-
11
-    protected $keyType = 'bigInteger';
12
-
13
-    protected $attributes = ['ordine' => 0];
14
-
15
-    protected $casts = [
16
-        'id' => 'integer',
17
-        'ordine_lavoro_id' => 'integer',
18
-        'prodotto_id' => 'integer',
19
-        'descrizione' => 'string',
20
-        'quantita' => 'decimal:2',
21
-        'unita_misura' => 'string',
22
-        'prezzo_unitario' => 'decimal:2',
23
-        'sconto' => 'decimal:2',
24
-        'ordine' => 'integer',
25
-        'note_private' => 'string',
26
-        'tempo_stimato' => 'decimal:2',
27
-        'created_at' => 'datetime',
28
-        'updated_at' => 'datetime',
29
-    ];
30
-
31
-    protected $fillable = [
32
-        'ordine_lavoro_id',
33
-        'prodotto_id',
34
-        'descrizione',
35
-        'quantita',
36
-        'unita_misura',
37
-        'prezzo_unitario',
38
-        'sconto',
39
-        'ordine',
40
-        'note_private',
41
-        'tempo_stimato',
42
-    ];
43
-
44
-    public function ordineLavoro()
45
-    {
46
-        return $this->belongsTo(OrdineLavoro::class, 'ordine_lavoro_id', 'id');
47
-    }
48
-
49
-    public function prodotto()
50
-    {
51
-        return $this->belongsTo(Prodotto::class, 'prodotto_id', 'id');
52
-    }
53
-
54
-    public function getUnitaMisuraLabelAttribute(): string
6
+  public function getUnitaMisuraLabelAttribute(): string
55 7
     {
56 8
         return Prodotto::UNITA_MISURA[$this->unita_misura] ?? ($this->unita_misura ?: '—');
57 9
     }

+ 0
- 5
app/Models/Prodotto.php Прегледај датотеку

@@ -49,11 +49,6 @@ class Prodotto extends \App\Models\AbstractModels\AbstractProdotto
49 49
         return self::UNITA_MISURA[$this->unita_misura] ?? ($this->unita_misura ?: '—');
50 50
     }
51 51
 
52
-    public function todos()
53
-    {
54
-        return $this->hasMany(ProdottoTodo::class, 'prodotto_id', 'id')->orderBy('ordine')->orderBy('id');
55
-    }
56
-
57 52
     /**
58 53
      * Clona i todo attivi del prodotto sulla segnalazione (idempotente se già presenti).
59 54
      */

+ 70
- 25
app/Models/Segnalazione.php Прегледај датотеку

@@ -14,31 +14,36 @@ class Segnalazione extends \App\Models\AbstractModels\AbstractSegnalazione imple
14 14
   /** Flag runtime: notifica l'utente "Aperta da" (non persistito). */
15 15
   public bool $notificaApertaDa = false;
16 16
 
17
-  public function getCasts()
18
-  {
19
-    return array_merge(parent::getCasts(), [
20
-      'categoria_sicurezza' => 'array',
21
-      'prodotto_id' => 'integer',
22
-    ]);
23
-  }
24
-
25
-  public function getFillable()
26
-  {
27
-    return array_merge(parent::getFillable(), [
28
-      'categoria_sicurezza',
29
-      'prodotto_id',
30
-    ]);
31
-  }
32
-
33
-  public function prodotto()
34
-  {
35
-    return $this->belongsTo(Prodotto::class, 'prodotto_id', 'id');
36
-  }
37
-
38
-  public function todos()
39
-  {
40
-    return $this->hasMany(SegnalazioneTodo::class, 'segnalazione_id', 'id')->orderBy('ordine')->orderBy('id');
41
-  }
17
+  // public function getCasts()
18
+  // {
19
+  //   return array_merge(parent::getCasts(), [
20
+  //     'categoria_sicurezza' => 'array',
21
+  //     'prodotto_id' => 'integer',
22
+  //   ]);
23
+  // }
24
+
25
+  // public function getFillable()
26
+  // {
27
+  //   return array_merge(parent::getFillable(), [
28
+  //     'categoria_sicurezza',
29
+  //     'prodotto_id',
30
+  //   ]);
31
+  // }
32
+
33
+  // public function prodotto()
34
+  // {
35
+  //   return $this->belongsTo(Prodotto::class, 'prodotto_id', 'id');
36
+  // }
37
+
38
+  // public function todos()
39
+  // {
40
+  //   return $this->hasMany(SegnalazioneTodo::class, 'segnalazione_id', 'id')->orderBy('ordine')->orderBy('id');
41
+  // }
42
+
43
+  // public function slots()
44
+  // {
45
+  //   return $this->hasMany(SegnalazioneSlot::class, 'segnalazione_id', 'id')->orderBy('starts_at');
46
+  // }
42 47
 
43 48
   public function getStati(){
44 49
     return \App\Models\ConfiguraSegnalazione::where('gruppo', 'stato')
@@ -117,6 +122,38 @@ class Segnalazione extends \App\Models\AbstractModels\AbstractSegnalazione imple
117 122
         return "#".$this->id." - ".$this->titolo;
118 123
     }
119 124
 
125
+    /**
126
+     * Timer tempo attivi (start) per questa segnalazione.
127
+     *
128
+     * @return array<int, array{user_id:int, started_at:int}>
129
+     */
130
+    public function getActiveTempoTimers(): array
131
+    {
132
+        $timers = data_get($this->meta ?? [], 'tempo_timer', []);
133
+        if (! is_array($timers) || $timers === []) {
134
+            return [];
135
+        }
136
+
137
+        $active = [];
138
+        foreach ($timers as $key => $timer) {
139
+            $startedAt = (int) data_get($timer, 'started_at', 0);
140
+            $userId = (int) data_get($timer, 'user_id', $key);
141
+            if ($userId > 0 && $startedAt > 0) {
142
+                $active[] = [
143
+                    'user_id' => $userId,
144
+                    'started_at' => $startedAt,
145
+                ];
146
+            }
147
+        }
148
+
149
+        return $active;
150
+    }
151
+
152
+    public function hasActiveTempoTimer(): bool
153
+    {
154
+        return $this->getActiveTempoTimers() !== [];
155
+    }
156
+
120 157
     /**
121 158
      * Subject email stabile per threading client ([#ID] Oggetto).
122 159
      * Per aggiornamenti/risposte antepone "Re: " (Gmail e simili raggruppano meglio).
@@ -133,6 +170,14 @@ class Segnalazione extends \App\Models\AbstractModels\AbstractSegnalazione imple
133 170
       return "<a href='".route('admin.segnalazione.show', ['id' => $this->id])."'>".$this->getLabel()."</a>";
134 171
     }
135 172
 
173
+    /**
174
+     * URL area cliente / consultazione pubblica via token.
175
+     */
176
+    public function urlCliente(): string
177
+    {
178
+        return route('segnalazione.show', ['token' => $this->token]);
179
+    }
180
+
136 181
     /**
137 182
      * ID utenti assegnatari visibili con solo il permesso view-segnalazione-assegnate (self + account gruppo di cui si fa parte).
138 183
      *

+ 30
- 0
app/Models/SegnalazioneSlot.php Прегледај датотеку

@@ -0,0 +1,30 @@
1
+<?php
2
+namespace App\Models;
3
+
4
+class SegnalazioneSlot extends \App\Models\AbstractModels\AbstractSegnalazioneSlot
5
+{
6
+  /** Durata in ore decimali. */
7
+    public function durataOre(): float
8
+    {
9
+        if (! $this->starts_at || ! $this->ends_at) {
10
+            return 0.0;
11
+        }
12
+
13
+        $minuti = $this->starts_at->diffInMinutes($this->ends_at, false);
14
+        if ($minuti <= 0) {
15
+            return 0.0;
16
+        }
17
+
18
+        return round($minuti / 60, 2);
19
+    }
20
+
21
+    public function label(): string
22
+    {
23
+        /** @var Carbon $start */
24
+        $start = $this->starts_at;
25
+        /** @var Carbon $end */
26
+        $end = $this->ends_at;
27
+
28
+        return $start->format('d/m/Y H:i').' → '.$end->format('H:i');
29
+    }
30
+}

+ 55
- 0
app/Notifications/RispostaAutoSegnalazioneEmail.php Прегледај датотеку

@@ -0,0 +1,55 @@
1
+<?php
2
+
3
+namespace App\Notifications;
4
+
5
+use App\Models\Config;
6
+use App\Models\Segnalazione;
7
+use Illuminate\Bus\Queueable;
8
+use Illuminate\Contracts\Queue\ShouldQueue;
9
+use Illuminate\Notifications\Messages\MailMessage;
10
+use Illuminate\Notifications\Notification;
11
+use Illuminate\Queue\InteractsWithQueue;
12
+
13
+class RispostaAutoSegnalazioneEmail extends Notification implements ShouldQueue
14
+{
15
+    use Queueable, InteractsWithQueue;
16
+
17
+    public function __construct(
18
+        protected Segnalazione $segnalazione,
19
+    ) {
20
+    }
21
+
22
+    /**
23
+     * @return array<int, string>
24
+     */
25
+    public function via(object $notifiable): array
26
+    {
27
+        return ['mail'];
28
+    }
29
+
30
+    public function toMail(object $notifiable): MailMessage
31
+    {
32
+        $body = Config::testoRispostaAutoEmail($this->segnalazione);
33
+        $paragraphs = preg_split("/\R{2,}/", trim($body)) ?: [];
34
+
35
+        return (new MailMessage)
36
+            ->subject($this->segnalazione->mailSubject(true))
37
+            ->markdown('mail.risposta-auto-segnalazione-email', [
38
+                'segnalazione' => $this->segnalazione,
39
+                'paragraphs' => array_values(array_filter(array_map(
40
+                    static fn ($p) => trim((string) $p),
41
+                    $paragraphs
42
+                ), static fn ($p) => $p !== '')),
43
+            ]);
44
+    }
45
+
46
+    /**
47
+     * @return array<string, mixed>
48
+     */
49
+    public function toArray(object $notifiable): array
50
+    {
51
+        return [
52
+            'segnalazione_id' => $this->segnalazione->id,
53
+        ];
54
+    }
55
+}

+ 31
- 18
app/Services/SegnalazioneEmailInboxService.php Прегледај датотеку

@@ -6,11 +6,10 @@ use App\Models\AllegatoAnnotazione;
6 6
 use App\Models\Annotazione;
7 7
 use App\Models\CasellaImap;
8 8
 use App\Models\Config;
9
-use App\Models\ConfigNotifiche;
10 9
 use App\Models\ContrattoServizio;
11 10
 use App\Models\Segnalazione;
12 11
 use App\Models\User;
13
-use App\Notifications\NuovaSegnalazioneUser;
12
+use App\Notifications\RispostaAutoSegnalazioneEmail;
14 13
 use App\Notifications\RispostaEmailAssegnatario;
15 14
 use Illuminate\Support\Facades\Log;
16 15
 use Illuminate\Support\Facades\Notification;
@@ -262,6 +261,8 @@ class SegnalazioneEmailInboxService
262 261
                 $segnalazione->save();
263 262
             }
264 263
             $this->notifyAssegnatarioRispostaEmail($segnalazione, $annotazione, $fromAddress);
264
+        } else {
265
+            $this->sendRispostaAutomaticaNuovaSegnalazione($segnalazione, $message, $fromAddress);
265 266
         }
266 267
 
267 268
         if ($messageId) {
@@ -303,6 +304,30 @@ class SegnalazioneEmailInboxService
303 304
         );
304 305
     }
305 306
 
307
+    protected function sendRispostaAutomaticaNuovaSegnalazione(
308
+        Segnalazione $segnalazione,
309
+        Message $message,
310
+        ?string $fromAddress,
311
+    ): void {
312
+        if (! Config::rispostaAutoEmailAttiva() || ! $fromAddress) {
313
+            return;
314
+        }
315
+
316
+        $fromName = $this->extractFromName($message) ?: $fromAddress;
317
+
318
+        try {
319
+            Notification::route('mail', [$fromAddress => $fromName])
320
+                ->notify(new RispostaAutoSegnalazioneEmail($segnalazione->fresh()));
321
+        } catch (\Throwable $e) {
322
+            report($e);
323
+            Log::warning('Risposta automatica email segnalazione non inviata', [
324
+                'segnalazione_id' => $segnalazione->id,
325
+                'to' => $fromAddress,
326
+                'error' => $e->getMessage(),
327
+            ]);
328
+        }
329
+    }
330
+
306 331
     protected function createSegnalazioneFromMessage(Message $message, string $subject, ?string $fromAddress): Segnalazione
307 332
     {
308 333
         $titolo = $this->cleanSubjectAsTitle($subject);
@@ -320,32 +345,20 @@ class SegnalazioneEmailInboxService
320 345
             ? ContrattoServizio::defaultImportEmailForAzienda((int) $azienda->id)
321 346
             : null;
322 347
 
323
-        $configConferma = ConfigNotifiche::where('event', ConfigNotifiche::SEGNALAZIONE_EMAIL_CONFERMA_APERTA_DA)->first();
324
-        $confermaApertaDa = $configConferma && $configConferma->is_attivo;
325
-
326 348
         $segnalazione = new Segnalazione();
327 349
         $segnalazione->titolo = Str::limit($titolo, 250);
328 350
         $segnalazione->stato = Config::getStatoSegnalazioneDefault();
329 351
         $segnalazione->priorita = Config::getPrioritaSegnalazioneDefault();
330
-        $segnalazione->tipo = 'ordinaria';
352
+        $segnalazione->tipo = Config::tipoSegnalazioneEmailDefault();
331 353
         $segnalazione->origine = 'email';
332 354
         $segnalazione->user_id = $user?->id;
333 355
         $segnalazione->azienda_id = $azienda?->id ?? $user?->azienda_id;
334 356
         $segnalazione->contratto_servizio_id = $contrattoDefault?->id;
335
-        // Se mittente è utente noto: la notifica parte dal listener NuovaSegnalazione
336
-        $segnalazione->notificaApertaDa = $confermaApertaDa && $user !== null;
357
+        // Conferma al mittente: risposta automatica dedicata (non NuovaSegnalazioneUser)
358
+        $segnalazione->notificaApertaDa = false;
337 359
         $segnalazione->save();
338 360
 
339
-        $fresh = $segnalazione->fresh(['user', 'azienda']);
340
-
341
-        // Mittente non registrato: invia comunque conferma all'indirizzo email
342
-        if ($confermaApertaDa && ! $user && $fromAddress) {
343
-            $fromName = $this->extractFromName($message) ?: $fromAddress;
344
-            Notification::route('mail', [$fromAddress => $fromName])
345
-                ->notify(new NuovaSegnalazioneUser($fresh, false, true));
346
-        }
347
-
348
-        return $fresh;
361
+        return $segnalazione->fresh(['user', 'azienda']);
349 362
     }
350 363
 
351 364
     protected function createAnnotazioneFromMessage(

+ 0
- 49
database/migrations/2026_08_22_000100_create_ordine_lavoro_riga_table.php Прегледај датотеку

@@ -1,49 +0,0 @@
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
-    public function up(): void
10
-    {
11
-        Schema::create('ordine_lavoro_riga', function (Blueprint $table) {
12
-            $table->bigInteger('id')->autoIncrement()->unsigned();
13
-            $table->bigInteger('ordine_lavoro_id')->nullable(true)->unsigned();
14
-            $table->bigInteger('prodotto_id')->nullable(true)->unsigned();
15
-            $table->string('descrizione')->nullable(true);
16
-            $table->decimal('quantita', 10, 2)->nullable(true);
17
-            $table->string('unita_misura')->nullable(true);
18
-            $table->decimal('prezzo_unitario', 10, 2)->nullable(true);
19
-            $table->decimal('sconto', 10, 2)->nullable(true);
20
-            $table->integer('ordine')->nullable(true)->default(0);
21
-            $table->text('note_private')->nullable(true);
22
-            $table->decimal('tempo_stimato', 10, 2)->nullable(true);
23
-            $table->timestamp('created_at')->nullable(true);
24
-            $table->timestamp('updated_at')->nullable(true);
25
-        });
26
-
27
-        Schema::table('ordine_lavoro_riga', function (Blueprint $table) {
28
-            $table->foreign('ordine_lavoro_id')->references('id')->on('ordine_lavoro')->onDelete('cascade');
29
-        });
30
-        Schema::table('ordine_lavoro_riga', function (Blueprint $table) {
31
-            $table->foreign('prodotto_id')->references('id')->on('prodotto')->onDelete('set null');
32
-        });
33
-    }
34
-
35
-    public function down(): void
36
-    {
37
-        if (Schema::hasTable('ordine_lavoro_riga')) {
38
-            Schema::table('ordine_lavoro_riga', function (Blueprint $table) {
39
-                $table->dropForeign(['prodotto_id']);
40
-            });
41
-        }
42
-        if (Schema::hasTable('ordine_lavoro_riga')) {
43
-            Schema::table('ordine_lavoro_riga', function (Blueprint $table) {
44
-                $table->dropForeign(['ordine_lavoro_id']);
45
-            });
46
-        }
47
-        Schema::dropIfExists('ordine_lavoro_riga');
48
-    }
49
-};

database/migrations/2026_08_21_234348_skipper_migrations_2026082123434842.php → database/migrations/2026_08_26_152346_skipper_migrations_2026082615234643.php Прегледај датотеку

@@ -1,15 +1,15 @@
1 1
 <?php
2 2
 /* 
3 3
  * Migrations generated by: Skipper (http://www.skipper18.com)
4
- * Migration id: 524213bb-470e-481b-a960-f6096b7d4436
5
- * Migration datetime: 2026-08-21 23:43:48.425051
4
+ * Migration id: 3404b97d-dc5c-4338-b8dd-c14acb80c725
5
+ * Migration datetime: 2026-08-26 15:23:46.436721
6 6
  */ 
7 7
 
8 8
 use Illuminate\Support\Facades\Schema;
9 9
 use Illuminate\Database\Schema\Blueprint;
10 10
 use Illuminate\Database\Migrations\Migration;
11 11
 
12
-class SkipperMigrations2026082123434842 extends Migration
12
+class SkipperMigrations2026082615234643 extends Migration
13 13
 {
14 14
     /**
15 15
      * Run the migrations.
@@ -70,6 +70,14 @@ class SkipperMigrations2026082123434842 extends Migration
70 70
             $table->timestamp('created_at')->nullable(true);
71 71
             $table->timestamp('updated_at')->nullable(true);
72 72
         });
73
+        Schema::create('segnalazione_slot', function (Blueprint $table) {
74
+            $table->bigInteger('id')->autoIncrement()->unsigned();
75
+            $table->bigInteger('segnalazione_id')->nullable(true)->unsigned();
76
+            $table->dateTime('starts_at')->nullable(true);
77
+            $table->dateTime('ends_at')->nullable(true);
78
+            $table->timestamp('created_at')->nullable(true);
79
+            $table->timestamp('updated_at')->nullable(true);
80
+        });
73 81
         Schema::table('fattura', function (Blueprint $table) {
74 82
             $table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
75 83
         });
@@ -100,6 +108,9 @@ class SkipperMigrations2026082123434842 extends Migration
100 108
         Schema::table('fattura_riga', function (Blueprint $table) {
101 109
             $table->foreign('prodotto_id')->references('id')->on('prodotto')->onDelete('set null');
102 110
         });
111
+        Schema::table('segnalazione_slot', function (Blueprint $table) {
112
+            $table->foreign('segnalazione_id')->references('id')->on('segnalazione')->onDelete('cascade');
113
+        });
103 114
     }
104 115
     /**
105 116
      * Reverse the migrations.
@@ -108,6 +119,11 @@ class SkipperMigrations2026082123434842 extends Migration
108 119
      */
109 120
     public function down()
110 121
     {
122
+        if (Schema::hasTable('segnalazione_slot')) {
123
+            Schema::table('segnalazione_slot', function (Blueprint $table) {
124
+                $table->dropForeign(['segnalazione_id']);
125
+            });
126
+        }
111 127
         if (Schema::hasTable('fattura_riga')) {
112 128
             Schema::table('fattura_riga', function (Blueprint $table) {
113 129
                 $table->dropForeign(['prodotto_id']);
@@ -158,6 +174,7 @@ class SkipperMigrations2026082123434842 extends Migration
158 174
                 $table->dropForeign(['user_id']);
159 175
             });
160 176
         }
177
+        Schema::dropIfExists('segnalazione_slot');
161 178
         Schema::dropIfExists('fattura_riga');
162 179
         Schema::dropIfExists('fattura');
163 180
     }

+ 206
- 0
database/migrations/2026_08_26_153903_skipper_migrations_2026082615390373.php Прегледај датотеку

@@ -0,0 +1,206 @@
1
+<?php
2
+/* 
3
+ * Migrations generated by: Skipper (http://www.skipper18.com)
4
+ * Migration id: 44d9ba7e-a043-4486-b25f-39fad8acf135
5
+ * Migration datetime: 2026-08-26 15:39:03.736234
6
+ */ 
7
+
8
+use Illuminate\Support\Facades\Schema;
9
+use Illuminate\Database\Schema\Blueprint;
10
+use Illuminate\Database\Migrations\Migration;
11
+
12
+class SkipperMigrations2026082615390373 extends Migration
13
+{
14
+    /**
15
+     * Run the migrations.
16
+     *
17
+     * @return void
18
+     */
19
+    public function up()
20
+    {
21
+        if (Schema::hasTable('offerta_riga')) {
22
+            Schema::table('offerta_riga', function (Blueprint $table) {
23
+                $table->dropForeign(['offerta_id']);
24
+            });
25
+        }
26
+        if (Schema::hasTable('contratto_template_todo')) {
27
+            Schema::table('contratto_template_todo', function (Blueprint $table) {
28
+                $table->dropForeign(['contratto_template_id']);
29
+            });
30
+        }
31
+        if (Schema::hasTable('contratto_servizio_todo')) {
32
+            Schema::table('contratto_servizio_todo', function (Blueprint $table) {
33
+                $table->dropForeign(['contratto_servizio_id']);
34
+            });
35
+        }
36
+        if (Schema::hasTable('contratto_template_riga')) {
37
+            Schema::table('contratto_template_riga', function (Blueprint $table) {
38
+                $table->dropForeign(['contratto_template_id']);
39
+            });
40
+        }
41
+        if (Schema::hasTable('contratto_servizio_riga')) {
42
+            Schema::table('contratto_servizio_riga', function (Blueprint $table) {
43
+                $table->dropForeign(['contratto_servizio_id']);
44
+            });
45
+        }
46
+        if (Schema::hasTable('prodotto_todo')) {
47
+            Schema::table('prodotto_todo', function (Blueprint $table) {
48
+                $table->dropForeign(['prodotto_id']);
49
+            });
50
+        }
51
+        if (Schema::hasTable('segnalazione_todo')) {
52
+            Schema::table('segnalazione_todo', function (Blueprint $table) {
53
+                $table->dropForeign(['segnalazione_id']);
54
+            });
55
+        }
56
+        if (Schema::hasTable('fattura_riga')) {
57
+            Schema::table('fattura_riga', function (Blueprint $table) {
58
+                $table->dropForeign(['fattura_id']);
59
+            });
60
+        }
61
+        if (Schema::hasTable('segnalazione_slot')) {
62
+            Schema::table('segnalazione_slot', function (Blueprint $table) {
63
+                $table->dropForeign(['segnalazione_id']);
64
+            });
65
+        }
66
+        Schema::create('ordine_lavoro_riga', function (Blueprint $table) {
67
+            $table->bigInteger('id')->autoIncrement()->unsigned();
68
+            $table->bigInteger('ordine_lavoro_id')->nullable(true)->unsigned();
69
+            $table->bigInteger('prodotto_id')->nullable(true)->unsigned();
70
+            $table->string('descrizione')->nullable(true);
71
+            $table->decimal('quantita', 10, 2)->nullable(true);
72
+            $table->string('unita_misura')->nullable(true);
73
+            $table->decimal('prezzo_unitario', 10, 2)->nullable(true);
74
+            $table->decimal('sconto', 10, 2)->nullable(true);
75
+            $table->integer('ordine')->nullable(true)->default(0);
76
+            $table->text('note_private')->nullable(true);
77
+            $table->decimal('tempo_stimato', 10, 2)->nullable(true);
78
+            $table->timestamp('created_at')->nullable(true);
79
+            $table->timestamp('updated_at')->nullable(true);
80
+        });
81
+        Schema::table('offerta_riga', function (Blueprint $table) {
82
+            $table->foreign('offerta_id')->references('id')->on('offerta')->onDelete('set null');
83
+        });
84
+        Schema::table('contratto_template_todo', function (Blueprint $table) {
85
+            $table->foreign('contratto_template_id')->references('id')->on('contratto_template')->onDelete('cascade');
86
+        });
87
+        Schema::table('contratto_servizio_todo', function (Blueprint $table) {
88
+            $table->foreign('contratto_servizio_id')->references('id')->on('contratto_servizio')->onDelete('cascade');
89
+        });
90
+        Schema::table('contratto_template_riga', function (Blueprint $table) {
91
+            $table->foreign('contratto_template_id')->references('id')->on('contratto_template')->onDelete('cascade');
92
+        });
93
+        Schema::table('contratto_servizio_riga', function (Blueprint $table) {
94
+            $table->foreign('contratto_servizio_id')->references('id')->on('contratto_servizio')->onDelete('cascade');
95
+        });
96
+        Schema::table('prodotto_todo', function (Blueprint $table) {
97
+            $table->foreign('prodotto_id')->references('id')->on('prodotto')->onDelete('cascade');
98
+        });
99
+        Schema::table('segnalazione_todo', function (Blueprint $table) {
100
+            $table->foreign('segnalazione_id')->references('id')->on('segnalazione')->onDelete('cascade');
101
+        });
102
+        Schema::table('fattura_riga', function (Blueprint $table) {
103
+            $table->foreign('fattura_id')->references('id')->on('fattura')->onDelete('cascade');
104
+        });
105
+        Schema::table('segnalazione_slot', function (Blueprint $table) {
106
+            $table->foreign('segnalazione_id')->references('id')->on('segnalazione')->onDelete('cascade');
107
+        });
108
+        Schema::table('ordine_lavoro_riga', function (Blueprint $table) {
109
+            $table->foreign('ordine_lavoro_id')->references('id')->on('ordine_lavoro')->onDelete('cascade');
110
+        });
111
+        Schema::table('ordine_lavoro_riga', function (Blueprint $table) {
112
+            $table->foreign('prodotto_id')->references('id')->on('prodotto')->onDelete('set null');
113
+        });
114
+    }
115
+    /**
116
+     * Reverse the migrations.
117
+     *
118
+     * @return void
119
+     */
120
+    public function down()
121
+    {
122
+        if (Schema::hasTable('ordine_lavoro_riga')) {
123
+            Schema::table('ordine_lavoro_riga', function (Blueprint $table) {
124
+                $table->dropForeign(['prodotto_id']);
125
+            });
126
+        }
127
+        if (Schema::hasTable('ordine_lavoro_riga')) {
128
+            Schema::table('ordine_lavoro_riga', function (Blueprint $table) {
129
+                $table->dropForeign(['ordine_lavoro_id']);
130
+            });
131
+        }
132
+        if (Schema::hasTable('segnalazione_slot')) {
133
+            Schema::table('segnalazione_slot', function (Blueprint $table) {
134
+                $table->dropForeign(['segnalazione_id']);
135
+            });
136
+        }
137
+        if (Schema::hasTable('fattura_riga')) {
138
+            Schema::table('fattura_riga', function (Blueprint $table) {
139
+                $table->dropForeign(['fattura_id']);
140
+            });
141
+        }
142
+        if (Schema::hasTable('segnalazione_todo')) {
143
+            Schema::table('segnalazione_todo', function (Blueprint $table) {
144
+                $table->dropForeign(['segnalazione_id']);
145
+            });
146
+        }
147
+        if (Schema::hasTable('prodotto_todo')) {
148
+            Schema::table('prodotto_todo', function (Blueprint $table) {
149
+                $table->dropForeign(['prodotto_id']);
150
+            });
151
+        }
152
+        if (Schema::hasTable('contratto_servizio_riga')) {
153
+            Schema::table('contratto_servizio_riga', function (Blueprint $table) {
154
+                $table->dropForeign(['contratto_servizio_id']);
155
+            });
156
+        }
157
+        if (Schema::hasTable('contratto_template_riga')) {
158
+            Schema::table('contratto_template_riga', function (Blueprint $table) {
159
+                $table->dropForeign(['contratto_template_id']);
160
+            });
161
+        }
162
+        if (Schema::hasTable('contratto_servizio_todo')) {
163
+            Schema::table('contratto_servizio_todo', function (Blueprint $table) {
164
+                $table->dropForeign(['contratto_servizio_id']);
165
+            });
166
+        }
167
+        if (Schema::hasTable('contratto_template_todo')) {
168
+            Schema::table('contratto_template_todo', function (Blueprint $table) {
169
+                $table->dropForeign(['contratto_template_id']);
170
+            });
171
+        }
172
+        if (Schema::hasTable('offerta_riga')) {
173
+            Schema::table('offerta_riga', function (Blueprint $table) {
174
+                $table->dropForeign(['offerta_id']);
175
+            });
176
+        }
177
+        Schema::dropIfExists('ordine_lavoro_riga');
178
+        Schema::table('segnalazione_slot', function (Blueprint $table) {
179
+            $table->foreign('segnalazione_id')->references('id')->on('segnalazione')->onDelete('cascade');
180
+        });
181
+        Schema::table('fattura_riga', function (Blueprint $table) {
182
+            $table->foreign('fattura_id')->references('id')->on('fattura')->onDelete('cascade');
183
+        });
184
+        Schema::table('segnalazione_todo', function (Blueprint $table) {
185
+            $table->foreign('segnalazione_id')->references('id')->on('segnalazione')->onDelete('cascade');
186
+        });
187
+        Schema::table('prodotto_todo', function (Blueprint $table) {
188
+            $table->foreign('prodotto_id')->references('id')->on('prodotto')->onDelete('cascade');
189
+        });
190
+        Schema::table('contratto_servizio_riga', function (Blueprint $table) {
191
+            $table->foreign('contratto_servizio_id')->references('id')->on('contratto_servizio')->onDelete('cascade');
192
+        });
193
+        Schema::table('contratto_template_riga', function (Blueprint $table) {
194
+            $table->foreign('contratto_template_id')->references('id')->on('contratto_template')->onDelete('cascade');
195
+        });
196
+        Schema::table('contratto_servizio_todo', function (Blueprint $table) {
197
+            $table->foreign('contratto_servizio_id')->references('id')->on('contratto_servizio')->onDelete('cascade');
198
+        });
199
+        Schema::table('contratto_template_todo', function (Blueprint $table) {
200
+            $table->foreign('contratto_template_id')->references('id')->on('contratto_template')->onDelete('cascade');
201
+        });
202
+        Schema::table('offerta_riga', function (Blueprint $table) {
203
+            $table->foreign('offerta_id')->references('id')->on('offerta')->onDelete('set null');
204
+        });
205
+    }
206
+}

+ 7
- 0
resources/menu/verticalMenu.json Прегледај датотеку

@@ -23,6 +23,13 @@
23 23
       "url": "admin/dashboard",
24 24
       "can": "view-admin_dashboard"
25 25
     },
26
+    {
27
+      "name": "Statistiche",
28
+      "icon": "menu-icon icon-base bx bx-bar-chart-alt-2",
29
+      "slug": "admin.statistiche",
30
+      "url": "admin/statistiche",
31
+      "can": "view-admin_dashboard"
32
+    },
26 33
     {
27 34
       "name": "Segnalazioni",
28 35
       "icon": "menu-icon icon-base bx bx-flag",

+ 20
- 0
resources/views/config/index.blade.php Прегледај датотеку

@@ -93,10 +93,16 @@ $groupKeys = array_keys($groups);
93 93
                   <label class="form-check-label" for="{{ $fid }}-no">No</label>
94 94
                 </div>
95 95
               </div>
96
+              @if($config->key === Config::SEGNALAZIONE_EMAIL_RISPOSTA_AUTO_ATTIVA)
97
+              <div class="form-text mt-2">Se attiva, risponde automaticamente al mittente quando viene aperta una nuova segnalazione da email (non per le risposte a ticket già esistenti).</div>
98
+              @endif
96 99
               @break
97 100
 
98 101
               @case('textarea')
99 102
               <textarea class="form-control" name="value[{{ $config->key }}]" id="{{ $fid }}-control" rows="6" {{ $disabled }}>{{ $config->value }}</textarea>
103
+              @if($config->key === Config::SEGNALAZIONE_EMAIL_RISPOSTA_AUTO_TESTO)
104
+              <div class="form-text">Segnaposto disponibili: <code>{id}</code>, <code>{numero}</code>, <code>{titolo}</code>.</div>
105
+              @endif
100 106
               @break
101 107
 
102 108
               @case('integer')
@@ -132,6 +138,20 @@ $groupKeys = array_keys($groups);
132 138
               </select>
133 139
               @break
134 140
 
141
+              @case('tipo')
142
+              <select class="form-select" name="value[{{ $config->key }}]" id="{{ $fid }}-control" {{ $disabled }}>
143
+                <option value="">— Seleziona —</option>
144
+                @foreach($tipiSegnalazione as $tipo)
145
+                <option value="{{ $tipo->value }}" @selected((string) $config->value === (string) $tipo->value)>
146
+                  {{ $tipo->label }}
147
+                </option>
148
+                @endforeach
149
+              </select>
150
+              @if($config->key === Config::SEGNALAZIONE_EMAIL_TIPO_DEFAULT)
151
+              <div class="form-text">Usato quando una nuova segnalazione viene creata automaticamente dall'import email.</div>
152
+              @endif
153
+              @break
154
+
135 155
               @case('stati')
136 156
               @php $selectedStati = Config::decodeList($config->value); @endphp
137 157
               <div class="d-flex flex-column gap-2" id="{{ $fid }}-control">

+ 221
- 0
resources/views/dashboard/dashboards-manutenzioni-statistiche.blade.php Прегледај датотеку

@@ -0,0 +1,221 @@
1
+@php
2
+$configData = Helper::appClasses();
3
+@endphp
4
+
5
+@extends('layouts/layoutMaster')
6
+
7
+@section('title', 'Statistiche - Manutenzioni')
8
+
9
+@section('vendor-style')
10
+@vite(['resources/assets/vendor/libs/apex-charts/apex-charts.scss', 'resources/assets/vendor/fonts/flag-icons.scss'])
11
+@endsection
12
+
13
+@section('vendor-script')
14
+@vite('resources/assets/vendor/libs/apex-charts/apexcharts.js')
15
+@endsection
16
+
17
+@section('page-script')
18
+<script>
19
+  window.dashboardData = {
20
+    labelsTempo: @json($labelsTempo),
21
+    datiTempo: @json($datiTempo),
22
+    labelsStato: @json($labelsStato),
23
+    datiStato: @json($datiStato),
24
+    labelsPriorita: @json($labelsPriorita),
25
+    datiPriorita: @json($datiPriorita),
26
+    labelsAzienda: @json($labelsAzienda),
27
+    datiAzienda: @json($datiAzienda),
28
+    labelsManutentori: @json($labelsManutentori),
29
+    datiManutentori: @json($datiManutentori),
30
+    labelsCostiAzienda: @json($labelsCostiAzienda),
31
+    datiCostiAzienda: @json($datiCostiAzienda),
32
+    labelsTipo: @json($labelsTipo),
33
+    datiTipo: @json($datiTipo),
34
+    labelsCostiTipo: @json($labelsCostiTipo),
35
+    datiCostiTipo: @json($datiCostiTipo)
36
+  };
37
+</script>
38
+@vite('resources/assets/js/dashboards-manutenzioni.js')
39
+@endsection
40
+
41
+@section('pageTitle')
42
+<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2">
43
+  <div>
44
+    <h4 class="mb-1">Statistiche</h4>
45
+  </div>
46
+  <a href="{{ route('admin.dashboard') }}" class="btn btn-label-secondary">
47
+    <i class="bx bx-home-smile me-1"></i>Dashboard
48
+  </a>
49
+</div>
50
+@endsection
51
+
52
+@section('content')
53
+
54
+<div class="card mb-4">
55
+  <div class="card-header border-bottom">
56
+    <div class="d-flex align-items-center">
57
+      <div class="avatar avatar-sm me-2">
58
+        <span class="avatar-initial rounded bg-label-primary">
59
+          <i class="bx bx-flag fs-4"></i>
60
+        </span>
61
+      </div>
62
+      <div>
63
+        <h5 class="mb-0">Segnalazioni</h5>
64
+        <p class="text-body-secondary mb-0">Andamento e distribuzione</p>
65
+      </div>
66
+    </div>
67
+  </div>
68
+  <div class="card-body">
69
+    <div class="row mb-4">
70
+      <div class="col-12 col-xxl-8 mb-6">
71
+        <div class="card">
72
+          <div class="card-header d-flex align-items-center justify-content-between">
73
+            <div class="card-title mb-0">
74
+              <h5 class="m-0 me-2">Segnalazioni nel Tempo</h5>
75
+              <p class="text-body-secondary mb-0">Ultimi 30 giorni</p>
76
+            </div>
77
+          </div>
78
+          <div class="card-body">
79
+            <div id="segnalazioniTempoChart"></div>
80
+          </div>
81
+        </div>
82
+      </div>
83
+
84
+      <div class="col-12 col-xxl-4 mb-6">
85
+        <div class="card">
86
+          <div class="card-header d-flex align-items-center justify-content-between">
87
+            <div class="card-title mb-0">
88
+              <h5 class="m-0 me-2">Distribuzione per Stato</h5>
89
+            </div>
90
+          </div>
91
+          <div class="card-body">
92
+            <div id="distribuzioneStatoChart"></div>
93
+          </div>
94
+        </div>
95
+      </div>
96
+    </div>
97
+
98
+    <div class="row">
99
+      <div class="col-md-6 col-lg-4 mb-6">
100
+        <div class="card h-100">
101
+          <div class="card-header d-flex justify-content-between">
102
+            <div class="card-title mb-0">
103
+              <h5 class="mb-1 me-2">Distribuzione per Priorità</h5>
104
+            </div>
105
+          </div>
106
+          <div class="card-body">
107
+            <div id="distribuzionePrioritaChart"></div>
108
+          </div>
109
+        </div>
110
+      </div>
111
+
112
+      <div class="col-md-6 col-lg-4 mb-6">
113
+        <div class="card h-100">
114
+          <div class="card-header d-flex justify-content-between">
115
+            <div class="card-title mb-0">
116
+              <h5 class="mb-1 me-2">Segnalazioni per Azienda</h5>
117
+            </div>
118
+          </div>
119
+          <div class="card-body">
120
+            <div id="segnalazioniAziendaChart"></div>
121
+          </div>
122
+        </div>
123
+      </div>
124
+
125
+      <div class="col-md-6 col-lg-4 mb-6">
126
+        <div class="card h-100">
127
+          <div class="card-header d-flex justify-content-between">
128
+            <div class="card-title mb-0">
129
+              <h5 class="mb-1 me-2">Distribuzione per Tipo</h5>
130
+            </div>
131
+          </div>
132
+          <div class="card-body">
133
+            <div id="distribuzioneTipoChart"></div>
134
+          </div>
135
+        </div>
136
+      </div>
137
+    </div>
138
+  </div>
139
+</div>
140
+
141
+<div class="card mb-4">
142
+  <div class="card-header border-bottom">
143
+    <div class="d-flex align-items-center">
144
+      <div class="avatar avatar-sm me-2">
145
+        <span class="avatar-initial rounded bg-label-success">
146
+          <i class="bx bx-trending-up fs-4"></i>
147
+        </span>
148
+      </div>
149
+      <div>
150
+        <h5 class="mb-0">Performance</h5>
151
+        <p class="text-body-secondary mb-0">Metriche di efficienza e produttività</p>
152
+      </div>
153
+    </div>
154
+  </div>
155
+  <div class="card-body">
156
+    <div class="row mb-0">
157
+      <div class="col-lg-4 col-md-6 col-12 mb-4">
158
+        <div class="card h-100">
159
+          <div class="card-body">
160
+            <div class="d-flex justify-content-between align-items-start">
161
+              <div>
162
+                <span class="badge bg-label-secondary mb-2">Media</span>
163
+                <h4 class="card-title mb-0">{{ $tempoMedioRisoluzione }}</h4>
164
+                <p class="text-body-secondary mb-0">Giorni per Risoluzione</p>
165
+              </div>
166
+              <div class="avatar flex-shrink-0">
167
+                <span class="avatar-initial rounded bg-label-secondary">
168
+                  <i class="bx bx-timer fs-4"></i>
169
+                </span>
170
+              </div>
171
+            </div>
172
+          </div>
173
+        </div>
174
+      </div>
175
+      <div class="col-lg-4 col-md-6 col-12 mb-4">
176
+        <div class="card h-100">
177
+          <div class="card-body">
178
+            <div class="d-flex justify-content-between align-items-start">
179
+              <div>
180
+                <span class="badge bg-label-info mb-2">Questo Mese</span>
181
+                <h4 class="card-title mb-0">{{ $oreTotaliMese }}</h4>
182
+                <p class="text-body-secondary mb-0">Ore Lavorate</p>
183
+              </div>
184
+              <div class="avatar flex-shrink-0">
185
+                <span class="avatar-initial rounded bg-label-info">
186
+                  <i class="bx bx-time-five fs-4"></i>
187
+                </span>
188
+              </div>
189
+            </div>
190
+          </div>
191
+        </div>
192
+      </div>
193
+      <div class="col-lg-4 col-md-6 col-12 mb-4">
194
+        <div class="card h-100">
195
+          <div class="card-body">
196
+            <div class="d-flex justify-content-between align-items-start">
197
+              <div>
198
+                <span class="badge bg-label-primary mb-2">Questo Mese</span>
199
+                <h4 class="card-title mb-0">€ {{ number_format($costiMese, 2, ',', '.') }}</h4>
200
+                <p class="text-body-secondary mb-0">Costi Totali</p>
201
+                @if($variazioneCosti != 0)
202
+                <small class="text-{{ $variazioneCosti > 0 ? 'danger' : 'success' }}">
203
+                  <i class="bx bx-{{ $variazioneCosti > 0 ? 'trending-up' : 'trending-down' }}"></i>
204
+                  {{ abs($variazioneCosti) }}% vs mese precedente
205
+                </small>
206
+                @endif
207
+              </div>
208
+              <div class="avatar flex-shrink-0">
209
+                <span class="avatar-initial rounded bg-label-primary">
210
+                  <i class="bx bx-euro fs-4"></i>
211
+                </span>
212
+              </div>
213
+            </div>
214
+          </div>
215
+        </div>
216
+      </div>
217
+    </div>
218
+  </div>
219
+</div>
220
+
221
+@endsection

+ 235
- 257
resources/views/dashboard/dashboards-manutenzioni.blade.php Прегледај датотеку

@@ -6,51 +6,18 @@ $configData = Helper::appClasses();
6 6
 
7 7
 @section('title', 'Dashboard - Manutenzioni')
8 8
 
9
-@section('vendor-style')
10
-@vite(['resources/assets/vendor/libs/apex-charts/apex-charts.scss', 'resources/assets/vendor/fonts/flag-icons.scss'])
11
-@endsection
12
-
13
-@section('vendor-script')
14
-@vite('resources/assets/vendor/libs/apex-charts/apexcharts.js')
15
-@endsection
16
-
17
-@section('page-script')
18
-<script>
19
-  // Dati passati dal controller - deve essere definito PRIMA che il file JS venga eseguito
20
-  window.dashboardData = {
21
-    labelsTempo: @json($labelsTempo),
22
-    datiTempo: @json($datiTempo),
23
-    labelsStato: @json($labelsStato),
24
-    datiStato: @json($datiStato),
25
-    labelsPriorita: @json($labelsPriorita),
26
-    datiPriorita: @json($datiPriorita),
27
-    labelsAzienda: @json($labelsAzienda),
28
-    datiAzienda: @json($datiAzienda),
29
-    labelsManutentori: @json($labelsManutentori),
30
-    datiManutentori: @json($datiManutentori),
31
-    labelsCostiAzienda: @json($labelsCostiAzienda),
32
-    datiCostiAzienda: @json($datiCostiAzienda),
33
-    labelsTipo: @json($labelsTipo),
34
-    datiTipo: @json($datiTipo),
35
-    labelsCostiTipo: @json($labelsCostiTipo),
36
-    datiCostiTipo: @json($datiCostiTipo)
37
-  };
38
-  console.log('Dashboard Data loaded:', window.dashboardData);
39
-</script>
40
-@vite('resources/assets/js/dashboards-manutenzioni.js')
41
-@endsection
42
-
43 9
 @section('pageTitle')
44
-<div class="d-flex flex-column">
45
-  <h4 class="mb-1">Dashboard - Manutenzioni</h4>
10
+<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2">
11
+  <div>
12
+    <h4 class="mb-1">Dashboard - Manutenzioni</h4>
13
+  </div>
14
+  <a href="{{ route('admin.statistiche') }}" class="btn btn-label-primary">
15
+    <i class="bx bx-bar-chart-alt-2 me-1"></i>Statistiche
16
+  </a>
46 17
 </div>
47 18
 @endsection
48 19
 
49 20
 @section('content')
50
-
51
-<!-- ============================================ -->
52
-<!-- SEZIONE SEGNALAZIONI -->
53
-<!-- ============================================ -->
54 21
 <div class="card mb-4">
55 22
   <div class="card-header border-bottom">
56 23
     <div class="d-flex align-items-center">
@@ -61,12 +28,11 @@ $configData = Helper::appClasses();
61 28
       </div>
62 29
       <div>
63 30
         <h5 class="mb-0">Segnalazioni</h5>
64
-        <p class="text-body-secondary mb-0">Panoramica completa sulle segnalazioni</p>
31
+        <p class="text-body-secondary mb-0">Panoramica operativa</p>
65 32
       </div>
66 33
     </div>
67 34
   </div>
68 35
   <div class="card-body">
69
-    <!-- KPI Principali Segnalazioni -->
70 36
     <div class="row mb-4">
71 37
       <div class="col-lg-3 col-md-6 col-12 mb-4">
72 38
         <div class="card h-100">
@@ -112,52 +78,51 @@ $configData = Helper::appClasses();
112 78
       </div>
113 79
       <div class="col-lg-3 col-md-6 col-12 mb-4">
114 80
         <a class="text-decoration-none" href="{{ route('admin.segnalazione.status', 'in_lavorazione') }}">
115
-        <div class="card h-100">
116
-          <div class="card-body">
117
-            <div class="d-flex justify-content-between align-items-start">
118
-              <div>
119
-                <span class="badge bg-label-info mb-2">
120
-                  <i class="bx bx-cog me-1"></i>In Lavorazione
121
-                </span>
122
-                <h4 class="card-title mb-0">{{ $segnalazioniInLavorazione }}</h4>
123
-                <p class="text-body-secondary mb-0">In Corso</p>
124
-              </div>
125
-              <div class="avatar flex-shrink-0">
126
-                <span class="avatar-initial rounded bg-label-info">
127
-                  <i class="bx bx-cog fs-4"></i>
128
-                </span>
81
+          <div class="card h-100">
82
+            <div class="card-body">
83
+              <div class="d-flex justify-content-between align-items-start">
84
+                <div>
85
+                  <span class="badge bg-label-info mb-2">
86
+                    <i class="bx bx-cog me-1"></i>In Lavorazione
87
+                  </span>
88
+                  <h4 class="card-title mb-0">{{ $segnalazioniInLavorazione }}</h4>
89
+                  <p class="text-body-secondary mb-0">In Corso</p>
90
+                </div>
91
+                <div class="avatar flex-shrink-0">
92
+                  <span class="avatar-initial rounded bg-label-info">
93
+                    <i class="bx bx-cog fs-4"></i>
94
+                  </span>
95
+                </div>
129 96
               </div>
130 97
             </div>
131 98
           </div>
132
-        </div>
133 99
         </a>
134 100
       </div>
135 101
       <div class="col-lg-3 col-md-6 col-12 mb-4">
136 102
         <a class="text-decoration-none" href="{{ route('admin.segnalazione.status', 'chiuse') }}">
137
-        <div class="card h-100">
138
-          <div class="card-body">
139
-            <div class="d-flex justify-content-between align-items-start">
140
-              <div>
141
-                <span class="badge bg-label-success mb-2">
142
-                  <i class="bx bx-check-circle me-1"></i>Chiuse
143
-                </span>
144
-                <h4 class="card-title mb-0">{{ $segnalazioniChiuse }}</h4>
145
-                <p class="text-body-secondary mb-0">Completate</p>
146
-              </div>
147
-              <div class="avatar flex-shrink-0">
148
-                <span class="avatar-initial rounded bg-label-success">
149
-                  <i class="bx bx-check-circle fs-4"></i>
150
-                </span>
103
+          <div class="card h-100">
104
+            <div class="card-body">
105
+              <div class="d-flex justify-content-between align-items-start">
106
+                <div>
107
+                  <span class="badge bg-label-success mb-2">
108
+                    <i class="bx bx-check-circle me-1"></i>Chiuse
109
+                  </span>
110
+                  <h4 class="card-title mb-0">{{ $segnalazioniChiuse }}</h4>
111
+                  <p class="text-body-secondary mb-0">Completate</p>
112
+                </div>
113
+                <div class="avatar flex-shrink-0">
114
+                  <span class="avatar-initial rounded bg-label-success">
115
+                    <i class="bx bx-check-circle fs-4"></i>
116
+                  </span>
117
+                </div>
151 118
               </div>
152 119
             </div>
153 120
           </div>
154
-        </div>
155 121
         </a>
156 122
       </div>
157
-    </a>
123
+    </div>
158 124
 
159
-    <!-- Alert Cards - Segnalazioni Critiche -->
160
-    <div class="row mb-4">
125
+    <div class="row mb-0">
161 126
       <div class="col-lg-4 col-md-6 col-12 mb-4">
162 127
         <a class="text-decoration-none" href="{{ route('admin.segnalazione.status', 'urgente') }}">
163 128
           <div class="card h-100 border-start border-danger border-4">
@@ -180,229 +145,242 @@ $configData = Helper::appClasses();
180 145
           </div>
181 146
         </a>
182 147
       </div>
183
-
184 148
       <div class="col-lg-4 col-md-6 col-12 mb-4">
185 149
         <a class="text-decoration-none" href="{{ route('admin.segnalazione.status', 'senza_assegnazione') }}">
186
-        <div class="card h-100 border-start border-warning border-4">
187
-          <div class="card-body">
188
-            <div class="d-flex justify-content-between align-items-start">
189
-              <div>
190
-                <span class="badge bg-label-warning mb-2">
191
-                  <i class="bx bx-user-x me-1"></i>Senza Assegnazione
192
-                </span>
193
-                <h4 class="card-title mb-0 text-warning">{{ $segnalazioniSenzaAssegnazione }}</h4>
194
-                <p class="text-body-secondary mb-0">Non Assegnate</p>
195
-              </div>
196
-              <div class="avatar flex-shrink-0">
197
-                <span class="avatar-initial rounded bg-label-warning">
198
-                  <i class="bx bx-user-x fs-4"></i>
199
-                </span>
150
+          <div class="card h-100 border-start border-warning border-4">
151
+            <div class="card-body">
152
+              <div class="d-flex justify-content-between align-items-start">
153
+                <div>
154
+                  <span class="badge bg-label-warning mb-2">
155
+                    <i class="bx bx-user-x me-1"></i>Senza Assegnazione
156
+                  </span>
157
+                  <h4 class="card-title mb-0 text-warning">{{ $segnalazioniSenzaAssegnazione }}</h4>
158
+                  <p class="text-body-secondary mb-0">Non Assegnate</p>
159
+                </div>
160
+                <div class="avatar flex-shrink-0">
161
+                  <span class="avatar-initial rounded bg-label-warning">
162
+                    <i class="bx bx-user-x fs-4"></i>
163
+                  </span>
164
+                </div>
200 165
               </div>
201 166
             </div>
202 167
           </div>
203
-        </div>
204 168
         </a>
205 169
       </div>
206 170
       <div class="col-lg-4 col-md-6 col-12 mb-4">
207 171
         <a class="text-decoration-none" href="{{ route('admin.segnalazione.status', 'in_ritardo') }}">
208
-        <div class="card h-100 border-start border-info border-4">
209
-          <div class="card-body">
210
-            <div class="d-flex justify-content-between align-items-start">
211
-              <div>
212
-                <span class="badge bg-label-info mb-2">
213
-                  <i class="bx bx-time me-1"></i>In Ritardo
214
-                </span>
215
-                <h4 class="card-title mb-0 text-info">{{ $segnalazioniAperteVecchie }}</h4>
216
-                <p class="text-body-secondary mb-0">Aperte da >7 giorni</p>
217
-              </div>
218
-              <div class="avatar flex-shrink-0">
219
-                <span class="avatar-initial rounded bg-label-info">
220
-                  <i class="bx bx-time fs-4"></i>
221
-                </span>
172
+          <div class="card h-100 border-start border-info border-4">
173
+            <div class="card-body">
174
+              <div class="d-flex justify-content-between align-items-start">
175
+                <div>
176
+                  <span class="badge bg-label-info mb-2">
177
+                    <i class="bx bx-time me-1"></i>In Ritardo
178
+                  </span>
179
+                  <h4 class="card-title mb-0 text-info">{{ $segnalazioniAperteVecchie }}</h4>
180
+                  <p class="text-body-secondary mb-0">Aperte da >7 giorni</p>
181
+                </div>
182
+                <div class="avatar flex-shrink-0">
183
+                  <span class="avatar-initial rounded bg-label-info">
184
+                    <i class="bx bx-time fs-4"></i>
185
+                  </span>
186
+                </div>
222 187
               </div>
223 188
             </div>
224 189
           </div>
225
-        </div>
226 190
         </a>
227 191
       </div>
228 192
     </div>
193
+  </div>
194
+</div>
229 195
 
230
-    <!-- Grafici Segnalazioni -->
231
-    <div class="row mb-4">
232
-      <!-- Grafico Segnalazioni nel Tempo -->
233
-      <div class="col-12 col-xxl-8 mb-6">
234
-        <div class="card">
235
-          <div class="card-header d-flex align-items-center justify-content-between">
236
-            <div class="card-title mb-0">
237
-              <h5 class="m-0 me-2">Segnalazioni nel Tempo</h5>
238
-              <p class="text-body-secondary mb-0">Ultimi 30 giorni</p>
239
-            </div>
240
-          </div>
241
-          <div class="card-body">
242
-            <div id="segnalazioniTempoChart"></div>
243
-          </div>
196
+<div class="card mb-4">
197
+  <div class="card-header border-bottom">
198
+    <div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
199
+      <div class="d-flex align-items-center">
200
+        <div class="avatar avatar-sm me-2">
201
+          <span class="avatar-initial rounded bg-label-success">
202
+            <i class="bx bx-play-circle fs-4"></i>
203
+          </span>
244 204
         </div>
245
-      </div>
246
-      <!--/ Grafico Segnalazioni nel Tempo -->
247
-
248
-      <!-- Grafico Distribuzione per Stato -->
249
-      <div class="col-12 col-xxl-4 mb-6">
250
-        <div class="card">
251
-          <div class="card-header d-flex align-items-center justify-content-between">
252
-            <div class="card-title mb-0">
253
-              <h5 class="m-0 me-2">Distribuzione per Stato</h5>
254
-            </div>
255
-          </div>
256
-          <div class="card-body">
257
-            <div id="distribuzioneStatoChart"></div>
258
-          </div>
205
+        <div>
206
+          <h5 class="mb-0">Timer attivi</h5>
207
+          <p class="text-body-secondary mb-0">Segnalazioni con tracciamento tempo in corso</p>
259 208
         </div>
260 209
       </div>
261
-      <!--/ Grafico Distribuzione per Stato -->
210
+      <span class="badge bg-label-success">{{ $segnalazioniInLavoro->count() }}</span>
262 211
     </div>
263
-
264
-    <!-- Altri Grafici Segnalazioni -->
265
-    <div class="row">
266
-      <!-- Grafico Distribuzione per Priorità -->
267
-      <div class="col-md-6 col-lg-4 mb-6">
268
-        <div class="card h-100">
269
-          <div class="card-header d-flex justify-content-between">
270
-            <div class="card-title mb-0">
271
-              <h5 class="mb-1 me-2">Distribuzione per Priorità</h5>
272
-            </div>
273
-          </div>
274
-          <div class="card-body">
275
-            <div id="distribuzionePrioritaChart"></div>
212
+  </div>
213
+  <div class="card-body p-0">
214
+    @forelse($segnalazioniInLavoro as $segnalazione)
215
+      @php
216
+        $stato = $segnalazione->getConfiguraStato();
217
+        $priorita = $segnalazione->getConfiguraPriorita();
218
+        $timers = $segnalazione->getActiveTempoTimers();
219
+        $oldestStarted = collect($timers)->min('started_at');
220
+      @endphp
221
+      <a href="{{ route('admin.segnalazione.show', ['id' => $segnalazione->id]) }}"
222
+        class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2 px-4 py-3 text-decoration-none text-body border-bottom">
223
+        <div class="min-w-0">
224
+          <div class="fw-semibold text-truncate">{{ $segnalazione->getLabel() }}</div>
225
+          <div class="small text-muted">
226
+            @if($segnalazione->azienda)
227
+              {{ $segnalazione->azienda->full_name }}
228
+              <span class="mx-1">·</span>
229
+            @endif
230
+            @if($stato)
231
+              <span class="badge bg-label-{{ $stato->colore ?? 'secondary' }}">{{ $stato->label }}</span>
232
+            @else
233
+              {{ $segnalazione->stato }}
234
+            @endif
235
+            @if($priorita)
236
+              <span class="badge bg-label-{{ $priorita->colore ?? 'secondary' }} ms-1">{{ $priorita->label }}</span>
237
+            @endif
276 238
           </div>
277 239
         </div>
278
-      </div>
279
-      <!--/ Grafico Distribuzione per Priorità -->
280
-
281
-      <!-- Grafico Segnalazioni per Azienda -->
282
-      <div class="col-md-6 col-lg-4 mb-6">
283
-        <div class="card h-100">
284
-          <div class="card-header d-flex justify-content-between">
285
-            <div class="card-title mb-0">
286
-              <h5 class="mb-1 me-2">Segnalazioni per Azienda</h5>
240
+        <div class="text-md-end">
241
+          @foreach($timers as $timer)
242
+            @php $timerUser = $timerUsers->get($timer['user_id']); @endphp
243
+            <div class="small">
244
+              <span class="badge bg-label-success me-1"><i class="bx bx-play"></i> Start</span>
245
+              {{ $timerUser?->full_name ?? ('Utente #'.$timer['user_id']) }}
246
+              <span class="text-muted">da {{ \Carbon\Carbon::createFromTimestamp($timer['started_at'])->format('d/m/Y H:i') }}</span>
287 247
             </div>
288
-          </div>
289
-          <div class="card-body">
290
-            <div id="segnalazioniAziendaChart"></div>
291
-          </div>
248
+          @endforeach
249
+          @if($oldestStarted)
250
+            <div class="small text-muted mt-1 dashboard-timer-elapsed" data-started-at="{{ $oldestStarted }}">—</div>
251
+          @endif
292 252
         </div>
253
+      </a>
254
+    @empty
255
+      <div class="p-4 text-center text-muted">
256
+        Nessuna segnalazione con timer attivo in questo momento.
293 257
       </div>
294
-      <!--/ Grafico Segnalazioni per Azienda -->
295
-
296
-
297
-      <!-- Distribuzione per Tipo -->
298
-      <div class="col-md-6 col-lg-4 mb-6">
299
-        <div class="card h-100">
300
-          <div class="card-header d-flex justify-content-between">
301
-            <div class="card-title mb-0">
302
-              <h5 class="mb-1 me-2">Distribuzione per Tipo</h5>
303
-            </div>
304
-          </div>
305
-          <div class="card-body">
306
-            <div id="distribuzioneTipoChart"></div>
307
-          </div>
308
-        </div>
309
-      </div>
310
-      <!--/ Distribuzione per Tipo -->
311
-    </div>
312
-
313
-
258
+    @endforelse
314 259
   </div>
315 260
 </div>
316
-<!-- ============================================ -->
317
-<!-- FINE SEZIONE SEGNALAZIONI -->
318
-<!-- ============================================ -->
319 261
 
320
-<!-- ============================================ -->
321
-<!-- SEZIONE PERFORMANCE -->
322
-<!-- ============================================ -->
323 262
 <div class="card mb-4">
324 263
   <div class="card-header border-bottom">
325 264
     <div class="d-flex align-items-center">
326 265
       <div class="avatar avatar-sm me-2">
327
-        <span class="avatar-initial rounded bg-label-success">
328
-          <i class="bx bx-trending-up fs-4"></i>
266
+        <span class="avatar-initial rounded bg-label-info">
267
+          <i class="bx bx-calendar fs-4"></i>
329 268
         </span>
330 269
       </div>
331 270
       <div>
332
-        <h5 class="mb-0">Performance</h5>
333
-        <p class="text-body-secondary mb-0">Metriche di efficienza e produttività</p>
271
+        <h5 class="mb-0">Prenotazioni pianificate</h5>
272
+        <p class="text-body-secondary mb-0">Slot orari delle segnalazioni in stato pianificata</p>
334 273
       </div>
335 274
     </div>
336 275
   </div>
337 276
   <div class="card-body">
338
-    <div class="row mb-4">
339
-      <div class="col-lg-4 col-md-6 col-12 mb-4">
340
-        <div class="card h-100">
341
-          <div class="card-body">
342
-            <div class="d-flex justify-content-between align-items-start">
343
-              <div>
344
-                <span class="badge bg-label-secondary mb-2">Media</span>
345
-                <h4 class="card-title mb-0">{{ $tempoMedioRisoluzione }}</h4>
346
-                <p class="text-body-secondary mb-0">Giorni per Risoluzione</p>
347
-              </div>
348
-              <div class="avatar flex-shrink-0">
349
-                <span class="avatar-initial rounded bg-label-secondary">
350
-                  <i class="bx bx-timer fs-4"></i>
351
-                </span>
352
-              </div>
353
-            </div>
354
-          </div>
355
-        </div>
356
-      </div>
357
-      <div class="col-lg-4 col-md-6 col-12 mb-4">
358
-        <div class="card h-100">
359
-          <div class="card-body">
360
-            <div class="d-flex justify-content-between align-items-start">
361
-              <div>
362
-                <span class="badge bg-label-info mb-2">Questo Mese</span>
363
-                <h4 class="card-title mb-0">{{ $oreTotaliMese }}</h4>
364
-                <p class="text-body-secondary mb-0">Ore Lavorate</p>
365
-              </div>
366
-              <div class="avatar flex-shrink-0">
367
-                <span class="avatar-initial rounded bg-label-info">
368
-                  <i class="bx bx-time-five fs-4"></i>
369
-                </span>
370
-              </div>
371
-            </div>
372
-          </div>
373
-        </div>
374
-      </div>
375
-
376
-      <div class="col-lg-4 col-md-6 col-12 mb-4">
377
-        <div class="card h-100">
378
-          <div class="card-body">
379
-            <div class="d-flex justify-content-between align-items-start">
380
-              <div>
381
-                <span class="badge bg-label-primary mb-2">Questo Mese</span>
382
-                <h4 class="card-title mb-0">€ {{ number_format($costiMese, 2, ',', '.') }}</h4>
383
-                <p class="text-body-secondary mb-0">Costi Totali</p>
384
-                @if($variazioneCosti != 0)
385
-                <small class="text-{{ $variazioneCosti > 0 ? 'danger' : 'success' }}">
386
-                  <i class="bx bx-{{ $variazioneCosti > 0 ? 'trending-up' : 'trending-down' }}"></i>
387
-                  {{ abs($variazioneCosti) }}% vs mese precedente
388
-                </small>
389
-                @endif
390
-              </div>
391
-              <div class="avatar flex-shrink-0">
392
-                <span class="avatar-initial rounded bg-label-primary">
393
-                  <i class="bx bx-euro fs-4"></i>
394
-                </span>
395
-              </div>
396
-            </div>
397
-          </div>
398
-        </div>
399
-      </div>
400
-    </div>
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>
401 279
   </div>
402 280
 </div>
403
-<!-- ============================================ -->
404
-<!-- FINE SEZIONE PERFORMANCE -->
405
-<!-- ============================================ -->
281
+@endsection
406 282
 
283
+@section('vendor-style')
284
+@vite([
285
+  'resources/assets/vendor/libs/fullcalendar/fullcalendar.scss',
286
+])
287
+@endsection
288
+
289
+@section('page-style')
290
+<style>
291
+  #calendarioSlotPrenotazioni .fc-event,
292
+  #calendarioSlotPrenotazioni .fc-event-main,
293
+  #calendarioSlotPrenotazioni .fc-event a,
294
+  #calendarioSlotPrenotazioni .fc-event-title,
295
+  #calendarioSlotPrenotazioni .fc-list-event-title a {
296
+    color: #fff !important;
297
+    text-decoration: none !important;
298
+  }
299
+  #calendarioSlotPrenotazioni .fc-event {
300
+    border: none;
301
+  }
302
+</style>
303
+@endsection
407 304
 
305
+@section('vendor-script')
306
+@vite([
307
+  'resources/assets/vendor/libs/fullcalendar/fullcalendar.js',
308
+])
309
+@endsection
310
+
311
+@section('page-script')
312
+<script>
313
+(function () {
314
+  function formatElapsed(ms) {
315
+    var total = Math.max(0, Math.floor(ms / 1000));
316
+    var h = Math.floor(total / 3600);
317
+    var m = Math.floor((total % 3600) / 60);
318
+    var s = total % 60;
319
+    return String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
320
+  }
321
+
322
+  function tick() {
323
+    document.querySelectorAll('.dashboard-timer-elapsed').forEach(function (el) {
324
+      var started = parseInt(el.getAttribute('data-started-at'), 10);
325
+      if (!started) return;
326
+      el.textContent = 'In corso: ' + formatElapsed(Date.now() - (started * 1000));
327
+    });
328
+  }
329
+
330
+  tick();
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
+})();
385
+</script>
408 386
 @endsection

+ 1
- 1
resources/views/layouts/guest.blade.php Прегледај датотеку

@@ -14,7 +14,7 @@
14 14
         <div class="col-12 d-flex justify-content-between align-items-center">
15 15
           <!-- Pulsante Indietro a sinistra -->
16 16
           <div>
17
-            @if(Route::is('segnalazione.nuova') || Route::is('segnalazione.show'))
17
+            @if(Route::is('segnalazione.show'))
18 18
             <a style="" href="{{ url()->previous() }}" class="btn btn-outline-secondary">
19 19
               <i class="bx bx-arrow-back me-1"></i>Indietro
20 20
             </a>

+ 1
- 1
resources/views/mail/annotazione-pubblica-aperta-da.blade.php Прегледај датотеку

@@ -15,7 +15,7 @@
15 15
 {!! $annotazioneHtml !!}
16 16
 @endif
17 17
 
18
-@component('mail::button', ['url' => route('admin.segnalazione.show', ['id' => $segnalazione->id])])
18
+@component('mail::button', ['url' => $segnalazione->urlCliente()])
19 19
 Apri segnalazione
20 20
 @endcomponent
21 21
 

+ 1
- 1
resources/views/mail/nuova-segnalazione-reparto.blade.php Прегледај датотеку

@@ -10,7 +10,7 @@
10 10
 @endcomponent
11 11
 
12 12
 @if($segnalazione->token)
13
-@component('mail::button', ['url' => route('segnalazione.show', ['token' => $segnalazione->token])])
13
+@component('mail::button', ['url' => $segnalazione->urlCliente()])
14 14
 Visualizza segnalazione
15 15
 @endcomponent
16 16
 @endif

+ 1
- 1
resources/views/mail/nuova-segnalazione-user.blade.php Прегледај датотеку

@@ -22,7 +22,7 @@ Ti è stata assegnata una nuova richiesta: **{{ $segnalazione->titolo }}**.
22 22
 {!! $annotazioneHtml !!}
23 23
 @endif
24 24
 
25
-@component('mail::button', ['url' => route('admin.segnalazione.show', ['id' => $segnalazione->id])])
25
+@component('mail::button', ['url' => !empty($isApertaDa) ? $segnalazione->urlCliente() : route('admin.segnalazione.show', ['id' => $segnalazione->id])])
26 26
 Apri segnalazione
27 27
 @endcomponent
28 28
 

+ 14
- 0
resources/views/mail/risposta-auto-segnalazione-email.blade.php Прегледај датотеку

@@ -0,0 +1,14 @@
1
+@component('mail::message')
2
+# Segnalazione ricevuta
3
+
4
+@foreach($paragraphs as $paragraph)
5
+{!! nl2br(e($paragraph)) !!}
6
+
7
+@endforeach
8
+
9
+@component('mail::button', ['url' => $segnalazione->urlCliente()])
10
+Consulta la segnalazione
11
+@endcomponent
12
+
13
+Puoi seguire lo stato della richiesta e le risposte dalla tua area riservata.
14
+@endcomponent

+ 1
- 1
resources/views/mail/segnalazione-stato-cambiato-reparto.blade.php Прегледај датотеку

@@ -9,7 +9,7 @@ La segnalazione **{{ $segnalazione->titolo }}** ha cambiato stato.
9 9
 @endcomponent
10 10
 
11 11
 @if($segnalazione->token)
12
-@component('mail::button', ['url' => route('segnalazione.show', ['token' => $segnalazione->token])])
12
+@component('mail::button', ['url' => $segnalazione->urlCliente()])
13 13
 Visualizza segnalazione
14 14
 @endcomponent
15 15
 @endif

+ 190
- 4
resources/views/segnalazione/admin/show.blade.php Прегледај датотеку

@@ -1292,6 +1292,48 @@ use App\Models\Config;
1292 1292
                     </div>
1293 1293
                   </div>
1294 1294
                 </div>
1295
+
1296
+                <div id="slotPianificazioneBox" class="mt-3 {{ $segnalazione->stato === 'pianificata' ? '' : 'd-none' }}">
1297
+                  <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2">
1298
+                    <div>
1299
+                      <div class="fw-semibold">Slot di prenotazione</div>
1300
+                      <div class="small text-muted">Es. 26/08/2026 · 12:00 → 15:00</div>
1301
+                    </div>
1302
+                    <button type="button" class="btn btn-sm btn-label-primary" id="btnAggiungiSlotPrenotazione">
1303
+                      <i class="bx bx-plus me-1"></i>Aggiungi slot
1304
+                    </button>
1305
+                  </div>
1306
+                  <div id="slotPrenotazioneList" class="d-flex flex-column gap-2">
1307
+                    @forelse($segnalazione->slots as $slotIndex => $slot)
1308
+                      <div class="row g-2 align-items-end slot-prenotazione-row" data-slot-row>
1309
+                        <div class="col-md-4">
1310
+                          <label class="form-label small mb-1">Data</label>
1311
+                          <input type="text" class="form-control slot-data" name="slots[{{ $slotIndex }}][data]"
1312
+                            value="{{ $slot->starts_at->format('d/m/Y') }}" placeholder="dd/mm/yyyy" autocomplete="off">
1313
+                        </div>
1314
+                        <div class="col-md-3">
1315
+                          <label class="form-label small mb-1">Dalle</label>
1316
+                          <input type="text" class="form-control slot-ora-inizio" name="slots[{{ $slotIndex }}][ora_inizio]"
1317
+                            value="{{ $slot->starts_at->format('H:i') }}" placeholder="HH:mm" autocomplete="off">
1318
+                        </div>
1319
+                        <div class="col-md-3">
1320
+                          <label class="form-label small mb-1">Alle</label>
1321
+                          <input type="text" class="form-control slot-ora-fine" name="slots[{{ $slotIndex }}][ora_fine]"
1322
+                            value="{{ $slot->ends_at->format('H:i') }}" placeholder="HH:mm" autocomplete="off">
1323
+                        </div>
1324
+                        <div class="col-md-2">
1325
+                          <button type="button" class="btn btn-label-danger w-100 btn-rimuovi-slot" title="Rimuovi">
1326
+                            <i class="bx bx-trash"></i>
1327
+                          </button>
1328
+                        </div>
1329
+                      </div>
1330
+                    @empty
1331
+                    @endforelse
1332
+                  </div>
1333
+                  <div class="form-text mt-2">
1334
+                    Totale slot: <span id="slotPrenotazioneTotaleOre">0</span> ore
1335
+                  </div>
1336
+                </div>
1295 1337
               </div>
1296 1338
             </div>
1297 1339
             <div class="col-lg-5">
@@ -1543,14 +1585,158 @@ use App\Models\Config;
1543 1585
       return titles;
1544 1586
     }
1545 1587
 
1588
+    let slotFormAllowSubmit = false;
1589
+
1546 1590
     $('#formModificaSegnalazione').on('submit', function (e) {
1547 1591
       const stato = $('#segnalazione_stato').val();
1548
-      if (!statiChiusura.includes(stato)) return;
1549
-      const pending = incompleteObbligatoriTitles();
1550
-      if (!pending.length) return;
1592
+      if (statiChiusura.includes(stato)) {
1593
+        const pending = incompleteObbligatoriTitles();
1594
+        if (pending.length) {
1595
+          e.preventDefault();
1596
+          alert('Impossibile chiudere la segnalazione: completare i task obbligatori (' + pending.join(', ') + ').');
1597
+          return;
1598
+        }
1599
+      }
1600
+
1601
+      if (slotFormAllowSubmit || stato !== 'pianificata') {
1602
+        return;
1603
+      }
1604
+
1605
+      const oreSlot = calcolaOreSlotPrenotazione();
1606
+      if (oreSlot <= 0) {
1607
+        return;
1608
+      }
1609
+
1610
+      const $tempo = $('#tempo_stimato');
1611
+      const corrente = parseFloat(String($tempo.val() || '').replace(',', '.'));
1612
+      const hasTempo = Number.isFinite(corrente) && corrente > 0;
1613
+      const oreLabel = formatOreSlot(oreSlot);
1614
+
1615
+      if (!hasTempo) {
1616
+        $tempo.val(oreLabel);
1617
+        return;
1618
+      }
1619
+
1551 1620
       e.preventDefault();
1552
-      alert('Impossibile chiudere la segnalazione: completare i task obbligatori (' + pending.join(', ') + ').');
1621
+      const form = this;
1622
+      Swal.fire({
1623
+        title: 'Aggiornare il tempo stimato?',
1624
+        html: 'Il tempo stimato attuale è <strong>' + formatOreSlot(corrente) + ' ore</strong>.<br>'
1625
+          + 'La somma degli slot è <strong>' + oreLabel + ' ore</strong>.<br>'
1626
+          + 'Vuoi aggiornare il tempo stimato con la somma degli slot?',
1627
+        icon: 'question',
1628
+        showCancelButton: true,
1629
+        showDenyButton: true,
1630
+        confirmButtonText: 'Sì, aggiorna',
1631
+        denyButtonText: 'No, mantieni',
1632
+        cancelButtonText: 'Annulla'
1633
+      }).then(function (result) {
1634
+        if (result.isDismissed) {
1635
+          return;
1636
+        }
1637
+        if (result.isConfirmed) {
1638
+          $tempo.val(oreLabel);
1639
+        }
1640
+        slotFormAllowSubmit = true;
1641
+        form.requestSubmit ? form.requestSubmit() : $(form).trigger('submit');
1642
+      });
1643
+    });
1644
+
1645
+    function formatOreSlot(n) {
1646
+      const v = Math.round(n * 100) / 100;
1647
+      return String(v).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1');
1648
+    }
1649
+
1650
+    function parseTimeToMinutes(val) {
1651
+      const m = String(val || '').trim().match(/^(\d{1,2}):(\d{2})$/);
1652
+      if (!m) return null;
1653
+      const h = parseInt(m[1], 10);
1654
+      const min = parseInt(m[2], 10);
1655
+      if (h > 23 || min > 59) return null;
1656
+      return h * 60 + min;
1657
+    }
1658
+
1659
+    function calcolaOreSlotPrenotazione() {
1660
+      let totaleMin = 0;
1661
+      $('#slotPrenotazioneList [data-slot-row]').each(function () {
1662
+        const data = $(this).find('.slot-data').val();
1663
+        const inizio = parseTimeToMinutes($(this).find('.slot-ora-inizio').val());
1664
+        const fine = parseTimeToMinutes($(this).find('.slot-ora-fine').val());
1665
+        if (!data || inizio === null || fine === null || fine <= inizio) return;
1666
+        totaleMin += (fine - inizio);
1667
+      });
1668
+      return totaleMin / 60;
1669
+    }
1670
+
1671
+    function aggiornaTotaleOreSlot() {
1672
+      $('#slotPrenotazioneTotaleOre').text(formatOreSlot(calcolaOreSlotPrenotazione()));
1673
+    }
1674
+
1675
+    function initSlotRowFlatpickr($row) {
1676
+      const $data = $row.find('.slot-data');
1677
+      const $inizio = $row.find('.slot-ora-inizio');
1678
+      const $fine = $row.find('.slot-ora-fine');
1679
+      if ($data.length && !$data[0]._flatpickr) {
1680
+        flatpickr($data[0], { dateFormat: 'd/m/Y', locale: 'it', allowInput: true });
1681
+      }
1682
+      if ($inizio.length && !$inizio[0]._flatpickr) {
1683
+        flatpickr($inizio[0], { enableTime: true, noCalendar: true, dateFormat: 'H:i', time_24hr: true, locale: 'it', allowInput: true });
1684
+      }
1685
+      if ($fine.length && !$fine[0]._flatpickr) {
1686
+        flatpickr($fine[0], { enableTime: true, noCalendar: true, dateFormat: 'H:i', time_24hr: true, locale: 'it', allowInput: true });
1687
+      }
1688
+    }
1689
+
1690
+    function reindexSlotRows() {
1691
+      $('#slotPrenotazioneList [data-slot-row]').each(function (idx) {
1692
+        $(this).find('.slot-data').attr('name', 'slots[' + idx + '][data]');
1693
+        $(this).find('.slot-ora-inizio').attr('name', 'slots[' + idx + '][ora_inizio]');
1694
+        $(this).find('.slot-ora-fine').attr('name', 'slots[' + idx + '][ora_fine]');
1695
+      });
1696
+    }
1697
+
1698
+    function addSlotPrenotazioneRow(prefill) {
1699
+      const idx = $('#slotPrenotazioneList [data-slot-row]').length;
1700
+      const data = (prefill && prefill.data) || '';
1701
+      const inizio = (prefill && typeof prefill.ora_inizio !== 'undefined') ? prefill.ora_inizio : '';
1702
+      const fine = (prefill && typeof prefill.ora_fine !== 'undefined') ? prefill.ora_fine : '';
1703
+      const $row = $(
1704
+        '<div class="row g-2 align-items-end slot-prenotazione-row" data-slot-row>'
1705
+        + '<div class="col-md-4"><label class="form-label small mb-1">Data</label>'
1706
+        + '<input type="text" class="form-control slot-data" name="slots[' + idx + '][data]" value="' + escapeHtml(data) + '" placeholder="dd/mm/yyyy" autocomplete="off"></div>'
1707
+        + '<div class="col-md-3"><label class="form-label small mb-1">Dalle</label>'
1708
+        + '<input type="text" class="form-control slot-ora-inizio" name="slots[' + idx + '][ora_inizio]" value="' + escapeHtml(inizio) + '" placeholder="HH:mm" autocomplete="off"></div>'
1709
+        + '<div class="col-md-3"><label class="form-label small mb-1">Alle</label>'
1710
+        + '<input type="text" class="form-control slot-ora-fine" name="slots[' + idx + '][ora_fine]" value="' + escapeHtml(fine) + '" placeholder="HH:mm" autocomplete="off"></div>'
1711
+        + '<div class="col-md-2"><button type="button" class="btn btn-label-danger w-100 btn-rimuovi-slot" title="Rimuovi"><i class="bx bx-trash"></i></button></div>'
1712
+        + '</div>'
1713
+      );
1714
+      $('#slotPrenotazioneList').append($row);
1715
+      initSlotRowFlatpickr($row);
1716
+      aggiornaTotaleOreSlot();
1717
+    }
1718
+
1719
+    function syncSlotPianificazioneVisibility() {
1720
+      const isPianificata = $('#segnalazione_stato').val() === 'pianificata';
1721
+      $('#slotPianificazioneBox').toggleClass('d-none', !isPianificata);
1722
+      aggiornaTotaleOreSlot();
1723
+    }
1724
+
1725
+    $('#segnalazione_stato').on('change', syncSlotPianificazioneVisibility);
1726
+    $('#btnAggiungiSlotPrenotazione').on('click', function () {
1727
+      addSlotPrenotazioneRow();
1728
+    });
1729
+    $('#slotPrenotazioneList').on('click', '.btn-rimuovi-slot', function () {
1730
+      $(this).closest('[data-slot-row]').remove();
1731
+      reindexSlotRows();
1732
+      aggiornaTotaleOreSlot();
1733
+    });
1734
+    $('#slotPrenotazioneList').on('change input', '.slot-data, .slot-ora-inizio, .slot-ora-fine', aggiornaTotaleOreSlot);
1735
+
1736
+    $('#slotPrenotazioneList [data-slot-row]').each(function () {
1737
+      initSlotRowFlatpickr($(this));
1553 1738
     });
1739
+    syncSlotPianificazioneVisibility();
1554 1740
 
1555 1741
     function updateTodosKpi(done, total) {
1556 1742
       const label = done + '/' + total;

+ 3
- 3
resources/views/segnalazione/cliente/index.blade.php Прегледај датотеку

@@ -76,11 +76,11 @@
76 76
       if ($(e.target).closest('a, button').length) {
77 77
         return;
78 78
       }
79
-      const id = this.id;
80
-      if (! id) {
79
+      const token = this.id;
80
+      if (! token) {
81 81
         return;
82 82
       }
83
-      window.location.href = @json(url('segnalazione')).'/' + id;
83
+      window.location.href = @json(route('segnalazione.show', ['token' => '__TOKEN__'])).replace('__TOKEN__', token);
84 84
     });
85 85
   });
86 86
 </script>

+ 0
- 33
resources/views/segnalazione/cliente/show.blade.php Прегледај датотеку

@@ -541,42 +541,9 @@ use App\Models\Config;
541 541
         </div>
542 542
       </div>
543 543
       <div class="card-footer d-flex justify-content-end gap-2 flex-wrap">
544
-        @if($puoModificare)
545
-        <button type="button" class="btn btn-primary btn-sm" onclick="showCardModificaSegnalazione('modifica')">
546
-          <i class="bx bx-edit me-1"></i>Modifica
547
-        </button>
548
-        @endif
549 544
         <button type="button" class="btn btn-label-primary btn-sm" onclick="document.getElementById('cardRispostaCliente')?.classList.remove('d-none'); document.getElementById('cardRispostaCliente')?.scrollIntoView({behavior:'smooth'});">
550 545
           <i class="bx bx-reply me-1"></i>Rispondi
551 546
         </button>
552
-        <div class="btn-group">
553
-          <button type="button" class="btn btn-label-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
554
-            <i class="bx bx-file me-1"></i>Scarica PDF
555
-          </button>
556
-          <ul class="dropdown-menu dropdown-menu-end">
557
-            <li>
558
-              <a class="dropdown-item" href="{{ route('admin.segnalazione.pdf', ['id' => $segnalazione->id, 'destinatario' => 'cliente']) }}" target="_blank">
559
-                Versione cliente
560
-              </a>
561
-            </li>
562
-            @if(! $isVistaCliente)
563
-            <li>
564
-              <a class="dropdown-item" href="{{ route('admin.segnalazione.pdf', ['id' => $segnalazione->id, 'destinatario' => 'operatore']) }}" target="_blank">
565
-                Versione operatore
566
-              </a>
567
-            </li>
568
-            @endif
569
-          </ul>
570
-        </div>
571
-        @if($puoEliminare)
572
-        <button type="button" class="btn btn-danger btn-sm" onclick="eliminaSegnalazione()">
573
-          <i class="bx bx-trash me-1"></i>Elimina Segnalazione
574
-        </button>
575
-        <form id="formEliminaSegnalazione" action="{{ route('admin.segnalazione.delete', $segnalazione->id) }}" method="POST" class="d-none">
576
-          @csrf
577
-          @method('DELETE')
578
-        </form>
579
-        @endif
580 547
       </div>
581 548
     </div>
582 549
 

+ 443
- 0
resources/views/segnalazione/show.blade.php Прегледај датотеку

@@ -0,0 +1,443 @@
1
+<?php
2
+
3
+use Illuminate\Support\Facades\Auth;
4
+use App\Models\Config;
5
+?>
6
+
7
+@extends('layouts/guest')
8
+
9
+@section('title', 'Segnalazione')
10
+
11
+@section('vendor-script')
12
+@vite([
13
+'resources/assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js',
14
+'resources/assets/vendor/libs/quill/katex.js',
15
+'resources/assets/vendor/libs/quill/quill.js'
16
+])
17
+@endsection
18
+
19
+@section('vendor-style')
20
+@vite([
21
+'resources/assets/vendor/libs/datatables-rowgroup-bs5/rowgroup.bootstrap5.scss',
22
+'resources/assets/vendor/libs/quill/typography.scss',
23
+'resources/assets/vendor/libs/quill/katex.scss',
24
+'resources/assets/vendor/libs/quill/editor.scss'
25
+])
26
+
27
+@endsection
28
+
29
+@section('content')
30
+<div class="row justify-content-center mt-3">
31
+  <div class="col-lg-12">
32
+
33
+    @if(session('success'))
34
+    <div class="alert alert-success alert-dismissible fade show" role="alert">
35
+      <i class="bx bx-check-circle me-2"></i>
36
+      {{ session('success') }}
37
+      <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
38
+    </div>
39
+    @endif
40
+
41
+    @if(session('error'))
42
+    <div class="alert alert-danger alert-dismissible fade show" role="alert">
43
+      <i class="bx bx-error-circle me-2"></i>
44
+      {{ session('error') }}
45
+      <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
46
+    </div>
47
+    @endif
48
+
49
+    @if($errors->any())
50
+    <div class="alert alert-danger alert-dismissible fade show" role="alert">
51
+      <i class="bx bx-error-circle me-2"></i>
52
+      <ul class="mb-0">
53
+        @foreach($errors->all() as $error)
54
+        <li>{{ $error }}</li>
55
+        @endforeach
56
+      </ul>
57
+      <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
58
+    </div>
59
+    @endif
60
+
61
+    @php
62
+    $configStato = $segnalazione->getConfiguraStato();
63
+    $configPriorita = $segnalazione->getConfiguraPriorita();
64
+    $configTipo = $segnalazione->getConfiguraTipo();
65
+
66
+    // Crea timeline unificata con tutti gli eventi
67
+    $eventi = collect();
68
+
69
+    // Eventi: Annotazioni
70
+    $isFirst = true;
71
+    foreach($segnalazione->annotazioni()->orderBy('id', 'asc')->get() as $annotazione) {
72
+      $hasText = $annotazione->text && trim($annotazione->text) !== '';
73
+      $hasCosto = $annotazione->costo !== null && $annotazione->costo != 0;
74
+      $hasTempo = $annotazione->tempo_minuti !== null && $annotazione->tempo_minuti > 0;
75
+
76
+      if (!($hasText || $hasCosto || $hasTempo)) {
77
+        continue;
78
+      }
79
+
80
+      $eventi->push([
81
+      'tipo' => $isFirst ? 'creazione' : 'annotazione',
82
+      'data' => $annotazione->created_at,
83
+      'titolo' => $isFirst ? 'Segnalazione creata' : 'Annotazione',
84
+      'descrizione' => $annotazione->text,
85
+      'icona' => $isFirst ? 'bx-flag' : 'bx-comment-dots',
86
+      'colore' => $isFirst ? 'primary' : 'info',
87
+      'dati' => [
88
+        'id' => $annotazione->id,
89
+        'user' => [
90
+          'isUser' => $annotazione->user ? true : false,
91
+          'nome' => $annotazione->user ? $annotazione->user->full_name : $annotazione->riferimento,
92
+          'email' => $annotazione->user ? $annotazione->user->email : $annotazione->email,
93
+          'telefono' => $annotazione->user ? $annotazione->user->telefono : $annotazione->telefono,
94
+        ],
95
+        'allegati' => $annotazione->allegatiAnnotazione->count()
96
+      ]
97
+      ]);
98
+      $isFirst = false;
99
+    }
100
+
101
+    // Eventi: Audit cambiamenti di stato
102
+    $audits = $segnalazione->audits()->where('event', 'updated')->orderBy('created_at', 'desc')->get();
103
+    foreach($audits as $audit) {
104
+      // Gestisce sia array che stringhe JSON
105
+      $oldValues = [];
106
+      if($audit->old_values) {
107
+        $oldValues = is_array($audit->old_values) ? $audit->old_values : (json_decode($audit->old_values, true) ?: []);
108
+      }
109
+
110
+      $newValues = [];
111
+      if($audit->new_values) {
112
+        $newValues = is_array($audit->new_values) ? $audit->new_values : (json_decode($audit->new_values, true) ?: []);
113
+      }
114
+
115
+      // Controlla se lo stato è stato modificato
116
+      if(isset($oldValues['stato']) || isset($newValues['stato'])) {
117
+        $statoVecchio = $oldValues['stato'] ?? null;
118
+        $statoNuovo = $newValues['stato'] ?? null;
119
+
120
+        // Se lo stato è cambiato (non solo creato)
121
+        if($statoVecchio !== $statoNuovo && $statoNuovo !== null) {
122
+          // Recupera le configurazioni degli stati
123
+          $configStatoVecchio = $statoVecchio ? \App\Models\ConfiguraSegnalazione::where('gruppo', 'stato')->where('value', $statoVecchio)->first() : null;
124
+          $configStatoNuovo = \App\Models\ConfiguraSegnalazione::where('gruppo', 'stato')->where('value', $statoNuovo)->first();
125
+
126
+          // Recupera l'utente che ha fatto la modifica
127
+          $utente = null;
128
+          if($audit->user_id && $audit->user_type) {
129
+            try {
130
+              $userModel = $audit->user_type;
131
+              $utente = $userModel::find($audit->user_id);
132
+            } catch(\Exception $e) {
133
+              // Ignora errori
134
+            }
135
+          }
136
+
137
+          $eventi->push([
138
+          'tipo' => 'stato',
139
+          'data' => $audit->created_at,
140
+          'titolo' => 'Cambio stato',
141
+          'descrizione' => ($configStatoVecchio ? $configStatoVecchio->label : 'N/A') . ' → ' . ($configStatoNuovo ? $configStatoNuovo->label : 'N/A'),
142
+          'icona' => $configStatoNuovo ? $configStatoNuovo->icona : 'bx-transfer',
143
+          'colore' => $configStatoNuovo ? $configStatoNuovo->colore : 'secondary',
144
+          'dati' => [
145
+          'stato_vecchio' => $statoVecchio,
146
+          'stato_vecchio_label' => $configStatoVecchio ? $configStatoVecchio->label : null,
147
+          'stato_vecchio_colore' => $configStatoVecchio ? $configStatoVecchio->colore : null,
148
+          'stato_nuovo' => $statoNuovo,
149
+          'stato_nuovo_label' => $configStatoNuovo ? $configStatoNuovo->label : null,
150
+          'stato_nuovo_colore' => $configStatoNuovo ? $configStatoNuovo->colore : null,
151
+          'user' => [
152
+            'isUser' => $utente ? true : false,
153
+            'nome' => $utente ? $utente->full_name : 'Sistema',
154
+            'email' => $utente ? $utente->email : null,
155
+            'telefono' => $utente ? $utente->telefono : null,
156
+          ],
157
+          'ip_address' => $audit->ip_address
158
+          ]
159
+          ]);
160
+        }
161
+      }
162
+    }
163
+
164
+    // Ordina per data (dal più recente al più vecchio per la timeline)
165
+    $eventi = $eventi->sortByDesc('data')->values();
166
+
167
+    // Tipi di eventi disponibili per i filtri
168
+    $tipiEventi = $eventi->pluck('tipo')->unique()->values();
169
+    @endphp
170
+
171
+
172
+    <!-- Header Card con Titolo e Stato -->
173
+    <div class="card mb-4" id="cardSegnalazionePrincipale">
174
+      <div class="card-header">
175
+        <div class="row g-3 align-items-start">
176
+          <div class="col-12 col-lg-8">
177
+            <h4 class="card-title mb-2">
178
+              <i class="bx bx-flag me-2"></i>{{ $segnalazione->titolo }}
179
+            </h4>
180
+            <div class="d-flex flex-wrap gap-2">
181
+              @if($configStato)
182
+              <span class="badge bg-label-{{ $configStato->colore }}">
183
+                <b>Stato:</b> <i class="bx icon-base {{ $configStato->icona }} me-1"></i>{{ $configStato->label }}
184
+              </span>
185
+              @endif
186
+              @if($configPriorita)
187
+              <span class="badge bg-label-{{ $configPriorita->colore }}">
188
+                <b>Priorità:</b> <i class="bx icon-base {{ $configPriorita->icona }} me-1"></i>{{ $configPriorita->label }}
189
+              </span>
190
+              @endif
191
+              @if($configTipo)
192
+              <span class="badge bg-label-{{ $configTipo->colore ?? 'primary' }}">
193
+                <b>Tipo:</b> <i class="bx {{ $configTipo->icona }} me-1"></i>{{ $configTipo->label }}
194
+              </span>
195
+              @endif
196
+            </div>
197
+          </div>
198
+        </div>
199
+      </div>
200
+    </div>
201
+
202
+    <!-- Timeline Unificata con Filtri -->
203
+    <div class="card mb-4">
204
+      <div class="card-header">
205
+        <div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
206
+          <h5 class="card-title mb-0">
207
+            <i class="bx bx-time me-2"></i>Timeline Completa
208
+          </h5>
209
+
210
+        </div>
211
+      </div>
212
+      <div class="card-body">
213
+
214
+        <!-- Timeline Eventi -->
215
+        @if($eventi->count() > 0)
216
+        <ul class="timeline mb-0" id="timelineEventi">
217
+          @foreach($eventi as $evento)
218
+          <li class="timeline-item timeline-item-transparent evento-timeline" data-tipo="{{ $evento['tipo'] }}">
219
+            <span class="timeline-point timeline-point-{{ $evento['colore'] }}"></span>
220
+            <div class="timeline-event">
221
+              <div class="timeline-header mb-2">
222
+                <div class="d-flex justify-content-between align-items-start flex-wrap gap-2">
223
+                  <div>
224
+                    <h6 class="mb-0">
225
+                      <i class="bx {{ $evento['icona'] }} me-2 text-{{ $evento['colore'] }}"></i>
226
+                      {{ $evento['titolo'] }}
227
+                    </h6>
228
+                    @if($evento['dati']['user'])
229
+                    @php
230
+                      $userNome = $evento['dati']['user']['nome'] ?? '';
231
+                      $userTelefono = $evento['dati']['user']['telefono'] ?? '';
232
+                      $userEmail = $evento['dati']['user']['email'] ?? '';
233
+                      $userIsAdmin = (bool)($evento['dati']['user']['isUser'] ?? false);
234
+                      $userIcon = $userIsAdmin ? 'bx-user-check' : 'bx-user';
235
+                      $userIconClass = $userIsAdmin ? 'text-primary' : '';
236
+                      $popoverContent = '<div class="small">';
237
+                      $popoverContent .= '<div class="fw-semibold">' . e($userNome) . '</div>';
238
+                      if ($userTelefono) {
239
+                        $popoverContent .= '<div><i class="bx bx-phone me-1"></i>' . e($userTelefono) . '</div>';
240
+                      }
241
+                      if ($userEmail) {
242
+                        $popoverContent .= '<div><i class="bx bx-envelope me-1"></i>' . e($userEmail) . '</div>';
243
+                      }
244
+                      $popoverContent .= '</div>';
245
+                    @endphp
246
+                    <small class="text-muted">
247
+                      <span role="button"
248
+                        tabindex="0"
249
+                        class="user-popover-trigger"
250
+                        data-bs-toggle="popover"
251
+                        data-bs-html="true"
252
+                        data-bs-trigger="focus"
253
+                        data-bs-placement="bottom"
254
+                        data-bs-content='{!! $popoverContent !!}'>
255
+                        <i class="bx {{ $userIcon }} me-1 {{ $userIconClass }}"></i>{{ $userNome }}
256
+                      </span>
257
+                    </small>
258
+                    @endif
259
+                    <small class="text-body-secondary">
260
+                      {{ $evento['data'] ? $evento['data']->diffForHumans() : '' }}
261
+                      @if($evento['data'])
262
+                      <span class="text-muted">({{ $evento['data']->format('d/m/Y H:i') }})</span>
263
+                      @endif
264
+                    </small>
265
+                  </div>
266
+                  <span class="badge bg-label-{{ $evento['colore'] }}">{{ ucfirst($evento['tipo']) }}</span>
267
+                </div>
268
+              </div>
269
+
270
+              @if($evento['tipo'] == 'creazione')
271
+              <div class="card border mb-2">
272
+                <div class="card-body">
273
+                  {{-- <h6 class="fw-semibold mb-2">{{ $evento['titolo'] }}</h6> --}}
274
+                  <p class="mb-2">{!! $evento['descrizione'] !!}</p>
275
+                  <div class="row g-2">
276
+
277
+                  </div>
278
+                </div>
279
+              </div>
280
+              @elseif($evento['tipo'] == 'annotazione')
281
+              <div class="card border mb-2">
282
+                <div class="card-body">
283
+                  <p class="mb-2">{!! $evento['descrizione'] !!}</p>
284
+                  @if($evento['dati']['allegati'] > 0)
285
+                  <span class="badge bg-label-secondary ms-2">
286
+                    <i class="bx bx-paperclip me-1"></i>{{ $evento['dati']['allegati'] }} allegato/i
287
+                  </span>
288
+                  @endif
289
+                </div>
290
+              </div>
291
+              @elseif($evento['tipo'] == 'allegato')
292
+              <div class="card border mb-2">
293
+                <div class="card-body">
294
+                  <div class="d-flex align-items-start gap-3">
295
+                    <div class="avatar flex-shrink-0">
296
+                      <div class="avatar-initial bg-label-primary rounded">
297
+                        <i class="bx {{ $evento['icona'] }}"></i>
298
+                      </div>
299
+                    </div>
300
+                    <div class="flex-grow-1">
301
+                      <h6 class="mb-1">{{ $evento['dati']['nome'] ?? 'Allegato' }}</h6>
302
+                      @if($evento['dati']['descrizione'])
303
+                      <p class="small text-muted mb-1">{!! $evento['dati']['descrizione'] !!}</p>
304
+                      @endif
305
+                      @if($evento['dati']['isImage'])
306
+                      <button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="openModalImmagineAllegato('{{ $evento['dati']['url'] }}', '{{ $evento['dati']['nome'] ?? 'Immagine' }}')">
307
+                        <i class="bx bx-image me-1"></i>Visualizza
308
+                      </button>
309
+                      @endif
310
+                    </div>
311
+                  </div>
312
+                </div>
313
+              </div>
314
+              @elseif($evento['tipo'] == 'stato')
315
+              <div class="card border mb-2">
316
+                <div class="card-body">
317
+                  <div class="d-flex align-items-center gap-3">
318
+                    <div class="flex-grow-1">
319
+                      <div class="d-flex align-items-center gap-2 mb-2">
320
+                        @if($evento['dati']['stato_vecchio_label'])
321
+                        <span class="badge bg-label-{{ $evento['dati']['stato_vecchio_colore'] ?? 'secondary' }}">
322
+                          <i class="bx {{ $evento['icona'] }} me-1"></i>{{ $evento['dati']['stato_vecchio_label'] }}
323
+                        </span>
324
+                        @else
325
+                        <span class="badge bg-label-secondary">N/A</span>
326
+                        @endif
327
+                        <i class="bx bx-right-arrow-alt text-muted"></i>
328
+                        @if($evento['dati']['stato_nuovo_label'])
329
+                        <span class="badge bg-label-{{ $evento['dati']['stato_nuovo_colore'] ?? 'primary' }}">
330
+                          <i class="bx {{ $evento['icona'] }} me-1"></i>{{ $evento['dati']['stato_nuovo_label'] }}
331
+                        </span>
332
+                        @else
333
+                        <span class="badge bg-label-secondary">N/A</span>
334
+                        @endif
335
+                      </div>
336
+                    </div>
337
+                    {{-- <div class="avatar">
338
+                      <div class="avatar-initial bg-label-{{ $evento['colore'] }} rounded">
339
+                        <i class="bx {{ $evento['icona'] }}"></i>
340
+                      </div>
341
+                    </div> --}}
342
+                  </div>
343
+                </div>
344
+              </div>
345
+              @endif
346
+            </div>
347
+          </li>
348
+          @endforeach
349
+        </ul>
350
+        @else
351
+        <div class="text-center py-5">
352
+          <div class="avatar-initial bg-label-secondary rounded mx-auto mb-3" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center;">
353
+            <i class="bx bx-time" style="font-size: 2.5rem;"></i>
354
+          </div>
355
+          <p class="text-muted mb-3">Nessun evento presente nella timeline</p>
356
+        </div>
357
+        @endif
358
+      </div>
359
+    </div>
360
+
361
+  </div>
362
+</div>
363
+
364
+<!-- Modale Visualizza Immagine -->
365
+<div class="modal fade" id="modalImmagineAllegato" tabindex="-1" aria-hidden="true">
366
+  <div class="modal-dialog modal-dialog-centered modal-xl">
367
+    <div class="modal-content bg-dark">
368
+      <div class="modal-header border-secondary">
369
+        <h5 class="modal-title text-white" id="modalImmagineTitle">
370
+          <i class="bx bx-image me-2"></i>Immagine
371
+        </h5>
372
+        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
373
+      </div>
374
+      <div class="modal-body p-0 text-center">
375
+        <img id="modalImmagineSrc" src="" alt="Immagine" class="img-fluid" style="max-height: 80vh; width: auto;">
376
+      </div>
377
+      <div class="modal-footer border-secondary justify-content-center">
378
+        <a id="modalImmagineDownload" href="" download class="btn btn-primary">
379
+          <i class="bx bx-download me-1"></i>Scarica Immagine
380
+        </a>
381
+        <button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal">
382
+          <i class="bx bx-x me-1"></i>Chiudi
383
+        </button>
384
+      </div>
385
+    </div>
386
+  </div>
387
+</div>
388
+
389
+<!-- Modale Visualizza/Modifica Annotazione -->
390
+
391
+
392
+@endsection
393
+
394
+@section('page-style')
395
+<style>
396
+  .user-popover-trigger {
397
+    cursor: pointer;
398
+    text-decoration: none;
399
+  }
400
+  .user-popover-trigger:hover {
401
+    text-decoration: underline;
402
+  }
403
+</style>
404
+@endsection
405
+
406
+@section('page-script')
407
+<script type="module">
408
+
409
+  $(document).ready(function(){
410
+    $.ajaxSetup({
411
+      headers: {
412
+        'X-CSRF-TOKEN': '{{csrf_token()}}'
413
+      }
414
+    });
415
+
416
+    if (window.bootstrap && window.bootstrap.Popover) {
417
+      const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
418
+      popoverTriggerList.forEach((popoverTriggerEl) => {
419
+        new bootstrap.Popover(popoverTriggerEl, { html: true, sanitize: false, trigger: 'focus' });
420
+      });
421
+    }
422
+
423
+  });
424
+
425
+  function openModalImmagineAllegato(imageUrl, imageName) {
426
+    var modalSrc = document.getElementById('modalImmagineSrc');
427
+    var modalTitle = document.getElementById('modalImmagineTitle');
428
+    var modalDownload = document.getElementById('modalImmagineDownload');
429
+
430
+    if (modalSrc) modalSrc.src = imageUrl;
431
+    if (modalTitle) modalTitle.innerHTML = '<i class="bx bx-image me-2"></i>' + imageName;
432
+    if (modalDownload) {
433
+      modalDownload.href = imageUrl;
434
+      modalDownload.download = imageName;
435
+    }
436
+  }
437
+
438
+
439
+</script>
440
+
441
+<script>
442
+</script>
443
+@endsection

+ 1
- 6
routes/web.php Прегледај датотеку

@@ -162,6 +162,7 @@ Route::middleware(['auth:sanctum', config('jetstream.auth_session')])->prefix('a
162 162
 
163 163
     // Dashboard
164 164
     Route::get('dashboard', [HomePageController::class, 'admin_dashboard'])->name('admin.dashboard');
165
+    Route::get('statistiche', [HomePageController::class, 'admin_statistiche'])->name('admin.statistiche');
165 166
 
166 167
     // Azienda
167 168
     Route::get('azienda', [AziendaController::class, 'admin_index'])->name('admin.azienda.index');
@@ -272,18 +273,12 @@ Route::get('acquisto-servizio/cancel/{token}', fn ($token) => redirect()->route(
272 273
 Route::post('webhook/stripe/acquisto-servizio', [AcquistoServizioPublicController::class, 'stripeWebhook']);
273 274
 Route::post('webhook/paypal/acquisto-servizio', [AcquistoServizioPublicController::class, 'paypalWebhook']);
274 275
 
275
-Route::get('segnalazione/reparto/{token}', [SegnalazioneController::class, 'reparto'])->name('segnalazione.reparto');
276
-Route::get('segnalazione/nuova/{token}', [SegnalazioneController::class, 'nuova'])->name('segnalazione.nuova');
277
-Route::post('segnalazione/crea', [SegnalazioneController::class, 'crea'])->name('segnalazione.crea');
278 276
 Route::get('segnalazione/show/{token}', [SegnalazioneController::class, 'show'])->name('segnalazione.show');
279 277
 
280 278
 Route::get('allegato_annotazione/show', [AllegatoAnnotazioneController::class, 'show'])->name('allegato_annotazione.show');
281 279
 Route::delete('allegato_annotazione/delete', [AllegatoAnnotazioneController::class, 'delete'])->name('allegato_annotazione.delete');
282 280
 Route::get('allegato_annotazione/download/{allegato_annotazione_id}', [AllegatoAnnotazioneController::class, 'download'])->name('allegato_annotazione.download');
283 281
 
284
-Route::get('segnalazione/{id}/pdf', [SegnalazioneController::class, 'esportaPdf'])->name('segnalazione.pdf');
285
-Route::get('segnalazione/{id}/pdf-preview', [SegnalazioneController::class, 'previewPdf'])->name('segnalazione.pdf-preview');
286
-
287 282
 Route::get('test', function () {
288 283
     // event(new \App\Events\NuovaSegnalazione(\App\Models\Segnalazione::find(12)));
289 284
     $configNotifiche = \App\Models\ConfigNotifiche::where('event', \App\Models\ConfigNotifiche::NUOVA_SEGNALAZIONE_ADMIN)->first();

Loading…
Откажи
Сачувај