@fatagnus/convex-feedback 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +177 -0
  2. package/README.md +382 -0
  3. package/dist/convex/agents/bugReportAgent.d.ts +30 -0
  4. package/dist/convex/agents/bugReportAgent.d.ts.map +1 -0
  5. package/dist/convex/agents/bugReportAgent.js +243 -0
  6. package/dist/convex/agents/bugReportAgent.js.map +1 -0
  7. package/dist/convex/agents/feedbackAgent.d.ts +29 -0
  8. package/dist/convex/agents/feedbackAgent.d.ts.map +1 -0
  9. package/dist/convex/agents/feedbackAgent.js +232 -0
  10. package/dist/convex/agents/feedbackAgent.js.map +1 -0
  11. package/dist/convex/bugReports.d.ts +49 -0
  12. package/dist/convex/bugReports.d.ts.map +1 -0
  13. package/dist/convex/bugReports.js +321 -0
  14. package/dist/convex/bugReports.js.map +1 -0
  15. package/dist/convex/convex.config.d.ts +3 -0
  16. package/dist/convex/convex.config.d.ts.map +1 -0
  17. package/dist/convex/convex.config.js +6 -0
  18. package/dist/convex/convex.config.js.map +1 -0
  19. package/dist/convex/emails/bugReportEmails.d.ts +16 -0
  20. package/dist/convex/emails/bugReportEmails.d.ts.map +1 -0
  21. package/dist/convex/emails/bugReportEmails.js +403 -0
  22. package/dist/convex/emails/bugReportEmails.js.map +1 -0
  23. package/dist/convex/emails/feedbackEmails.d.ts +16 -0
  24. package/dist/convex/emails/feedbackEmails.d.ts.map +1 -0
  25. package/dist/convex/emails/feedbackEmails.js +389 -0
  26. package/dist/convex/emails/feedbackEmails.js.map +1 -0
  27. package/dist/convex/feedback.d.ts +49 -0
  28. package/dist/convex/feedback.d.ts.map +1 -0
  29. package/dist/convex/feedback.js +327 -0
  30. package/dist/convex/feedback.js.map +1 -0
  31. package/dist/convex/index.d.ts +10 -0
  32. package/dist/convex/index.d.ts.map +1 -0
  33. package/dist/convex/index.js +12 -0
  34. package/dist/convex/index.js.map +1 -0
  35. package/dist/convex/schema.d.ts +200 -0
  36. package/dist/convex/schema.d.ts.map +1 -0
  37. package/dist/convex/schema.js +150 -0
  38. package/dist/convex/schema.js.map +1 -0
  39. package/dist/convex/supportTeams.d.ts +29 -0
  40. package/dist/convex/supportTeams.d.ts.map +1 -0
  41. package/dist/convex/supportTeams.js +159 -0
  42. package/dist/convex/supportTeams.js.map +1 -0
  43. package/dist/index.d.ts +70 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +63 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/react/BugReportButton.d.ts +70 -0
  48. package/dist/react/BugReportButton.d.ts.map +1 -0
  49. package/dist/react/BugReportButton.js +371 -0
  50. package/dist/react/BugReportButton.js.map +1 -0
  51. package/dist/react/BugReportContext.d.ts +59 -0
  52. package/dist/react/BugReportContext.d.ts.map +1 -0
  53. package/dist/react/BugReportContext.js +107 -0
  54. package/dist/react/BugReportContext.js.map +1 -0
  55. package/dist/react/index.d.ts +36 -0
  56. package/dist/react/index.d.ts.map +1 -0
  57. package/dist/react/index.js +36 -0
  58. package/dist/react/index.js.map +1 -0
  59. package/dist/types.d.ts +89 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +5 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +101 -0
  64. package/src/convex/agents/bugReportAgent.ts +277 -0
  65. package/src/convex/agents/feedbackAgent.ts +264 -0
  66. package/src/convex/bugReports.ts +350 -0
  67. package/src/convex/convex.config.ts +7 -0
  68. package/src/convex/emails/bugReportEmails.ts +479 -0
  69. package/src/convex/emails/feedbackEmails.ts +465 -0
  70. package/src/convex/feedback.ts +356 -0
  71. package/src/convex/index.ts +28 -0
  72. package/src/convex/schema.ts +207 -0
  73. package/src/convex/supportTeams.ts +179 -0
  74. package/src/index.ts +77 -0
  75. package/src/react/BugReportButton.tsx +755 -0
  76. package/src/react/BugReportContext.tsx +146 -0
  77. package/src/react/index.ts +46 -0
  78. package/src/types.ts +93 -0
