Files
check-printing/src/app.js
T
steve e4feafa82b Add Delete Account button with confirmation modal
Red "Delete Account" button at top of Account Settings. Clicking opens
a second modal with an irreversibility warning and the account name,
requiring a second explicit confirmation before deletion. Deletes the
account row plus all associated checks and deposits. Redirects to the
setup wizard if no accounts remain.
2026-03-18 21:15:05 -06:00

210 lines
7.9 KiB
JavaScript

'use strict';
const express = require('express');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { execFileSync } = require('child_process');
const multer = require('multer');
const app = express();
const upload = multer({ dest: os.tmpdir() });
app.use(express.json());
app.use(express.static(path.join(__dirname, '../public')));
// Routes
app.use('/api/checks', require('./routes/checks'));
app.use('/api/pdf', require('./routes/pdf'));
app.use('/api/deposits', require('./routes/deposits'));
app.use('/api/deposit-pdf', require('./routes/deposit-pdf'));
// 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);
});
// PUT /api/account/:id - update account settings
app.put('/api/account/:id', (req, res) => {
const db = require('./db/database');
const account = db.prepare('SELECT id FROM account WHERE id = ?').get(req.params.id);
if (!account) return res.status(404).json({ error: 'Account not found.' });
const {
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,
logo_data, second_signature,
} = req.body;
if (!company1 || !routing_number || !account_number) {
return res.status(400).json({ error: 'Organization name, routing number, and account number are required.' });
}
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 = ?,
logo_data = CASE WHEN ? IS NOT NULL THEN ? ELSE logo_data END,
updated_at = datetime('now')
WHERE id = ?
`).run(
company1 || null, company2 || null, company3 || null, company4 || null,
bank_name || '', bank_info1 || null, bank_info2 || null, bank_info3 || null, transit_code || null,
routing_number, account_number,
parseFloat(offset_left) || 0, parseFloat(offset_right) || 0,
parseFloat(offset_up) || 0, parseFloat(offset_down) || 0,
second_signature ? 1 : 0,
logo_data || null, logo_data || null,
req.params.id
);
res.json(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, second_signature FROM account WHERE id = ?'
).get(req.params.id));
});
// 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, second_signature FROM account WHERE id = ?'
).get(req.params.id);
if (!account) return res.status(404).json({ error: 'Account not found.' });
res.json(account);
});
// PUT /api/account/:id/check-no - override the next check number
app.put('/api/account/:id/check-no', (req, res) => {
const db = require('./db/database');
const account = db.prepare('SELECT id FROM account WHERE id = ?').get(req.params.id);
if (!account) return res.status(404).json({ error: 'Account not found.' });
const next = parseInt(req.body.next_check_no, 10);
if (isNaN(next) || next < 1) {
return res.status(400).json({ error: 'Next check number must be a positive integer.' });
}
// current_check_no is the last-used number; next check will be current_check_no + 1
db.prepare("UPDATE account SET current_check_no = ?, updated_at = datetime('now') WHERE id = ?")
.run(next - 1, req.params.id);
res.json({ next_check_no: next });
});
// DELETE /api/account/:id - delete account and all associated data
app.delete('/api/account/:id', (req, res) => {
const db = require('./db/database');
const account = db.prepare('SELECT id FROM account WHERE id = ?').get(req.params.id);
if (!account) return res.status(404).json({ error: 'Account not found.' });
db.transaction(() => {
// deposit_items deleted via ON DELETE CASCADE from deposits
db.prepare('DELETE FROM deposits WHERE account_id = ?').run(req.params.id);
db.prepare('DELETE FROM checks WHERE account_id = ?').run(req.params.id);
db.prepare('DELETE FROM account WHERE id = ?').run(req.params.id);
})();
res.status(204).end();
});
// POST /api/account/setup - create a new account (wizard)
app.post('/api/account/setup', (req, res) => {
const db = require('./db/database');
const {
company1, company2, company3, company4,
bank_name, bank_info1, bank_info2, transit_code,
routing_number, account_number, start_check_no, logo_data,
} = req.body;
if (!company1 || !routing_number || !account_number || !start_check_no) {
return res.status(400).json({ error: 'Organization name, routing number, account number, and starting check number are required.' });
}
const checkNo = parseInt(start_check_no, 10);
if (isNaN(checkNo) || checkNo < 1) {
return res.status(400).json({ error: 'Starting check number must be a positive integer.' });
}
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,
company1, company2, company3, company4, logo_data
) VALUES (
@bank_name, @bank_info1, @bank_info2, @transit_code,
@routing_number, @account_number, @start_check_no, @current_check_no,
@company1, @company2, @company3, @company4, @logo_data
)
`).run({
bank_name: bank_name || '',
bank_info1: bank_info1 || null,
bank_info2: bank_info2 || null,
transit_code: transit_code || null,
routing_number,
account_number,
start_check_no: checkNo,
current_check_no: checkNo,
company1: company1 || null,
company2: company2 || null,
company3: company3 || null,
company4: company4 || null,
logo_data: logo_data || null,
});
res.status(201).json({ success: true, accountId: result.lastInsertRowid });
});
// 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
app.use('/api/qbo-import', require('./routes/qbo-import'));
// .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 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, () => {});
}
});
// Catch-all: serve index.html for client-side routing
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../public/index.html'));
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`ezcheck running on http://localhost:${PORT}`);
});
module.exports = app;