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
This commit is contained in:
+2
-1
@@ -886,7 +886,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="layout-save-status" style="font-size:11px;color:var(--text-muted);min-width:56px"></div>
|
<div id="layout-save-status" style="font-size:11px;color:var(--text-muted);min-width:56px"></div>
|
||||||
<div style="margin-left:auto">
|
<div style="margin-left:auto;display:flex;gap:8px;align-items:center">
|
||||||
|
<button id="btn-layout-preview" class="btn-secondary btn-sm">⎙ Preview PDF</button>
|
||||||
<button id="btn-layout-reset" class="btn-secondary btn-sm" data-admin-only>↺ Reset to Default</button>
|
<button id="btn-layout-reset" class="btn-secondary btn-sm" data-admin-only>↺ Reset to Default</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2007,6 +2007,7 @@ async function init() {
|
|||||||
document.getElementById('nudge-left').addEventListener('click', () => nudgeLayoutField(-1, 0));
|
document.getElementById('nudge-left').addEventListener('click', () => nudgeLayoutField(-1, 0));
|
||||||
document.getElementById('nudge-right').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-reset').addEventListener('click', resetLayoutToDefault);
|
||||||
|
document.getElementById('btn-layout-preview').addEventListener('click', previewLayoutPdf);
|
||||||
|
|
||||||
// Initial auth check → loads app if already signed in
|
// Initial auth check → loads app if already signed in
|
||||||
const authed = await checkAuth();
|
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() {
|
async function resetLayoutToDefault() {
|
||||||
if (!confirm('Reset all layout fields to default positions? This cannot be undone.')) return;
|
if (!confirm('Reset all layout fields to default positions? This cannot be undone.')) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user