++ add creation S-13_I
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -20,6 +20,11 @@
|
|||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*.sql
|
*.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
|
docker-compose.override.yml
|
||||||
db_data/
|
db_data/
|
||||||
redis_data/
|
redis_data/
|
||||||
|
|||||||
@@ -11,11 +11,18 @@ use App\Models\Proclamatore;
|
|||||||
use App\Models\Territorio;
|
use App\Models\Territorio;
|
||||||
use App\Models\Zona;
|
use App\Models\Zona;
|
||||||
use App\Models\Tipologia;
|
use App\Models\Tipologia;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
|
||||||
class Registro extends Component
|
class Registro extends Component
|
||||||
{
|
{
|
||||||
use WithPagination;
|
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 $search = '';
|
||||||
public string $filtroAnno = '';
|
public string $filtroAnno = '';
|
||||||
public string $filtroZona = '';
|
public string $filtroZona = '';
|
||||||
@@ -167,6 +174,156 @@ class Registro extends Component
|
|||||||
$this->showDeleteConfirm = false;
|
$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<int, array> $righe
|
||||||
|
* @return \Illuminate\Support\Collection<int, \Illuminate\Support\Collection<int, array>>
|
||||||
|
*/
|
||||||
|
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<int, array>
|
||||||
|
*/
|
||||||
|
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<int, int> $territorioIds
|
||||||
|
* @return \Illuminate\Support\Collection<int, string>
|
||||||
|
*/
|
||||||
|
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<int, array> $celle
|
||||||
|
* @return array<int, 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
|
public function mount(): void
|
||||||
{
|
{
|
||||||
if ($this->filtroAnno === '') {
|
if ($this->filtroAnno === '') {
|
||||||
|
|||||||
BIN
resources/pdf/S-13_I.pdf
Normal file
BIN
resources/pdf/S-13_I.pdf
Normal file
Binary file not shown.
@@ -2,10 +2,21 @@
|
|||||||
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<h1 class="text-2xl font-bold text-gray-900">Registro Assegnazioni</h1>
|
<h1 class="text-2xl font-bold text-gray-900">Registro Assegnazioni</h1>
|
||||||
@can('settings.manage')
|
@can('settings.manage')
|
||||||
<button wire:click="openCreate" class="btn-action btn-action-indigo" style="padding:8px 18px;font-size:14px;border-radius:8px">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
<button wire:click="downloadS13"
|
||||||
Nuova voce
|
wire:loading.attr="disabled"
|
||||||
</button>
|
wire:target="downloadS13"
|
||||||
|
class="btn-action btn-action-green" style="padding:8px 18px;font-size:14px;border-radius:8px"
|
||||||
|
title="Scarica il modulo S-13 compilato con le ultime 4 assegnazioni dell'anno selezionato">
|
||||||
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h5l2 2h7a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
|
||||||
|
<span wire:loading.remove wire:target="downloadS13">Modulo S-13</span>
|
||||||
|
<span wire:loading wire:target="downloadS13">Generazione…</span>
|
||||||
|
</button>
|
||||||
|
<button wire:click="openCreate" class="btn-action btn-action-indigo" style="padding:8px 18px;font-size:14px;border-radius:8px">
|
||||||
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||||
|
Nuova voce
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@endcan
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
163
resources/views/pdf/registro-s13.blade.php
Normal file
163
resources/views/pdf/registro-s13.blade.php
Normal file
@@ -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 <colgroup>
|
||||||
|
(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
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>S-13 — Registrazione delle assegnazioni del territorio</title>
|
||||||
|
<style>
|
||||||
|
@page { margin: 12mm 10mm 10mm 10mm; }
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "DejaVu Sans", sans-serif;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interrompi { page-break-after: always; }
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 13pt;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.2pt;
|
||||||
|
margin-bottom: 9mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anno {
|
||||||
|
font-size: 10pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 3mm;
|
||||||
|
}
|
||||||
|
.anno .valore {
|
||||||
|
display: inline-block;
|
||||||
|
width: 32mm;
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: normal;
|
||||||
|
padding-bottom: 0.4mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1.6pt solid #000;
|
||||||
|
}
|
||||||
|
td, th {
|
||||||
|
border: 0.5pt solid #000;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Intestazione */
|
||||||
|
thead th { font-weight: normal; padding: 0.5mm 0.5mm; }
|
||||||
|
th.terr { font-size: 7.5pt; line-height: 1.15; }
|
||||||
|
th.restituito { font-size: 6.6pt; line-height: 1.15; }
|
||||||
|
th.assegnato-a { font-size: 8pt; }
|
||||||
|
th.sotto { font-size: 6.4pt; }
|
||||||
|
|
||||||
|
/* Corpo */
|
||||||
|
tr.riga-nome td { height: 6mm; }
|
||||||
|
tr.riga-date td { height: 4mm; }
|
||||||
|
|
||||||
|
td.numero { font-size: 9.5pt; font-weight: bold; }
|
||||||
|
td.rientro-prec { font-size: 7.5pt; }
|
||||||
|
td.data { font-size: 7.5pt; }
|
||||||
|
td.nome { line-height: 1.1; padding: 0 0.6mm; white-space: nowrap; }
|
||||||
|
|
||||||
|
.nota { font-size: 7.4pt; margin-top: 1.6mm; }
|
||||||
|
.codice { font-size: 8pt; margin-top: 5mm; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
@foreach($pagine as $indice => $righe)
|
||||||
|
<div @if($indice < $ultimaPagina) class="interrompi" @endif>
|
||||||
|
<h1>REGISTRAZIONE DELLE ASSEGNAZIONI DEL TERRITORIO</h1>
|
||||||
|
|
||||||
|
<div class="anno">Anno di servizio: <span class="valore">{{ $annoLabel }}</span></div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:6.5%">
|
||||||
|
<col style="width:12.5%">
|
||||||
|
@for($c = 0; $c < 8; $c++)
|
||||||
|
<col style="width:10.125%">
|
||||||
|
@endfor
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="terr" rowspan="2">Terr.<br>N.</th>
|
||||||
|
<th class="restituito" rowspan="2">Restituito<br>l’ultima volta il*</th>
|
||||||
|
@for($g = 0; $g < 4; $g++)
|
||||||
|
<th class="assegnato-a" colspan="2">Assegnato a</th>
|
||||||
|
@endfor
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
@for($g = 0; $g < 4; $g++)
|
||||||
|
<th class="sotto">Assegnato il</th>
|
||||||
|
<th class="sotto">Restituito il</th>
|
||||||
|
@endfor
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($righe as $riga)
|
||||||
|
<tr class="riga-nome">
|
||||||
|
<td class="numero" rowspan="2">{{ $riga['numero'] }}</td>
|
||||||
|
<td class="rientro-prec" rowspan="2">{{ $riga['rientro_precedente'] }}</td>
|
||||||
|
@foreach($riga['assegnazioni'] as $a)
|
||||||
|
<td class="nome" colspan="2" style="font-size:{{ $nomeFontSize($a['nome']) }}">{{ $a['nome'] }}</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
<tr class="riga-date">
|
||||||
|
@foreach($riga['assegnazioni'] as $a)
|
||||||
|
<td class="data">{{ $a['assegnato_il'] }}</td>
|
||||||
|
<td class="data">{{ $a['restituito_il'] }}</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="nota">*Quando si inizia un nuovo foglio, registrare in questa colonna l’ultima data in cui è stato restituito ogni territorio.</div>
|
||||||
|
<div class="codice">S-13-I 1/22</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user