++ fix permessi e mobile portrait

This commit is contained in:
Francesco Picone
2026-07-25 23:48:56 +02:00
parent 34415862d2
commit 69072b6f09
17 changed files with 194 additions and 56 deletions

View File

@@ -147,6 +147,40 @@ assistente e operatore); creazione/modifica/cestino restano su `territori.manage
sono sotto `@can('territori.manage')`; il link "Dettaglio" resta visibile a sono sotto `@can('territori.manage')`; il link "Dettaglio" resta visibile a
tutti. Il menu laterale mostra "Territori" a chi ha `territori.assign`. tutti. Il menu laterale mostra "Territori" a chi ha `territori.assign`.
### Sola lettura per proclamatori e campagne (non-admin)
Stessa impostazione dei territori estesa ad altre due sezioni:
- **Proclamatori**: `index` e `show` restano su `proclamatori.manage` (l'assistente
li vede in sola lettura); `create`/`edit`/`cestino` e tutte le mutazioni
(`ProclamatoreIndex::toggleActive`/`deleteProclamatore`/`anonimizza`,
`ProclamatoreCreate::save`, `ProclamatoreEdit::save`,
`ProclamatoreCestino::restore`/`forceDelete`) richiedono `settings.manage`.
`ProclamatoreIndex` è servito da una rotta rilassata, quindi i suoi metodi di
mutazione hanno il check interno; i pulsanti in `proclamatore-index`/`-show`/
`-cestino` sono sotto `@can('settings.manage')`. Le rotte letterali
create/cestino restano prima del wildcard `{proclamatore}`.
- **Campagne**: la **creazione** è ora admin-only come già modifica/eliminazione
— `CampagnaCreate::save` chiama `authorize('settings.manage')`, la rotta
`campagne/create` è nel gruppo `settings.manage` (registrata prima del
wildcard) e il pulsante "Nuova Campagna" è sotto `@can('settings.manage')`.
`index`/`show` restano su `campagne.manage` (sola lettura per l'assistente).
### Tabelle-elenco responsive su mobile (portrait)
Le liste principali usano una sola "politica" CSS invece di layout duplicati:
la classe `table-cards` (in `resources/css/app.css`) sotto `640px` nasconde il
`thead` e trasforma ogni riga in una scheda, con ogni cella resa come
"ETICHETTA … valore" (l'etichetta viene dall'attributo `data-label` della `<td>`).
Un telefono in portrait sta sotto la soglia → schede; ruotato in landscape la
larghezza supera i 640px → torna la tabella normale (per questo landscape "non ha
problemi"). Convenzioni: ogni `<td>` dati ha `data-label="..."`; la cella azioni
ha classe `cell-actions` (niente label, occupa la riga a piena larghezza); le
celle `colspan` (stato vuoto, form di modifica utente inline) restano `block` a
piena larghezza. Applicata a: territori, proclamatori, campagne, registro,
utenti, audit-log e i due cestini. La pagina *Assegnazioni* era già ottimizzata e
non è stata toccata.
### Assegnare un territorio ne azzera il flag "prioritario" ### Assegnare un territorio ne azzera il flag "prioritario"
`prioritario` è un flag **manuale**; `is_prioritario` è true se il flag è acceso `prioritario` è un flag **manuale**; `is_prioritario` è true se il flag è acceso

View File

@@ -22,6 +22,9 @@ class CampagnaCreate extends Component
public function save() public function save()
{ {
// Creazione campagne riservata all'amministratore.
$this->authorize('settings.manage');
$this->validate(); $this->validate();
$campagna = Campagna::create([ $campagna = Campagna::create([

View File

@@ -12,6 +12,8 @@ class ProclamatoreCestino extends Component
public function restore(int $id) public function restore(int $id)
{ {
$this->authorize('settings.manage');
$proclamatore = Proclamatore::onlyTrashed()->findOrFail($id); $proclamatore = Proclamatore::onlyTrashed()->findOrFail($id);
$proclamatore->restore(); $proclamatore->restore();
activity()->causedBy(auth()->user()) activity()->causedBy(auth()->user())
@@ -22,6 +24,8 @@ class ProclamatoreCestino extends Component
public function forceDelete(int $id) public function forceDelete(int $id)
{ {
$this->authorize('settings.manage');
$proclamatore = Proclamatore::onlyTrashed()->findOrFail($id); $proclamatore = Proclamatore::onlyTrashed()->findOrFail($id);
if ($proclamatore->assegnazioni()->exists()) { if ($proclamatore->assegnazioni()->exists()) {

View File

@@ -22,6 +22,8 @@ class ProclamatoreCreate extends Component
public function save() public function save()
{ {
$this->authorize('settings.manage');
$this->validate(); $this->validate();
$proclamatore = Proclamatore::create([ $proclamatore = Proclamatore::create([

View File

@@ -31,6 +31,8 @@ class ProclamatoreEdit extends Component
public function save() public function save()
{ {
$this->authorize('settings.manage');
$this->validate(); $this->validate();
$this->proclamatore->update([ $this->proclamatore->update([

View File

@@ -42,12 +42,20 @@ class ProclamatoreIndex extends Component
public function toggleActive(int $id) public function toggleActive(int $id)
{ {
// La lista è in sola lettura per i non-admin: creare/modificare/
// eliminare proclamatori è riservato all'amministratore. Il check sta
// nel metodo perché la rotta index resta accessibile con
// proclamatori.manage.
$this->authorize('settings.manage');
$proclamatore = Proclamatore::findOrFail($id); $proclamatore = Proclamatore::findOrFail($id);
$proclamatore->update(['attivo' => !$proclamatore->attivo]); $proclamatore->update(['attivo' => !$proclamatore->attivo]);
} }
public function deleteProclamatore(int $id) public function deleteProclamatore(int $id)
{ {
$this->authorize('settings.manage');
$proclamatore = Proclamatore::findOrFail($id); $proclamatore = Proclamatore::findOrFail($id);
if ($proclamatore->assegnazioni()->aperte()->exists()) { if ($proclamatore->assegnazioni()->aperte()->exists()) {
@@ -61,6 +69,8 @@ class ProclamatoreIndex extends Component
public function anonimizza(int $id) public function anonimizza(int $id)
{ {
$this->authorize('settings.manage');
$proclamatore = Proclamatore::findOrFail($id); $proclamatore = Proclamatore::findOrFail($id);
$proclamatore->anonimizza(); $proclamatore->anonimizza();
session()->flash('success', "Dati personali del proclamatore anonimizzati (GDPR)."); session()->flash('success', "Dati personali del proclamatore anonimizzati (GDPR).");

View File

@@ -101,3 +101,68 @@
0%, 100% { opacity: 1; } 0%, 100% { opacity: 1; }
50% { opacity: 0.7; } 50% { opacity: 0.7; }
} }
/* ─── Responsive list tables ───────────────────────────────────────────────
Under 640px (a phone held in portrait; rotate to landscape and the width
grows past the breakpoint, restoring the normal table) every table with the
`table-cards` class reflows: the header is hidden and each row becomes a
card, with each cell shown as "LABEL … value" (the label comes from the
cell's data-label attribute). Cells with no data-label (the actions column,
marked `.cell-actions`) span the row full width. */
@media (max-width: 640px) {
table.table-cards,
table.table-cards tbody {
display: block;
width: 100%;
}
table.table-cards thead {
display: none;
}
table.table-cards tr {
display: block;
border: 1px solid #e5e7eb;
border-radius: 10px;
margin-bottom: 12px;
padding: 4px 2px;
background: #fff;
}
/* zebra striping would fight the card borders */
table.table-cards.table-striped tbody tr:nth-child(even) {
background: #fff;
}
table.table-cards td {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
text-align: right;
padding: 6px 12px;
border: none !important;
}
table.table-cards td[data-label]::before {
content: attr(data-label);
flex: 0 0 auto;
margin-right: auto;
text-align: left;
font-size: 11px;
font-weight: 600;
color: #6b7280;
text-transform: uppercase;
letter-spacing: .3px;
}
/* Actions row: full width, buttons wrapped to the right */
table.table-cards td.cell-actions {
justify-content: flex-end;
flex-wrap: wrap;
margin-top: 2px;
padding-top: 10px;
border-top: 1px solid #f3f4f6 !important;
}
/* Cells spanning all columns (empty state, inline edit form) stay full
width and block, so their inner layout isn't squeezed by the flex row. */
table.table-cards td[colspan] {
display: block;
text-align: center;
}
}

View File

@@ -26,7 +26,7 @@
{{-- Table --}} {{-- Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm table-striped"> <table class="min-w-full divide-y divide-gray-200 text-sm table-striped table-cards">
<thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)"> <thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)">
<tr> <tr>
<th class="px-3 py-3 text-left text-xs font-semibold text-gray-600 uppercase">Data/Ora</th> <th class="px-3 py-3 text-left text-xs font-semibold text-gray-600 uppercase">Data/Ora</th>
@@ -39,9 +39,9 @@
<tbody class="divide-y divide-gray-100"> <tbody class="divide-y divide-gray-100">
@forelse($activities as $activity) @forelse($activities as $activity)
<tr class="hover:bg-indigo-50/30 transition-colors"> <tr class="hover:bg-indigo-50/30 transition-colors">
<td class="px-3 py-2 text-xs text-gray-500 whitespace-nowrap">{{ $activity->created_at->format('d/m/Y H:i:s') }}</td> <td data-label="Data/Ora" class="px-3 py-2 text-xs text-gray-500 whitespace-nowrap">{{ $activity->created_at->format('d/m/Y H:i:s') }}</td>
<td class="px-3 py-2 text-xs">{{ $activity->causer?->name ?? data_get($activity->properties, 'causer_name') ?? 'Sistema' }}</td> <td data-label="Utente" class="px-3 py-2 text-xs">{{ $activity->causer?->name ?? data_get($activity->properties, 'causer_name') ?? 'Sistema' }}</td>
<td class="px-3 py-2"> <td data-label="Evento" class="px-3 py-2">
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full <span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full
{{ match($activity->description) { {{ match($activity->description) {
'created' => 'bg-green-100 text-green-800', 'created' => 'bg-green-100 text-green-800',
@@ -56,14 +56,14 @@
{{ $activity->description }} {{ $activity->description }}
</span> </span>
</td> </td>
<td class="px-3 py-2 text-xs text-gray-600"> <td data-label="Soggetto" class="px-3 py-2 text-xs text-gray-600">
@if($activity->subject_type) @if($activity->subject_type)
{{ class_basename($activity->subject_type) }} #{{ $activity->subject_id }} {{ class_basename($activity->subject_type) }} #{{ $activity->subject_id }}
@else @else
- -
@endif @endif
</td> </td>
<td class="px-3 py-2 text-xs text-gray-500"> <td data-label="Dettagli" class="px-3 py-2 text-xs text-gray-500">
@if($activity->properties->isNotEmpty()) @if($activity->properties->isNotEmpty())
<details> <details>
<summary class="cursor-pointer text-indigo-600 hover:text-indigo-800">Mostra</summary> <summary class="cursor-pointer text-indigo-600 hover:text-indigo-800">Mostra</summary>

View File

@@ -1,7 +1,7 @@
<div> <div>
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 class="text-2xl font-bold text-gray-900">Campagne</h1> <h1 class="text-2xl font-bold text-gray-900">Campagne</h1>
@can('campagne.manage') @can('settings.manage')
<a href="{{ route('campagne.create') }}" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white rounded-lg transition" style="background:#4f46e5" onmouseover="this.style.background='#4338ca'" onmouseout="this.style.background='#4f46e5'"> <a href="{{ route('campagne.create') }}" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white rounded-lg transition" style="background:#4f46e5" onmouseover="this.style.background='#4338ca'" onmouseout="this.style.background='#4f46e5'">
<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="M12 4v16m8-8H4"/></svg> <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="M12 4v16m8-8H4"/></svg>
Nuova Campagna Nuova Campagna
@@ -11,7 +11,7 @@
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 table-striped"> <table class="min-w-full divide-y divide-gray-200 table-striped table-cards">
<thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)"> <thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)">
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase">Descrizione</th> <th class="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase">Descrizione</th>
@@ -25,10 +25,10 @@
<tbody class="divide-y divide-gray-100"> <tbody class="divide-y divide-gray-100">
@forelse($campagne as $campagna) @forelse($campagne as $campagna)
<tr class="hover:bg-indigo-50/30 transition-colors"> <tr class="hover:bg-indigo-50/30 transition-colors">
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ $campagna->descrizione }}</td> <td data-label="Descrizione" class="px-4 py-3 text-sm font-medium text-gray-900">{{ $campagna->descrizione }}</td>
<td class="px-4 py-3 text-sm text-gray-600">{{ $campagna->start_date->format('d/m/Y') }}</td> <td data-label="Inizio" class="px-4 py-3 text-sm text-gray-600">{{ $campagna->start_date->format('d/m/Y') }}</td>
<td class="px-4 py-3 text-sm text-gray-600">{{ $campagna->end_date->format('d/m/Y') }}</td> <td data-label="Fine" class="px-4 py-3 text-sm text-gray-600">{{ $campagna->end_date->format('d/m/Y') }}</td>
<td class="px-4 py-3 text-sm"> <td data-label="Stato" class="px-4 py-3 text-sm">
@if($campagna->is_attiva) @if($campagna->is_attiva)
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-800">Attiva</span> <span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-800">Attiva</span>
@elseif($campagna->end_date->isPast()) @elseif($campagna->end_date->isPast())
@@ -37,7 +37,7 @@
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-blue-100 text-blue-800">Futura</span> <span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-blue-100 text-blue-800">Futura</span>
@endif @endif
</td> </td>
<td class="px-4 py-3 text-sm"> <td data-label="% Percorrenza" class="px-4 py-3 text-sm">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-20 bg-gray-200 rounded-full h-2"> <div class="w-20 bg-gray-200 rounded-full h-2">
<div class="bg-indigo-500 h-2 rounded-full" style="width:{{ min($campagna->percentuale_percorrenza, 100) }}%"></div> <div class="bg-indigo-500 h-2 rounded-full" style="width:{{ min($campagna->percentuale_percorrenza, 100) }}%"></div>
@@ -45,7 +45,7 @@
<span class="text-xs text-gray-600">{{ $campagna->percentuale_percorrenza }}%</span> <span class="text-xs text-gray-600">{{ $campagna->percentuale_percorrenza }}%</span>
</div> </div>
</td> </td>
<td class="px-4 py-3 text-sm text-right"> <td class="px-4 py-3 text-sm text-right cell-actions">
<div class="flex flex-wrap items-center justify-end gap-1"> <div class="flex flex-wrap items-center justify-end gap-1">
<a href="{{ route('campagne.show', $campagna) }}" class="btn-action btn-action-indigo" title="Dettaglio"> <a href="{{ route('campagne.show', $campagna) }}" class="btn-action btn-action-indigo" title="Dettaglio">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>

View File

@@ -9,7 +9,7 @@
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 table-striped"> <table class="min-w-full divide-y divide-gray-200 table-striped table-cards">
<thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)"> <thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)">
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase">Nome Completo</th> <th class="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase">Nome Completo</th>
@@ -20,13 +20,15 @@
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200">
@forelse($proclamatori as $proclamatore) @forelse($proclamatori as $proclamatore)
<tr class="hover:bg-indigo-50/30 transition-colors"> <tr class="hover:bg-indigo-50/30 transition-colors">
<td class="px-4 py-3 text-sm font-semibold text-gray-900">{{ $proclamatore->nome_completo }}</td> <td data-label="Nome completo" class="px-4 py-3 text-sm font-semibold text-gray-900">{{ $proclamatore->nome_completo }}</td>
<td class="px-4 py-3 text-sm text-gray-600">{{ $proclamatore->deleted_at->format('d/m/Y H:i') }}</td> <td data-label="Eliminato il" class="px-4 py-3 text-sm text-gray-600">{{ $proclamatore->deleted_at->format('d/m/Y H:i') }}</td>
<td class="px-4 py-3 text-sm text-right"> <td class="px-4 py-3 text-sm text-right cell-actions">
@can('settings.manage')
<div class="flex flex-wrap items-center justify-end gap-1"> <div class="flex flex-wrap items-center justify-end gap-1">
<button wire:click="restore({{ $proclamatore->id }})" class="btn-action btn-action-green">Ripristina</button> <button wire:click="restore({{ $proclamatore->id }})" class="btn-action btn-action-green">Ripristina</button>
<button wire:click="forceDelete({{ $proclamatore->id }})" wire:confirm="Eliminare DEFINITIVAMENTE questo proclamatore? Questa azione è irreversibile." class="btn-action btn-action-red">Elimina</button> <button wire:click="forceDelete({{ $proclamatore->id }})" wire:confirm="Eliminare DEFINITIVAMENTE questo proclamatore? Questa azione è irreversibile." class="btn-action btn-action-red">Elimina</button>
</div> </div>
@endcan
</td> </td>
</tr> </tr>
@empty @empty

View File

@@ -1,7 +1,7 @@
<div> <div>
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 class="text-2xl font-bold text-gray-900">Proclamatori</h1> <h1 class="text-2xl font-bold text-gray-900">Proclamatori</h1>
@can('proclamatori.manage') @can('settings.manage')
<a href="{{ route('proclamatori.create') }}" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white rounded-lg transition" style="background:#4f46e5" onmouseover="this.style.background='#4338ca'" onmouseout="this.style.background='#4f46e5'"> <a href="{{ route('proclamatori.create') }}" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white rounded-lg transition" style="background:#4f46e5" onmouseover="this.style.background='#4338ca'" onmouseout="this.style.background='#4f46e5'">
<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="M12 4v16m8-8H4"/></svg> <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="M12 4v16m8-8H4"/></svg>
Nuovo Proclamatore Nuovo Proclamatore
@@ -19,7 +19,7 @@
<option value="inattivo">Inattivi</option> <option value="inattivo">Inattivi</option>
</select> </select>
<div class="text-xs text-gray-500 flex items-center"> <div class="text-xs text-gray-500 flex items-center">
@can('proclamatori.manage') @can('settings.manage')
<a href="{{ route('proclamatori.cestino') }}" class="text-red-500 hover:text-red-700">🗑 Cestino</a> <a href="{{ route('proclamatori.cestino') }}" class="text-red-500 hover:text-red-700">🗑 Cestino</a>
@endcan @endcan
</div> </div>
@@ -29,7 +29,7 @@
{{-- Table --}} {{-- Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 table-striped"> <table class="min-w-full divide-y divide-gray-200 table-striped table-cards">
<thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)"> <thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)">
<tr> <tr>
<th wire:click="sortBy('cognome')" class="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase cursor-pointer hover:text-gray-800 transition-colors"> <th wire:click="sortBy('cognome')" class="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase cursor-pointer hover:text-gray-800 transition-colors">
@@ -46,22 +46,22 @@
<tbody class="divide-y divide-gray-100"> <tbody class="divide-y divide-gray-100">
@forelse($proclamatori as $proclamatore) @forelse($proclamatori as $proclamatore)
<tr class="hover:bg-indigo-50/30 transition-colors"> <tr class="hover:bg-indigo-50/30 transition-colors">
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ $proclamatore->cognome }}</td> <td data-label="Cognome" class="px-4 py-3 text-sm font-medium text-gray-900">{{ $proclamatore->cognome }}</td>
<td class="px-4 py-3 text-sm text-gray-700">{{ $proclamatore->nome }}</td> <td data-label="Nome" class="px-4 py-3 text-sm text-gray-700">{{ $proclamatore->nome }}</td>
<td class="px-4 py-3 text-sm"> <td data-label="Stato" class="px-4 py-3 text-sm">
@if($proclamatore->attivo) @if($proclamatore->attivo)
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-800">Attivo</span> <span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-800">Attivo</span>
@else @else
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-600">Inattivo</span> <span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-600">Inattivo</span>
@endif @endif
</td> </td>
<td class="px-4 py-3 text-sm text-gray-600">{{ $proclamatore->assegnazioni_aperte_count ?? $proclamatore->assegnazioni()->aperte()->count() }}</td> <td data-label="Territori assegnati" class="px-4 py-3 text-sm text-gray-600">{{ $proclamatore->assegnazioni_aperte_count ?? $proclamatore->assegnazioni()->aperte()->count() }}</td>
<td class="px-4 py-3 text-sm text-right"> <td class="px-4 py-3 text-sm text-right cell-actions">
<div class="flex flex-wrap items-center justify-end gap-1"> <div class="flex flex-wrap items-center justify-end gap-1">
<a href="{{ route('proclamatori.show', $proclamatore) }}" class="btn-action btn-action-indigo" title="Dettaglio"> <a href="{{ route('proclamatori.show', $proclamatore) }}" class="btn-action btn-action-indigo" title="Dettaglio">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
</a> </a>
@can('proclamatori.manage') @can('settings.manage')
<a href="{{ route('proclamatori.edit', $proclamatore) }}" class="btn-action btn-action-gray" title="Modifica"> <a href="{{ route('proclamatori.edit', $proclamatore) }}" class="btn-action btn-action-gray" title="Modifica">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
</a> </a>

View File

@@ -7,7 +7,7 @@
</a> </a>
<h1 class="text-2xl font-bold text-gray-900">{{ $proclamatore->nome_completo }}</h1> <h1 class="text-2xl font-bold text-gray-900">{{ $proclamatore->nome_completo }}</h1>
</div> </div>
@can('proclamatori.manage') @can('settings.manage')
<a href="{{ route('proclamatori.edit', $proclamatore) }}" class="btn-action btn-action-indigo" style="padding:8px 16px;font-size:14px;border-radius:8px"> <a href="{{ route('proclamatori.edit', $proclamatore) }}" class="btn-action btn-action-indigo" style="padding:8px 16px;font-size:14px;border-radius:8px">
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> <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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Modifica Modifica

View File

@@ -42,7 +42,7 @@
{{-- Table --}} {{-- Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm table-striped"> <table class="min-w-full divide-y divide-gray-200 text-sm table-striped table-cards">
<thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)"> <thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)">
<tr> <tr>
<th wire:click="sortBy('territorio_numero')" class="px-3 py-3 text-left text-xs font-semibold text-gray-600 uppercase cursor-pointer"> <th wire:click="sortBy('territorio_numero')" class="px-3 py-3 text-left text-xs font-semibold text-gray-600 uppercase cursor-pointer">
@@ -67,7 +67,7 @@
@forelse($assegnazioni as $a) @forelse($assegnazioni as $a)
<tr class="hover:bg-indigo-50/30 transition-colors"> <tr class="hover:bg-indigo-50/30 transition-colors">
@php($temporaryPdfUrl = !$a->returned_at ? $a->shortPdfUrl() : null) @php($temporaryPdfUrl = !$a->returned_at ? $a->shortPdfUrl() : null)
<td class="px-3 py-2"> <td data-label="Territorio" class="px-3 py-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@if($a->territorio?->thumbnail_path) @if($a->territorio?->thumbnail_path)
<img src="{{ asset('storage/' . $a->territorio->thumbnail_path) }}" <img src="{{ asset('storage/' . $a->territorio->thumbnail_path) }}"
@@ -80,18 +80,18 @@
</div> </div>
</div> </div>
</td> </td>
<td 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' }}</td>
<td class="px-3 py-2">{{ $a->assigned_at->format('d/m/Y') }}</td> <td data-label="Assegnato" class="px-3 py-2">{{ $a->assigned_at->format('d/m/Y') }}</td>
<td class="px-3 py-2"> <td data-label="Rientrato" class="px-3 py-2">
@if($a->returned_at) @if($a->returned_at)
{{ $a->returned_at->format('d/m/Y') }} {{ $a->returned_at->format('d/m/Y') }}
@else @else
<span class="text-amber-600 font-medium text-xs">In corso</span> <span class="text-amber-600 font-medium text-xs">In corso</span>
@endif @endif
</td> </td>
<td class="px-3 py-2">{{ $a->giorni }}</td> <td data-label="Giorni" class="px-3 py-2">{{ $a->giorni }}</td>
<td class="px-3 py-2 text-xs text-gray-500">{{ $a->annoTeocratico?->label }}</td> <td data-label="Anno" class="px-3 py-2 text-xs text-gray-500">{{ $a->annoTeocratico?->label }}</td>
<td class="px-3 py-2 text-xs"> <td data-label="Campagna" class="px-3 py-2 text-xs">
@if($a->counted_in_campaign) @if($a->counted_in_campaign)
<span class="text-green-600">{{ $a->campagna?->descrizione }}</span> <span class="text-green-600">{{ $a->campagna?->descrizione }}</span>
@else @else
@@ -99,8 +99,8 @@
@endif @endif
</td> </td>
@can('settings.manage') @can('settings.manage')
<td class="px-3 py-2 whitespace-nowrap"> <td class="px-3 py-2 whitespace-nowrap cell-actions">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1 flex-wrap">
@if($temporaryPdfUrl) @if($temporaryPdfUrl)
<a href="{{ $temporaryPdfUrl }}" <a href="{{ $temporaryPdfUrl }}"
target="_blank" target="_blank"

View File

@@ -60,7 +60,7 @@
<h2 class="text-lg font-semibold text-gray-900 mb-4">Utenti esistenti</h2> <h2 class="text-lg font-semibold text-gray-900 mb-4">Utenti esistenti</h2>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm"> <table class="min-w-full divide-y divide-gray-200 text-sm table-cards">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th class="px-4 py-2 text-left font-medium text-gray-600">Nome</th> <th class="px-4 py-2 text-left font-medium text-gray-600">Nome</th>
@@ -111,9 +111,9 @@
</div> </div>
</td> </td>
@else @else
<td class="px-4 py-2 text-gray-900">{{ $user->name }}</td> <td data-label="Nome" class="px-4 py-2 text-gray-900">{{ $user->name }}</td>
<td class="px-4 py-2 text-gray-700">{{ $user->email }}</td> <td data-label="Email" class="px-4 py-2 text-gray-700">{{ $user->email }}</td>
<td class="px-4 py-2"> <td data-label="Ruoli" class="px-4 py-2">
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
@forelse($user->roles as $role) @forelse($user->roles as $role)
<span class="inline-flex items-center rounded-full bg-indigo-50 px-2 py-0.5 text-xs font-medium text-indigo-700">{{ $role->name }}</span> <span class="inline-flex items-center rounded-full bg-indigo-50 px-2 py-0.5 text-xs font-medium text-indigo-700">{{ $role->name }}</span>
@@ -122,7 +122,7 @@
@endforelse @endforelse
</div> </div>
</td> </td>
<td class="px-4 py-2"> <td class="px-4 py-2 cell-actions">
<div class="flex gap-2"> <div class="flex gap-2">
<button wire:click="startEdit({{ $user->id }})" type="button" class="px-2 py-1 text-xs font-medium text-indigo-700 bg-indigo-50 rounded hover:bg-indigo-100">Modifica</button> <button wire:click="startEdit({{ $user->id }})" type="button" class="px-2 py-1 text-xs font-medium text-indigo-700 bg-indigo-50 rounded hover:bg-indigo-100">Modifica</button>
<button <button

View File

@@ -9,7 +9,7 @@
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 table-striped"> <table class="min-w-full divide-y divide-gray-200 table-striped table-cards">
<thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)"> <thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)">
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase">N°</th> <th class="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase">N°</th>
@@ -20,9 +20,9 @@
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200">
@forelse($territori as $territorio) @forelse($territori as $territorio)
<tr class="hover:bg-indigo-50/30 transition-colors"> <tr class="hover:bg-indigo-50/30 transition-colors">
<td class="px-4 py-3 text-sm font-semibold text-gray-900">{{ $territorio->numero }}</td> <td data-label="N°" class="px-4 py-3 text-sm font-semibold text-gray-900">{{ $territorio->numero }}</td>
<td class="px-4 py-3 text-sm text-gray-600">{{ $territorio->deleted_at->format('d/m/Y H:i') }}</td> <td data-label="Eliminato il" class="px-4 py-3 text-sm text-gray-600">{{ $territorio->deleted_at->format('d/m/Y H:i') }}</td>
<td class="px-4 py-3 text-sm text-right"> <td class="px-4 py-3 text-sm text-right cell-actions">
<div class="flex flex-wrap items-center justify-end gap-1"> <div class="flex flex-wrap items-center justify-end gap-1">
<button wire:click="restore({{ $territorio->id }})" class="btn-action btn-action-green">Ripristina</button> <button wire:click="restore({{ $territorio->id }})" class="btn-action btn-action-green">Ripristina</button>
<button wire:click="forceDelete({{ $territorio->id }})" wire:confirm="Eliminare DEFINITIVAMENTE il territorio {{ $territorio->numero }}? Questa azione è irreversibile." class="btn-action btn-action-red">Elimina</button> <button wire:click="forceDelete({{ $territorio->id }})" wire:confirm="Eliminare DEFINITIVAMENTE il territorio {{ $territorio->numero }}? Questa azione è irreversibile." class="btn-action btn-action-red">Elimina</button>

View File

@@ -89,7 +89,7 @@
{{-- Table --}} {{-- Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 table-striped"> <table class="min-w-full divide-y divide-gray-200 table-striped table-cards">
<thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)"> <thead style="background:linear-gradient(180deg,#f9fafb,#f3f4f6)">
<tr> <tr>
<th wire:click="sortBy('numero')" class="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase cursor-pointer hover:text-gray-800 transition-colors"> <th wire:click="sortBy('numero')" class="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase cursor-pointer hover:text-gray-800 transition-colors">
@@ -105,7 +105,7 @@
<tbody class="divide-y divide-gray-100"> <tbody class="divide-y divide-gray-100">
@forelse($territori as $territorio) @forelse($territori as $territorio)
<tr class="hover:bg-indigo-50/30 transition-colors"> <tr class="hover:bg-indigo-50/30 transition-colors">
<td class="px-4 py-3 text-sm font-semibold text-gray-900"> <td data-label="N°" class="px-4 py-3 text-sm font-semibold text-gray-900">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@if($territorio->thumbnail_path) @if($territorio->thumbnail_path)
<img src="{{ asset('storage/' . $territorio->thumbnail_path) }}" <img src="{{ asset('storage/' . $territorio->thumbnail_path) }}"
@@ -115,9 +115,9 @@
<a href="{{ route('territori.show', $territorio) }}" class="hover:text-indigo-600 transition-colors">{{ $territorio->numero }}</a> <a href="{{ route('territori.show', $territorio) }}" class="hover:text-indigo-600 transition-colors">{{ $territorio->numero }}</a>
</div> </div>
</td> </td>
<td class="px-4 py-3 text-sm text-gray-600">{{ $territorio->zona?->nome ?? '-' }}</td> <td data-label="Zona" class="px-4 py-3 text-sm text-gray-600">{{ $territorio->zona?->nome ?? '-' }}</td>
<td class="px-4 py-3 text-sm text-gray-600">{{ $territorio->tipologia?->nome ?? '-' }}</td> <td data-label="Tipologia" class="px-4 py-3 text-sm text-gray-600">{{ $territorio->tipologia?->nome ?? '-' }}</td>
<td class="px-4 py-3 text-sm"> <td data-label="Stato" class="px-4 py-3 text-sm">
@php $stato = $territorio->stato; @endphp @php $stato = $territorio->stato; @endphp
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium <span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium
{{ match($stato) { {{ match($stato) {
@@ -135,10 +135,10 @@
</span> </span>
@endif @endif
</td> </td>
<td class="px-4 py-3 text-sm text-gray-600"> <td data-label="Assegnatario" class="px-4 py-3 text-sm text-gray-600">
{{ $territorio->assegnatario?->nome_completo ?? '-' }} {{ $territorio->assegnatario?->nome_completo ?? '-' }}
</td> </td>
<td class="px-4 py-3 text-sm"> <td class="px-4 py-3 text-sm cell-actions">
<div class="flex items-center justify-end gap-1 flex-wrap"> <div class="flex items-center justify-end gap-1 flex-wrap">
{{-- Primary actions: Assegna / Rientra --}} {{-- Primary actions: Assegna / Rientra --}}
@can('territori.assign') @can('territori.assign')

View File

@@ -90,12 +90,21 @@ Route::middleware('auth')->group(function () {
Route::get('territori/{territorio}/edit', TerritorioEdit::class)->name('territori.edit'); Route::get('territori/{territorio}/edit', TerritorioEdit::class)->name('territori.edit');
}); });
// Proclamatori // Proclamatori — visualizzazione (lista + dettaglio) per chi ha
// proclamatori.manage; sola lettura per i non-admin.
Route::middleware('permission:proclamatori.manage')->group(function () { Route::middleware('permission:proclamatori.manage')->group(function () {
Route::get('proclamatori', ProclamatoreIndex::class)->name('proclamatori.index'); Route::get('proclamatori', ProclamatoreIndex::class)->name('proclamatori.index');
});
// Gestione proclamatori (crea/modifica/cestino): solo amministratore. Le
// rotte letterali (create/cestino) devono precedere il wildcard {proclamatore}.
Route::middleware('permission:settings.manage')->group(function () {
Route::get('proclamatori/create', ProclamatoreCreate::class)->name('proclamatori.create'); Route::get('proclamatori/create', ProclamatoreCreate::class)->name('proclamatori.create');
Route::get('proclamatori/cestino', ProclamatoreCestino::class)->name('proclamatori.cestino'); Route::get('proclamatori/cestino', ProclamatoreCestino::class)->name('proclamatori.cestino');
});
Route::middleware('permission:proclamatori.manage')->group(function () {
Route::get('proclamatori/{proclamatore}', ProclamatoreShow::class)->name('proclamatori.show'); Route::get('proclamatori/{proclamatore}', ProclamatoreShow::class)->name('proclamatori.show');
});
Route::middleware('permission:settings.manage')->group(function () {
Route::get('proclamatori/{proclamatore}/edit', ProclamatoreEdit::class)->name('proclamatori.edit'); Route::get('proclamatori/{proclamatore}/edit', ProclamatoreEdit::class)->name('proclamatori.edit');
}); });
@@ -107,10 +116,17 @@ Route::middleware('auth')->group(function () {
Route::get('assegnazioni/{assegnazione}/rientra', Rientra::class)->name('assegnazioni.rientra'); Route::get('assegnazioni/{assegnazione}/rientra', Rientra::class)->name('assegnazioni.rientra');
}); });
// Campagne — creazione/visualizzazione per chi ha campagne.manage. // Campagne — visualizzazione per chi ha campagne.manage; sola lettura per i
// non-admin.
Route::middleware('permission:campagne.manage')->group(function () { Route::middleware('permission:campagne.manage')->group(function () {
Route::get('campagne', CampagnaIndex::class)->name('campagne.index'); Route::get('campagne', CampagnaIndex::class)->name('campagne.index');
});
// Creazione campagne: solo amministratore. La rotta letterale create deve
// precedere il wildcard {campagna}.
Route::middleware('permission:settings.manage')->group(function () {
Route::get('campagne/create', CampagnaCreate::class)->name('campagne.create'); Route::get('campagne/create', CampagnaCreate::class)->name('campagne.create');
});
Route::middleware('permission:campagne.manage')->group(function () {
Route::get('campagne/{campagna}', CampagnaShow::class)->name('campagne.show'); Route::get('campagne/{campagna}', CampagnaShow::class)->name('campagne.show');
}); });
// Modifica campagne: solo amministratore (settings.manage). L'eliminazione // Modifica campagne: solo amministratore (settings.manage). L'eliminazione