diff --git a/HANDOFF.md b/HANDOFF.md index 03dcdbc..9e7cef3 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -219,6 +219,21 @@ Decisioni successive dell'utente (20 ottobre 2026): temporanea". Si spegne a password cambiata (pagina obbligatoria o profilo); il ripristino e l'import XML la spengono per tutti, perché lì il motivo è il ripristino. +- **Bug: ogni clic del sorvegliante finiva in 403.** In Livewire 3.7 la + rotta delle azioni si chiama `default.livewire.update`, e + `LimitaSorvegliante` lasciava passare solo `livewire.*`: ora `*livewire.*`. + I test non lo vedevano perché `Livewire::test()` non passa dai middleware + delle rotte; ora un test chiama la rotta vera. +- **Priorità e filtri** (richiesta dell'utente): la regola della Home ("Da + assegnare": prioritari — spunta o giacenza oltre `giorni_giacenza_prioritari` + — poi giacenza, poi numero) è ora `Territorio::ordinaPerPriorita()`, usata + dalla Home, dall'aggiunta al pacchetto (`GruppoShow`: ricerca, zona, + tipologia, solo prioritari, "Seleziona tutti quelli mostrati") e da "Il mio + gruppo" (prima i da dare in ordine di priorità, poi i già dati dal più + vecchio; filtri stato, zona, tipologia, solo prioritari, nell'indirizzo). + Gli attributi calcolati si chiamano `giacenza_giorni` e `in_priorita` + (erano `home_giorni_giacenza` e `home_is_prioritario`, anche nel PDF). +- "Il mio gruppo" mostra sotto il titolo nome e cognome del sorvegliante. - **Anche i membri si segnalano**: `Proclamatore::gruppo_etichetta` ("Gruppo Zisa" / "Sorvegliante del gruppo Zisa") come badge nell'elenco (più il filtro *In un gruppo di servizio*, `filtroStato=gruppo`) e in testa diff --git a/README.md b/README.md index 99f5fec..ee8c3c5 100644 --- a/README.md +++ b/README.md @@ -750,14 +750,21 @@ docker compose --profile dev up -d --build chiede, non riceve posta e non va confermata. Due gruppi non possono avere lo stesso nome. Se perde la password, l'amministratore ne genera un'altra dalla pagina del gruppo -- I territori del pacchetto si aggiungono in blocco tra quelli in reparto. +- I territori del pacchetto si aggiungono in blocco tra quelli in reparto, + **in ordine di priorità** come "Da assegnare" nella Home (prima i + prioritari, poi quelli fermi da più tempo), con ricerca, filtri per zona, + tipologia e "solo prioritari", e "Seleziona tutti quelli mostrati". Restano **"In gruppo …"**: non sono un'assegnazione (statistiche e medie non cambiano), escono da "Da assegnare" e dalla pagina *Assegna* - Il sorvegliante entra e vede **solo "Il mio gruppo"** e il proprio profilo (ogni altra pagina risponde 403): dà i territori ai membri — o a sé — e ne nasce un'**assegnazione normale**, che conta come le altre ed è segnata "via gruppo …"; poi manda il link (copia o condividi), registra i rientri, - apre la piantina e rimanda in reparto quelli non ancora dati + apre la piantina e rimanda in reparto quelli non ancora dati. Il suo + pacchetto è in ordine di priorità (prima i da dare, prioritari in testa, + poi quelli già dati) e si filtra per stato, zona, tipologia e "solo + prioritari"; sotto il nome del gruppo compaiono nome e cognome del + sorvegliante - Al **rientro** il territorio torna in reparto ed esce dal pacchetto - **Sciogliere il gruppo** toglie il ruolo al sorvegliante e **elimina il gruppo**, che non resta nello storico: i territori solo nel pacchetto diff --git a/app/Http/Middleware/LimitaSorvegliante.php b/app/Http/Middleware/LimitaSorvegliante.php index b0071aa..724e555 100644 --- a/app/Http/Middleware/LimitaSorvegliante.php +++ b/app/Http/Middleware/LimitaSorvegliante.php @@ -28,7 +28,10 @@ class LimitaSorvegliante 'password.change.store', 'logout', 'verification.*', - 'livewire.*', + // Le azioni Livewire: la rotta si chiama "default.livewire.update" + // (Livewire 3.7), non "livewire.update". Con 'livewire.*' ogni clic + // del sorvegliante finiva in 403. + '*livewire.*', ]; public function handle(Request $request, Closure $next): Response diff --git a/app/Livewire/Gruppi/GruppoShow.php b/app/Livewire/Gruppi/GruppoShow.php index 0056824..2bec168 100644 --- a/app/Livewire/Gruppi/GruppoShow.php +++ b/app/Livewire/Gruppi/GruppoShow.php @@ -32,6 +32,16 @@ class GruppoShow extends Component /** @var int[] territori da aggiungere al pacchetto */ public array $nuoviTerritori = []; + // Filtri dei territori in reparto da aggiungere al pacchetto. L'ordine è + // quello di priorità (Territorio::ordinaPerPriorita), come nella Home. + public string $cercaTerritorio = ''; + + public string $filtroZona = ''; + + public string $filtroTipologia = ''; + + public bool $soloPrioritari = false; + #[Locked] public ?string $passwordTemporanea = null; @@ -91,6 +101,45 @@ class GruppoShow extends Component $this->nuoviTerritori = []; } + /** Tutti i territori che i filtri mostrano finiscono tra quelli da aggiungere. */ + public function selezionaMostrati(): void + { + $this->nuoviTerritori = collect($this->nuoviTerritori) + ->map(fn ($id) => (string) $id) + ->merge($this->candidatiFiltrati()->pluck('id')->map(fn ($id) => (string) $id)) + ->unique() + ->values() + ->all(); + } + + public function deselezionaTutti(): void + { + $this->nuoviTerritori = []; + } + + /** I territori in reparto, in ordine di priorità. */ + private function candidati(): \Illuminate\Support\Collection + { + return Territorio::ordinaPerPriorita( + Territorio::inReparto()->with(['zona', 'tipologia', 'ultimaAssegnazione'])->get() + ); + } + + private function candidatiFiltrati(?\Illuminate\Support\Collection $candidati = null): \Illuminate\Support\Collection + { + $cerca = Str::lower(Str::ascii(trim($this->cercaTerritorio))); + + return ($candidati ?? $this->candidati()) + ->when($this->filtroZona !== '', fn ($c) => $c->where('zona_id', (int) $this->filtroZona)) + ->when($this->filtroTipologia !== '', fn ($c) => $c->where('tipologia_id', (int) $this->filtroTipologia)) + ->when($this->soloPrioritari, fn ($c) => $c->where('in_priorita', true)) + ->when($cerca !== '', fn ($c) => $c->filter(fn (Territorio $t) => str_contains( + Str::lower(Str::ascii("{$t->numero} {$t->zona?->nome} {$t->tipologia?->nome}")), + $cerca + ))) + ->values(); + } + public function rimanda(int $territorioId): void { $territorio = Territorio::findOrFail($territorioId); @@ -135,13 +184,21 @@ class GruppoShow extends Component $membri = $this->gruppo->membri()->get()->sortBy(fn ($p) => mb_strtolower($p->cognome . ' ' . $p->nome))->values(); $territori = $this->gruppo->territori()->with(['zona', 'assegnazioneCorrente.proclamatore'])->get()->sortBy('numero', SORT_NATURAL)->values(); + // I territori in reparto (attivi, non assegnati, fuori da ogni + // pacchetto), in ordine di priorità; zone e tipologie dei filtri sono + // quelle che ci sono tra loro. + $candidati = $this->candidati(); + return view('livewire.gruppi.gruppo-show', [ 'membri' => $membri, 'territori' => $territori, // Si possono aggiungere i proclamatori attivi senza gruppo... 'candidatiMembri' => Proclamatore::attivi()->whereNull('gruppo_servizio_id')->get()->sortBy(fn ($p) => mb_strtolower($p->cognome . ' ' . $p->nome))->values(), - // ...e i territori in reparto (attivi, non assegnati, fuori da ogni pacchetto). - 'candidatiTerritori' => Territorio::inReparto()->with('zona')->get()->sortBy('numero', SORT_NATURAL)->values(), + // ...e i territori in reparto. + 'totaleCandidati' => $candidati->count(), + 'candidatiTerritori' => $this->candidatiFiltrati($candidati), + 'zoneCandidati' => $candidati->filter(fn ($t) => $t->zona)->mapWithKeys(fn ($t) => [$t->zona_id => $t->zona->nome])->sort(), + 'tipologieCandidati' => $candidati->filter(fn ($t) => $t->tipologia)->mapWithKeys(fn ($t) => [$t->tipologia_id => $t->tipologia->nome])->sort(), ]); } } diff --git a/app/Livewire/Gruppi/MioGruppo.php b/app/Livewire/Gruppi/MioGruppo.php index 99280a2..86593bb 100644 --- a/app/Livewire/Gruppi/MioGruppo.php +++ b/app/Livewire/Gruppi/MioGruppo.php @@ -12,6 +12,7 @@ use App\Services\GruppiServizio; use Carbon\Carbon; use InvalidArgumentException; use Livewire\Attributes\Locked; +use Livewire\Attributes\Url; use Livewire\Component; /** @@ -35,6 +36,20 @@ class MioGruppo extends Component public string $dataAssegnazione = ''; + // Filtri del pacchetto, nell'indirizzo: tornando dal rientro si ritrovano. + #[Url(except: '')] + public string $filtroZona = ''; + + #[Url(except: '')] + public string $filtroTipologia = ''; + + /** '' tutti, 'da_dare', 'dati' */ + #[Url(except: '')] + public string $filtroStato = ''; + + #[Url(except: false)] + public bool $soloPrioritari = false; + public function mount(): void { $this->dataAssegnazione = now()->format('Y-m-d'); @@ -170,11 +185,25 @@ class MioGruppo extends Component public function render() { - $gruppo = $this->gruppo(); + $gruppo = $this->gruppo()?->load('sorvegliante'); - $territori = $gruppo - ? $gruppo->territori()->with(['zona', 'assegnazioneCorrente.proclamatore'])->get()->sortBy('numero', SORT_NATURAL)->values() + // Prima quelli da dare, in ordine di priorità come "Da assegnare" + // nella Home (Territorio::ordinaPerPriorita); poi quelli già dati, dal + // più vecchio. + $pacchetto = $gruppo + ? $gruppo->territori()->with(['zona', 'tipologia', 'ultimaAssegnazione', 'assegnazioneCorrente.proclamatore'])->get() : collect(); + [$dati, $daDare] = $pacchetto->partition(fn (Territorio $t) => $t->assegnazioneCorrente !== null); + $pacchetto = Territorio::ordinaPerPriorita($daDare) + ->concat($dati->sortBy(fn (Territorio $t) => $t->assegnazioneCorrente->assigned_at)->values()); + + $territori = $pacchetto + ->when($this->filtroZona !== '', fn ($c) => $c->where('zona_id', (int) $this->filtroZona)) + ->when($this->filtroTipologia !== '', fn ($c) => $c->where('tipologia_id', (int) $this->filtroTipologia)) + ->when($this->filtroStato === 'da_dare', fn ($c) => $c->whereNull('assegnazioneCorrente')) + ->when($this->filtroStato === 'dati', fn ($c) => $c->whereNotNull('assegnazioneCorrente')) + ->when($this->soloPrioritari, fn ($c) => $c->where('in_priorita', true)) + ->values(); $membri = $gruppo ? $gruppo->membri()->where('attivo', true)->get()->sortBy(fn ($p) => mb_strtolower($p->cognome . ' ' . $p->nome))->values() @@ -182,7 +211,10 @@ class MioGruppo extends Component return view('livewire.gruppi.mio-gruppo', [ 'gruppo' => $gruppo, + 'pacchetto' => $pacchetto, 'territori' => $territori, + 'zone' => $pacchetto->filter(fn ($t) => $t->zona)->mapWithKeys(fn ($t) => [$t->zona_id => $t->zona->nome])->sort(), + 'tipologie' => $pacchetto->filter(fn ($t) => $t->tipologia)->mapWithKeys(fn ($t) => [$t->tipologia_id => $t->tipologia->nome])->sort(), 'membri' => $membri, 'opzioniMembri' => Proclamatore::opzioniScelta($membri, conGruppo: false), 'invio' => $this->invioCorrente(), diff --git a/app/Livewire/Home.php b/app/Livewire/Home.php index c306a3e..9b2645b 100644 --- a/app/Livewire/Home.php +++ b/app/Livewire/Home.php @@ -18,35 +18,10 @@ class Home extends Component $this->authorize('territori.assign'); $settings = Setting::instance(); - $priorityThreshold = (int) ($settings->giorni_giacenza_prioritari ?? 180); - $territori = Territorio::inReparto() - ->with('zona', 'tipologia', 'ultimaAssegnazione') - ->get() - ->map(function (Territorio $territorio) use ($priorityThreshold) { - $ultima = $territorio->ultimaAssegnazione; - if ($ultima && $ultima->returned_at) { - $giorniGiacenza = $ultima->returned_at->startOfDay()->diffInDays(today()); - } elseif (! $ultima) { - $giorniGiacenza = $territorio->created_at->startOfDay()->diffInDays(today()); - } else { - $giorniGiacenza = 0; - } - $territorio->setAttribute('home_giorni_giacenza', $giorniGiacenza); - $territorio->setAttribute( - 'home_is_prioritario', - (bool) $territorio->prioritario || $giorniGiacenza > $priorityThreshold - ); - return $territorio; - }) - ->sort(function (Territorio $left, Territorio $right) { - $p = (int) $right->home_is_prioritario <=> (int) $left->home_is_prioritario; - if ($p !== 0) return $p; - $g = $right->home_giorni_giacenza <=> $left->home_giorni_giacenza; - if ($g !== 0) return $g; - return strnatcasecmp((string) $left->numero, (string) $right->numero); - }) - ->values(); + $territori = Territorio::ordinaPerPriorita( + Territorio::inReparto()->with('zona', 'tipologia', 'ultimaAssegnazione')->get() + ); $pdf = Pdf::loadView('pdf.territori-lista', [ 'titolo' => 'Territori da Assegnare', @@ -90,7 +65,6 @@ class Home extends Component { $settings = Setting::instance(); $homeLimit = max(1, (int) ($settings->home_limit_list ?? 10)); - $priorityThreshold = (int) ($settings->giorni_giacenza_prioritari ?? 180); $annoCorrente = AnnoTeocratico::corrente(); $campagnaAttiva = Campagna::attiva(); @@ -149,46 +123,11 @@ class Home extends Component ]; } - // Quick lists - $territoriDaAssegnare = Territorio::inReparto() - ->with('zona', 'tipologia', 'ultimaAssegnazione') - ->get() - ->map(function (Territorio $territorio) use ($priorityThreshold) { - $ultima = $territorio->ultimaAssegnazione; - - if ($ultima && $ultima->returned_at) { - $giorniGiacenza = $ultima->returned_at->startOfDay()->diffInDays(today()); - } elseif (! $ultima) { - $giorniGiacenza = $territorio->created_at->startOfDay()->diffInDays(today()); - } else { - $giorniGiacenza = 0; - } - - $territorio->setAttribute('home_giorni_giacenza', $giorniGiacenza); - $territorio->setAttribute( - 'home_is_prioritario', - (bool) $territorio->prioritario || $giorniGiacenza > $priorityThreshold - ); - - return $territorio; - }) - ->sort(function (Territorio $left, Territorio $right) { - $priorityComparison = (int) $right->home_is_prioritario <=> (int) $left->home_is_prioritario; - - if ($priorityComparison !== 0) { - return $priorityComparison; - } - - $giacenzaComparison = $right->home_giorni_giacenza <=> $left->home_giorni_giacenza; - - if ($giacenzaComparison !== 0) { - return $giacenzaComparison; - } - - return strnatcasecmp((string) $left->numero, (string) $right->numero); - }) - ->take($homeLimit) - ->values(); + // Quick lists: la logica di priorità sta in Territorio::ordinaPerPriorita + // (la usano anche il pacchetto dei gruppi di servizio e "Il mio gruppo"). + $territoriDaAssegnare = Territorio::ordinaPerPriorita( + Territorio::inReparto()->with('zona', 'tipologia', 'ultimaAssegnazione')->get() + )->take($homeLimit)->values(); $daRientrare = Territorio::daRientrare() ->with(['zona', 'assegnazioneCorrente.proclamatore']) diff --git a/app/Models/Territorio.php b/app/Models/Territorio.php index 349ce80..85ff8a7 100644 --- a/app/Models/Territorio.php +++ b/app/Models/Territorio.php @@ -169,6 +169,41 @@ class Territorio extends Model return false; } + /** + * La logica di priorità dei territori da distribuire, la stessa ovunque + * (Home "Da assegnare", pacchetto di un gruppo, "Il mio gruppo"): prima i + * prioritari — spunta manuale o fermi da più giorni della soglia + * "giorni_giacenza_prioritari" — poi chi è fermo da più tempo, poi il + * numero. Su ogni territorio imposta giacenza_giorni (dall'ultimo rientro, + * o dalla creazione se mai assegnato; 0 se è fuori) e in_priorita, da + * mostrare. Vuole 'ultimaAssegnazione' già caricata, per non fare una + * query a territorio. + */ + public static function ordinaPerPriorita(iterable $territori): \Illuminate\Support\Collection + { + $soglia = (int) (Setting::instance()->giorni_giacenza_prioritari ?? 180); + + return collect($territori) + ->each(function (self $territorio) use ($soglia) { + $ultima = $territorio->ultimaAssegnazione; + if ($ultima && $ultima->returned_at) { + $giorni = (int) $ultima->returned_at->startOfDay()->diffInDays(today()); + } elseif (! $ultima) { + $giorni = (int) $territorio->created_at->startOfDay()->diffInDays(today()); + } else { + $giorni = 0; + } + + $territorio->setAttribute('giacenza_giorni', $giorni); + $territorio->setAttribute('in_priorita', (bool) $territorio->prioritario || $giorni > $soglia); + }) + ->sort(function (self $a, self $b) { + return [(int) $b->in_priorita, $b->giacenza_giorni] <=> [(int) $a->in_priorita, $a->giacenza_giorni] + ?: strnatcasecmp((string) $a->numero, (string) $b->numero); + }) + ->values(); + } + /** * L'opzione per il campo di scelta con ricerca (): si cerca per * numero, zona e tipologia. Con $motivo compare cercando ma non si può diff --git a/resources/views/livewire/gruppi/gruppo-show.blade.php b/resources/views/livewire/gruppi/gruppo-show.blade.php index 65b552c..f3b7295 100644 --- a/resources/views/livewire/gruppi/gruppo-show.blade.php +++ b/resources/views/livewire/gruppi/gruppo-show.blade.php @@ -49,27 +49,65 @@ @endforelse - {{-- Assegnazione in blocco: si spuntano i territori in reparto --}} -
+ {{-- Assegnazione in blocco: si spuntano i territori in reparto, in + ordine di priorità (come "Da assegnare" nella Home), filtrabili. --}} +

Aggiungi territori al pacchetto

- @if($candidatiTerritori->isEmpty()) + @if($totaleCandidati === 0)

Non ci sono territori in reparto da aggiungere.

@else - -
- @foreach($candidatiTerritori as $c) - - @endforeach +
+ + + +
-
+
+ {{ $candidatiTerritori->count() }} di {{ $totaleCandidati }} in reparto, prima i prioritari e quelli fermi da più tempo + @if($candidatiTerritori->isNotEmpty()) + Seleziona tutti quelli mostrati + @endif + Deseleziona +
+
+ @forelse($candidatiTerritori as $c) + + @empty +

Nessun territorio con questi filtri.

+ @endforelse +
+
Aggiungi i territori selezionati + Selezionati: {{ count($nuoviTerritori) }}
@endif
diff --git a/resources/views/livewire/gruppi/mio-gruppo.blade.php b/resources/views/livewire/gruppi/mio-gruppo.blade.php index 6f3c47c..f9f255a 100644 --- a/resources/views/livewire/gruppi/mio-gruppo.blade.php +++ b/resources/views/livewire/gruppi/mio-gruppo.blade.php @@ -10,11 +10,14 @@ @else

Gruppo {{ $gruppo->nome }}

+ @if($gruppo->sorvegliante) +

Sorvegliante: {{ trim($gruppo->sorvegliante->nome . ' ' . $gruppo->sorvegliante->cognome) }}

+ @endif {{-- In questa vista solo blocchi php…endphp: la forma in linea, se viene prima di un blocco, confonde il compilatore di Blade. --}} - @php $daDare = $territori->filter(fn ($t) => ! $t->assegnazioneCorrente)->count(); @endphp + @php $daDare = $pacchetto->filter(fn ($t) => ! $t->assegnazioneCorrente)->count(); @endphp

- {{ $territori->count() }} {{ $territori->count() === 1 ? 'territorio' : 'territori' }} nel pacchetto · + {{ $pacchetto->count() }} {{ $pacchetto->count() === 1 ? 'territorio' : 'territori' }} nel pacchetto · {{ $daDare }} da dare · {{ $membri->count() }} {{ $membri->count() === 1 ? 'membro' : 'membri' }}

@@ -29,12 +32,43 @@ -
-

Territori del pacchetto

- @if($territori->count() > 5) - +
+
+

Territori del pacchetto

+ @if($pacchetto->count() > 5) + + @endif +
+ {{-- Filtri; l'ordine è di priorità: prima i prioritari e quelli fermi da più tempo --}} + @if($pacchetto->isNotEmpty()) +
+ + + + +
@endif
@@ -47,11 +81,12 @@ @endphp
- N° {{ $t->numero }} @if($t->zona?->nome){{ $t->zona->nome }}@endif + N° {{ $t->numero }} @if($t->zona?->nome){{ $t->zona->nome }}@endif + @if($t->in_priorita)★ Prioritario@endif @if($a) {{ $a->proclamatore?->nome_completo }} · dal {{ $a->assigned_at->format('d/m/Y') }} · {{ $a->giorni }}g @else - Da dare + Da dare@if($t->tipologia?->nome) · {{ $t->tipologia->nome }}@endif @if($t->giacenza_giorni > 0) · fermo da {{ $t->giacenza_giorni }} gg @endif @endif
@@ -101,7 +136,9 @@ @endif
@empty -

Il pacchetto è vuoto: l'amministratore non ha ancora assegnato territori al gruppo.

+

+ {{ $pacchetto->isEmpty() ? "Il pacchetto è vuoto: l'amministratore non ha ancora assegnato territori al gruppo." : 'Nessun territorio con questi filtri.' }} +

@endforelse

Nessun risultato per «».

@@ -111,7 +148,7 @@

Membri del gruppo

    @foreach($membri as $m) - @php $quanti = $territori->filter(fn ($t) => $t->assegnazioneCorrente?->proclamatore_id === $m->id)->count(); @endphp + @php $quanti = $pacchetto->filter(fn ($t) => $t->assegnazioneCorrente?->proclamatore_id === $m->id)->count(); @endphp
  • {{ $m->nome_completo }} @if($m->id === $gruppo->sorvegliante_id)(sorvegliante)@endif {{ $quanti }} {{ $quanti === 1 ? 'territorio' : 'territori' }} diff --git a/resources/views/livewire/home.blade.php b/resources/views/livewire/home.blade.php index b76fa9e..dca2ed0 100644 --- a/resources/views/livewire/home.blade.php +++ b/resources/views/livewire/home.blade.php @@ -110,14 +110,14 @@
    N° {{ $t->numero }} - @if($t->home_is_prioritario) + @if($t->in_priorita) ★ Prioritario @endif

    {{ $t->zona?->nome }} — {{ $t->tipologia?->nome }} - @if($t->home_giorni_giacenza > 0) - — {{ $t->home_giorni_giacenza }} gg in reparto + @if($t->giacenza_giorni > 0) + — {{ $t->giacenza_giorni }} gg in reparto @endif

    diff --git a/resources/views/pdf/territori-lista.blade.php b/resources/views/pdf/territori-lista.blade.php index 058f5e1..5234aa3 100644 --- a/resources/views/pdf/territori-lista.blade.php +++ b/resources/views/pdf/territori-lista.blade.php @@ -44,9 +44,9 @@ {{ $t->numero }} {{ $t->zona?->nome ?? '-' }} {{ $t->tipologia?->nome ?? '-' }} - {{ $t->home_giorni_giacenza }} gg + {{ $t->giacenza_giorni }} gg - @if($t->home_is_prioritario) + @if($t->in_priorita) ★ Prioritario @else - diff --git a/tests/Feature/GruppiServizioTest.php b/tests/Feature/GruppiServizioTest.php index 5e98d01..1544b10 100644 --- a/tests/Feature/GruppiServizioTest.php +++ b/tests/Feature/GruppiServizioTest.php @@ -609,6 +609,104 @@ class GruppiServizioTest extends TestCase } } + // ─── Le azioni del sorvegliante passano dal middleware ────── + + public function test_le_azioni_livewire_del_sorvegliante_non_finiscono_in_403(): void + { + // Livewire::test() non passa dai middleware delle rotte: qui si chiama + // la rotta vera delle azioni (in Livewire 3.7 "default.livewire.update", + // che il filtro 'livewire.*' non riconosceva: ogni clic era un 403). + ['sorvegliante' => $s] = $this->gruppo(); + $this->flushSession(); + $rotta = collect(app('router')->getRoutes()->getRoutes()) + ->first(fn ($r) => str_ends_with($r->uri(), 'livewire/update')); + $this->assertNotNull($rotta); + + $this->withoutExceptionHandling(); + try { + $this->actingAs($s)->post('/' . ltrim($rotta->uri(), '/'), ['components' => []], ['X-Livewire' => 'true']); + } catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { + $this->assertNotSame(403, $e->getStatusCode(), $e->getMessage()); + } catch (\Throwable) { + // Livewire rifiuta la richiesta vuota: non è il 403 del sorvegliante. + } + $this->addToAssertionCount(1); + } + + // ─── Priorità e filtri ────────────────────────────────────── + + /** Territori in reparto con giacenze diverse: 40 (200 gg), 41 (50 gg), 42 (spunta prioritario), 43 (nuovo). */ + private function territoriConPriorita(): array + { + $zona = \App\Models\Zona::create(['nome' => 'Noce', 'attivo' => true]); + $altra = \App\Models\Zona::create(['nome' => 'Zisa', 'attivo' => true]); + $t = []; + foreach (['40' => 200, '41' => 50, '42' => 0, '43' => 0] as $numero => $giorni) { + $t[$numero] = $this->territorio($numero); + $t[$numero]->forceFill([ + 'created_at' => now()->subDays($giorni), + 'prioritario' => $numero === '42', + 'zona_id' => $numero === '43' ? $altra->id : $zona->id, + ])->save(); + } + + return $t; + } + + public function test_aggiungendo_al_pacchetto_i_territori_sono_in_ordine_di_priorita_e_filtrabili(): void + { + ['gruppo' => $gruppo] = $this->gruppo(); + $t = $this->territoriConPriorita(); + + $pagina = Livewire::test(GruppoShow::class, ['gruppo' => $gruppo]); + // Prima i prioritari (per giacenza), poi gli altri: la regola della Home. + $this->assertSame(['40', '42', '41', '43'], $pagina->viewData('candidatiTerritori')->pluck('numero')->all()); + $pagina->assertSee('★ Prioritario')->assertSee('fermo da 200 gg'); + + $pagina->set('soloPrioritari', true); + $this->assertSame(['40', '42'], $pagina->viewData('candidatiTerritori')->pluck('numero')->all()); + + $pagina->set('soloPrioritari', false)->set('filtroZona', (string) $t['43']->zona_id); + $this->assertSame(['43'], $pagina->viewData('candidatiTerritori')->pluck('numero')->all()); + + $pagina->set('filtroZona', '')->set('cercaTerritorio', '41'); + $this->assertSame(['41'], $pagina->viewData('candidatiTerritori')->pluck('numero')->all()); + + // "Seleziona tutti quelli mostrati" e aggiunta in blocco. + $pagina->set('cercaTerritorio', '')->set('soloPrioritari', true) + ->call('selezionaMostrati') + ->call('aggiungiTerritori'); + $this->assertSame($gruppo->id, $t['40']->fresh()->gruppo_servizio_id); + $this->assertSame($gruppo->id, $t['42']->fresh()->gruppo_servizio_id); + $this->assertNull($t['41']->fresh()->gruppo_servizio_id); + } + + public function test_il_sorvegliante_vede_il_pacchetto_in_ordine_di_priorita_e_lo_filtra(): void + { + ['gruppo' => $gruppo, 'sorvegliante' => $s, 'verdi' => $verdi, 'territori' => $vecchi] = $this->gruppo(); + $t = $this->territoriConPriorita(); + app(GruppiServizio::class)->aggiungiAlPacchetto($gruppo, collect($t)->pluck('id')->all()); + // Il pacchetto di gruppo() (12, 14, 35) va via, tranne il 12 che si dà. + app(GruppiServizio::class)->assegna($gruppo, $vecchi[0], $verdi, now()); + app(GruppiServizio::class)->rimandaInReparto($gruppo, $vecchi[1]->fresh()); + app(GruppiServizio::class)->rimandaInReparto($gruppo, $vecchi[2]->fresh()); + $this->flushSession(); + + $pagina = Livewire::actingAs($s)->test(MioGruppo::class) + ->assertSee('Sorvegliante: Mario Rossi', false); + // Da dare in ordine di priorità, poi quelli già dati. + $this->assertSame(['40', '42', '41', '43', '12'], $pagina->viewData('territori')->pluck('numero')->all()); + + $pagina->set('filtroStato', 'da_dare'); + $this->assertSame(['40', '42', '41', '43'], $pagina->viewData('territori')->pluck('numero')->all()); + $pagina->set('filtroStato', 'dati'); + $this->assertSame(['12'], $pagina->viewData('territori')->pluck('numero')->all()); + $pagina->set('filtroStato', '')->set('soloPrioritari', true); + $this->assertSame(['40', '42'], $pagina->viewData('territori')->pluck('numero')->all()); + $pagina->set('soloPrioritari', false)->set('filtroZona', (string) $t['43']->zona_id); + $this->assertSame(['43'], $pagina->viewData('territori')->pluck('numero')->all()); + } + // ─── Backup e azzeramento ─────────────────────────────────── public function test_l_azzeramento_toglie_anche_gruppi_e_utenza_del_sorvegliante(): void