# Customer Feedback Collection and Management

> Gathering, analyzing, and acting on user input through surveys, feedback widgets, rating systems, sentiment analysis, and integration with product roadmap to improve products and services.

- Skill: `majiayu000/customer-feedback-collection-and-management` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/customer-feedback-collection-and-management`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/customer-feedback-collection-and-management/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/customer-feedback-collection-and-management

---


# Customer Feedback Collection and Management

> **Current Level:** Intermediate  
> **Domain:** Customer Support / Product

---

## Overview

Customer feedback collection enables businesses to gather, analyze, and act on user input to improve products and services. Effective feedback systems include multiple collection methods, sentiment analysis, categorization, routing, and integration with product development.

---

## Core Concepts

### Table of Contents

1. [Feedback Collection Methods](#feedback-collection-methods)
2. [Survey Design](#survey-design)
3. [Feedback Widgets](#feedback-widgets)
4. [Rating Systems](#rating-systems)
5. [Feedback Categorization](#feedback-categorization)
6. [Sentiment Analysis](#sentiment-analysis)
7. [Feedback Routing](#feedback-routing)
8. [Response Management](#response-management)
9. [Analytics and Reporting](#analytics-and-reporting)
10. [Integration with Product Roadmap](#integration-with-product-roadmap)
11. [Tools](#tools)
12. [Best Practices](#best-practices)

---

## Feedback Collection Methods

### In-App Surveys

```typescript
interface InAppSurvey {
  id: string;
  name: string;
  type: 'nps' | 'csat' | 'ces' | 'custom';
  trigger: SurveyTrigger;
  questions: SurveyQuestion[];
  targeting: SurveyTargeting;
  status: 'draft' | 'active' | 'paused' | 'archived';
}

interface SurveyTrigger {
  type: 'event' | 'time' | 'session_count' | 'feature_usage';
  config: Record<string, any>;
}

interface SurveyQuestion {
  id: string;
  type: 'rating' | 'text' | 'multiple_choice' | 'checkbox';
  question: string;
  options?: string[];
  required: boolean;
  min?: number;
  max?: number;
}

class InAppSurveyManager {
  constructor(private prisma: PrismaClient) {}

  /**
   * Create survey
   */
  async createSurvey(survey: Omit<InAppSurvey, 'id' | 'status'>): Promise<string> {
    const created = await this.prisma.inAppSurvey.create({
      data: {
        ...survey,
        status: 'draft',
      },
    });

    return created.id;
  }

  /**
   * Trigger survey
   */
  async triggerSurvey(
    surveyId: string,
    userId: string,
    context?: Record<string, any>
  ): Promise<void> {
    const survey = await this.prisma.inAppSurvey.findUnique({
      where: { id: surveyId },
      include: { questions: true },
    });

    if (!survey || survey.status !== 'active') {
      return;
    }

    // Check targeting
    const eligible = await this.checkEligibility(survey, userId, context);
    if (!eligible) return;

    // Create survey instance
    const instance = await this.prisma.surveyInstance.create({
      data: {
        surveyId,
        userId,
        context,
        status: 'pending',
        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
      },
    });

    // Send notification
    await this.sendSurveyNotification(userId, instance.id, survey);
  }

  /**
   * Check survey eligibility
   */
  private async checkEligibility(
    survey: InAppSurvey,
    userId: string,
    context?: Record<string, any>
  ): Promise<boolean> {
    // Check user targeting
    if (survey.targeting.userSegments && survey.targeting.userSegments.length > 0) {
      const userSegments = await this.getUserSegments(userId);
      const hasRequiredSegment = survey.targeting.userSegments.some(segment =>
        userSegments.includes(segment)
      );

      if (!hasRequiredSegment) return false;
    }

    // Check trigger conditions
    if (survey.trigger.type === 'event') {
      const lastEvent = await this.getLastEvent(userId, survey.trigger.config.eventName);
      if (!lastEvent) return false;

      const eventAge = Date.now() - new Date(lastEvent.createdAt).getTime();
      const minDelay = survey.trigger.config.minDelay || 0;

      if (eventAge < minDelay) return false;
    }

    // Check if already responded
    const existingResponse = await this.prisma.surveyResponse.findFirst({
      where: {
        surveyId,
        userId,
      },
    });

    if (existingResponse) return false;

    return true;
  }

  /**
   * Submit survey response
   */
  async submitResponse(params: {
    instanceId: string;
    userId: string;
    answers: Record<string, any>;
  }): Promise<void> {
    const instance = await this.prisma.surveyInstance.findUnique({
      where: { id: params.instanceId },
      include: { survey: true },
    });

    if (!instance || instance.status !== 'pending') {
      throw new Error('Survey instance not found or already completed');
    }

    // Check expiry
    if (instance.expiresAt && instance.expiresAt < new Date()) {
      throw new Error('Survey has expired');
    }

    // Save response
    await this.prisma.surveyResponse.create({
      data: {
        instanceId: params.instanceId,
        userId: params.userId,
        answers: params.answers,
        submittedAt: new Date(),
      },
    });

    // Update instance
    await this.prisma.surveyInstance.update({
      where: { id: params.instanceId },
      data: { status: 'completed' },
    });

    // Trigger follow-up actions
    await this.handleSurveyResponse(instance.survey, params.answers);
  }

