S-13 sul modulo ufficiale e correzioni all'integrità dei dati

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 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 16:35:51 +02:00
parent 9c37c1ad7b
commit d433c9392d
17 changed files with 1361 additions and 457 deletions

View File

@@ -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()

View File

@@ -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()