Editor piantine: bozza automatica e pubblicazione con anteprima

- Il disegno è bozza e si salva da solo; i dati del territorio si
  modificano dall'editor e valgono subito nel gestionale.
- Un solo "Pubblica" (immagine + PDF), uguale in editor e gestione:
  anteprima preparata in coda su disco privato, conferma obbligatoria,
  rifiutata se nel frattempo è cambiato qualcosa di stampato.
- Stati Bozza / Pubblicata / Modifiche da pubblicare, calcolati
  sull'impronta di confine e dati stampati e sul PDF in uso.
- Gestione riscritta: stato, ricerca, filtri, storico paginato.
- Editor ridisegnato a tre zone, utilizzabile anche su tablet e telefono.
- Avvisi via evento (trait Avvisa + <x-avvisi>).
- .gitattributes: script shell sempre LF, altrimenti il container
  locale su Windows non parte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 12:08:34 +02:00
parent d433c9392d
commit 056d95cfcb
25 changed files with 3084 additions and 980 deletions

View File

@@ -0,0 +1,50 @@
{{--
Avvisi galleggianti: raccolgono l'evento "avviso" che i componenti
Livewire lanciano con il trait Avvisa, e che arriva anche dopo un'azione
che ridisegna solo il componente.
sessione: true per mostrare anche i messaggi flash della richiesta (il
telaio a schermo intero, che non ha una fascia messaggi sua). Il telaio
normale li mostra già in testa alla pagina, quindi lì resta false.
Gli avvisi di conferma si chiudono da soli; quelli di errore restano
finché non li si chiude, perché vanno letti.
--}}
@props(['sessione' => false])
@php
$iniziali = [];
if ($sessione && session()->has('success')) {
$iniziali[] = ['tipo' => 'ok', 'testo' => session('success')];
}
if ($sessione && session()->has('error')) {
$iniziali[] = ['tipo' => 'errore', 'testo' => session('error')];
}
@endphp
<div class="tm-avvisi" role="status" aria-live="polite"
x-data="{
avvisi: [],
prossimo: 0,
aggiungi(tipo, testo) {
const id = ++this.prossimo;
this.avvisi.push({ id, tipo, testo });
if (tipo !== 'errore') setTimeout(() => this.togli(id), 4500);
},
togli(id) { this.avvisi = this.avvisi.filter(a => a.id !== id); },
}"
x-init="@js($iniziali).forEach(a => aggiungi(a.tipo, a.testo))"
@avviso.window="aggiungi($event.detail.tipo, $event.detail.testo)">
<template x-for="avviso in avvisi" :key="avviso.id">
<div class="tm-avviso" :class="avviso.tipo === 'errore' ? 'tm-avviso-errore' : 'tm-avviso-ok'"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-end="opacity-0">
<svg x-show="avviso.tipo !== 'errore'" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
<svg x-show="avviso.tipo === 'errore'" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><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>
<span x-text="avviso.testo"></span>
<button type="button" @click="togli(avviso.id)" class="tm-avviso-chiudi" aria-label="Chiudi il messaggio">&times;</button>
</div>
</template>
</div>

View File

@@ -212,6 +212,11 @@
</main>
</div>
</div>
{{-- Gli avvisi delle azioni Livewire (trait Avvisa). I flash della
richiesta restano nella fascia in testa al contenuto, qui sopra. --}}
<x-avvisi />
@livewireScripts
</body>
</html>

View File

@@ -5,8 +5,8 @@
Serve a una cosa sola: la mappa dell'editor delle piantine, che deve
prendersi tutta la finestra. I messaggi non spingono il contenuto verso il
basso come nelle altre pagine — comparirebbe una fascia che sposta la mappa
mentre ci si sta disegnando sopra — ma galleggiano sopra, in alto al centro,
e si tolgono da soli.
mentre ci si sta disegnando sopra — ma galleggiano sopra, in alto al centro
(<x-avvisi>, che raccoglie anche quelli lanciati dalle azioni Livewire).
--}}
<html lang="it" class="h-full tm-app">
<head>
@@ -22,33 +22,7 @@
{{ $slot }}
</main>
<div class="tm-avvisi" role="status" aria-live="polite">
@if (session()->has('success'))
<div x-data="{ mostra: true }" x-show="mostra" x-cloak
x-init="setTimeout(() => mostra = false, 4500)"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-end="opacity-0"
class="tm-avviso tm-avviso-ok">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
<span>{{ session('success') }}</span>
<button type="button" @click="mostra = false" class="tm-avviso-chiudi" aria-label="Chiudi il messaggio">&times;</button>
</div>
@endif
@if (session()->has('error'))
<div x-data="{ mostra: true }" x-show="mostra" x-cloak
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-end="opacity-0"
class="tm-avviso tm-avviso-errore">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><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>
<span>{{ session('error') }}</span>
<button type="button" @click="mostra = false" class="tm-avviso-chiudi" aria-label="Chiudi il messaggio">&times;</button>
</div>
@endif
</div>
<x-avvisi :sessione="true" />
@livewireScripts
</body>

