/** * Feedback Email Notifications * * Sends email notifications using Resend when feedback is analyzed. * - Reporter receives a thank-you email with summary * - Team members receive full analysis report */ import { v } from "convex/values"; import { internalAction, internalQuery } from "../_generated/server"; import { internal } from "../_generated/api"; import { Resend } from "resend"; import type { FeedbackType } from "../schema"; // Feedback type labels for display const feedbackTypeLabels: Record = { feature_request: "Feature Request", change_request: "Change Request", general: "General Feedback", }; // Priority labels and colors const priorityConfig: Record = { nice_to_have: { label: "Nice to Have", color: "#22c55e" }, important: { label: "Important", color: "#f59e0b" }, critical: { label: "Critical", color: "#ef4444" }, }; // Effort labels const effortLabels: Record = { low: "Low (< 1 week)", medium: "Medium (1-4 weeks)", high: "High (> 4 weeks)", }; // ============================================================================ // Internal Queries // ============================================================================ /** * Get full feedback data for email */ export const getFeedbackForEmail = internalQuery({ args: { feedbackId: v.id("feedback"), }, returns: v.union( v.object({ _id: v.id("feedback"), type: v.string(), title: v.string(), description: v.string(), priority: v.string(), status: v.string(), reporterName: v.string(), reporterEmail: v.string(), url: v.string(), createdAt: v.number(), }), v.null() ), handler: async (ctx, args) => { const feedback = await ctx.db.get(args.feedbackId); if (!feedback) { return null; } return { _id: feedback._id, type: feedback.type, title: feedback.title, description: feedback.description, priority: feedback.priority, status: feedback.status, reporterName: feedback.reporterName, reporterEmail: feedback.reporterEmail, url: feedback.url, createdAt: feedback.createdAt, }; }, }); // ============================================================================ // Email Templates // ============================================================================ function generateReporterEmailHtml(feedback: { title: string; type: string; summary: string; reporterName: string; }): string { const typeLabel = feedbackTypeLabels[feedback.type] || feedback.type; return ` Thank You for Your Feedback

Thank You for Your Feedback!

Hi ${feedback.reporterName},

Thank you for submitting your ${typeLabel.toLowerCase()}. We've received it and our team has already started reviewing it.

${feedback.title}

${feedback.summary}

We appreciate you taking the time to share your thoughts with us. Your feedback helps us improve and prioritize what matters most to our users.

We'll keep you updated on the progress of your submission.

Best regards,
The Support Team

This is an automated message. Please do not reply directly to this email.

