preselectedTerritorioId = $territorioId; $this->territorio_id = $territorioId; $this->assigned_at = now()->format('Y-m-d'); // ?invia=: riprende l'invio di un link rimasto in sospeso (dalla // scheda del territorio). if ($invia = request()->integer('invia')) { $this->apriInvio($invia); } } protected function rules(): array { return [ 'territorio_id' => 'required|exists:territori,id', 'proclamatore_id' => 'required|exists:proclamatori,id', 'assigned_at' => 'required|date|before_or_equal:today', ]; } public function save() { $this->validate(); $territorio = Territorio::findOrFail($this->territorio_id); // Gli errori vanno sul campo e non in sessione: il messaggio flash // sta nel telaio della pagina, che Livewire non ridisegna, e il // rifiuto passerebbe inosservato. if ($territorio->assegnazioneCorrente) { $this->addError('territorio_id', "Il territorio {$territorio->numero} è già assegnato a {$territorio->assegnazioneCorrente->proclamatore->nome_completo}."); return; } if (!$territorio->attivo) { $this->addError('territorio_id', "Il territorio {$territorio->numero} è inattivo."); return; } $proclamatore = Proclamatore::findOrFail($this->proclamatore_id); if (!$proclamatore->attivo) { $this->addError('proclamatore_id', "Il proclamatore {$proclamatore->nome_completo} è inattivo."); return; } $assignedDate = \Carbon\Carbon::parse($this->assigned_at); $annoTeocratico = AnnoTeocratico::perData($assignedDate); // Controllo e inserimento sotto lo stesso lock sulla riga del // territorio: un doppio clic o due schede aperte non possono più // superare entrambi il controllo e creare due assegnazioni aperte. $esito = DB::transaction(function () use ($territorio, $assignedDate, $annoTeocratico) { Territorio::whereKey($territorio->id)->lockForUpdate()->first(); $conflitto = Assegnazione::sovrappostaA($territorio->id, $assignedDate); if ($conflitto) { return "Il territorio {$territorio->numero} risulta assegnato {$conflitto->descrizionePeriodo()}: " . ($conflitto->returned_at ? 'la data di assegnazione non può essere precedente al suo rientro.' : 'registrane prima il rientro.'); } return Assegnazione::create([ 'territorio_id' => $this->territorio_id, 'proclamatore_id' => $this->proclamatore_id, 'anno_teocratico_id' => $annoTeocratico->id, 'assigned_at' => $assignedDate, 'created_by' => auth()->id(), // C'è un link da mandare solo se il territorio ha il PDF. 'link_da_inviare' => (bool) $territorio->pdf_path, ]); }); if (is_string($esito)) { $this->addError('assigned_at', $esito); return; } $assegnazione = $esito; activity()->causedBy(auth()->user()) ->performedOn($assegnazione) // L'id, non il nome: nome e cognome sono cifrati nel database, e // copiati qui in chiaro restavano leggibili anche dopo // l'anonimizzazione. Il registro mostra il nome di oggi. ->withProperties([ 'territorio' => $territorio->numero, 'proclamatore_id' => $proclamatore->id, ]) ->log('assigned'); // Non si va avanti: si apre l'invio del link (vedi in cima). $this->apriInvio($assegnazione->id); } /** * Apre l'invio del link: subito dopo l'assegnazione, o da un'assegnazione * rimasta in sospeso ("Invia link" nell'elenco, che è un componente a sé * e lo chiede con l'evento apri-invio). */ #[On('apri-invio')] public function apriInvio(int $assegnazioneId): void { $this->authorize('territori.assign'); $assegnazione = Assegnazione::aperte()->find($assegnazioneId); if (! $assegnazione) { return; } $this->invioId = $assegnazione->id; $this->canaleUsato = null; $this->erroreInvio = ''; } /** Copia link o WhatsApp: l'invio vero lo fa chi usa l'app, qui si annota il canale. */ public function canale(string $canale): void { $assegnazione = $this->assegnazioneInInvio(); if (! $assegnazione || ! in_array($canale, ['copia', 'whatsapp'], true)) { return; } if ($canale === 'whatsapp' && ! $assegnazione->proclamatore?->whatsappUrl()) { return; } $this->canaleUsato = $canale; $this->erroreInvio = ''; } /** Email: la manda l'applicazione, subito (vedi LinkTerritorio). */ public function inviaEmail(): void { $assegnazione = $this->assegnazioneInInvio(); $email = trim((string) $assegnazione?->proclamatore?->email); $link = $assegnazione?->shortPdfUrl(); if (! $assegnazione || $email === '' || ! $link || ! app(ConfigurazionePosta::class)->funzionante()) { return; } try { Notification::route('mail', $email)->notifyNow(new LinkTerritorio($assegnazione, $link)); } catch (\Throwable $e) { report($e); $this->erroreInvio = "L'email non è partita: controlla il server di posta, oppure usa un altro canale."; return; } $this->canaleUsato = 'email'; $this->erroreInvio = ''; } /** * La risposta a "Hai inviato il link?": la si registra e solo adesso si * va avanti. "Sì" vuole un canale usato; "No" lascia il link da inviare. */ public function concludiInvio(bool $inviato) { $assegnazione = $this->assegnazioneInInvio(); if (! $assegnazione) { $this->invioId = null; return null; } if ($inviato && $this->canaleUsato === null) { $this->erroreInvio = 'Scegli prima come mandare il link.'; return null; } $assegnazione->registraInvioLink($inviato, $this->canaleUsato); // Nel log il canale e l'id del proclamatore, mai nome, numero o email. activity()->causedBy(auth()->user()) ->performedOn($assegnazione) ->withProperties([ 'territorio' => $assegnazione->territorio?->numero, 'proclamatore_id' => $assegnazione->proclamatore_id, 'canale' => $inviato ? $this->canaleUsato : null, ]) ->log($inviato ? 'link_inviato' : 'link_non_inviato'); $numero = $assegnazione->territorio?->numero; $nome = $assegnazione->proclamatore?->nome_completo; $this->reset(['invioId', 'canaleUsato', 'erroreInvio']); session()->flash('success', $inviato ? "Territorio {$numero} assegnato a {$nome}: link inviato." : "Territorio {$numero} assegnato a {$nome}. Il link è ancora da inviare: lo trovi segnalato nell'elenco."); return $this->redirect($this->destinazione(route('assegnazioni.assegna')), navigate: true); } private function assegnazioneInInvio(): ?Assegnazione { if (! $this->invioId) { return null; } $this->authorize('territori.assign'); return Assegnazione::with(['territorio', 'proclamatore'])->find($this->invioId); } public function render() { // Le opzioni dei campi di scelta con ricerca (). Ci sono anche // i territori già assegnati, che non si possono scegliere: cercandone // uno si legge a chi è, invece di un "nessun risultato". La miniatura // serve all'anteprima, che si aggiorna nel browser. $territori = Territorio::attivi() ->with(['zona', 'tipologia', 'assegnazioneCorrente.proclamatore']) ->get() ->sortBy('numero', SORT_NATURAL); $opzioniTerritori = $territori ->map(fn (Territorio $t) => $t->opzioneScelta( $t->assegnazioneCorrente ? 'già assegnato a ' . ($t->assegnazioneCorrente->proclamatore?->nome_completo ?? '—') : null ) + array_filter(['miniatura' => $t->miniaturaUrl()])) ->values() ->all(); $opzioniProclamatori = Proclamatore::opzioniScelta(Proclamatore::attivi()->get()); // L'elenco dei territori assegnati sta in ElencoAssegnati. // L'invio in corso; se nel frattempo l'assegnazione è rientrata o // sparita non c'è più niente da mandare. $invio = $this->invioId ? Assegnazione::aperte()->with(['territorio', 'proclamatore'])->find($this->invioId) : null; if ($this->invioId && ! $invio) { $this->reset(['invioId', 'canaleUsato', 'erroreInvio']); } return view('livewire.assegnazioni.assegna', [ 'invio' => $invio, 'postaAttiva' => $invio ? app(ConfigurazionePosta::class)->funzionante() : false, 'opzioniTerritori' => $opzioniTerritori, 'opzioniProclamatori' => $opzioniProclamatori, 'anteprimaIniziale' => $this->territorio_id ? $territori->firstWhere('id', $this->territorio_id)?->miniaturaUrl() : null, ]); } }