From d433c9392d2bd52f23cffd87f9d3a8b19eede728 Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Fri, 9 Oct 2026 16:35:51 +0200 Subject: [PATCH] =?UTF-8?q?S-13=20sul=20modulo=20ufficiale=20e=20correzion?= =?UTF-8?q?i=20all'integrit=C3=A0=20dei=20dati?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modulo S-13 - Il PDF scrive sopra il modulo ufficiale (FPDI, matrice convertita in PDF 1.4) invece di ricostruirlo in HTML: 20 righe per foglio come l'originale. - Una riga per ogni territorio attivo; riporto dell'ultimo rientro prima dell'anno; assegnazioni a cavallo d'anno su entrambi i fogli; scelta per data e non per anno registrato; nomi come "Cognome I.". - Logica spostata in App\Services\Registro\ModuloS13. Integrità dei dati - Export/import XML (e quindi backup/ripristino): le date non slittano più di un giorno; i file vecchi in UTC tornano al giorno giusto. - Import XML: rifiuta file estranei o incompleti senza toccare i dati, errore con riga e motivo, territori senza zona importati con il loro storico, caratteri di controllo rimossi in export, chi importa conserva password e ruolo di amministratore. - Assegnazioni: assegnazioneCorrente corretta (filtro dentro ofMany), periodi sovrapposti rifiutati in Assegna e Registro sotto lock, anno di servizio ricavato dalla data, niente date future nel Registro. - Utenti: non si declassa sé stessi né l'ultimo amministratore. - Entrypoint: non sovrascrive più la copia persistente di APP_KEY con un valore diverso; serve APP_KEY_ROTATE=1 per un cambio voluto. Co-Authored-By: Claude Opus 5.5 --- .env.example | 4 + HANDOFF.md | 41 ++- app/Livewire/Assegnazioni/Assegna.php | 49 ++- app/Livewire/Registro.php | 197 +++-------- app/Livewire/Settings/UsersIndex.php | 21 ++ app/Livewire/Settings/XmlExchange.php | 166 +++++++-- app/Models/Assegnazione.php | 38 ++ app/Models/Territorio.php | 8 +- app/Services/Registro/ModuloS13.php | 329 ++++++++++++++++++ docker/php/entrypoint.sh | 15 + resources/pdf/S-13_I.pdf | Bin 80686 -> 130948 bytes resources/views/livewire/registro.blade.php | 16 +- resources/views/pdf/registro-s13.blade.php | 222 ------------ .../AssegnazioneSovrapposizioneTest.php | 211 +++++++++++ tests/Feature/RegistroS13Test.php | 207 +++++++++-- tests/Feature/UltimoAmministratoreTest.php | 78 +++++ tests/Feature/XmlExchangeImportTest.php | 216 ++++++++++++ 17 files changed, 1361 insertions(+), 457 deletions(-) create mode 100644 app/Services/Registro/ModuloS13.php delete mode 100644 resources/views/pdf/registro-s13.blade.php create mode 100644 tests/Feature/AssegnazioneSovrapposizioneTest.php create mode 100644 tests/Feature/UltimoAmministratoreTest.php create mode 100644 tests/Feature/XmlExchangeImportTest.php diff --git a/.env.example b/.env.example index 48389d8..0198a86 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,10 @@ APP_URL=http://192.0.2.10:8080 # I dati personali dei proclamatori sono cifrati con questa chiave: se la # perdi non sono piu' leggibili nemmeno avendo il backup del database. # Conservala in un password manager. +# Una copia sta nel volume dello storage: se questo valore cambia e non +# coincide con la copia, il container si rifiuta di partire invece di rendere +# illeggibili i dati. Per un cambio voluto aggiungi APP_KEY_ROTATE=1 (la +# chiave vecchia viene conservata accanto alla copia), poi toglila. APP_KEY= LOG_CHANNEL=stderr diff --git a/HANDOFF.md b/HANDOFF.md index a4785d3..d84c611 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -938,10 +938,12 @@ Le scelte che vale la pena conoscere: non parla di cifratura riesce a scrivere una riga. Non ha alcun rapporto con la chiave di produzione, che vive nell'ambiente e nel volume. -- **`righeS13()` si chiama in riflessione.** È privata di proposito (non è - un'azione Livewire) ma è il calcolo, non il PDF, quello che va protetto: - renderla pubblica solo per poterla testare avrebbe allargato la superficie - del componente per comodità del test. +- **Il modulo S-13 è un servizio, non un metodo del componente.** Calcolo e + PDF stanno in `App\Services\Registro\ModuloS13`, che i test chiamano + direttamente; il componente `Registro` si limita a validare l'anno e + scaricare. Il PDF scrive sopra il modulo ufficiale + (`resources/pdf/S-13_I.pdf`, convertito in PDF 1.4 per FPDI) alle + coordinate della sua griglia, come la cartolina S-12. ### Un solo container esegue le migrazioni @@ -956,6 +958,12 @@ mancante — con il rischio di chiavi diverse a ogni richiesta e dati cifrati irrecuperabili. Ora è risolta dall'entrypoint (env → volume → generata) e il provider **solleva un'eccezione** se manca, invece di rigenerarla. +Dal 09/10/2026 l'entrypoint **si rifiuta di partire** se `APP_KEY` +nell'ambiente è diversa dalla copia in `storage/app/.app_key`: prima la +sovrascriveva, e un valore sbagliato in Dokploy distruggeva l'unica copia della +chiave giusta. Per un cambio voluto si aggiunge `APP_KEY_ROTATE=1` (la chiave +vecchia resta in `.app_key.prev-`) e poi la si toglie. + --- ## Bug corretti — i più significativi @@ -984,6 +992,14 @@ Contesto utile se ti imbatti in comportamenti che sembrano regressioni. | Eliminazione definitiva territorio | La riga spariva, mappa e miniatura restavano orfane sul volume per sempre | `Territori/TerritorioCestino` | | Virgolette doppie in `x-data` | Attributo HTML chiuso a metà: il JavaScript finiva a video come testo | `backup-manager.blade.php` | | Warning di `addFile()` al posto del messaggio | Su file illeggibile l'amministratore leggeva `ZipArchive::addFile(): No such file or directory` invece della frase che dice quale file e cosa controllare | `Services/BackupArchive` | +| Date esportate in UTC | Ogni backup → ripristino (e export → import XML) spostava **tutte** le date indietro di un giorno, sommandosi a ogni giro. I file vecchi vengono riportati al giorno giusto all'import | `XmlExchange::righeEsportate()`, `normalizeDateForDb()` | +| Import di qualsiasi XML | Un XML ben formato ma estraneo svuotava il database con "completato con successo". Ora servono la radice `` e le sezioni principali | `XmlExchange::importXmlFromContent()` | +| Caratteri di controllo nell'export | Uno solo, in una nota, rendeva il file (e il `data.xml` del backup) non reimportabile | `XmlExchange::addXmlText()` | +| Territori senza zona scartati all'import | Sparivano con tutto il loro storico di assegnazioni | `XmlExchange::importXmlFromContent()` | +| Import che estromette chi lo esegue | Password vecchia e ruoli del file applicati anche all'amministratore che importava | `XmlExchange::importXmlFromContent()` | +| `whereNull` fuori da `latestOfMany` | Con una chiusa più recente dell'aperta il territorio risultava libero mentre era fuori, e assegnabile due volte | `Territorio::assegnazioneCorrente()` | +| Periodi di assegnazione accavallati | Uscite retrodatate a prima dell'ultimo rientro, doppie aperte dal Registro, anno scelto a mano incoerente con la data | `Assegnazione::sovrappostaA()`, `Assegna`, `Registro` | +| Declassamento dell'ultimo amministratore | Togliendosi il ruolo l'installazione restava senza nessuno che potesse gestirla | `Settings/UsersIndex` | | `GREATEST()` in migrazione | Sintassi MySQL: lo schema non si costruiva su SQLite, quindi la suite di test non poteva partire | migrazione `2026_04_08_123500` | | `@disabled` dentro `` | Direttiva non compilata: il tag restava letterale e la pagina moriva con «unexpected token endif» | viste di `settings` | | Segno del download consumato dal componente sbagliato | L'archivio pronto non sarebbe arrivato a nessuno — e per l'azzeramento è il passo che sblocca la cancellazione | `SegueOperazioneBackup::scaricaOra()` | @@ -1031,7 +1047,7 @@ docker compose --profile test run --build --rm test |------|---------------| | `tests/Unit/BackupArchiveTest.php` | Round-trip crea → riestrai, manifest, avanzamento fino al totale, fallimento su file illeggibile con archivio rimosso, rifiuto di manifest mancante/illeggibile/di formato futuro e di `data.xml` mancante o vuoto, zip slip (percorso assoluto, `..`, unità Windows) e normalizzazione dei percorsi validi, staging ripulito dopo un fallimento, sostituzione totale del disco al ripristino, svuotamento con conteggio | | `tests/Unit/BackupOperationStateTest.php` | Il posto unico: occupazione, rilascio a completamento e a fallimento, puntatore orfano, operazione già chiusa, chiusura tardiva che non deve liberare il posto della nuova, attesa-download che lo tiene occupato, azzeramento della barra al cambio fase, coda dei log limitata | -| `tests/Feature/RegistroS13Test.php` | Il ciclo da quattro gruppi del modulo S-13 (1/3/4/5/8/9 assegnazioni), la colonna "Restituito l'ultima volta il" compreso il caso in cui l'ultima del giro precedente è ancora aperta, esclusione di territori disattivati e cestinati, filtro sull'anno di servizio, ordinamento naturale dei numeri | +| `tests/Feature/RegistroS13Test.php` | Il ciclo da quattro gruppi del modulo S-13 (1/3/4/5/8/9 assegnazioni), la colonna "Restituito l'ultima volta il" (fine ciclo e ultimo rientro prima dell'anno), assegnazioni a cavallo d'anno su entrambi i fogli, una riga per ogni territorio attivo anche senza assegnazioni, esclusione di territori disattivati e cestinati, ordinamento naturale dei numeri, fogli da 20 righe e generazione del PDF | | `tests/Feature/ZonaPericolosaTest.php` | I due tempi dell'azzeramento: frase di conferma, backup preventivo senza cancellazione, rifiuto se il posto è occupato, conferma senza archivio servito, senza password, con password sbagliata, blocco dopo cinque tentativi, annullamento che libera il posto lasciando il backup valido. In più: che ogni operazione abbia un solo componente a raccontarla, e che il segno del download non venga consumato da chi non la possiede | | `tests/Feature/PaginaBackupImportTest.php` | La pagina servita per intero: permessi, le cinque zone con la loro ancora **e nel loro ordine**, la console unica, l'assenza dei vecchi riquadri di log, che nessun tag `territorio_id); - // Check territory is available (not currently assigned) + // Gli errori vanno sul campo e non in sessione: il messaggio flash + // sta nel telaio della pagina, che Livewire non ridisegna, e il + // rifiuto passerebbe inosservato. if ($territorio->assegnazioneCorrente) { - session()->flash('error', "Il territorio {$territorio->numero} è già assegnato a {$territorio->assegnazioneCorrente->proclamatore->nome_completo}."); + $this->addError('territorio_id', "Il territorio {$territorio->numero} è già assegnato a {$territorio->assegnazioneCorrente->proclamatore->nome_completo}."); return; } - // Check territory is active if (!$territorio->attivo) { - session()->flash('error', "Il territorio {$territorio->numero} è inattivo."); + $this->addError('territorio_id', "Il territorio {$territorio->numero} è inattivo."); return; } $proclamatore = Proclamatore::findOrFail($this->proclamatore_id); if (!$proclamatore->attivo) { - session()->flash('error', "Il proclamatore {$proclamatore->nome_completo} è inattivo."); + $this->addError('proclamatore_id', "Il proclamatore {$proclamatore->nome_completo} è inattivo."); return; } $assignedDate = \Carbon\Carbon::parse($this->assigned_at); $annoTeocratico = AnnoTeocratico::perData($assignedDate); - $assegnazione = Assegnazione::create([ - 'territorio_id' => $this->territorio_id, - 'proclamatore_id' => $this->proclamatore_id, - 'anno_teocratico_id' => $annoTeocratico->id, - 'assigned_at' => $assignedDate, - 'created_by' => auth()->id(), - ]); + // Controllo e inserimento sotto lo stesso lock sulla riga del + // territorio: un doppio clic o due schede aperte non possono più + // superare entrambi il controllo e creare due assegnazioni aperte. + $esito = DB::transaction(function () use ($territorio, $assignedDate, $annoTeocratico) { + Territorio::whereKey($territorio->id)->lockForUpdate()->first(); + + $conflitto = Assegnazione::sovrappostaA($territorio->id, $assignedDate); + if ($conflitto) { + return "Il territorio {$territorio->numero} risulta assegnato {$conflitto->descrizionePeriodo()}: " + . ($conflitto->returned_at + ? 'la data di assegnazione non può essere precedente al suo rientro.' + : 'registrane prima il rientro.'); + } + + return Assegnazione::create([ + 'territorio_id' => $this->territorio_id, + 'proclamatore_id' => $this->proclamatore_id, + 'anno_teocratico_id' => $annoTeocratico->id, + 'assigned_at' => $assignedDate, + 'created_by' => auth()->id(), + ]); + }); + + if (is_string($esito)) { + $this->addError('assigned_at', $esito); + + return; + } + + $assegnazione = $esito; activity()->causedBy(auth()->user()) ->performedOn($assegnazione) diff --git a/app/Livewire/Registro.php b/app/Livewire/Registro.php index 304bfda..8be842d 100644 --- a/app/Livewire/Registro.php +++ b/app/Livewire/Registro.php @@ -11,18 +11,14 @@ use App\Models\Proclamatore; use App\Models\Territorio; use App\Models\Zona; use App\Models\Tipologia; -use Barryvdh\DomPDF\Facade\Pdf; +use App\Services\Registro\ModuloS13; +use Carbon\Carbon; +use Illuminate\Support\Facades\DB; 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 = ''; @@ -37,7 +33,6 @@ class Registro extends Component public string $form_territorio_id = ''; public string $form_proclamatore_id = ''; - public string $form_anno_id = ''; public string $form_assigned_at = ''; public string $form_returned_at = ''; public bool $form_counted_in_campaign = false; @@ -101,7 +96,6 @@ class Registro extends Component $this->editingId = null; $this->form_territorio_id = ''; $this->form_proclamatore_id = ''; - $this->form_anno_id = ''; $this->form_assigned_at = now()->format('Y-m-d'); $this->form_returned_at = ''; $this->form_counted_in_campaign = false; @@ -118,7 +112,6 @@ class Registro extends Component $this->editingId = $id; $this->form_territorio_id = (string) $a->territorio_id; $this->form_proclamatore_id = (string) $a->proclamatore_id; - $this->form_anno_id = (string) $a->anno_teocratico_id; $this->form_assigned_at = $a->assigned_at?->format('Y-m-d') ?? ''; $this->form_returned_at = $a->returned_at?->format('Y-m-d') ?? ''; $this->form_counted_in_campaign = (bool) $a->counted_in_campaign; @@ -134,28 +127,54 @@ class Registro extends Component $this->validate([ 'form_territorio_id' => 'required|exists:territori,id', 'form_proclamatore_id' => 'required|exists:proclamatori,id', - 'form_anno_id' => 'required|exists:anni_teocratici,id', - 'form_assigned_at' => 'required|date', - 'form_returned_at' => 'nullable|date|after_or_equal:form_assigned_at', + 'form_assigned_at' => 'required|date|before_or_equal:today', + 'form_returned_at' => 'nullable|date|after_or_equal:form_assigned_at|before_or_equal:today', 'form_campaign_id' => 'nullable|exists:campagne,id', + ], [ + 'form_assigned_at.before_or_equal' => 'La data di assegnazione non può essere futura.', + 'form_returned_at.before_or_equal' => 'La data di rientro non può essere futura.', + 'form_returned_at.after_or_equal' => 'Il rientro non può precedere l\'assegnazione.', ]); + $uscita = Carbon::parse($this->form_assigned_at); + $rientro = $this->form_returned_at ? Carbon::parse($this->form_returned_at) : null; + + // L'anno di servizio non si sceglie: è quello della data di uscita. + // Scelto a mano poteva restare incoerente con la data, e l'assegnazione + // finiva contata nell'anno sbagliato da Home, filtri e modulo S-13. $data = [ 'territorio_id' => $this->form_territorio_id, 'proclamatore_id' => $this->form_proclamatore_id, - 'anno_teocratico_id' => $this->form_anno_id, - 'assigned_at' => $this->form_assigned_at, - 'returned_at' => $this->form_returned_at ?: null, - 'counted_in_campaign' => $this->form_counted_in_campaign, + 'anno_teocratico_id' => AnnoTeocratico::perData($uscita)->id, + 'assigned_at' => $uscita, + 'returned_at' => $rientro, + // "Contata nella campagna" senza campagna non ha significato. + 'counted_in_campaign' => $this->form_counted_in_campaign && $this->form_campaign_id !== '', 'campaign_id' => $this->form_campaign_id ?: null, 'note' => $this->form_note ?: null, ]; - if ($this->editingId) { - Assegnazione::findOrFail($this->editingId)->update($data); - } else { - $data['created_by'] = auth()->id(); - Assegnazione::create($data); + $conflitto = DB::transaction(function () use ($data, $uscita, $rientro) { + Territorio::whereKey($data['territorio_id'])->lockForUpdate()->first(); + + $conflitto = Assegnazione::sovrappostaA((int) $data['territorio_id'], $uscita, $rientro, $this->editingId); + if ($conflitto) { + return $conflitto; + } + + if ($this->editingId) { + Assegnazione::findOrFail($this->editingId)->update($data); + } else { + Assegnazione::create($data + ['created_by' => auth()->id()]); + } + + return null; + }); + + if ($conflitto) { + $this->addError('form_assigned_at', "Il periodo si sovrappone all'assegnazione {$conflitto->descrizionePeriodo()}."); + + return; } $this->showModal = false; @@ -199,16 +218,15 @@ class Registro extends Component } /** - * Modulo ufficiale S-13-I ("Registrazione delle assegnazioni del - * territorio") compilato con le assegnazioni dell'anno di servizio scelto - * nella modale. + * Modulo ufficiale S-13-I compilato per l'anno di servizio scelto nella + * modale; le regole di compilazione stanno in ModuloS13. * * 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() + public function downloadS13(ModuloS13 $modulo) { abort_if(!auth()->user()->can('settings.manage'), 403); @@ -224,134 +242,15 @@ class Registro extends Component $this->showS13Modal = false; - $pdf = Pdf::loadView('pdf.registro-s13', [ - 'annoLabel' => $anno->label, - 'pagine' => $this->impaginaS13($this->righeS13($anno)), - ])->setPaper('a4', 'portrait'); + $pdf = $modulo->genera($anno); return response()->streamDownload( - fn () => print($pdf->output()), - 'S-13-registro-assegnazioni-' . $anno->label . '.pdf' + fn () => print($pdf), + 'S-13-registro-assegnazioni-' . $anno->label . '.pdf', + ['Content-Type' => 'application/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' => '', - 'riporto' => '', - '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'); - - return $assegnazioni - ->map(function ($gruppo) { - $gruppo = $gruppo->values(); - - // Il ciclo di registrazione è lungo quanto i gruppi "Assegnato - // a": riempito il quarto si ricomincia da capo, come sul modulo - // cartaceo si aprirebbe una riga nuova per quel territorio. - // Cinque assegnazioni non mostrano quindi le ultime quattro, ma - // solo la quinta, in prima colonna. - $inizioCiclo = intdiv($gruppo->count() - 1, self::S13_ASSEGNAZIONI_PER_RIGA) - * self::S13_ASSEGNAZIONI_PER_RIGA; - - $celle = $gruppo - ->slice($inizioCiclo, self::S13_ASSEGNAZIONI_PER_RIGA) - ->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(); - - return [ - 'numero' => $gruppo->first()->territorio?->numero ?? '', - 'riporto' => $this->riportoS13($gruppo, $inizioCiclo), - '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": la data di - * rientro dell'ultima assegnazione *prima* che il ciclo ricominciasse, - * cioè quella che occupava il quarto gruppo del giro precedente. - * - * È il riporto che il modulo cartaceo chiede quando si apre una riga nuova - * per un territorio già registrato: la riga mostra da dove si riparte, - * senza il nome di chi lo aveva (la colonna ha solo la data). - * - * Finché il primo ciclo non è esaurito — quattro assegnazioni o meno - * nell'anno — non c'è niente da riportare e la colonna resta vuota. - * - * @param \Illuminate\Support\Collection $gruppo - */ - private function riportoS13(\Illuminate\Support\Collection $gruppo, int $inizioCiclo): string - { - if ($inizioCiclo === 0) { - return ''; - } - - return $gruppo->get($inizioCiclo - 1)?->returned_at?->format('d/m/Y') ?? ''; - } - - /** - * 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/app/Livewire/Settings/UsersIndex.php b/app/Livewire/Settings/UsersIndex.php index 16f5e0b..dd44eac 100644 --- a/app/Livewire/Settings/UsersIndex.php +++ b/app/Livewire/Settings/UsersIndex.php @@ -6,6 +6,7 @@ use App\Models\Assegnazione; use App\Models\User; use Illuminate\Support\Facades\DB; use Illuminate\Validation\Rule; +use Livewire\Attributes\Locked; use Livewire\Component; use Spatie\Activitylog\Models\Activity; use Spatie\Permission\Models\Role; @@ -17,6 +18,9 @@ class UsersIndex extends Component public string $password = ''; public string $password_confirmation = ''; public string $selectedRole = ''; + // Locked: è la lista che le regole di validazione usano per accettare un + // ruolo; modificabile dal client, ammetterebbe qualsiasi nome. + #[Locked] public array $availableRoles = []; public ?int $editingUserId = null; @@ -115,6 +119,23 @@ class UsersIndex extends Component $validated = $this->validate($this->editRules()); $user = User::query()->findOrFail($this->editingUserId); + // Togliere il ruolo di amministratore a sé stessi, o all'ultimo + // rimasto, lascia l'installazione senza nessuno che possa gestire + // utenti, impostazioni e backup. + if ($user->hasRole('amministratore') && $validated['editSelectedRole'] !== 'amministratore') { + if ($user->id === auth()->id()) { + $this->addError('editSelectedRole', 'Non puoi togliere a te stesso il ruolo di amministratore: chiedilo a un altro amministratore.'); + + return; + } + + if (User::role('amministratore')->count() <= 1) { + $this->addError('editSelectedRole', "È l'ultimo amministratore: assegna prima il ruolo a un altro utente."); + + return; + } + } + $user->name = $validated['editName']; $user->email = $validated['editEmail']; diff --git a/app/Livewire/Settings/XmlExchange.php b/app/Livewire/Settings/XmlExchange.php index d666fb6..ed565bb 100644 --- a/app/Livewire/Settings/XmlExchange.php +++ b/app/Livewire/Settings/XmlExchange.php @@ -241,11 +241,33 @@ class XmlExchange extends Component $this->importStats = []; $this->importIssues = []; - $xml = @simplexml_load_string($content); - if (! $xml) { - return ['error' => 'Impossibile leggere il file XML.']; + $xml = $this->leggiXml($content); + if (is_string($xml)) { + return ['error' => $xml]; } + // L'import parte svuotando le tabelle: un XML ben formato ma estraneo + // (un sitemap, l'export di un altro programma) lascerebbe + // l'installazione vuota con un "completato con successo". Si accetta + // solo un file prodotto da questa applicazione, con le sue sezioni. + if ($xml->getName() !== 'termanager2') { + return ['error' => 'Il file non è un export di TerManager2: l\'elemento principale è <' . $xml->getName() . '>, atteso . Nessun dato è stato modificato.']; + } + + $mancanti = array_values(array_filter( + ['territori', 'proclamatori', 'assegnazioni'], + fn (string $sezione) => ! isset($xml->{$sezione}) + )); + if ($mancanti !== []) { + return ['error' => 'Il file è incompleto: mancano le sezioni ' . implode(', ', $mancanti) . '. Nessun dato è stato modificato.']; + } + + // Chi esegue l'import (o il ripristino, che imposta l'utente a mano) + // non deve uscirne estromesso: il file può contenere il suo account + // con una password vecchia o senza il ruolo di amministratore. + $attore = auth()->user(); + $attoreEraAmministratore = $attore?->hasRole('amministratore') ?? false; + $actorId = auth()->id() ?? User::query()->value('id'); if (! $actorId) { return ['error' => 'Serve almeno un utente nel sistema per importare i dati.']; @@ -264,7 +286,8 @@ class XmlExchange extends Component 'assegnazioni_saltate' => 0, ]; - DB::transaction(function () use ($xml, $actorId, &$stats) { + try { + DB::transaction(function () use ($xml, $actorId, $attore, $attoreEraAmministratore, &$stats) { Assegnazione::query()->delete(); Campagna::query()->delete(); Territorio::query()->withTrashed()->forceDelete(); @@ -334,6 +357,10 @@ class XmlExchange extends Component $roleNames[] = $roleName; } } + $eAttore = $attore !== null && $user->id === $attore->id; + if ($eAttore && $attoreEraAmministratore && ! in_array('amministratore', $roleNames, true)) { + $roleNames[] = 'amministratore'; + } $user->syncRoles($roleNames); // Password hash, remember_token, verification flag and @@ -341,13 +368,17 @@ class XmlExchange extends Component // hash is preserved exactly (the 'hashed' cast would re-validate // the bcrypt cost and could reject a hash made with a different // BCRYPT_ROUNDS), and Eloquent does not stamp its own timestamps. + // Per chi sta importando password e token restano i suoi: + // quelli del file possono essere di mesi fa. $meta = []; $passwordHash = (string) ($userNode->password ?? ''); - if ($passwordHash !== '') { + if ($passwordHash !== '' && ! $eAttore) { $meta['password'] = $passwordHash; } - $rememberToken = (string) ($userNode->remember_token ?? ''); - $meta['remember_token'] = $rememberToken !== '' ? $rememberToken : null; + if (! $eAttore) { + $rememberToken = (string) ($userNode->remember_token ?? ''); + $meta['remember_token'] = $rememberToken !== '' ? $rememberToken : null; + } $meta['email_verified_at'] = $this->normalizeDateTimeForDb((string) ($userNode->email_verified_at ?? '')); $createdAt = $this->normalizeDateTimeForDb((string) ($userNode->created_at ?? '')); @@ -415,8 +446,8 @@ class XmlExchange extends Component $label = (string) $annoNode->label; $anno = AnnoTeocratico::create([ 'label' => $label, - 'start_date' => (string) $annoNode->start_date, - 'end_date' => (string) $annoNode->end_date, + 'start_date' => $this->normalizeDateForDb((string) $annoNode->start_date, true), + 'end_date' => $this->normalizeDateForDb((string) $annoNode->end_date, true), ]); $this->applyRowMeta('anni_teocratici', $anno->id, $annoNode); $stats['anni_importati']++; @@ -430,8 +461,8 @@ class XmlExchange extends Component foreach (($xml->campagne->campagna ?? []) as $campagnaNode) { $campagna = Campagna::create([ 'descrizione' => (string) $campagnaNode->descrizione, - 'start_date' => (string) $campagnaNode->start_date, - 'end_date' => (string) $campagnaNode->end_date, + 'start_date' => $this->normalizeDateForDb((string) $campagnaNode->start_date, true), + 'end_date' => $this->normalizeDateForDb((string) $campagnaNode->end_date, true), ]); $this->applyRowMeta('campagne', $campagna->id, $campagnaNode); $stats['campagne_importate']++; @@ -455,9 +486,14 @@ class XmlExchange extends Component continue; } - if (!isset($zoneMap[$legacyZonaId], $tipologiaMap[$legacyTipologiaId])) { - $this->pushImportIssue('territorio', $legacyId, 'Riferimento zona/tipologia non trovato (zona=' . $legacyZonaId . ', tipologia=' . $legacyTipologiaId . ')'); - continue; + // Zona e tipologia sono facoltative (le colonne sono nullable e + // un territorio resta senza quando la sua zona viene + // eliminata): un riferimento vuoto o sconosciuto non è motivo + // per scartare il territorio, che si porterebbe dietro tutto + // il suo storico di assegnazioni. + if (($legacyZonaId !== '' && ! isset($zoneMap[$legacyZonaId])) + || ($legacyTipologiaId !== '' && ! isset($tipologiaMap[$legacyTipologiaId]))) { + $this->pushImportIssue('territorio', $legacyId, 'Zona o tipologia non trovata (zona=' . $legacyZonaId . ', tipologia=' . $legacyTipologiaId . '): territorio importato senza'); } $pdfPath = (string) ($territorioNode->pdf_path ?? ''); @@ -465,8 +501,8 @@ class XmlExchange extends Component $territorio = Territorio::create([ 'numero' => $numero, - 'zona_id' => $zoneMap[$legacyZonaId], - 'tipologia_id' => $tipologiaMap[$legacyTipologiaId], + 'zona_id' => $zoneMap[$legacyZonaId] ?? null, + 'tipologia_id' => $tipologiaMap[$legacyTipologiaId] ?? null, 'confini' => (string) ($territorioNode->confini ?? ''), 'note' => (string) ($territorioNode->note ?? ''), 'attivo' => ((int) ($territorioNode->attivo ?? 1)) === 1, @@ -540,7 +576,15 @@ class XmlExchange extends Component $this->applyRowMeta('assegnazioni', $assegnazione->id, $assegnazioneNode); $stats['assegnazioni_importate']++; } - }); + }); + } catch (\Illuminate\Database\QueryException $e) { + // La transazione è già stata annullata: il database è com'era. + // Senza questo l'amministratore vedrebbe un "Server Error" senza + // sapere quale dato del file ha violato un vincolo. + report($e); + + return ['error' => 'Import annullato, nessun dato è stato modificato: il file contiene un dato che il database rifiuta (' . Str::limit($e->getPrevious()?->getMessage() ?? $e->getMessage(), 240) . ').']; + } return [ 'stats' => $stats, @@ -581,16 +625,75 @@ class XmlExchange extends Component 'created_at' => optional($u->created_at)->toIso8601String() ?? '', 'updated_at' => optional($u->updated_at)->toIso8601String() ?? '', ])->all(), - 'zones' => Zona::query()->orderBy('id')->get(['id', 'nome', 'attivo', 'created_at', 'updated_at'])->toArray(), - 'tipologie' => Tipologia::query()->orderBy('id')->get(['id', 'nome', 'attivo', 'created_at', 'updated_at'])->toArray(), - 'proclamatori' => Proclamatore::query()->withTrashed()->orderBy('id')->get(['id', 'nome', 'cognome', 'attivo', 'deleted_at', 'created_at', 'updated_at'])->toArray(), - 'territori' => Territorio::query()->withTrashed()->orderBy('id')->get(['id', 'numero', 'zona_id', 'tipologia_id', 'confini', 'note', 'attivo', 'prioritario', 'pdf_path', 'thumbnail_path', 'deleted_at', 'created_at', 'updated_at'])->toArray(), - 'anni_teocratici' => AnnoTeocratico::query()->orderBy('id')->get(['id', 'label', 'start_date', 'end_date', 'created_at', 'updated_at'])->toArray(), - 'campagne' => Campagna::query()->orderBy('id')->get(['id', 'descrizione', 'start_date', 'end_date', 'created_at', 'updated_at'])->toArray(), - 'assegnazioni' => Assegnazione::query()->orderBy('id')->get(['id', 'territorio_id', 'proclamatore_id', 'anno_teocratico_id', 'campaign_id', 'assigned_at', 'returned_at', 'counted_in_campaign', 'note', 'created_by', 'returned_by', 'link_sent', 'pdf_access_code', 'created_at', 'updated_at'])->toArray(), + 'zones' => $this->righeEsportate(Zona::query()->orderBy('id'), ['id', 'nome', 'attivo', 'created_at', 'updated_at']), + 'tipologie' => $this->righeEsportate(Tipologia::query()->orderBy('id'), ['id', 'nome', 'attivo', 'created_at', 'updated_at']), + 'proclamatori' => $this->righeEsportate(Proclamatore::query()->withTrashed()->orderBy('id'), ['id', 'nome', 'cognome', 'attivo', 'deleted_at', 'created_at', 'updated_at']), + 'territori' => $this->righeEsportate(Territorio::query()->withTrashed()->orderBy('id'), ['id', 'numero', 'zona_id', 'tipologia_id', 'confini', 'note', 'attivo', 'prioritario', 'pdf_path', 'thumbnail_path', 'deleted_at', 'created_at', 'updated_at']), + 'anni_teocratici' => $this->righeEsportate(AnnoTeocratico::query()->orderBy('id'), ['id', 'label', 'start_date', 'end_date', 'created_at', 'updated_at']), + 'campagne' => $this->righeEsportate(Campagna::query()->orderBy('id'), ['id', 'descrizione', 'start_date', 'end_date', 'created_at', 'updated_at']), + 'assegnazioni' => $this->righeEsportate(Assegnazione::query()->orderBy('id'), ['id', 'territorio_id', 'proclamatore_id', 'anno_teocratico_id', 'campaign_id', 'assigned_at', 'returned_at', 'counted_in_campaign', 'note', 'created_by', 'returned_by', 'link_sent', 'pdf_access_code', 'created_at', 'updated_at']), ]; } + /** + * Righe da esportare, con le date scritte in modo che il reimport le + * ritrovi identiche. + * + * toArray() serializza ogni data in UTC: con l'applicazione su + * Europe/Rome il 15/03 a mezzanotte diventava "2024-03-14T23:00:00Z" e + * al reimport tornava 14/03, un giorno perso a ogni backup e ripristino. + * Le colonne con cast date escono come Y-m-d, che non ha fuso; i + * timestamp come ISO 8601 con lo scostamento esplicito. + * + * @param array $colonne + * @return array> + */ + private function righeEsportate(\Illuminate\Database\Eloquent\Builder $query, array $colonne): array + { + return $query->get($colonne)->map(function (\Illuminate\Database\Eloquent\Model $riga) use ($colonne) { + $dati = []; + foreach ($colonne as $colonna) { + $valore = $riga->getAttribute($colonna); + + if ($valore instanceof \Carbon\CarbonInterface) { + $valore = $riga->hasCast($colonna, ['date', 'immutable_date']) + ? $valore->format('Y-m-d') + : $valore->toIso8601String(); + } + + $dati[$colonna] = $valore; + } + + return $dati; + })->all(); + } + + /** + * Il file come SimpleXMLElement, oppure il messaggio d'errore con riga e + * motivo: "impossibile leggere il file" da solo non aiuta a correggerlo. + */ + private function leggiXml(string $content): \SimpleXMLElement|string + { + $precedente = libxml_use_internal_errors(true); + libxml_clear_errors(); + + try { + $xml = simplexml_load_string($content); + if ($xml !== false) { + return $xml; + } + + $errore = libxml_get_errors()[0] ?? null; + + return $errore + ? sprintf('Impossibile leggere il file XML (riga %d): %s.', $errore->line, trim($errore->message)) + : 'Impossibile leggere il file XML.'; + } finally { + libxml_clear_errors(); + libxml_use_internal_errors($precedente); + } + } + private function legacySqlToDataset(string $sql): array { $rows = $this->extractInsertRows($sql); @@ -880,8 +983,12 @@ class XmlExchange extends Component return $nullable ? null : null; } + // Riportata nel fuso dell'applicazione prima di tenere solo il giorno: + // i backup fatti prima della correzione dell'export hanno le date in + // UTC ("2024-03-14T23:00:00Z" per il 15/03), e così tornano giuste. + // Una data senza orario e senza fuso resta com'è. try { - return Carbon::parse($raw)->toDateString(); + return Carbon::parse($raw)->setTimezone(config('app.timezone'))->toDateString(); } catch (\Throwable $e) { return $nullable ? null : null; } @@ -1022,6 +1129,13 @@ class XmlExchange extends Component private function addXmlText(\SimpleXMLElement $parent, string $name, string $value): \SimpleXMLElement { + // XML 1.0 non ammette i caratteri di controllo (tranne tab e a capo) + // né l'UTF-8 non valido: uno solo, magari incollato da Word in una + // nota, rende l'intero file illeggibile al reimport — e quel file è + // anche il data.xml dei backup. + $value = mb_scrub($value, 'UTF-8'); + $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value) ?? ''; + $child = $parent->addChild($name); $dom = dom_import_simplexml($child); $dom->textContent = $value; @@ -1062,7 +1176,7 @@ class XmlExchange extends Component } try { - return Carbon::parse($raw)->toDateTimeString(); + return Carbon::parse($raw)->setTimezone(config('app.timezone'))->toDateTimeString(); } catch (\Throwable $e) { return null; } diff --git a/app/Models/Assegnazione.php b/app/Models/Assegnazione.php index 01e99d6..d1b1b81 100644 --- a/app/Models/Assegnazione.php +++ b/app/Models/Assegnazione.php @@ -55,6 +55,44 @@ class Assegnazione extends Model }); } + /** + * L'assegnazione dello stesso territorio il cui periodo si sovrappone a + * quello indicato, oppure null. Un'assegnazione aperta dura fino a oggi e + * oltre; rientro e nuova uscita nello stesso giorno non si sovrappongono. + * + * Copre in un colpo solo la doppia assegnazione aperta, l'uscita + * retrodatata a prima dell'ultimo rientro e le correzioni a mano nel + * Registro che accavallano due periodi. Il confronto è fatto in PHP sulle + * poche assegnazioni del territorio: in SQL dipenderebbe da come il + * driver memorizza le DATE (MariaDB "Y-m-d", SQLite "Y-m-d H:i:s"). + */ + public static function sovrappostaA( + int $territorioId, + Carbon $uscita, + ?Carbon $rientro = null, + ?int $esclusaId = null + ): ?self { + $uscita = $uscita->copy()->startOfDay(); + $rientro = $rientro?->copy()->startOfDay(); + + return static::with('proclamatore') + ->where('territorio_id', $territorioId) + ->when($esclusaId, fn ($q) => $q->whereKeyNot($esclusaId)) + ->orderBy('assigned_at') + ->get() + ->first(fn (self $altra) => ($rientro === null || $altra->assigned_at->lt($rientro)) + && ($altra->returned_at === null || $uscita->lt($altra->returned_at))); + } + + /** Descrizione del periodo per i messaggi d'errore: "a Rossi Mario dal 01/09/2025 al 15/10/2025". */ + public function descrizionePeriodo(): string + { + $chi = $this->proclamatore?->nome_completo ?? 'un proclamatore'; + $al = $this->returned_at ? ' al ' . $this->returned_at->format('d/m/Y') : ', non ancora rientrato'; + + return "a {$chi} dal {$this->assigned_at->format('d/m/Y')}{$al}"; + } + // ─── Relationships ───────────────────────────────────────── public function territorio() diff --git a/app/Models/Territorio.php b/app/Models/Territorio.php index e49ea6a..c627123 100644 --- a/app/Models/Territorio.php +++ b/app/Models/Territorio.php @@ -61,9 +61,13 @@ class Territorio extends Model public function assegnazioneCorrente() { + // Il filtro sulle aperte va nella closure di ofMany: scritto fuori con + // whereNull() vale solo per la query esterna, mentre il MAX(assigned_at) + // del sottoquery si calcola su tutte le assegnazioni. Bastava una + // chiusa più recente dell'aperta per far risultare il territorio + // libero mentre era fuori, e quindi assegnabile una seconda volta. return $this->hasOne(Assegnazione::class, 'territorio_id') - ->whereNull('returned_at') - ->latestOfMany('assigned_at'); + ->ofMany(['assigned_at' => 'max', 'id' => 'max'], fn ($q) => $q->whereNull('returned_at')); } public function ultimaAssegnazione() diff --git a/app/Services/Registro/ModuloS13.php b/app/Services/Registro/ModuloS13.php new file mode 100644 index 0000000..e2dca25 --- /dev/null +++ b/app/Services/Registro/ModuloS13.php @@ -0,0 +1,329 @@ +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; + } +} diff --git a/docker/php/entrypoint.sh b/docker/php/entrypoint.sh index 819f3d1..f24a64c 100755 --- a/docker/php/entrypoint.sh +++ b/docker/php/entrypoint.sh @@ -78,6 +78,21 @@ ok "Storage directories ready." KEY_FILE="/var/www/html/storage/app/.app_key" if [ -n "${APP_KEY:-}" ]; then + # The persisted copy is the backup of the key the data was encrypted + # with. If the environment now carries a different one (a typo in + # Dokploy, a value pasted from another install), overwriting the file + # would destroy the only copy of the right key: refuse to start instead. + # A deliberate rotation keeps the old key aside and needs an explicit flag. + if [ -s "$KEY_FILE" ] && [ "$(cat "$KEY_FILE")" != "$APP_KEY" ]; then + if [ "${APP_KEY_ROTATE:-}" = "1" ]; then + BACKUP_FILE="${KEY_FILE}.prev-$(date +%Y%m%d-%H%M%S)" + cp "$KEY_FILE" "$BACKUP_FILE" + warn "APP_KEY_ROTATE=1: the previous key was saved to $BACKUP_FILE." + warn "Data encrypted with it stays unreadable until re-encrypted." + else + die "APP_KEY in the environment differs from the key persisted in $KEY_FILE. Starting would make the encrypted personal data unreadable. Restore the previous APP_KEY in the environment, or set APP_KEY_ROTATE=1 if the change is deliberate (the old key is kept aside)." + fi + fi ok "APP_KEY provided via environment." printf '%s' "$APP_KEY" > "$KEY_FILE" elif [ -s "$KEY_FILE" ]; then diff --git a/resources/pdf/S-13_I.pdf b/resources/pdf/S-13_I.pdf index ee10beca8c8927e7d614aa480314c289594d3ee3..4e33a915298a1fef6a18f2ede05ecbfb26b76a45 100644 GIT binary patch literal 130948 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%0AL-8^@4^jVt&i62o1B&07=Ozm z;lYd#KUBp;Q@Uk-c$lPdD+#-}K5n_6 zMf&-E3&!_-{i(@j#-is5CmpJc-yiKPCV7UhEq=ljzn=WO^u)cFH}}>aev>!rQAYbs zd+VMAcj)B&VN{waEyr`+Gps987azOC`uM!qON_ry5st^s|o25gf&4|-0# z_VR#b^V>^X&%1s$+&K4H@S~C?H!q}YAAaEHD?e4HIVVINns}+a_-bY3(bD!&V{5F8 z?Y(Zq#0UNxe#X9CwpEiyX4vMBe@<}CD42A>Zm!Aw?TtfAV@FN7(Cume%8L3pTf=t` z|5~4SMrYH^a9`~SS?_f~yx79NS8HtS>ivA?dX{Z*)U81H|6qq~3M&D$|) z)?s0zdfFc_E_&)4#BTTa;y#h<*8Elb&DSMnuHDz{{c`6-an)ci*YJh=?DLZU`BOW! zvS#*-Lm|1TYwIGc9mY&}wLZo=E9lYhMoezs1Ai^PosgKF%BKv>pWu~r-)PCwnXj|% z#T!0O8tCJmU!TDqd(e;A zw=vW|t3#5>Yi4w5WQ5+7XK#$Q`3%^bpJ~!x8gV=D(xS$me8QZ}+=}I0R;~S6Z)M26 zDv$Ggd4l#PGgSF=6e74u%5Oo;_OiTPR+01do9j0`@iATEI?|&XlR5MJ`?3%xIVj04_nZEAzAQ2}-r`o> zF!qM^`M))<=-YvUhb*#zyooSf$ zGVj5sR`0F!_0q4@txGza(N%ah(O|Y!=%NF6A73%4-7)h1=<#>+Ea$`xzyJA3`nL|N zjP#Q>mj`qnK5o5i^)GuS_uc1!NvAtkSZC{aT>mgEKzl`K-!ShXP6c7!J6}~lu77*` z@}VNF&dy%ddHT6S(}$G!F8;70=#MtjL*gt4`1?(|(&Sl}k>q^H#d%0yyYNZQIef#K z-$s0TeZ{DYc6p@sw3Kz31r-y6H`{+r6WlF)14>|2@qt&dm5FCh3QVJG=Oz1&D#>hjp*ot@h2Z%-%$iMQnSC>TgTE!MS~N%c7?IS|G28wZ9uIdTkj!dVqX! zXzcLamAex+ZjSO9m%pa`)pvGz#f}ZB6+?HVB#uupZ{oYpiI{CPb3wPXDE`5~bMfaK z9!)Nenb39k(}oPcaLa4Ut!8`0=r_m4*Zn>}vCHnSJ*xgN=f*sfqF+XJDOp;QAAIAQ z;jA!0cg^8wTZdG;1(u#EC#OY!dOQ5upE2PsSua-S9tjRh{`>b3Ki681f?fI#txcDD z_vq)6RzKA;FEH-cl7yGrPVCDx_%J``)P?J(9-Q-Ydt$b^GAtx-_SDyI^%09_{%BBE zeK`6<=Ya9jn_z>n&IJb!+6BL|thx{pb9>jBPu@FS_4GQ~JK1e?*xk$8>z`^YBXa{|M|XGK~4evBlGV3Rc(R{dF7BdOzM zWJW+6%fb7}9Mg(TR)3b&$ULL+%QCoDEF(zjwPG2WRr_FBjVPp5Vbyz((n5~!G{*FcaNj6+s14h3-& zH9W*2GAzU)vQS@Gk(D40k%RieiaZB#hyv6XRup82Lli}bLzJ-Qh%$q5$iM-ts7Qc^ zqyY|~45$Ae{(t$1l~@hLA+cCXBo1mjEAbq}AqlwOBteEaBvAxBB#Fm3WCrk%WUQT% zg0)!I2#{AY!vY>uvLOzcm0^F#9Pl(N^H@7&0c)o$;Qp0G%w1VR++?|xcUF)CWCi$~ zRWw-d6$WdL!s32YSj;tr!*y18tdoks0&a?kby5*=e<+k3>O56s9`Z#|pssP0-s*fN zo2z6AC6(5K9ddhB}XL!;$S#ljR(i@P)|uQz&;`y=u2I>{ZGN7qBmQdOgr3Tr2vIY>Ju}IG$Kk?StqI;^0Xr!gB)jU|cKuk9HC8J}n;)dBw{ttp|A}$;juD zf_yG1$mcTZ98PAC&t(?*TxOBaWe)kABnZSIw@iHgS5tGch=W&&dXSPCFX-oah7ioK^t%oY!EVM>gPdULyjZ(J=uWId4>d~ zDqo25fX`{H2Yk*mBpTqeM8IGpG6NeC1K6m@q7C?*XK3@obF`7bMoFo*(bz)0KX{s@ zK^!ate9p5h@Hx+t>H;5x9NJ)>Lkbx9oMZ&>IVlF}=fX`dTVSopbGQ`1&z~|H@1E2F8HQDNQ<`m#_p4u&lgCK%9 zcn0{KNk5=Y-JZ)cyLliMzMC5ax=5??xlm?Jjk_P#RmJ0Gp;y@qp5)En9dqxrw zUx|ia@Eq!AUdD3`YCo|4$)upw-zW3PM>6VN${^4OG|t9)rL=;t!XSSrGzCzI9lOGqIl3gf=D}E9WU`E#6hhP|bdry?oknvI2hj(_A&H>n1sdf79wh$&4@m~@ z7HISjaZtMeamWnT2s$zZztmpBJJVSf;6`Uz=!2@edQD{#VrWBrTbXZ=)Porcb>_H@9hdzi|)$d2+ z2#AC36rm5CH;FX9LLUn5b1H1$gT|fUgU*xy50MU2)$dH>afpN3c=SOBVena;D~lox z2H-hr+QFtE=G1C{4{A*z4v9g`B^rXNeMoc?3pO4xZ{_@m!a&SPHlYs^UEo7T%q5(8 z&`n9iT*mvDE!VHW2Wcr-D;dv;N&O+_bhHC;kd{Ips2%B+4DNH%Qh+&K69gZmrN9Si zDTqVC`ykY%Am$3*hZ9Llp%2>pYK&UB14K#+F;{R_Dw38$ALLl&gVGslyF^+_y;fv{ zeNJr!U{1%YfH|ol*yj>yDfEG7A`)pSbsS`aeNI~tFsIQWVBXU9s4=JGU%;Fc4PZ_- z*yj>yDTsr?vrmb%6vRPV3hv2k@ZlL*Rq76!@S4D#Sr02jZY@i9UG5oCZ$lgD$bC*QJ&F z*>ueV_c=A~7zbU_RDT~ej^KmJB*f9m{Tzw58~7kCg+6%1obF?v+QA2vKfr@Z1jIpF3gVDxu&iD`x+;m!(M!W~3d~ArLIG#Tl1zsQ>d(@(Ylwri z54;O$A9xq~@fnDXu5+Oen&N?95$>kYlpkh{WP>?1O(Sp(=(;@kAUy2N7?@Z%5^nrH>BpUmm54qE+oV-N%oPPNCpH4i$p)CWj2+c$ zCDUk59S5ZqU`{r`oF)o@Ih|Aj=5(D4Fee*ePCpI?m=p5=<|OX{^Oo@`#6cr-HRiNE z{y*j}8*Eptwcnttudh3Pnv;XGE`5jYT;1_=ot<5D>Aa`q@AFn|wQu?1Kzd?|?h<{G zvtvX%TRXbQDk-+St?b|+*wRfFWf>hq3HGdkz8#qz=pGue%`%4P?C9RYGJ05)?QB_P knZ3P%rLKWNr;RSQ8(jYLqlYvZq#wNMG;EmhER#p+mXE4#*yVkLcwK2@t)Mo0;@?&{!_GCG*JXvmbpcMwfBr4I+5D{d#*>k!>G(4eSoc@rt*gkHcP4Wh3 zdoObmMwGH7jW0)Jag_tYfR?;TC{7e08?wCEKAxaq&{-X}n>Xx>vA2Ir1-MoiLZzsZ|D*>*-OZ*wxxFWH#o?@iWq1qxg< z@=I&vm#(ES=qu2Oy|c|4wtw__QE}Xo1Y~ma2EFqlQAeuvWc#>-hB($(M|>a~k&QiV z+`QZgDeMBsT1I4TmanrN%W&NqP@l1ltrxgu#4aO}dXXsOl|ocJrW74#7jKp)S;qy4 zqs@}GW81UH8(D6S-cBS0RY90UIWKQdmW``OCVQ42igRo-n)AmzgtEqJDlV_R`qxns4bvyF?sF?p>UXxQ1!k!s1N_6B zqy$PwmuwLQUNT-X!u=*l_QyZTYxKzmY)@Ak7qXoV2^n9A@YMfnA+6OD3lT#B6aqdl z=8*rDozY2lR$9Y$vHu?!8=-*a49VQa#tF+K=s0V0CY|x(amr4RZ=xqnGWS^5;5MIZ za-6XLe`#}u%6OXtcOY6HwK?PeYIDW}o1;ulTO^c1p%AWRNx~;c$~9$AvAdItjfYSO=Z??B_^CoKW0vhJ%-m`R<4zvyCRy8zgy29EWN_6ezzYl# z0y89JWU?g6{T$?{7aQIU7*_f6veUlbdb%U!Pl|3%1NA*hAWe!fC2saq>6DZ_5*pgG?uBu^TI?XxuW_ZpJM6iP$;o50 zUaG`a%flXll4|$+H$I1K zXgwTZ9TgxSIk#rpeRUbdf4c&C7C7IUmVZ35_|w*X>eC#04p}eU`@VI%nAv3>>FASZ zuqzQJ5BPAK>CYSj1S&c$1k%i`x*Ct8%U|)=oSV`4uwOhxDrfkCo>;nyvg(pG|JGnP zUd`c~Ev3~nn-zW^xtwz7ZBhT6{Gxw1XlAdNh*SAKTFFQ0fT@87BuwCicw4@UU3zZKnvDtA5ch%>qoN?oEtmLc2mf}YX z^R^z8QMcHgnQYo*E+zU+l`rxqdVGeNb8z|A{wGZz4wOY||HI=%S-!Pg6?5jfblB&q zq-<=cqNAak7$5J(Lply0ROY)2Op9O(%wpwF`RCEXy#<1y%k8$A>o1<`zh%}oim-NJ z2KMRA{8t)wb=U0j@OdxTGO4Fl57P@wb_BSTfRzcZXU9Go^G|c!P6Y= zO(D;DTT#Aa$ZzHNX+QOOJ#{|+*w5qWpsy6nvt>{5g{}h)vEu8`G~5V^v+uM=LZ3~$ za`tfv?+*KWV#cK>k8R&;uw!25HjB9{zOUC_x8ul^e?DXxMU(eM2H=Qmfq{+20wtZ* zRp{JZlsD+joyIHd1&nm2%3xPkUQc5dOqF^an#O+^+33(A*>+jZjoh2Ddz-2_Mb0*%qrAe@^a}SVXpgb?R{|2 zU{}6g9nD!f{;^QWmV#rIl9#_6ShDtj_mjZvM+VNz7hgP5cfJJm5{j{iT|iRniODrb zM9-%L@6VBaIAeR$?lqAH^$vo;)9#@NE^>8) zsMbSrMN#TVvJDX2V zyHJs;wY4|3XzDbNqIRVZCpRy!nf7Du6YUFkN$m5=k+ZzA{yn({ofBo4>^0S34da4n z6@Kp1;mZrNZmvyzR{Pd;aaz`9BfUjo12GHBt&$&J#$t>OXG&dMxUx)I{e7qD`ONRT zZ+WztU!Y9;Dv`Lo@igVM;=wgpdPtW!&oQ|=cGY7c+Xd7kckH5ELJ!%*KN6HiTH-r* z#fDI18v6s~GSnr-o@mMZGI?)+?mr&>;^C*$?Rb@u#Zmf{&VtyhM~{Suuv%xK`k$5^ z313?NTBrA6e87tU0h6e3rP+r}KYg5i-t@GPxv^sRkq7njZI_%l^6K%Poom`Zth|QAbh^Ec*H|_Isvhm}#H#@$bpCQ-3b^!hJ1LDR+_CkqPrOZQ{fn1)~dG z^y+=90|YD`SUV2p2z_zPd!Q=FxVI@Jo_lww5y1{Nm`Os~b$mhxO}Bc;8+#Ow?V6yE$YP@UCrceA1qOFc)%P=qqL)4L zE81_D%c!lyGi~bIeV^>`-&NAy^Y|BX-e~{Dd;1D!_5BR^wKUc^TdVukCBq03x$fI@ z-dV3&Kkhkv7EjsTG&jp_-N%Zl3;}OrO3spjr9Gj3Q`}!XxY%)R*E?nvyYRD~redTD zV-u6FGR39!sd`Aw#dBtFf+Awg?yQ(Qt@&cu?2@=WPAf|CaPjkHXv@hvmH~LdS<_Ff zmS3tzXCA$upUleF%V*u&b-C=H%(<%8Qx|>q?Jy14^|0M0`{<%OzU+Ht3%)kq7xoi+ zz0|J!$Ac&LEw0r*FD%t#7hDsrc-?nINcgIEeD9s9SETa49{cpOj>Zmiu3J{f*Vc9~ z_X>ZOOm~)y+&uHPVBEFzp6j`FqQ`{31(n44WgiLJ(Wd>&x8)1pqb0j7YLU*`U$Z_w zQD>{Q)G5AC5NK|%J#w(~Vv?7YI0v&aXTLM}iwdY}OzkHAG zi7Mga?Y`&NH1D40xfMJ!Mdt21yUNx*+xT>AqH;y-ue>wa$I83&w&Lh(wdoD&m@RXD zoV=gFJ*Cw0w$+-g>C$Q>uZ4rw~;sP|beoQX! zzF}9PE|=q2I12%`CWkQWPR`Q|U{^nDxXcrut^E2}ORezVZRGsn)@;1LMq#M7 zyW^Cm=E2VX4)*Wi&K*g@ohQ-~o>eZxe>!$ZZ=6C-RZte&pi!{=ana7cqIXxV_q=t~ z{@mZA_ALV+jNW~CSk+ABqug-&Cfvwii-+5z;cun*@3M^L6$vv^5B?Ub5b>=p(Cahc zy%98gX;`@DQcG#aZG5o!lAg!a{d<0jtxDC6Ia9y5ab>51zwP@u5=Z+&&NZ5dXXN_d z!w05hob}I3{ub0Mu6|bO9kxloS0Y3#$>91X;fiN*vr=o6-%VppFWmK-efrIDhM35Q z(s{pszQh6rzsG!N-M4j^5pzCw_65tU%l>IPJ$t9}!QPlYarNjmPy6sCZtpY-+OK@@ zHuKCeF*92H0FzI;8`$Jppb*%$#OhAjsR#Q@6#R*l#9SVYrVWgKL$a0kV`uU)=dOY8 z&(4^M%IuUYky#k`GmfQ}S?wOyIbdRWw$skU$H!iLqXmVwC@A=5L7W9kky?|Ix|hl+QL(96mqy9!g7LnDf3ywQgHrsVQAL)>opHY96Pg z1kZ|fwkuOOL?m?o{V1+&QZT)I@uRL87nWP|l!VRG5D)M7%I$W-oc! zgyptKf=vFGx7&<-Ruq+8Isg5%q}ZmDHzg}SZ1`6qEbo*lqmCi$7x49Z&%B{z`@Pk3 z-Wl|#9P9c%gI#ZIBI@3YKU~+6X3@Vhg%aj(`&mNHwdlL=yB4WBRu^R;J>-A6UH{^S z&JA}rgh^ew8|b%vAkwn6{f=S2NzIRWwG|RCF8f>_-shNhy(6&L2hU4av^eCbX5|x~ z(Jm;_WTF=I(q|;E^F&Iegk{qj^=R^z&=bwqlePvv*!$4^RI8}@xnsXuWUiJX?Jtiu$0aQc{Iu8Eug(3T_o+^U4d-sO z2Wpvp{)*?sbR75kn%*s9c@=M*r|6<~&Z=4V&V#l7z9-dwvA&)s#3`-J@ZLmpeNWN{ ziMvVZ?h6tgpIWLl;PE;L{a}1}ljPT~I-7RvPUIkd=lL-6W0`@PrTp1DhQFA17d$xg z()*;#=h1HGC#AnwkRU_L=h5)FEc)H0wR=~iQO)D&mFfE(^My5hK(Af$exCexwcY-Z zV|Ot*bQ58!U(1fKT36yx9`M6XwpG3OgVrQ#nBOtP$<0y*yUg&^H@+X5+VqC_H(A=~ zUgD4^=nMth%M~y{uWKO-wl~ zm~J;X;BPSSylg?D{^`cTFPC<*gnzd2>vs*5yY!{x1;un{#-D3F{Ml%W#)+I`cZB>~ zFN@~od_5Jtyr%2^O}2WJ+o`n6IlZT%Q)<}31M?+ne-F+NFt}57FgS~MU~N~N_hlj8 zc*#4WvC+yt3lh8WxS&<@E!!8ax@#c&QCDS$xj{nQovy=0mWgU^d-XJjK0L2oX4PBQ zc>Y`3t}L-zQrDk4_lIB6w3Fk1tNh9AiPtNd8sDuAAK#yPk~wUQ3p>?Xf1B<9Y7RE# zmTMI`#_|49lE5w9X0tLcPM&V_sj|$5+?jlDd%D(dGB~&Y1%7c+l1-ESbNT*{4VR0K zys#`#Bl~=-&n`^c<=ZEB{GYqQO7+>R@B1`~RBg4Z6&MN+m}}X-W)(;s4|sJ}-gcE* zXwXyjh;*M>iI!A}e5Zt--bl%+#rh`{hJLzg26(2FEBxcP?`uxavY{W*hU}!kH-2Fm zP8SGS@^c=>#o!rEyX`hichbyJ3aSV7yjhSa!_0S2=rKJbS(UXq-!oyYS@}#N<(<&e z5-(Y`0-J05jHLLC(-?3lsPD3AP1`%G~w!7I# z*y*kH43KIy0)A{{99DG7obiD%W)%E~kuk?AA!M^^^Blg!p|$&7-P+GKtKO4i3(On> zDjNiq?Gpl3^ce|%3d|1&efVWG-O23P2GHoQbGEC_QVAh~$AsWt4uo({@ywG+iVF&h z&SIQZeoXFDmUp}Km9HV7&8XLPGyB2vr+8e->w|d5v%OL;?p|-;>7jki5)lA8n6X^o zmMbOsQoH45pumTT$J4V{7YFw?ECX44HzSQnmBupvSh>~z`)+;R)H}_l(-=Vmr&EA6 zzTACiM!kPtk>{-yWtTx2NMBSOt?C-6N}Tylpl3(&%&PQDr$D6)t>WM;5u9jI3bd#z zZv3prHyJpXJ|We4+k#E!QkMg#Y763WD%GDKYZDueo8;kq)<4rNUn`e^A)?SOve~Mc z#j%9>g)!ZU&*4`^egJ{Pfp(-e@;ZA1FH5!5J=$e2lW(c82{)+k*M6^DC6ldj`CL%? z=Z+H@18(|x7uq^3d8>wPJ-xPr^pGWzc+H8>aikz`hu&@xU<*fMVJDm}2|E1>bXqx_ zkb^NMN6VNTcP|nyA^S;p@7|)U{9nI&x&vo@xvD95-0G{PUfufosrbEa!*wJuDnz!M z47R4ESiVkw4`e&vBjA*FW9SXxR#KO2SKVDYAG9QTRkA9|24tCE;&>Y29E;}gzr8f0 z>%-c~<731k#lQ-0fJN}C2XQfAtY@1|{}i|(0MtwcE>W%mfoFD@rFpEYsjQJaZssKZK9Q~#C;>eZF5t&2KtB~D>qXuGxsJA zh5ei6d}LL%_ft_`*R|b>Z5RCXii3R{2S*#|?o%Di&q^dRvqT0?dRMDm=BR3D^t%51 z@sh~>#=n@Yi#nPx(7j;X1ibeNFv$JwJ_D5|CU>t}CV>H$@k%Q3MoEQZ2)Gh&NX#4= zBa%o~O2w7Dk}(f&LUM^v!UC*lYLyv_U1PsA~d&fq)|$4B0> z3W7)C=Cx7M*|YXC8u3q>lE;e`ttHnlloAyd+P|12wno zr<2c4z4bXk_IQ}OiF&cJr{U7GwT6$(!_EK64j})0O+Qx_6ZmF-ki1QF;=8gX@=a&5 zax{;crzI;dRTRPfU)kqZE^FMLjlaL;oPRQMceU%wixwxY#f0yAJ^!r%;(9H>qPfNL z#r~->9;+hfF?1s?Y~UTzz487^;`N_Tg?7gUf3SNcrLHS`n=<#Dpz84{{Dm`()~MxP zP+aDkr>zv}tEc7o&-LYkOQ=m}*ZKW(eY;MRbXFtZK=ewt@VUO`-y}_+%zE+3A4$h& zE+?(p`tML`>qR>f->pu)vfuaSI-4Kphy9)X&mRam_Q*}`$Exsk`10$%?KbNT@s}dj%ina=RCyXTKm2sl{CcR8>c1zp zH(WTn{Ys-wK#7&`FEgJAzGjD3j}SM9)+|Yzy2#ZfhqgF``M!|Bwnn@?7%7OXQ;HN2 z%B%dAw)Oa2owAn-b%`GZUtt3>=E*s$%S7Uze10Vw6#KFuz0LhwYtyoA0aL1Tw$#kM zX7Vzu8b4RxWL+rj+qVXPvG?VEmE?W=1L7|VC|gW-3Rxoh9R2xZ=^bUlxta)Q*~HI*Z?Swo86P5vp3=f~P%injL@7X^!cNSh1W!@!KSx z?1k1V?Oa&dB=sxWn~$$HeWNtpAwqDi&P9;5(c~85?WK5kDl%UM|s&{H8uHA*R-TGS<$mLV$3CP3#HTe*bj0%E|wf*y!do`cd&ktiX*hawiOVRE7w zy^<|iHDQb}+$<~`AM#MYmwUgzx2ZQNwfWS;jLyVI-`~|Xy-d=-_2Q^z(C_-(7;;)q z@7j#*Z=0Lj-Apz*?<ZQA zxy(oiI>}T}*5r4xIw2f(+x*M)fwem)ZNYqPT$5#qb1`z95KAk@GRrB~O z;+G77OYuuUB6KYAq?;$qAoH2{`JhAJ^kimzKD3#=uxH85K>OTBo~<4}YUc(6AMJf= z(DKFda}*m-lqxt9i8_d z+B}`3)I-}dgDD!qk`*wr$}|gRx#XZ?Ay+frY$yQl3%V(>1L3(-q{9ktx}V9jRpu=< zFq&)d+^k%_@wM~k2hQLkUz%3Zwl-ad!SWZ!%5mnId2FF(SDSlEE3CGrU$>Ci;1nNL z_@d^^&I*k~cYUM<+SP@xw7#oh!AQc`J&_0GHbK;SHX5tp#hRBKrTOM)=gHQ z_wM#=0bnWHgsQ_&v&%m?%vkhjpkD5o?*j?H23t6w}108VmB1v9+DRJ&vscMX2H!HckkdNPX*g0Z5vH- zOl3VN{9=}kG;8>@fm@!VvP}%i=V~#upz@)0$FAOb(E`%fDDheq;8@@z;dfNgkcf@D zJ9gFo<1Q^A0onvE*!VgWQnAO3tH8*?A6R-qKKWqMpn@dA8|1rKf+~ zBQA41FOw@pjHBrCpQo=bI_+_y-%&Le?@!1*uLMZU2hQxx=BFIG+g#+E z{+zNjJ>yMZ@ZP9)rf>z;_I`jEDq)2IODrvj-rGt2$;^mvIqmHMoRBvPYfB9Qm z&gibVbzqkL&{yB|p?9q*vc_O6xf-n%uc`T>QWcc$&P=;{;V|C5tC!GHD^Z+qHj$*( z1suQQiSbx+zZ|ssix^L#g=poah)%_pf80M&SqZul?}#B)<@`iFS~-s}WNSj|lOx|V zx2&7rytKR0dz|O>tXi@3{?M zj6>zd0P7aDZz3Asj;On{X5|=8m%iaX#j|3j+XKe#ro;1^^t5uKB_6M`4@J;LGy7Y_ za}5t0e^@UN@ak2?qc`(NwRf}c>(&}wkGuL&^^0Z2E9<#-<&53sVRwymBsw#qEOEPc zZ5qGLy&5mqQ`%jX?=$!Q?8QI&@2$1m_Dt~DrReTV@qoT$IkA-d`zo?l=M1vLYQTKV zmcU)=Hu(E^@RNpo)jIjVNcvUxe62lIs>#Mz4&M%-4`FZbXw=J!k$dvbrE50e{t*{C zH{r4k(wcR$K`J`})f?hEZ%5Vt?jHEuHSn{g`(W>8xxtS4N)~lH64LSh{+8Y@O^KX0 znnUg0-M@Z4Z|mE$^T*?w%F^a*zFocFYKz0pJ`bt<{WJ2nTwst#fQQe(kH*5Ysbxn? zr?D!TOMBZ|-j_ACmTh|H7A4(Y#XPdYzO4ZoKy6#s4|x+%c>su;O^ z&l`F5J4NeG4Sq6|+hG{!{TM&0+|yLtAnrfosPgw}dCfa7!cKLkzBB#g*E-KHDBw-b zgZF&x_vY4DC>C|xbE{o?=7pY~d~0r!;qG$x)^*=Z-+O#NOf#}l@2bss1)jy&?CTnWk(0JT;sXp3$Q_4!+-b*!(?>FOFD^+?H zzm$0H7=u}6q-}E8OB-4u`nbowCVI!aP7LG(9|N>A&F9njraD-|KbpFN`!KPaPZ-?4NVvXPM{hS?2?% z_n&n7ewz8g>`}(QRyJ|7OZF0Xb8bl&I-pQxmd0SSM@S` z_-jNg{ItKmUe_h{n7ugv*q~0AREVw;UL$3Ztxe?3Ha~YgZbp`FPyG6`dg7%gP7`;W zu0CQRv6i@8o;7Tvl74m}M&%TZSj!W$h^Qs+v5vXlv3Ki(>@s`i)R@RixTLF0&gu>O!WRXN-ZH6Qt0RXDii2b?L_K}$Y^r9z(^0cy)uni&Tg@T9DV3tNZ{{)8 zp0O0Jec?F~zu{){8sG3z(VEw%CDf+dwqjpiABcCjt|gGw$2NbH_0UBsKTB>`?WL?e z&S%53Y8<7kv&gL4wRl#9-C4z~R-0vkWohOQYP+gpWV6z)$r~w^Iv>_7E8TXc{H9rq z@lA`Pmz7ssv?dSq4!dR)_h#)RAM17V%V!P`-~G)O86hYDU8^xTyLZPnX@UK@_C9tj z&!xk^dGvXldF*&Rd2lw57Z01qfybLCorlC@2GX89_8^7wP-yx4tdg{=dFu{9de)t$1VN{O8pr z${fGlDl^nnhS&T*=x6-yuK!#iMnh5h=ni3o#KdR_ecXPoY`vAdXAi^o2#@T+<8;}a zL@_5ZXcS8l2{R|546vsWY#sy~xS1+MipI$Un_w~G+URu@6rs`RWB-VvbaN7&&HyPY zb*vVu3aA)IQIJVPK@t>apeS)0iH;y3m(Ju=15ym4La?QBtWu&WkS4BxicwA-3{V|N z0Xea;`@nT3XpSgFbdacFv?P%RS5+`jXe5acLXFgkFhO@Xm0FVi?vEVVr#z|?P)+oC zL-EW&!+c)piFf_kyv&g^+QtZr-fMDAZ{kf{`+&#x+W*;zy;XnX*ye6jg{d;maM6N% z^yt-m;Ub>)4yrb`Y#;BH3m1~2pYSa^&c9V{LMF-I)y2(A)!uI9LMLx;cU3ak-N(~K zneFLFwzng*I6EZ0y~v0%LSCpL7_ByC@uW)4_?JZ=;eR$McgGav%+pg+r_cdznRLzV;C$ay!h zkyV9k2KK9hE%v}u$Rh_AIGbHLn@LCJ3PZB4wz)bT(?;mptFYg3;EZ8h`J=|I;nR#>)S=BND-A<3|v9{6nkp`|;9Cy7c=mL6Ows+UsYC%<5RE zw|>3(lb_t=rYg!Fz2-g`q%)fV@Du#d?z?K~!`BO$#n zojJSSU2=D#+V1lT1Br}*_wI2Af<7I+=)O3ru$noS#T4at=Zh*O3FAdGg-Z@ghI_dS z8kAg>41G?Kc5L*HJBT5Cj&^(%#Q~BBkLB>RHZQa|RGlMfhX|aqmR5dvIY+wLLuSc) zL5st>vHKs#hNrsIt~7182vI|1YMOQGg3nU;Gb!TPjSl67E_I>ih?tkhG`DsA$~jVg zHX2_G*Q_YmnIYeG z-sQ+Zo!J-tukvk)e7_bXwme+A;{_#Lg0@jV+2#^$r{qw?@Y>%OM(-bx_+R>n{~-Ub z)%E{U75@K0rvFJlaG?DExdK>we~0^>?n)<2_osCKoA1^`wCklmev|#A68U!8EHc(+ z89yjlnqT1>u76#Hu0=C0o|_z{w1waN{NR%;@vOpEukpFJZ>-FED4HCyJ2_(D^{i_* zFyGZ$)KzxWHR4~w>u*T)t}>!NOmN!Q;&d?PwVZ3Uz@GCmZ|&G$BKB6zlBt^U>pWSs zU>|dOya(k@<7Vmo53AzbFIXp$5zxWSYywV5Jd zJ2TXpVoE)tKX2q#%H4T{^CB98Bc_=7`dB7(%J3JUl&`U6g|(P=b{Mj|eakqe1c@b_bf7)Ci~#A_z(df=_Z93?c~zkp$zCghBz4C0D~3L=fPn zf9~VhpRf-GkpzQCfWpvNvIG>s1Ql0<0K)3AbzM2L8uTxz=!}^ zz$cDbDnt?*L=qaeBorD%5E?`f+IT_6^iP9G0#*xu>@*@9m>3|E&>$wEK?I>g1i|SL zN$3zs=nzTh<0TpGAsr$J9U=&^xB^2FpD<;D>5)rfFd=c}F(8sKAc8O;f-oisGOB+D zL=px>5(Y#P2E-&_S;*aKh#*XGnwjfHCPWe@L=xr%Nk;U~ga`trTkbYS1;NJ?iwTj0 z36X>ekwgU|hzdjym5G9k=wAgQi3&s#KsGK(R3Ii%fe4}k5ky4==c*6^Je=GJ0tm>R zH&GHo{|JR!5QM@l2tpYX1pgZ_AQWy%5WsxyhPfpHPlmYf1Ls+}%7Q0a6Y>cCBj7yd zgiKBzL=tcylKZ|fNyZ5RiG~nxa-X{_lZ10;LL`Ck9YH5aLUfvVYBQ1VAP*wQU%Z3I zPM{$1gP;&eP>4w&d`BRB2PfgVJHv%{pnn9ycLc(B1j2U&!gmlULK=n$Lf{>rtS$)O z5eVNw_{iM|gzpFh@5ng3|B3$)2;UJ1-w_Dk5eVOr5x!5rJHm~3oPt33jzIX1K=_V8 z_>N5A`>1b0@Q!fsJ}Le~Abdw4d`BRBM<9GhAb3Z{<9(EW6a>O|1j2U&!gmD1cVv|B zM3k;~j!`1c&e)f$$xH@Ew8h9ht;;&ddtIJ2Hy*3Gp8S z;X4B1I|AW50^vKN!fg`pjR@m4J)(c`Y<7GmXV8LoE|5HK=RzTTNB{8sFBMS;-cbt9 zjYX8gJxovt-@*5XaCdMr-+?G71n($#zQ7rEBji9+~}Limn?ujJt_ zJJBQ@{ezcV+&4n_PP|p&su03=;)x3PeGoxVI(PhsLimnC_>MyOj&kvxc)S4dtv`4l z7yqFUzM~MnqY%EMkmWlHS-zu_@lNO;h43AP@EwKl9fj~6yrbk+KLqb6z&ls`heG&{ zLimnC_>OY(odUighr2U>@%}gdLm_-eA$&(6d`BUCM z@922EkMIvT2!!w8tGu{71K~Rg;X4XhzM~Ml<0yphD1`6e-7};!6JbIq8=J^?j{Y$S z-mww9PmKS->shWwFbLl<2;VUX-!aJY9h-pnQT<~OzGD!+V-UV$5WZt$d>{9C2ZP`p z!y$afAbiIle8(Vs$G|J}$s-4Y;2j&q`-J!pgYX@L@EwEj9fK_2G05^An~3)j{bLZm zV-UV$5WZs&zGHv*{x|+3Hhyqv2E)0>I|ku9IPd`}%pE&olle}J2ngP>F}#nD|1b#O zF$mu=2;VUX-!aJY9h-!Aj{Y$S-!TZ^F$mwmYcX!k{N?+f$2(vT668LNJO0BUe8(Vs z2U{>86+%n`!8`T`@8jY>48nH|!gmb9cMQUJ46=O3CgYvZKX{ACEeVA07=-T_gzwlm zzK=a!!ytIa0N%OcKMcZm48nH|!gq|D?-UGzckD0T|Hgk9gzp%H?-+#d7=-T_WciM9 z;T`B7gYX@L@EzDhKbA z;y)^c?^FoisSv(XA$+Go@DASb|JBwA|G-;Q2;Zp?zEdH5r$YEng)HBx5WM452;Zp? zzEdH5r$YEnoyd2N{;3eWQ%CSVG5(`M_)dlJoeJSQ6~cEaWcf~=fcH`TQz3i@JAMD! zX~d5xR0!Xx5Wa&w8{B0fc&7sWLvG}bRHzWXQz3k(PU1V!Zz=@u)KR=oi2tY%zEdH5 zr$YEnh47sUS-w*z;(bK_VCxFE79f15a;@KibP&E%|M2~9{6~f0ol3_cGZ%#KR0!Xx z5WZ6<^PNyX1n<-_ypNCns1UwWA$+Go_)dlJoeEjLgRlCWplOc&sSv(XA$+Go_)dlJ zo%)yWf8swX1n*QPg**PELikRF@SO_bJC%#?K>ZNBQ~%(7T>M9c@SO_bI~BrrDunM; z$nu>!8SjMtsSv(XA$+Go_)dlJojQ*1WAPsqf_EywJ6HTi?2VtyavFs1Gzi~m+`~SEahVY#R;X6280VxX+ zguwgc_>TtRI}O5j8ieoQlm~ai5WLgI;r&niM}zR42H`ug@sLY15WdqO%Xb<#-f;>I z!gm^k?=%SCX%M~>8~V692*EpzgZD}C9}U8H8iemO2;XTCzSAIhr;T5}kMIvTA%yQV z2;XTCzSAIlr$LtQGzi{-|3St(gzq#6-@)DmZdFg@I}v|SXb`*;XK{d;CdPln4puJP zLHJIC@SO(XI}NgYr%k~7sQzgXzSAIlr$P8mgYca;#`kg0|7Z}r({KpiX%N2CAbh7m z_)eR|caHiYcn9C<&C&FP_>TtRI}O5j8iemO2;XUtT@*ztbUnr$hJ-jtg?lA9M)b z>3{G(F8-rK_)drLoetqU9kP6=n%Zq3A|5^|L73D(;8Ycpu>(a1aRJ=@7otA$+Gp_)do`-{}y%<8%n$=@7otA$+Gp z_)ee5_Yv=h;GI5#_lfZz9m01ygzt0+-{}y((;>@u`UJd>>YonbI~~GzI)v|Z2;b>r zd>;`-J$90pU9X!gmIQ?+ggv8Ia{W zVR}K>4+sbVLNl4pD+5l>hDSasKe%|L}PIf9@Xmb9*db f;HsckWa5xN_?tu|j^ZZun+cvEjr5yulg|GE;TJp` diff --git a/resources/views/livewire/registro.blade.php b/resources/views/livewire/registro.blade.php index b8d6509..822e5d7 100644 --- a/resources/views/livewire/registro.blade.php +++ b/resources/views/livewire/registro.blade.php @@ -8,7 +8,7 @@ Nuova voce + title="Scarica il modulo S-13 compilato per un anno di servizio"> Modulo S-13 @@ -210,18 +210,8 @@ @error('form_proclamatore_id') {{ $message }} @enderror - {{-- Anno teocratico --}} -
- - - @error('form_anno_id') {{ $message }} @enderror -
- + {{-- L'anno di servizio non si sceglie: lo ricava il salvataggio + dalla data di assegnazione. --}} {{-- Campagna --}}
diff --git a/resources/views/pdf/registro-s13.blade.php b/resources/views/pdf/registro-s13.blade.php deleted file mode 100644 index 317253b..0000000 --- a/resources/views/pdf/registro-s13.blade.php +++ /dev/null @@ -1,222 +0,0 @@ -{{-- - 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 - /** - * 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. - * - * 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. - * - * La larghezza qui sotto è misurata sul PDF renderizzato, non dedotta - * dalle percentuali: al netto di padding e bordi la cella risulta 35.9mm, - * e la costante tiene un margine su quel valore. Se cambiano le - * percentuali delle colonne o i margini di pagina, va rimisurata. - * - * "Assegnato a" è nowrap su una riga sola: basta il corpo che ci sta, e - * oltre il minimo si tronca. La colonna del riporto ospita solo una data, - * quindi non ha bisogno di adattamenti. - */ - $MM_PER_CARATTERE_PER_PUNTO = 0.20; - $CORPO_MIN_PT = 4.5; - - $LARGHEZZA_NOME_MM = 34.0; // cella misurata 35.9mm - - $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 $corpoMaxPt . 'pt'; - } - - $pt = $larghezzaMm / ($len * $MM_PER_CARATTERE_PER_PUNTO); - - 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; - }; - - $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 - - - {{-- 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. --}} - - - - @for($g = 0; $g < 4; $g++) - - @endfor - - - @for($g = 0; $g < 4; $g++) - - - @endfor - - - - @foreach($righe as $riga) - - - - @foreach($riga['assegnazioni'] as $a) - @php($nome = $troncaPer($a['nome'], $LARGHEZZA_NOME_MM)) - - @endforeach - - - @foreach($riga['assegnazioni'] as $a) - - - @endforeach - - @endforeach - -
Terr.
N.
Restituito
l’ultima volta il*
Assegnato a
Assegnato ilRestituito il
{{ $riga['numero'] }}{{ $riga['riporto'] }}{{ $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 - - diff --git a/tests/Feature/AssegnazioneSovrapposizioneTest.php b/tests/Feature/AssegnazioneSovrapposizioneTest.php new file mode 100644 index 0000000..ca4121f --- /dev/null +++ b/tests/Feature/AssegnazioneSovrapposizioneTest.php @@ -0,0 +1,211 @@ + $permesso, 'guard_name' => 'web']); + } + Role::create(['name' => 'amministratore', 'guard_name' => 'web'])->givePermissionTo(Permission::all()); + + $this->admin = User::create(['name' => 'Admin', 'email' => 'admin@example.test', 'password' => 'password']); + $this->admin->assignRole('amministratore'); + $this->actingAs($this->admin); + + $this->territorio = Territorio::create(['numero' => '5', 'attivo' => true]); + $this->proclamatore = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true]); + } + + protected function tearDown(): void + { + Carbon::setTestNow(); + parent::tearDown(); + } + + // ─── L'assegnazione in corso ──────────────────────────────── + + /** + * Il caso che rendeva il territorio "libero" mentre era fuori: una chiusa + * con data più recente dell'aperta. + */ + public function test_l_assegnazione_in_corso_si_trova_anche_con_una_chiusa_piu_recente(): void + { + $aperta = $this->assegna('2026-08-20'); + $this->assegna('2026-09-01', '2026-09-15'); + + $this->assertTrue($this->territorio->fresh()->assegnazioneCorrente?->is($aperta)); + } + + // ─── Il controllo di sovrapposizione ──────────────────────── + + public function test_rientro_e_nuova_uscita_nello_stesso_giorno_non_si_sovrappongono(): void + { + $this->assegna('2026-09-01', '2026-09-20'); + + $this->assertNull(Assegnazione::sovrappostaA($this->territorio->id, Carbon::parse('2026-09-20'))); + } + + public function test_un_uscita_prima_dell_ultimo_rientro_si_sovrappone(): void + { + $chiusa = $this->assegna('2026-09-01', '2026-09-20'); + + $this->assertTrue(Assegnazione::sovrappostaA($this->territorio->id, Carbon::parse('2026-09-10'))?->is($chiusa)); + } + + public function test_con_un_assegnazione_aperta_ogni_nuova_uscita_si_sovrappone(): void + { + $aperta = $this->assegna('2026-09-01'); + + $this->assertTrue(Assegnazione::sovrappostaA($this->territorio->id, Carbon::parse('2026-10-01'))?->is($aperta)); + } + + public function test_un_periodo_chiuso_inserito_prima_di_tutti_non_si_sovrappone(): void + { + $this->assegna('2026-09-01'); + + $this->assertNull(Assegnazione::sovrappostaA($this->territorio->id, Carbon::parse('2026-06-01'), Carbon::parse('2026-07-01'))); + } + + // ─── Pagina Assegna ───────────────────────────────────────── + + public function test_assegna_rifiuta_un_uscita_retrodatata_a_prima_dell_ultimo_rientro(): void + { + $this->assegna('2026-09-01', '2026-09-20'); + + Livewire::test(Assegna::class, ['territorioId' => $this->territorio->id]) + ->set('proclamatore_id', $this->proclamatore->id) + ->set('assigned_at', '2026-09-10') + ->call('save') + ->assertHasErrors('assigned_at'); + + $this->assertSame(1, Assegnazione::count()); + } + + public function test_assegna_accetta_l_uscita_dal_giorno_del_rientro(): void + { + $this->assegna('2026-09-01', '2026-09-20'); + + Livewire::test(Assegna::class, ['territorioId' => $this->territorio->id]) + ->set('proclamatore_id', $this->proclamatore->id) + ->set('assigned_at', '2026-09-20') + ->call('save') + ->assertHasNoErrors(); + + $this->assertSame(2, Assegnazione::count()); + } + + // ─── Registro (correzioni dell'amministratore) ────────────── + + public function test_il_registro_ricava_l_anno_di_servizio_dalla_data(): void + { + Livewire::test(Registro::class) + ->call('openCreate') + ->set('form_territorio_id', (string) $this->territorio->id) + ->set('form_proclamatore_id', (string) $this->proclamatore->id) + ->set('form_assigned_at', '2025-08-10') + ->set('form_returned_at', '2025-09-05') + ->call('save') + ->assertHasNoErrors(); + + $this->assertSame('2024-2025', Assegnazione::sole()->annoTeocratico->label); + } + + public function test_il_registro_non_crea_una_seconda_assegnazione_aperta(): void + { + $this->assegna('2026-09-01'); + + Livewire::test(Registro::class) + ->call('openCreate') + ->set('form_territorio_id', (string) $this->territorio->id) + ->set('form_proclamatore_id', (string) $this->proclamatore->id) + ->set('form_assigned_at', '2026-10-01') + ->call('save') + ->assertHasErrors('form_assigned_at'); + + $this->assertSame(1, Assegnazione::count()); + } + + public function test_il_registro_non_riapre_un_assegnazione_chiusa_se_il_territorio_e_fuori(): void + { + $chiusa = $this->assegna('2026-08-01', '2026-08-20'); + $this->assegna('2026-09-01'); + + Livewire::test(Registro::class) + ->call('openEdit', $chiusa->id) + ->set('form_returned_at', '') + ->call('save') + ->assertHasErrors('form_assigned_at'); + + $this->assertNotNull($chiusa->fresh()->returned_at); + } + + public function test_il_registro_rifiuta_le_date_future(): void + { + Livewire::test(Registro::class) + ->call('openCreate') + ->set('form_territorio_id', (string) $this->territorio->id) + ->set('form_proclamatore_id', (string) $this->proclamatore->id) + ->set('form_assigned_at', '2026-11-01') + ->call('save') + ->assertHasErrors('form_assigned_at'); + } + + public function test_il_registro_permette_di_correggere_la_stessa_assegnazione(): void + { + $aperta = $this->assegna('2026-09-01'); + + Livewire::test(Registro::class) + ->call('openEdit', $aperta->id) + ->set('form_assigned_at', '2026-09-03') + ->call('save') + ->assertHasNoErrors(); + + $this->assertSame('2026-09-03', $aperta->fresh()->assigned_at->toDateString()); + } + + // ─── helper ───────────────────────────────────────────────── + + private function assegna(string $uscita, ?string $rientro = null): Assegnazione + { + return Assegnazione::create([ + 'territorio_id' => $this->territorio->id, + 'proclamatore_id' => $this->proclamatore->id, + 'anno_teocratico_id' => AnnoTeocratico::perData(Carbon::parse($uscita))->id, + 'assigned_at' => $uscita, + 'returned_at' => $rientro, + 'created_by' => $this->admin->id, + ]); + } +} diff --git a/tests/Feature/RegistroS13Test.php b/tests/Feature/RegistroS13Test.php index 04b2f23..1c8470b 100644 --- a/tests/Feature/RegistroS13Test.php +++ b/tests/Feature/RegistroS13Test.php @@ -2,27 +2,23 @@ namespace Tests\Feature; -use App\Livewire\Registro; use App\Models\AnnoTeocratico; use App\Models\Assegnazione; use App\Models\Proclamatore; use App\Models\Territorio; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use App\Services\Registro\ModuloS13; use Illuminate\Support\Collection; -use ReflectionMethod; use Tests\TestCase; /** - * Modulo S-13-I: il ciclo da quattro gruppi "Assegnato a" e la colonna - * "Restituito l'ultima volta il". + * Modulo S-13-I: il ciclo da quattro gruppi "Assegnato a", la colonna + * "Restituito l'ultima volta il" e le assegnazioni a cavallo d'anno. * * E' la regola meno intuitiva del Registro — la quinta assegnazione non si * aggiunge alle quattro precedenti, le sostituisce — e finisce su un modulo che * viene firmato e archiviato: se sbaglia, sbaglia su carta. - * - * righeS13() e' privata di proposito (non e' un'azione Livewire); qui la si - * chiama in riflessione perche' e' il calcolo, non il PDF, quello da proteggere. */ class RegistroS13Test extends TestCase { @@ -68,7 +64,7 @@ class RegistroS13Test extends TestCase $this->assertSame('', $riga['riporto']); $this->assertCount(4, $riga['assegnazioni']); $this->assertSame( - ['nome' => 'Rossi Mario', 'assegnato_il' => '10/09/2025', 'restituito_il' => '15/10/2025'], + ['nome' => 'Rossi M.', 'assegnato_il' => '10/09/2025', 'restituito_il' => '15/10/2025'], $riga['assegnazioni'][0] ); @@ -82,6 +78,19 @@ class RegistroS13Test extends TestCase } } + public function test_il_nome_e_cognome_piu_iniziale_del_nome(): void + { + $proclamatore = Proclamatore::create([ + 'nome' => 'élena Maria', + 'cognome' => "Dell'Acqua Bianchi", + 'attivo' => true, + ]); + $territorio = $this->territorio('1'); + $this->assegna($territorio, '2025-09-10')->update(['proclamatore_id' => $proclamatore->id]); + + $this->assertSame("Dell'Acqua Bianchi É.", $this->righe()->sole()['assegnazioni'][0]['nome']); + } + public function test_un_assegnazione_ancora_aperta_ha_la_data_di_rientro_vuota(): void { $territorio = $this->territorio('1'); @@ -255,17 +264,11 @@ class RegistroS13Test extends TestCase $this->assertSame(['1'], $this->righe()->pluck('numero')->all()); } - public function test_le_assegnazioni_di_un_altro_anno_di_servizio_non_compaiono(): void + public function test_le_assegnazioni_chiuse_di_un_altro_anno_di_servizio_non_compaiono(): void { - $altroAnno = AnnoTeocratico::create([ - 'label' => '2024-2025', - 'start_date' => '2024-09-01', - 'end_date' => '2025-08-31', - ]); - $territorio = $this->territorio('1'); $this->assegna($territorio, '2025-09-01', '2025-09-20'); - $this->assegna($territorio, '2025-01-10', '2025-02-10', $altroAnno); + $this->assegna($territorio, '2025-01-10', '2025-02-10', $this->annoPrecedente()); $riga = $this->righe()->sole(); @@ -275,11 +278,168 @@ class RegistroS13Test extends TestCase ); } - public function test_un_territorio_senza_assegnazioni_nell_anno_non_produce_una_riga(): void + /** + * Il registro serve a vedere anche i territori fermi: una riga vuota con + * la data dell'ultimo rientro dice da quanto non escono. + */ + public function test_un_territorio_senza_assegnazioni_nell_anno_ha_comunque_la_sua_riga(): void + { + $territorio = $this->territorio('1'); + $this->assegna($territorio, '2025-03-01', '2025-04-12', $this->annoPrecedente()); + + $riga = $this->righe()->sole(); + + $this->assertSame('1', $riga['numero']); + $this->assertSame('12/04/2025', $riga['riporto']); + $this->assertSame(['', '', '', ''], array_column($riga['assegnazioni'], 'nome')); + } + + public function test_un_territorio_mai_assegnato_ha_la_riga_vuota(): void { $this->territorio('1'); - $this->assertTrue($this->righe()->isEmpty()); + $riga = $this->righe()->sole(); + + $this->assertSame('', $riga['riporto']); + $this->assertSame(['', '', '', ''], array_column($riga['assegnazioni'], 'assegnato_il')); + } + + // ─── Il riporto a inizio anno ─────────────────────────────── + + /** + * La nota del modulo: "quando si inizia un nuovo foglio, registrare in + * questa colonna l'ultima data in cui e' stato restituito ogni territorio". + */ + public function test_nel_primo_ciclo_il_riporto_e_l_ultimo_rientro_prima_dell_anno(): void + { + $precedente = $this->annoPrecedente(); + $territorio = $this->territorio('1'); + $this->assegna($territorio, '2025-01-10', '2025-02-10', $precedente); + $this->assegna($territorio, '2025-05-02', '2025-06-30', $precedente); + $this->assegna($territorio, '2025-10-01', '2025-10-20'); + + $riga = $this->righe()->sole(); + + $this->assertSame('30/06/2025', $riga['riporto']); + $this->assertSame('01/10/2025', $riga['assegnazioni'][0]['assegnato_il']); + } + + // ─── A cavallo d'anno ─────────────────────────────────────── + + /** + * Uscito ad agosto, rientrato a ottobre: sul foglio nuovo occupa il primo + * gruppo con entrambe le date, e il riporto e' il rientro precedente. + */ + public function test_l_assegnazione_a_cavallo_apre_il_foglio_nuovo_con_entrambe_le_date(): void + { + $precedente = $this->annoPrecedente(); + $territorio = $this->territorio('1'); + $this->assegna($territorio, '2025-05-02', '2025-06-30', $precedente); + $this->assegna($territorio, '2025-08-10', '2025-10-05', $precedente); + $this->assegna($territorio, '2025-11-01', '2025-11-20'); + + $riga = $this->righe()->sole(); + + $this->assertSame('30/06/2025', $riga['riporto']); + $this->assertSame( + ['nome' => 'Rossi M.', 'assegnato_il' => '10/08/2025', 'restituito_il' => '05/10/2025'], + $riga['assegnazioni'][0] + ); + $this->assertSame('01/11/2025', $riga['assegnazioni'][1]['assegnato_il']); + } + + public function test_sul_foglio_vecchio_l_assegnazione_a_cavallo_resta_senza_rientro(): void + { + $precedente = $this->annoPrecedente(); + $territorio = $this->territorio('1'); + $this->assegna($territorio, '2025-08-10', '2025-10-05', $precedente); + + $riga = $this->righe($precedente)->sole(); + + $this->assertSame('10/08/2025', $riga['assegnazioni'][0]['assegnato_il']); + $this->assertSame('', $riga['assegnazioni'][0]['restituito_il']); + } + + public function test_un_assegnazione_ancora_fuori_da_un_anno_precedente_compare_sul_foglio_nuovo(): void + { + $territorio = $this->territorio('1'); + $this->assegna($territorio, '2025-07-01', null, $this->annoPrecedente()); + + $riga = $this->righe()->sole(); + + $this->assertSame('01/07/2025', $riga['assegnazioni'][0]['assegnato_il']); + $this->assertSame('', $riga['assegnazioni'][0]['restituito_il']); + } + + /** L'assegnazione a cavallo conta nel ciclo: con altre quattro nell'anno, la riga ricomincia. */ + public function test_l_assegnazione_a_cavallo_conta_nel_ciclo_da_quattro(): void + { + $territorio = $this->territorio('1'); + $this->assegna($territorio, '2025-08-10', '2025-09-15', $this->annoPrecedente()); + $this->assegna($territorio, '2025-10-01', '2025-10-20'); + $this->assegna($territorio, '2025-11-01', '2025-11-20'); + $this->assegna($territorio, '2025-12-01', '2025-12-24'); + $this->assegna($territorio, '2026-01-05'); + + $riga = $this->righe()->sole(); + + $this->assertSame('24/12/2025', $riga['riporto']); + $this->assertSame(['05/01/2026', '', '', ''], array_column($riga['assegnazioni'], 'assegnato_il')); + } + + /** + * Il foglio sceglie per data, non per anno_teocratico_id: una data + * corretta a mano nel Registro o un import legacy possono lasciare l'anno + * registrato incoerente con il giorno di uscita. + */ + public function test_conta_la_data_di_uscita_non_l_anno_registrato(): void + { + $precedente = $this->annoPrecedente(); + $territorio = $this->territorio('1'); + $this->assegna($territorio, '2025-10-01', '2025-10-20', $precedente); + + $this->assertSame('01/10/2025', $this->righe()->sole()['assegnazioni'][0]['assegnato_il']); + $this->assertSame('', $this->righe($precedente)->sole()['assegnazioni'][0]['assegnato_il']); + } + + public function test_l_ultimo_giorno_dell_anno_appartiene_al_foglio(): void + { + $territorio = $this->territorio('1'); + $this->assegna($territorio, '2026-08-31', '2026-08-31'); + + $riga = $this->righe()->sole(); + + $this->assertSame('31/08/2026', $riga['assegnazioni'][0]['assegnato_il']); + $this->assertSame('31/08/2026', $riga['assegnazioni'][0]['restituito_il']); + } + + // ─── Impaginazione e PDF ──────────────────────────────────── + + public function test_ogni_foglio_ha_venti_righe_come_il_modulo(): void + { + foreach (range(1, 21) as $numero) { + $this->territorio((string) $numero); + } + + $pagine = (new ModuloS13())->pagine($this->righe()); + + $this->assertCount(2, $pagine); + $this->assertCount(20, $pagine[0]); + $this->assertCount(20, $pagine[1]); + $this->assertSame('21', $pagine[1][0]['numero']); + $this->assertSame('', $pagine[1][1]['numero']); + } + + public function test_il_pdf_e_il_modulo_ufficiale_con_una_pagina_per_foglio(): void + { + foreach (range(1, 21) as $numero) { + $this->assegna($this->territorio((string) $numero), '2025-09-01'); + } + + $pdf = (new ModuloS13())->genera($this->anno); + + $this->assertStringStartsWith('%PDF-', $pdf); + $this->assertSame(2, preg_match_all('#/Type /Page\b(?!s)#', $pdf)); } /** @@ -326,8 +486,15 @@ class RegistroS13Test extends TestCase */ private function righe(?AnnoTeocratico $anno = null): Collection { - $metodo = new ReflectionMethod(Registro::class, 'righeS13'); + return (new ModuloS13())->righe($anno ?? $this->anno); + } - return $metodo->invoke(new Registro(), $anno ?? $this->anno); + private function annoPrecedente(): AnnoTeocratico + { + return AnnoTeocratico::create([ + 'label' => '2024-2025', + 'start_date' => '2024-09-01', + 'end_date' => '2025-08-31', + ]); } } diff --git a/tests/Feature/UltimoAmministratoreTest.php b/tests/Feature/UltimoAmministratoreTest.php new file mode 100644 index 0000000..9762648 --- /dev/null +++ b/tests/Feature/UltimoAmministratoreTest.php @@ -0,0 +1,78 @@ + 'amministratore', 'guard_name' => 'web']); + Role::create(['name' => 'assistente', 'guard_name' => 'web']); + + $this->admin = User::create(['name' => 'Admin', 'email' => 'admin@example.test', 'password' => 'password']); + $this->admin->assignRole('amministratore'); + $this->actingAs($this->admin); + } + + public function test_non_si_toglie_a_se_stessi_il_ruolo_di_amministratore(): void + { + // Anche con un secondo amministratore: il declassamento di sé stessi + // va chiesto a un altro. + User::create(['name' => 'Altro', 'email' => 'altro@example.test', 'password' => 'password']) + ->assignRole('amministratore'); + + $this->modificaRuolo($this->admin, 'assistente')->assertHasErrors('editSelectedRole'); + + $this->assertTrue($this->admin->fresh()->hasRole('amministratore')); + } + + public function test_non_si_declassa_l_ultimo_amministratore(): void + { + // Chi modifica non è l'amministratore stesso, così la regola su sé + // stessi non entra in gioco: si prova solo quella sull'ultimo. Il + // permesso della pagina sta nel middleware della rotta, che + // Livewire::test non attraversa. + $operatore = User::create(['name' => 'Operatore', 'email' => 'op@example.test', 'password' => 'password']); + $operatore->assignRole('assistente'); + $this->actingAs($operatore); + + $this->modificaRuolo($this->admin, 'assistente')->assertHasErrors('editSelectedRole'); + + $this->assertTrue($this->admin->fresh()->hasRole('amministratore')); + } + + public function test_un_amministratore_ne_declassa_un_altro_se_non_e_l_ultimo(): void + { + $altro = User::create(['name' => 'Altro', 'email' => 'altro@example.test', 'password' => 'password']); + $altro->assignRole('amministratore'); + + $this->modificaRuolo($altro, 'assistente')->assertHasNoErrors(); + + $this->assertTrue($altro->fresh()->hasRole('assistente')); + $this->assertFalse($altro->fresh()->hasRole('amministratore')); + } + + private function modificaRuolo(User $utente, string $ruolo) + { + return Livewire::test(UsersIndex::class) + ->set('editingUserId', $utente->id) + ->set('editName', $utente->name) + ->set('editEmail', $utente->email) + ->set('editSelectedRole', $ruolo) + ->call('updateUser'); + } +} diff --git a/tests/Feature/XmlExchangeImportTest.php b/tests/Feature/XmlExchangeImportTest.php new file mode 100644 index 0000000..59a876b --- /dev/null +++ b/tests/Feature/XmlExchangeImportTest.php @@ -0,0 +1,216 @@ + 'amministratore', 'guard_name' => 'web']); + Role::create(['name' => 'assistente', 'guard_name' => 'web']); + + $this->admin = User::create([ + 'name' => 'Amministratore', + 'email' => 'admin@example.test', + 'password' => 'password-attuale', + ]); + $this->admin->assignRole('amministratore'); + $this->actingAs($this->admin); + } + + // ─── Round-trip ───────────────────────────────────────────── + + /** + * Il difetto che spostava ogni data indietro di un giorno a ogni giro di + * backup e ripristino: toArray() le serializzava in UTC. + */ + public function test_export_e_reimport_conservano_le_date_esatte(): void + { + $anno = AnnoTeocratico::create(['label' => '2025-2026', 'start_date' => '2025-09-01', 'end_date' => '2026-08-31']); + Campagna::create(['descrizione' => 'Commemorazione', 'start_date' => '2026-03-01', 'end_date' => '2026-03-31']); + $territorio = Territorio::create(['numero' => '7', 'attivo' => true]); + $proclamatore = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true]); + Assegnazione::create([ + 'territorio_id' => $territorio->id, + 'proclamatore_id' => $proclamatore->id, + 'anno_teocratico_id' => $anno->id, + 'assigned_at' => '2026-03-15', + 'returned_at' => '2026-04-01', + 'created_by' => $this->admin->id, + ]); + + $this->reimporta(); + + $assegnazione = Assegnazione::sole(); + $this->assertSame('2026-03-15', $assegnazione->assigned_at->toDateString()); + $this->assertSame('2026-04-01', $assegnazione->returned_at->toDateString()); + + $anno = AnnoTeocratico::sole(); + $this->assertSame('2025-09-01', $anno->start_date->toDateString()); + $this->assertSame('2026-08-31', $anno->end_date->toDateString()); + + $campagna = Campagna::sole(); + $this->assertSame('2026-03-01', $campagna->start_date->toDateString()); + $this->assertSame('2026-03-31', $campagna->end_date->toDateString()); + } + + /** I backup fatti prima della correzione hanno le date in UTC: tornano giuste. */ + public function test_un_backup_vecchio_con_date_in_utc_viene_riportato_al_giorno_giusto(): void + { + $xml = $this->xmlMinimo( + anni: '2025-08-31T22:00:00.000000Z2026-08-30T22:00:00.000000Z', + assegnazioni: '1112026-03-14T23:00:00.000000Z', + ); + + $this->assertArrayNotHasKey('error', $this->importa($xml)); + + $this->assertSame('2026-03-15', Assegnazione::sole()->assigned_at->toDateString()); + $this->assertSame('2025-09-01', AnnoTeocratico::sole()->start_date->toDateString()); + $this->assertSame('2026-08-31', AnnoTeocratico::sole()->end_date->toDateString()); + } + + /** Un carattere di controllo in una nota rendeva l'intero file illeggibile. */ + public function test_un_carattere_di_controllo_in_una_nota_non_rompe_il_file(): void + { + Territorio::create(['numero' => '1', 'attivo' => true, 'note' => "Citofono\x0Brotto\x01"]); + + $xml = (new XmlExchange())->currentXmlBackup(); + + $this->assertNotFalse(simplexml_load_string($xml)); + $this->reimporta($xml); + $this->assertSame('Citofonorotto', Territorio::sole()->note); + } + + /** + * Un territorio resta senza zona quando la sua zona viene eliminata: al + * reimport veniva scartato insieme a tutte le sue assegnazioni. + */ + public function test_un_territorio_senza_zona_viene_importato_con_il_suo_storico(): void + { + $anno = AnnoTeocratico::create(['label' => '2025-2026', 'start_date' => '2025-09-01', 'end_date' => '2026-08-31']); + $territorio = Territorio::create(['numero' => '3', 'attivo' => true, 'zona_id' => null]); + $proclamatore = Proclamatore::create(['nome' => 'Anna', 'cognome' => 'Bianchi', 'attivo' => true]); + Assegnazione::create([ + 'territorio_id' => $territorio->id, + 'proclamatore_id' => $proclamatore->id, + 'anno_teocratico_id' => $anno->id, + 'assigned_at' => '2025-10-01', + 'created_by' => $this->admin->id, + ]); + + $this->reimporta(); + + $this->assertNull(Territorio::sole()->zona_id); + $this->assertSame(1, Assegnazione::count()); + } + + // ─── File da rifiutare ────────────────────────────────────── + + public function test_un_xml_estraneo_viene_rifiutato_senza_toccare_i_dati(): void + { + Territorio::create(['numero' => '1', 'attivo' => true]); + + $esito = $this->importa('https://example.org'); + + $this->assertStringContainsString('non è un export di TerManager2', $esito['error']); + $this->assertSame(1, Territorio::count()); + } + + public function test_un_export_senza_le_sezioni_principali_viene_rifiutato(): void + { + Territorio::create(['numero' => '1', 'attivo' => true]); + + $esito = $this->importa(''); + + $this->assertStringContainsString('mancano le sezioni', $esito['error']); + $this->assertSame(1, Territorio::count()); + } + + public function test_un_file_malformato_indica_riga_e_motivo(): void + { + $esito = $this->importa("\n\n\n"); + + $this->assertStringContainsString('riga', $esito['error']); + } + + // ─── Chi importa non resta fuori ──────────────────────────── + + /** + * Il file può contenere l'account di chi importa con una password vecchia + * e senza ruoli: l'amministratore che lo carica non deve perdere né l'una + * né gli altri. + */ + public function test_chi_importa_conserva_password_e_ruolo_di_amministratore(): void + { + $hashVecchio = Hash::make('password-di-un-anno-fa'); + $xml = $this->xmlMinimo( + utenti: 'Amministratoreadmin@example.test' . $hashVecchio . '' + . 'Altroaltro@example.test' . $hashVecchio . 'assistente', + ); + + $this->assertArrayNotHasKey('error', $this->importa($xml)); + + $admin = $this->admin->fresh(); + $this->assertTrue(Hash::check('password-attuale', $admin->password)); + $this->assertTrue($admin->hasRole('amministratore')); + + // Gli altri account invece prendono i dati del file. + $altro = User::where('email', 'altro@example.test')->sole(); + $this->assertTrue(Hash::check('password-di-un-anno-fa', $altro->password)); + $this->assertTrue($altro->hasRole('assistente')); + } + + // ─── helper ───────────────────────────────────────────────── + + private function reimporta(?string $xml = null): void + { + $xml ??= (new XmlExchange())->currentXmlBackup(); + + $esito = $this->importa($xml); + + $this->assertArrayNotHasKey('error', $esito, $esito['error'] ?? ''); + $this->assertSame([], $esito['issues'], json_encode($esito['issues'])); + } + + private function importa(string $xml): array + { + return (new XmlExchange())->importXmlFromContent($xml); + } + + private function xmlMinimo(string $utenti = '', string $anni = '', string $assegnazioni = ''): string + { + return '' + . '' . $utenti . '' + . 'MarioRossi1' + . '11' + . '' . $anni . '' + . '' . $assegnazioni . '' + . ''; + } +}