From b30b87e74a35d2a24a5cfb83c6bde52cd0b19ff2 Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Tue, 28 Jul 2026 17:52:22 +0200 Subject: [PATCH] ++ fix grafica e compilazione s-13 --- app/Livewire/Registro.php | 60 +++++++-- resources/views/pdf/registro-s13.blade.php | 150 ++++++++++++++++----- 2 files changed, 168 insertions(+), 42 deletions(-) diff --git a/app/Livewire/Registro.php b/app/Livewire/Registro.php index b1d771e..fa9b15d 100644 --- a/app/Livewire/Registro.php +++ b/app/Livewire/Registro.php @@ -249,9 +249,9 @@ class Registro extends Component private function impaginaS13(\Illuminate\Support\Collection $righe): \Illuminate\Support\Collection { $vuota = [ - 'numero' => '', - 'rientro_precedente' => '', - 'assegnazioni' => $this->completaCelleS13([]), + 'numero' => '', + 'riporto' => ['nome' => '', 'data' => ''], + 'assegnazioni' => $this->completaCelleS13([]), ]; $pagine = $righe->isEmpty() @@ -292,9 +292,10 @@ class Registro extends Component return $assegnazioni ->map(function ($gruppo, $territorioId) use ($rientriPrecedenti) { - // Se un territorio è girato più di quattro volte nell'anno il - // modulo non ha spazio per tutte: si stampano le ultime - // quattro, mantenendo l'ordine cronologico da sinistra. + $gruppo = $gruppo->values(); + + // I quattro gruppi "Assegnato a" ospitano le ultime quattro + // assegnazioni, in ordine cronologico da sinistra. $ultime = $gruppo->slice(-self::S13_ASSEGNAZIONI_PER_RIGA)->values(); $celle = $ultime->map(fn (Assegnazione $a) => [ @@ -303,18 +304,55 @@ class Registro extends Component 'restituito_il' => $a->returned_at?->format('d/m/Y') ?? '', ])->all(); - $rientro = $rientriPrecedenti->get($territorioId); - return [ - 'numero' => $gruppo->first()->territorio?->numero ?? '', - 'rientro_precedente' => $rientro ? \Carbon\Carbon::parse($rientro)->format('d/m/Y') : '', - 'assegnazioni' => $this->completaCelleS13($celle), + 'numero' => $gruppo->first()->territorio?->numero ?? '', + 'riporto' => $this->riportoS13($gruppo, $rientriPrecedenti->get($territorioId)), + 'assegnazioni' => $this->completaCelleS13($celle), ]; }) ->sort(fn (array $a, array $b) => strnatcasecmp((string) $a['numero'], (string) $b['numero'])) ->values(); } + /** + * Contenuto della colonna "Restituito l'ultima volta il", che funziona da + * quinta casella della riga. + * + * Quando le assegnazioni dell'anno superano le quattro che i gruppi + * "Assegnato a" possono ospitare, la colonna accoglie quella + * immediatamente precedente alle quattro mostrate — nome e data di + * rientro — così la riga copre cinque passaggi invece di quattro. In quel + * caso il riporto dell'anno precedente non ha più senso: la continuità + * con il passato ce l'ha già l'assegnazione in colonna. + * + * Con quattro o meno assegnazioni nell'anno la colonna torna al suo uso + * da modulo: l'ultima data di rientro dell'anno di servizio precedente, + * senza nome. + * + * @param \Illuminate\Support\Collection $gruppo + * @return array{nome: string, data: string} + */ + private function riportoS13(\Illuminate\Support\Collection $gruppo, ?string $rientroAnnoPrecedente): array + { + $eccedenti = $gruppo->count() - self::S13_ASSEGNAZIONI_PER_RIGA; + + if ($eccedenti > 0) { + $precedente = $gruppo->get($eccedenti - 1); + + return [ + 'nome' => $precedente->proclamatore?->nome_completo ?? '', + 'data' => $precedente->returned_at?->format('d/m/Y') ?? '', + ]; + } + + return [ + 'nome' => '', + 'data' => $rientroAnnoPrecedente + ? \Carbon\Carbon::parse($rientroAnnoPrecedente)->format('d/m/Y') + : '', + ]; + } + /** * Ultima data di rientro di ogni territorio *precedente* all'inizio * dell'anno di servizio: è il dato che la colonna "Restituito l'ultima diff --git a/resources/views/pdf/registro-s13.blade.php b/resources/views/pdf/registro-s13.blade.php index c8cd593..4c83403 100644 --- a/resources/views/pdf/registro-s13.blade.php +++ b/resources/views/pdf/registro-s13.blade.php @@ -18,39 +18,100 @@ --}} @php /** - * Il nome del proclamatore deve stare su una riga: le righe hanno altezza - * fissa e la cella è nowrap, quindi mandare a capo sfonderebbe la griglia. - * Invece di andare a capo si scala il corpo. + * I nomi devono entrare nella loro cella senza sfondare la griglia: le + * righe hanno altezza fissa, quindi una riga di troppo sballerebbe tutto + * il foglio. Invece di andare a capo si scala il corpo. * - * Larghezza utile della cella ~34.8mm (due colonne meno il padding). - * DejaVu Sans occupa in media 0.52em per carattere, cioè ~0.1835mm per - * carattere per punto di corpo: da lì il corpo massimo che entra. + * La larghezza si stima a caratteri: 0.20mm per carattere per punto di + * corpo. È deliberatamente pessimistica — la media reale di DejaVu Sans è + * ~0.52em (0.1835mm), ma sui nomi propri, pieni di maiuscole, la media non + * regge e con zero margine il testo deborda dalla cella. Sotto i 4.5pt il + * testo non sarebbe più leggibile, quindi oltre quel limite si tronca: + * l'ellissi rende visibile l'accorciamento, mentre un nome che deborda + * corromperebbe la griglia in silenzio. * - * Sotto i 4.5pt il testo non sarebbe più leggibile, quindi oltre i 42 - * caratteri si tronca — l'ellissi rende visibile l'accorciamento, mentre - * un nome che deborda corromperebbe la griglia in silenzio. + * Le due celle hanno vincoli diversi. + * + * Le larghezze qui sotto sono misurate sul PDF renderizzato, non dedotte + * dalle percentuali: al netto di padding e bordi le celle risultano + * 35.9mm e 22.2mm, e queste costanti tengono un margine su quei valori. + * Se cambiano le percentuali delle colonne o i margini di pagina, vanno + * rimisurate. + * + * "Assegnato a" è nowrap su una riga sola: basta il corpo che ci sta, e + * oltre il minimo si tronca. + * + * Il riporto sta in una colonna stretta alta due righe. Lì il + * nome NON viene mandato a capo da dompdf: lo spezziamo noi in due righe + * bilanciate e le teniamo nowrap. Lasciar decidere al wrapper significa + * non sapere quante righe verranno fuori — e una terza riga non è un + * problema estetico, sfonda l'altezza del blocco e manda l'ultimo + * territorio su una pagina nuova, raddoppiando i fogli. Spezzando a mano + * l'altezza della cella è esatta per costruzione. */ - $LARGHEZZA_CELLA_MM = 34.8; - $MM_PER_CARATTERE_PER_PUNTO = 0.1835; - $CORPO_MAX_PT = 8.0; + $MM_PER_CARATTERE_PER_PUNTO = 0.20; $CORPO_MIN_PT = 4.5; - $maxCaratteri = (int) floor($LARGHEZZA_CELLA_MM / ($CORPO_MIN_PT * $MM_PER_CARATTERE_PER_PUNTO)); + $LARGHEZZA_NOME_MM = 34.0; // cella misurata 35.9mm + $LARGHEZZA_RIPORTO_MM = 20.5; // colonna misurata 22.2mm + $CORPO_RIPORTO_MAX_PT = 6.0; - $nomeTroncato = static fn (string $nome): string => mb_strlen($nome) > $maxCaratteri - ? mb_substr($nome, 0, $maxCaratteri - 1) . '…' - : $nome; - - $nomeFontSize = static function (string $nome) use ($LARGHEZZA_CELLA_MM, $MM_PER_CARATTERE_PER_PUNTO, $CORPO_MAX_PT, $CORPO_MIN_PT): string { - $len = mb_strlen($nome); + $corpoPer = static function (string $testo, float $larghezzaMm, float $corpoMaxPt) use ($MM_PER_CARATTERE_PER_PUNTO, $CORPO_MIN_PT): string { + $len = mb_strlen($testo); if ($len === 0) { - return $CORPO_MAX_PT . 'pt'; + return $corpoMaxPt . 'pt'; } - $pt = $LARGHEZZA_CELLA_MM / ($len * $MM_PER_CARATTERE_PER_PUNTO); + $pt = $larghezzaMm / ($len * $MM_PER_CARATTERE_PER_PUNTO); - return round(max($CORPO_MIN_PT, min($CORPO_MAX_PT, $pt)), 1) . 'pt'; + return round(max($CORPO_MIN_PT, min($corpoMaxPt, $pt)), 1) . 'pt'; + }; + + $troncaPer = static function (string $testo, float $larghezzaMm) use ($MM_PER_CARATTERE_PER_PUNTO, $CORPO_MIN_PT): string { + $max = (int) floor($larghezzaMm / ($CORPO_MIN_PT * $MM_PER_CARATTERE_PER_PUNTO)); + + return mb_strlen($testo) > $max ? mb_substr($testo, 0, $max - 1) . '…' : $testo; + }; + + /** + * Spezza il nome in due righe scegliendo, fra tutti i punti fra una parola + * e l'altra, quello che rende più corta la riga più lunga: è la riga più + * lunga a decidere il corpo, quindi bilanciare significa poter scrivere + * più grande. + * + * @return array{righe: array, corpo: string} + */ + $adattaRiporto = static function (string $testo) use ($corpoPer, $troncaPer, $LARGHEZZA_RIPORTO_MM, $CORPO_RIPORTO_MAX_PT): array { + $parole = preg_split('/\s+/', trim($testo), -1, PREG_SPLIT_NO_EMPTY); + + $righe = [$testo]; + + if (count($parole) > 1) { + $migliore = null; + + for ($i = 1; $i < count($parole); $i++) { + $prima = implode(' ', array_slice($parole, 0, $i)); + $dopo = implode(' ', array_slice($parole, $i)); + $peggio = max(mb_strlen($prima), mb_strlen($dopo)); + + if ($migliore === null || $peggio < $migliore['peggio']) { + $migliore = ['righe' => [$prima, $dopo], 'peggio' => $peggio]; + } + } + + $righe = $migliore['righe']; + } + + // Una riga più larga della colonna deborderebbe: si accorcia. + $righe = array_map(fn (string $r) => $troncaPer($r, $LARGHEZZA_RIPORTO_MM), $righe); + + $piuLunga = array_reduce($righe, fn (?string $a, string $b) => mb_strlen($b) > mb_strlen((string) $a) ? $b : $a); + + return [ + 'righe' => $righe, + 'corpo' => $corpoPer((string) $piuLunga, $LARGHEZZA_RIPORTO_MM, $CORPO_RIPORTO_MAX_PT), + ]; }; $ultimaPagina = $pagine->count() - 1; @@ -109,6 +170,12 @@ width: 100%; border-collapse: collapse; border: 1.6pt solid #000; + /* Obbligatorio, non cosmetico: in layout automatico le larghezze + del sono solo un suggerimento e una cella con del + testo lungo (la colonna del riporto, quando ospita un nome) si + allarga schiacciando le altre. I corpi dei nomi sono calcolati + su larghezze fisse, quindi a quel punto il testo deborda. */ + table-layout: fixed; } td, th { border: 0.5pt solid #000; @@ -127,12 +194,21 @@ th.sotto { font-size: 6.4pt; } /* Corpo */ - tr.riga-nome td { height: 6mm; } - tr.riga-date td { height: 4mm; } + /* 9.4mm a blocco. Il vincolo non sono i nomi (8pt = 2.8mm) ma la cella + del riporto, che a cavallo delle due righe ospita due righe di nome + a 6pt più la data: ~7.2mm. */ + tr.riga-nome td { height: 5.6mm; } + tr.riga-date td { height: 3.8mm; } td.numero { font-size: 9.5pt; font-weight: bold; } - td.rientro-prec { font-size: 7.5pt; } td.data { font-size: 7.5pt; } + + /* La colonna del riporto è alta due righe: quando ospita + un'assegnazione ci stanno il nome (che qui può andare a capo, c'è + spazio in verticale) e sotto la data di rientro. */ + td.riporto { padding: 0 0.5mm; } + .riporto-nome { line-height: 1.05; white-space: nowrap; } + .riporto-data { font-size: 7pt; line-height: 1.1; } td.nome { line-height: 1.1; padding: 0 0.6mm; white-space: nowrap; } .nota { font-size: 7.4pt; margin-top: 1.6mm; } @@ -155,11 +231,15 @@ @endfor + {{-- Le larghezze stanno sia nel sia qui: in layout + fisso dompdf le ricava dalle celle della prima riga, e + senza queste le due colonne rowspan si spartiscono lo + spazio in parti uguali invece di rispettare il colgroup. --}} - Terr.
N. - Restituito
l’ultima volta il* + Terr.
N. + Restituito
l’ultima volta il* @for($g = 0; $g < 4; $g++) - Assegnato a + Assegnato a @endfor @@ -171,12 +251,20 @@ @foreach($righe as $riga) + @php($riporto = $adattaRiporto($riga['riporto']['nome'])) {{ $riga['numero'] }} - {{ $riga['rientro_precedente'] }} + + @if($riga['riporto']['nome'] !== '') +
+ @foreach($riporto['righe'] as $r)
{{ $r }}
@endforeach +
+ @endif +
{{ $riga['riporto']['data'] }}
+ @foreach($riga['assegnazioni'] as $a) - @php($nome = $nomeTroncato($a['nome'])) - {{ $nome }} + @php($nome = $troncaPer($a['nome'], $LARGHEZZA_NOME_MM)) + {{ $nome }} @endforeach