From d198ad0b54419dd3567e56fea93b1a6c6f92ec6b Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Wed, 21 Oct 2026 17:49:08 +0200 Subject: [PATCH] ++ 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 --- HANDOFF.md | 14 ++++-- README.md | 8 +++- app/Livewire/Richieste/ModuloRichiesta.php | 48 +++++++++++++++++-- resources/views/livewire/privacy.blade.php | 2 +- .../richieste/modulo-richiesta.blade.php | 8 ++-- tests/Feature/RichiesteTest.php | 31 ++++++++++++ 6 files changed, 96 insertions(+), 15 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index f32ad61..e411b21 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -56,7 +56,7 @@ hanno dati da convertire. | Sviluppo locale | ✅ stesso compose, profilo `dev` | | Backup / ripristino / azzeramento | ✅ verificati end-to-end | | Ripristino da browser in linea | ⚠️ **limitato a 100 MB da Cloudflare** — vedi [Cosa resta aperto](#cosa-resta-aperto) | -| Test automatici | ✅ **612 test PHP + 13 JavaScript, verdi** (21/10/2026); copertura ancora parziale | +| Test automatici | ✅ **613 test PHP + 13 JavaScript, verdi** (21/10/2026); copertura ancora parziale | | Prove nel browser | ⚠️ le funzioni del 19-21 ottobre sono verificate dai test e sul server; nel browser le usa l'utente sulla demo (richieste, cartacei e S-13 non ancora provati da lui) | La storia precedente (luglio-agosto: backup e ripristino, sotto-app Piantine, @@ -76,8 +76,14 @@ Dal documento `_ARCHIVE/ATTIVITA sviluppo Territori Manager 2.0.docx` - **Modulo pubblico** `/richiesta` (rotta `richiesta`, fuori dal login, `throttle:60,1`; componente `Richieste\ModuloRichiesta`, layout guest): - nome e cognome, *nuovo territorio* o *riconsegna* (con numero), nota. Non - mostra dati esistenti. Anti-spam: campo trappola `sito` (classe + nome e cognome, *nuovo territorio* o *riconsegna* (con numero), nota. + Nella riconsegna il numero si sceglie con `` (richiesta + dell'utente del 21/10: "il territorio fallo inserire con completamento + automatico"): opzioni con solo numero e zona, valore = numero; i territori + attivi più quelli disattivati ma ancora fuori; l'elenco va nella pagina + solo con *riconsegna* scelta. Il server accetta solo un numero di + quell'elenco (confronto senza maiuscole). Nessun altro dato esistente: né + nomi né stato delle assegnazioni. Anti-spam: campo trappola `sito` (classe `.tm-trappola`), tempo minimo `SECONDI_MINIMI` = 3 s (in questi due casi risponde "inviata" senza salvare), `MASSIMO_ORA` = 5 per IP (chiave del RateLimiter con lo sha1 dell'IP). Il tempo si legge con `now()`, così i @@ -1754,7 +1760,7 @@ blocchi che impedivano di scrivere il primo test sono stati rimossi: stage docker compose --profile test run --build --rm test ``` -**Coperto oggi — 612 test PHP e 13 JavaScript, tutti verdi (21/10/2026).** +**Coperto oggi — 613 test PHP e 13 JavaScript, tutti verdi (21/10/2026).** Senza Docker in locale si fanno girare sul server: vedi [Dove gira davvero](#dove-gira-davvero). I file storici, con cosa proteggono: diff --git a/README.md b/README.md index b01cedf..e9bba3d 100644 --- a/README.md +++ b/README.md @@ -767,8 +767,12 @@ docker compose --profile dev up -d --build ### Richieste dal modulo pubblico - **Modulo pubblico** all'indirizzo `/richiesta`, senza login (l'indirizzo completo è scritto nella Home, da passare ai proclamatori): nome e cognome, - *nuovo territorio* o *riconsegna* (con il numero del territorio), una nota. - Non mostra nessun dato già presente + *nuovo territorio* o *riconsegna*, una nota. Nella riconsegna il territorio + si sceglie con il **completamento automatico**: si scrive il numero o la + zona e compaiono i territori (attivi, o disattivati ma ancora fuori), con + numero e zona soltanto; si accetta solo un territorio dell'elenco. A parte + questo elenco il modulo non mostra nessun dato già presente: né nomi, né a + chi è assegnato un territorio o se lo è - **Contro lo spam**: un campo trappola nascosto, un tempo minimo fra apertura e invio (in questi due casi risponde "inviata" senza salvare) e al massimo 5 richieste all'ora dallo stesso indirizzo diff --git a/app/Livewire/Richieste/ModuloRichiesta.php b/app/Livewire/Richieste/ModuloRichiesta.php index 4313297..2fd065a 100644 --- a/app/Livewire/Richieste/ModuloRichiesta.php +++ b/app/Livewire/Richieste/ModuloRichiesta.php @@ -4,15 +4,20 @@ 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. Non mostra nessun dato già - * presente: chi lo apre vede solo i campi da compilare. + * 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; @@ -67,12 +72,17 @@ class ModuloRichiesta extends Component $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', + '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' => 'Scrivi il numero del territorio che riconsegni.', + 'numero.required_if' => 'Scegli il territorio che riconsegni.', ]); RateLimiter::hit($chiave, 3600); @@ -88,10 +98,40 @@ class ModuloRichiesta extends Component $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 (): 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']); } } diff --git a/resources/views/livewire/privacy.blade.php b/resources/views/livewire/privacy.blade.php index 037e409..405a705 100644 --- a/resources/views/livewire/privacy.blade.php +++ b/resources/views/livewire/privacy.blade.php @@ -61,7 +61,7 @@ @endforeach

Il proclamatore non ha un account: riceve solo il link alla piantina del territorio che gli è assegnato. Fa eccezione il sorvegliante di un gruppo di servizio, che ha un'utenza (email fittizia e password) con cui vede solo il proprio gruppo: i territori del suo pacchetto e i nomi dei membri.

-

Il modulo pubblico di richiesta non mostra nessun dato: chi lo compila scrive soltanto. Le richieste le vedono e le gestiscono, dalla Home, solo i ruoli con il permesso sulle richieste.

+

Il modulo pubblico di richiesta non mostra dati personali: chi annuncia una riconsegna sceglie il territorio da un elenco con solo numero e zona, senza sapere a chi è assegnato né se lo è. Le richieste le vedono e le gestiscono, dalla Home, solo i ruoli con il permesso sulle richieste.

diff --git a/resources/views/livewire/richieste/modulo-richiesta.blade.php b/resources/views/livewire/richieste/modulo-richiesta.blade.php index 0012cce..9778a65 100644 --- a/resources/views/livewire/richieste/modulo-richiesta.blade.php +++ b/resources/views/livewire/richieste/modulo-richiesta.blade.php @@ -1,6 +1,7 @@
{{-- Modulo pubblico (ModuloRichiesta): nessun dato già presente si vede - qui, solo i campi da compilare. --}} + qui, tranne l'elenco dei territori (numero e zona) per scegliere + quello da riconsegnare. --}}
T2

Richiesta territorio

@@ -40,9 +41,8 @@ @if($tipo === \App\Models\Richiesta::RICONSEGNA)
- - + + @error('numero')

{{ $message }}

@enderror
@endif diff --git a/tests/Feature/RichiesteTest.php b/tests/Feature/RichiesteTest.php index 0470f07..ec3d40d 100644 --- a/tests/Feature/RichiesteTest.php +++ b/tests/Feature/RichiesteTest.php @@ -15,6 +15,7 @@ use App\Models\Richiesta; use App\Models\Setting; use App\Models\Territorio; use App\Models\User; +use App\Models\Zona; use App\Notifications\RichiestaRicevuta; use App\Services\RichiesteServizio; use Database\Seeders\RolesAndPermissionsSeeder; @@ -146,6 +147,7 @@ class RichiesteTest extends TestCase public function test_senza_posta_la_richiesta_resta_comunque(): void { Notification::fake(); + $this->territorio('12'); auth()->logout(); $this->compila(['tipo' => Richiesta::RICONSEGNA, 'nome' => 'Mario Rossi', 'numero' => '12'])->assertHasNoErrors(); @@ -162,6 +164,35 @@ class RichiesteTest extends TestCase $this->assertSame(0, Richiesta::count()); } + public function test_nella_riconsegna_il_territorio_si_sceglie_dall_elenco(): void + { + $zona = Zona::create(['nome' => 'Centro storico']); + Territorio::create(['numero' => '12', 'zona_id' => $zona->id, 'attivo' => true]); + Territorio::create(['numero' => '3B', 'attivo' => true]); + $fuori = Territorio::create(['numero' => '40', 'attivo' => true]); + $this->assegna($fuori, $this->proclamatore('Verdi', 'Anna'), '2026-10-01'); + $fuori->update(['attivo' => false]); + Territorio::create(['numero' => '77', 'attivo' => false]); + auth()->logout(); + + // L'elenco c'è solo nella riconsegna: numero e zona, nient'altro. + $modulo = Livewire::test(ModuloRichiesta::class) + ->assertViewHas('opzioniTerritori', []) + ->set('tipo', Richiesta::RICONSEGNA) + ->assertSee('Centro storico') + ->assertDontSee('Verdi') + ->assertDontSee('assegnat'); + $this->assertSame(['3B', '12', '40'], array_column($modulo->viewData('opzioniTerritori'), 'valore'), + 'In ordine di numero; c\'è anche quello disattivato ma ancora fuori, non quello disattivato e rientrato'); + + $this->compila(['tipo' => Richiesta::RICONSEGNA, 'nome' => 'Anna Verdi', 'numero' => '99'])->assertHasErrors('numero'); + $this->compila(['tipo' => Richiesta::RICONSEGNA, 'nome' => 'Anna Verdi', 'numero' => '77'])->assertHasErrors('numero'); + $this->assertSame(0, Richiesta::count()); + + $this->compila(['tipo' => Richiesta::RICONSEGNA, 'nome' => 'Anna Verdi', 'numero' => '3b'])->assertHasNoErrors(); + $this->assertSame('3b', Richiesta::sole()->numero_territorio); + } + public function test_lo_spam_non_si_salva_e_non_se_ne_accorge(): void { // Il campo trappola riempito.