diff --git a/HANDOFF.md b/HANDOFF.md index 5f64fdb..b7f6afb 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -150,21 +150,12 @@ e non vede nient'altro. Scelte dell'utente: pacchetti li gestisce **solo l'amministratore**. - Utenza: password temporanea da cambiare al primo accesso, **senza conferma dell'email** (`User::hasVerifiedEmail()` è vero per il - sorvegliante). *Dal 20 ottobre: niente email, nome utente dal nome del - gruppo — vedi sotto.* -- **Togliere il ruolo = sciogliere il gruppo** (`GruppiServizio::sciogli`): - non dati in reparto, membri liberi, utenza senza ruolo e disattivata - (`users.disattivato_at`: login rifiutato, sessione aperta chiusa da - `LimitaSorvegliante`). I territori già dati restano ai membri. Si riparte - con un gruppo nuovo. *Dal 20 ottobre il gruppo sciolto si elimina — vedi + sorvegliante). *Dal 20 ottobre l'email è fittizia, creata dall'app — vedi sotto.* - -Casi aggiunti di mio, da far confermare all'utente: -- Un ex sorvegliante può guidare un gruppo nuovo: `crea()` **riattiva** la - sua utenza disattivata (un proclamatore, un'utenza: `users.proclamatore_id` - è unico), con nome utente e password temporanea nuovi. -- `Proclamatore::anonimizza()` anonimizza anche l'utenza dell'ex sorvegliante - (nome, email, password, ruoli). +- **Togliere il ruolo = sciogliere il gruppo** (`GruppiServizio::sciogli`): + non dati in reparto, membri liberi. I territori già dati restano ai membri. + Si riparte con un gruppo nuovo. *Dal 20 ottobre gruppo e utenza del + sorvegliante si eliminano — vedi sotto.* Decisioni successive dell'utente (20 ottobre 2026): - **Sorvegliante non più attivo ⇒ gruppo sciolto** e territori non dati @@ -179,7 +170,7 @@ Decisioni successive dell'utente (20 ottobre 2026): territori); nel cestino esce dal gruppo. - **Dettagli nel profilo**: la scheda del proclamatore (`ProclamatoreShow`) mostra il gruppo che guida con i numeri (`GruppoServizio::riepilogo()`), - l'utenza (nome utente solo all'amministratore, password temporanea o no, + l'utenza (email solo all'amministratore, password temporanea o no, ultimo accesso dal registro), oppure il gruppo di cui è membro. Il *Profilo* del sorvegliante mostra il suo gruppo con gli stessi numeri e "Torna al mio gruppo". @@ -192,19 +183,33 @@ Decisioni successive dell'utente (20 ottobre 2026): territorio): **confermato dall'utente il 20 ottobre 2026**. Dopo lo scioglimento si torna all'elenco dei gruppi; nella scheda del proclamatore non c'è più "In passato sorvegliante di". -- **Nome utente al posto dell'email**: creando il gruppo non si chiede - l'email; `GruppiServizio::nomeUtente()` ricava `users.username` dal nome - del gruppo (`Str::slug` con il punto: «Gruppo Zisa» → `gruppo.zisa`, - numero in coda se già preso; mai una chiocciola). `users.email` è diventata - facoltativa. Il login ha un solo campo, "Email o nome utente" - (`LoginController`: con la chiocciola cerca l'email, senza il nome utente, - in minuscolo). Dove si mostrava l'email dell'utente ora c'è - `User::identificativo` (email, o nome utente). Il profilo del sorvegliante - mostra il nome utente e non chiede l'email. Il nome del gruppo è unico. - Sciolto il gruppo, il nome utente si libera e l'utenza disattivata prende - `ex.sorvegliante.` (l'anonimizzata `anonimo.`): resta riconoscibile - per l'import XML, che individua gli utenti per email o, senza, per nome - utente. Un gruppo con `` in un XML vecchio non si importa. +- **Email fittizia, niente nome utente** (seconda decisione del 20 ottobre, + che sostituisce il nome utente ricavato dal nome del gruppo fatto qualche + ora prima): creando il gruppo l'email non si chiede, + `GruppiServizio::emailFittizia()` la ricava da iniziale del nome e cognome, + minuscoli, senza accenti, spazi né apostrofi, sul dominio + `GruppiServizio::DOMINIO_EMAIL` = `termanager.it` (Francesco Fontana → + `ffontana@termanager.it`, D'Angelo → `fdangelo@…`, numero in coda se già + presa) — la stessa convenzione degli altri utenti. Il login è di nuovo solo + per email. Il profilo del sorvegliante mostra l'email ma non la fa + cambiare. Il nome del gruppo è unico. +- **Sciolto il gruppo, l'utenza del sorvegliante si elimina** ("torna + proclamatore normale, non tenere traccia"): `User::eliminaConservandoStorico()` + — la stessa eliminazione della pagina *Utenti*, ora in un metodo solo — passa + le assegnazioni che aveva fatto o fatto rientrare a chi scioglie (o al primo + amministratore) e lascia le sue voci nel registro a nome "Sorvegliante del + gruppo …", senza email. Niente più utenze disattivate: via + `users.disattivato_at` e i controlli in `LoginController` e + `LimitaSorvegliante`; una sessione aperta resta senza utente. + `Proclamatore::anonimizza()` non tocca più utenze: se era sorvegliante, il + salvataggio scioglie il gruppo e l'utenza sparisce. +- Migrazione `2026_10_20_000030_sorveglianti_con_email_fittizia`: elimina le + utenze già disattivate (assegnazioni al primo amministratore, registro a + nome "Ex sorvegliante di gruppo"), dà l'email fittizia a chi non l'aveva, + toglie `users.username` e `users.disattivato_at`, rende di nuovo + obbligatoria `users.email`. XML: niente più `` né + ``; da un file vecchio non si importano le utenze + disattivate né i gruppi con ``. - **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 @@ -220,8 +225,8 @@ Decisioni successive dell'utente (20 ottobre 2026): Dove sta: - Migrazione `2026_10_20_000010_gruppi_di_servizio`: tabella `gruppi_servizio`, colonne `gruppo_servizio_id` su proclamatori, territori e - assegnazioni, `users.proclamatore_id` (unico) e `users.disattivato_at`, - permesso `gruppo.gestisci` e ruolo `sorvegliante`. Il seeder dà + assegnazioni, `users.proclamatore_id` (unico) e `users.disattivato_at` + (tolto dalla `000030`), permesso `gruppo.gestisci` e ruolo `sorvegliante`. Il seeder dà all'amministratore tutti i permessi **tranne** `gruppo.gestisci`. - Regole in `App\Services\GruppiServizio` (crea, aggiungiMembri, togliMembro, aggiungiAlPacchetto, rimandaInReparto, assegna, sciogli); i componenti @@ -238,7 +243,7 @@ Dove sta: - La voce di menu "Il mio gruppo" guarda il **ruolo**, non `@can`: l'amministratore passa ogni `@can` (`Gate::before`). - XML: si esportano e reimportano gruppi, membri, pacchetto, provenienza delle - assegnazioni (id e nome del gruppo), nome utente e legame utenza ↔ + assegnazioni (id e nome del gruppo) e legame utenza ↔ proclamatore. - Informativa privacy aggiornata (utenza del sorvegliante, gruppo tra i dati del proclamatore). diff --git a/README.md b/README.md index 7b67265..2d87484 100644 --- a/README.md +++ b/README.md @@ -741,12 +741,13 @@ docker compose --profile dev up -d --build **sorvegliante** (un proclamatore attivo), **membri** e **pacchetto** di territori. Un proclamatore sta in un solo gruppo; il sorvegliante ne è anche membro -- Creando il gruppo il sorvegliante riceve un'**utenza senza email**: un - **nome utente ricavato dal nome del gruppo** («Gruppo Zisa» → - `gruppo.zisa`; se è già preso, un numero in coda) e una password - temporanea, mostrati una volta sola, da cambiare al primo accesso. Al login - il campo accetta l'email o il nome utente. Due gruppi non possono avere lo - stesso nome. Se perde la password, l'amministratore ne genera un'altra +- Creando il gruppo il sorvegliante riceve un'**utenza** con un'**email + fittizia** creata dall'applicazione: iniziale del nome e cognome sul + dominio `termanager.it` (Francesco Fontana → `ffontana@termanager.it`; se + è già presa, un numero in coda), e una password temporanea. Si vedono una + volta sola; la password va cambiata al primo accesso. L'email non si + 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. Restano **"In gruppo …"**: non sono un'assegnazione (statistiche e medie non @@ -759,18 +760,19 @@ docker compose --profile dev up -d --build - 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 - tornano in reparto, i membri si liberano, l'utenza viene disattivata (anche - una sessione aperta si chiude) e il suo nome utente torna libero. I - territori già dati restano ai membri come assegnazioni qualsiasi, che del - gruppo conservano solo il nome ("via gruppo …"). Per ripartire si crea un - gruppo nuovo; se il sorvegliante è lo stesso, si riattiva la sua utenza + tornano in reparto, i membri si liberano e l'**utenza del sorvegliante si + elimina** (anche una sessione aperta si chiude): torna un proclamatore come + gli altri. Le assegnazioni che aveva fatto passano a chi scioglie il + gruppo; nel registro delle attività le sue voci restano a nome + "Sorvegliante del gruppo …". I territori già dati restano ai membri come + assegnazioni qualsiasi, che del gruppo conservano solo il nome ("via + gruppo …"). Per ripartire si crea un gruppo nuovo - **Un sorvegliante che non è più attivo scioglie il gruppo**, come togliergli il ruolo: succede se lo si rende non attivo (dall'elenco o dalla modifica), se lo si sposta nel cestino o lo si anonimizza. Prima di disattivarlo o cestinarlo dall'elenco si chiede conferma; riattivarlo non ricostruisce il gruppo. Un membro non attivo resta nel gruppo ma non riceve territori; nel - cestino esce dal gruppo. Anonimizzando un ex sorvegliante si anonimizza - anche la sua utenza + cestino esce dal gruppo - **Chi fa parte di un gruppo è segnalato**: badge "Gruppo …" o "Sorvegliante del gruppo …" nell'elenco dei proclamatori (con il filtro *In un gruppo di servizio*) e in testa alla scheda; nella scelta del @@ -778,7 +780,7 @@ docker compose --profile dev up -d --build resta comunque sceglibile - **Dettagli del gruppo**: la scheda del proclamatore dice se è sorvegliante (gruppo, membri, territori nel pacchetto e da dare, assegnazioni nate dal - gruppo, nome utente, stato della password e ultimo accesso) o membro; il + gruppo, email per entrare, stato della password e ultimo accesso) o membro; il sorvegliante ritrova gli stessi numeri nel suo *Profilo* @@ -892,7 +894,7 @@ una revoca ha effetto immediato senza bisogno di ricaricare la pagina. | Tabella | Descrizione | |-------------------|--------------------------------------------------------------| -| `users` | Utenti; `password_change_required` blocca l'accesso finché non si sceglie una password nuova; `email` facoltativa e `username` per il sorvegliante, che entra con il nome utente del gruppo; `proclamatore_id` lega l'utenza del sorvegliante al suo proclamatore, `disattivato_at` la chiude allo scioglimento del gruppo | +| `users` | Utenti; `password_change_required` blocca l'accesso finché non si sceglie una password nuova; `proclamatore_id` lega l'utenza del sorvegliante (email fittizia) al suo proclamatore; sciolto il gruppo l'utenza si elimina | | `settings` | Configurazione singleton | | `zone` | Zone territoriali | | `tipologie` | Tipologie di territorio | diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 853cfc2..5dae3b3 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -13,11 +13,8 @@ class LoginController extends Controller { public function __invoke(Request $request): RedirectResponse { - // Il campo si chiama "email" ma accetta anche il nome utente del - // sorvegliante di gruppo, che l'email non ce l'ha: con la chiocciola è - // un'email, senza un nome utente (che la chiocciola non la contiene mai). $credentials = $request->validate([ - 'email' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email'], 'password' => ['required', 'string', 'min:6'], 'remember' => ['nullable', 'boolean'], ]); @@ -32,11 +29,8 @@ class LoginController extends Controller ->withInput($request->only('email', 'remember')); } - $identificativo = trim($credentials['email']); - $campo = str_contains($identificativo, '@') ? 'email' : 'username'; - if (! Auth::attempt([ - $campo => $campo === 'username' ? Str::lower($identificativo) : $identificativo, + 'email' => $credentials['email'], 'password' => $credentials['password'], ], $request->boolean('remember'))) { RateLimiter::hit($throttleKey); @@ -46,16 +40,6 @@ class LoginController extends Controller ->withInput($request->only('email', 'remember')); } - // Utenza disattivata (gruppo di servizio sciolto): la password è giusta - // ma non si entra. - if (Auth::user()->isDisattivato()) { - Auth::logout(); - - return back() - ->withErrors(['email' => 'Questa utenza è stata disattivata.']) - ->withInput($request->only('email', 'remember')); - } - RateLimiter::clear($throttleKey); $request->session()->regenerate(); activity()->causedBy(auth()->user())->log('login'); diff --git a/app/Http/Middleware/LimitaSorvegliante.php b/app/Http/Middleware/LimitaSorvegliante.php index 41104af..b0071aa 100644 --- a/app/Http/Middleware/LimitaSorvegliante.php +++ b/app/Http/Middleware/LimitaSorvegliante.php @@ -4,23 +4,18 @@ namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; use Symfony\Component\HttpFoundation\Response; /** - * Due regole per gli utenti autenticati. - * - * 1. Un'utenza disattivata (gruppo di servizio sciolto) non può più lavorare: - * anche una sessione già aperta viene chiusa alla richiesta successiva. - * - * 2. Il sorvegliante di gruppo vede solo il proprio gruppo e il proprio - * profilo. La Home lo porta a "Il mio gruppo"; ogni altra pagina è 403, - * anche scrivendone l'indirizzo a mano. Le azioni Livewire passano: i - * componenti che può aprire controllano da sé che territori e assegnazioni - * siano del suo gruppo, e gli altri non può caricarli. + * Il sorvegliante di gruppo vede solo il proprio gruppo e il proprio + * profilo. La Home lo porta a "Il mio gruppo"; ogni altra pagina è 403, + * anche scrivendone l'indirizzo a mano. Le azioni Livewire passano: i + * componenti che può aprire controllano da sé che territori e assegnazioni + * siano del suo gruppo, e gli altri non può caricarli. * * Vale solo per chi ha il ruolo sorvegliante e nessun altro: un utente con - * anche un altro ruolo resta regolato dai permessi di quel ruolo. + * anche un altro ruolo resta regolato dai permessi di quel ruolo. Sciolto il + * gruppo l'utenza si elimina: una sessione ancora aperta non ha più utente. */ class LimitaSorvegliante { @@ -43,14 +38,6 @@ class LimitaSorvegliante return $next($request); } - if ($user->isDisattivato()) { - Auth::guard('web')->logout(); - $request->session()->invalidate(); - $request->session()->regenerateToken(); - - return redirect()->route('login')->withErrors(['email' => 'Questa utenza è stata disattivata.']); - } - if (! $user->isSorvegliante() || $user->roles->count() > 1) { return $next($request); } diff --git a/app/Livewire/Gruppi/GruppiIndex.php b/app/Livewire/Gruppi/GruppiIndex.php index 5b66375..f7ac9fa 100644 --- a/app/Livewire/Gruppi/GruppiIndex.php +++ b/app/Livewire/Gruppi/GruppiIndex.php @@ -12,10 +12,10 @@ use Livewire\Component; /** * Gruppi di servizio (solo amministratore): elenco e creazione. Creando un - * gruppo si sceglie il sorvegliante, che riceve un'utenza con un nome utente - * ricavato dal nome del gruppo (niente email) e una password temporanea, - * mostrati qui una volta sola. Membri e pacchetto si gestiscono nella pagina - * del gruppo (GruppoShow). + * gruppo si sceglie il sorvegliante, che riceve un'utenza con un'email + * fittizia ricavata dal suo nome (GruppiServizio::emailFittizia) e una + * password temporanea, mostrate qui una volta sola. Membri e pacchetto si + * gestiscono nella pagina del gruppo (GruppoShow). */ class GruppiIndex extends Component { @@ -72,11 +72,11 @@ class GruppiIndex extends Component ->orderBy('nome') ->get(); - // Possono diventare sorveglianti i proclamatori attivi senza gruppo e - // senza un'utenza attiva (quella di un gruppo sciolto si riattiva). + // Possono diventare sorveglianti i proclamatori attivi senza gruppo (e + // quindi senza utenza: sciolto il gruppo, la si elimina). $candidati = Proclamatore::attivi() ->whereNull('gruppo_servizio_id') - ->whereDoesntHave('utente', fn ($q) => $q->whereNull('disattivato_at')) + ->whereDoesntHave('utente') ->get(); return view('livewire.gruppi.gruppi-index', [ diff --git a/app/Livewire/Gruppi/GruppoShow.php b/app/Livewire/Gruppi/GruppoShow.php index 4b95596..ea7a5d0 100644 --- a/app/Livewire/Gruppi/GruppoShow.php +++ b/app/Livewire/Gruppi/GruppoShow.php @@ -124,7 +124,7 @@ class GruppoShow extends Component $nome = $this->gruppo->nome; $this->servizio()->sciogli($this->gruppo); - session()->flash('success', "Gruppo {$nome} sciolto: i territori non ancora dati sono tornati in reparto e l'utenza del sorvegliante è disattivata."); + session()->flash('success', "Gruppo {$nome} sciolto: i territori non ancora dati sono tornati in reparto e l'utenza del sorvegliante è stata eliminata."); return $this->redirect(route('gruppi.index'), navigate: true); } diff --git a/app/Livewire/Proclamatori/ProclamatoreIndex.php b/app/Livewire/Proclamatori/ProclamatoreIndex.php index 4a97eeb..47cbb5b 100644 --- a/app/Livewire/Proclamatori/ProclamatoreIndex.php +++ b/app/Livewire/Proclamatori/ProclamatoreIndex.php @@ -98,7 +98,7 @@ class ProclamatoreIndex extends Component /** L'avviso quando un sorvegliante esce di scena: anche per ProclamatoreEdit. */ public static function gruppoSciolto(GruppoServizio $gruppo): string { - 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."; + 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 è stata eliminata."; } public function render() diff --git a/app/Livewire/Profile.php b/app/Livewire/Profile.php index 0304ba1..28fdf4b 100644 --- a/app/Livewire/Profile.php +++ b/app/Livewire/Profile.php @@ -20,24 +20,24 @@ class Profile extends Component { $user = auth()->user(); $this->name = $user->name; - $this->email = (string) $user->email; + $this->email = $user->email; } public function updateProfile() { $user = auth()->user(); - // Il sorvegliante di gruppo entra con il nome utente: l'email non gli - // si chiede, e il nome utente lo decide il nome del gruppo. - $senzaEmail = $user->isSorvegliante(); + // Il sorvegliante di gruppo entra con l'email fittizia creata col + // gruppo (GruppiServizio::emailFittizia): dal profilo non la cambia. + $emailFissa = $user->isSorvegliante(); $validated = $this->validate([ 'name' => ['required', 'string', 'max:255'], - ] + ($senzaEmail ? [] : [ + ] + ($emailFissa ? [] : [ 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($user->id)], ])); - $user->fill(['name' => $validated['name']] + ($senzaEmail ? [] : ['email' => $validated['email']])); + $user->fill(['name' => $validated['name']] + ($emailFissa ? [] : ['email' => $validated['email']])); $emailCambiata = $user->isDirty('email'); $user->save(); diff --git a/app/Livewire/Settings/UsersIndex.php b/app/Livewire/Settings/UsersIndex.php index 364311e..7c39537 100644 --- a/app/Livewire/Settings/UsersIndex.php +++ b/app/Livewire/Settings/UsersIndex.php @@ -3,13 +3,10 @@ namespace App\Livewire\Settings; use App\Livewire\Concerns\Avvisa; -use App\Models\Assegnazione; use App\Models\User; -use Illuminate\Support\Facades\DB; use Illuminate\Validation\Rule; use Livewire\Attributes\Locked; use Livewire\Component; -use Spatie\Activitylog\Models\Activity; use Spatie\Permission\Models\Role; class UsersIndex extends Component @@ -115,7 +112,7 @@ class UsersIndex extends Component } /** - * Le utenze dei sorveglianti di gruppo (attive o disattivate) si gestiscono + * Le utenze dei sorveglianti di gruppo si gestiscono * da Gruppi di servizio: cambiarle o cancellarle da qui lascerebbe un * gruppo senza sorvegliante, o un sorvegliante con un ruolo diverso. */ @@ -232,37 +229,7 @@ class UsersIndex extends Component return; } - DB::transaction(function () use ($user, $currentUser) { - $causerName = $user->name; - $causerEmail = $user->email; - $deletedAt = now()->toDateTimeString(); - - Activity::query() - ->where('causer_type', User::class) - ->where('causer_id', $user->id) - ->chunkById(200, function ($activities) use ($causerName, $causerEmail, $deletedAt) { - foreach ($activities as $activity) { - $properties = $activity->properties?->toArray() ?? []; - $properties['causer_name'] = $causerName; - $properties['causer_email'] = $causerEmail; - $properties['causer_deleted_at'] = $deletedAt; - - $activity->properties = $properties; - $activity->save(); - } - }); - - Assegnazione::query() - ->where('created_by', $user->id) - ->update(['created_by' => $currentUser->id]); - - Assegnazione::query() - ->where('returned_by', $user->id) - ->update(['returned_by' => $currentUser->id]); - - $user->syncRoles([]); - $user->delete(); - }); + $user->eliminaConservandoStorico($currentUser, $user->name, $user->email); if ($this->editingUserId === $userId) { $this->cancelEdit(); diff --git a/app/Livewire/Settings/XmlExchange.php b/app/Livewire/Settings/XmlExchange.php index 2cef2c3..9f9aa04 100644 --- a/app/Livewire/Settings/XmlExchange.php +++ b/app/Livewire/Settings/XmlExchange.php @@ -525,18 +525,22 @@ class XmlExchange extends Component $utentiDaCollegare = []; foreach (($xml->users->user ?? []) as $userNode) { $email = trim((string) ($userNode->email ?? '')); - $username = trim((string) ($userNode->username ?? '')); $legacyId = (string) ($userNode['legacy_id'] ?? ''); - // Il sorvegliante di gruppo non ha email: si riconosce dal nome utente. - if ($email === '' && $username === '') { - $this->pushImportIssue('utente', $legacyId, 'Email e nome utente mancanti'); + // Nei file del 19-20 ottobre 2026 c'erano le utenze disattivate + // degli ex sorveglianti: ora, sciolto il gruppo, l'utenza non + // esiste più. Le loro assegnazioni passano a chi importa. + if (trim((string) ($userNode->disattivato_at ?? '')) !== '') { continue; } - $user = User::query()->firstOrNew($email !== '' ? ['email' => $email] : ['username' => $username]); - $user->name = (string) ($userNode->name ?? ($email !== '' ? $email : $username)); - $user->username = $username !== '' ? $username : null; + if ($email === '') { + $this->pushImportIssue('utente', $legacyId, 'Email mancante'); + continue; + } + + $user = User::query()->firstOrNew(['email' => $email]); + $user->name = (string) ($userNode->name ?? $email); if (! $user->exists) { // password is NOT NULL; the real hash is written raw below. $user->password = Str::password(24); @@ -597,10 +601,7 @@ class XmlExchange extends Component // Utenza di un sorvegliante: si collega al proclamatore dopo che // i proclamatori sono stati importati (vedi i gruppi di servizio). - $utentiDaCollegare[$user->id] = [ - 'proclamatore' => (string) ($userNode->legacy_proclamatore_id ?? ''), - 'disattivato_at' => $this->normalizeDateTimeForDb((string) ($userNode->disattivato_at ?? '')), - ]; + $utentiDaCollegare[$user->id] = (string) ($userNode->legacy_proclamatore_id ?? ''); $stats['utenti_importati']++; if ($legacyId !== '') { @@ -681,10 +682,9 @@ class XmlExchange extends Component DB::table('proclamatori')->where('id', $idProclamatore)->update(['gruppo_servizio_id' => $gruppoMap[$legacyGruppo]]); } } - foreach ($utentiDaCollegare as $idUtente => $collegamento) { + foreach ($utentiDaCollegare as $idUtente => $legacyProclamatore) { DB::table('users')->where('id', $idUtente)->update([ - 'proclamatore_id' => $proclamatoreMap[$collegamento['proclamatore']] ?? null, - 'disattivato_at' => $collegamento['disattivato_at'], + 'proclamatore_id' => $proclamatoreMap[$legacyProclamatore] ?? null, ]); } @@ -869,15 +869,13 @@ class XmlExchange extends Component 'legacy_id' => (string) $u->id, 'name' => (string) $u->name, 'email' => (string) $u->email, - 'username' => (string) $u->username, 'password' => (string) $u->getAuthPassword(), 'email_verified_at' => optional($u->email_verified_at)->toIso8601String() ?? '', // remember_token no: all'import si azzera comunque, e nel file // sarebbe solo una credenziale in più per chi lo tiene. 'roles' => $u->roles->pluck('name')->all(), - // Sorvegliante di gruppo: il proclamatore collegato e se l'utenza è disattivata. + // Sorvegliante di gruppo: il proclamatore collegato. 'proclamatore_id' => $u->proclamatore_id ? (string) $u->proclamatore_id : '', - 'disattivato_at' => optional($u->disattivato_at)->toIso8601String() ?? '', 'created_at' => optional($u->created_at)->toIso8601String() ?? '', 'updated_at' => optional($u->updated_at)->toIso8601String() ?? '', ])->all(), @@ -1285,11 +1283,9 @@ class XmlExchange extends Component } $this->addXmlText($node, 'name', (string) ($user['name'] ?? '')); $this->addXmlText($node, 'email', (string) ($user['email'] ?? '')); - $this->addXmlText($node, 'username', (string) ($user['username'] ?? '')); $this->addXmlText($node, 'password', (string) ($user['password'] ?? '')); $node->addChild('email_verified_at', (string) ($user['email_verified_at'] ?? '')); $node->addChild('legacy_proclamatore_id', (string) ($user['proclamatore_id'] ?? '')); - $node->addChild('disattivato_at', (string) ($user['disattivato_at'] ?? '')); $node->addChild('created_at', (string) ($user['created_at'] ?? '')); $node->addChild('updated_at', (string) ($user['updated_at'] ?? '')); $rolesNode = $node->addChild('roles'); diff --git a/app/Models/Proclamatore.php b/app/Models/Proclamatore.php index f958ae7..bb6aa23 100644 --- a/app/Models/Proclamatore.php +++ b/app/Models/Proclamatore.php @@ -175,22 +175,9 @@ class Proclamatore extends Model $this->cognome = 'Proclamatore #' . $this->id; $this->attivo = false; $this->gruppo_servizio_id = null; + // Se era sorvegliante, il salvataggio (non più attivo) scioglie il + // gruppo ed elimina la sua utenza: booted(). $this->save(); $this->delete(); // soft delete - - // L'utenza di un ex sorvegliante (già disattivata: il sorvegliante di - // un gruppo attivo non si anonimizza) porta nome ed email in chiaro. - $utente = $this->utente()->first(); - if ($utente) { - $utente->syncRoles([]); - $utente->forceFill([ - 'name' => 'Anonimo', - 'email' => null, - 'username' => "anonimo.{$utente->id}", - 'password' => \Illuminate\Support\Str::random(40), - 'remember_token' => null, - 'disattivato_at' => $utente->disattivato_at ?? now(), - ])->save(); - } } } diff --git a/app/Models/User.php b/app/Models/User.php index dedc229..ea57ed3 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,6 +9,8 @@ use Illuminate\Notifications\Notifiable; use Spatie\Permission\Traits\HasRoles; use Spatie\Activitylog\Traits\LogsActivity; use Spatie\Activitylog\LogOptions; +use Spatie\Activitylog\Models\Activity; +use Illuminate\Support\Facades\DB; /** * MustVerifyEmail: l'account funziona solo con l'indirizzo confermato @@ -22,11 +24,9 @@ class User extends Authenticatable implements MustVerifyEmail protected $fillable = [ 'name', 'email', - 'username', 'password', 'password_change_required', 'proclamatore_id', - 'disattivato_at', ]; protected $hidden = [ @@ -40,7 +40,6 @@ class User extends Authenticatable implements MustVerifyEmail 'email_verified_at' => 'datetime', 'password' => 'hashed', 'password_change_required' => 'boolean', - 'disattivato_at' => 'datetime', 'proclamatore_id' => 'integer', ]; } @@ -60,7 +59,7 @@ class User extends Authenticatable implements MustVerifyEmail return $this->hasRole('sorvegliante'); } - /** Il gruppo di cui è sorvegliante, se c'è (sciolto, il gruppo non esiste più). */ + /** Il gruppo di cui è sorvegliante (sciolto il gruppo, l'utenza non esiste più). */ public function gruppoSorvegliato(): ?GruppoServizio { return $this->proclamatore_id @@ -69,18 +68,37 @@ class User extends Authenticatable implements MustVerifyEmail } /** - * Con cosa entra: l'email, o il nome utente per il sorvegliante di - * gruppo, che l'email non ce l'ha. Da mostrare dove prima c'era l'email. + * Elimina l'utenza senza perdere lo storico: le assegnazioni che ha fatto + * o fatto rientrare passano a $subentra (la colonna non può restare + * vuota), e nel registro delle attività le sue voci si tengono, con + * scritto chi era ($nome, $email) al posto del collegamento all'utenza. */ - public function getIdentificativoAttribute(): string + public function eliminaConservandoStorico(self $subentra, string $nome, ?string $email): void { - return (string) ($this->email ?? $this->username ?? $this->name); - } + DB::transaction(function () use ($subentra, $nome, $email) { + $eliminatoIl = now()->toDateTimeString(); - /** Utenza disattivata (gruppo sciolto): non può più entrare. */ - public function isDisattivato(): bool - { - return $this->disattivato_at !== null; + Activity::query() + ->where('causer_type', self::class) + ->where('causer_id', $this->id) + ->chunkById(200, function ($attivita) use ($nome, $email, $eliminatoIl) { + foreach ($attivita as $voce) { + $proprieta = $voce->properties?->toArray() ?? []; + $proprieta['causer_name'] = $nome; + $proprieta['causer_email'] = $email; + $proprieta['causer_deleted_at'] = $eliminatoIl; + + $voce->properties = $proprieta; + $voce->save(); + } + }); + + Assegnazione::query()->where('created_by', $this->id)->update(['created_by' => $subentra->id]); + Assegnazione::query()->where('returned_by', $this->id)->update(['returned_by' => $subentra->id]); + + $this->syncRoles([]); + $this->delete(); + }); } /** diff --git a/app/Services/GruppiServizio.php b/app/Services/GruppiServizio.php index 55f2edf..9e312fb 100644 --- a/app/Services/GruppiServizio.php +++ b/app/Services/GruppiServizio.php @@ -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()) { diff --git a/database/migrations/2026_10_20_000030_sorveglianti_con_email_fittizia.php b/database/migrations/2026_10_20_000030_sorveglianti_con_email_fittizia.php new file mode 100644 index 0000000..829205e --- /dev/null +++ b/database/migrations/2026_10_20_000030_sorveglianti_con_email_fittizia.php @@ -0,0 +1,92 @@ +join('roles', 'roles.id', '=', 'model_has_roles.role_id') + ->where('roles.name', 'amministratore') + ->where('model_has_roles.model_type', 'App\\Models\\User') + ->min('model_has_roles.model_id'); + + if (Schema::hasColumn('users', 'disattivato_at')) { + foreach (DB::table('users')->whereNotNull('disattivato_at')->get() as $utente) { + if (! $amministratore) { + continue; // nessuno a cui passare le assegnazioni: resta + } + + DB::table('assegnazioni')->where('created_by', $utente->id)->update(['created_by' => $amministratore]); + DB::table('assegnazioni')->where('returned_by', $utente->id)->update(['returned_by' => $amministratore]); + + foreach (DB::table('activity_log')->where('causer_type', 'App\\Models\\User')->where('causer_id', $utente->id)->get() as $voce) { + $proprieta = json_decode((string) $voce->properties, true) ?: []; + $proprieta['causer_name'] = 'Ex sorvegliante di gruppo'; + $proprieta['causer_email'] = null; + $proprieta['causer_deleted_at'] = now()->toDateTimeString(); + DB::table('activity_log')->where('id', $voce->id)->update(['properties' => json_encode($proprieta)]); + } + + DB::table('model_has_roles')->where('model_type', 'App\\Models\\User')->where('model_id', $utente->id)->delete(); + DB::table('model_has_permissions')->where('model_type', 'App\\Models\\User')->where('model_id', $utente->id)->delete(); + DB::table('users')->where('id', $utente->id)->delete(); + } + } + + // Chi è rimasto senza email (sorveglianti col nome utente): la fittizia. + foreach (DB::table('users')->whereNull('email')->get() as $utente) { + $proclamatore = $utente->proclamatore_id ? Proclamatore::withTrashed()->find($utente->proclamatore_id) : null; + $email = $proclamatore + ? GruppiServizio::emailFittizia($proclamatore) + : "utente{$utente->id}@" . GruppiServizio::DOMINIO_EMAIL; + DB::table('users')->where('id', $utente->id)->update(['email' => $email]); + } + + if (Schema::hasColumn('users', 'username')) { + Schema::table('users', function (Blueprint $table) { + $table->dropUnique(['username']); + }); + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('username'); + }); + } + if (Schema::hasColumn('users', 'disattivato_at')) { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('disattivato_at'); + }); + } + Schema::table('users', function (Blueprint $table) { + $table->string('email')->nullable(false)->change(); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('email')->nullable()->change(); + }); + Schema::table('users', function (Blueprint $table) { + $table->string('username', 60)->nullable()->unique()->after('email'); + $table->timestamp('disattivato_at')->nullable(); + }); + } +}; diff --git a/resources/views/auth/password-change.blade.php b/resources/views/auth/password-change.blade.php index 8e5703d..c1cc438 100644 --- a/resources/views/auth/password-change.blade.php +++ b/resources/views/auth/password-change.blade.php @@ -5,7 +5,7 @@ {{ $forced ? 'Imposta una nuova password' : 'Cambia password' }}

- Account: {{ $user->identificativo }} + Account: {{ $user->email }}

@@ -46,7 +46,7 @@ {{-- L'email in un campo nascosto: senza, i gestori di password non sanno a quale account associare la credenziale che salvano. --}} - + @unless($forced) {{-- Dopo un ripristino non si chiede: la password attuale e' diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php index dd1c83a..4cd311d 100644 --- a/resources/views/components/layouts/app.blade.php +++ b/resources/views/components/layouts/app.blade.php @@ -50,7 +50,7 @@ {{ auth()->user()->name }} - {{ auth()->user()->identificativo }} · {{ ucfirst(auth()->user()->roles->first()?->name ?? 'utente') }} + {{ auth()->user()->email }} · {{ ucfirst(auth()->user()->roles->first()?->name ?? 'utente') }} diff --git a/resources/views/livewire/auth/login.blade.php b/resources/views/livewire/auth/login.blade.php index d2364de..3b2c852 100644 --- a/resources/views/livewire/auth/login.blade.php +++ b/resources/views/livewire/auth/login.blade.php @@ -16,10 +16,8 @@
@csrf
- {{-- Email, o nome utente per il sorvegliante di un gruppo di servizio --}} - - Email + @error('email')

{{ $message }}

@enderror
diff --git a/resources/views/livewire/gruppi/gruppi-index.blade.php b/resources/views/livewire/gruppi/gruppi-index.blade.php index 16abd8e..7a91dec 100644 --- a/resources/views/livewire/gruppi/gruppi-index.blade.php +++ b/resources/views/livewire/gruppi/gruppi-index.blade.php @@ -12,7 +12,7 @@

Consegna queste credenziali a {{ $creato->sorvegliante?->nome_completo }}: la password si vede solo ora e al primo accesso va cambiata.

Indirizzo
{{ route('login') }}
-
Nome utente
{{ $creato->sorvegliante?->utente?->username }}
+
Email
{{ $creato->sorvegliante?->utente?->email }}
Password
{{ $passwordTemporanea }}
@@ -29,13 +29,12 @@ -

Dal nome nasce il nome utente del sorvegliante (es. «Gruppo Zisa» → gruppo.zisa).

@error('nome')

{{ $message }}

@enderror
-

Un proclamatore attivo che non fa già parte di un gruppo. Diventa anche membro del gruppo.

+

Un proclamatore attivo che non fa già parte di un gruppo. Diventa anche membro del gruppo, e riceve un'utenza con un'email fittizia: iniziale del nome e cognome (es. Francesco Fontana → ffontana@{{ \App\Services\GruppiServizio::DOMINIO_EMAIL }}).

@error('sorveglianteId')

{{ $message }}

@enderror
Crea il gruppo diff --git a/resources/views/livewire/gruppi/gruppo-show.blade.php b/resources/views/livewire/gruppi/gruppo-show.blade.php index 74ef3ab..65b552c 100644 --- a/resources/views/livewire/gruppi/gruppo-show.blade.php +++ b/resources/views/livewire/gruppi/gruppo-show.blade.php @@ -8,7 +8,7 @@

Sorvegliante: {{ $gruppo->sorvegliante?->nome_completo ?? '—' }} @if($gruppo->sorvegliante?->utente) - · nome utente {{ $gruppo->sorvegliante->utente->identificativo }} + · accede con {{ $gruppo->sorvegliante->utente->email }} @endif

@@ -121,7 +121,7 @@ Nuova password temporanea Togli il ruolo e sciogli il gruppo + wire:confirm="Togliere il ruolo di sorvegliante a {{ $gruppo->sorvegliante?->nome_completo }}? Il gruppo si scioglie e viene eliminato: i territori non ancora dati tornano in reparto, i membri restano senza gruppo e l'utenza del sorvegliante viene eliminata. Le assegnazioni già fatte restano ai membri.">Togli il ruolo e sciogli il gruppo diff --git a/resources/views/livewire/privacy.blade.php b/resources/views/livewire/privacy.blade.php index 137ab77..22177c2 100644 --- a/resources/views/livewire/privacy.blade.php +++ b/resources/views/livewire/privacy.blade.php @@ -19,7 +19,7 @@
  • Proclamatori: solo nome e cognome, cifrati nel database ({{ $cifratura }}), se sono attivi e l'eventuale gruppo di servizio di cui fanno parte. Nessun numero di telefono né indirizzo email.
  • Territori e piantine: numero, zona, tipologia, confini, mappe e PDF — nessun dato personale.
  • Assegnazioni: collegamento tra proclamatore e territorio, con date di assegnazione e rientro, conteggio nelle campagne, codice del link alla piantina, stato del suo invio (inviato o no, e con quale canale) e, se l'ha data il sorvegliante di un gruppo di servizio, il nome del gruppo.
  • -
  • Utenti dell'applicazione: nome, email (che va confermata), password conservata solo come impronta non reversibile, ruolo. Il sorvegliante di un gruppo di servizio non ha email: entra con un nome utente ricavato dal nome del gruppo. La sua utenza è collegata al suo proclamatore e viene disattivata quando il gruppo si scioglie; il gruppo, sciolto, viene eliminato.
  • +
  • Utenti dell'applicazione: nome, email (che va confermata), password conservata solo come impronta non reversibile, ruolo. Il sorvegliante di un gruppo di servizio entra con un'email fittizia creata dall'applicazione (iniziale del nome e cognome), che non riceve posta e non va confermata. La sua utenza è collegata al suo proclamatore e viene eliminata quando il gruppo si scioglie; anche il gruppo, sciolto, viene eliminato.
  • Registro delle attività: chi ha fatto cosa e quando. I proclamatori vi compaiono solo con un identificativo numerico, mai con il nome.
  • @@ -58,7 +58,7 @@
  • {{ ucfirst($ruolo['nome']) }}: {{ $ruolo['permessi'] ? implode('; ', $ruolo['permessi']) : 'nessun permesso' }}.
  • @endforeach -

    Il proclamatore non ha un account: riceve solo il link alla piantina del territorio che gli è assegnato. Fa eccezione il sorvegliante di un gruppo di servizio, che ha un'utenza (nome utente e password) con cui vede solo il proprio gruppo: i territori del suo pacchetto e i nomi dei membri.

    +

    Il proclamatore non ha un account: riceve solo il link alla piantina del territorio che gli è assegnato. Fa eccezione il sorvegliante di un gruppo di servizio, che ha un'utenza (email fittizia e password) con cui vede solo il proprio gruppo: i territori del suo pacchetto e i nomi dei membri.

    @@ -100,7 +100,7 @@

    Conservazione dei dati

      -
    • I dati di un proclamatore possono essere anonimizzati su richiesta: nome e cognome vengono cancellati in modo irreversibile, insieme al nome dell'utenza se è stato sorvegliante di un gruppo.
    • +
    • I dati di un proclamatore possono essere anonimizzati su richiesta: nome e cognome vengono cancellati in modo irreversibile; se era sorvegliante di un gruppo, il gruppo si scioglie e la sua utenza viene eliminata.
    • Territori e proclamatori eliminati restano nel cestino finché non vengono eliminati definitivamente.
    • Il registro delle attività è conservato per {{ $auditRetention }} giorni (modificabile nelle impostazioni), poi cancellato automaticamente.
    • Lo storico delle assegnazioni è conservato per la durata necessaria alla gestione territoriale.
    • diff --git a/resources/views/livewire/proclamatori/proclamatore-form.blade.php b/resources/views/livewire/proclamatori/proclamatore-form.blade.php index b06f646..8779e4d 100644 --- a/resources/views/livewire/proclamatori/proclamatore-form.blade.php +++ b/resources/views/livewire/proclamatori/proclamatore-form.blade.php @@ -28,7 +28,7 @@ @if($gruppoSorvegliato ?? null)

      È il sorvegliante del gruppo {{ $gruppoSorvegliato->nome }}: se non è più attivo il gruppo si scioglie, - i territori non ancora dati tornano in reparto e la sua utenza viene disattivata. + i territori non ancora dati tornano in reparto e la sua utenza viene eliminata.

      @endif diff --git a/resources/views/livewire/proclamatori/proclamatore-index.blade.php b/resources/views/livewire/proclamatori/proclamatore-index.blade.php index 67cd283..3692ed8 100644 --- a/resources/views/livewire/proclamatori/proclamatore-index.blade.php +++ b/resources/views/livewire/proclamatori/proclamatore-index.blade.php @@ -83,7 +83,7 @@ {{-- Sorvegliante: disattivarlo scioglie il gruppo, si chiede conferma --}} @@ -101,7 +101,7 @@ @endif diff --git a/resources/views/livewire/proclamatori/proclamatore-show.blade.php b/resources/views/livewire/proclamatori/proclamatore-show.blade.php index 6cdf9c6..ed05027 100644 --- a/resources/views/livewire/proclamatori/proclamatore-show.blade.php +++ b/resources/views/livewire/proclamatori/proclamatore-show.blade.php @@ -78,7 +78,7 @@ @if($utenza)

      Utenza - @can('settings.manage') {{ $utenza->identificativo }} @endcan + @can('settings.manage') {{ $utenza->email }} @endcan · {{ $utenza->password_change_required ? 'password temporanea, ancora da cambiare' : 'password personale impostata' }} · {{ $ultimoAccesso ? 'ultimo accesso il ' . \Illuminate\Support\Carbon::parse($ultimoAccesso)->format('d/m/Y H:i') : 'non è ancora entrato' }}

      diff --git a/resources/views/livewire/profile.blade.php b/resources/views/livewire/profile.blade.php index 152d4f1..7641a7a 100644 --- a/resources/views/livewire/profile.blade.php +++ b/resources/views/livewire/profile.blade.php @@ -17,7 +17,7 @@

      {{ $user->name }}

      - {{ $user->identificativo }} + {{ $user->email }} {{ ucfirst($user->roles->first()?->name ?? 'utente') }} @@ -74,11 +74,11 @@ @error('name')

      {{ $message }}

      @enderror
      @if($user->isSorvegliante()) - {{-- Il sorvegliante entra con il nome utente del gruppo, senza email --}} + {{-- Il sorvegliante entra con l'email fittizia creata col gruppo --}}
      -

      Nome utente

      -

      {{ $user->username }}

      -

      Viene dal nome del gruppo: lo usi per entrare, insieme alla password.

      +

      Email per entrare

      +

      {{ $user->email }}

      +

      L'ha creata l'applicazione insieme al gruppo: la usi per entrare, insieme alla password. Non riceve posta.

      @else
      diff --git a/resources/views/livewire/settings/users-index.blade.php b/resources/views/livewire/settings/users-index.blade.php index 772d940..7a1fec9 100644 --- a/resources/views/livewire/settings/users-index.blade.php +++ b/resources/views/livewire/settings/users-index.blade.php @@ -111,14 +111,8 @@ @else {{ $user->name }} - @if(! $user->email) - {{-- Sorvegliante di gruppo: entra con il nome utente, senza email --}} - @if($user->username)nome utente {{ $user->username }}@else—@endif - @else {{ $user->email }} - @endif - @if(! $user->email) - @elseif($user->hasVerifiedEmail()) + @if($user->hasVerifiedEmail()) confermata @else proclamatore_id) {{-- Utenza di un sorvegliante di gruppo: si gestisce dal gruppo. --}} - {{ $user->disattivato_at ? 'Disattivata (gruppo sciolto)' : 'Sorvegliante: si gestisce da Gruppi di servizio' }} + Sorvegliante: si gestisce da Gruppi di servizio @else
      diff --git a/tests/Feature/GruppiServizioTest.php b/tests/Feature/GruppiServizioTest.php index 2de76d7..12a3d9d 100644 --- a/tests/Feature/GruppiServizioTest.php +++ b/tests/Feature/GruppiServizioTest.php @@ -73,7 +73,7 @@ class GruppiServizioTest extends TestCase app(GruppiServizio::class)->aggiungiAlPacchetto($gruppo, $territori->pluck('id')->all()); $territori = $territori->map->fresh(); - $sorvegliante = User::where('username', 'zisa')->sole(); + $sorvegliante = User::where('email', 'mrossi@termanager.it')->sole(); // Il cambio password al primo accesso ha i suoi test: qui si lavora. // E l'email non confermata, come nella realtà (TestCase le conferma). $sorvegliante->forceFill(['password_change_required' => false, 'email_verified_at' => null])->save(); @@ -87,20 +87,18 @@ class GruppiServizioTest extends TestCase { $rossi = $this->proclamatore('Rossi', 'Mario'); - // Niente email: il nome utente viene dal nome del gruppo. + // L'email non si chiede: è fittizia, iniziale del nome e cognome. $pagina = Livewire::test(GruppiIndex::class) ->assertDontSee('Email per l') ->set('nome', 'Zisa') ->set('sorveglianteId', $rossi->id) ->call('crea') ->assertHasNoErrors() - ->assertSee('Nome utente') - ->assertSee('zisa'); + ->assertSee('mrossi@termanager.it'); $gruppo = GruppoServizio::sole(); - $utente = User::where('username', 'zisa')->sole(); + $utente = User::where('email', 'mrossi@termanager.it')->sole(); - $this->assertNull($utente->email); $this->assertSame($rossi->id, $gruppo->sorvegliante_id); $this->assertSame($gruppo->id, $rossi->fresh()->gruppo_servizio_id, 'Il sorvegliante è anche membro'); $this->assertSame($rossi->id, $utente->proclamatore_id); @@ -118,21 +116,22 @@ class GruppiServizioTest extends TestCase $this->assertTrue(\Illuminate\Support\Facades\Hash::check($password, $utente->password)); } - public function test_il_nome_utente_viene_dal_nome_del_gruppo(): void + public function test_l_email_fittizia_viene_da_iniziale_del_nome_e_cognome(): void { - $servizio = app(GruppiServizio::class); + $email = fn (string $cognome, string $nome) => GruppiServizio::emailFittizia($this->proclamatore($cognome, $nome)); - $servizio->crea('Gruppo Noce Città', $this->proclamatore('Neri')); - $this->assertSame('gruppo.noce.citta', User::whereNotNull('username')->sole()->username); + $this->assertSame('ffontana@termanager.it', $email('Fontana', 'Francesco')); + $this->assertSame('fdangelo@termanager.it', $email("D'Angelo", 'Francesco')); + $this->assertSame('ndeluca@termanager.it', $email('De Luca', 'Niccolò')); - // Già preso (per esempio da un'utenza riattivata): un numero in coda. - User::create(['name' => 'Altro', 'username' => 'zisa', 'password' => 'password-di-prova']); - $servizio->crea('Zisa', $this->proclamatore('Gialli')); - $this->assertTrue(User::where('username', 'zisa2')->exists()); + // Già presa da un altro utente: un numero in coda. + User::create(['name' => 'Francesco Fontana', 'email' => 'ffontana@termanager.it', 'password' => 'password-di-prova']); + $this->assertSame('ffontana2@termanager.it', $email('Fontana', 'Federica')); // Due gruppi con lo stesso nome no. + app(GruppiServizio::class)->crea('Zisa', $this->proclamatore('Gialli')); $this->expectException(InvalidArgumentException::class); - $servizio->crea('Zisa', $this->proclamatore('Blu')); + app(GruppiServizio::class)->crea('Zisa', $this->proclamatore('Blu')); } public function test_un_proclamatore_sta_in_un_solo_gruppo(): void @@ -220,25 +219,16 @@ class GruppiServizioTest extends TestCase $this->get(route('dashboard'))->assertOk()->assertDontSee('Il mio gruppo')->assertSee('Gruppi di servizio'); } - public function test_il_sorvegliante_entra_con_il_nome_utente_e_va_al_suo_gruppo(): void + public function test_il_sorvegliante_entra_con_l_email_fittizia_senza_conferma_e_va_al_suo_gruppo(): void { $this->gruppo(); $this->flushSession(); auth()->logout(); $password = 'una-password-nuova-1'; - User::where('username', 'zisa')->sole()->forceFill(['password' => $password])->save(); + User::where('email', 'mrossi@termanager.it')->sole()->forceFill(['password' => $password])->save(); - $this->get(route('login'))->assertSee('Email o nome utente'); - - // Senza badare alle maiuscole (la tastiera del telefono le mette da sé). - $this->post(route('login.store'), ['email' => 'Zisa', 'password' => $password]) + $this->post(route('login.store'), ['email' => 'mrossi@termanager.it', 'password' => $password]) ->assertRedirect(route('mio-gruppo')); - - // Gli altri continuano a entrare con l'email. - auth()->logout(); - $this->flushSession(); - $this->post(route('login.store'), ['email' => 'admin@example.test', 'password' => 'password-di-prova']) - ->assertRedirect(route('dashboard')); } // ─── Il sorvegliante al lavoro ────────────────────────────── @@ -350,7 +340,10 @@ class GruppiServizioTest extends TestCase public function test_togliere_il_ruolo_scioglie_il_gruppo(): void { ['gruppo' => $gruppo, 'sorvegliante' => $s, 'verdi' => $verdi, 'territori' => $territori] = $this->gruppo(); + // L'assegnazione la fa il sorvegliante, come nella realtà. + auth()->setUser($s); $data = app(GruppiServizio::class)->assegna($gruppo, $territori[0], $verdi, now()); + auth()->setUser($this->admin); Livewire::test(GruppoShow::class, ['gruppo' => $gruppo]) ->call('sciogli') @@ -368,20 +361,26 @@ class GruppiServizioTest extends TestCase $this->assertNull($data->fresh()->gruppo_servizio_id); $this->assertSame('Zisa', $data->fresh()->gruppo_nome); $this->get(route('territori.show', $territori[0]))->assertOk()->assertSee('via gruppo Zisa'); - // Membri liberi; utenza disattivata, senza ruolo, e il nome utente del - // gruppo è libero. + // Membri liberi; l'utenza del sorvegliante non c'è più, lui torna un + // proclamatore come gli altri. $this->assertSame(0, Proclamatore::whereNotNull('gruppo_servizio_id')->count()); - $this->assertTrue($s->fresh()->isDisattivato()); - $this->assertFalse($s->fresh()->hasRole('sorvegliante')); - $this->assertSame("ex.sorvegliante.{$s->id}", $s->fresh()->username); - $this->assertFalse(User::where('username', 'zisa')->exists()); + $this->assertNull(User::find($s->id)); + $this->assertNotNull(Proclamatore::find($s->proclamatore_id)); + $this->assertSame(0, \Illuminate\Support\Facades\DB::table('model_has_roles')->where('model_id', $s->id)->count()); + // Quello che aveva fatto passa a chi ha sciolto il gruppo; nel registro + // le sue voci restano, senza dati personali. + $this->assertSame($this->admin->id, $data->fresh()->created_by); + $voce = \Spatie\Activitylog\Models\Activity::where('description', 'assigned')->sole(); + $this->assertNull($voce->causer); + $this->assertSame('Sorvegliante del gruppo Zisa', $voce->properties['causer_name']); + $this->assertNull($voce->properties['causer_email']); // Per ripartire serve un gruppo nuovo. $this->expectException(InvalidArgumentException::class); app(GruppiServizio::class)->aggiungiAlPacchetto($gruppo, [$territori[1]->id]); } - public function test_un_utenza_disattivata_non_entra_e_viene_buttata_fuori(): void + public function test_sciolto_il_gruppo_l_ex_sorvegliante_non_entra_piu(): void { ['gruppo' => $gruppo, 'sorvegliante' => $s] = $this->gruppo(); $password = 'una-password-nuova-1'; @@ -390,29 +389,25 @@ class GruppiServizioTest extends TestCase $this->flushSession(); auth()->logout(); - $this->post(route('login.store'), ['email' => 'zisa', 'password' => $password]) + $this->post(route('login.store'), ['email' => 'mrossi@termanager.it', 'password' => $password]) ->assertSessionHasErrors('email'); $this->assertGuest(); - - $this->actingAs($s->fresh())->get(route('profile'))->assertRedirect(route('login')); } - public function test_l_ex_sorvegliante_puo_guidare_un_gruppo_nuovo_con_la_stessa_utenza(): void + public function test_l_ex_sorvegliante_puo_guidare_un_gruppo_nuovo(): void { - ['gruppo' => $vecchio, 'rossi' => $rossi, 'sorvegliante' => $s] = $this->gruppo(); + ['gruppo' => $vecchio, 'rossi' => $rossi] = $this->gruppo(); app(GruppiServizio::class)->sciogli($vecchio); $pagina = Livewire::test(GruppiIndex::class); $this->assertContains((string) $rossi->id, collect($pagina->viewData('opzioniSorveglianti'))->pluck('valore')->all(), 'Torna tra i candidati'); - // Sciolto, il nome non è più occupato: si può rifare "Zisa". + // Sciolto, il nome del gruppo e l'email sono di nuovo liberi. [$nuovo, $password] = app(GruppiServizio::class)->crea('Zisa', $rossi->fresh()); - $utente = $s->fresh(); - $this->assertSame(1, User::where('proclamatore_id', $rossi->id)->count(), 'Un proclamatore, un\'utenza'); - $this->assertFalse($utente->isDisattivato()); + $utente = User::where('proclamatore_id', $rossi->id)->sole(); $this->assertTrue($utente->hasRole('sorvegliante')); - $this->assertSame('zisa', $utente->username); + $this->assertSame('mrossi@termanager.it', $utente->email); $this->assertTrue($utente->password_change_required); $this->assertTrue(\Illuminate\Support\Facades\Hash::check($password, $utente->password)); $this->assertSame($nuovo->id, $utente->gruppoSorvegliato()?->id); @@ -429,8 +424,7 @@ class GruppiServizioTest extends TestCase $this->assertSame('in_reparto', $g['territori'][2]->fresh()->stato); $this->assertNull($data->fresh()->returned_at, 'Quello già dato resta al membro'); $this->assertNull($g['bianchi']->fresh()->gruppo_servizio_id, 'Membri liberi'); - $this->assertTrue($g['sorvegliante']->fresh()->isDisattivato()); - $this->assertFalse($g['sorvegliante']->fresh()->hasRole('sorvegliante')); + $this->assertNull(User::find($g['sorvegliante']->id), 'Utenza eliminata'); } public function test_disattivare_il_sorvegliante_dall_elenco_scioglie_il_gruppo(): void @@ -501,7 +495,7 @@ class GruppiServizioTest extends TestCase $this->get(route('proclamatori.show', $g['rossi']))->assertOk() ->assertSeeInOrder(['Gruppo di servizio', 'Sorvegliante', 'Zisa', 'Membri', '3', 'Nel pacchetto', '3', 'Da dare', '2', 'Assegnazioni dal gruppo', '1']) - ->assertSee('zisa', false) + ->assertSee('mrossi@termanager.it', false) ->assertSee('non è ancora entrato'); $this->get(route('proclamatori.show', $g['verdi']))->assertOk() @@ -563,19 +557,15 @@ class GruppiServizioTest extends TestCase // ─── Proclamatori: anonimizzazione, registro ──────────────── - public function test_anonimizzare_un_ex_sorvegliante_anonimizza_anche_la_sua_utenza(): void + public function test_anonimizzare_il_sorvegliante_scioglie_il_gruppo_e_ne_elimina_l_utenza(): void { ['gruppo' => $gruppo, 'rossi' => $rossi, 'sorvegliante' => $s] = $this->gruppo(); - app(GruppiServizio::class)->sciogli($gruppo); Livewire::test(\App\Livewire\Proclamatori\ProclamatoreIndex::class)->call('anonimizza', $rossi->id); - $utente = $s->fresh(); - $this->assertSame('Anonimo', $utente->name); - $this->assertNull($utente->email); - $this->assertSame("anonimo.{$utente->id}", $utente->username); - $this->assertTrue($utente->isDisattivato()); - $this->assertCount(0, $utente->roles); + $this->assertNull(GruppoServizio::find($gruppo->id)); + $this->assertNull(User::find($s->id), "Nome ed email dell'utenza spariscono con lei"); + $this->assertSame('Anonimo', Proclamatore::withTrashed()->find($rossi->id)->nome); } public function test_nel_registro_delle_attivita_l_utenza_del_sorvegliante_non_ha_nome_ne_email(): void @@ -624,8 +614,8 @@ class GruppiServizioTest extends TestCase $this->assertEqualsCanonicalizing(['12', '14', '35'], $g->territori->pluck('numero')->all()); $this->assertSame($g->id, Assegnazione::sole()->gruppo_servizio_id); $this->assertSame('Zisa', Assegnazione::sole()->gruppo_nome); - $utenza = User::where('username', 'zisa')->sole(); - $this->assertNull($utenza->email); + $utenza = User::where('email', 'mrossi@termanager.it')->sole(); + $this->assertTrue($utenza->hasRole('sorvegliante')); $this->assertSame($g->sorvegliante_id, $utenza->proclamatore_id); } @@ -641,7 +631,7 @@ class GruppiServizioTest extends TestCase $this->assertSame(0, GruppoServizio::count()); $this->assertNull(Assegnazione::sole()->gruppo_servizio_id); $this->assertSame('Zisa', Assegnazione::sole()->gruppo_nome); - // L'utenza disattivata, autrice dell'assegnazione, sopravvive al giro. - $this->assertTrue(User::where('username', 'like', 'ex.sorvegliante.%')->sole()->isDisattivato()); + // Dell'ex sorvegliante non resta un'utenza: solo l'amministratore. + $this->assertSame([$this->admin->id], User::pluck('id')->all()); } }