# Community Management Phase 2.1 – Analytics Dashboard

## Projekt-Kontext

Du arbeitest am Community Management System für Complex Care Solutions (CCS).
Phase 1 ist abgeschlossen und umfasst:
- Community Inbox View (Desktop + Mobile) unter `/admin/community/inbox`
- YouTube OAuth + Auto-Sync
- Rules Engine für automatische Aktionen
- Export-Funktion (PDF/Excel/CSV)
- Claude AI Sentiment-Analyse
- Bestehende Stats-API unter `/api/community/stats`

**Deine Aufgabe:** Implementiere das Analytics Dashboard als neue Admin View.

---

## Tech Stack

| Technologie | Version |
|-------------|---------|
| Payload CMS | 3.69.0 |
| Next.js | 15.x (App Router) |
| React | 19.x |
| TypeScript | 5.x |
| Datenbank | PostgreSQL 17 |
| Charts | Recharts 2.x |
| Datum | date-fns 4.x |
| Styling | SCSS (BEM-Konvention) |

---

## Bestehende Datenbankstruktur

### Collection: CommunityInteractions

```typescript
{
  id: number
  platform: Relation<SocialPlatforms>
  socialAccount: Relation<SocialAccounts>
  linkedContent?: Relation<YouTubeContent>  // youtube-content Collection
  type: 'comment' | 'reply' | 'dm' | 'mention' | 'review' | 'question'
  externalId: string  // Unique, indexed
  parentInteraction?: Relation<CommunityInteractions>  // für Threads
  author: {
    name: string
    handle: string
    profileUrl: string
    avatarUrl: string
    isVerified: boolean
    isSubscriber: boolean
    isMember: boolean  // Channel Member
    subscriberCount: number
  }
  message: string
  messageHtml?: string  // Original HTML falls vorhanden
  attachments: Array<{ type: 'image' | 'video' | 'link' | 'sticker', url: string }>
  publishedAt: Date
  analysis: {
    sentiment: 'positive' | 'neutral' | 'negative' | 'question' | 'gratitude' | 'frustration'
    sentimentScore: number      // -1.0 bis +1.0
    confidence: number          // 0 bis 100 (Prozent!)
    topics: Array<{ topic: string }>
    language: string
    suggestedTemplate?: Relation<CommunityTemplates>
    suggestedReply?: string
    analyzedAt?: Date
  }
  flags: {
    isMedicalQuestion: boolean  // Erfordert ärztliche Review
    requiresEscalation: boolean
    isSpam: boolean
    isFromInfluencer: boolean   // >10k Follower
  }
  status: 'new' | 'in_review' | 'waiting' | 'replied' | 'resolved' | 'archived' | 'spam'
  priority: 'urgent' | 'high' | 'normal' | 'low'
  assignedTo?: Relation<Users>
  responseDeadline?: Date
  response?: {
    text: string
    usedTemplate?: Relation<CommunityTemplates>
    sentAt: Date
    sentBy: Relation<Users>
    externalReplyId?: string
  }
  engagement: {
    likes: number
    replies: number
    isHearted: boolean  // Creator Heart
    isPinned: boolean
  }
  internalNotes?: string
  createdAt: Date
  updatedAt: Date
}
```

### Collection: SocialAccounts

```typescript
{
  id: number
  platform: Relation<SocialPlatforms>
  linkedChannel?: Relation<YouTubeChannels>
  displayName: string           // z.B. "BlogWoman YouTube"
  accountHandle?: string        // z.B. "@blogwoman"
  externalId?: string           // YouTube Channel ID, etc.
  accountUrl?: string
  isActive: boolean
  stats: {
    followers: number
    totalPosts: number
    lastSyncedAt?: Date
  }
  syncSettings: {
    autoSyncEnabled: boolean
    syncIntervalMinutes: number
    syncComments: boolean
    syncDMs: boolean
  }
}
```

---

## Bestehende API-Endpoints

### GET /api/community/stats (EXISTIERT BEREITS)

```typescript
// Response:
{
  new: number           // Neue Interaktionen
  urgent: number        // Dringende (priority=urgent, nicht resolved/archived/spam)
  waiting: number       // Status = waiting
  today: number         // Heute eingegangen
  sentiment: {
    positive: number
    neutral: number
    negative: number
  }
}
```

> **Hinweis:** Diese API kann als Basis für die Overview-Daten verwendet oder erweitert werden.

---

## Implementierungs-Anforderungen

### 1. Dateistruktur erstellen

```
src/
├── app/(payload)/
│   ├── admin/views/community/
│   │   └── analytics/
│   │       ├── page.tsx
│   │       ├── AnalyticsDashboard.tsx
│   │       ├── components/
│   │       │   ├── KPICards.tsx
│   │       │   ├── SentimentTrendChart.tsx
│   │       │   ├── ResponseMetrics.tsx
│   │       │   ├── ChannelComparison.tsx
│   │       │   ├── TopContent.tsx
│   │       │   └── TopicCloud.tsx
│   │       └── analytics.scss
│   └── api/community/analytics/
│       ├── overview/route.ts
│       ├── sentiment-trend/route.ts
│       ├── response-metrics/route.ts
│       ├── channel-comparison/route.ts
│       ├── top-content/route.ts
│       └── topic-cloud/route.ts
└── lib/
    └── analytics/
        └── calculateMetrics.ts
```

### 2. API Endpoints implementieren

#### GET /api/community/analytics/overview

```typescript
// Query Parameter:
// - period: '7d' | '30d' | '90d' (default: '30d')
// - channel: 'all' | string (socialAccount.id)

// Response:
interface OverviewResponse {
  period: string
  totalInteractions: number
  newInteractions: number
  responseRate: number          // 0-100
  avgResponseTimeHours: number
  avgSentimentScore: number     // -1 bis +1
  medicalQuestions: number
  escalations: number
  // Sentiment-Verteilung (alle 6 Typen!)
  sentimentDistribution: {
    positive: number
    neutral: number
    negative: number
    question: number
    gratitude: number
    frustration: number
  }
  comparison: {
    totalInteractions: number   // Änderung vs. Vorperiode in %
    responseRate: number
    avgSentimentScore: number
  }
}
```

