Compare commits
10 Commits
c4e4a8c246
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d105bce21 | |||
| 0b21f4ea3c | |||
| f91fc7bd8a | |||
| bb935acfa9 | |||
| 3957cf5518 | |||
| 4a47394923 | |||
| 189ae53d34 | |||
| 657de9e61a | |||
| 0ee95dbb09 | |||
| a2de7e2d9d |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ezcheck",
|
"name": "ezcheck",
|
||||||
"version": "0.4.3",
|
"version": "0.4.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ezcheck",
|
"name": "ezcheck",
|
||||||
"version": "0.4.3",
|
"version": "0.4.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-sqlite3": "^9.4.3",
|
"better-sqlite3": "^9.4.3",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ezcheck",
|
"name": "ezcheck",
|
||||||
"version": "0.4.3",
|
"version": "0.4.6",
|
||||||
"description": "Self-hosted check printing web app",
|
"description": "Self-hosted check printing web app",
|
||||||
"main": "src/app.js",
|
"main": "src/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -679,7 +679,7 @@ input[type="file"] {
|
|||||||
right: 0;
|
right: 0;
|
||||||
width: 560px;
|
width: 560px;
|
||||||
max-width: 98vw;
|
max-width: 98vw;
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
z-index: 101;
|
z-index: 101;
|
||||||
box-shadow: -4px 0 24px rgba(0,0,0,0.15);
|
box-shadow: -4px 0 24px rgba(0,0,0,0.15);
|
||||||
@@ -687,7 +687,7 @@ input[type="file"] {
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
#deposit-panel.open { transform: translateX(0); }
|
#deposit-panel.open { transform: translateX(0); }
|
||||||
|
|
||||||
@@ -697,6 +697,8 @@ input[type="file"] {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dep-summary {
|
.dep-summary {
|
||||||
|
|||||||
+2
-1
@@ -886,7 +886,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="layout-save-status" style="font-size:11px;color:var(--text-muted);min-width:56px"></div>
|
<div id="layout-save-status" style="font-size:11px;color:var(--text-muted);min-width:56px"></div>
|
||||||
<div style="margin-left:auto">
|
<div style="margin-left:auto;display:flex;gap:8px;align-items:center">
|
||||||
|
<button id="btn-layout-preview" class="btn-secondary btn-sm">⎙ Preview PDF</button>
|
||||||
<button id="btn-layout-reset" class="btn-secondary btn-sm" data-admin-only>↺ Reset to Default</button>
|
<button id="btn-layout-reset" class="btn-secondary btn-sm" data-admin-only>↺ Reset to Default</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+99
-12
@@ -780,6 +780,18 @@ async function deleteCheck(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Firefox on iOS blocks window.open(blob:) in a new tab; use a temporary <a download> instead.
|
||||||
|
function openPdfBlob(blob, filename) {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
||||||
|
}
|
||||||
|
|
||||||
async function generatePdf() {
|
async function generatePdf() {
|
||||||
const ids = [...state.selected];
|
const ids = [...state.selected];
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
@@ -801,7 +813,7 @@ async function generatePdf() {
|
|||||||
throw new Error(err.error || res.statusText);
|
throw new Error(err.error || res.statusText);
|
||||||
}
|
}
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
window.open(URL.createObjectURL(blob), '_blank');
|
openPdfBlob(blob, 'checks.pdf');
|
||||||
await loadChecks(); // refresh to show printed status
|
await loadChecks(); // refresh to show printed status
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
countSpan.textContent = savedCount;
|
countSpan.textContent = savedCount;
|
||||||
@@ -1430,14 +1442,18 @@ async function openDepositPanel(id = null) {
|
|||||||
document.getElementById('dep-coin').value = dep.coin || '';
|
document.getElementById('dep-coin').value = dep.coin || '';
|
||||||
document.getElementById('dep-cashback').value = dep.cash_back || '';
|
document.getElementById('dep-cashback').value = dep.cash_back || '';
|
||||||
depState.items = (dep.items || []).map(it => ({ ...it }));
|
depState.items = (dep.items || []).map(it => ({ ...it }));
|
||||||
|
while (depState.items.length < 30) depState.items.push(newDepItem());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Error loading deposit: ' + err.message);
|
alert('Error loading deposit: ' + err.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
depState.items = [newDepItem()];
|
depState.items = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always start with at least 30 slots (one full deposit slip page)
|
||||||
|
while (depState.items.length < 30) depState.items.push(newDepItem());
|
||||||
|
|
||||||
renderDepItems();
|
renderDepItems();
|
||||||
recalcDepTotals();
|
recalcDepTotals();
|
||||||
|
|
||||||
@@ -1463,6 +1479,13 @@ function newDepItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderDepItems() {
|
function renderDepItems() {
|
||||||
|
const addBtn = document.getElementById('btn-add-dep-item');
|
||||||
|
if (addBtn) {
|
||||||
|
const count = depState.items.length;
|
||||||
|
addBtn.hidden = count >= 60; // hide once back page rows are added
|
||||||
|
addBtn.disabled = count >= 60;
|
||||||
|
addBtn.textContent = 'Add Back Page Rows';
|
||||||
|
}
|
||||||
const tbody = document.getElementById('dep-items-tbody');
|
const tbody = document.getElementById('dep-items-tbody');
|
||||||
tbody.innerHTML = depState.items.map((item, i) => `
|
tbody.innerHTML = depState.items.map((item, i) => `
|
||||||
<tr data-idx="${i}">
|
<tr data-idx="${i}">
|
||||||
@@ -1485,6 +1508,8 @@ function renderDepItems() {
|
|||||||
tbody.querySelectorAll('.dep-item-remove').forEach(btn => {
|
tbody.querySelectorAll('.dep-item-remove').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
depState.items.splice(parseInt(btn.dataset.idx, 10), 1);
|
depState.items.splice(parseInt(btn.dataset.idx, 10), 1);
|
||||||
|
// Maintain 30-slot minimum (one full slip page)
|
||||||
|
while (depState.items.length < 30) depState.items.push(newDepItem());
|
||||||
renderDepItems();
|
renderDepItems();
|
||||||
recalcDepTotals();
|
recalcDepTotals();
|
||||||
});
|
});
|
||||||
@@ -1595,7 +1620,7 @@ async function generateDepositPdf(type) {
|
|||||||
throw new Error(err.error || res.statusText);
|
throw new Error(err.error || res.statusText);
|
||||||
}
|
}
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
window.open(URL.createObjectURL(blob), '_blank');
|
openPdfBlob(blob, type === 'slip' ? 'deposit-slip.pdf' : 'deposit-report.pdf');
|
||||||
if (type === 'slip') await loadDeposits();
|
if (type === 'slip') await loadDeposits();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('PDF error: ' + err.message);
|
alert('PDF error: ' + err.message);
|
||||||
@@ -1888,7 +1913,7 @@ async function init() {
|
|||||||
document.getElementById('dep-panel-overlay').addEventListener('click', closeDepositPanel);
|
document.getElementById('dep-panel-overlay').addEventListener('click', closeDepositPanel);
|
||||||
document.getElementById('btn-save-deposit').addEventListener('click', saveDeposit);
|
document.getElementById('btn-save-deposit').addEventListener('click', saveDeposit);
|
||||||
document.getElementById('btn-add-dep-item').addEventListener('click', () => {
|
document.getElementById('btn-add-dep-item').addEventListener('click', () => {
|
||||||
depState.items.push(newDepItem());
|
while (depState.items.length < 60) depState.items.push(newDepItem());
|
||||||
renderDepItems();
|
renderDepItems();
|
||||||
});
|
});
|
||||||
document.getElementById('btn-dep-slip').addEventListener('click', () => generateDepositPdf('slip'));
|
document.getElementById('btn-dep-slip').addEventListener('click', () => generateDepositPdf('slip'));
|
||||||
@@ -1982,6 +2007,7 @@ async function init() {
|
|||||||
document.getElementById('nudge-left').addEventListener('click', () => nudgeLayoutField(-1, 0));
|
document.getElementById('nudge-left').addEventListener('click', () => nudgeLayoutField(-1, 0));
|
||||||
document.getElementById('nudge-right').addEventListener('click', () => nudgeLayoutField( 1, 0));
|
document.getElementById('nudge-right').addEventListener('click', () => nudgeLayoutField( 1, 0));
|
||||||
document.getElementById('btn-layout-reset').addEventListener('click', resetLayoutToDefault);
|
document.getElementById('btn-layout-reset').addEventListener('click', resetLayoutToDefault);
|
||||||
|
document.getElementById('btn-layout-preview').addEventListener('click', previewLayoutPdf);
|
||||||
|
|
||||||
// Initial auth check → loads app if already signed in
|
// Initial auth check → loads app if already signed in
|
||||||
const authed = await checkAuth();
|
const authed = await checkAuth();
|
||||||
@@ -2082,6 +2108,12 @@ function populateLayoutDropdown() {
|
|||||||
).join('');
|
).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Printing safe zone for user-adjustable fields (inches). MICR is exempt.
|
||||||
|
const SAFE_LEFT = 11 / 64;
|
||||||
|
const SAFE_RIGHT = 8.5 - 11 / 64;
|
||||||
|
const SAFE_TOP = 13 / 64;
|
||||||
|
const SAFE_BOTTOM = 3.5 - 0.5;
|
||||||
|
|
||||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||||
function svgEl(tag, attrs, text) {
|
function svgEl(tag, attrs, text) {
|
||||||
const el = document.createElementNS(SVG_NS, tag);
|
const el = document.createElementNS(SVG_NS, tag);
|
||||||
@@ -2104,11 +2136,41 @@ function renderLayoutCanvas() {
|
|||||||
// White check background
|
// White check background
|
||||||
svg.appendChild(svgEl('rect', { x:0, y:0, width:W, height:H, fill:'#fff', stroke:'#bbb', 'stroke-width':1 }));
|
svg.appendChild(svgEl('rect', { x:0, y:0, width:W, height:H, fill:'#fff', stroke:'#bbb', 'stroke-width':1 }));
|
||||||
|
|
||||||
|
// Grid at 1/8" increments (darker every 1/4", darkest on whole inches)
|
||||||
|
for (let n8 = 1; n8 < Math.ceil(8.5 * 8); n8++) {
|
||||||
|
const x = (n8 / 8) * SCALE;
|
||||||
|
if (x >= W) break;
|
||||||
|
const isInch = n8 % 8 === 0;
|
||||||
|
const isQtr = n8 % 2 === 0;
|
||||||
|
const stroke = isInch ? '#d0d7de' : isQtr ? '#e4e8ed' : '#f0f2f5';
|
||||||
|
svg.appendChild(svgEl('line', { x1:x, y1:0, x2:x, y2:H, stroke, 'stroke-width':1 }));
|
||||||
|
}
|
||||||
|
for (let n8 = 1; n8 < Math.ceil(3.5 * 8); n8++) {
|
||||||
|
const y = (n8 / 8) * SCALE;
|
||||||
|
if (y >= H) break;
|
||||||
|
const isInch = n8 % 8 === 0;
|
||||||
|
const isQtr = n8 % 2 === 0;
|
||||||
|
const stroke = isInch ? '#d0d7de' : isQtr ? '#e4e8ed' : '#f0f2f5';
|
||||||
|
svg.appendChild(svgEl('line', { x1:0, y1:y, x2:W, y2:y, stroke, 'stroke-width':1 }));
|
||||||
|
}
|
||||||
|
|
||||||
// MICR reference line
|
// MICR reference line
|
||||||
const micrY = (3.5 - 0.267) * SCALE;
|
const micrY = (3.5 - 0.267) * SCALE;
|
||||||
svg.appendChild(svgEl('line', { x1:0, y1:micrY, x2:W, y2:micrY, stroke:'#ccc', 'stroke-width':1, 'stroke-dasharray':'4,4' }));
|
svg.appendChild(svgEl('line', { x1:0, y1:micrY, x2:W, y2:micrY, stroke:'#ccc', 'stroke-width':1, 'stroke-dasharray':'4,4' }));
|
||||||
svg.appendChild(svgEl('text', { x:4, y:micrY - 3, 'font-size':9, fill:'#bbb', 'font-family':'sans-serif' }, 'MICR'));
|
svg.appendChild(svgEl('text', { x:4, y:micrY - 3, 'font-size':9, fill:'#bbb', 'font-family':'sans-serif' }, 'MICR'));
|
||||||
|
|
||||||
|
// Safe zone outline for user-adjustable fields
|
||||||
|
svg.appendChild(svgEl('rect', {
|
||||||
|
x: SAFE_LEFT * SCALE,
|
||||||
|
y: SAFE_TOP * SCALE,
|
||||||
|
width: (SAFE_RIGHT - SAFE_LEFT) * SCALE,
|
||||||
|
height: (SAFE_BOTTOM - SAFE_TOP) * SCALE,
|
||||||
|
fill: 'none',
|
||||||
|
stroke: '#60a5fa',
|
||||||
|
'stroke-width': 1,
|
||||||
|
'stroke-dasharray': '3,3',
|
||||||
|
}));
|
||||||
|
|
||||||
for (const f of layoutState.fields) {
|
for (const f of layoutState.fields) {
|
||||||
const g = createFieldSvgElement(f, SCALE, layoutState.selectedId === f.id);
|
const g = createFieldSvgElement(f, SCALE, layoutState.selectedId === f.id);
|
||||||
svg.appendChild(g);
|
svg.appendChild(g);
|
||||||
@@ -2333,11 +2395,11 @@ function onLayoutDragMove(e) {
|
|||||||
const dy = (e.clientY - layoutDrag.mouseY) / layoutState.scale;
|
const dy = (e.clientY - layoutDrag.mouseY) / layoutState.scale;
|
||||||
const f = layoutState.fields.find(x => x.id === layoutDrag.fieldId);
|
const f = layoutState.fields.find(x => x.id === layoutDrag.fieldId);
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
f.x_pos = clampIn(round16(layoutDrag.origX + dx), 0, 8.5);
|
f.x_pos = clampIn(round16(layoutDrag.origX + dx), SAFE_LEFT, SAFE_RIGHT);
|
||||||
f.y_pos = clampIn(round16(layoutDrag.origY + dy), 0, 3.5);
|
f.y_pos = clampIn(round16(layoutDrag.origY + dy), SAFE_TOP, SAFE_BOTTOM);
|
||||||
if (layoutDrag.moveEnd) {
|
if (layoutDrag.moveEnd) {
|
||||||
f.x_end_pos = clampIn(round16(layoutDrag.origX2 + dx), 0, 8.5);
|
f.x_end_pos = clampIn(round16(layoutDrag.origX2 + dx), SAFE_LEFT, SAFE_RIGHT);
|
||||||
f.y_end_pos = clampIn(round16(layoutDrag.origY2 + dy), 0, 3.5);
|
f.y_end_pos = clampIn(round16(layoutDrag.origY2 + dy), SAFE_TOP, SAFE_BOTTOM);
|
||||||
}
|
}
|
||||||
// Update just the dragged element for smooth performance
|
// Update just the dragged element for smooth performance
|
||||||
const svg = document.querySelector('#layout-canvas-container svg');
|
const svg = document.querySelector('#layout-canvas-container svg');
|
||||||
@@ -2364,11 +2426,11 @@ function nudgeLayoutField(dx, dy) {
|
|||||||
const f = layoutState.fields.find(x => x.id === layoutState.selectedId);
|
const f = layoutState.fields.find(x => x.id === layoutState.selectedId);
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
const S = 1 / 16;
|
const S = 1 / 16;
|
||||||
f.x_pos = clampIn(round16(f.x_pos + dx * S), 0, 8.5);
|
f.x_pos = clampIn(round16(f.x_pos + dx * S), SAFE_LEFT, SAFE_RIGHT);
|
||||||
f.y_pos = clampIn(round16(f.y_pos + dy * S), 0, 3.5);
|
f.y_pos = clampIn(round16(f.y_pos + dy * S), SAFE_TOP, SAFE_BOTTOM);
|
||||||
if (f.field_type === 'Line' || f.field_type === 'Graph') {
|
if (f.field_type === 'Line' || f.field_type === 'Graph') {
|
||||||
f.x_end_pos = clampIn(round16(f.x_end_pos + dx * S), 0, 8.5);
|
f.x_end_pos = clampIn(round16(f.x_end_pos + dx * S), SAFE_LEFT, SAFE_RIGHT);
|
||||||
f.y_end_pos = clampIn(round16(f.y_end_pos + dy * S), 0, 3.5);
|
f.y_end_pos = clampIn(round16(f.y_end_pos + dy * S), SAFE_TOP, SAFE_BOTTOM);
|
||||||
}
|
}
|
||||||
updateLayoutSidebar(f);
|
updateLayoutSidebar(f);
|
||||||
renderLayoutCanvas();
|
renderLayoutCanvas();
|
||||||
@@ -2395,6 +2457,31 @@ async function saveLayoutField(f) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function previewLayoutPdf() {
|
||||||
|
const btn = document.getElementById('btn-layout-preview');
|
||||||
|
const orig = btn.textContent;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '…';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/pdf/preview', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ account_id: state.activeAccountId }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
|
throw new Error(err.error || res.statusText);
|
||||||
|
}
|
||||||
|
const blob = await res.blob();
|
||||||
|
openPdfBlob(blob, 'layout-preview.pdf');
|
||||||
|
} catch (err) {
|
||||||
|
alert('Preview error: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = orig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function resetLayoutToDefault() {
|
async function resetLayoutToDefault() {
|
||||||
if (!confirm('Reset all layout fields to default positions? This cannot be undone.')) return;
|
if (!confirm('Reset all layout fields to default positions? This cannot be undone.')) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -70,4 +70,55 @@ router.post('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/pdf/preview
|
||||||
|
* Body: { account_id: X }
|
||||||
|
*
|
||||||
|
* Generates a layout preview PDF using dummy check data — no real checks touched.
|
||||||
|
* Shows all three slots filled with sample data so every visible field is visible.
|
||||||
|
*/
|
||||||
|
router.post('/preview', async (req, res) => {
|
||||||
|
const resolvedAccountId = parseInt(req.body.account_id, 10);
|
||||||
|
if (!resolvedAccountId) return res.status(400).json({ error: 'account_id required' });
|
||||||
|
|
||||||
|
const account = db.prepare('SELECT * FROM account WHERE id = ?').get(resolvedAccountId);
|
||||||
|
if (!account) return res.status(404).json({ error: 'Account not found.' });
|
||||||
|
|
||||||
|
const fields = db.prepare('SELECT * FROM layout_fields WHERE account_id = ?').all(resolvedAccountId);
|
||||||
|
|
||||||
|
const DUMMY_CHECK = {
|
||||||
|
id: 0,
|
||||||
|
check_no: 1001,
|
||||||
|
payee: 'Sample Payee Name',
|
||||||
|
amount: 1234.56,
|
||||||
|
check_date: new Date().toISOString().slice(0, 10),
|
||||||
|
memo: 'Sample Memo',
|
||||||
|
payee_address1: '123 Sample Street',
|
||||||
|
payee_address2: 'City, ST 12345',
|
||||||
|
payee_address3: null,
|
||||||
|
payee_address4: null,
|
||||||
|
printed: 0,
|
||||||
|
account_id: resolvedAccountId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
{ ...DUMMY_CHECK, check_no: 1001 },
|
||||||
|
{ ...DUMMY_CHECK, check_no: 1002 },
|
||||||
|
{ ...DUMMY_CHECK, check_no: 1003 },
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pdfBuffer = await generateCheckPdf(account, checks, fields);
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Disposition': 'inline; filename="layout-preview.pdf"',
|
||||||
|
'Content-Length': pdfBuffer.length,
|
||||||
|
});
|
||||||
|
res.send(pdfBuffer);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Preview PDF error:', err);
|
||||||
|
res.status(500).json({ error: 'Preview generation failed.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const SL = {
|
|||||||
cX: 0.65,
|
cX: 0.65,
|
||||||
|
|
||||||
// ── Depositor block ───────────────────────────────────────────────────────
|
// ── Depositor block ───────────────────────────────────────────────────────
|
||||||
depositorY: 0.28, // Y of company name (first depositor line)
|
depositorY: 0.42, // Y of company name (first depositor line)
|
||||||
|
|
||||||
// ── Date ─────────────────────────────────────────────────────────────────
|
// ── Date ─────────────────────────────────────────────────────────────────
|
||||||
dateY: 1.38, // Y of DATE label
|
dateY: 1.38, // Y of DATE label
|
||||||
@@ -87,8 +87,8 @@ const SL = {
|
|||||||
checkCountValY: 6.1, // check count value start
|
checkCountValY: 6.1, // check count value start
|
||||||
|
|
||||||
// ── Colours ───────────────────────────────────────────────────────────────
|
// ── Colours ───────────────────────────────────────────────────────────────
|
||||||
bgLineColor: '#888888',
|
bgLineColor: '#333333',
|
||||||
bgLabelColor: '#444444',
|
bgLabelColor: '#111111',
|
||||||
bgHeaderColor: '#000000',
|
bgHeaderColor: '#000000',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -238,7 +238,17 @@ function generateDepositSlip(account, deposit, items) {
|
|||||||
const depositTotal = subTotal - (deposit.cash_back || 0);
|
const depositTotal = subTotal - (deposit.cash_back || 0);
|
||||||
const checkCount = items.length;
|
const checkCount = items.length;
|
||||||
|
|
||||||
const totalRows = SL.firstCheckRow + SL.maxChecks;
|
// Split items: first 30 on front, up to 30 more on back
|
||||||
|
const frontItems = items.slice(0, SL.maxChecks);
|
||||||
|
const backItems = items.slice(SL.maxChecks, SL.maxChecks * 2);
|
||||||
|
const hasBackPage = backItems.some(it => (it.amount || 0) > 0 || it.check_no || it.payee);
|
||||||
|
const backTotal = hasBackPage ? backItems.reduce((s, i) => s + (i.amount || 0), 0) : 0;
|
||||||
|
|
||||||
|
// When back page exists, add one extra row on front for "FROM REVERSE"
|
||||||
|
const fromReverseRow = hasBackPage ? SL.firstCheckRow + SL.maxChecks : null;
|
||||||
|
const totalRows = fromReverseRow != null
|
||||||
|
? SL.firstCheckRow + SL.maxChecks + 1
|
||||||
|
: SL.firstCheckRow + SL.maxChecks;
|
||||||
const totalRowY_ = rowTopY(totalRows);
|
const totalRowY_ = rowTopY(totalRows);
|
||||||
const gridBottom = totalRowY_ + SL.rowH;
|
const gridBottom = totalRowY_ + SL.rowH;
|
||||||
|
|
||||||
@@ -304,7 +314,7 @@ function generateDepositSlip(account, deposit, items) {
|
|||||||
doc.text('TOTAL $', SL.cX * PT, rowY(totalRows) * PT - 5, { lineBreak: false });
|
doc.text('TOTAL $', SL.cX * PT, rowY(totalRows) * PT - 5, { lineBreak: false });
|
||||||
|
|
||||||
// Top disclaimer (above grid)
|
// Top disclaimer (above grid)
|
||||||
doc.font('Helvetica').fontSize(5).fillColor('#666666')
|
doc.font('Helvetica').fontSize(5).fillColor('#333333')
|
||||||
.text(
|
.text(
|
||||||
'DEPOSITS MAY NOT BE AVAILABLE FOR IMMEDIATE WITHDRAWAL',
|
'DEPOSITS MAY NOT BE AVAILABLE FOR IMMEDIATE WITHDRAWAL',
|
||||||
SL.cX * PT, SL.disclaimerY * PT,
|
SL.cX * PT, SL.disclaimerY * PT,
|
||||||
@@ -312,7 +322,7 @@ function generateDepositSlip(account, deposit, items) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Bottom disclaimer (below grid)
|
// Bottom disclaimer (below grid)
|
||||||
doc.font('Helvetica').fontSize(5).fillColor('#666666')
|
doc.font('Helvetica').fontSize(5).fillColor('#333333')
|
||||||
.text(
|
.text(
|
||||||
'Checks and other items are received for deposit subject to the provisions of the Uniform Commercial Code or any applicable collection agreements.',
|
'Checks and other items are received for deposit subject to the provisions of the Uniform Commercial Code or any applicable collection agreements.',
|
||||||
SL.cX * PT, (gridBottom + 0.05) * PT,
|
SL.cX * PT, (gridBottom + 0.05) * PT,
|
||||||
@@ -321,7 +331,7 @@ function generateDepositSlip(account, deposit, items) {
|
|||||||
|
|
||||||
// DEPOSIT TICKET header
|
// DEPOSIT TICKET header
|
||||||
doc.font('Helvetica-Bold').fontSize(9).fillColor(SL.bgHeaderColor)
|
doc.font('Helvetica-Bold').fontSize(9).fillColor(SL.bgHeaderColor)
|
||||||
.text('D E P O S I T T I C K E T', SL.cX * PT, 0.08 * PT,
|
.text('D E P O S I T T I C K E T', SL.cX * PT, 0.20 * PT,
|
||||||
{ width: (SL.W - SL.cX - 0.05) * PT, align: 'center', lineBreak: false });
|
{ width: (SL.W - SL.cX - 0.05) * PT, align: 'center', lineBreak: false });
|
||||||
|
|
||||||
// ── Depositor block — account info, then bank info stacked below ────────
|
// ── Depositor block — account info, then bank info stacked below ────────
|
||||||
@@ -368,25 +378,32 @@ function generateDepositSlip(account, deposit, items) {
|
|||||||
|
|
||||||
function drawAmountRow(amount, rowIdx) {
|
function drawAmountRow(amount, rowIdx) {
|
||||||
const y = (rowY(rowIdx) - 0.015) * PT;
|
const y = (rowY(rowIdx) - 0.015) * PT;
|
||||||
doc.font('Courier').fontSize(8).fillColor('#000000');
|
doc.font('Courier-Bold').fontSize(8).fillColor('#000000');
|
||||||
drawDigitAmount(doc, amount, dollarsRightX, y);
|
drawDigitAmount(doc, amount, dollarsRightX, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawAmountRow(deposit.currency || 0, SL.currencyRow);
|
drawAmountRow(deposit.currency || 0, SL.currencyRow);
|
||||||
drawAmountRow(deposit.coin || 0, SL.coinRow);
|
drawAmountRow(deposit.coin || 0, SL.coinRow);
|
||||||
|
|
||||||
items.slice(0, SL.maxChecks).forEach((item, i) => {
|
frontItems.forEach((item, i) => {
|
||||||
const r = SL.firstCheckRow + i;
|
const r = SL.firstCheckRow + i;
|
||||||
const y = (rowY(r) - 0.015) * PT;
|
const y = (rowY(r) - 0.015) * PT;
|
||||||
if (item.check_no) {
|
if (item.check_no) {
|
||||||
doc.font('Courier').fontSize(7).fillColor('#000000')
|
doc.font('Courier-Bold').fontSize(7).fillColor('#000000')
|
||||||
.text(String(item.check_no).slice(0, 8),
|
.text(String(item.check_no).slice(0, 8),
|
||||||
(SL.cX + 0.16) * PT, y,
|
(SL.cX + 0.28) * PT, y,
|
||||||
{ width: SL.checkNoW * PT, lineBreak: false });
|
{ width: SL.checkNoW * PT, lineBreak: false });
|
||||||
}
|
}
|
||||||
drawAmountRow(item.amount || 0, r);
|
drawAmountRow(item.amount || 0, r);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// "FROM REVERSE" row carries back-page subtotal onto the front
|
||||||
|
if (fromReverseRow != null) {
|
||||||
|
doc.font('Courier').fontSize(6).fillColor(SL.bgLabelColor)
|
||||||
|
.text('FROM REVERSE', SL.cX * PT, rowY(fromReverseRow) * PT - 4, { lineBreak: false });
|
||||||
|
drawAmountRow(backTotal, fromReverseRow);
|
||||||
|
}
|
||||||
|
|
||||||
drawAmountRow(depositTotal, totalRows);
|
drawAmountRow(depositTotal, totalRows);
|
||||||
|
|
||||||
// ── Rotated left strip elements ─────────────────────────────────────────
|
// ── Rotated left strip elements ─────────────────────────────────────────
|
||||||
@@ -440,10 +457,124 @@ function generateDepositSlip(account, deposit, items) {
|
|||||||
|
|
||||||
doc.restore(); // end slip position translate
|
doc.restore(); // end slip position translate
|
||||||
|
|
||||||
|
if (hasBackPage) {
|
||||||
|
doc.addPage();
|
||||||
|
renderDepositBackPage(doc, backItems, backTotal);
|
||||||
|
}
|
||||||
|
|
||||||
doc.end();
|
doc.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Back page renderer ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function renderDepositBackPage(doc, backItems, backTotal) {
|
||||||
|
// Same slip position and width as front (slipX=0, W=3.375").
|
||||||
|
// No left strip elements; grid starts near the top.
|
||||||
|
// Vertically center the grid on the 8.5" page.
|
||||||
|
// Grid height = (checksRow + maxChecks + 1 TOTAL row + 1 border) * rowH = 33 * 0.175 = 5.775"
|
||||||
|
// Allow ~0.45" above grid for title + column headers; remainder splits top/bottom.
|
||||||
|
const BK_GRID_HEIGHT = (1 + SL.maxChecks + 1 + 1) * SL.rowH; // 33 rows
|
||||||
|
const BK_TITLE_AREA = 0.45;
|
||||||
|
const BK_GRID_TOP = (SL.H - BK_GRID_HEIGHT - BK_TITLE_AREA) / 2 + BK_TITLE_AREA;
|
||||||
|
const BK_TITLE_Y = (SL.H - BK_GRID_HEIGHT - BK_TITLE_AREA) / 2;
|
||||||
|
const BK = {
|
||||||
|
gridTop: BK_GRID_TOP,
|
||||||
|
titleY: BK_TITLE_Y,
|
||||||
|
checksRow: 0,
|
||||||
|
firstRow: 1,
|
||||||
|
maxChecks: SL.maxChecks, // 30
|
||||||
|
};
|
||||||
|
const totalRows = BK.firstRow + BK.maxChecks; // "TOTAL $" row index
|
||||||
|
|
||||||
|
const bkRowTopY = r => BK.gridTop + r * SL.rowH;
|
||||||
|
const bkRowY = r => BK.gridTop + r * SL.rowH + SL.rowH * 0.7;
|
||||||
|
|
||||||
|
const gridTopPt = bkRowTopY(0) * PT;
|
||||||
|
const gridBotPt = (bkRowTopY(totalRows) + SL.rowH) * PT;
|
||||||
|
|
||||||
|
doc.save();
|
||||||
|
doc.translate(SL.slipX * PT, 0);
|
||||||
|
|
||||||
|
// ── Title ─────────────────────────────────────────────────────────────────
|
||||||
|
doc.font('Helvetica-Bold').fontSize(9).fillColor(SL.bgHeaderColor)
|
||||||
|
.text('A D D I T I O N A L C H E C K L I S T I N G',
|
||||||
|
SL.cX * PT, BK.titleY * PT,
|
||||||
|
{ width: (SL.W - SL.cX - 0.05) * PT, align: 'center', lineBreak: false });
|
||||||
|
|
||||||
|
// ── Grid verticals (same column positions as front) ───────────────────────
|
||||||
|
const dollarsRightX = SL.colCentsR - SL.colCentsW - SL.colDollarSep;
|
||||||
|
const dividerX = (dollarsRightX - 7 * SL.digitW) * PT;
|
||||||
|
const dollarsCentsX = dollarsRightX * PT;
|
||||||
|
|
||||||
|
doc.moveTo(dividerX, gridTopPt).lineTo(dividerX, gridBotPt).lineWidth(0.5).stroke(SL.bgLineColor);
|
||||||
|
doc.moveTo(dollarsCentsX, gridTopPt).lineTo(dollarsCentsX, gridBotPt).lineWidth(0.5).stroke(SL.bgLineColor);
|
||||||
|
doc.moveTo(SL.colCentsR * PT, gridTopPt).lineTo(SL.colCentsR * PT, gridBotPt).lineWidth(0.5).stroke(SL.bgLineColor);
|
||||||
|
|
||||||
|
// Column headers
|
||||||
|
doc.font('Helvetica').fontSize(6).fillColor(SL.bgLabelColor);
|
||||||
|
const hdrY = (BK.gridTop - 0.10) * PT;
|
||||||
|
doc.text('DOLLARS', dollarsCentsX - 7 * SL.digitW * PT, hdrY,
|
||||||
|
{ width: 7 * SL.digitW * PT, align: 'center', lineBreak: false });
|
||||||
|
doc.text('CENTS', (SL.colCentsR - SL.colCentsW) * PT, hdrY,
|
||||||
|
{ width: SL.colCentsW * PT, align: 'center', lineBreak: false });
|
||||||
|
|
||||||
|
// "CHECKS:" header label
|
||||||
|
doc.font('Courier').fontSize(7).fillColor(SL.bgLabelColor)
|
||||||
|
.text('CHECKS:', SL.cX * PT, bkRowY(BK.checksRow) * PT - 5, { lineBreak: false });
|
||||||
|
|
||||||
|
// ── Horizontal grid lines ─────────────────────────────────────────────────
|
||||||
|
for (let r = 0; r <= totalRows + 1; r++) {
|
||||||
|
const y = bkRowTopY(r) * PT;
|
||||||
|
const isOuter = r === 0 || r === totalRows + 1;
|
||||||
|
doc.moveTo(SL.stripX * PT, y).lineTo(SL.colCentsR * PT, y)
|
||||||
|
.lineWidth(isOuter ? 0.75 : 0.3).stroke(SL.bgLineColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Row numbers (continuing from front: 31–60) ────────────────────────────
|
||||||
|
doc.font('Courier').fontSize(6).fillColor(SL.bgLabelColor);
|
||||||
|
for (let i = 0; i < BK.maxChecks; i++) {
|
||||||
|
const r = BK.firstRow + i;
|
||||||
|
doc.text(String(SL.maxChecks + i + 1), SL.cX * PT, bkRowY(r) * PT - 4,
|
||||||
|
{ width: 14, align: 'right', lineBreak: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── "TOTAL $" footer label ────────────────────────────────────────────────
|
||||||
|
doc.font('Courier-Bold').fontSize(7).fillColor('#000000')
|
||||||
|
.text('T O T A L $', SL.cX * PT, bkRowY(totalRows) * PT - 5, { lineBreak: false });
|
||||||
|
|
||||||
|
// ── "Forward to other side" in left strip (rotated) ───────────────────────
|
||||||
|
const fwdY = bkRowTopY(totalRows) + SL.rowH * 0.5;
|
||||||
|
doc.save();
|
||||||
|
doc.translate(SL.stripCenterX * PT, fwdY * PT);
|
||||||
|
doc.rotate(90);
|
||||||
|
doc.font('Helvetica').fontSize(6).fillColor(SL.bgLabelColor)
|
||||||
|
.text('Forward to other side', 0, 0, { lineBreak: false });
|
||||||
|
doc.restore();
|
||||||
|
|
||||||
|
// ── Amount data ───────────────────────────────────────────────────────────
|
||||||
|
backItems.forEach((item, i) => {
|
||||||
|
const r = BK.firstRow + i;
|
||||||
|
const y = (bkRowY(r) - 0.015) * PT;
|
||||||
|
if (item.check_no) {
|
||||||
|
doc.font('Courier-Bold').fontSize(7).fillColor('#000000')
|
||||||
|
.text(String(item.check_no).slice(0, 8),
|
||||||
|
(SL.cX + 0.28) * PT, y,
|
||||||
|
{ width: SL.checkNoW * PT, lineBreak: false });
|
||||||
|
}
|
||||||
|
if ((item.amount || 0) > 0) {
|
||||||
|
doc.font('Courier-Bold').fontSize(8).fillColor('#000000');
|
||||||
|
drawDigitAmount(doc, item.amount, dollarsRightX, y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Back page total
|
||||||
|
doc.font('Courier-Bold').fontSize(8).fillColor('#000000');
|
||||||
|
drawDigitAmount(doc, backTotal, dollarsRightX, (bkRowY(totalRows) - 0.015) * PT);
|
||||||
|
|
||||||
|
doc.restore();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Amount rendering helpers ──────────────────────────────────────────────────
|
// ── Amount rendering helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -235,17 +235,27 @@ function generateCheckPdf(account, checks, fields) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- MICR line ---
|
// --- MICR line ---
|
||||||
|
// Anchor the second transit symbol (the 'A' after routing) at 2 59/64" from left.
|
||||||
|
// Routing extends left from that anchor; account + check extend right.
|
||||||
const micrLine = formatMicrLine(account.routing_number, account.account_number, check.check_no);
|
const micrLine = formatMicrLine(account.routing_number, account.account_number, check.check_no);
|
||||||
const micrPos = pt(0.3, MICR_Y_IN);
|
const ANCHOR_IN = 2 + 59 / 64;
|
||||||
|
|
||||||
if (hasMicrFont) {
|
if (hasMicrFont) {
|
||||||
doc.font('MICR').fontSize(12).fillColor('#000000')
|
doc.font('MICR').fontSize(12).fillColor('#000000');
|
||||||
.text(micrLine, micrPos.x, micrPos.y, { lineBreak: false });
|
|
||||||
} else {
|
} else {
|
||||||
doc.font('Courier').fontSize(10).fillColor('#000000')
|
doc.font('Courier').fontSize(10).fillColor('#000000');
|
||||||
.text(micrLine, micrPos.x, micrPos.y, { lineBreak: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefix = everything up to and including the second 'A' (first A + routing + second A).
|
||||||
|
const secondA = micrLine.indexOf('A', 1) + 1;
|
||||||
|
const prefix = micrLine.slice(0, secondA);
|
||||||
|
const prefixWidthPts = doc.widthOfString(prefix);
|
||||||
|
const anchorXPts = (ANCHOR_IN + offX) * POINTS_PER_INCH;
|
||||||
|
const micrXPts = anchorXPts - prefixWidthPts;
|
||||||
|
const micrYPts = (slotOriginY + MICR_Y_IN + offY) * POINTS_PER_INCH;
|
||||||
|
|
||||||
|
doc.text(micrLine, micrXPts, micrYPts, { lineBreak: false });
|
||||||
|
|
||||||
} // end slot loop
|
} // end slot loop
|
||||||
} // end page loop
|
} // end page loop
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user