View File

@@ -1,7 +1,27 @@
<div class="tm-editor">
@php
use App\Models\TerritorioMappa;
$stato = $mappa?->stato();
$motivo = $mappa?->motivoNonPubblicabile();
$tagStato = [
TerritorioMappa::STATO_BOZZA => 'tm-tag-attesa',
TerritorioMappa::STATO_MODIFICATA => 'tm-tag-attesa',
TerritorioMappa::STATO_PUBBLICATA => 'tm-tag-ok',
];
@endphp
{{--
Editor a tre zone: elenco (a sinistra), mappa, pannello della piantina (a
destra; sotto i 720px sale dal basso). Elenco e mappa li disegna
resources/js/piantine.js e stanno in wire:ignore: Livewire ridisegna solo
testata e pannello. Elenco e pannello si aprono e chiudono in Alpine, senza
giri sul server.
--}}
<div class="tm-editor"
x-data="{ elenco: window.matchMedia('(min-width: 1100px)').matches, pannello: true }"
@piantina-selezionata.window="if ($event.detail.mappaId) pannello = true">
@vite('resources/js/piantine.js')
{{-- ── Barra strumenti ───────────────────────────────────────────────── --}}
{{-- ── Testata ───────────────────────────────────────────────────────── --}}
<header class="tm-editor-barra">
<a href="{{ route('piantine.index') }}" class="tm-editor-torna">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
@@ -10,188 +30,228 @@
<span class="tm-editor-sep" aria-hidden="true"></span>
<h1 class="tm-editor-titolo">
{{ $mappa?->etichetta() ?? 'Nuova piantina' }}
@if($geometriaInAttesa)
<span class="tm-tag tm-tag-attesa">disegno non salvato</span>
@elseif($mappa && ! $mappa->collegata())
<span class="tm-tag">scollegata</span>
<x-btn variante="ghost" dimensione="sm" icona @click="elenco = ! elenco" ::aria-pressed="elenco"
aria-label="Mostra o nascondi l'elenco delle piantine" title="Elenco delle piantine">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
</x-btn>
<div class="tm-editor-titolo">
<h1>{{ $mappa?->etichetta() ?? 'Editor delle piantine' }}</h1>
@if($mappa)
<span class="tm-tag {{ $tagStato[$stato] }}">{{ $mappa->statoEtichetta() }}</span>
<span class="tm-editor-salvato" aria-live="polite">
<span wire:loading.delay.shortest>Salvataggio…</span>
<span wire:loading.remove>
@if($salvatoAlle) Salvato alle {{ $salvatoAlle }} @endif
</span>
</span>
@endif
</h1>
</div>
<div class="tm-editor-azioni">
<x-btn variante="secondary" dimensione="sm" wire:click="nuova">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
Nuova
</x-btn>
<x-btn variante="secondary" dimensione="sm" wire:click="$toggle('pannelloAperto')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"/></svg>
Scheda
<span>Nuova piantina</span>
</x-btn>
@if($mappa)
<x-btn variante="secondary" dimensione="sm"
wire:click="generaImmagine" wire:loading.attr="disabled" wire:target="generaImmagine">
<span wire:loading.remove wire:target="generaImmagine">Genera immagine</span>
<span wire:loading wire:target="generaImmagine">Rendering…</span>
<x-btn variante="ghost" dimensione="sm" class="tm-solo-stretto" @click="pannello = ! pannello">
Scheda
</x-btn>
@if($mappa->collegata() && $mappa->image_path)
<x-btn variante="secondary" dimensione="sm"
:href="route('piantine.anteprima', $mappa)" target="_blank" rel="noopener">
<svg viewBox="0 0 24 24" fill="none" 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>
Anteprima PDF
</x-btn>
@endif
<x-btn variante="primary" dimensione="sm" wire:click="pubblica"
:disabled="$motivo !== null || $stato === TerritorioMappa::STATO_PUBBLICATA"
:title="$motivo ?? ($stato === TerritorioMappa::STATO_PUBBLICATA ? 'Il PDF del territorio corrisponde già a questa piantina.' : 'Anteprima del PDF e pubblicazione')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
<span>Pubblica</span>
</x-btn>
@endif
<x-btn variante="primary" dimensione="sm" wire:click="salva"
wire:loading.attr="disabled" wire:target="salva">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Salva
</x-btn>
</div>
</header>
{{-- ── Mappa ─────────────────────────────────────────────────────────── --}}
<div class="tm-editor-mappa">
<div wire:ignore id="piantine-map" class="tm-mappa"></div>
<div class="tm-editor-corpo" :class="{ 'con-elenco': elenco, 'con-pannello': pannello }">
{{-- ── Elenco ────────────────────────────────────────────────────── --}}
<aside class="tm-editor-elenco" x-show="elenco" x-cloak aria-label="Elenco delle piantine" wire:ignore>
<div class="tm-editor-elenco-cerca">
<input type="search" id="piantine-cerca" placeholder="Cerca per numero o nome" autocomplete="off"
aria-label="Cerca una piantina">
</div>
<ul id="piantine-elenco" class="tm-editor-elenco-righe"></ul>
</aside>
@unless($areaCaricata)
<p class="tm-editor-nota">
Nessuna area città caricata: la mappa parte senza perimetro di riferimento.
Il KML si carica dalla <a href="{{ route('piantine.index') }}">pagina di gestione</a>.
</p>
@endunless
{{-- ── Mappa ─────────────────────────────────────────────────────── --}}
<div class="tm-editor-mappa">
<div wire:ignore id="piantine-map" class="tm-mappa"></div>
<div wire:ignore id="piantine-strumenti" class="tm-strumenti" role="toolbar" aria-label="Strumenti di disegno"></div>
{{-- Pannello: scorre sopra la mappa, non la restringe. --}}
<aside class="tm-editor-pannello" @if(! $pannelloAperto) hidden @endif
aria-label="Scheda della piantina">
@unless($areaCaricata)
<p class="tm-editor-nota">
Nessuna area città caricata: la mappa parte senza perimetro di riferimento.
Il KML si carica dalla <a href="{{ route('piantine.index') }}">pagina di gestione</a>.
</p>
@endunless
</div>
{{-- ── Pannello della piantina ───────────────────────────────────── --}}
<aside class="tm-editor-pannello" x-show="pannello" x-cloak aria-label="La piantina aperta">
<div class="tm-editor-pannello-testata">
<h2>Scheda</h2>
<button type="button" wire:click="chiudiPannello" class="tm-editor-chiudi" aria-label="Chiudi il pannello">&times;</button>
<h2>{{ $mappa ? 'La piantina' : 'Come si comincia' }}</h2>
<button type="button" @click="pannello = false" class="tm-editor-chiudi" aria-label="Chiudi il pannello">&times;</button>
</div>
<div class="tm-editor-pannello-corpo">
@if($geometriaInAttesa)
<div class="tm-nota tm-nota-attesa">
<p>C'è un poligono disegnato non ancora salvato.</p>
<x-btn variante="ghost" dimensione="sm" wire:click="scartaDisegno">Scarta il disegno</x-btn>
</div>
@elseif(! $mappa)
@if(! $mappa)
<ol class="tm-passi">
<li>Apri una piantina dall'elenco o cliccandola sulla mappa,</li>
<li>oppure premi <strong>Nuova piantina</strong> e traccia il confine punto per punto.</li>
<li>Collega la piantina a un territorio e controlla i dati.</li>
<li>Premi <strong>Pubblica</strong>: vedi il PDF e lo confermi.</li>
</ol>
<p class="tm-editor-aiuto">
Usa lo strumento poligono in alto a sinistra sulla mappa per tracciare il confine.
La scheda del territorio si compila qui sotto, anche dopo.
Tutto quello che fai qui si salva da solo come bozza. Il PDF del territorio cambia
solo quando pubblichi.
</p>
@endif
{{-- ── La piantina ───────────────────────────────────────── --}}
<fieldset class="tm-gruppo">
<legend>La piantina</legend>
<div class="tm-campo">
<label for="nome">Nome interno</label>
<input type="text" id="nome" wire:model="nome" autocomplete="off"
placeholder="Es. Zona porto — lato nord">
<p class="tm-campo-aiuto">
Serve solo a ritrovarla in elenco finché non è collegata a un territorio.
Non viene stampato.
</p>
</div>
</fieldset>
{{-- ── Il territorio ─────────────────────────────────────── --}}
<fieldset class="tm-gruppo">
<legend>Il territorio</legend>
<div class="tm-campo">
<label for="territorioScelto">Collegamento</label>
<select id="territorioScelto" wire:model.live="territorioScelto">
<option value="">— nessuno, piantina scollegata —</option>
<option value="nuovo">+ Crea un territorio nuovo</option>
@foreach($territori as $territorio)
<option value="{{ $territorio->id }}">Territorio {{ $territorio->numero }}</option>
@endforeach
</select>
@if($territorioScelto === '')
<p class="tm-campo-aiuto">
Senza territorio la piantina si disegna e si renderizza lo stesso; per il PDF
sul modulo S-12 serve il collegamento.
@else
{{-- ── Pubblicazione ─────────────────────────────────── --}}
<section class="tm-gruppo tm-stato-piantina">
@if($stato === TerritorioMappa::STATO_BOZZA)
<p>Mai pubblicata: il territorio non ha ancora il PDF di questa piantina.</p>
@elseif($stato === TerritorioMappa::STATO_MODIFICATA)
<p>
Il PDF in uso è quello pubblicato il {{ $mappa->pubblicata_at->format('d/m/Y') }}.
Le modifiche fatte da allora non sono ancora nel PDF.
</p>
@endif
</div>
@if($territorioScelto !== '')
<div class="tm-campo">
<label for="numero">Numero</label>
<input type="text" id="numero" wire:model="numero" autocomplete="off"
maxlength="20" placeholder="Es. 42">
@error('numero') <p class="tm-campo-errore">{{ $message }}</p> @enderror
</div>
<div class="tm-campo">
<label for="zonaId">Zona <span class="tm-campo-nota">stampata come «Località» sulla cartolina</span></label>
<select id="zonaId" wire:model="zonaId">
<option value="">— nessuna —</option>
@foreach($zone as $zona)
<option value="{{ $zona->id }}">{{ $zona->nome }}</option>
@endforeach
</select>
<div class="tm-campo-riga tm-campo-crea">
<input type="text" wire:model="nuovaZona" autocomplete="off"
placeholder="Zona che non c'è ancora"
aria-label="Nome della nuova zona"
wire:keydown.enter.prevent="creaZona">
<x-btn variante="secondary" dimensione="sm" wire:click="creaZona">Crea</x-btn>
</div>
@error('nuovaZona') <p class="tm-campo-errore">{{ $message }}</p> @enderror
</div>
<div class="tm-campo">
<label for="tipologiaId">Tipologia</label>
<select id="tipologiaId" wire:model="tipologiaId">
<option value="">— nessuna —</option>
@foreach($tipologie as $tipologia)
<option value="{{ $tipologia->id }}">{{ $tipologia->nome }}</option>
@endforeach
</select>
<div class="tm-campo-riga tm-campo-crea">
<input type="text" wire:model="nuovaTipologia" autocomplete="off"
placeholder="Tipologia che non c'è ancora"
aria-label="Nome della nuova tipologia"
wire:keydown.enter.prevent="creaTipologia">
<x-btn variante="secondary" dimensione="sm" wire:click="creaTipologia">Crea</x-btn>
</div>
@error('nuovaTipologia') <p class="tm-campo-errore">{{ $message }}</p> @enderror
</div>
<div class="tm-campo">
<label for="confini">Confini</label>
<textarea id="confini" wire:model="confini" rows="6"
placeholder="Nord: …&#10;Est: …&#10;Sud: …&#10;Ovest: …"></textarea>
@error('confini') <p class="tm-campo-errore">{{ $message }}</p> @enderror
<p class="tm-campo-aiuto">
Vengono stampati sulla prima pagina della cartolina, nello spazio centrale
del modulo. Il corpo del testo si adatta da solo alla lunghezza.
</p>
</div>
@endif
</fieldset>
@if($mappa)
<div class="tm-editor-pannello-piede">
@if($mappa->image_path)
<a href="{{ asset('storage/' . $mappa->image_path) }}" target="_blank" rel="noopener"
class="tm-collegamento">
Immagine del {{ $mappa->rendered_at?->format('d/m/Y H:i') }}
</a>
@else
<span class="tm-editor-aiuto">Immagine non ancora generata.</span>
<p>Il PDF del territorio corrisponde a questa piantina, pubblicata il {{ $mappa->pubblicata_at->format('d/m/Y') }}.</p>
@endif
@if($motivo)
<p class="tm-campo-aiuto tm-campo-aiuto-attesa">Per pubblicare: {{ lcfirst($motivo) }}</p>
@endif
<div class="tm-azioni-riga">
@if($mappa->collegata() && $mappa->territorio->pdf_path)
<a href="{{ asset('storage/' . $mappa->territorio->pdf_path) }}" target="_blank" rel="noopener" class="tm-collegamento">PDF in uso</a>
@endif
@if($mappa->bozzaScartabile())
<x-btn variante="ghost" dimensione="sm" wire:click="scartaModifiche"
wire:confirm="Riportare il confine a quello pubblicato? Il disegno attuale va perso.">
Ripristina il confine pubblicato
</x-btn>
@endif
</div>
</section>
{{-- ── Territorio ────────────────────────────────────── --}}
<fieldset class="tm-gruppo">
<legend>Territorio</legend>
<div class="tm-campo">
<label for="territorioScelto">Collegata a</label>
<select id="territorioScelto" wire:model.live="territorioScelto">
<option value="">— nessun territorio —</option>
<option value="nuovo">+ Crea un territorio nuovo</option>
@foreach($territori as $territorio)
<option value="{{ $territorio->id }}">
Territorio {{ $territorio->numero }}@if($territorio->trashed()) (nel cestino)@elseif(! $territorio->attivo) (disattivato)@endif
</option>
@endforeach
</select>
@error('territorioScelto') <p class="tm-campo-errore">{{ $message }}</p> @enderror
@if($mappa->territorioNelCestino())
<p class="tm-campo-aiuto tm-campo-aiuto-attesa">
Il territorio è nel cestino: il collegamento resta, ma per pubblicare va ripristinato.
</p>
@endif
</div>
@if($territorioScelto !== '')
@if($territorioScelto === 'nuovo')
<p class="tm-campo-aiuto">Compila i dati e premi <strong>Crea territorio</strong>.</p>
@else
<p class="tm-campo-aiuto">
Questi sono i dati del territorio in tutto il gestionale: le modifiche valgono
subito. Nel PDF entrano alla prossima pubblicazione.
</p>
@endif
@php($modello = $territorioScelto === 'nuovo' ? 'wire:model' : 'wire:model.blur')
<div class="tm-campo">
<label for="numero">Numero</label>
<input type="text" id="numero" {{ $modello }}="numero" autocomplete="off" maxlength="20" placeholder="Es. 42">
@error('numero') <p class="tm-campo-errore">{{ $message }}</p> @enderror
</div>
<div class="tm-campo" x-data="{ crea: false }">
<label for="zonaId">Zona <span class="tm-campo-nota">stampata come «Località»</span></label>
<select id="zonaId" {{ $territorioScelto === 'nuovo' ? 'wire:model' : 'wire:model.live' }}="zonaId">
<option value="">— nessuna —</option>
@foreach($zone as $zona)
<option value="{{ $zona->id }}">{{ $zona->nome }}</option>
@endforeach
</select>
@error('zonaId') <p class="tm-campo-errore">{{ $message }}</p> @enderror
<button type="button" class="tm-collegamento tm-collegamento-piccolo" @click="crea = ! crea" x-show="! crea">+ Zona che non c'è ancora</button>
<div class="tm-campo-riga tm-campo-crea" x-show="crea" x-cloak>
<input type="text" wire:model="nuovaZona" autocomplete="off" placeholder="Nome della nuova zona"
aria-label="Nome della nuova zona" wire:keydown.enter.prevent="creaZona">
<x-btn variante="secondary" dimensione="sm" wire:click="creaZona">Crea</x-btn>
</div>
@error('nuovaZona') <p class="tm-campo-errore">{{ $message }}</p> @enderror
</div>
<div class="tm-campo" x-data="{ crea: false }">
<label for="tipologiaId">Tipologia</label>
<select id="tipologiaId" {{ $territorioScelto === 'nuovo' ? 'wire:model' : 'wire:model.live' }}="tipologiaId">
<option value="">— nessuna —</option>
@foreach($tipologie as $tipologia)
<option value="{{ $tipologia->id }}">{{ $tipologia->nome }}</option>
@endforeach
</select>
@error('tipologiaId') <p class="tm-campo-errore">{{ $message }}</p> @enderror
<button type="button" class="tm-collegamento tm-collegamento-piccolo" @click="crea = ! crea" x-show="! crea">+ Tipologia che non c'è ancora</button>
<div class="tm-campo-riga tm-campo-crea" x-show="crea" x-cloak>
<input type="text" wire:model="nuovaTipologia" autocomplete="off" placeholder="Nome della nuova tipologia"
aria-label="Nome della nuova tipologia" wire:keydown.enter.prevent="creaTipologia">
<x-btn variante="secondary" dimensione="sm" wire:click="creaTipologia">Crea</x-btn>
</div>
@error('nuovaTipologia') <p class="tm-campo-errore">{{ $message }}</p> @enderror
</div>
<div class="tm-campo">
<label for="confini">Confini</label>
<textarea id="confini" {{ $modello }}="confini" rows="6"
placeholder="Nord: …&#10;Est: …&#10;Sud: …&#10;Ovest: …"></textarea>
@error('confini') <p class="tm-campo-errore">{{ $message }}</p> @enderror
<p class="tm-campo-aiuto">Stampati sulla prima pagina del PDF; il testo si adatta da solo allo spazio.</p>
</div>
@if($territorioScelto === 'nuovo')
<x-btn variante="primary" dimensione="sm" wire:click="creaTerritorio"
wire:loading.attr="disabled" wire:target="creaTerritorio">
Crea territorio
</x-btn>
@endif
@endif
</fieldset>
{{-- ── Nome interno, solo finché è scollegata ───────── --}}
@if(! $mappa->territorio)
<fieldset class="tm-gruppo">
<legend>Nome interno</legend>
<div class="tm-campo">
<label for="nome" class="tm-solo-lettori">Nome interno</label>
<input type="text" id="nome" wire:model.blur="nome" autocomplete="off" maxlength="120"
placeholder="Es. Zona porto — lato nord">
<p class="tm-campo-aiuto">Serve a ritrovarla in elenco finché non ha un territorio. Non viene stampato.</p>
</div>
</fieldset>
@endif
<div class="tm-editor-pannello-piede">
<x-btn variante="danger" tono="tenue" dimensione="sm" wire:click="elimina"
wire:confirm="Eliminare {{ $mappa->etichetta() }}? Poligono e immagine vengono rimossi; il territorio e il suo PDF restano.">
wire:confirm="Eliminare {{ $mappa->etichetta() }}? Il disegno va perso; il territorio e il suo PDF restano com'erano.">
Elimina piantina
</x-btn>
</div>
@@ -200,5 +260,5 @@
</aside>
</div>
<script type="application/json" id="piantine-dati">@json($datiMappaIniziali)</script>
<livewire:piantine.pubblicazione />
</div>

