Piantine staccate dai territori, editor a schermo intero, restyle

Le piantine non dipendono piu' da un territorio esistente: territorio_id e'
nullable, la chiave esterna e' nullOnDelete invece di cascadeOnDelete, e `nome`
fa da etichetta finche' il collegamento manca. Si disegna guardando le strade e
si decide dopo di chi e' il disegno; se il territorio non c'e', lo si crea
dall'editor con numero, zona, tipologia e confini — zona e tipologia comprese,
create sul posto quando mancano.

Il cambio di cascata chiude anche una perdita di dati silenziosa: ogni import
XML svuota `territori` in blocco e finora si portava via tutte le piantine.

Il disegno passa su /piantine/editor, a schermo intero con telaio proprio: su
una mappa larga un terzo di schermo un confine si sbaglia, e finisce stampato.
/piantine resta la gestione — area, generazione, collegamenti, storico.

Cartolina S-12 reimpaginata: pagina 1 il modulo con la Localita' (che ora e' il
nome della zona, non un campo duplicato) e i confini scritti nello spazio
centrale, con il corpo del testo che si adatta alla lunghezza; pagina 2 la
piantina sola al 98%. Aggiunta l'anteprima, che compone il PDF al volo senza
toccare pdf_path ne' lo storico.

Immagine: JPEG invece di PNG (0,9 MB contro 5 per territorio), composta alle
tessere dello zoom piu' alto e ridotta con Lanczos, nelle proporzioni della
pagina cosi' riempie il foglio invece di lasciare fasce bianche. Il tratto del
confine e' espresso in pixel dell'immagine finale: si vede il confine e si legge
la via che ci passa sotto. L'attribuzione OSM si dimensiona con l'immagine,
come la ODbL richiede.

Rimossa la rigenerazione massiva: job, stato in cache e interfaccia.

Sistema visivo ripassato su tutta l'applicazione: le scale gray e indigo di
Tailwind sono ridefinite nei token, cosi' le 37 viste si riallineano insieme
invece di essere riscritte una per una. Grigi caldi, un accento solo con quattro
significati, carattere servito dall'applicazione e non da Google Fonts, via il
gradiente della testata. Aggiunto il link "salta al contenuto", che mancava.

Corretto un guasto d'ambiente che bloccava TUTTI gli upload in sviluppo: dalla
1.26 il fastcgi_params di nginx passa HTTP_HOST come $host, che perde la porta,
e ogni URL firmato smetteva di validare — l'endpoint di upload di Livewire
rispondeva 401.