#### GET /api/community/analytics/sentiment-trend

```typescript
// Query Parameter:
// - period: '7d' | '30d' | '90d'
// - channel: 'all' | string
// - granularity: 'day' | 'week' (default: basierend auf period)

// Response:
interface SentimentTrendResponse {
  data: Array<{
    date: string              // ISO Date
    positive: number
    neutral: number
    negative: number
    question: number
    gratitude: number
    frustration: number
    avgScore: number
    total: number
  }>
}
```

#### GET /api/community/analytics/response-metrics

```typescript
// Query Parameter:
// - period: '7d' | '30d' | '90d'
// - channel: 'all' | string

// Response:
interface ResponseMetricsResponse {
  firstResponseTime: {
    median: number            // Stunden
    p90: number
    trend: number             // vs. Vorperiode
  }
  resolutionRate: number      // 0-100 (status=resolved / total)
  escalationRate: number      // flags.requiresEscalation / total
  templateUsageRate: number   // response.usedTemplate vorhanden / responded
  byPriority: {
    urgent: { count: number, avgResponseTime: number }
    high: { count: number, avgResponseTime: number }
    normal: { count: number, avgResponseTime: number }
    low: { count: number, avgResponseTime: number }
  }
  byStatus: {
    new: number
    in_review: number
    waiting: number
    replied: number
    resolved: number
    archived: number
    spam: number
  }
}
```

#### GET /api/community/analytics/channel-comparison

```typescript
// Query Parameter:
// - period: '7d' | '30d' | '90d'

// Response:
interface ChannelComparisonResponse {
  channels: Array<{
    id: number
    name: string              // displayName
    platform: string          // platform.name
    metrics: {
      totalInteractions: number
      avgSentiment: number
      responseRate: number
      avgResponseTimeHours: number
      topTopics: string[]
    }
  }>
}
```

#### GET /api/community/analytics/top-content

```typescript
// Query Parameter:
// - period: '7d' | '30d' | '90d'
// - limit: number (default: 10)
// - sortBy: 'comments' | 'sentiment' | 'medical' (default: 'comments')
// - channel: 'all' | string

// Response:
interface TopContentResponse {
  content: Array<{
    contentId: number         // YouTubeContent.id
    videoId: string           // YouTube Video ID
    title: string
    channelName: string
    thumbnailUrl: string
    commentCount: number
    avgSentiment: number
    medicalQuestions: number
    topTopics: string[]
    publishedAt: string
  }>
}
```

#### GET /api/community/analytics/topic-cloud

```typescript
// Query Parameter:
// - period: '7d' | '30d' | '90d'
// - channel: 'all' | string
// - limit: number (default: 30)

// Response:
interface TopicCloudResponse {
  topics: Array<{
    topic: string
    count: number
    avgSentiment: number
    channels: string[]
  }>
}
```

---

## 3. React Komponenten

### page.tsx (View Registration via App Router)

```tsx
// src/app/(payload)/admin/views/community/analytics/page.tsx

import React from 'react'
import { DefaultTemplate } from '@payloadcms/next/templates'
import { Gutter } from '@payloadcms/ui'
import { AnalyticsDashboard } from './AnalyticsDashboard'

import './analytics.scss'

export const metadata = {
  title: 'Community Analytics',
  description: 'Performance insights across all channels',
}

export default function CommunityAnalyticsPage() {
  return (
    <DefaultTemplate>
      <Gutter>
        <AnalyticsDashboard />
      </Gutter>
    </DefaultTemplate>
  )
}
```

### AnalyticsDashboard.tsx (Hauptkomponente)

```tsx
// src/app/(payload)/admin/views/community/analytics/AnalyticsDashboard.tsx

'use client'

import React, { useState, useEffect } from 'react'
import { KPICards } from './components/KPICards'
import { SentimentTrendChart } from './components/SentimentTrendChart'
import { ResponseMetrics } from './components/ResponseMetrics'
import { ChannelComparison } from './components/ChannelComparison'
import { TopContent } from './components/TopContent'
import { TopicCloud } from './components/TopicCloud'

type Period = '7d' | '30d' | '90d'

interface Channel {
  id: string
  name: string
}

export const AnalyticsDashboard: React.FC = () => {
  const [period, setPeriod] = useState<Period>('30d')
  const [channel, setChannel] = useState<string>('all')
  const [channels, setChannels] = useState<Channel[]>([])

  // Fetch available channels on mount
  useEffect(() => {
    const fetchChannels = async () => {
      try {
        const response = await fetch('/api/social-accounts?where[isActive][equals]=true&limit=100')
        if (response.ok) {
          const data = await response.json()
          setChannels(data.docs.map((acc: { id: number; displayName: string }) => ({
            id: String(acc.id),
            name: acc.displayName
          })))
        }
      } catch (err) {
        console.error('Failed to fetch channels:', err)
      }
    }
    fetchChannels()
  }, [])

  const periodLabels: Record<Period, string> = {
    '7d': '7 Tage',
    '30d': '30 Tage',
    '90d': '90 Tage'
  }

  return (
    <div className="analytics-dashboard">
      {/* Header */}
      <div className="analytics-dashboard__header">
        <div className="analytics-dashboard__title">
          <h1>Community Analytics</h1>
          <p className="analytics-dashboard__subtitle">
            Performance-Übersicht aller Kanäle
          </p>
        </div>

        <div className="analytics-dashboard__filters">
          {/* Period Selector */}
          <div className="analytics-dashboard__filter">
            <label htmlFor="period-select">Zeitraum:</label>
            <select
              id="period-select"
              value={period}
              onChange={(e) => setPeriod(e.target.value as Period)}
              className="analytics-dashboard__select"
            >
              {Object.entries(periodLabels).map(([value, label]) => (
                <option key={value} value={value}>{label}</option>
              ))}
            </select>
          </div>

          {/* Channel Filter */}
          <div className="analytics-dashboard__filter">
            <label htmlFor="channel-select">Kanal:</label>
            <select
              id="channel-select"
              value={channel}
              onChange={(e) => setChannel(e.target.value)}
              className="analytics-dashboard__select"
            >
              <option value="all">Alle Kanäle</option>
              {channels.map((ch) => (
                <option key={ch.id} value={ch.id}>{ch.name}</option>
              ))}
            </select>
          </div>
        </div>
      </div>

      {/* KPI Cards Row */}
      <div className="analytics-dashboard__kpi-grid">
        <KPICards period={period} channel={channel} />
      </div>

      {/* Charts Grid - Row 1 */}
      <div className="analytics-dashboard__charts-grid">
        <div className="analytics-dashboard__card analytics-dashboard__card--wide">
          <h3>Sentiment-Trend</h3>
          <SentimentTrendChart period={period} channel={channel} />
        </div>

        <div className="analytics-dashboard__card">
          <h3>Response-Metriken</h3>
          <ResponseMetrics period={period} channel={channel} />
        </div>
      </div>

      {/* Charts Grid - Row 2 */}
      <div className="analytics-dashboard__charts-grid">
        <div className="analytics-dashboard__card">
          <h3>Kanal-Vergleich</h3>
          <ChannelComparison period={period} />
        </div>

        <div className="analytics-dashboard__card">
          <h3>Themen-Wolke</h3>
          <TopicCloud period={period} channel={channel} />
        </div>
      </div>

      {/* Top Content */}
      <div className="analytics-dashboard__card">
        <h3>Top Content nach Engagement</h3>
        <TopContent period={period} channel={channel} />
      </div>
    </div>
  )
}
```

