Files
termanager2/app/Livewire/Assegnazioni/Assegna.php
Francesco Picone fc8623f7db Posta configurabile dall'app, contatti dei proclamatori, conferma email
- Server di posta in Impostazioni (password cifrata, email di prova): se
  compilato prende il posto del .env; non si esporta e l'import XML non lo
  cancella. MAIL_SCHEME=tls nel .env.example non era valido per Laravel 11.
- Proclamatori con email e telefono facoltativi, cifrati, nell'XML e
  cancellati dall'anonimizzazione; il numero apre WhatsApp.
- Dopo l'assegnazione un riquadro in evidenza con il link del PDF,
  "Invia su WhatsApp" con il messaggio pronto e la scelta "inviato /
  non ancora", che resta finché non si sceglie.
- Conferma dell'email obbligatoria per tutti gli utenti, anche esistenti:
  email in italiano, link valido senza login, nuova conferma a ogni cambio
  di indirizzo, "Reinvia conferma" in Utenti. Senza conferma restano
  raggiungibili profilo, cambio password e, per l'admin, il server di
  posta; comando utenti:conferma-email per l'emergenza.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:12:31 +02:00

222 lines
8.8 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;
class Assegna extends Component
{
/** Chiave di sessione delle assegnazioni appena fatte, in attesa della scelta sul link. */
public const DA_INVIARE = 'assegnazioni_da_inviare';
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)
// L'id, non il nome: nome e cognome sono cifrati nel database, e
// copiati qui in chiaro restavano leggibili anche dopo
// l'anonimizzazione. Il registro mostra il nome di oggi.
->withProperties([
'territorio' => $territorio->numero,
'proclamatore_id' => $proclamatore->id,
])
->log('assigned');
// Si resta qui: in cima compare il riquadro con il link da mandare al
// proclamatore (WhatsApp) e la scelta "inviato / non ancora". La lista
// sta in sessione, così il riquadro resta finché non si sceglie, anche
// ricaricando la pagina o assegnando altri territori nel frattempo.
$daInviare = session(self::DA_INVIARE, []);
$daInviare[] = $assegnazione->id;
session([self::DA_INVIARE => array_values(array_unique($daInviare))]);
session()->flash('success', "Territorio {$territorio->numero} assegnato a {$proclamatore->nome_completo}.");
return $this->redirect(route('assegnazioni.assegna'), navigate: true);
}
/** La scelta nel riquadro: il link è stato mandato, oppure non ancora. */
public function esitoInvio(int $assegnazioneId, bool $inviato): void
{
$this->authorize('territori.assign');
$assegnazione = Assegnazione::find($assegnazioneId);
$assegnazione?->forceFill(['link_sent' => $inviato])->saveQuietly();
session([self::DA_INVIARE => array_values(array_diff(session(self::DA_INVIARE, []), [$assegnazioneId]))]);
}
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?->miniaturaUrl();
}
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));
// I riquadri in evidenza: solo le assegnazioni ancora aperte (una
// rientrata o cancellata nel frattempo non ha più un link da mandare).
$idDaInviare = session(self::DA_INVIARE, []);
$daInviare = $idDaInviare === [] ? collect() : Assegnazione::aperte()
->whereIn('id', $idDaInviare)
->with(['territorio', 'proclamatore'])
->get();
if ($daInviare->count() !== count($idDaInviare)) {
session([self::DA_INVIARE => $daInviare->pluck('id')->all()]);
}
return view('livewire.assegnazioni.assegna', [
'daInviare' => $daInviare,
'territoriDisponibili' => $territoriDisponibili,
'proclamatoriAttivi' => $proclamatoriAttivi,
'assegnazioniAperte' => $assegnazioniAperte,
'assegnazioniAperteVisibili' => $assegnazioniAperteVisibili,
'sogliaRientro' => $sogliaRientro,
]);
}
}