294 test verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-28 16:41:22 +02:00
parent 7e4c8aa9b8
commit 9c37c1ad7b
30 changed files with 3032 additions and 982 deletions

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="it" class="h-full bg-gray-50">
<html lang="it" class="h-full bg-gray-50 tm-app">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -9,6 +9,8 @@
@livewireStyles
</head>
<body class="h-full">
{{-- Chi naviga da tastiera non deve ripercorrere il menu a ogni pagina. --}}
<a href="#contenuto" class="tm-salta">Salta al contenuto</a>
<div class="min-h-full">
{{-- Header --}}
<nav class="header-gradient shadow-lg">
@@ -179,7 +181,7 @@
</aside>
{{-- Main content --}}
<main class="flex-1 px-4 sm:px-6 lg:px-8 py-6 min-h-screen">
<main id="contenuto" tabindex="-1" class="flex-1 px-4 sm:px-6 lg:px-8 py-6 min-h-screen">
{{-- Flash messages --}}
@if (session()->has('success'))
<div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 4000)"

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
{{--
Telaio a schermo intero, senza barra laterale e senza testata.
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.
--}}
<html lang="it" class="h-full tm-app">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? 'TerManager2' }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body class="h-full overflow-hidden" style="background: var(--tm-fondo)">
<main class="tm-schermo">
{{ $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>
@livewireScripts
</body>
</html>

View File

@@ -0,0 +1,204 @@
<div class="tm-editor">
@vite('resources/js/piantine.js')
{{-- ── Barra strumenti ───────────────────────────────────────────────── --}}
<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>
<span>Piantine</span>
</a>
<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>
@endif
</h1>
<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
</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>
@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
@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>
@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
{{-- Pannello: scorre sopra la mappa, non la restringe. --}}
<aside class="tm-editor-pannello" @if(! $pannelloAperto) hidden @endif
aria-label="Scheda della piantina">
<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>
</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)
<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.
</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.
</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>
@endif
<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.">
Elimina piantina
</x-btn>
</div>
@endif
</div>
</aside>
</div>
<script type="application/json" id="piantine-dati">@json($datiMappaIniziali)</script>
</div>

View File

@@ -1,221 +1,204 @@
<div>
@vite('resources/js/piantine.js')
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">Piantine dei territori</h1>
<p class="mt-1 text-sm text-gray-600">
Carica il KML dell'area della città, disegna i poligoni dei territori sulla mappa e genera
immagini ad alta definizione e PDF sul modulo S-12. I PDF sostituiti finiscono nello storico
versioni, sempre ripristinabili.
</p>
</div>
{{-- ── 1. Area della città (KML) ─────────────────────────────────── --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4">
<h2 class="text-base font-semibold text-gray-900 mb-3">1 · Area della città</h2>
<div class="grid gap-4 lg:grid-cols-2">
<form wire:submit="caricaKml">
<label for="kmlFile" class="block text-sm font-medium text-gray-700">File .kml dell'area</label>
<div class="mt-1 flex items-center gap-3">
<input type="file" id="kmlFile" wire:model="kmlFile" accept=".kml,.xml"
class="block w-full text-sm text-gray-600 file:mr-3 file:rounded-lg file:border-0 file:bg-indigo-50 file:px-3 file:py-2 file:text-sm file:font-medium file:text-indigo-700 hover:file:bg-indigo-100">
<x-btn variante="primary" type="submit" wire:loading.attr="disabled" wire:target="kmlFile,caricaKml">
Carica
</x-btn>
</div>
<div wire:loading wire:target="kmlFile" class="mt-1 text-xs text-gray-500">Caricamento del file…</div>
@error('kmlFile') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
@if($config['kml_nome'])
<p class="mt-2 text-xs text-gray-500">
Area attiva: <span class="font-medium text-gray-700">{{ $config['kml_nome'] }}</span>
(aggiornata il {{ $config['aggiornato_il'] }}).
Caricare un nuovo KML sostituisce l'area; i poligoni già disegnati restano.
</p>
@else
<p class="mt-2 text-xs text-amber-600">Nessuna area caricata: la mappa parte senza perimetro di riferimento.</p>
@endif
</form>
<form wire:submit="salvaLocalita">
<label for="localita" class="block text-sm font-medium text-gray-700">
Località (intestazione del modulo S-12)
</label>
<div class="mt-1 flex items-center gap-3">
<input type="text" id="localita" wire:model="localita" placeholder="Es. Palermo — Congregazione …"
class="block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
<x-btn variante="secondary" type="submit">Salva</x-btn>
</div>
<p class="mt-2 text-xs text-gray-500">Se vuota, viene usato il nome della congregazione dalle impostazioni.</p>
</form>
<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.
</p>
</div>
</div>
<x-btn variante="primary" :href="route('piantine.editor')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
Apri l'editor
</x-btn>
</header>
{{-- ── 2. Editor della mappa ─────────────────────────────────────── --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4">
<h2 class="text-base font-semibold text-gray-900 mb-3">2 · Disegno dei territori</h2>
<div class="grid gap-4 lg:grid-cols-3">
<div class="lg:col-span-2">
<div wire:ignore id="piantine-map" class="w-full rounded-xl border border-gray-300 z-0" style="height: 65vh; min-height: 420px;"></div>
<p class="mt-2 text-xs text-gray-500">
Usa lo strumento poligono (in alto a sinistra sulla mappa) per disegnare il territorio.
Il tratteggio blu è l'area del KML; i poligoni grigi sono i territori già salvati.
{{-- ── Area della città ──────────────────────────────────────────────── --}}
<section class="tm-card tm-card-primaria {{ $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>
</span>
<div>
<h2 class="tm-card-titolo">
Area della città
@if($config['kml_nome'])
<span class="tm-tag tm-tag-ok">caricata</span>
@else
<span class="tm-tag tm-tag-attesa">da caricare</span>
@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.
</p>
</div>
<div>
<label for="territorioSelezionato" class="block text-sm font-medium text-gray-700">Territorio da associare</label>
<select id="territorioSelezionato" wire:model.live="territorioSelezionato"
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
<option value="">— scegli il territorio —</option>
@foreach($territori as $territorio)
<option value="{{ $territorio->id }}">
{{ $territorio->numero }}{{ $mappe->has($territorio->id) ? ' · poligono presente' : '' }}
</option>
@endforeach
</select>
@if($geometriaInAttesa)
<div class="mt-3 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
C'è un poligono disegnato non ancora salvato.
</div>
<div class="mt-2 flex gap-2">
<x-btn variante="primary" wire:click="salvaPoligono" wire:loading.attr="disabled">
Salva poligono
</x-btn>
<x-btn variante="ghost" wire:click="scartaPoligono">Scarta</x-btn>
</div>
@else
<p class="mt-3 text-xs text-gray-500">
Disegna un poligono e poi salvalo sul territorio selezionato. Ridisegnare e salvare
sostituisce il poligono esistente.
</p>
@endif
{{-- Territori con poligono --}}
<div class="mt-4 border-t border-gray-200 pt-3">
<h3 class="text-sm font-semibold text-gray-800 mb-2">Territori con poligono ({{ $mappe->count() }})</h3>
<ul class="divide-y divide-gray-100 max-h-72 overflow-y-auto pr-1">
@forelse($mappe->sortBy(fn ($m) => [strlen($m->territorio?->numero ?? ''), $m->territorio?->numero]) as $mappa)
<li class="py-2 text-sm" wire:key="mappa-{{ $mappa->id }}">
<div class="flex items-center justify-between gap-2">
<button type="button" wire:click="$set('territorioSelezionato', {{ $mappa->territorio_id }})"
class="font-medium {{ $territorioSelezionato === $mappa->territorio_id ? 'text-indigo-700' : 'text-gray-800' }} hover:underline">
Terr. {{ $mappa->territorio?->numero ?? '?' }}
</button>
<span class="text-xs {{ $mappa->image_path ? 'text-green-600' : 'text-gray-400' }}">
{{ $mappa->image_path ? 'immagine del ' . $mappa->rendered_at?->format('d/m/Y H:i') : 'immagine da generare' }}
</span>
</div>
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
<button type="button" wire:click="generaImmagine({{ $mappa->id }})"
wire:loading.attr="disabled" wire:target="generaImmagine({{ $mappa->id }})"
class="text-indigo-600 hover:underline disabled:opacity-50">
<span wire:loading.remove wire:target="generaImmagine({{ $mappa->id }})">Genera immagine</span>
<span wire:loading wire:target="generaImmagine({{ $mappa->id }})">Rendering in corso…</span>
</button>
@if($mappa->image_path)
<a href="{{ asset('storage/' . $mappa->image_path) }}" target="_blank" class="text-indigo-600 hover:underline">Scarica immagine</a>
<button type="button" wire:click="generaPdf({{ $mappa->id }})"
wire:loading.attr="disabled" wire:target="generaPdf({{ $mappa->id }})"
wire:confirm="Sostituire il PDF del territorio {{ $mappa->territorio?->numero }}? Quello attuale finisce nello storico versioni."
class="text-indigo-600 hover:underline disabled:opacity-50">
<span wire:loading.remove wire:target="generaPdf({{ $mappa->id }})">Genera PDF e sostituisci</span>
<span wire:loading wire:target="generaPdf({{ $mappa->id }})">Composizione…</span>
</button>
@endif
<button type="button" wire:click="eliminaPoligono({{ $mappa->id }})"
wire:confirm="Eliminare poligono e immagine del territorio {{ $mappa->territorio?->numero }}? Il PDF del territorio resta com'è."
class="text-red-600 hover:underline">Elimina</button>
</div>
</li>
@empty
<li class="py-2 text-sm text-gray-500">Nessun poligono ancora disegnato.</li>
@endforelse
</ul>
</div>
</div>
</div>
</div>
{{-- ── 3. Rigenerazione massiva ──────────────────────────────────── --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4"
@if($stato && in_array($stato['status'], ['queued', 'running'], true)) wire:poll.2s @endif>
<h2 class="text-base font-semibold text-gray-900 mb-3">3 · Rigenerazione massiva</h2>
<p class="text-sm text-gray-600 mb-3">
Rigenera in coda le immagini di tutti i territori con poligono (utile dopo aggiornamenti della
cartografia). Con la seconda opzione vengono ricomposti e sostituiti anche i PDF.
</p>
<div class="flex flex-wrap gap-2">
<x-btn variante="secondary" wire:click="rigeneraTutto(false)"
wire:confirm="Rigenerare le immagini di tutti i territori con poligono?"
wire:loading.attr="disabled" :disabled="$stato && in_array($stato['status'], ['queued', 'running'], true)">
Rigenera tutte le immagini
</x-btn>
<x-btn variante="secondary" wire:click="rigeneraTutto(true)"
wire:confirm="Rigenerare immagini E sostituire tutti i PDF? Le versioni attuali finiscono nello storico."
wire:loading.attr="disabled" :disabled="$stato && in_array($stato['status'], ['queued', 'running'], true)">
Rigenera immagini + PDF
</x-btn>
</div>
@if($stato)
<div class="mt-4">
@php
$percento = ($stato['total'] ?? 0) > 0 ? (int) round(($stato['processed'] / $stato['total']) * 100) : 0;
@endphp
<div class="flex items-center justify-between text-xs text-gray-600 mb-1">
<span>
@if($stato['status'] === 'queued') In coda…
@elseif($stato['status'] === 'running') In corso: {{ $stato['processed'] }}/{{ $stato['total'] }}
@elseif($stato['status'] === 'completed') Completata: {{ $stato['processed'] }}/{{ $stato['total'] }} ({{ $stato['falliti'] }} fallite)
@else Fallita: {{ $stato['error'] }}
@endif
</span>
<span>{{ $percento }}%</span>
</div>
<div class="h-2 w-full rounded-full bg-gray-100 overflow-hidden">
<div class="h-2 rounded-full transition-all duration-500 {{ $stato['status'] === 'failed' ? 'bg-red-500' : 'bg-indigo-500' }}"
style="width: {{ $percento }}%"></div>
</div>
@if(!empty($stato['logs']))
<div class="mt-3 max-h-48 overflow-y-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-gray-100 space-y-0.5">
@foreach($stato['logs'] as $riga)
<div>{{ $riga }}</div>
@endforeach
</div>
@endif
<form wire:submit="caricaKml" class="tm-campo">
<label for="kmlFile">File .kml dell'area</label>
<div class="tm-campo-riga">
<input type="file" id="kmlFile" wire:model="kmlFile" accept=".kml,.xml" class="tm-file">
<x-btn variante="secondary" type="submit" wire:loading.attr="disabled" wire:target="kmlFile,caricaKml">
Carica
</x-btn>
</div>
@endif
</div>
<p wire:loading wire:target="kmlFile" class="tm-campo-aiuto">Caricamento del file…</p>
@error('kmlFile') <p class="tm-campo-errore">{{ $message }}</p> @enderror
{{-- ── 4. Storico versioni PDF ───────────────────────────────────── --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<h2 class="text-base font-semibold text-gray-900 mb-3">4 · Storico versioni PDF</h2>
@if($versioni->isEmpty())
<p class="text-sm text-gray-500">Nessun PDF è ancora stato sostituito.</p>
@if($config['kml_nome'])
<p class="tm-campo-aiuto">
Area attiva: <strong>{{ $config['kml_nome'] }}</strong>, aggiornata il {{ $config['aggiornato_il'] }}.
Caricare un nuovo KML la sostituisce; le piantine già disegnate restano.
</p>
@else
<p class="tm-campo-aiuto tm-campo-aiuto-attesa">
Nessuna area caricata: l'editor parte senza perimetro di riferimento.
</p>
@endif
</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
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<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>
</div>
@if($versioni->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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<p>Nessun PDF è ancora stato sostituito.</p>
</div>
@else
<div class="tm-tabella-scorre">
<table class="tm-tabella table-cards">
<thead>
<tr class="text-left text-xs uppercase tracking-wider text-gray-500 border-b border-gray-200">
<th class="py-2 pr-4">Territorio</th>
<th class="py-2 pr-4">Sostituito il</th>
<th class="py-2 pr-4">Origine</th>
<th class="py-2"></th>
<tr>
<th scope="col">Territorio</th>
<th scope="col">Sostituito il</th>
<th scope="col">Origine</th>
<th scope="col"><span class="tm-solo-lettori">Azioni</span></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tbody>
@foreach($versioni as $versione)
<tr wire:key="versione-{{ $versione->id }}">
<td class="py-2 pr-4 font-medium text-gray-800">{{ $versione->territorio?->numero ?? '?' }}</td>
<td class="py-2 pr-4 text-gray-600">{{ $versione->sostituito_il?->format('d/m/Y H:i') }}</td>
<td class="py-2 pr-4 text-gray-600">{{ $versione->origine }}</td>
<td class="py-2 text-right space-x-3 whitespace-nowrap">
<a href="{{ asset('storage/' . $versione->pdf_path) }}" target="_blank" class="text-indigo-600 hover:underline text-xs">Apri</a>
<button type="button" wire:click="ripristinaVersione({{ $versione->id }})"
wire:confirm="Ripristinare questa versione per il territorio {{ $versione->territorio?->numero }}? Il PDF attuale finisce a sua volta nello storico."
class="text-indigo-600 hover:underline text-xs">Ripristina</button>
<button type="button" wire:click="eliminaVersione({{ $versione->id }})"
wire:confirm="Eliminare definitivamente questa versione archiviata?"
class="text-red-600 hover:underline text-xs">Elimina</button>
<td data-label="Territorio" class="tm-tabella-forte">{{ $versione->territorio?->numero ?? '—' }}</td>
<td data-label="Sostituito il">{{ $versione->sostituito_il?->format('d/m/Y H:i') }}</td>
<td data-label="Origine">{{ $versione->origine }}</td>
<td class="cell-actions">
<a href="{{ asset('storage/' . $versione->pdf_path) }}" target="_blank" rel="noopener" class="tm-collegamento">Apri</a>
<x-btn variante="secondary" tono="tenue" dimensione="sm"
wire:click="ripristinaVersione({{ $versione->id }})"
wire:confirm="Ripristinare questa versione per il territorio {{ $versione->territorio?->numero }}? Il PDF attuale finisce a sua volta nello storico.">
Ripristina
</x-btn>
<x-btn variante="danger" tono="tenue" dimensione="sm"
wire:click="eliminaVersione({{ $versione->id }})"
wire:confirm="Eliminare definitivamente questa versione archiviata?">
Elimina
</x-btn>
</td>
</tr>
@endforeach
@@ -223,7 +206,5 @@
</table>
</div>
@endif
</div>
<script type="application/json" id="piantine-dati">@json($datiMappaIniziali)</script>
</section>
</div>