### KPICards.tsx

```tsx
// src/app/(payload)/admin/views/community/analytics/components/KPICards.tsx

'use client'

import React, { useState, useEffect } from 'react'

interface KPICardsProps {
  period: string
  channel: string
}

interface OverviewData {
  totalInteractions: number
  newInteractions: number
  responseRate: number
  avgResponseTimeHours: number
  avgSentimentScore: number
  medicalQuestions: number
  escalations: number
  sentimentDistribution: {
    positive: number
    neutral: number
    negative: number
    question: number
    gratitude: number
    frustration: number
  }
  comparison: {
    totalInteractions: number
    responseRate: number
    avgSentimentScore: number
  }
}

export const KPICards: React.FC<KPICardsProps> = ({ period, channel }) => {
  const [data, setData] = useState<OverviewData | null>(null)
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true)
      setError(null)

      try {
        const params = new URLSearchParams({ period, channel })
        const response = await fetch(`/api/community/analytics/overview?${params}`)

        if (!response.ok) throw new Error('Failed to fetch overview data')

        const result = await response.json()
        setData(result)
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error')
      } finally {
        setIsLoading(false)
      }
    }

    fetchData()
  }, [period, channel])

  if (isLoading) {
    return (
      <>
        {[...Array(5)].map((_, i) => (
          <div key={i} className="kpi-card kpi-card--loading">
            <div className="kpi-card__skeleton" />
          </div>
        ))}
      </>
    )
  }

  if (error || !data) {
    return <div className="kpi-card kpi-card--error">Fehler beim Laden der Daten</div>
  }

  const getTrendClass = (value: number): string => {
    if (value > 0) return 'kpi-card__trend--up'
    if (value < 0) return 'kpi-card__trend--down'
    return 'kpi-card__trend--neutral'
  }

  const getTrendIcon = (value: number): string => {
    if (value > 0) return '↑'
    if (value < 0) return '↓'
    return '→'
  }

  const getSentimentEmoji = (score: number): string => {
    if (score > 0.3) return '😊'
    if (score < -0.3) return '😟'
    return '😐'
  }

  const formatTime = (hours: number): string => {
    if (hours < 1) return `${Math.round(hours * 60)} Min`
    return `${hours.toFixed(1)}h`
  }

  return (
    <>
      {/* Neue Interaktionen */}
      <div className="kpi-card">
        <div className="kpi-card__icon kpi-card__icon--blue">💬</div>
        <div className="kpi-card__value">{data.newInteractions}</div>
        <div className="kpi-card__label">Neue Interaktionen</div>
        <div className={`kpi-card__trend ${getTrendClass(data.comparison.totalInteractions)}`}>
          {getTrendIcon(data.comparison.totalInteractions)} {Math.abs(data.comparison.totalInteractions)}% vs. Vorperiode
        </div>
      </div>

      {/* Response Rate */}
      <div className="kpi-card">
        <div className="kpi-card__icon kpi-card__icon--green">✅</div>
        <div className="kpi-card__value">{data.responseRate.toFixed(0)}%</div>
        <div className="kpi-card__label">Response Rate</div>
        <div className={`kpi-card__trend ${getTrendClass(data.comparison.responseRate)}`}>
          {getTrendIcon(data.comparison.responseRate)} {Math.abs(data.comparison.responseRate).toFixed(1)}%
        </div>
      </div>

      {/* Ø Antwortzeit */}
      <div className="kpi-card">
        <div className="kpi-card__icon kpi-card__icon--purple">⏱️</div>
        <div className="kpi-card__value">{formatTime(data.avgResponseTimeHours)}</div>
        <div className="kpi-card__label">Ø Antwortzeit</div>
        <div className="kpi-card__trend kpi-card__trend--neutral">
          Median
        </div>
      </div>

      {/* Sentiment Score */}
      <div className="kpi-card">
        <div className="kpi-card__icon kpi-card__icon--yellow">
          {getSentimentEmoji(data.avgSentimentScore)}
        </div>
        <div className="kpi-card__value">
          {data.avgSentimentScore > 0 ? '+' : ''}{data.avgSentimentScore.toFixed(2)}
        </div>
        <div className="kpi-card__label">Sentiment Score</div>
        <div className={`kpi-card__trend ${getTrendClass(data.comparison.avgSentimentScore)}`}>
          {getTrendIcon(data.comparison.avgSentimentScore)} {Math.abs(data.comparison.avgSentimentScore).toFixed(2)}
        </div>
      </div>

      {/* Medical Flags */}
      <div className="kpi-card">
        <div className={`kpi-card__icon ${data.medicalQuestions > 0 ? 'kpi-card__icon--red' : 'kpi-card__icon--gray'}`}>
          ⚕️
        </div>
        <div className="kpi-card__value">{data.medicalQuestions}</div>
        <div className="kpi-card__label">Med. Anfragen</div>
        {data.escalations > 0 && (
          <div className="kpi-card__trend kpi-card__trend--down">
            ⚠️ {data.escalations} Eskalationen
          </div>
        )}
      </div>
    </>
  )
}
```

