++ 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:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'] ?? ''));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user