['except' => ''], 'filtroAnno' => ['except' => ''], 'filtroZona' => ['except' => ''], 'filtroTipologia' => ['except' => ''], 'filtroStato' => ['except' => ''], ]; public function updatingSearch() { $this->resetPage(); } /** * Columns that may be sorted on. * * $sortField reaches orderBy() directly, and it is a public Livewire * property, so the client controls it. Laravel's grammar prevents SQL * injection, but an unknown column still raises "Column not found" and * returns a 500 — trivially triggerable from the browser console. */ private const SORTABLE_FIELDS = [ 'territorio_numero', 'assigned_at', 'returned_at', ]; public function sortBy(string $field) { if (! in_array($field, self::SORTABLE_FIELDS, true)) { return; } if ($this->sortField === $field) { $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; } else { $this->sortField = $field; $this->sortDirection = 'desc'; } } // ─── Admin CRUD ───────────────────────────────────────────── public function openCreate(): void { abort_if(!auth()->user()->can('settings.manage'), 403); $this->editingId = null; $this->form_territorio_id = ''; $this->form_proclamatore_id = ''; $this->form_assigned_at = now()->format('Y-m-d'); $this->form_returned_at = ''; $this->form_counted_in_campaign = false; $this->form_campaign_id = ''; $this->form_note = ''; $this->resetValidation(); $this->showModal = true; } public function openEdit(int $id): void { abort_if(!auth()->user()->can('settings.manage'), 403); $a = Assegnazione::findOrFail($id); $this->editingId = $id; $this->form_territorio_id = (string) $a->territorio_id; $this->form_proclamatore_id = (string) $a->proclamatore_id; $this->form_assigned_at = $a->assigned_at?->format('Y-m-d') ?? ''; $this->form_returned_at = $a->returned_at?->format('Y-m-d') ?? ''; $this->form_counted_in_campaign = (bool) $a->counted_in_campaign; $this->form_campaign_id = (string) ($a->campaign_id ?? ''); $this->form_note = $a->note ?? ''; $this->resetValidation(); $this->showModal = true; } public function save(): void { abort_if(!auth()->user()->can('settings.manage'), 403); $this->validate([ 'form_territorio_id' => 'required|exists:territori,id', 'form_proclamatore_id' => 'required|exists:proclamatori,id', 'form_assigned_at' => 'required|date|before_or_equal:today', 'form_returned_at' => 'nullable|date|after_or_equal:form_assigned_at|before_or_equal:today', 'form_campaign_id' => 'nullable|exists:campagne,id', ], [ 'form_assigned_at.before_or_equal' => 'La data di assegnazione non può essere futura.', 'form_returned_at.before_or_equal' => 'La data di rientro non può essere futura.', 'form_returned_at.after_or_equal' => 'Il rientro non può precedere l\'assegnazione.', ]); $uscita = Carbon::parse($this->form_assigned_at); $rientro = $this->form_returned_at ? Carbon::parse($this->form_returned_at) : null; // L'anno di servizio non si sceglie: è quello della data di uscita. // Scelto a mano poteva restare incoerente con la data, e l'assegnazione // finiva contata nell'anno sbagliato da Home, filtri e modulo S-13. $data = [ 'territorio_id' => $this->form_territorio_id, 'proclamatore_id' => $this->form_proclamatore_id, 'anno_teocratico_id' => AnnoTeocratico::perData($uscita)->id, 'assigned_at' => $uscita, 'returned_at' => $rientro, // "Contata nella campagna" senza campagna non ha significato. 'counted_in_campaign' => $this->form_counted_in_campaign && $this->form_campaign_id !== '', 'campaign_id' => $this->form_campaign_id ?: null, 'note' => $this->form_note ?: null, ]; $assegnazione = null; $prima = [false, null]; $conflitto = DB::transaction(function () use ($data, $uscita, $rientro, &$assegnazione, &$prima) { Territorio::whereKey($data['territorio_id'])->lockForUpdate()->first(); $conflitto = Assegnazione::sovrappostaA((int) $data['territorio_id'], $uscita, $rientro, $this->editingId); if ($conflitto) { return $conflitto; } if ($this->editingId) { $assegnazione = Assegnazione::findOrFail($this->editingId); $prima = [(bool) $assegnazione->counted_in_campaign, $assegnazione->campaign_id ? (int) $assegnazione->campaign_id : null]; $assegnazione->update($data); } else { $assegnazione = Assegnazione::create($data + ['created_by' => auth()->id()]); } return null; }); if ($conflitto) { $this->addError('form_assigned_at', "Il periodo si sovrappone all'assegnazione {$conflitto->descrizionePeriodo()}."); return; } // Log the campaign choice when the admin sets or changes it here. $dopo = [(bool) $assegnazione->counted_in_campaign, $assegnazione->campaign_id ? (int) $assegnazione->campaign_id : null]; if ($dopo !== $prima && ($dopo[0] || $prima[0])) { $assegnazione->logCampagnaPercorso( $assegnazione->campagna ?? ($prima[1] ? Campagna::find($prima[1]) : null), $dopo[0], 'Registro' ); } $this->showModal = false; } public function askDelete(int $id): void { abort_if(!auth()->user()->can('settings.manage'), 403); $this->deleteId = $id; $this->showDeleteConfirm = true; } public function deleteConfirmed(): void { abort_if(!auth()->user()->can('settings.manage'), 403); if ($this->deleteId) { Assegnazione::findOrFail($this->deleteId)->delete(); } $this->deleteId = null; $this->showDeleteConfirm = false; } // ─── Modulo S-13 ──────────────────────────────────────────── /** * Chiede quale anno di servizio stampare. L'anno del filtro è solo la * proposta iniziale: il modulo finisce in archivio e viene firmato, quindi * la scelta va confermata invece di essere ereditata da uno stato della * pagina che l'utente può non avere presente. */ public function openS13(): void { abort_if(!auth()->user()->can('settings.manage'), 403); $this->s13Anno = $this->filtroAnno !== '' ? $this->filtroAnno : (string) AnnoTeocratico::corrente()->id; $this->resetValidation(); $this->showS13Modal = true; } /** * Modulo ufficiale S-13-I compilato per l'anno di servizio scelto nella * modale; le regole di compilazione stanno in ModuloS13. * * Solo amministratore, come il resto del CRUD di questa pagina: il modulo * espone in un unico file l'anagrafica completa di chi ha avuto quale * territorio, dati personali che il ruolo assegnatario non deve poter * esportare in blocco. */ public function downloadS13(ModuloS13 $modulo) { abort_if(!auth()->user()->can('settings.manage'), 403); $this->validate( ['s13Anno' => 'required|exists:anni_teocratici,id'], [ 's13Anno.required' => "Seleziona l'anno di servizio da stampare.", 's13Anno.exists' => "L'anno di servizio selezionato non esiste.", ] ); $anno = AnnoTeocratico::findOrFail($this->s13Anno); $this->showS13Modal = false; $pdf = $modulo->genera($anno); return response()->streamDownload( fn () => print($pdf), 'S-13-registro-assegnazioni-' . $anno->label . '.pdf', ['Content-Type' => 'application/pdf'] ); } public function mount(): void { if ($this->filtroAnno === '') { $annoCorrente = AnnoTeocratico::corrente(); $this->filtroAnno = (string) $annoCorrente->id; } } public function render() { $query = Assegnazione::with(['territorio.zona', 'territorio.assegnazioneCorrente', 'proclamatore', 'annoTeocratico', 'campagna', 'gruppo']); if ($this->filtroAnno) { $query->where('anno_teocratico_id', $this->filtroAnno); } if ($this->filtroStato === 'aperte') { $query->aperte(); } elseif ($this->filtroStato === 'chiuse') { $query->chiuse(); } if ($this->filtroZona) { $query->whereHas('territorio', fn($q) => $q->where('zona_id', $this->filtroZona)); } if ($this->filtroTipologia) { $query->whereHas('territorio', fn($q) => $q->where('tipologia_id', $this->filtroTipologia)); } // Re-check here too: sortBy() guards the action, but the property can // also be set directly in a Livewire update payload. $sortField = in_array($this->sortField, self::SORTABLE_FIELDS, true) ? $this->sortField : 'territorio_numero'; $dir = $this->sortDirection === 'asc' ? 'ASC' : 'DESC'; if ($sortField === 'territorio_numero') { $query->orderByRaw( "CAST((SELECT numero FROM territori WHERE territori.id = assegnazioni.territorio_id) AS UNSIGNED) $dir" ); } else { $query->orderBy($sortField, $dir); } // In-memory search for encrypted proclamatore fields / territorio numero if ($this->search !== '') { $all = $query->get(); $filtered = $all->filter(function ($a) { $haystack = mb_strtolower( ($a->territorio?->numero ?? '') . ' ' . ($a->proclamatore?->nome ?? '') . ' ' . ($a->proclamatore?->cognome ?? '') ); return str_contains($haystack, mb_strtolower($this->search)); }); $page = $this->getPage(); $perPage = 25; $items = $filtered->slice(($page - 1) * $perPage, $perPage)->values(); $assegnazioni = new \Illuminate\Pagination\LengthAwarePaginator( $items, $filtered->count(), $perPage, $page, ['path' => request()->url()] ); } else { $assegnazioni = $query->paginate(25); } return view('livewire.registro', [ 'assegnazioni' => $assegnazioni, 'anni' => AnnoTeocratico::orderByDesc('start_date')->get(), 'zone' => Zona::attive()->orderBy('nome')->get(), 'tipologie' => Tipologia::orderBy('nome')->get(), // Campi di scelta con ricerca della finestra di modifica. Ci sono // anche i disattivati, non sceglibili: modificando una vecchia // assegnazione il campo mostra comunque il nome giusto. 'opzioniTerritori' => Territorio::with(['zona', 'tipologia'])->get() ->sortBy('numero', SORT_NATURAL) ->map(fn (Territorio $t) => $t->opzioneScelta($t->attivo ? null : 'territorio disattivato')) ->values()->all(), 'opzioniProclamatori' => Proclamatore::opzioniScelta(Proclamatore::all()), 'campagne' => Campagna::orderByDesc('created_at')->get(), ]); } }