diff --git a/public/admin.html b/public/admin.html index 5faa962..55cc6d5 100644 --- a/public/admin.html +++ b/public/admin.html @@ -57,6 +57,9 @@ + + + +

Settings

diff --git a/public/admin.js b/public/admin.js index eb3c6f9..8465c4b 100644 --- a/public/admin.js +++ b/public/admin.js @@ -120,7 +120,7 @@ document.querySelectorAll('[data-nav]').forEach((btn) => const PANEL_TITLES = { dashboard: 'Dashboard', kids: 'Kids', courses: 'Courses', planner: 'Planner', grading: 'Grading', messages: 'Messages', gradebook: 'Gradebook', - spelling: 'Spelling', decks: 'Flashcards', settings: 'Settings', + spelling: 'Spelling', decks: 'Flashcards', docs: 'Documents', settings: 'Settings', 'course-detail': 'Course Detail', 'item-editor': 'Item Editor', }; @@ -132,7 +132,7 @@ function showPanel(name) { const loaders = { dashboard: loadDashboard, kids: loadKids, courses: loadCourses, planner: loadPlannerPanel, grading: loadGrading, messages: loadMessages, gradebook: loadGradebookPanel, spelling: loadSpelling, decks: loadDecks, - settings: loadSettings, + docs: loadAdminDocs, settings: loadSettings, }; if (loaders[name]) loaders[name](); if (name === 'course-detail') openCourseDetail(currentCourseId); @@ -3290,3 +3290,81 @@ $('#app-settings-form').addEventListener('submit', async (e) => { msg('Settings saved.'); }); + +// ── Admin: Submitted Documents ──────────────────────────────────────────────── + +let adminDocsEditorInstance = null; + +async function loadAdminDocs() { + const { docs } = await api('/api/admin/docs'); + const el = $('#admin-docs-list'); + if (!docs.length) { + el.innerHTML = '

No submitted documents yet.

'; + return; + } + el.innerHTML = docs.map((d) => ` +
+
πŸ“„
+
+
${esc(d.title)}${!d.parent_read ? ' ' : ''}
+
${esc(d.student_name)} Β· Submitted ${new Date(d.submitted_at + 'Z').toLocaleDateString()}
+
+
+ `).join(''); + + el.querySelectorAll('.doc-card').forEach((card) => { + card.addEventListener('click', () => openAdminDocViewer( + +card.dataset.id, card.dataset.key, card.dataset.filename, card.dataset.title + )); + }); +} + +async function openAdminDocViewer(id, key, filename, title) { + await api(`/api/admin/docs/${id}/read`, { method: 'PUT' }); + $('#admin-docs-list').hidden = true; + $('#admin-doc-viewer').hidden = false; + $('#admin-doc-viewer-title').textContent = title; + $('#admin-doc-download').href = `/api/docs/file/${filename}`; + $('#admin-doc-download').download = title + '.docx'; + + const frame = $('#admin-doc-frame'); + frame.innerHTML = ''; + if (adminDocsEditorInstance) { + try { adminDocsEditorInstance.destroyEditor(); } catch { /* ignore */ } + adminDocsEditorInstance = null; + } + + const { onlyofficeUrl, config } = await api(`/api/docs/bykey/${key}/editor-config`).catch(() => null) || {}; + if (!onlyofficeUrl || !config) { + frame.innerHTML = `
Could not load document config.
`; + return; + } + // Parent views in read mode + config.editorConfig = config.editorConfig || {}; + config.editorConfig.mode = 'view'; + + function initEditor() { + adminDocsEditorInstance = new window.DocsAPI.DocEditor('admin-doc-frame', config); + } + if (window.DocsAPI) { + initEditor(); + } else { + const s = document.createElement('script'); + s.src = `${onlyofficeUrl}/web-apps/apps/api/documents/api.js`; + s.onload = initEditor; + s.onerror = () => { frame.innerHTML = `
Could not reach OnlyOffice server.
`; }; + document.head.appendChild(s); + } +} + +document.getElementById('btn-admin-doc-back')?.addEventListener('click', () => { + if (adminDocsEditorInstance) { + try { adminDocsEditorInstance.destroyEditor(); } catch { /* ignore */ } + adminDocsEditorInstance = null; + } + $('#admin-doc-frame').innerHTML = ''; + $('#admin-doc-viewer').hidden = true; + $('#admin-docs-list').hidden = false; + loadAdminDocs(); +}); diff --git a/public/app.js b/public/app.js index e8d9511..8bd9831 100644 --- a/public/app.js +++ b/public/app.js @@ -2571,6 +2571,7 @@ async function openDocEditor(id, key, title) { $('#btn-docs-submit').disabled = false; $('#btn-docs-submit').textContent = 'Submit to Parent βœ“'; + document.body.classList.add('docs-editor-open'); const frame = $('#docs-editor-frame'); frame.innerHTML = ''; if (docsEditorInstance) { @@ -2626,6 +2627,7 @@ $('#docs-new-form').addEventListener('submit', async (e) => { }); $('#btn-docs-back').addEventListener('click', () => { + document.body.classList.remove('docs-editor-open'); if (docsEditorInstance) { try { docsEditorInstance.destroyEditor(); } catch { /* ignore */ } docsEditorInstance = null; diff --git a/public/styles.css b/public/styles.css index 2f1504a..c01d190 100644 --- a/public/styles.css +++ b/public/styles.css @@ -2213,6 +2213,10 @@ body.bg-bubbles { display: flex; flex-direction: column; background: #fff; } +body.docs-editor-open .kid-hero, +body.docs-editor-open .kid-nav, +body.docs-editor-open .page-confetti, +body.docs-editor-open .tab-panel { display: none !important; } #docs-editor-frame { flex: 1; min-height: 0; } #docs-editor-frame iframe { width: 100%; height: 100%; border: none; display: block; } @@ -2243,3 +2247,11 @@ body.bg-bubbles { .docs-error { padding: 1.5rem; color: var(--muted); font-size: .9rem; } .docs-error code { background: var(--surface); padding: .1rem .4rem; border-radius: 4px; } + +/* Admin doc viewer */ +#admin-doc-viewer { + position: fixed; inset: 0; z-index: 10000; + display: flex; flex-direction: column; background: #fff; +} +#admin-doc-frame { flex: 1; min-height: 0; } +#admin-doc-frame iframe { width: 100%; height: 100%; border: none; display: block; } diff --git a/server.js b/server.js index c51c097..def4550 100644 --- a/server.js +++ b/server.js @@ -2546,6 +2546,20 @@ app.get('/api/docs/config', (req, res) => { res.json({ onlyofficeUrl: ONLYOFFICE_URL, serverUrl: SERVER_URL }); }); +// Returns a fully-signed OnlyOffice editor config β€” lookup by doc key (for admin) +app.get('/api/docs/bykey/:key/editor-config', (req, res) => { + const doc = db.prepare(`SELECT d.*, s.name AS student_name FROM documents d JOIN students s ON s.id = d.student_id WHERE d.doc_key = ?`).get(req.params.key); + if (!doc) return res.status(404).json({ error: 'Not found' }); + const config = { + document: { fileType: 'docx', key: doc.doc_key, title: doc.title, url: `${SERVER_URL}/api/docs/file/${doc.filename}`, permissions: { edit: false, download: true, print: true } }, + editorConfig: { user: { id: 'parent', name: 'Parent' }, mode: 'view', lang: 'en', customization: { logo: { visible: false }, compactHeader: true, toolbarNoTabs: true } }, + type: 'desktop', height: '100%', width: '100%', + }; + const token = jwtSign(config); + if (token) config.token = token; + res.json({ onlyofficeUrl: ONLYOFFICE_URL, config }); +}); + // Returns a fully-signed OnlyOffice editor config for a specific document app.get('/api/docs/:id/editor-config', (req, res) => { const doc = db.prepare(`SELECT d.*, s.name AS student_name FROM documents d JOIN students s ON s.id = d.student_id WHERE d.id = ?`).get(req.params.id);