From deb31d248fbf16947f647d4107e651ee45f970f8 Mon Sep 17 00:00:00 2001 From: Steve Dogiakos Date: Fri, 10 Apr 2026 19:54:17 -0600 Subject: [PATCH] feat: add check position selector and fix logo not rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add per-account check position setting (top/middle/bottom/3-per-page) so checks print in a specific slot on the page. Fix logos never appearing on checks or in the layout editor — the Logo layout field was missing from the default seed data and existing accounts. --- public/index.html | 12 ++++++++++++ public/js/app.js | 2 ++ src/app.js | 9 ++++++--- src/db/database.js | 17 +++++++++++++++++ src/services/pdfService.js | 14 +++++++++++--- 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/public/index.html b/public/index.html index 0e1ceca..62212b4 100644 --- a/public/index.html +++ b/public/index.html @@ -472,6 +472,18 @@ +

Check Position

+
+ + + Choose which slot(s) on the page to print checks in. +
+ diff --git a/public/js/app.js b/public/js/app.js index 28c5442..509ccc4 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -963,6 +963,7 @@ function openAccountSettings() { f.elements.offset_up.value = a.offset_up || 0; f.elements.offset_down.value = a.offset_down || 0; document.getElementById('as-second-sig').checked = !!a.second_signature; + document.getElementById('as-check-position').value = a.check_position || '3-per-page'; document.getElementById('as-logo').value = ''; document.getElementById('as-logo-preview').hidden = true; @@ -1001,6 +1002,7 @@ async function saveAccountSettings() { offset_up: parseFloat(f.elements.offset_up.value) || 0, offset_down: parseFloat(f.elements.offset_down.value) || 0, second_signature: document.getElementById('as-second-sig').checked ? 1 : 0, + check_position: document.getElementById('as-check-position').value, logo_data: acctSettings.logoData || null, }; diff --git a/src/app.js b/src/app.js index 69a5aaf..d407bef 100644 --- a/src/app.js +++ b/src/app.js @@ -106,7 +106,7 @@ app.put('/api/account/:id', requireAdmin, (req, res) => { bank_name, bank_info1, bank_info2, bank_info3, transit_code, routing_number, account_number, offset_left, offset_right, offset_up, offset_down, - logo_data, second_signature, + logo_data, second_signature, check_position, } = req.body; if (!company1 || !routing_number || !account_number) { @@ -117,13 +117,16 @@ app.put('/api/account/:id', requireAdmin, (req, res) => { return res.status(400).json({ error: 'Logo image must be smaller than 512 KB.' }); } + const VALID_POSITIONS = ['3-per-page', 'top', 'middle', 'bottom']; + const resolvedPosition = VALID_POSITIONS.includes(check_position) ? check_position : '3-per-page'; + db.prepare(` UPDATE account SET company1 = ?, company2 = ?, company3 = ?, company4 = ?, bank_name = ?, bank_info1 = ?, bank_info2 = ?, bank_info3 = ?, transit_code = ?, routing_number = ?, account_number = ?, offset_left = ?, offset_right = ?, offset_up = ?, offset_down = ?, - second_signature = ?, + second_signature = ?, check_position = ?, logo_data = CASE WHEN ? IS NOT NULL THEN ? ELSE logo_data END, updated_at = datetime('now') WHERE id = ? @@ -133,7 +136,7 @@ app.put('/api/account/:id', requireAdmin, (req, res) => { routing_number, account_number, parseFloat(offset_left) || 0, parseFloat(offset_right) || 0, parseFloat(offset_up) || 0, parseFloat(offset_down) || 0, - second_signature ? 1 : 0, + second_signature ? 1 : 0, resolvedPosition, logo_data || null, logo_data || null, req.params.id ); diff --git a/src/db/database.js b/src/db/database.js index 61e544e..98bb911 100644 --- a/src/db/database.js +++ b/src/db/database.js @@ -160,6 +160,8 @@ db.exec(` // Default layout fields used for seeding and migration. const DEFAULT_LAYOUT_FIELDS = [ + // Logo — top left corner (Graph type, rendered as image from account.logo_data) + { field_name: 'Logo', field_type: 'Graph', x_pos: 0.10, y_pos: 0.08, x_end_pos: 0.45, y_end_pos: 0.58, font_name: 'Helvetica', font_size: 10, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, // Company block — top left { field_name: 'Company Name', field_type: 'Regular', x_pos: 0.50, y_pos: 0.12, x_end_pos: 0, y_end_pos: 0, font_name: 'Helvetica-Bold', font_size: 10, font_bold: 1, field_text: null, line_thick: 1, visible: 1 }, { field_name: 'Company Name2', field_type: 'Regular', x_pos: 0.50, y_pos: 0.30, x_end_pos: 0, y_end_pos: 0, font_name: 'Helvetica', font_size: 9, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, @@ -227,6 +229,21 @@ if (!db.prepare("SELECT value FROM settings WHERE key = 'layout_reset_v1'").get( })(); } +// Migration: add Logo field to existing accounts that don't have one. +(function addLogoField() { + const accounts = db.prepare('SELECT id FROM account').all(); + const insertLogo = db.prepare(` + INSERT OR IGNORE INTO layout_fields + (account_id, field_name, field_text, font_name, font_size, font_bold, + field_type, line_thick, x_pos, y_pos, x_end_pos, y_end_pos, visible) + VALUES (?, 'Logo', NULL, 'Helvetica', 10, 0, 'Graph', 1, 0.10, 0.08, 0.45, 0.58, 1) + `); + for (const { id } of accounts) { + const existing = db.prepare("SELECT id FROM layout_fields WHERE account_id = ? AND field_name = 'Logo'").get(id); + if (!existing) insertLogo.run(id); + } +})(); + // Migration: seed default layout fields for any account that has none (ongoing, idempotent). (function seedMissingLayoutFields() { const accounts = db.prepare('SELECT id FROM account').all(); diff --git a/src/services/pdfService.js b/src/services/pdfService.js index 8b74d08..33ea1ef 100644 --- a/src/services/pdfService.js +++ b/src/services/pdfService.js @@ -131,13 +131,21 @@ function generateCheckPdf(account, checks, fields) { const offX = (account.offset_right - account.offset_left); const offY = (account.offset_down - account.offset_up); - // Render checks in pages of 3; add a new page for each additional group - const pages = Math.ceil(checks.length / 3); + // Determine slot assignment based on check_position setting + const position = account.check_position || '3-per-page'; + const SLOT_MAP = { top: 0, middle: 1, bottom: 2 }; + const fixedSlot = SLOT_MAP[position]; // undefined for '3-per-page' + const checksPerPage = fixedSlot !== undefined ? 1 : 3; + + const pages = Math.ceil(checks.length / checksPerPage); for (let page = 0; page < pages; page++) { if (page > 0) doc.addPage(); for (let slot = 0; slot < 3; slot++) { - const check = checks[page * 3 + slot] || null; + // For fixed-slot mode, only render in the designated slot + if (fixedSlot !== undefined && slot !== fixedSlot) continue; + const checkIndex = fixedSlot !== undefined ? page : page * 3 + slot; + const check = checks[checkIndex] || null; const slotOriginY = slot * SLOT_HEIGHT_IN; // Helper: convert inches (relative to slot) to PDF points (absolute page)