353 lines
11 KiB
PHP
353 lines
11 KiB
PHP
<!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="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>
|
||
|
||
@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>
|