'use client' import React, { useState, useEffect, useCallback } from 'react' import './YouTubeAnalyticsDashboard.scss' // Types type Tab = 'performance' | 'pipeline' | 'goals' | 'community' type Period = '7d' | '30d' | '90d' interface Channel { id: number name: string slug: string } interface VideoSummary { id: number title: string thumbnailText: string views: number ctr: number avgRetention: number likes: number } interface PerformanceData { stats: { totalViews: number totalWatchTimeHours: number avgCtr: number subscribersGained: number } comparison: { views: number watchTime: number subscribers: number } topVideos: VideoSummary[] bottomVideos: VideoSummary[] engagement: { likes: number comments: number shares: number } } interface PipelineData { stats: { inProduction: number thisWeekCount: number overdueTasksCount: number pendingApprovals: number } pipeline: Record thisWeekVideos: Array<{ id: number title: string status: string scheduledPublishDate: string channel: string }> overdueTasks: Array<{ id: number title: string dueDate: string assignedTo: string video: string | null }> } interface GoalsData { stats: { contentProgress: number contentLabel: string subscriberProgress: number subscriberLabel: string viewsProgress: number viewsLabel: string overall: number } goals: any[] customGoals: Array<{ metric: string target: string current: string status: string }> } interface CommunityData { stats: { unresolvedCount: number positivePct: number avgResponseHours: number escalationsCount: number } sentiment: Record topTopics: Array<{ topic: string count: number }> recentUnresolved: Array<{ id: number message: string authorName: string authorHandle: string publishedAt: string platform: string sentiment: string }> } interface ApiResponse { tab: string channel: string period: string channels: Channel[] data: PerformanceData | PipelineData | GoalsData | CommunityData } const tabConfig: Array<{ key: Tab; label: string; icon: React.ReactNode }> = [ { key: 'performance', label: 'Performance', icon: ( ), }, { key: 'pipeline', label: 'Pipeline', icon: ( ), }, { key: 'goals', label: 'Ziele', icon: ( ), }, { key: 'community', label: 'Community', icon: ( ), }, ] const periodLabels: Record = { '7d': 'Letzte 7 Tage', '30d': 'Letzte 30 Tage', '90d': 'Letzte 90 Tage', } const statusLabels: Record = { idea: 'Idee', script_draft: 'Skript', script_review: 'Review', script_approved: 'Skript', shoot_scheduled: 'Produktion', shot: 'Produktion', rough_cut: 'Schnitt', fine_cut: 'Schnitt', final_review: 'Review', approved: 'Bereit', upload_scheduled: 'Bereit', published: 'Veröffentlicht', } const formatNumber = (n: number): string => { if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M` if (n >= 1000) return `${(n / 1000).toFixed(1)}K` return n.toString() } const formatDate = (dateString: string): string => { return new Date(dateString).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }) } const renderTrend = (value: number): React.ReactNode => { if (value === 0) return null const isUp = value > 0 return ( {isUp ? ( ) : ( )} {Math.abs(value)}% ) } export const YouTubeAnalyticsDashboard: React.FC = () => { const [activeTab, setActiveTab] = useState('performance') const [period, setPeriod] = useState('30d') const [channel, setChannel] = useState('all') const [channels, setChannels] = useState([]) const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const fetchData = useCallback(async () => { setLoading(true) setError(null) try { const response = await fetch( `/api/youtube/analytics?tab=${activeTab}&channel=${channel}&period=${period}`, { credentials: 'include', }, ) if (!response.ok) { throw new Error(`API returned ${response.status}`) } const result: ApiResponse = await response.json() if (result.channels && result.channels.length > 0) { setChannels(result.channels) } setData(result.data) } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Laden der Daten') } finally { setLoading(false) } }, [activeTab, channel, period]) useEffect(() => { fetchData() }, [fetchData]) const renderPerformance = () => { const perfData = data as PerformanceData if (!perfData?.stats) return null return ( <>
Aufrufe
{formatNumber(perfData.stats.totalViews)} {renderTrend(perfData.comparison.views)}
Wiedergabezeit
{formatNumber(perfData.stats.totalWatchTimeHours)}h {renderTrend(perfData.comparison.watchTime)}
CTR
{perfData.stats.avgCtr.toFixed(1)}%
Neue Abos
{formatNumber(perfData.stats.subscribersGained)} {renderTrend(perfData.comparison.subscribers)}
{perfData.topVideos && perfData.topVideos.length > 0 && (

Top 5 Videos

{perfData.topVideos.map((video) => (
{video.title}
{formatNumber(video.views)} Aufrufe
{video.ctr.toFixed(1)}% CTR
{video.avgRetention.toFixed(0)}% Retention
))}
)} {perfData.bottomVideos && perfData.bottomVideos.length > 0 && (

Schwächste Videos

{perfData.bottomVideos.map((video) => (
{video.title}
{formatNumber(video.views)} Aufrufe
{video.ctr.toFixed(1)}% CTR
{video.avgRetention.toFixed(0)}% Retention
))}
)}

Engagement

