- 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>
234 lines
7.5 KiB
PHP
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',
|
|
]);
|
|
}
|
|
}
|