### SentimentTrendChart.tsx

```tsx
// src/app/(payload)/admin/views/community/analytics/components/SentimentTrendChart.tsx

'use client'

import React, { useState, useEffect } from 'react'
import {
  ComposedChart,
  Area,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
  ResponsiveContainer
} from 'recharts'

interface SentimentTrendProps {
  period: string
  channel: string
}

interface TrendDataPoint {
  date: string
  positive: number
  neutral: number
  negative: number
  question: number
  gratitude: number
  frustration: number
  avgScore: number
  total: number
}

const COLORS = {
  positive: '#10B981',
  neutral: '#6B7280',
  negative: '#EF4444',
  question: '#3B82F6',
  gratitude: '#8B5CF6',
  frustration: '#F97316',
  avgScore: '#1F2937'
}

export const SentimentTrendChart: React.FC<SentimentTrendProps> = ({ period, channel }) => {
  const [data, setData] = useState<TrendDataPoint[]>([])
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true)
      setError(null)

      try {
        const params = new URLSearchParams({ period, channel })
        const response = await fetch(`/api/community/analytics/sentiment-trend?${params}`)

        if (!response.ok) throw new Error('Failed to fetch sentiment trend')

        const result = await response.json()
        setData(result.data)
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error')
      } finally {
        setIsLoading(false)
      }
    }

    fetchData()
  }, [period, channel])

  if (isLoading) {
    return <div className="chart-loading">Lade Daten...</div>
  }

  if (error) {
    return <div className="chart-error">Fehler: {error}</div>
  }

  if (data.length === 0) {
    return <div className="chart-empty">Keine Daten für den gewählten Zeitraum</div>
  }

  const formatDate = (dateStr: string): string => {
    const date = new Date(dateStr)
    return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })
  }

  return (
    <div className="sentiment-trend-chart">
      <ResponsiveContainer width="100%" height={300}>
        <ComposedChart data={data} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
          <CartesianGrid strokeDasharray="3 3" stroke="#E5E7EB" />
          <XAxis
            dataKey="date"
            tickFormatter={formatDate}
            stroke="#9CA3AF"
            fontSize={12}
          />
          <YAxis
            yAxisId="left"
            stroke="#9CA3AF"
            fontSize={12}
          />
          <YAxis
            yAxisId="right"
            orientation="right"
            domain={[-1, 1]}
            stroke={COLORS.avgScore}
            fontSize={12}
          />
          <Tooltip />
          <Legend />

          <Area
            yAxisId="left"
            type="monotone"
            dataKey="positive"
            name="Positiv"
            stackId="1"
            fill={COLORS.positive}
            stroke={COLORS.positive}
            fillOpacity={0.6}
          />
          <Area
            yAxisId="left"
            type="monotone"
            dataKey="neutral"
            name="Neutral"
            stackId="1"
            fill={COLORS.neutral}
            stroke={COLORS.neutral}
            fillOpacity={0.6}
          />
          <Area
            yAxisId="left"
            type="monotone"
            dataKey="negative"
            name="Negativ"
            stackId="1"
            fill={COLORS.negative}
            stroke={COLORS.negative}
            fillOpacity={0.6}
          />
          <Area
            yAxisId="left"
            type="monotone"
            dataKey="question"
            name="Frage"
            stackId="1"
            fill={COLORS.question}
            stroke={COLORS.question}
            fillOpacity={0.6}
          />

          <Line
            yAxisId="right"
            type="monotone"
            dataKey="avgScore"
            name="Ø Score"
            stroke={COLORS.avgScore}
            strokeWidth={2}
            dot={false}
          />
        </ComposedChart>
      </ResponsiveContainer>
    </div>
  )
}
```

### ResponseMetrics.tsx

