++ gruppi: email fittizia iniziale+cognome per il sorvegliante; sciolto il gruppo l'utenza si elimina

- L'utenza del sorvegliante ha un'email fittizia creata dall'app:
  iniziale del nome e cognome @termanager.it (ffontana@termanager.it),
  come gli altri utenti. Via il nome utente: login di nuovo per email.
- Sciolto il gruppo l'utenza si elimina, il sorvegliante torna un
  proclamatore come gli altri: assegnazioni a chi scioglie, registro a nome
  "Sorvegliante del gruppo ...". Via le utenze disattivate.
- User::eliminaConservandoStorico(): l'eliminazione della pagina Utenti in
  un metodo solo, usato anche dallo scioglimento.
- Migrazione 000030: elimina le utenze gia' disattivate, toglie
  users.username e users.disattivato_at, email di nuovo obbligatoria.
- XML, informativa privacy, README, HANDOFF e test aggiornati.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Francesco Picone
2026-10-20 12:09:15 +02:00
parent 69250ecb8c
commit ec2e7bd7e9
26 changed files with 347 additions and 311 deletions

View File

@@ -21,12 +21,15 @@ use InvalidArgumentException;
*/
class GruppiServizio
{
/** Il dominio delle email fittizie dei sorveglianti (iniziale+cognome@…). */
public const DOMINIO_EMAIL = 'termanager.it';
/**
* Crea il gruppo e l'utenza del sorvegliante: nome utente ricavato dal
* nome del gruppo (niente email), ruolo "sorvegliante", password
* temporanea da cambiare al primo accesso. Restituisce il gruppo e la
* password temporanea, da mostrare una volta sola all'amministratore
* insieme al nome utente ($gruppo->sorvegliante->utente->username).
* Crea il gruppo e l'utenza del sorvegliante: email fittizia ricavata dal
* suo nome (emailFittizia), ruolo "sorvegliante", password temporanea da
* cambiare al primo accesso, nessuna conferma dell'email. Restituisce il
* gruppo e la password temporanea, da mostrare una volta sola
* all'amministratore insieme all'email.
*
* @param int[] $membri id dei proclamatori, oltre al sorvegliante
* @return array{0: GruppoServizio, 1: string}
@@ -46,29 +49,25 @@ class GruppiServizio
if ($sorvegliante->gruppo_servizio_id) {
throw new InvalidArgumentException("{$sorvegliante->nome_completo} fa già parte di un altro gruppo.");
}
// Chi è stato sorvegliante di un gruppo sciolto ha già un'utenza,
// disattivata: si riattiva quella (un proclamatore, un'utenza).
$esistente = $sorvegliante->utente()->first();
if ($esistente && ! $esistente->isDisattivato()) {
if ($sorvegliante->utente()->exists()) {
throw new InvalidArgumentException("{$sorvegliante->nome_completo} ha già un'utenza.");
}
$password = Str::password(12, symbols: false);
$gruppo = DB::transaction(function () use ($nome, $sorvegliante, $membri, $password, $esistente) {
$gruppo = DB::transaction(function () use ($nome, $sorvegliante, $membri, $password) {
$gruppo = GruppoServizio::create(['nome' => $nome, 'sorvegliante_id' => $sorvegliante->id]);
$sorvegliante->update(['gruppo_servizio_id' => $gruppo->id]);
$this->aggiungiMembri($gruppo, $membri);
$utente = $esistente ?? new User(['proclamatore_id' => $sorvegliante->id]);
$utente->forceFill([
$utente = User::create([
'name' => $sorvegliante->nome_completo,
'username' => self::nomeUtente($nome),
'email' => self::emailFittizia($sorvegliante),
'password' => $password,
'password_change_required' => true,
'disattivato_at' => null,
])->save();
$utente->syncRoles(['sorvegliante']);
'proclamatore_id' => $sorvegliante->id,
]);
$utente->assignRole('sorvegliante');
activity()->causedBy(auth()->user())->performedOn($gruppo)
->withProperties(['sorvegliante_id' => $sorvegliante->id, 'utente_id' => $utente->id])
@@ -81,20 +80,27 @@ class GruppiServizio
}
/**
* Il nome utente dal nome del gruppo: minuscole, senza accenti, parole
* unite da un punto ("Gruppo Noce" → "gruppo.noce"); se è già preso, un
* numero in coda ("zisa2"). Mai una chiocciola: al login distingue il
* nome utente dall'email.
* L'email fittizia del sorvegliante, con cui entra: iniziale del nome e
* cognome, minuscoli, senza accenti, spazi né apostrofi, sul dominio
* dell'app (Francesco Fontana → ffontana@termanager.it, Francesco
* D'Angelo → fdangelo@…). Se è già presa, un numero in coda
* (ffontana2@…). Non riceve posta: il sorvegliante non conferma l'email.
*/
public static function nomeUtente(string $nomeGruppo): string
public static function emailFittizia(Proclamatore $proclamatore): string
{
$base = Str::limit(Str::slug($nomeGruppo, '.') ?: 'gruppo', 50, '');
$nome = $base;
for ($n = 2; User::where('username', $nome)->exists(); $n++) {
$nome = $base . $n;
$pulisci = fn (string $testo) => preg_replace('/[^a-z0-9]/', '', Str::lower(Str::ascii($testo)));
$base = Str::limit(mb_substr($pulisci((string) $proclamatore->nome), 0, 1) . $pulisci((string) $proclamatore->cognome), 60, '');
if ($base === '') {
$base = "sorvegliante{$proclamatore->id}";
}
return $nome;
$email = "{$base}@" . self::DOMINIO_EMAIL;
for ($n = 2; User::where('email', $email)->exists(); $n++) {
$email = "{$base}{$n}@" . self::DOMINIO_EMAIL;
}
return $email;
}
/** @param int[] $ids */
@@ -250,8 +256,10 @@ class GruppiServizio
* I territori del pacchetto non ancora dati tornano in reparto; quelli
* già dati restano ai membri come assegnazioni qualsiasi, che conservano
* solo il nome del gruppo (gruppo_nome). I membri restano senza gruppo.
* L'utenza del sorvegliante si disattiva e libera il nome utente; resta
* perché è l'autrice delle assegnazioni che ha fatto.
* L'utenza del sorvegliante si elimina: torna un proclamatore come gli
* altri. Le assegnazioni che aveva fatto o fatto rientrare passano a chi
* scioglie il gruppo; nel registro le sue voci restano, a nome
* "Sorvegliante del gruppo …", senza dati personali.
*/
public function sciogli(GruppoServizio $gruppo, ?string $motivo = null): void
{
@@ -269,30 +277,39 @@ class GruppiServizio
Assegnazione::where('gruppo_servizio_id', $gruppo->id)->update(['gruppo_servizio_id' => null]);
$utente = User::where('proclamatore_id', $gruppo->sorvegliante_id)->first();
if ($utente) {
$utente->removeRole('sorvegliante');
// Il nome utente del gruppo si libera; al suo posto uno che
// identifica l'utenza (backup XML) e con cui non si entra.
$utente->forceFill([
'disattivato_at' => now(),
'remember_token' => null,
'username' => "ex.sorvegliante.{$utente->id}",
])->save();
}
activity()->causedBy(auth()->user())->performedOn($gruppo)
->withProperties(array_filter([
'gruppo' => $gruppo->nome,
'territori_liberati' => $liberati->all(),
'utente_disattivato' => $utente?->id,
'utente_eliminato' => $utente?->id,
'motivo' => $motivo,
], fn ($v) => $v !== null))
->log('gruppo_sciolto');
if ($utente) {
$utente->eliminaConservandoStorico($this->subentrante($utente), "Sorvegliante del gruppo {$gruppo->nome}", null);
}
$gruppo->delete();
});
}
/**
* Chi prende le assegnazioni fatte dal sorvegliante quando la sua utenza
* si elimina: chi sta sciogliendo il gruppo, altrimenti il primo
* amministratore.
*/
private function subentrante(User $sorvegliante): User
{
$chi = auth()->user();
if ($chi instanceof User && $chi->id !== $sorvegliante->id) {
return $chi;
}
return User::role('amministratore')->orderBy('id')->firstOrFail();
}
private function attivo(GruppoServizio $gruppo): void
{
if (! $gruppo->exists || ! GruppoServizio::whereKey($gruppo->id)->exists()) {