  /**
   * Handle survey response
   */
  private async handleSurveyResponse(
    survey: any,
    answers: Record<string, any>
  ): Promise<void> {
    switch (survey.type) {
      case 'nps':
        await this.handleNPSResponse(survey, answers);
        break;
      case 'csat':
        await this.handleCSATResponse(survey, answers);
        break;
      case 'ces':
        await this.handleCESResponse(survey, answers);
        break;
      default:
        await this.handleCustomResponse(survey, answers);
    }
  }

  private async handleNPSResponse(survey: any, answers: Record<string, any>): Promise<void> {
    const score = answers['score'];
    const category = this.getNPSCategory(score);

    await this.prisma.npsScore.create({
      data: {
        surveyId: survey.id,
        score,
        category,
        comment: answers['comment'],
      },
    });
  }

  private getNPSCategory(score: number): 'detractor' | 'passive' | 'promoter' {
    if (score >= 9) return 'promoter';
    if (score >= 7) return 'passive';
    return 'detractor';
  }

  private async sendSurveyNotification(
    userId: string,
    instanceId: string,
    survey: any
  ): Promise<void> {
    // Implement notification logic
    console.log(`Sending survey ${survey.id} to user ${userId}`);
  }

  private async getLastEvent(userId: string, eventName: string): Promise<any> {
    // Implement event tracking
    return null;
  }

  private async getUserSegments(userId: string): Promise<string[]> {
    // Implement user segmentation
    return [];
  }

  constructor(private prisma: PrismaClient) {}
}
```

### Email Surveys

```typescript
class EmailSurveyManager {
  constructor(private prisma: PrismaClient) {}

  /**
   * Create email campaign
   */
  async createCampaign(params: {
    name: string;
    subject: string;
    fromEmail: string;
    templateId: string;
    recipientIds: string[];
    sendAt?: Date;
  }): Promise<string> {
    const campaign = await this.prisma.emailSurveyCampaign.create({
      data: {
        ...params,
        status: 'scheduled',
      },
    });

    // Schedule send
    if (params.sendAt) {
      await this.scheduleCampaignSend(campaign.id, params.sendAt);
    } else {
      await this.sendCampaign(campaign.id);
    }

    return campaign.id;
  }

  /**
   * Send campaign
   */
  private async sendCampaign(campaignId: string): Promise<void> {
    const campaign = await this.prisma.emailSurveyCampaign.findUnique({
      where: { id: campaignId },
      include: { recipients: true },
    });

    if (!campaign) {
      throw new Error('Campaign not found');
    }

    // Get template
    const template = await this.prisma.surveyTemplate.findUnique({
      where: { id: campaign.templateId },
    });

    // Send emails
    for (const recipient of campaign.recipients) {
      const surveyUrl = this.generateSurveyUrl(campaign.id, recipient.id);

      await emailService.send({
        to: recipient.email,
        subject: campaign.subject,
        from: campaign.fromEmail,
        templateId: campaign.templateId,
        dynamicTemplateData: {
          surveyUrl,
          recipientName: recipient.name,
        },
      });
    }

    // Update campaign status
    await this.prisma.emailSurveyCampaign.update({
      where: { id: campaignId },
      data: {
        status: 'sent',
        sentAt: new Date(),
      },
    });
  }

  /**
   * Generate survey URL
   */
  private generateSurveyUrl(campaignId: string, recipientId: string): string {
    const token = this.generateSurveyToken(campaignId, recipientId);

    return `${process.env.APP_URL}/survey/email/${campaignId}/${recipientId}?token=${token}`;
  }

  /**
   * Generate survey token
   */
  private generateSurveyToken(campaignId: string, recipientId: string): string {
    const crypto = require('crypto');
    return crypto
      .createHmac('sha256', process.env.SURVEY_SECRET!)
      .update(`${campaignId}:${recipientId}`)
      .digest('hex');
  }

  /**
   * Schedule campaign send
   */
  private async scheduleCampaignSend(campaignId: string, sendAt: Date): Promise<void> {
    // Implement scheduling logic
    console.log(`Scheduling campaign ${campaignId} for ${sendAt}`);
  }
}
```

---

## Survey Design

### Survey Templates

```typescript
interface SurveyTemplate {
  id: string;
  name: string;
  type: 'nps' | 'csat' | 'ces' | 'custom';
  subject?: string;
  htmlContent: string;
  textContent?: string;
  variables: TemplateVariable[];
}

interface TemplateVariable {
  name: string;
  description: string;
  type: 'text' | 'link' | 'rating';
  required: boolean;
}