```tsx
// src/app/(payload)/admin/views/community/analytics/components/ResponseMetrics.tsx

'use client'

import React, { useState, useEffect } from 'react'

interface ResponseMetricsProps {
  period: string
  channel: string
}

interface MetricsData {
  firstResponseTime: {
    median: number
    p90: number
    trend: number
  }
  resolutionRate: number
  escalationRate: number
  templateUsageRate: number
  byPriority: {
    urgent: { count: number; avgResponseTime: number }
    high: { count: number; avgResponseTime: number }
    normal: { count: number; avgResponseTime: number }
    low: { count: number; avgResponseTime: number }
  }
  byStatus: {
    new: number
    in_review: number
    waiting: number
    replied: number
    resolved: number
    archived: number
    spam: number
  }
}

export const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ period, channel }) => {
  const [data, setData] = useState<MetricsData | null>(null)
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true)
      setError(null)

      try {
        const params = new URLSearchParams({ period, channel })
        const response = await fetch(`/api/community/analytics/response-metrics?${params}`)

        if (!response.ok) throw new Error('Failed to fetch response metrics')

        const result = await response.json()
        setData(result)
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error')
      } finally {
        setIsLoading(false)
      }
    }

    fetchData()
  }, [period, channel])

  if (isLoading) {
    return <div className="metrics-loading">Lade Metriken...</div>
  }

  if (error || !data) {
    return <div className="metrics-error">Fehler beim Laden</div>
  }

  const formatTime = (hours: number): string => {
    if (hours < 1) return `${Math.round(hours * 60)}m`
    return `${hours.toFixed(1)}h`
  }

  const getProgressColor = (value: number, target: number, inverse: boolean = false): string => {
    const ratio = value / target
    if (inverse) {
      if (ratio <= 1) return '#10B981'
      if (ratio <= 1.5) return '#F59E0B'
      return '#EF4444'
    }
    if (ratio >= 1) return '#10B981'
    if (ratio >= 0.7) return '#F59E0B'
    return '#EF4444'
  }

  return (
    <div className="response-metrics">
      {/* Response Time */}
      <div className="response-metrics__item">
        <div className="response-metrics__label">Erste Antwort (Median)</div>
        <div className="response-metrics__value">
          {formatTime(data.firstResponseTime.median)}
        </div>
        <div className="response-metrics__bar">
          <div
            className="response-metrics__bar-fill"
            style={{
              width: `${Math.min((4 / data.firstResponseTime.median) * 100, 100)}%`,
              backgroundColor: getProgressColor(data.firstResponseTime.median, 4, true)
            }}
          />
        </div>
        <div className="response-metrics__target">Ziel: &lt; 4h</div>
      </div>

      {/* Resolution Rate */}
      <div className="response-metrics__item">
        <div className="response-metrics__label">Resolution Rate</div>
        <div className="response-metrics__value">{data.resolutionRate.toFixed(0)}%</div>
        <div className="response-metrics__bar">
          <div
            className="response-metrics__bar-fill"
            style={{
              width: `${data.resolutionRate}%`,
              backgroundColor: getProgressColor(data.resolutionRate, 90)
            }}
          />
        </div>
        <div className="response-metrics__target">Ziel: &gt; 90%</div>
      </div>

      {/* Escalation Rate */}
      <div className="response-metrics__item">
        <div className="response-metrics__label">Eskalationsrate</div>
        <div className="response-metrics__value">{data.escalationRate.toFixed(1)}%</div>
        <div className="response-metrics__bar">
          <div
            className="response-metrics__bar-fill"
            style={{
              width: `${Math.min(data.escalationRate * 10, 100)}%`,
              backgroundColor: getProgressColor(data.escalationRate, 5, true)
            }}
          />
        </div>
        <div className="response-metrics__target">Ziel: &lt; 5%</div>
      </div>

      {/* Template Usage */}
      <div className="response-metrics__item">
        <div className="response-metrics__label">Template-Nutzung</div>
        <div className="response-metrics__value">{data.templateUsageRate.toFixed(0)}%</div>
        <div className="response-metrics__bar">
          <div
            className="response-metrics__bar-fill"
            style={{
              width: `${data.templateUsageRate}%`,
              backgroundColor: getProgressColor(data.templateUsageRate, 60)
            }}
          />
        </div>
        <div className="response-metrics__target">Ziel: &gt; 60%</div>
      </div>

      {/* Priority Breakdown */}
      <div className="response-metrics__priority-section">
        <h4>Nach Priorität</h4>
        <div className="response-metrics__priority-grid">
          {Object.entries(data.byPriority).map(([priority, stats]) => (
            <div key={priority} className={`response-metrics__priority response-metrics__priority--${priority}`}>
              <span className="response-metrics__priority-label">
                {priority.charAt(0).toUpperCase() + priority.slice(1)}
              </span>
              <span className="response-metrics__priority-count">{stats.count}</span>
              <span className="response-metrics__priority-time">{formatTime(stats.avgResponseTime)}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}
```

### ChannelComparison.tsx

```tsx
// src/app/(payload)/admin/views/community/analytics/components/ChannelComparison.tsx

'use client'

import React, { useState, useEffect } from 'react'

interface ChannelComparisonProps {
  period: string
}

interface ChannelData {
  id: number
  name: string
  platform: string
  metrics: {
    totalInteractions: number
    avgSentiment: number
    responseRate: number
    avgResponseTimeHours: number
    topTopics: string[]
  }
}

export const ChannelComparison: React.FC<ChannelComparisonProps> = ({ period }) => {
  const [channels, setChannels] = useState<ChannelData[]>([])
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true)
      setError(null)

      try {
        const params = new URLSearchParams({ period })
        const response = await fetch(`/api/community/analytics/channel-comparison?${params}`)

        if (!response.ok) throw new Error('Failed to fetch channel comparison')

        const result = await response.json()
        setChannels(result.channels)
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error')
      } finally {
        setIsLoading(false)
      }
    }

    fetchData()
  }, [period])

  if (isLoading) {
    return <div className="channel-comparison-loading">Lade Vergleich...</div>
  }

  if (error) {
    return <div className="channel-comparison-error">Fehler: {error}</div>
  }

  const getSentimentEmoji = (score: number): string => {
    if (score > 0.3) return '😊'
    if (score < -0.3) return '😟'
    return '😐'
  }

  const formatTime = (hours: number): string => {
    if (hours < 1) return `${Math.round(hours * 60)}m`
    return `${hours.toFixed(1)}h`
  }

  return (
    <div className="channel-comparison">
      <table className="channel-comparison__table">
        <thead>
          <tr>
            <th>Kanal</th>
            <th>Interaktionen</th>
            <th>Sentiment</th>
            <th>Response Rate</th>
            <th>Ø Antwortzeit</th>
            <th>Top Themen</th>
          </tr>
        </thead>
        <tbody>
          {channels.map((channel) => (
            <tr key={channel.id}>
              <td>
                <div className="channel-comparison__channel">
                  <span className="channel-comparison__platform">{channel.platform}</span>
                  <strong>{channel.name}</strong>
                </div>
              </td>
              <td>{channel.metrics.totalInteractions}</td>
              <td>
                <span className="channel-comparison__sentiment">
                  {getSentimentEmoji(channel.metrics.avgSentiment)}
                  {channel.metrics.avgSentiment > 0 ? '+' : ''}
                  {channel.metrics.avgSentiment.toFixed(2)}
                </span>
              </td>
              <td>
                <span className={`channel-comparison__rate ${channel.metrics.responseRate >= 90 ? 'channel-comparison__rate--good' : ''}`}>
                  {channel.metrics.responseRate.toFixed(0)}%
                </span>
              </td>
              <td>{formatTime(channel.metrics.avgResponseTimeHours)}</td>
              <td>
                <div className="channel-comparison__topics">
                  {channel.metrics.topTopics.slice(0, 3).map((topic, i) => (
                    <span key={i} className="channel-comparison__topic-tag">{topic}</span>
                  ))}
                </div>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}
```

