diff --git a/.gitignore b/.gitignore index 62abd1c..0823e78 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,11 @@ *.swp *.swo *.sql +*.pdf +# Eccezione: i moduli ufficiali usati come matrice per i template di stampa +# (resources/views/pdf/*) vanno versionati, altrimenti il template perde il +# riferimento da cui è stato ricostruito. +!resources/pdf/*.pdf docker-compose.override.yml db_data/ redis_data/ diff --git a/app/Livewire/Registro.php b/app/Livewire/Registro.php index 9245636..66b32f3 100644 --- a/app/Livewire/Registro.php +++ b/app/Livewire/Registro.php @@ -11,11 +11,18 @@ use App\Models\Proclamatore; use App\Models\Territorio; use App\Models\Zona; use App\Models\Tipologia; +use Barryvdh\DomPDF\Facade\Pdf; class Registro extends Component { use WithPagination; + /** Blocchi-territorio per foglio, come nel modulo cartaceo S-13. */ + private const S13_RIGHE_PER_PAGINA = 21; + + /** Gruppi "Assegnato a" disponibili su ogni riga del modulo S-13. */ + private const S13_ASSEGNAZIONI_PER_RIGA = 4; + public string $search = ''; public string $filtroAnno = ''; public string $filtroZona = ''; @@ -167,6 +174,156 @@ class Registro extends Component $this->showDeleteConfirm = false; } + // ─── Modulo S-13 ──────────────────────────────────────────── + + /** + * Modulo ufficiale S-13-I ("Registrazione delle assegnazioni del + * territorio") compilato con le assegnazioni dell'anno di servizio + * selezionato nei filtri. + * + * Solo amministratore, come il resto del CRUD di questa pagina: il modulo + * espone in un unico file l'anagrafica completa di chi ha avuto quale + * territorio, dati personali che il ruolo assegnatario non deve poter + * esportare in blocco. + */ + public function downloadS13() + { + abort_if(!auth()->user()->can('settings.manage'), 403); + + // Il modulo è per definizione relativo a un singolo anno di servizio: + // con "Tutti gli anni" selezionato si ripiega sull'anno corrente. + $anno = $this->filtroAnno + ? AnnoTeocratico::find($this->filtroAnno) + : null; + $anno ??= AnnoTeocratico::corrente(); + + $pdf = Pdf::loadView('pdf.registro-s13', [ + 'annoLabel' => $anno->label, + 'pagine' => $this->impaginaS13($this->righeS13($anno)), + ])->setPaper('a4', 'portrait'); + + return response()->streamDownload( + fn () => print($pdf->output()), + 'S-13-registro-assegnazioni-' . $anno->label . '.pdf' + ); + } + + /** + * Impagina le righe in fogli da 21 blocchi, completando l'ultimo con + * blocchi vuoti: il foglio stampato resta identico al modulo cartaceo e le + * righe libere restano disponibili per la compilazione a mano. + * + * Senza righe da stampare si restituisce comunque un foglio vuoto, così il + * modulo scaricato è un S-13 in bianco anziché un PDF senza pagine. + * + * @param \Illuminate\Support\Collection $righe + * @return \Illuminate\Support\Collection> + */ + private function impaginaS13(\Illuminate\Support\Collection $righe): \Illuminate\Support\Collection + { + $vuota = [ + 'numero' => '', + 'rientro_precedente' => '', + 'assegnazioni' => $this->completaCelleS13([]), + ]; + + $pagine = $righe->isEmpty() + ? collect([collect()]) + : $righe->chunk(self::S13_RIGHE_PER_PAGINA)->map->values()->values(); + + return $pagine->map(fn ($pagina) => collect( + array_pad($pagina->all(), self::S13_RIGHE_PER_PAGINA, $vuota) + )); + } + + /** + * Righe del modulo: un blocco per ogni territorio attivo con almeno + * un'assegnazione (chiusa o in corso) nell'anno indicato, in ordine di + * numero crescente. + * + * @return \Illuminate\Support\Collection + */ + private function righeS13(AnnoTeocratico $anno): \Illuminate\Support\Collection + { + // territorio() è definita withTrashed(), quindi la condizione sui + // cestinati va scritta a mano: senza, i territori eliminati + // rientrerebbero nel modulo. + $assegnazioni = Assegnazione::with(['proclamatore', 'territorio']) + ->where('anno_teocratico_id', $anno->id) + ->whereHas('territorio', fn ($q) => $q + ->where('attivo', true) + ->whereNull('territori.deleted_at')) + ->orderBy('assigned_at') + ->orderBy('id') + ->get() + ->groupBy('territorio_id'); + + $rientriPrecedenti = $this->ultimiRientriPrimaDi( + $assegnazioni->keys()->all(), + $anno + ); + + 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. + $ultime = $gruppo->slice(-self::S13_ASSEGNAZIONI_PER_RIGA)->values(); + + $celle = $ultime->map(fn (Assegnazione $a) => [ + 'nome' => $a->proclamatore?->nome_completo ?? '', + 'assegnato_il' => $a->assigned_at?->format('d/m/Y') ?? '', + '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), + ]; + }) + ->sort(fn (array $a, array $b) => strnatcasecmp((string) $a['numero'], (string) $b['numero'])) + ->values(); + } + + /** + * Ultima data di rientro di ogni territorio *precedente* all'inizio + * dell'anno di servizio: è il dato che la colonna "Restituito l'ultima + * volta il" del modulo chiede di riportare quando si apre un foglio nuovo. + * + * @param array $territorioIds + * @return \Illuminate\Support\Collection + */ + private function ultimiRientriPrimaDi(array $territorioIds, AnnoTeocratico $anno): \Illuminate\Support\Collection + { + if ($territorioIds === []) { + return collect(); + } + + return Assegnazione::whereIn('territorio_id', $territorioIds) + ->whereNotNull('returned_at') + ->whereDate('returned_at', '<', $anno->start_date) + ->groupBy('territorio_id') + ->selectRaw('territorio_id, MAX(returned_at) as ultimo_rientro') + ->pluck('ultimo_rientro', 'territorio_id'); + } + + /** + * Porta le celle a quattro riempiendo con voci vuote: le colonne + * inutilizzate restano stampate e a disposizione della compilazione a mano. + * + * @param array $celle + * @return array + */ + private function completaCelleS13(array $celle): array + { + $vuota = ['nome' => '', 'assegnato_il' => '', 'restituito_il' => '']; + + return array_pad($celle, self::S13_ASSEGNAZIONI_PER_RIGA, $vuota); + } + public function mount(): void { if ($this->filtroAnno === '') { diff --git a/resources/pdf/S-13_I.pdf b/resources/pdf/S-13_I.pdf new file mode 100644 index 0000000..ee10bec Binary files /dev/null and b/resources/pdf/S-13_I.pdf differ diff --git a/resources/views/livewire/registro.blade.php b/resources/views/livewire/registro.blade.php index 9bfd043..99d8e89 100644 --- a/resources/views/livewire/registro.blade.php +++ b/resources/views/livewire/registro.blade.php @@ -2,10 +2,21 @@

