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

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

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

View File

@@ -5,7 +5,7 @@
{{ $forced ? 'Imposta una nuova password' : 'Cambia password' }}
</h1>
<p class="mt-1 text-sm text-gray-500">
Account: <span class="font-medium text-gray-700 break-all">{{ $user->identificativo }}</span>
Account: <span class="font-medium text-gray-700 break-all">{{ $user->email }}</span>
</p>
</div>
@@ -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. --}}
<input type="hidden" name="email" autocomplete="username" value="{{ $user->identificativo }}">
<input type="hidden" name="email" autocomplete="username" value="{{ $user->email }}">
@unless($forced)
{{-- Dopo un ripristino non si chiede: la password attuale e'

View File

@@ -50,7 +50,7 @@
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true" style="width:20px;height:20px;opacity:.75"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
<span class="flex flex-col leading-tight text-left">
<span class="text-sm font-medium">{{ auth()->user()->name }}</span>
<span class="text-xs" style="opacity:.7">{{ auth()->user()->identificativo }} · {{ ucfirst(auth()->user()->roles->first()?->name ?? 'utente') }}</span>
<span class="text-xs" style="opacity:.7">{{ auth()->user()->email }} · {{ ucfirst(auth()->user()->roles->first()?->name ?? 'utente') }}</span>
</span>
</x-btn>
</div>

View File

@@ -16,10 +16,8 @@
<form method="POST" action="{{ route('login.store') }}" class="space-y-5">
@csrf
<div>
{{-- Email, o nome utente per il sorvegliante di un gruppo di servizio --}}
<label for="email" class="block text-sm font-medium text-gray-700">Email o nome utente</label>
<input name="email" value="{{ old('email') }}" type="text" id="email" autocomplete="username"
autocapitalize="none" autocorrect="off" spellcheck="false"
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input name="email" value="{{ old('email') }}" type="email" id="email" autocomplete="email"
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm px-4 py-2.5">
@error('email') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>

View File

@@ -12,7 +12,7 @@
<p class="mt-1">Consegna queste credenziali a {{ $creato->sorvegliante?->nome_completo }}: la password si vede <strong>solo ora</strong> e al primo accesso va cambiata.</p>
<dl class="mt-3 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1">
<dt class="text-green-700">Indirizzo</dt><dd><code>{{ route('login') }}</code></dd>
<dt class="text-green-700">Nome utente</dt><dd><code class="select-all">{{ $creato->sorvegliante?->utente?->username }}</code></dd>
<dt class="text-green-700">Email</dt><dd><code class="select-all">{{ $creato->sorvegliante?->utente?->email }}</code></dd>
<dt class="text-green-700">Password</dt><dd><code class="select-all">{{ $passwordTemporanea }}</code></dd>
</dl>
<div class="mt-3 flex flex-wrap gap-2">
@@ -29,13 +29,12 @@
<label for="nome-gruppo" class="block text-sm font-medium text-gray-700 mb-1">Nome del gruppo *</label>
<input wire:model="nome" id="nome-gruppo" type="text" placeholder="es. Zisa"
class="block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
<p class="text-xs text-gray-500 mt-1">Dal nome nasce il nome utente del sorvegliante (es. «Gruppo Zisa» → <code>gruppo.zisa</code>).</p>
@error('nome') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label for="sorvegliante" class="block text-sm font-medium text-gray-700 mb-1">Sorvegliante *</label>
<x-scelta id="sorvegliante" wire:model="sorveglianteId" :opzioni="$opzioniSorveglianti" segnaposto="Cognome o nome" :vuoto="null" />
<p class="text-xs text-gray-500 mt-1">Un proclamatore attivo che non fa già parte di un gruppo. Diventa anche membro del gruppo.</p>
<p class="text-xs text-gray-500 mt-1">Un proclamatore attivo che non fa già parte di un gruppo. Diventa anche membro del gruppo, e riceve un'utenza con un'email fittizia: iniziale del nome e cognome (es. Francesco Fontana → <code>ffontana@{{ \App\Services\GruppiServizio::DOMINIO_EMAIL }}</code>).</p>
@error('sorveglianteId') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
<x-btn variante="primary" type="submit">Crea il gruppo</x-btn>

View File

@@ -8,7 +8,7 @@
<p class="text-sm text-gray-500 mt-1">
Sorvegliante: <strong>{{ $gruppo->sorvegliante?->nome_completo ?? '—' }}</strong>
@if($gruppo->sorvegliante?->utente)
· nome utente <code class="select-all">{{ $gruppo->sorvegliante->utente->identificativo }}</code>
· accede con <code class="select-all">{{ $gruppo->sorvegliante->utente->email }}</code>
@endif
</p>
</div>
@@ -121,7 +121,7 @@
<x-btn variante="secondary" wire:click="nuovaPassword"
wire:confirm="Generare una nuova password temporanea per {{ $gruppo->sorvegliante?->nome_completo }}? Quella attuale smette di funzionare.">Nuova password temporanea</x-btn>
<x-btn variante="danger" wire:click="sciogli"
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</x-btn>
wire:confirm="Togliere il ruolo di sorvegliante a {{ $gruppo->sorvegliante?->nome_completo }}? Il gruppo si scioglie e viene eliminato: i territori non ancora dati tornano in reparto, i membri restano senza gruppo e l'utenza del sorvegliante viene eliminata. Le assegnazioni già fatte restano ai membri.">Togli il ruolo e sciogli il gruppo</x-btn>
</div>
</section>
</div>

View File

@@ -19,7 +19,7 @@
<li><strong>Proclamatori</strong>: 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.</li>
<li><strong>Territori e piantine</strong>: numero, zona, tipologia, confini, mappe e PDF — nessun dato personale.</li>
<li><strong>Assegnazioni</strong>: 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.</li>
<li><strong>Utenti dell'applicazione</strong>: 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.</li>
<li><strong>Utenti dell'applicazione</strong>: nome, email (che va confermata), password conservata solo come impronta non reversibile, ruolo. Il sorvegliante di un gruppo di servizio entra con un'email fittizia creata dall'applicazione (iniziale del nome e cognome), che non riceve posta e non va confermata. La sua utenza è collegata al suo proclamatore e viene eliminata quando il gruppo si scioglie; anche il gruppo, sciolto, viene eliminato.</li>
<li><strong>Registro delle attività</strong>: chi ha fatto cosa e quando. I proclamatori vi compaiono solo con un identificativo numerico, mai con il nome.</li>
</ul>
</section>
@@ -58,7 +58,7 @@
<li><strong>{{ ucfirst($ruolo['nome']) }}</strong>: {{ $ruolo['permessi'] ? implode('; ', $ruolo['permessi']) : 'nessun permesso' }}.</li>
@endforeach
</ul>
<p class="mt-2">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.</p>
<p class="mt-2">Il proclamatore non ha un account: riceve solo il link alla piantina del territorio che gli è assegnato. Fa eccezione il sorvegliante di un gruppo di servizio, che ha un'utenza (email fittizia e password) con cui vede solo il proprio gruppo: i territori del suo pacchetto e i nomi dei membri.</p>
</section>
<section>
@@ -100,7 +100,7 @@
<section>
<h2 class="text-lg font-semibold text-gray-900 mb-2">Conservazione dei dati</h2>
<ul class="list-disc list-inside space-y-1">
<li>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.</li>
<li>I dati di un proclamatore possono essere anonimizzati su richiesta: nome e cognome vengono cancellati in modo irreversibile; se era sorvegliante di un gruppo, il gruppo si scioglie e la sua utenza viene eliminata.</li>
<li>Territori e proclamatori eliminati restano nel cestino finché non vengono eliminati definitivamente.</li>
<li>Il registro delle attività è conservato per <strong>{{ $auditRetention }} giorni</strong> (modificabile nelle impostazioni), poi cancellato automaticamente.</li>
<li>Lo storico delle assegnazioni è conservato per la durata necessaria alla gestione territoriale.</li>

View File

@@ -28,7 +28,7 @@
@if($gruppoSorvegliato ?? null)
<p class="-mt-2 text-xs text-amber-800">
È il sorvegliante del gruppo <strong>{{ $gruppoSorvegliato->nome }}</strong>: se non è più attivo il gruppo si scioglie,
i territori non ancora dati tornano in reparto e la sua utenza viene disattivata.
i territori non ancora dati tornano in reparto e la sua utenza viene eliminata.
</p>
@endif

View File

@@ -83,7 +83,7 @@
{{-- Sorvegliante: disattivarlo scioglie il gruppo, si chiede conferma --}}
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona
wire:click="toggleActive({{ $proclamatore->id }})"
wire:confirm="{{ $proclamatore->nome_completo }} è il sorvegliante del gruppo {{ $gruppiSorvegliati[$proclamatore->id] }}. Se non è più attivo il gruppo si scioglie: i territori non ancora dati tornano in reparto, i membri restano senza gruppo e la sua utenza viene disattivata. Procedere?"
wire:confirm="{{ $proclamatore->nome_completo }} è il sorvegliante del gruppo {{ $gruppiSorvegliati[$proclamatore->id] }}. Se non è più attivo il gruppo si scioglie: i territori non ancora dati tornano in reparto, i membri restano senza gruppo e la sua utenza viene eliminata. Procedere?"
title="Disattiva" aria-label="Disattiva il proclamatore">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/></svg>
</x-btn>
@@ -101,7 +101,7 @@
@endif
<x-btn variante="danger" tono="tenue" dimensione="sm" icona
wire:click="deleteProclamatore({{ $proclamatore->id }})"
wire:confirm="{{ isset($gruppiSorvegliati[$proclamatore->id]) ? 'Spostare nel cestino ' . $proclamatore->nome_completo . '? È il sorvegliante del gruppo ' . $gruppiSorvegliati[$proclamatore->id] . ': il gruppo si scioglie, i territori non ancora dati tornano in reparto e la sua utenza viene disattivata.' : 'Spostare il proclamatore nel cestino?' }}"
wire:confirm="{{ isset($gruppiSorvegliati[$proclamatore->id]) ? 'Spostare nel cestino ' . $proclamatore->nome_completo . '? È il sorvegliante del gruppo ' . $gruppiSorvegliati[$proclamatore->id] . ': il gruppo si scioglie, i territori non ancora dati tornano in reparto e la sua utenza viene eliminata.' : 'Spostare il proclamatore nel cestino?' }}"
title="Elimina" aria-label="Sposta nel cestino il proclamatore">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</x-btn>

View File

@@ -78,7 +78,7 @@
@if($utenza)
<p class="mt-4 border-t border-gray-100 pt-3 text-sm text-gray-600">
Utenza
@can('settings.manage') <code>{{ $utenza->identificativo }}</code> @endcan
@can('settings.manage') <code>{{ $utenza->email }}</code> @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' }}
</p>

View File

@@ -17,7 +17,7 @@
<div class="min-w-0">
<h2 class="text-xl font-bold text-gray-900 truncate">{{ $user->name }}</h2>
<p class="text-sm text-gray-500 truncate mt-0.5">
<span class="break-all">{{ $user->identificativo }}</span>
<span class="break-all">{{ $user->email }}</span>
<span class="ml-1 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-50 text-indigo-700 align-middle">
{{ ucfirst($user->roles->first()?->name ?? 'utente') }}
</span>
@@ -74,11 +74,11 @@
@error('name') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
@if($user->isSorvegliante())
{{-- Il sorvegliante entra con il nome utente del gruppo, senza email --}}
{{-- Il sorvegliante entra con l'email fittizia creata col gruppo --}}
<div>
<p class="block text-sm font-medium text-gray-700">Nome utente</p>
<p class="mt-1 text-sm"><code class="select-all">{{ $user->username }}</code></p>
<p class="mt-1 text-xs text-gray-500">Viene dal nome del gruppo: lo usi per entrare, insieme alla password.</p>
<p class="block text-sm font-medium text-gray-700">Email per entrare</p>
<p class="mt-1 text-sm"><code class="select-all">{{ $user->email }}</code></p>
<p class="mt-1 text-xs text-gray-500">L'ha creata l'applicazione insieme al gruppo: la usi per entrare, insieme alla password. Non riceve posta.</p>
</div>
@else
<div>

View File

@@ -111,14 +111,8 @@
@else
<td data-label="Nome" class="px-4 py-2 text-gray-900">{{ $user->name }}</td>
<td data-label="Email" class="px-4 py-2 text-gray-700">
@if(! $user->email)
{{-- Sorvegliante di gruppo: entra con il nome utente, senza email --}}
@if($user->username)<span class="text-gray-500">nome utente</span> <code>{{ $user->username }}</code>@else<span class="text-gray-400">—</span>@endif
@else
{{ $user->email }}
@endif
@if(! $user->email)
@elseif($user->hasVerifiedEmail())
@if($user->hasVerifiedEmail())
<span class="ml-1 inline-flex items-center rounded-full bg-green-50 px-2 py-0.5 text-xs font-medium text-green-700">confermata</span>
@else
<span class="ml-1 inline-flex items-center rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-800"
@@ -138,7 +132,7 @@
@if($user->proclamatore_id)
{{-- Utenza di un sorvegliante di gruppo: si gestisce dal gruppo. --}}
<span class="text-xs text-gray-500">
{{ $user->disattivato_at ? 'Disattivata (gruppo sciolto)' : 'Sorvegliante: si gestisce da Gruppi di servizio' }}
Sorvegliante: si gestisce da Gruppi di servizio
</span>
@else
<div class="flex gap-2">