### TopContent.tsx

```tsx
// src/app/(payload)/admin/views/community/analytics/components/TopContent.tsx

'use client'

import React, { useState, useEffect } from 'react'

interface TopContentProps {
  period: string
  channel: string
}

type SortBy = 'comments' | 'sentiment' | 'medical'

interface ContentData {
  contentId: number
  videoId: string
  title: string
  channelName: string
  thumbnailUrl: string
  commentCount: number
  avgSentiment: number
  medicalQuestions: number
  topTopics: string[]
  publishedAt: string
}

export const TopContent: React.FC<TopContentProps> = ({ period, channel }) => {
  const [content, setContent] = useState<ContentData[]>([])
  const [sortBy, setSortBy] = useState<SortBy>('comments')
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true)
      setError(null)

      try {
        const params = new URLSearchParams({ period, channel, sortBy, limit: '10' })
        const response = await fetch(`/api/community/analytics/top-content?${params}`)

        if (!response.ok) throw new Error('Failed to fetch top content')

        const result = await response.json()
        setContent(result.content)
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error')
      } finally {
        setIsLoading(false)
      }
    }

    fetchData()
  }, [period, channel, sortBy])

  const getSentimentEmoji = (score: number): string => {
    if (score > 0.3) return '😊'
    if (score < -0.3) return '😟'
    return '😐'
  }

  const sortOptions: { value: SortBy; label: string }[] = [
    { value: 'comments', label: 'Interaktionen' },
    { value: 'sentiment', label: 'Sentiment' },
    { value: 'medical', label: 'Med. Anfragen' }
  ]

  return (
    <div className="top-content">
      <div className="top-content__header">
        <div className="top-content__sort">
          <label>Sortieren nach:</label>
          <select
            value={sortBy}
            onChange={(e) => setSortBy(e.target.value as SortBy)}
          >
            {sortOptions.map((opt) => (
              <option key={opt.value} value={opt.value}>{opt.label}</option>
            ))}
          </select>
        </div>
      </div>

      {isLoading && <div className="top-content__loading">Lade Content...</div>}

      {error && <div className="top-content__error">Fehler: {error}</div>}

      {!isLoading && !error && (
        <div className="top-content__list">
          {content.map((item, index) => (
            <div key={item.contentId} className="top-content__item">
              <span className="top-content__rank">{index + 1}</span>

              <div className="top-content__thumbnail">
                {item.thumbnailUrl ? (
                  <img src={item.thumbnailUrl} alt={item.title} />
                ) : (
                  <div className="top-content__thumbnail-placeholder">🎬</div>
                )}
              </div>

              <div className="top-content__info">
                <h4 className="top-content__title">{item.title}</h4>
                <span className="top-content__channel">{item.channelName}</span>
                <div className="top-content__topics">
                  {item.topTopics.slice(0, 3).map((topic, i) => (
                    <span key={i} className="top-content__topic">{topic}</span>
                  ))}
                </div>
              </div>

              <div className="top-content__stats">
                <div className="top-content__stat">
                  <span className="top-content__stat-value">{item.commentCount}</span>
                  <span className="top-content__stat-label">Interaktionen</span>
                </div>
                <div className="top-content__stat">
                  <span className="top-content__stat-value">
                    {getSentimentEmoji(item.avgSentiment)} {item.avgSentiment.toFixed(2)}
                  </span>
                  <span className="top-content__stat-label">Sentiment</span>
                </div>
                {item.medicalQuestions > 0 && (
                  <div className="top-content__stat top-content__stat--medical">
                    <span className="top-content__stat-value">⚕️ {item.medicalQuestions}</span>
                    <span className="top-content__stat-label">Med. Fragen</span>
                  </div>
                )}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  )
}
```

### TopicCloud.tsx

