exists()) { throw new InvalidArgumentException("Esiste già un gruppo «{$nome}»."); } if (! $sorvegliante->attivo || $sorvegliante->trashed()) { throw new InvalidArgumentException('Il sorvegliante deve essere un proclamatore attivo.'); } if ($sorvegliante->gruppo_servizio_id) { throw new InvalidArgumentException("{$sorvegliante->nome_completo} fa già parte di un altro gruppo."); } if ($sorvegliante->utente()->exists()) { throw new InvalidArgumentException("{$sorvegliante->nome_completo} ha già un'utenza."); } $password = Str::password(12, symbols: false); $gruppo = DB::transaction(function () use ($nome, $sorvegliante, $membri, $password) { $gruppo = GruppoServizio::create(['nome' => $nome, 'sorvegliante_id' => $sorvegliante->id]); $sorvegliante->update(['gruppo_servizio_id' => $gruppo->id]); $this->aggiungiMembri($gruppo, $membri); $utente = User::create([ 'name' => $sorvegliante->nome_completo, 'email' => self::emailFittizia($sorvegliante), 'password' => $password, 'password_change_required' => true, 'password_temporanea' => true, 'proclamatore_id' => $sorvegliante->id, ]); $utente->assignRole('sorvegliante'); activity()->causedBy(auth()->user())->performedOn($gruppo) ->withProperties(['sorvegliante_id' => $sorvegliante->id, 'utente_id' => $utente->id]) ->log('gruppo_creato'); return $gruppo; }); return [$gruppo, $password]; } /** * L'email fittizia del sorvegliante, con cui entra: iniziale del nome e * cognome, minuscoli, senza accenti, spazi né apostrofi, sul dominio * dell'app (Francesco Fontana → ffontana@termanager.it, Francesco * D'Angelo → fdangelo@…). Se è già presa, un numero in coda * (ffontana2@…). Non riceve posta: il sorvegliante non conferma l'email. */ public static function emailFittizia(Proclamatore $proclamatore): string { $pulisci = fn (string $testo) => preg_replace('/[^a-z0-9]/', '', Str::lower(Str::ascii($testo))); $base = Str::limit(mb_substr($pulisci((string) $proclamatore->nome), 0, 1) . $pulisci((string) $proclamatore->cognome), 60, ''); if ($base === '') { $base = "sorvegliante{$proclamatore->id}"; } $email = "{$base}@" . self::DOMINIO_EMAIL; for ($n = 2; User::where('email', $email)->exists(); $n++) { $email = "{$base}{$n}@" . self::DOMINIO_EMAIL; } return $email; } /** @param int[] $ids */ public function aggiungiMembri(GruppoServizio $gruppo, array $ids): void { $this->attivo($gruppo); foreach (Proclamatore::whereIn('id', $ids)->get() as $p) { if ($p->gruppo_servizio_id && $p->gruppo_servizio_id !== $gruppo->id) { throw new InvalidArgumentException("{$p->nome_completo} fa già parte di un altro gruppo."); } if (! $p->attivo) { throw new InvalidArgumentException("{$p->nome_completo} non è attivo."); } $p->update(['gruppo_servizio_id' => $gruppo->id]); } } public function togliMembro(GruppoServizio $gruppo, Proclamatore $membro): void { $this->attivo($gruppo); if ($membro->id === $gruppo->sorvegliante_id) { throw new InvalidArgumentException('Il sorvegliante non si toglie: per cambiarlo si scioglie il gruppo.'); } if ($membro->gruppo_servizio_id === $gruppo->id) { $membro->update(['gruppo_servizio_id' => null]); } } /** * Mette nel pacchetto del gruppo dei territori in reparto (attivi, non * assegnati, non già in un altro pacchetto). Restituisce quanti. * * @param int[] $ids */ public function aggiungiAlPacchetto(GruppoServizio $gruppo, array $ids): int { $this->attivo($gruppo); $territori = Territorio::whereIn('id', $ids)->get(); foreach ($territori as $t) { if (! $t->attivo) { throw new InvalidArgumentException("Il territorio {$t->numero} non è attivo."); } if ($t->gruppo_servizio_id && $t->gruppo_servizio_id !== $gruppo->id) { throw new InvalidArgumentException("Il territorio {$t->numero} è già nel pacchetto di un altro gruppo."); } if ($t->assegnazioneCorrente) { throw new InvalidArgumentException("Il territorio {$t->numero} è assegnato: va prima fatto rientrare."); } } Territorio::whereKey($territori->pluck('id'))->update(['gruppo_servizio_id' => $gruppo->id]); activity()->causedBy(auth()->user())->performedOn($gruppo) ->withProperties(['territori' => $territori->pluck('numero')->all()]) ->log('pacchetto_aggiunti'); return $territori->count(); } /** * Rimanda in reparto un territorio del pacchetto non ancora dato a * nessuno (lo fa l'amministratore o il sorvegliante). */ public function rimandaInReparto(GruppoServizio $gruppo, Territorio $territorio): void { if ($territorio->gruppo_servizio_id !== $gruppo->id) { throw new InvalidArgumentException("Il territorio {$territorio->numero} non è nel pacchetto del gruppo."); } if ($territorio->assegnazioneCorrente) { throw new InvalidArgumentException("Il territorio {$territorio->numero} è assegnato: prima va registrato il rientro."); } $territorio->update(['gruppo_servizio_id' => null]); activity()->causedBy(auth()->user())->performedOn($gruppo) ->withProperties(['territorio' => $territorio->numero]) ->log('pacchetto_rimandato_in_reparto'); } /** * Il sorvegliante dà un territorio del pacchetto a un membro (o a sé): * nasce un'assegnazione normale, segnata come proveniente dal gruppo. Il * territorio resta nel pacchetto finché non rientra. */ public function assegna(GruppoServizio $gruppo, Territorio $territorio, Proclamatore $membro, Carbon $data): Assegnazione { $this->attivo($gruppo); // Si decide sui dati di adesso, non su quelli con cui è partita la // pagina: nel frattempo l'amministratore può aver cambiato pacchetto // o membri. $territorio->refresh(); $membro->refresh(); if ($territorio->gruppo_servizio_id !== $gruppo->id) { throw new InvalidArgumentException("Il territorio {$territorio->numero} non è nel pacchetto del gruppo."); } if ($membro->gruppo_servizio_id !== $gruppo->id) { throw new InvalidArgumentException("{$membro->nome_completo} non fa parte del gruppo."); } if (! $membro->attivo) { throw new InvalidArgumentException("{$membro->nome_completo} non è attivo."); } if ($data->isFuture()) { throw new InvalidArgumentException('La data di assegnazione non può essere nel futuro.'); } $esito = DB::transaction(function () use ($gruppo, $territorio, $membro, $data) { $bloccato = Territorio::whereKey($territorio->id)->lockForUpdate()->first(); if (! $bloccato || $bloccato->gruppo_servizio_id !== $gruppo->id) { return "Il territorio {$territorio->numero} non è più nel pacchetto del gruppo."; } $conflitto = Assegnazione::sovrappostaA($territorio->id, $data); if ($conflitto) { return "Il territorio {$territorio->numero} risulta assegnato {$conflitto->descrizionePeriodo()}."; } return Assegnazione::create([ 'territorio_id' => $territorio->id, 'proclamatore_id' => $membro->id, 'anno_teocratico_id' => AnnoTeocratico::perData($data)->id, '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, ]); }); if (is_string($esito)) { throw new InvalidArgumentException($esito); } activity()->causedBy(auth()->user())->performedOn($esito) ->withProperties([ 'territorio' => $territorio->numero, 'proclamatore_id' => $membro->id, 'gruppo_servizio_id' => $gruppo->id, ]) ->log('assigned'); return $esito; } /** * 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 elimina: torna un proclamatore come gli * altri. Le assegnazioni che aveva fatto o fatto rientrare passano a chi * scioglie il gruppo; nel registro le sue voci restano, a nome * "Sorvegliante del gruppo …", senza dati personali. */ public function sciogli(GruppoServizio $gruppo, ?string $motivo = null): void { if (! GruppoServizio::whereKey($gruppo->id)->exists()) { return; } DB::transaction(function () use ($gruppo, $motivo) { $liberati = Territorio::withTrashed() ->where('gruppo_servizio_id', $gruppo->id) ->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(); activity()->causedBy(auth()->user())->performedOn($gruppo) ->withProperties(array_filter([ 'gruppo' => $gruppo->nome, 'territori_liberati' => $liberati->all(), 'utente_eliminato' => $utente?->id, 'motivo' => $motivo, ], fn ($v) => $v !== null)) ->log('gruppo_sciolto'); if ($utente) { $utente->eliminaConservandoStorico($this->subentrante($utente), "Sorvegliante del gruppo {$gruppo->nome}", null); } $gruppo->delete(); }); } /** * Chi prende le assegnazioni fatte dal sorvegliante quando la sua utenza * si elimina: chi sta sciogliendo il gruppo, altrimenti il primo * amministratore. */ private function subentrante(User $sorvegliante): User { $chi = auth()->user(); if ($chi instanceof User && $chi->id !== $sorvegliante->id) { return $chi; } return User::role('amministratore')->orderBy('id')->firstOrFail(); } private function attivo(GruppoServizio $gruppo): void { if (! $gruppo->exists || ! GruppoServizio::whereKey($gruppo->id)->exists()) { throw new InvalidArgumentException('Il gruppo non esiste più: è stato sciolto.'); } } }