diff --git a/HANDOFF.md b/HANDOFF.md index 9620b1e..aec7cc2 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -106,11 +106,9 @@ Richieste dell'utente, con le sue scelte: nell'export/import XML, cancellati dall'anonimizzazione. Il numero apre WhatsApp (``, `Proclamatore::whatsappUrl()`: senza prefisso si considera italiano). -- **Dopo l'assegnazione** si resta sulla pagina Assegna: in cima un riquadro - per ogni assegnazione appena fatta con il link del PDF, "Invia su WhatsApp" - (messaggio già scritto con il link) e la scelta "Sì, inviato / Non ancora", - che imposta `link_sent`. Resta finché non si sceglie - (`Assegna::DA_INVIARE` in sessione). +- **Dopo l'assegnazione** — *rivisto il 19 ottobre 2026, vedi "Invio del link + dopo l'assegnazione" qui sotto*. Il riquadro in sessione + (`Assegna::DA_INVIARE`) non esiste più. - **Conferma email obbligatoria** per tutti gli utenti, anche quelli già esistenti (la migrazione `2026_10_13_000010` azzera `email_verified_at`). L'admin crea l'utente con la password come prima; parte l'email di conferma @@ -129,6 +127,41 @@ email di prova e di conferma arrivate, link aperto senza login, account sbloccato; riquadro dopo l'assegnazione con WhatsApp e scelta. Configurazione e dati di prova rimossi dopo. +### Invio del link dopo l'assegnazione, nuovo territorio dal designer (19 ottobre 2026) + +Richieste dell'utente: + +- **Dopo l'assegnazione non si va avanti** finché non si è deciso del link: si + apre una finestra che blocca la pagina (`Assegna::apriInvio`) con i canali + - *Copia link*: sempre; + - *WhatsApp*: solo se il proclamatore ha un numero utilizzabile; + - *Email*: solo se ha un indirizzo **e** la posta funziona + (`ConfigurazionePosta::funzionante`). La manda l'app con + `Notification::route('mail', …)->notifyNow(new LinkTerritorio)`: **mai in + coda**, perché il job salverebbe in chiaro su Redis/`failed_jobs` + l'indirizzo che nel database è cifrato. + + Poi "Hai inviato il link?": *Sì* vuole un canale usato, *No* è sempre + possibile. Solo dopo la risposta si ricarica la pagina. +- **Se la procedura non si conclude** (scheda chiusa) o si risponde *No*, resta + `assegnazioni.link_da_inviare = true` e compare "Link ancora da inviare" + nell'elenco della pagina Assegna (con "Invia link", che riapre la finestra) + e nella scheda del territorio (che rimanda a `assegnazioni/assegna?invia=`). + L'avviso sparisce quando il link risulta inviato, anche dal pulsante + "Non inviato / Link inviato" dell'elenco. +- Colonne nuove (migrazione `2026_10_19_000010`): `link_da_inviare` (le + assegnazioni già esistenti restano false: niente avvisi a posteriori su + link di cui non si sa niente) e `link_canale` (`copia`/`whatsapp`/`email`, + `Assegnazione::CANALI_LINK`). Entrambe nell'XML. +- Nel log `link_inviato` / `link_non_inviato` con canale e `proclamatore_id`: + mai nome, numero o email. Email e telefono restano solo, cifrati, nel + proclamatore. +- **"Nuovo territorio"** (elenco territori) apre il designer delle piantine + con `?nuovo=1` (`PiantineEditor::$creaTerritorio`): la mappa parte già in + disegno e la prima piantina disegnata si apre con "nuovo territorio" scelto. + L'editor è solo dell'amministratore: chi ha `territori.manage` ma non + `settings.manage` resta sul form `territori.create`, che esiste ancora. + ### Revisione profonda: blocchi 2 e 3 chiusi | Problema | Correzione | diff --git a/app/Livewire/Assegnazioni/Assegna.php b/app/Livewire/Assegnazioni/Assegna.php index a8101df..2d04345 100644 --- a/app/Livewire/Assegnazioni/Assegna.php +++ b/app/Livewire/Assegnazioni/Assegna.php @@ -4,16 +4,35 @@ namespace App\Livewire\Assegnazioni; use Livewire\Component; use Livewire\Attributes\Computed; +use Livewire\Attributes\Locked; use App\Models\Territorio; use App\Models\Proclamatore; use App\Models\Assegnazione; use App\Models\AnnoTeocratico; +use App\Notifications\LinkTerritorio; +use App\Services\ConfigurazionePosta; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Notification; class Assegna extends Component { - /** Chiave di sessione delle assegnazioni appena fatte, in attesa della scelta sul link. */ - public const DA_INVIARE = 'assegnazioni_da_inviare'; + // ─── Invio del link ───────────────────────────────────────── + // + // Dopo l'assegnazione non si torna subito alla pagina: si sceglie il + // canale (copia link sempre; WhatsApp ed email solo se il proclamatore ha + // numero / indirizzo), si dice se il link è partito, e solo allora si va + // avanti. Se la procedura si interrompe, l'assegnazione resta con + // link_da_inviare e lo si segnala dove compare. + + /** L'assegnazione di cui si sta mandando il link. */ + #[Locked] + public ?int $invioId = null; + + /** Il canale usato in questo invio (Assegnazione::CANALI_LINK). */ + #[Locked] + public ?string $canaleUsato = null; + + public string $erroreInvio = ''; public ?int $territorio_id = null; public ?int $proclamatore_id = null; @@ -29,6 +48,12 @@ class Assegna extends Component $this->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 @@ -88,6 +113,8 @@ class Assegna extends Component '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, ]); }); @@ -110,34 +137,123 @@ class Assegna extends Component ]) ->log('assigned'); - // Si resta qui: in cima compare il riquadro con il link da mandare al - // proclamatore (WhatsApp) e la scelta "inviato / non ancora". La lista - // sta in sessione, così il riquadro resta finché non si sceglie, anche - // ricaricando la pagina o assegnando altri territori nel frattempo. - $daInviare = session(self::DA_INVIARE, []); - $daInviare[] = $assegnazione->id; - session([self::DA_INVIARE => array_values(array_unique($daInviare))]); - - session()->flash('success', "Territorio {$territorio->numero} assegnato a {$proclamatore->nome_completo}."); - return $this->redirect(route('assegnazioni.assegna'), navigate: true); + // Non si va avanti: si apre l'invio del link (vedi in cima). + $this->apriInvio($assegnazione->id); } - /** La scelta nel riquadro: il link è stato mandato, oppure non ancora. */ - public function esitoInvio(int $assegnazioneId, bool $inviato): void + /** Apre l'invio del link: subito dopo l'assegnazione, o da un'assegnazione rimasta in sospeso. */ + public function apriInvio(int $assegnazioneId): void { $this->authorize('territori.assign'); - $assegnazione = Assegnazione::find($assegnazioneId); - $assegnazione?->forceFill(['link_sent' => $inviato])->saveQuietly(); + $assegnazione = Assegnazione::aperte()->find($assegnazioneId); + if (! $assegnazione) { + return; + } - session([self::DA_INVIARE => array_values(array_diff(session(self::DA_INVIARE, []), [$assegnazioneId]))]); + $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; + 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."); + + $this->reset(['invioId', 'canaleUsato', 'erroreInvio']); + + return $this->redirect(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 toggleLinkSent(int $assegnazioneId): void { $this->authorize('territori.assign'); $assegnazione = Assegnazione::findOrFail($assegnazioneId); - $assegnazione->forceFill(['link_sent' => ! $assegnazione->link_sent])->saveQuietly(); + $assegnazione->registraInvioLink(! $assegnazione->link_sent, $assegnazione->link_canale); } #[Computed] @@ -198,19 +314,16 @@ class Assegna extends Component // Il link scade alla soglia rientro (giorni_per_smarrito). $sogliaRientro = max(1, (int) \App\Models\Setting::getValue('giorni_per_smarrito', 120)); - // I riquadri in evidenza: solo le assegnazioni ancora aperte (una - // rientrata o cancellata nel frattempo non ha più un link da mandare). - $idDaInviare = session(self::DA_INVIARE, []); - $daInviare = $idDaInviare === [] ? collect() : Assegnazione::aperte() - ->whereIn('id', $idDaInviare) - ->with(['territorio', 'proclamatore']) - ->get(); - if ($daInviare->count() !== count($idDaInviare)) { - session([self::DA_INVIARE => $daInviare->pluck('id')->all()]); + // 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', [ - 'daInviare' => $daInviare, + 'invio' => $invio, + 'postaAttiva' => $invio ? app(ConfigurazionePosta::class)->funzionante() : false, 'territoriDisponibili' => $territoriDisponibili, 'proclamatoriAttivi' => $proclamatoriAttivi, 'assegnazioniAperte' => $assegnazioniAperte, diff --git a/app/Livewire/Piantine/PiantineEditor.php b/app/Livewire/Piantine/PiantineEditor.php index 2453846..35122b2 100644 --- a/app/Livewire/Piantine/PiantineEditor.php +++ b/app/Livewire/Piantine/PiantineEditor.php @@ -66,10 +66,20 @@ class PiantineEditor extends Component public string $nuovaTipologia = ''; + /** + * Arrivati da "Nuovo territorio" (?nuovo=1): la mappa parte già in + * disegno, e la prima piantina disegnata si apre con "nuovo territorio" + * scelto, pronta da compilare e creare. + */ + #[Locked] + public bool $creaTerritorio = false; + public function mount(?int $mappa = null): void { if ($mappa !== null && TerritorioMappa::whereKey($mappa)->exists()) { $this->carica(TerritorioMappa::with('territorio')->find($mappa)); + } elseif (request()->boolean('nuovo')) { + $this->creaTerritorio = true; } } @@ -154,6 +164,11 @@ class PiantineEditor extends Component $mappa = TerritorioMappa::create(['geojson' => $geojson]); $this->carica($mappa); activity()->causedBy(auth()->user())->log('piantina_creata'); + + if ($this->creaTerritorio) { + $this->creaTerritorio = false; + $this->territorioScelto = 'nuovo'; + } } $this->salvato($mappa, seleziona: true); diff --git a/app/Livewire/Settings/XmlExchange.php b/app/Livewire/Settings/XmlExchange.php index 951afa7..7a822a6 100644 --- a/app/Livewire/Settings/XmlExchange.php +++ b/app/Livewire/Settings/XmlExchange.php @@ -768,6 +768,8 @@ class XmlExchange extends Component 'counted_in_campaign' => ((int) ($assegnazioneNode->counted_in_campaign ?? 0)) === 1, 'campaign_id' => $legacyCampagna !== '' && isset($campagnaMap[$legacyCampagna]) ? $campagnaMap[$legacyCampagna] : null, 'link_sent' => ((int) ($assegnazioneNode->link_sent ?? 0)) === 1, + 'link_da_inviare' => ((int) ($assegnazioneNode->link_da_inviare ?? 0)) === 1, + '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 ?? ''), 'created_by' => $createdBy, @@ -832,7 +834,7 @@ class XmlExchange extends Component 'territori' => $this->righeEsportate(Territorio::query()->withTrashed()->orderBy('id'), ['id', 'numero', 'zona_id', 'tipologia_id', 'confini', 'note', 'attivo', 'prioritario', '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', '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', 'pdf_access_code', 'created_at', 'updated_at']), ]; } @@ -1321,6 +1323,8 @@ class XmlExchange extends Component $node->addChild('returned_at', (string) ($assegnazione['returned_at'] ?? '')); $node->addChild('counted_in_campaign', (string) ((int) ($assegnazione['counted_in_campaign'] ?? 0))); $node->addChild('link_sent', (string) ((int) ($assegnazione['link_sent'] ?? 0))); + $node->addChild('link_da_inviare', (string) ((int) ($assegnazione['link_da_inviare'] ?? 0))); + $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'] ?? '')); $this->addTimestampNodes($node, $assegnazione); diff --git a/app/Models/Assegnazione.php b/app/Models/Assegnazione.php index 445442b..680de45 100644 --- a/app/Models/Assegnazione.php +++ b/app/Models/Assegnazione.php @@ -20,6 +20,8 @@ class Assegnazione extends Model 'campaign_id', 'pdf_access_code', 'link_sent', + 'link_da_inviare', + 'link_canale', 'note', 'created_by', 'returned_by', @@ -32,6 +34,7 @@ class Assegnazione extends Model 'returned_at' => 'date', 'counted_in_campaign' => 'boolean', 'link_sent' => 'boolean', + 'link_da_inviare' => 'boolean', ]; } @@ -211,9 +214,43 @@ class Assegnazione extends Model return route('assignments.pdf.short', ['code' => $this->ensurePdfAccessCode()]); } - public function markLinkSent(): void + public function markLinkSent(?string $canale = null): void { - $this->forceFill(['link_sent' => true])->saveQuietly(); + $this->registraInvioLink(true, $canale); + } + + /** Canali con cui si manda il link al proclamatore. */ + public const CANALI_LINK = ['copia' => 'Copia link', 'whatsapp' => 'WhatsApp', 'email' => 'Email']; + + /** + * Esito dell'invio del link: inviato (con il canale usato) oppure no. Un + * link non inviato resta "da inviare" e lo si segnala sull'assegnazione, + * a meno che non ci sia proprio un link da mandare (territorio senza PDF). + */ + public function registraInvioLink(bool $inviato, ?string $canale = null): void + { + $this->forceFill([ + 'link_sent' => $inviato, + 'link_da_inviare' => ! $inviato && $this->shortPdfUrl() !== null, + 'link_canale' => $inviato ? $canale : $this->link_canale, + ])->saveQuietly(); + } + + /** Il link va ancora mandato: l'avviso da mostrare sull'assegnazione. */ + public function getLinkInSospesoAttribute(): bool + { + return $this->link_da_inviare && ! $this->link_sent && $this->is_aperta; + } + + /** Il messaggio con il link, uguale per WhatsApp ed email. */ + public function messaggioLink(): ?string + { + $link = $this->shortPdfUrl(); + if ($link === null) { + return null; + } + + return "Ciao {$this->proclamatore?->nome}, ti è stato assegnato il territorio N° {$this->territorio?->numero}. Ecco la piantina: {$link}"; } // ─── Scopes ───────────────────────────────────────────────── diff --git a/app/Notifications/LinkTerritorio.php b/app/Notifications/LinkTerritorio.php new file mode 100644 index 0000000..463e6eb --- /dev/null +++ b/app/Notifications/LinkTerritorio.php @@ -0,0 +1,45 @@ +assegnazione->territorio?->numero; + $congregazione = trim((string) Setting::getValue('congregazione_nome', '')); + // Stessa regola di AssignmentPdfController::linkScaduto(). + $scadenza = $this->assegnazione->assigned_at->copy() + ->addDays(max(1, (int) Setting::getValue('giorni_per_smarrito', 120))); + + return (new MailMessage) + ->subject("Territorio N° {$numero} — piantina") + ->greeting('Ciao ' . $this->assegnazione->proclamatore?->nome . ',') + ->line("ti è stato assegnato il territorio N° {$numero}.") + ->action('Apri la piantina', $this->link) + ->line("Il link funziona finché il territorio è assegnato a te, al massimo fino al {$scadenza->format('d/m/Y')}.") + ->salutation($congregazione !== '' ? $congregazione : 'TerManager2'); + } +} diff --git a/database/migrations/2026_10_19_000010_invio_link_assegnazioni.php b/database/migrations/2026_10_19_000010_invio_link_assegnazioni.php new file mode 100644 index 0000000..432e142 --- /dev/null +++ b/database/migrations/2026_10_19_000010_invio_link_assegnazioni.php @@ -0,0 +1,35 @@ +boolean('link_da_inviare')->default(false)->after('link_sent'); + $table->string('link_canale', 16)->nullable()->after('link_da_inviare'); + }); + } + + public function down(): void + { + Schema::table('assegnazioni', function (Blueprint $table) { + $table->dropColumn(['link_da_inviare', 'link_canale']); + }); + } +}; diff --git a/resources/css/app.css b/resources/css/app.css index 60cc2b4..0ff81fa 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -598,6 +598,14 @@ html { scroll-behavior: smooth; } background: var(--tm-accento-tenue); box-shadow: var(--tm-ombra-2); } +/* Lo stesso riquadro come finestra sopra la pagina (invio del link dopo + l'assegnazione): sfondo pieno, perché sotto c'è il velo scuro. */ +.tm-da-inviare-finestra { + width: 100%; + max-width: 34rem; + margin-bottom: 0; + background: var(--tm-superficie, #fff); +} .tm-da-inviare-titolo { margin: 0; font-size: 16px; diff --git a/resources/js/piantine.js b/resources/js/piantine.js index 18060fc..00c6144 100644 --- a/resources/js/piantine.js +++ b/resources/js/piantine.js @@ -312,6 +312,9 @@ function initPiantine() { } else { mappa.setView([41.9, 12.5], 6); // Italia: nessun dato ancora caricato } + + // Arrivati da "Nuovo territorio": si comincia subito a disegnare. + if (componente().creaTerritorio) avviaDisegno(false); } mappa.setView([41.9, 12.5], 6); diff --git a/resources/views/livewire/assegnazioni/assegna.blade.php b/resources/views/livewire/assegnazioni/assegna.blade.php index a0fc2d0..6fad26d 100644 --- a/resources/views/livewire/assegnazioni/assegna.blade.php +++ b/resources/views/livewire/assegnazioni/assegna.blade.php @@ -8,68 +8,107 @@

Questa funzione consente un'assegnazione arbitraria, indipendente dalle tre schede della Home.

- {{-- Appena assegnato: il link da mandare al proclamatore, in evidenza, e - la scelta "inviato / non ancora". Resta finché non si sceglie - (Assegna::DA_INVIARE in sessione). --}} - @foreach($daInviare as $nuova) + {{-- Invio del link (Assegna::apriInvio): blocca la pagina finché non si + risponde "inviato / non ancora". Se si chiude la scheda prima, + l'assegnazione resta segnalata con "link ancora da inviare". --}} + @if($invio) @php - $link = $nuova->shortPdfUrl(); - $messaggio = $link - ? "Ciao {$nuova->proclamatore->nome}, ti è stato assegnato il territorio N° {$nuova->territorio->numero}. Ecco la piantina: {$link}" - : null; - $whatsapp = $messaggio ? $nuova->proclamatore->whatsappUrl($messaggio) : null; + $link = $invio->shortPdfUrl(); + $messaggio = $invio->messaggioLink(); + $whatsapp = $messaggio ? $invio->proclamatore?->whatsappUrl($messaggio) : null; + $email = trim((string) $invio->proclamatore?->email); @endphp -
-

- Territorio N° {{ $nuova->territorio->numero }} assegnato a {{ $nuova->proclamatore->nome_completo }} -

+
+ + @endif
@@ -203,6 +242,15 @@
+ {{-- Procedura di invio interrotta, o risposto "non ancora" --}} + @if($a->link_in_sospeso) +
+ + Link ancora da inviare + Invia link +
+ @endif + {{-- Avviso link scaduto (solo per non loggati) --}} @if($linkScaduto)
diff --git a/resources/views/livewire/territori/territorio-index.blade.php b/resources/views/livewire/territori/territorio-index.blade.php index c599506..01121a6 100644 --- a/resources/views/livewire/territori/territorio-index.blade.php +++ b/resources/views/livewire/territori/territorio-index.blade.php @@ -6,7 +6,11 @@
@can('territori.manage')
- + {{-- Un territorio nuovo nasce dal designer delle piantine: si + disegna il confine e lo si crea da lì (PiantineEditor, ?nuovo=1). + L'editor è dell'amministratore: chi ha solo territori.manage + resta sul form. --}} + Nuovo territorio diff --git a/resources/views/livewire/territori/territorio-show.blade.php b/resources/views/livewire/territori/territorio-show.blade.php index 75ddfbb..e8eb24f 100644 --- a/resources/views/livewire/territori/territorio-show.blade.php +++ b/resources/views/livewire/territori/territorio-show.blade.php @@ -36,6 +36,16 @@
+ {{-- L'invio del link dopo l'assegnazione non si è concluso --}} + @if($territorio->assegnazioneCorrente?->link_in_sospeso) +
+ Il link della piantina è ancora da inviare a {{ $territorio->assegnazioneCorrente->proclamatore?->nome_completo }}. + @can('territori.assign') + Invia link + @endcan +
+ @endif + {{-- Info card --}}
diff --git a/tests/Feature/ContattiProclamatoriTest.php b/tests/Feature/ContattiProclamatoriTest.php index 0dc4f11..6b53383 100644 --- a/tests/Feature/ContattiProclamatoriTest.php +++ b/tests/Feature/ContattiProclamatoriTest.php @@ -11,9 +11,11 @@ use App\Models\Proclamatore; use App\Models\Setting; use App\Models\Territorio; use App\Models\User; +use App\Notifications\LinkTerritorio; use Database\Seeders\RolesAndPermissionsSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Storage; use Livewire\Livewire; use PHPUnit\Framework\Attributes\DataProvider; @@ -154,57 +156,176 @@ class ContattiProclamatoriTest extends TestCase return Territorio::create(['numero' => $numero, 'attivo' => true, 'pdf_path' => "territori-pdf/t{$numero}.pdf"]); } - public function test_dopo_l_assegnazione_compare_il_link_da_mandare_su_whatsapp(): void + public function test_dopo_l_assegnazione_non_si_va_avanti_ma_si_sceglie_il_canale(): void { $mario = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true, 'telefono' => '333 1234567']); - $territorio = $this->territorioConPdf(); - $this->assegna($mario, $territorio) + $pagina = $this->assegna($mario, $this->territorioConPdf()) ->assertHasNoErrors() - ->assertRedirect(route('assegnazioni.assegna')); + ->assertNoRedirect(); $assegnazione = Assegnazione::sole(); - $this->assertSame([$assegnazione->id], session(Assegna::DA_INVIARE)); + $this->assertTrue($assegnazione->link_da_inviare); + $this->assertFalse($assegnazione->link_sent); - $link = $assegnazione->fresh()->shortPdfUrl(); - Livewire::actingAs($this->admin)->test(Assegna::class) + $link = $assegnazione->shortPdfUrl(); + $pagina->assertSet('invioId', $assegnazione->id) ->assertSee('Territorio N° 7 assegnato a Rossi Mario') + ->assertSee('Come vuoi mandare il link della piantina?') ->assertSee($link) ->assertSee('https://wa.me/393331234567?text=', false) ->assertSee(rawurlencode($link), false) - ->assertSee('Il link è stato inviato?'); + ->assertSee('Hai inviato il link?'); } - public function test_la_scelta_registra_l_invio_e_toglie_il_riquadro(): void + public function test_si_dice_inviato_solo_dopo_aver_scelto_un_canale(): void { $mario = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true, 'telefono' => '3331234567']); - $anna = Proclamatore::create(['nome' => 'Anna', 'cognome' => 'Verdi', 'attivo' => true]); - $this->assegna($mario, $this->territorioConPdf('7')); - $this->assegna($anna, $this->territorioConPdf('8')); - [$prima, $seconda] = Assegnazione::orderBy('id')->get()->all(); + $this->assegna($mario, $this->territorioConPdf()) + ->call('concludiInvio', true) + ->assertNoRedirect() + ->assertSet('erroreInvio', 'Scegli prima come mandare il link.') + ->call('canale', 'whatsapp') + ->call('concludiInvio', true) + ->assertRedirect(route('assegnazioni.assegna')); - Livewire::actingAs($this->admin)->test(Assegna::class) - ->assertSee('Territorio N° 7 assegnato') - ->assertSee('Territorio N° 8 assegnato') - ->call('esitoInvio', $prima->id, true) - ->assertDontSee('Territorio N° 7 assegnato') - ->assertSee('Territorio N° 8 assegnato') - ->call('esitoInvio', $seconda->id, false) - ->assertDontSee('Territorio N° 8 assegnato'); + $assegnazione = Assegnazione::sole(); + $this->assertTrue($assegnazione->link_sent); + $this->assertFalse($assegnazione->link_da_inviare); + $this->assertSame('whatsapp', $assegnazione->link_canale); - $this->assertTrue((bool) $prima->fresh()->link_sent); - $this->assertFalse((bool) $seconda->fresh()->link_sent); - $this->assertSame([], session(Assegna::DA_INVIARE)); + $log = \Spatie\Activitylog\Models\Activity::where('description', 'link_inviato')->sole(); + $this->assertSame('whatsapp', $log->properties['canale']); + $this->assertSame($mario->id, $log->properties['proclamatore_id']); + $this->assertStringNotContainsString('333', json_encode($log->properties)); } - public function test_senza_telefono_il_riquadro_lo_dice(): void + public function test_senza_contatti_resta_solo_copia_link(): void { $anna = Proclamatore::create(['nome' => 'Anna', 'cognome' => 'Verdi', 'attivo' => true]); - $this->assegna($anna, $this->territorioConPdf()); - Livewire::actingAs($this->admin)->test(Assegna::class) - ->assertSee('non ha un numero di telefono') - ->assertDontSee('wa.me', false); + $this->assegna($anna, $this->territorioConPdf()) + ->assertSee('Copia link') + ->assertSee('nel profilo non c\'è un numero di telefono') + ->assertSee('nel profilo non c\'è un indirizzo email') + ->assertDontSee('wa.me', false) + // WhatsApp senza numero non si può segnare come canale usato. + ->call('canale', 'whatsapp') + ->assertSet('canaleUsato', null) + ->call('canale', 'copia') + ->assertSet('canaleUsato', 'copia'); } + + public function test_l_email_parte_solo_con_indirizzo_e_posta_configurata(): void + { + Notification::fake(); + $anna = Proclamatore::create(['nome' => 'Anna', 'cognome' => 'Verdi', 'attivo' => true, 'email' => 'anna@example.test']); + + // Posta non configurata: il canale c'è ma è spento, e non parte nulla. + $pagina = $this->assegna($anna, $this->territorioConPdf()) + ->assertSee('il server di posta non è configurato') + ->call('inviaEmail') + ->assertSet('canaleUsato', null); + Notification::assertNothingSent(); + + // Posta configurata dall'app: l'email parte, subito e non in coda. + Setting::instance()->update(['mail_host' => 'smtp.example.test', 'mail_port' => 587, 'mail_from_address' => 'noreply@example.test']); + $pagina->call('inviaEmail')->assertSet('canaleUsato', 'email'); + + Notification::assertSentOnDemand(LinkTerritorio::class, function ($notifica, $canali, $destinatario) { + return $destinatario->routes['mail'] === 'anna@example.test'; + }); + } + + public function test_se_non_si_conclude_l_invio_l_assegnazione_lo_segnala(): void + { + $mario = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true]); + $this->assegna($mario, $this->territorioConPdf()); + $assegnazione = Assegnazione::sole(); + + // La pagina viene chiusa senza rispondere: l'invio è perso, l'avviso resta. + Livewire::actingAs($this->admin)->test(Assegna::class) + ->assertSet('invioId', null) + ->assertSee('Link ancora da inviare') + ->call('apriInvio', $assegnazione->id) + ->assertSee('Hai inviato il link?') + ->call('concludiInvio', false) + ->assertRedirect(route('assegnazioni.assegna')); + + // "No, non ancora": la procedura è finita, ma il link resta da inviare. + $this->assertTrue($assegnazione->fresh()->link_in_sospeso); + $this->get(route('territori.show', $assegnazione->territorio_id)) + ->assertSee('Il link della piantina è ancora da inviare'); + + // Segnato come inviato dall'elenco: l'avviso sparisce. + Livewire::actingAs($this->admin)->test(Assegna::class) + ->call('toggleLinkSent', $assegnazione->id) + ->assertDontSee('Link ancora da inviare'); + $this->assertFalse($assegnazione->fresh()->link_in_sospeso); + } + + public function test_dalla_scheda_territorio_si_riprende_l_invio(): void + { + $mario = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true]); + $this->assegna($mario, $this->territorioConPdf()); + $assegnazione = Assegnazione::sole(); + + $this->actingAs($this->admin) + ->get(route('assegnazioni.assegna', ['invia' => $assegnazione->id])) + ->assertOk() + ->assertSee('Hai inviato il link?'); + } + + public function test_senza_pdf_non_c_e_un_link_da_inviare(): void + { + $anna = Proclamatore::create(['nome' => 'Anna', 'cognome' => 'Verdi', 'attivo' => true]); + $territorio = Territorio::create(['numero' => '9', 'attivo' => true]); + + $this->assegna($anna, $territorio) + ->assertSee('non ha un PDF') + ->call('concludiInvio', false); + + $this->assertFalse(Assegnazione::sole()->link_in_sospeso); + } + + public function test_le_assegnazioni_gia_esistenti_non_vengono_segnalate(): void + { + $mario = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true]); + $territorio = $this->territorioConPdf(); + $vecchia = Assegnazione::create([ + 'territorio_id' => $territorio->id, + 'proclamatore_id' => $mario->id, + 'anno_teocratico_id' => AnnoTeocratico::sole()->id, + 'assigned_at' => now(), + 'created_by' => $this->admin->id, + ]); + + $this->assertFalse($vecchia->fresh()->link_in_sospeso); + } + + public function test_l_xml_porta_con_se_lo_stato_dell_invio(): void + { + $this->actingAs($this->admin); + $mario = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true]); + $this->assegna($mario, $this->territorioConPdf())->call('canale', 'copia')->call('concludiInvio', true); + + $xml = (new XmlExchange())->currentXmlBackup(); + $this->assertArrayNotHasKey('error', (new XmlExchange())->importXmlFromContent($xml)); + + $assegnazione = Assegnazione::sole(); + $this->assertTrue($assegnazione->link_sent); + $this->assertFalse($assegnazione->link_da_inviare); + $this->assertSame('copia', $assegnazione->link_canale); + } + + // ─── Nuovo territorio dal designer ────────────────────────── + + public function test_nuovo_territorio_porta_al_designer_delle_piantine(): void + { + $this->actingAs($this->admin) + ->get(route('territori.index')) + ->assertSee(route('piantine.editor', ['nuovo' => 1]), false); + } + } diff --git a/tests/Feature/PiantineEditorTest.php b/tests/Feature/PiantineEditorTest.php index b5dccd8..f012e02 100644 --- a/tests/Feature/PiantineEditorTest.php +++ b/tests/Feature/PiantineEditorTest.php @@ -62,6 +62,29 @@ class PiantineEditorTest extends TestCase $this->actingAs($this->admin())->get(route('piantine.editor'))->assertOk(); } + public function test_da_nuovo_territorio_la_prima_piantina_disegnata_crea_un_territorio(): void + { + Livewire::withQueryParams(['nuovo' => 1]) + ->actingAs($this->admin()) + ->test(PiantineEditor::class) + ->assertSet('creaTerritorio', true) + ->dispatch('geometria-modificata', geometry: self::GEOMETRIA, nuova: true) + ->assertSet('territorioScelto', 'nuovo') + ->assertSet('creaTerritorio', false) + // Solo la prima: la seconda piantina disegnata è una bozza qualunque. + ->dispatch('geometria-modificata', geometry: self::ALTRA_GEOMETRIA, nuova: true) + ->assertSet('territorioScelto', ''); + } + + public function test_senza_nuovo_l_editor_non_prepara_un_territorio(): void + { + Livewire::actingAs($this->admin()) + ->test(PiantineEditor::class) + ->assertSet('creaTerritorio', false) + ->dispatch('geometria-modificata', geometry: self::GEOMETRIA, nuova: true) + ->assertSet('territorioScelto', ''); + } + public function test_la_pagina_si_apre_su_una_piantina_esistente(): void { $mappa = TerritorioMappa::create(['nome' => 'Zona porto', 'geojson' => json_encode(self::GEOMETRIA)]);