diff --git a/app/Http/Controllers/AssignmentPdfController.php b/app/Http/Controllers/AssignmentPdfController.php new file mode 100644 index 0000000..297a24d --- /dev/null +++ b/app/Http/Controllers/AssignmentPdfController.php @@ -0,0 +1,56 @@ +validateAccess($request, $assignment, $code); + + $expiresAt = Carbon::createFromTimestamp((int) $request->query('expires')); + $pdfUrl = URL::temporarySignedRoute( + 'assignments.pdf.file', + $expiresAt, + ['assignment' => $assignment->id, 'code' => $code] + ); + + return view('assignments.pdf-viewer', [ + 'assignment' => $assignment, + 'pdfUrl' => $pdfUrl, + ]); + } + + public function file(Request $request, Assegnazione $assignment, string $code): StreamedResponse + { + $this->validateAccess($request, $assignment, $code); + + $pdfPath = $assignment->territorio?->pdf_path; + abort_unless($pdfPath && Storage::disk('public')->exists($pdfPath), 404); + + return Storage::disk('public')->response( + $pdfPath, + 'territorio-' . $assignment->territorio?->numero . '.pdf', + [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'inline; filename="territorio-' . $assignment->territorio?->numero . '.pdf"', + ] + ); + } + + protected function validateAccess(Request $request, Assegnazione $assignment, string $code): void + { + abort_unless($request->hasValidSignature(), 403); + abort_unless($assignment->pdf_access_code && hash_equals($assignment->pdf_access_code, $code), 404); + abort_unless($assignment->is_aperta, 403); + abort_unless($assignment->territorio?->pdf_path, 404); + } +} \ No newline at end of file diff --git a/app/Livewire/Home.php b/app/Livewire/Home.php index dd2cac9..4859359 100644 --- a/app/Livewire/Home.php +++ b/app/Livewire/Home.php @@ -15,6 +15,8 @@ class Home extends Component public function render() { $settings = Setting::instance(); + $homeLimit = max(1, (int) ($settings->home_limit_list ?? 10)); + $priorityThreshold = (int) ($settings->giorni_giacenza_prioritari ?? 180); $annoCorrente = AnnoTeocratico::corrente(); $campagnaAttiva = Campagna::attiva(); @@ -52,14 +54,33 @@ class Home extends Component $territoriDaAssegnare = Territorio::inReparto() ->with('zona', 'tipologia', 'ultimaAssegnazione') ->get() + ->map(function (Territorio $territorio) use ($priorityThreshold) { + $ultima = $territorio->ultimaAssegnazione; + + if ($ultima && $ultima->returned_at) { + $giorniGiacenza = $ultima->returned_at->startOfDay()->diffInDays(today()); + } elseif (! $ultima) { + $giorniGiacenza = $territorio->created_at->startOfDay()->diffInDays(today()); + } else { + $giorniGiacenza = 0; + } + + $territorio->setAttribute('home_giorni_giacenza', $giorniGiacenza); + $territorio->setAttribute( + 'home_is_prioritario', + (bool) $territorio->prioritario || $giorniGiacenza > $priorityThreshold + ); + + return $territorio; + }) ->sort(function (Territorio $left, Territorio $right) { - $priorityComparison = (int) $right->is_prioritario <=> (int) $left->is_prioritario; + $priorityComparison = (int) $right->home_is_prioritario <=> (int) $left->home_is_prioritario; if ($priorityComparison !== 0) { return $priorityComparison; } - $giacenzaComparison = $right->giorni_giacenza <=> $left->giorni_giacenza; + $giacenzaComparison = $right->home_giorni_giacenza <=> $left->home_giorni_giacenza; if ($giacenzaComparison !== 0) { return $giacenzaComparison; @@ -67,12 +88,12 @@ class Home extends Component return strnatcasecmp((string) $left->numero, (string) $right->numero); }) - ->take(10) + ->take($homeLimit) ->values(); $daRientrare = Territorio::daRientrare() ->with(['zona', 'assegnazioneCorrente.proclamatore']) - ->take(10) + ->take($homeLimit) ->get(); return view('livewire.home', [ @@ -84,6 +105,7 @@ class Home extends Component 'territoriPercorsi' => $territoriPercorsi, 'mediaPercorrenzaMensile' => $mediaPercorrenzaMensile, 'campagnaStats' => $campagnaStats, + 'homeLimit' => $homeLimit, 'territoriDaAssegnare' => $territoriDaAssegnare, 'daRientrare' => $daRientrare, ]); diff --git a/app/Livewire/Registro.php b/app/Livewire/Registro.php index fa18103..1f30906 100644 --- a/app/Livewire/Registro.php +++ b/app/Livewire/Registro.php @@ -151,7 +151,7 @@ class Registro extends Component public function render() { - $query = Assegnazione::with(['territorio.zona', 'proclamatore', 'annoTeocratico', 'campagna']); + $query = Assegnazione::with(['territorio.zona', 'territorio.assegnazioneCorrente', 'proclamatore', 'annoTeocratico', 'campagna']); if ($this->filtroAnno) { $query->where('anno_teocratico_id', $this->filtroAnno); diff --git a/app/Livewire/Settings/SettingsEdit.php b/app/Livewire/Settings/SettingsEdit.php index 64bba09..05b00af 100644 --- a/app/Livewire/Settings/SettingsEdit.php +++ b/app/Livewire/Settings/SettingsEdit.php @@ -12,6 +12,7 @@ class SettingsEdit extends Component public int $giorni_giacenza_prioritari = 180; public int $giorni_per_smarrito = 120; public int $home_limit_list = 10; + public int $assignment_link_ttl_months = 1; public int $audit_retention_days = 365; public function mount() @@ -22,6 +23,7 @@ class SettingsEdit extends Component $this->giorni_giacenza_prioritari = $settings->giorni_giacenza_prioritari ?? 180; $this->giorni_per_smarrito = $settings->giorni_per_smarrito ?? 120; $this->home_limit_list = $settings->home_limit_list ?? 10; + $this->assignment_link_ttl_months = $settings->assignment_link_ttl_hours ?? 1; $this->audit_retention_days = $settings->audit_retention_days ?? 365; } @@ -33,6 +35,7 @@ class SettingsEdit extends Component 'giorni_giacenza_prioritari' => 'required|integer|min:1|max:730', 'giorni_per_smarrito' => 'required|integer|min:30|max:365', 'home_limit_list' => 'required|integer|min:1|max:100', + 'assignment_link_ttl_months' => 'required|integer|min:1|max:24', 'audit_retention_days' => 'required|integer|min:30|max:3650', ]; } @@ -48,6 +51,7 @@ class SettingsEdit extends Component 'giorni_giacenza_prioritari' => $this->giorni_giacenza_prioritari, 'giorni_per_smarrito' => $this->giorni_per_smarrito, 'home_limit_list' => $this->home_limit_list, + 'assignment_link_ttl_hours' => $this->assignment_link_ttl_months, 'audit_retention_days' => $this->audit_retention_days, ]); diff --git a/app/Livewire/Settings/XmlExchange.php b/app/Livewire/Settings/XmlExchange.php index f8241c9..39b16ea 100644 --- a/app/Livewire/Settings/XmlExchange.php +++ b/app/Livewire/Settings/XmlExchange.php @@ -118,6 +118,7 @@ class XmlExchange extends Component 'giorni_giacenza_prioritari' => (int) ($settingsNode->giorni_giacenza_prioritari ?? 180), 'giorni_per_smarrito' => (int) ($settingsNode->giorni_per_smarrito ?? 120), 'home_limit_list' => (int) ($settingsNode->home_limit_list ?? 10), + 'assignment_link_ttl_hours' => (int) ($settingsNode->assignment_link_ttl_months ?? $settingsNode->assignment_link_ttl_hours ?? 1), 'audit_retention_days' => (int) ($settingsNode->audit_retention_days ?? 730), 'setup_completed' => true, ]); @@ -387,6 +388,7 @@ class XmlExchange extends Component 'giorni_giacenza_prioritari' => (int) ($settings->giorni_giacenza_prioritari ?? 180), 'giorni_per_smarrito' => (int) ($settings->giorni_per_smarrito ?? 120), 'home_limit_list' => (int) ($settings->home_limit_list ?? 10), + 'assignment_link_ttl_months' => (int) ($settings->assignment_link_ttl_hours ?? 1), 'audit_retention_days' => (int) ($settings->audit_retention_days ?? 730), ], 'zones' => Zona::query()->orderBy('id')->get(['id', 'nome', 'attivo'])->toArray(), @@ -412,6 +414,7 @@ class XmlExchange extends Component 'giorni_giacenza_prioritari' => (int) ($impostazioni[1] ?? 180), 'giorni_per_smarrito' => (int) ($impostazioni[3] ?? 120), 'home_limit_list' => (int) ($impostazioni[2] ?? 10), + 'assignment_link_ttl_months' => 1, 'audit_retention_days' => 730, ]; diff --git a/app/Livewire/Territori/TerritorioShow.php b/app/Livewire/Territori/TerritorioShow.php index c0482fb..9cecc9f 100644 --- a/app/Livewire/Territori/TerritorioShow.php +++ b/app/Livewire/Territori/TerritorioShow.php @@ -5,7 +5,7 @@ namespace App\Livewire\Territori; use Livewire\Component; use App\Models\Territorio; use App\Models\Assegnazione; -use App\Models\AnnoTeocratico; +use App\Models\Setting; class TerritorioShow extends Component { @@ -13,7 +13,7 @@ class TerritorioShow extends Component public function mount(Territorio $territorio) { - $this->territorio = $territorio->load(['zona', 'tipologia']); + $this->territorio = $territorio->load(['zona', 'tipologia', 'assegnazioneCorrente.proclamatore', 'ultimaAssegnazione']); } public function render() @@ -25,6 +25,8 @@ class TerritorioShow extends Component ->groupBy(fn($a) => $a->annoTeocratico->label); return view('livewire.territori.territorio-show', [ + 'activeAssignment' => $this->territorio->assegnazioneCorrente, + 'assignmentLinkTtlMonths' => (int) Setting::getValue('assignment_link_ttl_hours', 1), 'assegnazioniPerAnno' => $assegnazioni, ]); } diff --git a/app/Models/Assegnazione.php b/app/Models/Assegnazione.php index 87ad40f..2cce97f 100644 --- a/app/Models/Assegnazione.php +++ b/app/Models/Assegnazione.php @@ -4,6 +4,8 @@ namespace App\Models; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\URL; +use Illuminate\Support\Str; class Assegnazione extends Model { @@ -17,6 +19,7 @@ class Assegnazione extends Model 'returned_at', 'counted_in_campaign', 'campaign_id', + 'pdf_access_code', 'note', 'created_by', 'returned_by', @@ -79,6 +82,39 @@ class Assegnazione extends Model return is_null($this->returned_at); } + public function ensurePdfAccessCode(): string + { + if ($this->pdf_access_code) { + return $this->pdf_access_code; + } + + do { + $code = strtoupper(Str::random(12)); + } while (static::query()->where('pdf_access_code', $code)->exists()); + + $this->forceFill(['pdf_access_code' => $code])->saveQuietly(); + + return $code; + } + + public function temporaryPdfViewerUrl(): ?string + { + if (! $this->is_aperta || ! $this->territorio?->pdf_path) { + return null; + } + + $months = max(1, (int) Setting::getValue('assignment_link_ttl_hours', 1)); + + return URL::temporarySignedRoute( + 'assignments.pdf.viewer', + now()->addMonths($months), + [ + 'assignment' => $this->id, + 'code' => $this->ensurePdfAccessCode(), + ] + ); + } + // ─── Scopes ───────────────────────────────────────────────── public function scopeAperte($query) diff --git a/app/Models/Setting.php b/app/Models/Setting.php index dab8a7e..212446d 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model; class Setting extends Model { + protected static ?self $cachedInstance = null; + protected $fillable = [ 'congregazione_nome', 'logo_path', @@ -13,6 +15,7 @@ class Setting extends Model 'giorni_giacenza_prioritari', 'giorni_per_smarrito', 'home_limit_list', + 'assignment_link_ttl_hours', 'audit_retention_days', 'setup_completed', ]; @@ -25,22 +28,41 @@ class Setting extends Model 'giorni_giacenza_prioritari' => 'integer', 'giorni_per_smarrito' => 'integer', 'home_limit_list' => 'integer', + 'assignment_link_ttl_hours' => 'integer', 'audit_retention_days' => 'integer', ]; } + protected static function booted(): void + { + static::saved(function (): void { + static::$cachedInstance = null; + }); + + static::deleted(function (): void { + static::$cachedInstance = null; + }); + } + /** * Get the singleton settings instance (first row). */ public static function instance(): static { - return static::firstOrCreate([], [ + if (static::$cachedInstance instanceof static) { + return static::$cachedInstance; + } + + static::$cachedInstance = static::firstOrCreate([], [ 'giorni_giacenza_da_assegnare' => 120, 'giorni_giacenza_prioritari' => 180, 'giorni_per_smarrito' => 120, 'home_limit_list' => 10, + 'assignment_link_ttl_hours' => 1, 'audit_retention_days' => 730, ]); + + return static::$cachedInstance; } public static function isSetupComplete(): bool diff --git a/app/Models/Territorio.php b/app/Models/Territorio.php index 06411fd..e49ea6a 100644 --- a/app/Models/Territorio.php +++ b/app/Models/Territorio.php @@ -107,11 +107,11 @@ class Territorio extends Model $ultima = $this->ultimaAssegnazione; if ($ultima && $ultima->returned_at) { - return Carbon::parse($ultima->returned_at)->diffInDays(now()); + return Carbon::parse($ultima->returned_at)->startOfDay()->diffInDays(today()); } if (!$ultima) { - return $this->created_at->diffInDays(now()); + return $this->created_at->startOfDay()->diffInDays(today()); } // Currently assigned, no giacenza concept diff --git a/database/migrations/2026_04_08_120000_add_assignment_link_ttl_hours_to_settings_table.php b/database/migrations/2026_04_08_120000_add_assignment_link_ttl_hours_to_settings_table.php new file mode 100644 index 0000000..f4b1232 --- /dev/null +++ b/database/migrations/2026_04_08_120000_add_assignment_link_ttl_hours_to_settings_table.php @@ -0,0 +1,22 @@ +unsignedInteger('assignment_link_ttl_hours')->default(24)->after('home_limit_list'); + }); + } + + public function down(): void + { + Schema::table('settings', function (Blueprint $table) { + $table->dropColumn('assignment_link_ttl_hours'); + }); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_04_08_120100_add_pdf_access_code_to_assegnazioni_table.php b/database/migrations/2026_04_08_120100_add_pdf_access_code_to_assegnazioni_table.php new file mode 100644 index 0000000..cf09d16 --- /dev/null +++ b/database/migrations/2026_04_08_120100_add_pdf_access_code_to_assegnazioni_table.php @@ -0,0 +1,23 @@ +string('pdf_access_code', 32)->nullable()->unique()->after('campaign_id'); + }); + } + + public function down(): void + { + Schema::table('assegnazioni', function (Blueprint $table) { + $table->dropUnique(['pdf_access_code']); + $table->dropColumn('pdf_access_code'); + }); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_04_08_123500_convert_assignment_link_ttl_hours_to_months.php b/database/migrations/2026_04_08_123500_convert_assignment_link_ttl_hours_to_months.php new file mode 100644 index 0000000..7ef859a --- /dev/null +++ b/database/migrations/2026_04_08_123500_convert_assignment_link_ttl_hours_to_months.php @@ -0,0 +1,25 @@ +whereNotNull('assignment_link_ttl_hours') + ->update([ + 'assignment_link_ttl_hours' => DB::raw('GREATEST(1, CEIL(assignment_link_ttl_hours / 720))'), + ]); + } + + public function down(): void + { + DB::table('settings') + ->whereNotNull('assignment_link_ttl_hours') + ->update([ + 'assignment_link_ttl_hours' => DB::raw('assignment_link_ttl_hours * 720'), + ]); + } +}; \ No newline at end of file diff --git a/resources/views/assignments/pdf-viewer.blade.php b/resources/views/assignments/pdf-viewer.blade.php new file mode 100644 index 0000000..b507bc0 --- /dev/null +++ b/resources/views/assignments/pdf-viewer.blade.php @@ -0,0 +1,44 @@ + + +
+ + +{{ $t->zona?->nome }} — {{ $t->tipologia?->nome }} - @if($t->giorni_giacenza > 0) - — in reparto da {{ $t->giorni_giacenza }} giorni + @if($t->home_giorni_giacenza > 0) + — in reparto da {{ $t->home_giorni_giacenza }} giorni @endif
+ {{ $activeAssignment->proclamatore?->nome_completo ?? 'N/A' }} + — assegnato il {{ $activeAssignment->assigned_at->format('d/m/Y') }} + — {{ $activeAssignment->giorni }} giorni +
+Link copiato.
+