++ Il mio gruppo: il numero del territorio apre l'anteprima
Il numero diventa un pulsante che apre una finestra con la miniatura e il collegamento al PDF. La miniatura passa dalla rotta nuova mio-gruppo.miniatura, con la stessa regola del PDF: solo i territori del pacchetto del gruppo sorvegliato. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
16
HANDOFF.md
16
HANDOFF.md
@@ -111,6 +111,19 @@ lavoro nuovi, stesso master), `php-fpm -i` riporta
|
|||||||
`session.upload_progress.enabled => Off` e `session.use_strict_mode => On`,
|
`session.upload_progress.enabled => Off` e `session.use_strict_mode => On`,
|
||||||
login 200 su entrambi.
|
login 200 su entrambi.
|
||||||
|
|
||||||
|
### Anteprima dal numero in "Il mio gruppo" (21 ottobre 2026, solo su dev)
|
||||||
|
|
||||||
|
Richiesta dell'utente: anche il sorvegliante deve poter vedere l'anteprima del
|
||||||
|
territorio, con un popup al clic del numero. Il numero diventa un pulsante
|
||||||
|
(stesso aspetto, `button.tm-assegnato-numero` in `app.css`) che apre una
|
||||||
|
finestra (`tm-modale`, stato Alpine `anteprima` sulla radice della vista) con
|
||||||
|
la miniatura e "Apri la piantina (PDF)". La miniatura passa da una rotta
|
||||||
|
nuova, `mio-gruppo.miniatura` (`TerritorioFileController::miniaturaGruppo`,
|
||||||
|
`Territorio::miniaturaGruppoUrl()`), con la stessa regola di
|
||||||
|
`mio-gruppo.pdf`: solo i territori del pacchetto del gruppo che l'utente
|
||||||
|
sorveglia. La rotta normale delle miniature resta chiusa al sorvegliante.
|
||||||
|
Senza miniatura né PDF il numero resta testo.
|
||||||
|
|
||||||
### Assegna aperta dalla Home: senza l'elenco dei territori assegnati (20 ottobre 2026)
|
### Assegna aperta dalla Home: senza l'elenco dei territori assegnati (20 ottobre 2026)
|
||||||
|
|
||||||
Richiesta dell'utente: "quando provengo da un territorio dalla home, non
|
Richiesta dell'utente: "quando provengo da un territorio dalla home, non
|
||||||
@@ -197,7 +210,8 @@ qui sotto **com'è oggi**, in fondo la storia.
|
|||||||
(nell'indirizzo). Il sorvegliante assegna, manda il link (copia o
|
(nell'indirizzo). Il sorvegliante assegna, manda il link (copia o
|
||||||
condividi: trait `Concerns\InviaLink` + partial `assegnazioni/_invio-link`,
|
condividi: trait `Concerns\InviaLink` + partial `assegnazioni/_invio-link`,
|
||||||
gli stessi di *Assegna*), registra i rientri, apre la piantina, rimanda in
|
gli stessi di *Assegna*), registra i rientri, apre la piantina, rimanda in
|
||||||
reparto quelli non dati.
|
reparto quelli non dati. Il numero del territorio apre l'anteprima (finestra
|
||||||
|
Alpine nella vista, miniatura e collegamento al PDF).
|
||||||
- **Assegnazione dal pacchetto**: un'**assegnazione normale**, con
|
- **Assegnazione dal pacchetto**: un'**assegnazione normale**, con
|
||||||
`gruppo_servizio_id` e `gruppo_nome` ("via gruppo …" in registro e schede).
|
`gruppo_servizio_id` e `gruppo_nome` ("via gruppo …" in registro e schede).
|
||||||
`GruppiServizio::assegna()` rilegge territorio e membro e ricontrolla il
|
`GruppiServizio::assegna()` rilegge territorio e membro e ricontrolla il
|
||||||
|
|||||||
@@ -774,7 +774,8 @@ docker compose --profile dev up -d --build
|
|||||||
pacchetto è in ordine di priorità (prima i da dare, prioritari in testa,
|
pacchetto è in ordine di priorità (prima i da dare, prioritari in testa,
|
||||||
poi quelli già dati) e si filtra per stato, zona, tipologia e "solo
|
poi quelli già dati) e si filtra per stato, zona, tipologia e "solo
|
||||||
prioritari"; sotto il nome del gruppo compaiono nome e cognome del
|
prioritari"; sotto il nome del gruppo compaiono nome e cognome del
|
||||||
sorvegliante
|
sorvegliante. Toccando il numero di un territorio si apre l'**anteprima**
|
||||||
|
(la miniatura della piantina) con il collegamento al PDF
|
||||||
- Al **rientro** il territorio torna in reparto ed esce dal pacchetto
|
- Al **rientro** il territorio torna in reparto ed esce dal pacchetto
|
||||||
- **Sciogliere il gruppo** toglie il ruolo al sorvegliante e **elimina il
|
- **Sciogliere il gruppo** toglie il ruolo al sorvegliante e **elimina il
|
||||||
gruppo**, che non resta nello storico: i territori solo nel pacchetto
|
gruppo**, che non resta nello storico: i territori solo nel pacchetto
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ class TerritorioFileController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function pdfGruppo(Territorio $territorio): Response
|
public function pdfGruppo(Territorio $territorio): Response
|
||||||
{
|
{
|
||||||
$gruppo = auth()->user()->gruppoSorvegliato();
|
$this->delPacchetto($territorio);
|
||||||
abort_unless($gruppo && $territorio->gruppo_servizio_id === $gruppo->id, 403);
|
|
||||||
|
|
||||||
return $this->pdf($territorio);
|
return $this->pdf($territorio);
|
||||||
}
|
}
|
||||||
@@ -42,6 +41,21 @@ class TerritorioFileController extends Controller
|
|||||||
return $this->servi($territorio->thumbnail_path, null, null);
|
return $this->servi($territorio->thumbnail_path, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** La miniatura per l'anteprima in "Il mio gruppo", con la stessa regola del PDF. */
|
||||||
|
public function miniaturaGruppo(Territorio $territorio): Response
|
||||||
|
{
|
||||||
|
$this->delPacchetto($territorio);
|
||||||
|
|
||||||
|
return $this->miniatura($territorio);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Il territorio deve stare nel pacchetto del gruppo che l'utente sorveglia. */
|
||||||
|
private function delPacchetto(Territorio $territorio): void
|
||||||
|
{
|
||||||
|
$gruppo = auth()->user()->gruppoSorvegliato();
|
||||||
|
abort_unless($gruppo && $territorio->gruppo_servizio_id === $gruppo->id, 403);
|
||||||
|
}
|
||||||
|
|
||||||
public function versione(TerritorioPdfVersione $versione): Response
|
public function versione(TerritorioPdfVersione $versione): Response
|
||||||
{
|
{
|
||||||
$numero = $versione->territorio?->numero ?? 'senza-territorio';
|
$numero = $versione->territorio?->numero ?? 'senza-territorio';
|
||||||
|
|||||||
@@ -328,4 +328,12 @@ class Territorio extends Model
|
|||||||
? route('territori.file.miniatura', ['territorio' => $this->id, 'v' => substr(md5($this->thumbnail_path), 0, 8)])
|
? route('territori.file.miniatura', ['territorio' => $this->id, 'v' => substr(md5($this->thumbnail_path), 0, 8)])
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** La miniatura per il sorvegliante di gruppo ("Il mio gruppo"): solo del suo pacchetto. */
|
||||||
|
public function miniaturaGruppoUrl(): ?string
|
||||||
|
{
|
||||||
|
return $this->thumbnail_path
|
||||||
|
? route('mio-gruppo.miniatura', ['territorio' => $this->id, 'v' => substr(md5($this->thumbnail_path), 0, 8)])
|
||||||
|
: null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1805,6 +1805,14 @@ html { scroll-behavior: smooth; }
|
|||||||
}
|
}
|
||||||
.tm-assegnato-numero { color: #4f46e5; font-size: 14px; font-weight: 700; }
|
.tm-assegnato-numero { color: #4f46e5; font-size: 14px; font-weight: 700; }
|
||||||
.tm-assegnato-numero:hover { text-decoration: underline; }
|
.tm-assegnato-numero:hover { text-decoration: underline; }
|
||||||
|
/* Il numero come pulsante ("Il mio gruppo": apre l'anteprima): stesso aspetto. */
|
||||||
|
button.tm-assegnato-numero {
|
||||||
|
background: none; border: 0; padding: 0; cursor: pointer;
|
||||||
|
font-family: inherit; line-height: inherit;
|
||||||
|
}
|
||||||
|
button.tm-assegnato-numero:focus-visible {
|
||||||
|
outline: 2px solid var(--tm-accento); outline-offset: 2px; border-radius: 2px;
|
||||||
|
}
|
||||||
.tm-assegnato-zona {
|
.tm-assegnato-zona {
|
||||||
margin-left: 6px; padding: 2px 6px; border-radius: 4px;
|
margin-left: 6px; padding: 2px 6px; border-radius: 4px;
|
||||||
background: #f3f4f6; color: #9ca3af; font-size: 12px;
|
background: #f3f4f6; color: #9ca3af; font-size: 12px;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<div>
|
<div x-data="{ anteprima: null }">
|
||||||
{{-- Il mio gruppo (MioGruppo): la sola pagina del sorvegliante di gruppo.
|
{{-- Il mio gruppo (MioGruppo): la sola pagina del sorvegliante di gruppo.
|
||||||
Pensata prima per il telefono: schede una sotto l'altra, pulsanti
|
Pensata prima per il telefono: schede una sotto l'altra, pulsanti
|
||||||
grandi. Copia, Condividi e ricerca vengono da elencoCercabile
|
grandi. Copia, Condividi e ricerca vengono da elencoCercabile
|
||||||
(resources/js/app.js), l'invio del link dal partial _invio-link. --}}
|
(resources/js/app.js), l'invio del link dal partial _invio-link.
|
||||||
|
Il numero del territorio apre l'anteprima (stato Alpine `anteprima`,
|
||||||
|
finestra in fondo alla vista): miniatura e PDF dalle rotte
|
||||||
|
mio-gruppo.*, che rispondono solo per il pacchetto del gruppo. --}}
|
||||||
@if(! $gruppo)
|
@if(! $gruppo)
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 text-sm text-gray-600">
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 text-sm text-gray-600">
|
||||||
Non sei il sorvegliante di un gruppo di servizio attivo.
|
Non sei il sorvegliante di un gruppo di servizio attivo.
|
||||||
@@ -81,7 +84,11 @@
|
|||||||
@endphp
|
@endphp
|
||||||
<div wire:key="pacchetto-{{ $t->id }}" data-cerca="{{ $cerca }}" x-show="corrisponde($el.dataset.cerca)" class="tm-assegnato">
|
<div wire:key="pacchetto-{{ $t->id }}" data-cerca="{{ $cerca }}" x-show="corrisponde($el.dataset.cerca)" class="tm-assegnato">
|
||||||
<div class="tm-assegnato-testa">
|
<div class="tm-assegnato-testa">
|
||||||
<span><span class="tm-assegnato-numero">N° {{ $t->numero }}</span> @if($t->zona?->nome)<span class="tm-assegnato-zona">{{ $t->zona->nome }}</span>@endif
|
<span>@if($t->thumbnail_path || $t->pdf_path)<button type="button" class="tm-assegnato-numero" title="Anteprima del territorio"
|
||||||
|
data-titolo="Territorio N° {{ $t->numero }}{{ $t->zona?->nome ? ' — ' . $t->zona->nome : '' }}"
|
||||||
|
data-miniatura="{{ $t->miniaturaGruppoUrl() }}"
|
||||||
|
data-pdf="{{ $t->pdf_path ? route('mio-gruppo.pdf', $t) : '' }}"
|
||||||
|
x-on:click="anteprima = { titolo: $el.dataset.titolo, miniatura: $el.dataset.miniatura, pdf: $el.dataset.pdf }">N° {{ $t->numero }}</button>@else<span class="tm-assegnato-numero">N° {{ $t->numero }}</span>@endif @if($t->zona?->nome)<span class="tm-assegnato-zona">{{ $t->zona->nome }}</span>@endif
|
||||||
@if($t->in_priorita)<span class="ml-1 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide" style="background:#fef3c7;color:#92400e">★ Prioritario</span>@endif</span>
|
@if($t->in_priorita)<span class="ml-1 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide" style="background:#fef3c7;color:#92400e">★ Prioritario</span>@endif</span>
|
||||||
@if($a)
|
@if($a)
|
||||||
<span class="tm-assegnato-info"><b>{{ $a->proclamatore?->nome_completo }}</b> · dal {{ $a->assigned_at->format('d/m/Y') }} · <span class="tm-assegnato-giorni {{ $a->giorni > 120 ? 'tm-g-tardi' : ($a->giorni > 90 ? 'tm-g-presto' : '') }}">{{ $a->giorni }}g</span></span>
|
<span class="tm-assegnato-info"><b>{{ $a->proclamatore?->nome_completo }}</b> · dal {{ $a->assigned_at->format('d/m/Y') }} · <span class="tm-assegnato-giorni {{ $a->giorni > 120 ? 'tm-g-tardi' : ($a->giorni > 90 ? 'tm-g-presto' : '') }}">{{ $a->giorni }}g</span></span>
|
||||||
@@ -156,5 +163,27 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{{-- Anteprima del territorio: si apre dal numero, si chiude con la X,
|
||||||
|
con Esc o toccando fuori. --}}
|
||||||
|
<div class="tm-modale-sfondo" x-show="anteprima" x-cloak x-transition.opacity
|
||||||
|
x-on:click.self="anteprima = null" x-on:keydown.escape.window="anteprima = null">
|
||||||
|
<div class="tm-modale" role="dialog" aria-modal="true" aria-labelledby="anteprima-titolo">
|
||||||
|
<header class="tm-modale-testata">
|
||||||
|
<h2 id="anteprima-titolo" x-text="anteprima?.titolo"></h2>
|
||||||
|
<button type="button" class="tm-editor-chiudi" x-on:click="anteprima = null" aria-label="Chiudi l'anteprima">×</button>
|
||||||
|
</header>
|
||||||
|
<div class="tm-modale-corpo">
|
||||||
|
<template x-if="anteprima?.miniatura">
|
||||||
|
<img x-bind:src="anteprima.miniatura" x-bind:alt="'Anteprima: ' + anteprima.titolo"
|
||||||
|
class="mx-auto max-w-full rounded-lg border border-gray-200 bg-gray-50">
|
||||||
|
</template>
|
||||||
|
<p x-show="! anteprima?.miniatura" class="text-sm text-gray-500">Per questo territorio non c'è ancora un'anteprima.</p>
|
||||||
|
</div>
|
||||||
|
<footer class="tm-modale-piede" x-show="anteprima?.pdf">
|
||||||
|
<a x-bind:href="anteprima?.pdf" target="_blank" rel="noopener" class="tm-collegamento">Apri la piantina (PDF)</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||||||
Route::get('mio-gruppo', MioGruppo::class)->name('mio-gruppo');
|
Route::get('mio-gruppo', MioGruppo::class)->name('mio-gruppo');
|
||||||
Route::get('mio-gruppo/territori/{territorio}/pdf', [TerritorioFileController::class, 'pdfGruppo'])
|
Route::get('mio-gruppo/territori/{territorio}/pdf', [TerritorioFileController::class, 'pdfGruppo'])
|
||||||
->name('mio-gruppo.pdf');
|
->name('mio-gruppo.pdf');
|
||||||
|
Route::get('mio-gruppo/territori/{territorio}/miniatura', [TerritorioFileController::class, 'miniaturaGruppo'])
|
||||||
|
->name('mio-gruppo.miniatura');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Campagne — visualizzazione per chi ha campagne.manage; sola lettura per i
|
// Campagne — visualizzazione per chi ha campagne.manage; sola lettura per i
|
||||||
|
|||||||
@@ -352,6 +352,29 @@ class GruppiServizioTest extends TestCase
|
|||||||
$this->assertSame('in_reparto', $territori[2]->fresh()->stato);
|
$this->assertSame('in_reparto', $territori[2]->fresh()->stato);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_il_sorvegliante_apre_l_anteprima_dal_numero_del_territorio(): void
|
||||||
|
{
|
||||||
|
['sorvegliante' => $s, 'territori' => $territori] = $this->gruppo();
|
||||||
|
$fuori = $this->territorio('88');
|
||||||
|
foreach ([$territori[0], $fuori] as $t) {
|
||||||
|
Storage::disk('public')->put("territori-thumbnails/t{$t->numero}.png", 'PNG');
|
||||||
|
$t->forceFill(['thumbnail_path' => "territori-thumbnails/t{$t->numero}.png"])->save();
|
||||||
|
}
|
||||||
|
$dodici = $territori[0]->fresh();
|
||||||
|
$this->flushSession();
|
||||||
|
|
||||||
|
// Il numero è un pulsante che porta miniatura e PDF del pacchetto.
|
||||||
|
$this->actingAs($s)->get(route('mio-gruppo'))->assertOk()
|
||||||
|
->assertSee('data-miniatura="' . $dodici->miniaturaGruppoUrl() . '"', false)
|
||||||
|
->assertSee('data-pdf="' . route('mio-gruppo.pdf', $dodici) . '"', false)
|
||||||
|
->assertSee('Apri la piantina (PDF)');
|
||||||
|
|
||||||
|
// La miniatura si apre solo per il proprio pacchetto, e solo da qui.
|
||||||
|
$this->actingAs($s)->get($dodici->miniaturaGruppoUrl())->assertOk();
|
||||||
|
$this->actingAs($s)->get(route('mio-gruppo.miniatura', $fuori))->assertForbidden();
|
||||||
|
$this->actingAs($s)->get($dodici->miniaturaUrl())->assertForbidden();
|
||||||
|
}
|
||||||
|
|
||||||
public function test_la_piantina_del_pacchetto_si_apre_solo_per_il_proprio_gruppo(): void
|
public function test_la_piantina_del_pacchetto_si_apre_solo_per_il_proprio_gruppo(): void
|
||||||
{
|
{
|
||||||
['sorvegliante' => $s, 'territori' => $territori] = $this->gruppo();
|
['sorvegliante' => $s, 'territori' => $territori] = $this->gruppo();
|
||||||
|
|||||||
Reference in New Issue
Block a user