@@ -0,0 +1,465 @@
1
+ /**
2
+ * Feedback Email Notifications
3
+ *
4
+ * Sends email notifications using Resend when feedback is analyzed.
5
+ * - Reporter receives a thank-you email with summary
6
+ * - Team members receive full analysis report
7
+ */
8
+
9
+ import { v } from "convex/values";
10
+ import { internalAction, internalQuery } from "../_generated/server";
11
+ import { internal } from "../_generated/api";
12
+ import { Resend } from "resend";
13
+ import type { FeedbackType } from "../schema";
14
+
15
+ // Feedback type labels for display
16
+ const feedbackTypeLabels: Record<string, string> = {
17
+ feature_request: "Feature Request",
18
+ change_request: "Change Request",
19
+ general: "General Feedback",
20
+ };
21
+
22
+ // Priority labels and colors
23
+ const priorityConfig: Record<string, { label: string; color: string }> = {
24
+ nice_to_have: { label: "Nice to Have", color: "#22c55e" },
25
+ important: { label: "Important", color: "#f59e0b" },
26
+ critical: { label: "Critical", color: "#ef4444" },
27
+ };
28
+
29
+ // Effort labels
30
+ const effortLabels: Record<string, string> = {
31
+ low: "Low (< 1 week)",
32
+ medium: "Medium (1-4 weeks)",
33
+ high: "High (> 4 weeks)",
34
+ };
35
+
36
+ // ============================================================================
37
+ // Internal Queries
38
+ // ============================================================================
39
+
40
+ /**
41
+ * Get full feedback data for email
42
+ */
43
+ export const getFeedbackForEmail = internalQuery({
44
+ args: {
45
+ feedbackId: v.id("feedback"),
46
+ },
47
+ returns: v.union(
48
+ v.object({
49
+ _id: v.id("feedback"),
50
+ type: v.string(),
51
+ title: v.string(),
52
+ description: v.string(),
53
+ priority: v.string(),
54
+ status: v.string(),
55
+ reporterName: v.string(),
56
+ reporterEmail: v.string(),
57
+ url: v.string(),
58
+ createdAt: v.number(),
59
+ }),
60
+ v.null()
61
+ ),
62
+ handler: async (ctx, args) => {
63
+ const feedback = await ctx.db.get(args.feedbackId);
64
+ if (!feedback) {
65
+ return null;
66
+ }
67
+ return {
68
+ _id: feedback._id,
69
+ type: feedback.type,
70
+ title: feedback.title,
71
+ description: feedback.description,
72
+ priority: feedback.priority,
73
+ status: feedback.status,
74
+ reporterName: feedback.reporterName,
75
+ reporterEmail: feedback.reporterEmail,
76
+ url: feedback.url,
77
+ createdAt: feedback.createdAt,
78
+ };
79
+ },
80
+ });
81
+
82
+ // ============================================================================
83
+ // Email Templates
84
+ // ============================================================================
85
+
86
+ function generateReporterEmailHtml(feedback: {
87
+ title: string;
88
+ type: string;
89
+ summary: string;
90
+ reporterName: string;
91
+ }): string {
92
+ const typeLabel = feedbackTypeLabels[feedback.type] || feedback.type;
93
+
94
+ return `
95
+ <!DOCTYPE html>
96
+ <html>
97
+ <head>
98
+ <meta charset="utf-8">
99
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
100
+ <title>Thank You for Your Feedback</title>
101
+ </head>
102
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
103
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px 10px 0 0;">
104
+ <h1 style="color: white; margin: 0; font-size: 24px;">Thank You for Your Feedback!</h1>
105
+ </div>
106
+
107
+ <div style="background: #f8f9fa; padding: 30px; border: 1px solid #e9ecef; border-top: none; border-radius: 0 0 10px 10px;">
108
+ <p style="margin-top: 0;">Hi ${feedback.reporterName},</p>
109
+
110
+ <p>Thank you for submitting your ${typeLabel.toLowerCase()}. We've received it and our team has already started reviewing it.</p>
111
+
112
+ <div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #667eea; margin: 20px 0;">
113
+ <h3 style="margin: 0 0 10px 0; color: #667eea;">${feedback.title}</h3>
114
+ <p style="margin: 0; color: #666;">${feedback.summary}</p>
115
+ </div>
116
+
117
+ <p>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.</p>
118
+
119
+ <p>We'll keep you updated on the progress of your submission.</p>
120
+
121
+ <p style="margin-bottom: 0;">Best regards,<br><strong>The Support Team</strong></p>
122
+ </div>
123
+
124
+ <div style="text-align: center; padding: 20px; color: #999; font-size: 12px;">
125
+ <p>This is an automated message. Please do not reply directly to this email.</p>
126
+ </div>
127
+ </body>
128
+ </html>
129
+ `;
130
+ }
131
+
132
+ function generateTeamEmailHtml(
133
+ feedback: {
134
+ title: string;
135
+ type: string;
136
+ description: string;
137
+ priority: string;
138
+ reporterName: string;
139
+ reporterEmail: string;
140
+ url: string;
141
+ createdAt: number;
142
+ },
143
+ analysis: {
144
+ summary: string;
145
+ impactAnalysis: string;
146
+ actionItems: string[];
147
+ estimatedEffort: string;
148
+ suggestedPriority: string;
149
+ }
150
+ ): string {
151
+ const typeLabel = feedbackTypeLabels[feedback.type] || feedback.type;
152
+ const priorityInfo = priorityConfig[analysis.suggestedPriority] || priorityConfig.important;
153
+ const effortLabel = effortLabels[analysis.estimatedEffort] || analysis.estimatedEffort;
154
+ const submittedDate = new Date(feedback.createdAt).toLocaleString();
155
+
156
+ const actionItemsHtml = analysis.actionItems
157
+ .map((item) => `<li style="margin-bottom: 8px;">${item}</li>`)
158
+ .join("");
159
+
160
+ return `
161
+ <!DOCTYPE html>
162
+ <html>
163
+ <head>
164
+ <meta charset="utf-8">
165
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
166
+ <title>New Feedback Analysis</title>
167
+ </head>
168
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 700px; margin: 0 auto; padding: 20px; background: #f5f5f5;">
169
+ <div style="background: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden;">
170
+ <!-- Header -->
171
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 25px 30px;">
172
+ <h1 style="color: white; margin: 0; font-size: 22px;">New ${typeLabel} Received</h1>
173
+ <p style="color: rgba(255,255,255,0.9); margin: 10px 0 0 0; font-size: 14px;">AI-Analyzed Feedback Report</p>
174
+ </div>
175
+
176
+ <!-- Content -->
177
+ <div style="padding: 30px;">
178
+ <!-- Title & Priority Badge -->
179
+ <div style="margin-bottom: 25px;">
180
+ <h2 style="margin: 0 0 10px 0; color: #1a1a2e;">${feedback.title}</h2>
181
+ <span style="display: inline-block; background: ${priorityInfo.color}; color: white; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;">
182
+ ${priorityInfo.label} Priority
183
+ </span>
184
+ <span style="display: inline-block; background: #e0e7ff; color: #4338ca; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; margin-left: 8px;">
185
+ ${effortLabel}
186
+ </span>
187
+ </div>
188
+
189
+ <!-- AI Summary -->
190
+ <div style="background: #f0f4ff; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
191
+ <h3 style="margin: 0 0 10px 0; color: #4338ca; font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px;">📋 AI Summary</h3>
192
+ <p style="margin: 0; color: #333;">${analysis.summary}</p>
193
+ </div>
194
+
195
+ <!-- Impact Analysis -->
196
+ <div style="background: #fff7ed; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
197
+ <h3 style="margin: 0 0 10px 0; color: #c2410c; font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px;">⚡ Impact Analysis</h3>
198
+ <p style="margin: 0; color: #333;">${analysis.impactAnalysis}</p>
199
+ </div>
200
+
201
+ <!-- Action Items -->
202
+ <div style="background: #f0fdf4; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
203
+ <h3 style="margin: 0 0 15px 0; color: #166534; font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px;">✅ Suggested Action Items</h3>
204
+ <ol style="margin: 0; padding-left: 20px; color: #333;">
205
+ ${actionItemsHtml}
206
+ </ol>
207
+ </div>
208
+
209
+ <!-- Original Feedback -->
210
+ <div style="border: 1px solid #e5e7eb; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
211
+ <h3 style="margin: 0 0 10px 0; color: #6b7280; font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px;">📝 Original Feedback</h3>
212
+ <p style="margin: 0; color: #333; white-space: pre-wrap;">${feedback.description}</p>
213
+ </div>
214
+
215
+ <!-- Reporter Info -->
216
+ <div style="background: #f9fafb; padding: 15px 20px; border-radius: 8px; font-size: 14px;">
217
+ <table style="width: 100%; border-collapse: collapse;">
218
+ <tr>
219
+ <td style="padding: 5px 0; color: #6b7280;"><strong>Reporter:</strong></td>
220
+ <td style="padding: 5px 0;">${feedback.reporterName}</td>
221
+ </tr>
222
+ <tr>
223
+ <td style="padding: 5px 0; color: #6b7280;"><strong>Email:</strong></td>
224
+ <td style="padding: 5px 0;"><a href="mailto:${feedback.reporterEmail}" style="color: #4338ca;">${feedback.reporterEmail}</a></td>
225
+ </tr>
226
+ <tr>
227
+ <td style="padding: 5px 0; color: #6b7280;"><strong>Submitted:</strong></td>
228
+ <td style="padding: 5px 0;">${submittedDate}</td>
229
+ </tr>
230
+ <tr>
231
+ <td style="padding: 5px 0; color: #6b7280;"><strong>Page URL:</strong></td>
232
+ <td style="padding: 5px 0;"><a href="${feedback.url}" style="color: #4338ca; word-break: break-all;">${feedback.url}</a></td>
233
+ </tr>
234
+ </table>
235
+ </div>
236
+ </div>
237
+ </div>
238
+
239
+ <div style="text-align: center; padding: 20px; color: #999; font-size: 12px;">
240
+ <p>This analysis was generated by AI. Please review and verify before taking action.</p>
241
+ </div>
242
+ </body>
243
+ </html>
244
+ `;
245
+ }
246
+
247
+ function generateSimpleTeamEmailHtml(feedback: {
248
+ title: string;
249
+ type: string;
250
+ description: string;
251
+ priority: string;
252
+ reporterName: string;
253
+ reporterEmail: string;
254
+ url: string;
255
+ createdAt: number;
256
+ }): string {
257
+ const typeLabel = feedbackTypeLabels[feedback.type] || feedback.type;
258
+ const priorityInfo = priorityConfig[feedback.priority] || priorityConfig.important;
259
+ const submittedDate = new Date(feedback.createdAt).toLocaleString();
260
+
261
+ return `
262
+ <!DOCTYPE html>
263
+ <html>
264
+ <head>
265
+ <meta charset="utf-8">
266
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
267
+ <title>New Feedback</title>
268
+ </head>
269
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 700px; margin: 0 auto; padding: 20px; background: #f5f5f5;">
270
+ <div style="background: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden;">
271
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 25px 30px;">
272
+ <h1 style="color: white; margin: 0; font-size: 22px;">New ${typeLabel} Received</h1>
273
+ </div>
274
+
275
+ <div style="padding: 30px;">
276
+ <div style="margin-bottom: 25px;">
277
+ <h2 style="margin: 0 0 10px 0; color: #1a1a2e;">${feedback.title}</h2>
278
+ <span style="display: inline-block; background: ${priorityInfo.color}; color: white; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;">
279
+ ${priorityInfo.label} Priority
280
+ </span>
281
+ </div>
282
+
283
+ <div style="border: 1px solid #e5e7eb; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
284
+ <h3 style="margin: 0 0 10px 0; color: #6b7280; font-size: 14px;">Description</h3>
285
+ <p style="margin: 0; color: #333; white-space: pre-wrap;">${feedback.description}</p>
286
+ </div>
287
+
288
+ <div style="background: #f9fafb; padding: 15px 20px; border-radius: 8px; font-size: 14px;">
289
+ <table style="width: 100%; border-collapse: collapse;">
290
+ <tr>
291
+ <td style="padding: 5px 0; color: #6b7280;"><strong>Reporter:</strong></td>
292
+ <td style="padding: 5px 0;">${feedback.reporterName} (${feedback.reporterEmail})</td>
293
+ </tr>
294
+ <tr>
295
+ <td style="padding: 5px 0; color: #6b7280;"><strong>Submitted:</strong></td>
296
+ <td style="padding: 5px 0;">${submittedDate}</td>
297
+ </tr>
298
+ <tr>
299
+ <td style="padding: 5px 0; color: #6b7280;"><strong>Page URL:</strong></td>
300
+ <td style="padding: 5px 0;"><a href="${feedback.url}" style="color: #4338ca;">${feedback.url}</a></td>
301
+ </tr>
302
+ </table>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ </body>
307
+ </html>
308
+ `;
309
+ }
310
+
311
+ // ============================================================================
312
+ // Email Sending Action
313
+ // ============================================================================
314
+
315
+ /**
316
+ * Send feedback notifications to reporter and team.
317
+ */
318
+ export const sendFeedbackNotifications = internalAction({
319
+ args: {
320
+ feedbackId: v.id("feedback"),
321
+ analysis: v.union(
322
+ v.object({
323
+ summary: v.string(),
324
+ impactAnalysis: v.string(),
325
+ actionItems: v.array(v.string()),
326
+ estimatedEffort: v.string(),
327
+ suggestedPriority: v.string(),
328
+ }),
329
+ v.null()
330
+ ),
331
+ },
332
+ returns: v.object({
333
+ success: v.boolean(),
334
+ reporterEmailSent: v.boolean(),
335
+ teamEmailsSent: v.number(),
336
+ error: v.optional(v.string()),
337
+ }),
338
+ handler: async (
339
+ ctx,
340
+ args
341
+ ): Promise<{
342
+ success: boolean;
343
+ reporterEmailSent: boolean;
344
+ teamEmailsSent: number;
345
+ error?: string;
346
+ }> => {
347
+ // Check for Resend API key
348
+ const resendApiKey = process.env.RESEND_API_KEY;
349
+ if (!resendApiKey) {
350
+ console.warn("RESEND_API_KEY not configured. Skipping email notifications.");
351
+ return {
352
+ success: false,
353
+ reporterEmailSent: false,
354
+ teamEmailsSent: 0,
355
+ error: "RESEND_API_KEY not configured",
356
+ };
357
+ }
358
+
359
+ const resend = new Resend(resendApiKey);
360
+ const fromEmail = process.env.RESEND_FROM_EMAIL || "feedback@resend.dev";
361
+
362
+ // Get feedback data
363
+ const feedback = await ctx.runQuery(internal.emails.feedbackEmails.getFeedbackForEmail, {
364
+ feedbackId: args.feedbackId,
365
+ });
366
+
367
+ if (!feedback) {
368
+ return {
369
+ success: false,
370
+ reporterEmailSent: false,
371
+ teamEmailsSent: 0,
372
+ error: "Feedback not found",
373
+ };
374
+ }
375
+
376
+ let reporterEmailSent = false;
377
+ let teamEmailsSent = 0;
378
+
379
+ // Send email to reporter
380
+ try {
381
+ const summary = args.analysis?.summary || "We are reviewing your feedback.";
382
+ const reporterHtml = generateReporterEmailHtml({
383
+ title: feedback.title,
384
+ type: feedback.type,
385
+ summary,
386
+ reporterName: feedback.reporterName,
387
+ });
388
+
389
+ await resend.emails.send({
390
+ from: fromEmail,
391
+ to: [feedback.reporterEmail],
392
+ subject: `Thank you for your feedback: ${feedback.title}`,
393
+ html: reporterHtml,
394
+ });
395
+
396
+ reporterEmailSent = true;
397
+ console.log(`Reporter email sent to ${feedback.reporterEmail}`);
398
+ } catch (error) {
399
+ console.error("Failed to send reporter email:", error);
400
+ }
401
+
402
+ // Get teams for this feedback type
403
+ const teams = await ctx.runQuery(internal.supportTeams.getTeamsForFeedbackType, {
404
+ feedbackType: feedback.type as FeedbackType,
405
+ });
406
+
407
+ // Generate team email HTML
408
+ const teamHtml = args.analysis
409
+ ? generateTeamEmailHtml(
410
+ {
411
+ title: feedback.title,
412
+ type: feedback.type,
413
+ description: feedback.description,
414
+ priority: feedback.priority,
415
+ reporterName: feedback.reporterName,
416
+ reporterEmail: feedback.reporterEmail,
417
+ url: feedback.url,
418
+ createdAt: feedback.createdAt,
419
+ },
420
+ args.analysis
421
+ )
422
+ : generateSimpleTeamEmailHtml({
423
+ title: feedback.title,
424
+ type: feedback.type,
425
+ description: feedback.description,
426
+ priority: feedback.priority,
427
+ reporterName: feedback.reporterName,
428
+ reporterEmail: feedback.reporterEmail,
429
+ url: feedback.url,
430
+ createdAt: feedback.createdAt,
431
+ });
432
+
433
+ const typeLabel = feedbackTypeLabels[feedback.type] || feedback.type;
434
+
435
+ // Send email to all team members from all matching teams
436
+ for (const team of teams) {
437
+ if (team.isActive && team.memberEmails.length > 0) {
438
+ for (const email of team.memberEmails) {
439
+ try {
440
+ await resend.emails.send({
441
+ from: fromEmail,
442
+ to: [email],
443
+ subject: `[${typeLabel}] ${feedback.title}${args.analysis ? " - AI Analysis" : ""}`,
444
+ html: teamHtml,
445
+ });
446
+ teamEmailsSent++;
447
+ console.log(`Team email sent to ${email}`);
448
+ } catch (error) {
449
+ console.error(`Failed to send team email to ${email}:`, error);
450
+ }
451
+ }
452
+ }
453
+ }
454
+
455
+ if (teams.length === 0) {
456
+ console.log(`No teams configured for feedback type: ${feedback.type}`);
457
+ }
458
+
459
+ return {
460
+ success: true,
461
+ reporterEmailSent,
462
+ teamEmailsSent,
463
+ };
464
+ },
465
+ });