++ gruppi: sorvegliante non piu' attivo scioglie il gruppo; dettagli del gruppo nella scheda e nel profilo

- Proclamatore::booted: reso non attivo, spostato nel cestino o
  anonimizzato, il sorvegliante scioglie il gruppo (territori non dati in
  reparto, membri liberi, utenza disattivata); il motivo va nel registro.
  L'elenco chiede conferma, la modifica avvisa sotto "Attivo". Un membro
  nel cestino esce dal gruppo.
- Scheda del proclamatore: gruppo guidato con membri, pacchetto, da dare,
  assegnazioni dal gruppo, utenza e ultimo accesso; oppure gruppo di cui
  e' membro; gruppi sciolti guidati in passato.
- Profilo del sorvegliante: il suo gruppo con gli stessi numeri.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Francesco Picone
2026-10-20 10:48:37 +02:00
parent 9999a3b031
commit 4d9e445cba
14 changed files with 370 additions and 48 deletions

View File

@@ -38,13 +38,17 @@ class ProclamatoreEdit extends Component
$this->validate();
// Un sorvegliante reso non attivo scioglie il gruppo (Proclamatore::booted).
$gruppo = ! $this->attivo ? $this->proclamatore->gruppoSorvegliato() : null;
$this->proclamatore->update([
'nome' => $this->nome,
'cognome' => $this->cognome,
'attivo' => $this->attivo,
]);
session()->flash('success', "Proclamatore {$this->proclamatore->nome_completo} aggiornato.");
$avviso = "Proclamatore {$this->proclamatore->nome_completo} aggiornato." . ($gruppo ? ' ' . ProclamatoreIndex::gruppoSciolto($gruppo) : '');
session()->flash('success', $avviso);
return $this->redirect($this->destinazione(route('proclamatori.index')), navigate: true);
}
@@ -53,6 +57,7 @@ class ProclamatoreEdit extends Component
return view('livewire.proclamatori.proclamatore-form', [
'titolo' => "Modifica {$this->proclamatore->nome_completo}",
'btnLabel' => 'Salva Modifiche',
'gruppoSorvegliato' => $this->proclamatore->gruppoSorvegliato(),
]);
}
}

View File

@@ -58,7 +58,13 @@ class ProclamatoreIndex extends Component
$this->authorize('settings.manage');
$proclamatore = Proclamatore::findOrFail($id);
$gruppo = $proclamatore->attivo ? $proclamatore->gruppoSorvegliato() : null;
// Un sorvegliante reso non attivo scioglie il gruppo (Proclamatore::booted).
$proclamatore->update(['attivo' => !$proclamatore->attivo]);
if ($gruppo) {
$this->avvisa("{$proclamatore->nome_completo} non è più attivo. " . self::gruppoSciolto($gruppo));
}
}
public function deleteProclamatore(int $id)
@@ -71,14 +77,12 @@ class ProclamatoreIndex extends Component
$this->avvisaErrore("Il proclamatore {$proclamatore->nome_completo} ha assegnazioni aperte. Rientra prima i territori.");
return;
}
if ($this->sorveglianteDiUnGruppo($proclamatore, 'spostarlo nel cestino')) {
return;
}
// Nel cestino non resta membro di un gruppo.
$proclamatore->update(['gruppo_servizio_id' => null]);
// Nel cestino lascia il gruppo; se ne era il sorvegliante, il gruppo
// si scioglie (Proclamatore::booted).
$gruppo = $proclamatore->gruppoSorvegliato();
$proclamatore->delete();
$this->avvisa("Proclamatore {$proclamatore->nome_completo} spostato nel cestino.");
$this->avvisa("Proclamatore {$proclamatore->nome_completo} spostato nel cestino." . ($gruppo ? ' ' . self::gruppoSciolto($gruppo) : ''));
}
public function anonimizza(int $id)
@@ -86,25 +90,15 @@ class ProclamatoreIndex extends Component
$this->authorize('settings.manage');
$proclamatore = Proclamatore::findOrFail($id);
if ($this->sorveglianteDiUnGruppo($proclamatore, 'anonimizzarlo')) {
return;
}
$gruppo = $proclamatore->gruppoSorvegliato();
$proclamatore->anonimizza();
$this->avvisa("Dati personali del proclamatore anonimizzati (GDPR).");
$this->avvisa("Dati personali del proclamatore anonimizzati (GDPR)." . ($gruppo ? ' ' . self::gruppoSciolto($gruppo) : ''));
}
/** Il sorvegliante di un gruppo attivo si toglie sciogliendo il gruppo, non da qui. */
private function sorveglianteDiUnGruppo(Proclamatore $proclamatore, string $azione): bool
/** L'avviso quando un sorvegliante esce di scena: anche per ProclamatoreEdit. */
public static function gruppoSciolto(GruppoServizio $gruppo): string
{
$gruppo = GruppoServizio::attivi()->where('sorvegliante_id', $proclamatore->id)->first();
if (! $gruppo) {
return false;
}
$this->avvisaErrore("{$proclamatore->nome_completo} è il sorvegliante del gruppo {$gruppo->nome}: per {$azione} va prima sciolto il gruppo (Gruppi di servizio).");
return true;
return "Era il sorvegliante del gruppo {$gruppo->nome}: il gruppo è stato sciolto, i territori non ancora dati sono tornati in reparto e la sua utenza è disattivata.";
}
public function render()
@@ -176,6 +170,8 @@ class ProclamatoreIndex extends Component
return view('livewire.proclamatori.proclamatore-index', [
'proclamatori' => $proclamatori,
// Per avvisare, prima di disattivare o cestinare, che il gruppo si scioglie.
'gruppiSorvegliati' => GruppoServizio::attivi()->pluck('nome', 'sorvegliante_id'),
]);
}
}

