++ 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:
43
HANDOFF.md
43
HANDOFF.md
@@ -106,11 +106,9 @@ Richieste dell'utente, con le sue scelte:
|
||||
nell'export/import XML, cancellati dall'anonimizzazione. Il numero apre
|
||||
WhatsApp (`<x-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=<id>`).
|
||||
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 |
|
||||
|
||||
@@ -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=<id>: 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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────
|
||||
|
||||
45
app/Notifications/LinkTerritorio.php
Normal file
45
app/Notifications/LinkTerritorio.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
/**
|
||||
* Il link della piantina, mandato per email al proclamatore a cui è stato
|
||||
* assegnato il territorio.
|
||||
*
|
||||
* Si invia con notifyNow(), mai in coda: in coda il job salverebbe in chiaro
|
||||
* (Redis, failed_jobs) l'indirizzo che nel database è cifrato.
|
||||
*/
|
||||
class LinkTerritorio extends Notification
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Assegnazione $assegnazione,
|
||||
private readonly string $link,
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$numero = $this->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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Invio del link dopo l'assegnazione.
|
||||
*
|
||||
* - link_da_inviare: l'assegnazione aspetta che il link venga mandato al
|
||||
* proclamatore. Nasce vero con l'assegnazione fatta dalla pagina Assegna e
|
||||
* torna falso quando il link risulta inviato. Le assegnazioni già esistenti
|
||||
* restano false: non si segnalano a posteriori centinaia di link di cui non
|
||||
* si sa niente.
|
||||
* - link_canale: come è stato mandato l'ultima volta (copia, whatsapp, email).
|
||||
* Non è un dato personale: email e telefono restano solo, cifrati, nel
|
||||
* proclamatore.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('assegnazioni', function (Blueprint $table) {
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,68 +8,107 @@
|
||||
<p class="text-sm text-gray-500 mt-1">Questa funzione consente un'assegnazione arbitraria, indipendente dalle tre schede della Home.</p>
|
||||
</div>
|
||||
|
||||
{{-- 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
|
||||
<section class="tm-da-inviare" wire:key="da-inviare-{{ $nuova->id }}" aria-labelledby="da-inviare-{{ $nuova->id }}">
|
||||
<h2 id="da-inviare-{{ $nuova->id }}" class="tm-da-inviare-titolo">
|
||||
Territorio N° {{ $nuova->territorio->numero }} assegnato a {{ $nuova->proclamatore->nome_completo }}
|
||||
</h2>
|
||||
<div class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-gray-900/50 p-4 sm:items-center"
|
||||
role="dialog" aria-modal="true" aria-labelledby="invio-titolo" wire:key="invio-{{ $invio->id }}">
|
||||
<section class="tm-da-inviare tm-da-inviare-finestra">
|
||||
<h2 id="invio-titolo" class="tm-da-inviare-titolo">
|
||||
Territorio N° {{ $invio->territorio?->numero }} assegnato a {{ $invio->proclamatore?->nome_completo }}
|
||||
</h2>
|
||||
|
||||
@if($link)
|
||||
<div x-data="{ copiato: false }" class="tm-da-inviare-link">
|
||||
<code x-ref="link" x-on:click="window.getSelection().selectAllChildren($el)">{{ $link }}</code>
|
||||
<x-btn variante="secondary" tono="tenue" dimensione="sm"
|
||||
x-on:click="navigator.clipboard.writeText($refs.link.textContent).then(() => { copiato = true; setTimeout(() => copiato = false, 2000) })">
|
||||
<span x-show="! copiato">Copia link</span>
|
||||
<span x-show="copiato" x-cloak>Copiato</span>
|
||||
</x-btn>
|
||||
</div>
|
||||
@if($link)
|
||||
<p class="text-sm text-gray-700 mb-2">Come vuoi mandare il link della piantina?</p>
|
||||
|
||||
<div class="tm-da-inviare-azioni">
|
||||
@if($whatsapp)
|
||||
<a href="{{ $whatsapp }}" target="_blank" rel="noopener" class="tm-whatsapp tm-whatsapp-grande">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
|
||||
<span>Invia su WhatsApp</span>
|
||||
<span class="tm-whatsapp-numero">{{ $nuova->proclamatore->telefono }}</span>
|
||||
</a>
|
||||
@else
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ $nuova->proclamatore->nome_completo }} non ha un numero di telefono: copia il link e mandalo come preferisci.
|
||||
@can('settings.manage')
|
||||
<a href="{{ route('proclamatori.edit', $nuova->proclamatore) }}" class="text-indigo-700 underline">Aggiungi il numero</a>
|
||||
@endcan
|
||||
</p>
|
||||
<div class="tm-da-inviare-link">
|
||||
<code id="invio-link" x-data x-on:click="window.getSelection().selectAllChildren($el)">{{ $link }}</code>
|
||||
</div>
|
||||
|
||||
<div class="tm-da-inviare-azioni" x-data="{ copiato: false }">
|
||||
{{-- Copia link: sempre disponibile. Il link si legge dal <code> qui
|
||||
sopra: dentro <x-btn> le direttive Blade (@js) non si compilano. --}}
|
||||
<x-btn :variante="$canaleUsato === 'copia' ? 'primary' : 'secondary'"
|
||||
x-on:click="navigator.clipboard?.writeText(document.getElementById('invio-link').textContent.trim()).then(() => { copiato = true; setTimeout(() => copiato = false, 2000) }).catch(() => {}); $wire.canale('copia')">
|
||||
<span x-show="! copiato">Copia link</span>
|
||||
<span x-show="copiato" x-cloak>Copiato</span>
|
||||
</x-btn>
|
||||
|
||||
{{-- WhatsApp: solo con un numero di telefono nel profilo --}}
|
||||
@if($whatsapp)
|
||||
<a href="{{ $whatsapp }}" target="_blank" rel="noopener" wire:click="canale('whatsapp')"
|
||||
class="tm-whatsapp tm-whatsapp-grande">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
|
||||
<span>WhatsApp</span>
|
||||
</a>
|
||||
@else
|
||||
<x-btn variante="secondary" :disabled="true" title="Nel profilo del proclamatore non c'è un numero di telefono">WhatsApp</x-btn>
|
||||
@endif
|
||||
|
||||
{{-- Email: solo con un indirizzo nel profilo e la posta configurata --}}
|
||||
@if($email !== '' && $postaAttiva)
|
||||
<x-btn :variante="$canaleUsato === 'email' ? 'primary' : 'secondary'"
|
||||
wire:click="inviaEmail" wire:loading.attr="disabled" wire:target="inviaEmail">
|
||||
<span wire:loading.remove wire:target="inviaEmail">Email</span>
|
||||
<span wire:loading wire:target="inviaEmail">Invio…</span>
|
||||
</x-btn>
|
||||
@else
|
||||
<x-btn variante="secondary" :disabled="true"
|
||||
:title="$email === '' ? 'Nel profilo del proclamatore non c\'è un indirizzo email' : 'Il server di posta non è configurato'">Email</x-btn>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<ul class="mt-1 text-xs text-gray-500 space-y-0.5">
|
||||
@unless($whatsapp)
|
||||
<li>WhatsApp non disponibile: nel profilo non c'è un numero di telefono.</li>
|
||||
@endunless
|
||||
@if($email === '')
|
||||
<li>Email non disponibile: nel profilo non c'è un indirizzo email.</li>
|
||||
@elseif(! $postaAttiva)
|
||||
<li>Email non disponibile: il server di posta non è configurato.</li>
|
||||
@endif
|
||||
@if((! $whatsapp || $email === '') && auth()->user()->can('settings.manage') && $invio->proclamatore)
|
||||
<li><a href="{{ route('proclamatori.edit', $invio->proclamatore) }}" class="text-indigo-700 underline">Aggiungi i contatti al proclamatore</a></li>
|
||||
@endif
|
||||
</ul>
|
||||
|
||||
@if($canaleUsato === 'email')
|
||||
<p class="mt-2 text-sm text-green-700">Email inviata.</p>
|
||||
@elseif($canaleUsato)
|
||||
<p class="mt-2 text-sm text-gray-700">Canale scelto: {{ \App\Models\Assegnazione::CANALI_LINK[$canaleUsato] }}.</p>
|
||||
@endif
|
||||
@if($erroreInvio)
|
||||
<p class="mt-2 text-sm text-red-600" role="alert">{{ $erroreInvio }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="tm-da-inviare-scelta">
|
||||
<p>Il link è stato inviato?</p>
|
||||
<div>
|
||||
<x-btn variante="primary" wire:click="esitoInvio({{ $nuova->id }}, true)">Sì, inviato</x-btn>
|
||||
<x-btn variante="secondary" wire:click="esitoInvio({{ $nuova->id }}, false)">Non ancora</x-btn>
|
||||
<div class="tm-da-inviare-scelta">
|
||||
<p>Hai inviato il link?</p>
|
||||
<div>
|
||||
<x-btn variante="primary" wire:click="concludiInvio(true)" :disabled="$canaleUsato === null"
|
||||
:title="$canaleUsato === null ? 'Scegli prima come mandare il link' : null">Sì, inviato</x-btn>
|
||||
<x-btn variante="secondary" wire:click="concludiInvio(false)">No, non ancora</x-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-sm text-gray-600">
|
||||
Il territorio non ha un PDF: non c'è un link da mandare. Caricalo dalla scheda del territorio.
|
||||
</p>
|
||||
<div class="tm-da-inviare-scelta">
|
||||
<div>
|
||||
<x-btn variante="secondary" wire:click="esitoInvio({{ $nuova->id }}, false)">Ho capito</x-btn>
|
||||
@else
|
||||
<p class="text-sm text-gray-600">
|
||||
Il territorio non ha un PDF: non c'è un link da mandare. Caricalo dalla scheda del territorio.
|
||||
</p>
|
||||
<div class="tm-da-inviare-scelta">
|
||||
<div>
|
||||
<x-btn variante="secondary" wire:click="concludiInvio(false)">Ho capito</x-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
@endforeach
|
||||
@endif
|
||||
</section>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div x-data="{ formOpen: true }">
|
||||
|
||||
@@ -203,6 +242,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Procedura di invio interrotta, o risposto "non ancora" --}}
|
||||
@if($a->link_in_sospeso)
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2 rounded-md px-3 py-1.5 text-xs font-semibold bg-amber-50 border border-amber-200 text-amber-800" role="status">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>
|
||||
Link ancora da inviare
|
||||
<x-btn variante="secondary" tono="tenue" dimensione="sm" wire:click="apriInvio({{ $a->id }})">Invia link</x-btn>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Avviso link scaduto (solo per non loggati) --}}
|
||||
@if($linkScaduto)
|
||||
<div class="mt-3 inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-semibold bg-red-50 border border-red-200 text-red-700">
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
</div>
|
||||
@can('territori.manage')
|
||||
<div class="mt-4 sm:mt-0 flex gap-2">
|
||||
<x-btn variante="primary" :href="route('territori.create')">
|
||||
{{-- 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. --}}
|
||||
<x-btn variante="primary" :href="auth()->user()->can('settings.manage') ? route('piantine.editor', ['nuovo' => 1]) : route('territori.create')">
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||
Nuovo territorio
|
||||
</x-btn>
|
||||
|
||||
@@ -36,6 +36,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- L'invio del link dopo l'assegnazione non si è concluso --}}
|
||||
@if($territorio->assegnazioneCorrente?->link_in_sospeso)
|
||||
<div class="mb-6 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800" role="status">
|
||||
<span class="font-semibold">Il link della piantina è ancora da inviare a {{ $territorio->assegnazioneCorrente->proclamatore?->nome_completo }}.</span>
|
||||
@can('territori.assign')
|
||||
<x-btn variante="secondary" dimensione="sm" :href="route('assegnazioni.assegna', ['invia' => $territorio->assegnazioneCorrente->id])">Invia link</x-btn>
|
||||
@endcan
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Info card --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)]);
|
||||
|
||||
Reference in New Issue
Block a user