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:
@@ -472,6 +472,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<div id="acct-settings-error" class="wizard-error" hidden></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -963,6 +963,7 @@ function openAccountSettings() {
|
|||||||
f.elements.offset_up.value = a.offset_up || 0;
|
f.elements.offset_up.value = a.offset_up || 0;
|
||||||
f.elements.offset_down.value = a.offset_down || 0;
|
f.elements.offset_down.value = a.offset_down || 0;
|
||||||
document.getElementById('as-second-sig').checked = !!a.second_signature;
|
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').value = '';
|
||||||
document.getElementById('as-logo-preview').hidden = true;
|
document.getElementById('as-logo-preview').hidden = true;
|
||||||
@@ -1001,6 +1002,7 @@ async function saveAccountSettings() {
|
|||||||
offset_up: parseFloat(f.elements.offset_up.value) || 0,
|
offset_up: parseFloat(f.elements.offset_up.value) || 0,
|
||||||
offset_down: parseFloat(f.elements.offset_down.value) || 0,
|
offset_down: parseFloat(f.elements.offset_down.value) || 0,
|
||||||
second_signature: document.getElementById('as-second-sig').checked ? 1 : 0,
|
second_signature: document.getElementById('as-second-sig').checked ? 1 : 0,
|
||||||
|
check_position: document.getElementById('as-check-position').value,
|
||||||
logo_data: acctSettings.logoData || null,
|
logo_data: acctSettings.logoData || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -106,7 +106,7 @@ app.put('/api/account/:id', requireAdmin, (req, res) => {
|
|||||||
bank_name, bank_info1, bank_info2, bank_info3, transit_code,
|
bank_name, bank_info1, bank_info2, bank_info3, transit_code,
|
||||||
routing_number, account_number,
|
routing_number, account_number,
|
||||||
offset_left, offset_right, offset_up, offset_down,
|
offset_left, offset_right, offset_up, offset_down,
|
||||||
logo_data, second_signature,
|
logo_data, second_signature, check_position,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!company1 || !routing_number || !account_number) {
|
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.' });
|
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(`
|
db.prepare(`
|
||||||
UPDATE account SET
|
UPDATE account SET
|
||||||
company1 = ?, company2 = ?, company3 = ?, company4 = ?,
|
company1 = ?, company2 = ?, company3 = ?, company4 = ?,
|
||||||
bank_name = ?, bank_info1 = ?, bank_info2 = ?, bank_info3 = ?, transit_code = ?,
|
bank_name = ?, bank_info1 = ?, bank_info2 = ?, bank_info3 = ?, transit_code = ?,
|
||||||
routing_number = ?, account_number = ?,
|
routing_number = ?, account_number = ?,
|
||||||
offset_left = ?, offset_right = ?, offset_up = ?, offset_down = ?,
|
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,
|
logo_data = CASE WHEN ? IS NOT NULL THEN ? ELSE logo_data END,
|
||||||
updated_at = datetime('now')
|
updated_at = datetime('now')
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
@@ -133,7 +136,7 @@ app.put('/api/account/:id', requireAdmin, (req, res) => {
|
|||||||
routing_number, account_number,
|
routing_number, account_number,
|
||||||
parseFloat(offset_left) || 0, parseFloat(offset_right) || 0,
|
parseFloat(offset_left) || 0, parseFloat(offset_right) || 0,
|
||||||
parseFloat(offset_up) || 0, parseFloat(offset_down) || 0,
|
parseFloat(offset_up) || 0, parseFloat(offset_down) || 0,
|
||||||
second_signature ? 1 : 0,
|
second_signature ? 1 : 0, resolvedPosition,
|
||||||
logo_data || null, logo_data || null,
|
logo_data || null, logo_data || null,
|
||||||
req.params.id
|
req.params.id
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ db.exec(`
|
|||||||
|
|
||||||
// Default layout fields used for seeding and migration.
|
// Default layout fields used for seeding and migration.
|
||||||
const DEFAULT_LAYOUT_FIELDS = [
|
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
|
// 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 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 },
|
{ 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).
|
// Migration: seed default layout fields for any account that has none (ongoing, idempotent).
|
||||||
(function seedMissingLayoutFields() {
|
(function seedMissingLayoutFields() {
|
||||||
const accounts = db.prepare('SELECT id FROM account').all();
|
const accounts = db.prepare('SELECT id FROM account').all();
|
||||||
|
|||||||
@@ -131,13 +131,21 @@ function generateCheckPdf(account, checks, fields) {
|
|||||||
const offX = (account.offset_right - account.offset_left);
|
const offX = (account.offset_right - account.offset_left);
|
||||||
const offY = (account.offset_down - account.offset_up);
|
const offY = (account.offset_down - account.offset_up);
|
||||||
|
|
||||||
// Render checks in pages of 3; add a new page for each additional group
|
// Determine slot assignment based on check_position setting
|
||||||
const pages = Math.ceil(checks.length / 3);
|
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++) {
|
for (let page = 0; page < pages; page++) {
|
||||||
if (page > 0) doc.addPage();
|
if (page > 0) doc.addPage();
|
||||||
|
|
||||||
for (let slot = 0; slot < 3; slot++) {
|
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;
|
const slotOriginY = slot * SLOT_HEIGHT_IN;
|
||||||
|
|
||||||
// Helper: convert inches (relative to slot) to PDF points (absolute page)
|
// Helper: convert inches (relative to slot) to PDF points (absolute page)
|
||||||
|
|||||||
Reference in New Issue
Block a user