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' }}
@@ -127,8 +121,7 @@
Nuova password temporaneaTogli 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
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.
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' }}