- 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>
183 lines
5.8 KiB
PHP
183 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
/**
|
|
* Il disegno di un'area sulla mappa.
|
|
*
|
|
* Puo' essere collegato a un territorio oppure no: si disegna guardando la
|
|
* cartografia, il numero si decide dopo. Finche' resta scollegato vale il
|
|
* campo `nome` come etichetta; una volta collegato comanda il numero del
|
|
* territorio, che e' il riferimento con cui lo chiamano tutti.
|
|
*
|
|
* `geojson` e' la bozza: l'editor la salva da solo a ogni modifica, e non
|
|
* tocca il PDF del territorio. Il PDF cambia solo con la pubblicazione, che
|
|
* ricorda cosa e' stato pubblicato (`geojson_pubblicato`,
|
|
* `impronta_pubblicata`, `pubblicata_at`).
|
|
*/
|
|
class TerritorioMappa extends Model
|
|
{
|
|
public const STATO_BOZZA = 'bozza';
|
|
|
|
public const STATO_PUBBLICATA = 'pubblicata';
|
|
|
|
public const STATO_MODIFICATA = 'modificata';
|
|
|
|
protected $table = 'territorio_mappe';
|
|
|
|
protected $fillable = [
|
|
'territorio_id',
|
|
'nome',
|
|
'geojson',
|
|
'geojson_pubblicato',
|
|
'impronta_pubblicata',
|
|
'pubblicata_at',
|
|
'image_path',
|
|
'rendered_at',
|
|
];
|
|
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'rendered_at' => 'datetime',
|
|
'pubblicata_at' => 'datetime',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* withTrashed: un territorio nel cestino resta collegato. Senza, la
|
|
* relazione tornava null, l'editor mostrava la piantina come scollegata e
|
|
* il primo salvataggio cancellava il collegamento — che al ripristino del
|
|
* territorio non c'era piu'.
|
|
*/
|
|
public function territorio()
|
|
{
|
|
return $this->belongsTo(Territorio::class, 'territorio_id')->withTrashed();
|
|
}
|
|
|
|
/**
|
|
* La geometria decodificata (Polygon/MultiPolygon GeoJSON) o null se
|
|
* il contenuto non è più interpretabile.
|
|
*/
|
|
public function geometria(): ?array
|
|
{
|
|
$decoded = json_decode((string) $this->geojson, true);
|
|
|
|
return is_array($decoded) ? $decoded : null;
|
|
}
|
|
|
|
/** Collegata a un territorio vivo: è la condizione per pubblicare. */
|
|
public function collegata(): bool
|
|
{
|
|
return $this->territorio_id !== null
|
|
&& $this->territorio !== null
|
|
&& ! $this->territorio->trashed();
|
|
}
|
|
|
|
public function territorioNelCestino(): bool
|
|
{
|
|
return $this->territorio !== null && $this->territorio->trashed();
|
|
}
|
|
|
|
/**
|
|
* Come si chiama questa piantina nell'interfaccia: il numero del
|
|
* territorio se c'è, altrimenti il nome scelto a mano, altrimenti un
|
|
* ripiego che almeno la distingue dalle altre.
|
|
*/
|
|
public function etichetta(): string
|
|
{
|
|
if ($this->territorio !== null) {
|
|
return 'Terr. ' . $this->territorio->numero;
|
|
}
|
|
|
|
$nome = trim((string) $this->nome);
|
|
|
|
return $nome !== '' ? $nome : 'Piantina #' . $this->id;
|
|
}
|
|
|
|
// ─── Bozza e pubblicazione ──────────────────────────────────────────────
|
|
|
|
/**
|
|
* Impronta di tutto cio' che finisce nel PDF: confine, territorio e i
|
|
* suoi dati stampati sulla cartolina (numero, zona come «Località»,
|
|
* confini). Se cambia uno qualunque di questi, il PDF in vigore non
|
|
* corrisponde piu' e la piantina ha modifiche da pubblicare — anche quando
|
|
* il disegno e' rimasto lo stesso.
|
|
*/
|
|
public function impronta(): string
|
|
{
|
|
$territorio = $this->territorio;
|
|
|
|
return hash('sha256', json_encode([
|
|
$this->geometria(),
|
|
$this->territorio_id,
|
|
$territorio ? trim((string) $territorio->numero) : null,
|
|
$territorio?->zona?->nome,
|
|
$territorio ? trim((string) $territorio->confini) : null,
|
|
]));
|
|
}
|
|
|
|
public function stato(): string
|
|
{
|
|
if ($this->pubblicata_at === null) {
|
|
return self::STATO_BOZZA;
|
|
}
|
|
|
|
return $this->impronta() === $this->impronta_pubblicata && $this->pdfInUsoDallaPiantina()
|
|
? self::STATO_PUBBLICATA
|
|
: self::STATO_MODIFICATA;
|
|
}
|
|
|
|
/**
|
|
* Il PDF in vigore del territorio è uno composto da una piantina. Se nel
|
|
* frattempo è stato sostituito per altre vie (ripristino di una versione,
|
|
* caricamento a mano, import), quello che arriva ai proclamatori non è
|
|
* più questo disegno, anche se l'impronta torna.
|
|
*/
|
|
public function pdfInUsoDallaPiantina(): bool
|
|
{
|
|
return $this->territorio_id !== null
|
|
&& str_starts_with((string) $this->territorio?->pdf_path, 'territori-pdf/piantina-' . $this->territorio_id . '-');
|
|
}
|
|
|
|
/** Il testo dell'etichetta di stato, uguale in ogni pagina. */
|
|
public function statoEtichetta(): string
|
|
{
|
|
return match ($this->stato()) {
|
|
self::STATO_BOZZA => 'Bozza',
|
|
self::STATO_PUBBLICATA => 'Pubblicata',
|
|
self::STATO_MODIFICATA => 'Modifiche da pubblicare',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Perché non si può pubblicare, o null se si può. Il testo va a schermo
|
|
* accanto al pulsante: dice cosa fare, non solo che manca qualcosa.
|
|
*/
|
|
public function motivoNonPubblicabile(): ?string
|
|
{
|
|
if ($this->geometria() === null) {
|
|
return 'Disegna il confine della piantina.';
|
|
}
|
|
|
|
if ($this->territorioNelCestino()) {
|
|
return 'Il territorio collegato è nel cestino: ripristinalo o collega un altro territorio.';
|
|
}
|
|
|
|
if (! $this->collegata()) {
|
|
return 'Collega la piantina a un territorio: il PDF è quello del territorio.';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Il confine pubblicato è diverso da quello in bozza: si può scartare. */
|
|
public function bozzaScartabile(): bool
|
|
{
|
|
return $this->geojson_pubblicato !== null
|
|
&& json_decode($this->geojson_pubblicato, true) != $this->geometria();
|
|
}
|
|
}
|