++ 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:
46
HANDOFF.md
46
HANDOFF.md
@@ -148,19 +148,21 @@ e non vede nient'altro. Scelte dell'utente:
|
||||
- Il sorvegliante: assegna, manda il link (copia/condividi), registra i
|
||||
rientri, apre la piantina, rimanda in reparto i non dati. Gruppi, membri e
|
||||
pacchetti li gestisce **solo l'amministratore**.
|
||||
- Utenza: email e password temporanea da cambiare al primo accesso, **senza
|
||||
- Utenza: password temporanea da cambiare al primo accesso, **senza
|
||||
conferma dell'email** (`User::hasVerifiedEmail()` è vero per il
|
||||
sorvegliante; anche cambiandola dal profilo).
|
||||
sorvegliante). *Dal 20 ottobre: niente email, nome utente dal nome del
|
||||
gruppo — vedi sotto.*
|
||||
- **Togliere il ruolo = sciogliere il gruppo** (`GruppiServizio::sciogli`):
|
||||
non dati in reparto, membri liberi, utenza senza ruolo e disattivata
|
||||
(`users.disattivato_at`: login rifiutato, sessione aperta chiusa da
|
||||
`LimitaSorvegliante`), gruppo archiviato (`sciolto_at`) per lo storico. I
|
||||
territori già dati restano ai membri. Si riparte con un gruppo nuovo.
|
||||
`LimitaSorvegliante`). I territori già dati restano ai membri. Si riparte
|
||||
con un gruppo nuovo. *Dal 20 ottobre il gruppo sciolto si elimina — vedi
|
||||
sotto.*
|
||||
|
||||
Casi aggiunti di mio, da far confermare all'utente:
|
||||
- Un ex sorvegliante può guidare un gruppo nuovo: `crea()` **riattiva** la
|
||||
sua utenza disattivata (un proclamatore, un'utenza: `users.proclamatore_id`
|
||||
è unico), con email e password temporanea nuove.
|
||||
è unico), con nome utente e password temporanea nuovi.
|
||||
- `Proclamatore::anonimizza()` anonimizza anche l'utenza dell'ex sorvegliante
|
||||
(nome, email, password, ruoli).
|
||||
|
||||
@@ -177,10 +179,33 @@ Decisioni successive dell'utente (20 ottobre 2026):
|
||||
territori); nel cestino esce dal gruppo.
|
||||
- **Dettagli nel profilo**: la scheda del proclamatore (`ProclamatoreShow`)
|
||||
mostra il gruppo che guida con i numeri (`GruppoServizio::riepilogo()`),
|
||||
l'utenza (email solo all'amministratore, password temporanea o no, ultimo
|
||||
accesso dal registro), oppure il gruppo di cui è membro, e i gruppi sciolti
|
||||
che ha guidato. Il *Profilo* del sorvegliante mostra il suo gruppo con gli
|
||||
stessi numeri e "Torna al mio gruppo".
|
||||
l'utenza (nome utente solo all'amministratore, password temporanea o no,
|
||||
ultimo accesso dal registro), oppure il gruppo di cui è membro. Il
|
||||
*Profilo* del sorvegliante mostra il suo gruppo con gli stessi numeri e
|
||||
"Torna al mio gruppo".
|
||||
- **Il gruppo sciolto non resta nello storico**: `sciogli()` lo elimina
|
||||
(colonna `sciolto_at` tolta dalla migrazione `2026_10_20_000020`, che
|
||||
elimina anche i gruppi già sciolti). I territori solo nel pacchetto tornano
|
||||
in reparto (conferma esplicita dell'utente); quelli già dati restano ai
|
||||
membri come assegnazioni qualsiasi. Sulla traccia delle assegnazioni
|
||||
l'utente non si è espresso: ho tenuto la scelta che non perde dati, il
|
||||
nome del gruppo copiato sull'assegnazione (`assegnazioni.gruppo_nome`,
|
||||
"via gruppo …" in registro e scheda territorio). Toglierlo è un attimo, se
|
||||
lo chiede. Dopo lo scioglimento si torna all'elenco dei gruppi; nella
|
||||
scheda del proclamatore non c'è più "In passato sorvegliante di".
|
||||
- **Nome utente al posto dell'email**: creando il gruppo non si chiede
|
||||
l'email; `GruppiServizio::nomeUtente()` ricava `users.username` dal nome
|
||||
del gruppo (`Str::slug` con il punto: «Gruppo Zisa» → `gruppo.zisa`,
|
||||
numero in coda se già preso; mai una chiocciola). `users.email` è diventata
|
||||
facoltativa. Il login ha un solo campo, "Email o nome utente"
|
||||
(`LoginController`: con la chiocciola cerca l'email, senza il nome utente,
|
||||
in minuscolo). Dove si mostrava l'email dell'utente ora c'è
|
||||
`User::identificativo` (email, o nome utente). Il profilo del sorvegliante
|
||||
mostra il nome utente e non chiede l'email. Il nome del gruppo è unico.
|
||||
Sciolto il gruppo, il nome utente si libera e l'utenza disattivata prende
|
||||
`ex.sorvegliante.<id>` (l'anonimizzata `anonimo.<id>`): resta riconoscibile
|
||||
per l'import XML, che individua gli utenti per email o, senza, per nome
|
||||
utente. Un gruppo con `<sciolto_at>` in un XML vecchio non si importa.
|
||||
- **Anche i membri si segnalano**: `Proclamatore::gruppo_etichetta`
|
||||
("Gruppo Zisa" / "Sorvegliante del gruppo Zisa") come badge nell'elenco
|
||||
(più il filtro *In un gruppo di servizio*, `filtroStato=gruppo`) e in testa
|
||||
@@ -214,7 +239,8 @@ Dove sta:
|
||||
- La voce di menu "Il mio gruppo" guarda il **ruolo**, non `@can`:
|
||||
l'amministratore passa ogni `@can` (`Gate::before`).
|
||||
- XML: si esportano e reimportano gruppi, membri, pacchetto, provenienza delle
|
||||
assegnazioni e il legame utenza ↔ proclamatore.
|
||||
assegnazioni (id e nome del gruppo), nome utente e legame utenza ↔
|
||||
proclamatore.
|
||||
- Informativa privacy aggiornata (utenza del sorvegliante, gruppo tra i dati
|
||||
del proclamatore).
|
||||
- L'azzeramento dell'applicazione (*Zona pericolosa*) cancella anche i gruppi.
|
||||
|
||||
31
README.md
31
README.md
@@ -741,10 +741,13 @@ docker compose --profile dev up -d --build
|
||||
**sorvegliante** (un proclamatore attivo), **membri** e **pacchetto** di
|
||||
territori. Un proclamatore sta in un solo gruppo; il sorvegliante ne è
|
||||
anche membro
|
||||
- Creando il gruppo il sorvegliante riceve un'**utenza**: l'email indicata e
|
||||
una password temporanea, mostrata una volta sola e da cambiare al primo
|
||||
accesso. **Nessuna conferma dell'email**. Se la perde, l'amministratore ne
|
||||
genera un'altra dalla pagina del gruppo
|
||||
- Creando il gruppo il sorvegliante riceve un'**utenza senza email**: un
|
||||
**nome utente ricavato dal nome del gruppo** («Gruppo Zisa» →
|
||||
`gruppo.zisa`; se è già preso, un numero in coda) e una password
|
||||
temporanea, mostrati una volta sola, da cambiare al primo accesso. Al login
|
||||
il campo accetta l'email o il nome utente. Due gruppi non possono avere lo
|
||||
stesso nome. Se perde la password, l'amministratore ne genera un'altra
|
||||
dalla pagina del gruppo
|
||||
- I territori del pacchetto si aggiungono in blocco tra quelli in reparto.
|
||||
Restano **"In gruppo …"**: non sono un'assegnazione (statistiche e medie non
|
||||
cambiano), escono da "Da assegnare" e dalla pagina *Assegna*
|
||||
@@ -754,10 +757,12 @@ docker compose --profile dev up -d --build
|
||||
"via gruppo …"; poi manda il link (copia o condividi), registra i rientri,
|
||||
apre la piantina e rimanda in reparto quelli non ancora dati
|
||||
- Al **rientro** il territorio torna in reparto ed esce dal pacchetto
|
||||
- **Sciogliere il gruppo** toglie il ruolo al sorvegliante: i territori non
|
||||
ancora dati tornano in reparto, i membri si liberano, l'utenza viene
|
||||
disattivata (anche una sessione aperta si chiude) e il gruppo resta nello
|
||||
storico. I territori già dati restano ai membri. Per ripartire si crea un
|
||||
- **Sciogliere il gruppo** toglie il ruolo al sorvegliante e **elimina il
|
||||
gruppo**, che non resta nello storico: i territori solo nel pacchetto
|
||||
tornano in reparto, i membri si liberano, l'utenza viene disattivata (anche
|
||||
una sessione aperta si chiude) e il suo nome utente torna libero. I
|
||||
territori già dati restano ai membri come assegnazioni qualsiasi, che del
|
||||
gruppo conservano solo il nome ("via gruppo …"). Per ripartire si crea un
|
||||
gruppo nuovo; se il sorvegliante è lo stesso, si riattiva la sua utenza
|
||||
- **Un sorvegliante che non è più attivo scioglie il gruppo**, come togliergli
|
||||
il ruolo: succede se lo si rende non attivo (dall'elenco o dalla modifica),
|
||||
@@ -773,8 +778,8 @@ docker compose --profile dev up -d --build
|
||||
resta comunque sceglibile
|
||||
- **Dettagli del gruppo**: la scheda del proclamatore dice se è sorvegliante
|
||||
(gruppo, membri, territori nel pacchetto e da dare, assegnazioni nate dal
|
||||
gruppo, stato dell'utenza e ultimo accesso), membro, o sorvegliante di
|
||||
gruppi ormai sciolti; il sorvegliante ritrova gli stessi numeri nel suo
|
||||
gruppo, nome utente, stato della password e ultimo accesso) o membro; il
|
||||
sorvegliante ritrova gli stessi numeri nel suo
|
||||
*Profilo*
|
||||
|
||||
### Campagne
|
||||
@@ -887,16 +892,16 @@ una revoca ha effetto immediato senza bisogno di ricaricare la pagina.
|
||||
|
||||
| Tabella | Descrizione |
|
||||
|-------------------|--------------------------------------------------------------|
|
||||
| `users` | Utenti; `password_change_required` blocca l'accesso finché non si sceglie una password nuova; `proclamatore_id` lega l'utenza del sorvegliante al suo proclamatore, `disattivato_at` la chiude allo scioglimento del gruppo |
|
||||
| `users` | Utenti; `password_change_required` blocca l'accesso finché non si sceglie una password nuova; `email` facoltativa e `username` per il sorvegliante, che entra con il nome utente del gruppo; `proclamatore_id` lega l'utenza del sorvegliante al suo proclamatore, `disattivato_at` la chiude allo scioglimento del gruppo |
|
||||
| `settings` | Configurazione singleton |
|
||||
| `zone` | Zone territoriali |
|
||||
| `tipologie` | Tipologie di territorio |
|
||||
| `proclamatori` | Assegnatari, con nome/cognome cifrati e soft delete (niente contatti); `gruppo_servizio_id` è il gruppo di cui fanno parte |
|
||||
| `gruppi_servizio` | Gruppi di servizio: nome, sorvegliante, `sciolto_at` (sciolti restano per lo storico) |
|
||||
| `gruppi_servizio` | Gruppi di servizio attivi: nome (unico), sorvegliante. Sciolto, il gruppo si elimina |
|
||||
| `territori` | Cartoline con zona, tipologia, PDF, miniatura, soft delete; `gruppo_servizio_id` se sono nel pacchetto di un gruppo |
|
||||
| `anni_teocratici` | Periodi settembre → agosto |
|
||||
| `campagne` | Periodi speciali con date e descrizione |
|
||||
| `assegnazioni` | Territorio ↔ proclamatore, con date, anno e campagna; stato dell'invio del link (`link_sent`, `link_da_inviare`, `link_canale`); `gruppo_servizio_id` se data dal sorvegliante di un gruppo |
|
||||
| `assegnazioni` | Territorio ↔ proclamatore, con date, anno e campagna; stato dell'invio del link (`link_sent`, `link_da_inviare`, `link_canale`); `gruppo_servizio_id` se data dal sorvegliante di un gruppo ancora attivo, `gruppo_nome` che resta anche dopo lo scioglimento |
|
||||
| `activity_log` | Audit trail |
|
||||
|
||||
**Vincoli e indici principali**
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Gruppi di servizio, 20 ottobre 2026 (decisioni dell'utente):
|
||||
*
|
||||
* - un gruppo sciolto non resta nello storico: si elimina. Le assegnazioni
|
||||
* già date dal sorvegliante conservano il nome del gruppo
|
||||
* (assegnazioni.gruppo_nome), così "via gruppo …" resta leggibile;
|
||||
* - il sorvegliante entra con un nome utente ricavato dal nome del gruppo,
|
||||
* non con l'email: users.username, e users.email diventa facoltativa.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('username', 60)->nullable()->unique()->after('email');
|
||||
});
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('email')->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('assegnazioni', function (Blueprint $table) {
|
||||
$table->string('gruppo_nome', 100)->nullable()->after('gruppo_servizio_id');
|
||||
});
|
||||
|
||||
foreach (DB::table('gruppi_servizio')->get() as $gruppo) {
|
||||
DB::table('assegnazioni')->where('gruppo_servizio_id', $gruppo->id)->update(['gruppo_nome' => $gruppo->nome]);
|
||||
|
||||
if ($gruppo->sciolto_at !== null) {
|
||||
// Già sciolto: pacchetto e membri erano già stati liberati.
|
||||
DB::table('territori')->where('gruppo_servizio_id', $gruppo->id)->update(['gruppo_servizio_id' => null]);
|
||||
DB::table('proclamatori')->where('gruppo_servizio_id', $gruppo->id)->update(['gruppo_servizio_id' => null]);
|
||||
DB::table('assegnazioni')->where('gruppo_servizio_id', $gruppo->id)->update(['gruppo_servizio_id' => null]);
|
||||
DB::table('gruppi_servizio')->where('id', $gruppo->id)->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sorvegliante di un gruppo attivo: il nome utente dal nome del gruppo.
|
||||
$utente = DB::table('users')->where('proclamatore_id', $gruppo->sorvegliante_id)->first();
|
||||
if ($utente && $utente->username === null) {
|
||||
$base = Str::slug($gruppo->nome, '.') ?: 'gruppo';
|
||||
$base = Str::limit($base, 50, '');
|
||||
$username = $base;
|
||||
for ($n = 2; DB::table('users')->where('username', $username)->exists(); $n++) {
|
||||
$username = $base . $n;
|
||||
}
|
||||
DB::table('users')->where('id', $utente->id)->update(['username' => $username]);
|
||||
}
|
||||
}
|
||||
|
||||
Schema::table('gruppi_servizio', function (Blueprint $table) {
|
||||
$table->dropColumn('sciolto_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('gruppi_servizio', function (Blueprint $table) {
|
||||
$table->timestamp('sciolto_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('assegnazioni', function (Blueprint $table) {
|
||||
$table->dropColumn('gruppo_nome');
|
||||
});
|
||||
|
||||
// Senza email l'utenza non si può tenere com'era: chi non ce l'ha
|
||||
// riceve un indirizzo segnaposto, da correggere a mano.
|
||||
DB::table('users')->whereNull('email')->orderBy('id')->each(function ($u) {
|
||||
DB::table('users')->where('id', $u->id)->update(['email' => ($u->username ?? "utente-{$u->id}") . '@senza-email.invalid']);
|
||||
});
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropUnique(['username']);
|
||||
$table->dropColumn('username');
|
||||
});
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('email')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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->email }}</span>
|
||||
Account: <span class="font-medium text-gray-700 break-all">{{ $user->identificativo }}</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->email }}">
|
||||
<input type="hidden" name="email" autocomplete="username" value="{{ $user->identificativo }}">
|
||||
|
||||
@unless($forced)
|
||||
{{-- Dopo un ripristino non si chiede: la password attuale e'
|
||||
|
||||
@@ -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()->email }} · {{ ucfirst(auth()->user()->roles->first()?->name ?? 'utente') }}</span>
|
||||
<span class="text-xs" style="opacity:.7">{{ auth()->user()->identificativo }} · {{ ucfirst(auth()->user()->roles->first()?->name ?? 'utente') }}</span>
|
||||
</span>
|
||||
</x-btn>
|
||||
</div>
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
<form method="POST" action="{{ route('login.store') }}" class="space-y-5">
|
||||
@csrf
|
||||
<div>
|
||||
<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"
|
||||
{{-- 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"
|
||||
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>
|
||||
|
||||
@@ -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">Email</dt><dd><code>{{ $creato->sorvegliante?->utente?->email }}</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">Password</dt><dd><code class="select-all">{{ $passwordTemporanea }}</code></dd>
|
||||
</dl>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
@@ -29,6 +29,7 @@
|
||||
<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>
|
||||
@@ -37,13 +38,6 @@
|
||||
<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>
|
||||
@error('sorveglianteId') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="email-sorvegliante" class="block text-sm font-medium text-gray-700 mb-1">Email per l'accesso *</label>
|
||||
<input wire:model="email" id="email-sorvegliante" type="email" autocomplete="off"
|
||||
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">Resta solo nell'utenza del sorvegliante (non nel proclamatore) e non va confermata.</p>
|
||||
@error('email') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<x-btn variante="primary" type="submit">Crea il gruppo</x-btn>
|
||||
</form>
|
||||
</section>
|
||||
@@ -55,15 +49,12 @@
|
||||
<li class="px-4 py-3 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<a data-ritorno href="{{ route('gruppi.show', $g) }}" class="font-semibold text-indigo-600 hover:underline">{{ $g->nome }}</a>
|
||||
@if($g->sciolto_at)
|
||||
<span class="ml-1 inline-flex rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600">sciolto il {{ $g->sciolto_at->format('d/m/Y') }}</span>
|
||||
@endif
|
||||
<p class="text-xs text-gray-500">
|
||||
Sorvegliante: {{ $g->sorvegliante?->nome_completo ?? '—' }}
|
||||
@unless($g->sciolto_at) · {{ $g->membri_count }} membri · {{ $g->territori_count }} territori nel pacchetto @endunless
|
||||
· {{ $g->membri_count }} membri · {{ $g->territori_count }} territori nel pacchetto
|
||||
</p>
|
||||
</div>
|
||||
<x-btn data-ritorno variante="secondary" tono="tenue" dimensione="sm" :href="route('gruppi.show', $g)">{{ $g->sciolto_at ? 'Dettaglio' : 'Gestisci' }}</x-btn>
|
||||
<x-btn data-ritorno variante="secondary" tono="tenue" dimensione="sm" :href="route('gruppi.show', $g)">Gestisci</x-btn>
|
||||
</li>
|
||||
@empty
|
||||
<li class="px-4 py-8 text-center text-sm text-gray-500">Nessun gruppo di servizio.</li>
|
||||
|
||||
@@ -7,15 +7,10 @@
|
||||
<h1 class="text-2xl font-bold text-gray-900">Gruppo {{ $gruppo->nome }}</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Sorvegliante: <strong>{{ $gruppo->sorvegliante?->nome_completo ?? '—' }}</strong>
|
||||
@if($attivo && $gruppo->sorvegliante?->utente)
|
||||
· accede con <code>{{ $gruppo->sorvegliante->utente->email }}</code>
|
||||
@if($gruppo->sorvegliante?->utente)
|
||||
· nome utente <code class="select-all">{{ $gruppo->sorvegliante->utente->identificativo }}</code>
|
||||
@endif
|
||||
</p>
|
||||
@unless($attivo)
|
||||
<p class="mt-2 inline-flex rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-700">
|
||||
Sciolto il {{ $gruppo->sciolto_at->format('d/m/Y') }} · {{ $assegnazioniDelGruppo }} assegnazioni nate dal gruppo restano nello storico
|
||||
</p>
|
||||
@endunless
|
||||
</div>
|
||||
|
||||
@if($passwordTemporanea)
|
||||
@@ -25,7 +20,6 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($attivo)
|
||||
{{-- ─── Pacchetto ─────────────────────────────────────────── --}}
|
||||
<section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mb-6" aria-labelledby="titolo-pacchetto">
|
||||
<h2 id="titolo-pacchetto" class="px-4 py-3 border-b border-gray-100 text-sm font-semibold text-gray-800">
|
||||
@@ -127,8 +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: 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.">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 disattivata. Le assegnazioni già fatte restano ai membri.">Togli il ruolo e sciogli il gruppo</x-btn>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<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 e stato del suo invio (inviato o no, e con quale canale).</li>
|
||||
<li><strong>Utenti dell'applicazione</strong>: nome, email (che va confermata, tranne per i sorveglianti di gruppo), password conservata solo come impronta non reversibile, ruolo. L'utenza di un sorvegliante è collegata al suo proclamatore e viene disattivata quando il gruppo si scioglie.</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>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 (email 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 (nome utente 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 a nome ed email 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, insieme al nome dell'utenza se è stato sorvegliante di un gruppo.</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>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Gruppo di servizio --}}
|
||||
@if($gruppoSorvegliato || $gruppoMembro || $gruppiPassati->isNotEmpty())
|
||||
@if($gruppoSorvegliato || $gruppoMembro)
|
||||
<section class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6" aria-labelledby="titolo-gruppo">
|
||||
<h3 id="titolo-gruppo" class="text-lg font-semibold text-gray-900 mb-3">Gruppo di servizio</h3>
|
||||
|
||||
@@ -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->email }}</code> @endcan
|
||||
@can('settings.manage') <code>{{ $utenza->identificativo }}</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>
|
||||
@@ -94,15 +94,6 @@
|
||||
sorvegliante {{ $gruppoMembro->sorvegliante?->nome_completo ?? '—' }}.
|
||||
</p>
|
||||
@endif
|
||||
|
||||
@if($gruppiPassati->isNotEmpty())
|
||||
<p class="{{ $gruppoSorvegliato || $gruppoMembro ? 'mt-3' : '' }} text-xs text-gray-500">
|
||||
In passato sorvegliante di:
|
||||
@foreach($gruppiPassati as $g)
|
||||
{{ $g->nome }} (sciolto il {{ $g->sciolto_at->format('d/m/Y') }}){{ $loop->last ? '.' : ',' }}
|
||||
@endforeach
|
||||
</p>
|
||||
@endif
|
||||
</section>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -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->email }}</span>
|
||||
<span class="break-all">{{ $user->identificativo }}</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>
|
||||
@@ -59,7 +59,7 @@
|
||||
{{-- Dati profilo --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-1">Dati profilo</h3>
|
||||
<p class="text-sm text-gray-500 mb-4">Aggiorna nome e cognome ed email.</p>
|
||||
<p class="text-sm text-gray-500 mb-4">{{ $user->isSorvegliante() ? 'Aggiorna nome e cognome.' : 'Aggiorna nome e cognome ed email.' }}</p>
|
||||
|
||||
@if(session('profile_success'))
|
||||
<div class="mb-4 rounded-lg bg-green-50 border border-green-200 px-4 py-2.5 text-sm text-green-800">
|
||||
@@ -73,11 +73,20 @@
|
||||
<input wire:model="name" type="text" id="name" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||
@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 --}}
|
||||
<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>
|
||||
</div>
|
||||
@else
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
|
||||
<input wire:model="email" type="email" id="email" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||
@error('email') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex justify-end">
|
||||
<x-btn variante="primary" type="submit">Salva modifiche</x-btn>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Proclamatore" class="px-3 py-2">{{ $a->proclamatore?->nome_completo ?? 'N/A' }}@if($a->gruppo)<span class="ml-1 inline-flex rounded-full bg-purple-50 px-2 py-0.5 text-xs text-purple-700" title="Assegnato dal sorvegliante del gruppo di servizio">via gruppo {{ $a->gruppo->nome }}</span>@endif</td>
|
||||
<td data-label="Proclamatore" class="px-3 py-2">{{ $a->proclamatore?->nome_completo ?? 'N/A' }}@if($a->gruppo_nome)<span class="ml-1 inline-flex rounded-full bg-purple-50 px-2 py-0.5 text-xs text-purple-700" title="Assegnato dal sorvegliante del gruppo di servizio">via gruppo {{ $a->gruppo_nome }}</span>@endif</td>
|
||||
<td data-label="Assegnato" class="px-3 py-2">{{ $a->assigned_at->format('d/m/Y') }}</td>
|
||||
<td data-label="Rientrato" class="px-3 py-2">
|
||||
@if($a->returned_at)
|
||||
|
||||
@@ -111,8 +111,14 @@
|
||||
@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 }}
|
||||
@if($user->hasVerifiedEmail())
|
||||
@endif
|
||||
@if(! $user->email)
|
||||
@elseif($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"
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@foreach($assegnazioni as $assegnazione)
|
||||
<tr>
|
||||
<td data-label="Proclamatore" class="px-3 py-2">{{ $assegnazione->proclamatore?->nome_completo ?? 'N/A' }}@if($assegnazione->gruppo)<span class="ml-1 inline-flex rounded-full bg-purple-50 px-2 py-0.5 text-xs text-purple-700">via gruppo {{ $assegnazione->gruppo->nome }}</span>@endif</td>
|
||||
<td data-label="Proclamatore" class="px-3 py-2">{{ $assegnazione->proclamatore?->nome_completo ?? 'N/A' }}@if($assegnazione->gruppo_nome)<span class="ml-1 inline-flex rounded-full bg-purple-50 px-2 py-0.5 text-xs text-purple-700">via gruppo {{ $assegnazione->gruppo_nome }}</span>@endif</td>
|
||||
<td data-label="Assegnato" class="px-3 py-2">{{ $assegnazione->assigned_at->format('d/m/Y') }}</td>
|
||||
<td data-label="Rientrato" class="px-3 py-2">
|
||||
@if($assegnazione->returned_at)
|
||||
|
||||
@@ -68,12 +68,12 @@ class GruppiServizioTest extends TestCase
|
||||
$rossi = $this->proclamatore('Rossi', 'Mario');
|
||||
$verdi = $this->proclamatore('Verdi', 'Anna');
|
||||
$bianchi = $this->proclamatore('Bianchi', 'Luca');
|
||||
[$gruppo, $password] = app(GruppiServizio::class)->crea('Zisa', $rossi, 'mario@example.test', [$verdi->id, $bianchi->id]);
|
||||
[$gruppo, $password] = app(GruppiServizio::class)->crea('Zisa', $rossi, [$verdi->id, $bianchi->id]);
|
||||
$territori = collect(['12', '14', '35'])->map(fn ($n) => $this->territorio($n));
|
||||
app(GruppiServizio::class)->aggiungiAlPacchetto($gruppo, $territori->pluck('id')->all());
|
||||
$territori = $territori->map->fresh();
|
||||
|
||||
$sorvegliante = User::where('email', 'mario@example.test')->sole();
|
||||
$sorvegliante = User::where('username', 'zisa')->sole();
|
||||
// Il cambio password al primo accesso ha i suoi test: qui si lavora.
|
||||
// E l'email non confermata, come nella realtà (TestCase le conferma).
|
||||
$sorvegliante->forceFill(['password_change_required' => false, 'email_verified_at' => null])->save();
|
||||
@@ -87,16 +87,20 @@ class GruppiServizioTest extends TestCase
|
||||
{
|
||||
$rossi = $this->proclamatore('Rossi', 'Mario');
|
||||
|
||||
// Niente email: il nome utente viene dal nome del gruppo.
|
||||
$pagina = Livewire::test(GruppiIndex::class)
|
||||
->assertDontSee('Email per l')
|
||||
->set('nome', 'Zisa')
|
||||
->set('sorveglianteId', $rossi->id)
|
||||
->set('email', 'mario@example.test')
|
||||
->call('crea')
|
||||
->assertHasNoErrors();
|
||||
->assertHasNoErrors()
|
||||
->assertSee('Nome utente')
|
||||
->assertSee('zisa');
|
||||
|
||||
$gruppo = GruppoServizio::sole();
|
||||
$utente = User::where('email', 'mario@example.test')->sole();
|
||||
$utente = User::where('username', 'zisa')->sole();
|
||||
|
||||
$this->assertNull($utente->email);
|
||||
$this->assertSame($rossi->id, $gruppo->sorvegliante_id);
|
||||
$this->assertSame($gruppo->id, $rossi->fresh()->gruppo_servizio_id, 'Il sorvegliante è anche membro');
|
||||
$this->assertSame($rossi->id, $utente->proclamatore_id);
|
||||
@@ -114,10 +118,27 @@ class GruppiServizioTest extends TestCase
|
||||
$this->assertTrue(\Illuminate\Support\Facades\Hash::check($password, $utente->password));
|
||||
}
|
||||
|
||||
public function test_il_nome_utente_viene_dal_nome_del_gruppo(): void
|
||||
{
|
||||
$servizio = app(GruppiServizio::class);
|
||||
|
||||
$servizio->crea('Gruppo Noce Città', $this->proclamatore('Neri'));
|
||||
$this->assertSame('gruppo.noce.citta', User::whereNotNull('username')->sole()->username);
|
||||
|
||||
// Già preso (per esempio da un'utenza riattivata): un numero in coda.
|
||||
User::create(['name' => 'Altro', 'username' => 'zisa', 'password' => 'password-di-prova']);
|
||||
$servizio->crea('Zisa', $this->proclamatore('Gialli'));
|
||||
$this->assertTrue(User::where('username', 'zisa2')->exists());
|
||||
|
||||
// Due gruppi con lo stesso nome no.
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$servizio->crea('Zisa', $this->proclamatore('Blu'));
|
||||
}
|
||||
|
||||
public function test_un_proclamatore_sta_in_un_solo_gruppo(): void
|
||||
{
|
||||
['gruppo' => $zisa, 'verdi' => $verdi] = $this->gruppo();
|
||||
[$noce] = app(GruppiServizio::class)->crea('Noce', $this->proclamatore('Neri'), 'neri@example.test');
|
||||
[$noce] = app(GruppiServizio::class)->crea('Noce', $this->proclamatore('Neri'));
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
app(GruppiServizio::class)->aggiungiMembri($noce, [$verdi->id]);
|
||||
@@ -199,16 +220,25 @@ class GruppiServizioTest extends TestCase
|
||||
$this->get(route('dashboard'))->assertOk()->assertDontSee('Il mio gruppo')->assertSee('Gruppi di servizio');
|
||||
}
|
||||
|
||||
public function test_il_sorvegliante_entra_senza_conferma_dell_email_e_va_al_suo_gruppo(): void
|
||||
public function test_il_sorvegliante_entra_con_il_nome_utente_e_va_al_suo_gruppo(): void
|
||||
{
|
||||
$this->gruppo();
|
||||
$this->flushSession();
|
||||
auth()->logout();
|
||||
$password = 'una-password-nuova-1';
|
||||
User::where('email', 'mario@example.test')->sole()->forceFill(['password' => $password])->save();
|
||||
User::where('username', 'zisa')->sole()->forceFill(['password' => $password])->save();
|
||||
|
||||
$this->post(route('login.store'), ['email' => 'mario@example.test', 'password' => $password])
|
||||
$this->get(route('login'))->assertSee('Email o nome utente');
|
||||
|
||||
// Senza badare alle maiuscole (la tastiera del telefono le mette da sé).
|
||||
$this->post(route('login.store'), ['email' => 'Zisa', 'password' => $password])
|
||||
->assertRedirect(route('mio-gruppo'));
|
||||
|
||||
// Gli altri continuano a entrare con l'email.
|
||||
auth()->logout();
|
||||
$this->flushSession();
|
||||
$this->post(route('login.store'), ['email' => 'admin@example.test', 'password' => 'password-di-prova'])
|
||||
->assertRedirect(route('dashboard'));
|
||||
}
|
||||
|
||||
// ─── Il sorvegliante al lavoro ──────────────────────────────
|
||||
@@ -322,19 +352,29 @@ class GruppiServizioTest extends TestCase
|
||||
['gruppo' => $gruppo, 'sorvegliante' => $s, 'verdi' => $verdi, 'territori' => $territori] = $this->gruppo();
|
||||
$data = app(GruppiServizio::class)->assegna($gruppo, $territori[0], $verdi, now());
|
||||
|
||||
Livewire::test(GruppoShow::class, ['gruppo' => $gruppo])->call('sciogli');
|
||||
Livewire::test(GruppoShow::class, ['gruppo' => $gruppo])
|
||||
->call('sciogli')
|
||||
->assertRedirect(route('gruppi.index'));
|
||||
|
||||
$gruppo->refresh();
|
||||
$this->assertNotNull($gruppo->sciolto_at);
|
||||
// I non dati tornano in reparto; quello dato resta a Verdi, assegnazione normale.
|
||||
// Il gruppo non resta nello storico: non esiste più.
|
||||
$this->assertNull(GruppoServizio::find($gruppo->id));
|
||||
$this->get(route('gruppi.index'))->assertOk()->assertSee('Nessun gruppo di servizio');
|
||||
// I non dati tornano in reparto; quello dato resta a Verdi, assegnazione
|
||||
// normale, che del gruppo conserva solo il nome.
|
||||
$this->assertNull($territori[1]->fresh()->gruppo_servizio_id);
|
||||
$this->assertNull($territori[2]->fresh()->gruppo_servizio_id);
|
||||
$this->assertSame('in_reparto', $territori[2]->fresh()->stato);
|
||||
$this->assertNull($data->fresh()->returned_at);
|
||||
$this->assertSame($gruppo->id, $data->fresh()->gruppo_servizio_id, 'Lo storico continua a dirlo');
|
||||
// Membri liberi, utenza disattivata e senza ruolo.
|
||||
$this->assertSame(0, Proclamatore::where('gruppo_servizio_id', $gruppo->id)->count());
|
||||
$this->assertSame('assegnato', $territori[0]->fresh()->stato);
|
||||
$this->assertNull($data->fresh()->gruppo_servizio_id);
|
||||
$this->assertSame('Zisa', $data->fresh()->gruppo_nome);
|
||||
$this->get(route('territori.show', $territori[0]))->assertOk()->assertSee('via gruppo Zisa');
|
||||
// Membri liberi; utenza disattivata, senza ruolo, e il nome utente del
|
||||
// gruppo è libero.
|
||||
$this->assertSame(0, Proclamatore::whereNotNull('gruppo_servizio_id')->count());
|
||||
$this->assertTrue($s->fresh()->isDisattivato());
|
||||
$this->assertFalse($s->fresh()->hasRole('sorvegliante'));
|
||||
$this->assertSame("ex.sorvegliante.{$s->id}", $s->fresh()->username);
|
||||
$this->assertFalse(User::where('username', 'zisa')->exists());
|
||||
|
||||
// Per ripartire serve un gruppo nuovo.
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
@@ -350,7 +390,7 @@ class GruppiServizioTest extends TestCase
|
||||
$this->flushSession();
|
||||
auth()->logout();
|
||||
|
||||
$this->post(route('login.store'), ['email' => 'mario@example.test', 'password' => $password])
|
||||
$this->post(route('login.store'), ['email' => 'zisa', 'password' => $password])
|
||||
->assertSessionHasErrors('email');
|
||||
$this->assertGuest();
|
||||
|
||||
@@ -365,13 +405,14 @@ class GruppiServizioTest extends TestCase
|
||||
$pagina = Livewire::test(GruppiIndex::class);
|
||||
$this->assertContains((string) $rossi->id, collect($pagina->viewData('opzioniSorveglianti'))->pluck('valore')->all(), 'Torna tra i candidati');
|
||||
|
||||
[$nuovo, $password] = app(GruppiServizio::class)->crea('Zisa 2', $rossi->fresh(), 'mario.nuovo@example.test');
|
||||
// Sciolto, il nome non è più occupato: si può rifare "Zisa".
|
||||
[$nuovo, $password] = app(GruppiServizio::class)->crea('Zisa', $rossi->fresh());
|
||||
|
||||
$utente = $s->fresh();
|
||||
$this->assertSame(1, User::where('proclamatore_id', $rossi->id)->count(), 'Un proclamatore, un\'utenza');
|
||||
$this->assertFalse($utente->isDisattivato());
|
||||
$this->assertTrue($utente->hasRole('sorvegliante'));
|
||||
$this->assertSame('mario.nuovo@example.test', $utente->email);
|
||||
$this->assertSame('zisa', $utente->username);
|
||||
$this->assertTrue($utente->password_change_required);
|
||||
$this->assertTrue(\Illuminate\Support\Facades\Hash::check($password, $utente->password));
|
||||
$this->assertSame($nuovo->id, $utente->gruppoSorvegliato()?->id);
|
||||
@@ -382,7 +423,8 @@ class GruppiServizioTest extends TestCase
|
||||
/** Il gruppo è sciolto come da "Togli il ruolo": tutto tranne l'assegnazione già fatta. */
|
||||
private function assertSciolto(array $g, Assegnazione $data): void
|
||||
{
|
||||
$this->assertNotNull($g['gruppo']->fresh()->sciolto_at, 'Gruppo sciolto');
|
||||
$this->assertNull(GruppoServizio::find($g['gruppo']->id), 'Gruppo sciolto ed eliminato');
|
||||
$this->assertSame('Zisa', $data->fresh()->gruppo_nome);
|
||||
$this->assertNull($g['territori'][1]->fresh()->gruppo_servizio_id, 'I non dati tornano in reparto');
|
||||
$this->assertSame('in_reparto', $g['territori'][2]->fresh()->stato);
|
||||
$this->assertNull($data->fresh()->returned_at, 'Quello già dato resta al membro');
|
||||
@@ -406,7 +448,7 @@ class GruppiServizioTest extends TestCase
|
||||
|
||||
// Riattivarlo non ricostruisce il gruppo: se ne crea uno nuovo.
|
||||
Livewire::test(\App\Livewire\Proclamatori\ProclamatoreIndex::class)->call('toggleActive', $g['rossi']->id);
|
||||
$this->assertNotNull($g['gruppo']->fresh()->sciolto_at);
|
||||
$this->assertSame(0, GruppoServizio::count());
|
||||
$this->assertNull($g['rossi']->fresh()->gruppoSorvegliato());
|
||||
}
|
||||
|
||||
@@ -445,7 +487,7 @@ class GruppiServizioTest extends TestCase
|
||||
->call('toggleActive', $verdi->id)
|
||||
->call('deleteProclamatore', $bianchi->id);
|
||||
|
||||
$this->assertNull($gruppo->fresh()->sciolto_at);
|
||||
$this->assertNotNull(GruppoServizio::find($gruppo->id));
|
||||
$this->assertSame($gruppo->id, $verdi->fresh()->gruppo_servizio_id, 'Non attivo resta membro, ma non riceve territori');
|
||||
$this->assertNull($bianchi->fresh()->gruppo_servizio_id, 'Nel cestino lascia il gruppo');
|
||||
}
|
||||
@@ -459,17 +501,18 @@ class GruppiServizioTest extends TestCase
|
||||
|
||||
$this->get(route('proclamatori.show', $g['rossi']))->assertOk()
|
||||
->assertSeeInOrder(['Gruppo di servizio', 'Sorvegliante', 'Zisa', 'Membri', '3', 'Nel pacchetto', '3', 'Da dare', '2', 'Assegnazioni dal gruppo', '1'])
|
||||
->assertSee('mario@example.test')
|
||||
->assertSee('<code>zisa</code>', false)
|
||||
->assertSee('non è ancora entrato');
|
||||
|
||||
$this->get(route('proclamatori.show', $g['verdi']))->assertOk()
|
||||
->assertSee('Membro del gruppo')
|
||||
->assertSee('sorvegliante Rossi Mario');
|
||||
|
||||
// Sciolto il gruppo, nella scheda non ne resta traccia.
|
||||
app(GruppiServizio::class)->sciogli($g['gruppo']);
|
||||
$this->get(route('proclamatori.show', $g['rossi']))->assertOk()
|
||||
->assertSee('In passato sorvegliante di:')
|
||||
->assertDontSee('Assegnazioni dal gruppo');
|
||||
->assertDontSee('Assegnazioni dal gruppo')
|
||||
->assertDontSee('In passato');
|
||||
}
|
||||
|
||||
public function test_chi_fa_parte_di_un_gruppo_e_segnalato_in_elenco_scheda_e_scelta(): void
|
||||
@@ -529,7 +572,8 @@ class GruppiServizioTest extends TestCase
|
||||
|
||||
$utente = $s->fresh();
|
||||
$this->assertSame('Anonimo', $utente->name);
|
||||
$this->assertStringEndsWith('@anonimo.invalid', $utente->email);
|
||||
$this->assertNull($utente->email);
|
||||
$this->assertSame("anonimo.{$utente->id}", $utente->username);
|
||||
$this->assertTrue($utente->isDisattivato());
|
||||
$this->assertCount(0, $utente->roles);
|
||||
}
|
||||
@@ -579,6 +623,25 @@ class GruppiServizioTest extends TestCase
|
||||
$this->assertEqualsCanonicalizing(['Rossi Mario', 'Verdi Anna', 'Bianchi Luca'], $g->membri->map->nome_completo->all());
|
||||
$this->assertEqualsCanonicalizing(['12', '14', '35'], $g->territori->pluck('numero')->all());
|
||||
$this->assertSame($g->id, Assegnazione::sole()->gruppo_servizio_id);
|
||||
$this->assertSame($g->sorvegliante_id, User::where('email', 'mario@example.test')->sole()->proclamatore_id);
|
||||
$this->assertSame('Zisa', Assegnazione::sole()->gruppo_nome);
|
||||
$utenza = User::where('username', 'zisa')->sole();
|
||||
$this->assertNull($utenza->email);
|
||||
$this->assertSame($g->sorvegliante_id, $utenza->proclamatore_id);
|
||||
}
|
||||
|
||||
public function test_l_xml_conserva_il_nome_del_gruppo_sulle_assegnazioni_dopo_lo_scioglimento(): void
|
||||
{
|
||||
['gruppo' => $gruppo, 'verdi' => $verdi, 'territori' => $territori] = $this->gruppo();
|
||||
app(GruppiServizio::class)->assegna($gruppo, $territori[0], $verdi, now());
|
||||
app(GruppiServizio::class)->sciogli($gruppo);
|
||||
|
||||
$xml = (new XmlExchange())->currentXmlBackup();
|
||||
$this->assertArrayNotHasKey('error', (new XmlExchange())->importXmlFromContent($xml));
|
||||
|
||||
$this->assertSame(0, GruppoServizio::count());
|
||||
$this->assertNull(Assegnazione::sole()->gruppo_servizio_id);
|
||||
$this->assertSame('Zisa', Assegnazione::sole()->gruppo_nome);
|
||||
// L'utenza disattivata, autrice dell'assegnazione, sopravvive al giro.
|
||||
$this->assertTrue(User::where('username', 'like', 'ex.sorvegliante.%')->sole()->isDisattivato());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user