++ invio del link dopo l'assegnazione; nuovo territorio dal designer

- Dopo l'assegnazione si apre una finestra con i canali (copia link
  sempre; WhatsApp ed email solo se il proclamatore ha numero /
  indirizzo, l'email solo con la posta configurata) e la domanda "Hai
  inviato il link?". Si va avanti solo dopo la risposta.
- L'email la manda l'app con notifyNow, mai in coda: l'indirizzo è
  cifrato nel database e non deve finire in chiaro nei job.
- Procedura interrotta o "non ancora": assegnazioni.link_da_inviare
  resta vero e "Link ancora da inviare" compare nell'elenco Assegna e
  nella scheda territorio, con "Invia link" per riprendere.
- Colonne link_da_inviare e link_canale (anche nell'XML); log con
  canale e proclamatore_id, senza dati personali.
- "Nuovo territorio" apre il designer delle piantine già in disegno, con
  la prima piantina pronta a creare il territorio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Francesco Picone
2026-10-19 14:36:44 +02:00
parent f8911afe64
commit f8539b58bd
14 changed files with 619 additions and 120 deletions

View File

@@ -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);
}
}

View File

@@ -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)]);