mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-10-05 09:32:08 +00:00
184 lines
5.1 KiB
JavaScript
184 lines
5.1 KiB
JavaScript
|
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
||
|
|
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
|
||
|
|
|
||
|
|
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient());
|
||
|
|
const TABLE_NAME = process.env.TABLE_NAME;
|
||
|
|
const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
|
||
|
|
|
||
|
|
export const handler = async (event) => {
|
||
|
|
const body = JSON.parse(event.body || "{}");
|
||
|
|
|
||
|
|
// Handle Slack URL verification challenge
|
||
|
|
if (body.type === "url_verification") {
|
||
|
|
return { statusCode: 200, body: body.challenge };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Only process app_home_opened events
|
||
|
|
if (body.event?.type !== "app_home_opened") {
|
||
|
|
return { statusCode: 200, body: JSON.stringify({ ok: true }) };
|
||
|
|
}
|
||
|
|
|
||
|
|
const userId = body.event.user;
|
||
|
|
|
||
|
|
// Get dashboard data from DynamoDB
|
||
|
|
const { Item: dashboard } = await ddb.send(
|
||
|
|
new GetCommand({
|
||
|
|
TableName: TABLE_NAME,
|
||
|
|
Key: { pk: "current_dashboard" },
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!dashboard) {
|
||
|
|
return { statusCode: 200, body: JSON.stringify({ error: "No dashboard data" }) };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build and publish the home view
|
||
|
|
const view = buildHomeView(dashboard);
|
||
|
|
|
||
|
|
const res = await fetch("https://slack.com/api/views.publish", {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
Authorization: `Bearer ${SLACK_BOT_TOKEN}`,
|
||
|
|
"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 API error:", JSON.stringify(data));
|
||
|
|
return { statusCode: 500, body: JSON.stringify(data) };
|
||
|
|
}
|
||
|
|
|
||
|
|
return { statusCode: 200, body: JSON.stringify({ ok: true }) };
|
||
|
|
};
|
||
|
|
|
||
|
|
function buildHomeView(dashboard) {
|
||
|
|
const payments = Array.isArray(dashboard.payments) ? dashboard.payments : [];
|
||
|
|
|
||
|
|
const formatCurrency = (value) =>
|
||
|
|
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(
|
||
|
|
Number(value || 0)
|
||
|
|
);
|
||
|
|
|
||
|
|
const parseMDYLocal = (value) => {
|
||
|
|
if (!value) return null;
|
||
|
|
const parts = String(value).split("/");
|
||
|
|
if (parts.length !== 3) return null;
|
||
|
|
const [mm, dd, yyyy] = parts.map((p) => parseInt(p, 10));
|
||
|
|
if (!mm || !dd || !yyyy) return null;
|
||
|
|
return new Date(yyyy, mm - 1, dd);
|
||
|
|
};
|
||
|
|
|
||
|
|
const formatDateKey = (date) => {
|
||
|
|
const y = date.getFullYear();
|
||
|
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||
|
|
const d = String(date.getDate()).padStart(2, "0");
|
||
|
|
return `${y}-${m}-${d}`;
|
||
|
|
};
|
||
|
|
|
||
|
|
const formatDisplayDate = (dateKey) => {
|
||
|
|
const [y, m, d] = dateKey.split("-").map(Number);
|
||
|
|
const dt = new Date(y, m - 1, d);
|
||
|
|
return dt.toLocaleDateString("en-US", {
|
||
|
|
weekday: "short",
|
||
|
|
month: "short",
|
||
|
|
day: "numeric",
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const today = new Date();
|
||
|
|
today.setHours(0, 0, 0, 0);
|
||
|
|
|
||
|
|
const futurePayments = payments.filter((p) => {
|
||
|
|
const dt = parseMDYLocal(p.send_payment_on);
|
||
|
|
return dt && dt >= today && (p.method === "ACH" || p.method === "Check");
|
||
|
|
});
|
||
|
|
|
||
|
|
const groupByMethodAndDay = (methodName) => {
|
||
|
|
const grouped = {};
|
||
|
|
|
||
|
|
for (const payment of futurePayments) {
|
||
|
|
if (payment.method !== methodName) continue;
|
||
|
|
const dt = parseMDYLocal(payment.send_payment_on);
|
||
|
|
if (!dt) continue;
|
||
|
|
const key = formatDateKey(dt);
|
||
|
|
|
||
|
|
if (!grouped[key]) grouped[key] = { count: 0, total: 0 };
|
||
|
|
grouped[key].count += 1;
|
||
|
|
grouped[key].total += payment.amount_usd;
|
||
|
|
}
|
||
|
|
|
||
|
|
return Object.keys(grouped)
|
||
|
|
.sort()
|
||
|
|
.map((dateKey) => ({
|
||
|
|
date_key: dateKey,
|
||
|
|
display_date: formatDisplayDate(dateKey),
|
||
|
|
count: grouped[dateKey].count,
|
||
|
|
total: Number(grouped[dateKey].total.toFixed(2)),
|
||
|
|
}));
|
||
|
|
};
|
||
|
|
|
||
|
|
const achDays = groupByMethodAndDay("ACH");
|
||
|
|
const checkDays = groupByMethodAndDay("Check");
|
||
|
|
|
||
|
|
const buildDayBlocks = (title, days) => {
|
||
|
|
const total = days.reduce((sum, d) => sum + d.total, 0);
|
||
|
|
const count = days.reduce((sum, d) => sum + d.count, 0);
|
||
|
|
|
||
|
|
const blocks = [
|
||
|
|
{
|
||
|
|
type: "section",
|
||
|
|
text: {
|
||
|
|
type: "mrkdwn",
|
||
|
|
text: `*${title}*\n${count} payments • ${formatCurrency(total)}`,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{ type: "divider" },
|
||
|
|
];
|
||
|
|
|
||
|
|
if (!days.length) {
|
||
|
|
blocks.push({
|
||
|
|
type: "section",
|
||
|
|
text: { type: "mrkdwn", text: "_No upcoming payments found._" },
|
||
|
|
});
|
||
|
|
blocks.push({ type: "divider" });
|
||
|
|
return blocks;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const day of days) {
|
||
|
|
blocks.push({
|
||
|
|
type: "section",
|
||
|
|
fields: [
|
||
|
|
{ type: "mrkdwn", text: `*${day.display_date}*` },
|
||
|
|
{ type: "mrkdwn", text: `${day.count} payments • ${formatCurrency(day.total)}` },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
blocks.push({ type: "divider" });
|
||
|
|
return blocks;
|
||
|
|
};
|
||
|
|
|
||
|
|
return {
|
||
|
|
type: "home",
|
||
|
|
blocks: [
|
||
|
|
{
|
||
|
|
type: "header",
|
||
|
|
text: { type: "plain_text", text: "Payments Dashboard" },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
type: "context",
|
||
|
|
elements: [
|
||
|
|
{ type: "mrkdwn", text: `*Last updated:* ${dashboard.last_updated || "N/A"}` },
|
||
|
|
{ type: "mrkdwn", text: `*Source:* ${dashboard.file_name || "N/A"}` },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{ type: "divider" },
|
||
|
|
...buildDayBlocks("ACH", achDays),
|
||
|
|
...buildDayBlocks("Check", checkDays),
|
||
|
|
],
|
||
|
|
};
|
||
|
|
}
|