← back to Daily Reporting

slack-formatter.ts

296 lines

/**
 * Slack Message Formatter for Daily Reports
 *
 * Formats daily reports into clean Slack markdown with charts and metrics
 */

import type { DailyReport } from './report-generator';

/**
 * Generate a health bar visualization
 */
function generateHealthBar(percentage: number, width: number = 20): string {
  const filled = Math.round((percentage / 100) * width);
  const empty = width - filled;

  const filledChar = percentage >= 80 ? '🟢' : percentage >= 60 ? '🟡' : '🔴';
  const emptyChar = '⚪';

  return filledChar.repeat(Math.max(0, filled)) + emptyChar.repeat(Math.max(0, empty)) + ` ${percentage}%`;
}

/**
 * Format severity emoji
 */
function getSeverityEmoji(severity: string): string {
  switch (severity) {
    case 'critical': return '🚨';
    case 'high': return '⚠️';
    case 'medium': return '⚡';
    case 'low': return 'ℹ️';
    default: return '•';
  }
}

/**
 * Format status emoji
 */
function getStatusEmoji(status: string): string {
  switch (status) {
    case 'online': return '✅';
    case 'offline': return '❌';
    case 'degraded': return '⚠️';
    default: return '❓';
  }
}

/**
 * Format report header
 */
function formatHeader(report: DailyReport): string {
  const emoji = report.reportType === 'morning' ? '☀️' : '🌙';
  const title = report.reportType === 'morning' ? 'MORNING STANDUP REPORT' : 'END-OF-DAY SUMMARY';
  const date = new Date(report.timestamp).toLocaleDateString('en-US', {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });

  return `${emoji} *${title}*\n${date}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
}

/**
 * Format executive summary
 */
function formatExecutiveSummary(summary: DailyReport['executiveSummary']): string {
  let text = '*📊 EXECUTIVE SUMMARY*\n\n';

  text += `*System Health:* ${generateHealthBar(summary.systemHealth, 15)}\n`;
  text += `*Agents Online:* ${summary.agentsOnline}/${summary.totalAgents}\n`;
  text += `*Tasks Completed Today:* ${summary.tasksCompletedToday}\n`;
  text += `*Critical Issues:* ${summary.criticalIssues === 0 ? '✅ None' : `🚨 ${summary.criticalIssues}`}\n\n`;

  if (summary.topAchievements.length > 0) {
    text += '*🏆 Top Achievements:*\n';
    summary.topAchievements.forEach((achievement, index) => {
      text += `  ${index + 1}. ${achievement}\n`;
    });
  }

  return text + '\n';
}

/**
 * Format department reports
 */
function formatDepartmentReports(reports: DailyReport['departmentReports']): string {
  let text = '*🏢 DEPARTMENT REPORTS*\n\n';

  // Group by executive, department heads, and support
  const sortedReports = [...reports].sort((a, b) => {
    const order: Record<string, number> = {
      'Executive': 0,
      'Legal': 1,
      'Finance': 2,
      'Marketing': 3,
      'Operations': 4,
      'Strategy': 5,
      'Customer Service': 6,
      'Sales': 7,
      'IT Operations': 8,
      'Product': 9
    };
    return (order[a.department] || 99) - (order[b.department] || 99);
  });

  for (const dept of sortedReports) {
    const healthPercentage = Math.round((dept.onlineCount / dept.agentsCount) * 100);

    text += `*${dept.department}* (${dept.onlineCount}/${dept.agentsCount} online)\n`;
    text += `${generateHealthBar(healthPercentage, 10)}\n`;
    text += `Tasks Completed: ${dept.totalTasks}\n`;

    // Show agent details
    dept.metrics.forEach(metric => {
      text += `  ${getStatusEmoji(metric.status)} _${metric.agentName}_`;
      if (metric.status === 'online') {
        text += ` • ${metric.tasksCompleted} tasks`;
        if (metric.errorsToday > 0) {
          text += ` • ⚠️ ${metric.errorsToday} errors`;
        }
      }
      text += '\n';
    });

    // Highlights
    if (dept.highlights.length > 0) {
      text += '  ✨ *Highlights:*\n';
      dept.highlights.forEach(h => text += `    • ${h}\n`);
    }

    // Issues
    if (dept.issues.length > 0) {
      text += '  ⚠️ *Issues:*\n';
      dept.issues.forEach(i => text += `    • ${i}\n`);
    }

    text += '\n';
  }

  return text;
}

/**
 * Format issues requiring attention
 */
function formatIssues(issues: DailyReport['issuesRequiringAttention']): string {
  if (issues.length === 0) {
    return '*🎉 ISSUES REQUIRING ATTENTION*\n\n✅ *No critical issues detected!* All systems operational.\n\n';
  }

  let text = '*🚨 ISSUES REQUIRING ATTENTION*\n\n';

  issues.forEach((issue, index) => {
    text += `${getSeverityEmoji(issue.severity)} *[${issue.severity.toUpperCase()}]* ${issue.agent}\n`;
    text += `   _Issue:_ ${issue.issue}\n`;
    text += `   _Action:_ ${issue.actionRequired}\n\n`;
  });

  return text;
}

/**
 * Format action items
 */
function formatActionItems(actionItems: string[]): string {
  if (actionItems.length === 0) {
    return '*✅ ACTION ITEMS*\n\nNo pending action items.\n\n';
  }

  let text = '*✅ ACTION ITEMS*\n\n';

  actionItems.forEach((item, index) => {
    text += `${index + 1}. ${item}\n`;
  });

  return text + '\n';
}

/**
 * Format Q&A readiness status
 */
function formatQnaStatus(qnaStatus: DailyReport['qnaStatus']): string {
  let text = '*💬 Q&A READINESS STATUS*\n\n';

  text += `*Overall Readiness:* ${generateHealthBar(qnaStatus.overallReadiness, 15)}\n\n`;

  text += '*Agent Readiness Scores:*\n';
  qnaStatus.agentReadiness.slice(0, 10).forEach(agent => {
    const emoji = agent.score >= 80 ? '🟢' : agent.score >= 60 ? '🟡' : '🔴';
    text += `${emoji} ${agent.agent}: ${agent.score}%\n`;
  });

  return text + '\n';
}

/**
 * Format footer
 */
function formatFooter(report: DailyReport): string {
  const nextReport = report.reportType === 'morning'
    ? 'End-of-Day Summary at 5:00 PM'
    : 'Morning Standup at 6:00 AM';

  return `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
         `📅 Next Report: ${nextReport}\n` +
         `🔗 Master Hub: http://45.61.58.125:9893\n` +
         `Generated at ${new Date(report.timestamp).toLocaleTimeString()}`;
}