```tsx
// src/app/(payload)/admin/views/community/analytics/components/TopicCloud.tsx

'use client'

import React, { useState, useEffect } from 'react'

interface TopicCloudProps {
  period: string
  channel: string
}

interface TopicData {
  topic: string
  count: number
  avgSentiment: number
  channels: string[]
}

export const TopicCloud: React.FC<TopicCloudProps> = ({ period, channel }) => {
  const [topics, setTopics] = useState<TopicData[]>([])
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true)
      setError(null)

      try {
        const params = new URLSearchParams({ period, channel, limit: '30' })
        const response = await fetch(`/api/community/analytics/topic-cloud?${params}`)

        if (!response.ok) throw new Error('Failed to fetch topics')

        const result = await response.json()
        setTopics(result.topics)
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error')
      } finally {
        setIsLoading(false)
      }
    }

    fetchData()
  }, [period, channel])

  if (isLoading) {
    return <div className="topic-cloud-loading">Lade Themen...</div>
  }

  if (error) {
    return <div className="topic-cloud-error">Fehler: {error}</div>
  }

  if (topics.length === 0) {
    return <div className="topic-cloud-empty">Keine Themen gefunden</div>
  }

  // Calculate min/max for sizing
  const maxCount = Math.max(...topics.map(t => t.count))
  const minCount = Math.min(...topics.map(t => t.count))

  const getSize = (count: number): number => {
    if (maxCount === minCount) return 1
    const normalized = (count - minCount) / (maxCount - minCount)
    return 0.75 + (normalized * 1.25) // Scale from 0.75rem to 2rem
  }

  const getColor = (sentiment: number): string => {
    if (sentiment > 0.3) return '#10B981' // Green
    if (sentiment < -0.3) return '#EF4444' // Red
    return '#6B7280' // Gray
  }

  return (
    <div className="topic-cloud">
      <div className="topic-cloud__container">
        {topics.map((topic, index) => (
          <span
            key={index}
            className="topic-cloud__tag"
            style={{
              fontSize: `${getSize(topic.count)}rem`,
              color: getColor(topic.avgSentiment),
              opacity: 0.6 + (getSize(topic.count) / 2 * 0.4)
            }}
            title={`${topic.count} Erwähnungen | Sentiment: ${topic.avgSentiment.toFixed(2)}`}
          >
            {topic.topic}
          </span>
        ))}
      </div>

      <div className="topic-cloud__legend">
        <span className="topic-cloud__legend-item">
          <span style={{ color: '#10B981' }}>●</span> Positiv
        </span>
        <span className="topic-cloud__legend-item">
          <span style={{ color: '#6B7280' }}>●</span> Neutral
        </span>
        <span className="topic-cloud__legend-item">
          <span style={{ color: '#EF4444' }}>●</span> Negativ
        </span>
      </div>
    </div>
  )
}
```

---

## 4. SCSS Styling

Kopiere das SCSS aus der bestehenden Inbox-Implementierung und passe es an:
`src/app/(payload)/admin/views/community/inbox/inbox.scss`

Die grundlegende Struktur bleibt gleich, verwende CSS-Variablen von Payload UI:
- `var(--theme-elevation-X)` für Farben
- Responsive Breakpoints: 480px, 768px, 1024px, 1200px

---

## 5. API Endpoint Beispiel-Implementierung

### overview/route.ts

```typescript
// src/app/(payload)/api/community/analytics/overview/route.ts

import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'
import { subDays, differenceInHours } from 'date-fns'
import { createSafeLogger } from '@/lib/security'

const logger = createSafeLogger('API:AnalyticsOverview')

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url)
    const period = searchParams.get('period') || '30d'
    const channelId = searchParams.get('channel') || 'all'

    const payload = await getPayload({ config })

    // Authentifizierung prüfen
    const { user } = await payload.auth({ headers: request.headers })
    if (!user) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    // Calculate date ranges
    const now = new Date()
    let periodStart: Date
    let previousPeriodStart: Date
    let previousPeriodEnd: Date

    switch (period) {
      case '7d':
        periodStart = subDays(now, 7)
        previousPeriodStart = subDays(now, 14)
        previousPeriodEnd = subDays(now, 7)
        break
      case '90d':
        periodStart = subDays(now, 90)
        previousPeriodStart = subDays(now, 180)
        previousPeriodEnd = subDays(now, 90)
        break
      default: // 30d
        periodStart = subDays(now, 30)
        previousPeriodStart = subDays(now, 60)
        previousPeriodEnd = subDays(now, 30)
    }

    // Build where clause
    const baseWhere: Record<string, unknown> = {
      publishedAt: {
        greater_than_equal: periodStart.toISOString(),
      },
    }

    if (channelId !== 'all') {
      baseWhere.socialAccount = { equals: parseInt(channelId) }
    }

    // Fetch current period data
    const currentInteractions = await payload.find({
      collection: 'community-interactions',
      where: baseWhere,
      limit: 10000,
    })

    // Calculate metrics
    const docs = currentInteractions.docs
    const total = docs.length
    const newCount = docs.filter(i => i.status === 'new').length
    const repliedCount = docs.filter(i =>
      ['replied', 'resolved'].includes(i.status as string)
    ).length

    const responseRate = total > 0 ? (repliedCount / total) * 100 : 0

    // Calculate average response time
    const responseTimes = docs
      .filter(i => i.response?.sentAt && i.publishedAt)
      .map(i => differenceInHours(
        new Date(i.response!.sentAt as string),
        new Date(i.publishedAt as string)
      ))

    const avgResponseTime = responseTimes.length > 0
      ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
      : 0

    // Calculate sentiment (use sentimentScore, normalize confidence from 0-100 to 0-1)
    const sentimentScores = docs
      .filter(i => i.analysis?.sentimentScore != null)
      .map(i => {
        const score = i.analysis!.sentimentScore as number
        const confidence = ((i.analysis!.confidence as number) || 100) / 100
        return score * confidence
      })

    const avgSentiment = sentimentScores.length > 0
      ? sentimentScores.reduce((a, b) => a + b, 0) / sentimentScores.length
      : 0

    // Sentiment distribution (all 6 types)
    const sentimentDistribution = {
      positive: docs.filter(i => i.analysis?.sentiment === 'positive').length,
      neutral: docs.filter(i => i.analysis?.sentiment === 'neutral').length,
      negative: docs.filter(i => i.analysis?.sentiment === 'negative').length,
      question: docs.filter(i => i.analysis?.sentiment === 'question').length,
      gratitude: docs.filter(i => i.analysis?.sentiment === 'gratitude').length,
      frustration: docs.filter(i => i.analysis?.sentiment === 'frustration').length,
    }

    // Count flags
    const medicalQuestions = docs.filter(
      i => i.flags?.isMedicalQuestion
    ).length

    const escalations = docs.filter(
      i => i.flags?.requiresEscalation
    ).length

    // Fetch previous period for comparison
    const previousWhere: Record<string, unknown> = {
      publishedAt: {
        greater_than_equal: previousPeriodStart.toISOString(),
        less_than: previousPeriodEnd.toISOString(),
      },
    }

    if (channelId !== 'all') {
      previousWhere.socialAccount = { equals: parseInt(channelId) }
    }

    const previousInteractions = await payload.find({
      collection: 'community-interactions',
      where: previousWhere,
      limit: 10000,
    })

    const prevDocs = previousInteractions.docs
    const prevTotal = prevDocs.length
    const prevRepliedCount = prevDocs.filter(i =>
      ['replied', 'resolved'].includes(i.status as string)
    ).length
    const prevResponseRate = prevTotal > 0 ? (prevRepliedCount / prevTotal) * 100 : 0

    const prevSentimentScores = prevDocs
      .filter(i => i.analysis?.sentimentScore != null)
      .map(i => {
        const score = i.analysis!.sentimentScore as number
        const confidence = ((i.analysis!.confidence as number) || 100) / 100
        return score * confidence
      })

    const prevAvgSentiment = prevSentimentScores.length > 0
      ? prevSentimentScores.reduce((a, b) => a + b, 0) / prevSentimentScores.length
      : 0

    // Calculate comparisons
    const totalChange = prevTotal > 0
      ? ((total - prevTotal) / prevTotal) * 100
      : 0
    const responseRateChange = responseRate - prevResponseRate
    const sentimentChange = avgSentiment - prevAvgSentiment

    return NextResponse.json({
      period,
      totalInteractions: total,
      newInteractions: newCount,
      responseRate,
      avgResponseTimeHours: avgResponseTime,
      avgSentimentScore: avgSentiment,
      medicalQuestions,
      escalations,
      sentimentDistribution,
      comparison: {
        totalInteractions: Math.round(totalChange),
        responseRate: responseRateChange,
        avgSentimentScore: sentimentChange,
      },
    })
  } catch (error: unknown) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error'
    logger.error('Analytics overview error:', { error: errorMessage })
    return NextResponse.json(
      { error: 'Failed to fetch analytics' },
      { status: 500 }
    )
  }
}

export const dynamic = 'force-dynamic'
```

