8
n8n 中文网amn8n.com

统一支持团队绩效仪表板(Zendesk & Freshdesk → Google Sheets + Slack + Gmail)

高级

这是一个Content Creation, Multimodal AI领域的自动化工作流,包含 24 个节点。主要使用 If, Code, Cron, Gmail, Merge 等节点。 使用Sheets、Slack和Gmail跨Zendesk和Freshdesk跟踪支持绩效指标

前置要求
  • Google 账号和 Gmail API 凭证
  • Slack Bot Token 或 Webhook URL
  • Google Sheets API 凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "QyegIMfwfC0kN3kN",
  "meta": {
    "instanceId": "8443f10082278c46aa5cf3acf8ff0f70061a2c58bce76efac814b16290845177",
    "templateCredsSetupCompleted": true
  },
  "name": "统一支持团队绩效仪表板 (Zendesk & Freshdesk → Google Sheets + Slack + Gmail)",
  "tags": [],
  "nodes": [
    {
      "id": "af98b36c-8053-4882-b3ba-2b90fb5c7523",
      "name": "发送 Slack 警报",
      "type": "n8n-nodes-base.slack",
      "position": [
        1936,
        -64
      ],
      "webhookId": "830d4420-a0bc-4f78-b5d2-178ee2154efc",
      "parameters": {
        "text": "={{ $json.text }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C09FM9N8UEA",
          "cachedResultName": "zendesk-churn-alerts"
        },
        "otherOptions": {
          "mrkdwn": true
        }
      },
      "credentials": {
        "slackApi": {
          "id": "rNqvWj9TfChPVRYY",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "df0ba9dc-3737-4030-a225-e58cd3c17a86",
      "name": "发送周报邮件",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1936,
        128
      ],
      "parameters": {
        "toList": [
          "{Enter Your  Email}"
        ],
        "message": "=Support Dashboard Alert",
        "subject": "={{ $json.subject }}",
        "resource": "message",
        "htmlMessage": "={{ $json.html }}",
        "includeHtml": true,
        "additionalFields": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "gEIaWCTvGfYjMSb3",
          "name": "Gmail credentials"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "b96bf81b-8486-4545-b23e-518643ca9b89",
      "name": "每周触发器",
      "type": "n8n-nodes-base.cron",
      "position": [
        0,
        32
      ],
      "parameters": {
        "triggerTimes": {
          "item": [
            {
              "hour": 20,
              "mode": "everyWeek"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "bb68236c-376b-4c00-80fd-6db781100bc2",
      "name": "从 Zendesk 获取工单",
      "type": "n8n-nodes-base.zendesk",
      "position": [
        368,
        -64
      ],
      "parameters": {
        "options": {},
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "zendeskApi": {
          "id": "gyKfBorFigBI1uj8",
          "name": "Zendesk account vivek"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "972d1914-47a0-48a3-8018-46519599bc2f",
      "name": "在 Google Sheets 中记录 KPI",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1040,
        32
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "platform",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "platform",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ticket_id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ticket_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ticket_url",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ticket_url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "subject",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "subject",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "priority",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "priority",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "priority_level",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "priority_level",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status_normalized",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "status_normalized",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "created_timestamp",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "created_timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "updated_timestamp",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "updated_timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "original_created_at",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "original_created_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "description_preview",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "description_preview",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "description",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "description",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "tags",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "tags",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "requester_id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "requester_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "assignee_id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "assignee_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "channel",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "channel",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ticket_age_hours",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ticket_age_hours",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "needs_attention",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "needs_attention",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ticket_type",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ticket_type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "due_by",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "due_by",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "is_escalated",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "is_escalated",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sentiment_score",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "sentiment_score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [
            "ticket_id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1VLAw8-Om4ULgGFfG_xwYdgni1uggTAP0UA24Nfop3RQ/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1VLAw8-Om4ULgGFfG_xwYdgni1uggTAP0UA24Nfop3RQ",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1VLAw8-Om4ULgGFfG_xwYdgni1uggTAP0UA24Nfop3RQ/edit?usp=drivesdk",
          "cachedResultName": "Performance Report Support"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "kpPEOLCGn963qpoh",
          "name": "automations@techdome.ai"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "42d5b2ac-2c2b-44ad-bb68-654213112f67",
      "name": "计算支持 KPI",
      "type": "n8n-nodes-base.code",
      "position": [
        1264,
        32
      ],
      "parameters": {
        "jsCode": "// Unified KPI Calculation Code Node for n8n Support Team Performance\n// Handles both Zendesk and Freshdesk tickets\n// Get all input items from n8n\nconst inputItems = $input.all();\nlet tickets = [];\n\n// Extract tickets from all input items\ninputItems.forEach(item => {\n  if (item.json) {\n    if (Array.isArray(item.json)) {\n      // If the item contains an array of tickets\n      tickets = tickets.concat(item.json);\n    } else {\n      // If the item is a single ticket\n      tickets.push(item.json);\n    }\n  }\n});\n\nconsole.log('Total tickets processed:', tickets.length);\n\n// Configuration thresholds (you can adjust these or get from previous node)\nconst thresholds = {\n  response_time_hours: 4,     // 4 hours response time threshold\n  resolution_time_hours: 24,  // 24 hours resolution time threshold\n  csat_threshold: 4.0,        // Minimum CSAT score\n  high_priority_threshold: 48, // Hours for high priority escalation\n};\n\n// Initialize KPI tracking variables\nlet totalTickets = tickets.length;\nlet resolvedTickets = 0;\nlet openTickets = 0;\nlet urgentTickets = 0;\nlet highPriorityTickets = 0;\nlet slaBreachedTickets = 0;\nlet channelDistribution = {};\nlet priorityDistribution = { low: 0, medium: 0, normal: 0, high: 0, urgent: 0 };\nlet platformDistribution = { zendesk: 0, freshdesk: 0, unknown: 0 };\nlet totalAgeHours = 0;\nlet needsAttentionCount = 0;\nlet escalatedTickets = 0;\nlet overdueTickets = 0;\n\n// Enhanced status mapping for both platforms\nfunction isTicketResolved(ticket) {\n  const resolvedStatuses = {\n    zendesk: ['solved', 'closed'],\n    freshdesk: ['resolved', 'closed']\n  };\n  \n  const platform = ticket.platform || 'zendesk';\n  const normalizedStatus = ticket.status_normalized || ticket.status;\n  \n  return resolvedStatuses[platform]?.includes(normalizedStatus) || \n         resolvedStatuses[platform]?.includes(ticket.status) ||\n         ['solved', 'closed', 'resolved'].includes(normalizedStatus);\n}\n\n// Enhanced priority normalization\nfunction normalizePriority(ticket) {\n  const platform = ticket.platform || 'zendesk';\n  let priority = ticket.priority;\n  \n  // Handle Freshdesk numeric priorities that weren't converted\n  if (platform === 'freshdesk' && typeof priority === 'number') {\n    const freshdeskPriorityMap = {\n      1: 'low',\n      2: 'medium',\n      3: 'high',\n      4: 'urgent'\n    };\n    priority = freshdeskPriorityMap[priority] || 'normal';\n  }\n  \n  // Normalize medium to normal for consistency\n  if (priority === 'medium') {\n    priority = 'normal';\n  }\n  \n  return priority || 'normal';\n}\n\n// Check if ticket is overdue (for Freshdesk)\nfunction isTicketOverdue(ticket) {\n  if (ticket.platform === 'freshdesk' && ticket.due_by) {\n    const dueDate = new Date(ticket.due_by);\n    const now = new Date();\n    return dueDate < now;\n  }\n  return false;\n}\n\n// Process each ticket\ntickets.forEach(ticket => {\n  // Platform distribution\n  const platform = ticket.platform || 'unknown';\n  platformDistribution[platform] = (platformDistribution[platform] || 0) + 1;\n  \n  // Basic counts\n  if (isTicketResolved(ticket)) {\n    resolvedTickets++;\n  } else {\n    openTickets++;\n  }\n  \n  // Priority analysis with normalization\n  const priority = normalizePriority(ticket);\n  priorityDistribution[priority] = (priorityDistribution[priority] || 0) + 1;\n  \n  if (priority === 'urgent') {\n    urgentTickets++;\n  }\n  if (priority === 'high' || priority === 'urgent') {\n    highPriorityTickets++;\n  }\n  \n  // Channel distribution\n  const channel = ticket.channel || 'unknown';\n  channelDistribution[channel] = (channelDistribution[channel] || 0) + 1;\n  \n  // Age and SLA analysis\n  const ageHours = ticket.ticket_age_hours || 0;\n  totalAgeHours += ageHours;\n  \n  // Escalation tracking (Freshdesk specific)\n  if (ticket.platform === 'freshdesk' && ticket.is_escalated) {\n    escalatedTickets++;\n  }\n  \n  // Overdue tracking (Freshdesk specific)\n  if (isTicketOverdue(ticket)) {\n    overdueTickets++;\n  }\n  \n  // Enhanced SLA breach conditions for both platforms\n  let isSLABreached = false;\n  \n  if (isTicketResolved(ticket)) {\n    // For resolved tickets, check if resolution time exceeded threshold\n    if (ageHours > thresholds.resolution_time_hours) {\n      isSLABreached = true;\n    }\n  } else {\n    // For open tickets, check various conditions\n    if (priority === 'urgent' && ageHours > thresholds.response_time_hours) {\n      isSLABreached = true;\n    } else if ((priority === 'high' || priority === 'urgent') && ageHours > thresholds.high_priority_threshold) {\n      isSLABreached = true;\n    } else if (ageHours > thresholds.resolution_time_hours) {\n      isSLABreached = true;\n    }\n    \n    // Additional Freshdesk-specific SLA conditions\n    if (ticket.platform === 'freshdesk') {\n      if (ticket.is_escalated || isTicketOverdue(ticket)) {\n        isSLABreached = true;\n      }\n    }\n  }\n  \n  if (isSLABreached) {\n    slaBreachedTickets++;\n  }\n  \n  // Needs attention count\n  if (ticket.needs_attention) {\n    needsAttentionCount++;\n  }\n});\n\n// Calculate averages and percentages\nconst avgTicketAge = totalTickets > 0 ? Math.round(totalAgeHours / totalTickets) : 0;\nconst resolutionRate = totalTickets > 0 ? Math.round((resolvedTickets / totalTickets) * 100) : 0;\nconst slaBreachRate = totalTickets > 0 ? Math.round((slaBreachedTickets / totalTickets) * 100) : 0;\nconst urgentTicketRate = totalTickets > 0 ? Math.round((urgentTickets / totalTickets) * 100) : 0;\nconst highPriorityRate = totalTickets > 0 ? Math.round((highPriorityTickets / totalTickets) * 100) : 0;\nconst attentionRate = totalTickets > 0 ? Math.round((needsAttentionCount / totalTickets) * 100) : 0;\nconst escalationRate = totalTickets > 0 ? Math.round((escalatedTickets / totalTickets) * 100) : 0;\nconst overdueRate = totalTickets > 0 ? Math.round((overdueTickets / totalTickets) * 100) : 0;\n\n// Platform-specific rates\nconst platformRates = {};\nObject.keys(platformDistribution).forEach(platform => {\n  if (platformDistribution[platform] > 0) {\n    platformRates[platform] = Math.round((platformDistribution[platform] / totalTickets) * 100);\n  }\n});\n\n// Enhanced response and resolution time estimation\nconst avgResponseTimeHours = Math.round(avgTicketAge * 0.15); // Estimate 15% of total age\nconst avgResolutionTimeHours = avgTicketAge;\n\n// Enhanced CSAT score calculation\n// In production, fetch actual satisfaction ratings from both platforms\nlet csatScore = 0;\nlet csatCount = 0;\n\n// Simulate CSAT based on platform and performance metrics\ntickets.forEach(ticket => {\n  if (isTicketResolved(ticket)) {\n    // Simulate CSAT based on resolution time and priority\n    let simulatedCsat = 4.5;\n    \n    if (ticket.ticket_age_hours > thresholds.resolution_time_hours * 2) {\n      simulatedCsat -= 1.0;\n    } else if (ticket.ticket_age_hours > thresholds.resolution_time_hours) {\n      simulatedCsat -= 0.5;\n    }\n    \n    if (ticket.priority === 'urgent' || ticket.priority === 'high') {\n      simulatedCsat -= 0.2;\n    }\n    \n    // Add some randomness\n    simulatedCsat += (Math.random() - 0.5) * 0.6;\n    \n    // Ensure bounds\n    simulatedCsat = Math.max(1, Math.min(5, simulatedCsat));\n    \n    csatScore += simulatedCsat;\n    csatCount++;\n  }\n});\n\nconst avgCsatScore = csatCount > 0 ? Math.round((csatScore / csatCount) * 100) / 100 : 4.0;\n\n// Current timestamp\nconst now = new Date();\nconst dateString = now.toISOString().split('T')[0];\nconst timestamp = now.toISOString();\n\n// Build comprehensive KPI object\nconst kpis = {\n  // Metadata\n  date: dateString,\n  timestamp: timestamp,\n  calculation_time: now.toLocaleString(),\n  platforms_included: Object.keys(platformDistribution).filter(p => platformDistribution[p] > 0),\n  \n  // Volume Metrics\n  total_tickets: totalTickets,\n  open_tickets: openTickets,\n  resolved_tickets: resolvedTickets,\n  urgent_tickets: urgentTickets,\n  high_priority_tickets: highPriorityTickets,\n  needs_attention_count: needsAttentionCount,\n  sla_breached_tickets: slaBreachedTickets,\n  escalated_tickets: escalatedTickets,\n  overdue_tickets: overdueTickets,\n  \n  // Performance Metrics\n  avg_ticket_age_hours: avgTicketAge,\n  avg_response_time_hours: avgResponseTimeHours,\n  avg_resolution_time_hours: avgResolutionTimeHours,\n  csat_score: avgCsatScore,\n  csat_sample_size: csatCount,\n  \n  // Rate Metrics (Percentages)\n  resolution_rate: resolutionRate,\n  sla_breach_rate: slaBreachRate,\n  urgent_ticket_rate: urgentTicketRate,\n  high_priority_rate: highPriorityRate,\n  attention_required_rate: attentionRate,\n  escalation_rate: escalationRate,\n  overdue_rate: overdueRate,\n  \n  // Distribution Analysis\n  priority_distribution: priorityDistribution,\n  channel_distribution: channelDistribution,\n  platform_distribution: platformDistribution,\n  platform_rates: platformRates,\n  \n  // Thresholds for Reference\n  thresholds: thresholds\n};\n\n// Enhanced Alert Conditions\nkpis.alerts = {\n  high_sla_breach: slaBreachRate > 20,\n  high_urgent_rate: urgentTicketRate > 15,\n  high_backlog: openTickets > 10,\n  low_resolution_rate: resolutionRate < 70,\n  high_attention_rate: attentionRate > 50,\n  long_avg_age: avgTicketAge > thresholds.resolution_time_hours,\n  low_csat: avgCsatScore < thresholds.csat_threshold,\n  high_escalation_rate: escalationRate > 10,\n  high_overdue_rate: overdueRate > 5\n};\n\n// Overall Alert Flag\nkpis.any_alert = Object.values(kpis.alerts).some(alert => alert === true);\nkpis.alert_count = Object.values(kpis.alerts).filter(alert => alert === true).length;\n\n// Enhanced Performance Grading System\nlet gradePoints = 0;\n\n// Resolution rate (20 points max)\nif (resolutionRate >= 90) gradePoints += 20;\nelse if (resolutionRate >= 80) gradePoints += 16;\nelse if (resolutionRate >= 70) gradePoints += 12;\nelse if (resolutionRate >= 60) gradePoints += 8;\n\n// SLA compliance (20 points max)\nif (slaBreachRate <= 5) gradePoints += 20;\nelse if (slaBreachRate <= 10) gradePoints += 16;\nelse if (slaBreachRate <= 15) gradePoints += 12;\nelse if (slaBreachRate <= 20) gradePoints += 8;\n\n// Response time performance (20 points max)\nif (avgResponseTimeHours <= 2) gradePoints += 20;\nelse if (avgResponseTimeHours <= 4) gradePoints += 16;\nelse if (avgResponseTimeHours <= 8) gradePoints += 12;\nelse if (avgResponseTimeHours <= 12) gradePoints += 8;\n\n// Customer satisfaction (20 points max)\nif (avgCsatScore >= 4.5) gradePoints += 20;\nelse if (avgCsatScore >= 4.0) gradePoints += 16;\nelse if (avgCsatScore >= 3.5) gradePoints += 12;\nelse if (avgCsatScore >= 3.0) gradePoints += 8;\n\n// Platform-specific metrics (20 points max)\nlet platformBonus = 0;\nif (escalationRate <= 5) platformBonus += 10;\nelse if (escalationRate <= 10) platformBonus += 6;\n\nif (overdueRate <= 2) platformBonus += 10;\nelse if (overdueRate <= 5) platformBonus += 6;\n\ngradePoints += platformBonus;\n\nkpis.performance_grade = gradePoints >= 85 ? 'A' : gradePoints >= 70 ? 'B' : gradePoints >= 55 ? 'C' : 'D';\nkpis.grade_percentage = gradePoints;\nkpis.grade_description = \n  gradePoints >= 85 ? 'Excellent Performance' :\n  gradePoints >= 70 ? 'Good Performance' :\n  gradePoints >= 55 ? 'Needs Improvement' : 'Critical Issues';\n\n// Enhanced Productivity Insights\nkpis.insights = {\n  busiest_channel: Object.keys(channelDistribution).length > 0 ? \n    Object.keys(channelDistribution).reduce((a, b) => \n      channelDistribution[a] > channelDistribution[b] ? a : b) : 'none',\n  dominant_priority: Object.keys(priorityDistribution).length > 0 ?\n    Object.keys(priorityDistribution).reduce((a, b) => \n      priorityDistribution[a] > priorityDistribution[b] ? a : b) : 'none',\n  primary_platform: Object.keys(platformDistribution).length > 0 ?\n    Object.keys(platformDistribution).reduce((a, b) => \n      platformDistribution[a] > platformDistribution[b] ? a : b) : 'none',\n  tickets_per_priority: priorityDistribution,\n  tickets_per_channel: channelDistribution,\n  tickets_per_platform: platformDistribution,\n  oldest_ticket_age: tickets.length > 0 ? Math.max(...tickets.map(t => t.ticket_age_hours || 0)) : 0,\n  multi_platform: Object.keys(platformDistribution).filter(p => platformDistribution[p] > 0).length > 1\n};\n\n// Enhanced Recommendations based on KPIs\nkpis.recommendations = [];\n\nif (slaBreachRate > 15) {\n  kpis.recommendations.push(\"High SLA breach rate detected - consider increasing support staff or reviewing priority workflows\");\n}\nif (urgentTicketRate > 20) {\n  kpis.recommendations.push(\"High percentage of urgent tickets - review escalation criteria or provide customer education\");\n}\nif (avgTicketAge > 36) {\n  kpis.recommendations.push(\"Long average ticket age - implement automated routing and priority queue management\");\n}\nif (needsAttentionCount > totalTickets * 0.4) {\n  kpis.recommendations.push(\"High attention-required rate - consider workflow automation or additional training\");\n}\nif (resolutionRate < 75) {\n  kpis.recommendations.push(\"Low resolution rate - focus on closing resolved tickets and improving first-contact resolution\");\n}\nif (escalationRate > 10) {\n  kpis.recommendations.push(\"High escalation rate - review first-line resolution capabilities and knowledge base\");\n}\nif (overdueRate > 5) {\n  kpis.recommendations.push(\"High overdue rate - implement better due date tracking and reminder systems\");\n}\nif (kpis.insights.multi_platform) {\n  kpis.recommendations.push(\"Multi-platform support detected - ensure consistent SLA and quality standards across platforms\");\n}\n\n// Platform-specific insights\nif (platformDistribution.freshdesk > 0) {\n  kpis.freshdesk_metrics = {\n    escalated_tickets: escalatedTickets,\n    overdue_tickets: overdueTickets,\n    escalation_rate: escalationRate,\n    overdue_rate: overdueRate\n  };\n}\n\n// Return the comprehensive KPI data (n8n format)\nreturn [{ \n  json: {\n    kpis: kpis,\n    raw_ticket_count: totalTickets,\n    processing_timestamp: timestamp\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9bf80187-1b1f-4810-baa7-7234d65f5099",
      "name": "评估警报",
      "type": "n8n-nodes-base.if",
      "position": [
        1488,
        32
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "d65c4d80-4613-4790-a08c-57b20182df82",
              "operator": {
                "type": "boolean",
                "operation": "equal"
              },
              "leftValue": "={{ $json.kpis.any_alert }}",
              "rightValue": true
            },
            {
              "id": "998efe97-543f-42d3-8950-55f7fd4ccbac",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.kpis.alerts.high_sla_breach }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "d9a716a3-9732-4d92-b5c3-d84881ca015f",
      "name": "生成 Slack 警报消息",
      "type": "n8n-nodes-base.code",
      "position": [
        1712,
        -64
      ],
      "parameters": {
        "jsCode": "// N8N Code Node - Format Support KPIs for Slack Alert\nconst data = $json.kpis;\n\n// Function to get grade emoji\nfunction getGradeEmoji(grade) {\n  const gradeEmojis = {\n    'A': '🟢',\n    'B': '🟡',\n    'C': '🟠',\n    'D': '🔴',\n    'F': '⛔'\n  };\n  return gradeEmojis[grade] || '❓';\n}\n\n// Function to get alert emoji\nfunction getAlertEmoji(hasAlert) {\n  return hasAlert ? '🚨' : '✅';\n}\n\n// Format priority distribution\nfunction formatPriorityDistribution(priorities) {\n  return Object.entries(priorities)\n    .filter(([_, count]) => count > 0)\n    .map(([priority, count]) => `${priority.charAt(0).toUpperCase() + priority.slice(1)}: ${count}`)\n    .join(' | ');\n}\n\n// Format channel distribution\nfunction formatChannelDistribution(channels) {\n  return Object.entries(channels)\n    .map(([channel, count]) => `${channel.toUpperCase()}: ${count}`)\n    .join(' | ');\n}\n\n// Build the main message\nlet message = `${getAlertEmoji(data.any_alert)} *Support Ticket Dashboard Alert*\\n`;\nmessage += `📊 *Performance Grade:* ${getGradeEmoji(data.performance_grade)} ${data.performance_grade} (${data.grade_percentage}%) - ${data.grade_description}\\n\\n`;\n\n// Key Metrics Section\nmessage += `*📈 Key Metrics*\\n`;\nmessage += `• Total Tickets: ${data.total_tickets}\\n`;\nmessage += `• Open: ${data.open_tickets} | Resolved: ${data.resolved_tickets}\\n`;\nmessage += `• Resolution Rate: ${data.resolution_rate}%\\n`;\nmessage += `• CSAT Score: ${data.csat_score}/5.0\\n\\n`;\n\n// Performance Metrics\nmessage += `*⏱️ Performance Metrics*\\n`;\nmessage += `• Avg Response Time: ${data.avg_response_time_hours}h\\n`;\nmessage += `• Avg Resolution Time: ${data.avg_resolution_time_hours}h\\n`;\nmessage += `• Avg Ticket Age: ${data.avg_ticket_age_hours}h\\n\\n`;\n\n// Priority & Channel Breakdown\nmessage += `*🎯 Breakdown*\\n`;\nmessage += `• Priority: ${formatPriorityDistribution(data.priority_distribution)}\\n`;\nmessage += `• Channels: ${formatChannelDistribution(data.channel_distribution)}\\n\\n`;\n\n// Alerts Section (only if there are alerts)\nif (data.any_alert) {\n  message += `*🚨 Active Alerts (${data.alert_count})*\\n`;\n  \n  if (data.alerts.high_sla_breach) {\n    message += `• ⚠️ High SLA Breach Rate: ${data.sla_breach_rate}%\\n`;\n  }\n  if (data.alerts.high_attention_rate) {\n    message += `• ⚠️ High Attention Required Rate: ${data.attention_required_rate}%\\n`;\n  }\n  if (data.alerts.long_avg_age) {\n    message += `• ⚠️ Long Average Ticket Age: ${data.avg_ticket_age_hours}h\\n`;\n  }\n  if (data.alerts.high_urgent_rate) {\n    message += `• ⚠️ High Urgent Ticket Rate: ${data.urgent_ticket_rate}%\\n`;\n  }\n  if (data.alerts.high_backlog) {\n    message += `• ⚠️ High Backlog: ${data.open_tickets} open tickets\\n`;\n  }\n  if (data.alerts.low_resolution_rate) {\n    message += `• ⚠️ Low Resolution Rate: ${data.resolution_rate}%\\n`;\n  }\n  if (data.alerts.low_csat) {\n    message += `• ⚠️ Low CSAT Score: ${data.csat_score}/5.0\\n`;\n  }\n  message += `\\n`;\n}\n\n// Recommendations Section\nif (data.recommendations && data.recommendations.length > 0) {\n  message += `*💡 Recommendations*\\n`;\n  data.recommendations.forEach(rec => {\n    message += `• ${rec}\\n`;\n  });\n  message += `\\n`;\n}\n\n// Footer with timestamp\nconst reportTime = new Date(data.timestamp).toLocaleString();\nmessage += `_Report generated: ${reportTime}_`;\n\n// Return the formatted message for Slack\nreturn {\n  json: {\n    text: message,\n    // Optional: Add structured blocks for better formatting\n    blocks: [\n      {\n        \"type\": \"header\",\n        \"text\": {\n          \"type\": \"plain_text\",\n          \"text\": `${getAlertEmoji(data.any_alert)} Support Dashboard Alert`,\n          \"emoji\": true\n        }\n      },\n      {\n        \"type\": \"section\",\n        \"text\": {\n          \"type\": \"mrkdwn\",\n          \"text\": message\n        }\n      }\n    ]\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "fbdbe576-9d94-4328-933e-062e2ba090fb",
      "name": "生成每周 HTML 报告",
      "type": "n8n-nodes-base.code",
      "position": [
        1712,
        128
      ],
      "parameters": {
        "jsCode": "// N8N Code Node - Generate Corporate Style HTML Email Report\n// Handle different data structures\nlet data;\nif ($json.kpis) {\n    data = $json.kpis;\n} else if ($json[0] && $json[0].kpis) {\n    data = $json[0].kpis;\n} else if (Array.isArray($json) && $json[0] && $json[0].kpis) {\n    data = $json[0].kpis;\n} else {\n    data = $json;\n}\n\n// Validate data\nif (!data || !data.performance_grade) {\n    return {\n        json: {\n            error: \"Invalid data structure. Expected KPIs object with performance_grade property.\",\n            receivedData: $json\n        }\n    };\n}\n\n// Function to get grade styling\nfunction getGradeStyle(grade) {\n    const styles = {\n        'A': { color: '#059669', bg: '#ECFDF5', border: '#10B981' },\n        'B': { color: '#D97706', bg: '#FFFBEB', border: '#F59E0B' },\n        'C': { color: '#DC2626', bg: '#FEF2F2', border: '#EF4444' },\n        'D': { color: '#B91C1C', bg: '#FEF2F2', border: '#DC2626' },\n        'F': { color: '#991B1B', bg: '#FEF2F2', border: '#B91C1C' }\n    };\n    return styles[grade] || { color: '#6B7280', bg: '#F9FAFB', border: '#D1D5DB' };\n}\n\n// Format timestamp\nfunction formatTimestamp(timestamp) {\n    return new Date(timestamp).toLocaleDateString('en-US', {\n        year: 'numeric',\n        month: 'long',\n        day: 'numeric',\n        hour: '2-digit',\n        minute: '2-digit'\n    });\n}\n\nconst gradeStyle = getGradeStyle(data.performance_grade);\nconst alertStatus = data.any_alert ? 'ALERT' : 'HEALTHY';\nconst alertColor = data.any_alert ? '#DC2626' : '#059669';\n\n// Generate corporate-style HTML email\nconst htmlReport = `\n<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Support Dashboard Report</title>\n    <!--[if mso]>\n    <noscript>\n        <xml>\n            <o:OfficeDocumentSettings>\n                <o:PixelsPerInch>96</o:PixelsPerInch>\n            </o:OfficeDocumentSettings>\n        </xml>\n    </noscript>\n    <![endif]-->\n</head>\n<body style=\"margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f4f4f4;\">\n    <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color: #f4f4f4; padding: 20px 0;\">\n        <tr>\n            <td align=\"center\">\n                <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"600\" style=\"background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);\">\n                    \n                    <!-- Header -->\n                    <tr>\n                        <td style=\"background: linear-gradient(135deg, #1e40af 0%, #3730a3 100%); padding: 30px; text-align: center;\">\n                            <h1 style=\"color: #ffffff; margin: 0; font-size: 24px; font-weight: 600;\">Support Dashboard Report</h1>\n                            <p style=\"color: #e0e7ff; margin: 8px 0 0 0; font-size: 16px;\">Daily Performance Analysis</p>\n                        </td>\n                    </tr>\n\n                    <!-- Status Banner -->\n                    <tr>\n                        <td style=\"padding: 0;\">\n                            <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n                                <tr>\n                                    <td style=\"background-color: ${alertColor}; color: #ffffff; text-align: center; padding: 12px; font-weight: 600; font-size: 14px; letter-spacing: 0.5px;\">\n                                        SYSTEM STATUS: ${alertStatus}\n                                    </td>\n                                </tr>\n                            </table>\n                        </td>\n                    </tr>\n\n                    <!-- Performance Grade Section -->\n                    <tr>\n                        <td style=\"padding: 30px;\">\n                            <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n                                <tr>\n                                    <td style=\"text-align: center; padding: 25px; background-color: ${gradeStyle.bg}; border: 2px solid ${gradeStyle.border}; border-radius: 8px;\">\n                                        <div style=\"font-size: 48px; font-weight: 900; color: ${gradeStyle.color}; line-height: 1; margin-bottom: 10px;\">${data.performance_grade}</div>\n                                        <div style=\"font-size: 18px; color: ${gradeStyle.color}; font-weight: 600;\">${data.grade_percentage}% - ${data.grade_description}</div>\n                                    </td>\n                                </tr>\n                            </table>\n                        </td>\n                    </tr>\n\n                    <!-- Key Metrics Grid -->\n                    <tr>\n                        <td style=\"padding: 0 30px 30px 30px;\">\n                            <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n                                <tr>\n                                    <td style=\"padding-right: 10px; width: 25%;\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;\">\n                                            <tr>\n                                                <td style=\"text-align: center; padding: 20px;\">\n                                                    <div style=\"font-size: 28px; font-weight: 700; color: #1e40af; line-height: 1; margin-bottom: 5px;\">${data.total_tickets}</div>\n                                                    <div style=\"font-size: 12px; color: #64748b; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;\">Total Tickets</div>\n                                                </td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                    <td style=\"padding-right: 10px; width: 25%;\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 6px;\">\n                                            <tr>\n                                                <td style=\"text-align: center; padding: 20px;\">\n                                                    <div style=\"font-size: 28px; font-weight: 700; color: #059669; line-height: 1; margin-bottom: 5px;\">${data.resolved_tickets}</div>\n                                                    <div style=\"font-size: 12px; color: #064e3b; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;\">Resolved</div>\n                                                </td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                    <td style=\"padding-right: 10px; width: 25%;\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color: ${data.open_tickets > 0 ? '#fef3c7' : '#f0fdf4'}; border: 1px solid ${data.open_tickets > 0 ? '#fbbf24' : '#bbf7d0'}; border-radius: 6px;\">\n                                            <tr>\n                                                <td style=\"text-align: center; padding: 20px;\">\n                                                    <div style=\"font-size: 28px; font-weight: 700; color: ${data.open_tickets > 0 ? '#d97706' : '#059669'}; line-height: 1; margin-bottom: 5px;\">${data.open_tickets}</div>\n                                                    <div style=\"font-size: 12px; color: ${data.open_tickets > 0 ? '#92400e' : '#064e3b'}; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;\">Open</div>\n                                                </td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                    <td style=\"width: 25%;\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color: #faf5ff; border: 1px solid #d8b4fe; border-radius: 6px;\">\n                                            <tr>\n                                                <td style=\"text-align: center; padding: 20px;\">\n                                                    <div style=\"font-size: 28px; font-weight: 700; color: #7c3aed; line-height: 1; margin-bottom: 5px;\">${data.csat_score}</div>\n                                                    <div style=\"font-size: 12px; color: #581c87; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;\">CSAT Score</div>\n                                                </td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                </tr>\n                            </table>\n                        </td>\n                    </tr>\n\n                    <!-- Performance Metrics Section -->\n                    <tr>\n                        <td style=\"padding: 0 30px 30px 30px;\">\n                            <h2 style=\"color: #1f2937; font-size: 18px; font-weight: 600; margin: 0 0 15px 0; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px;\">Performance Metrics</h2>\n                            <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color: #f8fafc; border-radius: 6px; border: 1px solid #e2e8f0;\">\n                                <tr>\n                                    <td style=\"padding: 15px 20px; border-bottom: 1px solid #e2e8f0;\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n                                            <tr>\n                                                <td style=\"font-weight: 500; color: #374151;\">Average Response Time</td>\n                                                <td align=\"right\" style=\"font-weight: 600; color: ${data.avg_response_time_hours <= data.thresholds.response_time_hours ? '#059669' : '#dc2626'};\">${data.avg_response_time_hours}h</td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td style=\"padding: 15px 20px; border-bottom: 1px solid #e2e8f0;\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n                                            <tr>\n                                                <td style=\"font-weight: 500; color: #374151;\">Average Resolution Time</td>\n                                                <td align=\"right\" style=\"font-weight: 600; color: ${data.avg_resolution_time_hours <= data.thresholds.resolution_time_hours ? '#059669' : '#dc2626'};\">${data.avg_resolution_time_hours}h</td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td style=\"padding: 15px 20px; border-bottom: 1px solid #e2e8f0;\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n                                            <tr>\n                                                <td style=\"font-weight: 500; color: #374151;\">Resolution Rate</td>\n                                                <td align=\"right\" style=\"font-weight: 600; color: #059669;\">${data.resolution_rate}%</td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td style=\"padding: 15px 20px;\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n                                            <tr>\n                                                <td style=\"font-weight: 500; color: #374151;\">SLA Breach Rate</td>\n                                                <td align=\"right\" style=\"font-weight: 600; color: ${data.sla_breach_rate > 20 ? '#dc2626' : '#059669'};\">${data.sla_breach_rate}%</td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                </tr>\n                            </table>\n                        </td>\n                    </tr>\n\n                    ${data.any_alert ? `\n                    <!-- Alerts Section -->\n                    <tr>\n                        <td style=\"padding: 0 30px 30px 30px;\">\n                            <h2 style=\"color: #dc2626; font-size: 18px; font-weight: 600; margin: 0 0 15px 0; border-bottom: 2px solid #fecaca; padding-bottom: 8px;\">⚠️ Active Alerts (${data.alert_count})</h2>\n                            <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color: #fef2f2; border-radius: 6px; border: 1px solid #fecaca;\">\n                                ${data.alerts.high_sla_breach ? `\n                                <tr>\n                                    <td style=\"padding: 12px 20px; border-bottom: 1px solid #fecaca; color: #b91c1c; font-weight: 500;\">\n                                        🔴 High SLA Breach Rate: ${data.sla_breach_rate}%\n                                    </td>\n                                </tr>` : ''}\n                                ${data.alerts.high_attention_rate ? `\n                                <tr>\n                                    <td style=\"padding: 12px 20px; ${data.alerts.long_avg_age ? 'border-bottom: 1px solid #fecaca;' : ''} color: #b91c1c; font-weight: 500;\">\n                                        🟠 High Attention Required Rate: ${data.attention_required_rate}%\n                                    </td>\n                                </tr>` : ''}\n                                ${data.alerts.long_avg_age ? `\n                                <tr>\n                                    <td style=\"padding: 12px 20px; color: #b91c1c; font-weight: 500;\">\n                                        ⏰ Long Average Ticket Age: ${data.avg_ticket_age_hours}h\n                                    </td>\n                                </tr>` : ''}\n                            </table>\n                        </td>\n                    </tr>\n                    ` : ''}\n\n                    ${data.recommendations && data.recommendations.length > 0 ? `\n                    <!-- Recommendations Section -->\n                    <tr>\n                        <td style=\"padding: 0 30px 30px 30px;\">\n                            <h2 style=\"color: #1e40af; font-size: 18px; font-weight: 600; margin: 0 0 15px 0; border-bottom: 2px solid #dbeafe; padding-bottom: 8px;\">💡 Recommendations</h2>\n                            <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color: #eff6ff; border-radius: 6px; border: 1px solid #bfdbfe;\">\n                                ${data.recommendations.map((rec, index) => `\n                                <tr>\n                                    <td style=\"padding: 12px 20px; ${index < data.recommendations.length - 1 ? 'border-bottom: 1px solid #bfdbfe;' : ''} color: #1e40af; font-weight: 500;\">\n                                        • ${rec}\n                                    </td>\n                                </tr>`).join('')}\n                            </table>\n                        </td>\n                    </tr>\n                    ` : ''}\n\n                    <!-- Distribution Section -->\n                    <tr>\n                        <td style=\"padding: 0 30px 30px 30px;\">\n                            <h2 style=\"color: #1f2937; font-size: 18px; font-weight: 600; margin: 0 0 15px 0; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px;\">Distribution Analysis</h2>\n                            <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n                                <tr>\n                                    <td style=\"padding-right: 15px; width: 50%; vertical-align: top;\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color: #ffffff; border: 1px solid #e2e8f0; border-radius: 6px;\">\n                                            <tr>\n                                                <td style=\"padding: 15px 20px; background-color: #f8fafc; font-weight: 600; color: #374151; border-bottom: 1px solid #e2e8f0;\">Priority Distribution</td>\n                                            </tr>\n                                            ${Object.entries(data.priority_distribution).map(([priority, count]) => `\n                                            <tr>\n                                                <td style=\"padding: 10px 20px; border-bottom: 1px solid #f1f5f9;\">\n                                                    <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n                                                        <tr>\n                                                            <td style=\"text-transform: capitalize; color: #64748b;\">${priority}</td>\n                                                            <td align=\"right\" style=\"font-weight: 600; color: #1f2937;\">${count}</td>\n                                                        </tr>\n                                                    </table>\n                                                </td>\n                                            </tr>`).join('')}\n                                        </table>\n                                    </td>\n                                    <td style=\"width: 50%; vertical-align: top;\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color: #ffffff; border: 1px solid #e2e8f0; border-radius: 6px;\">\n                                            <tr>\n                                                <td style=\"padding: 15px 20px; background-color: #f8fafc; font-weight: 600; color: #374151; border-bottom: 1px solid #e2e8f0;\">Channel Distribution</td>\n                                            </tr>\n                                            ${Object.entries(data.channel_distribution).map(([channel, count]) => `\n                                            <tr>\n                                                <td style=\"padding: 10px 20px; border-bottom: 1px solid #f1f5f9;\">\n                                                    <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n                                                        <tr>\n                                                            <td style=\"text-transform: uppercase; color: #64748b;\">${channel}</td>\n                                                            <td align=\"right\" style=\"font-weight: 600; color: #1f2937;\">${count}</td>\n                                                        </tr>\n                                                    </table>\n                                                </td>\n                                            </tr>`).join('')}\n                                        </table>\n                                    </td>\n                                </tr>\n                            </table>\n                        </td>\n                    </tr>\n\n                    <!-- Footer -->\n                    <tr>\n                        <td style=\"background-color: #f8fafc; padding: 20px; text-align: center; border-top: 1px solid #e2e8f0;\">\n                            <p style=\"margin: 0; color: #64748b; font-size: 14px; line-height: 1.5;\">\n                                <strong>Report Generated:</strong> ${formatTimestamp(data.timestamp)}<br>\n                                Support Dashboard Analytics System\n                            </p>\n                        </td>\n                    </tr>\n\n                </table>\n            </td>\n        </tr>\n    </table>\n</body>\n</html>\n`;\n\n// Return the corporate-style HTML report\nreturn {\n    json: {\n        html: htmlReport,\n        subject: `Support Dashboard Report - Grade ${data.performance_grade} | ${alertStatus}`,\n        plainText: `Support Ticket Dashboard Report\n\nPerformance Grade: ${data.performance_grade} (${data.grade_percentage}%) - ${data.grade_description}\nStatus: ${alertStatus}\n\nKey Metrics:\n- Total Tickets: ${data.total_tickets}\n- Open: ${data.open_tickets} | Resolved: ${data.resolved_tickets}\n- Resolution Rate: ${data.resolution_rate}%\n- CSAT Score: ${data.csat_score}/5.0\n\nPerformance:\n- Avg Response Time: ${data.avg_response_time_hours}h\n- Avg Resolution Time: ${data.avg_resolution_time_hours}h\n- SLA Breach Rate: ${data.sla_breach_rate}%\n\n${data.any_alert ? `Active Alerts: ${data.alert_count}` : 'No active alerts'}\n\nReport generated: ${formatTimestamp(data.timestamp)}`\n    }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "a8dd2d83-a156-4290-a846-19cf9abd1591",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -288,
        -96
      ],
      "parameters": {
        "height": 352,
        "content": "## ⏰ 每周定时触发器"
      },
      "typeVersion": 1
    },
    {
      "id": "94e3f206-14af-4b8d-a162-f08942b823a2",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        112,
        -368
      ],
      "parameters": {
        "height": 336,
        "content": "## 🎟️ 从 Zendesk 获取工单"
      },
      "typeVersion": 1
    },
    {
      "id": "862bbe05-8d70-4c61-aeab-9e8c0983610e",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1392,
        192
      ],
      "parameters": {
        "height": 400,
        "content": "## 🚨 评估警报"
      },
      "typeVersion": 1
    },
    {
      "id": "811a9d1b-4a72-4aad-8cd6-b369ca74025e",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        976,
        224
      ],
      "parameters": {
        "height": 336,
        "content": "## 📑 在 Google Sheets 中记录警报"
      },
      "typeVersion": 1
    },
    {
      "id": "4eb74eb7-93c5-4cc1-bf16-2ae9cac49d59",
      "name": "便签4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1200,
        -512
      ],
      "parameters": {
        "height": 528,
        "content": "## 📊 计算支持 KPI"
      },
      "typeVersion": 1
    },
    {
      "id": "7dc027d7-e021-47c7-9f57-e2b8ac3d0a41",
      "name": "便签5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        736,
        -400
      ],
      "parameters": {
        "height": 416,
        "content": "## 🧹 格式化工单数据 (Zendesk & FreshDesk)"
      },
      "typeVersion": 1
    },
    {
      "id": "f858aee3-3dd8-4f92-a0af-e3c2dcd9aa5d",
      "name": "便签6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1648,
        -496
      ],
      "parameters": {
        "height": 416,
        "content": "## 💬 生成 Slack 警报消息"
      },
      "typeVersion": 1
    },
    {
      "id": "a0ac0fc2-4ee4-4cda-a16c-01171d483515",
      "name": "便签7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        288
      ],
      "parameters": {
        "height": 464,
        "content": "## 📧 生成每周 HTML 报告"
      },
      "typeVersion": 1
    },
    {
      "id": "eaf1f4d6-c806-40d7-80d7-f6a44185a923",
      "name": "便签8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2048,
        -400
      ],
      "parameters": {
        "height": 336,
        "content": "## 📢 发送警报到 Slack"
      },
      "typeVersion": 1
    },
    {
      "id": "6bfaa7e3-eb49-4c03-b069-f679de45574a",
      "name": "便签9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2064,
        224
      ],
      "parameters": {
        "height": 336,
        "content": "## ✉️ 邮件发送周报"
      },
      "typeVersion": 1
    },
    {
      "id": "1eb5d03c-4862-4cde-b580-e4c5b6fbf479",
      "name": "合并",
      "type": "n8n-nodes-base.merge",
      "position": [
        608,
        32
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "db824881-6986-4114-a95b-e3dfd6a3e7d1",
      "name": "便签10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        96,
        224
      ],
      "parameters": {
        "height": 352,
        "content": "## 🎫 从 FreshDesk 获取工单"
      },
      "typeVersion": 1
    },
    {
      "id": "ced09d5d-a36c-4979-82e1-4375f87df16d",
      "name": "便利贴11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        192
      ],
      "parameters": {
        "height": 352,
        "content": "## 🔗 合并工单数据"
      },
      "typeVersion": 1
    },
    {
      "id": "3860e769-9d69-4564-9c3a-5584c01e4510",
      "name": "格式化工单数据 (Zendesk & Freshdesk)",
      "type": "n8n-nodes-base.code",
      "position": [
        816,
        32
      ],
      "parameters": {
        "jsCode": "// N8N Code Node - Unified Ticket Formatter (Zendesk & Freshdesk)\n// This code processes both Zendesk and Freshdesk ticket data and formats them into a clean unified JSON structure\n\n// Function to detect ticket source\nfunction detectTicketSource(ticketData) {\n  // Zendesk tickets have 'via' field and specific URL structure\n  if (ticketData.via || ticketData.url?.includes('zendesk.com')) {\n    return 'zendesk';\n  }\n  // Freshdesk tickets have 'source' as number and different structure\n  if (typeof ticketData.source === 'number' || ticketData.email_config_id) {\n    return 'freshdesk';\n  }\n  return 'unknown';\n}\n\n// Function to format Zendesk ticket data\nfunction formatZendeskTicket(ticketData) {\n  const createdAt = new Date(ticketData.created_at);\n  const updatedAt = new Date(ticketData.updated_at);\n  const now = new Date();\n  \n  return {\n    platform: 'zendesk',\n    ticket_id: ticketData.id,\n    ticket_url: `https://softwarecompany-66332.zendesk.com/agent/tickets/${ticketData.id}`,\n    subject: ticketData.subject || \"No Subject\",\n    priority: ticketData.priority || \"normal\",\n    priority_level: getZendeskPriorityLevel(ticketData.priority),\n    status: ticketData.status,\n    status_normalized: normalizeZendeskStatus(ticketData.status),\n    created_timestamp: createdAt.toISOString(),\n    updated_timestamp: updatedAt.toISOString(),\n    original_created_at: ticketData.created_at,\n    description_preview: cleanDescription(ticketData.description),\n    description: ticketData.description || \"No Description\",\n    tags: Array.isArray(ticketData.tags) ? ticketData.tags.join(', ') : '',\n    requester_id: ticketData.requester_id,\n    assignee_id: ticketData.assignee_id || 'Unassigned',\n    channel: ticketData.via?.channel || \"unknown\",\n    ticket_age_hours: Math.round((now - createdAt) / (1000 * 60 * 60)),\n    needs_attention: checkZendeskNeedsAttention(ticketData, now, createdAt),\n    ticket_type: ticketData.type || 'ticket'\n  };\n}\n\n// Function to format Freshdesk ticket data\nfunction formatFreshdeskTicket(ticketData) {\n  const createdAt = new Date(ticketData.created_at);\n  const updatedAt = new Date(ticketData.updated_at);\n  const now = new Date();\n  \n  return {\n    platform: 'freshdesk',\n    ticket_id: ticketData.id,\n    ticket_url: `https://your-domain.freshdesk.com/helpdesk/tickets/${ticketData.id}`, // Update with your domain\n    subject: ticketData.subject || \"No Subject\",\n    priority: getFreshdeskPriorityText(ticketData.priority),\n    priority_level: ticketData.priority || 1,\n    status: getFreshdeskStatusText(ticketData.status),\n    status_normalized: normalizeFreshdeskStatus(ticketData.status),\n    created_timestamp: createdAt.toISOString(),\n    updated_timestamp: updatedAt.toISOString(),\n    original_created_at: ticketData.created_at,\n    description_preview: cleanDescription(ticketData.structured_description),\n    description: ticketData.structured_description || \"No Description\",\n    tags: Array.isArray(ticketData.tags) ? ticketData.tags.join(', ') : '',\n    requester_id: ticketData.requester_id,\n    assignee_id: ticketData.responder_id || 'Unassigned',\n    channel: getFreshdeskSourceText(ticketData.source),\n    ticket_age_hours: Math.round((now - createdAt) / (1000 * 60 * 60)),\n    needs_attention: checkFreshdeskNeedsAttention(ticketData, now, createdAt),\n    ticket_type: ticketData.type || 'ticket',\n    due_by: ticketData.due_by,\n    is_escalated: ticketData.is_escalated || false,\n    sentiment_score: ticketData.sentiment_score\n  };\n}\n\n// Helper functions for Zendesk\nfunction getZendeskPriorityLevel(priority) {\n  const levels = {\n    'low': 1,\n    'normal': 2,\n    'high': 3,\n    'urgent': 4\n  };\n  return levels[priority] || 2;\n}\n\nfunction normalizeZendeskStatus(status) {\n  const statusMap = {\n    'new': 'open',\n    'open': 'open',\n    'pending': 'pending',\n    'solved': 'closed',\n    'closed': 'closed'\n  };\n  return statusMap[status] || status;\n}\n\nfunction checkZendeskNeedsAttention(ticketData, now, createdAt) {\n  const ageHours = Math.round((now - createdAt) / (1000 * 60 * 60));\n  const hasUrgentTag = (ticketData.tags || []).includes('urgent');\n  const isHighPriority = ['high', 'urgent'].includes(ticketData.priority);\n  \n  return ageHours > 24 || hasUrgentTag || isHighPriority;\n}\n\n// Helper functions for Freshdesk\nfunction getFreshdeskPriorityText(priority) {\n  const priorityMap = {\n    1: 'low',\n    2: 'medium',\n    3: 'high',\n    4: 'urgent'\n  };\n  return priorityMap[priority] || 'medium';\n}\n\nfunction getFreshdeskStatusText(status) {\n  const statusMap = {\n    2: 'open',\n    3: 'pending',\n    4: 'resolved',\n    5: 'closed'\n  };\n  return statusMap[status] || 'open';\n}\n\nfunction normalizeFreshdeskStatus(status) {\n  const statusMap = {\n    2: 'open',\n    3: 'pending',\n    4: 'closed',\n    5: 'closed'\n  };\n  return statusMap[status] || 'open';\n}\n\nfunction getFreshdeskSourceText(source) {\n  const sourceMap = {\n    1: 'email',\n    2: 'portal',\n    3: 'phone',\n    7: 'chat',\n    8: 'mobihelp',\n    9: 'feedback_widget',\n    10: 'outbound_email'\n  };\n  return sourceMap[source] || 'unknown';\n}\n\nfunction checkFreshdeskNeedsAttention(ticketData, now, createdAt) {\n  const ageHours = Math.round((now - createdAt) / (1000 * 60 * 60));\n  const isHighPriority = ticketData.priority >= 3; // High or Urgent\n  const isEscalated = ticketData.is_escalated;\n  const isOverdue = ticketData.due_by ? new Date(ticketData.due_by) < now : false;\n  \n  return ageHours > 24 || isHighPriority || isEscalated || isOverdue;\n}\n\n// Common helper function to clean description for preview\nfunction cleanDescription(description) {\n  if (!description) return \"No description available\";\n  \n  const cleaned = description\n    .replace(/<[^>]*>/g, '') // Remove HTML tags\n    .replace(/\\s+/g, ' ') // Replace multiple spaces with single space\n    .trim();\n  \n  return cleaned.length > 100 ? cleaned.substring(0, 100) + '...' : cleaned;\n}\n\n// Main processing function\nfunction formatTicketData(ticketData) {\n  const source = detectTicketSource(ticketData);\n  \n  switch (source) {\n    case 'zendesk':\n      return formatZendeskTicket(ticketData);\n    case 'freshdesk':\n      return formatFreshdeskTicket(ticketData);\n    default:\n      console.warn('Unknown ticket source:', ticketData);\n      return {\n        platform: 'unknown',\n        ticket_id: ticketData.id,\n        subject: ticketData.subject || \"No Subject\",\n        error: 'Unable to determine ticket platform',\n        raw_data: ticketData\n      };\n  }\n}\n\n// Process all input items\nconst processedItems = [];\nfor (const item of $input.all()) {\n  const originalTicket = item.json;\n  const formattedTicket = formatTicketData(originalTicket);\n  \n  processedItems.push({\n    json: formattedTicket\n  });\n}\n\nreturn processedItems;"
      },
      "typeVersion": 2
    },
    {
      "id": "dda2d4aa-d81e-4066-b650-8f4e749889c3",
      "name": "从 Freshdesk 获取工单",
      "type": "n8n-nodes-base.freshdesk",
      "position": [
        368,
        128
      ],
      "parameters": {
        "options": {},
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "freshdeskApi": {
          "id": "bAKXYKz3snwrMW38",
          "name": "Freshdesk account"
        }
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "d4fc6ea9-a3dc-436c-bf4d-a3777e8b2827",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Format  Ticket Data (Zendesk & Freshdesk)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weekly Trigger": {
      "main": [
        [
          {
            "node": "Fetch Tickets From Zendesk",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Tickets From Freshdesk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate Alerts": {
      "main": [
        [
          {
            "node": "Generate Slack Alert Message",
            "type": "main",
            "index": 0
          },
          {
            "node": "Generate Weekly HTML Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Support KPIs": {
      "main": [
        [
          {
            "node": "Evaluate Alerts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log KPIs in Google Sheets": {
      "main": [
        [
          {
            "node": "Calculate Support KPIs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Tickets From Zendesk": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Weekly HTML Report": {
      "main": [
        [
          {
            "node": "Send Weekly Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Tickets From Freshdesk": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Generate Slack Alert Message": {
      "main": [
        [
          {
            "node": "Send Slack Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format  Ticket Data (Zendesk & Freshdesk)": {
      "main": [
        [
          {
            "node": "Log KPIs in Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级 - 内容创作, 多模态 AI

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量24
分类2
节点类型10
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

作者
Rahul Joshi

Rahul Joshi

@rahul08

Rahul Joshi is a seasoned technology leader specializing in the n8n automation tool and AI-driven workflow automation. With deep expertise in building open-source workflow automation and self-hosted automation platforms, he helps organizations eliminate manual processes through intelligent n8n ai agent automation solutions.

外部链接
在 n8n.io 查看

分享此工作流