Files
termanager2/app/Livewire/Assegnazioni/Assegna.php
Francesco Picone d433c9392d 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>
2026-10-09 16:35:51 +02:00

186 lines
7.0 KiB
PHP

<?php
namespace App\Livewire\Assegnazioni;
use Livewire\Component;
use Livewire\Attributes\Computed;
use App\Models\Territorio;
use App\Models\Proclamatore;
use App\Models\Assegnazione;
use App\Models\AnnoTeocratico;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class Assegna extends Component
{
public ?int $territorio_id = null;
public ?int $proclamatore_id = null;
public string $assigned_at = '';
public string $territorioSearch = '';
public string $linkSearch = '';
// Optional pre-selection from parent context
public ?int $preselectedTerritorioId = null;
public function mount(?int $territorioId = null)
{
$this->preselectedTerritorioId = $territorioId;
$this->territorio_id = $territorioId;
$this->assigned_at = now()->format('Y-m-d');
}
protected function rules(): array
{
return [
'territorio_id' => 'required|exists:territori,id',
'proclamatore_id' => 'required|exists:proclamatori,id',
'assigned_at' => 'required|date|before_or_equal:today',
];
}
public function save()
{
$this->validate();
$territorio = Territorio::findOrFail($this->territorio_id);
// 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) {
$this->addError('territorio_id', "Il territorio {$territorio->numero} è già assegnato a {$territorio->assegnazioneCorrente->proclamatore->nome_completo}.");
return;
}
if (!$territorio->attivo) {
$this->addError('territorio_id', "Il territorio {$territorio->numero} è inattivo.");
return;
}
$proclamatore = Proclamatore::findOrFail($this->proclamatore_id);
if (!$proclamatore->attivo) {
$this->addError('proclamatore_id', "Il proclamatore {$proclamatore->nome_completo} è inattivo.");
return;
}
$assignedDate = \Carbon\Carbon::parse($this->assigned_at);
$annoTeocratico = AnnoTeocratico::perData($assignedDate);
// 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)
->withProperties([
'territorio' => $territorio->numero,
'proclamatore' => $proclamatore->nome_completo,
])
->log('assigned');
session()->flash('success', "Territorio {$territorio->numero} assegnato a {$proclamatore->nome_completo}.");
return $this->redirect(route('territori.show', $territorio), navigate: true);
}
public function toggleLinkSent(int $assegnazioneId): void
{
$this->authorize('territori.assign');
$assegnazione = Assegnazione::findOrFail($assegnazioneId);
$assegnazione->forceFill(['link_sent' => ! $assegnazione->link_sent])->saveQuietly();
}
#[Computed]
public function selectedThumbnailUrl(): ?string
{
if (!$this->territorio_id) {
return null;
}
$t = Territorio::find($this->territorio_id);
return $t?->thumbnail_path ? Storage::url($t->thumbnail_path) : null;
}
public function render()
{
$territoriQuery = Territorio::where('attivo', true)
->whereDoesntHave('assegnazioni', fn($q) => $q->aperte())
->with(['zona', 'tipologia']);
if (trim($this->territorioSearch) !== '') {
$term = trim($this->territorioSearch);
$territoriQuery->where(function ($q) use ($term) {
$q->where('numero', 'like', "%{$term}%")
->orWhereHas('zona', fn($z) => $z->where('nome', 'like', "%{$term}%"))
->orWhereHas('tipologia', fn($t) => $t->where('nome', 'like', "%{$term}%"));
});
}
$territoriDisponibili = $territoriQuery
->orderBy('numero')
->get();
$proclamatoriAttivi = Proclamatore::attivi()
->get()
->sortBy(fn($p) => mb_strtolower($p->cognome . ' ' . $p->nome));
// All currently assigned territories with links
$assegnazioniAperte = Assegnazione::aperte()
->with(['territorio.zona', 'proclamatore'])
->get()
->sortBy(fn($a) => (int) $a->territorio?->numero);
// Ricerca rapida nella lista dei link: numero territorio, zona,
// proclamatore in un solo campo. In memoria perché nome/cognome del
// proclamatore sono cifrati e non filtrabili via SQL.
$assegnazioniAperteVisibili = $assegnazioniAperte;
if (trim($this->linkSearch) !== '') {
$term = mb_strtolower(trim($this->linkSearch));
$assegnazioniAperteVisibili = $assegnazioniAperte->filter(function ($a) use ($term) {
$haystack = mb_strtolower(
($a->territorio?->numero ?? '') . ' ' .
($a->territorio?->zona?->nome ?? '') . ' ' .
($a->proclamatore?->nome_completo ?? '')
);
return str_contains($haystack, $term);
})->values();
}
// Il link scade alla soglia rientro (giorni_per_smarrito).
$sogliaRientro = max(1, (int) \App\Models\Setting::getValue('giorni_per_smarrito', 120));
return view('livewire.assegnazioni.assegna', [
'territoriDisponibili' => $territoriDisponibili,
'proclamatoriAttivi' => $proclamatoriAttivi,
'assegnazioniAperte' => $assegnazioniAperte,
'assegnazioniAperteVisibili' => $assegnazioniAperteVisibili,
'sogliaRientro' => $sogliaRientro,
]);
}
}