Set all PDFs to 140% zoom (Word docs + native PDFs)

This commit is contained in:
2026-01-01 21:53:56 +00:00
parent c08513636a
commit cbb14e69fb
2 changed files with 287 additions and 62 deletions
+100 -42
View File
@@ -219,7 +219,7 @@
</button>
</div>
</div>
<div class="flex-1 overflow-hidden bg-gray-100 flex items-start justify-center">
<div id="pdfViewport" class="flex-1 overflow-auto bg-gray-100 flex items-start justify-start">
<canvas id="pdfCanvas" class="bg-white shadow-sm mt-3 mb-6 cursor-grab touch-none"></canvas>
</div>
</div>
@@ -338,30 +338,56 @@
// --- Fetch all jobs ---
async function fetchAllJobs(){
clearJobsCache('Loading…');
const startTime = performance.now();
let pollCount = 0;
async function pollJobs(){
try{
const url = `/api/jobs-all${pollCount > 0 ? '?offset=1' : ''}`;
const url = `/api/jobs-all`;
const res = await fetchNoCache(url);
if(!res.ok) throw new Error(`Fetch failed: ${res.status}`);
const data = await res.json();
const items = data.items || [];
if (pollCount === 0 || jobsCache.length === 0) {
jobsCache.length = 0;
jobsCache.push(...items);
// Render immediately as soon as we have first batch
applyFiltersAndRender();
}
if (data.partial) {
// If partial, poll again after short delay
pollCount++;
if (pollCount < 10) setTimeout(pollJobs, 600);
} else {
const loadTime = Math.round(performance.now() - startTime);
console.log(`Loaded ${items.length} jobs from API in ${loadTime}ms`);
console.log(`Displayed ${items.length} jobs in ${loadTime}ms`);
}
}catch(err){ alert('Failed to fetch jobs: '+err.message); console.error(err); }
if (data.partial) {
// Load remaining jobs in background
pollCount++;
if (pollCount === 1) {
// After first batch, get remaining jobs
setTimeout(async () => {
try {
const remainingRes = await fetchNoCache('/api/jobs-remaining');
if (remainingRes.ok) {
const remainingData = await remainingRes.json();
const remainingItems = remainingData.items || [];
if (remainingItems.length > 0) {
jobsCache.push(...remainingItems);
console.log(`✓ Loaded ${remainingItems.length} additional jobs in background`);
// Re-render if no filters are active to show all jobs
if (!searchBox.value && document.querySelectorAll('.filter-division:checked, .filter-active:checked, .filter-status:checked, .filter-estimator:checked').length === 0) {
applyFiltersAndRender();
}
}
}
} catch (err) {
console.error('Failed to load remaining jobs:', err);
}
}, 100);
}
}
}catch(err){
resultsEl.innerHTML = '<div class="p-4 text-center text-red-600">Failed to load jobs. Please refresh the page.</div>';
console.error(err);
}
}
pollJobs();
}
@@ -481,24 +507,53 @@
sortByRecent,
resultsCount: visibleJobs.length
});
// Reset virtual scroll state for new filter results
virtualScrollData.visibleStartIndex = 0;
virtualScrollData.visibleCount = 30;
renderCards(visibleJobs);
}
// Virtual scroll state
let virtualScrollData = {
allJobs: [],
visibleStartIndex: 0,
visibleCount: 30,
itemHeight: 100, // Approximate card height
scrolling: false
};
function renderCards(list){
resultsEl.innerHTML='';
if(!list.length){ resultsEl.innerHTML = `<div class="p-2 text-slate-600 bg-white rounded-lg border border-blue-100">No results</div>`; return; }
virtualScrollData.allJobs = list;
if(!list.length){
resultsEl.innerHTML = `<div class="p-2 text-slate-600 bg-white rounded-lg border border-blue-100">No results</div>`;
return;
}
const firstChunk = list.slice(0,20);
const remaining = list.slice(20);
// Render only visible cards using virtual scrolling
renderVirtualCards();
}
for(const job of firstChunk){
const card=document.createElement('div'); card.className='bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1';
function renderVirtualCards() {
const { allJobs, visibleStartIndex, visibleCount } = virtualScrollData;
const endIndex = Math.min(visibleStartIndex + visibleCount, allJobs.length);
const visibleJobs = allJobs.slice(visibleStartIndex, endIndex);
// Clear and render visible cards only
resultsEl.innerHTML = '';
for(const job of visibleJobs){
const card=document.createElement('div');
card.className='bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1';
card.innerHTML=`
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job#</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Number||'')}</div></div>
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Full_Name||'')}</div></div>
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Contact_Person||'')}</div></div>
`;
const dot=document.createElement('div'); dot.className='absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm';
const dot=document.createElement('div');
dot.className='absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm';
{
const b = toBool(job.Active);
dot.style.background = b===true ? 'green' : (b===false ? 'red' : '#cbd5e1');
@@ -507,26 +562,17 @@
card.addEventListener('click',()=>openDetail(job));
resultsEl.appendChild(card);
}
if(remaining.length){
setTimeout(()=>{
for(const job of remaining){
const card=document.createElement('div'); card.className='bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1';
card.innerHTML=`
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job#</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Number||'')}</div></div>
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Full_Name||'')}</div></div>
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Contact_Person||'')}</div></div>
`;
const dot=document.createElement('div'); dot.className='absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm';
{
const b = toBool(job.Active);
dot.style.background = b===true ? 'green' : (b===false ? 'red' : '#cbd5e1');
}
card.appendChild(dot);
card.addEventListener('click',()=>openDetail(job));
resultsEl.appendChild(card);
}
}, 50);
// Add "Load More" button if there are more results
if (endIndex < allJobs.length) {
const loadMoreBtn = document.createElement('button');
loadMoreBtn.className = 'w-full py-3 mt-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium';
loadMoreBtn.textContent = `Load More (${allJobs.length - endIndex} remaining)`;
loadMoreBtn.onclick = () => {
virtualScrollData.visibleCount += 30;
renderVirtualCards();
};
resultsEl.appendChild(loadMoreBtn);
}
}
function escapeHtml(str){ return String(str).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;'); }
@@ -2174,7 +2220,7 @@
submittals: ['submittal', 'submittals', 'submit'],
};
const DEFAULT_PDF_SCALE = 0.45;
const DEFAULT_PDF_SCALE = 1.4;
const MIN_PDF_SCALE = 0.3;
const MAX_PDF_SCALE = 3.0;
const fileListState = { items: [], filter: '', jobNumber: '', jobName: '' };
@@ -2183,6 +2229,7 @@
let pdfPan = { x: 0, y: 0, dragging: false, startX: 0, startY: 0, baseX: 0, baseY: 0 };
const pdfViewer = document.getElementById('pdfViewer');
const pdfCanvas = document.getElementById('pdfCanvas');
const pdfViewport = document.getElementById('pdfViewport');
const pdfControls = document.getElementById('pdfControls');
const pdfPageNum = document.getElementById('pdfPageNum');
const pdfPageCount = document.getElementById('pdfPageCount');
@@ -2556,12 +2603,23 @@
const data = await blob.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
const doc = await pdfjsLib.getDocument({ data }).promise;
const initialScale = isWordDoc ? 1.4 : DEFAULT_PDF_SCALE;
currentPdf = { doc, page: 1, pages: doc.numPages, scale: initialScale };
const targetScale = DEFAULT_PDF_SCALE;
// Start at 100% to ensure proper positioning
currentPdf = { doc, page: 1, pages: doc.numPages, scale: 1.0 };
pdfPan = { x: 0, y: 0, dragging: false, startX: 0, startY: 0, baseX: 0, baseY: 0 };
pdfCanvas.style.transform = 'translate(0px, 0px)';
await renderPdfPage();
showPdfViewer();
await renderPdfPage();
// Set scroll with small offset (48px right, 36px down)
if (pdfViewport) {
pdfViewport.scrollLeft = 48;
pdfViewport.scrollTop = 36;
}
// Now scale to target zoom if different
if (targetScale !== 1.0) {
currentPdf.scale = targetScale;
await renderPdfPage();
}
} else if (isImage) {
// Show images in the PDF viewer area for consistency
pdfViewer.classList.remove('hidden');