Add deposit slip and report generation

- New Deposits tab with ledger: date, checks total, cash, deposit total, item count, status
- Slide-in deposit panel: date, currency, coin, cash back, dynamic check entry rows, live totals
- Save deposit, then generate Deposit Slip or Deposit Report PDF
- Deposit slip: 3.375" x 8.5" portrait with Style A background drawn server-side,
  digit-column amounts, GnuMICR routing/account line rotated 90 deg, rotated
  deposit total and check count in left margin
- Deposit report: plain Courier ledger with depositor/bank info, check grid, totals
- deposits and deposit_items tables in schema; ON DELETE CASCADE for items
- Routes: GET/POST/PUT/DELETE /api/deposits, POST /api/deposit-pdf
- Generating a slip marks deposit as printed; date range and status filters
- README updated to describe deposit slip feature
This commit is contained in:
2026-03-13 08:43:34 -06:00
parent a89db179cd
commit 4fb7fd209c
9 changed files with 1345 additions and 3 deletions
+144
View File
@@ -0,0 +1,144 @@
'use strict';
const express = require('express');
const router = express.Router();
const db = require('../db/database');
// Helper: fetch deposit with items
function getDepositWithItems(id) {
const deposit = db.prepare('SELECT * FROM deposits WHERE id = ?').get(id);
if (!deposit) return null;
deposit.items = db.prepare(
'SELECT * FROM deposit_items WHERE deposit_id = ? ORDER BY sort_order ASC, id ASC'
).all(id);
return deposit;
}
// GET /api/deposits?account_id=X
router.get('/', (req, res) => {
const { account_id } = req.query;
if (!account_id) return res.status(400).json({ error: 'account_id is required.' });
const deposits = db.prepare(`
SELECT d.*, COUNT(di.id) AS item_count,
COALESCE(SUM(di.amount), 0) AS checks_total
FROM deposits d
LEFT JOIN deposit_items di ON di.deposit_id = d.id
WHERE d.account_id = ?
GROUP BY d.id
ORDER BY d.deposit_date DESC, d.id DESC
`).all(account_id);
res.json(deposits);
});
// GET /api/deposits/:id
router.get('/:id', (req, res) => {
const deposit = getDepositWithItems(req.params.id);
if (!deposit) return res.status(404).json({ error: 'Deposit not found.' });
res.json(deposit);
});
// POST /api/deposits
router.post('/', (req, res) => {
const { account_id, deposit_date, currency, coin, cash_back, items } = req.body;
if (!account_id) return res.status(400).json({ error: 'account_id is required.' });
if (!deposit_date) return res.status(400).json({ error: 'deposit_date is required.' });
const insert = db.transaction(() => {
const result = db.prepare(`
INSERT INTO deposits (account_id, deposit_date, currency, coin, cash_back)
VALUES (?, ?, ?, ?, ?)
`).run(
account_id,
deposit_date,
parseFloat(currency) || 0,
parseFloat(coin) || 0,
parseFloat(cash_back) || 0,
);
const depositId = result.lastInsertRowid;
if (Array.isArray(items)) {
const stmt = db.prepare(`
INSERT INTO deposit_items (deposit_id, sort_order, check_no, bank_no, payee, memo, amount)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
items.forEach((item, i) => {
stmt.run(
depositId, i,
item.check_no || null,
item.bank_no || null,
item.payee || null,
item.memo || null,
parseFloat(item.amount) || 0,
);
});
}
return depositId;
});
const depositId = insert();
res.status(201).json(getDepositWithItems(depositId));
});
// PUT /api/deposits/:id
router.put('/:id', (req, res) => {
const existing = db.prepare('SELECT id FROM deposits WHERE id = ?').get(req.params.id);
if (!existing) return res.status(404).json({ error: 'Deposit not found.' });
const { deposit_date, currency, coin, cash_back, items } = req.body;
if (!deposit_date) return res.status(400).json({ error: 'deposit_date is required.' });
const update = db.transaction(() => {
db.prepare(`
UPDATE deposits SET deposit_date = ?, currency = ?, coin = ?, cash_back = ?
WHERE id = ?
`).run(
deposit_date,
parseFloat(currency) || 0,
parseFloat(coin) || 0,
parseFloat(cash_back) || 0,
req.params.id,
);
if (Array.isArray(items)) {
db.prepare('DELETE FROM deposit_items WHERE deposit_id = ?').run(req.params.id);
const stmt = db.prepare(`
INSERT INTO deposit_items (deposit_id, sort_order, check_no, bank_no, payee, memo, amount)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
items.forEach((item, i) => {
stmt.run(
req.params.id, i,
item.check_no || null,
item.bank_no || null,
item.payee || null,
item.memo || null,
parseFloat(item.amount) || 0,
);
});
}
});
update();
res.json(getDepositWithItems(req.params.id));
});
// DELETE /api/deposits/:id
router.delete('/:id', (req, res) => {
const existing = db.prepare('SELECT id FROM deposits WHERE id = ?').get(req.params.id);
if (!existing) return res.status(404).json({ error: 'Deposit not found.' });
// deposit_items deleted via ON DELETE CASCADE
db.prepare('DELETE FROM deposits WHERE id = ?').run(req.params.id);
res.status(204).end();
});
// PATCH /api/deposits/:id/mark-printed
router.patch('/:id/mark-printed', (req, res) => {
db.prepare('UPDATE deposits SET printed = 1 WHERE id = ?').run(req.params.id);
res.json({ ok: true });
});
module.exports = router;