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'); });