Explorar el Código

Report, bilancio e vetrina pubblica attivita.

Allinea dashboard incassi e le pagine guest senza toccare il pay path.

Co-authored-by: Cursor <cursoragent@cursor.com>
marcofalabretti hace 2 semanas
padre
commit
6e9ae1ff56
Se han modificado 29 ficheros con 5851 adiciones y 2397 borrados
  1. 15
    4
      app/Http/Controllers/BilancioController.php
  2. 916
    166
      app/Http/Controllers/ReportController.php
  3. 4
    1
      app/Services/Bilancio/BilancioServizioService.php
  4. 6
    0
      config/filesystems.php
  5. 1
    27
      package-lock.json
  6. 2
    0
      resources/css/guest-client.css
  7. 142
    205
      resources/css/welcome.css
  8. 42
    0
      resources/views/attivita/public/_partials/eventi-prenotabili.blade.php
  9. 410
    0
      resources/views/attivita/public/_partials/public-overrides.blade.php
  10. 245
    512
      resources/views/attivita/public/show.blade.php
  11. 610
    189
      resources/views/attivita/show.blade.php
  12. 43
    18
      resources/views/bilancio/_partials/costi.blade.php
  13. 43
    18
      resources/views/bilancio/_partials/ricavi.blade.php
  14. 132
    88
      resources/views/bilancio/_partials/sintesi.blade.php
  15. 111
    36
      resources/views/bilancio/_partials/statistiche.blade.php
  16. 69
    32
      resources/views/bilancio/index.blade.php
  17. 193
    221
      resources/views/report/_partials/cucine.blade.php
  18. 222
    239
      resources/views/report/_partials/incassi.blade.php
  19. 348
    267
      resources/views/report/_partials/movimenti.blade.php
  20. 194
    0
      resources/views/report/_partials/staff.blade.php
  21. 1295
    233
      resources/views/report/index.blade.php
  22. 17
    8
      resources/views/report/pdf.blade.php
  23. 179
    0
      resources/views/report/pdf/_styles.blade.php
  24. 127
    0
      resources/views/report/pdf/cucine.blade.php
  25. 189
    0
      resources/views/report/pdf/incassi.blade.php
  26. 174
    0
      resources/views/report/pdf/movimenti.blade.php
  27. 8
    18
      resources/views/testi/show.blade.php
  28. 110
    113
      resources/views/welcome.blade.php
  29. 4
    2
      resources/views/welcome/_partials/attivita-card.blade.php

+ 15
- 4
app/Http/Controllers/BilancioController.php Ver fichero

@@ -35,6 +35,9 @@ class BilancioController extends Controller
35 35
     public function oggi(Request $request, BilancioServizioService $bilancioServizio)
36 36
     {
37 37
         $attivitaId = (int) session()->get('attivita_attuale');
38
+        if ($attivitaId <= 0 && ! auth()->user()?->hasRole('superadmin')) {
39
+            $attivitaId = -1;
40
+        }
38 41
         $attivita = Attivita::find($attivitaId);
39 42
         $period = $bilancioServizio->currentPeriod($attivitaId);
40 43
         $periodStart = $period['start'];
@@ -62,6 +65,9 @@ class BilancioController extends Controller
62 65
     public function index(Request $request, BilancioServizioService $bilancioServizio)
63 66
     {
64 67
         $attivitaId = (int) session()->get('attivita_attuale');
68
+        if ($attivitaId <= 0 && ! auth()->user()?->hasRole('superadmin')) {
69
+            $attivitaId = -1;
70
+        }
65 71
         $attivita = Attivita::find($attivitaId);
66 72
 
67 73
         $year = (int) $request->get('anno', now()->year);
@@ -82,12 +88,9 @@ class BilancioController extends Controller
82 88
             ->get();
83 89
 
84 90
         $movimenti = $movimentiCollection->map(function ($movimento) {
85
-            $tipoRaw = strtolower((string) ($movimento->tipo_movimento ?? ''));
86
-            $isUscita = $tipoRaw === 'uscita' || ((float) ($movimento->importo ?? 0)) < 0;
87
-
88 91
             return [
89 92
                 'data' => $movimento->created_at,
90
-                'tipo' => $isUscita ? 'uscita' : 'entrata',
93
+                'tipo' => $movimento->tipoPerAggregazione(),
91 94
                 'importo' => abs((float) ($movimento->importo ?? 0)),
92 95
                 'pagamento_id' => (int) ($movimento->pagamento_id ?? 0),
93 96
                 'fornitore_id' => (int) ($movimento->fornitore_id ?? 0),
@@ -105,6 +108,8 @@ class BilancioController extends Controller
105 108
 
106 109
         $ricaviTotali = (float) $movimenti->where('tipo', 'entrata')->sum('importo');
107 110
         $costiTotali = (float) $movimenti->where('tipo', 'uscita')->sum('importo');
111
+        $importoNonContabilizzato = (float) $movimenti->where('tipo', 'no_contabile')->sum('importo');
112
+        $movimentiNonContabilizzatiCount = (int) $movimenti->where('tipo', 'no_contabile')->count();
108 113
 
109 114
         // Fallback per ricavi se la prima nota non contiene entrate.
110 115
         $pagamentiCollection = Pagamento::query()
@@ -113,6 +118,7 @@ class BilancioController extends Controller
113 118
                 $query->where('attivita_id', $attivitaId);
114 119
             })
115 120
             ->whereBetween('created_at', [$yearStart, $yearEnd])
121
+            ->perIncasso()
116 122
             ->orderBy('created_at')
117 123
             ->get();
118 124
 
@@ -145,6 +151,9 @@ class BilancioController extends Controller
145 151
                 continue;
146 152
             }
147 153
             $row = $mesiBase->get($monthKey);
154
+            if ($movimento['tipo'] === 'no_contabile') {
155
+                continue;
156
+            }
148 157
             if ($movimento['tipo'] === 'entrata') {
149 158
                 $row['ricavi'] += $movimento['importo'];
150 159
             } else {
@@ -377,6 +386,8 @@ class BilancioController extends Controller
377 386
             'costiTotali' => $costiTotali,
378 387
             'saldoTotale' => $saldo,
379 388
             'movimentiCount' => $movimenti->count(),
389
+            'importoNonContabilizzato' => $importoNonContabilizzato,
390
+            'movimentiNonContabilizzatiCount' => $movimentiNonContabilizzatiCount,
380 391
             'andamentoMensile' => $andamentoMensile,
381 392
             'costiRows' => $costiRows,
382 393
             'ricaviRows' => $ricaviRows,

+ 916
- 166
app/Http/Controllers/ReportController.php
La diferencia del archivo ha sido suprimido porque es demasiado grande
Ver fichero


+ 4
- 1
app/Services/Bilancio/BilancioServizioService.php Ver fichero

@@ -21,7 +21,9 @@ class BilancioServizioService
21 21
                 $query
22 22
                     ->where('attivita_id', $attivitaId)
23 23
                     ->whereBetween('created_at', [$start, $end])
24
-                    ->whereHas('pagamenti');
24
+                    ->whereHas('pagamenti', function ($pagamentiQuery) {
25
+                        $pagamentiQuery->perIncasso();
26
+                    });
25 27
             })
26 28
             ->get();
27 29
     }
@@ -117,6 +119,7 @@ class BilancioServizioService
117 119
             ->with(['metodo_pagamento', 'ordine.righe_ordine.piatto.cucina'])
118 120
             ->where('attivita_id', $attivitaId)
119 121
             ->whereBetween('created_at', [$start, $end])
122
+            ->perIncasso()
120 123
             ->get();
121 124
 
122 125
         $paymentSummary = $pagamenti

+ 6
- 0
config/filesystems.php Ver fichero

@@ -88,6 +88,12 @@ return [
88 88
             'url' => env('APP_URL').'/storage/festAgent',
89 89
             'visibility' => 'public',
90 90
         ],
91
+        'templateCupon' => [
92
+            'driver' => 'local',
93
+            'root' => storage_path('app/public/templateCupon'),
94
+            'url' => env('APP_URL').'/storage/templateCupon',
95
+            'visibility' => 'public',
96
+        ],
91 97
         's3' => [
92 98
             'driver' => 's3',
93 99
             'key' => env('AWS_ACCESS_KEY_ID'),

+ 1
- 27
package-lock.json Ver fichero

@@ -5724,26 +5724,6 @@
5724 5724
         "node": ">= 6"
5725 5725
       }
5726 5726
     },
5727
-    "node_modules/caniuse-lite": {
5728
-      "version": "1.0.30001734",
5729
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz",
5730
-      "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==",
5731
-      "dev": true,
5732
-      "funding": [
5733
-        {
5734
-          "type": "opencollective",
5735
-          "url": "https://opencollective.com/browserslist"
5736
-        },
5737
-        {
5738
-          "type": "tidelift",
5739
-          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
5740
-        },
5741
-        {
5742
-          "type": "github",
5743
-          "url": "https://github.com/sponsors/ai"
5744
-        }
5745
-      ]
5746
-    },
5747 5727
     "node_modules/chalk": {
5748 5728
       "version": "4.1.2",
5749 5729
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -18131,12 +18111,6 @@
18131 18111
       "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
18132 18112
       "dev": true
18133 18113
     },
18134
-    "caniuse-lite": {
18135
-      "version": "1.0.30001734",
18136
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz",
18137
-      "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==",
18138
-      "dev": true
18139
-    },
18140 18114
     "chalk": {
18141 18115
       "version": "4.1.2",
18142 18116
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -24241,4 +24215,4 @@
24241 24215
       "dev": true
24242 24216
     }
24243 24217
   }
24244
-}
24218
+}

+ 2
- 0
resources/css/guest-client.css Ver fichero

@@ -361,7 +361,9 @@
361 361
 
