diff --git a/.env.example b/.env.example deleted file mode 100644 index 7adb4b8..0000000 --- a/.env.example +++ /dev/null @@ -1 +0,0 @@ -MICR_FONT_PATH=/app/fonts/micrenc.ttf \ No newline at end of file diff --git a/README.md b/README.md index b40f6b1..550777b 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/TODO.md b/TODO.md index 9bc0a60..e987278 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 4f19dbf..28f81b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/migrations/import-mdb.js b/migrations/import-mdb.js index 424432c..fb1d824 100644 --- a/migrations/import-mdb.js +++ b/migrations/import-mdb.js @@ -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 -------------------------------------------------------------------- diff --git a/public/js/app.js b/public/js/app.js index d97b703..a9cdc72 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -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(); } } diff --git a/src/app.js b/src/app.js index 7ba6627..c163587 100644 --- a/src/app.js +++ b/src/app.js @@ -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'); diff --git a/src/routes/checks.js b/src/routes/checks.js index 0fef4f1..b98cf1f 100644 --- a/src/routes/checks.js +++ b/src/routes/checks.js @@ -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, diff --git a/src/services/pdfService.js b/src/services/pdfService.js index 638e74b..0d98464 100644 --- a/src/services/pdfService.js +++ b/src/services/pdfService.js @@ -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.