View File

@@ -6,6 +6,8 @@ use App\Livewire\Concerns\RicordaRitorno;
use Livewire\Component;
use App\Models\Proclamatore;
use App\Models\Assegnazione;
use App\Models\GruppoServizio;
use Spatie\Activitylog\Models\Activity;
class ProclamatoreShow extends Component
{
@@ -37,9 +39,25 @@ class ProclamatoreShow extends Component
),
];
// Gruppo di servizio: sorvegliante (con i dettagli del gruppo e della
// sua utenza), membro, o sorvegliante di gruppi ormai sciolti.
$gruppoSorvegliato = $this->proclamatore->gruppoSorvegliato();
$utenza = $gruppoSorvegliato ? $this->proclamatore->utente : null;
return view('livewire.proclamatori.proclamatore-show', [
'assegnazioniPerAnno' => $assegnazioni,
'stats' => $stats,
'gruppoSorvegliato' => $gruppoSorvegliato,
'riepilogoGruppo' => $gruppoSorvegliato?->riepilogo(),
'gruppoMembro' => $gruppoSorvegliato ? null : $this->proclamatore->gruppo()->with('sorvegliante')->first(),
'gruppiPassati' => GruppoServizio::whereNotNull('sciolto_at')
->where('sorvegliante_id', $this->proclamatore->id)
->orderByDesc('sciolto_at')
->get(),
'utenza' => $utenza,
'ultimoAccesso' => $utenza
? Activity::whereMorphedTo('causer', $utenza)->where('description', 'login')->max('created_at')
: null,
]);
}
}

View File

@@ -87,8 +87,13 @@ class Profile extends Component
public function render()
{
// Il sorvegliante vede qui i dettagli del suo gruppo di servizio.
$gruppo = auth()->user()->gruppoSorvegliato();
return view('livewire.profile', [
'user' => auth()->user(),
'gruppo' => $gruppo,
'riepilogoGruppo' => $gruppo?->riepilogo(),
]);
}
}

View File

@@ -72,4 +72,24 @@ class GruppoServizio extends Model
{
return $query->whereNull('sciolto_at');
}
/**
* I numeri del gruppo per la scheda del proclamatore e il profilo del
* sorvegliante: solo conteggi in SQL, nessun nome da decifrare.
*
* @return array{membri: int, pacchetto: int, da_dare: int, dati: int, assegnazioni: int}
*/
public function riepilogo(): array
{
$pacchetto = $this->territori()->count();
$dati = $this->territori()->whereHas('assegnazioni', fn ($q) => $q->whereNull('returned_at'))->count();
return [
'membri' => $this->membri()->count(),
'pacchetto' => $pacchetto,
'da_dare' => $pacchetto - $dati,
'dati' => $dati,
'assegnazioni' => $this->assegnazioni()->count(),
];
}
}

View File

@@ -30,6 +30,46 @@ class Proclamatore extends Model
return $this->hasOne(User::class, 'proclamatore_id');
}
/**
* Un sorvegliante che non è più attivo — reso non attivo, spostato nel
* cestino o anonimizzato — non guida più il gruppo: il gruppo si scioglie
* (GruppiServizio::sciogli), da qualunque pagina arrivi la modifica. Un
* membro nel cestino lascia il gruppo.
*/
protected static function booted(): void
{
static::updated(function (self $proclamatore) {
if ($proclamatore->wasChanged('attivo') && ! $proclamatore->attivo) {
$proclamatore->lasciaIlGruppo('sorvegliante non più attivo');
}
});
static::deleted(function (self $proclamatore) {
$proclamatore->lasciaIlGruppo('sorvegliante eliminato');
});
}
/** Il gruppo attivo di cui è sorvegliante, se c'è. */
public function gruppoSorvegliato(): ?GruppoServizio
{
return GruppoServizio::attivi()->where('sorvegliante_id', $this->id)->first();
}
private function lasciaIlGruppo(string $motivo): void
{
$gruppo = $this->gruppoSorvegliato();
if ($gruppo) {
app(\App\Services\GruppiServizio::class)->sciogli($gruppo, $motivo);
return;
}
if ($this->trashed() && $this->gruppo_servizio_id) {
static::withTrashed()->whereKey($this->id)->update(['gruppo_servizio_id' => null]);
$this->gruppo_servizio_id = null;
}
}
protected function casts(): array
{
return [

View File

@@ -231,13 +231,13 @@ class GruppiServizio
* fatte restano com'erano, con il loro riferimento al gruppo, che resta
* nello storico come sciolto.
*/
public function sciogli(GruppoServizio $gruppo): void
public function sciogli(GruppoServizio $gruppo, ?string $motivo = null): void
{
if ($gruppo->sciolto_at) {
return;
}
DB::transaction(function () use ($gruppo) {
DB::transaction(function () use ($gruppo, $motivo) {
// Esce dal pacchetto solo quello non ancora dato: un territorio
// assegnato resta del suo gruppo finché non rientra (Assegnazione
// lo libera al rientro), così non può essere ridato due volte.
@@ -258,7 +258,11 @@ class GruppiServizio
$gruppo->update(['sciolto_at' => now()]);
activity()->causedBy(auth()->user())->performedOn($gruppo)
->withProperties(['territori_liberati' => $liberati->all(), 'utente_disattivato' => $utente?->id])
->withProperties(array_filter([
'territori_liberati' => $liberati->all(),
'utente_disattivato' => $utente?->id,
'motivo' => $motivo,
], fn ($v) => $v !== null))
->log('gruppo_sciolto');
});
}