Fix MICR font path, date import, PDF button bug; clean up config; add TODO markers
- Hardcode GnuMICR.otf path in pdfService.js; remove MICR_FONT_PATH env var - Fix normalizeDate to handle MM/DD/YY (2-digit year) and return null on no match - Fix generatePdf button DOM bug: update span directly instead of overwriting textContent - Remove .env.example and NTFY_URL from docker-compose (app has no required config) - Remove redundant fonts volume mount from docker-compose (fonts bundled in image) - Mark MVP TODO items complete; add // TODO comments in source for post-MVP features - Update README: correct slot height, remove stale env var docs
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
MICR_FONT_PATH=/app/fonts/micrenc.ttf
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# check-printing
|
# check-printing
|
||||||
|
|
||||||
Self-hosted web app for printing checks on blank check stock. Replaces ezCheckPrinting (Halfpricesoft) — a Windows-only desktop app — with a Dockerized Node.js web app accessible on the local network.
|
Self-hosted web app for printing checks on blank check stock. A Dockerized Node.js web app accessible on the local network.
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
@@ -11,42 +11,11 @@ Self-hosted web app for printing checks on blank check stock. Replaces ezCheckPr
|
|||||||
- **Frontend:** Vanilla JS, no framework
|
- **Frontend:** Vanilla JS, no framework
|
||||||
- **Container:** Docker Compose pulling from Docker Hub
|
- **Container:** Docker Compose pulling from Docker Hub
|
||||||
|
|
||||||
## Project structure
|
|
||||||
|
|
||||||
```
|
|
||||||
check-printing/
|
|
||||||
├── src/
|
|
||||||
│ ├── routes/
|
|
||||||
│ │ ├── checks.js # CRUD for check records
|
|
||||||
│ │ └── pdf.js # PDF generation endpoint
|
|
||||||
│ ├── services/
|
|
||||||
│ │ └── pdfService.js # PDFKit rendering, MICR line, amount-to-words
|
|
||||||
│ ├── db/
|
|
||||||
│ │ ├── schema.sql # SQLite schema
|
|
||||||
│ │ └── database.js # DB connection + WAL mode
|
|
||||||
│ └── app.js # Express app, all routes
|
|
||||||
├── migrations/
|
|
||||||
│ └── import-mdb.js # One-time .mdb → SQLite migration script
|
|
||||||
├── public/
|
|
||||||
│ ├── index.html
|
|
||||||
│ ├── css/style.css
|
|
||||||
│ └── js/app.js
|
|
||||||
├── fonts/
|
|
||||||
│ └── GnuMICR.otf
|
|
||||||
├── docker/
|
|
||||||
│ └── Dockerfile
|
|
||||||
├── docker-compose.yml
|
|
||||||
├── package.json
|
|
||||||
└── .env.example
|
|
||||||
```
|
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
### Production (Docker)
|
### Production (Docker)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
|
||||||
# Edit .env with your NTFY_URL if desired
|
|
||||||
docker compose pull
|
docker compose pull
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
@@ -59,7 +28,6 @@ If you have an existing ezCheckPrinting `.mdb` file, click **Import .mdb** inste
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
cp .env.example .env
|
|
||||||
npm run dev # nodemon src/app.js
|
npm run dev # nodemon src/app.js
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -85,7 +53,7 @@ The script imports account config (T100), logo (Settings), check layout (T200),
|
|||||||
|
|
||||||
1. Select 1–3 checks from the ledger (checkbox column)
|
1. Select 1–3 checks from the ledger (checkbox column)
|
||||||
2. Click **Generate PDF**
|
2. Click **Generate PDF**
|
||||||
3. A 3-up 8.5"×11" PDF opens in a new tab — three 3.667" check slots per page
|
3. A 3-up 8.5"×11" PDF opens in a new tab — three 3.5" check slots per page
|
||||||
4. Print from the browser; checks are marked as printed in the ledger
|
4. Print from the browser; checks are marked as printed in the ledger
|
||||||
|
|
||||||
Use the **Reprint** button on printed checks to regenerate without re-marking them.
|
Use the **Reprint** button on printed checks to regenerate without re-marking them.
|
||||||
@@ -93,8 +61,8 @@ Use the **Reprint** button on printed checks to regenerate without re-marking th
|
|||||||
## Check layout
|
## Check layout
|
||||||
|
|
||||||
- Page: 8.5" × 11", zero margins
|
- Page: 8.5" × 11", zero margins
|
||||||
- Three slots of 3.667" each
|
- Three check slots of 3.5" each; remaining ~0.5" is tear-off strip
|
||||||
- MICR line at Y = 3.4" from top of slot (0.267" from bottom)
|
- MICR line at 0.267" from bottom of each slot
|
||||||
- MICR format: `A{routing}A {account}C {checkNo}A` (GnuMICR E-13B encoding)
|
- MICR format: `A{routing}A {account}C {checkNo}A` (GnuMICR E-13B encoding)
|
||||||
|
|
||||||
## CI/CD
|
## CI/CD
|
||||||
@@ -103,11 +71,9 @@ Push to `main` triggers a GitHub Actions workflow that builds a multi-arch Docke
|
|||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
See `.env.example`. Key variables:
|
The app has no required configuration. These are set in `docker-compose.yml`:
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
| -------- | ------- | ----------- |
|
| -------- | ------- | ----------- |
|
||||||
| `PORT` | `3000` | HTTP port |
|
| `PORT` | `3000` | HTTP port |
|
||||||
| `DB_PATH` | `data/check-printing.db` | SQLite database path |
|
| `DB_PATH` | `/app/data/check-printing.db` | SQLite database path |
|
||||||
| `MICR_FONT_PATH` | *(see .env.example)* | Path to GnuMICR.otf inside container |
|
|
||||||
| `NTFY_URL` | — | ntfy topic URL for push notifications |
|
|
||||||
|
|||||||
@@ -2,23 +2,23 @@
|
|||||||
|
|
||||||
## MVP
|
## MVP
|
||||||
|
|
||||||
- [ ] Build `public/index.html` -- app shell, header with company name and current check number
|
- [x] Build `public/index.html` -- app shell, header with company name and current check number
|
||||||
- [ ] Build `public/css/style.css` -- functional, dense layout; ledger table is primary view
|
- [x] Build `public/css/style.css` -- functional, dense layout; ledger table is primary view
|
||||||
- [ ] Build `public/js/app.js` -- all frontend logic via `fetch()`
|
- [x] Build `public/js/app.js` -- all frontend logic via `fetch()`
|
||||||
- [ ] Check ledger table -- columns: check #, date, payee, amount, memo, printed status
|
- [x] Check ledger table -- columns: check #, date, payee, amount, memo, printed status
|
||||||
- [ ] Ledger: filter by printed / unprinted
|
- [x] Ledger: filter by printed / unprinted
|
||||||
- [ ] Ledger: sort by check number and date
|
- [x] Ledger: sort by check number and date
|
||||||
- [ ] New check form -- fields: payee, amount, date, memo, note1, note2; address fields collapsed by default
|
- [x] New check form -- fields: payee, amount, date, memo, note1, note2; address fields collapsed by default
|
||||||
- [ ] New check form -- slide-in panel or modal, not a separate page
|
- [x] New check form -- slide-in panel or modal, not a separate page
|
||||||
- [ ] Edit mode for unprinted checks (inline or same panel as new check form)
|
- [x] Edit mode for unprinted checks (inline or same panel as new check form)
|
||||||
- [ ] Checkbox selection of 1--3 checks for print; enforce the 3-check maximum in the UI
|
- [x] Checkbox selection of 1--3 checks for print; enforce the 3-check maximum in the UI
|
||||||
- [ ] "Generate PDF" button -- POST to `/api/pdf`, open resulting PDF in a new browser tab
|
- [x] "Generate PDF" button -- POST to `/api/pdf`, open resulting PDF in a new browser tab
|
||||||
- [ ] Reprint flow -- allow re-generating PDF for already-printed checks without re-marking (`?mark_printed=false`)
|
- [x] Reprint flow -- allow re-generating PDF for already-printed checks without re-marking (`?mark_printed=false`)
|
||||||
- [ ] Delete confirmation for unprinted checks
|
- [x] Delete confirmation for unprinted checks
|
||||||
- [ ] Basic error display for API failures (failed PDF generation, validation errors)
|
- [x] Basic error display for API failures (failed PDF generation, validation errors)
|
||||||
- [ ] Amount input validation -- numeric, two decimal places, greater than zero
|
- [x] Amount input validation -- numeric, two decimal places, greater than zero
|
||||||
- [ ] Date input defaults to today
|
- [x] Date input defaults to today
|
||||||
- [ ] Check number display -- show next check number on new check form (read from account)
|
- [x] Check number display -- show next check number on new check form (read from account)
|
||||||
- [ ] Run migration against `Montana Dinosaur Center.mdb` and verify all check records import correctly
|
- [ ] Run migration against `Montana Dinosaur Center.mdb` and verify all check records import correctly
|
||||||
- [ ] Verify PDF output: spot-check field positions against a printed check from the original software
|
- [ ] Verify PDF output: spot-check field positions against a printed check from the original software
|
||||||
- [ ] Verify MICR line renders using GnuMICR.otf and lands at correct Y position
|
- [ ] Verify MICR line renders using GnuMICR.otf and lands at correct Y position
|
||||||
|
|||||||
+1
-4
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
check-printing:
|
check-printing:
|
||||||
image: ${DOCKERHUB_USERNAME}/check-printing:latest
|
image: dogiakos/check-printing:latest
|
||||||
container_name: check-printing
|
container_name: check-printing
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -8,13 +8,10 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
# Persistent data: SQLite DB lives here
|
# Persistent data: SQLite DB lives here
|
||||||
- check-printing-data:/app/data
|
- check-printing-data:/app/data
|
||||||
- ./fonts:/app/fonts:ro
|
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
- DB_PATH=/app/data/check-printing.db
|
- DB_PATH=/app/data/check-printing.db
|
||||||
# Full path to MICR font inside container
|
|
||||||
- MICR_FONT_PATH=${MICR_FONT_PATH}
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
check-printing-data:
|
check-printing-data:
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ function normalizeFont(fontName, isBold) {
|
|||||||
return mapped;
|
return mapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Support multi-account .mdb import -- run migration per account and associate records with account_id
|
||||||
|
|
||||||
// ---- Import: T100 (account config) ------------------------------------------
|
// ---- Import: T100 (account config) ------------------------------------------
|
||||||
|
|
||||||
function importAccount() {
|
function importAccount() {
|
||||||
@@ -346,15 +348,18 @@ function importChecks() {
|
|||||||
|
|
||||||
function normalizeDate(raw) {
|
function normalizeDate(raw) {
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
// mdb-export outputs dates as "MM/DD/YYYY HH:MM:SS" or "YYYY-MM-DD"
|
// mdb-export outputs dates as "MM/DD/YYYY HH:MM:SS", "MM/DD/YY", or "YYYY-MM-DD"
|
||||||
const mdyMatch = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/);
|
const mdyMatch = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})/);
|
||||||
if (mdyMatch) {
|
if (mdyMatch) {
|
||||||
const [, m, d, y] = mdyMatch;
|
const [, m, d, y] = mdyMatch;
|
||||||
return `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;
|
const year = y.length === 2
|
||||||
|
? (parseInt(y, 10) >= 50 ? '19' : '20') + y
|
||||||
|
: y;
|
||||||
|
return `${year}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
const isoMatch = raw.match(/^(\d{4}-\d{2}-\d{2})/);
|
const isoMatch = raw.match(/^(\d{4}-\d{2}-\d{2})/);
|
||||||
if (isoMatch) return isoMatch[1];
|
if (isoMatch) return isoMatch[1];
|
||||||
return raw;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Run --------------------------------------------------------------------
|
// ---- Run --------------------------------------------------------------------
|
||||||
|
|||||||
+5
-3
@@ -294,7 +294,9 @@ async function generatePdf() {
|
|||||||
|
|
||||||
const btn = document.getElementById('btn-generate-pdf');
|
const btn = document.getElementById('btn-generate-pdf');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = 'Generating…';
|
const countSpan = document.getElementById('selected-count');
|
||||||
|
const savedCount = countSpan.textContent;
|
||||||
|
countSpan.textContent = '…';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/pdf', {
|
const res = await fetch('/api/pdf', {
|
||||||
@@ -310,9 +312,9 @@ async function generatePdf() {
|
|||||||
window.open(URL.createObjectURL(blob), '_blank');
|
window.open(URL.createObjectURL(blob), '_blank');
|
||||||
await loadChecks(); // refresh to show printed status
|
await loadChecks(); // refresh to show printed status
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
countSpan.textContent = savedCount;
|
||||||
|
btn.disabled = false;
|
||||||
alert(`PDF error: ${err.message}`);
|
alert(`PDF error: ${err.message}`);
|
||||||
} finally {
|
|
||||||
refreshPdfButton();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,12 @@ app.post('/api/account/setup', (req, res) => {
|
|||||||
res.status(201).json({ success: true });
|
res.status(201).json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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)
|
// Account info endpoint (read-only for Phase 1)
|
||||||
app.get('/api/account', (req, res) => {
|
app.get('/api/account', (req, res) => {
|
||||||
const db = require('./db/database');
|
const db = require('./db/database');
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const db = require('../db/database');
|
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 - list all checks, newest first
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
const { after, printed } = req.query;
|
const { after, printed } = req.query;
|
||||||
@@ -36,6 +38,8 @@ router.get('/:id', (req, res) => {
|
|||||||
res.json(check);
|
res.json(check);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Add payee address book -- store and recall payee name + address lines, autocomplete on new check form
|
||||||
|
|
||||||
// POST /api/checks - create a new check
|
// POST /api/checks - create a new check
|
||||||
router.post('/', (req, res) => {
|
router.post('/', (req, res) => {
|
||||||
const { payee, amount, check_date, memo, note1, note2,
|
const { payee, amount, check_date, memo, note1, note2,
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ const MICR_Y_IN = SLOT_HEIGHT_IN - 0.267; // 0.267" from bottom of slot
|
|||||||
// MICR line format: transit symbol (⑆) and on-us symbol (⑈) in E-13B encoding.
|
// MICR line format: transit symbol (⑆) and on-us symbol (⑈) in E-13B encoding.
|
||||||
// The GnuMICR / micrenc font maps these to specific characters.
|
// The GnuMICR / micrenc font maps these to specific characters.
|
||||||
// Standard MICR layout: [check#] ⑆[routing]⑆ [account#]⑈
|
// Standard MICR layout: [check#] ⑆[routing]⑆ [account#]⑈
|
||||||
const MICR_FONT_PATH = process.env.MICR_FONT_PATH
|
const MICR_FONT_PATH = path.join(__dirname, '../../fonts/GnuMICR.otf');
|
||||||
? path.resolve(process.env.MICR_FONT_PATH)
|
|
||||||
: path.join(__dirname, '../../fonts/micrenc.ttf');
|
|
||||||
|
|
||||||
// Amount in words conversion
|
// Amount in words conversion
|
||||||
function amountToWords(amount) {
|
function amountToWords(amount) {
|
||||||
@@ -121,9 +119,11 @@ function generateCheckPdf(account, checks, fields) {
|
|||||||
doc.on('end', () => resolve(Buffer.concat(buffers)));
|
doc.on('end', () => resolve(Buffer.concat(buffers)));
|
||||||
doc.on('error', reject);
|
doc.on('error', reject);
|
||||||
|
|
||||||
|
// TODO: Add 1-up with stub layout -- render Stub-prefixed fields from layout_fields alongside the check body
|
||||||
|
|
||||||
// Separate layout fields into check body vs stub fields
|
// Separate layout fields into check body vs stub fields
|
||||||
const bodyFields = fields.filter(f => !f.field_name.startsWith('Stub'));
|
const bodyFields = fields.filter(f => !f.field_name.startsWith('Stub'));
|
||||||
const stubFields = fields.filter(f => f.field_name.startsWith('Stub'));
|
const stubFields = fields.filter(f => f.field_name.startsWith('Stub')); // eslint-disable-line no-unused-vars
|
||||||
|
|
||||||
// We always render 3 slots; empty slots get a blank placeholder
|
// We always render 3 slots; empty slots get a blank placeholder
|
||||||
for (let slot = 0; slot < 3; slot++) {
|
for (let slot = 0; slot < 3; slot++) {
|
||||||
@@ -261,6 +261,8 @@ function resolveFieldValue(fieldName, check, account) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add visual layout editor -- UI to nudge field X/Y positions and printer offset calibration (offset_left/right/up/down)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the PDFKit font based on a layout field's font properties.
|
* Sets the PDFKit font based on a layout field's font properties.
|
||||||
* Falls back to Helvetica if the stored font name is not a built-in.
|
* Falls back to Helvetica if the stored font name is not a built-in.
|
||||||
|
|||||||
Reference in New Issue
Block a user