Registro Assegnazioni

@can('settings.manage') - +
+ + +
@endcan
diff --git a/resources/views/pdf/registro-s13.blade.php b/resources/views/pdf/registro-s13.blade.php new file mode 100644 index 0000000..10a4a60 --- /dev/null +++ b/resources/views/pdf/registro-s13.blade.php @@ -0,0 +1,163 @@ +{{-- + Modulo S-13-I — Registrazione delle assegnazioni del territorio. + + Ricostruzione fedele del modulo ufficiale, la cui copia è in + resources/pdf/S-13_I.pdf e va usata come matrice per qualsiasi modifica + alla griglia. Il modulo originale è un PDF 1.7 con font CID embeddati: + sovrapporvi testo richiederebbe il parser a pagamento di FPDI, mentre + dompdf è già in uso per gli altri export del progetto. + + Struttura per pagina: 21 blocchi-territorio, ognuno su due righe — quella + alta ospita i nomi dei proclamatori, quella bassa le date. Le colonne + "Terr. N." e "Restituito l'ultima volta il" occupano entrambe le righe. + + Il motore CSS di dompdf non è un browser: le larghezze stanno in + (con i soli colspan sarebbero ripartite a caso) e l'interruzione di pagina è + decisa dall'indice del ciclo, perché il supporto a :last-child non è + garantito e un foglio bianco in coda sarebbe visibile. +--}} +@php + /** + * Il nome del proclamatore deve stare su una riga in una cella larga + * ~35mm. DejaVu Sans ha larghezza media ~0.52em per carattere, quindi a + * 8pt entrano circa 23 caratteri: sotto quella soglia si scala il corpo + * invece di mandare a capo, che sfonderebbe l'altezza fissa della riga. + */ + $nomeFontSize = static function (string $nome): string { + $len = mb_strlen($nome); + + return match (true) { + $len <= 23 => '8pt', + $len <= 27 => '7pt', + $len <= 32 => '6.2pt', + default => '5.5pt', + }; + }; + + $ultimaPagina = $pagine->count() - 1; +@endphp + + + + + S-13 — Registrazione delle assegnazioni del territorio + + + +@foreach($pagine as $indice => $righe) +
+

REGISTRAZIONE DELLE ASSEGNAZIONI DEL TERRITORIO

+ +
Anno di servizio: {{ $annoLabel }}
+ + + + + + @for($c = 0; $c < 8; $c++) + + @endfor + + + + + + @for($g = 0; $g < 4; $g++) + + @endfor + + + @for($g = 0; $g < 4; $g++) + + + @endfor + + + + @foreach($righe as $riga) + + + + @foreach($riga['assegnazioni'] as $a) + + @endforeach + + + @foreach($riga['assegnazioni'] as $a) + + + @endforeach + + @endforeach + +
Terr.
N.
Restituito
l’ultima volta il*
Assegnato a
Assegnato ilRestituito il
{{ $riga['numero'] }}{{ $riga['rientro_precedente'] }}{{ $a['nome'] }}
{{ $a['assegnato_il'] }}{{ $a['restituito_il'] }}
+ +
*Quando si inizia un nuovo foglio, registrare in questa colonna l’ultima data in cui è stato restituito ogni territorio.
+
S-13-I  1/22
+
+@endforeach + +