Files
termanager2/resources/views/assignments/pdf-viewer.blade.php
Francesco Picone 426a95211d Revisione blocchi 2 e 3: file protetti, ripristino atomico, import sicuri
Blocco 2
- Esiti delle azioni Livewire come avvisi (trait Avvisa) invece di flash
  che comparivano alla pagina dopo.
- PDF, miniature e versioni dei territori non più serviti da nginx su
  /storage: TerritorioFileController con login, permessi e cache privata.
- Ripristino tutto o niente: verifica dell'archivio, una transazione con
  import, piantine, blocco account e scambio dei file annullabile.
- Backup formato 2 con piantine, storico PDF e area KML.
- Posto unico preso in un passo (Cache::lock) e rispettato anche da
  import XML e PDF.
- Import PDF da cartella: abbinamento corretto (1-A, 12 B, ambigui),
  caricamento a lotti, file non PDF ignorati, PDF sostituito nello storico.
- L'import XML elimina i file dei territori che non usa più; comando
  territori:file-orfani per quelli accumulati prima.

Blocco 3
- Password attuale richiesta al cambio volontario; AuthenticateSession
  chiude le sessioni aperte altrove.
- Niente nomi dei proclamatori in chiaro nel registro attività.
- Export XML senza remember_token; chi riceve l'hash dal file deve
  cambiare password.
- Campagne: ultimo giorno incluso, percentuale sui territori distinti.
- Coda: retry_after 960 s, stop_grace_period 16m, Redis volatile-lru,
  impostazioni rilette a ogni job.
- Azzeramento che cancella anche piantine, area KML e anteprime.
- Pulsanti profilo e menu che non si nascondevano; icona del sito.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:01:41 +02:00