362 362
 .guest-fest-actions {
363 363
   display: flex;
364
+  flex-wrap: wrap;
364 365
   justify-content: center;
366
+  gap: 0.5rem;
365 367
   padding: 0.5rem 0 0;
366 368
   margin-top: 0.35rem;
367 369
   border-top: 1px solid var(--fest-border);

+ 142
- 205
resources/css/welcome.css Ver fichero

@@ -1,5 +1,5 @@
1 1
 /**
2
- * Welcome — bacheca attività attive
2
+ * Welcome — bacheca (allineata a evento/prenota public)
3 3
  */
4 4
 
5 5
 .welcome-bacheca {
@@ -7,58 +7,79 @@
7 7
   --fest-orange-soft: #ff9a2e;
8 8
   --fest-purple: #602d91;
9 9
   --fest-navy: #15265c;
10
-  --fest-text: #1a2744;
11
-  --fest-text-muted: #4a5568;
12
-  --fest-surface: #eef1f7;
10
+  --fest-text: #15265c;
11
+  --fest-text-muted: #5b6579;
12
+  --fest-surface: #ffffff;
13 13
   --fest-card: #ffffff;
14
-  --fest-radius: 1.15rem;
15
-  --fest-shadow: 0 4px 24px rgba(21, 38, 92, 0.08), 0 1px 3px rgba(21, 38, 92, 0.06);
14
+  --fest-radius: 0.85rem;
15
+  --fest-shadow: 0 2px 12px rgba(21, 38, 92, 0.06);
16
+  --evento-accent: var(--fest-orange);
17
+  --evento-navy: var(--fest-navy);
18
+  --evento-muted: var(--fest-text-muted);
19
+  --evento-purple: var(--fest-purple);
20
+  --evento-border: rgba(21, 38, 92, 0.1);
21
+  --evento-gutter: clamp(0.85rem, 3vw, 1.25rem);
16 22
 
23
+  position: relative;
17 24
   width: 100%;
18
-  max-width: 72rem;
19
-  margin-inline: auto;
20
-  padding: 0 0.75rem 2.5rem;
25
+  max-width: 100%;
26
+  min-height: 100vh;
27
+  min-height: 100dvh;
28
+  display: flex;
29
+  flex-direction: column;
21 30
   font-family: "Plus Jakarta Sans", "Public Sans", system-ui, sans-serif;
22 31
   color: var(--fest-text);
32
+  overflow-x: hidden;
23 33
 }
24 34
 
35
+html.welcome-bacheca-page,
36
+html.welcome-bacheca-page body {
37
+  height: 100%;
38
+  margin: 0;
39
+  background: #f4f5f9;
40
+}
41
+
42
+html.welcome-bacheca-page .layout-wrapper,
43
+html.welcome-bacheca-page .content-wrapper,
44
+html.welcome-bacheca-page .layout-page,
45
+html.welcome-bacheca-page .container-xxl {
46
+  padding: 0 !important;
47
+  margin: 0 !important;
48
+  max-width: 100% !important;
49
+}
50
+
51
+html.welcome-bacheca-page .d-flex.flex-column.min-vh-100 {
52
+  min-height: 100vh !important;
53
+  min-height: 100dvh !important;
54
+}
55
+
56
+html.welcome-bacheca-page .misc-wrapper,
25 57
 .misc-wrapper.misc-wrapper--welcome-bacheca {
26
-  text-align: start;
27
-  align-items: flex-start;
28
-  justify-content: flex-start;
29
-  padding: 1rem 0 2rem;
58
+  display: flex !important;
59
+  flex-direction: column !important;
60
+  min-height: 100vh;
61
+  min-height: 100dvh;
62
+  width: 100%;
30 63
   max-width: 100%;
31
-  min-block-size: auto;
64
+  padding: 0;
65
+  margin: 0;
66
+  align-items: stretch !important;
67
+  justify-content: flex-start !important;
68
+  text-align: start !important;
32 69
   background: transparent;
33 70
 }
34 71
 
35
-html.welcome-bacheca-page,
36
-html.welcome-bacheca-page body {
37
-  background: #f6f7fb;
72
+html.welcome-bacheca-page .row.mt-3.mb-3,
73
+html.welcome-bacheca-page footer:not(.welcome-bacheca__footer) {
74
+  display: none !important;
38 75
 }
39 76
 
40
-/*
41
- * Prova colorazione sfondo — varianti:
42
- *   default (nessun attributo) = colorato Fest
43
- *   data-bg="soft"  = più tenue
44
- *   data-bg="bold"  = più saturo
45
- */
46 77
 .welcome-bacheca__bg {
47
-  --bg-warm: #fff6ee;
48
-  --bg-mid: #f3ebf8;
49
-  --bg-cool: #e6edf8;
50
-
51 78
   position: fixed;
52 79
   inset: 0;
53
-  z-index: -1;
54
-  overflow: hidden;
80
+  z-index: 0;
55 81
   pointer-events: none;
56
-  background: linear-gradient(
57
-    145deg,
58
-    var(--bg-warm) 0%,
59
-    var(--bg-mid) 38%,
60
-    var(--bg-cool) 100%
61
-  );
82
+  background: linear-gradient(150deg, #fff7f0 0%, #f3ebf8 42%, #e8eef8 100%);
62 83
 }
63 84
 
64 85
 .welcome-bacheca__bg::before {
@@ -66,154 +87,81 @@ html.welcome-bacheca-page body {
66 87
   position: absolute;
67 88
   inset: 0;
68 89
   background:
69
-    radial-gradient(ellipse 75% 55% at 8% 18%, rgba(245, 130, 32, 0.38) 0%, transparent 58%),
70
-    radial-gradient(ellipse 70% 50% at 95% 82%, rgba(96, 45, 145, 0.32) 0%, transparent 55%),
71
-    radial-gradient(ellipse 55% 40% at 55% 0%, rgba(21, 38, 92, 0.14) 0%, transparent 50%),
72
-    radial-gradient(ellipse 45% 35% at 72% 42%, rgba(245, 130, 32, 0.12) 0%, transparent 48%);
73
-  pointer-events: none;
74
-}
75
-
76
-.welcome-bacheca__bg::after {
77
-  content: "";
78
-  position: absolute;
79
-  inset: 0;
80
-  background: linear-gradient(
81
-    118deg,
82
-    rgba(245, 130, 32, 0.07) 0%,
83
-    transparent 32%,
84
-    transparent 58%,
85
-    rgba(96, 45, 145, 0.09) 78%,
86
-    rgba(21, 38, 92, 0.06) 100%
87
-  );
88
-  pointer-events: none;
89
-}
90
-
91
-.welcome-bacheca__bg[data-bg="soft"] {
92
-  --bg-warm: #faf8fc;
93
-  --bg-mid: #f4f2f8;
94
-  --bg-cool: #eef1f8;
90
+    radial-gradient(ellipse 70% 45% at 12% 12%, color-mix(in srgb, var(--evento-accent) 28%, transparent) 0%, transparent 55%),
91
+    radial-gradient(ellipse 55% 40% at 90% 80%, rgba(96, 45, 145, 0.18) 0%, transparent 52%);
95 92
 }
96 93
 
97
-.welcome-bacheca__bg[data-bg="soft"]::before {
98
-  opacity: 0.55;
99
-}
100
-
101
-.welcome-bacheca__bg[data-bg="bold"] {
102
-  --bg-warm: #ffe8d4;
103
-  --bg-mid: #ecd8f5;
104
-  --bg-cool: #d8e4f8;
105
-}
106
-
107
-.welcome-bacheca__bg[data-bg="bold"]::before {
108
-  background:
109
-    radial-gradient(ellipse 80% 60% at 5% 15%, rgba(245, 130, 32, 0.55) 0%, transparent 55%),
110
-    radial-gradient(ellipse 75% 55% at 98% 88%, rgba(96, 45, 145, 0.48) 0%, transparent 52%),
111
-    radial-gradient(ellipse 50% 40% at 50% 5%, rgba(21, 38, 92, 0.22) 0%, transparent 48%);
94
+.welcome-bacheca__top {
95
+  position: sticky;
96
+  top: 0;
97
+  z-index: 20;
98
+  display: flex;
99
+  align-items: center;
100
+  justify-content: space-between;
101
+  gap: 1rem;
102
+  width: 100%;
103
+  padding: calc(0.75rem + env(safe-area-inset-top, 0px)) var(--evento-gutter) 0.75rem;
104
+  background: rgba(255, 255, 255, 0.92);
105
+  backdrop-filter: blur(10px);
106
+  border-bottom: 1px solid var(--evento-border);
112 107
 }
113 108
 
114
-.welcome-bacheca__bg-stripe {
109
+.welcome-bacheca__top::after {
110
+  content: "";
115 111
   position: absolute;
116
-  top: 0;
117 112
   left: 0;
118 113
   right: 0;
119
-  height: 6px;
120
-  z-index: 2;
121
-  background: linear-gradient(
122
-    90deg,
123
-    var(--fest-orange) 0%,
124
-    var(--fest-purple) 50%,
125
-    var(--fest-navy) 100%
126
-  );
127
-  box-shadow: 0 3px 16px rgba(96, 45, 145, 0.25);
128
-}
129
-
130
-.welcome-bacheca__bg-pattern {
131
-  position: absolute;
132
-  inset: 0;
133
-  opacity: 0.55;
134
-  background-image: url("data:image/svg+xml,%3Csvg width='56' height='56' viewBox='0 0 56 56' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M28 4L52 28L28 52L4 28Z' stroke='%23f58220' stroke-opacity='.22' stroke-width='.7' fill='none'/%3E%3Cpath d='M28 12L44 28L28 44L12 28Z' stroke='%23602d91' stroke-opacity='.18' stroke-width='.55' fill='none'/%3E%3Ccircle cx='28' cy='28' r='1.35' fill='%2315265c' fill-opacity='.15'/%3E%3C/svg%3E");
135
-  background-size: 56px 56px;
136
-  mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.35) 50%, transparent 100%);
137
-}
138
-
139
-.welcome-bacheca__bg-glow {
140
-  position: absolute;
141
-  border-radius: 50%;
142
-  filter: blur(80px);
143
-  will-change: transform;
144
-  mix-blend-mode: multiply;
145
-}
146
-
147
-.welcome-bacheca__bg-glow--orange {
148
-  width: min(58vw, 520px);
149
-  height: min(58vw, 520px);
150
-  top: -14%;
151
-  right: -10%;
152
-  background: rgba(245, 130, 32, 0.45);
153
-  mix-blend-mode: normal;
154
-}
155
-
156
-.welcome-bacheca__bg-glow--purple {
157
-  width: min(54vw, 460px);
158
-  height: min(54vw, 460px);
159
-  bottom: -12%;
160
-  left: -8%;
161
-  background: rgba(96, 45, 145, 0.38);
162
-  mix-blend-mode: normal;
163
-}
164
-
165
-.welcome-bacheca__bg-glow--navy {
166
-  width: min(44vw, 400px);
167
-  height: min(44vw, 400px);
168
-  top: 42%;
169
-  left: 48%;
170
-  transform: translate(-50%, -50%);
171
-  background: rgba(21, 38, 92, 0.18);
172
-  filter: blur(100px);
173
-  mix-blend-mode: normal;
114
+  bottom: 0;
115
+  height: 3px;
116
+  background: linear-gradient(90deg, var(--evento-accent) 0%, var(--evento-purple) 55%, var(--evento-navy) 100%);
117
+  opacity: 0.85;
174 118
 }
175 119
 
176
-/* Masthead compatto */
177
-.welcome-bacheca__masthead {
178
-  margin-bottom: 1.25rem;
179
-  padding: 0 0.15rem;
180
-  background: transparent;
181
-  border: none;
182
-  box-shadow: none;
120
+.welcome-bacheca__top-link {
121
+  display: inline-flex;
122
+  align-items: center;
123
+  gap: 0.35rem;
124
+  font-size: 0.88rem;
125
+  font-weight: 600;
126
+  color: var(--evento-navy);
127
+  text-decoration: none;
128
+  max-width: 70%;
183 129
 }
184 130
 
185
-.welcome-bacheca__masthead-row {
186
-  display: flex;
187
-  flex-wrap: wrap;
188
-  align-items: center;
189
-  justify-content: space-between;
190
-  gap: 0.75rem;
191
-  margin-bottom: 0.85rem;
192
-  padding-bottom: 0;
193
-  border-bottom: none;
131
+.welcome-bacheca__top-link span {
132
+  overflow: hidden;
133
+  text-overflow: ellipsis;
134
+  white-space: nowrap;
194 135
 }
195 136
 
196
-.welcome-bacheca__brand {
197
-  max-width: min(200px, 50vw);
198
-  flex-shrink: 0;
137
+.welcome-bacheca__top-link:hover {
138
+  color: var(--evento-accent);
199 139
 }
200 140
 
201
-.welcome-bacheca__logo {
141
+.welcome-bacheca__brand-logo {
202 142
   display: block;
203
-  width: 100%;
204
-  height: auto;
205
-  max-height: 3.25rem;
143
+  height: 1.55rem !important;
144
+  width: auto !important;
145
+  max-height: 1.55rem !important;
146
+  max-width: 7.5rem;
206 147
   object-fit: contain;
207
-  object-position: left center;
148
+  opacity: 0.92;
208 149
 }
209 150
 
210
-.welcome-bacheca__masthead-actions {
151
+.welcome-bacheca__shell {
152
+  position: relative;
153
+  z-index: 2;
154
+  width: 100%;
155
+  max-width: 72rem;
156
+  margin: 0 auto;
157
+  padding: 1.15rem var(--evento-gutter) 2rem;
211 158
   display: flex;
212
-  flex-wrap: wrap;
213
-  gap: 0.5rem;
159
+  flex-direction: column;
160
+  flex: 1;
214 161
 }
215 162
 
216
-.welcome-bacheca__masthead-body {
163
+.welcome-bacheca__intro {
164
+  margin-bottom: 1.15rem;
217 165
   max-width: 40rem;
218 166
 }
219 167
 
@@ -222,19 +170,19 @@ html.welcome-bacheca-page body {
222 170
   align-items: center;
223 171
   justify-content: center;
224 172
   gap: 0.35rem;
225
-  padding: 0.5rem 1rem;
173
+  padding: 0.55rem 1.1rem;
226 174
   font-size: 0.875rem;
227 175
   font-weight: 600;
228
-  border-radius: 0.5rem;
176
+  border-radius: 0.65rem;
229 177
   text-decoration: none;
230 178
   border: 1px solid transparent;
231
-  transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
179
+  transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
232 180
 }
233 181
 
234 182
 .welcome-bacheca__btn--ghost {
235 183
   color: var(--fest-navy);
236 184
   background: #fff;
237
-  border-color: rgba(21, 38, 92, 0.2);
185
+  border-color: rgba(21, 38, 92, 0.16);
238 186
 }
239 187
 
240 188
 .welcome-bacheca__btn--ghost:hover {
@@ -252,14 +200,15 @@ html.welcome-bacheca-page body {
252 200
 .welcome-bacheca__btn--primary:hover {
253 201
   color: #fff;
254 202
   box-shadow: 0 4px 16px rgba(245, 130, 32, 0.38);
203
+  transform: translateY(-1px);
255 204
 }
256 205
 
257 206
 .welcome-bacheca__eyebrow {
258 207
   display: inline-flex;
259 208
   align-items: center;
260 209
   gap: 0.45rem;
261
-  margin: 0 0 0.65rem;
262
-  font-size: 0.75rem;
210
+  margin: 0 0 0.45rem;
211
+  font-size: 0.72rem;
263 212
   font-weight: 700;
264 213
   letter-spacing: 0.12em;
265 214
   text-transform: uppercase;
@@ -281,19 +230,19 @@ html.welcome-bacheca-page body {
281 230
 }
282 231
 
283 232
 .welcome-bacheca__title {
284
-  font-size: clamp(1.2rem, 3vw, 1.45rem);
285
-  font-weight: 700;
233
+  font-size: clamp(1.45rem, 3.5vw, 1.85rem);
234
+  font-weight: 800;
286 235
   color: var(--fest-navy);
287 236
   margin: 0;
288
-  line-height: 1.2;
289
-  letter-spacing: -0.02em;
237
+  line-height: 1.15;
238
+  letter-spacing: -0.03em;
290 239
 }
291 240
 
292 241
 .welcome-bacheca__lead {
293
-  font-size: 1rem;
294
-  line-height: 1.6;
242
+  font-size: 0.95rem;
243
+  line-height: 1.55;
295 244
   color: var(--fest-text-muted);
296
-  margin: 0;
245
+  margin: 0.45rem 0 0;
297 246
   max-width: 32rem;
298 247
 }
299 248
 
@@ -307,11 +256,11 @@ html.welcome-bacheca-page body {
307 256
   align-items: center;
308 257
   gap: 0.55rem;
309 258
   margin-bottom: 0.85rem;
310
-  padding: 0.55rem 0.85rem;
311
-  border-radius: 0.75rem;
312
-  background: rgba(255, 255, 255, 0.92);
313
-  border: 1px solid rgba(21, 38, 92, 0.1);
314
-  box-shadow: 0 2px 10px rgba(21, 38, 92, 0.05);
259
+  padding: 0.6rem 0.95rem;
260
+  border-radius: 0.85rem;
261
+  background: #fff;
262
+  border: 1px solid rgba(21, 38, 92, 0.06);
263
+  box-shadow: 0 2px 12px rgba(21, 38, 92, 0.06);
315 264
 }
316 265
 
317 266
 .welcome-bacheca__search i {
@@ -371,10 +320,10 @@ html.welcome-bacheca-page body {
371 320
   gap: 0.85rem;
372 321
   min-height: 0;
373 322
   padding: 0.85rem 1rem;
374
-  border-radius: 0.9rem;
323
+  border-radius: 0.85rem;
375 324
   background: #fff;
376
-  border: 1px solid rgba(21, 38, 92, 0.1);
377
-  box-shadow: 0 2px 10px rgba(21, 38, 92, 0.04);
325
+  border: 1px solid rgba(21, 38, 92, 0.06);
326
+  box-shadow: 0 2px 12px rgba(21, 38, 92, 0.06);
378 327
   transition: border-color 0.2s ease, box-shadow 0.2s ease;
379 328
 }
380 329
 
@@ -873,11 +822,11 @@ html.welcome-bacheca-page body {
873 822
 /* Empty */
874 823
 .welcome-bacheca__empty {
875 824
   text-align: center;
876
-  padding: 3.5rem 1.5rem;
877
-  background: rgba(255, 255, 255, 0.92);
878
-  border-radius: var(--fest-radius);
879
-  border: 1px dashed rgba(21, 38, 92, 0.12);
880
-  box-shadow: var(--fest-shadow);
825
+  padding: 3rem 1.35rem;
826
+  background: #fff;
827
+  border-radius: 0.85rem;
828
+  border: 1px solid rgba(21, 38, 92, 0.06);
829
+  box-shadow: 0 2px 12px rgba(21, 38, 92, 0.06);
881 830
 }
882 831
 
883 832
 .welcome-bacheca__empty-icon {
@@ -909,9 +858,10 @@ html.welcome-bacheca-page body {
909 858
 
910 859
 /* Footer */
911 860
 .welcome-bacheca__footer {
912
-  margin-top: 2.5rem;
913
-  font-size: 0.8rem;
914
-  color: #6b7280;
861
+  margin-top: auto;
862
+  padding-top: 2rem;
863
+  font-size: 0.78rem;
864
+  color: var(--fest-text-muted);
915 865
   text-align: center;
916 866
 }
917 867
 
@@ -1202,21 +1152,8 @@ button.welcome-bacheca__action {
1202 1152
 }
1203 1153
 
1204 1154
 @media (max-width: 575.98px) {
1205
-  .welcome-bacheca__masthead {
1206
-    padding: 1.1rem 1rem 1.25rem;
1207
-  }
1208
-
1209
-  .welcome-bacheca__masthead-row {
1155
+  .welcome-bacheca__intro {
1210 1156
     margin-bottom: 1rem;
1211
-    padding-bottom: 1rem;
1212
-  }
1213
-
1214
-  .welcome-bacheca__masthead-actions {
1215
-    width: 100%;
1216
-  }
1217
-
1218
-  .welcome-bacheca__masthead-actions .welcome-bacheca__btn {
1219
-    flex: 1;
1220 1157
   }
1221 1158
 
1222 1159
   .welcome-bacheca__grid {
@@ -1251,6 +1188,6 @@ button.welcome-bacheca__action {
1251 1188
   }
1252 1189
 
1253 1190
   .welcome-bacheca__org-stage-body {
1254
-    padding: 1rem;
1191
+    padding: 0;
1255 1192
   }
1256 1193
 }

+ 42
- 0
resources/views/attivita/public/_partials/eventi-prenotabili.blade.php Ver fichero

@@ -0,0 +1,42 @@
1
+@php
2
+  use Carbon\Carbon;
3
+  /** @var \Illuminate\Support\Collection<int, \App\Models\Evento> $eventiPubblici */
4
+@endphp
5
+
6
+@if($eventiPubblici->isNotEmpty())
7
+  <div class="attivita-public__eventi mt-3">
8
+    <p class="attivita-public__actions-hint mb-2">Prenotazioni evento</p>
9
+    @foreach($eventiPubblici as $evento)
10
+      @php
11
+        $prenotabile = $evento->isPrenotabileOra();
12
+        $dataInizio = $evento->data_inizio ? Carbon::parse($evento->data_inizio) : null;
13
+        $dataFine = $evento->data_fine ? Carbon::parse($evento->data_fine) : null;
14
+        $periodo = $dataInizio
15
+          ? ($dataFine && ! $dataInizio->isSameDay($dataFine)
16
+            ? $dataInizio->format('d/m/Y').' – '.$dataFine->format('d/m/Y')
17
+            : $dataInizio->format('d/m/Y'))
18
+          : null;
19
+      @endphp
20
+      <a
21
+        href="{{ $evento->publicUrl() }}"
22
+        class="attivita-public__action {{ $prenotabile ? 'attivita-public__action--evento-open' : '' }}"
23
+      >
24
+        <span class="attivita-public__action-icon attivita-public__action-icon--event">
25
+          <i class="bx bx-calendar-event"></i>
26
+        </span>
27
+        <span class="flex-grow-1 min-w-0">
28
+          <strong class="text-truncate d-block">{{ $evento->nome }}</strong>
29
+          @if($periodo)
30
+            <small>{{ $periodo }}</small>
31
+          @endif
32
+          @if($prenotabile)
33
+            <small class="d-block text-success fw-semibold mt-1">Prenotazioni aperte</small>
34
+          @else
35
+            <small class="d-block text-muted mt-1">{{ $evento->messaggioStatoPrenotazione() }}</small>
36
+          @endif
37
+        </span>
38
+        <i class="bx bx-chevron-right text-muted flex-shrink-0"></i>
39
+      </a>
40
+    @endforeach
41
+  </div>
42
+@endif

+ 410
- 0
resources/views/attivita/public/_partials/public-overrides.blade.php Ver fichero

@@ -0,0 +1,410 @@
1
+<style>
2
+  .attivita-public.evento-public {
3
+    font-family: "Plus Jakarta Sans", system-ui, sans-serif;
4
+    padding-bottom: 2rem;
5
+  }
6
+
7
+  .attivita-public.evento-public:has(.evento-public__sticky-cta) {
8
+    padding-bottom: calc(5.5rem + env(safe-area-inset-bottom, 0px));
9
+  }
10
+
11
+  .attivita-public .evento-public__back {
12
+    padding: 0.35rem 0.8rem 0.35rem 0.55rem;
13
+    border-radius: 999px;
14
+    background: rgba(255, 255, 255, 0.9);
15
+    box-shadow: 0 2px 10px rgba(21, 38, 92, 0.06);
16
+  }
17
+
18
+  .attivita-public__heading {
19
+    margin: 0 0 0.35rem;
20
+    font-family: "Outfit", "Plus Jakarta Sans", system-ui, sans-serif;
21
+    font-size: 1.2rem;
22
+    font-weight: 800;
23
+    letter-spacing: -0.02em;
24
+    line-height: 1.15;
25
+    color: var(--evento-navy);
26
+    text-transform: none;
27
+    border: 0;
28
+    padding: 0;
29
+  }
30
+
31
+  .attivita-public__lede {
32
+    margin: 0 0 0.85rem;
33
+    font-size: 0.88rem;
34
+    font-weight: 500;
35
+    line-height: 1.4;
36
+    color: var(--evento-muted);
37
+  }
38
+
39
+  /* ── Hero attività ── */
40
+
41
+  @media (min-width: 768px) {
42
+    .attivita-public.evento-public:has(.evento-public__sticky-cta) {
43
+      padding-bottom: 2rem;
44
+    }
45
+  }
46
+
47
+  .attivita-public-hero.evento-public__hero {
48
+    min-height: clamp(280px, 52vw, 400px);
49
+  }
50
+
51
+  .attivita-public-hero .evento-public__hero-shade {
52
+    z-index: 1;
53
+  }
54
+
55
+  .attivita-public-hero .evento-public__hero-inner {
56
+    position: absolute;
57
+    inset: 0;
58
+    z-index: 2;
59
+    max-width: none;
60
+    margin: 0;
61
+    padding: 0 var(--evento-gutter);
62
+    display: flex;
63
+    align-items: flex-end;
64
+    justify-content: center;
65
+    pointer-events: none;
66
+  }
67
+
68
+  .attivita-public-hero .evento-public__hero-content {
69
+    pointer-events: auto;
70
+    width: 100%;
71
+    max-width: 36rem;
72
+    min-height: 0;
73
+    align-items: center;
74
+    text-align: center;
75
+    padding: 1rem 0 2.85rem;
76
+  }
77
+
78
+  .attivita-public__logo-mark {
79
+    width: clamp(4.75rem, 16vw, 6.25rem);
80
+    height: clamp(4.75rem, 16vw, 6.25rem);
81
+    margin: 0 auto 0.75rem;
82
+    border-radius: 1.25rem;
83
+    background: #fff;
84
+    box-shadow: 0 10px 28px rgba(0, 0, 0, 0.22);
85
+    display: flex;
86
+    align-items: center;
87
+    justify-content: center;
88
+    overflow: hidden;
89
+    flex-shrink: 0;
90
+  }
91
+
92
+  .attivita-public__logo-mark img {
93
+    width: 82%;
94
+    height: 82%;
95
+    object-fit: contain;
96
+  }
97
+
98
+  .attivita-public__tagline {
99
+    margin: 0 0 0.4rem;
100
+    max-width: 100%;
101
+    font-size: 0.9rem;
102
+    font-weight: 600;
103
+    letter-spacing: 0;
104
+    text-transform: none;
105
+    line-height: 1.45;
106
+    color: rgba(255, 255, 255, 0.95);
107
+    display: -webkit-box;
108
+    -webkit-line-clamp: 2;
109
+    -webkit-box-orient: vertical;
110
+    overflow: hidden;
111
+  }
112
+
113
+  .attivita-public-hero .evento-public__title {
114
+    font-family: "Outfit", "Plus Jakarta Sans", system-ui, sans-serif;
115
+    font-size: clamp(1.55rem, 5vw, 2.25rem);
116
+    line-height: 1.08;
117
+    text-wrap: balance;
118
+  }
119
+
120
+  .attivita-public__status-chip {
121
+    display: inline-flex;
122
+    align-items: center;
123
+    gap: 0.35rem;
124
+    margin-top: 0.65rem;
125
+    padding: 0.38rem 0.85rem;
126
+    border-radius: 999px;
127
+    font-size: 0.78rem;
128
+    font-weight: 700;
129
+    letter-spacing: 0;
130
+    text-transform: none;
131
+    color: #fff;
132
+    background: var(--evento-accent);
133
+    box-shadow: 0 4px 16px color-mix(in srgb, var(--evento-accent) 38%, transparent);
134
+  }
135
+
136
+  /* ── Shell: pannelli bianchi come evento ── */
137
+  .attivita-public .evento-public__shell {
138
+    gap: 0.85rem;
139
+    padding-bottom: 1.25rem;
140
+  }
141
+
142
+  .attivita-public .evento-public__summary {
143
+    margin-top: -1.35rem;
144
+    padding: 0;
145
+  }
146
+
147
+  .attivita-public__panel {
148
+    background: #fff;
149
+    border-radius: 1.1rem;
150
+    border: 1px solid rgba(21, 38, 92, 0.05);
151
+    box-shadow: 0 4px 20px rgba(21, 38, 92, 0.07);
152
+    padding: 1.1rem 1.1rem 1rem;
153
+  }
154
+
155
+  .attivita-public__panel .attivita-public__heading {
156
+    margin-bottom: 0.2rem;
157
+  }
158
+
159
+  .attivita-public__panel .evento-public__facts {
160
+    box-shadow: none;
161
+    border: 0;
162
+    border-radius: 0;
163
+    padding: 0;
164
+    background: transparent;
165
+  }
166
+
167
+  @media (max-width: 479px) {
168
+    .attivita-public__panel .evento-public__facts {
169
+      gap: 0.65rem;
170
+    }
171
+  }
172
+
173
+  .attivita-public .evento-public__section {
174
+    margin-top: 0;
175
+    padding: 1.1rem 1.1rem 1.05rem;
176
+    background: #fff;
177
+    border-radius: 1.1rem;
178
+    border: 1px solid rgba(21, 38, 92, 0.05);
179
+    box-shadow: 0 4px 20px rgba(21, 38, 92, 0.07);
180
+  }
181
+
182
+  .attivita-public .evento-public__section + .evento-public__section {
183
+    padding-top: 1rem;
184
+    border-top: none;
185
+  }
186
+
187
+  .attivita-public__fact-link {
188
+    color: inherit;
189
+    text-decoration: none;
190
+  }
191
+
192
+  .attivita-public__fact-link:hover {
193
+    color: var(--evento-accent);
194
+  }
195
+
196
+  /* ── Lista eventi ── */
197
+  .attivita-public__event-list {
198
+    display: flex;
199
+    flex-direction: column;
200
+    gap: 0.5rem;
201
+  }
202
+
203
+  .attivita-public__panel .evento-public__fact {
204
+    font-size: 0.9rem;
205
+    font-weight: 600;
206
+  }
207
+
208
+  .attivita-public__event-row {
209
+    display: grid;
210
+    grid-template-columns: 3.25rem minmax(0, 1fr) auto;
211
+    align-items: stretch;
212
+    border-radius: 1rem;
213
+    text-decoration: none;
214
+    color: inherit;
215
+    background: #f7f8fc;
216
+    border: 1px solid rgba(21, 38, 92, 0.06);
217
+    overflow: hidden;
218
+    transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
219
+  }
220
+
221
+  .attivita-public__event-row:hover {
222
+    transform: translateY(-1px);
223
+    box-shadow: 0 6px 18px rgba(21, 38, 92, 0.08);
224
+    border-color: color-mix(in srgb, var(--evento-accent) 25%, rgba(21, 38, 92, 0.06));
225
+    color: inherit;
226
+  }
227
+
228
+  .attivita-public__event-row--open {
229
+    background: color-mix(in srgb, var(--evento-accent) 5%, #fff);
230
+    border-color: color-mix(in srgb, var(--evento-accent) 22%, rgba(21, 38, 92, 0.06));
231
+  }
232
+
233
+  .attivita-public__event-date {
234
+    display: flex;
235
+    flex-direction: column;
236
+    align-items: center;
237
+    justify-content: center;
238
+    padding: 0.7rem 0.35rem;
239
+    background: var(--evento-accent);
240
+    color: #fff;
241
+    line-height: 1.05;
242
+    text-align: center;
243
+  }
244
+
245
+  .attivita-public__event-date strong {
246
+    font-size: 1.05rem;
247
+    font-weight: 800;
248
+    line-height: 1;
249
+  }
250
+
251
+  .attivita-public__event-date span {
252
+    margin-top: 0.1rem;
253
+    font-size: 0.58rem;
254
+    font-weight: 800;
255
+    letter-spacing: 0.08em;
256
+    text-transform: uppercase;
257
+    opacity: 0.88;
258
+  }
259
+
260
+  .attivita-public__event-body {
261
+    min-width: 0;
262
+    padding: 0.75rem 0.8rem;
263
+    display: flex;
264
+    flex-direction: column;
265
+    justify-content: center;
266
+  }
267
+
268
+  .attivita-public__event-body strong {
269
+    display: block;
270
+    font-family: "Outfit", "Plus Jakarta Sans", system-ui, sans-serif;
271
+    font-size: 0.95rem;
272
+    font-weight: 700;
273
+    color: var(--evento-navy);
274
+    line-height: 1.25;
275
+    overflow: hidden;
276
+    text-overflow: ellipsis;
277
+    white-space: nowrap;
278
+  }
279
+
280
+  .attivita-public__event-body small {
281
+    display: block;
282
+    margin-top: 0.1rem;
283
+    font-size: 0.75rem;
284
+    color: var(--evento-muted);
285
+  }
286
+
287
+  .attivita-public__event-status {
288
+    display: inline-flex;
289
+    align-items: center;
290
+    align-self: flex-start;
291
+    margin-top: 0.35rem;
292
+    padding: 0.2rem 0.55rem;
293
+    border-radius: 999px;
294
+    font-size: 0.72rem;
295
+    font-weight: 700;
296
+  }
297
+
298
+  .attivita-public__event-status--open {
299
+    color: #0f6b3f;
300
+    background: color-mix(in srgb, #1b7a4a 12%, #fff);
301
+  }
302
+
303
+  .attivita-public__event-status--closed {
304
+    color: var(--evento-muted);
305
+    background: #f3f5f9;
306
+  }
307
+
308
+  .attivita-public__event-chevron {
309
+    align-self: center;
310
+    padding-right: 0.7rem;
311
+    color: #c5cad6;
312
+    font-size: 1.15rem;
313
+    flex-shrink: 0;
314
+  }
315
+
316
+  .attivita-public__event-row:hover .attivita-public__event-chevron {
317
+    color: var(--evento-accent);
318
+  }
319
+
320
+  /* ── Servizi ── */
321
+  .attivita-public__svc-grid {
322
+    display: grid;
323
+    grid-template-columns: 1fr;
324
+    gap: 0.6rem;
325
+  }
326
+
327
+  @media (min-width: 520px) {
328
+    .attivita-public__svc-grid {
329
+      grid-template-columns: 1fr 1fr;
330
+    }
331
+
332
+    .attivita-public__svc-grid--solo {
333
+      grid-template-columns: 1fr;
334
+      max-width: 20rem;
335
+    }
336
+  }
337
+
338
+  .attivita-public__svc-card {
339
+    display: flex;
340
+    flex-direction: column;
341
+    gap: 0.45rem;
342
+    padding: 1rem;
343
+    border-radius: 1rem;
344
+    text-decoration: none;
345
+    color: inherit;
346
+    background: #f7f8fc;
347
+    border: 1px solid rgba(21, 38, 92, 0.06);
348
+    transition: transform 0.15s ease, box-shadow 0.15s ease;
349
+  }
350
+
351
+  .attivita-public__svc-card:hover {
352
+    transform: translateY(-1px);
353
+    box-shadow: 0 6px 18px rgba(21, 38, 92, 0.08);
354
+    color: inherit;
355
+  }
356
+
357
+  .attivita-public__svc-card--hot {
358
+    background: color-mix(in srgb, var(--evento-accent) 7%, #fff);
359
+    border-color: color-mix(in srgb, var(--evento-accent) 28%, rgba(21, 38, 92, 0.06));
360
+  }
361
+
362
+  .attivita-public__svc-icon {
363
+    width: 2.5rem;
364
+    height: 2.5rem;
365
+    border-radius: 0.85rem;
366
+    display: flex;
367
+    align-items: center;
368
+    justify-content: center;
369
+    font-size: 1.15rem;
370
+    color: var(--evento-accent);
371
+    background: #fff;
372
+    box-shadow: 0 1px 4px rgba(21, 38, 92, 0.06);
373
+  }
374
+
375
+  .attivita-public__svc-card strong {
376
+    font-family: "Outfit", "Plus Jakarta Sans", system-ui, sans-serif;
377
+    font-size: 0.95rem;
378
+    font-weight: 800;
379
+    color: var(--evento-navy);
380
+  }
381
+
382
+  .attivita-public__svc-card small {
383
+    font-size: 0.8rem;
384
+    color: var(--evento-muted);
385
+    line-height: 1.4;
386
+  }
387
+
388
+  /* ── Footer & dock ── */
389
+  .attivita-public__foot {
390
+    position: relative;
391
+    z-index: 2;
392
+    padding: 0.5rem var(--evento-gutter) 1rem;
393
+    text-align: center;
394
+    font-size: 0.8rem;
395
+    font-weight: 600;
396
+    color: var(--evento-muted);
397
+  }
398
+
399
+  .attivita-public .evento-public__cta {
400
+    font-family: "Outfit", "Plus Jakarta Sans", system-ui, sans-serif;
401
+    letter-spacing: -0.01em;
402
+  }
403
+
404
+  .attivita-public .evento-public__sticky-cta {
405
+    display: flex;
406
+    justify-content: center;
407
+    padding-left: var(--evento-gutter);
408
+    padding-right: var(--evento-gutter);
409
+  }
410
+</style>

+ 245
- 512
resources/views/attivita/public/show.blade.php Ver fichero

@@ -2,6 +2,7 @@
2 2
   use Illuminate\Support\Facades\Route;
3 3
   use App\Models\Saltacoda;
4 4
   use App\Models\Attivita;
5
+  use Carbon\Carbon;
5 6
 
6 7
   /** @var \App\Models\Attivita|null $attivita */
7 8
   $accent = $attivita?->colore ?: '#f58220';
@@ -11,12 +12,97 @@
11 12
   $festLogoFallback = Attivita::defaultLogoUrl();
12 13
   $festLogoLarge = asset('assets/img/logo_fest_L.png');
13 14
   $backUrl = Route::has('welcome') ? route('welcome') : url('/');
15
+  $eventiPubblici = ($eventiPubblici ?? collect())
16
+    ->sortBy([
17
+      fn ($e) => $e->isPrenotabileOra() ? 0 : 1,
18
+      fn ($e) => $e->data_inizio ?? '9999-12-31',
19
+    ])
20
+    ->values();
14 21
 
15 22
   $saltacodaAttivo = $attivita
16 23
     && Saltacoda::query()
17 24
       ->where('attivita_id', $attivita->id)
18 25
       ->where('is_attivo', true)
19 26
       ->exists();
27
+
28
+  $infoDecoded = null;
29
+  if ($attivita && filled($attivita->info)) {
30
+    $decoded = json_decode($attivita->info, true);
31
+    if (is_array($decoded)) {
32
+      $infoDecoded = $decoded;
33
+    }
34
+  }
35
+
36
+  $infoLocation = collect([
37
+    trim((string) ($infoDecoded['indirizzo'] ?? '')),
38
+    trim((string) ($infoDecoded['città'] ?? '')),
39
+    trim((string) ($infoDecoded['provincia'] ?? '')),
40
+  ])->filter()->implode(', ');
41
+
42
+  $infoAddressMaps = collect([
43
+    trim((string) ($infoDecoded['indirizzo'] ?? '')),
44
+    trim((string) ($infoDecoded['cap'] ?? '')),
45
+    trim((string) ($infoDecoded['città'] ?? '')),
46
+    trim((string) ($infoDecoded['provincia'] ?? '')),
47
+    trim((string) ($infoDecoded['paese'] ?? '')),
48
+  ])->filter()->implode(', ');
49
+
50
+  $mapsUrl = $infoAddressMaps !== ''
51
+    ? 'https://www.google.com/maps/search/?api=1&query='.urlencode($infoAddressMaps)
52
+    : null;
53
+
54
+  $infoTelefono = trim((string) ($infoDecoded['telefono'] ?? ''));
55
+
56
+  $festaDataInizio = null;
57
+  $festaDataFine = null;
58
+  $festaDateLabel = null;
59
+  $festaDateSingola = false;
60
+
61
+  if ($eventiPubblici->isNotEmpty()) {
62
+    $festaDataInizio = $eventiPubblici
63
+      ->map(fn ($e) => filled($e->data_inizio) ? Carbon::parse($e->data_inizio) : null)
64
+      ->filter()
65
+      ->min();
66
+
67
+    $festaDataFine = $eventiPubblici
68
+      ->map(function ($e) {
69
+        if (filled($e->data_fine)) {
70
+          return Carbon::parse($e->data_fine);
71
+        }
72
+        if (filled($e->data_inizio)) {
73
+          return Carbon::parse($e->data_inizio);
74
+        }
75
+
76
+        return null;
77
+      })
78
+      ->filter()
79
+      ->max();
80
+
81
+    if ($festaDataInizio) {
82
+      $fine = $festaDataFine ?? $festaDataInizio;
83
+      $festaDateSingola = $festaDataInizio->isSameDay($fine);
84
+
85
+      if ($festaDateSingola) {
86
+        $festaDateLabel = $festaDataInizio->translatedFormat('l j F Y');
87
+      } elseif ($festaDataInizio->isSameMonth($fine) && $festaDataInizio->isSameYear($fine)) {
88
+        $festaDateLabel = $festaDataInizio->translatedFormat('j').' – '.$fine->translatedFormat('j F Y');
89
+      } elseif ($festaDataInizio->isSameYear($fine)) {
90
+        $festaDateLabel = $festaDataInizio->translatedFormat('j M').' – '.$fine->translatedFormat('j M Y');
91
+      } else {
92
+        $festaDateLabel = $festaDataInizio->translatedFormat('j M Y').' – '.$fine->translatedFormat('j M Y');
93
+      }
94
+    }
95
+  }
96
+
97
+  $infoSectionVisible = filled($festaDateLabel) || $infoLocation !== '' || $infoTelefono !== '';
98
+
99
+  $primoEventoPrenotabile = $eventiPubblici->first(fn ($e) => $e->isPrenotabileOra());
100
+  $haEventiPrenotabili = $primoEventoPrenotabile !== null;
101
+
102
+  $showDock = (bool) ($attivita && $saltacodaAttivo);
103
+  $saltacodaUrl = $attivita && $saltacodaAttivo
104
+    ? route('cliente.saltacoda.show', ['slug' => $attivita->slug])
105
+    : null;
20 106
 @endphp
21 107
 
22 108
 @extends('layouts/guest')
@@ -24,554 +110,201 @@
24 110
 @section('title', $attivita ? $attivita->nome : 'Attività')
25 111
 
26 112
 @section('page-meta')
27
-  <script>document.documentElement.classList.add('attivita-public-page');</script>
113
+  <script>document.documentElement.classList.add('attivita-public-page', 'evento-public-page');</script>
28 114
   <link rel="preconnect" href="https://fonts.googleapis.com" />
29 115
   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
30
-  <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400&display=swap" rel="stylesheet" />
116
+  <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@600;700;800&family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400&display=swap" rel="stylesheet" />
31 117
 @endsection
32 118
 
33 119
 @section('page-style')
34
-<style>
35
-  html.attivita-public-page,
36
-  html.attivita-public-page body {
37
-    height: 100%;
38
-    margin: 0;
39
-    background: #f6f7fb;
40
-  }
41
-
42
-  html.attivita-public-page .layout-wrapper,
43
-  html.attivita-public-page .content-wrapper,
44
-  html.attivita-public-page .layout-page,
45
-  html.attivita-public-page .container-xxl {
46
-    padding: 0 !important;
47
-    margin: 0 !important;
48
-    max-width: 100% !important;
49
-  }
50
-
51
-  html.attivita-public-page .d-flex.flex-column.min-vh-100 {
52
-    min-height: 100vh !important;
53
-    min-height: 100dvh !important;
54
-  }
55
-
56
-  html.attivita-public-page .misc-wrapper {
57
-    min-height: 100vh;
58
-    min-height: 100dvh;
59
-    width: 100%;
60
-    max-width: 100%;
61
-    padding: 0;
62
-    margin: 0;
63
-    align-items: stretch;
64
-    justify-content: stretch;
65
-    text-align: start;
66
-    background: transparent;
67
-  }
68
-
69
-  html.attivita-public-page .row.mt-3.mb-3,
70
-  html.attivita-public-page footer {
71
-    display: none !important;
72
-  }
73
-
74
-  .attivita-public {
75
-    --card-accent: {{ $accent }};
76
-    --text-main: #15265c;
77
-    --text-muted: #5b6579;
78
-    --purple: #602d91;
79
-    --orange: #f58220;
80
-
81
-    position: relative;
82
-    min-height: 100vh;
83
-    min-height: 100dvh;
84
-    display: flex;
85
-    flex-direction: column;
86
-    font-family: "Plus Jakarta Sans", "Public Sans", system-ui, sans-serif;
87
-  }
88
-
89
-  .attivita-public__bg {
90
-    position: fixed;
91
-    inset: 0;
92
-    z-index: 0;
93
-    pointer-events: none;
94
-    background: linear-gradient(145deg, #fff6ee 0%, #f3ebf8 38%, #e6edf8 100%);
95
-  }
96
-
97
-  .attivita-public__bg::before {
98
-    content: "";
99
-    position: absolute;
100
-    inset: 0;
101
-    background:
102
-      radial-gradient(ellipse 70% 50% at 8% 18%, color-mix(in srgb, var(--card-accent) 35%, transparent) 0%, transparent 58%),
103
-      radial-gradient(ellipse 65% 45% at 92% 82%, rgba(96, 45, 145, 0.22) 0%, transparent 55%);
104
-  }
105
-
106
-  .attivita-public__top {
107
-    position: relative;
108
-    z-index: 2;
109
-    display: flex;
110
-    align-items: center;
111
-    justify-content: space-between;
112
-    gap: 1rem;
113
-    padding: calc(.85rem + env(safe-area-inset-top, 0px)) 1.15rem .85rem;
114
-    background: rgba(255, 255, 255, .92);
115
-    backdrop-filter: blur(8px);
116
-    border-bottom: 1px solid rgba(21, 38, 92, .08);
117
-    box-shadow: 0 1px 10px rgba(21, 38, 92, .05);
118
-  }
119
-
120
-  .attivita-public__top::after {
121
-    content: "";
122
-    position: absolute;
123
-    left: 0;
124
-    right: 0;
125
-    bottom: 0;
126
-    height: 3px;
127
-    background: linear-gradient(90deg, var(--card-accent) 0%, var(--purple) 55%, #15265c 100%);
128
-    opacity: .85;
129
-  }
130
-
131
-  .attivita-public__back {
132
-    display: inline-flex;
133
-    align-items: center;
134
-    gap: .4rem;
135
-    font-size: .88rem;
136
-    font-weight: 600;
137
-    color: #15265c;
138
-    text-decoration: none;
139
-    transition: color .15s ease;
140
-  }
141
-
142
-  .attivita-public__back:hover {
143
-    color: var(--orange);
144
-  }
145
-
146
-  .attivita-public__brand img {
147
-    display: block;
148
-    height: 1.65rem;
149
-    width: auto;
150
-    opacity: .92;
151
-  }
152
-
153
-  .attivita-public__layout {
154
-    position: relative;
155
-    z-index: 1;
156
-    flex: 1;
157
-    min-height: 0;
158
-    display: grid;
159
-    grid-template-columns: minmax(300px, 50%) 1fr;
160
-  }
161
-
162
-  .attivita-public__media {
163
-    position: relative;
164
-    display: flex;
165
-    align-items: center;
166
-    justify-content: center;
167
-    padding: 1.5rem 1rem;
168
-    overflow: hidden;
169
-  }
170
-
171
-  .attivita-public__media-glow {
172
-    position: absolute;
173
-    width: min(70vw, 420px);
174
-    height: min(70vw, 420px);
175
-    border-radius: 50%;
176
-    background: color-mix(in srgb, var(--card-accent) 22%, transparent);
177
-    filter: blur(70px);
178
-    opacity: .75;
179
-    pointer-events: none;
180
-  }
181
-
182
-  .attivita-public__poster-wrap {
183
-    position: relative;
184
-    display: flex;
185
-    align-items: center;
186
-    justify-content: center;
187
-    width: min(100%, 480px);
188
-    aspect-ratio: 210 / 297;
189
-    max-height: min(88vh, 680px);
190
-    border-radius: .95rem;
191
-    box-shadow:
192
-      0 16px 44px rgba(21, 38, 92, .14),
193
-      0 0 0 1px rgba(255, 255, 255, .65);
194
-    overflow: hidden;
195
-    background: #e8ecf4;
196
-  }
197
-
198
-  .attivita-public__poster-wrap::after {
199
-    content: "";
200
-    position: absolute;
201
-    inset: 0;
202
-    pointer-events: none;
203
-    box-shadow: inset 0 0 0 1px rgba(21, 38, 92, .08);
204
-  }
205
-
206
-  .attivita-public__poster-wrap--placeholder {
207
-    background: linear-gradient(
208
-      165deg,
209
-      color-mix(in srgb, var(--card-accent) 28%, #fff) 0%,
210
-      color-mix(in srgb, var(--card-accent) 12%, #eef1f7) 38%,
211
-      #e8ecf4 68%,
212
-      color-mix(in srgb, var(--purple) 16%, #e8ecf4) 100%
213
-    );
214
-  }
215
-
216
-  .attivita-public__poster {
217
-    display: block;
218
-    width: 100%;
219
-    height: 100%;
220
-    object-fit: cover;
221
-    object-position: center top;
222
-  }
223
-
224
-  .attivita-public__poster-placeholder {
225
-    display: flex;
226
-    align-items: center;
227
-    justify-content: center;
228
-    width: 100%;
229
-    height: 100%;
230
-    padding: 2.5rem 1.75rem;
231
-  }
232
-
233
-  .attivita-public__poster-placeholder img {
234
-    width: min(72%, 8rem);
235
-    height: auto;
236
-    object-fit: contain;
237
-    opacity: .95;
238
-    filter: drop-shadow(0 8px 18px rgba(21, 38, 92, .16));
239
-  }
240
-
241
-  .attivita-public__main {
242
-    display: flex;
243
-    flex-direction: column;
244
-    justify-content: center;
245
-    padding: 1.9rem clamp(1.2rem, 4vw, 3rem);
246
-    padding-bottom: calc(1.9rem + env(safe-area-inset-bottom, 0px));
247
-  }
248
-
249
-  .attivita-public__panel {
250
-    max-width: 34rem;
251
-    padding: 1.35rem 1.4rem 1.5rem;
252
-    background: rgba(255, 255, 255, .94);
253
-    border: 1px solid rgba(21, 38, 92, .08);
254
-    border-radius: 1rem;
255
-    border-top: 3px solid var(--card-accent);
256
-    box-shadow: 0 10px 32px rgba(21, 38, 92, .08);
257
-  }
258
-
259
-  .attivita-public__head {
260
-    display: flex;
261
-    align-items: center;
262
-    flex-wrap: wrap;
263
-    gap: .65rem;
264
-    margin-bottom: .85rem;
265
-  }
266
-
267
-  .attivita-public__logo {
268
-    flex-shrink: 0;
269
-    width: 2.85rem;
270
-    height: 2.85rem;
271
-    display: flex;
272
-    align-items: center;
273
-    justify-content: center;
274
-    border-radius: .6rem;
275
-    background: #fff;
276
-    border: 1px solid rgba(21, 38, 92, .08);
277
-    box-shadow: 0 2px 8px rgba(21, 38, 92, .06);
278
-    overflow: hidden;
279
-  }
280
-
281
-  .attivita-public__logo img {
282
-    width: 88%;
283
-    height: 88%;
284
-    object-fit: contain;
285
-  }
286
-
287
-  .attivita-public__badge {
288
-    display: inline-flex;
289
-    align-items: center;
290
-    gap: .3rem;
291
-    width: fit-content;
292
-    font-size: .68rem;
293
-    font-weight: 700;
294
-    letter-spacing: .06em;
295
-    text-transform: uppercase;
296
-    color: #0d7a4a;
297
-    background: rgba(13, 122, 74, .1);
298
-    padding: .22rem .55rem;
299
-    border-radius: 999px;
300
-    margin: 0;
301
-  }
302
-
303
-  .attivita-public__kicker {
304
-    margin: 0 0 .35rem;
305
-    font-size: .72rem;
306
-    font-weight: 700;
307
-    letter-spacing: .08em;
308
-    text-transform: uppercase;
309
-    color: color-mix(in srgb, var(--card-accent) 70%, var(--purple));
310
-  }
311
-
312
-  .attivita-public__title {
313
-    margin: 0 0 .55rem;
314
-    font-size: clamp(1.5rem, 3.8vw, 2.35rem);
315
-    line-height: 1.12;
316
-    letter-spacing: -.03em;
317
-    font-weight: 800;
318
-    color: var(--text-main);
319
-  }
320
-
321
-  .attivita-public__desc {
322
-    margin: 0 0 1.25rem;
323
-    color: var(--text-muted);
324
-    line-height: 1.65;
325
-    font-size: .95rem;
326
-  }
327
-
328
-  .attivita-public__actions {
329
-    display: flex;
330
-    flex-direction: column;
331
-    gap: .65rem;
332
-  }
333
-
334
-  .attivita-public__actions-hint {
335
-    margin: 0 0 .1rem;
336
-    font-size: .72rem;
337
-    font-weight: 700;
338
-    letter-spacing: .06em;
339
-    text-transform: uppercase;
340
-    color: var(--text-muted);
341
-  }
342
-
343
-  .attivita-public__action {
344
-    display: flex;
345
-    align-items: center;
346
-    gap: .85rem;
347
-    padding: .9rem 1rem;
348
-    border-radius: .75rem;
349
-    text-decoration: none;
350
-    color: #1a2744;
351
-    background: #fff;
352
-    border: 1px solid rgba(21, 38, 92, .08);
353
-    box-shadow: 0 1px 4px rgba(21, 38, 92, .05);
354
-    transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
355
-  }
356
-
357
-  .attivita-public__action:hover {
358
-    color: #15265c;
359
-    transform: translateY(-1px);
360
-    border-color: color-mix(in srgb, var(--card-accent) 28%, rgba(21, 38, 92, .16));
361
-    box-shadow: 0 6px 16px rgba(21, 38, 92, .08);
362
-  }
363
-
364
-  .attivita-public__action--primary {
365
-    background: linear-gradient(
366
-      135deg,
367
-      color-mix(in srgb, var(--card-accent) 92%, #fff) 0%,
368
-      color-mix(in srgb, var(--card-accent) 78%, var(--purple)) 100%
369
-    );
370
-    border-color: transparent;
371
-    color: #fff;
372
-    box-shadow: 0 8px 22px color-mix(in srgb, var(--card-accent) 35%, transparent);
373
-  }
374
-
375
-  .attivita-public__action--primary:hover {
376
-    color: #fff;
377
-    transform: translateY(-2px);
378
-    box-shadow: 0 12px 28px color-mix(in srgb, var(--card-accent) 42%, transparent);
379
-  }
380
-
381
-  .attivita-public__action--primary strong,
382
-  .attivita-public__action--primary small {
383
-    color: #fff;
384
-  }
385
-
386
-  .attivita-public__action--primary .attivita-public__action-icon {
387
-    background: rgba(255, 255, 255, .22);
388
-    color: #fff;
389
-  }
390
-
391
-  .attivita-public__action-icon {
392
-    flex-shrink: 0;
393
-    width: 2.25rem;
394
-    height: 2.25rem;
395
-    border-radius: .58rem;
396
-    display: inline-flex;
397
-    align-items: center;
398
-    justify-content: center;
399
-    font-size: 1.12rem;
400
-    color: var(--text-main);
401
-    background: rgba(21, 38, 92, .08);
402
-  }
403
-
404
-  .attivita-public__action-icon--purple {
405
-    color: var(--purple);
406
-    background: rgba(96, 45, 145, .12);
407
-  }
408
-
409
-  .attivita-public__action strong {
410
-    display: block;
411
-    font-size: .92rem;
412
-    font-weight: 700;
413
-    color: #15265c;
414
-  }
415
-
416
-  .attivita-public__action small {
417
-    display: block;
418
-    margin-top: .12rem;
419
-    font-size: .78rem;
420
-    color: #6b7280;
421
-  }
422
-
423
-  .attivita-public__foot {
424
-    position: relative;
425
-    z-index: 1;
426
-    padding: .85rem 1.15rem calc(.85rem + env(safe-area-inset-bottom, 0px));
427
-    text-align: center;
428
-    font-size: .76rem;
429
-    color: #6b7280;
430
-  }
431
-
432
-  .attivita-public__empty {
433
-    position: relative;
434
-    z-index: 1;
435
-    flex: 1;
436
-    display: flex;
437
-    align-items: center;
438
-    justify-content: center;
439
-    text-align: center;
440
-    padding: 2rem;
441
-  }
442
-
443
-  @media (max-width: 899.98px) {
444
-    .attivita-public__layout {
445
-      grid-template-columns: 1fr;
446
-      grid-template-rows: auto 1fr;
447
-    }
448
-
449
-    .attivita-public__media {
450
-      padding: 1.15rem 1rem .35rem;
451
-    }
452
-
453
-    .attivita-public__poster-wrap {
454
-      width: min(92vw, 400px);
455
-      max-height: none;
456
-    }
457
-
458
-    .attivita-public__main {
459
-      justify-content: flex-start;
460
-      padding: 1.15rem 1rem 1.5rem;
461
-    }
462
-
463
-    .attivita-public__panel {
464
-      max-width: none;
465
-      padding: 1.15rem 1.1rem 1.25rem;
466
-    }
467
-  }
468
-</style>
120
+@include('evento._partials.public-styles')
121
+@include('attivita.public._partials.public-overrides')
469 122
 @endsection
470 123
 
471 124
 @section('content')
472
-<div class="attivita-public">
473
-  <div class="attivita-public__bg" aria-hidden="true"></div>
125
+<article class="evento-public attivita-public" style="--evento-accent: {{ $accent }};">
126
+  <div class="evento-public__bg" aria-hidden="true"></div>
474 127
 
475
-  <header class="attivita-public__top">
476
-    <a href="{{ $backUrl }}" class="attivita-public__back">
128
+  <header class="evento-public__top">
129
+    <a href="{{ $backUrl }}" class="evento-public__back">
477 130
       <i class="bx bx-arrow-back"></i>
478
-      Bacheca eventi
479
-    </a>
480
-    <a href="{{ $backUrl }}" class="attivita-public__brand" aria-label="{{ config('app.name') }}">
481
-      <img src="{{ $festLogoLarge }}" alt="{{ config('app.name') }}" loading="lazy">
131
+      <span>Indietro</span>
482 132
     </a>
133
+    <img src="{{ $festLogoLarge }}" alt="{{ config('app.name') }}" class="evento-public__brand-logo">
483 134
   </header>
484 135
 
485 136
   @if(!$attivita)
486
-    <div class="attivita-public__empty">
487
-      <div>
488
-        <i class="bx bx-error-circle bx-lg text-muted mb-2 d-block"></i>
489
-        <h1 class="h5 fw-bold mb-2">Attività non trovata</h1>
490
-        <p class="text-muted mb-3">La scheda richiesta non è disponibile.</p>
491
-        <a href="{{ $backUrl }}" class="btn btn-primary">Torna alla bacheca</a>
137
+    <div class="evento-public__shell" style="margin-top: 2rem;">
138
+      <div class="evento-public__cta-note">
139
+        <i class="bx bx-error-circle"></i>
140
+        <span>
141
+          <strong>Attività non trovata.</strong><br>
142
+          La scheda richiesta non è disponibile.
143
+          <a href="{{ $backUrl }}" class="d-inline-block mt-2 fw-bold">Torna alla bacheca</a>
144
+        </span>
492 145
       </div>
493 146
     </div>
494 147
   @else
495
-    <div class="attivita-public__layout">
496
-      <aside class="attivita-public__media" aria-label="Locandina evento">
497
-        <div class="attivita-public__media-glow" aria-hidden="true"></div>
498
-        <div class="attivita-public__poster-wrap {{ $hasCustomCover ? '' : 'attivita-public__poster-wrap--placeholder' }}">
499
-          @if($hasCustomCover)
148
+    <section class="evento-public__hero attivita-public-hero" aria-label="{{ $attivita->nome }}">
149
+      @if($hasCustomCover)
150
+        <img class="evento-public__hero-img" src="{{ $coverUrl }}" alt="">
151
+      @else
152
+        <div class="evento-public__hero-fallback" aria-hidden="true"></div>
153
+      @endif
154
+      <div class="evento-public__hero-shade"></div>
155
+      <div class="evento-public__hero-inner">
156
+        <div class="evento-public__hero-content">
157
+          <div class="attivita-public__logo-mark" aria-hidden="true">
500 158
             <img
501
-              src="{{ $coverUrl }}"
159
+              src="{{ $logoUrl }}"
502 160
               alt=""
503
-              class="attivita-public__poster"
504 161
               loading="lazy"
162
+              data-fest-logo-fallback="{{ $festLogoFallback }}"
163
+              onerror="if (this.dataset.festLogoFallback && this.src !== this.dataset.festLogoFallback) { this.src = this.dataset.festLogoFallback; }"
505 164
             >
506
-          @else
507
-            <span class="attivita-public__poster-placeholder" aria-hidden="true">
508
-              <img src="{{ $festLogoFallback }}" alt="" loading="lazy">
509
-            </span>
510
-          @endif
511
-        </div>
512
-      </aside>
513
-
514
-      <main class="attivita-public__main">
515
-        <div class="attivita-public__panel">
516
-          <div class="attivita-public__head">
517
-            <span class="attivita-public__logo" aria-hidden="true">
518
-              <img
519
-                src="{{ $logoUrl }}"
520
-                alt=""
521
-                loading="lazy"
522
-                data-fest-logo-fallback="{{ $festLogoFallback }}"
523
-                onerror="if (this.dataset.festLogoFallback && this.src !== this.dataset.festLogoFallback) { this.src = this.dataset.festLogoFallback; }"
524
-              >
525
-            </span>
526
-            @if($attivita->is_attiva)
527
-              <span class="attivita-public__badge"><i class="bx bx-check-circle"></i> In programma</span>
528
-            @endif
529 165
           </div>
530
-
531
-          <p class="attivita-public__kicker">Evento in bacheca</p>
532
-          <h1 class="attivita-public__title">{{ $attivita->nome }}</h1>
533
-
534 166
           @if(filled($attivita->descrizione))
535
-            <p class="attivita-public__desc">{{ $attivita->descrizione }}</p>
167
+            <p class="attivita-public__tagline">{{ $attivita->descrizione }}</p>
536 168
           @endif
537
-
538
-          <div class="attivita-public__actions">
539
-            <p class="attivita-public__actions-hint">Cosa vuoi fare?</p>
540
-            @if($saltacodaAttivo)
541
-              <a
542
-                href="{{ route('cliente.saltacoda.show', ['attivita_id' => $attivita->id]) }}"
543
-                class="attivita-public__action attivita-public__action--primary"
544
-              >
545
-                <span class="attivita-public__action-icon">
546
-                  <i class="bx bx-food-menu"></i>
169
+          <h1 class="evento-public__title">{{ $attivita->nome }}</h1>
170
+          @if($attivita->is_attiva)
171
+            <span class="attivita-public__status-chip"><i class="bx bx-star"></i> Ci siamo!</span>
172
+          @endif
173
+        </div>
174
+      </div>
175
+    </section>
176
+
177
+    <div class="evento-public__shell">
178
+      @if($infoSectionVisible)
179
+        <section class="evento-public__summary" aria-label="Vieni a trovarci">
180
+          <div class="attivita-public__panel">
181
+            <h2 class="attivita-public__heading">Vieni a trovarci</h2>
182
+            <p class="attivita-public__lede">Quando, dove e come raggiungerci.</p>
183
+            <ul class="evento-public__facts">
184
+            @if(filled($festaDateLabel))
185
+              <li class="evento-public__fact" aria-label="Quando">
186
+                <i class="bx bx-calendar" aria-hidden="true"></i>
187
+                <span>{{ $festaDateLabel }}</span>
188
+              </li>
189
+            @endif
190
+            @if($infoLocation !== '')
191
+              <li class="evento-public__fact" aria-label="Dove">
192
+                <i class="bx bx-map" aria-hidden="true"></i>
193
+                <span>
194
+                  {{ $infoLocation }}
195
+                  @if($mapsUrl)
196
+                    <a href="{{ $mapsUrl }}" class="evento-public__nav-link" target="_blank" rel="noopener noreferrer" title="Portami lì" aria-label="Apri il navigatore">
197
+                      <i class="bx bx-navigation"></i>
198
+                    </a>
199
+                  @endif
547 200
                 </span>
201
+              </li>
202
+            @endif
203
+            @if($infoTelefono !== '')
204
+              <li class="evento-public__fact" aria-label="Telefono">
205
+                <i class="bx bx-phone" aria-hidden="true"></i>
548 206
                 <span>
549
-                  <strong>Ordina (saltacoda)</strong>
550
-                  <small>Menu e ordine dal telefono</small>
207
+                  <a href="tel:{{ preg_replace('/\s+/', '', $infoTelefono) }}" class="attivita-public__fact-link">
208
+                    {{ $infoTelefono }}
209
+                  </a>
551 210
                 </span>
552
-              </a>
211
+              </li>
553 212
             @endif
213
+          </ul>
214
+          </div>
215
+        </section>
216
+      @endif
217
+
218
+      @if($eventiPubblici->isNotEmpty())
219
+        <section class="evento-public__section" aria-label="Eventi">
220
+          <h2 class="attivita-public__heading">
221
+            {{ $haEventiPrenotabili ? 'Scegli il tuo evento' : 'In programma' }}
222
+          </h2>
223
+          <div class="attivita-public__event-list">
224
+            @foreach($eventiPubblici as $evento)
225
+              @php
226
+                $prenotabile = $evento->isPrenotabileOra();
227
+                $dataInizio = $evento->data_inizio ? Carbon::parse($evento->data_inizio) : null;
228
+                $dataFine = $evento->data_fine ? Carbon::parse($evento->data_fine) : null;
229
+                $periodo = $dataInizio
230
+                  ? ($dataFine && ! $dataInizio->isSameDay($dataFine)
231
+                    ? $dataInizio->translatedFormat('d M').' – '.$dataFine->translatedFormat('d M Y')
232
+                    : $dataInizio->translatedFormat('d M Y'))
233
+                  : null;
234
+              @endphp
235
+              <a
236
+                href="{{ $evento->publicUrl() }}"
237
+                class="attivita-public__event-row {{ $prenotabile ? 'attivita-public__event-row--open' : '' }}"
238
+              >
239
+                <div class="attivita-public__event-date" aria-hidden="true">
240
+                  @if($dataInizio)
241
+                    <strong>{{ $dataInizio->format('d') }}</strong>
242
+                    <span>{{ $dataInizio->translatedFormat('M') }}</span>
243
+                  @else
244
+                    <strong>?</strong>
245
+                    <span>—</span>
246
+                  @endif
247
+                </div>
248
+                <div class="attivita-public__event-body">
249
+                  <strong>{{ $evento->nome }}</strong>
250
+                  @if($periodo)
251
+                    <small>{{ $periodo }}</small>
252
+                  @endif
253
+                  @if($prenotabile)
254
+                    <span class="attivita-public__event-status attivita-public__event-status--open">
255
+                      Prenota ora
256
+                    </span>
257
+                  @else
258
+                    <span class="attivita-public__event-status attivita-public__event-status--closed">
259
+                      {{ $evento->messaggioStatoPrenotazione() }}
260
+                    </span>
261
+                  @endif
262
+                </div>
263
+                <i class="bx bx-chevron-right attivita-public__event-chevron" aria-hidden="true"></i>
264
+              </a>
265
+            @endforeach
266
+          </div>
267
+        </section>
268
+      @endif
554 269
 
270
+      <section class="evento-public__section" aria-label="Servizi">
271
+        <h2 class="attivita-public__heading">Cosa ti va di fare?</h2>
272
+        <div class="attivita-public__svc-grid {{ $saltacodaAttivo ? '' : 'attivita-public__svc-grid--solo' }}">
273
+          @if($saltacodaAttivo)
555 274
             <a
556
-              href="{{ route('cliente.prenotazione-tavolo.index', ['attivita_id' => $attivita->id]) }}"
557
-              class="attivita-public__action"
275
+              href="{{ route('cliente.saltacoda.show', ['slug' => $attivita->slug]) }}"
276
+              class="attivita-public__svc-card attivita-public__svc-card--hot"
558 277
             >
559
-              <span class="attivita-public__action-icon attivita-public__action-icon--purple">
560
-                <i class="bx bx-calendar-check"></i>
561
-              </span>
562
-              <span>
563
-                <strong>Prenota tavolo</strong>
564
-                <small>Invia la richiesta di prenotazione</small>
565
-              </span>
278
+              <span class="attivita-public__svc-icon"><i class="bx bx-food-menu"></i></span>
279
+              <strong>Ordina</strong>
280
+              <small>Dal telefono, senza fare la fila</small>
566 281
             </a>
567
-          </div>
282
+          @endif
283
+          <a
284
+            href="{{ route('cliente.prenotazione-tavolo.index', ['attivita_id' => $attivita->id]) }}"
285
+            class="attivita-public__svc-card"
286
+          >
287
+            <span class="attivita-public__svc-icon"><i class="bx bx-chair"></i></span>
288
+            <strong>Prenota un tavolo</strong>
289
+            <small>Ti teniamo un posto</small>
290
+          </a>
568 291
         </div>
569
-      </main>
292
+      </section>
570 293
     </div>
571 294
 
295
+    @if($showDock && $saltacodaUrl)
296
+      <div class="evento-public__sticky-cta">
297
+        <a href="{{ $saltacodaUrl }}" class="evento-public__cta">
298
+          <span class="evento-public__cta-icon" aria-hidden="true"><i class="bx bx-food-menu"></i></span>
299
+          <span class="evento-public__cta-label">Ordina</span>
300
+          <span class="evento-public__cta-arrow" aria-hidden="true"><i class="bx bx-right-arrow-alt"></i></span>
301
+        </a>
302
+      </div>
303
+    @endif
304
+
572 305
     <footer class="attivita-public__foot">
573
-      {{ config('app.name') }} · la bacheca digitale per feste e sagre
306
+      Ci vediamo in festa
574 307
     </footer>
575 308
   @endif
576
-</div>
309
+</article>
577 310
 @endsection

+ 610
- 189
resources/views/attivita/show.blade.php Ver fichero

@@ -1,234 +1,655 @@
1
-<?php
2
-use App\Models\Role;
3
-use Illuminate\Support\Facades\Auth;
4
-?>
5 1
 @php
6
-$configData = Helper::appClasses();
7
-@endphp
2
+  use App\Models\Attivita;
3
+  use Carbon\Carbon;
4
+  use Illuminate\Support\Facades\Auth;
5
+  use Illuminate\Support\Facades\Storage;
8 6
 
9
-@extends('layouts/layoutMaster')
7
+  $accent = $attivita->colore ?: '#f58220';
8
+  $festLogoFallback = Attivita::defaultLogoUrl();
9
+  $festCoverFallback = Attivita::defaultCoverUrl();
10
+  $isSelezionata = (int) session('attivita_attuale') === (int) $attivita->id;
11
+  $publicUrl = $attivita->slug
12
+    ? route('cliente.attivita.show', ['slug' => $attivita->slug])
13
+    : null;
14
+  $saltacodaAttivo = $attivita->saltacoda && $attivita->saltacoda->is_attivo;
10 15
 
11
-@section('title', 'Attività - '.$attivita->nome)
16
+  $mediaUrl = function (?string $path): ?string {
17
+    if (! filled($path)) {
18
+      return null;
19
+    }
20
+    if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) {
21
+      return $path;
22
+    }
23
+    if (str_starts_with($path, 'assets/')) {
24
+      return asset($path);
25
+    }
12 26
 
13
-@section('vendor-style')
14
-@vite([
15
-'resources/assets/vendor/libs/datatables-bs5/datatables.bootstrap5.scss',
16
-'resources/assets/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.scss',
17
-'resources/assets/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.scss',
18
-'resources/assets/vendor/libs/flatpickr/flatpickr.scss',
19
-'resources/assets/vendor/libs/@form-validation/form-validation.scss'
20
-])
21
-@endsection
27
+    return Storage::disk('copertinaAttivita')->url($path);
28
+  };
22 29
 
23
-<!-- Vendor Scripts -->
24
-@section('vendor-script')
25
-@vite([
26
-'resources/assets/vendor/libs/moment/moment.js',
27
-'resources/assets/vendor/libs/flatpickr/flatpickr.js',
28
-'resources/assets/vendor/libs/@form-validation/popular.js',
29
-'resources/assets/vendor/libs/@form-validation/bootstrap5.js',
30
-'resources/assets/vendor/libs/@form-validation/auto-focus.js',
31
-'resources/assets/vendor/libs/@form-validation/popular.js',
32
-'resources/assets/vendor/libs/@form-validation/bootstrap5.js',
33
-'resources/assets/vendor/libs/@form-validation/auto-focus.js',
34
-'resources/assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js'
35
-])
36
-@endsection
30
+  $hasCustomLogo = filled($attivita->path_logo);
31
+  $hasCustomCover = $attivita->hasCustomCover();
32
+  $hasCustomIconFile = filled($attivita->path_icon);
33
+  $logoUrl = $hasCustomLogo ? ($mediaUrl($attivita->path_logo) ?: $festLogoFallback) : $festLogoFallback;
34
+  $coverUrl = $hasCustomCover ? ($mediaUrl($attivita->path_image) ?: $festCoverFallback) : $festCoverFallback;
35
+  $iconFileUrl = $hasCustomIconFile ? $mediaUrl($attivita->path_icon) : null;
36
+  $iconaHtml = filled($attivita->icona) && str_contains((string) $attivita->icona, '<');
37
+
38
+  $infoDecoded = null;
39
+  $infoInvalid = false;
40
+  if (filled($attivita->info)) {
41
+    $decoded = json_decode($attivita->info, true);
42
+    if (is_array($decoded)) {
43
+      $infoDecoded = $decoded;
44
+    } else {
45
+      $infoInvalid = true;
46
+    }
47
+  }
48
+
49
+  $infoFields = [
50
+    'indirizzo' => ['label' => 'Indirizzo', 'icon' => 'bx-map'],
51
+    'città' => ['label' => 'Città', 'icon' => 'bx-buildings'],
52
+    'cap' => ['label' => 'CAP', 'icon' => 'bx-mail-send'],
53
+    'provincia' => ['label' => 'Provincia', 'icon' => 'bx-map-pin'],
54
+    'paese' => ['label' => 'Paese', 'icon' => 'bx-world'],
55
+    'telefono' => ['label' => 'Telefono', 'icon' => 'bx-phone', 'link' => 'tel'],
56
+    'email' => ['label' => 'Email', 'icon' => 'bx-envelope', 'link' => 'mailto'],
57
+    'sito_web' => ['label' => 'Sito web', 'icon' => 'bx-link-external', 'link' => 'url'],
58
+    'note' => ['label' => 'Note', 'icon' => 'bx-note'],
59
+  ];
60
+
61
+  $infoLocationKeys = ['indirizzo', 'città', 'cap', 'provincia', 'paese'];
62
+  $infoContactKeys = ['telefono', 'email', 'sito_web', 'note'];
63
+
64
+  $infoHasValues = $infoDecoded
65
+    && collect($infoFields)->keys()->contains(fn ($key) => filled($infoDecoded[$key] ?? null));
66
+
67
+  $prossimiEventi = $attivita->eventi
68
+    ->sortBy(fn ($e) => $e->data_inizio ?? '9999-12-31')
69
+    ->take(3);
70
+
71
+  $hubLinks = [
72
+    ['route' => 'evento.index', 'permission' => 'view-evento', 'icon' => 'bx-calendar-event', 'label' => 'Eventi'],
73
+    ['route' => 'prenotazione.index', 'permission' => 'view-prenotazione', 'icon' => 'bx-book-content', 'label' => 'Prenotazioni'],
74
+    ['route' => 'punto-vendita.index', 'permission' => 'view-punto_vendita', 'icon' => 'bx-store', 'label' => 'Cassa'],
75
+    ['route' => 'piatto.index', 'permission' => 'view-piatto', 'icon' => 'bx-food-menu', 'label' => 'Menu'],
76
+    ['route' => 'ordine.index', 'permission' => 'view-ordine', 'icon' => 'bx-receipt', 'label' => 'Ordini'],
77
+    ['route' => 'bilancio.oggi', 'permission' => 'view-bilancio-oggi', 'icon' => 'bx-line-chart', 'label' => 'Bilancio oggi'],
78
+    ['route' => 'bilancio.index', 'permission' => 'view-bilancio', 'icon' => 'bx-pie-chart-alt-2', 'label' => 'Bilancio'],
79
+    ['route' => 'report.index', 'permission' => 'view-report', 'icon' => 'bx-wallet', 'label' => 'Report'],
80
+  ];
81
+@endphp
82
+
83
+@extends('layouts/layoutMaster')
84
+
85
+@section('title', 'Attività · '.$attivita->nome)
37 86
 
38 87
 @section('pageTitle')
39 88
 <div class="d-flex flex-column">
40
-  <h4 class="mb-1"> 
41
-    <i class="bx bx-aperture"></i>
42
-    <span class="text-muted">Stai gestendo</span>
43
-    <span class="text-primary fw-bold text-uppercase">{{ $attivita->nome }}</span></h4>
89
+  <nav aria-label="breadcrumb" style="font-size: smaller;">
90
+    <ol class="breadcrumb breadcrumb-custom-icon mb-0">
91
+      <li class="breadcrumb-item">
92
+        <a href="{{ route('attivita.index') }}">Attività</a>
93
+        <i class="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
94
+      </li>
95
+      <li class="breadcrumb-item active text-primary">{{ $attivita->nome }}</li>
96
+    </ol>
97
+  </nav>
44 98
 </div>
45 99
 @endsection
46 100
 
47 101
 @section('content')
48
-<div class="row g-3">
49
-  {{-- Dispositivi --}}
50
-  <div class="col-lg-3 col-sm-6">
51
-    <div class="card card-border-shadow-info h-100">
52
-      <div class="card-body">
53
-        <div class="d-flex align-items-center mb-2">
54
-          <div class="avatar me-4">
55
-            <span class="avatar-initial rounded bg-label-info"><i class="bx bx-devices bx-lg"></i></span>
56
-          </div>
57
-          <h4 class="mb-0">{{ $attivita->dispositivi->count() }}</h4>
58
-        </div>
59
-        <p class="mb-2">Dispositivi associati</p>
60
-        @if($attivita->dispositivi->count() > 0)
61
-          <ul class="list-unstyled mb-0 ps-1">
62
-            @foreach($attivita->dispositivi->take(3) as $dispositivo)
63
-              <li><i class="bx bx-dots-horizontal-rounded small"></i> {{ $dispositivo->nome ?? $dispositivo->id }}</li>
64
-            @endforeach
65
-            @if($attivita->dispositivi->count() > 3)
66
-              <li class="text-muted small">+ altri {{ $attivita->dispositivi->count() - 3 }}</li>
67
-            @endif
68
-          </ul>
69
-        @else
70
-          <p class="mb-0 text-muted small">Nessun dispositivo associato.</p>
71
-        @endif
72
-      </div>
102
+@include('_partials.status')
103
+
104
+<style>
105
+  .attivita-profile__hero {
106
+    position: relative;
107
+    min-height: 140px;
108
+    overflow: hidden;
109
+    border-bottom: 1px solid rgba(67, 89, 113, .1);
110
+  }
111
+  .attivita-profile__hero-img {
112
+    position: absolute;
113
+    inset: 0;
114
+    width: 100%;
115
+    height: 100%;
116
+    object-fit: cover;
117
+  }
118
+  .attivita-profile__hero-shade {
119
+    position: absolute;
120
+    inset: 0;
121
+    background: linear-gradient(180deg, rgba(15, 20, 35, .1) 0%, rgba(15, 20, 35, .75) 100%);
122
+  }
123
+  .attivita-profile__hero-inner {
124
+    position: relative;
125
+    z-index: 1;
126
+    display: flex;
127
+    align-items: flex-end;
128
+    gap: 1rem;
129
+    padding: 1.1rem 1.25rem;
130
+    min-height: 140px;
131
+  }
132
+  .attivita-profile__hero-logo {
133
+    flex-shrink: 0;
134
+    width: 4.25rem;
135
+    height: 4.25rem;
136
+    border-radius: .85rem;
137
+    background: #fff;
138
+    border: 2px solid rgba(255, 255, 255, .9);
139
+    box-shadow: 0 8px 20px rgba(0, 0, 0, .2);
140
+    display: flex;
141
+    align-items: center;
142
+    justify-content: center;
143
+    overflow: hidden;
144
+    padding: .35rem;
145
+  }
146
+  .attivita-profile__hero-logo img {
147
+    max-width: 100%;
148
+    max-height: 100%;
149
+    object-fit: contain;
150
+  }
151
+  .attivita-profile__hero-title {
152
+    margin: 0 0 .35rem;
153
+    font-size: 1.35rem;
154
+    font-weight: 700;
155
+    color: #fff;
156
+    line-height: 1.2;
157
+  }
158
+  .attivita-profile__panel {
159
+    border: 1px solid rgba(67, 89, 113, .1);
160
+    border-radius: .85rem;
161
+    padding: 1rem 1.05rem;
162
+    background: rgba(67, 89, 113, .02);
163
+    height: 100%;
164
+  }
165
+  .attivita-profile__panel-title {
166
+    margin: 0 0 .75rem;
167
+    font-size: .72rem;
168
+    font-weight: 800;
169
+    letter-spacing: .08em;
170
+    text-transform: uppercase;
171
+    color: #8592a3;
172
+  }
173
+  .attivita-profile__media-grid {
174
+    display: grid;
175
+    grid-template-columns: repeat(3, minmax(0, 1fr));
176
+    gap: .65rem;
177
+  }
178
+  .attivita-profile__media-item {
179
+    text-align: center;
180
+  }
181
+  .attivita-profile__media-thumb {
182
+    aspect-ratio: 1;
183
+    border-radius: .65rem;
184
+    border: 1px solid rgba(67, 89, 113, .12);
185
+    background: #fff;
186
+    display: flex;
187
+    align-items: center;
188
+    justify-content: center;
189
+    overflow: hidden;
190
+    padding: .35rem;
191
+    margin-bottom: .35rem;
192
+  }
193
+  .attivita-profile__media-thumb img {
194
+    max-width: 100%;
195
+    max-height: 100%;
196
+    object-fit: contain;
197
+  }
198
+  .attivita-profile__media-thumb--wide {
199
+    aspect-ratio: 16 / 10;
200
+  }
201
+  .attivita-profile__media-thumb--wide img {
202
+    width: 100%;
203
+    height: 100%;
204
+    object-fit: cover;
205
+  }
206
+  .attivita-profile__media-label {
207
+    font-size: .72rem;
208
+    font-weight: 600;
209
+    color: #697a8d;
210
+  }
211
+  .attivita-profile__media-hint {
212
+    font-size: .65rem;
213
+    color: #a1acb8;
214
+  }
215
+  .attivita-profile__kv {
216
+    display: flex;
217
+    justify-content: space-between;
218
+    align-items: flex-start;
219
+    gap: .75rem;
220
+    padding: .45rem 0;
221
+    border-bottom: 1px solid rgba(67, 89, 113, .08);
222
+    font-size: .88rem;
223
+  }
224
+  .attivita-profile__kv:last-child {
225
+    border-bottom: 0;
226
+    padding-bottom: 0;
227
+  }
228
+  .attivita-profile__kv-label {
229
+    color: #8592a3;
230
+    font-weight: 500;
231
+    flex-shrink: 0;
232
+  }
233
+  .attivita-profile__kv-value {
234
+    text-align: right;
235
+    color: #2f3a46;
236
+    font-weight: 600;
237
+    min-width: 0;
238
+  }
239
+  .attivita-profile__color {
240
+    display: inline-flex;
241
+    align-items: center;
242
+    gap: .45rem;
243
+  }
244
+  .attivita-profile__color-swatch {
245
+    width: 1.1rem;
246
+    height: 1.1rem;
247
+    border-radius: .25rem;
248
+    border: 1px solid rgba(67, 89, 113, .2);
249
+  }
250
+  .attivita-profile__icona-box {
251
+    display: inline-flex;
252
+    align-items: center;
253
+    justify-content: center;
254
+    min-width: 2rem;
255
+    min-height: 2rem;
256
+    padding: .25rem;
257
+    border-radius: .45rem;
258
+    background: rgba(67, 89, 113, .06);
259
+    border: 1px solid rgba(67, 89, 113, .1);
260
+  }
261
+  .attivita-profile__icona-box svg {
262
+    width: 1.25rem;
263
+    height: 1.25rem;
264
+  }
265
+  .attivita-hub-links {
266
+    display: grid;
267
+    grid-template-columns: repeat(2, minmax(0, 1fr));
268
+    gap: .55rem;
269
+  }
270
+  @media (min-width: 768px) {
271
+    .attivita-hub-links { grid-template-columns: repeat(4, minmax(0, 1fr)); }
272
+  }
273
+  .attivita-hub-links a {
274
+    display: flex;
275
+    align-items: center;
276
+    gap: .55rem;
277
+    padding: .7rem .8rem;
278
+    border-radius: .75rem;
279
+    border: 1px solid rgba(67, 89, 113, .1);
280
+    text-decoration: none;
281
+    color: #2f3a46;
282
+    font-weight: 600;
283
+    font-size: .88rem;
284
+    transition: background .15s ease, border-color .15s ease;
285
+  }
286
+  .attivita-hub-links a:hover {
287
+    color: #2f3a46;
288
+    background: color-mix(in srgb, var(--attivita-accent) 8%, #fff);
289
+    border-color: color-mix(in srgb, var(--attivita-accent) 30%, rgba(67, 89, 113, .12));
290
+  }
291
+  .attivita-hub-links a i {
292
+    font-size: 1.15rem;
293
+    color: color-mix(in srgb, var(--attivita-accent) 70%, #2f3a46);
294
+  }
295
+</style>
296
+
297
+@if(!$isSelezionata)
298
+  <div class="alert alert-warning d-flex align-items-start gap-2 mb-3">
299
+    <i class="bx bx-info-circle mt-1"></i>
300
+    <div class="flex-grow-1">
301
+      Seleziona <strong>{{ $attivita->nome }}</strong> per gestire eventi, cassa e prenotazioni.
302
+      @can('select-attivita')
303
+        <form action="{{ route('attivita.select') }}" method="POST" class="mt-2">
304
+          @csrf
305
+          <input type="hidden" name="attivita_id" value="{{ $attivita->id }}">
306
+          <button type="submit" class="btn btn-sm btn-primary">
307
+            <i class="bx bx-check-circle me-1"></i> Seleziona attività
308
+          </button>
309
+        </form>
310
+      @endcan
73 311
     </div>
74 312
   </div>
313
+@endif
75 314
 
76
-  {{-- Eventi --}}
77
-  <div class="col-lg-3 col-sm-6">
78
-    <div class="card card-border-shadow-primary h-100">
79
-      <div class="card-body">
80
-        <div class="d-flex align-items-center mb-2">
81
-          <div class="avatar me-4">
82
-            <span class="avatar-initial rounded bg-label-primary"><i class="bx bx-calendar-event bx-lg"></i></span>
83
-          </div>
84
-          <h4 class="mb-0">{{ $attivita->eventi->count() }}</h4>
85
-        </div>
86
-        <p class="mb-2">Eventi associati</p>
87
-        @if($attivita->eventi->count() > 0)
88
-          <ul class="list-unstyled mb-0 ps-1">
89
-            @foreach($attivita->eventi->take(3) as $evento)
90
-              <li><i class="bx bx-dots-horizontal-rounded small"></i> {{ $evento->nome ?? $evento->id }}</li>
91
-            @endforeach
92
-            @if($attivita->eventi->count() > 3)
93
-              <li class="text-muted small">+ altri {{ $attivita->eventi->count() - 3 }}</li>
94
-            @endif
95
-          </ul>
96
-        @else
97
-          <p class="mb-0 text-muted small">Nessun evento associato.</p>
98
-        @endif
99
-      </div>
315
+<div class="card mb-3" style="--attivita-accent: {{ $accent }};">
316
+  <div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2 py-3">
317
+    <div>
318
+      <h5 class="card-title mb-0">Profilo attività</h5>
319
+      <p class="card-subtitle mb-0 text-muted">Panoramica identità, media e contatti</p>
320
+    </div>
321
+    <div class="d-flex flex-wrap gap-2">
322
+      @can('edit-attivita')
323
+        <a href="{{ route('attivita.index') }}" class="btn btn-sm btn-label-primary">
324
+          <i class="bx bx-edit-alt me-1"></i> Modifica dalla lista
325
+        </a>
326
+      @endcan
327
+      @if($publicUrl)
328
+        <a href="{{ $publicUrl }}" class="btn btn-sm btn-label-secondary" target="_blank" rel="noopener noreferrer">
329
+          <i class="bx bx-link-external me-1"></i> Anteprima pubblica
330
+        </a>
331
+      @endif
332
+      @if($isSelezionata)
333
+        <a href="{{ route('dashboard') }}" class="btn btn-sm btn-label-primary">
334
+          <i class="bx bx-home-smile me-1"></i> Dashboard
335
+        </a>
336
+      @endif
100 337
     </div>
101 338
   </div>
102 339
 
103
-  {{-- Prenotazioni --}}
104
-  <div class="col-lg-3 col-sm-6">
105
-    <div class="card card-border-shadow-success h-100">
106
-      <div class="card-body">
107
-        <div class="d-flex align-items-center mb-2">
108
-          <div class="avatar me-4">
109
-            <span class="avatar-initial rounded bg-label-success"><i class="bx bx-book-content bx-lg"></i></span>
110
-          </div>
111
-          <h4 class="mb-0">{{ $attivita->prenotazioni->count() }}</h4>
340
+  <div class="attivita-profile__hero">
341
+    <img
342
+      class="attivita-profile__hero-img"
343
+      src="{{ $coverUrl }}"
344
+      alt="Copertina {{ $attivita->nome }}"
345
+    >
346
+    <div class="attivita-profile__hero-shade"></div>
347
+    <div class="attivita-profile__hero-inner">
348
+      <div class="attivita-profile__hero-logo">
349
+        <img
350
+          src="{{ $logoUrl }}"
351
+          alt="Logo {{ $attivita->nome }}"
352
+          data-fest-logo-fallback="{{ $festLogoFallback }}"
353
+          onerror="if (this.dataset.festLogoFallback && this.src !== this.dataset.festLogoFallback) { this.src = this.dataset.festLogoFallback; }"
354
+        >
355
+      </div>
356
+      <div class="flex-grow-1 min-w-0">
357
+        <h2 class="attivita-profile__hero-title">{{ $attivita->nome }}</h2>
358
+        <div class="d-flex flex-wrap align-items-center gap-1">
359
+          @if($attivita->is_attiva)
360
+            <span class="badge bg-label-success">Attiva</span>
361
+          @else
362
+            <span class="badge bg-label-secondary">Non attiva</span>
363
+          @endif
364
+          @if($isSelezionata)
365
+            <span class="badge bg-label-primary">In gestione</span>
366
+          @endif
367
+          @if($saltacodaAttivo)
368
+            <span class="badge bg-label-warning">Saltacoda</span>
369
+          @endif
370
+          @if(filled($attivita->tipo))
371
+            <span class="badge bg-label-info">{{ $attivita->tipo }}</span>
372
+          @endif
112 373
         </div>
113
-        <p class="mb-2">Prenotazioni</p>
114
-        @if($attivita->prenotazioni->count() > 0)
115
-          <ul class="list-unstyled mb-0 ps-1">
116
-            @foreach($attivita->prenotazioni->take(3) as $prenotazione)
117
-              <li><i class="bx bx-dots-horizontal-rounded small"></i>
118
-                @php $label = trim(($prenotazione->nome ?? '').' '.($prenotazione->cognome ?? '')); @endphp
119
-                {{ $label !== '' ? $label : ('#'.$prenotazione->id) }}
120
-              </li>
121
-            @endforeach
122
-            @if($attivita->prenotazioni->count() > 3)
123
-              <li class="text-muted small">+ altre {{ $attivita->prenotazioni->count() - 3 }}</li>
124
-            @endif
125
-          </ul>
126
-        @else
127
-          <p class="mb-0 text-muted small">Nessuna prenotazione.</p>
374
+        @if($attivita->slug && $publicUrl)
375
+          <a
376
+            href="{{ $publicUrl }}"
377
+            class="small text-white opacity-75 d-block mt-1"
378
+            target="_blank"
379
+            rel="noopener noreferrer"
380
+          >
381
+            <code class="text-white">/{{ $attivita->slug }}</code>
382
+            <i class="bx bx-link-external ms-1"></i>
383
+          </a>
384
+        @elseif($attivita->slug)
385
+          <span class="small text-white opacity-75 d-block mt-1">
386
+            <code class="text-white">/{{ $attivita->slug }}</code>
387
+          </span>
128 388
         @endif
129 389
       </div>
130 390
     </div>
131 391
   </div>
132 392
 
133
-  {{-- Piatti --}}
134
-  <div class="col-lg-3 col-sm-6">
135
-    <div class="card card-border-shadow-warning h-100">
136
-      <div class="card-body">
137
-        <div class="d-flex align-items-center mb-2">
138
-          <div class="avatar me-4">
139
-            <span class="avatar-initial rounded bg-label-warning"><i class="bx bx-food-menu bx-lg"></i></span>
393
+  <div class="card-body">
394
+    @if(filled($attivita->descrizione))
395
+      <p class="text-muted mb-4">{{ $attivita->descrizione }}</p>
396
+    @endif
397
+
398
+    <div class="row g-3 mb-3">
399
+      <div class="col-lg-4">
400
+        <div class="attivita-profile__panel">
401
+          <h6 class="attivita-profile__panel-title">
402
+            <i class="bx bx-image me-1"></i> Media
403
+          </h6>
404
+          <div class="attivita-profile__media-grid">
405
+            <div class="attivita-profile__media-item">
406
+              <div class="attivita-profile__media-thumb attivita-profile__media-thumb--wide">
407
+                <img src="{{ $coverUrl }}" alt="Copertina">
408
+              </div>
409
+              <div class="attivita-profile__media-label">Copertina</div>
410
+              @unless($hasCustomCover)
411
+                <div class="attivita-profile__media-hint">Predefinita</div>
412
+              @endunless
413
+            </div>
414
+            <div class="attivita-profile__media-item">
415
+              <div class="attivita-profile__media-thumb">
416
+                <img
417
+                  src="{{ $logoUrl }}"
418
+                  alt="Logo"
419
+                  data-fest-logo-fallback="{{ $festLogoFallback }}"
420
+                  onerror="if (this.dataset.festLogoFallback && this.src !== this.dataset.festLogoFallback) { this.src = this.dataset.festLogoFallback; }"
421
+                >
422
+              </div>
423
+              <div class="attivita-profile__media-label">Logo</div>
424
+              @unless($hasCustomLogo)
425
+                <div class="attivita-profile__media-hint">Predefinito</div>
426
+              @endunless
427
+            </div>
428
+            <div class="attivita-profile__media-item">
429
+              @if($iconFileUrl)
430
+                <div class="attivita-profile__media-thumb">
431
+                  <img src="{{ $iconFileUrl }}" alt="Icona file">
432
+                </div>
433
+                <div class="attivita-profile__media-label">Icona file</div>
434
+              @else
435
+                <div class="attivita-profile__media-thumb text-muted small px-2">
436
+                  <span>—</span>
437
+                </div>
438
+                <div class="attivita-profile__media-label">Icona file</div>
439
+                <div class="attivita-profile__media-hint">Non impostata</div>
440
+              @endif
441
+            </div>
140 442
           </div>
141
-          <h4 class="mb-0">{{ $attivita->piatti->count() }}</h4>
142 443
         </div>
143
-        <p class="mb-2">Piatti</p>
144
-        @if($attivita->piatti->count() > 0)
145
-          <ul class="list-unstyled mb-0 ps-1">
146
-            @foreach($attivita->piatti->take(3) as $piatto)
147
-              <li><i class="bx bx-dots-horizontal-rounded small"></i> {{ $piatto->nome ?? $piatto->id }}</li>
148
-            @endforeach
149
-            @if($attivita->piatti->count() > 3)
150
-              <li class="text-muted small">+ altri {{ $attivita->piatti->count() - 3 }}</li>
151
-            @endif
152
-          </ul>
153
-        @else
154
-          <p class="mb-0 text-muted small">Nessun piatto associato.</p>
155
-        @endif
156 444
       </div>
157
-    </div>
158
-  </div>
159 445
 
160
-  {{-- Fornitori --}}
161
-  <div class="col-lg-3 col-sm-6">
162
-    <div class="card card-border-shadow-secondary h-100">
163
-      <div class="card-body">
164
-        <div class="d-flex align-items-center mb-2">
165
-          <div class="avatar me-4">
166
-            <span class="avatar-initial rounded bg-label-secondary"><i class="bx bx-store bx-lg"></i></span>
446
+      <div class="col-lg-4">
447
+        <div class="attivita-profile__panel">
448
+          <h6 class="attivita-profile__panel-title">
449
+            <i class="bx bx-palette me-1"></i> Branding
450
+          </h6>
451
+          <div class="mb-0">
452
+            <div class="attivita-profile__kv">
453
+              <span class="attivita-profile__kv-label">Colore</span>
454
+              <span class="attivita-profile__kv-value">
455
+                <span class="attivita-profile__color">
456
+                  <span class="attivita-profile__color-swatch" style="background: {{ $accent }};"></span>
457
+                  <code>{{ $attivita->colore ?: '#f58220' }}</code>
458
+                </span>
459
+              </span>
460
+            </div>
461
+            <div class="attivita-profile__kv">
462
+              <span class="attivita-profile__kv-label">Tipo</span>
463
+              <span class="attivita-profile__kv-value">{{ filled($attivita->tipo) ? $attivita->tipo : '—' }}</span>
464
+            </div>
465
+            <div class="attivita-profile__kv">
466
+              <span class="attivita-profile__kv-label">Icona</span>
467
+              <span class="attivita-profile__kv-value">
468
+                @if(filled($attivita->icona))
469
+                  @if($iconaHtml)
470
+                    <span class="attivita-profile__icona-box">{!! $attivita->icona !!}</span>
471
+                  @elseif(str_contains((string) $attivita->icona, 'bx '))
472
+                    <span class="attivita-profile__icona-box"><i class="{{ $attivita->icona }}"></i></span>
473
+                  @else
474
+                    <code>{{ $attivita->icona }}</code>
475
+                  @endif
476
+                @else
477
+                  <span class="text-muted">—</span>
478
+                @endif
479
+              </span>
480
+            </div>
167 481
           </div>
168
-          <h4 class="mb-0">{{ $attivita->fornitori->count() }}</h4>
169 482
         </div>
170
-        <p class="mb-2">Fornitori associati</p>
171
-        @if($attivita->fornitori->count() > 0)
172
-          <ul class="list-unstyled mb-0 ps-1">
173
-            @foreach($attivita->fornitori->take(3) as $fornitore)
174
-              <li><i class="bx bx-dots-horizontal-rounded small"></i> {{ $fornitore->nome ?? $fornitore->id }}</li>
175
-            @endforeach
176
-            @if($attivita->fornitori->count() > 3)
177
-              <li class="text-muted small">+ altri {{ $attivita->fornitori->count() - 3 }}</li>
178
-            @endif
179
-          </ul>
180
-        @else
181
-          <p class="mb-0 text-muted small">Nessun fornitore associato.</p>
182
-        @endif
183 483
       </div>
184
-    </div>
185
-  </div>
186
-</div>
187 484
 
188
-{{-- Placeholder: da collegare a dati reali in seguito --}}
189
-<div class="row g-3 mt-1">
190
-  <div class="col-lg-4 col-md-6">
191
-    <div class="card card-border-shadow-danger h-100">
192
-      <div class="card-body">
193
-        <div class="d-flex align-items-center mb-2">
194
-          <div class="avatar me-3">
195
-            <span class="avatar-initial rounded bg-label-danger"><i class="bx bx-transfer-alt bx-lg"></i></span>
485
+      <div class="col-lg-4">
486
+        <div class="attivita-profile__panel">
487
+          <h6 class="attivita-profile__panel-title">
488
+            <i class="bx bx-link me-1"></i> URL pubblico
489
+          </h6>
490
+          <div class="mb-0">
491
+            <div class="attivita-profile__kv">
492
+              <span class="attivita-profile__kv-label">Slug</span>
493
+              <span class="attivita-profile__kv-value">
494
+                @if($attivita->slug)
495
+                  <code>/{{ $attivita->slug }}</code>
496
+                @else
497
+                  <span class="text-muted">{{ $attivita->publicSlug() }}</span>
498
+                @endif
499
+              </span>
500
+            </div>
501
+            <div class="attivita-profile__kv">
502
+              <span class="attivita-profile__kv-label">Pagina</span>
503
+              <span class="attivita-profile__kv-value">
504
+                @if($publicUrl)
505
+                  <a href="{{ $publicUrl }}" target="_blank" rel="noopener noreferrer">Apri</a>
506
+                @else
507
+                  <span class="text-muted">—</span>
508
+                @endif
509
+              </span>
510
+            </div>
196 511
           </div>
197
-          <h5 class="mb-0">Movimenti del giorno</h5>
198 512
         </div>
199
-        <p class="text-muted small mb-0">Placeholder: elenco movimenti della giornata corrente (da implementare).</p>
200 513
       </div>
201 514
     </div>
202
-  </div>
203
-  <div class="col-lg-4 col-md-6">
204
-    <div class="card card-border-shadow-dark h-100">
205
-      <div class="card-body">
206
-        <div class="d-flex align-items-center mb-2">
207
-          <div class="avatar me-3">
208
-            <span class="avatar-initial rounded bg-label-dark"><i class="bx bx-line-chart bx-lg"></i></span>
515
+
516
+    <div class="attivita-profile__panel">
517
+      <h6 class="attivita-profile__panel-title">
518
+        <i class="bx bx-id-card me-1"></i> Contatti e ubicazione
519
+      </h6>
520
+
521
+      @if($infoInvalid)
522
+        <p class="text-muted small mb-2">Il campo info non è un JSON valido:</p>
523
+        <pre class="small bg-lighter rounded p-2 mb-0">{{ $attivita->info }}</pre>
524
+      @elseif($infoDecoded)
525
+        <div class="row g-4">
526
+          <div class="col-md-6">
527
+            <p class="small fw-semibold text-muted mb-2">Ubicazione</p>
528
+            <div class="mb-0">
529
+              @foreach($infoLocationKeys as $key)
530
+                @php
531
+                  $meta = $infoFields[$key];
532
+                  $value = trim((string) ($infoDecoded[$key] ?? ''));
533
+                @endphp
534
+                <div class="attivita-profile__kv">
535
+                  <span class="attivita-profile__kv-label">
536
+                    <i class="bx {{ $meta['icon'] }} me-1"></i>{{ $meta['label'] }}
537
+                  </span>
538
+                  <span class="attivita-profile__kv-value">{{ $value !== '' ? $value : '—' }}</span>
539
+                </div>
540
+              @endforeach
541
+            </div>
209 542
           </div>
210
-          <h5 class="mb-0">Bilancio anno solare</h5>
211
-        </div>
212
-        <p class="text-muted small mb-0">Placeholder: sintesi bilancio per l’anno solare in corso (da implementare).</p>
213
-      </div>
214
-    </div>
215
-  </div>
216
-  <div class="col-lg-4 col-md-12">
217
-    <div class="card card-border-shadow-primary h-100">
218
-      <div class="card-body">
219
-        <div class="d-flex align-items-center mb-2">
220
-          <div class="avatar me-3">
221
-            <span class="avatar-initial rounded bg-label-primary"><i class="bx bx-wallet bx-lg"></i></span>
543
+          <div class="col-md-6">
544
+            <p class="small fw-semibold text-muted mb-2">Contatti</p>
545
+            <div class="mb-0">
546
+              @foreach($infoContactKeys as $key)
547
+                @php
548
+                  $meta = $infoFields[$key];
549
+                  $value = trim((string) ($infoDecoded[$key] ?? ''));
550
+                @endphp
551
+                <div class="attivita-profile__kv">
552
+                  <span class="attivita-profile__kv-label">
553
+                    <i class="bx {{ $meta['icon'] }} me-1"></i>{{ $meta['label'] }}
554
+                  </span>
555
+                  <span class="attivita-profile__kv-value">
556
+                    @if($value !== '')
557
+                      @if(($meta['link'] ?? null) === 'mailto')
558
+                        <a href="mailto:{{ $value }}">{{ $value }}</a>
559
+                      @elseif(($meta['link'] ?? null) === 'tel')
560
+                        <a href="tel:{{ preg_replace('/\s+/', '', $value) }}">{{ $value }}</a>
561
+                      @elseif(($meta['link'] ?? null) === 'url')
562
+                        @php
563
+                          $url = $value;
564
+                          if (! preg_match('/^https?:\/\//i', $url)) {
565
+                            $url = 'https://'.$url;
566
+                          }
567
+                        @endphp
568
+                        <a href="{{ $url }}" target="_blank" rel="noopener noreferrer">{{ $value }}</a>
569
+                      @elseif($key === 'note')
570
+                        <span class="text-break">{{ $value }}</span>
571
+                      @else
572
+                        {{ $value }}
573
+                      @endif
574
+                    @else
575
+                      <span class="text-muted">—</span>
576
+                    @endif
577
+                  </span>
578
+                </div>
579
+              @endforeach
580
+            </div>
222 581
           </div>
223
-          <h5 class="mb-0">Incasso giornaliero per tipo di pagamento</h5>
224 582
         </div>
225
-        <p class="text-muted small mb-0">Placeholder: incassi di oggi ripartiti per metodo di pagamento (da implementare).</p>
226
-      </div>
583
+        @unless($infoHasValues)
584
+          <p class="text-muted small mt-3 mb-0">Nessun contatto o indirizzo compilato.</p>
585
+        @endunless
586
+      @else
587
+        <p class="text-muted small mb-0">Nessun dato info salvato per questa attività.</p>
588
+      @endif
227 589
     </div>
590
+
591
+    @can('edit-attivita')
592
+      <p class="text-muted small mt-3 mb-0">
593
+        <i class="bx bx-info-circle"></i>
594
+        Per cambiare nome, testi o immagini usa <strong>Modifica</strong> nella <a href="{{ route('attivita.index') }}">lista attività</a>.
595
+      </p>
596
+    @endcan
228 597
   </div>
229 598
 </div>
230
-@endsection
231 599
 
232
-@section('page-script')
600
+@if($isSelezionata)
601
+  <div class="card mb-3" style="--attivita-accent: {{ $accent }};">
602
+    <div class="card-header py-3">
603
+      <h5 class="card-title mb-0">Vai a…</h5>
604
+    </div>
605
+    <div class="card-body pt-2">
606
+      <div class="attivita-hub-links">
607
+        @foreach($hubLinks as $link)
608
+          @can($link['permission'])
609
+            <a href="{{ route($link['route']) }}">
610
+              <i class="bx {{ $link['icon'] }}"></i>
611
+              {{ $link['label'] }}
612
+            </a>
613
+          @endcan
614
+        @endforeach
615
+      </div>
616
+    </div>
617
+  </div>
233 618
 
619
+  @if($prossimiEventi->isNotEmpty())
620
+    <div class="card">
621
+      <div class="card-header d-flex justify-content-between align-items-center py-3">
622
+        <h5 class="card-title mb-0">Prossimi eventi</h5>
623
+        @can('view-evento')
624
+          <a href="{{ route('evento.index') }}" class="btn btn-sm btn-label-primary">Tutti</a>
625
+        @endcan
626
+      </div>
627
+      <div class="list-group list-group-flush">
628
+        @foreach($prossimiEventi as $evento)
629
+          @php
630
+            $dataInizio = $evento->data_inizio ? Carbon::parse($evento->data_inizio) : null;
631
+            $dataLabel = $dataInizio ? $dataInizio->translatedFormat('d M Y') : 'Data da definire';
632
+          @endphp
633
+          @can('view-evento')
634
+            <a
635
+              href="{{ route('evento.admin.show', ['evento_id' => $evento->id]) }}"
636
+              class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
637
+            >
638
+              <span>
639
+                <strong>{{ $evento->nome }}</strong>
640
+                <small class="text-muted d-block">{{ $dataLabel }}</small>
641
+              </span>
642
+              <i class="bx bx-chevron-right text-muted"></i>
643
+            </a>
644
+          @else
645
+            <div class="list-group-item">
646
+              <strong>{{ $evento->nome }}</strong>
647
+              <small class="text-muted d-block">{{ $dataLabel }}</small>
648
+            </div>
649
+          @endcan
650
+        @endforeach
651
+      </div>
652
+    </div>
653
+  @endif
654
+@endif
234 655
 @endsection

+ 43
- 18
resources/views/bilancio/_partials/costi.blade.php Ver fichero

@@ -1,9 +1,17 @@
1 1
 <div class="tab-pane fade" id="bilancio-tab-costi" role="tabpanel">
2
+  <div class="card-body pt-4">
2 3
   <div class="row g-3">
3
-    <div class="col-6">
4
+    <div class="col-12 col-xl-6">
4 5
       <div class="card border">
5 6
         <div class="card-header pb-2">
6
-          <h6 class="mb-0">Tabella Costi</h6>
7
+          <h6 class="card-title mb-0">
8
+            Tabella Costi
9
+            <i class="bx bx-info-circle text-primary ms-1"
10
+               role="button" tabindex="0"
11
+               data-bs-toggle="tooltip" data-bs-placement="top"
12
+               title="Elenco delle categorie contabili di uscita nell'anno, con conteggio movimenti e totale."></i>
13
+          </h6>
14
+          <small class="text-muted">Fonte: Prima nota · uscite</small>
7 15
         </div>
8 16
         <div class="card-body">
9 17
           <div class="table-responsive">
@@ -42,13 +50,17 @@
42 50
     <div class="col-12 col-xl-6">
43 51
       <div class="card border h-100">
44 52
         <div class="card-header pb-2">
45
-          <h6 class="mb-0">Trend costi mensili</h6>
53
+          <h6 class="card-title mb-0">
54
+            Trend costi mensili
55
+            <i class="bx bx-info-circle text-primary ms-1"
56
+               role="button" tabindex="0"
57
+               data-bs-toggle="tooltip" data-bs-placement="top"
58
+               title="Andamento dei costi mese per mese: i picchi indicano periodi con spese maggiori."></i>
59
+          </h6>
60
+          <small class="text-muted">Fonte: Prima nota · uscite</small>
46 61
         </div>
47 62
         <div class="card-body">
48 63
           <div id="bilancio-costi-chart" style="min-height: 230px;"></div>
49
-          <p class="text-muted small mt-2 mb-0">
50
-            Mostra l'andamento dei costi mese per mese: picchi alti indicano periodi con spese maggiori.
51
-          </p>
52 64
         </div>
53 65
       </div>
54 66
     </div>
@@ -56,13 +68,17 @@
56 68
     <div class="col-12 col-xl-6">
57 69
       <div class="card border h-100">
58 70
         <div class="card-header pb-2">
59
-          <h6 class="mb-0">Top categorie costi</h6>
71
+          <h6 class="card-title mb-0">
72
+            Top categorie costi
73
+            <i class="bx bx-info-circle text-primary ms-1"
74
+               role="button" tabindex="0"
75
+               data-bs-toggle="tooltip" data-bs-placement="top"
76
+               title="Confronto tra categorie: le barre più lunghe sono le voci che pesano di più sul totale costi."></i>
77
+          </h6>
78
+          <small class="text-muted">Fonte: Prima nota · uscite per categoria</small>
60 79
         </div>
61 80
         <div class="card-body">
62 81
           <div id="bilancio-costi-categorie-chart" style="min-height: 260px;"></div>
63
-          <p class="text-muted small mt-2 mb-0">
64
-            Confronta le categorie: le barre piu lunghe sono le voci che pesano di piu sul totale costi.
65
-          </p>
66 82
         </div>
67 83
       </div>
68 84
     </div>
@@ -70,13 +86,17 @@
70 86
     <div class="col-12 col-xl-6">
71 87
       <div class="card border h-100">
72 88
         <div class="card-header pb-2">
73
-          <h6 class="mb-0">Composizione costi (%)</h6>
89
+          <h6 class="card-title mb-0">
90
+            Composizione costi (%)
91
+            <i class="bx bx-info-circle text-primary ms-1"
92
+               role="button" tabindex="0"
93
+               data-bs-toggle="tooltip" data-bs-placement="top"
94
+               title="Distribuzione percentuale: ogni fetta è la quota di una categoria sul totale costi dell'anno."></i>
95
+          </h6>
96
+          <small class="text-muted">Fonte: Prima nota · uscite per categoria</small>
74 97
         </div>
75 98
         <div class="card-body">
76 99
           <div id="bilancio-costi-percent-chart" style="min-height: 320px;"></div>
77
-          <p class="text-muted small mt-2 mb-0">
78
-            Distribuzione percentuale dei costi: ogni fetta rappresenta la quota di una categoria sul totale.
79
-          </p>
80 100
         </div>
81 101
       </div>
82 102
     </div>
@@ -84,15 +104,20 @@
84 104
     <div class="col-12 col-xl-6">
85 105
       <div class="card border h-100">
86 106
         <div class="card-header pb-2">
87
-          <h6 class="mb-0">Pareto costi (80/20)</h6>
107
+          <h6 class="card-title mb-0">
108
+            Pareto costi (80/20)
109
+            <i class="bx bx-info-circle text-primary ms-1"
110
+               role="button" tabindex="0"
111
+               data-bs-toggle="tooltip" data-bs-placement="top"
112
+               title="Barre = importo categoria · linea = % cumulata. Serve a vedere quali poche voci coprono gran parte dei costi."></i>
113
+          </h6>
114
+          <small class="text-muted">Fonte: Prima nota · uscite per categoria</small>
88 115
         </div>
89 116
         <div class="card-body">
90 117
           <div id="bilancio-costi-pareto-chart" style="min-height: 320px;"></div>
91
-          <p class="text-muted small mt-2 mb-0">
92
-            Pareto 80/20: le barre mostrano i valori, la linea indica la percentuale cumulata per capire quali categorie coprono gran parte dei costi.
93
-          </p>
94 118
         </div>
95 119
       </div>
96 120
     </div>
97 121
   </div>
122
+  </div>
98 123
 </div>

+ 43
- 18
resources/views/bilancio/_partials/ricavi.blade.php Ver fichero

@@ -1,9 +1,17 @@
1 1
 <div class="tab-pane fade" id="bilancio-tab-ricavi" role="tabpanel">
2
+  <div class="card-body pt-4">
2 3
   <div class="row g-3">
3
-    <div class="col-6">
4
+    <div class="col-12 col-xl-6">
4 5
       <div class="card border">
5 6
         <div class="card-header pb-2">
6
-          <h6 class="mb-0">Tabella Ricavi</h6>
7
+          <h6 class="card-title mb-0">
8
+            Tabella Ricavi
9
+            <i class="bx bx-info-circle text-primary ms-1"
10
+               role="button" tabindex="0"
11
+               data-bs-toggle="tooltip" data-bs-placement="top"
12
+               title="Elenco delle categorie contabili di entrata nell'anno, con conteggio movimenti e totale."></i>
13
+          </h6>
14
+          <small class="text-muted">Fonte: Prima nota · entrate</small>
7 15
         </div>
8 16
         <div class="card-body">
9 17
           <div class="table-responsive">
@@ -42,13 +50,17 @@
42 50
     <div class="col-12 col-xl-6">
43 51
       <div class="card border h-100">
44 52
         <div class="card-header pb-2">
45
-          <h6 class="mb-0">Trend ricavi mensili</h6>
53
+          <h6 class="card-title mb-0">
54
+            Trend ricavi mensili
55
+            <i class="bx bx-info-circle text-primary ms-1"
56
+               role="button" tabindex="0"
57
+               data-bs-toggle="tooltip" data-bs-placement="top"
58
+               title="Andamento dei ricavi mese per mese: una curva in crescita indica miglioramento delle entrate."></i>
59
+          </h6>
60
+          <small class="text-muted">Fonte: Prima nota · entrate</small>
46 61
         </div>
47 62
         <div class="card-body">
48 63
           <div id="bilancio-ricavi-chart" style="min-height: 230px;"></div>
49
-          <p class="text-muted small mt-2 mb-0">
50
-            Mostra l'andamento dei ricavi mese per mese: una curva in crescita indica un miglioramento delle entrate.
51
-          </p>
52 64
         </div>
53 65
       </div>
54 66
     </div>
@@ -56,13 +68,17 @@
56 68
     <div class="col-12 col-xl-6">
57 69
       <div class="card border h-100">
58 70
         <div class="card-header pb-2">
59
-          <h6 class="mb-0">Top categorie ricavi</h6>
71
+          <h6 class="card-title mb-0">
72
+            Top categorie ricavi
73
+            <i class="bx bx-info-circle text-primary ms-1"
74
+               role="button" tabindex="0"
75
+               data-bs-toggle="tooltip" data-bs-placement="top"
76
+               title="Confronto tra categorie di ricavo: le barre più lunghe hanno maggior impatto sul totale."></i>
77
+          </h6>
78
+          <small class="text-muted">Fonte: Prima nota · entrate per categoria</small>
60 79
         </div>
61 80
         <div class="card-body">
62 81
           <div id="bilancio-ricavi-categorie-chart" style="min-height: 260px;"></div>
63
-          <p class="text-muted small mt-2 mb-0">
64
-            Confronta le categorie che generano ricavi: le barre piu lunghe sono le voci con maggior impatto.
65
-          </p>
66 82
         </div>
67 83
       </div>
68 84
     </div>
@@ -70,13 +86,17 @@
70 86
     <div class="col-12 col-xl-6">
71 87
       <div class="card border h-100">
72 88
         <div class="card-header pb-2">
73
-          <h6 class="mb-0">Composizione ricavi (%)</h6>
89
+          <h6 class="card-title mb-0">
90
+            Composizione ricavi (%)
91
+            <i class="bx bx-info-circle text-primary ms-1"
92
+               role="button" tabindex="0"
93
+               data-bs-toggle="tooltip" data-bs-placement="top"
94
+               title="Distribuzione percentuale: ogni fetta indica quanto contribuisce una categoria al totale ricavi."></i>
95
+          </h6>
96
+          <small class="text-muted">Fonte: Prima nota · entrate per categoria</small>
74 97
         </div>
75 98
         <div class="card-body">
76 99
           <div id="bilancio-ricavi-percent-chart" style="min-height: 320px;"></div>
77
-          <p class="text-muted small mt-2 mb-0">
78
-            Distribuzione percentuale dei ricavi: ogni fetta indica quanto contribuisce una categoria al totale.
79
-          </p>
80 100
         </div>
81 101
       </div>
82 102
     </div>
@@ -84,15 +104,20 @@
84 104
     <div class="col-12 col-xl-6">
85 105
       <div class="card border h-100">
86 106
         <div class="card-header pb-2">
87
-          <h6 class="mb-0">Pareto ricavi (80/20)</h6>
107
+          <h6 class="card-title mb-0">
108
+            Pareto ricavi (80/20)
109
+            <i class="bx bx-info-circle text-primary ms-1"
110
+               role="button" tabindex="0"
111
+               data-bs-toggle="tooltip" data-bs-placement="top"
112
+               title="Barre = importo · linea = % cumulata. Individua quali categorie coprono la maggior parte dei ricavi."></i>
113
+          </h6>
114
+          <small class="text-muted">Fonte: Prima nota · entrate per categoria</small>
88 115
         </div>
89 116
         <div class="card-body">
90 117
           <div id="bilancio-ricavi-pareto-chart" style="min-height: 320px;"></div>
91
-          <p class="text-muted small mt-2 mb-0">
92
-            Pareto 80/20: individua rapidamente quali categorie coprono la maggior parte dei ricavi.
93
-          </p>
94 118
         </div>
95 119
       </div>
96 120
     </div>
97 121
   </div>
122
+  </div>
98 123
 </div>

+ 132
- 88
resources/views/bilancio/_partials/sintesi.blade.php Ver fichero

@@ -19,114 +19,158 @@
19 19
 @endphp
20 20
 
21 21
 <div class="tab-pane fade show active" id="bilancio-tab-sintesi" role="tabpanel">
22
-  <div class="row g-3 mb-3">
23
-    <div class="col-12 col-md-6 col-xl-3">
24
-      <div class="card card-border-shadow-primary h-100">
25
-        <div class="card-body">
26
-          <div class="d-flex align-items-center justify-content-between">
27
-            <div class="content-left">
28
-              <span class="text-heading">Saldo annuale</span>
29
-              <h5 class="mb-0 mt-1 {{ ($saldoTotale ?? 0) >= 0 ? 'text-success' : 'text-danger' }}">
30
-                € {{ number_format((float)($saldoTotale ?? 0), 2, ',', '.') }}
31
-              </h5>
32
-              <small class="{{ $saldoTrendClass }}">
33
-                <i class="bx {{ $saldoTrendIcon }}"></i>
34
-                {{ number_format(abs($saldoDelta), 2, ',', '.') }} vs mese precedente
35
-              </small>
22
+  <div class="card-body pt-4">
23
+    <div class="row g-3 mb-4">
24
+      <div class="col-12 col-md-6 col-xl-3">
25
+        <div class="card card-border-shadow-primary h-100">
26
+          <div class="card-body">
27
+            <div class="d-flex align-items-start justify-content-between gap-2">
28
+              <div class="content-left">
29
+                <span class="fw-medium text-muted">
30
+                  Saldo annuale
31
+                  <i class="bx bx-info-circle text-primary ms-1"
32
+                     role="button" tabindex="0"
33
+                     data-bs-toggle="tooltip" data-bs-placement="top"
34
+                     title="Ricavi meno costi dell'anno selezionato. Il delta confronta l'ultimo mese con quello precedente."></i>
35
+                </span>
36
+                <h5 class="mb-0 mt-2 {{ ($saldoTotale ?? 0) >= 0 ? 'text-success' : 'text-danger' }}">
37
+                  € {{ number_format((float)($saldoTotale ?? 0), 2, ',', '.') }}
38
+                </h5>
39
+                <small class="{{ $saldoTrendClass }}">
40
+                  <i class="bx {{ $saldoTrendIcon }}"></i>
41
+                  {{ number_format(abs($saldoDelta), 2, ',', '.') }} vs mese precedente
42
+                </small>
43
+                <div class="small text-muted mt-1">Fonte: Prima nota</div>
44
+              </div>
45
+              <span class="avatar-initial rounded bg-label-primary">
46
+                <i class="bx bx-line-chart bx-lg"></i>
47
+              </span>
36 48
             </div>
37
-            <span class="avatar-initial rounded bg-label-primary">
38
-              <i class="bx bx-line-chart bx-lg"></i>
39
-            </span>
40 49
           </div>
41 50
         </div>
42 51
       </div>
43
-    </div>
44 52
 
45
-    <div class="col-12 col-md-6 col-xl-3">
46
-      <div class="card card-border-shadow-success h-100">
47
-        <div class="card-body">
48
-          <div class="d-flex align-items-center justify-content-between">
49
-            <div class="content-left">
50
-              <span class="text-heading">Ricavi totali</span>
51
-              <h5 class="mb-0 mt-1 text-success">
52
-                € {{ number_format((float)($ricaviTotali ?? 0), 2, ',', '.') }}
53
-              </h5>
54
-              <small>{{ (int)($movimentiCount ?? 0) }} movimenti registrati</small>
53
+      <div class="col-12 col-md-6 col-xl-3">
54
+        <div class="card card-border-shadow-success h-100">
55
+          <div class="card-body">
56
+            <div class="d-flex align-items-start justify-content-between gap-2">
57
+              <div class="content-left">
58
+                <span class="fw-medium text-muted">
59
+                  Ricavi totali
60
+                  <i class="bx bx-info-circle text-primary ms-1"
61
+                     role="button" tabindex="0"
62
+                     data-bs-toggle="tooltip" data-bs-placement="top"
63
+                     title="Somma delle entrate contabilizzate nell'anno. I movimenti no_contabile (es. staff) sono esclusi dal totale e segnalati sotto."></i>
64
+                </span>
65
+                <h5 class="mb-0 mt-2 text-success">
66
+                  € {{ number_format((float)($ricaviTotali ?? 0), 2, ',', '.') }}
67
+                </h5>
68
+                <small>
69
+                  {{ (int)($movimentiCount ?? 0) }} movimenti
70
+                  @if(($movimentiNonContabilizzatiCount ?? 0) > 0)
71
+                    <span class="text-muted">· {{ (int) $movimentiNonContabilizzatiCount }} non contabilizzati</span>
72
+                  @endif
73
+                </small>
74
+                <div class="small text-muted mt-1">Fonte: Prima nota · entrate</div>
75
+              </div>
76
+              <span class="avatar-initial rounded bg-label-success">
77
+                <i class="bx bx-trending-up bx-lg"></i>
78
+              </span>
55 79
             </div>
56
-            <span class="avatar-initial rounded bg-label-success">
57
-              <i class="bx bx-trending-up bx-lg"></i>
58
-            </span>
59 80
           </div>
60 81
         </div>
61 82
       </div>
62
-    </div>
63 83
 
64
-    <div class="col-12 col-md-6 col-xl-3">
65
-      <div class="card card-border-shadow-danger h-100">
66
-        <div class="card-body">
67
-          <div class="d-flex align-items-center justify-content-between">
68
-            <div class="content-left">
69
-              <span class="text-heading">Costi totali</span>
70
-              <h5 class="mb-0 mt-1 text-danger">
71
-                € {{ number_format((float)($costiTotali ?? 0), 2, ',', '.') }}
72
-              </h5>
73
-              <small>Top costo: {{ $topCosto['voce'] ?? 'N/D' }}</small>
84
+      <div class="col-12 col-md-6 col-xl-3">
85
+        <div class="card card-border-shadow-danger h-100">
86
+          <div class="card-body">
87
+            <div class="d-flex align-items-start justify-content-between gap-2">
88
+              <div class="content-left">
89
+                <span class="fw-medium text-muted">
90
+                  Costi totali
91
+                  <i class="bx bx-info-circle text-primary ms-1"
92
+                     role="button" tabindex="0"
93
+                     data-bs-toggle="tooltip" data-bs-placement="top"
94
+                     title="Somma delle uscite contabilizzate nell'anno. La voce top è la categoria con importo più alto."></i>
95
+                </span>
96
+                <h5 class="mb-0 mt-2 text-danger">
97
+                  € {{ number_format((float)($costiTotali ?? 0), 2, ',', '.') }}
98
+                </h5>
99
+                <small>Top costo: {{ $topCosto['voce'] ?? 'N/D' }}</small>
100
+                <div class="small text-muted mt-1">Fonte: Prima nota · uscite</div>
101
+              </div>
102
+              <span class="avatar-initial rounded bg-label-danger">
103
+                <i class="bx bx-trending-down bx-lg"></i>
104
+              </span>
74 105
             </div>
75
-            <span class="avatar-initial rounded bg-label-danger">
76
-              <i class="bx bx-trending-down bx-lg"></i>
77
-            </span>
78 106
           </div>
79 107
         </div>
80 108
       </div>
81
-    </div>
82 109
 
83
-    <div class="col-12 col-md-6 col-xl-3">
84
-      <div class="card card-border-shadow-info h-100">
85
-        <div class="card-body">
86
-          <div class="d-flex align-items-center justify-content-between">
87
-            <div class="content-left">
88
-              <span class="text-heading">Insight rapido</span>
89
-              <h6 class="mb-0 mt-1">
90
-                {{ !empty($bestMonth['mese']) ? 'Mese migliore: ' . $bestMonth['mese'] : 'Mese migliore non disponibile' }}
91
-              </h6>
92
-              <small>Top ricavo: {{ $topRicavo['voce'] ?? 'N/D' }}</small>
110
+      <div class="col-12 col-md-6 col-xl-3">
111
+        <div class="card card-border-shadow-info h-100">
112
+          <div class="card-body">
113
+            <div class="d-flex align-items-start justify-content-between gap-2">
114
+              <div class="content-left">
115
+                <span class="fw-medium text-muted">
116
+                  Insight rapido
117
+                  <i class="bx bx-info-circle text-primary ms-1"
118
+                     role="button" tabindex="0"
119
+                     data-bs-toggle="tooltip" data-bs-placement="top"
120
+                     title="Sintesi automatica: mese con saldo migliore e categoria di ricavo principale."></i>
121
+                </span>
122
+                <h6 class="mb-0 mt-2">
123
+                  {{ !empty($bestMonth['mese']) ? 'Mese migliore: ' . $bestMonth['mese'] : 'Mese migliore non disponibile' }}
124
+                </h6>
125
+                <small>Top ricavo: {{ $topRicavo['voce'] ?? 'N/D' }}</small>
126
+                <div class="small text-muted mt-1">Fonte: Prima nota</div>
127
+              </div>
128
+              <span class="avatar-initial rounded bg-label-info">
129
+                <i class="bx bx-bulb bx-lg"></i>
130
+              </span>
93 131
             </div>
94
-            <span class="avatar-initial rounded bg-label-info">
95
-              <i class="bx bx-bulb bx-lg"></i>
96
-            </span>
97 132
           </div>
98 133
         </div>
99 134
       </div>
100 135
     </div>
101
-  </div>
102 136
 
103
-  <h6 class="mb-2">Tabella Bilancio</h6>
104
-  <div class="table-responsive">
105
-    <table class="table table-sm table-bordered align-middle mb-0">
106
-      <thead>
107
-        <tr>
108
-          <th class="text-end">Totale ricavi</th>
109
-          <th class="text-center" style="width: 40px;">-</th>
110
-          <th class="text-end">Totale costi</th>
111
-          <th class="text-center" style="width: 40px;">=</th>
112
-          <th class="text-end">Saldo totale</th>
113
-        </tr>
114
-      </thead>
115
-      <tbody>
116
-        <tr>
117
-          <td class="text-end fw-semibold">€ {{ number_format((float)($ricaviTotali ?? 0), 2, ',', '.') }}</td>
118
-          <td class="text-center fw-semibold">-</td>
119
-          <td class="text-end fw-semibold">€ {{ number_format((float)($costiTotali ?? 0), 2, ',', '.') }}</td>
120
-          <td class="text-center fw-semibold">=</td>
121
-          <td class="text-end fw-bold {{ (($saldoTotale ?? 0) >= 0) ? 'text-primary' : 'text-warning' }}">
122
-            € {{ number_format((float)($saldoTotale ?? 0), 2, ',', '.') }}
123
-          </td>
124
-        </tr>
125
-      </tbody>
126
-    </table>
137
+    <div class="mb-2">
138
+      <h6 class="mb-0">
139
+        Tabella Bilancio
140
+        <i class="bx bx-info-circle text-primary ms-1"
141
+           role="button" tabindex="0"
142
+           data-bs-toggle="tooltip" data-bs-placement="top"
143
+           title="Equazione annuale: ricavi − costi = saldo. Il grafico sotto mostra le stesse voci mese per mese."></i>
144
+      </h6>
145
+      <small class="text-muted">Fonte: Prima nota</small>
146
+    </div>
147
+    <div class="table-responsive">
148
+      <table class="table table-sm table-bordered align-middle mb-0">
149
+        <thead>
150
+          <tr>
151
+            <th class="text-end">Totale ricavi</th>
152
+            <th class="text-center" style="width: 40px;">-</th>
153
+            <th class="text-end">Totale costi</th>
154
+            <th class="text-center" style="width: 40px;">=</th>
155
+            <th class="text-end">Saldo totale</th>
156
+          </tr>
157
+        </thead>
158
+        <tbody>
159
+          <tr>
160
+            <td class="text-end fw-semibold">€ {{ number_format((float)($ricaviTotali ?? 0), 2, ',', '.') }}</td>
161
+            <td class="text-center fw-semibold">-</td>
162
+            <td class="text-end fw-semibold">€ {{ number_format((float)($costiTotali ?? 0), 2, ',', '.') }}</td>
163
+            <td class="text-center fw-semibold">=</td>
164
+            <td class="text-end fw-bold {{ (($saldoTotale ?? 0) >= 0) ? 'text-primary' : 'text-warning' }}">
165
+              € {{ number_format((float)($saldoTotale ?? 0), 2, ',', '.') }}
166
+            </td>
167
+          </tr>
168
+        </tbody>
169
+      </table>
170
+    </div>
171
+    <div id="bilancio-saldo-col-chart" class="mt-3" style="min-height: 280px;"></div>
172
+    <p class="text-muted small mt-2 mb-0">
173
+      Colonne = ricavi e costi per mese · linea = saldo. Se la linea sale, il margine migliora.
174
+    </p>
127 175
   </div>
128
-  <div id="bilancio-saldo-col-chart" class="mt-3" style="min-height: 280px;"></div>
129
-  <p class="text-muted small mt-2 mb-0">
130
-    Come leggere: le colonne mostrano ricavi e costi per mese, la linea mostra il saldo (ricavi - costi). Se la linea sale, il margine migliora.
131
-  </p>
132 176
 </div>

+ 111
- 36
resources/views/bilancio/_partials/statistiche.blade.php Ver fichero

@@ -10,127 +10,202 @@
10 10
 @endphp
11 11
 
12 12
 <div class="tab-pane fade" id="bilancio-tab-statistiche" role="tabpanel">
13
+  <div class="card-body pt-4">
13 14
   <div class="mb-3">
14
-    <h6 class="mb-1">Panoramica e Previsioni</h6>
15
-    <p class="text-muted small mb-0">Riassume lo stato economico medio e una stima del prossimo mese. [Fonte: Prima Nota e Pagamenti].</p>
15
+    <h6 class="mb-0">
16
+      Panoramica e Previsioni
17
+      <i class="bx bx-info-circle text-primary ms-1"
18
+         role="button" tabindex="0"
19
+         data-bs-toggle="tooltip" data-bs-placement="top"
20
+         title="Stato economico medio e stima del prossimo mese. La previsione è indicativa, basata sulla media dei mesi recenti."></i>
21
+    </h6>
16 22
   </div>
17
-  <div class="row g-3 mb-4">
18
-    <div class="col-12 col-md-6 col-xl-4">
23
+  <div class="row g-3 mb-3">
24
+    <div class="col-12 col-md-6">
19 25
       <div class="card card-border-shadow-warning h-100">
20
-        <div class="card-body">
21
-          <span class="text-heading">Previsione prossimo mese</span>
26
+        <div class="card-body py-3">
27
+          <span class="fw-medium text-muted">
28
+            Previsione prossimo mese
29
+            <i class="bx bx-info-circle text-primary ms-1"
30
+               role="button" tabindex="0"
31
+               data-bs-toggle="tooltip" data-bs-placement="top"
32
+               title="Media di ricavi/costi sugli ultimi mesi disponibili. Non sostituisce una pianificazione ufficiale."></i>
33
+          </span>
22 34
           <h5 class="mb-0 mt-2 {{ ($previsione['saldo'] ?? 0) >= 0 ? 'text-success' : 'text-danger' }}">
23 35
             Saldo stimato: € {{ number_format((float)($previsione['saldo'] ?? 0), 2, ',', '.') }}
24 36
           </h5>
25
-          <small class="d-block mt-1">Ricavi stimati: € {{ number_format((float)($previsione['ricavi'] ?? 0), 2, ',', '.') }}</small>
26
-          <small class="d-block">Costi stimati: € {{ number_format((float)($previsione['costi'] ?? 0), 2, ',', '.') }}</small>
37
+          <div class="d-flex flex-wrap gap-3 mt-1">
38
+            <small>Ricavi: € {{ number_format((float)($previsione['ricavi'] ?? 0), 2, ',', '.') }}</small>
39
+            <small>Costi: € {{ number_format((float)($previsione['costi'] ?? 0), 2, ',', '.') }}</small>
40
+          </div>
27 41
           <small class="text-warning d-block mt-2">
28
-            Stima indicativa basata sugli ultimi {{ (int)($previsione['mesi_considerati'] ?? 0) }} mesi: non fare affidamento esclusivo su questi valori.
42
+            Stima sugli ultimi {{ (int)($previsione['mesi_considerati'] ?? 0) }} mesi
29 43
           </small>
44
+          <div class="small text-muted mt-1">Fonte: Prima nota (media mensile)</div>
30 45
         </div>
31 46
       </div>
32 47
     </div>
33
-    <div class="col-12 col-md-6 col-xl-4">
48
+    <div class="col-12 col-md-6">
34 49
       <div class="card card-border-shadow-primary h-100">
35
-        <div class="card-body">
36
-          <span class="text-heading">Ticket medio</span>
50
+        <div class="card-body py-3">
51
+          <span class="fw-medium text-muted">
52
+            Ticket medio
53
+            <i class="bx bx-info-circle text-primary ms-1"
54
+               role="button" tabindex="0"
55
+               data-bs-toggle="tooltip" data-bs-placement="top"
56
+               title="Valore medio di ogni pagamento pagato nell'anno (staff escluso). Incasso cassa ÷ numero pagamenti."></i>
57
+          </span>
37 58
           <h5 class="mb-0 mt-2 text-primary">€ {{ number_format($ticketMedio, 2, ',', '.') }}</h5>
38
-          <small class="d-block mt-1">Calcolato su {{ $pagamentiAnno }} pagamenti dell'anno selezionato.</small>
39
-          <small class="text-muted d-block mt-2">Indica il valore medio di ogni pagamento emesso.</small>
59
+          <small class="d-block mt-1">Su {{ $pagamentiAnno }} pagamenti dell'anno</small>
60
+          <div class="small text-muted mt-1">Fonte: Pagamenti (staff escluso)</div>
40 61
         </div>
41 62
       </div>
42 63
     </div>
43
-    <div class="col-12 col-md-12 col-xl-4">
44
-      <div class="card h-100">
45
-        <div class="card-header d-flex justify-content-between align-items-center">
46
-          <h5 class="card-title m-0">Performance mensile</h5>
47
-          <small class="text-muted">Ricavi / Costi / Saldo</small>
64
+  </div>
65
+  <div class="row g-3 mb-4">
66
+    <div class="col-12">
67
+      <div class="card">
68
+        <div class="card-header">
69
+          <h5 class="card-title mb-0">
70
+            Performance mensile
71
+            <i class="bx bx-info-circle text-primary ms-1"
72
+               role="button" tabindex="0"
73
+               data-bs-toggle="tooltip" data-bs-placement="top"
74
+               title="Andamento mensile di ricavi, costi e saldo sull'anno selezionato."></i>
75
+          </h5>
76
+          <small class="text-muted">Fonte: Prima nota · Ricavi / Costi / Saldo</small>
48 77
         </div>
49 78
         <div class="card-body">
50 79
           <div id="bilancio-stat-performance-chart" style="min-height: 320px;"></div>
51
-          <p class="text-muted small mb-0">Andamento mensile delle metriche principali.</p>
52 80
         </div>
53 81
       </div>
54 82
     </div>
55 83
   </div>
56 84
 
57 85
   <div class="mb-3">
58
-    <h6 class="mb-1">Affluenza</h6>
59
-    <p class="text-muted small mb-0">Descrive l'affluenza della clientela per orari e giorni. [Fonte: emissione pagamenti].</p>
86
+    <h6 class="mb-0">
87
+      Affluenza
88
+      <i class="bx bx-info-circle text-primary ms-1"
89
+         role="button" tabindex="0"
90
+         data-bs-toggle="tooltip" data-bs-placement="top"
91
+         title="Quando e in quali giorni si concentrano i pagamenti. Utile per turni e picchi di servizio."></i>
92
+    </h6>
60 93
   </div>
61 94
   <div class="row g-3 mb-4">
62 95
     <div class="col-12 col-xl-6">
63 96
       <div class="card h-100">
64 97
         <div class="card-header">
65
-          <h6 class="m-0">Heatmap affluenza (pagamenti)</h6>
98
+          <h6 class="card-title mb-0">
99
+            Heatmap affluenza
100
+            <i class="bx bx-info-circle text-primary ms-1"
101
+               role="button" tabindex="0"
102
+               data-bs-toggle="tooltip" data-bs-placement="top"
103
+               title="Righe = giorni della settimana · colonne = ore. Più intenso = più pagamenti in quella fascia."></i>
104
+          </h6>
105
+          <small class="text-muted">Fonte: Pagamenti · data/ora</small>
66 106
         </div>
67 107
         <div class="card-body">
68 108
           <div id="bilancio-stat-affluenza-heatmap" style="min-height: 320px;"></div>
69
-          <p class="text-muted small mb-0">Righe = giorni, colonne = ore: evidenzia quando si concentra la domanda.</p>
70 109
         </div>
71 110
       </div>
72 111
     </div>
73 112
     <div class="col-12 col-xl-6">
74 113
       <div class="card h-100">
75 114
         <div class="card-header">
76
-          <h6 class="m-0">Affluenza per giorno (pagamenti)</h6>
115
+          <h6 class="card-title mb-0">
116
+            Affluenza per giorno
117
+            <i class="bx bx-info-circle text-primary ms-1"
118
+               role="button" tabindex="0"
119
+               data-bs-toggle="tooltip" data-bs-placement="top"
120
+               title="Volume di pagamenti aggregato per giorno della settimana."></i>
121
+          </h6>
122
+          <small class="text-muted">Fonte: Pagamenti · data/ora</small>
77 123
         </div>
78 124
         <div class="card-body">
79 125
           <div id="bilancio-stat-affluenza-giorni-chart" style="min-height: 320px;"></div>
80
-          <p class="text-muted small mb-0">Confronto rapido dei giorni con maggiore volume di pagamenti.</p>
81 126
         </div>
82 127
       </div>
83 128
     </div>
84 129
   </div>
85 130
 
86 131
   <div class="mb-3">
87
-    <h6 class="mb-1">Performance Operativa</h6>
88
-    <p class="text-muted small mb-0">Mostra quali categorie, piatti, cucine e metodi di pagamento incidono di più sui risultati.</p>
132
+    <h6 class="mb-0">
133
+      Performance Operativa
134
+      <i class="bx bx-info-circle text-primary ms-1"
135
+         role="button" tabindex="0"
136
+         data-bs-toggle="tooltip" data-bs-placement="top"
137
+         title="Quali categorie, piatti, cucine e metodi di pagamento incidono di più sul risultato annuale."></i>
138
+    </h6>
89 139
   </div>
90 140
   <div class="row g-3">
91 141
     <div class="col-12 col-xl-6">
92 142
       <div class="card h-100">
93 143
         <div class="card-header">
94
-          <h6 class="m-0">Performance categorie contabili</h6>
144
+          <h6 class="card-title mb-0">
145
+            Performance categorie contabili
146
+            <i class="bx bx-info-circle text-primary ms-1"
147
+               role="button" tabindex="0"
148
+               data-bs-toggle="tooltip" data-bs-placement="top"
149
+               title="Saldo per categoria (ricavi − costi): evidenzia voci che aiutano o penalizzano il risultato."></i>
150
+          </h6>
151
+          <small class="text-muted">Fonte: Prima nota · per categoria</small>
95 152
         </div>
96 153
         <div class="card-body">
97 154
           <div id="bilancio-stat-categoria-chart" style="min-height: 320px;"></div>
98
-          <p class="text-muted small mb-0">Saldo per categoria (ricavi - costi): evidenzia le categorie che aiutano o penalizzano il risultato.</p>
99 155
         </div>
100 156
       </div>
101 157
     </div>
102 158
     <div class="col-12 col-xl-6">
103 159
       <div class="card h-100">
104 160
         <div class="card-header">
105
-          <h6 class="m-0">Performance piatti</h6>
161
+          <h6 class="card-title mb-0">
162
+            Performance piatti
163
+            <i class="bx bx-info-circle text-primary ms-1"
164
+               role="button" tabindex="0"
165
+               data-bs-toggle="tooltip" data-bs-placement="top"
166
+               title="Top piatti per incasso annuale, da righe ordine legate a pagamenti reali (staff escluso)."></i>
167
+          </h6>
168
+          <small class="text-muted">Fonte: Righe ordine · pagamenti (staff escluso)</small>
106 169
         </div>
107 170
         <div class="card-body">
108 171
           <div id="bilancio-stat-piatti-chart" style="min-height: 320px;"></div>
109
-          <p class="text-muted small mb-0">Top piatti per incasso annuale (righe ordine incassate).</p>
110 172
         </div>
111 173
       </div>
112 174
     </div>
113 175
     <div class="col-12 col-xl-6">
114 176
       <div class="card h-100">
115 177
         <div class="card-header">
116
-          <h6 class="m-0">Performance cucine</h6>
178
+          <h6 class="card-title mb-0">
179
+            Performance cucine
180
+            <i class="bx bx-info-circle text-primary ms-1"
181
+               role="button" tabindex="0"
182
+               data-bs-toggle="tooltip" data-bs-placement="top"
183
+               title="Contributo di ciascuna cucina ai ricavi complessivi dell'anno."></i>
184
+          </h6>
185
+          <small class="text-muted">Fonte: Righe ordine · pagamenti (staff escluso)</small>
117 186
         </div>
118 187
         <div class="card-body">
119 188
           <div id="bilancio-stat-cucine-chart" style="min-height: 320px;"></div>
120
-          <p class="text-muted small mb-0">Contributo delle cucine ai ricavi complessivi.</p>
121 189
         </div>
122 190
       </div>
123 191
     </div>
124 192
     <div class="col-12 col-xl-6">
125 193
       <div class="card h-100">
126 194
         <div class="card-header">
127
-          <h6 class="m-0">Performance pagamenti</h6>
195
+          <h6 class="card-title mb-0">
196
+            Performance pagamenti
197
+            <i class="bx bx-info-circle text-primary ms-1"
198
+               role="button" tabindex="0"
199
+               data-bs-toggle="tooltip" data-bs-placement="top"
200
+               title="Metodi di pagamento con maggior impatto sugli incassi cassa (staff escluso)."></i>
201
+          </h6>
202
+          <small class="text-muted">Fonte: Pagamenti (staff escluso)</small>
128 203
         </div>
129 204
         <div class="card-body">
130 205
           <div id="bilancio-stat-pagamenti-chart" style="min-height: 320px;"></div>
131
-          <p class="text-muted small mb-0">Metodi di pagamento con impatto maggiore sugli incassi.</p>
132 206
         </div>
133 207
       </div>
134 208
     </div>
135 209
   </div>
210
+  </div>
136 211
 </div>

+ 69
- 32
resources/views/bilancio/index.blade.php Ver fichero

@@ -48,7 +48,7 @@ $configData = Helper::appClasses();
48 48
             <ol class="breadcrumb breadcrumb-custom-icon">
49 49
       
50 50
               <li class="breadcrumb-item">
51
-                <a href="#">Configurazioni</a>
51
+                <a href="#">Bilancio</a>
52 52
                 <i class="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
53 53
               </li>
54 54
               <li class="breadcrumb-item active text-primary">
@@ -65,14 +65,42 @@ $configData = Helper::appClasses();
65 65
     display: none !important;
66 66
   }
67 67
 
68
-  .bilancio-float-header {
69
-    max-width: 1100px;
70
-    margin: 0 auto 1rem auto;
71
-    border-radius: 1rem;
68
+  .bilancio-page {
69
+    width: 100%;
70
+    max-width: 100%;
72 71
   }
73 72
 
74
-  .bilancio-float-header .nav-pills .nav-link {
75
-    border-radius: .7rem;
73
+  /* Sneat aggiunge padding/bordo a .tab-content */
74
+  .bilancio-page .bilancio-tab-content {
75
+    padding: 0 !important;
76
+    border: 0 !important;
77
+    box-shadow: none !important;
78
+    width: 100%;
79
+    max-width: 100%;
80
+  }
81
+
82
+  .bilancio-page .bilancio-tab-content > .tab-pane {
83
+    padding: 0;
84
+    width: 100%;
85
+    max-width: 100%;
86
+  }
87
+
88
+  .bilancio-page .bilancio-tab-bar,
89
+  .bilancio-page .bilancio-content-card {
90
+    width: 100%;
91
+    border-radius: var(--bs-border-radius, .375rem);
92
+  }
93
+
94
+  .bilancio-page .bilancio-tab-bar {
95
+    margin-bottom: 1rem;
96
+  }
97
+
98
+  .bilancio-page .bilancio-tab-content .row {
99
+    --bs-gutter-x: 1.5rem;
100
+  }
101
+
102
+  .bilancio-tab-bar .nav-pills .nav-link {
103
+    border-radius: var(--bs-border-radius, .375rem);
76 104
     padding: .45rem .9rem;
77 105
   }
78 106
 
@@ -139,27 +167,28 @@ $configData = Helper::appClasses();
139 167
 </div>
140 168
 @endif
141 169
 
142
-<div class="card shadow-sm border bilancio-float-header">
143
-  <div class="card-body py-3">
144
-    <div class="d-flex flex-wrap align-items-center justify-content-center gap-3">
145
-      <div class="text-center text-md-start">
146
-        <!-- <span class="text-muted small d-block">Attivita</span> -->
147
-        <strong>{{ $attivita?->nome ?? 'N/D' }}</strong>
170
+<div class="bilancio-page">
171
+  <div class="card shadow-sm border bilancio-tab-bar">
172
+    <div class="card-body py-3">
173
+      <div class="d-flex flex-wrap align-items-center justify-content-between gap-3 w-100 mb-3">
174
+        <div class="text-center text-md-start">
175
+          <strong>{{ $attivita?->nome ?? 'N/D' }}</strong>
176
+        </div>
177
+
178
+        <form method="GET" action="{{ route('bilancio.index') }}" class="d-flex align-items-center gap-2">
179
+          <label for="bilancio-anno" class="form-label mb-0 small text-muted">Anno</label>
180
+          <select id="bilancio-anno" name="anno" class="form-select form-select-sm" style="min-width: 110px;">
181
+            @foreach(($anniSelezionabili ?? collect()) as $anno)
182
+              <option value="{{ $anno }}" {{ (int)($selectedYear ?? now()->year) === (int)$anno ? 'selected' : '' }}>
183
+                {{ $anno }}
184
+              </option>
185
+            @endforeach
186
+          </select>
187
+          <button type="submit" class="btn btn-sm btn-primary">Applica</button>
188
+        </form>
148 189
       </div>
149 190
 
150
-      <form method="GET" action="{{ route('bilancio.index') }}" class="d-flex align-items-center gap-2">
151
-        <label for="bilancio-anno" class="form-label mb-0 small text-muted">Anno</label>
152
-        <select id="bilancio-anno" name="anno" class="form-select form-select-sm" style="min-width: 110px;">
153
-          @foreach(($anniSelezionabili ?? collect()) as $anno)
154
-            <option value="{{ $anno }}" {{ (int)($selectedYear ?? now()->year) === (int)$anno ? 'selected' : '' }}>
155
-              {{ $anno }}
156
-            </option>
157
-          @endforeach
158
-        </select>
159
-        <button type="submit" class="btn btn-sm btn-primary">Applica</button>
160
-      </form>
161
-
162
-      <ul class="nav nav-pills gap-1" role="tablist">
191
+      <ul class="nav nav-pills nav-fill gap-1 w-100" role="tablist">
163 192
         <li class="nav-item" role="presentation">
164 193
           <button type="button" class="nav-link active" role="tab" data-bs-toggle="tab" data-bs-target="#bilancio-tab-sintesi" aria-controls="bilancio-tab-sintesi" aria-selected="true">
165 194
             <i class="bx bx-pie-chart-alt me-1"></i>Sintesi
@@ -183,13 +212,15 @@ $configData = Helper::appClasses();
183 212
       </ul>
184 213
     </div>
185 214
   </div>
186
-</div>
187 215
 
188
-<div class="tab-content pt-2">
189
-  @include('bilancio._partials.statistiche')
190
-  @include('bilancio._partials.sintesi')
191
-  @include('bilancio._partials.costi')
192
-  @include('bilancio._partials.ricavi')
216
+  <div class="card shadow-sm border bilancio-content-card">
217
+    <div class="tab-content bilancio-tab-content w-100">
218
+      @include('bilancio._partials.statistiche')
219
+      @include('bilancio._partials.sintesi')
220
+      @include('bilancio._partials.costi')
221
+      @include('bilancio._partials.ricavi')
222
+    </div>
223
+  </div>
193 224
 </div>
194 225
 
195 226
 @endsection
@@ -197,6 +228,12 @@ $configData = Helper::appClasses();
197 228
 @section('page-script')
198 229
 <script>
199 230
   document.addEventListener('DOMContentLoaded', function () {
231
+    if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
232
+      document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (el) {
233
+        new bootstrap.Tooltip(el);
234
+      });
235
+    }
236
+
200 237
     var bilancioCharts = [];
201 238
     var statAffluenzaHeatmapChart = null;
202 239
     var affluenzaHeatmapSeries = @json($affluenzaHeatmapSeries);

+ 193
- 221
resources/views/report/_partials/cucine.blade.php Ver fichero

@@ -16,256 +16,228 @@
16 16
       'righe' => $righe,
17 17
       'quantita' => $quantita,
18 18
       'incasso' => $incasso,
19
+      'ticket' => $ordini > 0 ? ($incasso / $ordini) : 0.0,
19 20
     ];
20
-  });
21
-
22
-  if ($rows->isEmpty() && isset($cucine)) {
23
-    $rows = collect($cucine)->map(function ($cucina) {
24
-      return [
25
-        'cucina' => $cucina->nome ?? 'Cucina',
26
-        'ordini' => 0,
27
-        'righe' => 0,
28
-        'quantita' => 0,
29
-        'incasso' => 0.0,
30
-      ];
31
-    });
32
-  }
33
-
34
-  $rows = $rows->sortByDesc('incasso')->values();
21
+  })->sortByDesc('incasso')->values();
35 22
 
36 23
   $totOrdini = (int) $rows->sum('ordini');
37 24
   $totRighe = (int) $rows->sum('righe');
38 25
   $totQuantita = (int) $rows->sum('quantita');
39 26
   $totIncasso = (float) $rows->sum('incasso');
27
+  $nCucineConfigurate = (int) $rows->count();
28
+  $nCucineConVendite = (int) $rows->filter(fn ($r) => ($r['incasso'] ?? 0) > 0 || ($r['quantita'] ?? 0) > 0)->count();
29
+  $maxIncasso = (float) ($rows->max('incasso') ?: 0);
30
+
31
+  $uid = 'rcuc-' . substr(md5(uniqid((string) mt_rand(), true)), 0, 8);
40 32
 @endphp
41 33
 
42 34
 <style>
43
-  .report-print {
44
-    background: #fff;
45
-    color: #111827;
46
-    padding: 24px;
47
-    border: 1px solid #e5e7eb;
48
-    border-radius: 8px;
49
-    font-family: Arial, Helvetica, sans-serif;
50
-  }
51
-
52
-  .report-head {
53
-    display: flex;
54
-    align-items: flex-start;
55
-    justify-content: space-between;
56
-    gap: 16px;
57
-    margin-bottom: 18px;
58
-  }
59
-
60
-  .report-title {
61
-    font-size: 24px;
62
-    font-weight: 700;
63
-    margin: 0 0 4px;
64
-    color: #111827;
65
-  }
66
-
67
-  .report-title-row {
68
-    display: flex;
69
-    align-items: baseline;
70
-    gap: 10px;
71
-    flex-wrap: wrap;
72
-  }
73
-
74
-  .report-title-periodo {
75
-    font-size: 20px;
76
-    font-weight: 700;
77
-    color: #374151;
78
-    margin: 0;
79
-  }
80
-
81
-  .report-subtitle {
82
-    margin: 0;
83
-    font-size: 13px;
84
-    color: #4b5563;
85
-  }
86
-
87
-  .report-meta {
88
-    text-align: right;
89
-    font-size: 12px;
90
-    color: #6b7280;
91
-  }
92
-
93
-  .report-currency {
94
-    margin-bottom: 10px;
95
-    font-size: 12px;
96
-    color: #6b7280;
97
-    font-weight: 700;
35
+  .rcuc-row.is-hidden { display: none !important; }
36
+  .rcuc-table-wrap {
37
+    max-height: min(62vh, 560px);
38
+    overflow: auto;
39
+    border: 1px solid var(--bs-border-color);
40
+    border-radius: var(--bs-border-radius);
41
+  }
42
+  .rcuc-table {
43
+    margin-bottom: 0;
44
+    --bs-table-hover-bg: rgba(67, 89, 113, 0.04);
45
+  }
46
+  .rcuc-table thead th {
47
+    position: sticky;
48
+    top: 0;
49
+    z-index: 2;
50
+    background: var(--bs-gray-100, #f5f5f9);
51
+    box-shadow: inset 0 -1px 0 var(--bs-border-color);
52
+    font-size: .75rem;
53
+    font-weight: 600;
98 54
     text-transform: uppercase;
99
-    letter-spacing: 0.03em;
100
-  }
101
-
102
-  .report-cards {
103
-    display: grid;
104
-    grid-template-columns: repeat(4, minmax(0, 1fr));
105
-    gap: 10px;
106
-    margin-bottom: 18px;
107
-  }
108
-
109
-  .report-card {
110
-    border: 1px solid #e5e7eb;
111
-    border-radius: 8px;
112
-    padding: 10px 12px;
113
-    background: #f9fafb;
114
-  }
115
-
116
-  .report-card .label {
117
-    display: block;
118
-    font-size: 12px;
119
-    color: #6b7280;
120
-    margin-bottom: 4px;
121
-  }
122
-
123
-  .report-card .value {
124
-    font-size: 18px;
55
+    letter-spacing: .02em;
56
+    white-space: nowrap;
57
+  }
58
+  .rcuc-table tfoot td {
59
+    position: sticky;
60
+    bottom: 0;
61
+    z-index: 2;
62
+    background: var(--bs-gray-100, #f5f5f9);
63
+    box-shadow: inset 0 1px 0 var(--bs-border-color);
125 64
     font-weight: 700;
126
-    color: #111827;
127
-  }
128
-
129
-  .report-table {
130
-    width: 100%;
131
-    border-collapse: collapse;
132
-    border: 1px solid #e5e7eb;
133 65
   }
134
-
135
-  .report-table thead th {
136
-    font-size: 12px;
137
-    font-weight: 700;
138
-    text-align: left;
139
-    padding: 8px 10px;
140
-    color: #374151;
141
-    background: #f3f4f6;
142
-    border-bottom: 1px solid #e5e7eb;
143
-    text-transform: uppercase;
144
-    letter-spacing: 0.03em;
145
-  }
146
-
147
-  .report-table tbody td {
148
-    font-size: 12px;
149
-    padding: 8px 10px;
150
-    border-bottom: 1px solid #f3f4f6;
151
-    color: #1f2937;
152
-  }
153
-
154
-  .report-table tbody tr:nth-child(even) {
155
-    background: #fcfcfd;
156
-  }
157
-
158
-  .report-table tfoot td {
159
-    font-size: 12px;
160
-    font-weight: 700;
161
-    padding: 8px 10px;
162
-    border-top: 2px solid #d1d5db;
163
-    background: #f9fafb;
66
+  .rcuc-table .rcuc-num {
67
+    font-variant-numeric: tabular-nums;
68
+    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
69
+    font-size: .8125rem;
70
+    white-space: nowrap;
164 71
   }
165
-
166
-  .text-right {
167
-    text-align: right;
72
+  .rcuc-table td,
73
+  .rcuc-table th {
74
+    vertical-align: middle;
75
+    padding: .5rem .75rem;
168 76
   }
169
-
170
-  .font-mono {
171
-    font-family: "Consolas", "Courier New", monospace;
172
-    font-variant-numeric: tabular-nums;
77
+  .rcuc-table .rcuc-col-incasso {
78
+    background: rgba(40, 199, 111, 0.04);
79
+    font-weight: 600;
173 80
   }
174
-
175
-  .report-footer {
176
-    margin-top: 12px;
177
-    font-size: 11px;
178
-    color: #6b7280;
81
+  .rcuc-table thead th.rcuc-col-incasso,
82
+  .rcuc-table tfoot td.rcuc-col-incasso {
83
+    background: rgba(40, 199, 111, 0.08);
179 84
   }
180
-
181 85
   @media print {
182
-    body {
183
-      background: #fff !important;
184
-    }
185
-
186
-    .report-print {
187
-      border: 0;
188
-      border-radius: 0;
189
-      padding: 0;
190
-    }
86
+    .rcuc-toolbar { display: none !important; }
87
+    .rcuc-table-wrap { max-height: none; overflow: visible; border: 0; }
88
+    .rcuc-table thead th,
89
+    .rcuc-table tfoot td { position: static; box-shadow: none; }
191 90
   }
192 91
 </style>
193 92
 
194
-<section class="report-print">
195
-  <header class="report-head">
93
+<div id="{{ $uid }}">
94
+  <div class="d-flex flex-wrap justify-content-between align-items-start gap-2 mb-4">
196 95
     <div>
197
-      <div class="report-title-row">
198
-        <h1 class="report-title">{{ $reportTitle }}</h1>
199
-        <h2 class="report-title-periodo">Periodo: {{ $periodoLabel }}</h2>
200
-      </div>
201
-      <p class="report-subtitle">{{ $attivitaNome }}</p>
96
+      <h5 class="mb-1">{{ $reportTitle }}</h5>
97
+      <small class="text-muted">
98
+        <strong>{{ $attivitaNome }}</strong> · Periodo {{ $periodoLabel }}
99
+      </small>
202 100
     </div>
203
-    <div class="report-meta">
101
+    <small class="text-muted text-end">
204 102
       Generato il<br>
205 103
       <strong>{{ $generatoIl }}</strong>
206
-    </div>
207
-  </header>
208
-
209
-  <div class="report-currency">Valuta: EUR</div>
104
+    </small>
105
+  </div>
210 106
 
211
-  <div class="report-cards">
212
-    <div class="report-card">
213
-      <span class="label">Cucine</span>
214
-      <span class="value">{{ number_format($rows->count(), 0, ',', '.') }}</span>
107
+  <div class="row g-3 mb-4">
108
+    <div class="col-sm-6 col-xl-3">
109
+      <div class="card card-border-shadow-secondary h-100">
110
+        <div class="card-body">
111
+          <span class="d-block text-muted small">Cucine con vendite</span>
112
+          <h5 class="mb-0 mt-1">{{ number_format($nCucineConVendite, 0, ',', '.') }}</h5>
113
+          <small class="text-muted">su {{ number_format($nCucineConfigurate, 0, ',', '.') }} configurate</small>
114
+        </div>
115
+      </div>
215 116
     </div>
216
-    <div class="report-card">
217
-      <span class="label">Ordini gestiti</span>
218
-      <span class="value">{{ number_format($totOrdini, 0, ',', '.') }}</span>
117
+    <div class="col-sm-6 col-xl-3">
118
+      <div class="card card-border-shadow-info h-100">
119
+        <div class="card-body">
120
+          <span class="d-block text-muted small">Ordini</span>
121
+          <h5 class="mb-0 mt-1">{{ number_format($totOrdini, 0, ',', '.') }}</h5>
122
+          <small class="text-muted">{{ number_format($totRighe, 0, ',', '.') }} righe</small>
123
+        </div>
124
+      </div>
219 125
     </div>
220
-    <div class="report-card">
221
-      <span class="label">Righe/Portate</span>
222
-      <span class="value">{{ number_format($totRighe, 0, ',', '.') }}</span>
126
+    <div class="col-sm-6 col-xl-3">
127
+      <div class="card card-border-shadow-warning h-100">
128
+        <div class="card-body">
129
+          <span class="d-block text-muted small">Pezzi</span>
130
+          <h5 class="mb-0 mt-1">{{ number_format($totQuantita, 0, ',', '.') }}</h5>
131
+          <small class="text-muted">Quantità venduta</small>
132
+        </div>
133
+      </div>
223 134
     </div>
224
-    <div class="report-card">
225
-      <span class="label">Incasso totale</span>
226
-      <span class="value">{{ number_format($totIncasso, 2, ',', '.') }}</span>
135
+    <div class="col-sm-6 col-xl-3">
136
+      <div class="card card-border-shadow-success h-100">
137
+        <div class="card-body">
138
+          <span class="d-block text-muted small">Incasso</span>
139
+          <h5 class="mb-0 mt-1 text-success">EUR {{ number_format($totIncasso, 2, ',', '.') }}</h5>
140
+          <small class="text-muted">Totale cucine nel periodo</small>
141
+        </div>
142
+      </div>
227 143
     </div>
228 144
   </div>
229 145
 
230
-  <table class="report-table">
231
-    <thead>
232
-      <tr>
233
-        <th>Cucina</th>
234
-        <th class="text-right font-mono">Ordini</th>
235
-        <th class="text-right font-mono">Righe</th>
236
-        <th class="text-right font-mono">Quantita</th>
237
-        <th class="text-right font-mono">Incasso</th>
238
-      </tr>
239
-    </thead>
240
-    <tbody>
241
-      @forelse($rows as $row)
242
-        <tr>
243
-          <td>{{ $row['cucina'] }}</td>
244
-          <td class="text-right font-mono">{{ number_format($row['ordini'], 0, ',', '.') }}</td>
245
-          <td class="text-right font-mono">{{ number_format($row['righe'], 0, ',', '.') }}</td>
246
-          <td class="text-right font-mono">{{ number_format($row['quantita'], 0, ',', '.') }}</td>
247
-          <td class="text-right font-mono">{{ number_format($row['incasso'], 2, ',', '.') }}</td>
248
-        </tr>
249
-      @empty
250
-        <tr>
251
-          <td colspan="5">Nessun dato cucina disponibile nel periodo selezionato.</td>
252
-        </tr>
253
-      @endforelse
254
-    </tbody>
255
-    @if($rows->isNotEmpty())
256
-      <tfoot>
257
-        <tr>
258
-          <td>Totale</td>
259
-          <td class="text-right font-mono">{{ number_format($totOrdini, 0, ',', '.') }}</td>
260
-          <td class="text-right font-mono">{{ number_format($totRighe, 0, ',', '.') }}</td>
261
-          <td class="text-right font-mono">{{ number_format($totQuantita, 0, ',', '.') }}</td>
262
-          <td class="text-right font-mono">{{ number_format($totIncasso, 2, ',', '.') }}</td>
263
-        </tr>
264
-      </tfoot>
265
-    @endif
266
-  </table>
267
-
268
-  <div class="report-footer">
269
-    Report per cucina dell'attivita nel periodo selezionato.
146
+  <div class="rcuc-toolbar d-flex flex-wrap justify-content-between align-items-end gap-2 mb-2">
147
+    <div>
148
+      <h6 class="mb-0">Performance per cucina</h6>
149
+      <small class="text-muted">Ordinato per incasso · header/footer fissi nello scroll</small>
150
+    </div>
151
+    <input type="search"
152
+           class="form-control form-control-sm"
153
+           id="{{ $uid }}-q"
154
+           placeholder="Cerca cucina…"
155
+           style="max-width: 240px;">
270 156
   </div>
271
-</section>
157
+
158
+  @if($rows->isEmpty())
159
+    <div class="alert alert-secondary mb-0">Nessun dato cucina nel periodo selezionato.</div>
160
+  @else
161
+    <div class="rcuc-table-wrap">
162
+      <table class="table table-sm table-hover table-striped rcuc-table align-middle">
163
+        <thead>
164
+          <tr>
165
+            <th style="width:48px;">#</th>
166
+            <th>Cucina</th>
167
+            <th class="text-end">Ordini</th>
168
+            <th class="text-end">Righe</th>
169
+            <th class="text-end">Pezzi</th>
170
+            <th class="text-end">Ticket</th>
171
+            <th class="text-end rcuc-col-incasso">Incasso</th>
172
+          </tr>
173
+        </thead>
174
+        <tbody>
175
+          @foreach($rows as $index => $row)
176
+            @php
177
+              $rank = $index + 1;
178
+              $share = $totIncasso > 0 ? (($row['incasso'] / $totIncasso) * 100) : 0;
179
+              $bar = $maxIncasso > 0 ? (($row['incasso'] / $maxIncasso) * 100) : 0;
180
+              $rankBadge = match ($rank) {
181
+                1 => 'bg-label-warning',
182
+                2 => 'bg-label-primary',
183
+                3 => 'bg-label-success',
184
+                default => 'bg-label-secondary',
185
+              };
186
+            @endphp
187
+            <tr class="rcuc-row" data-rcuc-search="{{ e(mb_strtolower($row['cucina'])) }}">
188
+              <td><span class="badge {{ $rankBadge }}">{{ $rank }}</span></td>
189
+              <td style="min-width: 180px;">
190
+                <div class="fw-medium">{{ $row['cucina'] }}</div>
191
+                <div class="progress mt-1" style="height: 4px;">
192
+                  <div class="progress-bar bg-warning" role="progressbar"
193
+                       style="width: {{ number_format($bar, 1, '.', '') }}%;"
194
+                       aria-valuenow="{{ (int) round($bar) }}" aria-valuemin="0" aria-valuemax="100"></div>
195
+                </div>
196
+                <small class="text-muted">{{ number_format($share, 1, ',', '.') }}%</small>
197
+              </td>
198
+              <td class="text-end rcuc-num">{{ number_format($row['ordini'], 0, ',', '.') }}</td>
199
+              <td class="text-end rcuc-num">{{ number_format($row['righe'], 0, ',', '.') }}</td>
200
+              <td class="text-end rcuc-num">{{ number_format($row['quantita'], 0, ',', '.') }}</td>
201
+              <td class="text-end rcuc-num">{{ number_format($row['ticket'], 2, ',', '.') }}</td>
202
+              <td class="text-end rcuc-num rcuc-col-incasso">{{ number_format($row['incasso'], 2, ',', '.') }}</td>
203
+            </tr>
204
+          @endforeach
205
+        </tbody>
206
+        <tfoot>
207
+          <tr>
208
+            <td></td>
209
+            <td>Totale</td>
210
+            <td class="text-end rcuc-num">{{ number_format($totOrdini, 0, ',', '.') }}</td>
211
+            <td class="text-end rcuc-num">{{ number_format($totRighe, 0, ',', '.') }}</td>
212
+            <td class="text-end rcuc-num">{{ number_format($totQuantita, 0, ',', '.') }}</td>
213
+            <td class="text-end rcuc-num">
214
+              {{ number_format($totOrdini > 0 ? ($totIncasso / $totOrdini) : 0, 2, ',', '.') }}
215
+            </td>
216
+            <td class="text-end rcuc-num rcuc-col-incasso">{{ number_format($totIncasso, 2, ',', '.') }}</td>
217
+          </tr>
218
+        </tfoot>
219
+      </table>
220
+    </div>
221
+  @endif
222
+
223
+  <small class="text-muted d-block mt-2">
224
+    Documento a uso interno · valori in euro · ordini pagati (staff escluso dall’incasso)
225
+  </small>
226
+</div>
227
+
228
+<script>
229
+(function () {
230
+  var root = document.getElementById(@json($uid));
231
+  if (!root) return;
232
+  var qInput = document.getElementById(@json($uid . '-q'));
233
+  if (!qInput) return;
234
+
235
+  qInput.addEventListener('input', function () {
236
+    var q = (qInput.value || '').trim().toLowerCase();
237
+    root.querySelectorAll('.rcuc-row').forEach(function (row) {
238
+      var hay = row.getAttribute('data-rcuc-search') || '';
239
+      row.classList.toggle('is-hidden', q.length > 0 && hay.indexOf(q) === -1);
240
+    });
241
+  });
242
+})();
243
+</script>

+ 222
- 239
resources/views/report/_partials/incassi.blade.php Ver fichero

@@ -3,6 +3,7 @@
3 3
   $attivitaNome = $attivitaNome ?? 'Attivita';
4 4
   $periodoLabel = $periodoLabel ?? 'Periodo non specificato';
5 5
   $generatoIl = $generatoIl ?? now()->format('d/m/Y H:i');
6
+
6 7
   $movimenti = collect($movimenti ?? [])
7 8
     ->map(function ($movimento) {
8 9
       $rawTs = $movimento['timestamp'] ?? $movimento['data'] ?? null;
@@ -10,317 +11,299 @@
10 11
 
11 12
       if ($parsedTs !== false) {
12 13
         $movimento['_sort_ts'] = $parsedTs;
13
-        $movimento['_display_ts'] = date('Y-m-d H:i:s', $parsedTs);
14 14
         $movimento['_date_key'] = date('Y-m-d', $parsedTs);
15 15
         $movimento['_date_label'] = date('d/m/Y', $parsedTs);
16
+        $movimento['_weekday'] = \Carbon\Carbon::createFromTimestamp($parsedTs)->locale('it')->isoFormat('ddd');
16 17
       } else {
17 18
         $movimento['_sort_ts'] = 0;
18
-        $movimento['_display_ts'] = '-';
19 19
         $movimento['_date_key'] = '0000-00-00';
20 20
         $movimento['_date_label'] = '-';
21
+        $movimento['_weekday'] = '';
21 22
       }
22 23
 
23 24
       $movimento['_metodo'] = (string) ($movimento['metodo'] ?? 'N/D');
24 25
       $tipo = strtolower((string) ($movimento['tipo'] ?? 'entrata'));
25 26
       $importo = (float) ($movimento['importo'] ?? 0);
26
-      // Per incassi i movimenti di uscita (es. rimborsi) impattano in negativo.
27
-      $movimento['_signed_importo'] = $tipo === 'uscita' ? -abs($importo) : abs($importo);
27
+
28
+      if ($tipo === 'uscita') {
29
+        $movimento['_signed_importo'] = -abs($importo);
30
+        $movimento['_in_saldo'] = true;
31
+      } elseif ($tipo === 'entrata') {
32
+        $movimento['_signed_importo'] = abs($importo);
33
+        $movimento['_in_saldo'] = true;
34
+      } else {
35
+        $movimento['_signed_importo'] = 0.0;
36
+        $movimento['_in_saldo'] = false;
37
+      }
28 38
 
29 39
       return $movimento;
30 40
     })
31 41
     ->sortByDesc('_sort_ts')
32 42
     ->values();
33 43
 
44
+  $movimentiSaldo = $movimenti->where('_in_saldo', true);
34 45
   $totEntrate = (float) ($totEntrate ?? $movimenti->where('tipo', 'entrata')->sum('importo'));
35 46
   $totUscite = (float) ($totUscite ?? $movimenti->where('tipo', 'uscita')->sum('importo'));
36 47
   $saldo = (float) ($saldo ?? ($totEntrate - $totUscite));
37 48
 
38
-  $metodiPagamento = $movimenti
49
+  $metodiPagamento = $movimentiSaldo
39 50
     ->pluck('_metodo')
40 51
     ->filter()
41 52
     ->unique()
42 53
     ->sort()
43 54
     ->values();
44 55
 
45
-  $giorni = $movimenti
56
+  $totPerMetodo = $metodiPagamento->mapWithKeys(function ($metodo) use ($movimentiSaldo) {
57
+    return [$metodo => (float) $movimentiSaldo->where('_metodo', $metodo)->sum('_signed_importo')];
58
+  });
59
+
60
+  $giorni = $movimentiSaldo
46 61
     ->groupBy('_date_key')
47 62
     ->sortKeysDesc()
48
-    ->map(function ($items, $dateKey) use ($metodiPagamento) {
63
+    ->map(function ($items) use ($metodiPagamento) {
49 64
       $primaRiga = $items->first();
50 65
       $celle = [];
51 66
       foreach ($metodiPagamento as $metodo) {
52
-        $celle[$metodo] = (float) $items
53
-          ->where('_metodo', $metodo)
54
-          ->sum('_signed_importo');
67
+        $celle[$metodo] = (float) $items->where('_metodo', $metodo)->sum('_signed_importo');
55 68
       }
56 69
 
57 70
       return [
58
-        'date_key' => $dateKey,
59 71
         'date_label' => $primaRiga['_date_label'] ?? '-',
72
+        'weekday' => $primaRiga['_weekday'] ?? '',
60 73
         'celle' => $celle,
61 74
         'totale' => array_sum($celle),
62 75
       ];
63 76
     })
64 77
     ->values();
78
+
79
+  $giorniAttivi = $giorni->count();
80
+  $mediaGiorno = $giorniAttivi > 0 ? ($saldo / $giorniAttivi) : 0.0;
81
+  $volume = max(abs($totEntrate) + abs($totUscite), 0.0001);
65 82
 @endphp
66 83
 
67 84
 <style>
68
-  .report-print {
69
-    background: #fff;
70
-    color: #111827;
71
-    padding: 24px;
72
-    border: 1px solid #e5e7eb;
73
-    border-radius: 8px;
74
-    font-family: Arial, Helvetica, sans-serif;
75
-  }
76
-
77
-  .report-head {
78
-    display: flex;
79
-    align-items: flex-start;
80
-    justify-content: space-between;
81
-    gap: 16px;
82
-    margin-bottom: 18px;
85
+  .rinc-matrix-wrap {
86
+    max-height: min(62vh, 560px);
87
+    overflow: auto;
88
+    border: 1px solid var(--bs-border-color);
89
+    border-radius: var(--bs-border-radius);
83 90
   }
84
-
85
-  .report-title {
86
-    font-size: 24px;
87
-    font-weight: 700;
88
-    margin: 0 0 4px;
89
-    color: #111827;
90
-  }
91
-
92
-  .report-title-row {
93
-    display: flex;
94
-    align-items: baseline;
95
-    gap: 10px;
96
-    flex-wrap: wrap;
97
-  }
98
-
99
-  .report-title-periodo {
100
-    font-size: 20px;
101
-    font-weight: 700;
102
-    color: #374151;
103
-    margin: 0;
104
-  }
105
-
106
-  .report-subtitle {
107
-    margin: 0;
108
-    font-size: 13px;
109
-    color: #4b5563;
110
-  }
111
-
112
-  .report-meta {
113
-    text-align: right;
114
-    font-size: 12px;
115
-    color: #6b7280;
91
+  .rinc-matrix {
92
+    margin-bottom: 0;
93
+    --bs-table-hover-bg: rgba(67, 89, 113, 0.04);
116 94
   }
117
-
118
-  .report-currency {
119
-    margin-bottom: 10px;
120
-    font-size: 12px;
121
-    color: #6b7280;
122
-    font-weight: 700;
95
+  .rinc-matrix thead th {
96
+    position: sticky;
97
+    top: 0;
98
+    z-index: 3;
99
+    background: var(--bs-gray-100, #f5f5f9);
100
+    box-shadow: inset 0 -1px 0 var(--bs-border-color);
101
+    white-space: nowrap;
102
+    font-size: .75rem;
103
+    font-weight: 600;
123 104
     text-transform: uppercase;
124
-    letter-spacing: 0.03em;
125
-  }
126
-
127
-  .report-cards {
128
-    display: grid;
129
-    grid-template-columns: repeat(3, minmax(0, 1fr));
130
-    gap: 10px;
131
-    margin-bottom: 18px;
132
-  }
133
-
134
-  .report-card {
135
-    border: 1px solid #e5e7eb;
136
-    border-radius: 8px;
137
-    padding: 10px 12px;
138
-    background: #f9fafb;
139
-  }
140
-
141
-  .report-card .label {
142
-    display: block;
143
-    font-size: 12px;
144
-    color: #6b7280;
145
-    margin-bottom: 4px;
105
+    letter-spacing: .02em;
146 106
   }
147
-
148
-  .report-card .value {
149
-    font-size: 18px;
150
-    font-weight: 700;
151
-    color: #111827;
107
+  .rinc-matrix th.rinc-sticky,
108
+  .rinc-matrix td.rinc-sticky {
109
+    position: sticky;
110
+    left: 0;
111
+    z-index: 2;
112
+    background: var(--bs-paper-bg, var(--bs-body-bg, #fff));
113
+    box-shadow: 1px 0 0 var(--bs-border-color);
114
+    min-width: 7.5rem;
152 115
   }
153
-
154
-  .report-table {
155
-    width: 100%;
156
-    border-collapse: collapse;
157
-    border: 1px solid #e5e7eb;
116
+  .rinc-matrix thead th.rinc-sticky {
117
+    z-index: 4;
118
+    background: var(--bs-gray-100, #f5f5f9);
158 119
   }
159
-
160
-  .report-table thead th {
161
-    font-size: 12px;
120
+  .rinc-matrix tfoot th,
121
+  .rinc-matrix tfoot td {
122
+    position: sticky;
123
+    bottom: 0;
124
+    z-index: 3;
125
+    background: var(--bs-gray-100, #f5f5f9);
126
+    box-shadow: inset 0 1px 0 var(--bs-border-color);
162 127
     font-weight: 700;
163
-    text-align: left;
164
-    padding: 8px 10px;
165
-    color: #374151;
166
-    background: #f3f4f6;
167
-    border-bottom: 1px solid #e5e7eb;
168
-    text-transform: uppercase;
169
-    letter-spacing: 0.03em;
170 128
   }
171
-
172
-  .report-table tbody td {
173
-    font-size: 12px;
174
-    padding: 8px 10px;
175
-    border-bottom: 1px solid #f3f4f6;
176
-    color: #1f2937;
177
-  }
178
-
179
-  .report-table tbody tr:nth-child(even) {
180
-    background: #fcfcfd;
129
+  .rinc-matrix tfoot td.rinc-sticky,
130
+  .rinc-matrix tfoot th.rinc-sticky {
131
+    z-index: 4;
181 132
   }
182
-
183
-  .text-right {
184
-    text-align: right;
185
-  }
186
-
187
-  .report-table th.text-right,
188
-  .report-table td.text-right {
189
-    text-align: right;
190
-  }
191
-
192
-  .font-mono {
193
-    font-family: "Consolas", "Courier New", monospace;
194
-    font-variant-numeric: tabular-nums;
195
-  }
196
-
197
-  .report-causale {
133
+  .rinc-matrix .rinc-col-total {
134
+    background: rgba(105, 108, 255, 0.04);
198 135
     font-weight: 600;
199
-    color: #111827;
200
-  }
201
-
202
-  .cell-negative {
203
-    color: #991b1b;
204 136
   }
205
-
206
-  .report-table tfoot td {
207
-    font-size: 12px;
208
-    font-weight: 700;
209
-    padding: 8px 10px;
210
-    border-top: 2px solid #d1d5db;
211
-    background: #f9fafb;
137
+  .rinc-matrix thead th.rinc-col-total,
138
+  .rinc-matrix tfoot td.rinc-col-total {
139
+    background: rgba(105, 108, 255, 0.08);
212 140
   }
213
-
214
-  .report-table .total-col {
215
-    font-weight: 700;
216
-    background: #fcfcfd;
141
+  .rinc-matrix .rinc-num {
142
+    font-variant-numeric: tabular-nums;
143
+    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
144
+    font-size: .8125rem;
145
+    white-space: nowrap;
217 146
   }
218
-
219
-  .report-footer {
220
-    margin-top: 12px;
221
-    font-size: 11px;
222
-    color: #6b7280;
147
+  .rinc-matrix td,
148
+  .rinc-matrix th {
149
+    vertical-align: middle;
150
+    padding: .45rem .7rem;
223 151
   }
224
-
225 152
   @media print {
226
-    body {
227
-      background: #fff !important;
228
-    }
229
-
230
-    .report-print {
231
-      border: 0;
232
-      border-radius: 0;
233
-      padding: 0;
234
-    }
153
+    .rinc-matrix-wrap { max-height: none; overflow: visible; border: 0; }
154
+    .rinc-matrix thead th,
155
+    .rinc-matrix th.rinc-sticky,
156
+    .rinc-matrix td.rinc-sticky,
157
+    .rinc-matrix tfoot td { position: static; box-shadow: none; }
235 158
   }
236 159
 </style>
237 160
 
238
-<section class="report-print">
239
-  <header class="report-head">
161
+<div>
162
+  <div class="d-flex flex-wrap justify-content-between align-items-start gap-2 mb-4">
240 163
     <div>
241
-      <div class="report-title-row">
242
-        <h1 class="report-title">{{ $reportTitle }}</h1>
243
-    
244
-        <h2 class="report-title-periodo">Periodo: {{ $periodoLabel }}</h2>
245
-      </div>
246
-      <p class="report-subtitle">
247
-        {{ $attivitaNome }}
248
-      </p>
164
+      <h5 class="mb-1">{{ $reportTitle }}</h5>
165
+      <small class="text-muted">
166
+        <strong>{{ $attivitaNome }}</strong> · Periodo {{ $periodoLabel }}
167
+      </small>
249 168
     </div>
250
-    <div class="report-meta">
169
+    <small class="text-muted text-end">
251 170
       Generato il<br>
252 171
       <strong>{{ $generatoIl }}</strong>
253
-    </div>
254
-  </header>
255
-
256
-  <div class="report-currency">Valuta: EUR</div>
172
+    </small>
173
+  </div>
257 174
 
258
-  <div class="report-cards">
259
-    <div class="report-card">
260
-      <span class="label">Totale Mov. Avere</span>
261
-      <span class="value">{{ number_format($totEntrate, 2, ',', '.') }}</span>
175
+  <div class="row g-3 mb-4">
176
+    <div class="col-sm-6 col-xl-3">
177
+      <div class="card card-border-shadow-success h-100">
178
+        <div class="card-body">
179
+          <span class="d-block text-muted small">Entrate</span>
180
+          <h5 class="mb-0 mt-1 text-success">EUR {{ number_format($totEntrate, 2, ',', '.') }}</h5>
181
+          <small class="text-muted">Movimenti in avere</small>
182
+        </div>
183
+      </div>
262 184
     </div>
263
-    <div class="report-card">
264
-      <span class="label">Totale Mov. Dare</span>
265
-      <span class="value">{{ number_format($totUscite, 2, ',', '.') }}</span>
185
+    <div class="col-sm-6 col-xl-3">
186
+      <div class="card card-border-shadow-danger h-100">
187
+        <div class="card-body">
188
+          <span class="d-block text-muted small">Uscite / storni</span>
189
+          <h5 class="mb-0 mt-1 text-danger">EUR {{ number_format($totUscite, 2, ',', '.') }}</h5>
190
+          <small class="text-muted">Movimenti in dare</small>
191
+        </div>
192
+      </div>
266 193
     </div>
267
-    <div class="report-card">
268
-      <span class="label">Saldo</span>
269
-      <span class="value">{{ number_format($saldo, 2, ',', '.') }}</span>
194
+    <div class="col-sm-6 col-xl-3">
195
+      <div class="card card-border-shadow-primary h-100">
196
+        <div class="card-body">
197
+          <span class="d-block text-muted small">Saldo netto</span>
198
+          <h5 class="mb-0 mt-1 {{ $saldo >= 0 ? 'text-success' : 'text-danger' }}">
199
+            EUR {{ number_format($saldo, 2, ',', '.') }}
200
+          </h5>
201
+          <small class="text-muted">Entrate − uscite</small>
202
+        </div>
203
+      </div>
204
+    </div>
205
+    <div class="col-sm-6 col-xl-3">
206
+      <div class="card card-border-shadow-info h-100">
207
+        <div class="card-body">
208
+          <span class="d-block text-muted small">Media / giorno</span>
209
+          <h5 class="mb-0 mt-1">EUR {{ number_format($mediaGiorno, 2, ',', '.') }}</h5>
210
+          <small class="text-muted">{{ number_format($giorniAttivi, 0, ',', '.') }} giorni con attività</small>
211
+        </div>
212
+      </div>
270 213
     </div>
271 214
   </div>
272 215
 
273
-  <table class="report-table">
274
-    <thead>
275
-      <tr>
276
-        <th>Data</th>
277
-        @foreach($metodiPagamento as $metodo)
278
-          <th class="text-right font-mono">{{ $metodo }}</th>
279
-        @endforeach
280
-        <th class="text-right font-mono">Totale</th>
281
-      </tr>
282
-    </thead>
283
-    <tbody>
284
-      @forelse($giorni as $giorno)
285
-        <tr>
286
-          <td class="font-mono">{{ $giorno['date_label'] }}</td>
287
-          @foreach($metodiPagamento as $metodo)
288
-            @php $val = (float) ($giorno['celle'][$metodo] ?? 0); @endphp
289
-            <td class="text-right font-mono {{ $val < 0 ? 'cell-negative' : '' }}">
290
-              {{ number_format($val, 2, ',', '.') }}
291
-            </td>
216
+  @if($metodiPagamento->isNotEmpty())
217
+    <div class="row g-3 mb-4">
218
+      @foreach($metodiPagamento as $metodo)
219
+        @php
220
+          $val = (float) ($totPerMetodo[$metodo] ?? 0);
221
+          $pct = round((abs($val) / $volume) * 100, 1);
222
+          $share = $totEntrate > 0 ? min(100, max(0, ($val / $totEntrate) * 100)) : 0;
223
+        @endphp
224
+        <div class="col-sm-6 col-lg-4 col-xl-3">
225
+          <div class="card h-100 border shadow-none">
226
+            <div class="card-body py-3">
227
+              <div class="d-flex justify-content-between align-items-start gap-2 mb-2">
228
+                <span class="fw-medium">{{ $metodo }}</span>
229
+                <span class="badge bg-label-secondary">{{ number_format($pct, 1, ',', '.') }}%</span>
230
+              </div>
231
+              <div class="mb-2 {{ $val < 0 ? 'text-danger' : '' }} fw-semibold">
232
+                EUR {{ number_format($val, 2, ',', '.') }}
233
+              </div>
234
+              <div class="progress" style="height: 4px;">
235
+                <div class="progress-bar bg-primary" role="progressbar"
236
+                     style="width: {{ number_format($share, 1, '.', '') }}%;"
237
+                     aria-valuenow="{{ (int) round($share) }}" aria-valuemin="0" aria-valuemax="100"></div>
238
+              </div>
239
+            </div>
240
+          </div>
241
+        </div>
242
+      @endforeach
243
+    </div>
244
+  @endif
245
+
246
+  <div class="mb-2 d-flex flex-wrap justify-content-between align-items-end gap-2">
247
+    <div>
248
+      <h6 class="mb-0">Incassi per giorno e metodo</h6>
249
+      <small class="text-muted">Header e totali fissi · prima colonna sticky in orizzontale</small>
250
+    </div>
251
+  </div>
252
+
253
+  @if($giorni->isEmpty())
254
+    <div class="alert alert-secondary mb-0">Nessun incasso nel periodo selezionato.</div>
255
+  @else
256
+    <div class="rinc-matrix-wrap">
257
+      <table class="table table-sm table-hover table-striped rinc-matrix align-middle">
258
+        <thead>
259
+          <tr>
260
+            <th class="rinc-sticky">Giorno</th>
261
+            @foreach($metodiPagamento as $metodo)
262
+              <th class="text-end">{{ $metodo }}</th>
263
+            @endforeach
264
+            <th class="text-end rinc-col-total">Totale</th>
265
+          </tr>
266
+        </thead>
267
+        <tbody>
268
+          @foreach($giorni as $giorno)
269
+            <tr>
270
+              <td class="rinc-sticky">
271
+                <span class="fw-medium">{{ $giorno['date_label'] }}</span>
272
+                @if($giorno['weekday'])
273
+                  <small class="text-muted ms-1 text-capitalize">{{ $giorno['weekday'] }}</small>
274
+                @endif
275
+              </td>
276
+              @foreach($metodiPagamento as $metodo)
277
+                @php $val = (float) ($giorno['celle'][$metodo] ?? 0); @endphp
278
+                <td class="text-end rinc-num {{ $val < 0 ? 'text-danger' : ($val > 0 ? 'text-body' : 'text-muted') }}">
279
+                  {{ $val == 0.0 ? '—' : number_format($val, 2, ',', '.') }}
280
+                </td>
281
+              @endforeach
282
+              <td class="text-end rinc-num rinc-col-total {{ $giorno['totale'] < 0 ? 'text-danger' : '' }}">
283
+                {{ number_format((float) $giorno['totale'], 2, ',', '.') }}
284
+              </td>
285
+            </tr>
292 286
           @endforeach
293
-          <td class="text-right font-mono total-col {{ $giorno['totale'] < 0 ? 'cell-negative' : '' }}">
294
-            {{ number_format((float) $giorno['totale'], 2, ',', '.') }}
295
-          </td>
296
-        </tr>
297
-      @empty
298
-        <tr>
299
-          <td colspan="{{ 2 + $metodiPagamento->count() }}">Nessun incasso trovato nel periodo selezionato.</td>
300
-        </tr>
301
-      @endforelse
302
-    </tbody>
303
-    @if($giorni->isNotEmpty())
304
-      <tfoot>
305
-        <tr>
306
-          <td>Totale periodo</td>
307
-          @foreach($metodiPagamento as $metodo)
308
-            @php
309
-              $totMetodo = (float) $movimenti->where('_metodo', $metodo)->sum('_signed_importo');
310
-            @endphp
311
-            <td class="text-right font-mono {{ $totMetodo < 0 ? 'cell-negative' : '' }}">
312
-              {{ number_format($totMetodo, 2, ',', '.') }}
287
+        </tbody>
288
+        <tfoot>
289
+          <tr>
290
+            <th class="rinc-sticky">Totale periodo</th>
291
+            @foreach($metodiPagamento as $metodo)
292
+              @php $totMetodo = (float) ($totPerMetodo[$metodo] ?? 0); @endphp
293
+              <td class="text-end rinc-num {{ $totMetodo < 0 ? 'text-danger' : '' }}">
294
+                {{ number_format($totMetodo, 2, ',', '.') }}
295
+              </td>
296
+            @endforeach
297
+            <td class="text-end rinc-num rinc-col-total {{ $saldo < 0 ? 'text-danger' : '' }}">
298
+              {{ number_format($saldo, 2, ',', '.') }}
313 299
             </td>
314
-          @endforeach
315
-          <td class="text-right font-mono total-col {{ $saldo < 0 ? 'cell-negative' : '' }}">
316
-            {{ number_format($saldo, 2, ',', '.') }}
317
-          </td>
318
-        </tr>
319
-      </tfoot>
320
-    @endif
321
-  </table>
300
+          </tr>
301
+        </tfoot>
302
+      </table>
303
+    </div>
304
+  @endif
322 305
 
323
-  <div class="report-footer">
324
-    Documento a uso interno. Valori espressi in euro.
325
-  </div>
326
-</section>
306
+  <small class="text-muted d-block mt-2">
307
+    Documento a uso interno · valori in euro
308
+  </small>
309
+</div>

+ 348
- 267
resources/views/report/_partials/movimenti.blade.php Ver fichero

@@ -3,6 +3,7 @@
3 3
   $attivitaNome = $attivitaNome ?? 'Attivita';
4 4
   $periodoLabel = $periodoLabel ?? 'Periodo non specificato';
5 5
   $generatoIl = $generatoIl ?? now()->format('d/m/Y H:i');
6
+
6 7
   $movimenti = collect($movimenti ?? [])
7 8
     ->map(function ($movimento) {
8 9
       $rawTs = $movimento['timestamp'] ?? $movimento['data'] ?? null;
@@ -10,314 +11,394 @@
10 11
 
11 12
       if ($parsedTs !== false) {
12 13
         $movimento['_sort_ts'] = $parsedTs;
13
-        $movimento['_display_ts'] = date('Y-m-d H:i:s', $parsedTs);
14
+        $movimento['_display_time'] = date('H:i', $parsedTs);
15
+        $movimento['_date_key'] = date('Y-m-d', $parsedTs);
16
+        $movimento['_date_label'] = date('d/m/Y', $parsedTs);
17
+        $movimento['_weekday'] = \Carbon\Carbon::createFromTimestamp($parsedTs)->locale('it')->isoFormat('dddd');
14 18
       } else {
15 19
         $movimento['_sort_ts'] = 0;
16
-        $movimento['_display_ts'] = '-';
20
+        $movimento['_display_time'] = '-';
21
+        $movimento['_date_key'] = '0000-00-00';
22
+        $movimento['_date_label'] = '-';
23
+        $movimento['_weekday'] = '';
17 24
       }
18 25
 
26
+      $tipo = strtolower(trim((string) ($movimento['tipo'] ?? '')));
27
+      $movimento['_tipo'] = $tipo;
28
+      $importo = abs((float) ($movimento['importo'] ?? 0));
29
+      // Segno contabile: entrata +, uscita -, no_contabile 0 sul saldo progressivo
30
+      $movimento['_signed'] = match ($tipo) {
31
+        'entrata' => $importo,
32
+        'uscita' => -$importo,
33
+        default => 0.0,
34
+      };
35
+      $movimento['_search'] = mb_strtolower(implode(' ', [
36
+        $movimento['causale'] ?? '',
37
+        $movimento['categoria'] ?? '',
38
+        $movimento['metodo'] ?? '',
39
+        $tipo,
40
+        $movimento['_date_label'] ?? '',
41
+      ]));
42
+
19 43
       return $movimento;
20 44
     })
21
-    ->sortByDesc('_sort_ts')
45
+    // Cronologico ASC per saldo progressivo leggibile; in UI mostriamo dal più recente in alto
46
+    ->sortBy('_sort_ts')
22 47
     ->values();
23 48
 
24
-  $totEntrate = (float) ($totEntrate ?? $movimenti->where('tipo', 'entrata')->sum('importo'));
25
-  $totUscite = (float) ($totUscite ?? $movimenti->where('tipo', 'uscita')->sum('importo'));
26
-  $saldo = (float) ($saldo ?? ($totEntrate - $totUscite));
27
-  $saldoClass = $saldo >= 0 ? 'value-positive' : 'value-negative';
28
-@endphp
49
+  // Saldo progressivo in ordine temporale
50
+  $running = 0.0;
51
+  $movimenti = $movimenti->map(function ($m) use (&$running) {
52
+    $running += (float) $m['_signed'];
53
+    $m['_running'] = $running;
54
+    return $m;
55
+  });
29 56
 
30
-<style>
31
-
32
-  @page {
33
-    margin: 68px 28px 44px;
34
-  }
57
+  // Vista: dal più recente
58
+  $movimentiView = $movimenti->sortByDesc('_sort_ts')->values();
35 59
 
36
-  .report-print {
37
-    background: #fff;
38
-    color: #0f172a;
39
-    padding: 0;
40
-    /* border: 1px solid #dbe3ee; */
41
-    border-radius: 10px;
42
-    box-shadow: 0 8px 22px rgba(15, 23, 42, 0.05);
43
-    overflow: hidden;
44
-    font-family: "DejaVu Sans", "Segoe UI", Arial, sans-serif;
45
-  }
46
-
47
-  .report-topline {
48
-    display: flex;
49
-    align-items: center;
50
-    justify-content: space-between;
51
-    gap: 12px;
52
-    margin: 16px 18px 10px;
53
-  }
54
-
55
-  .report-pill {
56
-    margin-top:10px;
57
-    display: inline-flex;
58
-    align-items: center;
59
-    gap: 8px;
60
-    border: 1px solid #cbd5e1;
61
-    border-radius: 999px;
62
-    padding: 6px 12px;
63
-    background: #f8fafc;
64
-    font-size: 12px;
65
-    color: #0f172a;
66
-    white-space: nowrap;
67
-  }
68
-
69
-  .report-pill-label {
70
-    font-weight: 700;
71
-    color: #64748b;
72
-    text-transform: uppercase;
73
-    letter-spacing: 0.04em;
74
-  }
75
-
76
-  .report-pill-value {
77
-    font-weight: 700;
78
-    color: #0f172a;
79
-  }
80
-
81
-  .report-title-wrap {
82
-    margin: 0 18px 14px;
83
-    text-align: center;
84
-  }
60
+  $totEntrate = (float) ($totEntrate ?? $movimenti->where('_tipo', 'entrata')->sum('importo'));
61
+  $totUscite = (float) ($totUscite ?? $movimenti->where('_tipo', 'uscita')->sum('importo'));
62
+  $totNonContabile = (float) ($totNonContabilizzato ?? $movimenti->where('_tipo', 'no_contabile')->sum('importo'));
63
+  $saldo = (float) ($saldo ?? ($totEntrate - $totUscite));
64
+  $countMovimenti = $movimenti->count();
65
+  $countEntrate = $movimenti->where('_tipo', 'entrata')->count();
66
+  $countUscite = $movimenti->where('_tipo', 'uscita')->count();
67
+
68
+  // Sezioni giorno (ancora raggruppate, ma in UNA sola tabella)
69
+  $giorni = $movimentiView
70
+    ->groupBy('_date_key')
71
+    ->map(function ($items) {
72
+      $first = $items->first();
73
+      $entrateGiorno = (float) $items->where('_tipo', 'entrata')->sum('importo');
74
+      $usciteGiorno = (float) $items->where('_tipo', 'uscita')->sum('importo');
75
+
76
+      return [
77
+        'date_label' => $first['_date_label'] ?? '-',
78
+        'weekday' => $first['_weekday'] ?? '',
79
+        'items' => $items->values(),
80
+        'entrate' => $entrateGiorno,
81
+        'uscite' => $usciteGiorno,
82
+        'netto' => $entrateGiorno - $usciteGiorno,
83
+        'count' => $items->count(),
84
+      ];
85
+    })
86
+    ->values();
85 87
 
86
-  .report-title {
87
-    font-size: 22px;
88
-    font-weight: 700;
89
-    margin: 0;
90
-    color: #0f172a;
91
-    letter-spacing: 0.03em;
92
-    text-transform: uppercase;
93
-  }
88
+  $uid = 'rmov-' . substr(md5(uniqid((string) mt_rand(), true)), 0, 8);
89
+@endphp
94 90
 
95
-  .report-title-sub {
96
-    margin-top: 2px;
97
-    font-size: 12px;
98
-    color: #64748b;
99
-    letter-spacing: 0.03em;
100
-    text-transform: uppercase;
101
-  }
91
+<style>
92
+  .rmov-row.is-hidden,
93
+  .rmov-day-band.is-hidden { display: none !important; }
102 94
 
103
-  .report-content {
104
-    padding: 0 18px 18px;
95
+  .rmov-ledger-wrap {
96
+    max-height: min(68vh, 640px);
97
+    overflow: auto;
98
+    border: 1px solid var(--bs-border-color);
99
+    border-radius: var(--bs-border-radius);
105 100
   }
106
-
107
-  .report-cards {
108
-    display: table;
101
+  .rmov-ledger {
102
+    margin-bottom: 0;
109 103
     table-layout: fixed;
110
-    width: 100%;
111
-    border-spacing: 10px 0;
112
-    border-collapse: separate;
113
-    margin-bottom: 18px;
114
-    margin-left: -10px;
115
-    margin-right: -10px;
116
-  }
117
-
118
-  .report-card {
119
-    display: table-cell;
120
-    border: 1px solid #dbe3ee;
121
-    border-radius: 10px;
122
-    padding: 10px 12px;
123
-    vertical-align: middle;
124
-  }
125
-  .report-card--entrate {
126
-    background: #ecfdf3;
127
-    border-color: #bbf7d0;
128
-  }
129
-  .report-card--uscite {
130
-    background: #fef2f2;
131
-    border-color: #fecaca;
132
-  }
133
-  .report-card--saldo {
134
-    background: #eff6ff;
135
-    border-color: #bfdbfe;
136
-  }
137
-
138
-  .report-card .label {
139
-    display: block;
140
-    font-size: 12px;
141
-    color: #475569;
142
-    margin-bottom: 4px;
104
+    --bs-table-hover-bg: rgba(67, 89, 113, 0.04);
105
+  }
106
+  .rmov-ledger thead th {
107
+    position: sticky;
108
+    top: 0;
109
+    z-index: 3;
110
+    background: var(--bs-gray-100, #f5f5f9);
111
+    box-shadow: inset 0 -1px 0 var(--bs-border-color);
112
+    font-size: .72rem;
113
+    font-weight: 600;
143 114
     text-transform: uppercase;
144
-    letter-spacing: 0.03em;
115
+    letter-spacing: .02em;
116
+    white-space: nowrap;
117
+    padding: .5rem .7rem;
145 118
   }
146
-
147
-  .report-card .value {
148
-    font-size: 19px;
149
-    font-weight: 700;
150
-    color: #0f172a;
119
+  .rmov-ledger td {
120
+    padding: .42rem .7rem;
121
+    vertical-align: middle;
151 122
   }
152
-
153
-  .report-card--entrate .label,
154
-  .report-card--entrate .value {
155
-    color: #166534;
123
+  .rmov-ledger .rmov-num {
124
+    font-variant-numeric: tabular-nums;
125
+    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
126
+    font-size: .8125rem;
127
+    white-space: nowrap;
156 128
   }
157
-
158
-  .report-card--uscite .label,
159
-  .report-card--uscite .value {
160
-    color: #991b1b;
129
+  /* Day band: pattern ledger moderno (section row in-table) */
130
+  .rmov-ledger .rmov-day-band td {
131
+    background: var(--bs-gray-100, #f5f5f9);
132
+    border-top: 1px solid var(--bs-border-color);
133
+    border-bottom: 1px solid var(--bs-border-color);
134
+    padding: .45rem .7rem;
135
+    position: sticky;
136
+    top: 2.15rem; /* sotto thead */
137
+    z-index: 2;
138
+  }
139
+  .rmov-ledger .rmov-col-avere,
140
+  .rmov-ledger .rmov-col-dare,
141
+  .rmov-ledger .rmov-col-saldo {
142
+    text-align: right;
143
+    width: 7.25rem;
161 144
   }
162
-
163
-  .report-card--saldo .label,
164
-  .report-card--saldo .value {
165
-    color: #1d4ed8;
145
+  .rmov-ledger .rmov-col-saldo {
146
+    background: rgba(105, 108, 255, 0.04);
147
+    font-weight: 600;
166 148
   }
167
-
168
-  .report-table {
169
-    width: 100%;
170
-    border-collapse: collapse;
171
-    border: 1px solid #dbe3ee;
172
-    border-radius: 8px;
173
-    overflow: hidden;
149
+  .rmov-ledger thead th.rmov-col-saldo {
150
+    background: rgba(105, 108, 255, 0.08);
174 151
   }
175
-
176
-  .report-table thead th {
177
-    font-size: 12px;
152
+  .rmov-ledger tfoot td {
153
+    position: sticky;
154
+    bottom: 0;
155
+    z-index: 3;
156
+    background: var(--bs-gray-100, #f5f5f9);
157
+    box-shadow: inset 0 1px 0 var(--bs-border-color);
178 158
     font-weight: 700;
179
-    text-align: left;
180
-    padding: 9px 10px;
181
-    color: #0f172a;
182
-    background: #e2e8f0;
183
-    border-bottom: 1px solid #cbd5e1;
184
-    text-transform: uppercase;
185
-    letter-spacing: 0.03em;
186
-  }
187
-  .report-table thead {
188
-    display: table-header-group;
189
-  }
190
-  .report-table tfoot {
191
-    display: table-row-group;
192
-  }
193
-
194
-  .report-table tbody td {
195
-    font-size: 12px;
196
-    padding: 8px 10px;
197
-    border-bottom: 1px solid #edf2f7;
198
-    color: #1e293b;
199
-  }
200
-
201
-  .report-table tbody tr:nth-child(even) {
202
-    background: #fcfdff;
203
-  }
204
-
205
-  .report-table tbody tr:hover {
206
-    background: #f1f5f9;
207
-  }
208
-  .report-table tr {
209
-    page-break-inside: avoid;
210
-  }
211
-
212
-  .text-right {
213
-    text-align: right;
214 159
   }
215
-
216
-  .font-mono {
217
-    font-family: "Consolas", "Courier New", monospace;
218
-    font-variant-numeric: tabular-nums;
160
+  .rmov-ledger .rmov-col-ora {
161
+    width: 3.75rem;
162
+    white-space: nowrap;
219 163
   }
220
-
221
-  .report-causale {
222
-    font-weight: 600;
223
-    color: #0f172a;
164
+  .rmov-ledger .rmov-col-tipo {
165
+    width: 6.25rem;
166
+    white-space: nowrap;
224 167
   }
225
-
226
-  .report-footer {
227
-    margin-top: 12px;
228
-    font-size: 11px;
229
-    color: #64748b;
230
-    border-top: 1px dashed #dbe3ee;
231
-    padding-top: 8px;
168
+  .rmov-causale {
169
+    font-size: 0.8125rem;
170
+    line-height: 1.35;
171
+    white-space: normal;
172
+    word-break: break-word;
232 173
   }
233
-
234 174
   @media print {
235
-    body {
236
-      background: #fff !important;
237
-    }
238
-
239
-    .report-print {
240
-      border: 0;
241
-      border-radius: 0;
242
-      padding: 0;
243
-      box-shadow: none;
244
-    }
245
-
246
-    .report-table tbody tr:hover {
247
-      background: transparent;
248
-    }
175
+    .rmov-toolbar { display: none !important; }
176
+    .rmov-ledger-wrap { max-height: none; overflow: visible; border: 0; }
177
+    .rmov-ledger thead th,
178
+    .rmov-ledger .rmov-day-band td,
179
+    .rmov-ledger tfoot td { position: static; box-shadow: none; }
249 180
   }
250 181
 </style>
251 182
 
252
-<section class="report-print">
253
-  <div class="report-topline">
254
-    <div class="report-pill">
255
-      <span class="report-pill-label">Attivita</span>
256
-      <span class="report-pill-value">{{ $attivitaNome }}</span>
257
-    </div>
258
-    <div class="report-pill">
259
-      <span class="report-pill-label">Periodo</span>
260
-      <span class="report-pill-value">{{ $periodoLabel }}</span>
183
+<div id="{{ $uid }}">
184
+  <div class="d-flex flex-wrap justify-content-between align-items-start gap-2 mb-4">
185
+    <div>
186
+      <h5 class="mb-1">{{ $reportTitle }}</h5>
187
+      <small class="text-muted">
188
+        <strong>{{ $attivitaNome }}</strong> · Periodo {{ $periodoLabel }}
189
+      </small>
261 190
     </div>
191
+    <small class="text-muted text-end">
192
+      Generato il<br>
193
+      <strong>{{ $generatoIl }}</strong>
194
+    </small>
262 195
   </div>
263 196
 
264
-  <div class="report-title-wrap">
265
-    <h1 class="report-title">Elenco dei movimenti in EURO</h1>
266
-    <div class="report-title-sub">Generato il {{ $generatoIl }}</div>
267
-  </div>
268
-
269
-  <div class="report-content">
270
-    <div class="report-cards">
271
-      <div class="report-card report-card--entrate">
272
-        <span class="label">Totale Entrate</span>
273
-        <span class="value">{{ number_format($totEntrate, 2, ',', '.') }}</span>
197
+  <div class="row g-3 mb-4">
198
+    <div class="col-sm-6 col-xl-3">
199
+      <div class="card card-border-shadow-info h-100">
200
+        <div class="card-body">
201
+          <span class="d-block text-muted small">Operazioni</span>
202
+          <h5 class="mb-0 mt-1">{{ number_format($countMovimenti, 0, ',', '.') }}</h5>
203
+          <small class="text-muted">
204
+            @if($totNonContabile > 0)
205
+              No contabile: EUR {{ number_format($totNonContabile, 2, ',', '.') }}
206
+            @else
207
+              Tutti i tipi nel periodo
208
+            @endif
209
+          </small>
210
+        </div>
211
+      </div>
212
+    </div>
213
+    <div class="col-sm-6 col-xl-3">
214
+      <div class="card card-border-shadow-danger h-100">
215
+        <div class="card-body">
216
+          <span class="d-block text-muted small">Uscite (dare)</span>
217
+          <h5 class="mb-0 mt-1 text-danger">EUR {{ number_format($totUscite, 2, ',', '.') }}</h5>
218
+          <small class="text-muted">{{ number_format($countUscite, 0, ',', '.') }} movimenti</small>
219
+        </div>
220
+      </div>
221
+    </div>
222
+    <div class="col-sm-6 col-xl-3">
223
+      <div class="card card-border-shadow-success h-100">
224
+        <div class="card-body">
225
+          <span class="d-block text-muted small">Entrate (avere)</span>
226
+          <h5 class="mb-0 mt-1 text-success">EUR {{ number_format($totEntrate, 2, ',', '.') }}</h5>
227
+          <small class="text-muted">{{ number_format($countEntrate, 0, ',', '.') }} movimenti</small>
228
+        </div>
274 229
       </div>
275
-      <div class="report-card report-card--uscite">
276
-        <span class="label">Totale Uscite</span>
277
-        <span class="value">{{ number_format($totUscite, 2, ',', '.') }}</span>
230
+    </div>
231
+    <div class="col-sm-6 col-xl-3">
232
+      <div class="card card-border-shadow-primary h-100">
233
+        <div class="card-body">
234
+          <span class="d-block text-muted small">Saldo</span>
235
+          <h5 class="mb-0 mt-1 {{ $saldo >= 0 ? 'text-success' : 'text-danger' }}">
236
+            EUR {{ number_format($saldo, 2, ',', '.') }}
237
+          </h5>
238
+          <small class="text-muted">Entrate − uscite</small>
239
+        </div>
278 240
       </div>
279
-      <div class="report-card report-card--saldo">
280
-        <span class="label">Saldo</span>
281
-        <span class="value">{{ number_format($saldo, 2, ',', '.') }}</span>
241
+    </div>
242
+  </div>
243
+
244
+  <div class="rmov-toolbar d-flex flex-wrap justify-content-between align-items-end gap-2 mb-2">
245
+    <div>
246
+      <h6 class="mb-0">Libro movimenti</h6>
247
+      <small class="text-muted">Layout ledger: dare/avere + saldo progressivo · bande giorno sticky</small>
248
+    </div>
249
+    <div class="d-flex flex-wrap align-items-center gap-2">
250
+      <input type="search"
251
+             class="form-control form-control-sm"
252
+             id="{{ $uid }}-q"
253
+             placeholder="Cerca causale, metodo…"
254
+             style="min-width: 200px; max-width: 260px;">
255
+      <div class="btn-group btn-group-sm" role="group" aria-label="Filtro tipo">
256
+        <button type="button" class="btn btn-primary" data-rmov-filter="all">Tutti</button>
257
+        <button type="button" class="btn btn-outline-primary" data-rmov-filter="entrata">Entrate</button>
258
+        <button type="button" class="btn btn-outline-primary" data-rmov-filter="uscita">Uscite</button>
259
+        <button type="button" class="btn btn-outline-primary" data-rmov-filter="no_contabile">No cont.</button>
282 260
       </div>
283 261
     </div>
262
+  </div>
284 263
 
285
-    <table class="report-table">
286
-      <thead>
287
-        <tr>
288
-          <th class="font-mono">Data e ora</th>
289
-          <th class="text-right font-mono">Mov. Dare</th>
290
-          <th class="text-right font-mono">Mov. Avere</th>
291
-          <th>Descrizione operazioni</th>
292
-        </tr>
293
-      </thead>
294
-      <tbody>
295
-        @forelse($movimenti as $movimento)
296
-          @php
297
-            $tipo = strtolower((string) ($movimento['tipo'] ?? ''));
298
-            $isEntrata = $tipo === 'entrata';
299
-            $importo = (float) ($movimento['importo'] ?? 0);
300
-          @endphp
264
+  @if($giorni->isEmpty())
265
+    <div class="alert alert-secondary mb-0">Nessun movimento nel periodo selezionato.</div>
266
+  @else
267
+    <div class="rmov-ledger-wrap">
268
+      <table class="table table-sm table-hover rmov-ledger align-middle">
269
+        <thead>
301 270
           <tr>
302
-            <td class="font-mono">{{ $movimento['_display_ts'] ?? '-' }}</td>
303
-            <td class="text-right font-mono">
304
-              {{ $isEntrata ? '-' : number_format(abs($importo), 2, ',', '.') }}
305
-            </td>
306
-            <td class="text-right font-mono">
307
-              {{ $isEntrata ? number_format(abs($importo), 2, ',', '.') : '-' }}
308
-            </td>
309
-            <td class="report-causale">{{ $movimento['causale'] ?? '-' }}</td>
271
+            <th class="rmov-col-ora">Ora</th>
272
+            <th class="rmov-col-tipo">Tipo</th>
273
+            <th>Causale</th>
274
+            <th style="width:110px;">Metodo</th>
275
+            <th class="rmov-col-dare">Dare</th>
276
+            <th class="rmov-col-avere">Avere</th>
277
+            <th class="rmov-col-saldo">Saldo</th>
310 278
           </tr>
311
-        @empty
279
+        </thead>
280
+        <tbody>
281
+          @foreach($giorni as $giorno)
282
+            <tr class="rmov-day-band" data-rmov-day>
283
+              <td colspan="7">
284
+                <div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
285
+                  <span class="fw-semibold text-capitalize">
286
+                    {{ $giorno['weekday'] }} {{ $giorno['date_label'] }}
287
+                    <small class="text-muted fw-normal ms-1">{{ $giorno['count'] }} mov.</small>
288
+                  </span>
289
+                  <span class="small">
290
+                    <span class="text-success me-2">+{{ number_format($giorno['entrate'], 2, ',', '.') }}</span>
291
+                    <span class="text-danger me-2">−{{ number_format($giorno['uscite'], 2, ',', '.') }}</span>
292
+                    <span class="fw-semibold {{ $giorno['netto'] >= 0 ? 'text-success' : 'text-danger' }}">
293
+                      Netto {{ ($giorno['netto'] >= 0 ? '+' : '−') }}{{ number_format(abs($giorno['netto']), 2, ',', '.') }}
294
+                    </span>
295
+                  </span>
296
+                </div>
297
+              </td>
298
+            </tr>
299
+            @foreach($giorno['items'] as $movimento)
300
+              @php
301
+                $tipo = $movimento['_tipo'] ?? '';
302
+                $importo = abs((float) ($movimento['importo'] ?? 0));
303
+                $isEntrata = $tipo === 'entrata';
304
+                $isUscita = $tipo === 'uscita';
305
+                $badge = match ($tipo) {
306
+                  'entrata' => ['bg-label-success', 'Entrata'],
307
+                  'uscita' => ['bg-label-danger', 'Uscita'],
308
+                  default => ['bg-label-secondary', 'No cont.'],
309
+                };
310
+              @endphp
311
+              <tr class="rmov-row"
312
+                  data-rmov-tipo="{{ $tipo }}"
313
+                  data-rmov-search="{{ e($movimento['_search'] ?? '') }}">
314
+                <td class="rmov-num rmov-col-ora">{{ $movimento['_display_time'] ?? '-' }}</td>
315
+                <td class="rmov-col-tipo"><span class="badge {{ $badge[0] }}">{{ $badge[1] }}</span></td>
316
+                <td>
317
+                  <div class="fw-medium rmov-causale" title="{{ $movimento['causale'] ?? '-' }}">
318
+                    {{ $movimento['causale'] ?? '-' }}
319
+                  </div>
320
+                  @if(!empty($movimento['categoria']))
321
+                    <small class="text-muted">{{ $movimento['categoria'] }}</small>
322
+                  @endif
323
+                </td>
324
+                <td><span class="badge bg-label-secondary">{{ $movimento['metodo'] ?? 'N/D' }}</span></td>
325
+                <td class="rmov-num rmov-col-dare {{ $isUscita ? 'text-danger' : 'text-muted' }}">
326
+                  {{ $isUscita ? number_format($importo, 2, ',', '.') : '—' }}
327
+                </td>
328
+                <td class="rmov-num rmov-col-avere {{ $isEntrata ? 'text-success' : 'text-muted' }}">
329
+                  {{ $isEntrata ? number_format($importo, 2, ',', '.') : '—' }}
330
+                </td>
331
+                <td class="rmov-num rmov-col-saldo {{ ($movimento['_running'] ?? 0) >= 0 ? '' : 'text-danger' }}">
332
+                  {{ number_format((float) ($movimento['_running'] ?? 0), 2, ',', '.') }}
333
+                </td>
334
+              </tr>
335
+            @endforeach
336
+          @endforeach
337
+        </tbody>
338
+        <tfoot>
312 339
           <tr>
313
-            <td colspan="4">Nessun movimento trovato nel periodo selezionato.</td>
340
+            <td colspan="4" class="fw-semibold">Totale periodo</td>
341
+            <td class="rmov-num rmov-col-dare text-danger">{{ number_format($totUscite, 2, ',', '.') }}</td>
342
+            <td class="rmov-num rmov-col-avere text-success">{{ number_format($totEntrate, 2, ',', '.') }}</td>
343
+            <td class="rmov-num rmov-col-saldo {{ $saldo >= 0 ? '' : 'text-danger' }}">
344
+              {{ number_format($saldo, 2, ',', '.') }}
345
+            </td>
314 346
           </tr>
315
-        @endforelse
316
-      </tbody>
317
-    </table>
318
-
319
-    <div class="report-footer">
320
-      Documento a uso interno. Valori espressi in euro.
347
+        </tfoot>
348
+      </table>
321 349
     </div>
322
-  </div>
323
-</section>
350
+  @endif
351
+
352
+  <small class="text-muted d-block mt-2">
353
+    Documento a uso interno · euro · saldo progressivo calcolato in ordine temporale (no contabile escluso dal saldo)
354
+  </small>
355
+</div>
356
+
357
+<script>
358
+(function () {
359
+  var root = document.getElementById(@json($uid));
360
+  if (!root) return;
361
+
362
+  var qInput = document.getElementById(@json($uid . '-q'));
363
+  var filterBtns = root.querySelectorAll('[data-rmov-filter]');
364
+  var activeTipo = 'all';
365
+  var query = '';
366
+
367
+  function applyFilters() {
368
+    root.querySelectorAll('.rmov-day-band').forEach(function (band) {
369
+      var visibleRows = 0;
370
+      var next = band.nextElementSibling;
371
+      while (next && !next.classList.contains('rmov-day-band')) {
372
+        if (next.classList.contains('rmov-row')) {
373
+          var tipo = next.getAttribute('data-rmov-tipo') || '';
374
+          var hay = next.getAttribute('data-rmov-search') || '';
375
+          var show = (activeTipo === 'all' || tipo === activeTipo) && (!query || hay.indexOf(query) !== -1);
376
+          next.classList.toggle('is-hidden', !show);
377
+          if (show) visibleRows++;
378
+        }
379
+        next = next.nextElementSibling;
380
+      }
381
+      band.classList.toggle('is-hidden', visibleRows === 0);
382
+    });
383
+  }
384
+
385
+  filterBtns.forEach(function (btn) {
386
+    btn.addEventListener('click', function () {
387
+      activeTipo = btn.getAttribute('data-rmov-filter') || 'all';
388
+      filterBtns.forEach(function (b) {
389
+        var on = b === btn;
390
+        b.classList.toggle('btn-primary', on);
391
+        b.classList.toggle('btn-outline-primary', !on);
392
+      });
393
+      applyFilters();
394
+    });
395
+  });
396
+
397
+  if (qInput) {
398
+    qInput.addEventListener('input', function () {
399
+      query = (qInput.value || '').trim().toLowerCase();
400
+      applyFilters();
401
+    });
402
+  }
403
+})();
404
+</script>

+ 194
- 0
resources/views/report/_partials/staff.blade.php Ver fichero

@@ -0,0 +1,194 @@
1
+@php
2
+  $staff = $staffReport ?? [];
3
+  $sk = $staff['kpi'] ?? [
4
+    'valore_staff' => 0,
5
+    'ordini' => 0,
6
+    'ticket_medio' => 0,
7
+    'pezzi' => 0,
8
+    'percent_su_totale' => null,
9
+    'delta_valore' => null,
10
+    'delta_ordini' => null,
11
+    'periodo_precedente_label' => null,
12
+  ];
13
+  $staffPrevLabel = $sk['periodo_precedente_label'] ?? ($periodoPrecedenteLabel ?? null);
14
+
15
+  $formatStaffDelta = function (?float $delta) use ($staffPrevLabel): array {
16
+    $vsRef = $staffPrevLabel ? 'vs ' . $staffPrevLabel : 'vs periodo precedente';
17
+    if ($delta === null) {
18
+      return ['class' => 'text-muted', 'icon' => 'bx-minus', 'text' => 'n/d ' . $vsRef];
19
+    }
20
+    $sign = $delta > 0 ? '+' : '';
21
+    return [
22
+      'class' => $delta > 0 ? 'text-success' : ($delta < 0 ? 'text-danger' : 'text-muted'),
23
+      'icon' => $delta > 0 ? 'bx-up-arrow-alt' : ($delta < 0 ? 'bx-down-arrow-alt' : 'bx-minus'),
24
+      'text' => $sign . number_format($delta, 1, ',', '.') . '% ' . $vsRef,
25
+    ];
26
+  };
27
+  $deltaStaffValore = $formatStaffDelta(isset($sk['delta_valore']) ? (float) $sk['delta_valore'] : null);
28
+  $deltaStaffOrdini = $formatStaffDelta(isset($sk['delta_ordini']) ? (float) $sk['delta_ordini'] : null);
29
+@endphp
30
+
31
+<div class="alert alert-secondary py-2 mb-4">
32
+  <small class="mb-0 d-block">
33
+    Sintesi dedicata ai consumi <strong>Staff</strong> (controvalore, non liquidità).
34
+    @if(!empty($periodoCorrenteLabel ?? null))
35
+      Periodo: <strong>{{ $periodoCorrenteLabel }}</strong>.
36
+    @endif
37
+    @if($staffPrevLabel)
38
+      Variazioni % rispetto a <strong>{{ $staffPrevLabel }}</strong>.
39
+    @endif
40
+  </small>
41
+  <small class="mb-0 d-block mt-1">
42
+    <i class="bx bx-info-circle text-primary"></i>
43
+    Il filtro <strong>Staff</strong> nel menu in alto apre questa tab.
44
+    I dati non entrano in cassa né nel saldo contabile.
45
+    Resta attivo il filtro cucina.
46
+  </small>
47
+</div>
48
+
49
+<div class="row g-4 mb-4">
50
+  <div class="col-sm-6 col-xl">
51
+    <div class="card h-100">
52
+      <div class="card-body">
53
+        <span class="fw-medium text-muted">
54
+          Valore staff
55
+          <i class="bx bx-info-circle text-primary ms-1"
56
+             role="button" tabindex="0" data-bs-toggle="tooltip" data-bs-placement="top"
57
+             title="Controvalore totale dei pagamenti tipo Staff nel periodo."></i>
58
+        </span>
59
+        <h3 class="mb-1 mt-2">EUR {{ number_format((float) $sk['valore_staff'], 2, ',', '.') }}</h3>
60
+        <small class="{{ $deltaStaffValore['class'] }}"><i class="bx {{ $deltaStaffValore['icon'] }}"></i> {{ $deltaStaffValore['text'] }}</small>
61
+        <div class="small text-muted mt-1">Controvalore · non liquidità</div>
62
+      </div>
63
+    </div>
64
+  </div>
65
+  <div class="col-sm-6 col-xl">
66
+    <div class="card h-100">
67
+      <div class="card-body">
68
+        <span class="fw-medium text-muted">
69
+          Ordini staff
70
+          <i class="bx bx-info-circle text-primary ms-1"
71
+             role="button" tabindex="0" data-bs-toggle="tooltip" data-bs-placement="top"
72
+             title="Ordini con almeno un pagamento Staff riuscito."></i>
73
+        </span>
74
+        <h3 class="mb-1 mt-2">{{ number_format((int) $sk['ordini'], 0, ',', '.') }}</h3>
75
+        <small class="{{ $deltaStaffOrdini['class'] }}"><i class="bx {{ $deltaStaffOrdini['icon'] }}"></i> {{ $deltaStaffOrdini['text'] }}</small>
76
+        <div class="small text-muted mt-1">Fonte: ordini con pagamento staff</div>
77
+      </div>
78
+    </div>
79
+  </div>
80
+  <div class="col-sm-6 col-xl">
81
+    <div class="card h-100">
82
+      <div class="card-body">
83
+        <span class="fw-medium text-muted">
84
+          Ticket medio staff
85
+          <i class="bx bx-info-circle text-primary ms-1"
86
+             role="button" tabindex="0" data-bs-toggle="tooltip" data-bs-placement="top"
87
+             title="Valore staff / ordini staff."></i>
88
+        </span>
89
+        <h3 class="mb-1 mt-2">EUR {{ number_format((float) $sk['ticket_medio'], 2, ',', '.') }}</h3>
90
+        <small class="text-muted">{{ number_format((int) $sk['pezzi'], 0, ',', '.') }} pezzi</small>
91
+        <div class="small text-muted mt-1">Fonte: valore / ordini</div>
92
+      </div>
93
+    </div>
94
+  </div>
95
+  <div class="col-sm-6 col-xl">
96
+    <div class="card h-100">
97
+      <div class="card-body">
98
+        <span class="fw-medium text-muted">
99
+          % su totale teorico
100
+          <i class="bx bx-info-circle text-primary ms-1"
101
+             role="button" tabindex="0" data-bs-toggle="tooltip" data-bs-placement="top"
102
+             title="Quota staff sul totale teorico = cassa + staff. Indica quanto del volume ‘teorico’ è consumo interno."></i>
103
+        </span>
104
+        <h3 class="mb-1 mt-2">
105
+          @if($sk['percent_su_totale'] === null)
106
+            n/d
107
+          @else
108
+            {{ number_format((float) $sk['percent_su_totale'], 1, ',', '.') }}%
109
+          @endif
110
+        </h3>
111
+        <small class="text-muted">staff / (cassa + staff)</small>
112
+        <div class="small text-muted mt-1">Confronto con liquidità reale</div>
113
+      </div>
114
+    </div>
115
+  </div>
116
+</div>
117
+
118
+<div class="row g-4 mb-4">
119
+  <div class="col-12">
120
+    <div class="card">
121
+      <div class="card-header">
122
+        <h5 class="card-title mb-0">Andamento controvalore staff</h5>
123
+        <small class="text-muted d-block">Fonte: pagamenti tipo Staff · per giorno</small>
124
+      </div>
125
+      <div class="card-body">
126
+        <div id="report-staff-trend-chart" style="min-height: 280px;"></div>
127
+      </div>
128
+    </div>
129
+  </div>
130
+</div>
131
+
132
+<div class="row g-4 mb-4">
133
+  <div class="col-12">
134
+    <div class="card">
135
+      <div class="card-header">
136
+        <h5 class="card-title mb-0 d-inline-flex align-items-center gap-1">
137
+          Picchi consumi staff (ora × giorno)
138
+          <i class="bx bx-info-circle text-primary"
139
+             role="button" tabindex="0" data-bs-toggle="tooltip" data-bs-placement="top"
140
+             title="Quando si concentra il consumo staff. X = ora, Y = giorno."></i>
141
+        </h5>
142
+        <small class="text-muted d-block">Fonte: ordini con pagamento Staff · solo ore con attività</small>
143
+      </div>
144
+      <div class="card-body">
145
+        <div id="report-staff-heatmap-stats" class="small text-muted mb-2"></div>
146
+        <div id="report-staff-heatmap-chart" class="w-100"></div>
147
+      </div>
148
+    </div>
149
+  </div>
150
+</div>
151
+
152
+<div class="row g-4 mb-4">
153
+  <div class="col-12 col-lg-6">
154
+    <div class="card h-100">
155
+      <div class="card-header">
156
+        <h5 class="card-title mb-0">Top piatti staff per controvalore</h5>
157
+        <small class="text-muted d-block">Fonte: righe di ordini con pagamento Staff</small>
158
+      </div>
159
+      <div class="card-body">
160
+        <div id="report-staff-piatti-chart" style="min-height: 320px;"></div>
161
+      </div>
162
+    </div>
163
+  </div>
164
+  <div class="col-12 col-lg-6">
165
+    <div class="card h-100">
166
+      <div class="card-header">
167
+        <h5 class="card-title mb-0">Top piatti staff per quantità</h5>
168
+        <small class="text-muted d-block">Fonte: pezzi su ordini Staff</small>
169
+      </div>
170
+      <div class="card-body">
171
+        <div id="report-staff-piatti-qty-chart" style="min-height: 320px;"></div>
172
+      </div>
173
+    </div>
174
+  </div>
175
+</div>
176
+
177
+<div class="row g-4">
178
+  <div class="col-12">
179
+    <div class="card">
180
+      <div class="card-header">
181
+        <h5 class="card-title mb-0 d-inline-flex align-items-center gap-1">
182
+          Carico cucine da consumi staff
183
+          <i class="bx bx-info-circle text-primary"
184
+             role="button" tabindex="0" data-bs-toggle="tooltip" data-bs-placement="top"
185
+             title="Pezzi preparati per cucina lungo le ore, solo da ordini Staff."></i>
186
+        </h5>
187
+        <small class="text-muted d-block">Fonte: pezzi su ordini Staff · max 6 cucine</small>
188
+      </div>
189
+      <div class="card-body">
190
+        <div id="report-staff-cucina-ora-chart" style="min-height: 340px;"></div>
191
+      </div>
192
+    </div>
193
+  </div>
194
+</div>

+ 1295
- 233
resources/views/report/index.blade.php
La diferencia del archivo ha sido suprimido porque es demasiado grande
Ver fichero


+ 17
- 8
resources/views/report/pdf.blade.php Ver fichero

@@ -1,33 +1,42 @@
1 1
 <!doctype html>
2 2
 <html lang="it">
3 3
 <head>
4
-  <meta charset="utf-8">
5
-  <title>Report PDF</title>
4
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
5
+  <title>{{ $reportTitle ?? 'Report PDF' }}</title>
6 6
 </head>
7 7
 <body>
8
-  @if(($reportType ?? 'movimenti') === 'incassi')
9
-    @include('report._partials.incassi', [
8
+  @php $type = $reportType ?? 'movimenti'; @endphp
9
+
10
+  @if($type === 'incassi')
11
+    @include('report.pdf.incassi', [
12
+      'reportTitle' => 'Report Incassi',
10 13
       'attivitaNome' => $attivitaNome ?? 'Attivita',
11 14
       'periodoLabel' => $periodoLabel ?? 'Periodo non specificato',
15
+      'generatoIl' => $generatoIl ?? now()->format('d/m/Y H:i'),
12 16
       'movimenti' => $movimenti ?? [],
13 17
       'totEntrate' => $totEntrate ?? 0,
14 18
       'totUscite' => $totUscite ?? 0,
19
+      'totNonContabilizzato' => $totNonContabilizzato ?? 0,
15 20
       'saldo' => $saldo ?? 0,
16 21
     ])
17
-  @elseif(($reportType ?? 'movimenti') === 'cucine')
18
-    @include('report._partials.cucine', [
22
+  @elseif($type === 'cucine')
23
+    @include('report.pdf.cucine', [
24
+      'reportTitle' => 'Report Cucine',
19 25
       'attivitaNome' => $attivitaNome ?? 'Attivita',
20 26
       'periodoLabel' => $periodoLabel ?? 'Periodo non specificato',
21
-      'cucine' => $cucine ?? collect(),
27
+      'generatoIl' => $generatoIl ?? now()->format('d/m/Y H:i'),
22 28
       'cucineReportRows' => $cucineReportRows ?? [],
23 29
     ])
24 30
   @else
25
-    @include('report._partials.movimenti', [
31
+    @include('report.pdf.movimenti', [
32
+      'reportTitle' => 'Report Movimenti',
26 33
       'attivitaNome' => $attivitaNome ?? 'Attivita',
27 34
       'periodoLabel' => $periodoLabel ?? 'Periodo non specificato',
35
+      'generatoIl' => $generatoIl ?? now()->format('d/m/Y H:i'),
28 36
       'movimenti' => $movimenti ?? [],
29 37
       'totEntrate' => $totEntrate ?? 0,
30 38
       'totUscite' => $totUscite ?? 0,
39
+      'totNonContabilizzato' => $totNonContabilizzato ?? 0,
31 40
       'saldo' => $saldo ?? 0,
32 41
     ])
33 42
   @endif

+ 179
- 0
resources/views/report/pdf/_styles.blade.php Ver fichero

@@ -0,0 +1,179 @@
1
+{{-- CSS condiviso DomPDF-safe: niente flex/grid moderni, sticky, JS --}}
2
+<style>
3
+  @page {
4
+    margin: 72px 32px 48px;
5
+  }
6
+
7
+  * {
8
+    box-sizing: border-box;
9
+  }
10
+
11
+  body {
12
+    font-family: DejaVu Sans, sans-serif;
13
+    font-size: 10px;
14
+    color: #1f2937;
15
+    margin: 0;
16
+    padding: 0;
17
+  }
18
+
19
+  h1 {
20
+    font-size: 16px;
21
+    margin: 0 0 4px;
22
+    color: #111827;
23
+  }
24
+
25
+  .meta {
26
+    color: #6b7280;
27
+    font-size: 9px;
28
+    margin-bottom: 14px;
29
+  }
30
+
31
+  .meta strong {
32
+    color: #374151;
33
+  }
34
+
35
+  .kpi {
36
+    width: 100%;
37
+    border-collapse: separate;
38
+    border-spacing: 6px 0;
39
+    margin: 0 0 14px -6px;
40
+  }
41
+
42
+  .kpi td {
43
+    width: 25%;
44
+    border: 1px solid #e5e7eb;
45
+    background: #f9fafb;
46
+    padding: 8px 10px;
47
+    vertical-align: top;
48
+  }
49
+
50
+  .kpi .label {
51
+    display: block;
52
+    font-size: 8px;
53
+    text-transform: uppercase;
54
+    letter-spacing: 0.04em;
55
+    color: #6b7280;
56
+    margin-bottom: 3px;
57
+  }
58
+
59
+  .kpi .value {
60
+    display: block;
61
+    font-size: 12px;
62
+    font-weight: bold;
63
+    color: #111827;
64
+  }
65
+
66
+  .kpi .hint {
67
+    display: block;
68
+    font-size: 8px;
69
+    color: #9ca3af;
70
+    margin-top: 2px;
71
+  }
72
+
73
+  .kpi .ok { color: #166534; }
74
+  .kpi .ko { color: #991b1b; }
75
+
76
+  table.data {
77
+    width: 100%;
78
+    border-collapse: collapse;
79
+    table-layout: fixed;
80
+  }
81
+
82
+  table.data th {
83
+    background: #e5e7eb;
84
+    color: #111827;
85
+    font-size: 8px;
86
+    text-transform: uppercase;
87
+    letter-spacing: 0.03em;
88
+    text-align: left;
89
+    padding: 5px 4px;
90
+    border: 1px solid #d1d5db;
91
+  }
92
+
93
+  table.data td {
94
+    padding: 4px 4px;
95
+    border: 1px solid #e5e7eb;
96
+    vertical-align: top;
97
+    font-size: 9px;
98
+    color: #1f2937;
99
+  }
100
+
101
+  table.data td.col-ora,
102
+  table.data td.col-tipo {
103
+    padding-left: 3px;
104
+    padding-right: 3px;
105
+  }
106
+
107
+  table.data td.col-num {
108
+    padding-left: 2px;
109
+    padding-right: 3px;
110
+    font-size: 8px;
111
+  }
112
+
113
+  table.data tr.day td {
114
+    background: #f3f4f6;
115
+    font-weight: bold;
116
+    font-size: 9px;
117
+  }
118
+
119
+  table.data tfoot td {
120
+    background: #f3f4f6;
121
+    font-weight: bold;
122
+    border-top: 2px solid #9ca3af;
123
+  }
124
+
125
+  .num {
126
+    text-align: right;
127
+    font-family: DejaVu Sans Mono, DejaVu Sans, monospace;
128
+    white-space: nowrap;
129
+    font-size: 8px;
130
+  }
131
+
132
+  .muted { color: #9ca3af; }
133
+  .ok { color: #166534; }
134
+  .ko { color: #991b1b; }
135
+  .center { text-align: center; }
136
+
137
+  .foot-note {
138
+    margin-top: 10px;
139
+    font-size: 8px;
140
+    color: #6b7280;
141
+  }
142
+
143
+  table.data .col-ora {
144
+    white-space: nowrap;
145
+    text-align: center;
146
+    font-size: 8px;
147
+  }
148
+
149
+  table.data .col-tipo {
150
+    white-space: nowrap;
151
+    font-size: 8px;
152
+  }
153
+
154
+  table.data .col-causale {
155
+    font-size: 8px;
156
+    line-height: 1.3;
157
+    white-space: normal;
158
+    word-wrap: break-word;
159
+  }
160
+
161
+  .col-causale .muted {
162
+    display: block;
163
+    margin-top: 1px;
164
+    font-size: 7px;
165
+  }
166
+
167
+  h2.section {
168
+    font-size: 10px;
169
+    margin: 14px 0 6px;
170
+    color: #374151;
171
+    font-weight: bold;
172
+  }
173
+
174
+  table.data .col-label {
175
+    white-space: normal;
176
+    word-wrap: break-word;
177
+    font-size: 8px;
178
+  }
179
+</style>

+ 127
- 0
resources/views/report/pdf/cucine.blade.php Ver fichero

@@ -0,0 +1,127 @@
1
+@php
2
+  $reportTitle = $reportTitle ?? 'Report Cucine';
3
+  $attivitaNome = $attivitaNome ?? 'Attivita';
4
+  $periodoLabel = $periodoLabel ?? 'Periodo non specificato';
5
+  $generatoIl = $generatoIl ?? now()->format('d/m/Y H:i');
6
+
7
+  $rows = collect($cucineReportRows ?? [])->map(function ($row) {
8
+    $ordini = (int) ($row['ordini'] ?? 0);
9
+    $incasso = (float) ($row['incasso'] ?? 0);
10
+    return [
11
+      'cucina' => (string) ($row['cucina'] ?? 'Cucina'),
12
+      'ordini' => $ordini,
13
+      'righe' => (int) ($row['righe'] ?? 0),
14
+      'quantita' => (int) ($row['quantita'] ?? 0),
15
+      'incasso' => $incasso,
16
+      'ticket' => $ordini > 0 ? $incasso / $ordini : 0.0,
17
+    ];
18
+  })->sortByDesc('incasso')->values();
19
+
20
+  $totOrdini = (int) $rows->sum('ordini');
21
+  $totRighe = (int) $rows->sum('righe');
22
+  $totQuantita = (int) $rows->sum('quantita');
23
+  $totIncasso = (float) $rows->sum('incasso');
24
+  $nCucineConfigurate = (int) $rows->count();
25
+  $nCucineConVendite = (int) $rows->filter(fn ($r) => $r['incasso'] > 0 || $r['quantita'] > 0)->count();
26
+@endphp
27
+
28
+@include('report.pdf._styles')
29
+
30
+<h1>{{ $reportTitle }}</h1>
31
+<div class="meta">
32
+  <strong>{{ $attivitaNome }}</strong>
33
+  · Periodo {{ $periodoLabel }}
34
+  · Generato il {{ $generatoIl }}
35
+</div>
36
+
37
+<table class="kpi">
38
+  <tr>
39
+    <td>
40
+      <span class="label">Cucine con vendite</span>
41
+      <span class="value">{{ number_format($nCucineConVendite, 0, ',', '.') }}</span>
42
+      <span class="hint">su {{ number_format($nCucineConfigurate, 0, ',', '.') }} configurate</span>
43
+    </td>
44
+    <td>
45
+      <span class="label">Ordini</span>
46
+      <span class="value">{{ number_format($totOrdini, 0, ',', '.') }}</span>
47
+      <span class="hint">{{ number_format($totRighe, 0, ',', '.') }} righe</span>
48
+    </td>
49
+    <td>
50
+      <span class="label">Pezzi</span>
51
+      <span class="value">{{ number_format($totQuantita, 0, ',', '.') }}</span>
52
+    </td>
53
+    <td>
54
+      <span class="label">Incasso</span>
55
+      <span class="value ok">{{ number_format($totIncasso, 2, ',', '.') }}</span>
56
+    </td>
57
+  </tr>
58
+</table>
59
+
60
+<table class="data">
61
+  <colgroup>
62
+    <col style="width: 24pt;">
63
+    <col style="width: 200pt;">
64
+    <col style="width: 44pt;">
65
+    <col style="width: 44pt;">
66
+    <col style="width: 44pt;">
67
+    <col style="width: 54pt;">
68
+    <col style="width: 60pt;">
69
+    <col style="width: 61pt;">
70
+  </colgroup>
71
+  <thead>
72
+    <tr>
73
+      <th class="center" style="width: 5%;">#</th>
74
+      <th class="col-label" style="width: 38%;">Cucina</th>
75
+      <th class="num col-num" style="width: 8%;">Ordini</th>
76
+      <th class="num col-num" style="width: 8%;">Righe</th>
77
+      <th class="num col-num" style="width: 8%;">Pezzi</th>
78
+      <th class="num col-num" style="width: 10%;">Ticket</th>
79
+      <th class="num col-num" style="width: 11%;">Incasso</th>
80
+      <th class="num col-num" style="width: 12%;">Quota</th>
81
+    </tr>
82
+  </thead>
83
+  <tbody>
84
+    @forelse($rows as $i => $row)
85
+      @php $share = $totIncasso > 0 ? ($row['incasso'] / $totIncasso) * 100 : 0; @endphp
86
+      <tr>
87
+        <td class="center">{{ $i + 1 }}</td>
88
+        <td class="col-label">{{ $row['cucina'] }}</td>
89
+        <td class="num col-num">{{ number_format($row['ordini'], 0, ',', '.') }}</td>
90
+        <td class="num col-num">{{ number_format($row['righe'], 0, ',', '.') }}</td>
91
+        <td class="num col-num">{{ number_format($row['quantita'], 0, ',', '.') }}</td>
92
+        <td class="num col-num">{{ number_format($row['ticket'], 2, ',', '.') }}</td>
93
+        <td class="num col-num">{{ number_format($row['incasso'], 2, ',', '.') }}</td>
94
+        <td class="num col-num">{{ number_format($share, 1, ',', '.') }}%</td>
95
+      </tr>
96
+    @empty
97
+      <tr>
98
+        <td></td>
99
+        <td class="center muted">Nessun dato cucina nel periodo selezionato.</td>
100
+        <td></td>
101
+        <td></td>
102
+        <td></td>
103
+        <td></td>
104
+        <td></td>
105
+        <td></td>
106
+      </tr>
107
+    @endforelse
108
+  </tbody>
109
+  @if($rows->isNotEmpty())
110
+    <tfoot>
111
+      <tr>
112
+        <td></td>
113
+        <td>Totale</td>
114
+        <td class="num col-num">{{ number_format($totOrdini, 0, ',', '.') }}</td>
115
+        <td class="num col-num">{{ number_format($totRighe, 0, ',', '.') }}</td>
116
+        <td class="num col-num">{{ number_format($totQuantita, 0, ',', '.') }}</td>
117
+        <td class="num col-num">{{ number_format($totOrdini > 0 ? $totIncasso / $totOrdini : 0, 2, ',', '.') }}</td>
118
+        <td class="num col-num">{{ number_format($totIncasso, 2, ',', '.') }}</td>
119
+        <td class="num col-num">100%</td>
120
+      </tr>
121
+    </tfoot>
122
+  @endif
123
+</table>
124
+
125
+<div class="foot-note">
126
+  Valori in euro · ordini pagati (staff escluso dall'incasso)
127
+</div>

+ 189
- 0
resources/views/report/pdf/incassi.blade.php Ver fichero

@@ -0,0 +1,189 @@
1
+@php
2
+  $reportTitle = $reportTitle ?? 'Report Incassi';
3
+  $attivitaNome = $attivitaNome ?? 'Attivita';
4
+  $periodoLabel = $periodoLabel ?? 'Periodo non specificato';
5
+  $generatoIl = $generatoIl ?? now()->format('d/m/Y H:i');
6
+
7
+  $movimenti = collect($movimenti ?? [])
8
+    ->map(function ($movimento) {
9
+      $rawTs = $movimento['timestamp'] ?? $movimento['data'] ?? null;
10
+      $parsedTs = $rawTs ? strtotime((string) $rawTs) : false;
11
+      $tipo = strtolower((string) ($movimento['tipo'] ?? 'entrata'));
12
+      $importo = (float) ($movimento['importo'] ?? 0);
13
+      $inSaldo = in_array($tipo, ['entrata', 'uscita'], true);
14
+
15
+      return [
16
+        '_sort_ts' => $parsedTs !== false ? $parsedTs : 0,
17
+        '_date_key' => $parsedTs !== false ? date('Y-m-d', $parsedTs) : '0000-00-00',
18
+        '_date_label' => $parsedTs !== false ? date('d/m/Y', $parsedTs) : '-',
19
+        '_metodo' => (string) ($movimento['metodo'] ?? 'N/D'),
20
+        '_tipo' => $tipo,
21
+        '_signed' => $inSaldo ? ($tipo === 'uscita' ? -abs($importo) : abs($importo)) : 0.0,
22
+        '_in_saldo' => $inSaldo,
23
+        'importo' => abs($importo),
24
+      ];
25
+    })
26
+    ->sortByDesc('_sort_ts')
27
+    ->values();
28
+
29
+  $saldoRows = $movimenti->where('_in_saldo', true);
30
+  $totEntrate = (float) ($totEntrate ?? $movimenti->where('_tipo', 'entrata')->sum('importo'));
31
+  $totUscite = (float) ($totUscite ?? $movimenti->where('_tipo', 'uscita')->sum('importo'));
32
+  $saldo = (float) ($saldo ?? ($totEntrate - $totUscite));
33
+
34
+  $metodi = $saldoRows->pluck('_metodo')->filter()->unique()->sort()->values();
35
+  $totPerMetodo = $metodi->mapWithKeys(fn ($m) => [
36
+    $m => (float) $saldoRows->where('_metodo', $m)->sum('_signed'),
37
+  ]);
38
+
39
+  $giorni = $saldoRows
40
+    ->groupBy('_date_key')
41
+    ->sortKeysDesc()
42
+    ->map(function ($items) use ($metodi) {
43
+      $first = $items->first();
44
+      $celle = [];
45
+      foreach ($metodi as $metodo) {
46
+        $celle[$metodo] = (float) $items->where('_metodo', $metodo)->sum('_signed');
47
+      }
48
+      return [
49
+        'date_label' => $first['_date_label'] ?? '-',
50
+        'celle' => $celle,
51
+        'totale' => array_sum($celle),
52
+      ];
53
+    })
54
+    ->values();
55
+
56
+  $giorniAttivi = $giorni->count();
57
+  $media = $giorniAttivi > 0 ? $saldo / $giorniAttivi : 0.0;
58
+  $colCount = 2 + $metodi->count();
59
+
60
+  $giornoPt = 90;
61
+  $totalePt = 75;
62
+  $metodoPt = $metodi->isNotEmpty()
63
+    ? (778 - $giornoPt - $totalePt) / $metodi->count()
64
+    : 0;
65
+@endphp
66
+
67
+@include('report.pdf._styles')
68
+
69
+<h1>{{ $reportTitle }}</h1>
70
+<div class="meta">
71
+  <strong>{{ $attivitaNome }}</strong>
72
+  · Periodo {{ $periodoLabel }}
73
+  · Generato il {{ $generatoIl }}
74
+</div>
75
+
76
+<table class="kpi">
77
+  <tr>
78
+    <td>
79
+      <span class="label">Uscite / storni</span>
80
+      <span class="value ko">{{ number_format($totUscite, 2, ',', '.') }}</span>
81
+    </td>
82
+    <td>
83
+      <span class="label">Entrate</span>
84
+      <span class="value ok">{{ number_format($totEntrate, 2, ',', '.') }}</span>
85
+    </td>
86
+    <td>
87
+      <span class="label">Saldo netto</span>
88
+      <span class="value {{ $saldo >= 0 ? 'ok' : 'ko' }}">{{ number_format($saldo, 2, ',', '.') }}</span>
89
+    </td>
90
+    <td>
91
+      <span class="label">Media / giorno</span>
92
+      <span class="value">{{ number_format($media, 2, ',', '.') }}</span>
93
+      <span class="hint">{{ $giorniAttivi }} giorni attivi</span>
94
+    </td>
95
+  </tr>
96
+</table>
97
+
98
+@if($metodi->isNotEmpty())
99
+  <h2 class="section">Ripartizione per metodo</h2>
100
+  <table class="data" style="margin-bottom: 12px;">
101
+    <colgroup>
102
+      <col style="width: 480pt;">
103
+      <col style="width: 170pt;">
104
+      <col style="width: 128pt;">
105
+    </colgroup>
106
+    <thead>
107
+      <tr>
108
+        <th class="col-label" style="width: 62%;">Metodo</th>
109
+        <th class="num col-num" style="width: 22%;">Importo</th>
110
+        <th class="num col-num" style="width: 16%;">Quota</th>
111
+      </tr>
112
+    </thead>
113
+    <tbody>
114
+      @foreach($metodi as $metodo)
115
+        @php
116
+          $val = (float) ($totPerMetodo[$metodo] ?? 0);
117
+          $vol = max(abs($totEntrate) + abs($totUscite), 0.0001);
118
+          $pct = round((abs($val) / $vol) * 100, 1);
119
+        @endphp
120
+        <tr>
121
+          <td class="col-label">{{ $metodo }}</td>
122
+          <td class="num col-num {{ $val < 0 ? 'ko' : '' }}">{{ number_format($val, 2, ',', '.') }}</td>
123
+          <td class="num col-num">{{ number_format($pct, 1, ',', '.') }}%</td>
124
+        </tr>
125
+      @endforeach
126
+    </tbody>
127
+  </table>
128
+@endif
129
+
130
+<h2 class="section">Matrice giorno × metodo</h2>
131
+<table class="data">
132
+  @if($metodi->isNotEmpty())
133
+    <colgroup>
134
+      <col style="width: {{ $giornoPt }}pt;">
135
+      @foreach($metodi as $metodo)
136
+        <col style="width: {{ $metodoPt }}pt;">
137
+      @endforeach
138
+      <col style="width: {{ $totalePt }}pt;">
139
+    </colgroup>
140
+  @endif
141
+  <thead>
142
+    <tr>
143
+      <th style="width: 11%;">Giorno</th>
144
+      @foreach($metodi as $metodo)
145
+        <th class="num col-num">{{ \Illuminate\Support\Str::limit($metodo, 14, '…') }}</th>
146
+      @endforeach
147
+      <th class="num col-num" style="width: 10%;">Totale</th>
148
+    </tr>
149
+  </thead>
150
+  <tbody>
151
+    @forelse($giorni as $giorno)
152
+      <tr>
153
+        <td>{{ $giorno['date_label'] }}</td>
154
+        @foreach($metodi as $metodo)
155
+          @php $val = (float) ($giorno['celle'][$metodo] ?? 0); @endphp
156
+          <td class="num col-num {{ $val < 0 ? 'ko' : ($val == 0.0 ? 'muted' : '') }}">
157
+            {{ $val == 0.0 ? '—' : number_format($val, 2, ',', '.') }}
158
+          </td>
159
+        @endforeach
160
+        <td class="num col-num {{ $giorno['totale'] < 0 ? 'ko' : '' }}">
161
+          {{ number_format((float) $giorno['totale'], 2, ',', '.') }}
162
+        </td>
163
+      </tr>
164
+    @empty
165
+      <tr>
166
+        <td class="center muted">Nessun incasso nel periodo selezionato.</td>
167
+        @for($i = 1; $i < max(1, $colCount); $i++)
168
+          <td></td>
169
+        @endfor
170
+      </tr>
171
+    @endforelse
172
+  </tbody>
173
+  @if($giorni->isNotEmpty())
174
+    <tfoot>
175
+      <tr>
176
+        <td>Totale periodo</td>
177
+        @foreach($metodi as $metodo)
178
+          @php $tot = (float) ($totPerMetodo[$metodo] ?? 0); @endphp
179
+          <td class="num col-num {{ $tot < 0 ? 'ko' : '' }}">{{ number_format($tot, 2, ',', '.') }}</td>
180
+        @endforeach
181
+        <td class="num col-num {{ $saldo < 0 ? 'ko' : '' }}">{{ number_format($saldo, 2, ',', '.') }}</td>
182
+      </tr>
183
+    </tfoot>
184
+  @endif
185
+</table>
186
+
187
+<div class="foot-note">
188
+  Valori in euro · storni in negativo · no contabile escluso
189
+</div>

+ 174
- 0
resources/views/report/pdf/movimenti.blade.php Ver fichero

@@ -0,0 +1,174 @@
1
+@php
2
+  $reportTitle = $reportTitle ?? 'Report Movimenti';
3
+  $attivitaNome = $attivitaNome ?? 'Attivita';
4
+  $periodoLabel = $periodoLabel ?? 'Periodo non specificato';
5
+  $generatoIl = $generatoIl ?? now()->format('d/m/Y H:i');
6
+
7
+  $movimenti = collect($movimenti ?? [])
8
+    ->map(function ($movimento) {
9
+      $rawTs = $movimento['timestamp'] ?? $movimento['data'] ?? null;
10
+      $parsedTs = $rawTs ? strtotime((string) $rawTs) : false;
11
+      $tipo = strtolower(trim((string) ($movimento['tipo'] ?? '')));
12
+      $importo = abs((float) ($movimento['importo'] ?? 0));
13
+
14
+      return array_merge($movimento, [
15
+        '_sort_ts' => $parsedTs !== false ? $parsedTs : 0,
16
+        '_time' => $parsedTs !== false ? date('H:i', $parsedTs) : '-',
17
+        '_date_key' => $parsedTs !== false ? date('Y-m-d', $parsedTs) : '0000-00-00',
18
+        '_date_label' => $parsedTs !== false ? date('d/m/Y', $parsedTs) : '-',
19
+        '_tipo' => $tipo,
20
+        '_signed' => $tipo === 'entrata' ? $importo : ($tipo === 'uscita' ? -$importo : 0.0),
21
+        'importo' => $importo,
22
+      ]);
23
+    })
24
+    ->sortBy('_sort_ts')
25
+    ->values();
26
+
27
+  $running = 0.0;
28
+  $movimenti = $movimenti->map(function ($m) use (&$running) {
29
+    $running += (float) $m['_signed'];
30
+    $m['_running'] = $running;
31
+    return $m;
32
+  })->sortByDesc('_sort_ts')->values();
33
+
34
+  $totEntrate = (float) ($totEntrate ?? $movimenti->where('_tipo', 'entrata')->sum('importo'));
35
+  $totUscite = (float) ($totUscite ?? $movimenti->where('_tipo', 'uscita')->sum('importo'));
36
+  $totNonContabile = (float) ($totNonContabilizzato ?? $movimenti->where('_tipo', 'no_contabile')->sum('importo'));
37
+  $saldo = (float) ($saldo ?? ($totEntrate - $totUscite));
38
+
39
+  $giorni = $movimenti->groupBy('_date_key')->values();
40
+@endphp
41
+
42
+@include('report.pdf._styles')
43
+
44
+<h1>{{ $reportTitle }}</h1>
45
+<div class="meta">
46
+  <strong>{{ $attivitaNome }}</strong>
47
+  · Periodo {{ $periodoLabel }}
48
+  · Generato il {{ $generatoIl }}
49
+</div>
50
+
51
+<table class="kpi">
52
+  <tr>
53
+    <td>
54
+      <span class="label">Operazioni</span>
55
+      <span class="value">{{ number_format($movimenti->count(), 0, ',', '.') }}</span>
56
+      @if($totNonContabile > 0)
57
+        <span class="hint">No cont.: {{ number_format($totNonContabile, 2, ',', '.') }}</span>
58
+      @endif
59
+    </td>
60
+    <td>
61
+      <span class="label">Uscite (dare)</span>
62
+      <span class="value ko">{{ number_format($totUscite, 2, ',', '.') }}</span>
63
+    </td>
64
+    <td>
65
+      <span class="label">Entrate (avere)</span>
66
+      <span class="value ok">{{ number_format($totEntrate, 2, ',', '.') }}</span>
67
+    </td>
68
+    <td>
69
+      <span class="label">Saldo</span>
70
+      <span class="value {{ $saldo >= 0 ? 'ok' : 'ko' }}">{{ number_format($saldo, 2, ',', '.') }}</span>
71
+    </td>
72
+  </tr>
73
+</table>
74
+
75
+<table class="data">
76
+  <colgroup>
77
+    <col style="width: 26pt;">
78
+    <col style="width: 37pt;">
79
+    <col style="width: 287pt;">
80
+    <col style="width: 48pt;">
81
+    <col style="width: 43pt;">
82
+    <col style="width: 43pt;">
83
+    <col style="width: 47pt;">
84
+  </colgroup>
85
+  <thead>
86
+    <tr>
87
+      <th class="col-ora" style="width: 5%;">Ora</th>
88
+      <th class="col-tipo" style="width: 7%;">Tipo</th>
89
+      <th class="col-causale" style="width: 54%;">Causale</th>
90
+      <th class="col-metodo" style="width: 9%;">Metodo</th>
91
+      <th class="num col-num" style="width: 8%;">Dare</th>
92
+      <th class="num col-num" style="width: 8%;">Avere</th>
93
+      <th class="num col-num" style="width: 9%;">Saldo</th>
94
+    </tr>
95
+  </thead>
96
+  <tbody>
97
+    @forelse($giorni as $items)
98
+      @php
99
+        $first = $items->first();
100
+        $entrateGiorno = (float) $items->where('_tipo', 'entrata')->sum('importo');
101
+        $usciteGiorno = (float) $items->where('_tipo', 'uscita')->sum('importo');
102
+        $netto = $entrateGiorno - $usciteGiorno;
103
+      @endphp
104
+      <tr class="day">
105
+        <td class="col-ora"></td>
106
+        <td class="col-tipo"></td>
107
+        <td class="col-causale">
108
+          {{ $first['_date_label'] ?? '-' }}
109
+          · {{ $items->count() }} mov.
110
+          · +{{ number_format($entrateGiorno, 2, ',', '.') }}
111
+          / −{{ number_format($usciteGiorno, 2, ',', '.') }}
112
+          · Netto {{ ($netto >= 0 ? '+' : '−') . number_format(abs($netto), 2, ',', '.') }}
113
+        </td>
114
+        <td></td>
115
+        <td></td>
116
+        <td></td>
117
+        <td></td>
118
+      </tr>
119
+      @foreach($items as $m)
120
+        @php
121
+          $tipo = $m['_tipo'] ?? '';
122
+          $importo = (float) ($m['importo'] ?? 0);
123
+          $tipoLabel = match ($tipo) {
124
+            'entrata' => 'Entr.',
125
+            'uscita' => 'Usc.',
126
+            'no_contabile' => 'N/C',
127
+            default => ucfirst($tipo ?: 'N/D'),
128
+          };
129
+        @endphp
130
+        <tr>
131
+          <td class="col-ora">{{ $m['_time'] ?? '-' }}</td>
132
+          <td class="col-tipo">{{ $tipoLabel }}</td>
133
+          <td class="col-causale">
134
+            {{ $m['causale'] ?? '-' }}
135
+            @if(!empty($m['categoria']))
136
+              <span class="muted">{{ $m['categoria'] }}</span>
137
+            @endif
138
+          </td>
139
+          <td>{{ $m['metodo'] ?? 'N/D' }}</td>
140
+          <td class="num col-num {{ $tipo === 'uscita' ? 'ko' : 'muted' }}">
141
+            {{ $tipo === 'uscita' ? number_format($importo, 2, ',', '.') : '—' }}
142
+          </td>
143
+          <td class="num col-num {{ $tipo === 'entrata' ? 'ok' : 'muted' }}">
144
+            {{ $tipo === 'entrata' ? number_format($importo, 2, ',', '.') : '—' }}
145
+          </td>
146
+          <td class="num col-num {{ ($m['_running'] ?? 0) < 0 ? 'ko' : '' }}">
147
+            {{ number_format((float) ($m['_running'] ?? 0), 2, ',', '.') }}
148
+          </td>
149
+        </tr>
150
+      @endforeach
151
+    @empty
152
+      <tr>
153
+        <td colspan="7" class="center muted">Nessun movimento nel periodo selezionato.</td>
154
+      </tr>
155
+    @endforelse
156
+  </tbody>
157
+  @if($movimenti->isNotEmpty())
158
+    <tfoot>
159
+      <tr>
160
+        <td></td>
161
+        <td></td>
162
+        <td>Totale periodo</td>
163
+        <td></td>
164
+        <td class="num col-num ko">{{ number_format($totUscite, 2, ',', '.') }}</td>
165
+        <td class="num col-num ok">{{ number_format($totEntrate, 2, ',', '.') }}</td>
166
+        <td class="num col-num {{ $saldo < 0 ? 'ko' : '' }}">{{ number_format($saldo, 2, ',', '.') }}</td>
167
+      </tr>
168
+    </tfoot>
169
+  @endif
170
+</table>
171
+
172
+<div class="foot-note">
173
+  Valori in euro · saldo progressivo in ordine temporale · no contabile escluso dal saldo
174
+</div>

+ 8
- 18
resources/views/testi/show.blade.php Ver fichero

@@ -1,19 +1,9 @@
1
-
2
-<div class="text-justify lh-base">
1
+@if($testi && filled($testi->testo ?? null))
2
+  <div class="text-justify lh-base">
3 3
     {!! preg_replace('/(<br\s*\/?>)+/', '<br><br>', nl2br(e($testi->testo))) !!}
4
-</div>
5
-<!-- Editor gratuito: utilizzo di CKEditor 5 Classic da CDN, gratuito per uso di base -->
6
-<!-- <div class="mt-3">
7
-
8
-    <textarea id="editor" class="form-control">{!! old('testo', $testi->testo) !!}</textarea>
9
-    <script src="https://cdn.ckeditor.com/ckeditor5/39.0.1/classic/ckeditor.js"></script>
10
-    <script>
11
-        ClassicEditor
12
-            .create(document.querySelector('#editor'), {
13
-                toolbar: ['heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote']
14
-            })
15
-            .catch(error => {
16
-                console.error(error);
17
-            });
18
-    </script>
19
-</div> -->
4
+  </div>
5
+@else
6
+  <p class="text-muted small mb-0">
7
+    Informativa privacy non ancora configurata. Contatta l&apos;organizzazione per i dettagli sul trattamento dei dati.
8
+  </p>
9
+@endif

+ 110
- 113
resources/views/welcome.blade.php Ver fichero

@@ -1,7 +1,4 @@
1 1
 @php
2
-  use Illuminate\Support\Facades\Route;
3
-  use Illuminate\Support\Str;
4
-  $registerUrl = Route::has('register') ? route('register') : url('auth/register-cover');
5 2
   $orgAccents = ['#f58220', '#602d91', '#15265c', '#0d9488', '#be185d', '#2563eb'];
6 3
 @endphp
7 4
 
@@ -21,127 +18,127 @@
21 18
 @endsection
22 19
 
23 20
 @section('content')
24
-  <div class="welcome-bacheca__bg" data-bg="soft" aria-hidden="true">
25
-    <div class="welcome-bacheca__bg-stripe"></div>
26
-  </div>
27
-
28
-  <div class="welcome-bacheca">
29
-    <header class="welcome-bacheca__masthead" aria-labelledby="bacheca-title">
30
-      <div class="welcome-bacheca__masthead-row">
31
-        <div class="welcome-bacheca__brand">
32
-          <img
33
-            src="{{ asset('assets/img/logo_fest_L.png') }}"
34
-            alt="{{ config('app.name') }}"
35
-            class="welcome-bacheca__logo"
36
-            width="200"
37
-            height="60"
38
-          >
39
-        </div>
40
-        <div class="welcome-bacheca__masthead-actions">
41
-          <a href="{{ route('login') }}" class="welcome-bacheca__btn welcome-bacheca__btn--ghost">
42
-            Accedi
43
-          </a>
44
-        </div>
45
-      </div>
46
-      <h1 id="bacheca-title" class="welcome-bacheca__title">Bacheca eventi</h1>
21
+  <article class="welcome-bacheca" style="--evento-accent: #f58220;">
22
+    <div class="welcome-bacheca__bg" aria-hidden="true"></div>
23
+
24
+    <header class="welcome-bacheca__top">
25
+      <a href="{{ route('login') }}" class="welcome-bacheca__top-link">
26
+        <i class="bx bx-log-in" aria-hidden="true"></i>
27
+        <span>Accedi</span>
28
+      </a>
29
+      <img
30
+        src="{{ asset('assets/img/logo_fest_L.png') }}"
31
+        alt="{{ config('app.name') }}"
32
+        class="welcome-bacheca__brand-logo"
33
+        width="120"
34
+        height="28"
35
+      >
47 36
     </header>
48 37
 
49
-    @if($organizzazioni->isEmpty())
50
-      <div class="welcome-bacheca__empty">
51
-        <div class="welcome-bacheca__empty-icon">
52
-          <i class="bx bx-calendar-event"></i>
38
+    <div class="welcome-bacheca__shell">
39
+      <header class="welcome-bacheca__intro" aria-labelledby="bacheca-title">
40
+        <p class="welcome-bacheca__eyebrow">In programma</p>
41
+        <h1 id="bacheca-title" class="welcome-bacheca__title">Bacheca eventi</h1>
42
+        <p class="welcome-bacheca__lead">Scegli l’organizzazione e apri la scheda dell’attività.</p>
43
+      </header>
44
+
45
+      @if($organizzazioni->isEmpty())
46
+        <div class="welcome-bacheca__empty">
47
+          <div class="welcome-bacheca__empty-icon">
48
+            <i class="bx bx-calendar-event"></i>
49
+          </div>
50
+          <h2 class="welcome-bacheca__empty-title">Nessuna attività in bacheca</h2>
51
+          <p class="welcome-bacheca__empty-text">Al momento non ci sono eventi pubblicati. Torna a trovarci presto.</p>
52
+          <a href="{{ route('login') }}" class="welcome-bacheca__btn welcome-bacheca__btn--primary">
53
+            Area operatore
54
+          </a>
53 55
         </div>
54
-        <h2 class="welcome-bacheca__empty-title">Nessuna attività in bacheca</h2>
55
-        <p class="welcome-bacheca__empty-text">Al momento non ci sono eventi pubblicati. Torna a trovarci presto.</p>
56
-        <a href="{{ route('login') }}" class="welcome-bacheca__btn welcome-bacheca__btn--primary">
57
-          Area operatore
58
-        </a>
59
-      </div>
60
-    @else
61
-      <section class="welcome-bacheca__board" aria-label="Organizzazioni e attività">
62
-        @if($totaleAttivita > 3 || $organizzazioni->count() > 1)
63
-          <label class="welcome-bacheca__search" for="welcomeBachecaSearch">
64
-            <i class="bx bx-search" aria-hidden="true"></i>
65
-            <input
66
-              type="search"
67
-              id="welcomeBachecaSearch"
68
-              class="welcome-bacheca__search-input"
69
-              placeholder="Cerca…"
70
-              autocomplete="off"
71
-            >
72
-          </label>
73
-        @endif
74
-
75
-        @if($organizzazioni->count() > 1)
76
-          <div class="welcome-bacheca__carousel" id="welcomeOrgCarousel" aria-roledescription="carousel" aria-label="Organizzazioni in programma">
77
-            <button
78
-              type="button"
79
-              class="welcome-bacheca__carousel-nav welcome-bacheca__carousel-nav--prev"
80
-              id="welcomeCarouselPrev"
81
-              aria-label="Organizzazione precedente"
82
-            >
83
-              <i class="bx bx-chevron-left" aria-hidden="true"></i>
84
-            </button>
85
-
86
-            <div class="welcome-bacheca__carousel-viewport">
87
-              <div class="welcome-bacheca__carousel-track" id="welcomeCarouselTrack">
88
-                @foreach($organizzazioni as $orgIndex => $organizzazione)
89
-                  @include('welcome._partials.org-slide', [
90
-                    'organizzazione' => $organizzazione,
91
-                    'orgIndex' => $orgIndex,
92
-                    'orgAccents' => $orgAccents,
93
-                    'isActive' => $orgIndex === 0,
94
-                  ])
95
-                @endforeach
56
+      @else
57
+        <section class="welcome-bacheca__board" aria-label="Organizzazioni e attività">
58
+          @if($totaleAttivita > 3 || $organizzazioni->count() > 1)
59
+            <label class="welcome-bacheca__search" for="welcomeBachecaSearch">
60
+              <i class="bx bx-search" aria-hidden="true"></i>
61
+              <input
62
+                type="search"
63
+                id="welcomeBachecaSearch"
64
+                class="welcome-bacheca__search-input"
65
+                placeholder="Cerca…"
66
+                autocomplete="off"
67
+              >
68
+            </label>
69
+          @endif
70
+
71
+          @if($organizzazioni->count() > 1)
72
+            <div class="welcome-bacheca__carousel" id="welcomeOrgCarousel" aria-roledescription="carousel" aria-label="Organizzazioni in programma">
73
+              <button
74
+                type="button"
75
+                class="welcome-bacheca__carousel-nav welcome-bacheca__carousel-nav--prev"
76
+                id="welcomeCarouselPrev"
77
+                aria-label="Organizzazione precedente"
78
+              >
79
+                <i class="bx bx-chevron-left" aria-hidden="true"></i>
80
+              </button>
81
+
82
+              <div class="welcome-bacheca__carousel-viewport">
83
+                <div class="welcome-bacheca__carousel-track" id="welcomeCarouselTrack">
84
+                  @foreach($organizzazioni as $orgIndex => $organizzazione)
85
+                    @include('welcome._partials.org-slide', [
86
+                      'organizzazione' => $organizzazione,
87
+                      'orgIndex' => $orgIndex,
88
+                      'orgAccents' => $orgAccents,
89
+                      'isActive' => $orgIndex === 0,
90
+                    ])
91
+                  @endforeach
92
+                </div>
96 93
               </div>
97
-            </div>
98 94
 
99
-            <button
100
-              type="button"
101
-              class="welcome-bacheca__carousel-nav welcome-bacheca__carousel-nav--next"
102
-              id="welcomeCarouselNext"
103
-              aria-label="Organizzazione successiva"
104
-            >
105
-              <i class="bx bx-chevron-right" aria-hidden="true"></i>
106
-            </button>
95
+              <button
96
+                type="button"
97
+                class="welcome-bacheca__carousel-nav welcome-bacheca__carousel-nav--next"
98
+                id="welcomeCarouselNext"
99
+                aria-label="Organizzazione successiva"
100
+              >
101
+                <i class="bx bx-chevron-right" aria-hidden="true"></i>
102
+              </button>
107 103
 
108
-            <div class="welcome-bacheca__carousel-dots" id="welcomeCarouselDots" role="tablist" aria-label="Seleziona organizzazione"></div>
109
-          </div>
110
-        @else
111
-          @php $soloOrg = $organizzazioni->first(); @endphp
112
-          <div class="welcome-bacheca__org-solo" style="--org-accent: {{ $soloOrg->attivita->first()?->colore ?: $orgAccents[0] }}">
113
-            @include('welcome._partials.org-slide', [
114
-              'organizzazione' => $soloOrg,
115
-              'orgIndex' => 0,
116
-              'orgAccents' => $orgAccents,
117
-              'isActive' => true,
118
-            ])
119
-          </div>
120
-        @endif
121
-
122
-        <div class="welcome-bacheca__org-stage" id="welcomeOrgStage">
123
-          <div class="welcome-bacheca__org-stage-body" id="welcomeOrgStageBody">
124
-            @foreach($organizzazioni as $orgIndex => $organizzazione)
125
-              @include('welcome._partials.org-panel', [
126
-                'organizzazione' => $organizzazione,
127
-                'orgIndex' => $orgIndex,
104
+              <div class="welcome-bacheca__carousel-dots" id="welcomeCarouselDots" role="tablist" aria-label="Seleziona organizzazione"></div>
105
+            </div>
106
+          @else
107
+            @php $soloOrg = $organizzazioni->first(); @endphp
108
+            <div class="welcome-bacheca__org-solo" style="--org-accent: {{ $soloOrg->attivita->first()?->colore ?: $orgAccents[0] }}">
109
+              @include('welcome._partials.org-slide', [
110
+                'organizzazione' => $soloOrg,
111
+                'orgIndex' => 0,
128 112
                 'orgAccents' => $orgAccents,
129
-                'isActive' => $orgIndex === 0,
113
+                'isActive' => true,
130 114
               ])
131
-            @endforeach
115
+            </div>
116
+          @endif
117
+
118
+          <div class="welcome-bacheca__org-stage" id="welcomeOrgStage">
119
+            <div class="welcome-bacheca__org-stage-body" id="welcomeOrgStageBody">
120
+              @foreach($organizzazioni as $orgIndex => $organizzazione)
121
+                @include('welcome._partials.org-panel', [
122
+                  'organizzazione' => $organizzazione,
123
+                  'orgIndex' => $orgIndex,
124
+                  'orgAccents' => $orgAccents,
125
+                  'isActive' => $orgIndex === 0,
126
+                ])
127
+              @endforeach
128
+            </div>
132 129
           </div>
133
-        </div>
134 130
 
135
-        <p class="welcome-bacheca__search-empty" id="welcomeBachecaSearchEmpty" hidden>
136
-          Nessuna organizzazione o attività corrisponde alla ricerca.
137
-        </p>
138
-      </section>
139
-    @endif
131
+          <p class="welcome-bacheca__search-empty" id="welcomeBachecaSearchEmpty" hidden>
132
+            Nessuna organizzazione o attività corrisponde alla ricerca.
133
+          </p>
134
+        </section>
135
+      @endif
140 136
 
141
-    <footer class="welcome-bacheca__footer">
142
-      {{ config('app.name') }} · la bacheca digitale per feste e sagre
143
-    </footer>
144
-  </div>
137
+      <footer class="welcome-bacheca__footer">
138
+        {{ config('app.name') }} · la bacheca digitale per feste e sagre
139
+      </footer>
140
+    </div>
141
+  </article>
145 142
 
146 143
   <div class="modal fade" id="welcomeAttivitaModal" tabindex="-1" aria-labelledby="welcomeAttivitaModalLabel" aria-hidden="true">
147 144
     <div class="modal-dialog modal-dialog-centered welcome-bacheca__modal-dialog">

+ 4
- 2
resources/views/welcome/_partials/attivita-card.blade.php Ver fichero

@@ -6,7 +6,9 @@
6 6
   $coverUrl = $hasCustomCover ? $attivita->coverUrl() : null;
7 7
   $festLogoFallback = \App\Models\Attivita::defaultLogoUrl();
8 8
   $saltacodaAttivo = (bool) ($attivita->saltacoda?->is_attivo);
9
-  $schedaUrl = route('cliente.attivita.show', ['attivita_id' => $attivita->id]);
9
+  $attivitaSlug = $attivita->publicSlug();
10
+  $schedaUrl = $attivita->publicUrl();
11
+  $saltacodaUrl = route('cliente.saltacoda.show', ['slug' => $attivitaSlug]);
10 12
   $searchText = Str::lower(
11 13
     ($orgSearch ?? '') . ' ' . $attivita->nome . ' ' . ($attivita->descrizione ?? '')
12 14
   );
@@ -54,7 +56,7 @@
54 56
       data-attivita-iniziali="{{ $attivita->iniziali() }}"
55 57
       data-attivita-accent="{{ $accent }}"
56 58
       data-saltacoda-attivo="{{ $saltacodaAttivo ? '1' : '0' }}"
57
-      data-saltacoda-url="{{ route('cliente.saltacoda.show', ['attivita_id' => $attivita->id]) }}"
59
+      data-saltacoda-url="{{ $saltacodaUrl }}"
58 60
       data-scheda-url="{{ $schedaUrl }}"
59 61
     >
60 62
       <i class="bx bx-dots-vertical-rounded" aria-hidden="true"></i>

Loading…
Cancelar
Guardar