Files
termanager2/app/Livewire/Piantine/PiantineManager.php
Francesco Picone 056d95cfcb 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>
2026-10-12 12:08:34 +02:00

234 lines
7.5 KiB
PHP

<?php
namespace App\Livewire\Piantine;
use App\Livewire\Concerns\Avvisa;
use App\Models\TerritorioMappa;
use App\Models\TerritorioPdfVersione;
use App\Services\Piantine\PiantinaPdfService;
use App\Services\Piantine\PiantineArea;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Livewire\Attributes\On;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\WithPagination;
/**
* Pagina di gestione delle piantine (solo amministratore).
*
* È il quadro d'insieme: a che punto è ogni piantina (bozza, pubblicata,
* con modifiche da pubblicare), l'area della città e lo storico dei PDF
* sostituiti. Il disegno e i dati del territorio si toccano nell'editor;
* da qui si apre, si pubblica (con la stessa finestra di anteprima
* dell'editor) e si elimina.
*/
class PiantineManager extends Component
{
use Avvisa;
use WithFileUploads;
use WithPagination;
public const FILTRI = [
'tutte' => 'Tutte',
'bozze' => 'Bozze',
'da-pubblicare' => 'Da pubblicare',
'pubblicate' => 'Pubblicate',
'scollegate' => 'Scollegate',
];
#[Url(as: 'q', except: '')]
public string $cerca = '';
#[Url(except: 'tutte')]
public string $filtro = 'tutte';
public $kmlFile = null;
public function updatedFiltro(): void
{
if (! array_key_exists($this->filtro, self::FILTRI)) {
$this->filtro = 'tutte';
}
}
// ─── Area città (KML) ───────────────────────────────────────────────────
public function caricaKml(PiantineArea $area): void
{
$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');
$this->avvisa("Area della città aggiornata: {$quante} geometrie riconosciute nel KML.");
}
// ─── Piantine ───────────────────────────────────────────────────────────
public function pubblica(int $mappaId): void
{
$mappa = TerritorioMappa::with('territorio.zona')->find($mappaId);
if (! $mappa) {
$this->avvisaErrore('La piantina non esiste più.');
return;
}
if ($motivo = $mappa->motivoNonPubblicabile()) {
$this->avvisaErrore("{$mappa->etichetta()}: {$motivo}");
return;
}
$this->dispatch('pubblica-piantina', mappaId: $mappa->id)->to(Pubblicazione::class);
}
/** La finestra di pubblicazione ha finito: basta ridisegnare l'elenco. */
#[On('piantina-pubblicata')]
public function pubblicata(): void {}
public function elimina(int $mappaId): void
{
$mappa = TerritorioMappa::with('territorio')->find($mappaId);
if (! $mappa) {
return;
}
$etichetta = $mappa->etichetta();
if ($mappa->image_path) {
Storage::disk('public')->delete($mappa->image_path);
}
$mappa->delete();
$this->avvisa("{$etichetta} eliminata. Il territorio e il suo PDF restano com'erano.");
}
// ─── Storico versioni ───────────────────────────────────────────────────
public function ripristinaVersione(int $versioneId, PiantinaPdfService $pdfService): void
{
$versione = TerritorioPdfVersione::with('territorio')->find($versioneId);
if (! $versione) {
return;
}
$numero = $versione->territorio?->numero ?? '?';
try {
$pdfService->ripristina($versione);
} catch (\Throwable $e) {
$this->avvisaErrore($e->getMessage());
return;
}
$this->avvisa("Versione precedente ripristinata per il territorio {$numero}. Il PDF che c'era è nello storico.");
}
public function eliminaVersione(int $versioneId): void
{
$versione = TerritorioPdfVersione::find($versioneId);
if (! $versione) {
return;
}
Storage::disk('public')->delete($versione->pdf_path);
$versione->delete();
$this->avvisa('Versione eliminata definitivamente.');
}
// ─── Dati per la vista ──────────────────────────────────────────────────
/**
* Tutte le piantine con il loro stato, in memoria: sono qualche centinaio
* al massimo, e lo stato dipende dall'impronta, che si calcola in PHP.
*
* @return Collection<int, TerritorioMappa>
*/
private function piantine(): Collection
{
return TerritorioMappa::with('territorio.zona')
->get()
->sortBy(fn (TerritorioMappa $m) => [
$m->territorio_id === null ? 1 : 0,
strlen((string) $m->territorio?->numero),
(string) $m->territorio?->numero,
mb_strtolower((string) $m->nome),
])
->values();
}
private function corrisponde(TerritorioMappa $mappa, string $filtro): bool
{
return match ($filtro) {
'bozze' => $mappa->stato() === TerritorioMappa::STATO_BOZZA,
'da-pubblicare' => $mappa->stato() === TerritorioMappa::STATO_MODIFICATA,
'pubblicate' => $mappa->stato() === TerritorioMappa::STATO_PUBBLICATA,
'scollegate' => ! $mappa->collegata(),
default => true,
};
}
public function render(PiantineArea $area)
{
$piantine = $this->piantine();
$conteggi = collect(self::FILTRI)
->map(fn ($etichetta, $filtro) => $piantine->filter(fn ($m) => $this->corrisponde($m, $filtro))->count());
$cerca = mb_strtolower(trim($this->cerca));
$elenco = $piantine
->filter(fn ($m) => $this->corrisponde($m, $this->filtro))
->filter(fn (TerritorioMappa $m) => $cerca === '' || str_contains(mb_strtolower(implode(' ', [
$m->etichetta(),
$m->territorio?->numero,
$m->nome,
$m->territorio?->zona?->nome,
])), $cerca))
->values();
return view('livewire.piantine.piantine-manager', [
'config' => $area->config(),
'elenco' => $elenco,
'totale' => $piantine->count(),
'conteggi' => $conteggi,
'versioni' => TerritorioPdfVersione::with('territorio')
->orderByDesc('sostituito_il')
->orderByDesc('id')
->paginate(15, pageName: 'versioni'),
'title' => 'Piantine dei territori',
]);
}
}