diff --git a/src/app.js b/src/app.js index 0f257dd..7dbe930 100644 --- a/src/app.js +++ b/src/app.js @@ -10,6 +10,7 @@ const multer = require('multer'); const session = require('express-session'); const db = require('./db/database'); +const { seedLayoutFields } = require('./db/database'); const { requireAuth, requireAdmin, canAccessAccount } = require('./middleware/auth'); const app = express(); @@ -191,64 +192,6 @@ app.delete('/api/account/:id', requireAdmin, (req, res) => { res.status(204).end(); }); -// Default layout fields for manually-created accounts (no .mdb import). -// Coordinates are in inches from the top-left of each check slot (8.5" × 3.5"). -// Field names for type 'Regular' must match the keys in pdfService.resolveFieldValue. -function seedDefaultLayoutFields(accountId) { - const fields = [ - // 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 }, - { field_name: 'Company Name3', field_type: 'Regular', x_pos: 0.50, y_pos: 0.44, 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 Name4', field_type: 'Regular', x_pos: 0.50, y_pos: 0.58, 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 }, - // Check number — top right - { field_name: 'Check Number', field_type: 'Regular', x_pos: 7.20, 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 }, - // Date — upper right - { field_name: 'Date Label', field_type: 'Text', x_pos: 5.80, y_pos: 0.40, x_end_pos: 0, y_end_pos: 0, font_name: 'Helvetica', font_size: 8, font_bold: 0, field_text: 'DATE', line_thick: 1, visible: 1 }, - { field_name: 'Date', field_type: 'Regular', x_pos: 6.30, y_pos: 0.40, 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 }, - // Pay to the order of - { field_name: 'Pay To Label', field_type: 'Text', x_pos: 0.30, y_pos: 0.82, x_end_pos: 0, y_end_pos: 0, font_name: 'Helvetica', font_size: 7, font_bold: 0, field_text: 'PAY TO THE ORDER OF', line_thick: 1, visible: 1 }, - { field_name: 'Payee Name', field_type: 'Regular', x_pos: 2.15, y_pos: 0.80, x_end_pos: 0, y_end_pos: 0, font_name: 'Helvetica', font_size: 10, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, - // Amount box - { field_name: 'Dollar Sign', field_type: 'Text', x_pos: 6.80, y_pos: 0.80, x_end_pos: 0, y_end_pos: 0, font_name: 'Helvetica', font_size: 10, font_bold: 0, field_text: '$', line_thick: 1, visible: 1 }, - { field_name: 'Amount', field_type: 'Regular', x_pos: 6.95, y_pos: 0.80, 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 }, - // Written amount - { field_name: 'Text Amount', field_type: 'Regular', x_pos: 0.30, y_pos: 1.28, 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: 'Dollars Label', field_type: 'Text', x_pos: 6.30, y_pos: 1.28, x_end_pos: 0, y_end_pos: 0, font_name: 'Helvetica', font_size: 8, font_bold: 0, field_text: 'DOLLARS', line_thick: 1, visible: 1 }, - // Bank info block - { field_name: 'Bank Information', field_type: 'Regular', x_pos: 0.30, y_pos: 1.82, x_end_pos: 0, y_end_pos: 0, font_name: 'Helvetica', font_size: 8, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, - { field_name: 'Bank Transit Code', field_type: 'Regular', x_pos: 0.30, y_pos: 2.38, x_end_pos: 0, y_end_pos: 0, font_name: 'Helvetica', font_size: 7, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, - // Payee address — center window area (for windowed envelopes) - { field_name: 'Payee Address', field_type: 'Regular', x_pos: 3.50, y_pos: 1.82, 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 }, - // Memo - { field_name: 'Memo Label', field_type: 'Text', x_pos: 0.30, y_pos: 2.82, x_end_pos: 0, y_end_pos: 0, font_name: 'Helvetica', font_size: 7, font_bold: 0, field_text: 'MEMO', line_thick: 1, visible: 1 }, - { field_name: 'Memo', field_type: 'Regular', x_pos: 0.72, y_pos: 2.82, 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 }, - // Auth signature label - { field_name: 'Auth Signature Label', field_type: 'Text', x_pos: 5.00, y_pos: 3.14, x_end_pos: 0, y_end_pos: 0, font_name: 'Helvetica', font_size: 6, font_bold: 0, field_text: 'AUTHORIZED SIGNATURE', line_thick: 1, visible: 1 }, - // Lines - { field_name: 'Payee Line', field_type: 'Line', x_pos: 2.10, y_pos: 1.00, x_end_pos: 6.70, y_end_pos: 1.00, font_name: 'Helvetica', font_size: 10, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, - { field_name: 'Amount Box Top', field_type: 'Line', x_pos: 6.75, y_pos: 0.70, x_end_pos: 8.30, y_end_pos: 0.70, font_name: 'Helvetica', font_size: 10, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, - { field_name: 'Amount Box Left', field_type: 'Line', x_pos: 6.75, y_pos: 0.70, x_end_pos: 6.75, y_end_pos: 1.05, font_name: 'Helvetica', font_size: 10, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, - { field_name: 'Amount Box Bottom',field_type: 'Line', x_pos: 6.75, y_pos: 1.05, x_end_pos: 8.30, y_end_pos: 1.05, font_name: 'Helvetica', font_size: 10, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, - { field_name: 'Text Amount Line', field_type: 'Line', x_pos: 0.30, y_pos: 1.48, x_end_pos: 6.30, y_end_pos: 1.48, font_name: 'Helvetica', font_size: 10, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, - { field_name: 'Memo Line', field_type: 'Line', x_pos: 0.68, y_pos: 3.00, x_end_pos: 4.00, y_end_pos: 3.00, font_name: 'Helvetica', font_size: 10, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, - { field_name: 'Signature Line', field_type: 'Line', x_pos: 5.00, y_pos: 3.10, x_end_pos: 8.20, y_end_pos: 3.10, font_name: 'Helvetica', font_size: 10, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, - ]; - - const stmt = 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - db.transaction(() => { - for (const f of fields) { - stmt.run(accountId, f.field_name, f.field_text, f.font_name, f.font_size, f.font_bold, - f.field_type, f.line_thick, f.x_pos, f.y_pos, f.x_end_pos, f.y_end_pos, f.visible); - } - })(); -} - // POST /api/account/setup (admin only — creates a new checking account) app.post('/api/account/setup', requireAdmin, (req, res) => { const { @@ -291,7 +234,7 @@ app.post('/api/account/setup', requireAdmin, (req, res) => { logo_data: logo_data || null, }); - seedDefaultLayoutFields(result.lastInsertRowid); + seedLayoutFields(result.lastInsertRowid); res.status(201).json({ success: true, accountId: result.lastInsertRowid }); }); diff --git a/src/db/database.js b/src/db/database.js index 00af3d1..eb705aa 100644 --- a/src/db/database.js +++ b/src/db/database.js @@ -147,19 +147,8 @@ db.exec(` ) `); -// Migration: seed default layout fields for any account that has none. -// Runs at every startup but INSERT OR IGNORE makes it idempotent. -(function seedMissingLayoutFields() { - const accounts = db.prepare('SELECT id FROM account').all(); - const countStmt = db.prepare('SELECT COUNT(*) AS n FROM layout_fields WHERE account_id = ?'); - const insertStmt = 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - const defaultFields = [ +// Default layout fields used for seeding and migration. +const DEFAULT_LAYOUT_FIELDS = [ // 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 }, @@ -199,18 +188,42 @@ db.exec(` { field_name: 'Signature Line', field_type: 'Line', x_pos: 5.00, y_pos: 3.10, x_end_pos: 8.20, y_end_pos: 3.10, font_name: 'Helvetica', font_size: 10, font_bold: 0, field_text: null, line_thick: 1, visible: 1 }, ]; - const seedAccount = db.transaction(accountId => { - for (const f of defaultFields) { - insertStmt.run(accountId, f.field_name, f.field_text, f.font_name, f.font_size, f.font_bold, +function seedLayoutFields(accountId) { + const insert = 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + db.transaction(() => { + for (const f of DEFAULT_LAYOUT_FIELDS) { + insert.run(accountId, f.field_name, f.field_text, f.font_name, f.font_size, f.font_bold, f.field_type, f.line_thick, f.x_pos, f.y_pos, f.x_end_pos, f.y_end_pos, f.visible); } - }); + })(); +} - for (const { id } of accounts) { - if (countStmt.get(id).n === 0) { - seedAccount(id); +// Migration: reset all accounts to default layout (runs once, gated by settings key). +// Replaces any .mdb-imported or legacy layout_fields with the clean default layout. +if (!db.prepare("SELECT value FROM settings WHERE key = 'layout_reset_v1'").get()) { + const accounts = db.prepare('SELECT id FROM account').all(); + db.transaction(() => { + for (const { id } of accounts) { + db.prepare('DELETE FROM layout_fields WHERE account_id = ?').run(id); + seedLayoutFields(id); } + db.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('layout_reset_v1', '1')").run(); + })(); +} + +// Migration: seed default layout fields for any account that has none (ongoing, idempotent). +(function seedMissingLayoutFields() { + const accounts = db.prepare('SELECT id FROM account').all(); + for (const { id } of accounts) { + const { n } = db.prepare('SELECT COUNT(*) AS n FROM layout_fields WHERE account_id = ?').get(id); + if (n === 0) seedLayoutFields(id); } })(); module.exports = db; +module.exports.seedLayoutFields = seedLayoutFields; diff --git a/src/services/pdfService.js b/src/services/pdfService.js index 9e1b1b0..8b74d08 100644 --- a/src/services/pdfService.js +++ b/src/services/pdfService.js @@ -138,7 +138,7 @@ function generateCheckPdf(account, checks, fields) { for (let slot = 0; slot < 3; slot++) { const check = checks[page * 3 + slot] || null; - const slotOriginY = slot * SLOT_HEIGHT_IN + (slot === 0 ? -0.25 : 0); + const slotOriginY = slot * SLOT_HEIGHT_IN; // Helper: convert inches (relative to slot) to PDF points (absolute page) const pt = (xIn, yIn) => ({