++ 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>
This commit is contained in:
14
HANDOFF.md
14
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 `<x-scelta>` (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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (<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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
@endforeach
|
||||
</ul>
|
||||
<p class="mt-2">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.</p>
|
||||
<p class="mt-2">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.</p>
|
||||
<p class="mt-2">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.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<div class="bg-white shadow-xl rounded-2xl p-6 sm:p-8 my-8" style="border-top:4px solid #6366f1">
|
||||
{{-- 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. --}}
|
||||
<div class="text-center mb-6">
|
||||
<div class="mx-auto h-14 w-14 rounded-xl flex items-center justify-center text-white font-bold text-xl mb-4 shadow-lg" style="background:linear-gradient(135deg,#4338ca,#6366f1)">T2</div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Richiesta territorio</h1>
|
||||
@@ -40,9 +41,8 @@
|
||||
|
||||
@if($tipo === \App\Models\Richiesta::RICONSEGNA)
|
||||
<div>
|
||||
<label for="numero" class="block text-sm font-medium text-gray-700">N° del territorio *</label>
|
||||
<input wire:model="numero" type="text" id="numero" inputmode="numeric" maxlength="20" autocomplete="off"
|
||||
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-base sm:text-sm px-4 py-2.5">
|
||||
<label for="numero" class="block text-sm font-medium text-gray-700">Territorio che riconsegni *</label>
|
||||
<x-scelta id="numero" wire:model="numero" :opzioni="$opzioniTerritori" segnaposto="Scrivi il numero o la zona" class="mt-1" />
|
||||
@error('numero') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user