mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-10-07 16:19:03 +00:00
Redesign dashboard: 6 outstanding age buckets, summary bar, improved layout
This commit is contained in:
parent
53842f0821
commit
7f94eee308
1 changed files with 87 additions and 46 deletions
|
|
@ -103,14 +103,14 @@ function buildHomeView(payments, metadata) {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const daysSince = (dt) => Math.floor((today - dt) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
// Categorize payments
|
// Categorize payments
|
||||||
const scheduledChecks = [];
|
const scheduledChecks = [];
|
||||||
const scheduledACH = [];
|
const scheduledACH = [];
|
||||||
const outstandingChecks = [];
|
const outstandingChecks = [];
|
||||||
|
|
||||||
const skipStatuses = ["voided", "cancelled", "canceled", "marked as void", "cleared"];
|
const skipStatuses = ["voided", "cancelled", "canceled", "marked as void", "cleared"];
|
||||||
const ninetyDaysAgo = new Date(today);
|
|
||||||
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
|
|
||||||
|
|
||||||
for (const p of payments) {
|
for (const p of payments) {
|
||||||
if (p.method !== "ACH" && p.method !== "Check") continue;
|
if (p.method !== "ACH" && p.method !== "Check") continue;
|
||||||
|
|
@ -122,14 +122,11 @@ function buildHomeView(payments, metadata) {
|
||||||
if (skipStatuses.includes(status)) continue;
|
if (skipStatuses.includes(status)) continue;
|
||||||
|
|
||||||
if (dt >= today) {
|
if (dt >= today) {
|
||||||
// Future — scheduled
|
|
||||||
if (p.method === "Check") scheduledChecks.push(p);
|
if (p.method === "Check") scheduledChecks.push(p);
|
||||||
else scheduledACH.push(p);
|
else scheduledACH.push(p);
|
||||||
} else if (p.method === "Check") {
|
} else if (p.method === "Check") {
|
||||||
// Past-due check not cleared — outstanding
|
|
||||||
outstandingChecks.push(p);
|
outstandingChecks.push(p);
|
||||||
}
|
}
|
||||||
// Past-due ACH not cleared is ignored (should have been auto-cleared)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by date
|
// Sort by date
|
||||||
|
|
@ -141,28 +138,50 @@ function buildHomeView(payments, metadata) {
|
||||||
scheduledChecks.sort(byDate);
|
scheduledChecks.sort(byDate);
|
||||||
scheduledACH.sort(byDate);
|
scheduledACH.sort(byDate);
|
||||||
|
|
||||||
// Split outstanding into <=90 days and >90 days
|
// Age buckets for outstanding checks
|
||||||
const outstanding90 = outstandingChecks.filter((p) => parseMDYLocal(p.send_payment_on) >= ninetyDaysAgo);
|
const ageBuckets = [
|
||||||
const outstandingOver90 = outstandingChecks.filter((p) => parseMDYLocal(p.send_payment_on) < ninetyDaysAgo);
|
{ 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 buildScheduledBlocks = (title, emoji, items) => {
|
||||||
const total = items.reduce((sum, p) => sum + p.amount_usd, 0);
|
const total = items.reduce((sum, p) => sum + p.amount_usd, 0);
|
||||||
|
|
||||||
const blocks = [
|
const blocks = [
|
||||||
{
|
{
|
||||||
type: "section",
|
type: "header",
|
||||||
text: {
|
text: { type: "plain_text", text: `${emoji} ${title}` },
|
||||||
type: "mrkdwn",
|
},
|
||||||
text: `*${emoji} ${title}*\n${items.length} payments • ${formatCurrency(total)}`,
|
{
|
||||||
},
|
type: "context",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: "mrkdwn",
|
||||||
|
text: `${items.length} payment${items.length !== 1 ? "s" : ""} · ${formatCurrency(total)} total`,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ type: "divider" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
blocks.push({
|
blocks.push({
|
||||||
type: "section",
|
type: "section",
|
||||||
text: { type: "mrkdwn", text: "_None_" },
|
text: { type: "mrkdwn", text: "_No upcoming payments_" },
|
||||||
});
|
});
|
||||||
blocks.push({ type: "divider" });
|
blocks.push({ type: "divider" });
|
||||||
return blocks;
|
return blocks;
|
||||||
|
|
@ -181,14 +200,16 @@ function buildHomeView(payments, metadata) {
|
||||||
const group = grouped[key];
|
const group = grouped[key];
|
||||||
const dayTotal = group.payments.reduce((sum, p) => sum + p.amount_usd, 0);
|
const dayTotal = group.payments.reduce((sum, p) => sum + p.amount_usd, 0);
|
||||||
const dateLabel = group.date ? formatDisplayDate(group.date) : "Unknown";
|
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({
|
blocks.push({
|
||||||
type: "section",
|
type: "section",
|
||||||
fields: [
|
fields: [
|
||||||
{ type: "mrkdwn", text: `*${dateLabel}*` },
|
{ type: "mrkdwn", text: `*${dateLabel}* ${daysTag}` },
|
||||||
{
|
{
|
||||||
type: "mrkdwn",
|
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 = [
|
const blocks = [
|
||||||
{
|
{
|
||||||
type: "section",
|
type: "header",
|
||||||
text: {
|
text: { type: "plain_text", text: ":warning: Outstanding Checks" },
|
||||||
type: "mrkdwn",
|
},
|
||||||
text: `*:warning: Outstanding Checks*\n${outstandingChecks.length} checks • ${formatCurrency(totalAll)}`,
|
{
|
||||||
},
|
type: "context",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: "mrkdwn",
|
||||||
|
text: `${outstandingChecks.length} check${outstandingChecks.length !== 1 ? "s" : ""} · ${formatCurrency(totalAll)} total`,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ type: "divider" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!outstandingChecks.length) {
|
if (!outstandingChecks.length) {
|
||||||
blocks.push({
|
blocks.push({
|
||||||
type: "section",
|
type: "section",
|
||||||
text: { type: "mrkdwn", text: "_None_" },
|
text: { type: "mrkdwn", text: ":white_check_mark: _All checks have cleared_" },
|
||||||
});
|
});
|
||||||
blocks.push({ type: "divider" });
|
blocks.push({ type: "divider" });
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
const total90 = outstanding90.reduce((sum, p) => sum + p.amount_usd, 0);
|
for (const bucket of bucketedOutstanding) {
|
||||||
const totalOver90 = outstandingOver90.reduce((sum, p) => sum + p.amount_usd, 0);
|
if (!bucket.items.length) continue;
|
||||||
|
|
||||||
blocks.push({
|
blocks.push({
|
||||||
type: "section",
|
type: "section",
|
||||||
fields: [
|
fields: [
|
||||||
{ type: "mrkdwn", text: `*90 days or less*` },
|
{ type: "mrkdwn", text: `*${bucket.label}*` },
|
||||||
{ type: "mrkdwn", text: `${outstanding90.length} checks • ${formatCurrency(total90)}` },
|
{
|
||||||
],
|
type: "mrkdwn",
|
||||||
});
|
text: `${bucket.items.length} check${bucket.items.length !== 1 ? "s" : ""} · *${formatCurrency(bucket.total)}*`,
|
||||||
|
},
|
||||||
blocks.push({
|
],
|
||||||
type: "section",
|
});
|
||||||
fields: [
|
}
|
||||||
{ type: "mrkdwn", text: `*Over 90 days*` },
|
|
||||||
{ type: "mrkdwn", text: `${outstandingOver90.length} checks • ${formatCurrency(totalOver90)}` },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
blocks.push({ type: "divider" });
|
blocks.push({ type: "divider" });
|
||||||
return blocks;
|
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 {
|
return {
|
||||||
type: "home",
|
type: "home",
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
type: "header",
|
type: "header",
|
||||||
text: { type: "plain_text", text: "Payments Dashboard" },
|
text: { type: "plain_text", text: ":bank: Payments Dashboard" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "context",
|
type: "context",
|
||||||
elements: [
|
elements: [
|
||||||
{ type: "mrkdwn", text: `*Last updated:* ${metadata?.last_updated || "N/A"}` },
|
{ type: "mrkdwn", text: `Last updated · ${metadata?.last_updated || "N/A"}` },
|
||||||
{ type: "mrkdwn", text: `*Source:* ${metadata?.file_name || "N/A"}` },
|
{ type: "mrkdwn", text: `Source · ${metadata?.file_name || "N/A"}` },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ type: "divider" },
|
{ 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(),
|
...buildOutstandingBlocks(),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue