import { DailyDigestConfig, DailyDigestEmail, LifecycleItem } from './types'; import { formatDisplayDate, getRelativeDueDateLabel, getTodayDateString } from './dateUtils'; function escapeHtml(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } export function buildDailyDigestEmail(params: { recipientEmail: string; recipientName: string; config: DailyDigestConfig; overdueItems: LifecycleItem[]; upcomingItems: LifecycleItem[]; }): DailyDigestEmail { const { recipientEmail, recipientName, config, overdueItems, upcomingItems } = params; const todayStr = getTodayDateString(); const totalCount = overdueItems.length + upcomingItems.length; const subject = totalCount === 0 ? `[Lifecycle Tracker] All clear for today (${todayStr})` : overdueItems.length > 0 ? `[Action Required] ${overdueItems.length} overdue & ${upcomingItems.length} upcoming lifecycle items` : `[Upcoming Actions] ${upcomingItems.length} upcoming lifecycle items`; // Text content const textLines: string[] = [ `PERSONAL LIFECYCLE TRACKER - DAILY DIGEST`, `Generated for: ${recipientName} (${recipientEmail})`, `Date: ${todayStr} | Scheduled delivery: ${config.deliveryTime} (${config.timeZone})`, `--------------------------------------------------`, ``, ]; if (overdueItems.length > 0) { textLines.push(`🔴 OVERDUE ITEMS (${overdueItems.length}):`); overdueItems.forEach((item, idx) => { const rel = getRelativeDueDateLabel(item.dueDate, item.status, todayStr); const groupInfo = item.groupName ? ` [Group: ${item.groupName}]` : ' [Personal]'; textLines.push(` ${idx + 1}. [${item.lifecycleType.toUpperCase()}] ${item.name}`); textLines.push(` Due: ${formatDisplayDate(item.dueDate)} (${rel})${groupInfo}`); }); textLines.push(``); } if (upcomingItems.length > 0) { textLines.push(`🟡 UPCOMING ITEMS (${upcomingItems.length}):`); upcomingItems.forEach((item, idx) => { const rel = getRelativeDueDateLabel(item.dueDate, item.status, todayStr); const groupInfo = item.groupName ? ` [Group: ${item.groupName}]` : ' [Personal]'; textLines.push(` ${idx + 1}. [${item.lifecycleType.toUpperCase()}] ${item.name}`); textLines.push(` Due: ${formatDisplayDate(item.dueDate)} (${rel})${groupInfo}`); }); textLines.push(``); } if (totalCount === 0) { textLines.push(`You have zero overdue or upcoming items scheduled for the next 30 days.`); textLines.push(``); } textLines.push(`--------------------------------------------------`); textLines.push(`Delivered automatically by your Daily Digest schedule.`); textLines.push(`Note: Reminder channels are always active.`); // HTML content const safeRecipientName = escapeHtml(recipientName); const safeSubject = escapeHtml(subject); const safeTimeZone = escapeHtml(config.timeZone); const htmlContent = ` ${safeSubject}

Personal Lifecycle Tracker

Daily Digest for ${safeRecipientName} • ${todayStr} • ${config.deliveryTime} (${safeTimeZone})

${ overdueItems.length > 0 ? `
⚠️ Overdue Items (${overdueItems.length})
${overdueItems .map( (item) => `
${escapeHtml(item.name)} ${item.lifecycleType}
Due: ${formatDisplayDate(item.dueDate)} • ${getRelativeDueDateLabel(item.dueDate, item.status, todayStr)} ${item.groupName ? ` • Group: ${escapeHtml(item.groupName)}` : ' • Personal'}
` ) .join('')} ` : '' } ${ upcomingItems.length > 0 ? `
⏳ Upcoming Actions (${upcomingItems.length})
${upcomingItems .map( (item) => `
${escapeHtml(item.name)} ${item.lifecycleType}
Due: ${formatDisplayDate(item.dueDate)} • ${getRelativeDueDateLabel(item.dueDate, item.status, todayStr)} ${item.groupName ? ` • Group: ${escapeHtml(item.groupName)}` : ' • Personal'}
` ) .join('')} ` : '' } ${ totalCount === 0 ? `
✅
All caught up!
No overdue or upcoming actions in the next 30 days.
` : '' }
`.trim(); return { id: `digest_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`, recipientEmail, recipientName, generatedAt: new Date().toISOString(), deliveryTime: config.deliveryTime, timeZone: config.timeZone, subject, overdueCount: overdueItems.length, upcomingCount: upcomingItems.length, overdueItems, upcomingItems, htmlContent, textContent: textLines.join('\n'), }; }