++ gruppi: le azioni del sorvegliante non finiscono piu' in 403; priorita' e filtri nel pacchetto; nome del sorvegliante
- LimitaSorvegliante: la rotta delle azioni di Livewire 3.7 si chiama "default.livewire.update"; con 'livewire.*' ogni clic era un 403. - Territorio::ordinaPerPriorita(): la regola di "Da assegnare" della Home in un metodo solo, usato dalla Home, dall'aggiunta al pacchetto e da "Il mio gruppo". - Aggiunta al pacchetto: ricerca, zona, tipologia, solo prioritari, "Seleziona tutti quelli mostrati", badge e giorni di fermo. - Il mio gruppo: da dare in ordine di priorita' poi i gia' dati; filtri stato, zona, tipologia, solo prioritari; sotto il titolo nome e cognome del sorvegliante. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
15
HANDOFF.md
15
HANDOFF.md
@@ -219,6 +219,21 @@ Decisioni successive dell'utente (20 ottobre 2026):
|
|||||||
temporanea". Si spegne a password cambiata (pagina obbligatoria o
|
temporanea". Si spegne a password cambiata (pagina obbligatoria o
|
||||||
profilo); il ripristino e l'import XML la spengono per tutti, perché lì il
|
profilo); il ripristino e l'import XML la spengono per tutti, perché lì il
|
||||||
motivo è il ripristino.
|
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`
|
- **Anche i membri si segnalano**: `Proclamatore::gruppo_etichetta`
|
||||||
("Gruppo Zisa" / "Sorvegliante del gruppo Zisa") come badge nell'elenco
|
("Gruppo Zisa" / "Sorvegliante del gruppo Zisa") come badge nell'elenco
|
||||||
(più il filtro *In un gruppo di servizio*, `filtroStato=gruppo`) e in testa
|
(più il filtro *In un gruppo di servizio*, `filtroStato=gruppo`) e in testa
|
||||||
|
|||||||
11
README.md
11
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
|
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
|
lo stesso nome. Se perde la password, l'amministratore ne genera un'altra
|
||||||
dalla pagina del gruppo
|
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
|
Restano **"In gruppo …"**: non sono un'assegnazione (statistiche e medie non
|
||||||
cambiano), escono da "Da assegnare" e dalla pagina *Assegna*
|
cambiano), escono da "Da assegnare" e dalla pagina *Assegna*
|
||||||
- Il sorvegliante entra e vede **solo "Il mio gruppo"** e il proprio profilo
|
- 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
|
(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
|
nasce un'**assegnazione normale**, che conta come le altre ed è segnata
|
||||||
"via gruppo …"; poi manda il link (copia o condividi), registra i rientri,
|
"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
|
- Al **rientro** il territorio torna in reparto ed esce dal pacchetto
|
||||||
- **Sciogliere il gruppo** toglie il ruolo al sorvegliante e **elimina il
|
- **Sciogliere il gruppo** toglie il ruolo al sorvegliante e **elimina il
|
||||||
gruppo**, che non resta nello storico: i territori solo nel pacchetto
|
gruppo**, che non resta nello storico: i territori solo nel pacchetto
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ class LimitaSorvegliante
|
|||||||
'password.change.store',
|
'password.change.store',
|
||||||
'logout',
|
'logout',
|
||||||
'verification.*',
|
'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
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ class GruppoShow extends Component
|
|||||||
/** @var int[] territori da aggiungere al pacchetto */
|
/** @var int[] territori da aggiungere al pacchetto */
|
||||||
public array $nuoviTerritori = [];
|
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]
|
#[Locked]
|
||||||
public ?string $passwordTemporanea = null;
|
public ?string $passwordTemporanea = null;
|
||||||
|
|
||||||
@@ -91,6 +101,45 @@ class GruppoShow extends Component
|
|||||||
$this->nuoviTerritori = [];
|
$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
|
public function rimanda(int $territorioId): void
|
||||||
{
|
{
|
||||||
$territorio = Territorio::findOrFail($territorioId);
|
$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();
|
$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();
|
$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', [
|
return view('livewire.gruppi.gruppo-show', [
|
||||||
'membri' => $membri,
|
'membri' => $membri,
|
||||||
'territori' => $territori,
|
'territori' => $territori,
|
||||||
// Si possono aggiungere i proclamatori attivi senza gruppo...
|
// 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(),
|
'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).
|
// ...e i territori in reparto.
|
||||||
'candidatiTerritori' => Territorio::inReparto()->with('zona')->get()->sortBy('numero', SORT_NATURAL)->values(),
|
'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(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use App\Services\GruppiServizio;
|
|||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Livewire\Attributes\Locked;
|
use Livewire\Attributes\Locked;
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +36,20 @@ class MioGruppo extends Component
|
|||||||
|
|
||||||
public string $dataAssegnazione = '';
|
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
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$this->dataAssegnazione = now()->format('Y-m-d');
|
$this->dataAssegnazione = now()->format('Y-m-d');
|
||||||
@@ -170,11 +185,25 @@ class MioGruppo extends Component
|
|||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
$gruppo = $this->gruppo();
|
$gruppo = $this->gruppo()?->load('sorvegliante');
|
||||||
|
|
||||||
$territori = $gruppo
|
// Prima quelli da dare, in ordine di priorità come "Da assegnare"
|
||||||
? $gruppo->territori()->with(['zona', 'assegnazioneCorrente.proclamatore'])->get()->sortBy('numero', SORT_NATURAL)->values()
|
// nella Home (Territorio::ordinaPerPriorita); poi quelli già dati, dal
|
||||||
|
// più vecchio.
|
||||||
|
$pacchetto = $gruppo
|
||||||
|
? $gruppo->territori()->with(['zona', 'tipologia', 'ultimaAssegnazione', 'assegnazioneCorrente.proclamatore'])->get()
|
||||||
: collect();
|
: 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
|
$membri = $gruppo
|
||||||
? $gruppo->membri()->where('attivo', true)->get()->sortBy(fn ($p) => mb_strtolower($p->cognome . ' ' . $p->nome))->values()
|
? $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', [
|
return view('livewire.gruppi.mio-gruppo', [
|
||||||
'gruppo' => $gruppo,
|
'gruppo' => $gruppo,
|
||||||
|
'pacchetto' => $pacchetto,
|
||||||
'territori' => $territori,
|
'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,
|
'membri' => $membri,
|
||||||
'opzioniMembri' => Proclamatore::opzioniScelta($membri, conGruppo: false),
|
'opzioniMembri' => Proclamatore::opzioniScelta($membri, conGruppo: false),
|
||||||
'invio' => $this->invioCorrente(),
|
'invio' => $this->invioCorrente(),
|
||||||
|
|||||||
@@ -18,35 +18,10 @@ class Home extends Component
|
|||||||
$this->authorize('territori.assign');
|
$this->authorize('territori.assign');
|
||||||
|
|
||||||
$settings = Setting::instance();
|
$settings = Setting::instance();
|
||||||
$priorityThreshold = (int) ($settings->giorni_giacenza_prioritari ?? 180);
|
|
||||||
|
|
||||||
$territori = Territorio::inReparto()
|
$territori = Territorio::ordinaPerPriorita(
|
||||||
->with('zona', 'tipologia', 'ultimaAssegnazione')
|
Territorio::inReparto()->with('zona', 'tipologia', 'ultimaAssegnazione')->get()
|
||||||
->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();
|
|
||||||
|
|
||||||
$pdf = Pdf::loadView('pdf.territori-lista', [
|
$pdf = Pdf::loadView('pdf.territori-lista', [
|
||||||
'titolo' => 'Territori da Assegnare',
|
'titolo' => 'Territori da Assegnare',
|
||||||
@@ -90,7 +65,6 @@ class Home extends Component
|
|||||||
{
|
{
|
||||||
$settings = Setting::instance();
|
$settings = Setting::instance();
|
||||||
$homeLimit = max(1, (int) ($settings->home_limit_list ?? 10));
|
$homeLimit = max(1, (int) ($settings->home_limit_list ?? 10));
|
||||||
$priorityThreshold = (int) ($settings->giorni_giacenza_prioritari ?? 180);
|
|
||||||
$annoCorrente = AnnoTeocratico::corrente();
|
$annoCorrente = AnnoTeocratico::corrente();
|
||||||
$campagnaAttiva = Campagna::attiva();
|
$campagnaAttiva = Campagna::attiva();
|
||||||
|
|
||||||
@@ -149,46 +123,11 @@ class Home extends Component
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quick lists
|
// Quick lists: la logica di priorità sta in Territorio::ordinaPerPriorita
|
||||||
$territoriDaAssegnare = Territorio::inReparto()
|
// (la usano anche il pacchetto dei gruppi di servizio e "Il mio gruppo").
|
||||||
->with('zona', 'tipologia', 'ultimaAssegnazione')
|
$territoriDaAssegnare = Territorio::ordinaPerPriorita(
|
||||||
->get()
|
Territorio::inReparto()->with('zona', 'tipologia', 'ultimaAssegnazione')->get()
|
||||||
->map(function (Territorio $territorio) use ($priorityThreshold) {
|
)->take($homeLimit)->values();
|
||||||
$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();
|
|
||||||
|
|
||||||
$daRientrare = Territorio::daRientrare()
|
$daRientrare = Territorio::daRientrare()
|
||||||
->with(['zona', 'assegnazioneCorrente.proclamatore'])
|
->with(['zona', 'assegnazioneCorrente.proclamatore'])
|
||||||
|
|||||||
@@ -169,6 +169,41 @@ class Territorio extends Model
|
|||||||
return false;
|
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 (<x-scelta>): si cerca per
|
* L'opzione per il campo di scelta con ricerca (<x-scelta>): si cerca per
|
||||||
* numero, zona e tipologia. Con $motivo compare cercando ma non si può
|
* numero, zona e tipologia. Con $motivo compare cercando ma non si può
|
||||||
|
|||||||
@@ -49,27 +49,65 @@
|
|||||||
@endforelse
|
@endforelse
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{{-- Assegnazione in blocco: si spuntano i territori in reparto --}}
|
{{-- Assegnazione in blocco: si spuntano i territori in reparto, in
|
||||||
<div class="border-t border-gray-100 bg-gray-50 p-4" x-data="elencoCercabile">
|
ordine di priorità (come "Da assegnare" nella Home), filtrabili. --}}
|
||||||
|
<div class="border-t border-gray-100 bg-gray-50 p-4">
|
||||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">Aggiungi territori al pacchetto</h3>
|
<h3 class="text-sm font-semibold text-gray-700 mb-2">Aggiungi territori al pacchetto</h3>
|
||||||
@if($candidatiTerritori->isEmpty())
|
@if($totaleCandidati === 0)
|
||||||
<p class="text-sm text-gray-500">Non ci sono territori in reparto da aggiungere.</p>
|
<p class="text-sm text-gray-500">Non ci sono territori in reparto da aggiungere.</p>
|
||||||
@else
|
@else
|
||||||
<input x-model="q" type="search" inputmode="search" autocomplete="off" aria-label="Filtra i territori in reparto"
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2 mb-2">
|
||||||
placeholder="Filtra per numero o zona…"
|
<input wire:model.live.debounce.300ms="cercaTerritorio" type="search" inputmode="search" autocomplete="off" aria-label="Cerca tra i territori in reparto"
|
||||||
class="mb-2 block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
placeholder="Numero, zona o tipologia…"
|
||||||
<div class="max-h-72 overflow-y-auto rounded-lg border border-gray-200 bg-white divide-y divide-gray-100">
|
class="block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
@foreach($candidatiTerritori as $c)
|
<select wire:model.live="filtroZona" aria-label="Zona"
|
||||||
<label class="flex items-center gap-3 px-3 py-2.5 text-sm cursor-pointer hover:bg-indigo-50/40"
|
class="block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
data-cerca="{{ \Illuminate\Support\Str::lower(\Illuminate\Support\Str::ascii($c->numero . ' ' . $c->zona?->nome)) }}"
|
<option value="">Tutte le zone</option>
|
||||||
x-show="corrisponde($el.dataset.cerca)" wire:key="candidato-t-{{ $c->id }}">
|
@foreach($zoneCandidati as $id => $nome)
|
||||||
<input type="checkbox" value="{{ $c->id }}" wire:model="nuoviTerritori" class="h-5 w-5 rounded border-gray-300 text-indigo-600">
|
<option value="{{ $id }}">{{ $nome }}</option>
|
||||||
<span><strong>N° {{ $c->numero }}</strong> <span class="text-gray-500">{{ $c->zona?->nome }}</span></span>
|
@endforeach
|
||||||
</label>
|
</select>
|
||||||
@endforeach
|
<select wire:model.live="filtroTipologia" aria-label="Tipologia"
|
||||||
|
class="block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
<option value="">Tutte le tipologie</option>
|
||||||
|
@foreach($tipologieCandidati as $id => $nome)
|
||||||
|
<option value="{{ $id }}">{{ $nome }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<label class="flex items-center gap-2 text-sm text-gray-700 px-1">
|
||||||
|
<input type="checkbox" wire:model.live="soloPrioritari" class="h-5 w-5 rounded border-gray-300 text-indigo-600">
|
||||||
|
Solo prioritari
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
<div class="mb-2 flex flex-wrap items-center gap-2 text-xs text-gray-500">
|
||||||
|
<span>{{ $candidatiTerritori->count() }} di {{ $totaleCandidati }} in reparto, prima i prioritari e quelli fermi da più tempo</span>
|
||||||
|
@if($candidatiTerritori->isNotEmpty())
|
||||||
|
<x-btn variante="secondary" tono="tenue" dimensione="sm" wire:click="selezionaMostrati">Seleziona tutti quelli mostrati</x-btn>
|
||||||
|
@endif
|
||||||
|
<x-btn variante="ghost" dimensione="sm" wire:click="deselezionaTutti">Deseleziona</x-btn>
|
||||||
|
</div>
|
||||||
|
<div class="max-h-96 overflow-y-auto rounded-lg border border-gray-200 bg-white divide-y divide-gray-100">
|
||||||
|
@forelse($candidatiTerritori as $c)
|
||||||
|
<label class="flex items-center gap-3 px-3 py-2.5 text-sm cursor-pointer hover:bg-indigo-50/40" wire:key="candidato-t-{{ $c->id }}">
|
||||||
|
<input type="checkbox" value="{{ $c->id }}" wire:model="nuoviTerritori" class="h-5 w-5 rounded border-gray-300 text-indigo-600">
|
||||||
|
<span class="min-w-0">
|
||||||
|
<strong>N° {{ $c->numero }}</strong>
|
||||||
|
@if($c->in_priorita)
|
||||||
|
<span class="ml-1 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide" style="background:#fef3c7;color:#92400e">★ Prioritario</span>
|
||||||
|
@endif
|
||||||
|
<span class="block text-xs text-gray-500">
|
||||||
|
{{ collect([$c->zona?->nome, $c->tipologia?->nome])->filter()->join(' — ') }}
|
||||||
|
@if($c->giacenza_giorni > 0) · fermo da {{ $c->giacenza_giorni }} gg @endif
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
@empty
|
||||||
|
<p class="px-3 py-6 text-center text-sm text-gray-500">Nessun territorio con questi filtri.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex flex-wrap items-center gap-3">
|
||||||
<x-btn variante="primary" wire:click="aggiungiTerritori">Aggiungi i territori selezionati</x-btn>
|
<x-btn variante="primary" wire:click="aggiungiTerritori">Aggiungi i territori selezionati</x-btn>
|
||||||
|
<span class="text-xs text-gray-500" x-data>Selezionati: <strong x-text="$wire.nuoviTerritori.length">{{ count($nuoviTerritori) }}</strong></span>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,11 +10,14 @@
|
|||||||
@else
|
@else
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<h1 class="text-2xl font-bold text-gray-900">Gruppo {{ $gruppo->nome }}</h1>
|
<h1 class="text-2xl font-bold text-gray-900">Gruppo {{ $gruppo->nome }}</h1>
|
||||||
|
@if($gruppo->sorvegliante)
|
||||||
|
<p class="text-sm text-gray-700 mt-0.5">Sorvegliante: <strong>{{ trim($gruppo->sorvegliante->nome . ' ' . $gruppo->sorvegliante->cognome) }}</strong></p>
|
||||||
|
@endif
|
||||||
{{-- In questa vista solo blocchi php…endphp: la forma in linea, se
|
{{-- In questa vista solo blocchi php…endphp: la forma in linea, se
|
||||||
viene prima di un blocco, confonde il compilatore di Blade. --}}
|
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
|
||||||
<p class="text-sm text-gray-500 mt-1">
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
{{ $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' }}
|
{{ $daDare }} da dare · {{ $membri->count() }} {{ $membri->count() === 1 ? 'membro' : 'membri' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,12 +32,43 @@
|
|||||||
<symbol id="tm-i-avviso" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></symbol>
|
<symbol id="tm-i-avviso" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></symbol>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<div class="px-4 py-3 border-b border-gray-100 flex flex-wrap items-center justify-between gap-2">
|
<div class="px-4 py-3 border-b border-gray-100 space-y-2">
|
||||||
<h2 id="titolo-pacchetto" class="text-sm font-semibold text-gray-800">Territori del pacchetto</h2>
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
@if($territori->count() > 5)
|
<h2 id="titolo-pacchetto" class="text-sm font-semibold text-gray-800">Territori del pacchetto</h2>
|
||||||
<input x-model="q" type="search" inputmode="search" autocomplete="off" aria-label="Cerca nel pacchetto"
|
@if($pacchetto->count() > 5)
|
||||||
placeholder="Cerca numero, zona o nome…"
|
<input x-model="q" type="search" inputmode="search" autocomplete="off" aria-label="Cerca nel pacchetto"
|
||||||
class="w-full sm:w-64 rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
placeholder="Cerca numero, zona o nome…"
|
||||||
|
class="w-full sm:w-64 rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
{{-- Filtri; l'ordine è di priorità: prima i prioritari e quelli fermi da più tempo --}}
|
||||||
|
@if($pacchetto->isNotEmpty())
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
|
<select wire:model.live="filtroStato" aria-label="Stato"
|
||||||
|
class="block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
<option value="">Tutti</option>
|
||||||
|
<option value="da_dare">Da dare</option>
|
||||||
|
<option value="dati">Già dati</option>
|
||||||
|
</select>
|
||||||
|
<select wire:model.live="filtroZona" aria-label="Zona"
|
||||||
|
class="block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
<option value="">Tutte le zone</option>
|
||||||
|
@foreach($zone as $id => $nome)
|
||||||
|
<option value="{{ $id }}">{{ $nome }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<select wire:model.live="filtroTipologia" aria-label="Tipologia"
|
||||||
|
class="block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
<option value="">Tutte le tipologie</option>
|
||||||
|
@foreach($tipologie as $id => $nome)
|
||||||
|
<option value="{{ $id }}">{{ $nome }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<label class="flex items-center gap-2 text-sm text-gray-700 px-1">
|
||||||
|
<input type="checkbox" wire:model.live="soloPrioritari" class="h-5 w-5 rounded border-gray-300 text-indigo-600">
|
||||||
|
Solo prioritari
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -47,11 +81,12 @@
|
|||||||
@endphp
|
@endphp
|
||||||
<div wire:key="pacchetto-{{ $t->id }}" data-cerca="{{ $cerca }}" x-show="corrisponde($el.dataset.cerca)" class="tm-assegnato">
|
<div wire:key="pacchetto-{{ $t->id }}" data-cerca="{{ $cerca }}" x-show="corrisponde($el.dataset.cerca)" class="tm-assegnato">
|
||||||
<div class="tm-assegnato-testa">
|
<div class="tm-assegnato-testa">
|
||||||
<span><span class="tm-assegnato-numero">N° {{ $t->numero }}</span> @if($t->zona?->nome)<span class="tm-assegnato-zona">{{ $t->zona->nome }}</span>@endif</span>
|
<span><span class="tm-assegnato-numero">N° {{ $t->numero }}</span> @if($t->zona?->nome)<span class="tm-assegnato-zona">{{ $t->zona->nome }}</span>@endif
|
||||||
|
@if($t->in_priorita)<span class="ml-1 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide" style="background:#fef3c7;color:#92400e">★ Prioritario</span>@endif</span>
|
||||||
@if($a)
|
@if($a)
|
||||||
<span class="tm-assegnato-info"><b>{{ $a->proclamatore?->nome_completo }}</b> · dal {{ $a->assigned_at->format('d/m/Y') }} · <span class="tm-assegnato-giorni {{ $a->giorni > 120 ? 'tm-g-tardi' : ($a->giorni > 90 ? 'tm-g-presto' : '') }}">{{ $a->giorni }}g</span></span>
|
<span class="tm-assegnato-info"><b>{{ $a->proclamatore?->nome_completo }}</b> · dal {{ $a->assigned_at->format('d/m/Y') }} · <span class="tm-assegnato-giorni {{ $a->giorni > 120 ? 'tm-g-tardi' : ($a->giorni > 90 ? 'tm-g-presto' : '') }}">{{ $a->giorni }}g</span></span>
|
||||||
@else
|
@else
|
||||||
<span class="tm-assegnato-info"><b>Da dare</b></span>
|
<span class="tm-assegnato-info"><b>Da dare</b>@if($t->tipologia?->nome) · {{ $t->tipologia->nome }}@endif @if($t->giacenza_giorni > 0) · fermo da {{ $t->giacenza_giorni }} gg @endif</span>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -101,7 +136,9 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@empty
|
@empty
|
||||||
<p class="px-4 py-8 text-center text-sm text-gray-500">Il pacchetto è vuoto: l'amministratore non ha ancora assegnato territori al gruppo.</p>
|
<p class="px-4 py-8 text-center text-sm text-gray-500">
|
||||||
|
{{ $pacchetto->isEmpty() ? "Il pacchetto è vuoto: l'amministratore non ha ancora assegnato territori al gruppo." : 'Nessun territorio con questi filtri.' }}
|
||||||
|
</p>
|
||||||
@endforelse
|
@endforelse
|
||||||
<p x-show="nessuno" x-cloak class="px-4 py-8 text-center text-sm text-gray-400">Nessun risultato per «<span x-text="q"></span>».</p>
|
<p x-show="nessuno" x-cloak class="px-4 py-8 text-center text-sm text-gray-400">Nessun risultato per «<span x-text="q"></span>».</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,7 +148,7 @@
|
|||||||
<h2 id="titolo-membri" class="text-sm font-semibold text-gray-800 mb-2">Membri del gruppo</h2>
|
<h2 id="titolo-membri" class="text-sm font-semibold text-gray-800 mb-2">Membri del gruppo</h2>
|
||||||
<ul class="text-sm text-gray-700 space-y-1">
|
<ul class="text-sm text-gray-700 space-y-1">
|
||||||
@foreach($membri as $m)
|
@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
|
||||||
<li class="flex justify-between gap-3">
|
<li class="flex justify-between gap-3">
|
||||||
<span>{{ $m->nome_completo }} @if($m->id === $gruppo->sorvegliante_id)<span class="text-xs text-gray-500">(sorvegliante)</span>@endif</span>
|
<span>{{ $m->nome_completo }} @if($m->id === $gruppo->sorvegliante_id)<span class="text-xs text-gray-500">(sorvegliante)</span>@endif</span>
|
||||||
<span class="text-gray-500">{{ $quanti }} {{ $quanti === 1 ? 'territorio' : 'territori' }}</span>
|
<span class="text-gray-500">{{ $quanti }} {{ $quanti === 1 ? 'territorio' : 'territori' }}</span>
|
||||||
|
|||||||
@@ -110,14 +110,14 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<a data-ritorno href="{{ route('territori.show', $t) }}" class="text-sm font-semibold text-gray-900 hover:text-indigo-600 transition-colors">N° {{ $t->numero }}</a>
|
<a data-ritorno href="{{ route('territori.show', $t) }}" class="text-sm font-semibold text-gray-900 hover:text-indigo-600 transition-colors">N° {{ $t->numero }}</a>
|
||||||
@if($t->home_is_prioritario)
|
@if($t->in_priorita)
|
||||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide badge-pulse" style="background:#fef3c7;color:#92400e">★ Prioritario</span>
|
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide badge-pulse" style="background:#fef3c7;color:#92400e">★ Prioritario</span>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500">
|
<p class="text-xs text-gray-500">
|
||||||
{{ $t->zona?->nome }} — {{ $t->tipologia?->nome }}
|
{{ $t->zona?->nome }} — {{ $t->tipologia?->nome }}
|
||||||
@if($t->home_giorni_giacenza > 0)
|
@if($t->giacenza_giorni > 0)
|
||||||
— <span class="font-medium">{{ $t->home_giorni_giacenza }} gg</span> in reparto
|
— <span class="font-medium">{{ $t->giacenza_giorni }} gg</span> in reparto
|
||||||
@endif
|
@endif
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,9 +44,9 @@
|
|||||||
<td style="font-weight:bold">{{ $t->numero }}</td>
|
<td style="font-weight:bold">{{ $t->numero }}</td>
|
||||||
<td>{{ $t->zona?->nome ?? '-' }}</td>
|
<td>{{ $t->zona?->nome ?? '-' }}</td>
|
||||||
<td>{{ $t->tipologia?->nome ?? '-' }}</td>
|
<td>{{ $t->tipologia?->nome ?? '-' }}</td>
|
||||||
<td>{{ $t->home_giorni_giacenza }} gg</td>
|
<td>{{ $t->giacenza_giorni }} gg</td>
|
||||||
<td>
|
<td>
|
||||||
@if($t->home_is_prioritario)
|
@if($t->in_priorita)
|
||||||
<span class="prioritario">★ Prioritario</span>
|
<span class="prioritario">★ Prioritario</span>
|
||||||
@else
|
@else
|
||||||
-
|
-
|
||||||
|
|||||||
@@ -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: <strong>Mario Rossi</strong>', 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 ───────────────────────────────────
|
// ─── Backup e azzeramento ───────────────────────────────────
|
||||||
|
|
||||||
public function test_l_azzeramento_toglie_anche_gruppi_e_utenza_del_sorvegliante(): void
|
public function test_l_azzeramento_toglie_anche_gruppi_e_utenza_del_sorvegliante(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user