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
|
||||
|
||||
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
|
||||
|
||||
@@ -11,42 +11,11 @@ Self-hosted web app for printing checks on blank check stock. Replaces ezCheckPr
|
||||
- **Frontend:** Vanilla JS, no framework
|
||||
- **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
|
||||
|
||||
### Production (Docker)
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your NTFY_URL if desired
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
@@ -59,7 +28,6 @@ If you have an existing ezCheckPrinting `.mdb` file, click **Import .mdb** inste
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cp .env.example .env
|
||||
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)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
- Page: 8.5" × 11", zero margins
|
||||
- Three slots of 3.667" each
|
||||
- MICR line at Y = 3.4" from top of slot (0.267" from bottom)
|
||||
- Three check slots of 3.5" each; remaining ~0.5" is tear-off strip
|
||||
- MICR line at 0.267" from bottom of each slot
|
||||
- MICR format: `A{routing}A {account}C {checkNo}A` (GnuMICR E-13B encoding)
|
||||
|
||||
## CI/CD
|
||||
@@ -103,11 +71,9 @@ Push to `main` triggers a GitHub Actions workflow that builds a multi-arch Docke
|
||||
|
||||
## Environment variables
|
||||
|
||||
See `.env.example`. Key variables:
|
||||
The app has no required configuration. These are set in `docker-compose.yml`:
|
||||
|
||||
| Variable | Default | Description |
|
||||
| -------- | ------- | ----------- |
|
||||
| `PORT` | `3000` | HTTP port |
|
||||
| `DB_PATH` | `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 |
|
||||
| `DB_PATH` | `/app/data/check-printing.db` | SQLite database path |
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
|
||||
## MVP
|
||||
|
||||
- [ ] 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
|
||||
- [ ] Build `public/js/app.js` -- all frontend logic via `fetch()`
|
||||
- [ ] Check ledger table -- columns: check #, date, payee, amount, memo, printed status
|
||||
- [ ] Ledger: filter by printed / unprinted
|
||||
- [ ] Ledger: sort by check number and date
|
||||
- [ ] 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
|
||||
- [ ] 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
|
||||
- [ ] "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`)
|
||||
- [ ] Delete confirmation for unprinted checks
|
||||
- [ ] Basic error display for API failures (failed PDF generation, validation errors)
|
||||
- [ ] Amount input validation -- numeric, two decimal places, greater than zero
|
||||
- [ ] Date input defaults to today
|
||||
- [ ] Check number display -- show next check number on new check form (read from account)
|
||||
- [x] Build `public/index.html` -- app shell, header with company name and current check number
|
||||
- [x] Build `public/css/style.css` -- functional, dense layout; ledger table is primary view
|
||||
- [x] Build `public/js/app.js` -- all frontend logic via `fetch()`
|
||||
- [x] Check ledger table -- columns: check #, date, payee, amount, memo, printed status
|
||||
- [x] Ledger: filter by printed / unprinted
|
||||
- [x] Ledger: sort by check number and date
|
||||
- [x] New check form -- fields: payee, amount, date, memo, note1, note2; address fields collapsed by default
|
||||
- [x] New check form -- slide-in panel or modal, not a separate page
|
||||
- [x] Edit mode for unprinted checks (inline or same panel as new check form)
|
||||
- [x] Checkbox selection of 1--3 checks for print; enforce the 3-check maximum in the UI
|
||||
- [x] "Generate PDF" button -- POST to `/api/pdf`, open resulting PDF in a new browser tab
|
||||
- [x] Reprint flow -- allow re-generating PDF for already-printed checks without re-marking (`?mark_printed=false`)
|
||||
- [x] Delete confirmation for unprinted checks
|
||||
- [x] Basic error display for API failures (failed PDF generation, validation errors)
|
||||
- [x] Amount input validation -- numeric, two decimal places, greater than zero
|
||||
- [x] Date input defaults to today
|
||||
- [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
|
||||
- [ ] 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
|
||||
|
||||
+1
-4
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
check-printing:
|
||||
image: ${DOCKERHUB_USERNAME}/check-printing:latest
|
||||
image: dogiakos/check-printing:latest
|
||||
container_name: check-printing
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -8,13 +8,10 @@ services:
|
||||
volumes:
|
||||
# Persistent data: SQLite DB lives here
|
||||
- check-printing-data:/app/data
|
||||
- ./fonts:/app/fonts:ro
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- DB_PATH=/app/data/check-printing.db
|
||||
# Full path to MICR font inside container
|
||||
- MICR_FONT_PATH=${MICR_FONT_PATH}
|
||||
|
||||
volumes:
|
||||
check-printing-data:
|
||||
|
||||
@@ -119,6 +119,8 @@ function normalizeFont(fontName, isBold) {
|
||||
return mapped;
|
||||
}
|
||||
|
||||
// TODO: Support multi-account .mdb import -- run migration per account and associate records with account_id
|
||||
|
||||
// ---- Import: T100 (account config) ------------------------------------------
|
||||
|
||||
function importAccount() {
|
||||
@@ -346,15 +348,18 @@ function importChecks() {
|
||||
|
||||
function normalizeDate(raw) {
|
||||
if (!raw) return null;
|
||||
// mdb-export outputs dates as "MM/DD/YYYY HH:MM:SS" or "YYYY-MM-DD"
|
||||
const mdyMatch = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/);
|
||||
// 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{2,4})/);
|
||||
if (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})/);
|
||||
if (isoMatch) return isoMatch[1];
|
||||
return raw;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- Run --------------------------------------------------------------------
|
||||
|
||||
+5
-3
@@ -294,7 +294,9 @@ async function generatePdf() {
|
||||
|
||||
const btn = document.getElementById('btn-generate-pdf');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Generating…';
|
||||
const countSpan = document.getElementById('selected-count');
|
||||
const savedCount = countSpan.textContent;
|
||||
countSpan.textContent = '…';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/pdf', {
|
||||
@@ -310,9 +312,9 @@ async function generatePdf() {
|
||||
window.open(URL.createObjectURL(blob), '_blank');
|
||||
await loadChecks(); // refresh to show printed status
|
||||
} catch (err) {
|
||||
countSpan.textContent = savedCount;
|
||||
btn.disabled = false;
|
||||
alert(`PDF error: ${err.message}`);
|
||||
} finally {
|
||||
refreshPdfButton();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,12 @@ app.post('/api/account/setup', (req, res) => {
|
||||
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)
|
||||
app.get('/api/account', (req, res) => {
|
||||
const db = require('./db/database');
|
||||
|
||||
@@ -4,6 +4,8 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
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
|
||||
router.get('/', (req, res) => {
|
||||
const { after, printed } = req.query;
|
||||
@@ -36,6 +38,8 @@ router.get('/:id', (req, res) => {
|
||||
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
|
||||
router.post('/', (req, res) => {
|
||||
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.
|
||||
// The GnuMICR / micrenc font maps these to specific characters.
|
||||
// Standard MICR layout: [check#] ⑆[routing]⑆ [account#]⑈
|
||||
const MICR_FONT_PATH = process.env.MICR_FONT_PATH
|
||||
? path.resolve(process.env.MICR_FONT_PATH)
|
||||
: path.join(__dirname, '../../fonts/micrenc.ttf');
|
||||
const MICR_FONT_PATH = path.join(__dirname, '../../fonts/GnuMICR.otf');
|
||||
|
||||
// Amount in words conversion
|
||||
function amountToWords(amount) {
|
||||
@@ -121,9 +119,11 @@ function generateCheckPdf(account, checks, fields) {
|
||||
doc.on('end', () => resolve(Buffer.concat(buffers)));
|
||||
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
|
||||
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
|
||||
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.
|
||||
* Falls back to Helvetica if the stored font name is not a built-in.
|
||||
|
||||
Reference in New Issue
Block a user