SetAutoPageBreak(false); $pdf->SetMargins(0, 0, 0); $pdf->setSourceFile(resource_path('pdf/S-13_I.pdf')); $modulo = $pdf->importPage(1); $pdf->SetTextColor(0, 0, 0); foreach ($this->pagine($this->righe($anno)) as $righe) { $pdf->AddPage('P', 'A4'); $pdf->useTemplate($modulo); [$x0, $x1, $y] = self::ANNO; $pdf->SetFont('Helvetica', '', 10); $pdf->SetXY($x0, $y - 4.6); $pdf->Cell($x1 - $x0, 4.4, $this->latin($anno->label), 0, 0, 'C'); foreach ($righe as $indice => $riga) { $this->scriviRiga($pdf, $indice, $riga); } } return $pdf->Output('S'); } /** * Righe del modulo: una per territorio attivo, in ordine naturale di * numero ("1", "1 A", "2", "10"). * * Ogni riga: ['numero' => string, 'riporto' => string, 'assegnazioni' => * quattro celle ['nome', 'assegnato_il', 'restituito_il'], le libere vuote]. * * @return Collection */ public function righe(AnnoTeocratico $anno): Collection { $inizio = $anno->start_date->toDateString(); $fine = $anno->end_date->copy()->endOfDay(); $territori = Territorio::where('attivo', true)->get(['id', 'numero']); // Le assegnazioni del foglio, scelte per data e non per // anno_teocratico_id (che una correzione a mano o un import possono // lasciare incoerente): quelle uscite nell'anno, più quelle uscite // prima e ancora fuori al suo inizio. Confronti con stringhe Y-m-d e // solo con < e >=: valgono sia sulle colonne DATE di MariaDB sia sulle // "Y-m-d H:i:s" di SQLite, e usano l'indice (whereDate non lo userebbe). $assegnazioni = Assegnazione::with('proclamatore') ->whereIn('territorio_id', $territori->pluck('id')) ->where('assigned_at', '<', $anno->end_date->copy()->addDay()->toDateString()) ->where(fn ($q) => $q ->where('assigned_at', '>=', $inizio) ->orWhereNull('returned_at') ->orWhere('returned_at', '>=', $inizio)) ->orderBy('assigned_at') ->orderBy('id') ->get() ->groupBy('territorio_id'); // Riporto del primo ciclo: l'ultima restituzione prima dell'anno. $rientriPrecedenti = Assegnazione::query() ->whereIn('territorio_id', $territori->pluck('id')) ->where('returned_at', '<', $inizio) ->groupBy('territorio_id') ->selectRaw('territorio_id, MAX(returned_at) as ultimo') ->pluck('ultimo', 'territorio_id'); return $territori ->map(function (Territorio $territorio) use ($assegnazioni, $rientriPrecedenti, $fine) { $gruppo = ($assegnazioni->get($territorio->id) ?? collect())->values(); $inizioCiclo = $gruppo->isEmpty() ? 0 : intdiv($gruppo->count() - 1, self::ASSEGNAZIONI_PER_RIGA) * self::ASSEGNAZIONI_PER_RIGA; $riporto = $inizioCiclo === 0 ? $this->data($rientriPrecedenti->get($territorio->id)) : $this->rientroEntro($gruppo->get($inizioCiclo - 1), $fine); $celle = $gruppo ->slice($inizioCiclo, self::ASSEGNAZIONI_PER_RIGA) ->map(fn (Assegnazione $a) => [ 'nome' => $this->nomeBreve($a->proclamatore), 'assegnato_il' => $this->data($a->assigned_at), 'restituito_il' => $this->rientroEntro($a, $fine), ]) ->values() ->all(); return [ 'numero' => (string) $territorio->numero, 'riporto' => $riporto, 'assegnazioni' => $this->completaCelle($celle), ]; }) ->sort(fn (array $a, array $b) => strnatcasecmp($a['numero'], $b['numero'])) ->values(); } /** * Fogli da venti righe, l'ultimo completato con righe vuote: sul modulo le * righe libere restano disponibili per la compilazione a mano. Senza * territori esce comunque un foglio, in bianco. * * @return Collection> */ public function pagine(Collection $righe): Collection { $vuota = ['numero' => '', 'riporto' => '', 'assegnazioni' => $this->completaCelle([])]; $pagine = $righe->isEmpty() ? collect([collect()]) : $righe->chunk(self::RIGHE_PER_PAGINA)->map->values()->values(); return $pagine->map(fn (Collection $pagina) => collect( array_pad($pagina->all(), self::RIGHE_PER_PAGINA, $vuota) )); } private function scriviRiga(Fpdi $pdf, int $indice, array $riga): void { $alto = self::Y_PRIMA_RIGA + $indice * self::PASSO_RIGA + self::LINEA_SPESSA; $meta = self::Y_PRIMA_RIGA + $indice * self::PASSO_RIGA + self::Y_META_RIGA; $basso = self::Y_PRIMA_RIGA + ($indice + 1) * self::PASSO_RIGA; // Numero e riporto occupano il blocco intero, nomi e date mezzo. $pdf->SetFont('Helvetica', 'B', 10); $this->cella($pdf, self::COL_NUMERO, $alto, $basso, $riga['numero']); $pdf->SetFont('Helvetica', '', 8); $this->cella($pdf, self::COL_RIPORTO, $alto, $basso, $riga['riporto']); foreach ($riga['assegnazioni'] as $g => $a) { [$inizio, $divisione, $fine] = self::GRUPPI[$g]; $this->scriviNome($pdf, [$inizio, $fine], $alto, $meta, $a['nome']); $pdf->SetFont('Helvetica', '', 7.5); $this->cella($pdf, [$inizio, $divisione], $meta + self::LINEA_SOTTILE, $basso, $a['assegnato_il']); $this->cella($pdf, [$divisione, $fine], $meta + self::LINEA_SOTTILE, $basso, $a['restituito_il']); } } /** * I nomi stanno su una riga sola: il corpo scende finché il nome entra * nella cella, e sotto il minimo leggibile si tronca con i puntini, che * rendono visibile l'accorciamento invece di sbordare sulla griglia. */ private function scriviNome(Fpdi $pdf, array $colonna, float $alto, float $basso, string $nome): void { if ($nome === '') { return; } $testo = $this->latin($nome); $spazio = $colonna[1] - $colonna[0] - 1.2; $pdf->SetFont('Helvetica', '', self::CORPO_NOME_MAX); $larghezza = $pdf->GetStringWidth($testo); if ($larghezza > $spazio) { $corpo = max(self::CORPO_NOME_MIN, floor(self::CORPO_NOME_MAX * $spazio / $larghezza * 10) / 10); $pdf->SetFont('Helvetica', '', $corpo); while ($pdf->GetStringWidth($testo) > $spazio && strlen($testo) > 1) { $testo = rtrim(substr(rtrim($testo, "\x85"), 0, -1)) . "\x85"; } } $this->cella($pdf, $colonna, $alto, $basso, $testo, giaConvertito: true); } /** Testo centrato in un rettangolo della griglia. */ private function cella(Fpdi $pdf, array $colonna, float $alto, float $basso, string $testo, bool $giaConvertito = false): void { if ($testo === '') { return; } $pdf->SetXY($colonna[0], $alto); $pdf->Cell( $colonna[1] - $colonna[0], $basso - $alto, $giaConvertito ? $testo : $this->latin($testo), 0, 0, 'C' ); } /** * Data di rientro così come la si leggeva alla fine dell'anno: se il * territorio è rientrato dopo, sul foglio di quell'anno risulta ancora * fuori. */ private function rientroEntro(?Assegnazione $assegnazione, CarbonInterface $fine): string { $rientro = $assegnazione?->returned_at; return $rientro && $rientro->lte($fine) ? $this->data($rientro) : ''; } /** * Cognome e iniziale del nome ("Rossi M."): la cella è stretta, e col nome * per intero i nomi lunghi scendevano a un corpo che su carta non si legge. */ private function nomeBreve(?Proclamatore $proclamatore): string { if (! $proclamatore) { return ''; } $nome = trim((string) $proclamatore->nome); $iniziale = $nome !== '' ? mb_strtoupper(mb_substr($nome, 0, 1)) . '.' : ''; return trim(trim((string) $proclamatore->cognome) . ' ' . $iniziale); } private function data(mixed $valore): string { if (! $valore) { return ''; } return ($valore instanceof CarbonInterface ? $valore : \Illuminate\Support\Carbon::parse($valore)) ->format('d/m/Y'); } private function completaCelle(array $celle): array { return array_pad($celle, self::ASSEGNAZIONI_PER_RIGA, ['nome' => '', 'assegnato_il' => '', 'restituito_il' => '']); } /** FPDF ragiona in windows-1252: gli accenti UTF-8 vanno convertiti. */ private function latin(string $testo): string { $convertito = @iconv('UTF-8', 'windows-1252//TRANSLIT', $testo); return $convertito !== false ? $convertito : $testo; } }