Files
termanager2/app/Livewire/Richieste/ModuloRichiesta.php
Francesco Picone d198ad0b54 ++ Modulo pubblico: nella riconsegna il territorio si sceglie con il completamento automatico
Elenco con solo numero e zona (territori attivi e quelli ancora fuori),
mostrato solo per la riconsegna; il server accetta solo un numero
dell'elenco. Privacy, README e HANDOFF aggiornati.

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

138 lines
4.8 KiB
PHP

<?php
namespace App\Livewire\Richieste;
use App\Models\Richiesta;
use App\Models\Setting;
use App\Models\Territorio;
use App\Services\RichiesteServizio;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Livewire\Attributes\Locked;
use Livewire\Component;
/**
* Il modulo pubblico (/richiesta, senza login): un proclamatore chiede un
* territorio nuovo o annuncia una riconsegna. L'unico dato già presente che
* mostra è l'elenco dei territori nella riconsegna, per il completamento
* automatico del numero: numero e zona, mai a chi è assegnato né se lo è.
* Nomi e altri dati non si vedono.
*
* Contro lo spam, tre difese che a chi compila davvero non costano niente:
* - un campo trappola ($sito) nascosto alle persone e riempito dai programmi;
* - un tempo minimo fra apertura e invio (i programmi inviano subito);
* - al massimo MASSIMO_ORA richieste all'ora per indirizzo.
* Nei primi due casi si risponde come se fosse andata bene, senza salvare:
* chi prova non impara cosa l'ha fermato.
*/
class ModuloRichiesta extends Component
{
public const MASSIMO_ORA = 5;
public const SECONDI_MINIMI = 3;
public string $tipo = Richiesta::NUOVO;
public string $nome = '';
public string $numero = '';
public string $note = '';
/** Trappola: nascosto alle persone, i programmi lo riempiono. */
public string $sito = '';
#[Locked]
public int $apertoAlle = 0;
#[Locked]
public bool $inviata = false;
public function mount(): void
{
$this->apertoAlle = now()->timestamp;
}
public function invia(RichiesteServizio $richieste): void
{
if ($this->sito !== '' || now()->timestamp - $this->apertoAlle < self::SECONDI_MINIMI) {
$this->inviata = true;
return;
}
$chiave = 'richiesta-pubblica:' . sha1((string) request()->ip());
if (RateLimiter::tooManyAttempts($chiave, self::MASSIMO_ORA)) {
$this->addError('nome', "Hai già mandato diverse richieste: riprova fra un po'.");
return;
}
$this->validate([
'tipo' => 'required|in:' . Richiesta::NUOVO . ',' . Richiesta::RICONSEGNA,
'nome' => 'required|string|min:3|max:100',
'numero' => ['nullable', 'required_if:tipo,' . Richiesta::RICONSEGNA, 'string', 'max:20',
function ($attributo, $valore, $errore) {
if ($this->tipo === Richiesta::RICONSEGNA && ! self::territori()->whereRaw('LOWER(numero) = ?', [Str::lower(trim((string) $valore))])->exists()) {
$errore("Scegli il territorio dall'elenco.");
}
}],
'note' => 'nullable|string|max:1000',
], [
'nome.required' => 'Scrivi nome e cognome.',
'nome.min' => 'Scrivi nome e cognome.',
'numero.required_if' => 'Scegli il territorio che riconsegni.',
]);
RateLimiter::hit($chiave, 3600);
$richieste->registra($this->tipo, $this->nome, $this->numero, $this->note);
$this->reset(['nome', 'numero', 'note']);
$this->inviata = true;
}
public function nuova(): void
{
$this->reset(['tipo', 'nome', 'numero', 'note', 'inviata']);
$this->apertoAlle = now()->timestamp;
}
/**
* I territori che si possono riconsegnare: quelli attivi e quelli ancora
* fuori anche se nel frattempo disattivati.
*/
private static function territori(): Builder
{
return Territorio::query()->where(fn ($q) => $q->where('attivo', true)
->orWhereHas('assegnazioni', fn ($a) => $a->whereNull('returned_at')));
}
/**
* Le proposte del campo con ricerca (<x-scelta>): solo numero e zona. Il
* valore è il numero, come lo salva la richiesta.
*/
private static function opzioniTerritori(): array
{
return self::territori()->with('zona')->get()
->sortBy('numero', SORT_NATURAL)
->map(fn (Territorio $t) => array_filter([
'valore' => (string) $t->numero,
'etichetta' => "N° {$t->numero}",
'dettaglio' => $t->zona?->nome,
'chiavi' => [(string) $t->numero, (string) $t->zona?->nome],
], fn ($v) => $v !== null))
->values()
->all();
}
public function render()
{
return view('livewire.richieste.modulo-richiesta', [
'congregazione' => Setting::instance()->congregazione_nome ?: 'TerManager2',
// Solo quando serve: chi chiede un territorio nuovo non vede l'elenco.
'opzioniTerritori' => $this->tipo === Richiesta::RICONSEGNA && ! $this->inviata ? self::opzioniTerritori() : [],
])->layout('components.layouts.guest', ['title' => 'Richiesta territorio']);
}
}