{formatNumber(perfData.engagement.likes)}
Likes
{formatNumber(perfData.engagement.comments)}
Kommentare
{formatNumber(perfData.engagement.shares)}
Shares
) } const renderPipeline = () => { const pipeData = data as PipelineData if (!pipeData?.pipeline) return null // Group statuses for pipeline bar const statusGroups: Record = { idea: { label: 'Idee', className: 'idea' }, script: { label: 'Skript', className: 'script' }, review: { label: 'Review', className: 'review' }, production: { label: 'Produktion', className: 'production' }, editing: { label: 'Schnitt', className: 'editing' }, ready: { label: 'Bereit', className: 'ready' }, published: { label: 'Veröffentlicht', className: 'published' }, } const grouped = { idea: (pipeData.pipeline.idea || 0), script: (pipeData.pipeline.script_draft || 0) + (pipeData.pipeline.script_approved || 0), review: (pipeData.pipeline.script_review || 0) + (pipeData.pipeline.final_review || 0), production: (pipeData.pipeline.shoot_scheduled || 0) + (pipeData.pipeline.shot || 0), editing: (pipeData.pipeline.rough_cut || 0) + (pipeData.pipeline.fine_cut || 0), ready: (pipeData.pipeline.approved || 0) + (pipeData.pipeline.upload_scheduled || 0), published: (pipeData.pipeline.published || 0), } const total = Object.values(grouped).reduce((sum, val) => sum + val, 0) return ( <>
In Produktion
{pipeData.stats.inProduction}
Diese Woche
{pipeData.stats.thisWeekCount}
Überfällige Tasks
{pipeData.stats.overdueTasksCount}
Offene Freigaben
{pipeData.stats.pendingApprovals}

Pipeline Übersicht

{Object.entries(statusGroups).map(([key, { label, className }]) => (
{label}
))}
{Object.entries(grouped).map(([key, count]) => { const percentage = total > 0 ? (count / total) * 100 : 0 const { label, className } = statusGroups[key] return count > 0 ? (
{percentage > 5 && `${count}`}
) : null })}
{pipeData.thisWeekVideos && pipeData.thisWeekVideos.length > 0 && (

Nächste Veröffentlichungen

{pipeData.thisWeekVideos.map((video) => (
{video.title}
{formatDate(video.scheduledPublishDate)} • {video.channel}
))}
)} {pipeData.overdueTasks && pipeData.overdueTasks.length > 0 && (

Überfällige Aufgaben

{pipeData.overdueTasks.map((task) => (
{task.title}
Fällig: {formatDate(task.dueDate)}
{task.assignedTo} {task.video && ` • ${task.video}`}
))}
)} ) } const renderGoals = () => { const goalsData = data as GoalsData if (!goalsData?.stats) return null return ( <>
Content
{goalsData.stats.contentLabel}
Abonnenten
{goalsData.stats.subscriberLabel}
Aufrufe
{goalsData.stats.viewsLabel}
Gesamt
{goalsData.stats.overall.toFixed(0)}%

Fortschritt

Content
{goalsData.stats.contentLabel}
Abonnenten
{goalsData.stats.subscriberLabel}
Aufrufe
{goalsData.stats.viewsLabel}
{goalsData.customGoals && goalsData.customGoals.length > 0 && (

Benutzerdefinierte Ziele

{goalsData.customGoals.map((goal, index) => (
{goal.metric}
{goal.current} / {goal.target}
{goal.status === 'on_track' && 'Im Plan'} {goal.status === 'at_risk' && 'Gefährdet'} {goal.status === 'achieved' && 'Erreicht'} {goal.status === 'missed' && 'Verfehlt'}
))}
)} ) } const renderCommunity = () => { const commData = data as CommunityData if (!commData?.stats) return null const sentimentMax = Math.max(...Object.values(commData.sentiment)) return ( <>
Unbearbeitet
{commData.stats.unresolvedCount}
Positiv
{commData.stats.positivePct.toFixed(0)}%
Antwortzeit
{commData.stats.avgResponseHours.toFixed(1)}h
Eskalationen
{commData.stats.escalationsCount}

Sentiment

{Object.entries(commData.sentiment).map(([sentiment, count]) => { const percentage = sentimentMax > 0 ? (count / sentimentMax) * 100 : 0 return (
{sentiment === 'positive' && 'Positiv'} {sentiment === 'neutral' && 'Neutral'} {sentiment === 'negative' && 'Negativ'} {sentiment === 'question' && 'Frage'}
{count}
) })}
{commData.topTopics && commData.topTopics.length > 0 && (

Top-Themen

{commData.topTopics.map((topic, index) => (
{topic.topic} {topic.count}
))}
)} {commData.recentUnresolved && commData.recentUnresolved.length > 0 && (

Neueste unbearbeitete Kommentare

{commData.recentUnresolved.map((comment) => (
{comment.authorName} (@{comment.authorHandle})
{comment.platform} • {formatDate(comment.publishedAt)}
{comment.message}
))}
)} ) } return (

YouTube Analytics

Übersicht über Performance, Pipeline, Ziele und Community

{tabConfig.map((tab) => ( ))}
{loading && !data && (

Lade Daten...

)} {error && (

{error}

)} {!loading && !error && data && (
{activeTab === 'performance' && renderPerformance()} {activeTab === 'pipeline' && renderPipeline()} {activeTab === 'goals' && renderGoals()} {activeTab === 'community' && renderCommunity()}
)}
) } export default YouTubeAnalyticsDashboard