// NPS Template
const npsTemplate: SurveyTemplate = {
  name: 'NPS Survey',
  type: 'nps',
  subject: 'How likely are you to recommend us?',
  htmlContent: `
    <html>
      <body>
        <h1>How likely are you to recommend [Company Name] to a friend or colleague?</h1>
        <div class="nps-scale">
          <button onclick="submitNPS(0)">0</button>
          <button onclick="submitNPS(1)">1</button>
          <button onclick="submitNPS(2)">2</button>
          <button onclick="submitNPS(3)">3</button>
          <button onclick="submitNPS(4)">4</button>
          <button onclick="submitNPS(5)">5</button>
          <button onclick="submitNPS(6)">6</button>
          <button onclick="submitNPS(7)">7</button>
          <button onclick="submitNPS(8)">8</button>
          <button onclick="submitNPS(9)">9</button>
          <button onclick="submitNPS(10)">10</button>
        </div>
        <textarea id="comment" placeholder="Please tell us why you gave this score"></textarea>
        <button onclick="submitSurvey()">Submit</button>
      </body>
    </html>
  `,
  variables: [
    { name: 'surveyUrl', description: 'Survey submission URL', type: 'link', required: true },
  ],
};

// CSAT Template
const csatTemplate: SurveyTemplate = {
  name: 'CSAT Survey',
  type: 'csat',
  subject: 'How was your experience?',
  htmlContent: `
    <html>
      <body>
        <h1>How would you rate your recent experience with us?</h1>
        <div class="rating-scale">
          <button onclick="submitRating(1)">😠</button>
          <button onclick="submitRating(2)">😞</button>
          <button onclick="submitRating(3)">😐</button>
          <button onclick="submitRating(4)">🙂</button>
          <button onclick="submitRating(5)">😄</button>
        </div>
        <textarea id="comment" placeholder="Please share your feedback"></textarea>
        <button onclick="submitSurvey()">Submit</button>
      </body>
    </html>
  `,
  variables: [
    { name: 'surveyUrl', description: 'Survey submission URL', type: 'link', required: true },
  ],
};

// CES Template
const cesTemplate: SurveyTemplate = {
  name: 'CES Survey',
  type: 'ces',
  subject: 'Was it easy to accomplish your task?',
  htmlContent: `
    <html>
      <body>
        <h1>How easy was it to accomplish your task?</h1>
        <div class="ces-scale">
          <button onclick="submitCES(1)">Very Difficult</button>
          <button onclick="submitCES(2)">Difficult</button>
          <button onclick="submitCES(3)">Neutral</button>
          <button onclick="submitCES(4)">Easy</button>
          <button onclick="submitCES(5)">Very Easy</button>
        </div>
        <textarea id="comment" placeholder="Please share your feedback"></textarea>
        <button onclick="submitSurvey()">Submit</button>
      </body>
    </html>
  `,
  variables: [
    { name: 'surveyUrl', description: 'Survey submission URL', type: 'link', required: true },
  ],
};
```

---

## Feedback Widgets

### React Feedback Widget

```tsx
import React, { useState } from 'react';

interface FeedbackWidgetProps {
  type: 'rating' | 'emoji' | 'thumbs';
  position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
  showOnLoad?: boolean;
  delay?: number; // in seconds
  onFeedback: (feedback: any) => void;
}

const FeedbackWidget: React.FC<FeedbackWidgetProps> = ({
  type,
  position = 'bottom-right',
  showOnLoad = false,
  delay = 30,
  onFeedback,
}) => {
  const [visible, setVisible] = useState(showOnLoad);

  React.useEffect(() => {
    if (!showOnLoad && delay > 0) {
      const timer = setTimeout(() => setVisible(true), delay * 1000);
      return () => clearTimeout(timer);
    }
  }, [showOnLoad, delay]);

  const handleSubmit = (feedback: any) => {
    onFeedback(feedback);
    setVisible(false);
  };

  const positionClasses: Record<string, string> = {
    'bottom-right': 'bottom-4 right-4',
    'bottom-left': 'bottom-4 left-4',
    'top-right': 'top-4 right-4',
    'top-left': 'top-4 left-4',
  };

  return (
    <div className={`feedback-widget ${positionClasses[position]} ${visible ? 'visible' : 'hidden'}`}>
      <button className="close-button" onClick={() => setVisible(false)}>
        ✕
      </button>

      {type === 'rating' && (
        <div className="rating-widget">
          <p>Rate your experience:</p>
          {[1, 2, 3, 4, 5].map(star => (
            <button
              key={star}
              onClick={() => handleSubmit({ type: 'rating', value: star })}
              className="star-button"
            >
              ★
            </button>
          ))}
        </div>
      )}

      {type === 'emoji' && (
        <div className="emoji-widget">
          <p>How was your experience?</p>
          <button onClick={() => handleSubmit({ type: 'emoji', value: '😠' })}>😠</button>
          <button onClick={() => handleSubmit({ type: 'emoji', value: '😞' })}>😞</button>
          <button onClick={() => handleSubmit({ type: 'emoji', value: '😐' })}>😐</button>
          <button onClick={() => handleSubmit({ type: 'emoji', value: '🙂' })}>🙂</button>
          <button onClick={() => handleSubmit({ type: 'emoji', value: '😄' })}>😄</button>
        </div>
      )}

      {type === 'thumbs' && (
        <div className="thumbs-widget">
          <p>Was this helpful?</p>
          <button onClick={() => handleSubmit({ type: 'thumbs', value: 'up' })}>
            👍
          </button>
          <button onClick={() => handleSubmit({ type: 'thumbs', value: 'down' })}>
            👎
          </button>
        </div>
      )}

      <div className="comment-section">
        <textarea
          placeholder="Tell us more..."
          rows={3}
        />
        <button onClick={() => handleSubmit({ type: 'comment', value: document.querySelector('textarea')?.value })}>
          Submit
        </button>
      </div>
    </div>
  );
};

