++ Prima versione del designer piantine + Skill grafica

This commit is contained in:
2026-08-28 15:32:14 +02:00
parent 8aa29cc467
commit 7e4c8aa9b8
62 changed files with 16340 additions and 4 deletions

View File

@@ -0,0 +1,229 @@
<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>
</div>
</div>
{{-- ── 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.
</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
</div>
@endif
</div>
{{-- ── 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>
@else
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<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>
</thead>
<tbody class="divide-y divide-gray-100">
@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>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
<script type="application/json" id="piantine-dati">@json($datiMappaIniziali)</script>
</div>