375 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5">
<title>Territorio {{ $assignment->territorio?->numero }}</title>
<link rel="icon" href="{{ asset('favicon.svg') }}" type="image/svg+xml">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.9.155/pdf_viewer.min.css" integrity="sha512-kMgaLfnBSAM0MFgr8fMDCMr2SYGQiMIFRbkBxRfFEqDqw/0hNh2GpcjYKjR0z4VoVVhYx1VlJdvfO1HCkhpg==" crossorigin="anonymous" referrerpolicy="no-referrer">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f1f5f9;
display: flex;
flex-direction: column;
overflow: hidden;
}
header {
background: #fff;
border-bottom: 1px solid #e2e8f0;
padding: 10px 16px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 16px;
z-index: 10;
}
.logo {
font-size: 15px;
font-weight: 700;
color: #4f46e5;
letter-spacing: -.3px;
flex: none;
}
.info {
display: flex;
flex-wrap: wrap;
gap: 4px 12px;
flex: 1;
}
.chip {
display: inline-flex;
align-items: center;
gap: 4px;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 3px 8px;
font-size: 12px;
color: #374151;
}
.chip .label {
font-size: 10px;
color: #94a3b8;
font-weight: 500;
text-transform: uppercase;
letter-spacing: .4px;
}
.chip .value {
font-weight: 600;
color: #1e293b;
}
.open-btn {
display: inline-flex;
align-items: center;
gap: 5px;
background: #4f46e5;
color: #fff;
text-decoration: none;
font-size: 12px;
font-weight: 600;
padding: 6px 12px;
border-radius: 6px;
flex: none;
transition: background .15s;
}
.open-btn:hover { background: #4338ca; }
/* Toolbar */
.pdf-toolbar {
background: #1e293b;
color: #fff;
padding: 6px 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 13px;
flex: none;
z-index: 10;
}
.pdf-toolbar button {
background: rgba(255,255,255,.1);
border: none;
color: #fff;
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background .15s;
font-size: 16px;
}
.pdf-toolbar button:hover { background: rgba(255,255,255,.2); }
.pdf-toolbar button:disabled { opacity: .3; cursor: default; }
.page-info {
font-variant-numeric: tabular-nums;
min-width: 80px;
text-align: center;
}
/* PDF viewport */
.pdf-viewport {
flex: 1 1 0;
min-height: 0;
overflow-y: auto;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
background: #64748b;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
gap: 12px;
}
.pdf-viewport canvas {
display: block;
max-width: 100%;
height: auto;
box-shadow: 0 2px 16px rgba(0,0,0,.25);
background: #fff;
}
/* Loading spinner */
.pdf-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 40px;
color: #fff;
font-size: 14px;
}
.spinner {
width: 36px;
height: 36px;
border: 3px solid rgba(255,255,255,.2);
border-top-color: #fff;
border-radius: 50%;
animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.pdf-error {
display: none;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 40px;
text-align: center;
color: #fff;
}
.pdf-error a {
background: #4f46e5;
color: #fff;
text-decoration: none;
font-weight: 600;
font-size: 14px;
padding: 10px 20px;
border-radius: 8px;
}
@media (max-width: 480px) {
header { padding: 8px 10px; }
.chip { font-size: 11px; padding: 2px 6px; }
.info { gap: 3px 8px; }
.pdf-toolbar { padding: 4px 8px; gap: 8px; }
.pdf-toolbar button { width: 28px; height: 28px; }
}
</style>
</head>
<body>
<header>
<span class="logo">TerManager2</span>
<div class="info">
<div class="chip">
<span class="label">Territorio</span>
<span class="value">N° {{ $assignment->territorio?->numero }}</span>
</div>
<div class="chip">
<span class="label">Assegnato a</span>
<span class="value">{{ $assignment->proclamatore?->nome_completo ?? '—' }}</span>
</div>
<div class="chip">
<span class="label">Data</span>
<span class="value">{{ $assignment->assigned_at->format('d/m/Y') }}</span>
</div>
<div class="chip">
<span class="label">Giorni assegnato</span>
<span class="value">{{ $assignment->giorni }}</span>
</div>
<div class="chip">
<span class="label">Consegna prevista</span>
<span class="value">{{ optional($assignment->data_consegna_presunta)->format('d/m/Y') ?? '—' }}</span>
</div>
@php($giorniScadenza = $assignment->giorni_alla_scadenza)
<div class="chip">
<span class="label">Scadenza</span>
<span class="value" @if($giorniScadenza !== null && $giorniScadenza < 0) style="color:#dc2626" @endif>
@if($giorniScadenza === null)
—
@elseif($giorniScadenza >= 0)
tra {{ $giorniScadenza }} {{ $giorniScadenza === 1 ? 'giorno' : 'giorni' }}
@else
scaduto da {{ abs($giorniScadenza) }} {{ abs($giorniScadenza) === 1 ? 'giorno' : 'giorni' }}
@endif
</span>
</div>
</div>
@if($showDownload)
<a href="{{ $pdfUrl }}" download class="open-btn">
<svg width="13" height="13" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5m0 0l5-5m-5 5V4"/></svg>
Scarica PDF
</a>
@endif
</header>
<div class="pdf-toolbar">
<button id="prevPage" title="Pagina precedente" disabled>‹</button>
<span class="page-info" id="pageInfo">—</span>
<button id="nextPage" title="Pagina successiva" disabled>›</button>
<button id="zoomOut" title="Riduci">−</button>
<button id="zoomIn" title="Ingrandisci">+</button>
</div>
<div class="pdf-viewport" id="pdfViewport">
<div class="pdf-loading" id="pdfLoading">
<div class="spinner"></div>
Caricamento PDF…
</div>
<div class="pdf-error" id="pdfError">
<svg width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/></svg>
<p>Impossibile caricare il PDF.</p>
@if($showDownload)
<a href="{{ $pdfUrl }}" download>Scarica PDF</a>
@endif
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.9.155/pdf.min.mjs" type="module"></script>
<script type="module">
import * as pdfjsLib from 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.9.155/pdf.min.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.9.155/pdf.worker.min.mjs';
const url = @json($pdfUrl);
const viewport = document.getElementById('pdfViewport');
const loading = document.getElementById('pdfLoading');
const error = document.getElementById('pdfError');
const pageInfo = document.getElementById('pageInfo');
const prevBtn = document.getElementById('prevPage');
const nextBtn = document.getElementById('nextPage');
const zoomIn = document.getElementById('zoomIn');
const zoomOut = document.getElementById('zoomOut');
let pdfDoc = null;
let scale = 1.5;
let rendering = false;
const canvases = [];
// Determine initial scale based on screen width
if (window.innerWidth <= 480) {
scale = 1.0;
} else if (window.innerWidth <= 768) {
scale = 1.2;
}
async function renderPage(pageNum) {
const page = await pdfDoc.getPage(pageNum);
const vp = page.getViewport({ scale });
// Reuse or create canvas
let canvas = canvases[pageNum - 1];
if (!canvas) {
canvas = document.createElement('canvas');
canvases[pageNum - 1] = canvas;
}
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.floor(vp.width * dpr);
canvas.height = Math.floor(vp.height * dpr);
canvas.style.width = Math.floor(vp.width) + 'px';
canvas.style.height = Math.floor(vp.height) + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
await page.render({ canvasContext: ctx, viewport: vp }).promise;
return canvas;
}
async function renderAllPages() {
if (rendering) return;
rendering = true;
// Clear viewport
viewport.querySelectorAll('canvas').forEach(c => c.remove());
for (let i = 1; i <= pdfDoc.numPages; i++) {
const canvas = await renderPage(i);
viewport.appendChild(canvas);
}
pageInfo.textContent = pdfDoc.numPages + (pdfDoc.numPages === 1 ? ' pagina' : ' pagine');
rendering = false;
}
async function init() {
try {
pdfDoc = await pdfjsLib.getDocument({ url, withCredentials: false }).promise;
loading.style.display = 'none';
prevBtn.disabled = true;
nextBtn.disabled = true;
// Render all pages as continuous scroll
await renderAllPages();
zoomIn.disabled = false;
zoomOut.disabled = false;
} catch (err) {
console.error('PDF load error:', err);
loading.style.display = 'none';
error.style.display = 'flex';
}
}
zoomIn.addEventListener('click', () => {
scale = Math.min(scale + 0.25, 4);
canvases.length = 0;
renderAllPages();
});
zoomOut.addEventListener('click', () => {
scale = Math.max(scale - 0.25, 0.5);
canvases.length = 0;
renderAllPages();
});
init();
</script>
</body>
</html>