- /richiesta: modulo pubblico (nuovo territorio o riconsegna) con campo trappola, tempo minimo e limite orario; nome e note cifrati, avviso per email a chi ha richieste.gestisci (amministratore, assistente) - Home: riquadro Richieste con i proclamatori dal nome simile; porta su Assegna/Rientra già esistenti, che chiudono la richiesta; Scarta - Cartaceo sull'assegnazione (Assegna e Il mio gruppo), etichetta nell'elenco degli assegnati, pagina Cartacei, passa per l'XML - registro:invia-s13 agli amministratori l'ultima domenica del mese; richieste:pulizia dopo 90 giorni dalla gestione - Privacy, README e HANDOFF aggiornati; test in RichiesteTest Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
348 lines
14 KiB
PHP
348 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\AnnoTeocratico;
|
|
use App\Models\Assegnazione;
|
|
use App\Models\GruppoServizio;
|
|
use App\Models\Proclamatore;
|
|
use App\Models\Territorio;
|
|
use App\Models\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use InvalidArgumentException;
|
|
|
|
/**
|
|
* Le regole dei gruppi di servizio, in un posto solo: le usano le pagine
|
|
* dell'amministratore (Gruppi di servizio) e quella del sorvegliante (Il mio
|
|
* gruppo). Ogni metodo lancia InvalidArgumentException con un messaggio da
|
|
* mostrare così com'è quando una regola non è rispettata.
|
|
*/
|
|
class GruppiServizio
|
|
{
|
|
/** Il dominio delle email fittizie dei sorveglianti (iniziale+cognome@…). */
|
|
public const DOMINIO_EMAIL = 'termanager.it';
|
|
|
|
/**
|
|
* Crea il gruppo e l'utenza del sorvegliante: email fittizia ricavata dal
|
|
* suo nome (emailFittizia), 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 insieme all'email.
|
|
*
|
|
* @param int[] $membri id dei proclamatori, oltre al sorvegliante
|
|
* @return array{0: GruppoServizio, 1: string}
|
|
*/
|
|
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.');
|
|
}
|
|
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];
|
|
}
|
|
|
|
/**
|
|
* Una password temporanea nuova per il sorvegliante che l'ha persa: da
|
|
* cambiare al primo accesso ("Password temporanea"). Le sessioni aperte
|
|
* con quella vecchia si chiudono alla richiesta successiva
|
|
* (AuthenticateSession: è cambiato l'hash), il "ricordami" si azzera.
|
|
* Restituisce la password, da mostrare una volta sola.
|
|
*/
|
|
public function nuovaPasswordTemporanea(User $utente): string
|
|
{
|
|
if (! $utente->proclamatore_id || ! $utente->isSorvegliante()) {
|
|
throw new InvalidArgumentException("{$utente->name} non è il sorvegliante di un gruppo.");
|
|
}
|
|
|
|
$password = Str::password(12, symbols: false);
|
|
$utente->forceFill([
|
|
'password' => $password,
|
|
'password_change_required' => true,
|
|
'password_temporanea' => true,
|
|
'remember_token' => null,
|
|
])->save();
|
|
|
|
activity()->causedBy(auth()->user())->performedOn($utente)->log('password_reimpostata');
|
|
|
|
return $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, bool $cartaceo = false): 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, $cartaceo) {
|
|
$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,
|
|
'cartaceo' => $cartaceo,
|
|
]);
|
|
});
|
|
|
|
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.');
|
|
}
|
|
}
|
|
}
|