From b7feb4a914556da4eec8f6befc49b671f492db46 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Fri, 8 May 2026 11:24:32 -0400 Subject: [PATCH] Collapsible dashboard sections and clean up BoA audit log (#22) * Make dashboard sections collapsible and clean up BoA audit log - Scheduled Checks, Scheduled ACH, and Outstanding Checks sections default to collapsed with summary line; Expand/Collapse button toggles detail view via private_metadata state - Filter backfill failure records from BoA submissions display - Limit Recent BoA Submissions to 5 most recent entries * Share Slack home publish pipeline Co-authored-by: Adam Moussa --------- Co-authored-by: Cursor Agent Co-authored-by: Adam Moussa --- src/slackAppHome.js | 94 ++++++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/src/slackAppHome.js b/src/slackAppHome.js index fa3c2cb..e6f47e9 100644 --- a/src/slackAppHome.js +++ b/src/slackAppHome.js @@ -134,7 +134,7 @@ export const handler = async (event) => { return { statusCode: 200, body: body.challenge }; } - // Handle button clicks → open modal + // Handle button clicks → toggle sections or open modal if (body.type === "block_actions") { return handleBlockAction(body); } @@ -146,6 +146,10 @@ export const handler = async (event) => { const userId = body.event.user; + return publishHomeView(userId, []); +}; + +async function publishHomeView(userId, expanded) { const { Item: metadata } = await ddb.send( new GetCommand({ TableName: TABLE_NAME, Key: { pk: "metadata" } }) ); @@ -159,7 +163,7 @@ export const handler = async (event) => { return { statusCode: 200, body: JSON.stringify({ error: "No payment data" }) }; } - const view = buildHomeView(payments, metadata, boaTransactions); + const view = buildHomeView(payments, metadata, boaTransactions, expanded); const slackToken = await getSlackToken(); const res = await fetch("https://slack.com/api/views.publish", { @@ -179,7 +183,7 @@ export const handler = async (event) => { } return { statusCode: 200, body: JSON.stringify({ ok: true }) }; -}; +} // --- Block action handler (button clicks) --- @@ -190,6 +194,19 @@ async function handleBlockAction(body) { const triggerId = body.trigger_id; const actionId = action.action_id; + // Toggle section expand/collapse → re-publish home view + if (actionId.startsWith("toggle_section_")) { + const section = actionId.replace("toggle_section_", ""); + const meta = JSON.parse(body.view?.private_metadata || "{}"); + const expanded = Array.isArray(meta.expanded) ? [...meta.expanded] : []; + const idx = expanded.indexOf(section); + if (idx >= 0) expanded.splice(idx, 1); + else expanded.push(section); + + const userId = body.user?.id; + return publishHomeView(userId, expanded); + } + const payments = await scanPayments(); const { today, daysSince, scheduledChecks, scheduledACH, bucketedOutstanding } = categorizePayments(payments); @@ -302,8 +319,9 @@ async function scanBoATransactions() { items.push(...result.Items); lastKey = result.LastEvaluatedKey; } while (lastKey); - items.sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || "")); - return items.slice(0, 10); + const filtered = items.filter((t) => !(t.backfill && !t.success)); + filtered.sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || "")); + return filtered.slice(0, 5); } // --- Scan all payments from DynamoDB --- @@ -354,7 +372,7 @@ function buildBoABlocks(transactions) { { type: "mrkdwn", text: transactions.length - ? `Last ${transactions.length} submission${transactions.length !== 1 ? "s" : ""} · retained 90 days` + ? `Last ${transactions.length} submission${transactions.length !== 1 ? "s" : ""} · 90-day retention` : "No submissions in the last 90 days", }, ], @@ -393,28 +411,34 @@ function buildBoABlocks(transactions) { return blocks; } -function buildHomeView(payments, metadata, boaTransactions = []) { +function buildHomeView(payments, metadata, boaTransactions = [], expanded = []) { const { today, daysSince, scheduledChecks, scheduledACH, outstandingChecks, bucketedOutstanding } = categorizePayments(payments); + const isExpanded = (key) => expanded.includes(key); - const buildScheduledBlocks = (title, emoji, items, methodKey) => { + const buildScheduledBlocks = (title, emoji, items, sectionKey, methodKey) => { const total = items.reduce((sum, p) => sum + p.amount_usd, 0); + const open = isExpanded(sectionKey); const blocks = [ { - 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: "section", + text: { + type: "mrkdwn", + text: `${open ? ":large_orange_diamond:" : ":small_orange_diamond:"} ${emoji} *${title}* · ${items.length} payment${items.length !== 1 ? "s" : ""} · ${formatCurrency(total)}`, + }, + accessory: { + type: "button", + text: { type: "plain_text", text: open ? "Collapse" : "Expand" }, + action_id: `toggle_section_${sectionKey}`, + }, }, ]; + if (!open) { + blocks.push({ type: "divider" }); + return blocks; + } + if (!items.length) { blocks.push({ type: "section", @@ -460,23 +484,28 @@ function buildHomeView(payments, metadata, boaTransactions = []) { const buildOutstandingBlocks = () => { const totalAll = outstandingChecks.reduce((sum, p) => sum + p.amount_usd, 0); + const open = isExpanded("outstanding"); const blocks = [ { - 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: "section", + text: { + type: "mrkdwn", + text: `${open ? ":large_orange_diamond:" : ":small_orange_diamond:"} :warning: *Outstanding Checks* · ${outstandingChecks.length} check${outstandingChecks.length !== 1 ? "s" : ""} · ${formatCurrency(totalAll)}`, + }, + accessory: { + type: "button", + text: { type: "plain_text", text: open ? "Collapse" : "Expand" }, + action_id: "toggle_section_outstanding", + }, }, ]; + if (!open) { + blocks.push({ type: "divider" }); + return blocks; + } + if (!outstandingChecks.length) { blocks.push({ type: "section", @@ -517,6 +546,7 @@ function buildHomeView(payments, metadata, boaTransactions = []) { return { type: "home", + private_metadata: JSON.stringify({ expanded }), blocks: [ { type: "header", @@ -555,8 +585,8 @@ function buildHomeView(payments, metadata, boaTransactions = []) { ], }, { type: "divider" }, - ...buildScheduledBlocks("Scheduled Checks", ":ledger:", scheduledChecks, "checks"), - ...buildScheduledBlocks("Scheduled ACH", ":electric_plug:", scheduledACH, "ach"), + ...buildScheduledBlocks("Scheduled Checks", ":ledger:", scheduledChecks, "scheduled_checks", "checks"), + ...buildScheduledBlocks("Scheduled ACH", ":electric_plug:", scheduledACH, "scheduled_ach", "ach"), ...buildOutstandingBlocks(), ...buildBoABlocks(boaTransactions), ],