fixed viewer and folder issues
This commit is contained in:
+57
-5
@@ -185,9 +185,56 @@ const fetchJson = async (url: string, token: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resolveShareLink = async (link: string, token: string) => {
|
const resolveShareLink = async (link: string, token: string) => {
|
||||||
const encoded = `u!${b64Url(link)}`;
|
// Decode the link in case it comes pre-encoded from the database
|
||||||
const data = await fetchJson(`https://graph.microsoft.com/v1.0/shares/${encoded}/driveItem`, token);
|
const decodedLink = decodeURIComponent(link);
|
||||||
return { driveId: data.parentReference?.driveId as string, itemId: data.id as string };
|
console.log('[resolveShareLink] Decoded link:', decodedLink);
|
||||||
|
|
||||||
|
// Check if this is a short sharing link (/:f:/ or /:b:/) or a direct document library URL
|
||||||
|
if (decodedLink.includes('/:f:/') || decodedLink.includes('/:b:/')) {
|
||||||
|
// Short sharing link - use shares API
|
||||||
|
const encoded = `u!${b64Url(decodedLink)}`;
|
||||||
|
console.log('[resolveShareLink] Using shares API, encoded:', encoded);
|
||||||
|
const data = await fetchJson(`https://graph.microsoft.com/v1.0/shares/${encoded}/driveItem`, token);
|
||||||
|
return { driveId: data.parentReference?.driveId as string, itemId: data.id as string };
|
||||||
|
} else {
|
||||||
|
// Direct document library URL - parse it and use drive API
|
||||||
|
// Example: https://czflex.sharepoint.com/sites/Team/Shared Documents/Forms/AllItems.aspx?RootFolder=/sites/Team/Shared Documents/General/...
|
||||||
|
const url = new URL(decodedLink);
|
||||||
|
const pathMatch = url.hostname.match(/^([^.]+)\.sharepoint\.com$/);
|
||||||
|
if (!pathMatch) throw new Error('Invalid SharePoint URL');
|
||||||
|
|
||||||
|
const hostname = pathMatch[1]; // e.g., 'czflex'
|
||||||
|
const sitePath = url.pathname.split('/Shared')[0]; // e.g., /sites/Team
|
||||||
|
const rootFolderParam = url.searchParams.get('RootFolder');
|
||||||
|
if (!rootFolderParam) throw new Error('RootFolder parameter missing');
|
||||||
|
|
||||||
|
// Extract the folder path relative to the document library
|
||||||
|
// RootFolder format: /sites/Team/Shared Documents/General/Operations [Server]/...
|
||||||
|
// We need: General/Operations [Server]/...
|
||||||
|
const folderPath = rootFolderParam.split('/Shared Documents/')[1];
|
||||||
|
if (!folderPath) throw new Error('Could not parse folder path');
|
||||||
|
|
||||||
|
console.log('[resolveShareLink] Hostname:', hostname);
|
||||||
|
console.log('[resolveShareLink] Site path:', sitePath);
|
||||||
|
console.log('[resolveShareLink] Folder path:', folderPath);
|
||||||
|
|
||||||
|
// Get the site ID first using hostname:sitePath format
|
||||||
|
const siteData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${hostname}.sharepoint.com:${sitePath}`, token);
|
||||||
|
const siteId = siteData.id;
|
||||||
|
|
||||||
|
// Get the default document library drive
|
||||||
|
const driveData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${siteId}/drive`, token);
|
||||||
|
const driveId = driveData.id;
|
||||||
|
|
||||||
|
// Get the folder item by path
|
||||||
|
const itemData = await fetchJson(
|
||||||
|
`https://graph.microsoft.com/v1.0/drives/${driveId}/root:/${folderPath}`,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('[resolveShareLink] Resolved to driveId:', driveId, 'itemId:', itemData.id);
|
||||||
|
return { driveId, itemId: itemData.id };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const listChildrenRecursive = async (
|
const listChildrenRecursive = async (
|
||||||
@@ -225,9 +272,12 @@ const filterByCategory = (items: DriveItem[], category?: string, pdfOnly: boolea
|
|||||||
if (i.folder) return false;
|
if (i.folder) return false;
|
||||||
const name = i.name.toLowerCase();
|
const name = i.name.toLowerCase();
|
||||||
const mimeType = i.file?.mimeType?.toLowerCase() || '';
|
const mimeType = i.file?.mimeType?.toLowerCase() || '';
|
||||||
// Include PDFs and Word documents
|
// Include PDFs, Word documents, and images
|
||||||
return name.endsWith('.pdf') || mimeType.includes('pdf') ||
|
return name.endsWith('.pdf') || mimeType.includes('pdf') ||
|
||||||
name.endsWith('.doc') || name.endsWith('.docx') || mimeType.includes('word');
|
name.endsWith('.doc') || name.endsWith('.docx') || mimeType.includes('word') ||
|
||||||
|
name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') ||
|
||||||
|
name.endsWith('.gif') || name.endsWith('.bmp') || name.endsWith('.tiff') ||
|
||||||
|
name.endsWith('.webp') || mimeType.includes('image');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +324,7 @@ app.get('/api/job-files', async (c) => {
|
|||||||
// Filter to show only PDFs by default (can be disabled with pdfOnly=false query param)
|
// Filter to show only PDFs by default (can be disabled with pdfOnly=false query param)
|
||||||
const pdfOnly = c.req.query('pdfOnly') !== 'false';
|
const pdfOnly = c.req.query('pdfOnly') !== 'false';
|
||||||
const filtered = filterByCategory(items, category, pdfOnly);
|
const filtered = filterByCategory(items, category, pdfOnly);
|
||||||
|
console.log(`[job-files] Found ${items.length} total items, ${filtered.length} after filtering`);
|
||||||
|
|
||||||
const mapped = filtered.map((i) => ({
|
const mapped = filtered.map((i) => ({
|
||||||
id: i.id,
|
id: i.id,
|
||||||
@@ -291,6 +342,7 @@ app.get('/api/job-files', async (c) => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = (err as Error)?.message || String(err);
|
const message = (err as Error)?.message || String(err);
|
||||||
const status = (err as any)?.status || 500;
|
const status = (err as any)?.status || 500;
|
||||||
|
console.error('[job-files] ERROR:', message);
|
||||||
logLine('logs/error.log', `/api/job-files error: ${message}`);
|
logLine('logs/error.log', `/api/job-files error: ${message}`);
|
||||||
return c.json({ error: message }, status);
|
return c.json({ error: message }, status);
|
||||||
}
|
}
|
||||||
|
|||||||
+183
-26
@@ -67,7 +67,7 @@
|
|||||||
<div class="relative mb-2">
|
<div class="relative mb-2">
|
||||||
<input id="searchBox" placeholder="Search jobs (just start typing or use voice)" class="w-full p-2.5 pr-12 rounded-lg border border-gray-300 text-lg placeholder:text-gray-400">
|
<input id="searchBox" placeholder="Search jobs (just start typing or use voice)" class="w-full p-2.5 pr-12 rounded-lg border border-gray-300 text-lg placeholder:text-gray-400">
|
||||||
<button id="voiceSearchBtn" class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full flex items-center justify-center hover:bg-gray-100 transition-colors" title="Voice search">
|
<button id="voiceSearchBtn" class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full flex items-center justify-center hover:bg-gray-100 transition-colors" title="Voice search">
|
||||||
<svg id="micIcon" class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg id="micIcon" class="w-5 h-5 text-gray-600 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<svg id="micRecordingIcon" class="hidden w-5 h-5 text-red-600 animate-pulse" fill="currentColor" viewBox="0 0 24 24">
|
<svg id="micRecordingIcon" class="hidden w-5 h-5 text-red-600 animate-pulse" fill="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -374,8 +374,17 @@
|
|||||||
// Load version from backend /version endpoint so UI matches package.json
|
// Load version from backend /version endpoint so UI matches package.json
|
||||||
async function loadVersion(){
|
async function loadVersion(){
|
||||||
if(!versionLabel) return;
|
if(!versionLabel) return;
|
||||||
// Display "Redis" for the Redis implementation branch
|
try{
|
||||||
versionLabel.textContent = 'Redis';
|
const res = await fetch('/version');
|
||||||
|
if(!res.ok) {
|
||||||
|
console.warn(`Version endpoint returned ${res.status}. Using default version.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json().catch(()=>null);
|
||||||
|
if(data?.version) versionLabel.textContent = `v${data.version}`;
|
||||||
|
}catch(err){
|
||||||
|
console.debug('Version load failed (non-critical):', err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
loadVersion();
|
loadVersion();
|
||||||
|
|
||||||
@@ -478,7 +487,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyFiltersAndRender(){
|
function applyFiltersAndRender(){
|
||||||
const q = safeLower(searchBox.value||'');
|
let q = safeLower(searchBox.value||'');
|
||||||
|
|
||||||
|
// Check for recency keywords
|
||||||
|
const recencyKeywords = ['most recent', 'newest', 'latest', 'current', 'recent'];
|
||||||
|
let sortByRecent = false;
|
||||||
|
|
||||||
|
// Check if query contains any recency keywords (check longer phrases first)
|
||||||
|
for (const keyword of recencyKeywords) {
|
||||||
|
if (q.includes(keyword)) {
|
||||||
|
sortByRecent = true;
|
||||||
|
// Remove the keyword from search query (use global replace)
|
||||||
|
const regex = new RegExp(keyword, 'gi');
|
||||||
|
q = q.replace(regex, ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up multiple spaces and trim
|
||||||
|
q = q.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
// Update the display to show cleaned query
|
||||||
|
if (sortByRecent && searchBox.value !== q) {
|
||||||
|
searchBox.value = q.split(' ').map(word =>
|
||||||
|
word.charAt(0).toUpperCase() + word.slice(1)
|
||||||
|
).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
const divisionVals = Array.from(document.querySelectorAll('.filter-division:checked')).map(i=>i.value);
|
const divisionVals = Array.from(document.querySelectorAll('.filter-division:checked')).map(i=>i.value);
|
||||||
const activeVals = Array.from(document.querySelectorAll('.filter-active:checked')).map(i=>i.value);
|
const activeVals = Array.from(document.querySelectorAll('.filter-active:checked')).map(i=>i.value);
|
||||||
const activeWanted = activeVals.map(v=>toBool(v)).filter(v=>v!==null);
|
const activeWanted = activeVals.map(v=>toBool(v)).filter(v=>v!==null);
|
||||||
@@ -503,11 +537,31 @@
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Data already sorted from PocketBase, only re-sort if filters applied
|
// If recency keyword used, show only THE most recent match
|
||||||
if(q || divisionVals.length || activeVals.length || statusVals.length || estimatorVals.length){
|
if(sortByRecent && visibleJobs.length > 0){
|
||||||
|
console.log('Before sorting, visibleJobs count:', visibleJobs.length);
|
||||||
|
|
||||||
|
// Sort by Job_Number (higher number = newer job)
|
||||||
|
visibleJobs.sort((a, b) => {
|
||||||
|
const numA = jobNumberValue(a) || 0;
|
||||||
|
const numB = jobNumberValue(b) || 0;
|
||||||
|
return numB - numA; // Higher job number first (newest)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('After sorting, newest job:', visibleJobs[0]?.Job_Number);
|
||||||
|
// Keep only the most recent
|
||||||
|
visibleJobs = visibleJobs.slice(0, 1);
|
||||||
|
console.log('✓ Showing only the most recent match');
|
||||||
|
} else if(q || divisionVals.length || activeVals.length || statusVals.length || estimatorVals.length){
|
||||||
sortJobsDescending(visibleJobs);
|
sortJobsDescending(visibleJobs);
|
||||||
}
|
}
|
||||||
console.log('Filter summary', { q, divisionVals, activeWanted, statusVals, estimatorValsLower, total: jobsCache.length, visible: visibleJobs.length });
|
|
||||||
|
console.log('Filter summary', {
|
||||||
|
originalSearch: searchBox.value,
|
||||||
|
cleanedQ: q,
|
||||||
|
sortByRecent,
|
||||||
|
resultsCount: visibleJobs.length
|
||||||
|
});
|
||||||
renderCards(visibleJobs);
|
renderCards(visibleJobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,9 +654,17 @@
|
|||||||
|
|
||||||
let autoStopTimer = null;
|
let autoStopTimer = null;
|
||||||
let silenceTimer = null;
|
let silenceTimer = null;
|
||||||
|
let lastResultIndex = 0;
|
||||||
|
let baseSearchText = '';
|
||||||
|
|
||||||
recognition.onstart = () => {
|
recognition.onstart = () => {
|
||||||
isListening = true;
|
isListening = true;
|
||||||
|
lastResultIndex = 0;
|
||||||
|
baseSearchText = searchBox.value.trim();
|
||||||
|
|
||||||
|
// Change mic icon to filled maroon/red
|
||||||
|
micIcon.style.fill = '#991b1b'; // Tailwind red-800
|
||||||
|
micIcon.style.stroke = '#991b1b';
|
||||||
|
|
||||||
// Auto-stop after 10 seconds as a safety measure
|
// Auto-stop after 10 seconds as a safety measure
|
||||||
autoStopTimer = setTimeout(() => {
|
autoStopTimer = setTimeout(() => {
|
||||||
@@ -619,17 +681,40 @@
|
|||||||
silenceTimer = null;
|
silenceTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const last = event.results.length - 1;
|
// Process only new final results
|
||||||
const transcript = event.results[last][0].transcript;
|
for (let i = lastResultIndex; i < event.results.length; i++) {
|
||||||
const isFinal = event.results[last].isFinal;
|
if (event.results[i].isFinal) {
|
||||||
|
let transcript = event.results[i][0].transcript.trim();
|
||||||
|
|
||||||
searchBox.value = transcript.trim();
|
// Remove trailing punctuation (period, comma, etc.)
|
||||||
|
transcript = transcript.replace(/[.,!?;:]+$/, '');
|
||||||
|
|
||||||
|
// Capitalize each word (title case)
|
||||||
|
transcript = transcript.split(' ').map(word =>
|
||||||
|
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
||||||
|
).join(' ');
|
||||||
|
|
||||||
|
if (transcript) {
|
||||||
|
// Add space if base text exists and doesn't end with space
|
||||||
|
if (baseSearchText && !baseSearchText.endsWith(' ')) {
|
||||||
|
baseSearchText += ' ';
|
||||||
|
}
|
||||||
|
baseSearchText += transcript;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastResultIndex = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update search box with accumulated text
|
||||||
|
searchBox.value = baseSearchText;
|
||||||
|
|
||||||
// Trigger search with results
|
// Trigger search with results
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
|
|
||||||
// Stop after final result with short delay
|
// Check if all results are final, if so prepare to stop
|
||||||
if (isFinal) {
|
const allFinal = Array.from(event.results).every(r => r.isFinal);
|
||||||
|
if (allFinal && event.results.length > 0) {
|
||||||
silenceTimer = setTimeout(() => {
|
silenceTimer = setTimeout(() => {
|
||||||
recognition.stop();
|
recognition.stop();
|
||||||
}, 1500);
|
}, 1500);
|
||||||
@@ -663,6 +748,10 @@
|
|||||||
if (autoStopTimer) clearTimeout(autoStopTimer);
|
if (autoStopTimer) clearTimeout(autoStopTimer);
|
||||||
if (silenceTimer) clearTimeout(silenceTimer);
|
if (silenceTimer) clearTimeout(silenceTimer);
|
||||||
|
|
||||||
|
// Reset mic icon to normal
|
||||||
|
micIcon.style.fill = 'none';
|
||||||
|
micIcon.style.stroke = '';
|
||||||
|
|
||||||
isListening = false;
|
isListening = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1310,7 +1399,11 @@
|
|||||||
window.openFileInViewer = openFileInViewer;
|
window.openFileInViewer = openFileInViewer;
|
||||||
|
|
||||||
async function renderPdfPage() {
|
async function renderPdfPage() {
|
||||||
const { doc, page, scale } = currentPdf;
|
const { doc, page, scale, isImage } = currentPdf;
|
||||||
|
if (isImage) {
|
||||||
|
renderImageOnCanvas();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!doc) return;
|
if (!doc) return;
|
||||||
const pageObj = await doc.getPage(page);
|
const pageObj = await doc.getPage(page);
|
||||||
const viewport = pageObj.getViewport({ scale });
|
const viewport = pageObj.getViewport({ scale });
|
||||||
@@ -1328,10 +1421,33 @@
|
|||||||
pdfZoomReset.textContent = `${Math.round(currentPdf.scale * 100)}%`;
|
pdfZoomReset.textContent = `${Math.round(currentPdf.scale * 100)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderImageOnCanvas() {
|
||||||
|
const { scale, imageElement, imageWidth, imageHeight } = currentPdf;
|
||||||
|
if (!imageElement) return;
|
||||||
|
const canvas = pdfCanvas;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
const scaledWidth = imageWidth * scale;
|
||||||
|
const scaledHeight = imageHeight * scale;
|
||||||
|
canvas.width = scaledWidth;
|
||||||
|
canvas.height = scaledHeight;
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
context.drawImage(imageElement, 0, 0, scaledWidth, scaledHeight);
|
||||||
|
// re-apply pan transform after render
|
||||||
|
pdfCanvas.style.transform = `translate(${pdfPan.x}px, ${pdfPan.y}px)`;
|
||||||
|
pdfZoomLabel.textContent = `${Math.round(scale * 100)}%`;
|
||||||
|
pdfZoomReset.textContent = `${Math.round(scale * 100)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
function showPdfViewer() {
|
function showPdfViewer() {
|
||||||
iframeViewer.classList.add('hidden');
|
iframeViewer.classList.add('hidden');
|
||||||
pdfViewer.classList.remove('hidden');
|
pdfViewer.classList.remove('hidden');
|
||||||
iframeLoader.classList.add('hidden');
|
iframeLoader.classList.add('hidden');
|
||||||
|
// Ensure PDF controls are visible (might be hidden for images)
|
||||||
|
pdfControls.style.display = '';
|
||||||
|
// Show page navigation for PDFs
|
||||||
|
pdfPrev.parentElement.style.display = '';
|
||||||
|
pdfNext.parentElement.style.display = '';
|
||||||
|
pdfPageNum.parentElement.style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfPrev.addEventListener('click', async () => {
|
pdfPrev.addEventListener('click', async () => {
|
||||||
@@ -1347,18 +1463,18 @@
|
|||||||
await renderPdfPage();
|
await renderPdfPage();
|
||||||
});
|
});
|
||||||
pdfZoomIn.addEventListener('click', async () => {
|
pdfZoomIn.addEventListener('click', async () => {
|
||||||
if (!currentPdf.doc) return;
|
if (!currentPdf.doc && !currentPdf.isImage) return;
|
||||||
currentPdf.scale = Math.min(currentPdf.scale + 0.1, MAX_PDF_SCALE);
|
currentPdf.scale = Math.min(currentPdf.scale + 0.1, MAX_PDF_SCALE);
|
||||||
await renderPdfPage();
|
await renderPdfPage();
|
||||||
});
|
});
|
||||||
pdfZoomOut.addEventListener('click', async () => {
|
pdfZoomOut.addEventListener('click', async () => {
|
||||||
if (!currentPdf.doc) return;
|
if (!currentPdf.doc && !currentPdf.isImage) return;
|
||||||
currentPdf.scale = Math.max(currentPdf.scale - 0.1, MIN_PDF_SCALE);
|
currentPdf.scale = Math.max(currentPdf.scale - 0.1, MIN_PDF_SCALE);
|
||||||
await renderPdfPage();
|
await renderPdfPage();
|
||||||
});
|
});
|
||||||
pdfZoomReset.addEventListener('click', async () => {
|
pdfZoomReset.addEventListener('click', async () => {
|
||||||
if (!currentPdf.doc) return;
|
if (!currentPdf.doc && !currentPdf.isImage) return;
|
||||||
currentPdf.scale = DEFAULT_PDF_SCALE;
|
currentPdf.scale = currentPdf.isImage ? 1 : DEFAULT_PDF_SCALE;
|
||||||
pdfPan = { x: 0, y: 0, dragging: false, startX: 0, startY: 0, baseX: 0, baseY: 0 };
|
pdfPan = { x: 0, y: 0, dragging: false, startX: 0, startY: 0, baseX: 0, baseY: 0 };
|
||||||
pdfCanvas.style.transform = 'translate(0px, 0px)';
|
pdfCanvas.style.transform = 'translate(0px, 0px)';
|
||||||
await renderPdfPage();
|
await renderPdfPage();
|
||||||
@@ -1383,7 +1499,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
pdfCanvas.addEventListener('wheel', async (e) => {
|
pdfCanvas.addEventListener('wheel', async (e) => {
|
||||||
if (!currentPdf.doc) return;
|
if (!currentPdf.doc && !currentPdf.isImage) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const delta = e.deltaY;
|
const delta = e.deltaY;
|
||||||
const step = 0.1;
|
const step = 0.1;
|
||||||
@@ -1403,7 +1519,7 @@
|
|||||||
fileListContainer.classList.remove('hidden');
|
fileListContainer.classList.remove('hidden');
|
||||||
});
|
});
|
||||||
pdfCanvas.addEventListener('pointerdown', (e) => {
|
pdfCanvas.addEventListener('pointerdown', (e) => {
|
||||||
if (!currentPdf.doc) return;
|
if (!currentPdf.doc && !currentPdf.isImage) return;
|
||||||
activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||||
|
|
||||||
if (activePointers.size === 2) {
|
if (activePointers.size === 2) {
|
||||||
@@ -1423,7 +1539,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
pdfCanvas.addEventListener('pointermove', (e) => {
|
pdfCanvas.addEventListener('pointermove', (e) => {
|
||||||
if (!currentPdf.doc) return;
|
if (!currentPdf.doc && !currentPdf.isImage) return;
|
||||||
|
|
||||||
if (activePointers.has(e.pointerId)) {
|
if (activePointers.has(e.pointerId)) {
|
||||||
activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||||
@@ -1523,6 +1639,9 @@
|
|||||||
|
|
||||||
// Word docs are converted to PDF, so treat them as PDFs
|
// Word docs are converted to PDF, so treat them as PDFs
|
||||||
const isPdf = isWordDoc || (contentType || '').toLowerCase().includes('pdf') || (name || '').toLowerCase().endsWith('.pdf');
|
const isPdf = isWordDoc || (contentType || '').toLowerCase().includes('pdf') || (name || '').toLowerCase().endsWith('.pdf');
|
||||||
|
const isImage = (contentType || '').toLowerCase().includes('image') ||
|
||||||
|
/\.(jpg|jpeg|png|gif|bmp|tiff|webp)$/i.test(name || '');
|
||||||
|
|
||||||
if (isPdf && window['pdfjsLib']) {
|
if (isPdf && window['pdfjsLib']) {
|
||||||
const data = await blob.arrayBuffer();
|
const data = await blob.arrayBuffer();
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||||||
@@ -1533,8 +1652,39 @@
|
|||||||
pdfCanvas.style.transform = 'translate(0px, 0px)';
|
pdfCanvas.style.transform = 'translate(0px, 0px)';
|
||||||
await renderPdfPage();
|
await renderPdfPage();
|
||||||
showPdfViewer();
|
showPdfViewer();
|
||||||
|
} else if (isImage) {
|
||||||
|
// Show images in the PDF viewer area for consistency
|
||||||
|
pdfViewer.classList.remove('hidden');
|
||||||
|
iframeViewer.classList.add('hidden');
|
||||||
|
iframeLoader.classList.add('hidden');
|
||||||
|
|
||||||
|
// Store original image for re-rendering at different scales
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
currentPdf = {
|
||||||
|
doc: null,
|
||||||
|
page: 1,
|
||||||
|
pages: 1,
|
||||||
|
scale: 1,
|
||||||
|
isImage: true,
|
||||||
|
imageElement: img,
|
||||||
|
imageWidth: img.width,
|
||||||
|
imageHeight: img.height
|
||||||
|
};
|
||||||
|
pdfPan = { x: 0, y: 0, dragging: false, startX: 0, startY: 0, baseX: 0, baseY: 0 };
|
||||||
|
pdfCanvas.style.transform = 'translate(0px, 0px)';
|
||||||
|
renderImageOnCanvas();
|
||||||
|
|
||||||
|
// Show PDF controls (zoom works for images too)
|
||||||
|
pdfControls.style.display = '';
|
||||||
|
// Hide page navigation for images
|
||||||
|
pdfPrev.parentElement.style.display = 'none';
|
||||||
|
pdfNext.parentElement.style.display = 'none';
|
||||||
|
pdfPageNum.parentElement.style.display = 'none';
|
||||||
|
};
|
||||||
|
img.src = currentPreviewUrl;
|
||||||
} else {
|
} else {
|
||||||
// fallback to iframe blob view (images, etc.)
|
// fallback to iframe blob view
|
||||||
iframeViewer.src = 'about:blank';
|
iframeViewer.src = 'about:blank';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
iframeViewer.src = currentPreviewUrl;
|
iframeViewer.src = currentPreviewUrl;
|
||||||
@@ -1554,13 +1704,16 @@
|
|||||||
const term = (fileListState.filter || '').toLowerCase().trim();
|
const term = (fileListState.filter || '').toLowerCase().trim();
|
||||||
const filtered = fileListState.items
|
const filtered = fileListState.items
|
||||||
.filter((f) => {
|
.filter((f) => {
|
||||||
// Show PDF files and Word documents (which will be converted to PDF)
|
// Show PDF files, Word documents (which will be converted to PDF), and images
|
||||||
if (f.isFolder) return false;
|
if (f.isFolder) return false;
|
||||||
const name = f.name.toLowerCase();
|
const name = f.name.toLowerCase();
|
||||||
const contentType = (f.contentType || '').toLowerCase();
|
const contentType = (f.contentType || '').toLowerCase();
|
||||||
const isPdfOrWord = name.endsWith('.pdf') || contentType.includes('pdf') ||
|
const isPdfOrWord = name.endsWith('.pdf') || contentType.includes('pdf') ||
|
||||||
name.endsWith('.doc') || name.endsWith('.docx') || contentType.includes('word');
|
name.endsWith('.doc') || name.endsWith('.docx') || contentType.includes('word');
|
||||||
if (!isPdfOrWord) return false;
|
const isImage = name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') ||
|
||||||
|
name.endsWith('.gif') || name.endsWith('.bmp') || name.endsWith('.tiff') ||
|
||||||
|
name.endsWith('.webp') || contentType.includes('image');
|
||||||
|
if (!isPdfOrWord && !isImage) return false;
|
||||||
// Apply search term filter if present
|
// Apply search term filter if present
|
||||||
return name.includes(term);
|
return name.includes(term);
|
||||||
});
|
});
|
||||||
@@ -1665,6 +1818,7 @@
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
console.log('Using cached files for job', job.Job_Number);
|
console.log('Using cached files for job', job.Job_Number);
|
||||||
fileListState.items = cached.items;
|
fileListState.items = cached.items;
|
||||||
|
iframeLoader.classList.add('hidden');
|
||||||
renderFileGroups(link);
|
renderFileGroups(link);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1683,7 +1837,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 15000);
|
const timeout = setTimeout(() => controller.abort(), 30000); // 30 seconds for large folders
|
||||||
let resp = await fetch(`/api/job-files?link=${encodeURIComponent(link)}`, { headers, signal: controller.signal });
|
let resp = await fetch(`/api/job-files?link=${encodeURIComponent(link)}`, { headers, signal: controller.signal });
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|
||||||
@@ -1713,10 +1867,11 @@
|
|||||||
fileCache.set(link, { items, driveId: rootDriveId });
|
fileCache.set(link, { items, driveId: rootDriveId });
|
||||||
fileListState.items = items;
|
fileListState.items = items;
|
||||||
|
|
||||||
|
console.log(`✓ Loaded ${items.length} files for job ${job.Job_Number}`);
|
||||||
iframeLoader.classList.add('hidden');
|
iframeLoader.classList.add('hidden');
|
||||||
renderFileGroups(link);
|
renderFileGroups(link);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('job-files error', err);
|
console.error('❌ job-files error for', job.Job_Number, err);
|
||||||
const authExpired = err?.message === 'AUTH_EXPIRED' || String(err || '').includes('401');
|
const authExpired = err?.message === 'AUTH_EXPIRED' || String(err || '').includes('401');
|
||||||
const timedOut = err?.name === 'AbortError';
|
const timedOut = err?.name === 'AbortError';
|
||||||
const friendly = authExpired
|
const friendly = authExpired
|
||||||
@@ -1786,6 +1941,8 @@
|
|||||||
URL.revokeObjectURL(currentPreviewUrl);
|
URL.revokeObjectURL(currentPreviewUrl);
|
||||||
currentPreviewUrl = null;
|
currentPreviewUrl = null;
|
||||||
}
|
}
|
||||||
|
// Clear file cache when closing to ensure fresh data on next open
|
||||||
|
fileCache.clear();
|
||||||
// Clear iframe src to stop any loading/video/audio and prevent stacking
|
// Clear iframe src to stop any loading/video/audio and prevent stacking
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
iframeViewer.src = 'about:blank';
|
iframeViewer.src = 'about:blank';
|
||||||
|
|||||||
Reference in New Issue
Block a user