exists()) { $this->carica(TerritorioMappa::with('territorio')->find($mappa)); } elseif (request()->boolean('nuovo')) { $this->creaTerritorio = true; } } // ─── Selezione ────────────────────────────────────────────────────────── #[On('piantina-selezionata-mappa')] public function apri($mappaId): void { $mappa = TerritorioMappa::with('territorio')->find(is_numeric($mappaId) ? (int) $mappaId : null); if (! $mappa) { $this->avvisaErrore('Questa piantina non esiste più: forse è stata eliminata da un\'altra scheda.'); $this->dispatch('piantina-rimossa', mappaId: (int) $mappaId); return; } $this->carica($mappa); $this->dispatch('piantina-selezionata', mappaId: $mappa->id, inquadra: true); } /** "Nuova piantina": si deseleziona e la mappa entra in modalità disegno. */ public function nuova(): void { $this->svuota(); $this->dispatch('piantina-selezionata', mappaId: null, inquadra: false); $this->dispatch('editor-disegna'); } private function carica(TerritorioMappa $mappa): void { $this->resetValidation(); $this->mappaId = $mappa->id; $this->nome = (string) $mappa->nome; $this->salvatoAlle = ''; $this->nuovaZona = ''; $this->nuovaTipologia = ''; $this->caricaTerritorio($mappa->territorio); } private function svuota(): void { $this->resetValidation(); $this->reset([ 'mappaId', 'nome', 'salvatoAlle', 'territorioScelto', 'numero', 'zonaId', 'tipologiaId', 'confini', 'nuovaZona', 'nuovaTipologia', ]); } private function caricaTerritorio(?Territorio $territorio): void { $this->territorioScelto = $territorio ? (string) $territorio->id : ''; $this->numero = (string) ($territorio->numero ?? ''); $this->zonaId = (string) ($territorio->zona_id ?? ''); $this->tipologiaId = (string) ($territorio->tipologia_id ?? ''); $this->confini = (string) ($territorio->confini ?? ''); } // ─── Confine (bozza, salvataggio automatico) ──────────────────────────── /** * Arriva dalla mappa dopo un disegno o lo spostamento di un vertice. * $nuova: il disegno è una piantina nuova (strumento "Disegna"), non il * confine ridisegnato di quella aperta. */ #[On('geometria-modificata')] public function salvaGeometria($geometry, bool $nuova = false): void { if (! $this->geometriaValida($geometry)) { $this->avvisaErrore('Il confine disegnato non è valido: ridisegnalo.'); return; } $geojson = json_encode($geometry); $mappa = (! $nuova && $this->mappaId) ? TerritorioMappa::find($this->mappaId) : null; if ($mappa) { $mappa->update(['geojson' => $geojson]); } else { $mappa = TerritorioMappa::create(['geojson' => $geojson]); $this->carica($mappa); activity()->causedBy(auth()->user())->log('piantina_creata'); if ($this->creaTerritorio) { $this->creaTerritorio = false; $this->territorioScelto = 'nuovo'; } } $this->salvato($mappa, seleziona: true); } /** * Anelli chiusi di almeno quattro punti con coordinate nei limiti: un * record malformato faceva fallire il disegno di tutte le piantine. */ private function geometriaValida($geometry): bool { if (! is_array($geometry) || ! is_array($geometry['coordinates'] ?? null)) { return false; } $poligoni = match ($geometry['type'] ?? null) { 'Polygon' => [$geometry['coordinates']], 'MultiPolygon' => $geometry['coordinates'], default => null, }; if (! $poligoni) { return false; } foreach ($poligoni as $anelli) { if (! is_array($anelli) || $anelli === []) { return false; } foreach ($anelli as $anello) { if (! is_array($anello) || count($anello) < 4) { return false; } foreach ($anello as $punto) { if (! is_array($punto) || count($punto) < 2 || ! is_numeric($punto[0]) || ! is_numeric($punto[1]) || abs($punto[0]) > 180 || abs($punto[1]) > 90) { return false; } } } } return true; } /** Riporta il confine a quello pubblicato. */ public function scartaModifiche(): void { $mappa = $this->mappaAperta(); if (! $mappa || ! $mappa->bozzaScartabile()) { return; } $mappa->update(['geojson' => $mappa->geojson_pubblicato]); $this->salvato($mappa, seleziona: true); $this->avvisa('Confine riportato a quello pubblicato.'); } public function updatedNome(): void { $mappa = $this->mappaAperta(); if (! $mappa) { return; } $nome = trim($this->nome); $mappa->update(['nome' => $nome !== '' ? mb_substr($nome, 0, 120) : null]); $this->salvato($mappa); } // ─── Collegamento al territorio ───────────────────────────────────────── public function updatedTerritorioScelto(string $valore): void { $this->resetValidation(); $mappa = $this->mappaAperta(); if (! $mappa) { return; } if ($valore === 'nuovo') { // Si compilano i campi e si crea con il pulsante: un territorio // non nasce da un salvataggio automatico a metà digitazione. $this->numero = ''; $this->zonaId = ''; $this->tipologiaId = ''; $this->confini = ''; return; } if ($valore === '') { $numero = $mappa->territorio?->numero; $mappa->update([ 'territorio_id' => null, // Senza territorio serve un'etichetta, altrimenti la piantina // finisce in elenco come un anonimo "Piantina #12". 'nome' => $mappa->nome ?: ($numero ? "Ex territorio {$numero}" : null), ]); $this->nome = (string) $mappa->nome; $this->caricaTerritorio(null); $this->salvato($mappa); return; } $territorio = Territorio::find($valore); if (! $territorio) { $this->addError('territorioScelto', 'Il territorio scelto non esiste più.'); $this->caricaTerritorio($mappa->territorio); return; } $occupata = TerritorioMappa::where('territorio_id', $territorio->id)->whereKeyNot($mappa->id)->exists(); if ($occupata) { $this->addError('territorioScelto', "Il territorio {$territorio->numero} ha già una piantina."); $this->caricaTerritorio($mappa->territorio); return; } $mappa->update(['territorio_id' => $territorio->id]); $this->caricaTerritorio($territorio); activity()->causedBy(auth()->user())->performedOn($territorio)->log('piantina_collegata'); $this->salvato($mappa); } public function creaTerritorio(): void { $mappa = $this->mappaAperta(); if (! $mappa || $this->territorioScelto !== 'nuovo') { return; } $this->numero = trim($this->numero); $this->confini = trim($this->confini); $this->validate( [ 'numero' => ['required', 'string', 'max:20', Rule::unique('territori', 'numero')], 'zonaId' => ['nullable', Rule::exists('zone', 'id')], 'tipologiaId' => ['nullable', Rule::exists('tipologie', 'id')], 'confini' => ['nullable', 'string', 'max:5000'], ], $this->messaggiTerritorio(), ); try { $territorio = DB::transaction(function () use ($mappa) { $territorio = Territorio::create($this->valoriTerritorio() + ['attivo' => true]); $mappa->update(['territorio_id' => $territorio->id]); return $territorio; }); } catch (\Illuminate\Database\UniqueConstraintViolationException) { $this->addError('numero', 'Esiste già un territorio con questo numero.'); return; } activity()->causedBy(auth()->user())->performedOn($territorio)->log('territorio_creato_da_piantine'); $this->caricaTerritorio($territorio); $this->salvato($mappa); $this->avvisa("Territorio {$territorio->numero} creato e collegato."); } // ─── Dati del territorio (salvati subito nel gestionale) ──────────────── public function updatedNumero(): void { $this->salvaCampoTerritorio('numero'); } public function updatedZonaId(): void { $this->salvaCampoTerritorio('zonaId'); } public function updatedTipologiaId(): void { $this->salvaCampoTerritorio('tipologiaId'); } public function updatedConfini(): void { $this->salvaCampoTerritorio('confini'); } private function salvaCampoTerritorio(string $campo): void { // In creazione i campi si raccolgono e basta: li salva creaTerritorio(). $territorio = $this->territorioCollegato(); if (! $territorio) { return; } $this->numero = trim($this->numero); $regole = [ 'numero' => ['required', 'string', 'max:20', Rule::unique('territori', 'numero')->ignore($territorio->id)], 'zonaId' => ['nullable', Rule::exists('zone', 'id')], 'tipologiaId' => ['nullable', Rule::exists('tipologie', 'id')], 'confini' => ['nullable', 'string', 'max:5000'], ]; $this->validateOnly($campo, $regole, $this->messaggiTerritorio()); // Solo la colonna del campo cambiato: con un altro campo ancora non // valido a schermo, riscrivere tutto salverebbe anche quel valore. $colonna = ['numero' => 'numero', 'zonaId' => 'zona_id', 'tipologiaId' => 'tipologia_id', 'confini' => 'confini'][$campo]; $territorio->update([$colonna => $this->valoriTerritorio()[$colonna]]); $this->salvato($this->mappaAperta()); } private function valoriTerritorio(): array { return [ 'numero' => trim($this->numero), 'zona_id' => $this->zonaId !== '' ? (int) $this->zonaId : null, 'tipologia_id' => $this->tipologiaId !== '' ? (int) $this->tipologiaId : null, 'confini' => trim($this->confini) !== '' ? trim($this->confini) : null, ]; } private function messaggiTerritorio(): array { return [ 'numero.required' => 'Il territorio ha bisogno di un numero.', 'numero.unique' => 'Esiste già un territorio con questo numero.', 'numero.max' => 'Il numero può avere al massimo 20 caratteri.', 'confini.max' => 'I confini possono avere al massimo 5000 caratteri.', ]; } // ─── Zona e tipologia create sul posto ────────────────────────────────── public function creaZona(): void { $this->nuovaZona = trim($this->nuovaZona); $dati = $this->validate( ['nuovaZona' => ['required', 'string', 'max:100', Rule::unique('zone', 'nome')]], [ 'nuovaZona.required' => 'Scrivi il nome della zona.', 'nuovaZona.unique' => 'Questa zona esiste già: sceglila dall\'elenco.', ], ); $zona = Zona::create(['nome' => $dati['nuovaZona'], 'attivo' => true]); $this->zonaId = (string) $zona->id; $this->nuovaZona = ''; $this->updatedZonaId(); $this->avvisa("Zona «{$zona->nome}» creata e selezionata."); } public function creaTipologia(): void { $this->nuovaTipologia = trim($this->nuovaTipologia); $dati = $this->validate( ['nuovaTipologia' => ['required', 'string', 'max:100', Rule::unique('tipologie', 'nome')]], [ 'nuovaTipologia.required' => 'Scrivi il nome della tipologia.', 'nuovaTipologia.unique' => 'Questa tipologia esiste già: sceglila dall\'elenco.', ], ); $tipologia = Tipologia::create(['nome' => $dati['nuovaTipologia'], 'attivo' => true]); $this->tipologiaId = (string) $tipologia->id; $this->nuovaTipologia = ''; $this->updatedTipologiaId(); $this->avvisa("Tipologia «{$tipologia->nome}» creata e selezionata."); } // ─── Pubblicazione ed eliminazione ────────────────────────────────────── public function pubblica(): void { $mappa = $this->mappaAperta(); if (! $mappa) { return; } if ($motivo = $mappa->motivoNonPubblicabile()) { $this->avvisaErrore($motivo); return; } $this->dispatch('pubblica-piantina', mappaId: $mappa->id)->to(Pubblicazione::class); } #[On('piantina-pubblicata')] public function pubblicata(int $mappaId): void { $mappa = TerritorioMappa::with('territorio.zona')->find($mappaId); if ($mappa) { $this->dispatch('piantina-aggiornata', piantina: $this->datiPiantina($mappa)); } } public function elimina(): void { $mappa = $this->mappaAperta(); if (! $mappa) { return; } $etichetta = $mappa->etichetta(); $id = $mappa->id; if ($mappa->image_path) { Storage::disk('public')->delete($mappa->image_path); } $mappa->delete(); $this->svuota(); $this->dispatch('piantina-rimossa', mappaId: $id); $this->avvisa("{$etichetta} eliminata. Il territorio e il suo PDF restano com'erano."); } // ─── Dati per la mappa ────────────────────────────────────────────────── /** Chiamata una volta dalla mappa all'avvio ($wire.datiMappa()). */ public function datiMappa(PiantineArea $area): array { return [ 'area' => $area->area(), 'piantine' => TerritorioMappa::with('territorio.zona')->get() ->filter(fn (TerritorioMappa $m) => $m->geometria() !== null) ->map(fn (TerritorioMappa $m) => $this->datiPiantina($m)) ->values() ->all(), 'selezionata' => $this->mappaId, ]; } private function datiPiantina(TerritorioMappa $mappa): array { return [ 'mappaId' => $mappa->id, 'etichetta' => $mappa->etichetta(), 'collegata' => $mappa->collegata(), 'stato' => $mappa->stato(), 'statoEtichetta' => $mappa->statoEtichetta(), 'geometry' => $mappa->geometria(), ]; } /** Dopo ogni salvataggio automatico: ora in testata, mappa e indirizzo allineati. */ private function salvato(?TerritorioMappa $mappa, bool $seleziona = false): void { if (! $mappa) { return; } $mappa = $mappa->fresh(['territorio.zona']); $this->salvatoAlle = now()->format('H:i'); $this->dispatch('piantina-aggiornata', piantina: $this->datiPiantina($mappa)); if ($seleziona) { $this->dispatch('piantina-selezionata', mappaId: $mappa->id, inquadra: false); } // La piantina aperta resta nell'indirizzo: ricaricando la pagina si // torna su questa e non su un editor vuoto. $this->dispatch('editor-indirizzo', url: route('piantine.editor', $mappa->id)); } private function mappaAperta(): ?TerritorioMappa { return $this->mappaId ? TerritorioMappa::with('territorio.zona')->find($this->mappaId) : null; } private function territorioCollegato(): ?Territorio { if ($this->territorioScelto === '' || $this->territorioScelto === 'nuovo') { return null; } $mappa = $this->mappaAperta(); return $mappa?->territorio; } public function render() { $mappa = $this->mappaAperta(); // Nella tendina: i territori attivi senza piantina, più quello già // collegato anche se nel frattempo è stato disattivato o cestinato. $occupati = TerritorioMappa::whereNotNull('territorio_id') ->when($mappa, fn ($q) => $q->whereKeyNot($mappa->id)) ->pluck('territorio_id'); $territori = Territorio::attivi() ->with(['zona:id,nome', 'tipologia:id,nome']) ->whereNotIn('id', $occupati) ->orderByRaw('LENGTH(numero), numero') ->get(['id', 'numero', 'zona_id', 'tipologia_id', 'attivo', 'deleted_at']); if ($mappa?->territorio && ! $territori->contains('id', $mappa->territorio_id)) { $territori->prepend($mappa->territorio); } // Il campo di scelta con ricerca (): due voci fisse in cima, // poi i territori, che qui restano sceglibili anche se cestinati o // disattivati (è il collegamento che già c'era). $opzioniTerritori = [ ['valore' => '', 'etichetta' => '— nessun territorio —', 'speciale' => true], ['valore' => 'nuovo', 'etichetta' => '+ Crea un territorio nuovo', 'speciale' => true], ...$territori->map(function (Territorio $t) { $opzione = $t->opzioneScelta(nota: $t->trashed() ? 'nel cestino' : (! $t->attivo ? 'disattivato' : null)); $opzione['etichetta'] = str_replace('N° ', 'Territorio ', $opzione['etichetta']); return $opzione; })->all(), ]; return view('livewire.piantine.piantine-editor', [ 'mappa' => $mappa, 'areaCaricata' => app(PiantineArea::class)->config()['kml_nome'] !== null, 'opzioniTerritori' => $opzioniTerritori, 'zone' => Zona::orderBy('nome')->get(['id', 'nome']), 'tipologie' => Tipologia::orderBy('nome')->get(['id', 'nome']), 'title' => $mappa ? $mappa->etichetta() . ' — Editor delle piantine' : 'Editor delle piantine', ]); } }