++ gruppi di servizio: sorvegliante con utenza limitata, pacchetto di territori, assegnazione ai membri
Il gruppo ha un sorvegliante (proclamatore con utenza: email e password temporanea, senza conferma dell'email) e dei membri; l'amministratore mette in blocco dei territori nel pacchetto del gruppo. Il pacchetto non e' un'assegnazione e non tocca le statistiche; quando il sorvegliante da' un territorio a un membro nasce un'assegnazione normale con gruppo_servizio_id. Al rientro il territorio torna in reparto. Sciogliere il gruppo libera pacchetto e membri e disattiva l'utenza. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
72
resources/views/livewire/assegnazioni/_invio-link.blade.php
Normal file
72
resources/views/livewire/assegnazioni/_invio-link.blade.php
Normal file
@@ -0,0 +1,72 @@
|
||||
{{-- Finestra di invio del link dopo un'assegnazione. Logica nel trait
|
||||
App\Livewire\Concerns\InviaLink; la includono Assegna e Il mio gruppo. --}}
|
||||
{{-- Invio del link (InviaLink::apriInvio): blocca la pagina finché non si
|
||||
risponde "inviato / non ancora". Se si chiude la scheda prima,
|
||||
l'assegnazione resta segnalata con "link ancora da inviare". --}}
|
||||
@if($invio)
|
||||
@php
|
||||
$link = $invio->shortPdfUrl();
|
||||
$messaggio = $invio->messaggioLink($link);
|
||||
@endphp
|
||||
<div class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-gray-900/50 p-4 sm:items-center"
|
||||
role="dialog" aria-modal="true" aria-labelledby="invio-titolo" wire:key="invio-{{ $invio->id }}">
|
||||
<section class="tm-da-inviare tm-da-inviare-finestra">
|
||||
<h2 id="invio-titolo" class="tm-da-inviare-titolo">
|
||||
Territorio N° {{ $invio->territorio?->numero }} assegnato a {{ $invio->proclamatore?->nome_completo }}
|
||||
</h2>
|
||||
|
||||
@if($link)
|
||||
<p class="text-sm text-gray-700 mb-2">Come vuoi mandare il link della piantina? Il destinatario lo scegli tu, tra i tuoi contatti.</p>
|
||||
|
||||
<div class="tm-da-inviare-link">
|
||||
<code id="invio-link" x-data x-on:click="window.getSelection().selectAllChildren($el)">{{ $link }}</code>
|
||||
</div>
|
||||
|
||||
{{-- Il messaggio sta in data-messaggio: dentro <x-btn> le direttive
|
||||
Blade (@js) non si compilano, quindi i pulsanti lo leggono da qui. --}}
|
||||
<div class="tm-da-inviare-azioni" x-data="{ copiato: false, condivisibile: !! navigator.share }"
|
||||
data-messaggio="{{ $messaggio }}" data-titolo="Territorio N° {{ $invio->territorio?->numero }}">
|
||||
<x-btn :variante="$canaleUsato === 'copia' ? 'primary' : 'secondary'"
|
||||
x-on:click="navigator.clipboard?.writeText(document.getElementById('invio-link').textContent.trim()).then(() => { copiato = true; setTimeout(() => copiato = false, 2000) }).catch(() => {}); $wire.canale('copia')">
|
||||
<span x-show="! copiato">Copia link</span>
|
||||
<span x-show="copiato" x-cloak>Copiato</span>
|
||||
</x-btn>
|
||||
|
||||
{{-- Condividi: il menu di condivisione del telefono (WhatsApp,
|
||||
Telegram, SMS, posta…), da cui si sceglie app e contatto. Solo
|
||||
dove il browser lo offre. Il canale si annota a condivisione
|
||||
fatta, non se la si annulla. --}}
|
||||
<x-btn :variante="$canaleUsato === 'condividi' ? 'primary' : 'secondary'" x-show="condivisibile" x-cloak
|
||||
x-on:click="const d = $el.closest('[data-messaggio]').dataset; navigator.share({ title: d.titolo, text: d.messaggio }).then(() => $wire.canale('condividi')).catch(() => {})">
|
||||
Condividi…
|
||||
</x-btn>
|
||||
</div>
|
||||
|
||||
@if($canaleUsato)
|
||||
<p class="mt-2 text-sm text-gray-700">Canale scelto: {{ \App\Models\Assegnazione::CANALI_LINK[$canaleUsato] }}.</p>
|
||||
@endif
|
||||
@if($erroreInvio)
|
||||
<p class="mt-2 text-sm text-red-600" role="alert">{{ $erroreInvio }}</p>
|
||||
@endif
|
||||
|
||||
<div class="tm-da-inviare-scelta">
|
||||
<p>Hai inviato il link?</p>
|
||||
<div>
|
||||
<x-btn variante="primary" wire:click="concludiInvio(true)" :disabled="$canaleUsato === null"
|
||||
:title="$canaleUsato === null ? 'Scegli prima come mandare il link' : null">Sì, inviato</x-btn>
|
||||
<x-btn variante="secondary" wire:click="concludiInvio(false)">No, non ancora</x-btn>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-sm text-gray-600">
|
||||
Il territorio non ha un PDF: non c'è un link da mandare. Caricalo dalla scheda del territorio.
|
||||
</p>
|
||||
<div class="tm-da-inviare-scelta">
|
||||
<div>
|
||||
<x-btn variante="secondary" wire:click="concludiInvio(false)">Ho capito</x-btn>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
</div>
|
||||
@endif
|
||||
@@ -8,76 +8,8 @@
|
||||
<p class="text-sm text-gray-500 mt-1">Questa funzione consente un'assegnazione arbitraria, indipendente dalle tre schede della Home.</p>
|
||||
</div>
|
||||
|
||||
{{-- Invio del link (Assegna::apriInvio): blocca la pagina finché non si
|
||||
risponde "inviato / non ancora". Se si chiude la scheda prima,
|
||||
l'assegnazione resta segnalata con "link ancora da inviare". --}}
|
||||
@if($invio)
|
||||
@php
|
||||
$link = $invio->shortPdfUrl();
|
||||
$messaggio = $invio->messaggioLink($link);
|
||||
@endphp
|
||||
<div class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-gray-900/50 p-4 sm:items-center"
|
||||
role="dialog" aria-modal="true" aria-labelledby="invio-titolo" wire:key="invio-{{ $invio->id }}">
|
||||
<section class="tm-da-inviare tm-da-inviare-finestra">
|
||||
<h2 id="invio-titolo" class="tm-da-inviare-titolo">
|
||||
Territorio N° {{ $invio->territorio?->numero }} assegnato a {{ $invio->proclamatore?->nome_completo }}
|
||||
</h2>
|
||||
@include('livewire.assegnazioni._invio-link')
|
||||
|
||||
@if($link)
|
||||
<p class="text-sm text-gray-700 mb-2">Come vuoi mandare il link della piantina? Il destinatario lo scegli tu, tra i tuoi contatti.</p>
|
||||
|
||||
<div class="tm-da-inviare-link">
|
||||
<code id="invio-link" x-data x-on:click="window.getSelection().selectAllChildren($el)">{{ $link }}</code>
|
||||
</div>
|
||||
|
||||
{{-- Il messaggio sta in data-messaggio: dentro <x-btn> le direttive
|
||||
Blade (@js) non si compilano, quindi i pulsanti lo leggono da qui. --}}
|
||||
<div class="tm-da-inviare-azioni" x-data="{ copiato: false, condivisibile: !! navigator.share }"
|
||||
data-messaggio="{{ $messaggio }}" data-titolo="Territorio N° {{ $invio->territorio?->numero }}">
|
||||
<x-btn :variante="$canaleUsato === 'copia' ? 'primary' : 'secondary'"
|
||||
x-on:click="navigator.clipboard?.writeText(document.getElementById('invio-link').textContent.trim()).then(() => { copiato = true; setTimeout(() => copiato = false, 2000) }).catch(() => {}); $wire.canale('copia')">
|
||||
<span x-show="! copiato">Copia link</span>
|
||||
<span x-show="copiato" x-cloak>Copiato</span>
|
||||
</x-btn>
|
||||
|
||||
{{-- Condividi: il menu di condivisione del telefono (WhatsApp,
|
||||
Telegram, SMS, posta…), da cui si sceglie app e contatto. Solo
|
||||
dove il browser lo offre. Il canale si annota a condivisione
|
||||
fatta, non se la si annulla. --}}
|
||||
<x-btn :variante="$canaleUsato === 'condividi' ? 'primary' : 'secondary'" x-show="condivisibile" x-cloak
|
||||
x-on:click="const d = $el.closest('[data-messaggio]').dataset; navigator.share({ title: d.titolo, text: d.messaggio }).then(() => $wire.canale('condividi')).catch(() => {})">
|
||||
Condividi…
|
||||
</x-btn>
|
||||
</div>
|
||||
|
||||
@if($canaleUsato)
|
||||
<p class="mt-2 text-sm text-gray-700">Canale scelto: {{ \App\Models\Assegnazione::CANALI_LINK[$canaleUsato] }}.</p>
|
||||
@endif
|
||||
@if($erroreInvio)
|
||||
<p class="mt-2 text-sm text-red-600" role="alert">{{ $erroreInvio }}</p>
|
||||
@endif
|
||||
|
||||
<div class="tm-da-inviare-scelta">
|
||||
<p>Hai inviato il link?</p>
|
||||
<div>
|
||||
<x-btn variante="primary" wire:click="concludiInvio(true)" :disabled="$canaleUsato === null"
|
||||
:title="$canaleUsato === null ? 'Scegli prima come mandare il link' : null">Sì, inviato</x-btn>
|
||||
<x-btn variante="secondary" wire:click="concludiInvio(false)">No, non ancora</x-btn>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-sm text-gray-600">
|
||||
Il territorio non ha un PDF: non c'è un link da mandare. Caricalo dalla scheda del territorio.
|
||||
</p>
|
||||
<div class="tm-da-inviare-scelta">
|
||||
<div>
|
||||
<x-btn variante="secondary" wire:click="concludiInvio(false)">Ho capito</x-btn>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div x-data="{ formOpen: true }">
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div>
|
||||
<div class="mb-6">
|
||||
<a href="{{ $ritorno ?? route('territori.show', $assegnazione->territorio) }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1">
|
||||
<a href="{{ $ritorno ?? $this->paginaPredefinita() }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
|
||||
{{ $ritorno ? 'Torna indietro' : 'Torna al territorio' }}
|
||||
{{ $ritorno ? 'Torna indietro' : (auth()->user()->can('territori.assign') ? 'Torna al territorio' : 'Torna al mio gruppo') }}
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Rientro Territorio</h1>
|
||||
</div>
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3 pt-4">
|
||||
<x-btn variante="primary" type="submit">Registra rientro</x-btn>
|
||||
<x-btn variante="secondary" :href="$ritorno ?? route('territori.show', $assegnazione->territorio)">Annulla</x-btn>
|
||||
<x-btn variante="secondary" :href="$ritorno ?? $this->paginaPredefinita()">Annulla</x-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
73
resources/views/livewire/gruppi/gruppi-index.blade.php
Normal file
73
resources/views/livewire/gruppi/gruppi-index.blade.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<div>
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Gruppi di servizio</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Ogni gruppo ha un sorvegliante, che con la sua utenza vede solo il proprio gruppo e assegna ai membri i territori del pacchetto.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if($creato && $passwordTemporanea)
|
||||
<div class="mb-6 rounded-xl border-2 border-green-200 bg-green-50 p-4 text-sm text-green-900" role="status">
|
||||
<p class="font-semibold">Gruppo «{{ $creato->nome }}» creato.</p>
|
||||
<p class="mt-1">Consegna queste credenziali a {{ $creato->sorvegliante?->nome_completo }}: la password si vede <strong>solo ora</strong> e al primo accesso va cambiata.</p>
|
||||
<dl class="mt-3 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1">
|
||||
<dt class="text-green-700">Indirizzo</dt><dd><code>{{ route('login') }}</code></dd>
|
||||
<dt class="text-green-700">Email</dt><dd><code>{{ $creato->sorvegliante?->utente?->email }}</code></dd>
|
||||
<dt class="text-green-700">Password</dt><dd><code class="select-all">{{ $passwordTemporanea }}</code></dd>
|
||||
</dl>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<x-btn variante="primary" dimensione="sm" :href="route('gruppi.show', $creato)">Aggiungi membri e territori</x-btn>
|
||||
<x-btn variante="ghost" dimensione="sm" wire:click="chiudiPassword">Ho annotato la password</x-btn>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<section class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-6 max-w-3xl" aria-labelledby="titolo-nuovo">
|
||||
<h2 id="titolo-nuovo" class="text-sm font-semibold text-gray-800 mb-3">Nuovo gruppo</h2>
|
||||
<form wire:submit="crea" class="space-y-4">
|
||||
<div>
|
||||
<label for="nome-gruppo" class="block text-sm font-medium text-gray-700 mb-1">Nome del gruppo *</label>
|
||||
<input wire:model="nome" id="nome-gruppo" type="text" placeholder="es. Zisa"
|
||||
class="block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
@error('nome') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="sorvegliante" class="block text-sm font-medium text-gray-700 mb-1">Sorvegliante *</label>
|
||||
<x-scelta id="sorvegliante" wire:model="sorveglianteId" :opzioni="$opzioniSorveglianti" segnaposto="Cognome o nome" :vuoto="null" />
|
||||
<p class="text-xs text-gray-500 mt-1">Un proclamatore attivo che non fa già parte di un gruppo. Diventa anche membro del gruppo.</p>
|
||||
@error('sorveglianteId') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="email-sorvegliante" class="block text-sm font-medium text-gray-700 mb-1">Email per l'accesso *</label>
|
||||
<input wire:model="email" id="email-sorvegliante" type="email" autocomplete="off"
|
||||
class="block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<p class="text-xs text-gray-500 mt-1">Resta solo nell'utenza del sorvegliante (non nel proclamatore) e non va confermata.</p>
|
||||
@error('email') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<x-btn variante="primary" type="submit">Crea il gruppo</x-btn>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden" aria-labelledby="titolo-elenco">
|
||||
<h2 id="titolo-elenco" class="px-4 py-3 border-b border-gray-100 text-sm font-semibold text-gray-800">Gruppi</h2>
|
||||
<ul class="divide-y divide-gray-100">
|
||||
@forelse($gruppi as $g)
|
||||
<li class="px-4 py-3 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<a data-ritorno href="{{ route('gruppi.show', $g) }}" class="font-semibold text-indigo-600 hover:underline">{{ $g->nome }}</a>
|
||||
@if($g->sciolto_at)
|
||||
<span class="ml-1 inline-flex rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600">sciolto il {{ $g->sciolto_at->format('d/m/Y') }}</span>
|
||||
@endif
|
||||
<p class="text-xs text-gray-500">
|
||||
Sorvegliante: {{ $g->sorvegliante?->nome_completo ?? '—' }}
|
||||
@unless($g->sciolto_at) · {{ $g->membri_count }} membri · {{ $g->territori_count }} territori nel pacchetto @endunless
|
||||
</p>
|
||||
</div>
|
||||
<x-btn data-ritorno variante="secondary" tono="tenue" dimensione="sm" :href="route('gruppi.show', $g)">{{ $g->sciolto_at ? 'Dettaglio' : 'Gestisci' }}</x-btn>
|
||||
</li>
|
||||
@empty
|
||||
<li class="px-4 py-8 text-center text-sm text-gray-500">Nessun gruppo di servizio.</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
136
resources/views/livewire/gruppi/gruppo-show.blade.php
Normal file
136
resources/views/livewire/gruppi/gruppo-show.blade.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<div>
|
||||
<div class="mb-6">
|
||||
<a href="{{ $ritorno ?? route('gruppi.index') }}" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-indigo-600 transition-colors mb-1">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
|
||||
{{ $ritorno ? 'Torna indietro' : 'Gruppi di servizio' }}
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Gruppo {{ $gruppo->nome }}</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Sorvegliante: <strong>{{ $gruppo->sorvegliante?->nome_completo ?? '—' }}</strong>
|
||||
@if($attivo && $gruppo->sorvegliante?->utente)
|
||||
· accede con <code>{{ $gruppo->sorvegliante->utente->email }}</code>
|
||||
@endif
|
||||
</p>
|
||||
@unless($attivo)
|
||||
<p class="mt-2 inline-flex rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-700">
|
||||
Sciolto il {{ $gruppo->sciolto_at->format('d/m/Y') }} · {{ $assegnazioniDelGruppo }} assegnazioni nate dal gruppo restano nello storico
|
||||
</p>
|
||||
@endunless
|
||||
</div>
|
||||
|
||||
@if($passwordTemporanea)
|
||||
<div class="mb-6 rounded-xl border-2 border-green-200 bg-green-50 p-4 text-sm text-green-900" role="status">
|
||||
<p>Nuova password temporanea per {{ $gruppo->sorvegliante?->nome_completo }}, da cambiare al primo accesso. Si vede <strong>solo ora</strong>:</p>
|
||||
<p class="mt-2"><code class="select-all text-base">{{ $passwordTemporanea }}</code></p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($attivo)
|
||||
{{-- ─── Pacchetto ─────────────────────────────────────────── --}}
|
||||
<section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mb-6" aria-labelledby="titolo-pacchetto">
|
||||
<h2 id="titolo-pacchetto" class="px-4 py-3 border-b border-gray-100 text-sm font-semibold text-gray-800">
|
||||
Pacchetto ({{ $territori->count() }} territori)
|
||||
</h2>
|
||||
<ul class="divide-y divide-gray-100">
|
||||
@forelse($territori as $t)
|
||||
@php($a = $t->assegnazioneCorrente)
|
||||
<li class="px-4 py-3 flex flex-wrap items-center justify-between gap-2" wire:key="pacchetto-{{ $t->id }}">
|
||||
<span class="text-sm">
|
||||
<a data-ritorno href="{{ route('territori.show', $t) }}" class="font-semibold text-indigo-600 hover:underline">N° {{ $t->numero }}</a>
|
||||
<span class="text-gray-500">{{ $t->zona?->nome }}</span>
|
||||
—
|
||||
@if($a)
|
||||
<span class="text-gray-700">a {{ $a->proclamatore?->nome_completo }} dal {{ $a->assigned_at->format('d/m/Y') }}</span>
|
||||
@else
|
||||
<span class="text-gray-700">da dare</span>
|
||||
@endif
|
||||
</span>
|
||||
@unless($a)
|
||||
<x-btn variante="secondary" tono="tenue" dimensione="sm" wire:click="rimanda({{ $t->id }})"
|
||||
wire:confirm="Rimandare in reparto il territorio {{ $t->numero }}?">Rimanda in reparto</x-btn>
|
||||
@endunless
|
||||
</li>
|
||||
@empty
|
||||
<li class="px-4 py-6 text-center text-sm text-gray-500">Il pacchetto è vuoto.</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
|
||||
{{-- Assegnazione in blocco: si spuntano i territori in reparto --}}
|
||||
<div class="border-t border-gray-100 bg-gray-50 p-4" x-data="elencoCercabile">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">Aggiungi territori al pacchetto</h3>
|
||||
@if($candidatiTerritori->isEmpty())
|
||||
<p class="text-sm text-gray-500">Non ci sono territori in reparto da aggiungere.</p>
|
||||
@else
|
||||
<input x-model="q" type="search" inputmode="search" autocomplete="off" aria-label="Filtra i territori in reparto"
|
||||
placeholder="Filtra per numero o zona…"
|
||||
class="mb-2 block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<div class="max-h-72 overflow-y-auto rounded-lg border border-gray-200 bg-white divide-y divide-gray-100">
|
||||
@foreach($candidatiTerritori as $c)
|
||||
<label class="flex items-center gap-3 px-3 py-2.5 text-sm cursor-pointer hover:bg-indigo-50/40"
|
||||
data-cerca="{{ \Illuminate\Support\Str::lower(\Illuminate\Support\Str::ascii($c->numero . ' ' . $c->zona?->nome)) }}"
|
||||
x-show="corrisponde($el.dataset.cerca)" wire:key="candidato-t-{{ $c->id }}">
|
||||
<input type="checkbox" value="{{ $c->id }}" wire:model="nuoviTerritori" class="h-5 w-5 rounded border-gray-300 text-indigo-600">
|
||||
<span><strong>N° {{ $c->numero }}</strong> <span class="text-gray-500">{{ $c->zona?->nome }}</span></span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<x-btn variante="primary" wire:click="aggiungiTerritori" :disabled="count($nuoviTerritori) === 0">
|
||||
Aggiungi {{ count($nuoviTerritori) ?: '' }} al pacchetto
|
||||
</x-btn>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- ─── Membri ───────────────────────────────────────────── --}}
|
||||
<section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mb-6" aria-labelledby="titolo-membri">
|
||||
<h2 id="titolo-membri" class="px-4 py-3 border-b border-gray-100 text-sm font-semibold text-gray-800">Membri ({{ $membri->count() }})</h2>
|
||||
<ul class="divide-y divide-gray-100">
|
||||
@foreach($membri as $m)
|
||||
<li class="px-4 py-2.5 flex items-center justify-between gap-2 text-sm" wire:key="membro-{{ $m->id }}">
|
||||
<span>{{ $m->nome_completo }} @if($m->id === $gruppo->sorvegliante_id)<span class="text-xs text-gray-500">(sorvegliante)</span>@endif</span>
|
||||
@if($m->id !== $gruppo->sorvegliante_id)
|
||||
<x-btn variante="secondary" tono="tenue" dimensione="sm" wire:click="togliMembro({{ $m->id }})">Togli</x-btn>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
<div class="border-t border-gray-100 bg-gray-50 p-4" x-data="elencoCercabile">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">Aggiungi membri</h3>
|
||||
@if($candidatiMembri->isEmpty())
|
||||
<p class="text-sm text-gray-500">Tutti i proclamatori attivi fanno già parte di un gruppo.</p>
|
||||
@else
|
||||
<input x-model="q" type="search" inputmode="search" autocomplete="off" aria-label="Filtra i proclamatori"
|
||||
placeholder="Filtra per nome…"
|
||||
class="mb-2 block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<div class="max-h-72 overflow-y-auto rounded-lg border border-gray-200 bg-white divide-y divide-gray-100">
|
||||
@foreach($candidatiMembri as $c)
|
||||
<label class="flex items-center gap-3 px-3 py-2.5 text-sm cursor-pointer hover:bg-indigo-50/40"
|
||||
data-cerca="{{ \Illuminate\Support\Str::lower(\Illuminate\Support\Str::ascii($c->nome_completo)) }}"
|
||||
x-show="corrisponde($el.dataset.cerca)" wire:key="candidato-p-{{ $c->id }}">
|
||||
<input type="checkbox" value="{{ $c->id }}" wire:model="nuoviMembri" class="h-5 w-5 rounded border-gray-300 text-indigo-600">
|
||||
<span>{{ $c->nome_completo }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<x-btn variante="primary" wire:click="aggiungiMembri">Aggiungi i membri selezionati</x-btn>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- ─── Sorvegliante ─────────────────────────────────────── --}}
|
||||
<section class="bg-white rounded-xl shadow-sm border border-gray-200 p-4" aria-labelledby="titolo-sorvegliante">
|
||||
<h2 id="titolo-sorvegliante" class="text-sm font-semibold text-gray-800 mb-2">Sorvegliante</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-btn variante="secondary" wire:click="nuovaPassword"
|
||||
wire:confirm="Generare una nuova password temporanea per {{ $gruppo->sorvegliante?->nome_completo }}? Quella attuale smette di funzionare.">Nuova password temporanea</x-btn>
|
||||
<x-btn variante="danger" wire:click="sciogli"
|
||||
wire:confirm="Togliere il ruolo di sorvegliante a {{ $gruppo->sorvegliante?->nome_completo }}? Il gruppo si scioglie: i territori non ancora dati tornano in reparto, i membri restano senza gruppo e l'utenza del sorvegliante viene disattivata. Le assegnazioni già fatte restano.">Togli il ruolo e sciogli il gruppo</x-btn>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
</div>
|
||||
121
resources/views/livewire/gruppi/mio-gruppo.blade.php
Normal file
121
resources/views/livewire/gruppi/mio-gruppo.blade.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<div>
|
||||
{{-- Il mio gruppo (MioGruppo): la sola pagina del sorvegliante di gruppo.
|
||||
Pensata prima per il telefono: schede una sotto l'altra, pulsanti
|
||||
grandi. Copia, Condividi e ricerca vengono da elencoCercabile
|
||||
(resources/js/app.js), l'invio del link dal partial _invio-link. --}}
|
||||
@if(! $gruppo)
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 text-sm text-gray-600">
|
||||
Non sei il sorvegliante di un gruppo di servizio attivo.
|
||||
</div>
|
||||
@else
|
||||
<div class="mb-5">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Gruppo {{ $gruppo->nome }}</h1>
|
||||
@php($daDare = $territori->filter(fn ($t) => ! $t->assegnazioneCorrente)->count())
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
{{ $territori->count() }} {{ $territori->count() === 1 ? 'territorio' : 'territori' }} nel pacchetto ·
|
||||
{{ $daDare }} da dare · {{ $membri->count() }} {{ $membri->count() === 1 ? 'membro' : 'membri' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@include('livewire.assegnazioni._invio-link')
|
||||
|
||||
<section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden" x-data="elencoCercabile" aria-labelledby="titolo-pacchetto">
|
||||
<svg aria-hidden="true" style="position:absolute;width:0;height:0;overflow:hidden">
|
||||
<symbol id="tm-i-copia" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></symbol>
|
||||
<symbol id="tm-i-spunta" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></symbol>
|
||||
<symbol id="tm-i-condividi" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/></symbol>
|
||||
<symbol id="tm-i-avviso" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></symbol>
|
||||
</svg>
|
||||
|
||||
<div class="px-4 py-3 border-b border-gray-100 flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 id="titolo-pacchetto" class="text-sm font-semibold text-gray-800">Territori del pacchetto</h2>
|
||||
@if($territori->count() > 5)
|
||||
<input x-model="q" type="search" inputmode="search" autocomplete="off" aria-label="Cerca nel pacchetto"
|
||||
placeholder="Cerca numero, zona o nome…"
|
||||
class="w-full sm:w-64 rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-gray-100">
|
||||
@forelse($territori as $t)
|
||||
@php
|
||||
$a = $t->assegnazioneCorrente;
|
||||
$pdfUrl = $a?->shortPdfUrl();
|
||||
$cerca = \Illuminate\Support\Str::lower(\Illuminate\Support\Str::ascii(implode(' ', array_filter([$t->numero, $t->zona?->nome, $a?->proclamatore?->nome_completo]))));
|
||||
@endphp
|
||||
<div wire:key="pacchetto-{{ $t->id }}" data-cerca="{{ $cerca }}" x-show="corrisponde($el.dataset.cerca)" class="tm-assegnato">
|
||||
<div class="tm-assegnato-testa">
|
||||
<span><span class="tm-assegnato-numero">N° {{ $t->numero }}</span> @if($t->zona?->nome)<span class="tm-assegnato-zona">{{ $t->zona->nome }}</span>@endif</span>
|
||||
@if($a)
|
||||
<span class="tm-assegnato-info"><b>{{ $a->proclamatore?->nome_completo }}</b> · dal {{ $a->assigned_at->format('d/m/Y') }} · <span class="tm-assegnato-giorni {{ $a->giorni > 120 ? 'tm-g-tardi' : ($a->giorni > 90 ? 'tm-g-presto' : '') }}">{{ $a->giorni }}g</span></span>
|
||||
@else
|
||||
<span class="tm-assegnato-info"><b>Da dare</b></span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($a?->link_in_sospeso)
|
||||
<div class="tm-assegnato-sospeso" role="status"><svg aria-hidden="true"><use href="#tm-i-avviso"/></svg> Link ancora da inviare <x-btn variante="secondary" tono="tenue" dimensione="sm" wire:click="apriInvio({{ $a->id }})">Invia link</x-btn></div>
|
||||
@endif
|
||||
|
||||
@if($a)
|
||||
{{-- Assegnato: rientro, link (copia / condividi), stato dell'invio --}}
|
||||
<div class="tm-assegnato-link" @if($pdfUrl) data-link data-id="{{ $a->id }}" data-messaggio="{{ $a->messaggioLink($pdfUrl) }}" data-titolo="Territorio N° {{ $t->numero }}" @endif>
|
||||
<x-btn data-ritorno variante="primary" dimensione="sm" :href="route('assegnazioni.rientra', $a)">Rientro</x-btn>
|
||||
@if($pdfUrl)
|
||||
<code x-on:click="window.getSelection().selectAllChildren($el)">{{ $pdfUrl }}</code>
|
||||
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona class="tm-copia" x-on:click="copia($el)" title="Copia link" aria-label="Copia il link della piantina"><svg aria-hidden="true"><use href="#tm-i-copia"/></svg><svg aria-hidden="true"><use href="#tm-i-spunta"/></svg></x-btn>
|
||||
<x-btn variante="secondary" tono="tenue" dimensione="sm" icona x-show="condivisibile" x-cloak x-on:click="condividi($el)" title="Condividi il link" aria-label="Condividi il link della piantina"><svg aria-hidden="true"><use href="#tm-i-condividi"/></svg></x-btn>
|
||||
<x-btn :variante="$a->link_sent ? 'primary' : 'secondary'" tono="tenue" dimensione="sm" wire:click="toggleLinkSent({{ $a->id }})" aria-pressed="{{ $a->link_sent ? 'true' : 'false' }}">{{ $a->link_sent ? 'Link inviato' : 'Non inviato' }}</x-btn>
|
||||
@endif
|
||||
</div>
|
||||
@elseif($territorioDaAssegnare === $t->id)
|
||||
{{-- Da dare: a chi e da quando --}}
|
||||
<div class="mt-3 rounded-lg border border-indigo-100 bg-indigo-50/50 p-3 space-y-3">
|
||||
<div>
|
||||
<label for="membro-{{ $t->id }}" class="block text-sm font-medium text-gray-700 mb-1">A chi lo dai?</label>
|
||||
<x-scelta id="membro-{{ $t->id }}" wire:model="membroId" :opzioni="$opzioniMembri" segnaposto="Cognome o nome" :vuoto="null" />
|
||||
@error('membroId') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="data-{{ $t->id }}" class="block text-sm font-medium text-gray-700 mb-1">Data</label>
|
||||
<input wire:model="dataAssegnazione" type="date" id="data-{{ $t->id }}" max="{{ now()->format('Y-m-d') }}"
|
||||
class="block w-full rounded-lg border-gray-300 text-base sm:text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
@error('dataAssegnazione') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-btn variante="primary" wire:click="assegna">Assegna</x-btn>
|
||||
<x-btn variante="ghost" wire:click="annullaAssegnazione">Annulla</x-btn>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="tm-assegnato-link">
|
||||
<x-btn variante="primary" dimensione="sm" wire:click="apriAssegnazione({{ $t->id }})">Assegna</x-btn>
|
||||
@if($t->pdf_path)
|
||||
<x-btn variante="secondary" tono="tenue" dimensione="sm" :href="route('mio-gruppo.pdf', $t)" target="_blank" rel="noopener">Piantina</x-btn>
|
||||
@endif
|
||||
<x-btn variante="secondary" tono="tenue" dimensione="sm" wire:click="rimanda({{ $t->id }})"
|
||||
wire:confirm="Rimandare in reparto il territorio {{ $t->numero }}? Uscirà dal pacchetto del gruppo.">Rimanda in reparto</x-btn>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<p class="px-4 py-8 text-center text-sm text-gray-500">Il pacchetto è vuoto: l'amministratore non ha ancora assegnato territori al gruppo.</p>
|
||||
@endforelse
|
||||
<p x-show="nessuno" x-cloak class="px-4 py-8 text-center text-sm text-gray-400">Nessun risultato per «<span x-text="q"></span>».</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-5 bg-white rounded-xl shadow-sm border border-gray-200 p-4" aria-labelledby="titolo-membri">
|
||||
<h2 id="titolo-membri" class="text-sm font-semibold text-gray-800 mb-2">Membri del gruppo</h2>
|
||||
<ul class="text-sm text-gray-700 space-y-1">
|
||||
@foreach($membri as $m)
|
||||
@php($quanti = $territori->filter(fn ($t) => $t->assegnazioneCorrente?->proclamatore_id === $m->id)->count())
|
||||
<li class="flex justify-between gap-3">
|
||||
<span>{{ $m->nome_completo }} @if($m->id === $gruppo->sorvegliante_id)<span class="text-xs text-gray-500">(sorvegliante)</span>@endif</span>
|
||||
<span class="text-gray-500">{{ $quanti }} {{ $quanti === 1 ? 'territorio' : 'territori' }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</section>
|
||||
@endif
|
||||
</div>
|
||||
@@ -16,10 +16,10 @@
|
||||
<section>
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-2">Dati trattati</h2>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li><strong>Proclamatori</strong>: solo nome e cognome, cifrati nel database ({{ $cifratura }}), e se sono attivi. Nessun numero di telefono né indirizzo email.</li>
|
||||
<li><strong>Proclamatori</strong>: solo nome e cognome, cifrati nel database ({{ $cifratura }}), se sono attivi e l'eventuale gruppo di servizio di cui fanno parte. Nessun numero di telefono né indirizzo email.</li>
|
||||
<li><strong>Territori e piantine</strong>: numero, zona, tipologia, confini, mappe e PDF — nessun dato personale.</li>
|
||||
<li><strong>Assegnazioni</strong>: collegamento tra proclamatore e territorio, con date di assegnazione e rientro, conteggio nelle campagne, codice del link alla piantina e stato del suo invio (inviato o no, e con quale canale).</li>
|
||||
<li><strong>Utenti dell'applicazione</strong>: nome, email (che va confermata), password conservata solo come impronta non reversibile, ruolo.</li>
|
||||
<li><strong>Utenti dell'applicazione</strong>: nome, email (che va confermata, tranne per i sorveglianti di gruppo), password conservata solo come impronta non reversibile, ruolo. L'utenza di un sorvegliante è collegata al suo proclamatore e viene disattivata quando il gruppo si scioglie.</li>
|
||||
<li><strong>Registro delle attività</strong>: chi ha fatto cosa e quando. I proclamatori vi compaiono solo con un identificativo numerico, mai con il nome.</li>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -58,7 +58,7 @@
|
||||
<li><strong>{{ ucfirst($ruolo['nome']) }}</strong>: {{ $ruolo['permessi'] ? implode('; ', $ruolo['permessi']) : 'nessun permesso' }}.</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<p class="mt-2">Il proclamatore non ha un account: riceve solo il link alla piantina del territorio che gli è assegnato.</p>
|
||||
<p class="mt-2">Il proclamatore non ha un account: riceve solo il link alla piantina del territorio che gli è assegnato. Fa eccezione il sorvegliante di un gruppo di servizio, che ha un'utenza (email e password) con cui vede solo il proprio gruppo: i territori del suo pacchetto e i nomi dei membri.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Proclamatore" class="px-3 py-2">{{ $a->proclamatore?->nome_completo ?? 'N/A' }}</td>
|
||||
<td data-label="Proclamatore" class="px-3 py-2">{{ $a->proclamatore?->nome_completo ?? 'N/A' }}@if($a->gruppo)<span class="ml-1 inline-flex rounded-full bg-purple-50 px-2 py-0.5 text-xs text-purple-700" title="Assegnato dal sorvegliante del gruppo di servizio">via gruppo {{ $a->gruppo->nome }}</span>@endif</td>
|
||||
<td data-label="Assegnato" class="px-3 py-2">{{ $a->assigned_at->format('d/m/Y') }}</td>
|
||||
<td data-label="Rientrato" class="px-3 py-2">
|
||||
@if($a->returned_at)
|
||||
|
||||
@@ -129,6 +129,12 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-2 cell-actions">
|
||||
@if($user->proclamatore_id)
|
||||
{{-- Utenza di un sorvegliante di gruppo: si gestisce dal gruppo. --}}
|
||||
<span class="text-xs text-gray-500">
|
||||
{{ $user->disattivato_at ? 'Disattivata (gruppo sciolto)' : 'Sorvegliante: si gestisce da Gruppi di servizio' }}
|
||||
</span>
|
||||
@else
|
||||
<div class="flex gap-2">
|
||||
<x-btn variante="secondary" tono="tenue" dimensione="sm" wire:click="startEdit({{ $user->id }})">Modifica</x-btn>
|
||||
@unless($user->hasVerifiedEmail())
|
||||
@@ -145,6 +151,7 @@
|
||||
Elimina
|
||||
</x-btn>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
@endif
|
||||
</tr>
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
<select wire:model.live="filterStato" class="rounded-lg border-gray-300 text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option value="">Tutti gli stati</option>
|
||||
<option value="in_reparto">In reparto</option>
|
||||
<option value="in_gruppo">In un gruppo di servizio</option>
|
||||
<option value="prioritari">Prioritari</option>
|
||||
<option value="assegnato">Assegnato</option>
|
||||
<option value="da_rientrare">Da rientrare</option>
|
||||
@@ -126,12 +127,13 @@
|
||||
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
{{ match($stato) {
|
||||
'in_reparto' => 'bg-green-100 text-green-800',
|
||||
'in_gruppo' => 'bg-purple-100 text-purple-800',
|
||||
'assegnato' => 'bg-blue-100 text-blue-800',
|
||||
'da_rientrare' => 'bg-red-100 text-red-800',
|
||||
'inattivo' => 'bg-gray-100 text-gray-600',
|
||||
default => 'bg-gray-100 text-gray-600'
|
||||
} }}">
|
||||
{{ str_replace('_', ' ', ucfirst($stato)) }}
|
||||
{{ $territorio->stato_etichetta }}
|
||||
</span>
|
||||
@if($territorio->is_prioritario)
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 ml-1">
|
||||
|
||||
@@ -55,12 +55,13 @@
|
||||
<span class="inline-flex items-center mt-1 px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
{{ match($stato) {
|
||||
'in_reparto' => 'bg-green-100 text-green-800',
|
||||
'in_gruppo' => 'bg-purple-100 text-purple-800',
|
||||
'assegnato' => 'bg-blue-100 text-blue-800',
|
||||
'da_rientrare' => 'bg-red-100 text-red-800',
|
||||
'inattivo' => 'bg-gray-100 text-gray-600',
|
||||
default => 'bg-gray-100 text-gray-600'
|
||||
} }}">
|
||||
{{ str_replace('_', ' ', ucfirst($stato)) }}
|
||||
{{ $territorio->stato_etichetta }}
|
||||
</span>
|
||||
@if($territorio->is_prioritario)
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 ml-1">★ Prioritario</span>
|
||||
@@ -177,7 +178,7 @@
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@foreach($assegnazioni as $assegnazione)
|
||||
<tr>
|
||||
<td data-label="Proclamatore" class="px-3 py-2">{{ $assegnazione->proclamatore?->nome_completo ?? 'N/A' }}</td>
|
||||
<td data-label="Proclamatore" class="px-3 py-2">{{ $assegnazione->proclamatore?->nome_completo ?? 'N/A' }}@if($assegnazione->gruppo)<span class="ml-1 inline-flex rounded-full bg-purple-50 px-2 py-0.5 text-xs text-purple-700">via gruppo {{ $assegnazione->gruppo->nome }}</span>@endif</td>
|
||||
<td data-label="Assegnato" class="px-3 py-2">{{ $assegnazione->assigned_at->format('d/m/Y') }}</td>
|
||||
<td data-label="Rientrato" class="px-3 py-2">
|
||||
@if($assegnazione->returned_at)
|
||||
|
||||
Reference in New Issue
Block a user