mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-10-05 00:12:11 +00:00
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
This commit is contained in:
parent
491d29de1e
commit
8073eb3662
1 changed files with 77 additions and 31 deletions
|
|
@ -134,7 +134,7 @@ export const handler = async (event) => {
|
||||||
return { statusCode: 200, body: body.challenge };
|
return { statusCode: 200, body: body.challenge };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle button clicks → open modal
|
// Handle button clicks → toggle sections or open modal
|
||||||
if (body.type === "block_actions") {
|
if (body.type === "block_actions") {
|
||||||
return handleBlockAction(body);
|
return handleBlockAction(body);
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +159,8 @@ export const handler = async (event) => {
|
||||||
return { statusCode: 200, body: JSON.stringify({ error: "No payment data" }) };
|
return { statusCode: 200, body: JSON.stringify({ error: "No payment data" }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
const view = buildHomeView(payments, metadata, boaTransactions);
|
const expanded = [];
|
||||||
|
const view = buildHomeView(payments, metadata, boaTransactions, expanded);
|
||||||
const slackToken = await getSlackToken();
|
const slackToken = await getSlackToken();
|
||||||
|
|
||||||
const res = await fetch("https://slack.com/api/views.publish", {
|
const res = await fetch("https://slack.com/api/views.publish", {
|
||||||
|
|
@ -190,6 +191,38 @@ async function handleBlockAction(body) {
|
||||||
const triggerId = body.trigger_id;
|
const triggerId = body.trigger_id;
|
||||||
const actionId = action.action_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 = meta.expanded || [];
|
||||||
|
const idx = expanded.indexOf(section);
|
||||||
|
if (idx >= 0) expanded.splice(idx, 1);
|
||||||
|
else expanded.push(section);
|
||||||
|
|
||||||
|
const userId = body.user?.id;
|
||||||
|
const { Item: metadata } = await ddb.send(
|
||||||
|
new GetCommand({ TableName: TABLE_NAME, Key: { pk: "metadata" } })
|
||||||
|
);
|
||||||
|
const [payments, boaTransactions] = await Promise.all([
|
||||||
|
scanPayments(),
|
||||||
|
scanBoATransactions(),
|
||||||
|
]);
|
||||||
|
const view = buildHomeView(payments, metadata, boaTransactions, expanded);
|
||||||
|
const slackToken = await getSlackToken();
|
||||||
|
const res = await fetch("https://slack.com/api/views.publish", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${slackToken}`,
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ user_id: userId, view }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.ok) console.error("Slack views.publish error:", JSON.stringify(data));
|
||||||
|
return { statusCode: 200, body: JSON.stringify({ ok: true }) };
|
||||||
|
}
|
||||||
|
|
||||||
const payments = await scanPayments();
|
const payments = await scanPayments();
|
||||||
const { today, daysSince, scheduledChecks, scheduledACH, bucketedOutstanding } = categorizePayments(payments);
|
const { today, daysSince, scheduledChecks, scheduledACH, bucketedOutstanding } = categorizePayments(payments);
|
||||||
|
|
||||||
|
|
@ -302,8 +335,9 @@ async function scanBoATransactions() {
|
||||||
items.push(...result.Items);
|
items.push(...result.Items);
|
||||||
lastKey = result.LastEvaluatedKey;
|
lastKey = result.LastEvaluatedKey;
|
||||||
} while (lastKey);
|
} while (lastKey);
|
||||||
items.sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || ""));
|
const filtered = items.filter((t) => !(t.backfill && !t.success));
|
||||||
return items.slice(0, 10);
|
filtered.sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || ""));
|
||||||
|
return filtered.slice(0, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Scan all payments from DynamoDB ---
|
// --- Scan all payments from DynamoDB ---
|
||||||
|
|
@ -354,7 +388,7 @@ function buildBoABlocks(transactions) {
|
||||||
{
|
{
|
||||||
type: "mrkdwn",
|
type: "mrkdwn",
|
||||||
text: transactions.length
|
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",
|
: "No submissions in the last 90 days",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -393,28 +427,34 @@ function buildBoABlocks(transactions) {
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHomeView(payments, metadata, boaTransactions = []) {
|
function buildHomeView(payments, metadata, boaTransactions = [], expanded = []) {
|
||||||
const { today, daysSince, scheduledChecks, scheduledACH, outstandingChecks, bucketedOutstanding } = categorizePayments(payments);
|
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 total = items.reduce((sum, p) => sum + p.amount_usd, 0);
|
||||||
|
const open = isExpanded(sectionKey);
|
||||||
|
|
||||||
const blocks = [
|
const blocks = [
|
||||||
{
|
{
|
||||||
type: "header",
|
type: "section",
|
||||||
text: { type: "plain_text", text: `${emoji} ${title}` },
|
text: {
|
||||||
},
|
type: "mrkdwn",
|
||||||
{
|
text: `${open ? ":large_orange_diamond:" : ":small_orange_diamond:"} ${emoji} *${title}* · ${items.length} payment${items.length !== 1 ? "s" : ""} · ${formatCurrency(total)}`,
|
||||||
type: "context",
|
},
|
||||||
elements: [
|
accessory: {
|
||||||
{
|
type: "button",
|
||||||
type: "mrkdwn",
|
text: { type: "plain_text", text: open ? "Collapse" : "Expand" },
|
||||||
text: `${items.length} payment${items.length !== 1 ? "s" : ""} · ${formatCurrency(total)} total`,
|
action_id: `toggle_section_${sectionKey}`,
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
blocks.push({ type: "divider" });
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
blocks.push({
|
blocks.push({
|
||||||
type: "section",
|
type: "section",
|
||||||
|
|
@ -460,23 +500,28 @@ function buildHomeView(payments, metadata, boaTransactions = []) {
|
||||||
|
|
||||||
const buildOutstandingBlocks = () => {
|
const buildOutstandingBlocks = () => {
|
||||||
const totalAll = outstandingChecks.reduce((sum, p) => sum + p.amount_usd, 0);
|
const totalAll = outstandingChecks.reduce((sum, p) => sum + p.amount_usd, 0);
|
||||||
|
const open = isExpanded("outstanding");
|
||||||
|
|
||||||
const blocks = [
|
const blocks = [
|
||||||
{
|
{
|
||||||
type: "header",
|
type: "section",
|
||||||
text: { type: "plain_text", text: ":warning: Outstanding Checks" },
|
text: {
|
||||||
},
|
type: "mrkdwn",
|
||||||
{
|
text: `${open ? ":large_orange_diamond:" : ":small_orange_diamond:"} :warning: *Outstanding Checks* · ${outstandingChecks.length} check${outstandingChecks.length !== 1 ? "s" : ""} · ${formatCurrency(totalAll)}`,
|
||||||
type: "context",
|
},
|
||||||
elements: [
|
accessory: {
|
||||||
{
|
type: "button",
|
||||||
type: "mrkdwn",
|
text: { type: "plain_text", text: open ? "Collapse" : "Expand" },
|
||||||
text: `${outstandingChecks.length} check${outstandingChecks.length !== 1 ? "s" : ""} · ${formatCurrency(totalAll)} total`,
|
action_id: "toggle_section_outstanding",
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
blocks.push({ type: "divider" });
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
if (!outstandingChecks.length) {
|
if (!outstandingChecks.length) {
|
||||||
blocks.push({
|
blocks.push({
|
||||||
type: "section",
|
type: "section",
|
||||||
|
|
@ -517,6 +562,7 @@ function buildHomeView(payments, metadata, boaTransactions = []) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "home",
|
type: "home",
|
||||||
|
private_metadata: JSON.stringify({ expanded }),
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
type: "header",
|
type: "header",
|
||||||
|
|
@ -555,8 +601,8 @@ function buildHomeView(payments, metadata, boaTransactions = []) {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ type: "divider" },
|
{ type: "divider" },
|
||||||
...buildScheduledBlocks("Scheduled Checks", ":ledger:", scheduledChecks, "checks"),
|
...buildScheduledBlocks("Scheduled Checks", ":ledger:", scheduledChecks, "scheduled_checks", "checks"),
|
||||||
...buildScheduledBlocks("Scheduled ACH", ":electric_plug:", scheduledACH, "ach"),
|
...buildScheduledBlocks("Scheduled ACH", ":electric_plug:", scheduledACH, "scheduled_ach", "ach"),
|
||||||
...buildOutstandingBlocks(),
|
...buildOutstandingBlocks(),
|
||||||
...buildBoABlocks(boaTransactions),
|
...buildBoABlocks(boaTransactions),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue