++ scelta di territori e proclamatori con ricerca
Al posto delle tendine (Assegna, Registro, editor delle piantine) un
campo in cui si scrive e compaiono le proposte più vicine: uguale, inizia
con, contiene, errore di battitura ("forse cercavi"); parole in qualsiasi
ordine, senza badare a maiuscole e accenti. Campo vuoto: la tendina
completa. Fatto anche per il telefono (16px, voci da 44px, scelta al tocco).
- Logica pura in resources/js/scelta.js con test node (tests/js).
- Opzioni dal server: Territorio/Proclamatore::opzioneScelta(); i
territori già assegnati si trovano ("già assegnato a …") ma non si
scelgono. Nomi cifrati ordinati e cercati decifrati, mai in SQL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
96
resources/views/components/scelta.blade.php
Normal file
96
resources/views/components/scelta.blade.php
Normal file
@@ -0,0 +1,96 @@
|
||||
{{--
|
||||
Campo di scelta con ricerca, al posto di <select> per territori e
|
||||
proclamatori. Si scrive e sotto compaiono le proposte più vicine (uguale,
|
||||
inizia con, contiene, errore di battitura); con il campo vuoto o appena
|
||||
aperto c'è la tendina completa. Logica in resources/js/scelta.js, provata
|
||||
da tests/js/scelta.test.mjs; comportamento in resources/js/app.js.
|
||||
|
||||
id: id del campo (per la <label for>)
|
||||
opzioni: array di opzioni, vedi Territorio::opzioneScelta() e
|
||||
Proclamatore::opzioneScelta(); per le voci fisse in cima
|
||||
['valore' => '', 'etichetta' => '…', 'speciale' => true]
|
||||
segnaposto: testo nel campo vuoto
|
||||
disabilitato: true per bloccare il campo (territorio già preselezionato)
|
||||
vuoto: il valore di "nessuna scelta": '' (default) per le proprietà
|
||||
stringa, null per quelle ?int
|
||||
|
||||
Il valore va a Livewire come con un <select>: wire:model sul componente.
|
||||
Si ridisegna da capo quando cambiano le opzioni (wire:key con l'impronta),
|
||||
perché x-data le legge una volta sola.
|
||||
--}}
|
||||
@props(['id', 'opzioni' => [], 'segnaposto' => 'Scrivi per cercare…', 'disabilitato' => false, 'vuoto' => ''])
|
||||
|
||||
@php($impronta = substr(md5(json_encode($opzioni)), 0, 12))
|
||||
|
||||
<div x-data="sceltaRicerca(@js(array_values($opzioni)), @js($vuoto))"
|
||||
x-modelable="valore"
|
||||
wire:key="scelta-{{ $id }}-{{ $impronta }}"
|
||||
{{ $attributes->whereStartsWith('wire:model') }}
|
||||
{{ $attributes->whereDoesntStartWith('wire:model')->class('tm-scelta') }}
|
||||
x-on:click.outside="aperto && chiudi()"
|
||||
x-on:keydown.escape="if (aperto) { $event.stopPropagation(); chiudi() }">
|
||||
<div class="tm-scelta-campo">
|
||||
<input x-ref="campo"
|
||||
id="{{ $id }}"
|
||||
type="text"
|
||||
role="combobox"
|
||||
aria-autocomplete="list"
|
||||
aria-controls="{{ $id }}-elenco"
|
||||
x-bind:aria-expanded="aperto ? 'true' : 'false'"
|
||||
x-bind:aria-activedescendant="aperto && risultati.length ? '{{ $id }}-voce-' + evidenziato : null"
|
||||
autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false"
|
||||
inputmode="search" enterkeyhint="done"
|
||||
placeholder="{{ $segnaposto }}"
|
||||
@disabled($disabilitato)
|
||||
x-model="testo"
|
||||
x-on:focus="$el.select(); apri()"
|
||||
x-on:click="apri()"
|
||||
x-on:input="scrivendo()"
|
||||
x-on:keydown.arrow-down.prevent="sposta(1)"
|
||||
x-on:keydown.arrow-up.prevent="sposta(-1)"
|
||||
x-on:keydown.enter.prevent="scegliEvidenziato()"
|
||||
x-on:keydown.tab="aperto && chiudi()"
|
||||
class="tm-scelta-input">
|
||||
|
||||
@unless($disabilitato)
|
||||
<button type="button" class="tm-scelta-pulisci" x-show="testo !== ''" x-cloak
|
||||
x-on:pointerdown.prevent x-on:click="svuota()"
|
||||
aria-label="Svuota il campo" tabindex="-1">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
<button type="button" class="tm-scelta-apri"
|
||||
x-on:pointerdown.prevent x-on:click="alterna()"
|
||||
x-bind:aria-label="aperto ? 'Chiudi l\'elenco' : 'Apri l\'elenco'" tabindex="-1">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true" x-bind:class="aperto && 'tm-scelta-giu'"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
||||
</button>
|
||||
@endunless
|
||||
</div>
|
||||
|
||||
<ul x-ref="elenco" id="{{ $id }}-elenco" role="listbox" class="tm-scelta-elenco" x-show="aperto" x-cloak>
|
||||
<template x-for="(opzione, i) in risultati" :key="String(opzione.valore) + '|' + i">
|
||||
<li x-bind:id="'{{ $id }}-voce-' + i"
|
||||
role="option"
|
||||
x-bind:aria-selected="String(opzione.valore) === String(valore ?? '') ? 'true' : 'false'"
|
||||
x-bind:aria-disabled="opzione.motivo ? 'true' : null"
|
||||
x-bind:data-evidenziato="i === evidenziato ? '1' : null"
|
||||
class="tm-scelta-voce"
|
||||
x-bind:class="{
|
||||
'tm-scelta-voce-attiva': i === evidenziato,
|
||||
'tm-scelta-voce-scelta': String(opzione.valore) === String(valore ?? ''),
|
||||
'tm-scelta-voce-spenta': !! opzione.motivo,
|
||||
'tm-scelta-voce-speciale': !! opzione.speciale,
|
||||
}"
|
||||
x-on:pointerdown.prevent
|
||||
x-on:click="scegli(opzione, true)"
|
||||
x-on:pointermove="evidenziato = i">
|
||||
<span class="tm-scelta-etichetta" x-text="opzione.etichetta"></span>
|
||||
<span class="tm-scelta-dettaglio" x-show="opzione.dettaglio" x-text="opzione.dettaglio"></span>
|
||||
<span class="tm-scelta-motivo" x-show="opzione.motivo" x-text="opzione.motivo"></span>
|
||||
<span class="tm-scelta-forse" x-show="opzione.forse">forse cercavi</span>
|
||||
</li>
|
||||
</template>
|
||||
<li class="tm-scelta-vuoto" x-show="risultati.length === 0" role="presentation">
|
||||
Nessun risultato per «<span x-text="testo"></span>»
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -127,25 +127,11 @@
|
||||
<div x-show="formOpen" x-cloak class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-6 max-w-3xl">
|
||||
<form wire:submit="save" class="space-y-4">
|
||||
<div>
|
||||
@if(!$preselectedTerritorioId)
|
||||
<label for="territorio_search" class="block text-sm font-medium text-gray-700">Filtra territori</label>
|
||||
<input wire:model.live.debounce.300ms="territorioSearch"
|
||||
type="text"
|
||||
id="territorio_search"
|
||||
placeholder="Cerca per numero, zona o tipologia"
|
||||
class="mt-1 mb-2 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||
@endif
|
||||
|
||||
<label for="territorio_id" class="block text-sm font-medium text-gray-700">Territorio *</label>
|
||||
<select wire:model.live="territorio_id" id="territorio_id" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm" @if($preselectedTerritorioId) disabled @endif>
|
||||
<option value="">Seleziona un territorio</option>
|
||||
@foreach($territoriDisponibili as $t)
|
||||
<option value="{{ $t->id }}">N° {{ $t->numero }} — {{ $t->zona?->nome }} ({{ $t->tipologia?->nome }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if($preselectedTerritorioId)
|
||||
<input type="hidden" wire:model="territorio_id" value="{{ $preselectedTerritorioId }}">
|
||||
@endif
|
||||
<label for="territorio_id" class="block text-sm font-medium text-gray-700 mb-1">Territorio *</label>
|
||||
<x-scelta id="territorio_id" wire:model.live="territorio_id"
|
||||
:opzioni="$opzioniTerritori"
|
||||
segnaposto="Numero, zona o tipologia" :vuoto="null"
|
||||
:disabilitato="(bool) $preselectedTerritorioId" />
|
||||
@error('territorio_id') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
|
||||
@if($territorio_id)
|
||||
@@ -167,13 +153,10 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="proclamatore_id" class="block text-sm font-medium text-gray-700">Proclamatore *</label>
|
||||
<select wire:model="proclamatore_id" id="proclamatore_id" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||
<option value="">Seleziona un proclamatore</option>
|
||||
@foreach($proclamatoriAttivi as $p)
|
||||
<option value="{{ $p->id }}">{{ $p->cognome }} {{ $p->nome }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label for="proclamatore_id" class="block text-sm font-medium text-gray-700 mb-1">Proclamatore *</label>
|
||||
<x-scelta id="proclamatore_id" wire:model="proclamatore_id"
|
||||
:opzioni="$opzioniProclamatori"
|
||||
segnaposto="Cognome o nome" :vuoto="null" />
|
||||
@error('proclamatore_id') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
|
||||
@@ -148,15 +148,8 @@
|
||||
|
||||
<div class="tm-campo">
|
||||
<label for="territorioScelto">Collegata a</label>
|
||||
<select id="territorioScelto" wire:model.live="territorioScelto">
|
||||
<option value="">— nessun territorio —</option>
|
||||
<option value="nuovo">+ Crea un territorio nuovo</option>
|
||||
@foreach($territori as $territorio)
|
||||
<option value="{{ $territorio->id }}">
|
||||
Territorio {{ $territorio->numero }}@if($territorio->trashed()) (nel cestino)@elseif(! $territorio->attivo) (disattivato)@endif
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<x-scelta id="territorioScelto" wire:model.live="territorioScelto"
|
||||
:opzioni="$opzioniTerritori" segnaposto="Numero, zona o tipologia" />
|
||||
@error('territorioScelto') <p class="tm-campo-errore">{{ $message }}</p> @enderror
|
||||
@if($mappa->territorioNelCestino())
|
||||
<p class="tm-campo-aiuto tm-campo-aiuto-attesa">
|
||||
|
||||
@@ -188,25 +188,17 @@
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;">
|
||||
{{-- Territorio --}}
|
||||
<div style="grid-column:1/-1;">
|
||||
<label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:4px;">Territorio *</label>
|
||||
<select wire:model="form_territorio_id" style="width:100%;border:1px solid #d1d5db;border-radius:8px;padding:8px 10px;font-size:14px;">
|
||||
<option value="">-- seleziona --</option>
|
||||
@foreach($territori as $t)
|
||||
<option value="{{ $t->id }}">N° {{ $t->numero }} — {{ $t->zona?->nome }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label for="form_territorio_id" style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:4px;">Territorio *</label>
|
||||
<x-scelta id="form_territorio_id" wire:model="form_territorio_id"
|
||||
:opzioni="$opzioniTerritori" segnaposto="Numero, zona o tipologia" />
|
||||
@error('form_territorio_id') <span style="color:#dc2626;font-size:12px;">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Proclamatore --}}
|
||||
<div style="grid-column:1/-1;">
|
||||
<label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:4px;">Proclamatore *</label>
|
||||
<select wire:model="form_proclamatore_id" style="width:100%;border:1px solid #d1d5db;border-radius:8px;padding:8px 10px;font-size:14px;">
|
||||
<option value="">-- seleziona --</option>
|
||||
@foreach($proclamatori as $p)
|
||||
<option value="{{ $p->id }}">{{ $p->cognome }} {{ $p->nome }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label for="form_proclamatore_id" style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:4px;">Proclamatore *</label>
|
||||
<x-scelta id="form_proclamatore_id" wire:model="form_proclamatore_id"
|
||||
:opzioni="$opzioniProclamatori" segnaposto="Cognome o nome" />
|
||||
@error('form_proclamatore_id') <span style="color:#dc2626;font-size:12px;">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user