feat: add check position selector and fix logo not rendering

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.
This commit is contained in:
2026-04-10 19:54:17 -06:00
parent 66374196c5
commit deb31d248f
5 changed files with 48 additions and 6 deletions
+12
View File
@@ -472,6 +472,18 @@
</div>
</div>
<p class="settings-section-label">Check Position</p>
<div class="form-group">
<label for="as-check-position">Print checks in</label>
<select id="as-check-position" name="check_position">
<option value="3-per-page">All 3 slots (3 per page)</option>
<option value="top">Top slot only</option>
<option value="middle">Middle slot only</option>
<option value="bottom">Bottom slot only</option>
</select>
<span class="field-hint">Choose which slot(s) on the page to print checks in.</span>
</div>
<div id="acct-settings-error" class="wizard-error" hidden></div>
</form>
</div>
+2
View File
@@ -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,
};
+6 -3
View File
@@ -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
);
+17
View File
@@ -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();
+11 -3
View File
@@ -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)