/**
 * Format complete report for Slack
 */
export function formatSlackReport(report: DailyReport, mentionChannel: boolean = false): string {
  let message = '';

  // Mention channel if there are critical issues
  if (mentionChannel && report.executiveSummary.criticalIssues > 0) {
    message += '<!channel> *Attention Required* - Critical issues detected!\n\n';
  }

  message += formatHeader(report);
  message += formatExecutiveSummary(report.executiveSummary);
  message += formatDepartmentReports(report.departmentReports);
  message += formatIssues(report.issuesRequiringAttention);
  message += formatActionItems(report.actionItems);
  message += formatQnaStatus(report.qnaStatus);
  message += formatFooter(report);

  return message;
}

/**
 * Format report as JSON for Slack blocks (advanced formatting)
 */
export function formatSlackBlocks(report: DailyReport): any[] {
  const blocks: any[] = [];

  // Header
  blocks.push({
    type: 'header',
    text: {
      type: 'plain_text',
      text: report.reportType === 'morning' ? '☀️ Morning Standup Report' : '🌙 End-of-Day Summary',
      emoji: true
    }
  });

  blocks.push({ type: 'divider' });

  // Executive Summary
  blocks.push({
    type: 'section',
    text: {
      type: 'mrkdwn',
      text: `*📊 EXECUTIVE SUMMARY*\n\n` +
            `*System Health:* ${report.executiveSummary.systemHealth}%\n` +
            `*Agents Online:* ${report.executiveSummary.agentsOnline}/${report.executiveSummary.totalAgents}\n` +
            `*Tasks Completed:* ${report.executiveSummary.tasksCompletedToday}\n` +
            `*Critical Issues:* ${report.executiveSummary.criticalIssues}`
    }
  });

  // Critical issues alert
  if (report.issuesRequiringAttention.length > 0) {
    blocks.push({
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*🚨 ${report.issuesRequiringAttention.length} Issues Require Attention*`
      }
    });
  }

  blocks.push({ type: 'divider' });

  // Action button
  blocks.push({
    type: 'actions',
    elements: [
      {
        type: 'button',
        text: {
          type: 'plain_text',
          text: '📊 View Full Dashboard',
          emoji: true
        },
        url: 'http://45.61.58.125:9893',
        style: 'primary'
      }
    ]
  });

  return blocks;
}