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>
This commit is contained in:
@@ -2,38 +2,60 @@
|
||||
|
||||
namespace App\Livewire\Piantine;
|
||||
|
||||
use App\Models\Territorio;
|
||||
use App\Livewire\Concerns\Avvisa;
|
||||
use App\Models\TerritorioMappa;
|
||||
use App\Models\TerritorioPdfVersione;
|
||||
use App\Services\Piantine\PiantinaPdfService;
|
||||
use App\Services\Piantine\PiantinaRenderer;
|
||||
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).
|
||||
*
|
||||
* Qui si tiene l'area della città, si generano immagini e PDF, si collegano
|
||||
* le piantine ai territori e si ripescano le versioni sostituite. Il disegno
|
||||
* non sta qui: ha una pagina sua a schermo intero (PiantineEditor), perché su
|
||||
* una mappa larga un terzo di schermo non si taglia un confine.
|
||||
* È 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;
|
||||
|
||||
/** Piantina di cui è aperto il pannello "collega a un territorio". */
|
||||
public ?int $collegamentoAperto = null;
|
||||
|
||||
public ?int $territorioDaCollegare = null;
|
||||
public function updatedFiltro(): void
|
||||
{
|
||||
if (! array_key_exists($this->filtro, self::FILTRI)) {
|
||||
$this->filtro = 'tutte';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Area città (KML) ───────────────────────────────────────────────────
|
||||
|
||||
public function caricaKml(PiantineArea $area)
|
||||
public function caricaKml(PiantineArea $area): void
|
||||
{
|
||||
$this->validate(
|
||||
['kmlFile' => 'required|file|max:10240'],
|
||||
@@ -62,111 +84,42 @@ class PiantineManager extends Component
|
||||
$this->kmlFile = null;
|
||||
|
||||
activity()->causedBy(auth()->user())->log('piantine_kml_caricato');
|
||||
session()->flash('success', "Area della città aggiornata: {$quante} geometrie riconosciute nel KML.");
|
||||
$this->avvisa("Area della città aggiornata: {$quante} geometrie riconosciute nel KML.");
|
||||
}
|
||||
|
||||
// ─── Generazione ────────────────────────────────────────────────────────
|
||||
// ─── Piantine ───────────────────────────────────────────────────────────
|
||||
|
||||
public function generaImmagine(int $mappaId, PiantinaRenderer $renderer)
|
||||
public function pubblica(int $mappaId): void
|
||||
{
|
||||
$mappa = TerritorioMappa::with('territorio')->findOrFail($mappaId);
|
||||
$mappa = TerritorioMappa::with('territorio.zona')->find($mappaId);
|
||||
|
||||
try {
|
||||
$renderer->render($mappa);
|
||||
} catch (\Throwable $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
if (! $mappa) {
|
||||
$this->avvisaErrore('La piantina non esiste più.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
session()->flash('success', "Immagine generata per {$mappa->etichetta()}.");
|
||||
}
|
||||
|
||||
public function generaPdf(int $mappaId, PiantinaPdfService $pdfService)
|
||||
{
|
||||
$mappa = TerritorioMappa::with('territorio')->findOrFail($mappaId);
|
||||
|
||||
if (! $mappa->collegata()) {
|
||||
session()->flash('error', 'Questa piantina non è collegata a nessun territorio: collegala prima di generare il PDF.');
|
||||
if ($motivo = $mappa->motivoNonPubblicabile()) {
|
||||
$this->avvisaErrore("{$mappa->etichetta()}: {$motivo}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdfService->sostituisci($mappa->territorio, $mappa);
|
||||
} catch (\Throwable $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
$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;
|
||||
}
|
||||
|
||||
session()->flash('success', "PDF del territorio {$mappa->territorio->numero} sostituito. Il precedente è nello storico versioni.");
|
||||
}
|
||||
|
||||
// ─── Collegamento al territorio ─────────────────────────────────────────
|
||||
|
||||
public function apriCollegamento(int $mappaId): void
|
||||
{
|
||||
$this->collegamentoAperto = $mappaId;
|
||||
$this->territorioDaCollegare = TerritorioMappa::find($mappaId)?->territorio_id;
|
||||
}
|
||||
|
||||
public function chiudiCollegamento(): void
|
||||
{
|
||||
$this->collegamentoAperto = null;
|
||||
$this->territorioDaCollegare = null;
|
||||
}
|
||||
|
||||
public function collega(int $mappaId)
|
||||
{
|
||||
$mappa = TerritorioMappa::findOrFail($mappaId);
|
||||
|
||||
if (! $this->territorioDaCollegare) {
|
||||
session()->flash('error', 'Scegli il territorio a cui collegare la piantina.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$territorio = Territorio::findOrFail($this->territorioDaCollegare);
|
||||
|
||||
// Un territorio ha una piantina sola: se ne aveva già un'altra il
|
||||
// vincolo di unicità la respingerebbe con un errore di database, che
|
||||
// non spiega niente a chi sta guardando la pagina.
|
||||
$occupato = TerritorioMappa::where('territorio_id', $territorio->id)
|
||||
->where('id', '!=', $mappa->id)
|
||||
->exists();
|
||||
|
||||
if ($occupato) {
|
||||
session()->flash('error', "Il territorio {$territorio->numero} ha già una piantina: scollegala prima di assegnargli questa.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$mappa->update(['territorio_id' => $territorio->id]);
|
||||
$this->chiudiCollegamento();
|
||||
|
||||
activity()->causedBy(auth()->user())->performedOn($territorio)->log('piantina_collegata');
|
||||
session()->flash('success', "Piantina collegata al territorio {$territorio->numero}.");
|
||||
}
|
||||
|
||||
public function scollega(int $mappaId)
|
||||
{
|
||||
$mappa = TerritorioMappa::with('territorio')->findOrFail($mappaId);
|
||||
$numero = $mappa->territorio?->numero ?? '?';
|
||||
|
||||
// Senza territorio serve un'etichetta, altrimenti la piantina sparisce
|
||||
// dall'elenco sotto un anonimo "Piantina #12".
|
||||
$mappa->update([
|
||||
'territorio_id' => null,
|
||||
'nome' => $mappa->nome ?: "Ex territorio {$numero}",
|
||||
]);
|
||||
|
||||
session()->flash('success', "Piantina scollegata dal territorio {$numero}. Il PDF del territorio resta com'è.");
|
||||
}
|
||||
|
||||
public function eliminaPiantina(int $mappaId)
|
||||
{
|
||||
$mappa = TerritorioMappa::with('territorio')->findOrFail($mappaId);
|
||||
$etichetta = $mappa->etichetta();
|
||||
|
||||
if ($mappa->image_path) {
|
||||
@@ -174,59 +127,106 @@ class PiantineManager extends Component
|
||||
}
|
||||
$mappa->delete();
|
||||
|
||||
session()->flash('success', "{$etichetta}: poligono e immagine eliminati. Il PDF del territorio resta com'è.");
|
||||
$this->avvisa("{$etichetta} eliminata. Il territorio e il suo PDF restano com'erano.");
|
||||
}
|
||||
|
||||
// ─── Storico versioni ───────────────────────────────────────────────────
|
||||
|
||||
public function ripristinaVersione(int $versioneId, PiantinaPdfService $pdfService)
|
||||
public function ripristinaVersione(int $versioneId, PiantinaPdfService $pdfService): void
|
||||
{
|
||||
$versione = TerritorioPdfVersione::with('territorio')->findOrFail($versioneId);
|
||||
$versione = TerritorioPdfVersione::with('territorio')->find($versioneId);
|
||||
|
||||
if (! $versione) {
|
||||
return;
|
||||
}
|
||||
|
||||
$numero = $versione->territorio?->numero ?? '?';
|
||||
|
||||
try {
|
||||
$pdfService->ripristina($versione);
|
||||
} catch (\Throwable $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
$this->avvisaErrore($e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
session()->flash('success', "Versione precedente ripristinata per il territorio {$numero}.");
|
||||
$this->avvisa("Versione precedente ripristinata per il territorio {$numero}. Il PDF che c'era è nello storico.");
|
||||
}
|
||||
|
||||
public function eliminaVersione(int $versioneId)
|
||||
public function eliminaVersione(int $versioneId): void
|
||||
{
|
||||
$versione = TerritorioPdfVersione::findOrFail($versioneId);
|
||||
$versione = TerritorioPdfVersione::find($versioneId);
|
||||
|
||||
if (Storage::disk('public')->exists($versione->pdf_path)) {
|
||||
Storage::disk('public')->delete($versione->pdf_path);
|
||||
if (! $versione) {
|
||||
return;
|
||||
}
|
||||
|
||||
Storage::disk('public')->delete($versione->pdf_path);
|
||||
$versione->delete();
|
||||
|
||||
session()->flash('success', 'Versione eliminata definitivamente.');
|
||||
$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(),
|
||||
'piantine' => TerritorioMappa::with('territorio')
|
||||
->get()
|
||||
->sortBy(fn ($m) => [
|
||||
$m->collegata() ? 0 : 1,
|
||||
strlen((string) $m->territorio?->numero),
|
||||
(string) $m->territorio?->numero,
|
||||
(string) $m->nome,
|
||||
])
|
||||
->values(),
|
||||
'territori' => Territorio::attivi()
|
||||
->orderByRaw('LENGTH(numero), numero')
|
||||
->get(['id', 'numero']),
|
||||
'elenco' => $elenco,
|
||||
'totale' => $piantine->count(),
|
||||
'conteggi' => $conteggi,
|
||||
'versioni' => TerritorioPdfVersione::with('territorio')
|
||||
->orderByDesc('sostituito_il')
|
||||
->limit(30)
|
||||
->get(),
|
||||
->orderByDesc('id')
|
||||
->paginate(15, pageName: 'versioni'),
|
||||
'title' => 'Piantine dei territori',
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user