From 189ae53d34e18216b0d984ca24dbefc2b247c18b Mon Sep 17 00:00:00 2001 From: Steve Dogiakos Date: Tue, 28 Apr 2026 09:04:44 -0600 Subject: [PATCH] feat(deposits): pre-fill 30 slots and add back-page overflow for 31-60 checks - Deposit panel now pre-fills all 30 check slots on open (new and existing) - Remove button maintains 30-slot minimum by appending a blank row - Add Row button hidden at <30, visible at 30-59, disabled at 60 - Deposit slip PDF splits items: first 30 on front, up to 30 more on back - Front page gains a FROM REVERSE row carrying back-page subtotal when needed - Back page renders with same column positions/width as front, titled 'ADDITIONAL CHECK LISTING', numbered rows 31-60, and 'Forward to other side / TOTAL $' footer Closes #10, closes #11 --- public/js/app.js | 16 +++- src/services/depositPdfService.js | 127 +++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 510008a..1bd947a 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1442,14 +1442,18 @@ async function openDepositPanel(id = null) { document.getElementById('dep-coin').value = dep.coin || ''; document.getElementById('dep-cashback').value = dep.cash_back || ''; depState.items = (dep.items || []).map(it => ({ ...it })); + while (depState.items.length < 30) depState.items.push(newDepItem()); } catch (err) { alert('Error loading deposit: ' + err.message); return; } } 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(); recalcDepTotals(); @@ -1475,6 +1479,13 @@ function newDepItem() { } function renderDepItems() { + const addBtn = document.getElementById('btn-add-dep-item'); + if (addBtn) { + const count = depState.items.length; + addBtn.hidden = count < 30; // only show when already at 30 (back-page territory) + addBtn.disabled = count >= 60; + addBtn.textContent = count >= 60 ? '+ Add Row (max 60)' : '+ Add Row (back page)'; + } const tbody = document.getElementById('dep-items-tbody'); tbody.innerHTML = depState.items.map((item, i) => ` @@ -1497,6 +1508,8 @@ function renderDepItems() { tbody.querySelectorAll('.dep-item-remove').forEach(btn => { btn.addEventListener('click', () => { 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(); recalcDepTotals(); }); @@ -1900,6 +1913,7 @@ async function init() { document.getElementById('dep-panel-overlay').addEventListener('click', closeDepositPanel); document.getElementById('btn-save-deposit').addEventListener('click', saveDeposit); document.getElementById('btn-add-dep-item').addEventListener('click', () => { + if (depState.items.length >= 60) return; depState.items.push(newDepItem()); renderDepItems(); }); diff --git a/src/services/depositPdfService.js b/src/services/depositPdfService.js index 4a9a302..1001aae 100644 --- a/src/services/depositPdfService.js +++ b/src/services/depositPdfService.js @@ -238,7 +238,17 @@ function generateDepositSlip(account, deposit, items) { const depositTotal = subTotal - (deposit.cash_back || 0); 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 gridBottom = totalRowY_ + SL.rowH; @@ -375,7 +385,7 @@ function generateDepositSlip(account, deposit, items) { drawAmountRow(deposit.currency || 0, SL.currencyRow); drawAmountRow(deposit.coin || 0, SL.coinRow); - items.slice(0, SL.maxChecks).forEach((item, i) => { + frontItems.forEach((item, i) => { const r = SL.firstCheckRow + i; const y = (rowY(r) - 0.015) * PT; if (item.check_no) { @@ -387,6 +397,13 @@ function generateDepositSlip(account, deposit, items) { 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); // ── Rotated left strip elements ───────────────────────────────────────── @@ -440,10 +457,116 @@ function generateDepositSlip(account, deposit, items) { doc.restore(); // end slip position translate + if (hasBackPage) { + doc.addPage(); + renderDepositBackPage(doc, backItems, backTotal); + } + 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. + const BK = { + gridTop: 0.48, + 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, 0.10 * 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').fontSize(7).fillColor('#000000') + .text(String(item.check_no).slice(0, 8), + (SL.cX + 0.16) * PT, y, + { width: SL.checkNoW * PT, lineBreak: false }); + } + if ((item.amount || 0) > 0) { + doc.font('Courier').fontSize(8).fillColor('#000000'); + drawDigitAmount(doc, item.amount, dollarsRightX, y); + } + }); + + // Back page total + doc.font('Courier').fontSize(8).fillColor('#000000'); + drawDigitAmount(doc, backTotal, dollarsRightX, (bkRowY(totalRows) - 0.015) * PT); + + doc.restore(); +} + // ── Amount rendering helpers ────────────────────────────────────────────────── /**