++ gruppi di servizio: sorvegliante con utenza limitata, pacchetto di territori, assegnazione ai membri

Il gruppo ha un sorvegliante (proclamatore con utenza: email e password
temporanea, senza conferma dell'email) e dei membri; l'amministratore
mette in blocco dei territori nel pacchetto del gruppo. Il pacchetto non
e' un'assegnazione e non tocca le statistiche; quando il sorvegliante da'
un territorio a un membro nasce un'assegnazione normale con
gruppo_servizio_id. Al rientro il territorio torna in reparto. Sciogliere
il gruppo libera pacchetto e membri e disattiva l'utenza.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Francesco Picone
2026-10-19 17:51:37 +02:00
parent 5d0ae06eca
commit 96084499f5
41 changed files with 2298 additions and 224 deletions

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Livewire\Gruppi;
use App\Livewire\Concerns\Avvisa;
use App\Models\GruppoServizio;
use App\Models\Proclamatore;
use App\Services\GruppiServizio;
use InvalidArgumentException;
use Livewire\Attributes\Locked;
use Livewire\Component;
/**
* Gruppi di servizio (solo amministratore): elenco e creazione. Creando un
* gruppo si sceglie il sorvegliante, che riceve un'utenza con l'email
* indicata e una password temporanea, mostrata qui una volta sola.
* Membri e pacchetto si gestiscono nella pagina del gruppo (GruppoShow).
*/
class GruppiIndex extends Component
{
use Avvisa;
public string $nome = '';
public ?int $sorveglianteId = null;
public string $email = '';
/** Gruppo appena creato e password temporanea del sorvegliante, da consegnare. */
#[Locked]
public ?int $creatoId = null;
#[Locked]
public ?string $passwordTemporanea = null;
public function crea(): void
{
$this->authorize('settings.manage');
$this->validate([
'nome' => 'required|string|max:100',
'sorveglianteId' => 'required|integer|exists:proclamatori,id',
'email' => 'required|email|max:255',
], [
'sorveglianteId.required' => 'Scegli il sorvegliante.',
'email.required' => "Serve l'email con cui il sorvegliante entrerà.",
]);
try {
[$gruppo, $password] = app(GruppiServizio::class)->crea(
$this->nome,
Proclamatore::findOrFail($this->sorveglianteId),
trim($this->email),
);
} catch (InvalidArgumentException $e) {
$this->addError('sorveglianteId', $e->getMessage());
return;
}
$this->reset(['nome', 'sorveglianteId', 'email']);
$this->creatoId = $gruppo->id;
$this->passwordTemporanea = $password;
}
public function chiudiPassword(): void
{
$this->reset(['creatoId', 'passwordTemporanea']);
}
public function render()
{
$gruppi = GruppoServizio::with('sorvegliante')
->withCount(['membri', 'territori'])
->orderByRaw('sciolto_at IS NOT NULL')
->orderBy('nome')
->get();
// Possono diventare sorveglianti i proclamatori attivi senza gruppo e
// senza già un'utenza.
$candidati = Proclamatore::attivi()
->whereNull('gruppo_servizio_id')
->whereDoesntHave('utente')
->get();
return view('livewire.gruppi.gruppi-index', [
'gruppi' => $gruppi,
'opzioniSorveglianti' => Proclamatore::opzioniScelta($candidati),
'creato' => $this->creatoId ? GruppoServizio::with('sorvegliante.utente')->find($this->creatoId) : null,
]);
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace App\Livewire\Gruppi;
use App\Livewire\Concerns\Avvisa;
use App\Livewire\Concerns\RicordaRitorno;
use App\Models\GruppoServizio;
use App\Models\Proclamatore;
use App\Models\Territorio;
use App\Models\User;
use App\Services\GruppiServizio;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Livewire\Attributes\Locked;
use Livewire\Component;
/**
* Un gruppo di servizio, lato amministratore: membri (aggiungere/togliere),
* pacchetto (aggiungere in blocco, rimandare in reparto quelli non dati),
* nuova password temporanea per il sorvegliante, scioglimento.
*/
class GruppoShow extends Component
{
use Avvisa;
use RicordaRitorno;
public GruppoServizio $gruppo;
/** @var int[] proclamatori da aggiungere */
public array $nuoviMembri = [];
/** @var int[] territori da aggiungere al pacchetto */
public array $nuoviTerritori = [];
#[Locked]
public ?string $passwordTemporanea = null;
public function mount(GruppoServizio $gruppo): void
{
$this->gruppo = $gruppo;
}
private function servizio(): GruppiServizio
{
$this->authorize('settings.manage');
return app(GruppiServizio::class);
}
private function esegui(callable $azione, string $ok): void
{
try {
$azione();
} catch (InvalidArgumentException $e) {
$this->avvisaErrore($e->getMessage());
return;
}
$this->gruppo->refresh();
$this->avvisa($ok);
}
public function aggiungiMembri(): void
{
$ids = array_map('intval', $this->nuoviMembri);
$this->esegui(fn () => $this->servizio()->aggiungiMembri($this->gruppo, $ids), count($ids) . ' membri aggiunti.');
$this->nuoviMembri = [];
}
public function togliMembro(int $proclamatoreId): void
{
$membro = Proclamatore::findOrFail($proclamatoreId);
$this->esegui(fn () => $this->servizio()->togliMembro($this->gruppo, $membro), "{$membro->nome_completo} tolto dal gruppo.");
}
public function aggiungiTerritori(): void
{
$ids = array_map('intval', $this->nuoviTerritori);
$this->esegui(fn () => $this->servizio()->aggiungiAlPacchetto($this->gruppo, $ids), count($ids) . ' territori aggiunti al pacchetto.');
$this->nuoviTerritori = [];
}
public function rimanda(int $territorioId): void
{
$territorio = Territorio::findOrFail($territorioId);
$this->esegui(fn () => $this->servizio()->rimandaInReparto($this->gruppo, $territorio), "Territorio {$territorio->numero} rimandato in reparto.");
}
/** Il sorvegliante ha perso la password: una nuova temporanea, da cambiare al primo accesso. */
public function nuovaPassword(): void
{
$this->servizio();
$utente = User::where('proclamatore_id', $this->gruppo->sorvegliante_id)->first();
if (! $utente || $this->gruppo->sciolto_at) {
$this->avvisaErrore('Il gruppo non ha un sorvegliante con utenza attiva.');
return;
}
$password = Str::password(12, symbols: false);
$utente->forceFill(['password' => $password, 'password_change_required' => true])->save();
activity()->causedBy(auth()->user())->performedOn($utente)->log('password_reimpostata');
$this->passwordTemporanea = $password;
}
/** Toglie il ruolo di sorvegliante: il gruppo si scioglie. */
public function sciogli(): void
{
$this->esegui(fn () => $this->servizio()->sciogli($this->gruppo), "Gruppo {$this->gruppo->nome} sciolto.");
$this->passwordTemporanea = null;
}
public function render()
{
$this->gruppo->load(['sorvegliante.utente']);
$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();
$attivo = ! $this->gruppo->sciolto_at;
return view('livewire.gruppi.gruppo-show', [
'attivo' => $attivo,
'membri' => $membri,
'territori' => $territori,
// Si possono aggiungere i proclamatori attivi senza gruppo...
'candidatiMembri' => $attivo
? Proclamatore::attivi()->whereNull('gruppo_servizio_id')->get()->sortBy(fn ($p) => mb_strtolower($p->cognome . ' ' . $p->nome))->values()
: collect(),
// ...e i territori in reparto (attivi, non assegnati, fuori da ogni pacchetto).
'candidatiTerritori' => $attivo
? Territorio::inReparto()->with('zona')->get()->sortBy('numero', SORT_NATURAL)->values()
: collect(),
'assegnazioniDelGruppo' => $this->gruppo->assegnazioni()->count(),
]);
}
}

View File

@@ -0,0 +1,191 @@
<?php
namespace App\Livewire\Gruppi;
use App\Livewire\Concerns\Avvisa;
use App\Livewire\Concerns\InviaLink;
use App\Models\Assegnazione;
use App\Models\GruppoServizio;
use App\Models\Proclamatore;
use App\Models\Territorio;
use App\Services\GruppiServizio;
use Carbon\Carbon;
use InvalidArgumentException;
use Livewire\Attributes\Locked;
use Livewire\Component;
/**
* "Il mio gruppo": la sola pagina del sorvegliante di gruppo. Vede il
* pacchetto del suo gruppo e i membri; dà i territori ai membri (o a sé),
* manda il link, registra i rientri (pagina Rientra), apre la piantina e
* rimanda in reparto quelli non ancora dati. Le regole stanno in
* App\Services\GruppiServizio; qui ogni azione ricontrolla che il territorio
* o l'assegnazione siano del suo gruppo.
*/
class MioGruppo extends Component
{
use Avvisa;
use InviaLink;
/** Il territorio del pacchetto che si sta assegnando. */
#[Locked]
public ?int $territorioDaAssegnare = null;
public ?int $membroId = null;
public string $dataAssegnazione = '';
public function mount(): void
{
$this->dataAssegnazione = now()->format('Y-m-d');
}
private function gruppo(): ?GruppoServizio
{
return auth()->user()->gruppoSorvegliato();
}
private function gruppoOFallisci(): GruppoServizio
{
$gruppo = $this->gruppo();
abort_unless($gruppo, 403, 'Non sei il sorvegliante di un gruppo di servizio attivo.');
return $gruppo;
}
private function territorioDelPacchetto(int $id): Territorio
{
$gruppo = $this->gruppoOFallisci();
$territorio = Territorio::find($id);
abort_unless($territorio && $territorio->gruppo_servizio_id === $gruppo->id, 403);
return $territorio;
}
// ─── Assegnare un territorio del pacchetto ──────────────────
public function apriAssegnazione(int $territorioId): void
{
$this->territorioDelPacchetto($territorioId);
$this->territorioDaAssegnare = $territorioId;
$this->membroId = null;
$this->dataAssegnazione = now()->format('Y-m-d');
$this->resetValidation();
}
public function annullaAssegnazione(): void
{
$this->reset(['territorioDaAssegnare', 'membroId']);
$this->resetValidation();
}
public function assegna(): void
{
$this->validate([
'membroId' => 'required|integer',
'dataAssegnazione' => 'required|date|before_or_equal:today',
], [
'membroId.required' => 'Scegli a chi dare il territorio.',
'dataAssegnazione.before_or_equal' => 'La data non può essere nel futuro.',
]);
$gruppo = $this->gruppoOFallisci();
$territorio = $this->territorioDelPacchetto((int) $this->territorioDaAssegnare);
$membro = Proclamatore::find($this->membroId);
if (! $membro) {
$this->addError('membroId', 'Scegli un membro del gruppo.');
return;
}
try {
$assegnazione = app(GruppiServizio::class)->assegna($gruppo, $territorio, $membro, Carbon::parse($this->dataAssegnazione));
} catch (InvalidArgumentException $e) {
$this->addError('membroId', $e->getMessage());
return;
}
$this->reset(['territorioDaAssegnare', 'membroId']);
// Come dalla pagina Assegna: si apre l'invio del link.
$this->apriInvio($assegnazione->id);
}
// ─── Rimandare in reparto ───────────────────────────────────
public function rimanda(int $territorioId): void
{
$territorio = $this->territorioDelPacchetto($territorioId);
try {
app(GruppiServizio::class)->rimandaInReparto($this->gruppoOFallisci(), $territorio);
} catch (InvalidArgumentException $e) {
$this->avvisaErrore($e->getMessage());
return;
}
$this->avvisa("Territorio {$territorio->numero} rimandato in reparto.");
}
// ─── Link già inviato / condiviso dall'elenco ───────────────
private function assegnazioneDelGruppo(int $id): Assegnazione
{
$assegnazione = Assegnazione::aperte()->with('territorio')->find($id);
abort_unless($assegnazione && $this->puoInviare($assegnazione), 403);
return $assegnazione;
}
public function toggleLinkSent(int $assegnazioneId): void
{
$assegnazione = $this->assegnazioneDelGruppo($assegnazioneId);
$assegnazione->registraInvioLink(! $assegnazione->link_sent, $assegnazione->link_canale);
}
/** "Condividi" dall'elenco andato a buon fine (elencoCercabile.condividi). */
public function segnaCondiviso(int $assegnazioneId): void
{
$this->assegnazioneDelGruppo($assegnazioneId)->registraInvioLink(true, 'condividi');
}
// ─── Invio del link (trait InviaLink) ───────────────────────
protected function puoInviare(Assegnazione $assegnazione): bool
{
$gruppo = $this->gruppo();
return $gruppo !== null
&& ($assegnazione->gruppo_servizio_id === $gruppo->id
|| $assegnazione->territorio?->gruppo_servizio_id === $gruppo->id);
}
protected function paginaDopoInvio(): string
{
return route('mio-gruppo');
}
public function render()
{
$gruppo = $this->gruppo();
$territori = $gruppo
? $gruppo->territori()->with(['zona', 'assegnazioneCorrente.proclamatore'])->get()->sortBy('numero', SORT_NATURAL)->values()
: collect();
$membri = $gruppo
? $gruppo->membri()->where('attivo', true)->get()->sortBy(fn ($p) => mb_strtolower($p->cognome . ' ' . $p->nome))->values()
: collect();
return view('livewire.gruppi.mio-gruppo', [
'gruppo' => $gruppo,
'territori' => $territori,
'membri' => $membri,
'opzioniMembri' => Proclamatore::opzioniScelta($membri),
'invio' => $this->invioCorrente(),
]);
}
}