export default FeedbackWidget;
```

---

## Rating Systems

### NPS (Net Promoter Score)

```typescript
interface NPSMetrics {
  surveyId: string;
  totalResponses: number;
  promoters: number; // 9-10
  passives: number; // 7-8
  detractors: number; // 0-6
  npsScore: number; // -100 to 100
  npsCategory: string;
}

class NPSManager {
  constructor(private prisma: PrismaClient) {}

  /**
   * Calculate NPS metrics
   */
  async calculateMetrics(surveyId: string): Promise<NPSMetrics> {
    const responses = await this.prisma.npsScore.findMany({
      where: { surveyId },
    });

    const promoters = responses.filter(r => r.score >= 9).length;
    const passives = responses.filter(r => r.score >= 7 && r.score <= 8).length;
    const detractors = responses.filter(r => r.score <= 6).length;
    const total = responses.length;

    if (total === 0) {
      return {
        surveyId,
        totalResponses: 0,
        promoters: 0,
        passives: 0,
        detractors: 0,
        npsScore: 0,
        npsCategory: 'no_data',
      };
    }

    const npsScore = ((promoters - detractors) / total) * 100;
    const npsCategory = this.getNPSCategory(npsScore);

    return {
      surveyId,
      totalResponses: total,
      promoters,
      passives,
      detractors,
      npsScore,
      npsCategory,
    };
  }

  /**
   * Get NPS category
   */
  private getNPSCategory(score: number): string {
    if (score >= 70) return 'excellent';
    if (score >= 50) return 'good';
    if (score >= 0) return 'average';
    if (score >= -50) return 'poor';
    return 'terrible';
  }

  /**
   * Get NPS trend
   */
  async getTrend(surveyId: string, days: number = 30): Promise<Array<{
    date: Date;
    npsScore: number;
    responseCount: number;
  }>> {
    const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000);

    const responses = await this.prisma.npsScore.findMany({
      where: {
        surveyId,
        createdAt: { gte: startDate },
      },
      orderBy: { createdAt: 'asc' },
    });

    // Group by day
    const grouped = new Map<string, { scores: number[]; count: number }>();

    for (const response of responses) {
      const dateKey = response.createdAt.toISOString().split('T')[0];
      const existing = grouped.get(dateKey) || { scores: [], count: 0 };
      existing.scores.push(response.score);
      existing.count++;
      grouped.set(dateKey, existing);
    }

    // Calculate daily NPS
    return Array.from(grouped.entries()).map(([date, data]) => {
      const scores = data.scores;
      const promoters = scores.filter(s => s >= 9).length;
      const detractors = scores.filter(s => s <= 6).length;
      const npsScore = (scores.length > 0)
        ? ((promoters - detractors) / scores.length) * 100
        : 0;

      return {
        date: new Date(date),
        npsScore,
        responseCount: data.count,
      };
    });
  }
}
```

---

## Feedback Categorization

### Feedback Categorizer

```typescript
interface FeedbackCategory {
  id: string;
  name: string;
  type: 'feature_request' | 'bug_report' | 'complaint' | 'compliment' | 'question' | 'other';
  keywords: string[];
  priority: 'low' | 'medium' | 'high' | 'critical';
}

class FeedbackCategorizer {
  private categories: FeedbackCategory[];

  constructor() {
    this.categories = [
      {
        id: 'feature_request',
        name: 'Feature Request',
        type: 'feature_request',
        keywords: ['feature', 'request', 'add', 'new', 'would like', 'wish', 'suggestion'],
        priority: 'medium',
      },
      {
        id: 'bug_report',
        name: 'Bug Report',
        type: 'bug_report',
        keywords: ['bug', 'error', 'issue', 'problem', 'broken', 'not working', 'crash', 'glitch'],
        priority: 'high',
      },
      {
        id: 'complaint',
        name: 'Complaint',
        type: 'complaint',
        keywords: ['complaint', 'unhappy', 'disappointed', 'frustrated', 'poor', 'slow'],
        priority: 'high',
      },
      {
        id: 'compliment',
        name: 'Compliment',
        type: 'compliment',
        keywords: ['great', 'awesome', 'love', 'excellent', 'amazing', 'thanks', 'helpful'],
        priority: 'low',
      },
      {
        id: 'question',
        name: 'Question',
        type: 'question',
        keywords: ['how', 'what', 'why', 'question', 'help', 'support'],
        priority: 'medium',
      },
      {
        id: 'other',
        name: 'Other',
        type: 'other',
        keywords: [],
        priority: 'low',
      },
    ];
  }

