Add multi-account support
- Schema: account_id FK on checks and layout_fields; UNIQUE per-account on check_no and field_name - DB: runtime migration recreates both tables to add account_id (assigns existing rows to account 1) - Routes: GET /api/accounts lists all; GET /api/account/:id replaces hardcoded id=1; POST /api/account/setup always creates a new account and returns accountId - checks.js: all queries scoped by account_id; POST requires account_id in body - pdf.js: resolves account from check's account_id instead of id=1; layout fields fetched per-account - import-mdb.js: always INSERTs a new account (never deletes existing); all records tagged with new accountId - Frontend: account switcher in header; activeAccountId persisted to localStorage; all API calls pass account_id; switching accounts reloads checks; wizard and import auto-switch to newly created account
This commit is contained in:
+42
-39
@@ -17,33 +17,30 @@ app.use(express.static(path.join(__dirname, '../public')));
|
||||
app.use('/api/checks', require('./routes/checks'));
|
||||
app.use('/api/pdf', require('./routes/pdf'));
|
||||
|
||||
// .mdb import endpoint
|
||||
app.post('/api/import', upload.single('mdbfile'), (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'No file uploaded.' });
|
||||
const tmpPath = req.file.path;
|
||||
try {
|
||||
const output = execFileSync(
|
||||
process.execPath,
|
||||
[path.join(__dirname, '../migrations/import-mdb.js'), '--file', tmpPath],
|
||||
{ encoding: 'utf8', timeout: 120000, env: process.env }
|
||||
);
|
||||
res.json({ success: true, log: output });
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
error: 'Import failed.',
|
||||
log: [err.stdout, err.stderr, err.message].filter(Boolean).join('\n'),
|
||||
});
|
||||
} finally {
|
||||
fs.unlink(tmpPath, () => {});
|
||||
}
|
||||
// GET /api/accounts - list all accounts (id + display name)
|
||||
app.get('/api/accounts', (req, res) => {
|
||||
const db = require('./db/database');
|
||||
const accounts = db.prepare(
|
||||
'SELECT id, company1, bank_name, current_check_no FROM account ORDER BY id ASC'
|
||||
).all();
|
||||
res.json(accounts);
|
||||
});
|
||||
|
||||
// Account setup endpoint (first-run wizard)
|
||||
// GET /api/account/:id - get full account by id
|
||||
app.get('/api/account/:id', (req, res) => {
|
||||
const db = require('./db/database');
|
||||
const account = db.prepare(
|
||||
'SELECT id, bank_name, bank_info1, bank_info2, bank_info3, transit_code, ' +
|
||||
'routing_number, account_number, current_check_no, ' +
|
||||
'company1, company2, company3, company4, check_position FROM account WHERE id = ?'
|
||||
).get(req.params.id);
|
||||
if (!account) return res.status(404).json({ error: 'Account not found.' });
|
||||
res.json(account);
|
||||
});
|
||||
|
||||
// POST /api/account/setup - create a new account (wizard)
|
||||
app.post('/api/account/setup', (req, res) => {
|
||||
const db = require('./db/database');
|
||||
const existing = db.prepare('SELECT id FROM account WHERE id = 1').get();
|
||||
if (existing) return res.status(409).json({ error: 'Account already configured.' });
|
||||
|
||||
const {
|
||||
company1, company2, company3, company4,
|
||||
bank_name, bank_info1, bank_info2, transit_code,
|
||||
@@ -58,7 +55,7 @@ app.post('/api/account/setup', (req, res) => {
|
||||
return res.status(400).json({ error: 'Starting check number must be a positive integer.' });
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
const result = db.prepare(`
|
||||
INSERT INTO account (
|
||||
bank_name, bank_info1, bank_info2, transit_code,
|
||||
routing_number, account_number, start_check_no, current_check_no,
|
||||
@@ -84,29 +81,35 @@ app.post('/api/account/setup', (req, res) => {
|
||||
logo_data: logo_data || null,
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true });
|
||||
res.status(201).json({ success: true, accountId: result.lastInsertRowid });
|
||||
});
|
||||
|
||||
// TODO: Add multi-account support -- account switcher, per-account routing/logo/layout, account_id FK on checks and layout_fields
|
||||
|
||||
// TODO: Add basic auth or simple password gate for any network-exposed deployment
|
||||
|
||||
// TODO: Add deposit slip support -- deposits table, PDF generation, ledger, and slide-in entry form
|
||||
|
||||
// Account info endpoint (read-only for Phase 1)
|
||||
app.get('/api/account', (req, res) => {
|
||||
// .mdb import endpoint — always creates a new account
|
||||
app.post('/api/import', upload.single('mdbfile'), (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'No file uploaded.' });
|
||||
const db = require('./db/database');
|
||||
const account = db.prepare(
|
||||
'SELECT id, bank_name, bank_info1, bank_info2, bank_info3, transit_code, ' +
|
||||
'routing_number, account_number, current_check_no, ' +
|
||||
'company1, company2, company3, company4, check_position FROM account WHERE id = 1'
|
||||
).get();
|
||||
if (!account) {
|
||||
return res.status(404).json({ error: 'No account configured. Run migration first.' });
|
||||
const tmpPath = req.file.path;
|
||||
try {
|
||||
const output = execFileSync(
|
||||
process.execPath,
|
||||
[path.join(__dirname, '../migrations/import-mdb.js'), '--file', tmpPath],
|
||||
{ encoding: 'utf8', timeout: 120000, env: process.env }
|
||||
);
|
||||
// Grab the newly created account (highest id)
|
||||
const newAccount = db.prepare('SELECT id, company1 FROM account ORDER BY id DESC LIMIT 1').get();
|
||||
res.json({ success: true, log: output, newAccountId: newAccount ? newAccount.id : null });
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
error: 'Import failed.',
|
||||
log: [err.stdout, err.stderr, err.message].filter(Boolean).join('\n'),
|
||||
});
|
||||
} finally {
|
||||
fs.unlink(tmpPath, () => {});
|
||||
}
|
||||
// Never send routing/account numbers in cleartext to the browser in production.
|
||||
// For local-only Phase 1 this is acceptable; redact for any network-exposed deployment.
|
||||
res.json(account);
|
||||
});
|
||||
|
||||
// Catch-all: serve index.html for client-side routing
|
||||
|
||||
+72
-2
@@ -7,7 +7,6 @@ const path = require('path');
|
||||
const DB_PATH = process.env.DB_PATH || path.join(__dirname, '../../data/ezcheck.db');
|
||||
const SCHEMA_PATH = path.join(__dirname, 'schema.sql');
|
||||
|
||||
// Ensure data directory exists
|
||||
const dataDir = path.dirname(DB_PATH);
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
@@ -15,7 +14,6 @@ if (!fs.existsSync(dataDir)) {
|
||||
|
||||
const db = new Database(DB_PATH);
|
||||
|
||||
// Enable WAL mode for better concurrent read performance
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
@@ -23,4 +21,76 @@ db.pragma('foreign_keys = ON');
|
||||
const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
|
||||
db.exec(schema);
|
||||
|
||||
// --- Runtime migrations for schema upgrades ---
|
||||
|
||||
// Migration: add account_id to checks, fix UNIQUE to be per-account
|
||||
const checksInfo = db.prepare('PRAGMA table_info(checks)').all();
|
||||
if (!checksInfo.some(c => c.name === 'account_id')) {
|
||||
db.exec(`
|
||||
ALTER TABLE checks RENAME TO checks_old;
|
||||
CREATE TABLE checks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL DEFAULT 1 REFERENCES account(id),
|
||||
check_no INTEGER NOT NULL,
|
||||
payee TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
check_date TEXT NOT NULL,
|
||||
memo TEXT,
|
||||
note1 TEXT,
|
||||
note2 TEXT,
|
||||
payee_address1 TEXT,
|
||||
payee_address2 TEXT,
|
||||
payee_address3 TEXT,
|
||||
payee_address4 TEXT,
|
||||
printed INTEGER NOT NULL DEFAULT 0,
|
||||
add_date TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
mdb_check_id INTEGER,
|
||||
UNIQUE(account_id, check_no)
|
||||
);
|
||||
INSERT INTO checks (id, account_id, check_no, payee, amount, check_date, memo, note1, note2,
|
||||
payee_address1, payee_address2, payee_address3, payee_address4, printed, add_date, mdb_check_id)
|
||||
SELECT id, 1, check_no, payee, amount, check_date, memo, note1, note2,
|
||||
payee_address1, payee_address2, payee_address3, payee_address4, printed, add_date, mdb_check_id
|
||||
FROM checks_old;
|
||||
DROP TABLE checks_old;
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_date ON checks(check_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_printed ON checks(printed);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_check_no ON checks(check_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_account ON checks(account_id);
|
||||
`);
|
||||
}
|
||||
|
||||
// Migration: add account_id to layout_fields, change UNIQUE to per-account
|
||||
const lfInfo = db.prepare('PRAGMA table_info(layout_fields)').all();
|
||||
if (!lfInfo.some(c => c.name === 'account_id')) {
|
||||
db.exec(`
|
||||
ALTER TABLE layout_fields RENAME TO layout_fields_old;
|
||||
CREATE TABLE layout_fields (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL DEFAULT 1 REFERENCES account(id),
|
||||
field_name TEXT NOT NULL,
|
||||
field_text TEXT,
|
||||
font_name TEXT NOT NULL DEFAULT 'Helvetica',
|
||||
font_size REAL NOT NULL DEFAULT 10,
|
||||
font_bold INTEGER NOT NULL DEFAULT 0,
|
||||
field_type TEXT NOT NULL DEFAULT 'Regular',
|
||||
line_thick INTEGER NOT NULL DEFAULT 1,
|
||||
x_pos REAL NOT NULL DEFAULT 0,
|
||||
y_pos REAL NOT NULL DEFAULT 0,
|
||||
x_end_pos REAL NOT NULL DEFAULT 0,
|
||||
y_end_pos REAL NOT NULL DEFAULT 0,
|
||||
visible INTEGER NOT NULL DEFAULT 1,
|
||||
not_for_preprint INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(account_id, field_name)
|
||||
);
|
||||
INSERT INTO layout_fields (id, 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, not_for_preprint)
|
||||
SELECT id, 1, 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, not_for_preprint
|
||||
FROM layout_fields_old;
|
||||
DROP TABLE layout_fields_old;
|
||||
CREATE INDEX IF NOT EXISTS idx_layout_account ON layout_fields(account_id);
|
||||
`);
|
||||
}
|
||||
|
||||
module.exports = db;
|
||||
|
||||
+26
-33
@@ -1,7 +1,4 @@
|
||||
-- ezcheck SQLite schema
|
||||
-- Mirrors .mdb structure (T100, T104, T200) with readable column names.
|
||||
-- One account per database is the Phase 1 assumption.
|
||||
-- Phase 2 will add foreign keys and an account switcher.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS account (
|
||||
id INTEGER PRIMARY KEY,
|
||||
@@ -16,21 +13,17 @@ CREATE TABLE IF NOT EXISTS account (
|
||||
current_check_no INTEGER NOT NULL DEFAULT 1000,
|
||||
check_width REAL NOT NULL DEFAULT 8.5,
|
||||
check_height REAL NOT NULL DEFAULT 3.5,
|
||||
-- Per-check offset adjustments in inches (for printer calibration)
|
||||
offset_left REAL NOT NULL DEFAULT 0,
|
||||
offset_right REAL NOT NULL DEFAULT 0,
|
||||
offset_up REAL NOT NULL DEFAULT 0,
|
||||
offset_down REAL NOT NULL DEFAULT 0,
|
||||
-- Company info lines (printed top-left of check)
|
||||
company1 TEXT,
|
||||
company2 TEXT,
|
||||
company3 TEXT,
|
||||
company4 TEXT,
|
||||
-- Images stored as base64 data URIs
|
||||
logo_data TEXT,
|
||||
signature_data TEXT,
|
||||
-- Metadata
|
||||
blank_stock INTEGER NOT NULL DEFAULT 1, -- 1 = blank check stock
|
||||
blank_stock INTEGER NOT NULL DEFAULT 1,
|
||||
check_position TEXT NOT NULL DEFAULT '3-per-page',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
@@ -38,10 +31,11 @@ CREATE TABLE IF NOT EXISTS account (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS checks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL DEFAULT 1 REFERENCES account(id),
|
||||
check_no INTEGER NOT NULL,
|
||||
payee TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
check_date TEXT NOT NULL, -- ISO date string YYYY-MM-DD
|
||||
check_date TEXT NOT NULL,
|
||||
memo TEXT,
|
||||
note1 TEXT,
|
||||
note2 TEXT,
|
||||
@@ -49,34 +43,33 @@ CREATE TABLE IF NOT EXISTS checks (
|
||||
payee_address2 TEXT,
|
||||
payee_address3 TEXT,
|
||||
payee_address4 TEXT,
|
||||
printed INTEGER NOT NULL DEFAULT 0, -- 0 = not printed, 1 = printed
|
||||
printed INTEGER NOT NULL DEFAULT 0,
|
||||
add_date TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
-- original .mdb CheckID preserved if migrated, null if created in app
|
||||
mdb_check_id INTEGER,
|
||||
UNIQUE(check_no)
|
||||
UNIQUE(account_id, check_no)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS layout_fields (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
field_name TEXT NOT NULL UNIQUE,
|
||||
field_text TEXT, -- static label text (for Text type fields)
|
||||
font_name TEXT NOT NULL DEFAULT 'Helvetica',
|
||||
font_size REAL NOT NULL DEFAULT 10,
|
||||
-- FldFontType from .mdb: 0=normal, 1=bold
|
||||
font_bold INTEGER NOT NULL DEFAULT 0,
|
||||
-- FldType: 'Regular' (data), 'Text' (static label), 'Graph' (image), 'Line'
|
||||
field_type TEXT NOT NULL DEFAULT 'Regular',
|
||||
line_thick INTEGER NOT NULL DEFAULT 1,
|
||||
x_pos REAL NOT NULL DEFAULT 0,
|
||||
y_pos REAL NOT NULL DEFAULT 0,
|
||||
x_end_pos REAL NOT NULL DEFAULT 0,
|
||||
y_end_pos REAL NOT NULL DEFAULT 0,
|
||||
visible INTEGER NOT NULL DEFAULT 1,
|
||||
-- 1 = only used on blank stock (not preprinted). We always render these.
|
||||
not_for_preprint INTEGER NOT NULL DEFAULT 0
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL DEFAULT 1 REFERENCES account(id),
|
||||
field_name TEXT NOT NULL,
|
||||
field_text TEXT,
|
||||
font_name TEXT NOT NULL DEFAULT 'Helvetica',
|
||||
font_size REAL NOT NULL DEFAULT 10,
|
||||
font_bold INTEGER NOT NULL DEFAULT 0,
|
||||
field_type TEXT NOT NULL DEFAULT 'Regular',
|
||||
line_thick INTEGER NOT NULL DEFAULT 1,
|
||||
x_pos REAL NOT NULL DEFAULT 0,
|
||||
y_pos REAL NOT NULL DEFAULT 0,
|
||||
x_end_pos REAL NOT NULL DEFAULT 0,
|
||||
y_end_pos REAL NOT NULL DEFAULT 0,
|
||||
visible INTEGER NOT NULL DEFAULT 1,
|
||||
not_for_preprint INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(account_id, field_name)
|
||||
);
|
||||
|
||||
-- Index for fast ledger queries
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_date ON checks(check_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_printed ON checks(printed);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_check_no ON checks(check_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_date ON checks(check_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_printed ON checks(printed);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_check_no ON checks(check_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_account ON checks(account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_layout_account ON layout_fields(account_id);
|
||||
|
||||
+21
-29
@@ -6,29 +6,25 @@ const db = require('../db/database');
|
||||
|
||||
// TODO: Add ledger reporting -- date range filter, payee search, total amount display, CSV export
|
||||
|
||||
// GET /api/checks - list all checks, newest first
|
||||
// GET /api/checks?account_id=X - list checks for an account, newest first
|
||||
router.get('/', (req, res) => {
|
||||
const { after, printed } = req.query;
|
||||
let query = 'SELECT * FROM checks';
|
||||
const params = [];
|
||||
const conditions = [];
|
||||
const { after, printed, account_id } = req.query;
|
||||
if (!account_id) return res.status(400).json({ error: 'account_id query param required' });
|
||||
|
||||
let query = 'SELECT * FROM checks WHERE account_id = ?';
|
||||
const params = [account_id];
|
||||
|
||||
if (after) {
|
||||
conditions.push('check_date >= ?');
|
||||
query += ' AND check_date >= ?';
|
||||
params.push(after);
|
||||
}
|
||||
if (printed !== undefined) {
|
||||
conditions.push('printed = ?');
|
||||
query += ' AND printed = ?';
|
||||
params.push(printed === 'true' || printed === '1' ? 1 : 0);
|
||||
}
|
||||
|
||||
if (conditions.length) {
|
||||
query += ' WHERE ' + conditions.join(' AND ');
|
||||
}
|
||||
query += ' ORDER BY check_no DESC';
|
||||
|
||||
const checks = db.prepare(query).all(...params);
|
||||
res.json(checks);
|
||||
res.json(db.prepare(query).all(...params));
|
||||
});
|
||||
|
||||
// GET /api/checks/:id
|
||||
@@ -42,43 +38,41 @@ router.get('/:id', (req, res) => {
|
||||
|
||||
// POST /api/checks - create a new check
|
||||
router.post('/', (req, res) => {
|
||||
const { payee, amount, check_date, memo, note1, note2,
|
||||
const { account_id, payee, amount, check_date, memo, note1, note2,
|
||||
payee_address1, payee_address2, payee_address3, payee_address4 } = req.body;
|
||||
|
||||
if (!payee || !amount || !check_date) {
|
||||
return res.status(400).json({ error: 'payee, amount, and check_date are required' });
|
||||
if (!account_id || !payee || !amount || !check_date) {
|
||||
return res.status(400).json({ error: 'account_id, payee, amount, and check_date are required' });
|
||||
}
|
||||
|
||||
// Get next check number from account
|
||||
const account = db.prepare('SELECT current_check_no FROM account WHERE id = 1').get();
|
||||
if (!account) return res.status(500).json({ error: 'No account configured. Run migration first.' });
|
||||
const account = db.prepare('SELECT current_check_no FROM account WHERE id = ?').get(account_id);
|
||||
if (!account) return res.status(400).json({ error: 'Account not found.' });
|
||||
|
||||
const checkNo = account.current_check_no + 1;
|
||||
|
||||
const insertCheck = db.prepare(`
|
||||
INSERT INTO checks (check_no, payee, amount, check_date, memo, note1, note2,
|
||||
INSERT INTO checks (account_id, check_no, payee, amount, check_date, memo, note1, note2,
|
||||
payee_address1, payee_address2, payee_address3, payee_address4)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const updateAccountCheckNo = db.prepare(
|
||||
'UPDATE account SET current_check_no = ?, updated_at = datetime(\'now\') WHERE id = 1'
|
||||
"UPDATE account SET current_check_no = ?, updated_at = datetime('now') WHERE id = ?"
|
||||
);
|
||||
|
||||
const transaction = db.transaction(() => {
|
||||
const result = insertCheck.run(
|
||||
checkNo, payee, parseFloat(amount), check_date,
|
||||
account_id, checkNo, payee, parseFloat(amount), check_date,
|
||||
memo || null, note1 || null, note2 || null,
|
||||
payee_address1 || null, payee_address2 || null,
|
||||
payee_address3 || null, payee_address4 || null
|
||||
);
|
||||
updateAccountCheckNo.run(checkNo);
|
||||
updateAccountCheckNo.run(checkNo, account_id);
|
||||
return result.lastInsertRowid;
|
||||
});
|
||||
|
||||
const newId = transaction();
|
||||
const newCheck = db.prepare('SELECT * FROM checks WHERE id = ?').get(newId);
|
||||
res.status(201).json(newCheck);
|
||||
res.status(201).json(db.prepare('SELECT * FROM checks WHERE id = ?').get(newId));
|
||||
});
|
||||
|
||||
// PUT /api/checks/:id - update a check
|
||||
@@ -115,18 +109,16 @@ router.put('/:id', (req, res) => {
|
||||
router.delete('/:id', (req, res) => {
|
||||
const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(req.params.id);
|
||||
if (!check) return res.status(404).json({ error: 'Check not found' });
|
||||
|
||||
db.prepare('DELETE FROM checks WHERE id = ?').run(req.params.id);
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
// POST /api/checks/mark-printed - mark checks as printed
|
||||
// POST /api/checks/mark-printed
|
||||
router.post('/mark-printed', (req, res) => {
|
||||
const { ids } = req.body;
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({ error: 'ids array required' });
|
||||
}
|
||||
|
||||
const placeholders = ids.map(() => '?').join(',');
|
||||
db.prepare(`UPDATE checks SET printed = 1 WHERE id IN (${placeholders})`).run(...ids);
|
||||
res.json({ updated: ids.length });
|
||||
|
||||
+22
-18
@@ -7,40 +7,44 @@ const { generateCheckPdf } = require('../services/pdfService');
|
||||
|
||||
/**
|
||||
* POST /api/pdf
|
||||
* Body: { checkIds: [1, 2, 3] } -- 1 to 3 check IDs
|
||||
* Body: { checkIds: [1, 2, ...], account_id: X }
|
||||
*
|
||||
* Returns a PDF with 1–3 checks in a 3-up layout.
|
||||
* Returns a multi-page PDF (3 checks per page).
|
||||
* After successful generation, marks all checks as printed.
|
||||
*
|
||||
* Query param: ?mark_printed=false to suppress auto-marking (for reprints).
|
||||
* Query param: ?mark_printed=false to suppress auto-marking.
|
||||
*/
|
||||
router.post('/', async (req, res) => {
|
||||
const { checkIds } = req.body;
|
||||
const { checkIds, account_id } = req.body;
|
||||
|
||||
if (!Array.isArray(checkIds) || checkIds.length === 0) {
|
||||
return res.status(400).json({ error: 'checkIds must be a non-empty array' });
|
||||
}
|
||||
|
||||
// Fetch account
|
||||
const account = db.prepare('SELECT * FROM account WHERE id = 1').get();
|
||||
if (!account) {
|
||||
return res.status(500).json({ error: 'No account configured. Run migration first.' });
|
||||
// Fetch checks in the order provided
|
||||
let checks;
|
||||
try {
|
||||
checks = checkIds.map(id => {
|
||||
const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(id);
|
||||
if (!check) throw new Error(`Check ID ${id} not found`);
|
||||
return check;
|
||||
});
|
||||
} catch (err) {
|
||||
return res.status(404).json({ error: err.message });
|
||||
}
|
||||
|
||||
// Fetch checks in the order provided
|
||||
const checks = checkIds.map(id => {
|
||||
const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(id);
|
||||
if (!check) throw new Error(`Check ID ${id} not found`);
|
||||
return check;
|
||||
});
|
||||
// Derive account from checks (all should belong to the same account)
|
||||
const resolvedAccountId = account_id || checks[0].account_id;
|
||||
const account = db.prepare('SELECT * FROM account WHERE id = ?').get(resolvedAccountId);
|
||||
if (!account) {
|
||||
return res.status(500).json({ error: 'No account configured.' });
|
||||
}
|
||||
|
||||
// Fetch layout fields (all visible fields)
|
||||
const fields = db.prepare('SELECT * FROM layout_fields WHERE visible = 1').all();
|
||||
// Fetch layout fields for this account
|
||||
const fields = db.prepare('SELECT * FROM layout_fields WHERE account_id = ? AND visible = 1').all(resolvedAccountId);
|
||||
|
||||
try {
|
||||
const pdfBuffer = await generateCheckPdf(account, checks, fields);
|
||||
|
||||
// Mark as printed unless explicitly suppressed (e.g., reprint)
|
||||
const markPrinted = req.query.mark_printed !== 'false';
|
||||
if (markPrinted) {
|
||||
const placeholders = checkIds.map(() => '?').join(',');
|
||||
|
||||
Reference in New Issue
Block a user