From 0553d4ef74f9a3d0316365cbbb6941c3b7adddd1 Mon Sep 17 00:00:00 2001 From: "Z.p8ibwg4jri" Date: Mon, 13 Apr 2026 15:05:37 +0000 Subject: [PATCH] ++ fix temporary PDF viewer URLs, which were causing issues with caching and expiring links. Instead, we now generate short-lived URLs that redirect to the PDF viewer route, ensuring that users can access the PDFs without running into expired links. This change affects the Assegnazione model, the ShortPdfLinkController, and the relevant Blade views for assignments and records. Additionally, I've updated the Home Livewire component to calculate and display the average duration of assignments in months, providing more insight into assignment durations on the dashboard. --- .../Controllers/AssignmentPdfController.php | 25 +- .../Settings/XmlExchangeUploadController.php | 74 +++++ .../Controllers/ShortPdfLinkController.php | 22 ++ app/Livewire/Home.php | 15 +- app/Livewire/Settings/XmlExchange.php | 293 +++++++++--------- app/Models/Assegnazione.php | 22 +- docker/nginx/default.conf | 12 + .../livewire/assegnazioni/assegna.blade.php | 2 +- resources/views/livewire/home.blade.php | 2 +- resources/views/livewire/registro.blade.php | 2 +- .../livewire/settings/xml-exchange.blade.php | 65 ++-- .../territori/territorio-index.blade.php | 5 +- .../territori/territorio-show.blade.php | 2 +- .../vendor/pagination/bootstrap-4.blade.php | 46 +++ .../vendor/pagination/bootstrap-5.blade.php | 88 ++++++ .../views/vendor/pagination/default.blade.php | 46 +++ .../vendor/pagination/semantic-ui.blade.php | 36 +++ .../pagination/simple-bootstrap-4.blade.php | 27 ++ .../pagination/simple-bootstrap-5.blade.php | 29 ++ .../pagination/simple-default.blade.php | 19 ++ .../pagination/simple-tailwind.blade.php | 25 ++ .../vendor/pagination/tailwind.blade.php | 131 ++++++++ routes/web.php | 5 + 23 files changed, 781 insertions(+), 212 deletions(-) create mode 100644 app/Http/Controllers/Settings/XmlExchangeUploadController.php create mode 100644 app/Http/Controllers/ShortPdfLinkController.php create mode 100644 resources/views/vendor/pagination/bootstrap-4.blade.php create mode 100644 resources/views/vendor/pagination/bootstrap-5.blade.php create mode 100644 resources/views/vendor/pagination/default.blade.php create mode 100644 resources/views/vendor/pagination/semantic-ui.blade.php create mode 100644 resources/views/vendor/pagination/simple-bootstrap-4.blade.php create mode 100644 resources/views/vendor/pagination/simple-bootstrap-5.blade.php create mode 100644 resources/views/vendor/pagination/simple-default.blade.php create mode 100644 resources/views/vendor/pagination/simple-tailwind.blade.php create mode 100644 resources/views/vendor/pagination/tailwind.blade.php diff --git a/app/Http/Controllers/AssignmentPdfController.php b/app/Http/Controllers/AssignmentPdfController.php index 297a24d..2e4f58e 100644 --- a/app/Http/Controllers/AssignmentPdfController.php +++ b/app/Http/Controllers/AssignmentPdfController.php @@ -3,35 +3,31 @@ namespace App\Http\Controllers; use App\Models\Assegnazione; -use Carbon\Carbon; use Illuminate\Contracts\View\View; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Facades\URL; use Symfony\Component\HttpFoundation\StreamedResponse; class AssignmentPdfController extends Controller { public function viewer(Request $request, Assegnazione $assignment, string $code): View { - $this->validateAccess($request, $assignment, $code); + $this->validateAccess($assignment, $code); - $expiresAt = Carbon::createFromTimestamp((int) $request->query('expires')); - $pdfUrl = URL::temporarySignedRoute( - 'assignments.pdf.file', - $expiresAt, - ['assignment' => $assignment->id, 'code' => $code] - ); + $pdfUrl = route('assignments.pdf.file', [ + 'assignment' => $assignment->id, + 'code' => $code, + ]); return view('assignments.pdf-viewer', [ 'assignment' => $assignment, - 'pdfUrl' => $pdfUrl, + 'pdfUrl' => $pdfUrl, ]); } public function file(Request $request, Assegnazione $assignment, string $code): StreamedResponse { - $this->validateAccess($request, $assignment, $code); + $this->validateAccess($assignment, $code); $pdfPath = $assignment->territorio?->pdf_path; abort_unless($pdfPath && Storage::disk('public')->exists($pdfPath), 404); @@ -40,17 +36,16 @@ class AssignmentPdfController extends Controller $pdfPath, 'territorio-' . $assignment->territorio?->numero . '.pdf', [ - 'Content-Type' => 'application/pdf', + 'Content-Type' => 'application/pdf', 'Content-Disposition' => 'inline; filename="territorio-' . $assignment->territorio?->numero . '.pdf"', ] ); } - protected function validateAccess(Request $request, Assegnazione $assignment, string $code): void + protected function validateAccess(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/Http/Controllers/Settings/XmlExchangeUploadController.php b/app/Http/Controllers/Settings/XmlExchangeUploadController.php new file mode 100644 index 0000000..9f546c4 --- /dev/null +++ b/app/Http/Controllers/Settings/XmlExchangeUploadController.php @@ -0,0 +1,74 @@ +validate([ + 'sqlDump' => ['required', 'file', 'max:256000'], + ]); + + $file = $request->file('sqlDump'); + $ext = strtolower($file->getClientOriginalExtension()); + if (! in_array($ext, ['sql', 'txt'])) { + return back()->withErrors(['sqlDump' => 'Il file deve essere .sql o .txt']); + } + + $content = file_get_contents($file->getRealPath()); + if (! $content) { + return back()->withErrors(['sqlDump' => 'File vuoto o non leggibile.']); + } + + $exchange = app(XmlExchange::class); + $dataset = $exchange->legacySqlToDatasetPublic($content); + $xml = $exchange->datasetToXmlPublic($dataset, 'legacy-sql-conversion'); + + return response()->streamDownload(function () use ($xml) { + echo $xml; + }, 'termanager-conversion.xml', ['Content-Type' => 'application/xml; charset=UTF-8']); + } + + public function importXml(Request $request) + { + $request->validate([ + 'xmlImport' => ['required', 'file', 'max:256000'], + ]); + + $file = $request->file('xmlImport'); + $ext = strtolower($file->getClientOriginalExtension()); + if (! in_array($ext, ['xml', 'txt'])) { + return back()->withErrors(['xmlImport' => 'Il file deve essere .xml o .txt']); + } + + $content = file_get_contents($file->getRealPath()); + if (! $content) { + return back()->withErrors(['xmlImport' => 'File vuoto o non leggibile.']); + } + + $exchange = new XmlExchange(); + $result = $exchange->importXmlFromContent($content); + + if (isset($result['error'])) { + return back()->withErrors(['xmlImport' => $result['error']]); + } + + $stats = $result['stats']; + $issues = $result['issues']; + + $message = 'Import XML completato con successo.'; + if (($stats['duplicate_territori'] ?? 0) > 0 || ($stats['assegnazioni_saltate'] ?? 0) > 0) { + $message .= ' Territori duplicati saltati: ' . $stats['duplicate_territori'] . '. Assegnazioni saltate: ' . $stats['assegnazioni_saltate'] . '.'; + } + + return redirect()->route('xml.exchange') + ->with('success', $message) + ->with('importStats', $stats) + ->with('importIssues', $issues); + } +} diff --git a/app/Http/Controllers/ShortPdfLinkController.php b/app/Http/Controllers/ShortPdfLinkController.php new file mode 100644 index 0000000..2133463 --- /dev/null +++ b/app/Http/Controllers/ShortPdfLinkController.php @@ -0,0 +1,22 @@ +firstOrFail(); + + abort_unless($assignment->is_aperta, 403); + abort_unless($assignment->territorio?->pdf_path, 404); + + return redirect()->route('assignments.pdf.viewer', [ + 'assignment' => $assignment->id, + 'code' => $code, + ]); + } +} diff --git a/app/Livewire/Home.php b/app/Livewire/Home.php index 5076db6..3426655 100644 --- a/app/Livewire/Home.php +++ b/app/Livewire/Home.php @@ -108,12 +108,24 @@ class Home extends Component ->count('territorio_id'); } - // Monthly average + // Monthly average (territories/month this year) $mediaPercorrenzaMensile = 0; if ($annoCorrente && $annoCorrente->mesi_trascorsi > 0) { $mediaPercorrenzaMensile = round($territoriPercorsi / $annoCorrente->mesi_trascorsi, 1); } + // Average assignment duration in months (current year only, matches old app "Media Percorrenza Congregazione") + $avgGiorni = null; + if ($annoCorrente) { + $avgGiorni = Assegnazione::where('anno_teocratico_id', $annoCorrente->id) + ->whereNotNull('returned_at') + ->whereRaw('YEAR(assigned_at) >= 1900') + ->whereRaw('DATEDIFF(returned_at, assigned_at) > 0') + ->selectRaw('AVG(DATEDIFF(returned_at, assigned_at)) as media_giorni') + ->value('media_giorni'); + } + $mediaDurataPercorrenzaMesi = $avgGiorni ? round($avgGiorni / 30.44, 1) : 0; + // Campaign stats $campagnaStats = null; if ($campagnaAttiva) { @@ -178,6 +190,7 @@ class Home extends Component 'totInReparto' => $totInReparto, 'territoriPercorsi' => $territoriPercorsi, 'mediaPercorrenzaMensile' => $mediaPercorrenzaMensile, + 'mediaDurataPercorrenzaMesi' => $mediaDurataPercorrenzaMesi, 'campagnaStats' => $campagnaStats, 'homeLimit' => $homeLimit, 'territoriDaAssegnare' => $territoriDaAssegnare, diff --git a/app/Livewire/Settings/XmlExchange.php b/app/Livewire/Settings/XmlExchange.php index 39b16ea..effcff7 100644 --- a/app/Livewire/Settings/XmlExchange.php +++ b/app/Livewire/Settings/XmlExchange.php @@ -25,8 +25,6 @@ class XmlExchange extends Component { use WithFileUploads; - public $sqlDump; - public $xmlImport; public array $importStats = []; public array $importIssues = []; public array $pdfFolder = []; @@ -46,46 +44,135 @@ class XmlExchange extends Component } } - public function convertLegacySqlToXml() + public function importTerritoryPdfFolder(): void { $this->validate([ - 'sqlDump' => ['required', 'file', 'mimes:sql,txt'], + 'pdfFolder' => ['required', 'array', 'min:1'], + 'pdfFolder.*' => ['file', 'mimes:pdf', 'max:10240'], ]); - $content = file_get_contents($this->sqlDump->getRealPath()); - $dataset = $this->legacySqlToDataset($content ?: ''); - $xml = $this->datasetToXml($dataset, 'legacy-sql-conversion'); + $importId = (string) Str::uuid(); + $storedFiles = []; + + foreach ($this->pdfFolder as $index => $file) { + $originalName = $file->getClientOriginalName(); + $safeName = Str::slug(pathinfo($originalName, PATHINFO_FILENAME)); + $extension = strtolower($file->getClientOriginalExtension() ?: 'pdf'); + $storedPath = $file->storeAs( + 'bulk-territori-imports/' . $importId, + str_pad((string) $index, 4, '0', STR_PAD_LEFT) . '-' . $safeName . '.' . $extension, + 'local' + ); + + $storedFiles[] = [ + 'original_name' => $originalName, + 'stored_path' => $storedPath, + ]; + } + + $this->pdfFolder = []; + + } + + public function refreshPdfImportStatus(): void + { + if (! $this->currentPdfImportId) { + return; + } + + $state = app(TerritorioPdfImportState::class)->get($this->currentPdfImportId); + + if (! $state) { + return; + } + + $this->pdfImportStatus = $state['status'] ?? 'idle'; + $this->pdfImportStats = $state['stats'] ?? []; + $this->pdfImportLogs = $state['logs'] ?? []; + $this->pdfImportIssues = $state['issues'] ?? []; + $this->pdfImportLogText = implode(PHP_EOL, $this->pdfImportLogs); + } + + protected function dispatchPdfImport(string $importId, array $storedFiles, string $initialLog): void + { + $state = app(TerritorioPdfImportDispatcher::class) + ->dispatchStoredFiles($importId, $storedFiles, auth()->id(), $initialLog); + + $this->currentPdfImportId = $importId; + $this->pdfImportStatus = $state['status'] ?? 'queued'; + $this->pdfImportStats = $state['stats'] ?? []; + $this->pdfImportLogs = $state['logs'] ?? []; + $this->pdfImportIssues = $state['issues'] ?? []; + $this->refreshPdfImportStatus(); + + session()->flash('success', 'Import PDF avviato in background. I log si aggiorneranno automaticamente.'); + } + + public function downloadImportLogPdf() + { + if (empty($this->importStats)) { + session()->flash('error', 'Nessun log da scaricare. Esegui prima un import XML.'); + return; + } + + $stats = $this->importStats; + $issues = $this->importIssues; + $generatedAt = now()->format('d/m/Y H:i:s'); + + $html = view('pdf.import-log', compact('stats', 'issues', 'generatedAt'))->render(); + + return response()->streamDownload(function () use ($html) { + echo Pdf::loadHTML($html) + ->setPaper('a4', 'portrait') + ->output(); + }, 'import-log-' . now()->format('Ymd-His') . '.pdf', ['Content-Type' => 'application/pdf']); + } + + public function exportCurrentAsXml() + { + $dataset = $this->currentDataset(); + $xml = $this->datasetToXml($dataset, 'current-app-export'); return response()->streamDownload(function () use ($xml) { echo $xml; - }, 'termanager-conversion.xml', ['Content-Type' => 'application/xml; charset=UTF-8']); + }, 'termanager-export.xml', ['Content-Type' => 'application/xml; charset=UTF-8']); } - public function importXmlIntoApp(): void + public function render() + { + if (session()->has('importStats')) { + $this->importStats = session('importStats'); + } + if (session()->has('importIssues')) { + $this->importIssues = session('importIssues'); + } + + return view('livewire.settings.xml-exchange'); + } + + public function legacySqlToDatasetPublic(string $sql): array + { + return $this->legacySqlToDataset($sql); + } + + public function datasetToXmlPublic(array $dataset, string $source): string + { + return $this->datasetToXml($dataset, $source); + } + + public function importXmlFromContent(string $content): array { $this->importStats = []; $this->importIssues = []; - $this->validate([ - 'xmlImport' => ['required', 'file', 'mimes:xml,txt'], - ]); - - $content = file_get_contents($this->xmlImport->getRealPath()); - if (! $content) { - $this->addError('xmlImport', 'File XML non valido.'); - return; - } - $xml = @simplexml_load_string($content); if (! $xml) { - $this->addError('xmlImport', 'Impossibile leggere il file XML.'); - return; + return ['error' => 'Impossibile leggere il file XML.']; } $actorId = auth()->id() ?? User::query()->value('id'); if (! $actorId) { - $this->addError('xmlImport', 'Serve almeno un utente nel sistema per importare i dati.'); - return; + return ['error' => 'Serve almeno un utente nel sistema per importare i dati.']; } $stats = [ @@ -268,113 +355,10 @@ class XmlExchange extends Component } }); - $this->importStats = $stats; - - $message = 'Import XML completato con successo.'; - if ($stats['duplicate_territori'] > 0 || $stats['assegnazioni_saltate'] > 0) { - $message .= ' Territori duplicati saltati: ' . $stats['duplicate_territori'] . '. Assegnazioni saltate: ' . $stats['assegnazioni_saltate'] . '.'; - } - - session()->flash('success', $message); - } - - public function importTerritoryPdfFolder(): void - { - $this->validate([ - 'pdfFolder' => ['required', 'array', 'min:1'], - 'pdfFolder.*' => ['file', 'mimes:pdf', 'max:10240'], - ]); - - $importId = (string) Str::uuid(); - $storedFiles = []; - - foreach ($this->pdfFolder as $index => $file) { - $originalName = $file->getClientOriginalName(); - $safeName = Str::slug(pathinfo($originalName, PATHINFO_FILENAME)); - $extension = strtolower($file->getClientOriginalExtension() ?: 'pdf'); - $storedPath = $file->storeAs( - 'bulk-territori-imports/' . $importId, - str_pad((string) $index, 4, '0', STR_PAD_LEFT) . '-' . $safeName . '.' . $extension, - 'local' - ); - - $storedFiles[] = [ - 'original_name' => $originalName, - 'stored_path' => $storedPath, - ]; - } - - $this->pdfFolder = []; - - } - - public function refreshPdfImportStatus(): void - { - if (! $this->currentPdfImportId) { - return; - } - - $state = app(TerritorioPdfImportState::class)->get($this->currentPdfImportId); - - if (! $state) { - return; - } - - $this->pdfImportStatus = $state['status'] ?? 'idle'; - $this->pdfImportStats = $state['stats'] ?? []; - $this->pdfImportLogs = $state['logs'] ?? []; - $this->pdfImportIssues = $state['issues'] ?? []; - $this->pdfImportLogText = implode(PHP_EOL, $this->pdfImportLogs); - } - - protected function dispatchPdfImport(string $importId, array $storedFiles, string $initialLog): void - { - $state = app(TerritorioPdfImportDispatcher::class) - ->dispatchStoredFiles($importId, $storedFiles, auth()->id(), $initialLog); - - $this->currentPdfImportId = $importId; - $this->pdfImportStatus = $state['status'] ?? 'queued'; - $this->pdfImportStats = $state['stats'] ?? []; - $this->pdfImportLogs = $state['logs'] ?? []; - $this->pdfImportIssues = $state['issues'] ?? []; - $this->refreshPdfImportStatus(); - - session()->flash('success', 'Import PDF avviato in background. I log si aggiorneranno automaticamente.'); - } - - public function downloadImportLogPdf() - { - if (empty($this->importStats)) { - session()->flash('error', 'Nessun log da scaricare. Esegui prima un import XML.'); - return; - } - - $stats = $this->importStats; - $issues = $this->importIssues; - $generatedAt = now()->format('d/m/Y H:i:s'); - - $html = view('pdf.import-log', compact('stats', 'issues', 'generatedAt'))->render(); - - return response()->streamDownload(function () use ($html) { - echo Pdf::loadHTML($html) - ->setPaper('a4', 'portrait') - ->output(); - }, 'import-log-' . now()->format('Ymd-His') . '.pdf', ['Content-Type' => 'application/pdf']); - } - - public function exportCurrentAsXml() - { - $dataset = $this->currentDataset(); - $xml = $this->datasetToXml($dataset, 'current-app-export'); - - return response()->streamDownload(function () use ($xml) { - echo $xml; - }, 'termanager-export.xml', ['Content-Type' => 'application/xml; charset=UTF-8']); - } - - public function render() - { - return view('livewire.settings.xml-exchange'); + return [ + 'stats' => $stats, + 'issues' => $this->importIssues, + ]; } private function currentDataset(): array @@ -512,7 +496,7 @@ class XmlExchange extends Component private function extractInsertRows(string $sql): array { $result = []; - preg_match_all('/INSERT INTO `([^`]+)` VALUES\s*(.+?);/s', $sql, $matches, PREG_SET_ORDER); + preg_match_all('/INSERT INTO `([^`]+)` VALUES\s*(.+);/m', $sql, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $table = $match[1]; @@ -545,7 +529,7 @@ class XmlExchange extends Component $escape = false; continue; } - if ($ch === '\\\\') { + if ($ch === '\\') { $escape = true; continue; } @@ -603,7 +587,7 @@ class XmlExchange extends Component $escape = false; continue; } - if ($ch === '\\\\') { + if ($ch === '\\') { $escape = true; continue; } @@ -645,7 +629,8 @@ class XmlExchange extends Component if (str_starts_with($raw, "'") && str_ends_with($raw, "'")) { $v = substr($raw, 1, -1); $v = str_replace(["\\\\", "\\'", '\\"', '\\n', '\\r', '\\t'], ['\\', "'", '"', "\n", "\r", "\t"], $v); - return html_entity_decode($v, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $v = html_entity_decode($v, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + return $this->normalizeUnicodeQuotes($v); } if (is_numeric($raw)) { @@ -655,6 +640,15 @@ class XmlExchange extends Component return $raw; } + private function normalizeUnicodeQuotes(string $value): string + { + return str_replace( + ["\u{2018}", "\u{2019}", "\u{2032}", "\u{2035}", "\u{201C}", "\u{201D}", "\u{201E}", "\u{2033}", "\u{2036}"], + ["'", "'", "'", "'", '"', '"', '"', '"', '"'], + $value + ); + } + private function datesFromLegacyAnnoLabel(string $label): array { if (preg_match('/(\d{4})\s*-\s*(\d{4})/', $label, $m)) { @@ -709,7 +703,7 @@ class XmlExchange extends Component $settings = $xml->addChild('settings'); foreach ($dataset['settings'] as $key => $value) { - $settings->addChild($key, htmlspecialchars((string) $value, ENT_QUOTES | ENT_XML1, 'UTF-8')); + $this->addXmlText($settings, $key, (string) $value); } $zonesNode = $xml->addChild('zones'); @@ -718,7 +712,7 @@ class XmlExchange extends Component if (isset($zone['legacy_id'])) { $node->addAttribute('legacy_id', (string) $zone['legacy_id']); } - $node->addChild('nome', htmlspecialchars((string) ($zone['nome'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8')); + $this->addXmlText($node, 'nome', (string) ($zone['nome'] ?? '')); $node->addChild('attivo', (string) ((int) ($zone['attivo'] ?? 1))); } @@ -728,7 +722,7 @@ class XmlExchange extends Component if (isset($tipologia['legacy_id'])) { $node->addAttribute('legacy_id', (string) $tipologia['legacy_id']); } - $node->addChild('nome', htmlspecialchars((string) ($tipologia['nome'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8')); + $this->addXmlText($node, 'nome', (string) ($tipologia['nome'] ?? '')); $node->addChild('attivo', (string) ((int) ($tipologia['attivo'] ?? 1))); } @@ -738,8 +732,8 @@ class XmlExchange extends Component if (isset($proclamatore['legacy_id'])) { $node->addAttribute('legacy_id', (string) $proclamatore['legacy_id']); } - $node->addChild('nome', htmlspecialchars((string) ($proclamatore['nome'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8')); - $node->addChild('cognome', htmlspecialchars((string) ($proclamatore['cognome'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8')); + $this->addXmlText($node, 'nome', (string) ($proclamatore['nome'] ?? '')); + $this->addXmlText($node, 'cognome', (string) ($proclamatore['cognome'] ?? '')); $node->addChild('attivo', (string) ((int) ($proclamatore['attivo'] ?? 1))); } @@ -749,11 +743,11 @@ class XmlExchange extends Component if (isset($territorio['legacy_id'])) { $node->addAttribute('legacy_id', (string) $territorio['legacy_id']); } - $node->addChild('numero', htmlspecialchars((string) ($territorio['numero'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8')); + $this->addXmlText($node, 'numero', (string) ($territorio['numero'] ?? '')); $node->addChild('legacy_zona_id', (string) ($territorio['legacy_zona_id'] ?? ($territorio['zona_id'] ?? ''))); $node->addChild('legacy_tipologia_id', (string) ($territorio['legacy_tipologia_id'] ?? ($territorio['tipologia_id'] ?? ''))); - $node->addChild('confini', htmlspecialchars((string) ($territorio['confini'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8')); - $node->addChild('note', htmlspecialchars((string) ($territorio['note'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8')); + $this->addXmlText($node, 'confini', (string) ($territorio['confini'] ?? '')); + $this->addXmlText($node, 'note', (string) ($territorio['note'] ?? '')); $node->addChild('attivo', (string) ((int) ($territorio['attivo'] ?? 1))); $node->addChild('prioritario', (string) ((int) ($territorio['prioritario'] ?? 0))); } @@ -764,7 +758,7 @@ class XmlExchange extends Component if (isset($anno['legacy_id'])) { $node->addAttribute('legacy_id', (string) $anno['legacy_id']); } - $node->addChild('label', htmlspecialchars((string) ($anno['label'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8')); + $this->addXmlText($node, 'label', (string) ($anno['label'] ?? '')); $node->addChild('start_date', (string) ($anno['start_date'] ?? '')); $node->addChild('end_date', (string) ($anno['end_date'] ?? '')); } @@ -775,7 +769,7 @@ class XmlExchange extends Component if (isset($campagna['legacy_id'])) { $node->addAttribute('legacy_id', (string) $campagna['legacy_id']); } - $node->addChild('descrizione', htmlspecialchars((string) ($campagna['descrizione'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8')); + $this->addXmlText($node, 'descrizione', (string) ($campagna['descrizione'] ?? '')); $node->addChild('start_date', (string) ($campagna['start_date'] ?? '')); $node->addChild('end_date', (string) ($campagna['end_date'] ?? '')); } @@ -793,9 +787,18 @@ class XmlExchange extends Component $node->addChild('assigned_at', (string) ($assegnazione['assigned_at'] ?? '')); $node->addChild('returned_at', (string) ($assegnazione['returned_at'] ?? '')); $node->addChild('counted_in_campaign', (string) ((int) ($assegnazione['counted_in_campaign'] ?? 0))); - $node->addChild('note', htmlspecialchars((string) ($assegnazione['note'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8')); + $this->addXmlText($node, 'note', (string) ($assegnazione['note'] ?? '')); } return $xml->asXML() ?: ''; } + + private function addXmlText(\SimpleXMLElement $parent, string $name, string $value): \SimpleXMLElement + { + $child = $parent->addChild($name); + $dom = dom_import_simplexml($child); + $dom->textContent = $value; + + return $child; + } } diff --git a/app/Models/Assegnazione.php b/app/Models/Assegnazione.php index 42b6094..32cb3ec 100644 --- a/app/Models/Assegnazione.php +++ b/app/Models/Assegnazione.php @@ -4,7 +4,6 @@ namespace App\Models; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\URL; use Illuminate\Support\Str; class Assegnazione extends Model @@ -103,18 +102,19 @@ class Assegnazione extends Model return null; } - $months = max(1, (int) Setting::getValue('assignment_link_ttl_hours', 1)); + return route('assignments.pdf.viewer', [ + 'assignment' => $this->id, + 'code' => $this->ensurePdfAccessCode(), + ]); + } - $url = URL::temporarySignedRoute( - 'assignments.pdf.viewer', - now()->addMonths($months), - [ - 'assignment' => $this->id, - 'code' => $this->ensurePdfAccessCode(), - ] - ); + public function shortPdfUrl(): ?string + { + if (! $this->is_aperta || ! $this->territorio?->pdf_path) { + return null; + } - return $url; + return route('assignments.pdf.short', ['code' => $this->ensurePdfAccessCode()]); } // ─── Scopes ───────────────────────────────────────────────── diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index 5725da9..c88b05d 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -21,6 +21,18 @@ server { fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; fastcgi_hide_header X-Powered-By; + + # Forward proxy headers so Laravel can validate signed URLs behind reverse proxy + fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for; + fastcgi_param HTTP_X_FORWARDED_HOST $http_host; + fastcgi_param HTTP_X_FORWARDED_PORT $http_x_forwarded_port; + + # Trust external proxy proto, or default to https for signed URL validation + set $fwd_proto $http_x_forwarded_proto; + if ($fwd_proto = "") { + set $fwd_proto "https"; + } + fastcgi_param HTTP_X_FORWARDED_PROTO $fwd_proto; } location ~ /\.(?!well-known).* { diff --git a/resources/views/livewire/assegnazioni/assegna.blade.php b/resources/views/livewire/assegnazioni/assegna.blade.php index 40d70e1..44a4ebd 100644 --- a/resources/views/livewire/assegnazioni/assegna.blade.php +++ b/resources/views/livewire/assegnazioni/assegna.blade.php @@ -89,7 +89,7 @@
@foreach($assegnazioniAperte as $a) - @php($pdfUrl = $a->temporaryPdfViewerUrl()) + @php($pdfUrl = $a->shortPdfUrl())
diff --git a/resources/views/livewire/home.blade.php b/resources/views/livewire/home.blade.php index fa6c8ef..82a8623 100644 --- a/resources/views/livewire/home.blade.php +++ b/resources/views/livewire/home.blade.php @@ -37,7 +37,7 @@

Percorsi (anno)

{{ $territoriPercorsi }}

-

media {{ $mediaPercorrenzaMensile }}/mese

+

durata media {{ $mediaDurataPercorrenzaMesi }} mesi

diff --git a/resources/views/livewire/registro.blade.php b/resources/views/livewire/registro.blade.php index 326d55f..b6cdf4a 100644 --- a/resources/views/livewire/registro.blade.php +++ b/resources/views/livewire/registro.blade.php @@ -66,7 +66,7 @@ @forelse($assegnazioni as $a) - @php($temporaryPdfUrl = !$a->returned_at ? $a->temporaryPdfViewerUrl() : null) + @php($temporaryPdfUrl = !$a->returned_at ? $a->shortPdfUrl() : null)
@if($a->territorio?->thumbnail_path) diff --git a/resources/views/livewire/settings/xml-exchange.blade.php b/resources/views/livewire/settings/xml-exchange.blade.php index 3c25ab4..9f67c37 100644 --- a/resources/views/livewire/settings/xml-exchange.blade.php +++ b/resources/views/livewire/settings/xml-exchange.blade.php @@ -246,47 +246,46 @@

Conversione dump SQL legacy

Carica il file SQL (es. dump-termanager-202604071526.sql) e genera un XML compatibile con l'import dell'app.

-
- - @error('sqlDump')

{{ $message }}

@enderror -
+
+ @csrf +
+ + @error('sqlDump')

{{ $message }}

@enderror +
-
- -
+
+ +
+

Import XML nell'app

Importa un XML nel formato TerManager2. L'import sostituisce i dati gestionali (zone, tipologie, proclamatori, territori, anni, campagne, assegnazioni e impostazioni).

-
- Importazione in corso... attendi il completamento. -
+
+ @csrf +
+ + @error('xmlImport')

{{ $message }}

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

{{ $message }}

@enderror -
- -
- -
+
+ +
+
@if(!empty($importStats)) diff --git a/resources/views/livewire/territori/territorio-index.blade.php b/resources/views/livewire/territori/territorio-index.blade.php index f99ddd7..7e81a23 100644 --- a/resources/views/livewire/territori/territorio-index.blade.php +++ b/resources/views/livewire/territori/territorio-index.blade.php @@ -128,9 +128,8 @@ {{ str_replace('_', ' ', ucfirst($stato)) }} @if($territorio->is_prioritario) - - ★ {{ $territorio->prioritario ? 'Man.' : 'Auto' }} + + ★ Prioritario @endif diff --git a/resources/views/livewire/territori/territorio-show.blade.php b/resources/views/livewire/territori/territorio-show.blade.php index da8ea7f..2a0a02d 100644 --- a/resources/views/livewire/territori/territorio-show.blade.php +++ b/resources/views/livewire/territori/territorio-show.blade.php @@ -81,7 +81,7 @@
@if($activeAssignment) - @php($temporaryPdfUrl = $activeAssignment->temporaryPdfViewerUrl()) + @php($temporaryPdfUrl = $activeAssignment->shortPdfUrl())
diff --git a/resources/views/vendor/pagination/bootstrap-4.blade.php b/resources/views/vendor/pagination/bootstrap-4.blade.php new file mode 100644 index 0000000..63c6f56 --- /dev/null +++ b/resources/views/vendor/pagination/bootstrap-4.blade.php @@ -0,0 +1,46 @@ +@if ($paginator->hasPages()) + +@endif diff --git a/resources/views/vendor/pagination/bootstrap-5.blade.php b/resources/views/vendor/pagination/bootstrap-5.blade.php new file mode 100644 index 0000000..a1795a4 --- /dev/null +++ b/resources/views/vendor/pagination/bootstrap-5.blade.php @@ -0,0 +1,88 @@ +@if ($paginator->hasPages()) + +@endif diff --git a/resources/views/vendor/pagination/default.blade.php b/resources/views/vendor/pagination/default.blade.php new file mode 100644 index 0000000..0db70b5 --- /dev/null +++ b/resources/views/vendor/pagination/default.blade.php @@ -0,0 +1,46 @@ +@if ($paginator->hasPages()) + +@endif diff --git a/resources/views/vendor/pagination/semantic-ui.blade.php b/resources/views/vendor/pagination/semantic-ui.blade.php new file mode 100644 index 0000000..ef0dbb1 --- /dev/null +++ b/resources/views/vendor/pagination/semantic-ui.blade.php @@ -0,0 +1,36 @@ +@if ($paginator->hasPages()) + +@endif diff --git a/resources/views/vendor/pagination/simple-bootstrap-4.blade.php b/resources/views/vendor/pagination/simple-bootstrap-4.blade.php new file mode 100644 index 0000000..4bb4917 --- /dev/null +++ b/resources/views/vendor/pagination/simple-bootstrap-4.blade.php @@ -0,0 +1,27 @@ +@if ($paginator->hasPages()) + +@endif diff --git a/resources/views/vendor/pagination/simple-bootstrap-5.blade.php b/resources/views/vendor/pagination/simple-bootstrap-5.blade.php new file mode 100644 index 0000000..a89005e --- /dev/null +++ b/resources/views/vendor/pagination/simple-bootstrap-5.blade.php @@ -0,0 +1,29 @@ +@if ($paginator->hasPages()) + +@endif diff --git a/resources/views/vendor/pagination/simple-default.blade.php b/resources/views/vendor/pagination/simple-default.blade.php new file mode 100644 index 0000000..36bdbc1 --- /dev/null +++ b/resources/views/vendor/pagination/simple-default.blade.php @@ -0,0 +1,19 @@ +@if ($paginator->hasPages()) + +@endif diff --git a/resources/views/vendor/pagination/simple-tailwind.blade.php b/resources/views/vendor/pagination/simple-tailwind.blade.php new file mode 100644 index 0000000..ea02400 --- /dev/null +++ b/resources/views/vendor/pagination/simple-tailwind.blade.php @@ -0,0 +1,25 @@ +@if ($paginator->hasPages()) + +@endif diff --git a/resources/views/vendor/pagination/tailwind.blade.php b/resources/views/vendor/pagination/tailwind.blade.php new file mode 100644 index 0000000..941c275 --- /dev/null +++ b/resources/views/vendor/pagination/tailwind.blade.php @@ -0,0 +1,131 @@ +@if ($paginator->hasPages()) + +@endif + + + {{-- Prev --}} + @if ($paginator->onFirstPage()) + + + + @else + + @endif + + {{-- Numeri pagina: nascosti su mobile, visibili da sm in su --}} + + + {{-- Indicatore pagina mobile --}} + + {{ $paginator->currentPage() }} / {{ $paginator->lastPage() }} + + + {{-- Next --}} + @if ($paginator->hasMorePages()) + + @else + + + + @endif + +
+ +@endif diff --git a/routes/web.php b/routes/web.php index bd4104c..48f339e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,7 +2,9 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\Settings\TerritoryPdfImportController; +use App\Http\Controllers\Settings\XmlExchangeUploadController; use App\Http\Controllers\AssignmentPdfController; +use App\Http\Controllers\ShortPdfLinkController; use App\Http\Controllers\Auth\LoginController; use App\Livewire\Home; use App\Livewire\Territori\TerritorioIndex; @@ -48,6 +50,7 @@ Route::post('logout', function () { return redirect('/login'); })->middleware('auth')->name('logout'); +Route::get('p/{code}', ShortPdfLinkController::class)->name('assignments.pdf.short'); Route::get('assegnazioni/pdf/{assignment}/{code}', [AssignmentPdfController::class, 'viewer']) ->name('assignments.pdf.viewer'); Route::get('assegnazioni/pdf/{assignment}/{code}/file', [AssignmentPdfController::class, 'file']) @@ -114,6 +117,8 @@ Route::middleware('auth')->group(function () { Route::get('zone', ZoneIndex::class)->name('zone.index'); Route::get('tipologie', TipologieIndex::class)->name('tipologie.index'); Route::get('xml-exchange', XmlExchange::class)->name('xml.exchange'); + Route::post('xml-exchange/convert-sql', [XmlExchangeUploadController::class, 'convertSqlToXml'])->name('xml.convert-sql'); + Route::post('xml-exchange/import-xml', [XmlExchangeUploadController::class, 'importXml'])->name('xml.import-xml'); Route::post('imports/territori/pdf-zip', [TerritoryPdfImportController::class, 'storeZip'])->name('imports.territori.pdf-zip'); });