diff --git a/app/Http/Controllers/AssignmentPdfController.php b/app/Http/Controllers/AssignmentPdfController.php index 2e4f58e..c55d8e9 100644 --- a/app/Http/Controllers/AssignmentPdfController.php +++ b/app/Http/Controllers/AssignmentPdfController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers; use App\Models\Assegnazione; use Illuminate\Contracts\View\View; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\Support\Facades\Storage; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -14,6 +15,10 @@ class AssignmentPdfController extends Controller { $this->validateAccess($assignment, $code); + if ($expired = $this->linkScaduto($assignment)) { + return $expired; + } + $pdfUrl = route('assignments.pdf.file', [ 'assignment' => $assignment->id, 'code' => $code, @@ -25,10 +30,14 @@ class AssignmentPdfController extends Controller ]); } - public function file(Request $request, Assegnazione $assignment, string $code): StreamedResponse + public function file(Request $request, Assegnazione $assignment, string $code): StreamedResponse|View { $this->validateAccess($assignment, $code); + if ($expired = $this->linkScaduto($assignment)) { + return $expired; + } + $pdfPath = $assignment->territorio?->pdf_path; abort_unless($pdfPath && Storage::disk('public')->exists($pdfPath), 404); @@ -48,4 +57,22 @@ class AssignmentPdfController extends Controller abort_unless($assignment->is_aperta, 403); abort_unless($assignment->territorio?->pdf_path, 404); } + + protected function linkScaduto(Assegnazione $assignment): ?View + { + if (auth()->check()) { + return null; + } + + $ttlMonths = max(1, (int) \App\Models\Setting::getValue('assignment_link_ttl_hours', 1)); + + if ($assignment->assigned_at->copy()->addMonths($ttlMonths)->isPast()) { + return view('assignments.link-scaduto', [ + 'numero' => $assignment->territorio?->numero, + ]); + } + + return null; + } } + diff --git a/app/Http/Controllers/ShortPdfLinkController.php b/app/Http/Controllers/ShortPdfLinkController.php index 2133463..5bf395a 100644 --- a/app/Http/Controllers/ShortPdfLinkController.php +++ b/app/Http/Controllers/ShortPdfLinkController.php @@ -3,20 +3,32 @@ namespace App\Http\Controllers; use App\Models\Assegnazione; +use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; class ShortPdfLinkController extends Controller { - public function __invoke(string $code): RedirectResponse + public function __invoke(string $code): RedirectResponse|View { $assignment = Assegnazione::where('pdf_access_code', $code)->firstOrFail(); abort_unless($assignment->is_aperta, 403); abort_unless($assignment->territorio?->pdf_path, 404); + // Unauthenticated users (proclamatori) are subject to link TTL + if (! auth()->check()) { + $ttlMonths = max(1, (int) \App\Models\Setting::getValue('assignment_link_ttl_hours', 1)); + if ($assignment->assigned_at->copy()->addMonths($ttlMonths)->isPast()) { + return view('assignments.link-scaduto', [ + 'numero' => $assignment->territorio?->numero, + ]); + } + } + return redirect()->route('assignments.pdf.viewer', [ 'assignment' => $assignment->id, 'code' => $code, ]); } } + diff --git a/app/Livewire/Assegnazioni/Assegna.php b/app/Livewire/Assegnazioni/Assegna.php index ee7f595..6380824 100644 --- a/app/Livewire/Assegnazioni/Assegna.php +++ b/app/Livewire/Assegnazioni/Assegna.php @@ -83,6 +83,13 @@ class Assegna extends Component return $this->redirect(route('territori.show', $territorio), navigate: true); } + public function toggleLinkSent(int $assegnazioneId): void + { + $this->authorize('territori.assign'); + $assegnazione = Assegnazione::findOrFail($assegnazioneId); + $assegnazione->forceFill(['link_sent' => ! $assegnazione->link_sent])->saveQuietly(); + } + #[Computed] public function selectedThumbnailUrl(): ?string { @@ -122,10 +129,13 @@ class Assegna extends Component ->get() ->sortBy(fn($a) => (int) $a->territorio?->numero); + $linkTtlMonths = max(1, (int) \App\Models\Setting::getValue('assignment_link_ttl_hours', 1)); + return view('livewire.assegnazioni.assegna', [ 'territoriDisponibili' => $territoriDisponibili, 'proclamatoriAttivi' => $proclamatoriAttivi, 'assegnazioniAperte' => $assegnazioniAperte, + 'linkTtlMonths' => $linkTtlMonths, ]); } } diff --git a/app/Livewire/Campagne/CampagnaShow.php b/app/Livewire/Campagne/CampagnaShow.php index 0ff31b2..a18febf 100644 --- a/app/Livewire/Campagne/CampagnaShow.php +++ b/app/Livewire/Campagne/CampagnaShow.php @@ -18,9 +18,12 @@ class CampagnaShow extends Component public function render() { - // All assignments with returned_at in campaign range that were counted - $conteggiate = Assegnazione::where('campagna_id', $this->campagna->id) + // Assignments counted for this campaign: + // - assigned on or after campaign start + // - linked to this campaign (campaign_id), regardless of returned_at (retroactive returns allowed) + $conteggiate = Assegnazione::where('campaign_id', $this->campagna->id) ->where('counted_in_campaign', true) + ->where('assigned_at', '>=', $this->campagna->start_date) ->with(['territorio', 'proclamatore']) ->orderBy('returned_at') ->get(); diff --git a/app/Models/Assegnazione.php b/app/Models/Assegnazione.php index 32cb3ec..741225b 100644 --- a/app/Models/Assegnazione.php +++ b/app/Models/Assegnazione.php @@ -19,6 +19,7 @@ class Assegnazione extends Model 'counted_in_campaign', 'campaign_id', 'pdf_access_code', + 'link_sent', 'note', 'created_by', 'returned_by', @@ -30,6 +31,7 @@ class Assegnazione extends Model 'assigned_at' => 'date', 'returned_at' => 'date', 'counted_in_campaign' => 'boolean', + 'link_sent' => 'boolean', ]; } @@ -117,6 +119,11 @@ class Assegnazione extends Model return route('assignments.pdf.short', ['code' => $this->ensurePdfAccessCode()]); } + public function markLinkSent(): void + { + $this->forceFill(['link_sent' => true])->saveQuietly(); + } + // ─── Scopes ───────────────────────────────────────────────── public function scopeAperte($query) diff --git a/app/Models/Campagna.php b/app/Models/Campagna.php index 9a06b06..b5f0678 100644 --- a/app/Models/Campagna.php +++ b/app/Models/Campagna.php @@ -72,19 +72,19 @@ class Campagna extends Model /** * Campaign coverage percentage. * Numerator: assignments counted for campaign - * Denominator: ALL assignments with assigned_at in campaign range (returned or not) + * Denominator: total active territories */ public function getPercentualePercorrenzaAttribute(): float { - $totaleNelRange = $this->assegnazioniNelRange()->count(); + $totaleAttivi = Territorio::where('attivo', true)->count(); - if ($totaleNelRange === 0) { + if ($totaleAttivi === 0) { return 0.0; } $conteggiate = $this->assegnazioniConteggiate()->count(); - return round(($conteggiate / $totaleNelRange) * 100, 1); + return round(($conteggiate / $totaleAttivi) * 100, 1); } public function scopeCompletate($query) diff --git a/database/migrations/2026_04_13_172154_add_link_sent_to_assegnazioni_table.php b/database/migrations/2026_04_13_172154_add_link_sent_to_assegnazioni_table.php new file mode 100644 index 0000000..d1aaee9 --- /dev/null +++ b/database/migrations/2026_04_13_172154_add_link_sent_to_assegnazioni_table.php @@ -0,0 +1,28 @@ +boolean('link_sent')->default(false)->after('pdf_access_code'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('assegnazioni', function (Blueprint $table) { + $table->dropColumn('link_sent'); + }); + } +}; diff --git a/resources/views/assignments/link-scaduto.blade.php b/resources/views/assignments/link-scaduto.blade.php new file mode 100644 index 0000000..1adb3b4 --- /dev/null +++ b/resources/views/assignments/link-scaduto.blade.php @@ -0,0 +1,80 @@ + + + + + + Link scaduto — TerManager2 + + + +
+
+ + + +
+

Link scaduto

+

Il link per questo territorio non è più valido.
Contatta il responsabile dei territori per ricevere un nuovo link.

+ @if($numero) + Territorio N° {{ $numero }} + @endif + +
+ + diff --git a/resources/views/assignments/pdf-viewer.blade.php b/resources/views/assignments/pdf-viewer.blade.php index b507bc0..a041ef8 100644 --- a/resources/views/assignments/pdf-viewer.blade.php +++ b/resources/views/assignments/pdf-viewer.blade.php @@ -3,42 +3,178 @@ - PDF territorio {{ $assignment->territorio?->numero }} + Territorio {{ $assignment->territorio?->numero }} - - Apri PDF +
+ + +
+
+ Territorio + N° {{ $assignment->territorio?->numero }} +
+
+ Assegnato a + {{ $assignment->proclamatore?->nome_completo ?? '—' }} +
+
+ Data + {{ $assignment->assigned_at->format('d/m/Y') }} +
+
+ + + + Scarica PDF + +
+ +
+
+ +
+
+ Il tuo dispositivo non supporta la visualizzazione del PDF nel browser. + Scarica il file PDF +
+
+ + - \ No newline at end of file + diff --git a/resources/views/livewire/assegnazioni/assegna.blade.php b/resources/views/livewire/assegnazioni/assegna.blade.php index 44a4ebd..6c69e06 100644 --- a/resources/views/livewire/assegnazioni/assegna.blade.php +++ b/resources/views/livewire/assegnazioni/assegna.blade.php @@ -8,112 +8,160 @@

Questa funzione consente un'assegnazione arbitraria, indipendente dalle tre schede della Home.

-
-
-
- @if(!$preselectedTerritorioId) - - - @endif +
- - - @if($preselectedTerritorioId) - - @endif - @error('territorio_id')

{{ $message }}

@enderror + {{-- Toggle form --}} + - @if($territorio_id) -
-

Anteprima territorio

- @if($this->selectedThumbnailUrl) -
- Thumbnail territorio selezionato -
-

Miniatura del territorio ottimizzata per consultazione rapida anche da mobile.

- @else -
- Nessuna thumbnail disponibile per questo territorio. + {{-- Form --}} +
+ +
+ @if(!$preselectedTerritorioId) + + + @endif + + + + @if($preselectedTerritorioId) + + @endif + @error('territorio_id')

{{ $message }}

@enderror + + @if($territorio_id) +
+

Anteprima territorio

+ @if($this->selectedThumbnailUrl) +
+ Thumbnail territorio selezionato +
+ @else +
+ Nessuna thumbnail disponibile per questo territorio. +
+ @endif
@endif
- @endif -
-
- - - @error('proclamatore_id')

{{ $message }}

@enderror -
+
+ + + @error('proclamatore_id')

{{ $message }}

@enderror +
-
- - - @error('assigned_at')

{{ $message }}

@enderror -
+
+ + + @error('assigned_at')

{{ $message }}

@enderror +
+ +
+ + +
+ +
-
- - Annulla -
-
- {{-- Elenco territori attualmente assegnati con link --}} + {{-- Elenco territori attualmente assegnati --}} @if($assegnazioniAperte->count()) -
+

Territori Assegnati ({{ $assegnazioniAperte->count() }})

-

Per ogni territorio è visibile il link da condividere

+

Link PDF · stato invio · link valido {{ $linkTtlMonths }} {{ $linkTtlMonths === 1 ? 'mese' : 'mesi' }}

@foreach($assegnazioniAperte as $a) - @php($pdfUrl = $a->shortPdfUrl()) + @php + $pdfUrl = $a->shortPdfUrl(); + $linkScaduto = ! auth()->check() && $a->assigned_at->copy()->addMonths($linkTtlMonths)->isPast(); + @endphp
-
-
- N° {{ $a->territorio?->numero }} - {{ $a->territorio?->zona?->nome }} + {{-- Testata riga --}} +
+
+ N° {{ $a->territorio?->numero }} + @if($a->territorio?->zona?->nome) + {{ $a->territorio?->zona?->nome }} + @endif
-
- {{ $a->proclamatore?->nome_completo ?? 'N/A' }} - {{ $a->assigned_at->format('d/m/Y') }} - {{ $a->giorni }}g +
+ {{ $a->proclamatore?->nome_completo ?? 'N/A' }} + · + {{ $a->assigned_at->format('d/m/Y') }} + · + + {{ $a->giorni }}g +
- @if($pdfUrl) -
+ + {{-- Avviso link scaduto (solo per non loggati) --}} + @if($linkScaduto) +
+ + Link scaduto — rigenerare dal dettaglio territorio +
+ + {{-- Link attivo --}} + @elseif($pdfUrl) +
{{ $pdfUrl }} + + {{-- Flag Link Inviato --}} + + Copiato!
@else