From 7f94eee308c270b37e788170bd0d9ba923746141 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:03:10 -0400 Subject: [PATCH] Redesign dashboard: 6 outstanding age buckets, summary bar, improved layout --- src/slackAppHome.js | 133 +++++++++++++++++++++++++++++--------------- 1 file changed, 87 insertions(+), 46 deletions(-) diff --git a/src/slackAppHome.js b/src/slackAppHome.js index c720491..4551776 100644 --- a/src/slackAppHome.js +++ b/src/slackAppHome.js @@ -103,14 +103,14 @@ function buildHomeView(payments, metadata) { const today = new Date(); today.setHours(0, 0, 0, 0); + const daysSince = (dt) => Math.floor((today - dt) / (1000 * 60 * 60 * 24)); + // Categorize payments const scheduledChecks = []; const scheduledACH = []; const outstandingChecks = []; const skipStatuses = ["voided", "cancelled", "canceled", "marked as void", "cleared"]; - const ninetyDaysAgo = new Date(today); - ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90); for (const p of payments) { if (p.method !== "ACH" && p.method !== "Check") continue; @@ -122,14 +122,11 @@ function buildHomeView(payments, metadata) { if (skipStatuses.includes(status)) continue; if (dt >= today) { - // Future — scheduled if (p.method === "Check") scheduledChecks.push(p); else scheduledACH.push(p); } else if (p.method === "Check") { - // Past-due check not cleared — outstanding outstandingChecks.push(p); } - // Past-due ACH not cleared is ignored (should have been auto-cleared) } // Sort by date @@ -141,28 +138,50 @@ function buildHomeView(payments, metadata) { scheduledChecks.sort(byDate); scheduledACH.sort(byDate); - // Split outstanding into <=90 days and >90 days - const outstanding90 = outstandingChecks.filter((p) => parseMDYLocal(p.send_payment_on) >= ninetyDaysAgo); - const outstandingOver90 = outstandingChecks.filter((p) => parseMDYLocal(p.send_payment_on) < ninetyDaysAgo); + // Age buckets for outstanding checks + const ageBuckets = [ + { label: "0 – 15 days", min: 0, max: 15 }, + { label: "15 – 30 days", min: 15, max: 30 }, + { label: "30 – 45 days", min: 30, max: 45 }, + { label: "45 – 60 days", min: 45, max: 60 }, + { label: "60 – 90 days", min: 60, max: 90 }, + { label: "90+ days", min: 90, max: Infinity }, + ]; + + const bucketedOutstanding = ageBuckets.map((bucket) => { + const items = outstandingChecks.filter((p) => { + const age = daysSince(parseMDYLocal(p.send_payment_on)); + return age >= bucket.min && age < bucket.max; + }); + const total = items.reduce((sum, p) => sum + p.amount_usd, 0); + return { ...bucket, items, total }; + }); + + // --- Block builders --- const buildScheduledBlocks = (title, emoji, items) => { const total = items.reduce((sum, p) => sum + p.amount_usd, 0); const blocks = [ { - type: "section", - text: { - type: "mrkdwn", - text: `*${emoji} ${title}*\n${items.length} payments • ${formatCurrency(total)}`, - }, + type: "header", + text: { type: "plain_text", text: `${emoji} ${title}` }, + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: `${items.length} payment${items.length !== 1 ? "s" : ""} · ${formatCurrency(total)} total`, + }, + ], }, - { type: "divider" }, ]; if (!items.length) { blocks.push({ type: "section", - text: { type: "mrkdwn", text: "_None_" }, + text: { type: "mrkdwn", text: "_No upcoming payments_" }, }); blocks.push({ type: "divider" }); return blocks; @@ -181,14 +200,16 @@ function buildHomeView(payments, metadata) { const group = grouped[key]; const dayTotal = group.payments.reduce((sum, p) => sum + p.amount_usd, 0); const dateLabel = group.date ? formatDisplayDate(group.date) : "Unknown"; + const daysUntil = group.date ? Math.ceil((group.date - today) / (1000 * 60 * 60 * 24)) : null; + const daysTag = daysUntil === 0 ? ":rotating_light: _Today_" : daysUntil === 1 ? "_Tomorrow_" : daysUntil != null ? `_in ${daysUntil} days_` : ""; blocks.push({ type: "section", fields: [ - { type: "mrkdwn", text: `*${dateLabel}*` }, + { type: "mrkdwn", text: `*${dateLabel}* ${daysTag}` }, { type: "mrkdwn", - text: `${group.payments.length} payments • ${formatCurrency(dayTotal)}`, + text: `${group.payments.length} payment${group.payments.length !== 1 ? "s" : ""} · *${formatCurrency(dayTotal)}*`, }, ], }); @@ -203,64 +224,84 @@ function buildHomeView(payments, metadata) { const blocks = [ { - type: "section", - text: { - type: "mrkdwn", - text: `*:warning: Outstanding Checks*\n${outstandingChecks.length} checks • ${formatCurrency(totalAll)}`, - }, + type: "header", + text: { type: "plain_text", text: ":warning: Outstanding Checks" }, + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: `${outstandingChecks.length} check${outstandingChecks.length !== 1 ? "s" : ""} · ${formatCurrency(totalAll)} total`, + }, + ], }, - { type: "divider" }, ]; if (!outstandingChecks.length) { blocks.push({ type: "section", - text: { type: "mrkdwn", text: "_None_" }, + text: { type: "mrkdwn", text: ":white_check_mark: _All checks have cleared_" }, }); blocks.push({ type: "divider" }); return blocks; } - const total90 = outstanding90.reduce((sum, p) => sum + p.amount_usd, 0); - const totalOver90 = outstandingOver90.reduce((sum, p) => sum + p.amount_usd, 0); + for (const bucket of bucketedOutstanding) { + if (!bucket.items.length) continue; - blocks.push({ - type: "section", - fields: [ - { type: "mrkdwn", text: `*90 days or less*` }, - { type: "mrkdwn", text: `${outstanding90.length} checks • ${formatCurrency(total90)}` }, - ], - }); - - blocks.push({ - type: "section", - fields: [ - { type: "mrkdwn", text: `*Over 90 days*` }, - { type: "mrkdwn", text: `${outstandingOver90.length} checks • ${formatCurrency(totalOver90)}` }, - ], - }); + blocks.push({ + type: "section", + fields: [ + { type: "mrkdwn", text: `*${bucket.label}*` }, + { + type: "mrkdwn", + text: `${bucket.items.length} check${bucket.items.length !== 1 ? "s" : ""} · *${formatCurrency(bucket.total)}*`, + }, + ], + }); + } blocks.push({ type: "divider" }); return blocks; }; + // --- Summary bar --- + const totalScheduled = scheduledChecks.length + scheduledACH.length; + const totalScheduledAmt = [...scheduledChecks, ...scheduledACH].reduce((sum, p) => sum + p.amount_usd, 0); + const totalOutstandingAmt = outstandingChecks.reduce((sum, p) => sum + p.amount_usd, 0); + return { type: "home", blocks: [ { type: "header", - text: { type: "plain_text", text: "Payments Dashboard" }, + text: { type: "plain_text", text: ":bank: Payments Dashboard" }, }, { type: "context", elements: [ - { type: "mrkdwn", text: `*Last updated:* ${metadata?.last_updated || "N/A"}` }, - { type: "mrkdwn", text: `*Source:* ${metadata?.file_name || "N/A"}` }, + { type: "mrkdwn", text: `Last updated · ${metadata?.last_updated || "N/A"}` }, + { type: "mrkdwn", text: `Source · ${metadata?.file_name || "N/A"}` }, ], }, { type: "divider" }, - ...buildScheduledBlocks("Scheduled Checks", ":calendar:", scheduledChecks), - ...buildScheduledBlocks("Scheduled ACH", ":calendar:", scheduledACH), + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: `:calendar: *Scheduled*\n${totalScheduled} payments · ${formatCurrency(totalScheduledAmt)}`, + }, + { + type: "mrkdwn", + text: `:warning: *Outstanding*\n${outstandingChecks.length} checks · ${formatCurrency(totalOutstandingAmt)}`, + }, + ], + }, + { type: "divider" }, + ...buildScheduledBlocks("Scheduled Checks", ":ledger:", scheduledChecks), + ...buildScheduledBlocks("Scheduled ACH", ":electric_plug:", scheduledACH), ...buildOutstandingBlocks(), ], };