++ dopo un'operazione si torna alla pagina di partenza, con i suoi filtri

- Gli elenchi tengono ricerca, filtri e ordinamento nell'indirizzo
  (#[Url]); la pagina c'era già.
- I link verso schede e operazioni (data-ritorno) portano ?da= con la
  pagina di partenza, o il da già ricevuto: si risale all'origine
  (elenco -> scheda -> Rientra -> elenco filtrato).
- Ritorno::valida accetta solo percorsi interni: niente rimandi esterni.
- Trait RicordaRitorno: redirect dopo il salvataggio, Annulla e
  "Torna indietro" usano la pagina di partenza. Senza da, tutto come prima.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Francesco Picone
2026-10-19 15:04:42 +02:00
parent 0c530b65b3
commit bc465f53f5
32 changed files with 451 additions and 58 deletions

View File

@@ -127,6 +127,29 @@ email di prova e di conferma arrivate, link aperto senza login, account
sbloccato; riquadro dopo l'assegnazione con WhatsApp e scelta. Configurazione sbloccato; riquadro dopo l'assegnazione con WhatsApp e scelta. Configurazione
e dati di prova rimossi dopo. e dati di prova rimossi dopo.
### Si torna da dove si è partiti (19 ottobre 2026)
Regola dell'utente: dopo un'operazione — confermata **o annullata** — si torna
alla pagina da cui il percorso era partito, con i suoi filtri, ricerca,
ordinamento e pagina. Elenco filtrato → scheda → Rientra → conferma riporta
all'**elenco filtrato**, non alla scheda (scelta esplicita).
- Gli elenchi (territori, proclamatori, registro) tengono ricerca, filtri e
ordinamento nell'indirizzo con `#[Url]`; la pagina c'era già (paginazione).
- I link verso schede e operazioni hanno `data-ritorno`: `resources/js/app.js`
gli aggiunge `?da=<percorso>` al momento dell'uso (pointerdown/click/menu
contestuale/focus, prima di wire:navigate). Se la pagina corrente ha già un
`da`, passa quello: si risale sempre all'origine.
- `App\Support\Ritorno::valida()` accetta solo percorsi interni (niente
`https://…`, `//…`, `\`, caratteri di controllo, `/livewire/…`): il
parametro non può diventare un rimando verso l'esterno.
- Le pagine di scheda e operazione usano il trait `RicordaRitorno`
(`$ritorno`, `destinazione()`): redirect dopo il salvataggio, "Annulla" e
"Torna…" (che con un ritorno diventa "Torna indietro").
- Aperte senza `da` (menu laterale, link salvato) tutto resta com'era.
- Link nuovi verso una scheda o un'operazione: metterci `data-ritorno`;
pagine nuove di operazione: usare `RicordaRitorno`.
### Invio del link dopo l'assegnazione, nuovo territorio dal designer (19 ottobre 2026) ### Invio del link dopo l'assegnazione, nuovo territorio dal designer (19 ottobre 2026)
Richieste dell'utente: Richieste dell'utente:

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Assegnazioni; namespace App\Livewire\Assegnazioni;
use App\Livewire\Concerns\RicordaRitorno;
use Livewire\Component; use Livewire\Component;
use Livewire\Attributes\Computed; use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
@@ -16,6 +17,8 @@ use Illuminate\Support\Facades\Notification;
class Assegna extends Component class Assegna extends Component
{ {
use RicordaRitorno;
// ─── Invio del link ───────────────────────────────────────── // ─── Invio del link ─────────────────────────────────────────
// //
// Dopo l'assegnazione non si torna subito alla pagina: si sceglie il // Dopo l'assegnazione non si torna subito alla pagina: si sceglie il
@@ -234,7 +237,7 @@ class Assegna extends Component
session()->flash('success', $inviato session()->flash('success', $inviato
? "Territorio {$numero} assegnato a {$nome}: link inviato." ? "Territorio {$numero} assegnato a {$nome}: link inviato."
: "Territorio {$numero} assegnato a {$nome}. Il link è ancora da inviare: lo trovi segnalato nell'elenco."); : "Territorio {$numero} assegnato a {$nome}. Il link è ancora da inviare: lo trovi segnalato nell'elenco.");
return $this->redirect(route('assegnazioni.assegna'), navigate: true); return $this->redirect($this->destinazione(route('assegnazioni.assegna')), navigate: true);
} }
private function assegnazioneInInvio(): ?Assegnazione private function assegnazioneInInvio(): ?Assegnazione

View File

@@ -3,12 +3,15 @@
namespace App\Livewire\Assegnazioni; namespace App\Livewire\Assegnazioni;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use App\Livewire\Concerns\RicordaRitorno;
use Livewire\Component; use Livewire\Component;
use App\Models\Assegnazione; use App\Models\Assegnazione;
use App\Models\Campagna; use App\Models\Campagna;
class Rientra extends Component class Rientra extends Component
{ {
use RicordaRitorno;
public Assegnazione $assegnazione; public Assegnazione $assegnazione;
public string $returned_at = ''; public string $returned_at = '';
public bool $showCampaignPrompt = false; public bool $showCampaignPrompt = false;
@@ -109,7 +112,7 @@ class Rientra extends Component
} }
session()->flash('success', "Territorio {$territorio->numero} rientrato da {$proclamatore->nome_completo} dopo {$this->assegnazione->giorni} giorni."); session()->flash('success', "Territorio {$territorio->numero} rientrato da {$proclamatore->nome_completo} dopo {$this->assegnazione->giorni} giorni.");
return $this->redirect(route('territori.show', $territorio), navigate: true); return $this->redirect($this->destinazione(route('territori.show', $territorio)), navigate: true);
} }
public function render() public function render()

View File

@@ -2,11 +2,14 @@
namespace App\Livewire\Campagne; namespace App\Livewire\Campagne;
use App\Livewire\Concerns\RicordaRitorno;
use Livewire\Component; use Livewire\Component;
use App\Models\Campagna; use App\Models\Campagna;
class CampagnaCreate extends Component class CampagnaCreate extends Component
{ {
use RicordaRitorno;
public string $descrizione = ''; public string $descrizione = '';
public string $start_date = ''; public string $start_date = '';
public string $end_date = ''; public string $end_date = '';
@@ -34,7 +37,7 @@ class CampagnaCreate extends Component
]); ]);
session()->flash('success', "Campagna '{$campagna->descrizione}' creata."); session()->flash('success', "Campagna '{$campagna->descrizione}' creata.");
return $this->redirect(route('campagne.index'), navigate: true); return $this->redirect($this->destinazione(route('campagne.index')), navigate: true);
} }
public function render() public function render()

View File

@@ -2,11 +2,14 @@
namespace App\Livewire\Campagne; namespace App\Livewire\Campagne;
use App\Livewire\Concerns\RicordaRitorno;
use Livewire\Component; use Livewire\Component;
use App\Models\Campagna; use App\Models\Campagna;
class CampagnaEdit extends Component class CampagnaEdit extends Component
{ {
use RicordaRitorno;
public Campagna $campagna; public Campagna $campagna;
public string $descrizione = ''; public string $descrizione = '';
public string $start_date = ''; public string $start_date = '';
@@ -47,7 +50,7 @@ class CampagnaEdit extends Component
]); ]);
session()->flash('success', "Campagna aggiornata."); session()->flash('success', "Campagna aggiornata.");
return $this->redirect(route('campagne.index'), navigate: true); return $this->redirect($this->destinazione(route('campagne.index')), navigate: true);
} }
public function render() public function render()

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Campagne; namespace App\Livewire\Campagne;
use App\Livewire\Concerns\RicordaRitorno;
use Livewire\Component; use Livewire\Component;
use App\Models\Campagna; use App\Models\Campagna;
use App\Models\Assegnazione; use App\Models\Assegnazione;
@@ -9,6 +10,8 @@ use App\Models\Territorio;
class CampagnaShow extends Component class CampagnaShow extends Component
{ {
use RicordaRitorno;
public Campagna $campagna; public Campagna $campagna;
public function mount(Campagna $campagna) public function mount(Campagna $campagna)

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Livewire\Concerns;
use App\Support\Ritorno;
use Livewire\Attributes\Locked;
/**
* Per le pagine che si aprono da un'altra (schede e operazioni): ricorda da
* dove si è partiti (?da=…, vedi App\Support\Ritorno) e ci torna dopo la
* conferma. Annulla e "Torna…" nella vista usano $ritorno allo stesso modo.
*
* Aperta senza `da` (menu laterale, link salvato) la pagina si comporta come
* prima: ogni componente passa la sua destinazione predefinita.
*/
trait RicordaRitorno
{
/** URL della pagina di partenza; null se aperta direttamente. */
#[Locked]
public ?string $ritorno = null;
/** Hook di Livewire: gira al mount di ogni componente che usa il trait. */
public function mountRicordaRitorno(): void
{
$this->ritorno = Ritorno::daRichiesta();
}
/** Dove andare dopo la conferma: la pagina di partenza, o quella predefinita. */
protected function destinazione(string $predefinita): string
{
return $this->ritorno ?? $predefinita;
}
}

View File

@@ -2,11 +2,14 @@
namespace App\Livewire\Proclamatori; namespace App\Livewire\Proclamatori;
use App\Livewire\Concerns\RicordaRitorno;
use Livewire\Component; use Livewire\Component;
use App\Models\Proclamatore; use App\Models\Proclamatore;
class ProclamatoreCreate extends Component class ProclamatoreCreate extends Component
{ {
use RicordaRitorno;
public string $nome = ''; public string $nome = '';
public string $cognome = ''; public string $cognome = '';
public bool $attivo = true; public bool $attivo = true;
@@ -52,7 +55,7 @@ class ProclamatoreCreate extends Component
->log('created'); ->log('created');
session()->flash('success', "Proclamatore {$proclamatore->nome_completo} creato."); session()->flash('success', "Proclamatore {$proclamatore->nome_completo} creato.");
return $this->redirect(route('proclamatori.index'), navigate: true); return $this->redirect($this->destinazione(route('proclamatori.index')), navigate: true);
} }
public function render() public function render()

View File

@@ -2,11 +2,14 @@
namespace App\Livewire\Proclamatori; namespace App\Livewire\Proclamatori;
use App\Livewire\Concerns\RicordaRitorno;
use Livewire\Component; use Livewire\Component;
use App\Models\Proclamatore; use App\Models\Proclamatore;
class ProclamatoreEdit extends Component class ProclamatoreEdit extends Component
{ {
use RicordaRitorno;
public Proclamatore $proclamatore; public Proclamatore $proclamatore;
public string $nome = ''; public string $nome = '';
public string $cognome = ''; public string $cognome = '';
@@ -59,7 +62,7 @@ class ProclamatoreEdit extends Component
]); ]);
session()->flash('success', "Proclamatore {$this->proclamatore->nome_completo} aggiornato."); session()->flash('success', "Proclamatore {$this->proclamatore->nome_completo} aggiornato.");
return $this->redirect(route('proclamatori.index'), navigate: true); return $this->redirect($this->destinazione(route('proclamatori.index')), navigate: true);
} }
public function render() public function render()

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Proclamatori; namespace App\Livewire\Proclamatori;
use App\Livewire\Concerns\Avvisa; use App\Livewire\Concerns\Avvisa;
use Livewire\Attributes\Url;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use App\Models\Proclamatore; use App\Models\Proclamatore;
@@ -12,9 +13,14 @@ class ProclamatoreIndex extends Component
use Avvisa; use Avvisa;
use WithPagination; use WithPagination;
// Ricerca, filtro e ordinamento nell'indirizzo: si ritrovano tornando.
#[Url(except: '')]
public string $search = ''; public string $search = '';
#[Url(except: '')]
public string $filtroStato = ''; public string $filtroStato = '';
#[Url(except: 'cognome')]
public string $sortField = 'cognome'; public string $sortField = 'cognome';
#[Url(except: 'asc')]
public string $sortDirection = 'asc'; public string $sortDirection = 'asc';
protected $queryString = [ protected $queryString = [

View File

@@ -2,12 +2,15 @@
namespace App\Livewire\Proclamatori; namespace App\Livewire\Proclamatori;
use App\Livewire\Concerns\RicordaRitorno;
use Livewire\Component; use Livewire\Component;
use App\Models\Proclamatore; use App\Models\Proclamatore;
use App\Models\Assegnazione; use App\Models\Assegnazione;
class ProclamatoreShow extends Component class ProclamatoreShow extends Component
{ {
use RicordaRitorno;
public Proclamatore $proclamatore; public Proclamatore $proclamatore;
public function mount(Proclamatore $proclamatore) public function mount(Proclamatore $proclamatore)

View File

@@ -2,6 +2,7 @@
namespace App\Livewire; namespace App\Livewire;
use Livewire\Attributes\Url;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use App\Models\Assegnazione; use App\Models\Assegnazione;
@@ -19,12 +20,20 @@ class Registro extends Component
{ {
use WithPagination; use WithPagination;
// Ricerca, filtri e ordinamento nell'indirizzo: si ritrovano tornando.
#[Url(except: '')]
public string $search = ''; public string $search = '';
#[Url(except: '')]
public string $filtroAnno = ''; public string $filtroAnno = '';
#[Url(except: '')]
public string $filtroZona = ''; public string $filtroZona = '';
#[Url(except: '')]
public string $filtroTipologia = ''; public string $filtroTipologia = '';
#[Url(except: '')]
public string $filtroStato = ''; // aperte, chiuse public string $filtroStato = ''; // aperte, chiuse
#[Url(except: 'territorio_numero')]
public string $sortField = 'territorio_numero'; public string $sortField = 'territorio_numero';
#[Url(except: 'asc')]
public string $sortDirection = 'asc'; public string $sortDirection = 'asc';
// ─── Modal create/edit ────────────────────────────────────── // ─── Modal create/edit ──────────────────────────────────────

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Territori; namespace App\Livewire\Territori;
use App\Livewire\Concerns\RicordaRitorno;
use Livewire\Component; use Livewire\Component;
use Livewire\WithFileUploads; use Livewire\WithFileUploads;
use App\Models\Territorio; use App\Models\Territorio;
@@ -11,6 +12,8 @@ use App\Services\TerritorioThumbnailService;
class TerritorioCreate extends Component class TerritorioCreate extends Component
{ {
use RicordaRitorno;
use WithFileUploads; use WithFileUploads;
public string $numero = ''; public string $numero = '';
@@ -64,7 +67,7 @@ class TerritorioCreate extends Component
->log('created'); ->log('created');
session()->flash('success', "Territorio {$territorio->numero} creato."); session()->flash('success', "Territorio {$territorio->numero} creato.");
return redirect()->route('territori.index'); return redirect()->to($this->destinazione(route('territori.index')));
} }
public function render() public function render()

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Territori; namespace App\Livewire\Territori;
use App\Livewire\Concerns\RicordaRitorno;
use Livewire\Component; use Livewire\Component;
use Livewire\WithFileUploads; use Livewire\WithFileUploads;
use App\Models\Territorio; use App\Models\Territorio;
@@ -12,6 +13,8 @@ use Illuminate\Support\Facades\Storage;
class TerritorioEdit extends Component class TerritorioEdit extends Component
{ {
use RicordaRitorno;
use WithFileUploads; use WithFileUploads;
public Territorio $territorio; public Territorio $territorio;
@@ -82,7 +85,7 @@ class TerritorioEdit extends Component
$this->territorio->update($data); $this->territorio->update($data);
session()->flash('success', "Territorio {$this->territorio->numero} aggiornato."); session()->flash('success', "Territorio {$this->territorio->numero} aggiornato.");
return redirect()->route('territori.show', $this->territorio); return redirect()->to($this->destinazione(route('territori.show', $this->territorio)));
} }
public function removePdf() public function removePdf()

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Territori;
use App\Livewire\Concerns\Avvisa; use App\Livewire\Concerns\Avvisa;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Livewire\Attributes\Url;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use App\Models\Territorio; use App\Models\Territorio;
@@ -15,14 +16,25 @@ class TerritorioIndex extends Component
use Avvisa; use Avvisa;
use WithPagination; use WithPagination;
// Ricerca, filtri e ordinamento stanno nell'indirizzo (#[Url]): tornando
// da una scheda o da un'operazione l'elenco si ritrova com'era.
#[Url(except: '')]
public string $search = ''; public string $search = '';
#[Url(except: '')]
public string $filterZona = ''; public string $filterZona = '';
#[Url(except: '')]
public string $filterTipologia = ''; public string $filterTipologia = '';
#[Url(except: '')]
public string $filterStato = ''; public string $filterStato = '';
#[Url(except: '')]
public string $filterPriorita = ''; public string $filterPriorita = '';
#[Url(except: '')]
public string $filterPdf = ''; public string $filterPdf = '';
#[Url(except: '')]
public string $filterContenuti = ''; public string $filterContenuti = '';
#[Url(except: 'numero')]
public string $sortField = 'numero'; public string $sortField = 'numero';
#[Url(except: 'asc')]
public string $sortDirection = 'asc'; public string $sortDirection = 'asc';
protected $queryString = [ protected $queryString = [

View File

@@ -2,12 +2,15 @@
namespace App\Livewire\Territori; namespace App\Livewire\Territori;
use App\Livewire\Concerns\RicordaRitorno;
use Livewire\Component; use Livewire\Component;
use App\Models\Territorio; use App\Models\Territorio;
use App\Models\Assegnazione; use App\Models\Assegnazione;
class TerritorioShow extends Component class TerritorioShow extends Component
{ {
use RicordaRitorno;
public Territorio $territorio; public Territorio $territorio;
public function mount(Territorio $territorio) public function mount(Territorio $territorio)

49
app/Support/Ritorno.php Normal file
View File

@@ -0,0 +1,49 @@
<?php
namespace App\Support;
/**
* La pagina da cui è partito un percorso, per tornarci dopo un'operazione.
*
* I link che aprono una scheda o un'operazione portano `?da=<percorso>`
* (lo aggiunge resources/js/app.js ai link con data-ritorno): il percorso
* della pagina di partenza con i suoi filtri, o il `da` che quella pagina
* aveva già ricevuto, così si risale sempre all'origine. Elenco → scheda →
* Rientra → conferma riporta all'elenco filtrato.
*
* Si accettano solo percorsi interni ("/territori?zona=3"): un indirizzo
* completo o "//altro.sito" farebbe del parametro un rimando verso l'esterno.
*/
final class Ritorno
{
public const PARAM = 'da';
/** L'URL a cui tornare, o null se il valore non è un percorso interno valido. */
public static function valida(mixed $percorso): ?string
{
if (! is_string($percorso) || $percorso === '' || strlen($percorso) > 2000) {
return null;
}
if (! str_starts_with($percorso, '/')
|| str_starts_with($percorso, '//')
|| str_contains($percorso, '\\')
|| preg_match('/[\x00-\x1F\x7F]/', $percorso)) {
return null;
}
// Le richieste interne di Livewire non sono pagine.
$path = (string) parse_url($percorso, PHP_URL_PATH);
if ($path === '' || str_starts_with($path, '/livewire')) {
return null;
}
return url($percorso);
}
/** Il ritorno indicato dalla richiesta corrente (?da=…), se valido. */
public static function daRichiesta(): ?string
{
return self::valida(request()->query(self::PARAM));
}
}

View File

@@ -1 +1,34 @@
import './bootstrap'; import './bootstrap';
// ─── Ritorno alla pagina di partenza ────────────────────────────────────────
//
// I link con data-ritorno (schede e operazioni) portano con sé ?da=<percorso>:
// la pagina di partenza con i suoi filtri, oppure il `da` che la pagina
// corrente aveva già ricevuto, così si risale sempre all'origine del percorso
// (elenco → scheda → Rientra → torna all'elenco). Il server lo valida e lo usa
// dopo conferma o annulla: App\Support\Ritorno, trait RicordaRitorno.
//
// Si calcola al momento dell'uso e non al caricamento: filtri e pagina degli
// elenchi cambiano l'indirizzo (#[Url] di Livewire) senza ricaricare.
function ritornoCorrente() {
const qui = new URL(window.location.href);
return qui.searchParams.get('da') || (qui.pathname + qui.search);
}
function aggiornaLink(link) {
const destinazione = new URL(link.getAttribute('href'), window.location.origin);
if (destinazione.origin !== window.location.origin) return;
destinazione.searchParams.set('da', ritornoCorrente());
link.setAttribute('href', destinazione.pathname + destinazione.search + destinazione.hash);
}
// Fase di cattura: si arriva prima di wire:navigate, che legge l'href al
// pointerdown (precaricamento) e al click. contextmenu e focusin coprono
// "apri in una nuova scheda" e la tastiera.
['pointerdown', 'click', 'contextmenu', 'focusin'].forEach((evento) => {
document.addEventListener(evento, (e) => {
const link = e.target instanceof Element ? e.target.closest('a[data-ritorno][href]') : null;
if (link) aggiornaLink(link);
}, true);
});

View File

@@ -1,8 +1,8 @@
<div> <div>
<div class="mb-6"> <div class="mb-6">
<a href="{{ route('territori.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1"> <a href="{{ $ritorno ?? route('territori.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
Torna ai territori {{ $ritorno ? 'Torna indietro' : 'Torna ai territori' }}
</a> </a>
<h1 class="text-2xl font-bold text-gray-900">Assegnazioni</h1> <h1 class="text-2xl font-bold text-gray-900">Assegnazioni</h1>
<p class="text-sm text-gray-500 mt-1">Questa funzione consente un'assegnazione arbitraria, indipendente dalle tre schede della Home.</p> <p class="text-sm text-gray-500 mt-1">Questa funzione consente un'assegnazione arbitraria, indipendente dalle tre schede della Home.</p>
@@ -75,7 +75,7 @@
<li>Email non disponibile: il server di posta non è configurato.</li> <li>Email non disponibile: il server di posta non è configurato.</li>
@endif @endif
@if((! $whatsapp || $email === '') && auth()->user()->can('settings.manage') && $invio->proclamatore) @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> <li><a data-ritorno href="{{ route('proclamatori.edit', $invio->proclamatore) }}" class="text-indigo-700 underline">Aggiungi i contatti al proclamatore</a></li>
@endif @endif
</ul> </ul>
@@ -185,7 +185,11 @@
<div class="flex flex-col-reverse gap-3 pt-4 sm:flex-row sm:items-center"> <div class="flex flex-col-reverse gap-3 pt-4 sm:flex-row sm:items-center">
<x-btn variante="primary" type="submit">Assegna</x-btn> <x-btn variante="primary" type="submit">Assegna</x-btn>
@if($ritorno)
<x-btn variante="secondary" :href="$ritorno">Annulla</x-btn>
@else
<x-btn variante="secondary" x-on:click="formOpen = false">Annulla</x-btn> <x-btn variante="secondary" x-on:click="formOpen = false">Annulla</x-btn>
@endif
</div> </div>
</form> </form>
</div> </div>
@@ -225,7 +229,7 @@
{{-- Testata riga --}} {{-- Testata riga --}}
<div class="flex flex-wrap items-start justify-between gap-x-6 gap-y-2"> <div class="flex flex-wrap items-start justify-between gap-x-6 gap-y-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="{{ route('territori.show', $a->territorio_id) }}" class="text-indigo-600 hover:underline font-bold text-sm">N° {{ $a->territorio?->numero }}</a> <a data-ritorno href="{{ route('territori.show', $a->territorio_id) }}" class="text-indigo-600 hover:underline font-bold text-sm">N° {{ $a->territorio?->numero }}</a>
@if($a->territorio?->zona?->nome) @if($a->territorio?->zona?->nome)
<span class="text-xs text-gray-400 bg-gray-100 rounded px-1.5 py-0.5">{{ $a->territorio?->zona?->nome }}</span> <span class="text-xs text-gray-400 bg-gray-100 rounded px-1.5 py-0.5">{{ $a->territorio?->zona?->nome }}</span>
@endif @endif

View File

@@ -1,8 +1,8 @@
<div> <div>
<div class="mb-6"> <div class="mb-6">
<a href="{{ route('territori.show', $assegnazione->territorio) }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1"> <a href="{{ $ritorno ?? route('territori.show', $assegnazione->territorio) }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
Torna al territorio {{ $ritorno ? 'Torna indietro' : 'Torna al territorio' }}
</a> </a>
<h1 class="text-2xl font-bold text-gray-900">Rientro Territorio</h1> <h1 class="text-2xl font-bold text-gray-900">Rientro Territorio</h1>
</div> </div>
@@ -62,7 +62,7 @@
<div class="flex flex-wrap items-center gap-3 pt-4"> <div class="flex flex-wrap items-center gap-3 pt-4">
<x-btn variante="primary" type="submit">Registra rientro</x-btn> <x-btn variante="primary" type="submit">Registra rientro</x-btn>
<x-btn variante="secondary" :href="route('territori.show', $assegnazione->territorio)">Annulla</x-btn> <x-btn variante="secondary" :href="$ritorno ?? route('territori.show', $assegnazione->territorio)">Annulla</x-btn>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -1,8 +1,8 @@
<div> <div>
<div class="mb-6"> <div class="mb-6">
<a href="{{ route('campagne.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1"> <a href="{{ $ritorno ?? route('campagne.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
Torna alla lista {{ $ritorno ? 'Torna indietro' : 'Torna alla lista' }}
</a> </a>
<h1 class="text-2xl font-bold text-gray-900">{{ $titolo }}</h1> <h1 class="text-2xl font-bold text-gray-900">{{ $titolo }}</h1>
</div> </div>
@@ -30,7 +30,7 @@
<div class="flex items-center gap-3 pt-4"> <div class="flex items-center gap-3 pt-4">
<x-btn variante="primary" type="submit">{{ $btnLabel }}</x-btn> <x-btn variante="primary" type="submit">{{ $btnLabel }}</x-btn>
<x-btn variante="secondary" :href="route('campagne.index')">Annulla</x-btn> <x-btn variante="secondary" :href="$ritorno ?? route('campagne.index')">Annulla</x-btn>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -2,7 +2,7 @@
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 class="text-2xl font-bold text-gray-900">Campagne</h1> <h1 class="text-2xl font-bold text-gray-900">Campagne</h1>
@can('settings.manage') @can('settings.manage')
<x-btn variante="primary" :href="route('campagne.create')"> <x-btn data-ritorno variante="primary" :href="route('campagne.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> <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>
Nuova campagna Nuova campagna
</x-btn> </x-btn>
@@ -47,13 +47,13 @@
</td> </td>
<td class="px-4 py-3 text-sm text-right cell-actions"> <td class="px-4 py-3 text-sm text-right cell-actions">
<div class="flex flex-wrap items-center justify-end gap-1"> <div class="flex flex-wrap items-center justify-end gap-1">
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona <x-btn data-ritorno variante="secondary" tono="tenue" dimensione="sm" icona
:href="route('campagne.show', $campagna)" :href="route('campagne.show', $campagna)"
title="Dettaglio" aria-label="Dettaglio della campagna {{ $campagna->descrizione }}"> title="Dettaglio" aria-label="Dettaglio della campagna {{ $campagna->descrizione }}">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
</x-btn> </x-btn>
@can('settings.manage') @can('settings.manage')
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona <x-btn data-ritorno variante="secondary" tono="tenue" dimensione="sm" icona
:href="route('campagne.edit', $campagna)" :href="route('campagne.edit', $campagna)"
title="Modifica" aria-label="Modifica la campagna {{ $campagna->descrizione }}"> title="Modifica" aria-label="Modifica la campagna {{ $campagna->descrizione }}">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>

View File

@@ -1,14 +1,14 @@
<div> <div>
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<a href="{{ route('campagne.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1"> <a href="{{ $ritorno ?? route('campagne.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
Torna alla lista {{ $ritorno ? 'Torna indietro' : 'Torna alla lista' }}
</a> </a>
<h1 class="text-2xl font-bold text-gray-900">{{ $campagna->descrizione }}</h1> <h1 class="text-2xl font-bold text-gray-900">{{ $campagna->descrizione }}</h1>
</div> </div>
@can('settings.manage') @can('settings.manage')
<x-btn variante="secondary" :href="route('campagne.edit', $campagna)"> <x-btn data-ritorno variante="secondary" :href="route('campagne.edit', $campagna)">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Modifica Modifica
</x-btn> </x-btn>
@@ -63,7 +63,7 @@
<tbody class="divide-y divide-gray-100"> <tbody class="divide-y divide-gray-100">
@forelse($conteggiate as $a) @forelse($conteggiate as $a)
<tr> <tr>
<td data-label="Territorio" class="px-4 py-2"><a href="{{ route('territori.show', $a->territorio_id) }}" class="text-indigo-600 hover:underline">N° {{ $a->territorio?->numero }}</a></td> <td data-label="Territorio" class="px-4 py-2"><a data-ritorno href="{{ route('territori.show', $a->territorio_id) }}" class="text-indigo-600 hover:underline">N° {{ $a->territorio?->numero }}</a></td>
<td data-label="Proclamatore" class="px-4 py-2">{{ $a->proclamatore?->nome_completo ?? 'N/A' }}</td> <td data-label="Proclamatore" class="px-4 py-2">{{ $a->proclamatore?->nome_completo ?? 'N/A' }}</td>
<td data-label="Assegnato" class="px-4 py-2">{{ $a->assigned_at->format('d/m/Y') }}</td> <td data-label="Assegnato" class="px-4 py-2">{{ $a->assigned_at->format('d/m/Y') }}</td>
<td data-label="Rientrato" class="px-4 py-2">{{ $a->returned_at?->format('d/m/Y') }}</td> <td data-label="Rientrato" class="px-4 py-2">{{ $a->returned_at?->format('d/m/Y') }}</td>
@@ -99,7 +99,7 @@
<tbody class="divide-y divide-gray-100"> <tbody class="divide-y divide-gray-100">
@forelse($inCorso as $a) @forelse($inCorso as $a)
<tr> <tr>
<td data-label="Territorio" class="px-4 py-2"><a href="{{ route('territori.show', $a->territorio_id) }}" class="text-indigo-600 hover:underline">N° {{ $a->territorio?->numero }}</a></td> <td data-label="Territorio" class="px-4 py-2"><a data-ritorno href="{{ route('territori.show', $a->territorio_id) }}" class="text-indigo-600 hover:underline">N° {{ $a->territorio?->numero }}</a></td>
<td data-label="Proclamatore" class="px-4 py-2">{{ $a->proclamatore?->nome_completo ?? 'N/A' }}</td> <td data-label="Proclamatore" class="px-4 py-2">{{ $a->proclamatore?->nome_completo ?? 'N/A' }}</td>
<td data-label="Assegnato" class="px-4 py-2">{{ $a->assigned_at->format('d/m/Y') }}</td> <td data-label="Assegnato" class="px-4 py-2">{{ $a->assigned_at->format('d/m/Y') }}</td>
<td data-label="Giorni" class="px-4 py-2">{{ $a->giorni }}</td> <td data-label="Giorni" class="px-4 py-2">{{ $a->giorni }}</td>

View File

@@ -103,7 +103,7 @@
<li class="px-5 py-3 flex items-center justify-between hover:bg-gray-50 transition-colors"> <li class="px-5 py-3 flex items-center justify-between hover:bg-gray-50 transition-colors">
<div> <div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="{{ route('territori.show', $t) }}" class="text-sm font-semibold text-gray-900 hover:text-indigo-600 transition-colors">N° {{ $t->numero }}</a> <a data-ritorno href="{{ route('territori.show', $t) }}" class="text-sm font-semibold text-gray-900 hover:text-indigo-600 transition-colors">N° {{ $t->numero }}</a>
@if($t->home_is_prioritario) @if($t->home_is_prioritario)
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide badge-pulse" style="background:#fef3c7;color:#92400e">★ Prioritario</span> <span class="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide badge-pulse" style="background:#fef3c7;color:#92400e">★ Prioritario</span>
@endif @endif
@@ -116,7 +116,7 @@
</p> </p>
</div> </div>
@can('territori.assign') @can('territori.assign')
<x-btn variante="primary" tono="tenue" dimensione="sm" :href="route('assegnazioni.assegna', ['territorioId' => $t->id])">Assegna →</x-btn> <x-btn data-ritorno variante="primary" tono="tenue" dimensione="sm" :href="route('assegnazioni.assegna', ['territorioId' => $t->id])">Assegna →</x-btn>
@endcan @endcan
</li> </li>
@empty @empty
@@ -158,7 +158,7 @@
@php($assegnazioneCorrente = $t->assegnazioneCorrente) @php($assegnazioneCorrente = $t->assegnazioneCorrente)
<li class="px-5 py-3 flex items-center justify-between hover:bg-gray-50 transition-colors"> <li class="px-5 py-3 flex items-center justify-between hover:bg-gray-50 transition-colors">
<div> <div>
<a href="{{ route('territori.show', $t) }}" class="text-sm font-semibold text-gray-900 hover:text-indigo-600 transition-colors">N° {{ $t->numero }}</a> <a data-ritorno href="{{ route('territori.show', $t) }}" class="text-sm font-semibold text-gray-900 hover:text-indigo-600 transition-colors">N° {{ $t->numero }}</a>
<p class="text-xs text-gray-500"> <p class="text-xs text-gray-500">
{{ $assegnazioneCorrente?->proclamatore?->nome_completo }} {{ $assegnazioneCorrente?->proclamatore?->nome_completo }}
— <span class="font-medium" style="color:#dc2626">{{ $assegnazioneCorrente?->giorni }} gg</span> — <span class="font-medium" style="color:#dc2626">{{ $assegnazioneCorrente?->giorni }} gg</span>
@@ -166,7 +166,7 @@
</div> </div>
@can('territori.return') @can('territori.return')
@if($assegnazioneCorrente) @if($assegnazioneCorrente)
<x-btn variante="primary" tono="tenue" dimensione="sm" :href="route('assegnazioni.rientra', ['assegnazione' => $assegnazioneCorrente->id])">Rientra →</x-btn> <x-btn data-ritorno variante="primary" tono="tenue" dimensione="sm" :href="route('assegnazioni.rientra', ['assegnazione' => $assegnazioneCorrente->id])">Rientra →</x-btn>
@endif @endif
@endcan @endcan
</li> </li>

View File

@@ -1,8 +1,8 @@
<div> <div>
<div class="mb-6"> <div class="mb-6">
<a href="{{ route('proclamatori.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1"> <a href="{{ $ritorno ?? route('proclamatori.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
Torna alla lista {{ $ritorno ? 'Torna indietro' : 'Torna alla lista' }}
</a> </a>
<h1 class="text-2xl font-bold text-gray-900">{{ $titolo }}</h1> <h1 class="text-2xl font-bold text-gray-900">{{ $titolo }}</h1>
</div> </div>
@@ -44,7 +44,7 @@
<div class="flex items-center gap-3 pt-4"> <div class="flex items-center gap-3 pt-4">
<x-btn variante="primary" type="submit">{{ $btnLabel }}</x-btn> <x-btn variante="primary" type="submit">{{ $btnLabel }}</x-btn>
<x-btn variante="secondary" :href="route('proclamatori.index')">Annulla</x-btn> <x-btn variante="secondary" :href="$ritorno ?? route('proclamatori.index')">Annulla</x-btn>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -6,7 +6,7 @@
link rosso perso in mezzo ai filtri, in un posto diverso rispetto link rosso perso in mezzo ai filtri, in un posto diverso rispetto
all'altra lista che fa esattamente la stessa cosa. --}} all'altra lista che fa esattamente la stessa cosa. --}}
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<x-btn variante="primary" :href="route('proclamatori.create')"> <x-btn data-ritorno variante="primary" :href="route('proclamatori.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> <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 proclamatore Nuovo proclamatore
</x-btn> </x-btn>
@@ -63,13 +63,13 @@
<td class="px-4 py-3 text-sm text-right cell-actions"> <td class="px-4 py-3 text-sm text-right cell-actions">
<div class="flex flex-wrap items-center justify-end gap-1"> <div class="flex flex-wrap items-center justify-end gap-1">
<x-whatsapp :proclamatore="$proclamatore" compatto /> <x-whatsapp :proclamatore="$proclamatore" compatto />
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona <x-btn data-ritorno variante="secondary" tono="tenue" dimensione="sm" icona
:href="route('proclamatori.show', $proclamatore)" :href="route('proclamatori.show', $proclamatore)"
title="Dettaglio" aria-label="Dettaglio del proclamatore"> title="Dettaglio" aria-label="Dettaglio del proclamatore">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
</x-btn> </x-btn>
@can('settings.manage') @can('settings.manage')
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona <x-btn data-ritorno variante="secondary" tono="tenue" dimensione="sm" icona
:href="route('proclamatori.edit', $proclamatore)" :href="route('proclamatori.edit', $proclamatore)"
title="Modifica" aria-label="Modifica il proclamatore"> title="Modifica" aria-label="Modifica il proclamatore">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>

View File

@@ -1,9 +1,9 @@
<div> <div>
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<a href="{{ route('proclamatori.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1"> <a href="{{ $ritorno ?? route('proclamatori.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
Torna alla lista {{ $ritorno ? 'Torna indietro' : 'Torna alla lista' }}
</a> </a>
<h1 class="text-2xl font-bold text-gray-900">{{ $proclamatore->nome_completo }}</h1> <h1 class="text-2xl font-bold text-gray-900">{{ $proclamatore->nome_completo }}</h1>
@if($proclamatore->telefono || $proclamatore->email) @if($proclamatore->telefono || $proclamatore->email)
@@ -16,7 +16,7 @@
@endif @endif
</div> </div>
@can('settings.manage') @can('settings.manage')
<x-btn variante="secondary" :href="route('proclamatori.edit', $proclamatore)"> <x-btn data-ritorno variante="secondary" :href="route('proclamatori.edit', $proclamatore)">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Modifica Modifica
</x-btn> </x-btn>
@@ -66,7 +66,7 @@
@foreach($assegnazioni as $assegnazione) @foreach($assegnazioni as $assegnazione)
<tr> <tr>
<td data-label="Territorio" class="px-3 py-2"> <td data-label="Territorio" class="px-3 py-2">
<a href="{{ route('territori.show', $assegnazione->territorio_id) }}" class="text-indigo-600 hover:underline"> <a data-ritorno href="{{ route('territori.show', $assegnazione->territorio_id) }}" class="text-indigo-600 hover:underline">
{{ $assegnazione->territorio?->numero ?? 'N/A' }} {{ $assegnazione->territorio?->numero ?? 'N/A' }}
</a> </a>
</td> </td>

View File

@@ -82,7 +82,7 @@
class="h-8 w-6 rounded border border-gray-200 object-cover bg-gray-50 flex-none"> class="h-8 w-6 rounded border border-gray-200 object-cover bg-gray-50 flex-none">
@endif @endif
<div class="min-w-0"> <div class="min-w-0">
<a href="{{ route('territori.show', $a->territorio_id) }}" class="text-indigo-600 hover:underline font-medium">N° {{ $a->territorio?->numero }}</a> <a data-ritorno href="{{ route('territori.show', $a->territorio_id) }}" class="text-indigo-600 hover:underline font-medium">N° {{ $a->territorio?->numero }}</a>
<span class="text-xs text-gray-400 ml-1">{{ $a->territorio?->zona?->nome }}</span> <span class="text-xs text-gray-400 ml-1">{{ $a->territorio?->zona?->nome }}</span>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,8 @@
<div> <div>
<div class="mb-6"> <div class="mb-6">
<a href="{{ route('territori.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1"> <a href="{{ $ritorno ?? route('territori.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
Torna alla lista {{ $ritorno ? 'Torna indietro' : 'Torna alla lista' }}
</a> </a>
<h1 class="text-2xl font-bold text-gray-900">{{ $title }}</h1> <h1 class="text-2xl font-bold text-gray-900">{{ $title }}</h1>
</div> </div>
@@ -65,7 +65,7 @@
dell'applicazione. Prima cambiava da una pagina all'altra. --}} dell'applicazione. Prima cambiava da una pagina all'altra. --}}
<div class="flex flex-wrap gap-3 pt-2"> <div class="flex flex-wrap gap-3 pt-2">
<x-btn variante="primary" type="submit">{{ $isEdit ? 'Aggiorna' : 'Crea' }}</x-btn> <x-btn variante="primary" type="submit">{{ $isEdit ? 'Aggiorna' : 'Crea' }}</x-btn>
<x-btn variante="secondary" :href="route('territori.index')">Annulla</x-btn> <x-btn variante="secondary" :href="$ritorno ?? route('territori.index')">Annulla</x-btn>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -10,7 +10,7 @@
disegna il confine e lo si crea da lì (PiantineEditor, ?nuovo=1). disegna il confine e lo si crea da lì (PiantineEditor, ?nuovo=1).
L'editor è dell'amministratore: chi ha solo territori.manage L'editor è dell'amministratore: chi ha solo territori.manage
resta sul form. --}} resta sul form. --}}
<x-btn variante="primary" :href="auth()->user()->can('settings.manage') ? route('piantine.editor', ['nuovo' => 1]) : route('territori.create')"> <x-btn data-ritorno 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> <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 Nuovo territorio
</x-btn> </x-btn>
@@ -116,7 +116,7 @@
alt="Thumbnail territorio {{ $territorio->numero }}" alt="Thumbnail territorio {{ $territorio->numero }}"
class="h-8 w-6 rounded border border-gray-200 object-cover bg-gray-50 flex-none shadow-sm"> class="h-8 w-6 rounded border border-gray-200 object-cover bg-gray-50 flex-none shadow-sm">
@endif @endif
<a href="{{ route('territori.show', $territorio) }}" class="hover:text-indigo-600 transition-colors">{{ $territorio->numero }}</a> <a data-ritorno href="{{ route('territori.show', $territorio) }}" class="hover:text-indigo-600 transition-colors">{{ $territorio->numero }}</a>
</div> </div>
</td> </td>
<td data-label="Zona" class="px-4 py-3 text-sm text-gray-600">{{ $territorio->zona?->nome ?? '-' }}</td> <td data-label="Zona" class="px-4 py-3 text-sm text-gray-600">{{ $territorio->zona?->nome ?? '-' }}</td>
@@ -147,7 +147,7 @@
{{-- Primary actions: Assegna / Rientra --}} {{-- Primary actions: Assegna / Rientra --}}
@can('territori.assign') @can('territori.assign')
@if(!$territorio->assegnazioneCorrente && $territorio->attivo) @if(!$territorio->assegnazioneCorrente && $territorio->attivo)
<x-btn variante="primary" tono="tenue" dimensione="sm" :href="route('assegnazioni.assegna', ['territorioId' => $territorio->id])"> <x-btn data-ritorno variante="primary" tono="tenue" dimensione="sm" :href="route('assegnazioni.assegna', ['territorioId' => $territorio->id])">
<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> <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>
Assegna Assegna
</x-btn> </x-btn>
@@ -158,7 +158,7 @@
{{-- Rientra non cancella niente: era rosso pieno {{-- Rientra non cancella niente: era rosso pieno
quanto un Elimina. È l'azione principale quanto un Elimina. È l'azione principale
della riga, alternativa ad Assegna. --}} della riga, alternativa ad Assegna. --}}
<x-btn variante="primary" tono="tenue" dimensione="sm" :href="route('assegnazioni.rientra', ['assegnazione' => $territorio->assegnazioneCorrente->id])"> <x-btn data-ritorno variante="primary" tono="tenue" dimensione="sm" :href="route('assegnazioni.rientra', ['assegnazione' => $territorio->assegnazioneCorrente->id])">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg>
Rientra Rientra
</x-btn> </x-btn>
@@ -168,13 +168,13 @@
{{-- Solo icona: l'aria-label non è ridondante col {{-- Solo icona: l'aria-label non è ridondante col
title, è l'unico nome che un lettore di title, è l'unico nome che un lettore di
schermo annuncia in modo affidabile. --}} schermo annuncia in modo affidabile. --}}
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona <x-btn data-ritorno variante="secondary" tono="tenue" dimensione="sm" icona
:href="route('territori.show', $territorio)" :href="route('territori.show', $territorio)"
title="Dettaglio" aria-label="Dettaglio del territorio {{ $territorio->numero }}"> title="Dettaglio" aria-label="Dettaglio del territorio {{ $territorio->numero }}">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
</x-btn> </x-btn>
@can('territori.manage') @can('territori.manage')
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona <x-btn data-ritorno variante="secondary" tono="tenue" dimensione="sm" icona
:href="route('territori.edit', $territorio)" :href="route('territori.edit', $territorio)"
title="Modifica" aria-label="Modifica il territorio {{ $territorio->numero }}"> title="Modifica" aria-label="Modifica il territorio {{ $territorio->numero }}">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>

View File

@@ -1,16 +1,16 @@
<div> <div>
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<a href="{{ route('territori.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1"> <a href="{{ $ritorno ?? route('territori.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
Torna alla lista {{ $ritorno ? 'Torna indietro' : 'Torna alla lista' }}
</a> </a>
<h1 class="text-2xl font-bold text-gray-900">Territorio {{ $territorio->numero }}</h1> <h1 class="text-2xl font-bold text-gray-900">Territorio {{ $territorio->numero }}</h1>
</div> </div>
<div class="flex items-center gap-2 flex-wrap"> <div class="flex items-center gap-2 flex-wrap">
@can('territori.assign') @can('territori.assign')
@if(!$territorio->assegnazioneCorrente && $territorio->attivo) @if(!$territorio->assegnazioneCorrente && $territorio->attivo)
<x-btn variante="primary" :href="route('assegnazioni.assegna', ['territorioId' => $territorio->id])"> <x-btn data-ritorno variante="primary" :href="route('assegnazioni.assegna', ['territorioId' => $territorio->id])">
<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> <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>
Assegna Assegna
</x-btn> </x-btn>
@@ -21,14 +21,14 @@
{{-- Rientra era rosso pieno quanto un Elimina. Non cancella {{-- Rientra era rosso pieno quanto un Elimina. Non cancella
niente: è l'azione principale di un territorio assegnato, niente: è l'azione principale di un territorio assegnato,
quindi primary come Assegna, che è la sua alternativa. --}} quindi primary come Assegna, che è la sua alternativa. --}}
<x-btn variante="primary" :href="route('assegnazioni.rientra', ['assegnazione' => $territorio->assegnazioneCorrente->id])"> <x-btn data-ritorno variante="primary" :href="route('assegnazioni.rientra', ['assegnazione' => $territorio->assegnazioneCorrente->id])">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg>
Rientra Rientra
</x-btn> </x-btn>
@endif @endif
@endcan @endcan
@can('territori.manage') @can('territori.manage')
<x-btn variante="secondary" :href="route('territori.edit', $territorio)"> <x-btn data-ritorno variante="secondary" :href="route('territori.edit', $territorio)">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Modifica Modifica
</x-btn> </x-btn>
@@ -41,7 +41,7 @@
<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"> <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> <span class="font-semibold">Il link della piantina è ancora da inviare a {{ $territorio->assegnazioneCorrente->proclamatore?->nome_completo }}.</span>
@can('territori.assign') @can('territori.assign')
<x-btn variante="secondary" dimensione="sm" :href="route('assegnazioni.assegna', ['invia' => $territorio->assegnazioneCorrente->id])">Invia link</x-btn> <x-btn data-ritorno variante="secondary" dimensione="sm" :href="route('assegnazioni.assegna', ['invia' => $territorio->assegnazioneCorrente->id])">Invia link</x-btn>
@endcan @endcan
</div> </div>
@endif @endif
@@ -109,7 +109,7 @@
</div> </div>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
@can('territori.return') @can('territori.return')
<x-btn variante="primary" :href="route('assegnazioni.rientra', ['assegnazione' => $activeAssignment->id])">Rientra</x-btn> <x-btn data-ritorno variante="primary" :href="route('assegnazioni.rientra', ['assegnazione' => $activeAssignment->id])">Rientra</x-btn>
@endcan @endcan
</div> </div>
</div> </div>

View File

@@ -0,0 +1,191 @@
<?php
namespace Tests\Feature;
use App\Livewire\Assegnazioni\Rientra;
use App\Livewire\Proclamatori\ProclamatoreEdit;
use App\Livewire\Proclamatori\ProclamatoreIndex;
use App\Livewire\Territori\TerritorioIndex;
use App\Models\AnnoTeocratico;
use App\Models\Assegnazione;
use App\Models\Proclamatore;
use App\Models\Setting;
use App\Models\Territorio;
use App\Models\User;
use App\Support\Ritorno;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
/**
* Dopo un'operazione (confermata o annullata) si torna alla pagina da cui il
* percorso era partito, con i suoi filtri: ?da=… (App\Support\Ritorno).
*/
class RitornoTest extends TestCase
{
use RefreshDatabase;
private User $admin;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
$this->seed(RolesAndPermissionsSeeder::class);
Setting::firstOrCreate([], ['congregazione_nome' => 'Congregazione di prova']);
AnnoTeocratico::create(['label' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-08-31']);
$this->admin = User::create(['name' => 'Admin', 'email' => 'admin@example.test', 'password' => 'password-di-prova']);
$this->admin->assignRole('amministratore');
}
// ─── Validazione ────────────────────────────────────────────
public function test_un_percorso_interno_diventa_l_url_di_ritorno(): void
{
$this->assertSame(url('/territori?filterZona=3&page=2'), Ritorno::valida('/territori?filterZona=3&page=2'));
$this->assertSame(url('/'), Ritorno::valida('/'));
}
#[DataProvider('ritorniNonValidi')]
public function test_un_ritorno_esterno_o_malformato_si_ignora(mixed $valore): void
{
$this->assertNull(Ritorno::valida($valore));
}
public static function ritorniNonValidi(): array
{
return [
'vuoto' => [''],
'null' => [null],
'array' => [['/territori']],
'url completo' => ['https://altro.sito/x'],
'senza schema' => ['//altro.sito/x'],
'barra rovesciata' => ['/\\altro.sito'],
'relativo' => ['territori'],
'javascript' => ['javascript:alert(1)'],
'a capo' => ["/territori\r\nX: y"],
'livewire' => ['/livewire/update'],
];
}
// ─── Dopo la conferma ───────────────────────────────────────
private function assegnazioneAperta(): Assegnazione
{
$territorio = Territorio::create(['numero' => '35', 'attivo' => true]);
$proclamatore = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true]);
return Assegnazione::create([
'territorio_id' => $territorio->id,
'proclamatore_id' => $proclamatore->id,
'anno_teocratico_id' => AnnoTeocratico::sole()->id,
'assigned_at' => now()->subDays(10),
'created_by' => $this->admin->id,
]);
}
public function test_il_rientro_torna_all_elenco_filtrato_da_cui_si_era_partiti(): void
{
$assegnazione = $this->assegnazioneAperta();
Livewire::withQueryParams(['da' => '/territori?filterStato=da_rientrare&page=2'])
->actingAs($this->admin)
->test(Rientra::class, ['assegnazione' => $assegnazione])
->call('save')
->assertRedirect(url('/territori?filterStato=da_rientrare&page=2'));
}
public function test_senza_pagina_di_partenza_il_rientro_va_alla_scheda_come_prima(): void
{
$assegnazione = $this->assegnazioneAperta();
Livewire::actingAs($this->admin)
->test(Rientra::class, ['assegnazione' => $assegnazione])
->call('save')
->assertRedirect(route('territori.show', $assegnazione->territorio_id));
}
public function test_un_ritorno_verso_l_esterno_non_viene_seguito(): void
{
$assegnazione = $this->assegnazioneAperta();
Livewire::withQueryParams(['da' => '//altro.sito/phishing'])
->actingAs($this->admin)
->test(Rientra::class, ['assegnazione' => $assegnazione])
->assertSet('ritorno', null)
->call('save')
->assertRedirect(route('territori.show', $assegnazione->territorio_id));
}
public function test_la_modifica_del_proclamatore_torna_alla_ricerca(): void
{
$mario = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true]);
Livewire::withQueryParams(['da' => '/proclamatori?search=Rossi'])
->actingAs($this->admin)
->test(ProclamatoreEdit::class, ['proclamatore' => $mario])
->set('nome', 'Mario Luigi')
->call('save')
->assertRedirect(url('/proclamatori?search=Rossi'));
}
// ─── Annulla e "Torna indietro" ─────────────────────────────
public function test_annulla_e_torna_indietro_portano_alla_pagina_di_partenza(): void
{
$assegnazione = $this->assegnazioneAperta();
$partenza = '/territori?search=35';
$this->actingAs($this->admin)
->get(route('assegnazioni.rientra', $assegnazione) . '?da=' . urlencode($partenza))
->assertOk()
->assertSee('Torna indietro')
->assertSee('href="' . e(url($partenza)) . '"', false);
}
public function test_senza_partenza_le_etichette_restano_quelle_di_prima(): void
{
$assegnazione = $this->assegnazioneAperta();
$this->actingAs($this->admin)
->get(route('assegnazioni.rientra', $assegnazione))
->assertOk()
->assertSee('Torna al territorio')
->assertDontSee('Torna indietro');
}
// ─── Gli elenchi ricordano filtri e ordinamento ─────────────
public function test_l_elenco_territori_legge_filtri_e_ordinamento_dall_indirizzo(): void
{
Livewire::withQueryParams(['search' => '35', 'filterStato' => 'assegnato', 'sortField' => 'zona', 'sortDirection' => 'desc'])
->actingAs($this->admin)
->test(TerritorioIndex::class)
->assertSet('search', '35')
->assertSet('filterStato', 'assegnato')
->assertSet('sortField', 'zona')
->assertSet('sortDirection', 'desc');
}
public function test_l_elenco_proclamatori_legge_la_ricerca_dall_indirizzo(): void
{
Livewire::withQueryParams(['search' => 'Rossi', 'filtroStato' => 'attivi'])
->actingAs($this->admin)
->test(ProclamatoreIndex::class)
->assertSet('search', 'Rossi')
->assertSet('filtroStato', 'attivi');
}
public function test_i_link_verso_schede_e_operazioni_portano_il_ritorno(): void
{
Territorio::create(['numero' => '35', 'attivo' => true]);
$html = $this->actingAs($this->admin)->get(route('territori.index'))->assertOk()->getContent();
// Il link alla scheda è marcato: il JS gli aggiunge ?da= al momento del clic.
$this->assertMatchesRegularExpression('/<a\b(?=[^>]*\bdata-ritorno\b)(?=[^>]*href="[^"]*\/territori\/\d+")[^>]*>/s', $html);
}
}