++ 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,317 @@
<?php
namespace App\Livewire\Piantine;
use App\Jobs\RigeneraPiantine;
use App\Models\Territorio;
use App\Models\TerritorioMappa;
use App\Models\TerritorioPdfVersione;
use App\Services\Piantine\PiantinaPdfService;
use App\Services\Piantine\PiantinaRenderer;
use App\Services\Piantine\PiantineArea;
use App\Services\Piantine\PiantineOperationState;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Livewire\Attributes\On;
use Livewire\Component;
use Livewire\WithFileUploads;
/**
* Sotto-app "Piantine" (solo amministratore).
*
* Dal KML dell'area città si disegnano i poligoni dei territori su una mappa
* OpenStreetMap; da ogni poligono si genera l'immagine ad alta definizione
* (container map-renderer) e, a scelta, il PDF sul modulo S-12 che sostituisce
* quello del territorio, con storico versioni.
*/
class PiantineManager extends Component
{
use WithFileUploads;
public $kmlFile = null;
public string $localita = '';
public ?int $territorioSelezionato = null;
/** Geometria (JSON) appena disegnata sulla mappa, in attesa di salvataggio. */
public ?string $geometriaInAttesa = null;
/** Id dell'operazione di rigenerazione massiva seguita dalla pagina. */
public ?string $operazioneId = null;
public function mount(PiantineArea $area, PiantineOperationState $state): void
{
$this->localita = $area->localita();
$this->operazioneId = $state->corrente();
}
// ─── Area città (KML) ───────────────────────────────────────────────────
public function caricaKml(PiantineArea $area)
{
$this->validate(
['kmlFile' => 'required|file|max:10240'],
[],
['kmlFile' => 'file KML'],
);
$estensione = strtolower($this->kmlFile->getClientOriginalExtension());
if (! in_array($estensione, ['kml', 'xml'], true)) {
$this->addError('kmlFile', 'Il file deve essere un .kml (o .xml in formato KML).');
return;
}
try {
$quante = $area->salvaKml(
file_get_contents($this->kmlFile->getRealPath()),
$this->kmlFile->getClientOriginalName(),
);
} catch (\Throwable $e) {
$this->addError('kmlFile', $e->getMessage());
return;
}
$this->kmlFile = null;
activity()->causedBy(auth()->user())->log('piantine_kml_caricato');
session()->flash('success', "Area della città aggiornata ({$quante} geometrie riconosciute nel KML).");
$this->aggiornaMappaJs();
}
public function salvaLocalita(PiantineArea $area): void
{
$area->impostaLocalita($this->localita);
session()->flash('success', 'Località aggiornata: comparirà sull\'intestazione dei prossimi PDF.');
}
// ─── Editor dei poligoni ────────────────────────────────────────────────
#[On('poligono-disegnato')]
public function poligonoDisegnato($geometry): void
{
if (! is_array($geometry)
|| ! in_array($geometry['type'] ?? null, ['Polygon', 'MultiPolygon'], true)
|| ! is_array($geometry['coordinates'] ?? null)) {
return;
}
$this->geometriaInAttesa = json_encode($geometry);
}
#[On('territorio-selezionato-mappa')]
public function territorioSelezionatoDaMappa($territorioId): void
{
$this->territorioSelezionato = is_numeric($territorioId) ? (int) $territorioId : null;
$this->aggiornaMappaJs();
}
public function updatedTerritorioSelezionato(): void
{
$this->territorioSelezionato = $this->territorioSelezionato ?: null;
$this->aggiornaMappaJs();
}
public function salvaPoligono()
{
if (! $this->territorioSelezionato) {
session()->flash('error', 'Scegli prima a quale territorio associare il poligono.');
return;
}
if (! $this->geometriaInAttesa) {
session()->flash('error', 'Disegna prima il poligono sulla mappa.');
return;
}
$territorio = Territorio::findOrFail($this->territorioSelezionato);
TerritorioMappa::updateOrCreate(
['territorio_id' => $territorio->id],
['geojson' => $this->geometriaInAttesa],
);
$this->geometriaInAttesa = null;
activity()->causedBy(auth()->user())->performedOn($territorio)->log('piantina_poligono_salvato');
session()->flash('success', "Poligono salvato per il territorio {$territorio->numero}. Ora puoi generare l'immagine.");
$this->aggiornaMappaJs();
}
public function scartaPoligono(): void
{
$this->geometriaInAttesa = null;
$this->aggiornaMappaJs();
}
public function eliminaPoligono(int $mappaId)
{
$mappa = TerritorioMappa::with('territorio')->findOrFail($mappaId);
$numero = $mappa->territorio?->numero ?? '?';
if ($mappa->image_path) {
Storage::disk('public')->delete($mappa->image_path);
}
$mappa->delete();
session()->flash('success', "Poligono e immagine del territorio {$numero} eliminati (il PDF resta com'è).");
$this->aggiornaMappaJs();
}
// ─── Generazione ────────────────────────────────────────────────────────
public function generaImmagine(int $mappaId, PiantinaRenderer $renderer)
{
$mappa = TerritorioMappa::with('territorio')->findOrFail($mappaId);
try {
$renderer->render($mappa);
} catch (\Throwable $e) {
session()->flash('error', $e->getMessage());
return;
}
session()->flash('success', "Immagine generata per il territorio {$mappa->territorio?->numero}.");
}
public function generaPdf(int $mappaId, PiantinaPdfService $pdfService)
{
$mappa = TerritorioMappa::with('territorio')->findOrFail($mappaId);
$territorio = $mappa->territorio;
if (! $territorio) {
session()->flash('error', 'Il territorio associato non esiste più.');
return;
}
try {
$pdfService->sostituisci($territorio, $mappa);
} catch (\Throwable $e) {
session()->flash('error', $e->getMessage());
return;
}
session()->flash('success', "PDF del territorio {$territorio->numero} sostituito. Il precedente è nello storico versioni.");
}
// ─── Rigenerazione massiva ──────────────────────────────────────────────
public function rigeneraTutto(bool $anchePdf, PiantineOperationState $state)
{
if ($state->corrente() !== null) {
session()->flash('error', 'C\'è già una rigenerazione in corso: attendi che finisca.');
return;
}
$totale = TerritorioMappa::count();
if ($totale === 0) {
session()->flash('error', 'Nessun territorio ha ancora un poligono disegnato.');
return;
}
$id = (string) Str::uuid();
$state->initialize($id, $totale, $anchePdf);
RigeneraPiantine::dispatch($id, $anchePdf);
$this->operazioneId = $id;
activity()->causedBy(auth()->user())->log($anchePdf ? 'piantine_rigenerazione_pdf' : 'piantine_rigenerazione');
}
// ─── Storico versioni ───────────────────────────────────────────────────
public function ripristinaVersione(int $versioneId, PiantinaPdfService $pdfService)
{
$versione = TerritorioPdfVersione::with('territorio')->findOrFail($versioneId);
$numero = $versione->territorio?->numero ?? '?';
try {
$pdfService->ripristina($versione);
} catch (\Throwable $e) {
session()->flash('error', $e->getMessage());
return;
}
session()->flash('success', "Versione precedente ripristinata per il territorio {$numero}.");
}
public function eliminaVersione(int $versioneId)
{
$versione = TerritorioPdfVersione::findOrFail($versioneId);
if (Storage::disk('public')->exists($versione->pdf_path)) {
Storage::disk('public')->delete($versione->pdf_path);
}
$versione->delete();
session()->flash('success', 'Versione eliminata definitivamente.');
}
// ─── Dati per la mappa e rendering ──────────────────────────────────────
/**
* Il payload che il JavaScript della mappa usa per disegnare: area della
* città, poligoni salvati, selezione corrente.
*/
public function datiMappa(?PiantineArea $area = null): array
{
$area ??= app(PiantineArea::class);
$poligoni = TerritorioMappa::with('territorio')->get()
->filter(fn ($mappa) => $mappa->territorio !== null && $mappa->geometria() !== null)
->map(fn ($mappa) => [
'territorioId' => $mappa->territorio_id,
'numero' => $mappa->territorio->numero,
'geometry' => $mappa->geometria(),
])
->values()
->all();
return [
'area' => $area->area(),
'poligoni' => $poligoni,
'selezionato' => $this->territorioSelezionato,
];
}
private function aggiornaMappaJs(): void
{
$this->dispatch('piantine-dati-aggiornati', dati: $this->datiMappa());
}
public function render(PiantineArea $area, PiantineOperationState $state)
{
$mappe = TerritorioMappa::with('territorio')->get()->keyBy('territorio_id');
$territori = Territorio::attivi()
->orderByRaw('LENGTH(numero), numero')
->get(['id', 'numero', 'pdf_path']);
$stato = $this->operazioneId ? $state->get($this->operazioneId) : null;
return view('livewire.piantine.piantine-manager', [
'config' => $area->config(),
'datiMappaIniziali' => $this->datiMappa($area),
'territori' => $territori,
'mappe' => $mappe,
'versioni' => TerritorioPdfVersione::with('territorio')
->orderByDesc('sostituito_il')
->limit(30)
->get(),
'stato' => $stato,
'title' => 'Piantine dei territori',
]);
}
}