From aaffeb7de7f4af94cdb8fb504f654781e61977ae Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Tue, 20 Oct 2026 11:45:00 +0200 Subject: [PATCH] ++ gruppi: il gruppo sciolto si elimina; il sorvegliante entra con un nome utente dal nome del gruppo - Sciolto, il gruppo non resta nello storico: si elimina (via sciolto_at; la migrazione elimina anche quelli gia' sciolti). I territori solo nel pacchetto tornano in reparto; quelli gia' dati restano ai membri, e l'assegnazione conserva il nome del gruppo (assegnazioni.gruppo_nome). - Creando il gruppo non si chiede l'email: users.username dal nome del gruppo ("Gruppo Zisa" -> gruppo.zisa), users.email facoltativa. Login con "Email o nome utente". Nome del gruppo unico. - Sciolto il gruppo il nome utente si libera (l'utenza disattivata prende ex.sorvegliante.). XML: nome utente e gruppo_nome. - Profilo, testata, cambio password e Utenti mostrano il nome utente dove manca l'email. Informativa privacy, README, HANDOFF e test aggiornati. Co-Authored-By: Claude Opus 5.5 --- HANDOFF.md | 46 +++++-- README.md | 31 +++-- app/Http/Controllers/Auth/LoginController.php | 10 +- app/Livewire/Gruppi/GruppiIndex.php | 18 ++- app/Livewire/Gruppi/GruppoShow.php | 28 ++--- .../Proclamatori/ProclamatoreIndex.php | 2 +- .../Proclamatori/ProclamatoreShow.php | 7 +- app/Livewire/Profile.php | 14 ++- app/Livewire/Registro.php | 2 +- app/Livewire/Settings/XmlExchange.php | 27 ++-- app/Livewire/Territori/TerritorioShow.php | 2 +- app/Models/Assegnazione.php | 1 + app/Models/GruppoServizio.php | 22 +--- app/Models/Proclamatore.php | 5 +- app/Models/User.php | 14 ++- app/Services/GruppiServizio.php | 84 ++++++++----- ...020_gruppi_senza_storico_e_nome_utente.php | 87 +++++++++++++ .../views/auth/password-change.blade.php | 4 +- .../views/components/layouts/app.blade.php | 2 +- resources/views/livewire/auth/login.blade.php | 6 +- .../livewire/gruppi/gruppi-index.blade.php | 17 +-- .../livewire/gruppi/gruppo-show.blade.php | 13 +- resources/views/livewire/privacy.blade.php | 8 +- .../proclamatori/proclamatore-show.blade.php | 13 +- resources/views/livewire/profile.blade.php | 13 +- resources/views/livewire/registro.blade.php | 2 +- .../livewire/settings/users-index.blade.php | 8 +- .../territori/territorio-show.blade.php | 2 +- tests/Feature/GruppiServizioTest.php | 119 +++++++++++++----- 29 files changed, 408 insertions(+), 199 deletions(-) create mode 100644 database/migrations/2026_10_20_000020_gruppi_senza_storico_e_nome_utente.php diff --git a/HANDOFF.md b/HANDOFF.md index 50a8349..2af5958 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -148,19 +148,21 @@ e non vede nient'altro. Scelte dell'utente: - Il sorvegliante: assegna, manda il link (copia/condividi), registra i rientri, apre la piantina, rimanda in reparto i non dati. Gruppi, membri e pacchetti li gestisce **solo l'amministratore**. -- Utenza: email e password temporanea da cambiare al primo accesso, **senza +- Utenza: password temporanea da cambiare al primo accesso, **senza conferma dell'email** (`User::hasVerifiedEmail()` è vero per il - sorvegliante; anche cambiandola dal profilo). + 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`), gruppo archiviato (`sciolto_at`) per lo storico. I - territori già dati restano ai membri. Si riparte con un gruppo nuovo. + `LimitaSorvegliante`). I territori già dati restano ai membri. Si riparte + con un gruppo nuovo. *Dal 20 ottobre il gruppo sciolto si elimina — 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 email e password temporanea nuove. + è unico), con nome utente e password temporanea nuovi. - `Proclamatore::anonimizza()` anonimizza anche l'utenza dell'ex sorvegliante (nome, email, password, ruoli). @@ -177,10 +179,33 @@ 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 (email solo all'amministratore, password temporanea o no, ultimo - accesso dal registro), oppure il gruppo di cui è membro, e i gruppi sciolti - che ha guidato. Il *Profilo* del sorvegliante mostra il suo gruppo con gli - stessi numeri e "Torna al mio gruppo". + l'utenza (nome utente 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". +- **Il gruppo sciolto non resta nello storico**: `sciogli()` lo elimina + (colonna `sciolto_at` tolta dalla migrazione `2026_10_20_000020`, che + elimina anche i gruppi già sciolti). I territori solo nel pacchetto tornano + in reparto (conferma esplicita dell'utente); quelli già dati restano ai + membri come assegnazioni qualsiasi. Sulla traccia delle assegnazioni + l'utente non si è espresso: ho tenuto la scelta che non perde dati, il + nome del gruppo copiato sull'assegnazione (`assegnazioni.gruppo_nome`, + "via gruppo …" in registro e scheda territorio). Toglierlo è un attimo, se + lo chiede. 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. - **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 @@ -214,7 +239,8 @@ 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 e il legame utenza ↔ proclamatore. + assegnazioni (id e nome del gruppo), nome utente e legame utenza ↔ + proclamatore. - Informativa privacy aggiornata (utenza del sorvegliante, gruppo tra i dati del proclamatore). - L'azzeramento dell'applicazione (*Zona pericolosa*) cancella anche i gruppi. diff --git a/README.md b/README.md index ac1d706..7b67265 100644 --- a/README.md +++ b/README.md @@ -741,10 +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**: l'email indicata e - una password temporanea, mostrata una volta sola e da cambiare al primo - accesso. **Nessuna conferma dell'email**. Se la perde, l'amministratore ne - genera un'altra dalla pagina del gruppo +- 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 + 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 cambiano), escono da "Da assegnare" e dalla pagina *Assegna* @@ -754,10 +757,12 @@ docker compose --profile dev up -d --build "via gruppo …"; poi manda il link (copia o condividi), registra i rientri, apre la piantina e rimanda in reparto quelli non ancora dati - Al **rientro** il territorio torna in reparto ed esce dal pacchetto -- **Sciogliere il gruppo** toglie il ruolo al sorvegliante: i territori non - ancora dati tornano in reparto, i membri si liberano, l'utenza viene - disattivata (anche una sessione aperta si chiude) e il gruppo resta nello - storico. I territori già dati restano ai membri. Per ripartire si crea un +- **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 - **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), @@ -773,8 +778,8 @@ 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, stato dell'utenza e ultimo accesso), membro, o sorvegliante di - gruppi ormai sciolti; il sorvegliante ritrova gli stessi numeri nel suo + gruppo, nome utente, stato della password e ultimo accesso) o membro; il + sorvegliante ritrova gli stessi numeri nel suo *Profilo* ### Campagne @@ -887,16 +892,16 @@ 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; `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; `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 | | `settings` | Configurazione singleton | | `zone` | Zone territoriali | | `tipologie` | Tipologie di territorio | | `proclamatori` | Assegnatari, con nome/cognome cifrati e soft delete (niente contatti); `gruppo_servizio_id` è il gruppo di cui fanno parte | -| `gruppi_servizio` | Gruppi di servizio: nome, sorvegliante, `sciolto_at` (sciolti restano per lo storico) | +| `gruppi_servizio` | Gruppi di servizio attivi: nome (unico), sorvegliante. Sciolto, il gruppo si elimina | | `territori` | Cartoline con zona, tipologia, PDF, miniatura, soft delete; `gruppo_servizio_id` se sono nel pacchetto di un gruppo | | `anni_teocratici` | Periodi settembre → agosto | | `campagne` | Periodi speciali con date e descrizione | -| `assegnazioni` | Territorio ↔ proclamatore, con date, anno e campagna; stato dell'invio del link (`link_sent`, `link_da_inviare`, `link_canale`); `gruppo_servizio_id` se data dal sorvegliante di un gruppo | +| `assegnazioni` | Territorio ↔ proclamatore, con date, anno e campagna; stato dell'invio del link (`link_sent`, `link_da_inviare`, `link_canale`); `gruppo_servizio_id` se data dal sorvegliante di un gruppo ancora attivo, `gruppo_nome` che resta anche dopo lo scioglimento | | `activity_log` | Audit trail | **Vincoli e indici principali** diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 8b2ba70..853cfc2 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -13,8 +13,11 @@ 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', 'email'], + 'email' => ['required', 'string', 'max:255'], 'password' => ['required', 'string', 'min:6'], 'remember' => ['nullable', 'boolean'], ]); @@ -29,8 +32,11 @@ class LoginController extends Controller ->withInput($request->only('email', 'remember')); } + $identificativo = trim($credentials['email']); + $campo = str_contains($identificativo, '@') ? 'email' : 'username'; + if (! Auth::attempt([ - 'email' => $credentials['email'], + $campo => $campo === 'username' ? Str::lower($identificativo) : $identificativo, 'password' => $credentials['password'], ], $request->boolean('remember'))) { RateLimiter::hit($throttleKey); diff --git a/app/Livewire/Gruppi/GruppiIndex.php b/app/Livewire/Gruppi/GruppiIndex.php index 5311b96..5b66375 100644 --- a/app/Livewire/Gruppi/GruppiIndex.php +++ b/app/Livewire/Gruppi/GruppiIndex.php @@ -12,9 +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 l'email - * indicata e una password temporanea, mostrata 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 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). */ class GruppiIndex extends Component { @@ -24,8 +25,6 @@ class GruppiIndex extends Component public ?int $sorveglianteId = null; - public string $email = ''; - /** Gruppo appena creato e password temporanea del sorvegliante, da consegnare. */ #[Locked] public ?int $creatoId = null; @@ -38,19 +37,17 @@ class GruppiIndex extends Component $this->authorize('settings.manage'); $this->validate([ - 'nome' => 'required|string|max:100', + 'nome' => 'required|string|max:100|unique:gruppi_servizio,nome', 'sorveglianteId' => 'required|integer|exists:proclamatori,id', - 'email' => 'required|email|max:255', ], [ + 'nome.unique' => 'Esiste già un gruppo con questo nome.', '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()); @@ -58,7 +55,7 @@ class GruppiIndex extends Component return; } - $this->reset(['nome', 'sorveglianteId', 'email']); + $this->reset(['nome', 'sorveglianteId']); $this->creatoId = $gruppo->id; $this->passwordTemporanea = $password; } @@ -72,7 +69,6 @@ class GruppiIndex extends Component { $gruppi = GruppoServizio::with('sorvegliante') ->withCount(['membri', 'territori']) - ->orderByRaw('sciolto_at IS NOT NULL') ->orderBy('nome') ->get(); diff --git a/app/Livewire/Gruppi/GruppoShow.php b/app/Livewire/Gruppi/GruppoShow.php index bf7dc45..4b95596 100644 --- a/app/Livewire/Gruppi/GruppoShow.php +++ b/app/Livewire/Gruppi/GruppoShow.php @@ -102,7 +102,7 @@ class GruppoShow extends Component { $this->servizio(); $utente = User::where('proclamatore_id', $this->gruppo->sorvegliante_id)->first(); - if (! $utente || $this->gruppo->sciolto_at) { + if (! $utente) { $this->avvisaErrore('Il gruppo non ha un sorvegliante con utenza attiva.'); return; @@ -115,11 +115,17 @@ class GruppoShow extends Component $this->passwordTemporanea = $password; } - /** Toglie il ruolo di sorvegliante: il gruppo si scioglie. */ - public function sciogli(): void + /** + * Toglie il ruolo di sorvegliante: il gruppo si scioglie e si elimina, si + * torna all'elenco dei gruppi. + */ + public function sciogli() { - $this->esegui(fn () => $this->servizio()->sciogli($this->gruppo), "Gruppo {$this->gruppo->nome} sciolto."); - $this->passwordTemporanea = null; + $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."); + return $this->redirect(route('gruppi.index'), navigate: true); } public function render() @@ -129,21 +135,13 @@ class GruppoShow extends Component $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(), + '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). - 'candidatiTerritori' => $attivo - ? Territorio::inReparto()->with('zona')->get()->sortBy('numero', SORT_NATURAL)->values() - : collect(), - 'assegnazioniDelGruppo' => $this->gruppo->assegnazioni()->count(), + 'candidatiTerritori' => Territorio::inReparto()->with('zona')->get()->sortBy('numero', SORT_NATURAL)->values(), ]); } } diff --git a/app/Livewire/Proclamatori/ProclamatoreIndex.php b/app/Livewire/Proclamatori/ProclamatoreIndex.php index 60f47fa..4a97eeb 100644 --- a/app/Livewire/Proclamatori/ProclamatoreIndex.php +++ b/app/Livewire/Proclamatori/ProclamatoreIndex.php @@ -175,7 +175,7 @@ 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'), + 'gruppiSorvegliati' => GruppoServizio::pluck('nome', 'sorvegliante_id'), ]); } } diff --git a/app/Livewire/Proclamatori/ProclamatoreShow.php b/app/Livewire/Proclamatori/ProclamatoreShow.php index 9817526..b5550b0 100644 --- a/app/Livewire/Proclamatori/ProclamatoreShow.php +++ b/app/Livewire/Proclamatori/ProclamatoreShow.php @@ -6,7 +6,6 @@ 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 @@ -40,7 +39,7 @@ class ProclamatoreShow extends Component ]; // Gruppo di servizio: sorvegliante (con i dettagli del gruppo e della - // sua utenza), membro, o sorvegliante di gruppi ormai sciolti. + // sua utenza) o membro. I gruppi sciolti non esistono più. $gruppoSorvegliato = $this->proclamatore->gruppoSorvegliato(); $utenza = $gruppoSorvegliato ? $this->proclamatore->utente : null; @@ -50,10 +49,6 @@ class ProclamatoreShow extends Component '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') diff --git a/app/Livewire/Profile.php b/app/Livewire/Profile.php index 3bc772d..0304ba1 100644 --- a/app/Livewire/Profile.php +++ b/app/Livewire/Profile.php @@ -20,22 +20,24 @@ class Profile extends Component { $user = auth()->user(); $this->name = $user->name; - $this->email = $user->email; + $this->email = (string) $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(); + $validated = $this->validate([ 'name' => ['required', 'string', 'max:255'], + ] + ($senzaEmail ? [] : [ 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($user->id)], - ]); + ])); - $user->fill([ - 'name' => $validated['name'], - 'email' => $validated['email'], - ]); + $user->fill(['name' => $validated['name']] + ($senzaEmail ? [] : ['email' => $validated['email']])); $emailCambiata = $user->isDirty('email'); $user->save(); diff --git a/app/Livewire/Registro.php b/app/Livewire/Registro.php index 52a828d..0f4117d 100644 --- a/app/Livewire/Registro.php +++ b/app/Livewire/Registro.php @@ -285,7 +285,7 @@ class Registro extends Component public function render() { - $query = Assegnazione::with(['territorio.zona', 'territorio.assegnazioneCorrente', 'proclamatore', 'annoTeocratico', 'campagna', 'gruppo']); + $query = Assegnazione::with(['territorio.zona', 'territorio.assegnazioneCorrente', 'proclamatore', 'annoTeocratico', 'campagna']); if ($this->filtroAnno) { $query->where('anno_teocratico_id', $this->filtroAnno); diff --git a/app/Livewire/Settings/XmlExchange.php b/app/Livewire/Settings/XmlExchange.php index deea148..2cef2c3 100644 --- a/app/Livewire/Settings/XmlExchange.php +++ b/app/Livewire/Settings/XmlExchange.php @@ -525,15 +525,18 @@ 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'] ?? ''); - if ($email === '') { - $this->pushImportIssue('utente', $legacyId, 'Email mancante'); + // Il sorvegliante di gruppo non ha email: si riconosce dal nome utente. + if ($email === '' && $username === '') { + $this->pushImportIssue('utente', $legacyId, 'Email e nome utente mancanti'); continue; } - $user = User::query()->firstOrNew(['email' => $email]); - $user->name = (string) ($userNode->name ?? $email); + $user = User::query()->firstOrNew($email !== '' ? ['email' => $email] : ['username' => $username]); + $user->name = (string) ($userNode->name ?? ($email !== '' ? $email : $username)); + $user->username = $username !== '' ? $username : null; if (! $user->exists) { // password is NOT NULL; the real hash is written raw below. $user->password = Str::password(24); @@ -657,10 +660,15 @@ class XmlExchange extends Component $gruppoMap = []; foreach (($xml->gruppi_servizio->gruppo ?? []) as $gruppoNode) { $legacySorvegliante = (string) ($gruppoNode->legacy_sorvegliante_id ?? ''); + // Nei file del 19-20 ottobre 2026 un gruppo sciolto c'era ancora + // (): ora un gruppo sciolto non esiste più, quindi + // non si importa. + if (trim((string) ($gruppoNode->sciolto_at ?? '')) !== '') { + continue; + } $g = GruppoServizio::create([ 'nome' => (string) $gruppoNode->nome, 'sorvegliante_id' => $proclamatoreMap[$legacySorvegliante] ?? null, - 'sciolto_at' => $this->normalizeDateTimeForDb((string) ($gruppoNode->sciolto_at ?? '')), ]); $this->applyRowMeta('gruppi_servizio', $g->id, $gruppoNode); $legacyId = (string) ($gruppoNode['legacy_id'] ?? ''); @@ -809,6 +817,7 @@ class XmlExchange extends Component 'link_sent' => ((int) ($assegnazioneNode->link_sent ?? 0)) === 1, 'link_da_inviare' => ((int) ($assegnazioneNode->link_da_inviare ?? 0)) === 1, 'gruppo_servizio_id' => $gruppoMap[(string) ($assegnazioneNode->legacy_gruppo_id ?? '')] ?? null, + 'gruppo_nome' => trim((string) ($assegnazioneNode->gruppo_nome ?? '')) ?: null, 'link_canale' => in_array((string) ($assegnazioneNode->link_canale ?? ''), array_keys(Assegnazione::CANALI_LINK), true) ? (string) $assegnazioneNode->link_canale : null, 'pdf_access_code' => $pdfAccessCode !== '' ? $pdfAccessCode : null, 'note' => (string) ($assegnazioneNode->note ?? ''), @@ -860,6 +869,7 @@ 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 @@ -874,11 +884,11 @@ class XmlExchange extends Component 'zones' => $this->righeEsportate(Zona::query()->orderBy('id'), ['id', 'nome', 'attivo', 'created_at', 'updated_at']), 'tipologie' => $this->righeEsportate(Tipologia::query()->orderBy('id'), ['id', 'nome', 'attivo', 'created_at', 'updated_at']), 'proclamatori' => $this->righeEsportate(Proclamatore::query()->withTrashed()->orderBy('id'), ['id', 'nome', 'cognome', 'attivo', 'gruppo_servizio_id', 'deleted_at', 'created_at', 'updated_at']), - 'gruppi_servizio' => $this->righeEsportate(GruppoServizio::query()->orderBy('id'), ['id', 'nome', 'sorvegliante_id', 'sciolto_at', 'created_at', 'updated_at']), + 'gruppi_servizio' => $this->righeEsportate(GruppoServizio::query()->orderBy('id'), ['id', 'nome', 'sorvegliante_id', 'created_at', 'updated_at']), 'territori' => $this->righeEsportate(Territorio::query()->withTrashed()->orderBy('id'), ['id', 'numero', 'zona_id', 'tipologia_id', 'confini', 'note', 'attivo', 'prioritario', 'gruppo_servizio_id', 'pdf_path', 'thumbnail_path', 'deleted_at', 'created_at', 'updated_at']), 'anni_teocratici' => $this->righeEsportate(AnnoTeocratico::query()->orderBy('id'), ['id', 'label', 'start_date', 'end_date', 'created_at', 'updated_at']), 'campagne' => $this->righeEsportate(Campagna::query()->orderBy('id'), ['id', 'descrizione', 'start_date', 'end_date', 'created_at', 'updated_at']), - 'assegnazioni' => $this->righeEsportate(Assegnazione::query()->orderBy('id'), ['id', 'territorio_id', 'proclamatore_id', 'anno_teocratico_id', 'campaign_id', 'assigned_at', 'returned_at', 'counted_in_campaign', 'note', 'created_by', 'returned_by', 'link_sent', 'link_da_inviare', 'link_canale', 'gruppo_servizio_id', 'pdf_access_code', 'created_at', 'updated_at']), + 'assegnazioni' => $this->righeEsportate(Assegnazione::query()->orderBy('id'), ['id', 'territorio_id', 'proclamatore_id', 'anno_teocratico_id', 'campaign_id', 'assigned_at', 'returned_at', 'counted_in_campaign', 'note', 'created_by', 'returned_by', 'link_sent', 'link_da_inviare', 'link_canale', 'gruppo_servizio_id', 'gruppo_nome', 'pdf_access_code', 'created_at', 'updated_at']), ]; } @@ -1275,6 +1285,7 @@ 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'] ?? '')); @@ -1352,7 +1363,6 @@ class XmlExchange extends Component $this->addLegacyIdAttribute($node, $gruppo); $this->addXmlText($node, 'nome', (string) ($gruppo['nome'] ?? '')); $node->addChild('legacy_sorvegliante_id', (string) ($gruppo['sorvegliante_id'] ?? '')); - $node->addChild('sciolto_at', (string) ($gruppo['sciolto_at'] ?? '')); $this->addTimestampNodes($node, $gruppo); } @@ -1382,6 +1392,7 @@ class XmlExchange extends Component $node->addChild('link_sent', (string) ((int) ($assegnazione['link_sent'] ?? 0))); $node->addChild('link_da_inviare', (string) ((int) ($assegnazione['link_da_inviare'] ?? 0))); $node->addChild('legacy_gruppo_id', (string) ($assegnazione['gruppo_servizio_id'] ?? '')); + $this->addXmlText($node, 'gruppo_nome', (string) ($assegnazione['gruppo_nome'] ?? '')); $this->addXmlText($node, 'link_canale', (string) ($assegnazione['link_canale'] ?? '')); $this->addXmlText($node, 'pdf_access_code', (string) ($assegnazione['pdf_access_code'] ?? '')); $this->addXmlText($node, 'note', (string) ($assegnazione['note'] ?? '')); diff --git a/app/Livewire/Territori/TerritorioShow.php b/app/Livewire/Territori/TerritorioShow.php index 7602ff5..d34b8ac 100644 --- a/app/Livewire/Territori/TerritorioShow.php +++ b/app/Livewire/Territori/TerritorioShow.php @@ -21,7 +21,7 @@ class TerritorioShow extends Component public function render() { $assegnazioni = Assegnazione::where('territorio_id', $this->territorio->id) - ->with(['proclamatore', 'annoTeocratico', 'campagna', 'creatoDa', 'rientratoDa', 'gruppo']) + ->with(['proclamatore', 'annoTeocratico', 'campagna', 'creatoDa', 'rientratoDa']) ->orderByDesc('assigned_at') ->get() ->groupBy(fn($a) => $a->annoTeocratico->label); diff --git a/app/Models/Assegnazione.php b/app/Models/Assegnazione.php index 1eeb547..a419562 100644 --- a/app/Models/Assegnazione.php +++ b/app/Models/Assegnazione.php @@ -26,6 +26,7 @@ class Assegnazione extends Model 'created_by', 'returned_by', 'gruppo_servizio_id', + 'gruppo_nome', ]; /** Il gruppo di servizio da cui è nata (data dal sorvegliante a un membro). */ diff --git a/app/Models/GruppoServizio.php b/app/Models/GruppoServizio.php index d185830..b9a0444 100644 --- a/app/Models/GruppoServizio.php +++ b/app/Models/GruppoServizio.php @@ -17,9 +17,9 @@ use Spatie\Activitylog\Traits\LogsActivity; * gruppo_servizio_id per dire da dove viene. Al rientro il territorio torna in * reparto ed esce dal pacchetto. * - * Un gruppo sciolto (sciolto_at) non ha più membri né pacchetto, ma resta: le - * assegnazioni nate da lui continuano a citarlo. Logica in - * App\Services\GruppiServizio. + * Sciolto, il gruppo si elimina: non resta nello storico (decisione del 20 + * ottobre 2026). Le assegnazioni già date dal sorvegliante ne conservano solo + * il nome (assegnazioni.gruppo_nome). Logica in App\Services\GruppiServizio. */ class GruppoServizio extends Model { @@ -27,17 +27,17 @@ class GruppoServizio extends Model protected $table = 'gruppi_servizio'; - protected $fillable = ['nome', 'sorvegliante_id', 'sciolto_at']; + protected $fillable = ['nome', 'sorvegliante_id']; protected function casts(): array { - return ['sciolto_at' => 'datetime', 'sorvegliante_id' => 'integer']; + return ['sorvegliante_id' => 'integer']; } public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() - ->logOnly(['nome', 'sorvegliante_id', 'sciolto_at']) + ->logOnly(['nome', 'sorvegliante_id']) ->logOnlyDirty() ->dontSubmitEmptyLogs(); } @@ -63,16 +63,6 @@ class GruppoServizio extends Model return $this->hasMany(Assegnazione::class, 'gruppo_servizio_id'); } - public function getScioltoAttribute(): bool - { - return $this->sciolto_at !== null; - } - - public function scopeAttivi($query) - { - 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. diff --git a/app/Models/Proclamatore.php b/app/Models/Proclamatore.php index 64c88c8..f958ae7 100644 --- a/app/Models/Proclamatore.php +++ b/app/Models/Proclamatore.php @@ -52,7 +52,7 @@ class Proclamatore extends Model /** Il gruppo attivo di cui è sorvegliante, se c'è. */ public function gruppoSorvegliato(): ?GruppoServizio { - return GruppoServizio::attivi()->where('sorvegliante_id', $this->id)->first(); + return GruppoServizio::where('sorvegliante_id', $this->id)->first(); } private function lasciaIlGruppo(string $motivo): void @@ -185,7 +185,8 @@ class Proclamatore extends Model $utente->syncRoles([]); $utente->forceFill([ 'name' => 'Anonimo', - 'email' => "utente-{$utente->id}@anonimo.invalid", + 'email' => null, + 'username' => "anonimo.{$utente->id}", 'password' => \Illuminate\Support\Str::random(40), 'remember_token' => null, 'disattivato_at' => $utente->disattivato_at ?? now(), diff --git a/app/Models/User.php b/app/Models/User.php index 8fb3b02..dedc229 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -22,6 +22,7 @@ class User extends Authenticatable implements MustVerifyEmail protected $fillable = [ 'name', 'email', + 'username', 'password', 'password_change_required', 'proclamatore_id', @@ -59,14 +60,23 @@ class User extends Authenticatable implements MustVerifyEmail return $this->hasRole('sorvegliante'); } - /** Il gruppo (attivo) di cui è sorvegliante, se c'è. */ + /** Il gruppo di cui è sorvegliante, se c'è (sciolto, il gruppo non esiste più). */ public function gruppoSorvegliato(): ?GruppoServizio { return $this->proclamatore_id - ? GruppoServizio::attivi()->where('sorvegliante_id', $this->proclamatore_id)->first() + ? GruppoServizio::where('sorvegliante_id', $this->proclamatore_id)->first() : null; } + /** + * 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. + */ + public function getIdentificativoAttribute(): string + { + return (string) ($this->email ?? $this->username ?? $this->name); + } + /** Utenza disattivata (gruppo sciolto): non può più entrare. */ public function isDisattivato(): bool { diff --git a/app/Services/GruppiServizio.php b/app/Services/GruppiServizio.php index 5513f07..55f2edf 100644 --- a/app/Services/GruppiServizio.php +++ b/app/Services/GruppiServizio.php @@ -22,20 +22,24 @@ use InvalidArgumentException; class GruppiServizio { /** - * Crea il gruppo e l'utenza del sorvegliante: 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. + * 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). * * @param int[] $membri id dei proclamatori, oltre al sorvegliante * @return array{0: GruppoServizio, 1: string} */ - public function crea(string $nome, Proclamatore $sorvegliante, string $email, array $membri = []): array + public function crea(string $nome, Proclamatore $sorvegliante, array $membri = []): array { $nome = trim($nome); if ($nome === '') { throw new InvalidArgumentException('Il nome del gruppo è obbligatorio.'); } + if (GruppoServizio::where('nome', $nome)->exists()) { + throw new InvalidArgumentException("Esiste già un gruppo «{$nome}»."); + } if (! $sorvegliante->attivo || $sorvegliante->trashed()) { throw new InvalidArgumentException('Il sorvegliante deve essere un proclamatore attivo.'); } @@ -48,13 +52,10 @@ class GruppiServizio if ($esistente && ! $esistente->isDisattivato()) { throw new InvalidArgumentException("{$sorvegliante->nome_completo} ha già un'utenza."); } - if (User::where('email', $email)->when($esistente, fn ($q) => $q->whereKeyNot($esistente->id))->exists()) { - throw new InvalidArgumentException("L'email {$email} è già usata da un altro utente."); - } $password = Str::password(12, symbols: false); - $gruppo = DB::transaction(function () use ($nome, $sorvegliante, $email, $membri, $password, $esistente) { + $gruppo = DB::transaction(function () use ($nome, $sorvegliante, $membri, $password, $esistente) { $gruppo = GruppoServizio::create(['nome' => $nome, 'sorvegliante_id' => $sorvegliante->id]); $sorvegliante->update(['gruppo_servizio_id' => $gruppo->id]); $this->aggiungiMembri($gruppo, $membri); @@ -62,7 +63,7 @@ class GruppiServizio $utente = $esistente ?? new User(['proclamatore_id' => $sorvegliante->id]); $utente->forceFill([ 'name' => $sorvegliante->nome_completo, - 'email' => $email, + 'username' => self::nomeUtente($nome), 'password' => $password, 'password_change_required' => true, 'disattivato_at' => null, @@ -79,6 +80,23 @@ class GruppiServizio return [$gruppo, $password]; } + /** + * 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. + */ + public static function nomeUtente(string $nomeGruppo): string + { + $base = Str::limit(Str::slug($nomeGruppo, '.') ?: 'gruppo', 50, ''); + $nome = $base; + for ($n = 2; User::where('username', $nome)->exists(); $n++) { + $nome = $base . $n; + } + + return $nome; + } + /** @param int[] $ids */ public function aggiungiMembri(GruppoServizio $gruppo, array $ids): void { @@ -205,6 +223,8 @@ class GruppiServizio 'assigned_at' => $data, 'created_by' => auth()->id(), 'gruppo_servizio_id' => $gruppo->id, + // Il nome resta anche quando il gruppo, sciolto, non c'è più. + 'gruppo_nome' => $gruppo->nome, 'link_da_inviare' => (bool) $territorio->pdf_path, ]); }); @@ -225,52 +245,58 @@ class GruppiServizio } /** - * Scioglie il gruppo (si toglie il ruolo di sorvegliante): i territori del - * pacchetto non ancora dati tornano in reparto, i membri restano senza - * gruppo, l'utenza del sorvegliante si disattiva. Le assegnazioni già - * fatte restano com'erano, con il loro riferimento al gruppo, che resta - * nello storico come sciolto. + * Scioglie il gruppo (si toglie il ruolo di sorvegliante, o il + * sorvegliante non è più attivo) e lo elimina: non resta nello storico. + * 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. */ public function sciogli(GruppoServizio $gruppo, ?string $motivo = null): void { - if ($gruppo->sciolto_at) { + if (! GruppoServizio::whereKey($gruppo->id)->exists()) { return; } 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. - $nonDati = Territorio::withTrashed() + $liberati = Territorio::withTrashed() ->where('gruppo_servizio_id', $gruppo->id) - ->whereDoesntHave('assegnazioni', fn ($q) => $q->whereNull('returned_at')); - $liberati = (clone $nonDati)->pluck('numero'); - $nonDati->update(['gruppo_servizio_id' => null]); - + ->whereDoesntHave('assegnazioni', fn ($q) => $q->whereNull('returned_at')) + ->pluck('numero'); + Territorio::withTrashed()->where('gruppo_servizio_id', $gruppo->id)->update(['gruppo_servizio_id' => null]); Proclamatore::withTrashed()->where('gruppo_servizio_id', $gruppo->id)->update(['gruppo_servizio_id' => null]); + 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'); - $utente->forceFill(['disattivato_at' => now(), 'remember_token' => null])->save(); + // 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(); } - $gruppo->update(['sciolto_at' => now()]); - activity()->causedBy(auth()->user())->performedOn($gruppo) ->withProperties(array_filter([ + 'gruppo' => $gruppo->nome, 'territori_liberati' => $liberati->all(), 'utente_disattivato' => $utente?->id, 'motivo' => $motivo, ], fn ($v) => $v !== null)) ->log('gruppo_sciolto'); + + $gruppo->delete(); }); } private function attivo(GruppoServizio $gruppo): void { - if ($gruppo->sciolto_at) { - throw new InvalidArgumentException('Il gruppo è stato sciolto: creane uno nuovo.'); + if (! $gruppo->exists || ! GruppoServizio::whereKey($gruppo->id)->exists()) { + throw new InvalidArgumentException('Il gruppo non esiste più: è stato sciolto.'); } } } diff --git a/database/migrations/2026_10_20_000020_gruppi_senza_storico_e_nome_utente.php b/database/migrations/2026_10_20_000020_gruppi_senza_storico_e_nome_utente.php new file mode 100644 index 0000000..1c17654 --- /dev/null +++ b/database/migrations/2026_10_20_000020_gruppi_senza_storico_e_nome_utente.php @@ -0,0 +1,87 @@ +string('username', 60)->nullable()->unique()->after('email'); + }); + Schema::table('users', function (Blueprint $table) { + $table->string('email')->nullable()->change(); + }); + + Schema::table('assegnazioni', function (Blueprint $table) { + $table->string('gruppo_nome', 100)->nullable()->after('gruppo_servizio_id'); + }); + + foreach (DB::table('gruppi_servizio')->get() as $gruppo) { + DB::table('assegnazioni')->where('gruppo_servizio_id', $gruppo->id)->update(['gruppo_nome' => $gruppo->nome]); + + if ($gruppo->sciolto_at !== null) { + // Già sciolto: pacchetto e membri erano già stati liberati. + DB::table('territori')->where('gruppo_servizio_id', $gruppo->id)->update(['gruppo_servizio_id' => null]); + DB::table('proclamatori')->where('gruppo_servizio_id', $gruppo->id)->update(['gruppo_servizio_id' => null]); + DB::table('assegnazioni')->where('gruppo_servizio_id', $gruppo->id)->update(['gruppo_servizio_id' => null]); + DB::table('gruppi_servizio')->where('id', $gruppo->id)->delete(); + + continue; + } + + // Sorvegliante di un gruppo attivo: il nome utente dal nome del gruppo. + $utente = DB::table('users')->where('proclamatore_id', $gruppo->sorvegliante_id)->first(); + if ($utente && $utente->username === null) { + $base = Str::slug($gruppo->nome, '.') ?: 'gruppo'; + $base = Str::limit($base, 50, ''); + $username = $base; + for ($n = 2; DB::table('users')->where('username', $username)->exists(); $n++) { + $username = $base . $n; + } + DB::table('users')->where('id', $utente->id)->update(['username' => $username]); + } + } + + Schema::table('gruppi_servizio', function (Blueprint $table) { + $table->dropColumn('sciolto_at'); + }); + } + + public function down(): void + { + Schema::table('gruppi_servizio', function (Blueprint $table) { + $table->timestamp('sciolto_at')->nullable(); + }); + + Schema::table('assegnazioni', function (Blueprint $table) { + $table->dropColumn('gruppo_nome'); + }); + + // Senza email l'utenza non si può tenere com'era: chi non ce l'ha + // riceve un indirizzo segnaposto, da correggere a mano. + DB::table('users')->whereNull('email')->orderBy('id')->each(function ($u) { + DB::table('users')->where('id', $u->id)->update(['email' => ($u->username ?? "utente-{$u->id}") . '@senza-email.invalid']); + }); + Schema::table('users', function (Blueprint $table) { + $table->dropUnique(['username']); + $table->dropColumn('username'); + }); + Schema::table('users', function (Blueprint $table) { + $table->string('email')->nullable(false)->change(); + }); + } +}; diff --git a/resources/views/auth/password-change.blade.php b/resources/views/auth/password-change.blade.php index c1cc438..8e5703d 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->email }} + Account: {{ $user->identificativo }}

@@ -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 4cd311d..dd1c83a 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()->email }} · {{ ucfirst(auth()->user()->roles->first()?->name ?? 'utente') }} + {{ auth()->user()->identificativo }} · {{ 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 3b2c852..d2364de 100644 --- a/resources/views/livewire/auth/login.blade.php +++ b/resources/views/livewire/auth/login.blade.php @@ -16,8 +16,10 @@
@csrf
- - Email o nome utente + @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 671a759..16abd8e 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') }}
-
Email
{{ $creato->sorvegliante?->utente?->email }}
+
Nome utente
{{ $creato->sorvegliante?->utente?->username }}
Password
{{ $passwordTemporanea }}
@@ -29,6 +29,7 @@ +

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

@error('nome')

{{ $message }}

@enderror
@@ -37,13 +38,6 @@

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

@error('sorveglianteId')

{{ $message }}

@enderror
-
- - -

Resta solo nell'utenza del sorvegliante (non nel proclamatore) e non va confermata.

- @error('email')

{{ $message }}

@enderror -
Crea il gruppo
@@ -55,15 +49,12 @@
  • {{ $g->nome }} - @if($g->sciolto_at) - sciolto il {{ $g->sciolto_at->format('d/m/Y') }} - @endif

    Sorvegliante: {{ $g->sorvegliante?->nome_completo ?? '—' }} - @unless($g->sciolto_at) · {{ $g->membri_count }} membri · {{ $g->territori_count }} territori nel pacchetto @endunless + · {{ $g->membri_count }} membri · {{ $g->territori_count }} territori nel pacchetto

    - {{ $g->sciolto_at ? 'Dettaglio' : 'Gestisci' }} + Gestisci
  • @empty
  • Nessun gruppo di servizio.
  • diff --git a/resources/views/livewire/gruppi/gruppo-show.blade.php b/resources/views/livewire/gruppi/gruppo-show.blade.php index 245b6ec..74ef3ab 100644 --- a/resources/views/livewire/gruppi/gruppo-show.blade.php +++ b/resources/views/livewire/gruppi/gruppo-show.blade.php @@ -7,15 +7,10 @@

    Gruppo {{ $gruppo->nome }}

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

    - @unless($attivo) -

    - Sciolto il {{ $gruppo->sciolto_at->format('d/m/Y') }} · {{ $assegnazioniDelGruppo }} assegnazioni nate dal gruppo restano nello storico -

    - @endunless @if($passwordTemporanea) @@ -25,7 +20,6 @@ @endif - @if($attivo) {{-- ─── Pacchetto ─────────────────────────────────────────── --}}

    @@ -127,8 +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 disattivata. Le assegnazioni già fatte restano ai membri.">Togli il ruolo e sciogli il gruppo

    - @endif diff --git a/resources/views/livewire/privacy.blade.php b/resources/views/livewire/privacy.blade.php index 6bad57b..137ab77 100644 --- a/resources/views/livewire/privacy.blade.php +++ b/resources/views/livewire/privacy.blade.php @@ -18,8 +18,8 @@
    • 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 e stato del suo invio (inviato o no, e con quale canale).
    • -
    • Utenti dell'applicazione: nome, email (che va confermata, tranne per i sorveglianti di gruppo), password conservata solo come impronta non reversibile, ruolo. L'utenza di un sorvegliante è collegata al suo proclamatore e viene disattivata quando il gruppo si scioglie.
    • +
    • 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.
    • 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 (email 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 (nome utente 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 a nome ed email 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, insieme al nome dell'utenza se è stato sorvegliante di un gruppo.
    • 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-show.blade.php b/resources/views/livewire/proclamatori/proclamatore-show.blade.php index ee9f1bb..6cdf9c6 100644 --- a/resources/views/livewire/proclamatori/proclamatore-show.blade.php +++ b/resources/views/livewire/proclamatori/proclamatore-show.blade.php @@ -40,7 +40,7 @@ {{-- Gruppo di servizio --}} - @if($gruppoSorvegliato || $gruppoMembro || $gruppiPassati->isNotEmpty()) + @if($gruppoSorvegliato || $gruppoMembro)

      Gruppo di servizio

      @@ -78,7 +78,7 @@ @if($utenza)

      Utenza - @can('settings.manage') {{ $utenza->email }} @endcan + @can('settings.manage') {{ $utenza->identificativo }} @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' }}

      @@ -94,15 +94,6 @@ sorvegliante {{ $gruppoMembro->sorvegliante?->nome_completo ?? '—' }}.

      @endif - - @if($gruppiPassati->isNotEmpty()) -

      - In passato sorvegliante di: - @foreach($gruppiPassati as $g) - {{ $g->nome }} (sciolto il {{ $g->sciolto_at->format('d/m/Y') }}){{ $loop->last ? '.' : ',' }} - @endforeach -

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

      {{ $user->name }}

      - {{ $user->email }} + {{ $user->identificativo }} {{ ucfirst($user->roles->first()?->name ?? 'utente') }} @@ -59,7 +59,7 @@ {{-- Dati profilo --}}

      Dati profilo

      -

      Aggiorna nome e cognome ed email.

      +

      {{ $user->isSorvegliante() ? 'Aggiorna nome e cognome.' : 'Aggiorna nome e cognome ed email.' }}

      @if(session('profile_success'))
      @@ -73,11 +73,20 @@ @error('name')

      {{ $message }}

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

      Nome utente

      +

      {{ $user->username }}

      +

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

      +
      + @else
      @error('email')

      {{ $message }}

      @enderror
      + @endif
      Salva modifiche
      diff --git a/resources/views/livewire/registro.blade.php b/resources/views/livewire/registro.blade.php index 05380b1..6ac9ccb 100644 --- a/resources/views/livewire/registro.blade.php +++ b/resources/views/livewire/registro.blade.php @@ -87,7 +87,7 @@
      - {{ $a->proclamatore?->nome_completo ?? 'N/A' }}@if($a->gruppo)via gruppo {{ $a->gruppo->nome }}@endif + {{ $a->proclamatore?->nome_completo ?? 'N/A' }}@if($a->gruppo_nome)via gruppo {{ $a->gruppo_nome }}@endif {{ $a->assigned_at->format('d/m/Y') }} @if($a->returned_at) diff --git a/resources/views/livewire/settings/users-index.blade.php b/resources/views/livewire/settings/users-index.blade.php index e912815..772d940 100644 --- a/resources/views/livewire/settings/users-index.blade.php +++ b/resources/views/livewire/settings/users-index.blade.php @@ -111,8 +111,14 @@ @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 }} - @if($user->hasVerifiedEmail()) + @endif + @if(! $user->email) + @elseif($user->hasVerifiedEmail()) confermata @else @foreach($assegnazioni as $assegnazione) - {{ $assegnazione->proclamatore?->nome_completo ?? 'N/A' }}@if($assegnazione->gruppo)via gruppo {{ $assegnazione->gruppo->nome }}@endif + {{ $assegnazione->proclamatore?->nome_completo ?? 'N/A' }}@if($assegnazione->gruppo_nome)via gruppo {{ $assegnazione->gruppo_nome }}@endif {{ $assegnazione->assigned_at->format('d/m/Y') }} @if($assegnazione->returned_at) diff --git a/tests/Feature/GruppiServizioTest.php b/tests/Feature/GruppiServizioTest.php index 34f4afe..2de76d7 100644 --- a/tests/Feature/GruppiServizioTest.php +++ b/tests/Feature/GruppiServizioTest.php @@ -68,12 +68,12 @@ class GruppiServizioTest extends TestCase $rossi = $this->proclamatore('Rossi', 'Mario'); $verdi = $this->proclamatore('Verdi', 'Anna'); $bianchi = $this->proclamatore('Bianchi', 'Luca'); - [$gruppo, $password] = app(GruppiServizio::class)->crea('Zisa', $rossi, 'mario@example.test', [$verdi->id, $bianchi->id]); + [$gruppo, $password] = app(GruppiServizio::class)->crea('Zisa', $rossi, [$verdi->id, $bianchi->id]); $territori = collect(['12', '14', '35'])->map(fn ($n) => $this->territorio($n)); app(GruppiServizio::class)->aggiungiAlPacchetto($gruppo, $territori->pluck('id')->all()); $territori = $territori->map->fresh(); - $sorvegliante = User::where('email', 'mario@example.test')->sole(); + $sorvegliante = User::where('username', 'zisa')->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,16 +87,20 @@ class GruppiServizioTest extends TestCase { $rossi = $this->proclamatore('Rossi', 'Mario'); + // Niente email: il nome utente viene dal nome del gruppo. $pagina = Livewire::test(GruppiIndex::class) + ->assertDontSee('Email per l') ->set('nome', 'Zisa') ->set('sorveglianteId', $rossi->id) - ->set('email', 'mario@example.test') ->call('crea') - ->assertHasNoErrors(); + ->assertHasNoErrors() + ->assertSee('Nome utente') + ->assertSee('zisa'); $gruppo = GruppoServizio::sole(); - $utente = User::where('email', 'mario@example.test')->sole(); + $utente = User::where('username', 'zisa')->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); @@ -114,10 +118,27 @@ 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 + { + $servizio = app(GruppiServizio::class); + + $servizio->crea('Gruppo Noce Città', $this->proclamatore('Neri')); + $this->assertSame('gruppo.noce.citta', User::whereNotNull('username')->sole()->username); + + // 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()); + + // Due gruppi con lo stesso nome no. + $this->expectException(InvalidArgumentException::class); + $servizio->crea('Zisa', $this->proclamatore('Blu')); + } + public function test_un_proclamatore_sta_in_un_solo_gruppo(): void { ['gruppo' => $zisa, 'verdi' => $verdi] = $this->gruppo(); - [$noce] = app(GruppiServizio::class)->crea('Noce', $this->proclamatore('Neri'), 'neri@example.test'); + [$noce] = app(GruppiServizio::class)->crea('Noce', $this->proclamatore('Neri')); $this->expectException(InvalidArgumentException::class); app(GruppiServizio::class)->aggiungiMembri($noce, [$verdi->id]); @@ -199,16 +220,25 @@ class GruppiServizioTest extends TestCase $this->get(route('dashboard'))->assertOk()->assertDontSee('Il mio gruppo')->assertSee('Gruppi di servizio'); } - public function test_il_sorvegliante_entra_senza_conferma_dell_email_e_va_al_suo_gruppo(): void + public function test_il_sorvegliante_entra_con_il_nome_utente_e_va_al_suo_gruppo(): void { $this->gruppo(); $this->flushSession(); auth()->logout(); $password = 'una-password-nuova-1'; - User::where('email', 'mario@example.test')->sole()->forceFill(['password' => $password])->save(); + User::where('username', 'zisa')->sole()->forceFill(['password' => $password])->save(); - $this->post(route('login.store'), ['email' => 'mario@example.test', 'password' => $password]) + $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]) ->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 ────────────────────────────── @@ -322,19 +352,29 @@ class GruppiServizioTest extends TestCase ['gruppo' => $gruppo, 'sorvegliante' => $s, 'verdi' => $verdi, 'territori' => $territori] = $this->gruppo(); $data = app(GruppiServizio::class)->assegna($gruppo, $territori[0], $verdi, now()); - Livewire::test(GruppoShow::class, ['gruppo' => $gruppo])->call('sciogli'); + Livewire::test(GruppoShow::class, ['gruppo' => $gruppo]) + ->call('sciogli') + ->assertRedirect(route('gruppi.index')); - $gruppo->refresh(); - $this->assertNotNull($gruppo->sciolto_at); - // I non dati tornano in reparto; quello dato resta a Verdi, assegnazione normale. + // Il gruppo non resta nello storico: non esiste più. + $this->assertNull(GruppoServizio::find($gruppo->id)); + $this->get(route('gruppi.index'))->assertOk()->assertSee('Nessun gruppo di servizio'); + // I non dati tornano in reparto; quello dato resta a Verdi, assegnazione + // normale, che del gruppo conserva solo il nome. $this->assertNull($territori[1]->fresh()->gruppo_servizio_id); - $this->assertNull($territori[2]->fresh()->gruppo_servizio_id); + $this->assertSame('in_reparto', $territori[2]->fresh()->stato); $this->assertNull($data->fresh()->returned_at); - $this->assertSame($gruppo->id, $data->fresh()->gruppo_servizio_id, 'Lo storico continua a dirlo'); - // Membri liberi, utenza disattivata e senza ruolo. - $this->assertSame(0, Proclamatore::where('gruppo_servizio_id', $gruppo->id)->count()); + $this->assertSame('assegnato', $territori[0]->fresh()->stato); + $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. + $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()); // Per ripartire serve un gruppo nuovo. $this->expectException(InvalidArgumentException::class); @@ -350,7 +390,7 @@ class GruppiServizioTest extends TestCase $this->flushSession(); auth()->logout(); - $this->post(route('login.store'), ['email' => 'mario@example.test', 'password' => $password]) + $this->post(route('login.store'), ['email' => 'zisa', 'password' => $password]) ->assertSessionHasErrors('email'); $this->assertGuest(); @@ -365,13 +405,14 @@ class GruppiServizioTest extends TestCase $pagina = Livewire::test(GruppiIndex::class); $this->assertContains((string) $rossi->id, collect($pagina->viewData('opzioniSorveglianti'))->pluck('valore')->all(), 'Torna tra i candidati'); - [$nuovo, $password] = app(GruppiServizio::class)->crea('Zisa 2', $rossi->fresh(), 'mario.nuovo@example.test'); + // Sciolto, il nome non è più occupato: si può rifare "Zisa". + [$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()); $this->assertTrue($utente->hasRole('sorvegliante')); - $this->assertSame('mario.nuovo@example.test', $utente->email); + $this->assertSame('zisa', $utente->username); $this->assertTrue($utente->password_change_required); $this->assertTrue(\Illuminate\Support\Facades\Hash::check($password, $utente->password)); $this->assertSame($nuovo->id, $utente->gruppoSorvegliato()?->id); @@ -382,7 +423,8 @@ class GruppiServizioTest extends TestCase /** Il gruppo è sciolto come da "Togli il ruolo": tutto tranne l'assegnazione già fatta. */ private function assertSciolto(array $g, Assegnazione $data): void { - $this->assertNotNull($g['gruppo']->fresh()->sciolto_at, 'Gruppo sciolto'); + $this->assertNull(GruppoServizio::find($g['gruppo']->id), 'Gruppo sciolto ed eliminato'); + $this->assertSame('Zisa', $data->fresh()->gruppo_nome); $this->assertNull($g['territori'][1]->fresh()->gruppo_servizio_id, 'I non dati tornano in reparto'); $this->assertSame('in_reparto', $g['territori'][2]->fresh()->stato); $this->assertNull($data->fresh()->returned_at, 'Quello già dato resta al membro'); @@ -406,7 +448,7 @@ class GruppiServizioTest extends TestCase // Riattivarlo non ricostruisce il gruppo: se ne crea uno nuovo. Livewire::test(\App\Livewire\Proclamatori\ProclamatoreIndex::class)->call('toggleActive', $g['rossi']->id); - $this->assertNotNull($g['gruppo']->fresh()->sciolto_at); + $this->assertSame(0, GruppoServizio::count()); $this->assertNull($g['rossi']->fresh()->gruppoSorvegliato()); } @@ -445,7 +487,7 @@ class GruppiServizioTest extends TestCase ->call('toggleActive', $verdi->id) ->call('deleteProclamatore', $bianchi->id); - $this->assertNull($gruppo->fresh()->sciolto_at); + $this->assertNotNull(GruppoServizio::find($gruppo->id)); $this->assertSame($gruppo->id, $verdi->fresh()->gruppo_servizio_id, 'Non attivo resta membro, ma non riceve territori'); $this->assertNull($bianchi->fresh()->gruppo_servizio_id, 'Nel cestino lascia il gruppo'); } @@ -459,17 +501,18 @@ 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('mario@example.test') + ->assertSee('zisa', false) ->assertSee('non è ancora entrato'); $this->get(route('proclamatori.show', $g['verdi']))->assertOk() ->assertSee('Membro del gruppo') ->assertSee('sorvegliante Rossi Mario'); + // Sciolto il gruppo, nella scheda non ne resta traccia. app(GruppiServizio::class)->sciogli($g['gruppo']); $this->get(route('proclamatori.show', $g['rossi']))->assertOk() - ->assertSee('In passato sorvegliante di:') - ->assertDontSee('Assegnazioni dal gruppo'); + ->assertDontSee('Assegnazioni dal gruppo') + ->assertDontSee('In passato'); } public function test_chi_fa_parte_di_un_gruppo_e_segnalato_in_elenco_scheda_e_scelta(): void @@ -529,7 +572,8 @@ class GruppiServizioTest extends TestCase $utente = $s->fresh(); $this->assertSame('Anonimo', $utente->name); - $this->assertStringEndsWith('@anonimo.invalid', $utente->email); + $this->assertNull($utente->email); + $this->assertSame("anonimo.{$utente->id}", $utente->username); $this->assertTrue($utente->isDisattivato()); $this->assertCount(0, $utente->roles); } @@ -579,6 +623,25 @@ class GruppiServizioTest extends TestCase $this->assertEqualsCanonicalizing(['Rossi Mario', 'Verdi Anna', 'Bianchi Luca'], $g->membri->map->nome_completo->all()); $this->assertEqualsCanonicalizing(['12', '14', '35'], $g->territori->pluck('numero')->all()); $this->assertSame($g->id, Assegnazione::sole()->gruppo_servizio_id); - $this->assertSame($g->sorvegliante_id, User::where('email', 'mario@example.test')->sole()->proclamatore_id); + $this->assertSame('Zisa', Assegnazione::sole()->gruppo_nome); + $utenza = User::where('username', 'zisa')->sole(); + $this->assertNull($utenza->email); + $this->assertSame($g->sorvegliante_id, $utenza->proclamatore_id); + } + + public function test_l_xml_conserva_il_nome_del_gruppo_sulle_assegnazioni_dopo_lo_scioglimento(): void + { + ['gruppo' => $gruppo, 'verdi' => $verdi, 'territori' => $territori] = $this->gruppo(); + app(GruppiServizio::class)->assegna($gruppo, $territori[0], $verdi, now()); + app(GruppiServizio::class)->sciogli($gruppo); + + $xml = (new XmlExchange())->currentXmlBackup(); + $this->assertArrayNotHasKey('error', (new XmlExchange())->importXmlFromContent($xml)); + + $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()); } }