  /**
   * Categorize feedback
   */
  categorize(feedback: {
    text?: string;
    rating?: number;
    type?: string;
  }): FeedbackCategory {
    const text = (feedback.text || '').toLowerCase();

    // Check for keyword matches
    for (const category of this.categories) {
      for (const keyword of category.keywords) {
        if (text.includes(keyword)) {
          return category;
        }
      }
    }

    // Check rating-based categorization
    if (feedback.rating !== undefined) {
      if (feedback.rating <= 2) {
        return this.categories.find(c => c.id === 'complaint')!;
      }
      if (feedback.rating >= 4) {
        return this.categories.find(c => c.id === 'compliment')!;
      }
    }

    // Check type-based categorization
    if (feedback.type) {
      const category = this.categories.find(c => c.id === feedback.type);
      if (category) return category;
    }

    // Default to question
    return this.categories.find(c => c.id === 'question')!;
  }

  /**
   * Get category by ID
   */
  getCategory(id: string): FeedbackCategory | undefined {
    return this.categories.find(c => c.id === id);
  }

  /**
   * Get all categories
   */
  getAllCategories(): FeedbackCategory[] {
    return this.categories;
  }
}
```

---

## Sentiment Analysis

### Sentiment Analyzer

```typescript
// npm install @tensorflow/tfjs-node
import * as tf from '@tensorflow/tfjs-node';

class SentimentAnalyzer {
  private model: tf.LayersModel | null = null;

  constructor() {
    this.loadModel();
  }

  /**
   * Load sentiment model
   */
  private async loadModel(): Promise<void> {
    // Load pre-trained model or train your own
    // This is a simplified example
    // In production, you'd use a proper ML model
    console.log('Loading sentiment model...');
  }

  /**
   * Analyze sentiment
   */
  async analyze(text: string): Promise<{
    sentiment: 'positive' | 'negative' | 'neutral';
    score: number; // -1 to 1
    confidence: number; // 0 to 1
  }> {
    if (!this.model) {
      // Fallback to rule-based analysis
      return this.ruleBasedAnalysis(text);
    }

    // Use ML model
    const prediction = await this.model.predict(tf.tensor([text]));

    return {
      sentiment: this.getSentimentLabel(prediction),
      score: parseFloat(prediction.dataSync()[0]),
      confidence: 0.85,
    };
  }

  /**
   * Rule-based sentiment analysis
   */
  private ruleBasedAnalysis(text: string): {
    sentiment: 'positive' | 'negative' | 'neutral';
    score: number;
    confidence: number;
  } {
    const lowerText = text.toLowerCase();

    const positiveWords = ['great', 'awesome', 'love', 'excellent', 'amazing', 'thanks', 'helpful', 'good', 'happy', 'pleased'];
    const negativeWords = ['terrible', 'awful', 'hate', 'disappointed', 'frustrated', 'poor', 'slow', 'broken', 'bug', 'error'];

    let positiveCount = 0;
    let negativeCount = 0;

    for (const word of positiveWords) {
      if (lowerText.includes(word)) positiveCount++;
    }

    for (const word of negativeWords) {
      if (lowerText.includes(word)) negativeCount++;
    }

    if (positiveCount > negativeCount) {
      return {
        sentiment: 'positive',
        score: 0.5,
        confidence: Math.min(positiveCount / 5, 1),
      };
    }

    if (negativeCount > positiveCount) {
      return {
        sentiment: 'negative',
        score: -0.5,
        confidence: Math.min(negativeCount / 5, 1),
      };
    }

    return {
      sentiment: 'neutral',
      score: 0,
      confidence: 0.5,
    };
  }

  /**
   * Get sentiment label from prediction
   */
  private getSentimentLabel(prediction: any): 'positive' | 'negative' | 'neutral' {
    const score = prediction.dataSync()[0];

    if (score > 0.3) return 'positive';
    if (score < -0.3) return 'negative';
    return 'neutral';
  }

  /**
   * Analyze batch
   */
  async analyzeBatch(texts: string[]): Promise<Array<{
    text: string;
    sentiment: string;
    score: number;
    confidence: number;
  }>> {
    const results = [];

    for (const text of texts) {
      const analysis = await this.analyze(text);
      results.push({
        text,
        ...analysis,
      });
    }

    return results;
  }
}
```

---

## Feedback Routing

### Feedback Router

```typescript
interface FeedbackRoute {
  id: string;
  name: string;
  conditions: RouteCondition[];
  destination: {
    type: 'email' | 'slack' | 'jira' | 'zendesk' | 'custom';
    config: Record<string, any>;
  };
  priority: number;
}

