From 8982011155d587fd6f5953b6ed083fe4496babeb Mon Sep 17 00:00:00 2001 From: Josh-Heaps Date: Thu, 12 Feb 2026 09:53:25 -0700 Subject: [PATCH] Add document viewer --- Media.JoshHeaps.Net/Pages/MedicalDocs.cshtml | 12 ++ .../wwwroot/css/medical-docs.css | 142 ++++++++++++++++++ .../wwwroot/js/medical-docs/documents.js | 91 +++++++++++ 3 files changed, 245 insertions(+) diff --git a/Media.JoshHeaps.Net/Pages/MedicalDocs.cshtml b/Media.JoshHeaps.Net/Pages/MedicalDocs.cshtml index 81b0c5b..abd2830 100644 --- a/Media.JoshHeaps.Net/Pages/MedicalDocs.cshtml +++ b/Media.JoshHeaps.Net/Pages/MedicalDocs.cshtml @@ -273,6 +273,18 @@ + + + @section Scripts { diff --git a/Media.JoshHeaps.Net/wwwroot/css/medical-docs.css b/Media.JoshHeaps.Net/wwwroot/css/medical-docs.css index b71b9d5..189a697 100644 --- a/Media.JoshHeaps.Net/wwwroot/css/medical-docs.css +++ b/Media.JoshHeaps.Net/wwwroot/css/medical-docs.css @@ -1369,6 +1369,131 @@ line-height: 1.6; } +/* ======================== */ +/* Document Viewer Modal */ +/* ======================== */ + +.doc-viewer-overlay { + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.doc-viewer-modal { + background: var(--bg-primary); + border: 1px solid var(--border-primary); + border-radius: 12px; + display: flex; + flex-direction: column; + max-width: 90vw; + max-height: 90vh; + width: auto; + min-width: 300px; + overflow: hidden; +} + +.doc-viewer-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--border-primary); + flex-shrink: 0; +} + +.doc-viewer-title { + font-size: 14px; + font-weight: 500; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.doc-viewer-close { + background: none; + border: none; + color: var(--text-secondary); + font-size: 24px; + line-height: 1; + cursor: pointer; + padding: 0 4px; + flex-shrink: 0; + margin-left: 12px; + transition: color 0.2s ease; +} + +.doc-viewer-close:hover { + color: var(--text-primary); +} + +.doc-viewer-body { + padding: 16px; + overflow: auto; + display: flex; + align-items: center; + justify-content: center; + flex: 1; + min-height: 0; +} + +.doc-viewer-loading, +.doc-viewer-error { + color: var(--text-secondary); + font-size: 14px; + padding: 40px 20px; + text-align: center; +} + +.doc-viewer-error { + color: var(--danger); +} + +.doc-viewer-image { + max-width: 100%; + max-height: calc(90vh - 80px); + object-fit: contain; + border-radius: 4px; +} + +.doc-viewer-audio { + width: 100%; + min-width: 300px; + max-width: 500px; +} + +.doc-viewer-pdf { + width: 80vw; + height: calc(90vh - 80px); + max-width: 100%; + border: none; + border-radius: 4px; +} + +.doc-viewer-text { + font-family: inherit; + font-size: 14px; + color: var(--text-primary); + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 6px; + padding: 16px; + max-height: calc(90vh - 120px); + overflow-y: auto; + white-space: pre-wrap; + word-wrap: break-word; + width: 100%; + margin: 0; + line-height: 1.6; +} + /* ======================== */ /* Responsive (<=768px) */ /* ======================== */ @@ -1532,6 +1657,23 @@ .doc-detail-row-inline > div[style*="grid-column"] { grid-column: auto !important; } + + .doc-viewer-overlay { + padding: 0; + } + + .doc-viewer-modal { + max-width: 100vw; + max-height: 100vh; + width: 100vw; + height: 100vh; + border-radius: 0; + } + + .doc-viewer-pdf { + width: 100%; + height: calc(100vh - 80px); + } } /* ======================== */ diff --git a/Media.JoshHeaps.Net/wwwroot/js/medical-docs/documents.js b/Media.JoshHeaps.Net/wwwroot/js/medical-docs/documents.js index 97fcf36..1280357 100644 --- a/Media.JoshHeaps.Net/wwwroot/js/medical-docs/documents.js +++ b/Media.JoshHeaps.Net/wwwroot/js/medical-docs/documents.js @@ -78,6 +78,18 @@ select.value = currentVal; }; + // --- Viewer --- + + app.getViewerType = function (doc) { + if (doc.documentType === 'note') return 'text'; + const mime = (doc.mimeType || '').toLowerCase(); + if (mime.startsWith('image/')) return 'image'; + if (mime.startsWith('audio/')) return 'audio'; + if (mime.startsWith('text/')) return 'text'; + if (mime === 'application/pdf') return 'pdf'; + return null; + }; + // --- Documents --- app.loadDocuments = async function () { @@ -135,6 +147,11 @@ const aiStatusBadge = app.getAiStatusBadge(doc); + const viewerType = app.getViewerType(doc); + const viewBtn = viewerType + ? `` + : ''; + const downloadBtn = doc.documentType === 'file' ? `` : ''; @@ -166,6 +183,7 @@
${processBtn} + ${viewBtn} ${downloadBtn}
@@ -293,6 +311,79 @@ document.body.removeChild(a); }; + // --- Document Viewer --- + + let currentBlobUrl = null; + + window.medDocsView = async function (id) { + const doc = state.currentDocuments.find(d => d.id === id); + if (!doc) return; + + const viewerType = app.getViewerType(doc); + if (!viewerType) return; + + const overlay = document.getElementById('docViewerOverlay'); + const title = document.getElementById('docViewerTitle'); + const body = document.getElementById('docViewerBody'); + + title.textContent = doc.title || doc.fileName || 'Untitled'; + body.innerHTML = '
Loading...
'; + overlay.style.display = ''; + document.body.style.overflow = 'hidden'; + + if (doc.documentType === 'note') { + body.innerHTML = `
${app.escapeHtml(doc.description || '')}
`; + return; + } + + try { + const res = await fetch(`${app.API}/documents/${id}/download`); + if (!res.ok) { + body.innerHTML = '
Failed to load document.
'; + return; + } + + const blob = await res.blob(); + currentBlobUrl = URL.createObjectURL(blob); + + if (viewerType === 'image') { + body.innerHTML = `${app.escapeAttr(doc.title || doc.fileName || '')}`; + } else if (viewerType === 'audio') { + body.innerHTML = ``; + } else if (viewerType === 'pdf') { + body.innerHTML = ``; + } else if (viewerType === 'text') { + const text = await blob.text(); + body.innerHTML = `
${app.escapeHtml(text)}
`; + URL.revokeObjectURL(currentBlobUrl); + currentBlobUrl = null; + } + } catch { + body.innerHTML = '
Error loading document.
'; + } + }; + + window.medDocsCloseViewer = function (event) { + if (event && event.target !== event.currentTarget) return; + const overlay = document.getElementById('docViewerOverlay'); + overlay.style.display = 'none'; + document.getElementById('docViewerBody').innerHTML = ''; + document.body.style.overflow = ''; + if (currentBlobUrl) { + URL.revokeObjectURL(currentBlobUrl); + currentBlobUrl = null; + } + }; + + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + const overlay = document.getElementById('docViewerOverlay'); + if (overlay && overlay.style.display !== 'none') { + window.medDocsCloseViewer(); + } + } + }); + window.medDocsDelete = async function (id) { if (!confirm('Delete this document? This cannot be undone.')) return;