Check select-all, filtered total, deposit slip layout fixes

- Add select-all checkbox to checks table header; checks/unchecks all
  visible (filtered) rows; supports indeterminate state
- Add summary bar below checks toolbar showing count and total amount
  of filtered checks
- Deposit slip: output to full 8.5x11 letter page for trimming
- Deposit slip: remove beige left strip fill (white background)
- Deposit slip: remove vertical separator between depositor/bank info
- Deposit slip: stack bank info below account info instead of side-by-side
- Deposit slip: lower date underline position
- Deposit slip left strip: flip text orientation to read tilt-left
  (rotate 90 instead of -90; reposition all strip element anchors)
- Deposit slip left strip: center MICR and labels in strip width
- Deposit slip total: include decimal point in rotated digit amount
This commit is contained in:
2026-03-13 09:48:21 -06:00
parent 4fb7fd209c
commit ac5670039a
4 changed files with 174 additions and 117 deletions
+8
View File
@@ -79,6 +79,14 @@ header {
.toolbar-right { display: flex; align-items: center; gap: 8px; } .toolbar-right { display: flex; align-items: center; gap: 8px; }
.toolbar label { font-size: 12px; font-weight: 500; color: var(--text-muted); } .toolbar label { font-size: 12px; font-weight: 500; color: var(--text-muted); }
.checks-summary {
padding: 3px 12px;
font-size: 12px;
color: var(--text-muted);
flex-shrink: 0;
min-height: 20px;
}
select { select {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 4px; border-radius: 4px;
+3 -1
View File
@@ -45,11 +45,13 @@
</div> </div>
</div> </div>
<div id="checks-summary" class="checks-summary"></div>
<div class="table-wrap"> <div class="table-wrap">
<table id="checks-table"> <table id="checks-table">
<thead> <thead>
<tr> <tr>
<th class="col-select"></th> <th class="col-select"><input type="checkbox" id="select-all-checks" title="Select all"></th>
<th class="col-no sortable" data-col="check_no"># <span class="sort-indicator"></span></th> <th class="col-no sortable" data-col="check_no"># <span class="sort-indicator"></span></th>
<th class="col-date sortable" data-col="check_date">Date <span class="sort-indicator"></span></th> <th class="col-date sortable" data-col="check_date">Date <span class="sort-indicator"></span></th>
<th class="col-payee sortable" data-col="payee">Payee <span class="sort-indicator"></span></th> <th class="col-payee sortable" data-col="payee">Payee <span class="sort-indicator"></span></th>
+47
View File
@@ -97,11 +97,15 @@ function renderTable() {
if (checks.length === 0) { if (checks.length === 0) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="8">No checks found.</td></tr>'; tbody.innerHTML = '<tr class="empty-row"><td colspan="8">No checks found.</td></tr>';
updateSortIndicators(); updateSortIndicators();
updateSelectAll();
updateChecksSummary();
return; return;
} }
tbody.innerHTML = checks.map(renderRow).join(''); tbody.innerHTML = checks.map(renderRow).join('');
updateSortIndicators(); updateSortIndicators();
updateSelectAll();
updateChecksSummary();
// Attach row-level event listeners // Attach row-level event listeners
tbody.querySelectorAll('input[type="checkbox"]').forEach(cb => { tbody.querySelectorAll('input[type="checkbox"]').forEach(cb => {
@@ -188,6 +192,36 @@ function updateSortIndicators() {
}); });
} }
function updateSelectAll() {
const selectAll = document.getElementById('select-all-checks');
const checks = filteredAndSortedChecks();
if (checks.length === 0) {
selectAll.checked = false;
selectAll.indeterminate = false;
return;
}
const nSelected = checks.filter(c => state.selected.has(c.id)).length;
selectAll.indeterminate = nSelected > 0 && nSelected < checks.length;
selectAll.checked = nSelected === checks.length;
}
function updateChecksSummary() {
const el = document.getElementById('checks-summary');
const filtered = filteredAndSortedChecks();
const all = state.checks.length;
const fmt = n => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n);
if (all === 0) { el.textContent = ''; return; }
const filteredTotal = filtered.reduce((s, c) => s + (parseFloat(c.amount) || 0), 0);
const isFiltered = filtered.length < all;
if (isFiltered) {
el.textContent = `${filtered.length} of ${all} checks · ${fmt(filteredTotal)}`;
} else {
el.textContent = `${all} check${all !== 1 ? 's' : ''} · ${fmt(filteredTotal)}`;
}
}
function refreshPdfButton() { function refreshPdfButton() {
const n = state.selected.size; const n = state.selected.size;
@@ -206,6 +240,7 @@ function onCheckboxChange(cb) {
state.selected.delete(id); state.selected.delete(id);
} }
refreshPdfButton(); refreshPdfButton();
updateSelectAll();
} }
// ── Slide-in panel ─────────────────────────────────────────────────────────── // ── Slide-in panel ───────────────────────────────────────────────────────────
@@ -916,6 +951,18 @@ function init() {
renderTable(); renderTable();
}); });
// Select-all checkbox
document.getElementById('select-all-checks').addEventListener('change', e => {
const checks = filteredAndSortedChecks();
if (e.target.checked) {
checks.forEach(c => state.selected.add(c.id));
} else {
checks.forEach(c => state.selected.delete(c.id));
}
renderTable();
refreshPdfButton();
});
// New check // New check
document.getElementById('btn-new-check').addEventListener('click', () => openPanel()); document.getElementById('btn-new-check').addEventListener('click', () => openPanel());
+116 -116
View File
@@ -5,13 +5,16 @@
* *
* Generates two PDF types for a deposit: * Generates two PDF types for a deposit:
* - Deposit Report: plain formatted document (Courier, monospaced) * - Deposit Report: plain formatted document (Courier, monospaced)
* - Deposit Slip: precisely positioned 3.375" × 8.5" slip with Style A background, * - Deposit Slip: precisely positioned 3.375" × 8.5" slip printed on an 8.5"×11"
* digit-column amounts, and a GnuMICR line rotated 90°. * letter page for trimming. Style A background (grid lines drawn
* server-side). Digit-column amounts. GnuMICR line in left strip.
* *
* All measurements in inches; converted to points (× 72) for PDFKit. * All measurements in inches; converted to points (× 72) for PDFKit.
* *
* TMDC slip layout is hardcoded. Tune the LAYOUT constants below if fields * Left strip elements use rotate(90) so text reads when tilting head LEFT (same
* print slightly off on physical stock. * orientation as Qslip/standard bank deposit slips).
*
* Tune the SL constants below if fields print slightly off on physical stock.
*/ */
const PDFDocument = require('pdfkit'); const PDFDocument = require('pdfkit');
@@ -22,78 +25,71 @@ const PT = 72; // points per inch
const MICR_FONT_PATH = path.join(__dirname, '../../fonts/GnuMICR.ttf'); const MICR_FONT_PATH = path.join(__dirname, '../../fonts/GnuMICR.ttf');
// ── Deposit Slip Layout Constants (inches) ──────────────────────────────────── // ── Deposit Slip Layout Constants (inches) ────────────────────────────────────
// Page is 3.375" wide × 8.5" tall (portrait). // Slip is 3.375" wide × 8.5" tall, printed on an 8.5"×11" letter page.
// A 0.625" strip on the LEFT holds all rotated elements (MICR, deposit total, // slipX offsets the slip horizontally on the letter page (0 = left edge).
// check count). The remaining 2.75" is the main form content. // A 0.625" strip on the LEFT holds all rotated strip elements.
// Strip elements use rotate(90): text reads when tilting head LEFT (Qslip orientation).
// Strip element Y positions are the TOP of each element; text flows downward.
const SL = { const SL = {
W: 3.375, W: 3.375,
H: 8.5, H: 8.5,
// Left rotated strip — right edge of reserved area // Horizontal offset of the slip on the 8.5"×11" letter page
slipX: 0,
// Left rotated strip — width of reserved area
stripX: 0.625, stripX: 0.625,
// Content X start (inside the strip) // Content X start (right of strip)
cX: 0.65, cX: 0.65,
// ── Depositor / Bank block ──────────────────────────────────────────────── // ── Depositor block ───────────────────────────────────────────────────────
depositorY: 0.28, // Y of first depositor line depositorY: 0.28, // Y of company name (first depositor line)
bankX: 1.9, // X of bank name (right column)
// ── Date ───────────────────────────────────────────────────────────────── // ── Date ─────────────────────────────────────────────────────────────────
dateY: 1.38, // Y of DATE label dateY: 1.38, // Y of DATE label
dateValueX: 0.92, // X where date value prints dateValueX: 0.92, // X where date value prints
// ── Disclaimer ──────────────────────────────────────────────────────────── // ── Disclaimer ────────────────────────────────────────────────────────────
disclaimerY: 1.56, disclaimerY: 1.56,
// ── Amount grid ─────────────────────────────────────────────────────────── // ── Amount grid ───────────────────────────────────────────────────────────
gridTop: 1.72, // top border of grid gridTop: 1.72, // top border of grid
rowH: 0.175, // height of each row rowH: 0.175, // height of each row
// Column positions (right edges, in inches from left of page) // Column positions (right edges, in inches from left of slip)
colCentsR: 3.26, // right edge of cents column colCentsR: 3.26, // right edge of cents column
colCentsW: 0.42, // width of cents column colCentsW: 0.42, // width of cents column
colDollarSep: 0.08, // gap between dollars and cents columns colDollarSep: 0.08, // gap between dollars and cents columns
// dollars column right edge = colCentsR - colCentsW - colDollarSep
// dollars column width = 7 digit slots × digitW
digitW: 0.115, // width of each digit slot (Courier 8pt ≈ 4.8pt + spacing) digitW: 0.115, // width per dollar digit slot
centDigitW: 0.115, centDigitW: 0.115,
// Row Y offsets from gridTop (label baseline) // Row indices
currencyRow: 1, // grid row index currencyRow: 1,
coinRow: 2, coinRow: 2,
checksRow: 3, // "CHECKS:" label row (no amount on this row) checksRow: 3, // "CHECKS:" label row no amount
firstCheckRow: 4, // first numbered check row firstCheckRow: 4,
maxChecks: 30, maxChecks: 30,
// Check number column X checkNoX: 0.67,
checkNoX: 0.67, checkNoW: 0.55,
checkNoW: 0.55, // max width for check number text
// ── Footer ─────────────────────────────────────────────────────────────── // ── Rotated left strip (rotate(90): text flows downward, reads tilt-left)
// "TOTAL $" row sits just below the last check row // All strip X positions are centered in the 0.625" strip.
// (computed dynamically from firstCheckRow + maxChecks) // Y positions are the TOP anchor; text flows DOWNWARD from there.
stripCenterX: 0.32, // ≈ stripX/2; tune to center text in strip width
// ── Rotated left strip ──────────────────────────────────────────────────── micrY: 6.0, // MICR starts here, flows ~2.4" to bottom
// Rotated text is drawn with doc.rotate(-90) centred in the strip. depTotalLabelY: 2.2, // "DEPOSIT TOTAL $" label start
// X positions below are distance from LEFT edge of page (strip area). depTotalAmtY: 3.3, // deposit total digits start (each 0.16" apart downward)
micrY: 8.3, // Y (on page) where rotated MICR baseline sits checkCountLabelY: 4.8, // "TOTAL ITEMS" label start
micrX: 0.12, // X anchor for rotated MICR (left side of text after rotation) checkCountValY: 5.7, // check count value start
depTotalLabelY: 6.8, // Y where "DEPOSIT TOTAL $" rotated label baseline sits // ── Colours ───────────────────────────────────────────────────────────────
depTotalAmtY: 5.6, // Y where deposit total digits start (reading upward) bgLineColor: '#888888',
depTotalX: 0.44, // X of rotated deposit total elements bgLabelColor: '#444444',
bgHeaderColor: '#000000',
checkCountLabelY: 3.5, // Y of rotated "TOTAL ITEMS" label
checkCountValY: 2.8, // Y of rotated check count value
checkCountX: 0.44,
// ── Style A background colours ───────────────────────────────────────────
bgStripColor: '#d4c9a8', // beige shaded strip (left margin)
bgLineColor: '#888888', // grid lines
bgLabelColor: '#444444', // row labels (CURRENCY, COIN, etc.)
bgHeaderColor: '#000000', // DEPOSIT TICKET header
}; };
// Grid row Y baseline (from top of page, in inches) // Grid row Y baseline (from top of page, in inches)
@@ -220,8 +216,9 @@ function generateDepositSlip(account, deposit, items) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const hasMicrFont = fs.existsSync(MICR_FONT_PATH); const hasMicrFont = fs.existsSync(MICR_FONT_PATH);
// Letter page — slip sits at (slipX, 0); remaining space is blank for trimming
const doc = new PDFDocument({ const doc = new PDFDocument({
size: [SL.W * PT, SL.H * PT], size: 'LETTER',
margins: { top: 0, bottom: 0, left: 0, right: 0 }, margins: { top: 0, bottom: 0, left: 0, right: 0 },
autoFirstPage: true, autoFirstPage: true,
}); });
@@ -239,32 +236,37 @@ 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; // last row index const totalRows = SL.firstCheckRow + SL.maxChecks;
const totalRowY_ = rowTopY(totalRows); // top of TOTAL $ row const totalRowY_ = rowTopY(totalRows);
const gridBottom = totalRowY_ + SL.rowH; const gridBottom = totalRowY_ + SL.rowH;
// Offset all slip drawing to its position on the letter page
doc.save();
doc.translate(SL.slipX * PT, 0);
// ── Style A background ────────────────────────────────────────────────── // ── Style A background ──────────────────────────────────────────────────
// Left beige strip // No fill on left strip (white/transparent per user preference)
doc.rect(0, 0, SL.stripX * PT, SL.H * PT)
.fill(SL.bgStripColor);
// Outer border // Outer border of main content area (right of strip)
doc.rect(SL.stripX * PT, 0, (SL.W - SL.stripX) * PT, SL.H * PT) doc.rect(SL.stripX * PT, 0, (SL.W - SL.stripX) * PT, SL.H * PT)
.lineWidth(1).stroke('#000000'); .lineWidth(1).stroke('#000000');
// Vertical divider between check# and amount columns // Vertical divider between check# and amount columns
const dividerX = (SL.colCentsR - SL.colCentsW - SL.colDollarSep - 7 * SL.digitW) * PT; const dividerX = (SL.colCentsR - SL.colCentsW - SL.colDollarSep - 7 * SL.digitW) * PT;
const gridTopPt = SL.gridTop * PT; const gridTopPt = SL.gridTop * PT;
const gridBotPt = gridBottom * PT; const gridBotPt = gridBottom * PT;
doc.moveTo(dividerX, gridTopPt).lineTo(dividerX, gridBotPt).lineWidth(0.5).stroke(SL.bgLineColor); doc.moveTo(dividerX, gridTopPt).lineTo(dividerX, gridBotPt)
.lineWidth(0.5).stroke(SL.bgLineColor);
// Vertical divider between dollars and cents // Vertical divider between dollars and cents
const dollarsCentsX = (SL.colCentsR - SL.colCentsW - SL.colDollarSep) * PT; const dollarsCentsX = (SL.colCentsR - SL.colCentsW - SL.colDollarSep) * PT;
doc.moveTo(dollarsCentsX, gridTopPt).lineTo(dollarsCentsX, gridBotPt).lineWidth(0.5).stroke(SL.bgLineColor); doc.moveTo(dollarsCentsX, gridTopPt).lineTo(dollarsCentsX, gridBotPt)
.lineWidth(0.5).stroke(SL.bgLineColor);
// Right border of cents column // Right border of cents column
doc.moveTo(SL.colCentsR * PT, gridTopPt).lineTo(SL.colCentsR * PT, 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 header labels // Column header labels
doc.font('Helvetica').fontSize(6).fillColor(SL.bgLabelColor); doc.font('Helvetica').fontSize(6).fillColor(SL.bgLabelColor);
@@ -274,7 +276,7 @@ function generateDepositSlip(account, deposit, items) {
doc.text('CENTS', (SL.colCentsR - SL.colCentsW) * PT, hdrY, doc.text('CENTS', (SL.colCentsR - SL.colCentsW) * PT, hdrY,
{ width: SL.colCentsW * PT, align: 'center', lineBreak: false }); { width: SL.colCentsW * PT, align: 'center', lineBreak: false });
// Horizontal grid lines for all rows // Horizontal grid lines
for (let r = 0; r <= totalRows + 1; r++) { for (let r = 0; r <= totalRows + 1; r++) {
const y = rowTopY(r) * PT; const y = rowTopY(r) * PT;
doc.moveTo(SL.stripX * PT, y).lineTo(SL.colCentsR * PT, y) doc.moveTo(SL.stripX * PT, y).lineTo(SL.colCentsR * PT, y)
@@ -303,7 +305,7 @@ function generateDepositSlip(account, deposit, items) {
doc.font('Courier-Bold').fontSize(7).fillColor('#000000'); doc.font('Courier-Bold').fontSize(7).fillColor('#000000');
doc.text('TOTAL $', SL.cX * PT, rowY(totalRows) * PT - 5, { lineBreak: false }); doc.text('TOTAL $', SL.cX * PT, rowY(totalRows) * PT - 5, { lineBreak: false });
// Disclaimer text (below date, above grid) // Top disclaimer (above grid)
doc.font('Helvetica').fontSize(5).fillColor('#666666') doc.font('Helvetica').fontSize(5).fillColor('#666666')
.text( .text(
'DEPOSITS MAY NOT BE AVAILABLE FOR IMMEDIATE WITHDRAWAL', 'DEPOSITS MAY NOT BE AVAILABLE FOR IMMEDIATE WITHDRAWAL',
@@ -311,7 +313,7 @@ function generateDepositSlip(account, deposit, items) {
{ width: (SL.W - SL.cX - 0.05) * PT, lineBreak: false } { width: (SL.W - SL.cX - 0.05) * PT, lineBreak: false }
); );
// Bottom disclaimer (inside form, near total row) // Bottom disclaimer (below grid)
doc.font('Helvetica').fontSize(5).fillColor('#666666') doc.font('Helvetica').fontSize(5).fillColor('#666666')
.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.',
@@ -320,56 +322,50 @@ 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)
const headerText = 'D E P O S I T T I C K E T'; .text('D E P O S I T T I C K E T', SL.cX * PT, 0.08 * PT,
doc.text(headerText, SL.cX * PT, 0.08 * 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 });
// Vertical separator between depositor and bank columns // ── Depositor block — account info, then bank info stacked below ────────
const midX = ((SL.cX + SL.bankX) / 2) * PT;
doc.moveTo(midX, 0.18 * PT).lineTo(midX, (SL.dateY - 0.05) * PT)
.lineWidth(0.5).stroke('#aaaaaa');
// ── Depositor / Bank block ──────────────────────────────────────────────
doc.font('Helvetica-Bold').fontSize(8).fillColor('#000000') doc.font('Helvetica-Bold').fontSize(8).fillColor('#000000')
.text(account.company1 || '', SL.cX * PT, SL.depositorY * PT, .text(account.company1 || '', SL.cX * PT, SL.depositorY * PT,
{ lineBreak: false }); { lineBreak: false });
let depY = SL.depositorY + 0.12; let blockY = SL.depositorY + 0.12;
doc.font('Helvetica').fontSize(7); doc.font('Helvetica').fontSize(7);
[account.company2, account.company3, account.company4].forEach(line => { [account.company2, account.company3, account.company4].forEach(line => {
if (line) { if (line) {
doc.text(line, SL.cX * PT, depY * PT, { lineBreak: false }); doc.text(line, SL.cX * PT, blockY * PT, { lineBreak: false });
depY += 0.10; blockY += 0.10;
} }
}); });
// Bank info — stacked below depositor, small gap
blockY += 0.06;
doc.font('Helvetica-Bold').fontSize(8).fillColor('#000000') doc.font('Helvetica-Bold').fontSize(8).fillColor('#000000')
.text(account.bank_name || '', SL.bankX * PT, SL.depositorY * PT, .text(account.bank_name || '', SL.cX * PT, blockY * PT, { lineBreak: false });
{ lineBreak: false }); blockY += 0.12;
let bnkY = SL.depositorY + 0.12;
doc.font('Helvetica').fontSize(7); doc.font('Helvetica').fontSize(7);
[account.bank_info1, account.bank_info2].forEach(line => { [account.bank_info1, account.bank_info2].forEach(line => {
if (line) { if (line) {
doc.text(line, SL.bankX * PT, bnkY * PT, { lineBreak: false }); doc.text(line, SL.cX * PT, blockY * PT, { lineBreak: false });
bnkY += 0.10; blockY += 0.10;
} }
}); });
// ── Date ─────────────────────────────────────────────────────────────── // ── Date ───────────────────────────────────────────────────────────────
doc.font('Helvetica').fontSize(7).fillColor(SL.bgLabelColor) doc.font('Helvetica').fontSize(7).fillColor(SL.bgLabelColor)
.text('DATE', SL.cX * PT, SL.dateY * PT, { lineBreak: false }); .text('DATE', SL.cX * PT, SL.dateY * PT, { lineBreak: false });
// Underline // Underline (positioned lower than the label text)
const dateUnderX1 = (SL.cX + 0.33) * PT; const dateUnderX1 = (SL.cX + 0.33) * PT;
const dateUnderX2 = (SL.dateValueX + 0.85) * PT; const dateUnderX2 = (SL.dateValueX + 0.85) * PT;
const dateUnderY = (SL.dateY + 0.01) * PT; const dateUnderY = (SL.dateY + 0.09) * PT;
doc.moveTo(dateUnderX1, dateUnderY).lineTo(dateUnderX2, dateUnderY) doc.moveTo(dateUnderX1, dateUnderY).lineTo(dateUnderX2, dateUnderY)
.lineWidth(0.5).stroke('#000000'); .lineWidth(0.5).stroke('#000000');
doc.font('Courier').fontSize(8).fillColor('#000000') doc.font('Courier').fontSize(8).fillColor('#000000')
.text(deposit.deposit_date || '', (SL.cX + 0.36) * PT, (SL.dateY - 0.03) * PT, .text(deposit.deposit_date || '', (SL.cX + 0.36) * PT, (SL.dateY - 0.01) * PT,
{ lineBreak: false }); { lineBreak: false });
// ── Amount data ───────────────────────────────────────────────────────── // ── Amount data ─────────────────────────────────────────────────────────
// Draw digits in fixed-width slots, right-aligned in the dollars column
const dollarsRightX = (SL.colCentsR - SL.colCentsW - SL.colDollarSep); const dollarsRightX = (SL.colCentsR - SL.colCentsW - SL.colDollarSep);
function drawAmountRow(amount, rowIdx) { function drawAmountRow(amount, rowIdx) {
@@ -381,12 +377,9 @@ function generateDepositSlip(account, deposit, items) {
drawAmountRow(deposit.currency || 0, SL.currencyRow); drawAmountRow(deposit.currency || 0, SL.currencyRow);
drawAmountRow(deposit.coin || 0, SL.coinRow); drawAmountRow(deposit.coin || 0, SL.coinRow);
// Check items
items.slice(0, SL.maxChecks).forEach((item, i) => { items.slice(0, SL.maxChecks).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;
// Check number
if (item.check_no) { if (item.check_no) {
doc.font('Courier').fontSize(7).fillColor('#000000') doc.font('Courier').fontSize(7).fillColor('#000000')
.text(String(item.check_no).slice(0, 8), .text(String(item.check_no).slice(0, 8),
@@ -396,19 +389,21 @@ function generateDepositSlip(account, deposit, items) {
drawAmountRow(item.amount || 0, r); drawAmountRow(item.amount || 0, r);
}); });
// Total $ row
drawAmountRow(checksTotal, totalRows); drawAmountRow(checksTotal, totalRows);
// ── Rotated left strip elements ───────────────────────────────────────── // ── Rotated left strip elements ─────────────────────────────────────────
// All elements use rotate(90): text flows downward on the page, which reads
// correctly when you tilt your head to the LEFT (standard bank deposit orientation).
// stripCenterX centers the text baseline within the 0.625" strip.
// MICR line (routing + account, no check number for deposits)
const routing = (account.routing_number || '').replace(/\D/g, ''); const routing = (account.routing_number || '').replace(/\D/g, '');
const acctNo = (account.account_number || '').replace(/[^0-9]/g, ''); const acctNo = (account.account_number || '').replace(/[^0-9]/g, '');
const micrStr = `A${routing}A ${acctNo}C`; const micrStr = `A${routing}A ${acctNo}C`;
// MICR line — centered in strip, near bottom
doc.save(); doc.save();
doc.translate(SL.micrX * PT, SL.micrY * PT); doc.translate(SL.stripCenterX * PT, SL.micrY * PT);
doc.rotate(-90); doc.rotate(90);
if (hasMicrFont) { if (hasMicrFont) {
doc.font('MICR').fontSize(11).fillColor('#000000') doc.font('MICR').fontSize(11).fillColor('#000000')
.text(micrStr, 0, 0, { lineBreak: false }); .text(micrStr, 0, 0, { lineBreak: false });
@@ -418,32 +413,35 @@ function generateDepositSlip(account, deposit, items) {
} }
doc.restore(); doc.restore();
// Rotated "DEPOSIT TOTAL $" label + amount // "DEPOSIT TOTAL $" label
doc.save(); doc.save();
doc.translate(SL.depTotalX * PT, SL.depTotalLabelY * PT); doc.translate(SL.stripCenterX * PT, SL.depTotalLabelY * PT);
doc.rotate(-90); doc.rotate(90);
doc.font('Helvetica').fontSize(6).fillColor(SL.bgLabelColor) doc.font('Helvetica').fontSize(6).fillColor(SL.bgLabelColor)
.text('DEPOSIT TOTAL $', 0, 0, { lineBreak: false }); .text('DEPOSIT TOTAL $', 0, 0, { lineBreak: false });
doc.restore(); doc.restore();
// Deposit total digits (rotated, spaced) // Deposit total digits (includes decimal point)
drawRotatedDigitAmount(doc, depositTotal, SL.depTotalX, SL.depTotalAmtY); drawRotatedDigitAmount(doc, depositTotal, SL.stripCenterX, SL.depTotalAmtY);
// Rotated "TOTAL ITEMS" label + count // "TOTAL ITEMS" label
doc.save(); doc.save();
doc.translate(SL.checkCountX * PT, SL.checkCountLabelY * PT); doc.translate(SL.stripCenterX * PT, SL.checkCountLabelY * PT);
doc.rotate(-90); doc.rotate(90);
doc.font('Helvetica').fontSize(6).fillColor(SL.bgLabelColor) doc.font('Helvetica').fontSize(6).fillColor(SL.bgLabelColor)
.text('TOTAL ITEMS', 0, 0, { lineBreak: false }); .text('TOTAL ITEMS', 0, 0, { lineBreak: false });
doc.restore(); doc.restore();
// Check count value
doc.save(); doc.save();
doc.translate(SL.checkCountX * PT, SL.checkCountValY * PT); doc.translate(SL.stripCenterX * PT, SL.checkCountValY * PT);
doc.rotate(-90); doc.rotate(90);
doc.font('Courier').fontSize(9).fillColor('#000000') doc.font('Courier').fontSize(9).fillColor('#000000')
.text(String(checkCount), 0, 0, { lineBreak: false }); .text(String(checkCount), 0, 0, { lineBreak: false });
doc.restore(); doc.restore();
doc.restore(); // end slip position translate
doc.end(); doc.end();
}); });
} }
@@ -486,21 +484,23 @@ function drawDigitAmount(doc, amount, dollarsRightX, y) {
} }
/** /**
* Draw deposit total digits rotated 90° in the left strip. * Draw deposit total digits in the left strip using rotate(90).
* Each digit is stacked vertically (reading downward when viewed in portrait). * Each character is stacked top-to-bottom on the page; reads correctly
* when tilting head left. Includes a '.' decimal separator.
*/ */
function drawRotatedDigitAmount(doc, amount, stripX, startY) { function drawRotatedDigitAmount(doc, amount, stripCenterX, startY) {
const totalCents = Math.round(Math.abs(amount) * 100); const totalCents = Math.round(Math.abs(amount) * 100);
const dollars = Math.floor(totalCents / 100); const dollars = Math.floor(totalCents / 100);
const cents = totalCents % 100; const cents = totalCents % 100;
const fullStr = String(dollars) + String(cents).padStart(2, '0'); // Include decimal point between dollars and cents
const spacing = 0.16; // inches between each digit const fullStr = String(dollars) + '.' + String(cents).padStart(2, '0');
const spacing = 0.16; // inches between each character
doc.font('Courier').fontSize(9).fillColor('#000000'); doc.font('Courier').fontSize(9).fillColor('#000000');
fullStr.split('').forEach((ch, i) => { fullStr.split('').forEach((ch, i) => {
doc.save(); doc.save();
doc.translate(stripX * PT, (startY - i * spacing) * PT); doc.translate(stripCenterX * PT, (startY + i * spacing) * PT);
doc.rotate(-90); doc.rotate(90);
doc.text(ch, 0, 0, { lineBreak: false }); doc.text(ch, 0, 0, { lineBreak: false });
doc.restore(); doc.restore();
}); });