++ pagina Assegna più leggera; "Vedi tutti" in testata nella Home
Assegna pesava 387 KB e Livewire li rimandava a ogni interazione. - Elenco dei territori assegnati in un componente lazy a sé (ElencoAssegnati): il form compare subito e non lo ridisegna. - Ricerca nell'elenco e anteprima della miniatura nel browser: nessuna richiesta al server mentre si scrive o si sceglie il territorio. - Icone SVG come simboli richiamati con <use>. - Assegnazione::giorni: cast a int (Carbon 3 restituisce un float). Home: "Vedi tutti" diventa un pulsante nella testata dei riquadri. README e HANDOFF aggiornati. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
32
HANDOFF.md
32
HANDOFF.md
@@ -127,6 +127,38 @@ 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.
|
||||||
|
|
||||||
|
### Pagina Assegna più leggera; "Vedi tutti" in testata nella Home (19 ottobre 2026)
|
||||||
|
|
||||||
|
Segnalazione dell'utente: la pagina Assegnazioni era lenta. Misurato su dev
|
||||||
|
con i dati di prod (79 assegnazioni aperte): il server la genera in ~80 ms
|
||||||
|
con 10 query, ma l'HTML pesava **387 KB** (299 l'elenco dei territori
|
||||||
|
assegnati, di cui 58 di icone SVG ripetute; 75 le opzioni dei campi di scelta)
|
||||||
|
e Livewire lo rimandava **per intero a ogni interazione**: scegliere un
|
||||||
|
territorio (anteprima con `wire:model.live`), scrivere nella ricerca
|
||||||
|
dell'elenco (`linkSearch`), aprire l'invio del link. Rimedi:
|
||||||
|
|
||||||
|
- L'elenco è un componente a sé, `Assegnazioni\ElencoAssegnati`, **lazy**: la
|
||||||
|
pagina mostra subito il form e le sue azioni non lo ridisegnano. "Invia
|
||||||
|
link" nell'elenco chiede alla pagina di aprire l'invio con l'evento
|
||||||
|
`apri-invio` (`Assegna::apriInvio` ha `#[On('apri-invio')]`);
|
||||||
|
`toggleLinkSent` sta nell'elenco.
|
||||||
|
- La ricerca nell'elenco filtra nel browser (`data-cerca` senza accenti,
|
||||||
|
Alpine `elencoCercabile`): nessuna richiesta mentre si scrive.
|
||||||
|
- Il territorio va a Livewire con `wire:model` (non più `.live`): l'anteprima
|
||||||
|
della miniatura segue la scelta nel browser (evento `scelta` di
|
||||||
|
`<x-scelta>`, URL della miniatura nelle opzioni).
|
||||||
|
- Le icone delle righe sono simboli SVG definiti una volta e richiamati con
|
||||||
|
`<use>`. Il ramo "link scaduto" dell'elenco (solo per non autenticati, cioè
|
||||||
|
mai su questa pagina) è stato tolto.
|
||||||
|
- `AssegnaPrestazioniTest` tiene fermi questi punti, compreso un tetto al
|
||||||
|
peso per riga dell'elenco.
|
||||||
|
- Corretto anche `Assegnazione::giorni`: con Carbon 3 `diffInDays()` è un
|
||||||
|
float e PHP segnalava una conversione implicita a ogni riga.
|
||||||
|
|
||||||
|
Home: "Vedi tutti" di *Da assegnare* e *Da rientrare* è ora un pulsante nella
|
||||||
|
testata del riquadro, accanto al PDF, invece che in fondo all'elenco. Compare
|
||||||
|
alle stesse condizioni di prima (elenco troncato al limite della Home).
|
||||||
|
|
||||||
### Scelta di territori e proclamatori con ricerca (19 ottobre 2026)
|
### Scelta di territori e proclamatori con ricerca (19 ottobre 2026)
|
||||||
|
|
||||||
Al posto delle tendine di territorio e proclamatore (Assegna, finestra del
|
Al posto delle tendine di territorio e proclamatore (Assegna, finestra del
|
||||||
|
|||||||
44
README.md
44
README.md
@@ -338,6 +338,16 @@ Cosa serve sapere:
|
|||||||
`CAST(... AS UNSIGNED)` nell'ordinamento del Registro — e che quindi va
|
`CAST(... AS UNSIGNED)` nell'ordinamento del Registro — e che quindi va
|
||||||
provato sullo stack vero, non qui.
|
provato sullo stack vero, non qui.
|
||||||
|
|
||||||
|
La logica JavaScript senza DOM (la ricerca nei campi di scelta,
|
||||||
|
`resources/js/scelta.js`) ha i suoi test in `tests/js/`, con il test runner di
|
||||||
|
Node:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:js
|
||||||
|
# oppure, senza Node installato:
|
||||||
|
docker run --rm -v "$PWD":/app -w /app node:20-alpine node --test tests/js/
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Variabili d'ambiente
|
## Variabili d'ambiente
|
||||||
@@ -678,11 +688,15 @@ docker compose --profile dev up -d --build
|
|||||||
- Media mensile di percorrenza e durata media delle assegnazioni
|
- Media mensile di percorrenza e durata media delle assegnazioni
|
||||||
- Campagna attiva con barra di progresso
|
- Campagna attiva con barra di progresso
|
||||||
- Tre liste rapide con azione diretta: **da assegnare**, **prioritari**,
|
- Tre liste rapide con azione diretta: **da assegnare**, **prioritari**,
|
||||||
**da rientrare**
|
**da rientrare**; quando l'elenco è troncato, **"Vedi tutti"** è un pulsante
|
||||||
|
in testata che apre l'elenco territori già filtrato
|
||||||
|
|
||||||
### Territori
|
### Territori
|
||||||
- CRUD completo con upload PDF e miniatura generata automaticamente
|
- CRUD completo con upload PDF e miniatura generata automaticamente
|
||||||
- Filtri per zona, tipologia e stato
|
- **"Nuovo territorio"** apre il designer delle piantine già in disegno: il
|
||||||
|
territorio si crea dal confine disegnato (per chi non è amministratore resta
|
||||||
|
il form)
|
||||||
|
- Filtri per zona, tipologia e stato, conservati nell'indirizzo della pagina
|
||||||
- Badge di stato colorati e indicatore di priorità (manuale o automatica)
|
- Badge di stato colorati e indicatore di priorità (manuale o automatica)
|
||||||
- Dettaglio con visualizzatore PDF e storico assegnazioni per anno teocratico
|
- Dettaglio con visualizzatore PDF e storico assegnazioni per anno teocratico
|
||||||
- Link PDF pubblico condivisibile, a scadenza configurabile
|
- Link PDF pubblico condivisibile, a scadenza configurabile
|
||||||
@@ -696,12 +710,28 @@ docker compose --profile dev up -d --build
|
|||||||
- Soft delete con cestino
|
- Soft delete con cestino
|
||||||
|
|
||||||
### Assegnazione e rientro
|
### Assegnazione e rientro
|
||||||
- Selezione territorio disponibile e proclamatore attivo
|
- **Scelta con ricerca** di territorio e proclamatore (anche nel Registro e nel
|
||||||
|
designer delle piantine): si scrive e compaiono le proposte più vicine —
|
||||||
|
uguale, inizia con, contiene, errore di battitura ("forse cercavi") — con
|
||||||
|
le parole in qualsiasi ordine, senza badare a maiuscole e accenti. Campo
|
||||||
|
vuoto: l'elenco completo. Pensata anche per il telefono
|
||||||
|
- I territori già assegnati si trovano cercando ("già assegnato a …") ma non
|
||||||
|
si possono scegliere; l'anteprima della miniatura segue la scelta
|
||||||
- Anno teocratico rilevato automaticamente dalla data
|
- Anno teocratico rilevato automaticamente dalla data
|
||||||
- Validazione: territorio non già assegnato, entrambi attivi
|
- Validazione: territorio non già assegnato, entrambi attivi
|
||||||
|
- **Invio del link dopo l'assegnazione**: una finestra chiede il canale —
|
||||||
|
*Copia link* sempre, *WhatsApp* se il proclamatore ha un numero, *Email* se
|
||||||
|
ha un indirizzo e la posta è configurata (la manda l'app) — e se il link è
|
||||||
|
stato inviato. Solo dopo la risposta si va avanti; altrimenti l'assegnazione
|
||||||
|
resta segnalata **"Link ancora da inviare"**, con "Invia link" per riprendere
|
||||||
|
- L'elenco dei territori assegnati si carica subito dopo la pagina e si filtra
|
||||||
|
nel browser, senza attese
|
||||||
- Rientro con calcolo automatico dei giorni
|
- Rientro con calcolo automatico dei giorni
|
||||||
- **Prompt campagna**: se il rientro ricade nel periodo di una campagna, viene
|
- **Prompt campagna**: se il territorio era fuori durante una campagna viene
|
||||||
chiesto se conteggiarlo
|
chiesto se è stato percorso per la campagna; oltre la soglia "da rientrare"
|
||||||
|
compare un avviso di tempo di percorrenza molto alto
|
||||||
|
- **Dopo una conferma o un annulla si torna alla pagina di partenza**, con i
|
||||||
|
suoi filtri, ricerca, ordinamento e pagina (aperta dal menu: come prima)
|
||||||
|
|
||||||
### Campagne
|
### Campagne
|
||||||
- CRUD con date di inizio e fine
|
- CRUD con date di inizio e fine
|
||||||
@@ -799,11 +829,11 @@ una revoca ha effetto immediato senza bisogno di ricaricare la pagina.
|
|||||||
| `settings` | Configurazione singleton |
|
| `settings` | Configurazione singleton |
|
||||||
| `zone` | Zone territoriali |
|
| `zone` | Zone territoriali |
|
||||||
| `tipologie` | Tipologie di territorio |
|
| `tipologie` | Tipologie di territorio |
|
||||||
| `proclamatori` | Assegnatari, con nome/cognome cifrati e soft delete |
|
| `proclamatori` | Assegnatari, con nome/cognome e i facoltativi email/telefono cifrati, soft delete |
|
||||||
| `territori` | Cartoline con zona, tipologia, PDF, miniatura, soft delete |
|
| `territori` | Cartoline con zona, tipologia, PDF, miniatura, soft delete |
|
||||||
| `anni_teocratici` | Periodi settembre → agosto |
|
| `anni_teocratici` | Periodi settembre → agosto |
|
||||||
| `campagne` | Periodi speciali con date e descrizione |
|
| `campagne` | Periodi speciali con date e descrizione |
|
||||||
| `assegnazioni` | Territorio ↔ proclamatore, con date, anno e campagna |
|
| `assegnazioni` | Territorio ↔ proclamatore, con date, anno e campagna; stato dell'invio del link (`link_sent`, `link_da_inviare`, `link_canale`) |
|
||||||
| `activity_log` | Audit trail |
|
| `activity_log` | Audit trail |
|
||||||
|
|
||||||
**Vincoli e indici principali**
|
**Vincoli e indici principali**
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ namespace App\Livewire\Assegnazioni;
|
|||||||
|
|
||||||
use App\Livewire\Concerns\RicordaRitorno;
|
use App\Livewire\Concerns\RicordaRitorno;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\Attributes\Computed;
|
|
||||||
use Livewire\Attributes\Locked;
|
use Livewire\Attributes\Locked;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
use App\Models\Territorio;
|
use App\Models\Territorio;
|
||||||
use App\Models\Proclamatore;
|
use App\Models\Proclamatore;
|
||||||
use App\Models\Assegnazione;
|
use App\Models\Assegnazione;
|
||||||
@@ -40,7 +40,6 @@ class Assegna extends Component
|
|||||||
public ?int $territorio_id = null;
|
public ?int $territorio_id = null;
|
||||||
public ?int $proclamatore_id = null;
|
public ?int $proclamatore_id = null;
|
||||||
public string $assigned_at = '';
|
public string $assigned_at = '';
|
||||||
public string $linkSearch = '';
|
|
||||||
|
|
||||||
// Optional pre-selection from parent context
|
// Optional pre-selection from parent context
|
||||||
public ?int $preselectedTerritorioId = null;
|
public ?int $preselectedTerritorioId = null;
|
||||||
@@ -143,7 +142,12 @@ class Assegna extends Component
|
|||||||
$this->apriInvio($assegnazione->id);
|
$this->apriInvio($assegnazione->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apre l'invio del link: subito dopo l'assegnazione, o da un'assegnazione rimasta in sospeso. */
|
/**
|
||||||
|
* Apre l'invio del link: subito dopo l'assegnazione, o da un'assegnazione
|
||||||
|
* rimasta in sospeso ("Invia link" nell'elenco, che è un componente a sé
|
||||||
|
* e lo chiede con l'evento apri-invio).
|
||||||
|
*/
|
||||||
|
#[On('apri-invio')]
|
||||||
public function apriInvio(int $assegnazioneId): void
|
public function apriInvio(int $assegnazioneId): void
|
||||||
{
|
{
|
||||||
$this->authorize('territori.assign');
|
$this->authorize('territori.assign');
|
||||||
@@ -250,65 +254,27 @@ class Assegna extends Component
|
|||||||
return Assegnazione::with(['territorio', 'proclamatore'])->find($this->invioId);
|
return Assegnazione::with(['territorio', 'proclamatore'])->find($this->invioId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleLinkSent(int $assegnazioneId): void
|
|
||||||
{
|
|
||||||
$this->authorize('territori.assign');
|
|
||||||
$assegnazione = Assegnazione::findOrFail($assegnazioneId);
|
|
||||||
$assegnazione->registraInvioLink(! $assegnazione->link_sent, $assegnazione->link_canale);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Computed]
|
|
||||||
public function selectedThumbnailUrl(): ?string
|
|
||||||
{
|
|
||||||
if (!$this->territorio_id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$t = Territorio::find($this->territorio_id);
|
|
||||||
return $t?->miniaturaUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
// Le opzioni dei campi di scelta con ricerca (<x-scelta>). Ci sono anche
|
// Le opzioni dei campi di scelta con ricerca (<x-scelta>). Ci sono anche
|
||||||
// i territori già assegnati, che non si possono scegliere: cercandone
|
// i territori già assegnati, che non si possono scegliere: cercandone
|
||||||
// uno si legge a chi è, invece di un "nessun risultato".
|
// uno si legge a chi è, invece di un "nessun risultato". La miniatura
|
||||||
$opzioniTerritori = Territorio::attivi()
|
// serve all'anteprima, che si aggiorna nel browser.
|
||||||
|
$territori = Territorio::attivi()
|
||||||
->with(['zona', 'tipologia', 'assegnazioneCorrente.proclamatore'])
|
->with(['zona', 'tipologia', 'assegnazioneCorrente.proclamatore'])
|
||||||
->get()
|
->get()
|
||||||
->sortBy('numero', SORT_NATURAL)
|
->sortBy('numero', SORT_NATURAL);
|
||||||
|
|
||||||
|
$opzioniTerritori = $territori
|
||||||
->map(fn (Territorio $t) => $t->opzioneScelta(
|
->map(fn (Territorio $t) => $t->opzioneScelta(
|
||||||
$t->assegnazioneCorrente ? 'già assegnato a ' . ($t->assegnazioneCorrente->proclamatore?->nome_completo ?? '—') : null
|
$t->assegnazioneCorrente ? 'già assegnato a ' . ($t->assegnazioneCorrente->proclamatore?->nome_completo ?? '—') : null
|
||||||
))
|
) + array_filter(['miniatura' => $t->miniaturaUrl()]))
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$opzioniProclamatori = Proclamatore::opzioniScelta(Proclamatore::attivi()->get());
|
$opzioniProclamatori = Proclamatore::opzioniScelta(Proclamatore::attivi()->get());
|
||||||
|
|
||||||
// All currently assigned territories with links
|
// L'elenco dei territori assegnati sta in ElencoAssegnati.
|
||||||
$assegnazioniAperte = Assegnazione::aperte()
|
|
||||||
->with(['territorio.zona', 'proclamatore'])
|
|
||||||
->get()
|
|
||||||
->sortBy(fn($a) => (int) $a->territorio?->numero);
|
|
||||||
|
|
||||||
// Ricerca rapida nella lista dei link: numero territorio, zona,
|
|
||||||
// proclamatore in un solo campo. In memoria perché nome/cognome del
|
|
||||||
// proclamatore sono cifrati e non filtrabili via SQL.
|
|
||||||
$assegnazioniAperteVisibili = $assegnazioniAperte;
|
|
||||||
if (trim($this->linkSearch) !== '') {
|
|
||||||
$term = mb_strtolower(trim($this->linkSearch));
|
|
||||||
$assegnazioniAperteVisibili = $assegnazioniAperte->filter(function ($a) use ($term) {
|
|
||||||
$haystack = mb_strtolower(
|
|
||||||
($a->territorio?->numero ?? '') . ' ' .
|
|
||||||
($a->territorio?->zona?->nome ?? '') . ' ' .
|
|
||||||
($a->proclamatore?->nome_completo ?? '')
|
|
||||||
);
|
|
||||||
return str_contains($haystack, $term);
|
|
||||||
})->values();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Il link scade alla soglia rientro (giorni_per_smarrito).
|
|
||||||
$sogliaRientro = max(1, (int) \App\Models\Setting::getValue('giorni_per_smarrito', 120));
|
|
||||||
|
|
||||||
// L'invio in corso; se nel frattempo l'assegnazione è rientrata o
|
// L'invio in corso; se nel frattempo l'assegnazione è rientrata o
|
||||||
// sparita non c'è più niente da mandare.
|
// sparita non c'è più niente da mandare.
|
||||||
$invio = $this->invioId ? Assegnazione::aperte()->with(['territorio', 'proclamatore'])->find($this->invioId) : null;
|
$invio = $this->invioId ? Assegnazione::aperte()->with(['territorio', 'proclamatore'])->find($this->invioId) : null;
|
||||||
@@ -321,9 +287,7 @@ class Assegna extends Component
|
|||||||
'postaAttiva' => $invio ? app(ConfigurazionePosta::class)->funzionante() : false,
|
'postaAttiva' => $invio ? app(ConfigurazionePosta::class)->funzionante() : false,
|
||||||
'opzioniTerritori' => $opzioniTerritori,
|
'opzioniTerritori' => $opzioniTerritori,
|
||||||
'opzioniProclamatori' => $opzioniProclamatori,
|
'opzioniProclamatori' => $opzioniProclamatori,
|
||||||
'assegnazioniAperte' => $assegnazioniAperte,
|
'anteprimaIniziale' => $this->territorio_id ? $territori->firstWhere('id', $this->territorio_id)?->miniaturaUrl() : null,
|
||||||
'assegnazioniAperteVisibili' => $assegnazioniAperteVisibili,
|
|
||||||
'sogliaRientro' => $sogliaRientro,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
60
app/Livewire/Assegnazioni/ElencoAssegnati.php
Normal file
60
app/Livewire/Assegnazioni/ElencoAssegnati.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Assegnazioni;
|
||||||
|
|
||||||
|
use App\Models\Assegnazione;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Livewire\Attributes\Lazy;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* L'elenco dei territori assegnati della pagina Assegna, separato dal form.
|
||||||
|
*
|
||||||
|
* Prima stava nello stesso componente: con ~80 righe la pagina pesava quasi
|
||||||
|
* 400 KB e Livewire li rimandava tutti a ogni interazione (scegliere un
|
||||||
|
* territorio, scrivere nella ricerca, aprire l'invio del link). Ora:
|
||||||
|
* - è un componente a sé, caricato subito dopo la pagina (#[Lazy]): il form
|
||||||
|
* compare subito e le sue azioni non ridisegnano l'elenco;
|
||||||
|
* - la ricerca filtra le righe nel browser (data-cerca), senza richieste;
|
||||||
|
* - le icone sono simboli SVG definiti una volta e richiamati con <use>.
|
||||||
|
*/
|
||||||
|
#[Lazy]
|
||||||
|
class ElencoAssegnati extends Component
|
||||||
|
{
|
||||||
|
public function toggleLinkSent(int $assegnazioneId): void
|
||||||
|
{
|
||||||
|
$this->authorize('territori.assign');
|
||||||
|
$assegnazione = Assegnazione::findOrFail($assegnazioneId);
|
||||||
|
$assegnazione->registraInvioLink(! $assegnazione->link_sent, $assegnazione->link_canale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function placeholder()
|
||||||
|
{
|
||||||
|
return view('livewire.assegnazioni.elenco-assegnati-attesa');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$assegnazioni = Assegnazione::aperte()
|
||||||
|
->with(['territorio.zona', 'proclamatore'])
|
||||||
|
->get()
|
||||||
|
->sortBy(fn ($a) => (string) $a->territorio?->numero, SORT_NATURAL)
|
||||||
|
->values();
|
||||||
|
|
||||||
|
// Il testo su cui filtra la ricerca nel browser: numero, zona e nome
|
||||||
|
// (decifrato qui: in SQL non si potrebbe cercare), senza accenti.
|
||||||
|
$cerca = $assegnazioni->mapWithKeys(fn ($a) => [$a->id => Str::lower(Str::ascii(implode(' ', array_filter([
|
||||||
|
$a->territorio?->numero,
|
||||||
|
$a->territorio?->zona?->nome,
|
||||||
|
$a->proclamatore?->nome_completo,
|
||||||
|
]))))]);
|
||||||
|
|
||||||
|
return view('livewire.assegnazioni.elenco-assegnati', [
|
||||||
|
'assegnazioni' => $assegnazioni,
|
||||||
|
'cerca' => $cerca,
|
||||||
|
// Il link scade alla soglia rientro (giorni_per_smarrito).
|
||||||
|
'sogliaRientro' => max(1, (int) Setting::getValue('giorni_per_smarrito', 120)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,7 +136,8 @@ class Assegnazione extends Model
|
|||||||
public function getGiorniAttribute(): int
|
public function getGiorniAttribute(): int
|
||||||
{
|
{
|
||||||
$end = $this->returned_at ?? now();
|
$end = $this->returned_at ?? now();
|
||||||
return Carbon::parse($this->assigned_at)->diffInDays($end);
|
// Carbon 3 restituisce un float: i giorni interi trascorsi.
|
||||||
|
return (int) Carbon::parse($this->assigned_at)->diffInDays($end);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getIsApertaAttribute(): bool
|
public function getIsApertaAttribute(): bool
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import './bootstrap';
|
import './bootstrap';
|
||||||
import { proponi } from './scelta';
|
import { proponi, normalizza } from './scelta';
|
||||||
|
|
||||||
// ─── Campo di scelta con ricerca (<x-scelta>) ───────────────────────────────
|
// ─── Campo di scelta con ricerca (<x-scelta>) ───────────────────────────────
|
||||||
//
|
//
|
||||||
@@ -8,6 +8,21 @@ import { proponi } from './scelta';
|
|||||||
// Il valore scelto arriva a Livewire con x-modelable + wire:model.
|
// Il valore scelto arriva a Livewire con x-modelable + wire:model.
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
|
// Elenco filtrato nel browser: ogni riga ha data-cerca (testo già senza
|
||||||
|
// accenti, minuscolo); valgono tutte le parole scritte, in qualsiasi
|
||||||
|
// ordine. Nessuna richiesta al server mentre si scrive.
|
||||||
|
window.Alpine.data('elencoCercabile', () => ({
|
||||||
|
q: '',
|
||||||
|
corrisponde(testo) {
|
||||||
|
const parole = normalizza(this.q).split(/\s+/).filter(Boolean);
|
||||||
|
return parole.every((p) => (testo ?? '').includes(p));
|
||||||
|
},
|
||||||
|
get nessuno() {
|
||||||
|
if (normalizza(this.q) === '') return false;
|
||||||
|
return ![...this.$root.querySelectorAll('[data-cerca]')].some((riga) => this.corrisponde(riga.dataset.cerca));
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
// vuoto: il valore di "nessuna scelta" per la proprietà Livewire collegata
|
// vuoto: il valore di "nessuna scelta" per la proprietà Livewire collegata
|
||||||
// ('' per le stringhe, null per i ?int: '' su un ?int sarebbe un errore).
|
// ('' per le stringhe, null per i ?int: '' su un ?int sarebbe un errore).
|
||||||
window.Alpine.data('sceltaRicerca', (opzioni, vuoto = '') => ({
|
window.Alpine.data('sceltaRicerca', (opzioni, vuoto = '') => ({
|
||||||
@@ -90,6 +105,9 @@ document.addEventListener('alpine:init', () => {
|
|||||||
this.valore = opzione.valore;
|
this.valore = opzione.valore;
|
||||||
this.aperto = false;
|
this.aperto = false;
|
||||||
this.testo = opzione.etichetta;
|
this.testo = opzione.etichetta;
|
||||||
|
// Per chi vuole reagire alla scelta senza passare dal server
|
||||||
|
// (l'anteprima della miniatura in Assegna).
|
||||||
|
this.$dispatch('scelta', { valore: opzione.valore, opzione });
|
||||||
if (daTocco) this.$refs.campo.blur();
|
if (daTocco) this.$refs.campo.blur();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -100,6 +118,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
svuota() {
|
svuota() {
|
||||||
this.valore = this.vuoto;
|
this.valore = this.vuoto;
|
||||||
this.testo = '';
|
this.testo = '';
|
||||||
|
this.$dispatch('scelta', { valore: this.vuoto, opzione: null });
|
||||||
this.$refs.campo.focus();
|
this.$refs.campo.focus();
|
||||||
this.aperto = true;
|
this.aperto = true;
|
||||||
this.evidenziato = 0;
|
this.evidenziato = 0;
|
||||||
|
|||||||
@@ -126,30 +126,28 @@
|
|||||||
{{-- Form --}}
|
{{-- Form --}}
|
||||||
<div x-show="formOpen" x-cloak class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-6 max-w-3xl">
|
<div x-show="formOpen" x-cloak class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-6 max-w-3xl">
|
||||||
<form wire:submit="save" class="space-y-4">
|
<form wire:submit="save" class="space-y-4">
|
||||||
<div>
|
{{-- L'anteprima segue la scelta nel browser (evento "scelta" di
|
||||||
|
<x-scelta>, miniatura nelle opzioni): scegliere un
|
||||||
|
territorio non fa più una richiesta al server. --}}
|
||||||
|
<div x-data="{ scelto: @js((bool) $territorio_id), anteprima: @js($anteprimaIniziale) }"
|
||||||
|
x-on:scelta="scelto = !! $event.detail.valore; anteprima = $event.detail.opzione?.miniatura ?? null">
|
||||||
<label for="territorio_id" class="block text-sm font-medium text-gray-700 mb-1">Territorio *</label>
|
<label for="territorio_id" class="block text-sm font-medium text-gray-700 mb-1">Territorio *</label>
|
||||||
<x-scelta id="territorio_id" wire:model.live="territorio_id"
|
<x-scelta id="territorio_id" wire:model="territorio_id"
|
||||||
:opzioni="$opzioniTerritori"
|
:opzioni="$opzioniTerritori"
|
||||||
segnaposto="Numero, zona o tipologia" :vuoto="null"
|
segnaposto="Numero, zona o tipologia" :vuoto="null"
|
||||||
:disabilitato="(bool) $preselectedTerritorioId" />
|
:disabilitato="(bool) $preselectedTerritorioId" />
|
||||||
@error('territorio_id') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
@error('territorio_id') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||||
|
|
||||||
@if($territorio_id)
|
<div class="mt-3" x-show="scelto" x-cloak>
|
||||||
<div class="mt-3">
|
|
||||||
<p class="text-xs text-gray-500 mb-2">Anteprima territorio</p>
|
<p class="text-xs text-gray-500 mb-2">Anteprima territorio</p>
|
||||||
@if($this->selectedThumbnailUrl)
|
<div class="overflow-hidden rounded-xl border border-gray-200 bg-gray-50 shadow-sm" x-show="anteprima">
|
||||||
<div class="overflow-hidden rounded-xl border border-gray-200 bg-gray-50 shadow-sm">
|
<img x-bind:src="anteprima" alt="Miniatura del territorio scelto" loading="lazy"
|
||||||
<img src="{{ $this->selectedThumbnailUrl }}"
|
|
||||||
alt="Thumbnail territorio selezionato"
|
|
||||||
class="block w-full h-auto max-h-[70vh] object-contain bg-white">
|
class="block w-full h-auto max-h-[70vh] object-contain bg-white">
|
||||||
</div>
|
</div>
|
||||||
@else
|
<div class="rounded-lg border border-dashed border-gray-300 bg-gray-50 px-4 py-6 text-sm text-gray-500" x-show="! anteprima">
|
||||||
<div class="rounded-lg border border-dashed border-gray-300 bg-gray-50 px-4 py-6 text-sm text-gray-500">
|
Nessuna miniatura disponibile per questo territorio.
|
||||||
Nessuna thumbnail disponibile per questo territorio.
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -179,115 +177,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Elenco territori attualmente assegnati --}}
|
{{-- Elenco dei territori assegnati: componente a sé, caricato subito
|
||||||
@if($assegnazioniAperte->count())
|
dopo la pagina (ElencoAssegnati, #[Lazy]). Le azioni del form non
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mt-2">
|
lo ridisegnano. --}}
|
||||||
<div class="px-5 py-4 border-b flex items-center gap-3" style="background:linear-gradient(135deg,#eef2ff,#e0e7ff);border-color:#c7d2fe">
|
<livewire:assegnazioni.elenco-assegnati />
|
||||||
<div class="h-8 w-8 rounded-lg flex items-center justify-center" style="background:#6366f1">
|
|
||||||
<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-sm font-semibold" style="color:#3730a3">Territori Assegnati ({{ $assegnazioniAperte->count() }})</h3>
|
|
||||||
<p class="text-xs" style="color:#4f46e5">Link PDF · stato invio · il link scade alla soglia rientro ({{ $sogliaRientro }} giorni)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Ricerca rapida: territorio, numero, zona, proclamatore in un solo campo --}}
|
|
||||||
<div class="px-5 py-3 border-b border-gray-100" style="background:#fafafa">
|
|
||||||
<div class="relative">
|
|
||||||
<svg class="h-4 w-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
|
||||||
<input wire:model.live.debounce.300ms="linkSearch" type="text"
|
|
||||||
placeholder="Cerca per territorio, numero, zona o proclamatore..."
|
|
||||||
class="block w-full rounded-lg border-gray-300 text-sm pl-9 focus:ring-indigo-500 focus:border-indigo-500">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divide-y divide-gray-100">
|
|
||||||
@forelse($assegnazioniAperteVisibili as $a)
|
|
||||||
@php
|
|
||||||
$pdfUrl = $a->shortPdfUrl();
|
|
||||||
$linkScaduto = ! auth()->check() && $a->data_consegna_presunta && $a->data_consegna_presunta->isPast();
|
|
||||||
@endphp
|
|
||||||
<div class="px-5 py-4 hover:bg-indigo-50/30 transition-colors">
|
|
||||||
{{-- Testata riga --}}
|
|
||||||
<div class="flex flex-wrap items-start justify-between gap-x-6 gap-y-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<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)
|
|
||||||
<span class="text-xs text-gray-400 bg-gray-100 rounded px-1.5 py-0.5">{{ $a->territorio?->zona?->nome }}</span>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3 text-sm flex-wrap">
|
|
||||||
<span class="font-medium text-gray-800">{{ $a->proclamatore?->nome_completo ?? 'N/A' }}</span>
|
|
||||||
<span class="text-gray-300">·</span>
|
|
||||||
<span class="text-gray-500 text-xs">{{ $a->assigned_at->format('d/m/Y') }}</span>
|
|
||||||
<span class="text-gray-300">·</span>
|
|
||||||
<span class="text-xs font-semibold px-2 py-0.5 rounded-full
|
|
||||||
{{ $a->giorni > 120 ? 'bg-red-50 text-red-600' : ($a->giorni > 90 ? 'bg-amber-50 text-amber-600' : 'bg-gray-100 text-gray-500') }}">
|
|
||||||
{{ $a->giorni }}g
|
|
||||||
</span>
|
|
||||||
</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">
|
|
||||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
||||||
Link scaduto — rigenerare dal dettaglio territorio
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Link attivo --}}
|
|
||||||
@elseif($pdfUrl)
|
|
||||||
<div x-data="{ copied: false }" class="mt-3 flex flex-wrap items-center gap-2">
|
|
||||||
<code class="flex-1 min-w-0 text-xs bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-gray-600 break-all select-all cursor-text" x-on:click="window.getSelection().selectAllChildren($el)">{{ $pdfUrl }}</code>
|
|
||||||
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona class="flex-none"
|
|
||||||
x-on:click="navigator.clipboard.writeText($el.closest('div').querySelector('code').textContent).then(() => { copied = true; setTimeout(() => copied = false, 2000) })"
|
|
||||||
title="Copia link" aria-label="Copia il link del PDF">
|
|
||||||
<svg x-show="!copied" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
|
||||||
<svg x-show="copied" x-cloak class="text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
||||||
</x-btn>
|
|
||||||
|
|
||||||
@if($a->proclamatore)
|
|
||||||
<x-whatsapp :proclamatore="$a->proclamatore" compatto class="flex-none"
|
|
||||||
:messaggio="'Ciao ' . $a->proclamatore->nome . ', ti è stato assegnato il territorio N° ' . $a->territorio?->numero . '. Ecco la piantina: ' . $pdfUrl" />
|
|
||||||
@endif
|
|
||||||
|
|
||||||
{{-- Flag Link Inviato --}}
|
|
||||||
{{-- Interruttore di stato, non un comando: lo stato "inviato"
|
|
||||||
si legge dalla variante (primary quando è acceso) e
|
|
||||||
aria-pressed lo dice anche a chi non lo vede. --}}
|
|
||||||
<x-btn :variante="$a->link_sent ? 'primary' : 'secondary'" tono="tenue" dimensione="sm" class="flex-none"
|
|
||||||
wire:click="toggleLinkSent({{ $a->id }})"
|
|
||||||
aria-pressed="{{ $a->link_sent ? 'true' : 'false' }}"
|
|
||||||
title="{{ $a->link_sent ? 'Link inviato — clicca per annullare' : 'Segna come inviato' }}">
|
|
||||||
@if($a->link_sent)
|
|
||||||
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
||||||
Link inviato
|
|
||||||
@else
|
|
||||||
<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 19l9 2-9-18-9 18 9-2zm0 0v-8"/></svg>
|
|
||||||
Non inviato
|
|
||||||
@endif
|
|
||||||
</x-btn>
|
|
||||||
|
|
||||||
<span x-show="copied" x-cloak class="flex-none text-xs text-green-600 font-medium">Copiato!</span>
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<p class="mt-2 text-xs text-gray-300">Nessun PDF disponibile</p>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@empty
|
|
||||||
<div class="px-5 py-8 text-center text-sm text-gray-400">Nessun risultato per "{{ $linkSearch }}".</div>
|
|
||||||
@endforelse
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mt-2 px-5 py-6 text-sm text-gray-500" role="status">
|
||||||
|
Caricamento dei territori assegnati…
|
||||||
|
</div>
|
||||||
110
resources/views/livewire/assegnazioni/elenco-assegnati.blade.php
Normal file
110
resources/views/livewire/assegnazioni/elenco-assegnati.blade.php
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<div>
|
||||||
|
@if($assegnazioni->count())
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mt-2" x-data="elencoCercabile">
|
||||||
|
{{-- Le icone delle righe, una volta sola: ogni riga le richiama con
|
||||||
|
<use>. Ripetute per esteso pesavano ~60 KB su 80 righe. --}}
|
||||||
|
<svg aria-hidden="true" style="position:absolute;width:0;height:0;overflow:hidden">
|
||||||
|
<symbol id="tm-i-copia" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></symbol>
|
||||||
|
<symbol id="tm-i-spunta" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></symbol>
|
||||||
|
<symbol id="tm-i-invio" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/></symbol>
|
||||||
|
<symbol id="tm-i-avviso" viewBox="0 0 24 24"><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"/></symbol>
|
||||||
|
<symbol id="tm-i-whatsapp" viewBox="0 0 24 24"><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"/></symbol>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class="px-5 py-4 border-b flex items-center gap-3" style="background:linear-gradient(135deg,#eef2ff,#e0e7ff);border-color:#c7d2fe">
|
||||||
|
<div class="h-8 w-8 rounded-lg flex items-center justify-center" style="background:#6366f1">
|
||||||
|
<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold" style="color:#3730a3">Territori Assegnati ({{ $assegnazioni->count() }})</h3>
|
||||||
|
<p class="text-xs" style="color:#4f46e5">Link PDF · stato invio · il link scade alla soglia rientro ({{ $sogliaRientro }} giorni)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Ricerca rapida: filtra nel browser, nessuna richiesta al server --}}
|
||||||
|
<div class="px-5 py-3 border-b border-gray-100" style="background:#fafafa">
|
||||||
|
<div class="relative">
|
||||||
|
<svg class="h-4 w-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||||
|
<input x-model="q" type="search" inputmode="search" autocomplete="off"
|
||||||
|
aria-label="Cerca tra i territori assegnati"
|
||||||
|
placeholder="Cerca per territorio, numero, zona o proclamatore..."
|
||||||
|
class="block w-full rounded-lg border-gray-300 text-base sm:text-sm pl-9 focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divide-y divide-gray-100">
|
||||||
|
@foreach($assegnazioni as $a)
|
||||||
|
@php
|
||||||
|
$pdfUrl = $a->shortPdfUrl();
|
||||||
|
$whatsapp = $pdfUrl && $a->proclamatore
|
||||||
|
? $a->proclamatore->whatsappUrl('Ciao ' . $a->proclamatore->nome . ', ti è stato assegnato il territorio N° ' . $a->territorio?->numero . '. Ecco la piantina: ' . $pdfUrl)
|
||||||
|
: null;
|
||||||
|
$nome = $a->proclamatore?->nome_completo ?? 'N/A';
|
||||||
|
@endphp
|
||||||
|
<div wire:key="assegnato-{{ $a->id }}" data-cerca="{{ $cerca[$a->id] }}" x-show="corrisponde($el.dataset.cerca)" class="px-5 py-4">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-x-6 gap-y-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<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)
|
||||||
|
<span class="text-xs text-gray-400 bg-gray-100 rounded px-1.5 py-0.5">{{ $a->territorio->zona->nome }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 text-sm flex-wrap">
|
||||||
|
<span class="font-medium text-gray-800">{{ $nome }}</span>
|
||||||
|
<span class="text-gray-300">·</span>
|
||||||
|
<span class="text-gray-500 text-xs">{{ $a->assigned_at->format('d/m/Y') }}</span>
|
||||||
|
<span class="text-gray-300">·</span>
|
||||||
|
<span class="text-xs font-semibold px-2 py-0.5 rounded-full {{ $a->giorni > 120 ? 'bg-red-50 text-red-600' : ($a->giorni > 90 ? 'bg-amber-50 text-amber-600' : 'bg-gray-100 text-gray-500') }}">{{ $a->giorni }}g</span>
|
||||||
|
</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" stroke="currentColor" aria-hidden="true"><use href="#tm-i-avviso"/></svg>
|
||||||
|
Link ancora da inviare
|
||||||
|
{{-- L'invio si apre nel componente della pagina (Assegna::apriInvio). --}}
|
||||||
|
<x-btn variante="secondary" tono="tenue" dimensione="sm" wire:click="$dispatch('apri-invio', { assegnazioneId: {{ $a->id }} })">Invia link</x-btn>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($pdfUrl)
|
||||||
|
<div x-data="{ copied: false }" class="mt-3 flex flex-wrap items-center gap-2">
|
||||||
|
<code class="flex-1 min-w-0 text-xs bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-gray-600 break-all select-all cursor-text" x-on:click="window.getSelection().selectAllChildren($el)">{{ $pdfUrl }}</code>
|
||||||
|
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona class="flex-none"
|
||||||
|
x-on:click="navigator.clipboard.writeText($el.closest('div').querySelector('code').textContent).then(() => { copied = true; setTimeout(() => copied = false, 2000) })"
|
||||||
|
title="Copia link" aria-label="Copia il link del PDF">
|
||||||
|
<svg x-show="!copied" fill="none" stroke="currentColor" aria-hidden="true"><use href="#tm-i-copia"/></svg>
|
||||||
|
<svg x-show="copied" x-cloak class="text-green-600" fill="none" stroke="currentColor" aria-hidden="true"><use href="#tm-i-spunta"/></svg>
|
||||||
|
</x-btn>
|
||||||
|
|
||||||
|
@if($whatsapp)
|
||||||
|
<a href="{{ $whatsapp }}" target="_blank" rel="noopener" class="tm-whatsapp tm-whatsapp-compatto flex-none"
|
||||||
|
title="Scrivi su WhatsApp a {{ $nome }}" aria-label="Scrivi su WhatsApp a {{ $nome }}">
|
||||||
|
<svg fill="none" stroke="currentColor" aria-hidden="true"><use href="#tm-i-whatsapp"/></svg>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Interruttore di stato, non un comando: lo stato "inviato"
|
||||||
|
si legge dalla variante (primary quando è acceso) e
|
||||||
|
aria-pressed lo dice anche a chi non lo vede. --}}
|
||||||
|
<x-btn :variante="$a->link_sent ? 'primary' : 'secondary'" tono="tenue" dimensione="sm" class="flex-none"
|
||||||
|
wire:click="toggleLinkSent({{ $a->id }})"
|
||||||
|
aria-pressed="{{ $a->link_sent ? 'true' : 'false' }}"
|
||||||
|
title="{{ $a->link_sent ? 'Link inviato — clicca per annullare' : 'Segna come inviato' }}">
|
||||||
|
<svg fill="none" stroke="currentColor" aria-hidden="true"><use href="{{ $a->link_sent ? '#tm-i-spunta' : '#tm-i-invio' }}"/></svg>
|
||||||
|
{{ $a->link_sent ? 'Link inviato' : 'Non inviato' }}
|
||||||
|
</x-btn>
|
||||||
|
|
||||||
|
<span x-show="copied" x-cloak class="flex-none text-xs text-green-600 font-medium">Copiato!</span>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="mt-2 text-xs text-gray-300">Nessun PDF disponibile</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
<div x-show="nessuno" x-cloak class="px-5 py-8 text-center text-sm text-gray-400">Nessun risultato per «<span x-text="q"></span>».</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{{-- Da assegnare --}}
|
{{-- Da assegnare --}}
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden card-hover">
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden card-hover">
|
||||||
<div class="px-5 py-4 border-b flex items-center gap-3" style="background:linear-gradient(135deg,#f0fdf4,#dcfce7);border-color:#bbf7d0">
|
<div class="px-5 py-4 border-b flex flex-wrap items-center gap-3" style="background:linear-gradient(135deg,#f0fdf4,#dcfce7);border-color:#bbf7d0">
|
||||||
<div class="h-8 w-8 rounded-lg flex items-center justify-center" style="background:#22c55e">
|
<div class="h-8 w-8 rounded-lg flex items-center justify-center" style="background:#22c55e">
|
||||||
<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,14 +90,20 @@
|
|||||||
<h3 class="text-sm font-semibold" style="color:#166534">Da Assegnare</h3>
|
<h3 class="text-sm font-semibold" style="color:#166534">Da Assegnare</h3>
|
||||||
<p class="text-xs" style="color:#15803d">Prima i prioritari, poi i territori con più tempo in reparto</p>
|
<p class="text-xs" style="color:#15803d">Prima i prioritari, poi i territori con più tempo in reparto</p>
|
||||||
</div>
|
</div>
|
||||||
|
{{-- "Vedi tutti" in testata, non in fondo all'elenco. --}}
|
||||||
|
<div class="ml-auto flex flex-none items-center gap-2">
|
||||||
|
@if($territoriDaAssegnare->count() >= $homeLimit)
|
||||||
|
<x-btn variante="secondary" tono="tenue" dimensione="sm" :href="route('territori.index', ['filterStato' => 'in_reparto'])">Vedi tutti</x-btn>
|
||||||
|
@endif
|
||||||
@hasanyrole('amministratore|assistente')
|
@hasanyrole('amministratore|assistente')
|
||||||
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona class="ml-auto"
|
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona
|
||||||
wire:click="downloadPdfDaAssegnare"
|
wire:click="downloadPdfDaAssegnare"
|
||||||
title="Scarica PDF" aria-label="Scarica in PDF l elenco dei territori da assegnare">
|
title="Scarica PDF" aria-label="Scarica in PDF l elenco dei territori da assegnare">
|
||||||
<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 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></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 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||||
</x-btn>
|
</x-btn>
|
||||||
@endhasanyrole
|
@endhasanyrole
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<ul class="divide-y divide-gray-100">
|
<ul class="divide-y divide-gray-100">
|
||||||
@forelse($territoriDaAssegnare as $t)
|
@forelse($territoriDaAssegnare as $t)
|
||||||
<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">
|
||||||
@@ -126,16 +132,11 @@
|
|||||||
</li>
|
</li>
|
||||||
@endforelse
|
@endforelse
|
||||||
</ul>
|
</ul>
|
||||||
@if($territoriDaAssegnare->count() >= $homeLimit)
|
|
||||||
<div class="px-5 py-3 border-t text-center" style="background:#fafafa">
|
|
||||||
<a href="{{ route('territori.index') }}?filterStato=in_reparto" class="text-xs font-medium" style="color:#4f46e5">Vedi tutti →</a>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Da rientrare --}}
|
{{-- Da rientrare --}}
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden card-hover">
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden card-hover">
|
||||||
<div class="px-5 py-4 border-b flex items-center gap-3" style="background:linear-gradient(135deg,#fef2f2,#fee2e2);border-color:#fecaca">
|
<div class="px-5 py-4 border-b flex flex-wrap items-center gap-3" style="background:linear-gradient(135deg,#fef2f2,#fee2e2);border-color:#fecaca">
|
||||||
<div class="h-8 w-8 rounded-lg flex items-center justify-center" style="background:#ef4444">
|
<div class="h-8 w-8 rounded-lg flex items-center justify-center" style="background:#ef4444">
|
||||||
<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,16 +144,21 @@
|
|||||||
<h3 class="text-sm font-semibold" style="color:#991b1b">Da Rientrare</h3>
|
<h3 class="text-sm font-semibold" style="color:#991b1b">Da Rientrare</h3>
|
||||||
<p class="text-xs" style="color:#b91c1c">Territori assegnati da più tempo</p>
|
<p class="text-xs" style="color:#b91c1c">Territori assegnati da più tempo</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ml-auto flex flex-none items-center gap-2">
|
||||||
|
@if($daRientrare->count() >= $homeLimit)
|
||||||
|
<x-btn variante="secondary" tono="tenue" dimensione="sm" :href="route('territori.index', ['filterStato' => 'da_rientrare'])">Vedi tutti</x-btn>
|
||||||
|
@endif
|
||||||
@hasanyrole('amministratore|assistente')
|
@hasanyrole('amministratore|assistente')
|
||||||
{{-- Scaricare un elenco non è distruttivo: era rosso solo perché
|
{{-- Scaricare un elenco non è distruttivo: era rosso solo perché
|
||||||
lo è la scheda che lo contiene. --}}
|
lo è la scheda che lo contiene. --}}
|
||||||
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona class="ml-auto"
|
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona
|
||||||
wire:click="downloadPdfDaRientrare"
|
wire:click="downloadPdfDaRientrare"
|
||||||
title="Scarica PDF" aria-label="Scarica in PDF l elenco dei territori da rientrare">
|
title="Scarica PDF" aria-label="Scarica in PDF l elenco dei territori da rientrare">
|
||||||
<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 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></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 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||||
</x-btn>
|
</x-btn>
|
||||||
@endhasanyrole
|
@endhasanyrole
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<ul class="divide-y divide-gray-100">
|
<ul class="divide-y divide-gray-100">
|
||||||
@forelse($daRientrare as $t)
|
@forelse($daRientrare as $t)
|
||||||
@php($assegnazioneCorrente = $t->assegnazioneCorrente)
|
@php($assegnazioneCorrente = $t->assegnazioneCorrente)
|
||||||
@@ -177,11 +183,6 @@
|
|||||||
</li>
|
</li>
|
||||||
@endforelse
|
@endforelse
|
||||||
</ul>
|
</ul>
|
||||||
@if($daRientrare->count() >= $homeLimit)
|
|
||||||
<div class="px-5 py-3 border-t text-center" style="background:#fafafa">
|
|
||||||
<a href="{{ route('territori.index') }}?filterStato=da_rientrare" class="text-xs font-medium" style="color:#4f46e5">Vedi tutti →</a>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
109
tests/Feature/AssegnaPrestazioniTest.php
Normal file
109
tests/Feature/AssegnaPrestazioniTest.php
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Livewire\Assegnazioni\Assegna;
|
||||||
|
use App\Livewire\Assegnazioni\ElencoAssegnati;
|
||||||
|
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\Models\Zona;
|
||||||
|
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* La pagina Assegna era lenta: ~400 KB rimandati per intero a ogni
|
||||||
|
* interazione. Questi test tengono fermi i rimedi: elenco in un componente
|
||||||
|
* lazy, ricerca e anteprima nel browser, righe leggere.
|
||||||
|
*/
|
||||||
|
class AssegnaPrestazioniTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private User $admin;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->withoutVite();
|
||||||
|
Storage::fake('public');
|
||||||
|
$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');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** $n assegnazioni aperte, ognuna con PDF e un proclamatore con telefono. */
|
||||||
|
private function assegnazioni(int $n): void
|
||||||
|
{
|
||||||
|
$zona = Zona::create(['nome' => 'Montepellegrino', 'attivo' => true]);
|
||||||
|
for ($i = 1; $i <= $n; $i++) {
|
||||||
|
Storage::disk('public')->put("territori-pdf/t{$i}.pdf", '%PDF-1.4');
|
||||||
|
$t = Territorio::create(['numero' => (string) $i, 'attivo' => true, 'zona_id' => $zona->id, 'pdf_path' => "territori-pdf/t{$i}.pdf"]);
|
||||||
|
$p = Proclamatore::create(['nome' => "Nicolò{$i}", 'cognome' => 'Rossi', 'attivo' => true, 'telefono' => '3331234567']);
|
||||||
|
Assegnazione::create([
|
||||||
|
'territorio_id' => $t->id, 'proclamatore_id' => $p->id, 'anno_teocratico_id' => AnnoTeocratico::sole()->id,
|
||||||
|
'assigned_at' => now()->subDays($i), 'created_by' => $this->admin->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_la_pagina_non_contiene_piu_l_elenco_che_arriva_dopo(): void
|
||||||
|
{
|
||||||
|
$this->assegnazioni(5);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->get(route('assegnazioni.assegna'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Caricamento dei territori assegnati')
|
||||||
|
->assertDontSee('Territori Assegnati (5)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_scegliere_un_territorio_non_chiede_niente_al_server(): void
|
||||||
|
{
|
||||||
|
$html = $this->actingAs($this->admin)->get(route('assegnazioni.assegna'))->getContent();
|
||||||
|
|
||||||
|
// wire:model senza .live: il valore parte solo con "Assegna".
|
||||||
|
$this->assertStringContainsString('wire:model="territorio_id"', $html);
|
||||||
|
$this->assertStringNotContainsString('wire:model.live="territorio_id"', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_le_opzioni_portano_la_miniatura_per_l_anteprima(): void
|
||||||
|
{
|
||||||
|
$t = Territorio::create(['numero' => '7', 'attivo' => true, 'thumbnail_path' => 'territori-thumbnails/t7.png']);
|
||||||
|
|
||||||
|
$opzioni = Livewire::actingAs($this->admin)->test(Assegna::class)->viewData('opzioniTerritori');
|
||||||
|
|
||||||
|
$this->assertSame($t->miniaturaUrl(), $opzioni[0]['miniatura']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_l_elenco_si_cerca_nel_browser_senza_accenti(): void
|
||||||
|
{
|
||||||
|
$this->assegnazioni(1);
|
||||||
|
|
||||||
|
Livewire::withoutLazyLoading()->actingAs($this->admin)->test(ElencoAssegnati::class)
|
||||||
|
->assertSee('Territori Assegnati (1)')
|
||||||
|
->assertSeeHtml('data-cerca="1 montepellegrino rossi nicolo1"')
|
||||||
|
->assertDontSeeHtml('wire:model.live.debounce.300ms="linkSearch"');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_le_righe_dell_elenco_restano_leggere(): void
|
||||||
|
{
|
||||||
|
$this->assegnazioni(30);
|
||||||
|
|
||||||
|
$html = Livewire::withoutLazyLoading()->actingAs($this->admin)->test(ElencoAssegnati::class)->html();
|
||||||
|
|
||||||
|
// Prima ~3,8 KB a riga. Le icone sono simboli richiamati con <use>.
|
||||||
|
$this->assertLessThan(2600, strlen($html) / 30, 'Ogni riga dell\'elenco deve restare sotto i 2,6 KB');
|
||||||
|
$this->assertSame(1, substr_count($html, '<symbol id="tm-i-copia"'));
|
||||||
|
$this->assertGreaterThanOrEqual(30, substr_count($html, 'href="#tm-i-copia"'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Livewire\Assegnazioni\Assegna;
|
use App\Livewire\Assegnazioni\Assegna;
|
||||||
|
use App\Livewire\Assegnazioni\ElencoAssegnati;
|
||||||
use App\Livewire\Proclamatori\ProclamatoreCreate;
|
use App\Livewire\Proclamatori\ProclamatoreCreate;
|
||||||
use App\Livewire\Settings\XmlExchange;
|
use App\Livewire\Settings\XmlExchange;
|
||||||
use App\Models\AnnoTeocratico;
|
use App\Models\AnnoTeocratico;
|
||||||
@@ -245,11 +246,15 @@ class ContattiProclamatoriTest extends TestCase
|
|||||||
$this->assegna($mario, $this->territorioConPdf());
|
$this->assegna($mario, $this->territorioConPdf());
|
||||||
$assegnazione = Assegnazione::sole();
|
$assegnazione = Assegnazione::sole();
|
||||||
|
|
||||||
// La pagina viene chiusa senza rispondere: l'invio è perso, l'avviso resta.
|
// La pagina viene chiusa senza rispondere: l'invio è perso, l'avviso resta
|
||||||
|
// nell'elenco, e "Invia link" chiede alla pagina di riaprirlo.
|
||||||
|
Livewire::withoutLazyLoading()->actingAs($this->admin)->test(ElencoAssegnati::class)
|
||||||
|
->assertSee('Link ancora da inviare')
|
||||||
|
->assertSeeHtml("\$dispatch('apri-invio', { assegnazioneId: {$assegnazione->id} })");
|
||||||
|
|
||||||
Livewire::actingAs($this->admin)->test(Assegna::class)
|
Livewire::actingAs($this->admin)->test(Assegna::class)
|
||||||
->assertSet('invioId', null)
|
->assertSet('invioId', null)
|
||||||
->assertSee('Link ancora da inviare')
|
->dispatch('apri-invio', assegnazioneId: $assegnazione->id)
|
||||||
->call('apriInvio', $assegnazione->id)
|
|
||||||
->assertSee('Hai inviato il link?')
|
->assertSee('Hai inviato il link?')
|
||||||
->call('concludiInvio', false)
|
->call('concludiInvio', false)
|
||||||
->assertRedirect(route('assegnazioni.assegna'));
|
->assertRedirect(route('assegnazioni.assegna'));
|
||||||
@@ -260,7 +265,7 @@ class ContattiProclamatoriTest extends TestCase
|
|||||||
->assertSee('Il link della piantina è ancora da inviare');
|
->assertSee('Il link della piantina è ancora da inviare');
|
||||||
|
|
||||||
// Segnato come inviato dall'elenco: l'avviso sparisce.
|
// Segnato come inviato dall'elenco: l'avviso sparisce.
|
||||||
Livewire::actingAs($this->admin)->test(Assegna::class)
|
Livewire::withoutLazyLoading()->actingAs($this->admin)->test(ElencoAssegnati::class)
|
||||||
->call('toggleLinkSent', $assegnazione->id)
|
->call('toggleLinkSent', $assegnazione->id)
|
||||||
->assertDontSee('Link ancora da inviare');
|
->assertDontSee('Link ancora da inviare');
|
||||||
$this->assertFalse($assegnazione->fresh()->link_in_sospeso);
|
$this->assertFalse($assegnazione->fresh()->link_in_sospeso);
|
||||||
|
|||||||
Reference in New Issue
Block a user