'use client' import React, { useState, useEffect, useCallback, useRef } from 'react' interface Interaction { id: number platform: { name: string; icon: string; slug: string } socialAccount: { displayName: string } linkedContent?: { title: string; youtubeVideoId: string } type: string externalId: string author: { name: string handle: string avatarUrl: string isVerified: boolean isSubscriber: boolean subscriberCount: number } message: string publishedAt: string analysis: { sentiment: string sentimentScore: number confidence: number topics: { topic: string }[] suggestedReply: string } flags: { isMedicalQuestion: boolean requiresEscalation: boolean isSpam: boolean isFromInfluencer: boolean } status: string priority: string assignedTo?: { id: number; email: string; name?: string } response?: { text: string sentAt: string sentBy: { email: string } } engagement: { likes: number replies: number isHearted: boolean isPinned: boolean } createdAt: string updatedAt: string } interface Filters { status: string[] priority: string[] platform: string[] channel: string[] sentiment: string[] flags: string[] assignedTo: string search: string dateFrom: string dateTo: string } const DEFAULT_FILTERS: Filters = { status: ['new', 'in_review', 'waiting'], priority: [], platform: [], channel: [], sentiment: [], flags: [], assignedTo: '', search: '', dateFrom: '', dateTo: '', } export const CommunityInbox: React.FC = () => { // State const [interactions, setInteractions] = useState([]) const [selectedInteraction, setSelectedInteraction] = useState(null) const [filters, setFilters] = useState(DEFAULT_FILTERS) const [loading, setLoading] = useState(true) const [syncing, setSyncing] = useState(false) const [replyText, setReplyText] = useState('') const [sendingReply, setSendingReply] = useState(false) const [exporting, setExporting] = useState(false) // Mobile state const [isMobile, setIsMobile] = useState(false) const [mobileView, setMobileView] = useState<'list' | 'detail' | 'filters'>('list') // Export state const [showExportModal, setShowExportModal] = useState(false) const [exportFormat, setExportFormat] = useState<'pdf' | 'excel' | 'csv'>('excel') const [exportDateRange, setExportDateRange] = useState({ from: '', to: '' }) // AI Reply state const [isGeneratingReply, setIsGeneratingReply] = useState(false) const [generatedReplies, setGeneratedReplies] = useState>([]) // Sync status state const [syncStatus, setSyncStatus] = useState<{ isRunning: boolean lastRunAt: string | null nextRunAt: string | null } | null>(null) // Refs const listRef = useRef(null) // Metadata for filters const [platforms, setPlatforms] = useState([]) const [channels, setChannels] = useState([]) const [templates, setTemplates] = useState([]) const [users, setUsers] = useState([]) // Stats const [stats, setStats] = useState({ total: 0, new: 0, urgent: 0, medical: 0, unassigned: 0, }) // Detect mobile useEffect(() => { const checkMobile = () => { setIsMobile(window.innerWidth < 768) } checkMobile() window.addEventListener('resize', checkMobile) return () => window.removeEventListener('resize', checkMobile) }, []) // Load metadata useEffect(() => { const loadMetadata = async () => { try { const [platformsRes, channelsRes, templatesRes, usersRes] = await Promise.all([ fetch('/api/social-platforms?limit=100'), fetch('/api/youtube-channels?limit=100'), fetch('/api/community-templates?where[isActive][equals]=true&limit=100'), fetch('/api/users?where[communityRole][not_equals]=none&limit=100'), ]) const [platformsData, channelsData, templatesData, usersData] = await Promise.all([ platformsRes.json(), channelsRes.json(), templatesRes.json(), usersRes.json(), ]) setPlatforms(platformsData.docs || []) setChannels(channelsData.docs || []) setTemplates(templatesData.docs || []) setUsers(usersData.docs || []) } catch (error) { console.error('Failed to load metadata:', error) } } loadMetadata() }, []) // Load interactions const loadInteractions = useCallback(async () => { setLoading(true) try { const params = new URLSearchParams() params.set('limit', '50') params.set('sort', '-createdAt') params.set('depth', '2') // Apply filters if (filters.status.length > 0) { params.set('where[status][in]', filters.status.join(',')) } if (filters.priority.length > 0) { params.set('where[priority][in]', filters.priority.join(',')) } if (filters.platform.length > 0) { params.set('where[platform][in]', filters.platform.join(',')) } if (filters.sentiment.length > 0) { params.set('where[analysis.sentiment][in]', filters.sentiment.join(',')) } if (filters.flags.includes('medical')) { params.set('where[flags.isMedicalQuestion][equals]', 'true') } if (filters.flags.includes('escalation')) { params.set('where[flags.requiresEscalation][equals]', 'true') } if (filters.flags.includes('influencer')) { params.set('where[flags.isFromInfluencer][equals]', 'true') } if (filters.assignedTo) { if (filters.assignedTo === 'unassigned') { params.set('where[assignedTo][exists]', 'false') } else { params.set('where[assignedTo][equals]', filters.assignedTo) } } if (filters.search) { params.set('where[or][0][message][contains]', filters.search) params.set('where[or][1][author.name][contains]', filters.search) } if (filters.dateFrom) { params.set('where[publishedAt][greater_than_equal]', filters.dateFrom) } if (filters.dateTo) { params.set('where[publishedAt][less_than_equal]', filters.dateTo) } const response = await fetch(`/api/community-interactions?${params}`) const data = await response.json() setInteractions(data.docs || []) // Update stats const statsResponse = await fetch('/api/community-interactions/stats') if (statsResponse.ok) { const statsData = await statsResponse.json() setStats(statsData) } } catch (error) { console.error('Failed to load interactions:', error) } finally { setLoading(false) } }, [filters]) useEffect(() => { loadInteractions() }, [loadInteractions]) // Fetch Sync Status useEffect(() => { const fetchSyncStatus = async () => { try { const response = await fetch('/api/community/sync') if (response.ok) { const status = await response.json() setSyncStatus(status) } } catch (error) { console.error('Failed to fetch sync status:', error) } } fetchSyncStatus() const interval = setInterval(fetchSyncStatus, 60000) // Jede Minute return () => clearInterval(interval) }, []) // Sync comments (all accounts) const handleSync = async () => { setSyncing(true) try { const response = await fetch('/api/community/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, }) if (response.ok) { const result = await response.json() if (result.success && result.results) { const totalNew = result.results.reduce((sum: number, r: { newComments: number }) => sum + r.newComments, 0) const totalUpdated = result.results.reduce((sum: number, r: { updatedComments: number }) => sum + r.updatedComments, 0) alert(`Sync abgeschlossen: ${totalNew} neue, ${totalUpdated} aktualisierte Kommentare`) } loadInteractions() // Refresh sync status const statusResponse = await fetch('/api/community/sync') if (statusResponse.ok) { const status = await statusResponse.json() setSyncStatus(status) } } else { const error = await response.json() throw new Error(error.error || 'Sync failed') } } catch (error) { console.error('Sync error:', error) alert('Sync fehlgeschlagen. Details in der Console.') } finally { setSyncing(false) } } // Export function const handleExport = async () => { setExporting(true) try { const params = new URLSearchParams() params.set('format', exportFormat) if (exportDateRange.from) { params.set('dateFrom', exportDateRange.from) } if (exportDateRange.to) { params.set('dateTo', exportDateRange.to) } if (filters.status.length > 0) { params.set('status', filters.status.join(',')) } if (filters.platform.length > 0) { params.set('platform', filters.platform.join(',')) } if (filters.sentiment.length > 0) { params.set('sentiment', filters.sentiment.join(',')) } const response = await fetch(`/api/community/export?${params}`) if (!response.ok) { throw new Error('Export failed') } const blob = await response.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url const extension = exportFormat === 'excel' ? 'xlsx' : exportFormat const timestamp = new Date().toISOString().split('T')[0] a.download = `community-report-${timestamp}.${extension}` document.body.appendChild(a) a.click() document.body.removeChild(a) window.URL.revokeObjectURL(url) setShowExportModal(false) } catch (error) { console.error('Export error:', error) alert('Export fehlgeschlagen') } finally { setExporting(false) } } // Update interaction status const updateStatus = async (id: number, status: string) => { try { await fetch(`/api/community-interactions/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }), }) setInteractions((prev) => prev.map((i) => (i.id === id ? { ...i, status } : i))) if (selectedInteraction?.id === id) { setSelectedInteraction((prev) => (prev ? { ...prev, status } : null)) } } catch (error) { console.error('Failed to update status:', error) } } // Assign interaction const assignTo = async (id: number, userId: string) => { try { await fetch(`/api/community-interactions/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ assignedTo: userId || null }), }) loadInteractions() } catch (error) { console.error('Failed to assign:', error) } } // Send reply const sendReply = async () => { if (!selectedInteraction || !replyText.trim()) return setSendingReply(true) try { const response = await fetch('/api/community/reply', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ interactionId: selectedInteraction.id, text: replyText, }), }) if (response.ok) { setReplyText('') updateStatus(selectedInteraction.id, 'replied') loadInteractions() alert('Antwort gesendet!') } else { throw new Error('Reply failed') } } catch (error) { console.error('Reply error:', error) alert('Antwort konnte nicht gesendet werden.') } finally { setSendingReply(false) } } // Apply template const applyTemplate = (template: any) => { let text = template.template if (selectedInteraction) { text = text.replace(/\{\{author_name\}\}/g, selectedInteraction.author?.name || 'there') text = text.replace(/\{\{video_title\}\}/g, selectedInteraction.linkedContent?.title || '') text = text.replace( /\{\{channel_name\}\}/g, selectedInteraction.socialAccount?.displayName || '', ) } setReplyText(text) } // Generate AI reply const generateAIReply = async (variants: boolean = false) => { if (!selectedInteraction) return setIsGeneratingReply(true) setGeneratedReplies([]) try { const response = await fetch('/api/community/generate-reply', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ interactionId: selectedInteraction.id, variants, }), }) if (response.ok) { const data = await response.json() if (variants && data.variants) { setGeneratedReplies(data.variants) } else if (data.reply) { // Einzelne Antwort direkt übernehmen setReplyText(data.reply.text) } } else { throw new Error('Generation failed') } } catch (error) { console.error('AI generation error:', error) alert('KI-Antwortgenerierung fehlgeschlagen') } finally { setIsGeneratingReply(false) } } // Use generated reply const applyGeneratedReply = (reply: { text: string }) => { setReplyText(reply.text) setGeneratedReplies([]) } // Select interaction const selectInteraction = (interaction: Interaction) => { setSelectedInteraction(interaction) if (interaction.status === 'new') { updateStatus(interaction.id, 'in_review') } if (isMobile) { setMobileView('detail') } } // Mobile back navigation const handleMobileBack = () => { if (mobileView === 'detail') { setMobileView('list') setSelectedInteraction(null) } else if (mobileView === 'filters') { setMobileView('list') } } // Render helpers const getSentimentEmoji = (sentiment: string) => { switch (sentiment) { case 'positive': return '😊' case 'negative': return '😟' case 'mixed': return '😐' case 'question': return '❓' default: return '•' } } const getPriorityColor = (priority: string) => { switch (priority) { case 'urgent': return '#dc2626' case 'high': return '#f97316' case 'normal': return '#3b82f6' case 'low': return '#22c55e' default: return '#6b7280' } } const getStatusBadge = (status: string) => { const badges: Record = { new: { label: 'Neu', color: '#3b82f6' }, in_review: { label: 'In Review', color: '#f97316' }, waiting: { label: 'Warten auf Info', color: '#8b5cf6' }, replied: { label: 'Beantwortet', color: '#22c55e' }, resolved: { label: 'Erledigt', color: '#10b981' }, archived: { label: 'Archiviert', color: '#1f2937' }, spam: { label: 'Spam', color: '#dc2626' }, } return badges[status] || { label: status, color: '#6b7280' } } const formatDate = (dateString: string) => { const date = new Date(dateString) const now = new Date() const diffMs = now.getTime() - date.getTime() const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMs / 3600000) const diffDays = Math.floor(diffMs / 86400000) if (diffMins < 60) return `${diffMins}m` if (diffHours < 24) return `${diffHours}h` if (diffDays < 7) return `${diffDays}d` return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) } const activeFiltersCount = [ filters.status.length !== 3 ? 1 : 0, filters.priority.length > 0 ? 1 : 0, filters.platform.length > 0 ? 1 : 0, filters.sentiment.length > 0 ? 1 : 0, filters.flags.length > 0 ? 1 : 0, filters.assignedTo ? 1 : 0, filters.search ? 1 : 0, ].reduce((a, b) => a + b, 0) // MOBILE RENDER if (isMobile) { return (
{/* Mobile Header */}
{mobileView === 'list' ? ( <>

📬 Inbox

📈
) : ( <> {mobileView === 'detail' ? 'Details' : 'Filter'} )}
{/* Mobile Stats */} {mobileView === 'list' && (
{stats.new} Neu
{stats.urgent} Dringend
{stats.medical} Medizin
)} {/* Mobile List */} {mobileView === 'list' && (
{loading ? (
Lade...
) : interactions.length === 0 ? (
Keine Interaktionen gefunden
) : (
    {interactions.map((interaction) => (
  • selectInteraction(interaction)} >
    {interaction.platform?.icon} {interaction.author?.name} {formatDate(interaction.publishedAt)}

    {interaction.message?.substring(0, 80)}...

    {interaction.analysis?.sentiment && ( {getSentimentEmoji(interaction.analysis.sentiment)} )} {interaction.flags?.isMedicalQuestion && ( ⚕️ )} {interaction.flags?.requiresEscalation && ( 🚨 )} {getStatusBadge(interaction.status).label}
  • ))}
)}
)} {/* Mobile Detail View */} {mobileView === 'detail' && selectedInteraction && (
{selectedInteraction.author?.avatarUrl && ( )}

{selectedInteraction.author?.name} {selectedInteraction.author?.isVerified && ' ✓'}

{selectedInteraction.author?.handle} {selectedInteraction.author?.subscriberCount > 0 && ` • ${selectedInteraction.author.subscriberCount.toLocaleString()} Follower`}

{(selectedInteraction.flags?.isMedicalQuestion || selectedInteraction.flags?.requiresEscalation) && (
{selectedInteraction.flags.isMedicalQuestion && (
⚕️ Medizinische Frage – Auf Hotline verweisen!
)} {selectedInteraction.flags.requiresEscalation && (
🚨 Eskalation erforderlich
)}
)} {selectedInteraction.linkedContent && ( 📺 {selectedInteraction.linkedContent.title} )}

{selectedInteraction.message}

{formatDate(selectedInteraction.publishedAt)} ❤️ {selectedInteraction.engagement?.likes || 0} 💬 {selectedInteraction.engagement?.replies || 0}
{selectedInteraction.analysis && (

🤖 KI-Analyse

Sentiment: {getSentimentEmoji(selectedInteraction.analysis.sentiment)}{' '} {selectedInteraction.analysis.sentiment}
{selectedInteraction.analysis.topics?.length > 0 && (
Themen:
{selectedInteraction.analysis.topics.map((t, i) => ( {t.topic} ))}
)}
)} {selectedInteraction.response?.text && (

✅ Unsere Antwort

{selectedInteraction.response.text}

{formatDate(selectedInteraction.response.sentAt)}
)} {!['resolved', 'archived', 'spam'].includes(selectedInteraction.status) && !selectedInteraction.response?.text && (