interface RouteCondition {
  field: 'category' | 'sentiment' | 'rating' | 'priority';
  operator: 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'in' | 'not_in';
  value: any;
}

class FeedbackRouter {
  private routes: FeedbackRoute[] = [];

  constructor() {
    this.setupDefaultRoutes();
  }

  /**
   * Setup default routes
   */
  private setupDefaultRoutes(): void {
    this.routes = [
      {
        id: 'critical_bug',
        name: 'Critical Bug Reports',
        priority: 1,
        conditions: [
          { field: 'category', operator: 'equals', value: 'bug_report' },
          { field: 'priority', operator: 'equals', value: 'critical' },
        ],
        destination: {
          type: 'slack',
          config: { channel: '#critical-issues' },
        },
      },
      {
        id: 'feature_requests',
        name: 'Feature Requests',
        priority: 5,
        conditions: [
          { field: 'category', operator: 'equals', value: 'feature_request' },
        ],
        destination: {
          type: 'jira',
          config: { project: 'FEAT' },
        },
      },
      {
        id: 'negative_feedback',
        name: 'Negative Feedback',
        priority: 3,
        conditions: [
          { field: 'sentiment', operator: 'equals', value: 'negative' },
        ],
        destination: {
          type: 'email',
          config: { recipients: ['support@company.com'] },
        },
      },
      {
        id: 'compliments',
        name: 'Compliments',
        priority: 10,
        conditions: [
          { field: 'category', operator: 'equals', value: 'compliment' },
        ],
        destination: {
          type: 'slack',
          config: { channel: '#kudos' },
        },
      },
    ];
  }

  /**
   * Route feedback
   */
  async routeFeedback(feedback: {
    category: string;
    sentiment: string;
    rating?: number;
    priority?: string;
    text: string;
    userId: string;
  }): Promise<void> {
    // Find matching route
    const route = this.findMatchingRoute(feedback);

    if (!route) {
      console.log('No matching route found for feedback');
      return;
    }

    // Send to destination
    await this.sendToDestination(route.destination, feedback);
  }

  /**
   * Find matching route
   */
  private findMatchingRoute(feedback: any): FeedbackRoute | null {
    const sortedRoutes = [...this.routes].sort((a, b) => a.priority - b.priority);

    for (const route of sortedRoutes) {
      if (this.matchesConditions(route.conditions, feedback)) {
        return route;
      }
    }

    return null;
  }

  /**
   * Check if feedback matches conditions
   */
  private matchesConditions(conditions: RouteCondition[], feedback: any): boolean {
    for (const condition of conditions) {
      const value = feedback[condition.field];

      let matches = false;

      switch (condition.operator) {
        case 'equals':
          matches = value === condition.value;
          break;
        case 'not_equals':
          matches = value !== condition.value;
          break;
        case 'greater_than':
          matches = typeof value === 'number' && value > condition.value;
          break;
        case 'less_than':
          matches = typeof value === 'number' && value < condition.value;
          break;
        case 'in':
          matches = Array.isArray(condition.value) && condition.value.includes(value);
          break;
        case 'not_in':
          matches = !Array.isArray(condition.value) || !condition.value.includes(value);
          break;
      }

      if (!matches) return false;
    }

    return true;
  }

  /**
   * Send to destination
   */
  private async sendToDestination(
    destination: FeedbackRoute['destination'],
    feedback: any
  ): Promise<void> {
    switch (destination.type) {
      case 'slack':
        await this.sendToSlack(destination.config, feedback);
        break;
      case 'email':
        await this.sendToEmail(destination.config, feedback);
        break;
      case 'jira':
        await this.sendToJira(destination.config, feedback);
        break;
      case 'zendesk':
        await this.sendToZendesk(destination.config, feedback);
        break;
      case 'custom':
        await this.sendToCustom(destination.config, feedback);
        break;
    }
  }

  private async sendToSlack(config: any, feedback: any): Promise<void> {
    // Implement Slack integration
    console.log(`Sending to Slack channel: ${config.channel}`);
  }

  private async sendToEmail(config: any, feedback: any): Promise<void> {
    // Implement email sending
    console.log(`Sending to email: ${config.recipients}`);
  }

  private async sendToJira(config: any, feedback: any): Promise<void> {
    // Implement Jira integration
    console.log(`Creating Jira issue in project: ${config.project}`);
  }

  private async sendToZendesk(config: any, feedback: any): Promise<void> {
    // Implement Zendesk integration
    console.log('Creating Zendesk ticket');
  }

