++ 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.<id>). 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 <noreply@anthropic.com>
This commit is contained in:
Francesco Picone
2026-10-20 11:45:00 +02:00
parent a93539242a
commit aaffeb7de7
29 changed files with 408 additions and 199 deletions

View File

@@ -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
// (<sciolto_at>): 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'] ?? ''));