From 0b21f4ea3c85dca24f1adcca2208d5ddd807a0a8 Mon Sep 17 00:00:00 2001 From: Steve Dogiakos Date: Sat, 2 May 2026 17:21:41 -0600 Subject: [PATCH] feat(layout): add preview PDF button to layout editor Generates a 3-up PDF with dummy check data (no real checks touched) so the layout can be proofed without printing live checks. Closes #12 --- public/index.html | 3 ++- public/js/app.js | 26 ++++++++++++++++++++++++ src/routes/pdf.js | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 9deddd6..cc82062 100644 --- a/public/index.html +++ b/public/index.html @@ -886,7 +886,8 @@
-
+
+
diff --git a/public/js/app.js b/public/js/app.js index 64da08b..5155505 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -2007,6 +2007,7 @@ async function init() { document.getElementById('nudge-left').addEventListener('click', () => nudgeLayoutField(-1, 0)); document.getElementById('nudge-right').addEventListener('click', () => nudgeLayoutField( 1, 0)); document.getElementById('btn-layout-reset').addEventListener('click', resetLayoutToDefault); + document.getElementById('btn-layout-preview').addEventListener('click', previewLayoutPdf); // Initial auth check → loads app if already signed in const authed = await checkAuth(); @@ -2456,6 +2457,31 @@ async function saveLayoutField(f) { } } +async function previewLayoutPdf() { + const btn = document.getElementById('btn-layout-preview'); + const orig = btn.textContent; + btn.disabled = true; + btn.textContent = '…'; + try { + const res = await fetch('/api/pdf/preview', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ account_id: state.activeAccountId }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(err.error || res.statusText); + } + const blob = await res.blob(); + openPdfBlob(blob, 'layout-preview.pdf'); + } catch (err) { + alert('Preview error: ' + err.message); + } finally { + btn.disabled = false; + btn.textContent = orig; + } +} + async function resetLayoutToDefault() { if (!confirm('Reset all layout fields to default positions? This cannot be undone.')) return; try { diff --git a/src/routes/pdf.js b/src/routes/pdf.js index 36940b8..55fa46b 100644 --- a/src/routes/pdf.js +++ b/src/routes/pdf.js @@ -70,4 +70,55 @@ router.post('/', async (req, res) => { } }); +/** + * POST /api/pdf/preview + * Body: { account_id: X } + * + * Generates a layout preview PDF using dummy check data — no real checks touched. + * Shows all three slots filled with sample data so every visible field is visible. + */ +router.post('/preview', async (req, res) => { + const resolvedAccountId = parseInt(req.body.account_id, 10); + if (!resolvedAccountId) return res.status(400).json({ error: 'account_id required' }); + + const account = db.prepare('SELECT * FROM account WHERE id = ?').get(resolvedAccountId); + if (!account) return res.status(404).json({ error: 'Account not found.' }); + + const fields = db.prepare('SELECT * FROM layout_fields WHERE account_id = ?').all(resolvedAccountId); + + const DUMMY_CHECK = { + id: 0, + check_no: 1001, + payee: 'Sample Payee Name', + amount: 1234.56, + check_date: new Date().toISOString().slice(0, 10), + memo: 'Sample Memo', + payee_address1: '123 Sample Street', + payee_address2: 'City, ST 12345', + payee_address3: null, + payee_address4: null, + printed: 0, + account_id: resolvedAccountId, + }; + + const checks = [ + { ...DUMMY_CHECK, check_no: 1001 }, + { ...DUMMY_CHECK, check_no: 1002 }, + { ...DUMMY_CHECK, check_no: 1003 }, + ]; + + try { + const pdfBuffer = await generateCheckPdf(account, checks, fields); + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Disposition': 'inline; filename="layout-preview.pdf"', + 'Content-Length': pdfBuffer.length, + }); + res.send(pdfBuffer); + } catch (err) { + console.error('Preview PDF error:', err); + res.status(500).json({ error: 'Preview generation failed.' }); + } +}); + module.exports = router;