`; } function generateTeamEmailHtml( feedback: { title: string; type: string; description: string; priority: string; reporterName: string; reporterEmail: string; url: string; createdAt: number; }, analysis: { summary: string; impactAnalysis: string; actionItems: string[]; estimatedEffort: string; suggestedPriority: string; } ): string { const typeLabel = feedbackTypeLabels[feedback.type] || feedback.type; const priorityInfo = priorityConfig[analysis.suggestedPriority] || priorityConfig.important; const effortLabel = effortLabels[analysis.estimatedEffort] || analysis.estimatedEffort; const submittedDate = new Date(feedback.createdAt).toLocaleString(); const actionItemsHtml = analysis.actionItems .map((item) => `
  • ${item}
  • `) .join(""); return ` New Feedback Analysis

    New ${typeLabel} Received

    AI-Analyzed Feedback Report

    ${feedback.title}

    ${priorityInfo.label} Priority ${effortLabel}

    📋 AI Summary

    ${analysis.summary}

    ⚡ Impact Analysis

    ${analysis.impactAnalysis}

    ✅ Suggested Action Items

      ${actionItemsHtml}

    📝 Original Feedback

    ${feedback.description}

    Reporter: ${feedback.reporterName}
    Email: ${feedback.reporterEmail}
    Submitted: ${submittedDate}
    Page URL: ${feedback.url}

    This analysis was generated by AI. Please review and verify before taking action.

    `; } function generateSimpleTeamEmailHtml(feedback: { title: string; type: string; description: string; priority: string; reporterName: string; reporterEmail: string; url: string; createdAt: number; }): string { const typeLabel = feedbackTypeLabels[feedback.type] || feedback.type; const priorityInfo = priorityConfig[feedback.priority] || priorityConfig.important; const submittedDate = new Date(feedback.createdAt).toLocaleString(); return ` New Feedback

    New ${typeLabel} Received

    ${feedback.title}

    ${priorityInfo.label} Priority

    Description

    ${feedback.description}

    Reporter: ${feedback.reporterName} (${feedback.reporterEmail})
    Submitted: ${submittedDate}
    Page URL: ${feedback.url}
    `; } // ============================================================================ // Email Sending Action // ============================================================================ /** * Send feedback notifications to reporter and team. */ export const sendFeedbackNotifications = internalAction({ args: { feedbackId: v.id("feedback"), analysis: v.union( v.object({ summary: v.string(), impactAnalysis: v.string(), actionItems: v.array(v.string()), estimatedEffort: v.string(), suggestedPriority: v.string(), }), v.null() ), // Optional API keys - passed from parent app since components don't inherit env vars resendApiKey: v.optional(v.string()), resendFromEmail: v.optional(v.string()), }, returns: v.object({ success: v.boolean(), reporterEmailSent: v.boolean(), teamEmailsSent: v.number(), error: v.optional(v.string()), }), handler: async ( ctx, args ): Promise<{ success: boolean; reporterEmailSent: boolean; teamEmailsSent: number; error?: string; }> => { // Check for Resend API key - prefer args (from parent app), fallback to env const resendApiKey = args.resendApiKey || process.env.RESEND_API_KEY; if (!resendApiKey) { console.warn("RESEND_API_KEY not configured. Skipping email notifications."); return { success: false, reporterEmailSent: false, teamEmailsSent: 0, error: "RESEND_API_KEY not configured", }; } const resend = new Resend(resendApiKey); const fromEmail = args.resendFromEmail || process.env.RESEND_FROM_EMAIL || "feedback@resend.dev"; // Get feedback data const feedback = await ctx.runQuery(internal.emails.feedbackEmails.getFeedbackForEmail, { feedbackId: args.feedbackId, }); if (!feedback) { return { success: false, reporterEmailSent: false, teamEmailsSent: 0, error: "Feedback not found", }; } let reporterEmailSent = false; let teamEmailsSent = 0; // Send email to reporter try { const summary = args.analysis?.summary || "We are reviewing your feedback."; const reporterHtml = generateReporterEmailHtml({ title: feedback.title, type: feedback.type, summary, reporterName: feedback.reporterName, }); await resend.emails.send({ from: fromEmail, to: [feedback.reporterEmail], subject: `Thank you for your feedback: ${feedback.title}`, html: reporterHtml, }); reporterEmailSent = true; console.log(`Reporter email sent to ${feedback.reporterEmail}`); } catch (error) { console.error("Failed to send reporter email:", error); } // Get teams for this feedback type const teams = await ctx.runQuery(internal.supportTeams.getTeamsForFeedbackType, { feedbackType: feedback.type as FeedbackType, }); // Generate team email HTML const teamHtml = args.analysis ? generateTeamEmailHtml( { title: feedback.title, type: feedback.type, description: feedback.description, priority: feedback.priority, reporterName: feedback.reporterName, reporterEmail: feedback.reporterEmail, url: feedback.url, createdAt: feedback.createdAt, }, args.analysis ) : generateSimpleTeamEmailHtml({ title: feedback.title, type: feedback.type, description: feedback.description, priority: feedback.priority, reporterName: feedback.reporterName, reporterEmail: feedback.reporterEmail, url: feedback.url, createdAt: feedback.createdAt, }); const typeLabel = feedbackTypeLabels[feedback.type] || feedback.type; // Send email to all team members from all matching teams for (const team of teams) { if (team.isActive && team.memberEmails.length > 0) { for (const email of team.memberEmails) { try { await resend.emails.send({ from: fromEmail, to: [email], subject: `[${typeLabel}] ${feedback.title}${args.analysis ? " - AI Analysis" : ""}`, html: teamHtml, }); teamEmailsSent++; console.log(`Team email sent to ${email}`); } catch (error) { console.error(`Failed to send team email to ${email}:`, error); } } } } if (teams.length === 0) { console.log(`No teams configured for feedback type: ${feedback.type}`); } return { success: true, reporterEmailSent, teamEmailsSent, }; }, });