View File

@@ -1,10 +1,21 @@
@php
use App\Models\TerritorioMappa;
$tagStato = [
TerritorioMappa::STATO_BOZZA => 'tm-tag-attesa',
TerritorioMappa::STATO_MODIFICATA => 'tm-tag-attesa',
TerritorioMappa::STATO_PUBBLICATA => 'tm-tag-ok',
];
@endphp
<div>
<header class="tm-pagina-testata">
<div>
<h1 class="tm-pagina-titolo">Piantine dei territori</h1>
<p class="tm-pagina-nota">
Il disegno dei confini si fa nell'editor a schermo intero. Qui si tiene l'area della
città, si generano immagini e PDF sul modulo S-12, e si ripescano le versioni sostituite.
A che punto è ogni piantina. Il disegno e i dati del territorio si modificano nell'editor,
dove le modifiche si salvano da sole come bozza; il PDF del territorio cambia solo quando
si pubblica, dopo averne visto l'anteprima.
</p>
</div>
<x-btn variante="primary" :href="route('piantine.editor')">
@@ -13,8 +24,101 @@
</x-btn>
</header>
{{-- ── Piantine ──────────────────────────────────────────────────────── --}}
<section class="tm-card">
<div class="tm-card-testata">
<h2 class="tm-card-titolo">Piantine <span class="tm-conteggio">{{ $totale }}</span></h2>
</div>
@if($totale === 0)
<div class="tm-vuoto">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>
<p>Nessuna piantina disegnata.</p>
<x-btn variante="primary" dimensione="sm" :href="route('piantine.editor')">Disegna la prima</x-btn>
</div>
@else
<div class="tm-filtri">
<input type="search" wire:model.live.debounce.300ms="cerca" class="tm-filtri-cerca"
placeholder="Cerca numero, nome o zona…" aria-label="Cerca una piantina">
<div class="tm-filtri-schede" role="group" aria-label="Filtra per stato">
@foreach(\App\Livewire\Piantine\PiantineManager::FILTRI as $chiave => $etichetta)
<button type="button" wire:click="$set('filtro', '{{ $chiave }}')"
class="tm-filtri-scheda {{ $filtro === $chiave ? 'tm-filtri-scheda-attiva' : '' }}"
aria-pressed="{{ $filtro === $chiave ? 'true' : 'false' }}">
{{ $etichetta }} <span class="tm-conteggio">{{ $conteggi[$chiave] }}</span>
</button>
@endforeach
</div>
</div>
@if($elenco->isEmpty())
<p class="tm-campo-aiuto">Nessuna piantina corrisponde alla ricerca.</p>
@else
<ul class="tm-elenco">
@foreach($elenco as $piantina)
@php
$stato = $piantina->stato();
$motivo = $piantina->motivoNonPubblicabile();
@endphp
<li class="tm-elenco-riga" wire:key="piantina-{{ $piantina->id }}">
<div class="tm-elenco-intestazione">
<a href="{{ route('piantine.editor', $piantina->id) }}" class="tm-elenco-nome">
{{ $piantina->etichetta() }}
</a>
<span class="tm-tag {{ $tagStato[$stato] }}">{{ $piantina->statoEtichetta() }}</span>
@if($piantina->territorioNelCestino())
<span class="tm-tag tm-tag-attesa">Territorio nel cestino</span>
@elseif(! $piantina->collegata())
<span class="tm-tag">Scollegata</span>
@elseif(! $piantina->territorio->zona_id)
<span class="tm-tag tm-tag-attesa" title="La zona viene stampata come «Località»: senza, l'intestazione della cartolina resta vuota">Zona mancante</span>
@endif
<span class="tm-elenco-stato">
@if($piantina->territorio?->zona)
{{ $piantina->territorio->zona->nome }} ·
@endif
{{ $piantina->pubblicata_at
? 'pubblicata il ' . $piantina->pubblicata_at->format('d/m/Y H:i')
: 'mai pubblicata' }}
</span>
</div>
<div class="tm-elenco-azioni">
<x-btn variante="secondary" dimensione="sm" :href="route('piantine.editor', $piantina->id)">
Apri
</x-btn>
@if($stato !== TerritorioMappa::STATO_PUBBLICATA)
<x-btn variante="primary" tono="tenue" dimensione="sm"
wire:click="pubblica({{ $piantina->id }})"
:disabled="$motivo !== null" :title="$motivo">
Pubblica
</x-btn>
@endif
@if($piantina->collegata() && $piantina->territorio->pdf_path)
<a href="{{ asset('storage/' . $piantina->territorio->pdf_path) }}" target="_blank" rel="noopener" class="tm-collegamento">PDF in uso</a>
@endif
<x-btn variante="danger" tono="tenue" dimensione="sm"
wire:click="elimina({{ $piantina->id }})"
wire:confirm="Eliminare {{ $piantina->etichetta() }}? Il disegno viene rimosso; il territorio e il suo PDF restano com'erano.">
Elimina
</x-btn>
</div>
@if($motivo && $stato !== TerritorioMappa::STATO_PUBBLICATA)
<p class="tm-campo-aiuto">{{ $motivo }}</p>
@endif
</li>
@endforeach
</ul>
@endif
@endif
</section>
{{-- ── Area della città ──────────────────────────────────────────────── --}}
<section class="tm-card tm-card-primaria {{ $config['kml_nome'] ? '' : 'tm-card-manca' }}">
<section class="tm-card {{ $config['kml_nome'] ? '' : 'tm-card-manca' }}">
<div class="tm-card-testata tm-card-testata-icona">
<span class="tm-card-icona" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.7" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>
@@ -29,8 +133,7 @@
@endif
</h2>
<p class="tm-card-nota">
Il perimetro di riferimento su cui si disegnano le piantine: è il primo passo,
senza l'editor parte su una mappa senza contorni.
Il perimetro di riferimento che l'editor mostra sotto le piantine.
</p>
</div>
</div>
@@ -59,110 +162,14 @@
</form>
</section>
{{-- ── Piantine ──────────────────────────────────────────────────────── --}}
<section class="tm-card">
<div class="tm-card-testata">
<h2 class="tm-card-titolo">Piantine <span class="tm-conteggio">{{ $piantine->count() }}</span></h2>
<p class="tm-card-nota">
Una piantina può vivere scollegata: per il PDF sul modulo S-12 serve invece un territorio.
</p>
</div>
@if($piantine->isEmpty())
<div class="tm-vuoto">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>
<p>Nessuna piantina disegnata.</p>
<x-btn variante="primary" dimensione="sm" :href="route('piantine.editor')">Disegna la prima</x-btn>
</div>
@else
<ul class="tm-elenco">
@foreach($piantine as $piantina)
<li class="tm-elenco-riga" wire:key="piantina-{{ $piantina->id }}">
<div class="tm-elenco-intestazione">
<a href="{{ route('piantine.editor', $piantina->id) }}" class="tm-elenco-nome">
{{ $piantina->etichetta() }}
</a>
@unless($piantina->collegata())
<span class="tm-tag">scollegata</span>
@endunless
@if($piantina->collegata() && ! $piantina->territorio->zona_id)
<span class="tm-tag tm-tag-attesa" title="La zona viene stampata come «Località»: senza, l'intestazione della cartolina resta vuota">Zona mancante</span>
@endif
<span class="tm-elenco-stato {{ $piantina->image_path ? 'tm-elenco-stato-ok' : '' }}">
{{ $piantina->image_path
? 'immagine del ' . $piantina->rendered_at?->format('d/m/Y H:i')
: 'immagine da generare' }}
</span>
</div>
<div class="tm-elenco-azioni">
<x-btn variante="secondary" tono="tenue" dimensione="sm"
wire:click="generaImmagine({{ $piantina->id }})"
wire:loading.attr="disabled" wire:target="generaImmagine({{ $piantina->id }})">
<span wire:loading.remove wire:target="generaImmagine({{ $piantina->id }})">Genera immagine</span>
<span wire:loading wire:target="generaImmagine({{ $piantina->id }})">Rendering…</span>
</x-btn>
@if($piantina->image_path)
<a href="{{ asset('storage/' . $piantina->image_path) }}" target="_blank" rel="noopener" class="tm-collegamento">Immagine</a>
@endif
@if($piantina->collegata())
@if($piantina->image_path)
<a href="{{ route('piantine.anteprima', $piantina) }}" target="_blank" rel="noopener" class="tm-collegamento">Anteprima PDF</a>
<x-btn variante="primary" tono="tenue" dimensione="sm"
wire:click="generaPdf({{ $piantina->id }})"
wire:loading.attr="disabled" wire:target="generaPdf({{ $piantina->id }})"
wire:confirm="Sostituire il PDF del territorio {{ $piantina->territorio->numero }}? Quello attuale finisce nello storico versioni.">
<span wire:loading.remove wire:target="generaPdf({{ $piantina->id }})">Genera PDF</span>
<span wire:loading wire:target="generaPdf({{ $piantina->id }})">Composizione…</span>
</x-btn>
@endif
<x-btn variante="ghost" dimensione="sm" wire:click="scollega({{ $piantina->id }})"
wire:confirm="Scollegare questa piantina dal territorio {{ $piantina->territorio->numero }}? Il PDF del territorio resta com'è.">
Scollega
</x-btn>
@else
<x-btn variante="secondary" dimensione="sm" wire:click="apriCollegamento({{ $piantina->id }})">
Collega a un territorio
</x-btn>
@endif
<x-btn variante="danger" tono="tenue" dimensione="sm"
wire:click="eliminaPiantina({{ $piantina->id }})"
wire:confirm="Eliminare {{ $piantina->etichetta() }}? Poligono e immagine vengono rimossi; il PDF del territorio resta com'è.">
Elimina
</x-btn>
</div>
@if($collegamentoAperto === $piantina->id)
<div class="tm-elenco-pannello">
<div class="tm-campo-riga">
<select wire:model="territorioDaCollegare" aria-label="Territorio a cui collegare la piantina">
<option value="">— scegli il territorio —</option>
@foreach($territori as $territorio)
<option value="{{ $territorio->id }}">{{ $territorio->numero }}</option>
@endforeach
</select>
<x-btn variante="primary" dimensione="sm" wire:click="collega({{ $piantina->id }})">Collega</x-btn>
<x-btn variante="ghost" dimensione="sm" wire:click="chiudiCollegamento">Annulla</x-btn>
</div>
<p class="tm-campo-aiuto">
Il territorio da creare non c'è? Si crea dall'editor, senza perdere il disegno.
</p>
</div>
@endif
</li>
@endforeach
</ul>
@endif
</section>
{{-- ── Storico versioni PDF ──────────────────────────────────────────── --}}
<section class="tm-card">
<div class="tm-card-testata">
<h2 class="tm-card-titolo">Storico versioni PDF</h2>
<p class="tm-card-nota">I PDF sostituiti da una piantina. Il ripristino è sempre reversibile.</p>
<h2 class="tm-card-titolo">Storico versioni PDF <span class="tm-conteggio">{{ $versioni->total() }}</span></h2>
<p class="tm-card-nota">
I PDF dei territori sostituiti da una pubblicazione o da un ripristino. Ripristinare una versione
rimette in uso quel PDF: la piantina del territorio torna "da pubblicare".
</p>
</div>
@if($versioni->isEmpty())
@@ -205,6 +212,10 @@
</tbody>
</table>
</div>
<div class="mt-4">{{ $versioni->links() }}</div>
@endif
</section>
<livewire:piantine.pubblicazione />
</div>