---

## 6. Dependencies installieren

```bash
pnpm add recharts date-fns
```

> **Hinweis:** Diese Dependencies sind NICHT installiert und müssen vor der Implementierung hinzugefügt werden.

---

## 7. Implementierungs-Reihenfolge

1. **Dependencies installieren** - `pnpm add recharts date-fns`
2. **Verzeichnisstruktur anlegen** - `/admin/views/community/analytics/`
3. **API Endpoints** (alle 6 implementieren)
4. **page.tsx** mit Haupt-Dashboard-Komponente
5. **KPICards** Komponente
6. **SentimentTrendChart** mit Recharts
7. **ResponseMetrics** Komponente
8. **ChannelComparison** Tabelle
9. **TopContent** Liste
10. **TopicCloud** Tag-Wolke
11. **SCSS Styling** mit BEM-Konvention
12. **Mobile Responsive** Testing

---

## 8. Testfälle

Nach Implementierung verifizieren:

- [ ] Dashboard lädt unter `/admin/community/analytics`
- [ ] Period-Filter (7d/30d/90d) aktualisiert alle Komponenten
- [ ] Channel-Filter funktioniert korrekt
- [ ] KPI-Vergleiche zeigen korrekte Trends
- [ ] Sentiment-Chart rendert alle 6 Sentiment-Typen
- [ ] Response-Metriken zeigen alle 7 Status-Typen
- [ ] Channel-Vergleich zeigt alle aktiven SocialAccounts
- [ ] Top-Content-Sortierung funktioniert
- [ ] Topic-Cloud skaliert korrekt
- [ ] Mobile Layout funktioniert (< 768px)
- [ ] Tablet Layout funktioniert (768px - 1024px)
- [ ] Leere Daten werden graceful gehandelt
- [ ] API-Fehler zeigen User-Feedback
- [ ] Loading States erscheinen während Datenabruf

---

## 9. Hinweise

1. **Keine externen API-Calls** – alle Daten kommen aus der PostgreSQL-Datenbank

2. **Payload Local API verwenden** für Datenbankabfragen:
   ```typescript
   import { getPayload } from 'payload'
   import config from '@payload-config'

   const payload = await getPayload({ config })
   const interactions = await payload.find({
     collection: 'community-interactions',
     where: { ... },
     limit: 10000,
   })
   ```

3. **date-fns** für Datumsberechnungen verwenden (subDays, differenceInHours, etc.)

4. **Caching** – API-Responses können für 5 Minuten gecacht werden:
   ```typescript
   return NextResponse.json(data, {
     headers: {
       'Cache-Control': 'private, max-age=300',
     },
   })
   ```

5. **Performance**: Bei großen Datenmengen (>10k Interactions) ggf. direkte SQL-Aggregation über Drizzle ORM verwenden

6. **Authentifizierung**: Alle API-Endpoints müssen User-Authentifizierung prüfen wie in `/api/community/stats`

7. **Sentiment-Werte beachten**:
   - `sentiment` ist ein String mit 6 möglichen Werten
   - `sentimentScore` ist numerisch (-1 bis +1)
   - `confidence` ist 0-100 (Prozent), nicht 0-1!

8. **Status-Werte beachten**: 7 verschiedene Status-Werte, nicht 6

---

## 10. Deliverables

Nach Abschluss solltest du haben:

1. ✅ Alle 6 API-Endpoints funktionsfähig
2. ✅ Dashboard-View im Admin-Panel unter `/admin/community/analytics`
3. ✅ Alle 6 Komponenten implementiert
4. ✅ SCSS vollständig mit BEM-Konvention
5. ✅ Mobile-responsiv getestet (3 Breakpoints)
6. ✅ Keine TypeScript-Fehler
7. ✅ Keine Console-Errors
8. ✅ Loading & Error States für alle Komponenten

---

**Beginne mit den Dependencies und API-Endpoints und arbeite dich zur UI vor.**

Bei Fragen oder Unklarheiten: Implementiere erst die Grundfunktion, Feinschliff kann danach erfolgen.