  private async sendToCustom(config: any, feedback: any): Promise<void> {
    // Implement custom destination
    console.log('Sending to custom destination');
  }
}
```

---

## Response Management

### Response Manager

```typescript
interface FeedbackResponse {
  id: string;
  feedbackId: string;
  responderId: string;
  response: string;
  status: 'draft' | 'sent' | 'acknowledged';
  sentAt?: Date;
  createdAt: Date;
}

class ResponseManager {
  constructor(private prisma: PrismaClient) {}

  /**
   * Create response
   */
  async createResponse(params: {
    feedbackId: string;
    responderId: string;
    response: string;
    status?: FeedbackResponse['status'];
  }): Promise<string> {
    const created = await this.prisma.feedbackResponse.create({
      data: {
        ...params,
        status: params.status || 'draft',
      },
    });

    return created.id;
  }

  /**
   * Send response
   */
  async sendResponse(responseId: string): Promise<void> {
    const response = await this.prisma.feedbackResponse.findUnique({
      where: { id: responseId },
      include: { feedback: true },
    });

    if (!response) {
      throw new Error('Response not found');
    }

    // Send response based on feedback type
    if (response.feedback.type === 'email') {
      await this.sendEmailResponse(response);
    } else if (response.feedback.type === 'in_app') {
      await this.sendInAppResponse(response);
    }

    // Update status
    await this.prisma.feedbackResponse.update({
      where: { id: responseId },
      data: {
        status: 'sent',
        sentAt: new Date(),
      },
    });
  }

  /**
   * Send email response
   */
  private async sendEmailResponse(response: any): Promise<void> {
    await emailService.send({
      to: response.feedback.email,
      subject: `Re: ${response.feedback.subject}`,
      templateId: 'feedback-response',
      dynamicTemplateData: {
        response: response.response,
        feedbackText: response.feedback.text,
      },
    });
  }

  /**
   * Send in-app response
   */
  private async sendInAppResponse(response: any): Promise<void> {
    // Implement in-app notification
    console.log(`Sending in-app response to user ${response.feedback.userId}`);
  }

  /**
   * Get canned responses
   */
  async getCannedResponses(category?: string): Promise<any[]> {
    const where: any = { isActive: true };

    if (category) {
      where.category = category;
    }

    return await this.prisma.cannedResponse.findMany({
      where,
      orderBy: { name: 'asc' },
    });
  }

  /**
   * Use canned response
   */
  async useCannedResponse(
    responseId: string,
    cannedResponseId: string
  ): Promise<void> {
    const canned = await this.prisma.cannedResponse.findUnique({
      where: { id: cannedResponseId },
    });

    if (!canned) {
      throw new Error('Canned response not found');
    }

    await this.prisma.feedbackResponse.update({
      where: { id: responseId },
      data: { response: canned.content },
    });
  }
}
```

---

## Analytics and Reporting

### Feedback Analytics

```typescript
interface FeedbackAnalytics {
  totalFeedback: number;
  byCategory: Record<string, number>;
  bySentiment: Record<string, number>;
  byRating: Record<number, number>;
  averageRating: number;
  npsScore: number;
  responseRate: number;
  averageResponseTime: number;
  topIssues: Array<{
    category: string;
    count: number;
    sentiment: string;
  }>;
}

class FeedbackAnalytics {
  constructor(private prisma: PrismaClient) {}

  /**
   * Get analytics
   */
  async getAnalytics(params: {
    startDate: Date;
    endDate: Date;
    category?: string;
  }): Promise<FeedbackAnalytics> {
    const where: any = {
      createdAt: {
        gte: params.startDate,
        lte: params.endDate,
      },
    };

    if (params.category) {
      where.category = params.category;
    }

    const [feedback, responses] = await Promise.all([
      this.prisma.feedback.findMany({ where }),
      this.prisma.feedbackResponse.findMany({
        where: {
          feedback: {
            createdAt: where.createdAt,
          },
        },
        include: { feedback: true },
      }),
    ]);

    // Calculate metrics
    const totalFeedback = feedback.length;
    const byCategory = this.groupBy(feedback, 'category');
    const bySentiment = this.groupBy(feedback, 'sentiment');
    const byRating = this.groupBy(feedback, 'rating');
    const averageRating = this.calculateAverageRating(feedback);
    const npsScore = this.calculateNPS(feedback);
    const responseRate = responses.length / totalFeedback;
    const averageResponseTime = this.calculateAverageResponseTime(responses);
    const topIssues = this.getTopIssues(feedback);

    return {
      totalFeedback,
      byCategory,
      bySentiment,
      byRating,
      averageRating,
      npsScore,
      responseRate,
      averageResponseTime,
      topIssues,
    };
  }

  /**
   * Group feedback by field
   */
  private groupBy(feedback: any[], field: string): Record<string, number> {
    return feedback.reduce((acc, f) => {
      const key = f[field] || 'unknown';
      acc[key] = (acc[key] || 0) + 1;
      return acc;
    }, {});
  }

