Files
CapaKraken/packages/api/src/lib/weekly-digest-template.ts
T
Hartmut ab4ec91e02 feat(digest): add weekly capacity digest email cron
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>
2026-04-09 13:33:12 +02:00

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
`;
}