ab4ec91e02
Sends a Monday digest to all ADMIN + MANAGER users with: - Team utilization % for the next 4 weeks - Overbooked resource count - Open demand count - Upcoming vacation count - Top 5 most utilized resources Route: GET /api/cron/weekly-digest (secured by CRON_SECRET). HTML template and plain-text fallback included. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
129 lines
5.5 KiB
TypeScript
129 lines
5.5 KiB
TypeScript
export interface WeeklyDigestData {
|
|
weekLabel: string;
|
|
teamUtilizationPct: number;
|
|
overbookedCount: number;
|
|
openDemandCount: number;
|
|
upcomingVacationCount: number;
|
|
topResources: Array<{ name: string; utilizationPct: number }>;
|
|
appBaseUrl: string;
|
|
}
|
|
|
|
export function buildWeeklyDigestHtml(data: WeeklyDigestData): string {
|
|
const utilizationColor =
|
|
data.teamUtilizationPct >= 90
|
|
? "#d97706"
|
|
: data.teamUtilizationPct >= 70
|
|
? "#059669"
|
|
: "#6b7280";
|
|
|
|
const resourceRows = data.topResources
|
|
.map(
|
|
(r) =>
|
|
`<tr>
|
|
<td style="padding:6px 12px;font-size:13px;color:#374151">${r.name}</td>
|
|
<td style="padding:6px 12px;font-size:13px;color:#374151;text-align:right">${Math.round(r.utilizationPct)}%</td>
|
|
</tr>`,
|
|
)
|
|
.join("");
|
|
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
|
<body style="margin:0;padding:0;background:#f9fafb;font-family:system-ui,-apple-system,sans-serif">
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f9fafb;padding:24px 0">
|
|
<tr><td align="center">
|
|
<table width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden">
|
|
|
|
<!-- Header -->
|
|
<tr>
|
|
<td style="background:#1e3a5f;padding:24px 32px">
|
|
<p style="margin:0;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.15em;color:#93c5fd">Weekly Digest</p>
|
|
<h1 style="margin:8px 0 0;font-size:20px;font-weight:700;color:#ffffff">CapaKraken — ${data.weekLabel}</h1>
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Stats row -->
|
|
<tr>
|
|
<td style="padding:24px 32px">
|
|
<table width="100%" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td style="text-align:center;padding:0 8px">
|
|
<p style="margin:0;font-size:32px;font-weight:700;color:${utilizationColor}">${Math.round(data.teamUtilizationPct)}%</p>
|
|
<p style="margin:4px 0 0;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.1em">Team Utilization</p>
|
|
</td>
|
|
<td style="text-align:center;padding:0 8px">
|
|
<p style="margin:0;font-size:32px;font-weight:700;color:${data.overbookedCount > 0 ? "#dc2626" : "#059669"}">${data.overbookedCount}</p>
|
|
<p style="margin:4px 0 0;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.1em">Overbooked</p>
|
|
</td>
|
|
<td style="text-align:center;padding:0 8px">
|
|
<p style="margin:0;font-size:32px;font-weight:700;color:#d97706">${data.openDemandCount}</p>
|
|
<p style="margin:4px 0 0;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.1em">Open Demand</p>
|
|
</td>
|
|
<td style="text-align:center;padding:0 8px">
|
|
<p style="margin:0;font-size:32px;font-weight:700;color:#6b7280">${data.upcomingVacationCount}</p>
|
|
<p style="margin:4px 0 0;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.1em">On Vacation</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
${
|
|
data.topResources.length > 0
|
|
? `<!-- Top utilization table -->
|
|
<tr>
|
|
<td style="padding:0 32px 24px">
|
|
<p style="margin:0 0 12px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.12em;color:#6b7280">Top Utilization</p>
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:8px;overflow:hidden">
|
|
<thead>
|
|
<tr style="background:#f9fafb">
|
|
<th style="padding:8px 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.1em;color:#6b7280;text-align:left">Resource</th>
|
|
<th style="padding:8px 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.1em;color:#6b7280;text-align:right">Utilization</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>${resourceRows}</tbody>
|
|
</table>
|
|
</td>
|
|
</tr>`
|
|
: ""
|
|
}
|
|
|
|
<!-- CTA -->
|
|
<tr>
|
|
<td style="padding:0 32px 32px;text-align:center">
|
|
<a href="${data.appBaseUrl}/timeline"
|
|
style="display:inline-block;background:#2563eb;color:#ffffff;font-weight:600;font-size:14px;padding:12px 28px;border-radius:8px;text-decoration:none">
|
|
Open Timeline
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Footer -->
|
|
<tr>
|
|
<td style="background:#f9fafb;border-top:1px solid #e5e7eb;padding:16px 32px">
|
|
<p style="margin:0;font-size:11px;color:#9ca3af;text-align:center">
|
|
CapaKraken · Automated weekly digest · Sent every Monday
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
|
|
</table>
|
|
</td></tr>
|
|
</table>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
export function buildWeeklyDigestText(data: WeeklyDigestData): string {
|
|
return `CapaKraken Weekly Digest — ${data.weekLabel}
|
|
|
|
Team Utilization: ${Math.round(data.teamUtilizationPct)}%
|
|
Overbooked: ${data.overbookedCount}
|
|
Open Demand: ${data.openDemandCount}
|
|
Upcoming Vacation: ${data.upcomingVacationCount}
|
|
|
|
${data.topResources.length > 0 ? `Top Utilization:\n${data.topResources.map((r) => ` ${r.name}: ${Math.round(r.utilizationPct)}%`).join("\n")}\n` : ""}
|
|
Open Timeline: ${data.appBaseUrl}/timeline
|
|
`;
|
|
}
|