View File

@@ -0,0 +1,54 @@
<div>
@if($mappaId)
<div class="tm-modale-sfondo" wire:keydown.escape.window="chiudi">
<div class="tm-modale tm-modale-larga" role="dialog" aria-modal="true" aria-labelledby="pubblicazione-titolo"
@if($fase === \App\Services\Piantine\PubblicazionePiantina::IN_CORSO) wire:poll.2s="aggiorna" @endif>
<header class="tm-modale-testata">
<h2 id="pubblicazione-titolo">Pubblica {{ $etichetta }}</h2>
<button type="button" wire:click="chiudi" class="tm-editor-chiudi" aria-label="Chiudi senza pubblicare">&times;</button>
</header>
<div class="tm-modale-corpo">
@if($fase === \App\Services\Piantine\PubblicazionePiantina::IN_CORSO)
<div class="tm-attesa">
<span class="tm-attesa-giro" aria-hidden="true"></span>
<div>
<p class="tm-attesa-titolo">Preparazione dell'anteprima…</p>
<p class="tm-editor-aiuto">
Immagine ad alta definizione e PDF sul modulo S-12. La prima volta per un
territorio può richiedere fino a un paio di minuti. Niente è ancora cambiato.
</p>
</div>
</div>
@elseif($fase === \App\Services\Piantine\PubblicazionePiantina::PRONTA)
<p class="tm-editor-aiuto">
Controlla il PDF. Confermando diventa il PDF del territorio, quello che arriva con i
link delle assegnazioni; il precedente resta nello storico e si può ripristinare.
</p>
<iframe src="{{ route('piantine.anteprima', $token) }}" class="tm-anteprima-pdf"
title="Anteprima del PDF da pubblicare"></iframe>
<a href="{{ route('piantine.anteprima', $token) }}" target="_blank" rel="noopener" class="tm-collegamento">
Apri l'anteprima in una nuova scheda
</a>
@else
<div class="tm-nota tm-nota-errore">
<p>{{ $messaggio ?: 'Qualcosa non ha funzionato.' }}</p>
</div>
@endif
</div>
<footer class="tm-modale-piede">
@if($fase === \App\Services\Piantine\PubblicazionePiantina::PRONTA)
<x-btn variante="primary" wire:click="conferma" wire:loading.attr="disabled" wire:target="conferma">
<span wire:loading.remove wire:target="conferma">Conferma e pubblica</span>
<span wire:loading wire:target="conferma">Pubblicazione…</span>
</x-btn>
@elseif($fase === \App\Services\Piantine\PubblicazionePiantina::ERRORE)
<x-btn variante="secondary" wire:click="riprova">Riprova</x-btn>
@endif
<x-btn variante="ghost" wire:click="chiudi">Annulla</x-btn>
</footer>
</div>
</div>
@endif
</div>