  /**
   * Calculate average rating
   */
  private calculateAverageRating(feedback: any[]): number {
    const ratings = feedback.filter(f => f.rating !== null && f.rating !== undefined);
    if (ratings.length === 0) return 0;

    const sum = ratings.reduce((acc, f) => acc + f.rating, 0);
    return sum / ratings.length;
  }

  /**
   * Calculate NPS
   */
  private calculateNPS(feedback: any[]): number {
    const npsScores = feedback.filter(f => f.npsScore !== null);
    if (npsScores.length === 0) return 0;

    return npsScores.reduce((acc, f) => acc + f.npsScore, 0) / npsScores.length;
  }

  /**
   * Calculate average response time
   */
  private calculateAverageResponseTime(responses: any[]): number {
    const withTime = responses.filter(r => r.sentAt && r.createdAt);
    if (withTime.length === 0) return 0;

    const totalTime = withTime.reduce((acc, r) => {
      return acc + (r.sentAt!.getTime() - r.createdAt.getTime());
    }, 0);

    return totalTime / withTime.length;
  }

  /**
   * Get top issues
   */
  private getTopIssues(feedback: any[]): Array<{
    category: string;
    count: number;
    sentiment: string;
  }> {
    const grouped = this.groupBy(feedback, 'category');
    const issues = Object.entries(grouped).map(([category, count]) => {
      const categoryFeedback = feedback.filter(f => f.category === category);
      const sentiment = this.getCategorySentiment(categoryFeedback);

      return { category, count, sentiment };
    });

    return issues.sort((a, b) => b.count - a.count).slice(0, 10);
  }

  /**
   * Get category sentiment
   */
  private getCategorySentiment(feedback: any[]): string {
    const sentiments = feedback.map(f => f.sentiment);
    const positive = sentiments.filter(s => s === 'positive').length;
    const negative = sentiments.filter(s => s === 'negative').length;

    if (positive > negative) return 'positive';
    if (negative > positive) return 'negative';
    return 'neutral';
  }
}
```

---

## Integration with Product Roadmap

### Roadmap Integration

```typescript
class RoadmapIntegration {
  constructor(private prisma: PrismaClient) {}

  /**
   * Add feature request to roadmap
   */
  async addFeatureRequest(params: {
    feedbackId: string;
    title: string;
    description: string;
    userId: string;
    votes?: number;
  }): Promise<string> {
    // Create or update roadmap item
    const item = await this.prisma.roadmapItem.upsert({
      where: { feedbackId: params.feedbackId },
      create: {
        title: params.title,
        description: params.description,
        userId: params.userId,
        status: 'proposed',
        votes: 1,
        feedbackId: params.feedbackId,
      },
      update: {
        title: params.title,
        description: params.description,
      },
    });

    // Update feedback with roadmap item
    await this.prisma.feedback.update({
      where: { id: params.feedbackId },
      data: { roadmapItemId: item.id },
    });

    return item.id;
  }

  /**
   * Vote for feature
   */
  async voteForFeature(itemId: string, userId: string): Promise<void> {
    // Check if already voted
    const existingVote = await this.prisma.roadmapVote.findUnique({
      where: {
        itemId_userId: {
          itemId,
          userId,
        },
      },
    });

    if (existingVote) {
      throw new Error('Already voted');
    }

    // Add vote
    await this.prisma.roadmapVote.create({
      data: {
        itemId,
        userId,
      },
    });

    // Update vote count
    await this.prisma.roadmapItem.update({
      where: { id: itemId },
      data: { votes: { increment: 1 } },
    });
  }

  /**
   * Update roadmap item status
   */
  async updateStatus(itemId: string, status: string): Promise<void> {
    await this.prisma.roadmapItem.update({
      where: { id: itemId },
      data: { status },
    });
  }

  /**
   * Get roadmap items
   */
  async getRoadmap(params?: {
    status?: string;
    sortBy?: 'votes' | 'created_at';
    limit?: number;
  }): Promise<any[]> {
    const where: any = {};

    if (params?.status) {
      where.status = params.status;
    }

    const orderBy: any = {};
    if (params?.sortBy) {
      orderBy[params.sortBy] = params.sortBy === 'votes' ? 'desc' : 'desc';
    }

    return await this.prisma.roadmapItem.findMany({
      where,
      orderBy,
      take: params?.limit || 20,
      include: {
        votes: true,
        user: true,
      },
    });
  }
}
```

---

## Tools

### Typeform Integration

```typescript
// npm install @typeform/node
import { createClient } from '@typeform/node';

const typeform = createClient({
  apiKey: process.env.TYPEFORM_API_KEY!,
});

class TypeformIntegration {
  /**
   * Create form
   */
  async createForm(form: {
    title: string;
    description?: string;
    fields: any[];
  }): Promise<string> {
    const response = await typeform.forms.create({
      data: form,
    });

    return response.id;
  }

  /**
   * Get responses
   */
  async getResponses(formId: string): Promise<any[]> {
    const responses = awa

…(truncated)
