cms.c2sgmbh/src/app/(payload)/admin/views/community/inbox/CommunityInbox.tsx
Martin Porwoll d94db78aec fix: resolve all ESLint errors for clean CI pipeline
- Extend admin component overrides to cover all Payload admin views
  (no-html-link-for-pages, no-img-element off for admin panel)
- Rename useGeneratedReply to applyGeneratedReply (not a hook)
- Fix useRealtimeUpdates: resolve circular dependency with connectRef,
  wrap ref assignments in useEffect for React 19 compiler compliance
- Fix MetaBaseClient: let -> const for single-assignment variable

ESLint now passes with 0 errors (68 warnings only).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 22:24:12 +00:00

1655 lines
60 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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<Interaction[]>([])
const [selectedInteraction, setSelectedInteraction] = useState<Interaction | null>(null)
const [filters, setFilters] = useState<Filters>(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<Array<{
text: string
tone: string
confidence: number
warnings: string[]
}>>([])
// Sync status state
const [syncStatus, setSyncStatus] = useState<{
isRunning: boolean
lastRunAt: string | null
nextRunAt: string | null
} | null>(null)
// Refs
const listRef = useRef<HTMLUListElement>(null)
// Metadata for filters
const [platforms, setPlatforms] = useState<any[]>([])
const [channels, setChannels] = useState<any[]>([])
const [templates, setTemplates] = useState<any[]>([])
const [users, setUsers] = useState<any[]>([])
// 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<string, { label: string; color: string }> = {
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 (
<div className="community-inbox mobile">
{/* Mobile Header */}
<div className="mobile-header">
{mobileView === 'list' ? (
<>
<h1>📬 Inbox</h1>
<div className="mobile-header-actions">
<a href="/admin/views/community/analytics" className="btn-icon">
📈
</a>
<button className="btn-icon" onClick={() => setMobileView('filters')}>
🎛 {activeFiltersCount > 0 && <span className="filter-badge">{activeFiltersCount}</span>}
</button>
<button className="btn-icon" onClick={() => setShowExportModal(true)}>
📊
</button>
<button className="btn-icon" onClick={handleSync} disabled={syncing}>
{syncing ? '⏳' : '🔄'}
</button>
</div>
</>
) : (
<>
<button className="btn-back" onClick={handleMobileBack}>
Zurück
</button>
<span className="mobile-title">{mobileView === 'detail' ? 'Details' : 'Filter'}</span>
</>
)}
</div>
{/* Mobile Stats */}
{mobileView === 'list' && (
<div className="mobile-stats">
<div className="stat">
<strong>{stats.new}</strong>
<small>Neu</small>
</div>
<div className="stat urgent">
<strong>{stats.urgent}</strong>
<small>Dringend</small>
</div>
<div className="stat medical">
<strong>{stats.medical}</strong>
<small>Medizin</small>
</div>
</div>
)}
{/* Mobile List */}
{mobileView === 'list' && (
<div className="mobile-list">
{loading ? (
<div className="loading">Lade...</div>
) : interactions.length === 0 ? (
<div className="empty-state">Keine Interaktionen gefunden</div>
) : (
<ul>
{interactions.map((interaction) => (
<li
key={interaction.id}
className={`mobile-item ${interaction.status === 'new' ? 'unread' : ''}`}
onClick={() => selectInteraction(interaction)}
>
<div className="item-row">
<span
className="priority-bar"
style={{ backgroundColor: getPriorityColor(interaction.priority) }}
/>
<div className="item-content">
<div className="item-header">
<span className="author">
{interaction.platform?.icon} {interaction.author?.name}
</span>
<span className="time">{formatDate(interaction.publishedAt)}</span>
</div>
<p className="message">{interaction.message?.substring(0, 80)}...</p>
<div className="item-badges">
{interaction.analysis?.sentiment && (
<span className="badge">
{getSentimentEmoji(interaction.analysis.sentiment)}
</span>
)}
{interaction.flags?.isMedicalQuestion && (
<span className="badge medical"></span>
)}
{interaction.flags?.requiresEscalation && (
<span className="badge urgent">🚨</span>
)}
<span
className="badge status"
style={{ backgroundColor: getStatusBadge(interaction.status).color }}
>
{getStatusBadge(interaction.status).label}
</span>
</div>
</div>
<span className="chevron"></span>
</div>
</li>
))}
</ul>
)}
</div>
)}
{/* Mobile Detail View */}
{mobileView === 'detail' && selectedInteraction && (
<div className="mobile-detail">
<div className="author-card">
{selectedInteraction.author?.avatarUrl && (
<img src={selectedInteraction.author.avatarUrl} alt="" className="avatar" />
)}
<div className="author-info">
<h3>
{selectedInteraction.author?.name}
{selectedInteraction.author?.isVerified && ' ✓'}
</h3>
<p>
{selectedInteraction.author?.handle}
{selectedInteraction.author?.subscriberCount > 0 &&
`${selectedInteraction.author.subscriberCount.toLocaleString()} Follower`}
</p>
</div>
</div>
<div className="quick-actions">
<select
value={selectedInteraction.status}
onChange={(e) => updateStatus(selectedInteraction.id, e.target.value)}
>
<option value="new">🆕 Neu</option>
<option value="in_review">👁 In Review</option>
<option value="waiting"> Warten auf Info</option>
<option value="replied"> Beantwortet</option>
<option value="resolved"> Erledigt</option>
<option value="archived">📦 Archiviert</option>
<option value="spam">🚫 Spam</option>
</select>
<select
value={String(selectedInteraction.assignedTo?.id || '')}
onChange={(e) => assignTo(selectedInteraction.id, e.target.value)}
>
<option value="">Nicht zugewiesen</option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.name || user.email}
</option>
))}
</select>
</div>
{(selectedInteraction.flags?.isMedicalQuestion ||
selectedInteraction.flags?.requiresEscalation) && (
<div className="mobile-alerts">
{selectedInteraction.flags.isMedicalQuestion && (
<div className="alert medical"> Medizinische Frage Auf Hotline verweisen!</div>
)}
{selectedInteraction.flags.requiresEscalation && (
<div className="alert urgent">🚨 Eskalation erforderlich</div>
)}
</div>
)}
{selectedInteraction.linkedContent && (
<a
href={`https://youtube.com/watch?v=${selectedInteraction.linkedContent.youtubeVideoId}`}
target="_blank"
rel="noopener noreferrer"
className="context-link"
>
📺 {selectedInteraction.linkedContent.title}
</a>
)}
<div className="message-card">
<p>{selectedInteraction.message}</p>
<div className="message-meta">
<span>{formatDate(selectedInteraction.publishedAt)}</span>
<span> {selectedInteraction.engagement?.likes || 0}</span>
<span>💬 {selectedInteraction.engagement?.replies || 0}</span>
</div>
</div>
{selectedInteraction.analysis && (
<div className="analysis-card">
<h4>🤖 KI-Analyse</h4>
<div className="analysis-row">
<span>Sentiment:</span>
<span>
{getSentimentEmoji(selectedInteraction.analysis.sentiment)}{' '}
{selectedInteraction.analysis.sentiment}
</span>
</div>
{selectedInteraction.analysis.topics?.length > 0 && (
<div className="analysis-row">
<span>Themen:</span>
<div className="topics">
{selectedInteraction.analysis.topics.map((t, i) => (
<span key={i} className="topic">
{t.topic}
</span>
))}
</div>
</div>
)}
</div>
)}
{selectedInteraction.response?.text && (
<div className="response-card">
<h4> Unsere Antwort</h4>
<p>{selectedInteraction.response.text}</p>
<small>{formatDate(selectedInteraction.response.sentAt)}</small>
</div>
)}
{!['resolved', 'archived', 'spam'].includes(selectedInteraction.status) &&
!selectedInteraction.response?.text && (
<div className="reply-section">
<div className="reply-header">
<select
onChange={(e) => {
const template = templates.find((t) => t.id === Number(e.target.value))
if (template) applyTemplate(template)
}}
defaultValue=""
>
<option value="">📝 Template...</option>
{templates.map((template) => (
<option key={template.id} value={template.id}>
{template.name}
</option>
))}
</select>
<button
className="btn-ai"
onClick={() => generateAIReply(false)}
disabled={isGeneratingReply}
>
{isGeneratingReply ? '...' : '🤖 KI'}
</button>
</div>
<textarea
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
placeholder="Antwort schreiben..."
rows={4}
/>
<button
className="btn btn-primary"
onClick={sendReply}
disabled={!replyText.trim() || sendingReply}
>
{sendingReply ? 'Sende...' : '📤 Antworten'}
</button>
</div>
)}
</div>
)}
{/* Mobile Filters */}
{mobileView === 'filters' && (
<div className="mobile-filters">
<div className="filter-section">
<h3>Status</h3>
<div className="filter-chips">
{['new', 'in_review', 'waiting', 'replied', 'resolved', 'archived', 'spam'].map(
(status) => (
<button
key={status}
className={`chip ${filters.status.includes(status) ? 'active' : ''}`}
onClick={() => {
setFilters((prev) => ({
...prev,
status: prev.status.includes(status)
? prev.status.filter((s) => s !== status)
: [...prev.status, status],
}))
}}
>
{getStatusBadge(status).label}
</button>
),
)}
</div>
</div>
<div className="filter-section">
<h3>Priorität</h3>
<div className="filter-chips">
{['urgent', 'high', 'normal', 'low'].map((priority) => (
<button
key={priority}
className={`chip ${filters.priority.includes(priority) ? 'active' : ''}`}
onClick={() => {
setFilters((prev) => ({
...prev,
priority: prev.priority.includes(priority)
? prev.priority.filter((p) => p !== priority)
: [...prev.priority, priority],
}))
}}
>
{priority}
</button>
))}
</div>
</div>
<div className="filter-section">
<h3>Sentiment</h3>
<div className="filter-chips">
{['positive', 'neutral', 'negative', 'mixed'].map((sentiment) => (
<button
key={sentiment}
className={`chip ${filters.sentiment.includes(sentiment) ? 'active' : ''}`}
onClick={() => {
setFilters((prev) => ({
...prev,
sentiment: prev.sentiment.includes(sentiment)
? prev.sentiment.filter((s) => s !== sentiment)
: [...prev.sentiment, sentiment],
}))
}}
>
{getSentimentEmoji(sentiment)} {sentiment}
</button>
))}
</div>
</div>
<div className="filter-section">
<h3>Flags</h3>
<div className="filter-chips">
<button
className={`chip ${filters.flags.includes('medical') ? 'active' : ''}`}
onClick={() => {
setFilters((prev) => ({
...prev,
flags: prev.flags.includes('medical')
? prev.flags.filter((f) => f !== 'medical')
: [...prev.flags, 'medical'],
}))
}}
>
Medizinisch
</button>
<button
className={`chip ${filters.flags.includes('escalation') ? 'active' : ''}`}
onClick={() => {
setFilters((prev) => ({
...prev,
flags: prev.flags.includes('escalation')
? prev.flags.filter((f) => f !== 'escalation')
: [...prev.flags, 'escalation'],
}))
}}
>
🚨 Eskalation
</button>
<button
className={`chip ${filters.flags.includes('influencer') ? 'active' : ''}`}
onClick={() => {
setFilters((prev) => ({
...prev,
flags: prev.flags.includes('influencer')
? prev.flags.filter((f) => f !== 'influencer')
: [...prev.flags, 'influencer'],
}))
}}
>
Influencer
</button>
</div>
</div>
<button className="btn btn-full" onClick={() => setFilters(DEFAULT_FILTERS)}>
Filter zurücksetzen
</button>
<button className="btn btn-primary btn-full" onClick={() => setMobileView('list')}>
Anwenden
</button>
</div>
)}
{/* Export Modal */}
{showExportModal && (
<div className="modal-overlay" onClick={() => setShowExportModal(false)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>📊 Report exportieren</h2>
<button className="modal-close" onClick={() => setShowExportModal(false)}>
</button>
</div>
<div className="modal-body">
<div className="form-group">
<label>Format</label>
<div className="format-options">
{(['excel', 'csv', 'pdf'] as const).map((format) => (
<label
key={format}
className={`format-option ${exportFormat === format ? 'selected' : ''}`}
>
<input
type="radio"
name="format"
value={format}
checked={exportFormat === format}
onChange={() => setExportFormat(format)}
/>
<span className="format-icon">
{format === 'excel' ? '📗' : format === 'csv' ? '📄' : '📕'}
</span>
<span>{format.toUpperCase()}</span>
</label>
))}
</div>
</div>
<div className="form-group">
<label>Zeitraum</label>
<div className="date-inputs">
<input
type="date"
value={exportDateRange.from}
onChange={(e) =>
setExportDateRange((prev) => ({ ...prev, from: e.target.value }))
}
/>
<input
type="date"
value={exportDateRange.to}
onChange={(e) =>
setExportDateRange((prev) => ({ ...prev, to: e.target.value }))
}
/>
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn" onClick={() => setShowExportModal(false)}>
Abbrechen
</button>
<button className="btn btn-primary" onClick={handleExport} disabled={exporting}>
{exporting ? '⏳ Exportiere...' : '📥 Exportieren'}
</button>
</div>
</div>
</div>
)}
</div>
)
}
// DESKTOP RENDER
return (
<div className="community-inbox desktop">
{/* Header */}
<div className="inbox-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<h1>📬 Community Inbox</h1>
<div className="inbox-stats">
<span className="stat stat-new">
<strong>{stats.new}</strong> Neu
</span>
<span className="stat stat-urgent">
<strong>{stats.urgent}</strong> Dringend
</span>
<span className="stat stat-medical">
<strong>{stats.medical}</strong> Medizinisch
</span>
</div>
</div>
<div className="inbox-actions">
{/* Sync Status Badge */}
{syncStatus && (
<div className="sync-status">
<span className={`sync-indicator ${syncStatus.isRunning ? 'active' : ''}`} />
<span className="sync-text">
{syncStatus.isRunning
? 'Sync läuft...'
: syncStatus.lastRunAt
? `Letzter Sync: ${new Date(syncStatus.lastRunAt).toLocaleTimeString('de-DE')}`
: 'Noch nicht synchronisiert'}
</span>
</div>
)}
<a href="/admin/views/community/analytics" className="btn">
📈 Analytics
</a>
<button className="btn" onClick={() => setShowExportModal(true)}>
📊 Export
</button>
<button className="btn btn-primary" onClick={handleSync} disabled={syncing || syncStatus?.isRunning}>
{syncing || syncStatus?.isRunning ? '⏳ Syncing...' : '🔄 Sync'}
</button>
</div>
</div>
{/* Layout */}
<div className="inbox-layout">
{/* Sidebar Filters */}
<aside className="inbox-sidebar">
<div className="filter-section">
<h3>Status</h3>
<div className="filter-options">
{['new', 'in_review', 'waiting', 'replied', 'resolved', 'archived', 'spam'].map(
(status) => (
<label key={status} className="filter-checkbox">
<input
type="checkbox"
checked={filters.status.includes(status)}
onChange={(e) => {
setFilters((prev) => ({
...prev,
status: e.target.checked
? [...prev.status, status]
: prev.status.filter((s) => s !== status),
}))
}}
/>
<span
className="status-dot"
style={{ backgroundColor: getStatusBadge(status).color }}
/>
{getStatusBadge(status).label}
</label>
),
)}
</div>
</div>
<div className="filter-section">
<h3>Priorität</h3>
<div className="filter-options">
{['urgent', 'high', 'normal', 'low'].map((priority) => (
<label key={priority} className="filter-checkbox">
<input
type="checkbox"
checked={filters.priority.includes(priority)}
onChange={(e) => {
setFilters((prev) => ({
...prev,
priority: e.target.checked
? [...prev.priority, priority]
: prev.priority.filter((p) => p !== priority),
}))
}}
/>
<span className="priority-dot" style={{ backgroundColor: getPriorityColor(priority) }} />
{priority}
</label>
))}
</div>
</div>
<div className="filter-section">
<h3>Sentiment</h3>
<div className="filter-options">
{['positive', 'neutral', 'negative', 'mixed'].map((sentiment) => (
<label key={sentiment} className="filter-checkbox">
<input
type="checkbox"
checked={filters.sentiment.includes(sentiment)}
onChange={(e) => {
setFilters((prev) => ({
...prev,
sentiment: e.target.checked
? [...prev.sentiment, sentiment]
: prev.sentiment.filter((s) => s !== sentiment),
}))
}}
/>
{getSentimentEmoji(sentiment)} {sentiment}
</label>
))}
</div>
</div>
<div className="filter-section">
<h3>Flags</h3>
<div className="filter-options">
<label className="filter-checkbox">
<input
type="checkbox"
checked={filters.flags.includes('medical')}
onChange={(e) => {
setFilters((prev) => ({
...prev,
flags: e.target.checked
? [...prev.flags, 'medical']
: prev.flags.filter((f) => f !== 'medical'),
}))
}}
/>
Medizinische Frage
</label>
<label className="filter-checkbox">
<input
type="checkbox"
checked={filters.flags.includes('escalation')}
onChange={(e) => {
setFilters((prev) => ({
...prev,
flags: e.target.checked
? [...prev.flags, 'escalation']
: prev.flags.filter((f) => f !== 'escalation'),
}))
}}
/>
🚨 Eskalation
</label>
<label className="filter-checkbox">
<input
type="checkbox"
checked={filters.flags.includes('influencer')}
onChange={(e) => {
setFilters((prev) => ({
...prev,
flags: e.target.checked
? [...prev.flags, 'influencer']
: prev.flags.filter((f) => f !== 'influencer'),
}))
}}
/>
Influencer
</label>
</div>
</div>
<div className="filter-section">
<h3>Zugewiesen an</h3>
<select
value={filters.assignedTo}
onChange={(e) => setFilters((prev) => ({ ...prev, assignedTo: e.target.value }))}
className="filter-select"
>
<option value="">Alle</option>
<option value="unassigned">Nicht zugewiesen</option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.name || user.email}
</option>
))}
</select>
</div>
<div className="filter-section">
<h3>Suche</h3>
<input
type="text"
placeholder="Nachricht oder Autor..."
value={filters.search}
onChange={(e) => setFilters((prev) => ({ ...prev, search: e.target.value }))}
className="filter-search"
/>
</div>
<button className="btn btn-reset" onClick={() => setFilters(DEFAULT_FILTERS)}>
Filter zurücksetzen
</button>
</aside>
{/* Interaction List */}
<div className="inbox-list">
{loading ? (
<div className="loading">
<div className="spinner" />
<p>Lade Interaktionen...</p>
</div>
) : interactions.length === 0 ? (
<div className="empty-state">
<p>Keine Interaktionen gefunden</p>
<button onClick={() => setFilters(DEFAULT_FILTERS)}>Filter zurücksetzen</button>
</div>
) : (
<ul className="interaction-list" ref={listRef}>
{interactions.map((interaction) => (
<li
key={interaction.id}
className={`interaction-item ${selectedInteraction?.id === interaction.id ? 'selected' : ''} ${interaction.status === 'new' ? 'unread' : ''}`}
onClick={() => selectInteraction(interaction)}
>
<div className="interaction-header">
<div className="interaction-meta">
<span
className="priority-indicator"
style={{ backgroundColor: getPriorityColor(interaction.priority) }}
title={interaction.priority}
/>
<span className="platform-icon" title={interaction.platform?.name}>
{interaction.platform?.icon || '📱'}
</span>
<span className="author-name">
{interaction.author?.name || 'Unknown'}
{interaction.author?.isVerified && ' ✓'}
</span>
{interaction.flags?.isFromInfluencer && (
<span className="badge badge-influencer"></span>
)}
</div>
<span className="interaction-time">{formatDate(interaction.publishedAt)}</span>
</div>
<div className="interaction-content">
<p className="message-preview">
{interaction.message?.substring(0, 120)}
{interaction.message?.length > 120 && '...'}
</p>
</div>
<div className="interaction-footer">
<div className="interaction-badges">
{interaction.analysis?.sentiment && (
<span className="badge badge-sentiment">
{getSentimentEmoji(interaction.analysis.sentiment)}
</span>
)}
{interaction.flags?.isMedicalQuestion && (
<span className="badge badge-medical"></span>
)}
{interaction.flags?.requiresEscalation && (
<span className="badge badge-escalation">🚨</span>
)}
<span
className="badge badge-status"
style={{ backgroundColor: getStatusBadge(interaction.status).color }}
>
{getStatusBadge(interaction.status).label}
</span>
</div>
{interaction.linkedContent && (
<span className="linked-content" title={interaction.linkedContent.title}>
📺
</span>
)}
</div>
</li>
))}
</ul>
)}
</div>
{/* Detail Panel */}
<div className="inbox-detail">
{selectedInteraction ? (
<>
{/* Author Info */}
<div className="detail-author">
{selectedInteraction.author?.avatarUrl && (
<img
src={selectedInteraction.author.avatarUrl}
alt=""
className="author-avatar"
/>
)}
<div className="author-details">
<h3>
{selectedInteraction.author?.name}
{selectedInteraction.author?.isVerified && (
<span className="verified-badge"></span>
)}
</h3>
<p>
{selectedInteraction.author?.handle}
{selectedInteraction.author?.subscriberCount > 0 && (
<span className="subscriber-count">
{selectedInteraction.author.subscriberCount.toLocaleString()} Follower
</span>
)}
</p>
</div>
</div>
{/* Quick Actions */}
<div className="detail-actions">
<select
value={selectedInteraction.status}
onChange={(e) => updateStatus(selectedInteraction.id, e.target.value)}
>
<option value="new">🆕 Neu</option>
<option value="in_review">👁 In Review</option>
<option value="waiting"> Warten auf Info</option>
<option value="replied"> Beantwortet</option>
<option value="resolved"> Erledigt</option>
<option value="archived">📦 Archiviert</option>
<option value="spam">🚫 Spam</option>
</select>
<select
value={String(selectedInteraction.assignedTo?.id || '')}
onChange={(e) => assignTo(selectedInteraction.id, e.target.value)}
>
<option value="">Nicht zugewiesen</option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.name || user.email}
</option>
))}
</select>
</div>
{/* Alerts */}
{(selectedInteraction.flags?.isMedicalQuestion ||
selectedInteraction.flags?.requiresEscalation) && (
<div className="detail-alerts">
{selectedInteraction.flags.isMedicalQuestion && (
<div className="alert alert-medical">
Medizinische Frage Auf Hotline verweisen!
</div>
)}
{selectedInteraction.flags.requiresEscalation && (
<div className="alert alert-escalation">🚨 Eskalation erforderlich</div>
)}
</div>
)}
{/* Linked Content */}
{selectedInteraction.linkedContent && (
<a
href={`https://youtube.com/watch?v=${selectedInteraction.linkedContent.youtubeVideoId}`}
target="_blank"
rel="noopener noreferrer"
className="linked-video"
>
📺 {selectedInteraction.linkedContent.title}
</a>
)}
{/* Message */}
<div className="detail-message">
<p>{selectedInteraction.message}</p>
<div className="message-meta">
<span>{formatDate(selectedInteraction.publishedAt)}</span>
<span> {selectedInteraction.engagement?.likes || 0}</span>
<span>💬 {selectedInteraction.engagement?.replies || 0}</span>
</div>
</div>
{/* AI Analysis */}
{selectedInteraction.analysis && (
<div className="detail-analysis">
<h4>🤖 KI-Analyse</h4>
<div className="analysis-grid">
<div className="analysis-item">
<span className="label">Sentiment</span>
<span className="value">
{getSentimentEmoji(selectedInteraction.analysis.sentiment)}{' '}
{selectedInteraction.analysis.sentiment}
</span>
</div>
<div className="analysis-item">
<span className="label">Score</span>
<span className="value">{selectedInteraction.analysis.sentimentScore}</span>
</div>
<div className="analysis-item">
<span className="label">Konfidenz</span>
<span className="value">
{Math.round(selectedInteraction.analysis.confidence * 100)}%
</span>
</div>
</div>
{selectedInteraction.analysis.topics?.length > 0 && (
<div className="analysis-topics">
<span className="label">Themen:</span>
<div className="topics-list">
{selectedInteraction.analysis.topics.map((t, i) => (
<span key={i} className="topic-tag">
{t.topic}
</span>
))}
</div>
</div>
)}
</div>
)}
{/* Previous Response */}
{selectedInteraction.response?.text && (
<div className="detail-response">
<h4> Unsere Antwort</h4>
<p>{selectedInteraction.response.text}</p>
<small>
Gesendet am {formatDate(selectedInteraction.response.sentAt)} von{' '}
{selectedInteraction.response.sentBy?.email}
</small>
</div>
)}
{/* Reply Section */}
{!['resolved', 'archived', 'spam'].includes(selectedInteraction.status) &&
!selectedInteraction.response?.text && (
<div className="detail-reply">
<div className="reply-toolbar">
<select
onChange={(e) => {
const template = templates.find((t) => t.id === Number(e.target.value))
if (template) applyTemplate(template)
}}
defaultValue=""
>
<option value="">📝 Template auswählen...</option>
{templates.map((template) => (
<option key={template.id} value={template.id}>
{template.name}
</option>
))}
</select>
<div className="ai-buttons">
<button
className="btn btn-ai"
onClick={() => generateAIReply(false)}
disabled={isGeneratingReply}
>
{isGeneratingReply ? '...' : '🤖 KI-Antwort'}
</button>
<button
className="btn btn-ai-variants"
onClick={() => generateAIReply(true)}
disabled={isGeneratingReply}
title="3 Varianten generieren"
>
{isGeneratingReply ? '...' : '🤖 3 Varianten'}
</button>
</div>
</div>
{/* AI Generated Replies */}
{generatedReplies.length > 0 && (
<div className="ai-reply-suggestions">
<h5>🤖 KI-Antwortvorschläge</h5>
<div className="suggestions-grid">
{generatedReplies.map((reply, index) => (
<div key={index} className="suggestion-card">
{reply.tone && (
<span className="suggestion-tone">{reply.tone}</span>
)}
<p className="suggestion-text">{reply.text}</p>
{reply.warnings?.length > 0 && (
<div className="suggestion-warnings">
{reply.warnings.map((w, i) => (
<span key={i} className="warning-tag"> {w}</span>
))}
</div>
)}
<button
className="btn btn-use-suggestion"
onClick={() => applyGeneratedReply(reply)}
>
Übernehmen
</button>
</div>
))}
</div>
<button
className="btn btn-clear-suggestions"
onClick={() => setGeneratedReplies([])}
>
Vorschläge ausblenden
</button>
</div>
)}
<textarea
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
placeholder="Antwort schreiben..."
rows={4}
className="reply-textarea"
/>
<div className="reply-actions">
<span className="char-count">{replyText.length} Zeichen</span>
<button
className="btn btn-primary"
onClick={sendReply}
disabled={!replyText.trim() || sendingReply}
>
{sendingReply ? 'Sende...' : '📤 Antworten'}
</button>
</div>
</div>
)}
</>
) : (
<div className="no-selection">
<p>Wähle eine Interaktion aus der Liste</p>
</div>
)}
</div>
</div>
{/* Export Modal */}
{showExportModal && (
<div className="modal-overlay" onClick={() => setShowExportModal(false)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>📊 Report exportieren</h2>
<button className="modal-close" onClick={() => setShowExportModal(false)}>
</button>
</div>
<div className="modal-body">
<div className="form-group">
<label>Format</label>
<div className="format-options">
{(['excel', 'csv', 'pdf'] as const).map((format) => (
<label
key={format}
className={`format-option ${exportFormat === format ? 'selected' : ''}`}
>
<input
type="radio"
name="format"
value={format}
checked={exportFormat === format}
onChange={() => setExportFormat(format)}
/>
<span className="format-icon">
{format === 'excel' ? '📗' : format === 'csv' ? '📄' : '📕'}
</span>
<span>{format === 'excel' ? 'Excel (.xlsx)' : format.toUpperCase()}</span>
</label>
))}
</div>
</div>
<div className="form-group">
<label>Zeitraum</label>
<div className="date-inputs">
<div className="date-input">
<span>Von</span>
<input
type="date"
value={exportDateRange.from}
onChange={(e) =>
setExportDateRange((prev) => ({ ...prev, from: e.target.value }))
}
/>
</div>
<div className="date-input">
<span>Bis</span>
<input
type="date"
value={exportDateRange.to}
onChange={(e) =>
setExportDateRange((prev) => ({ ...prev, to: e.target.value }))
}
/>
</div>
</div>
</div>
<div className="form-group">
<label>Optionen</label>
<div className="export-info">
<p>Der Export enthält:</p>
<ul>
<li>Alle Interaktionen im gewählten Zeitraum</li>
<li>Aktuelle Filter werden berücksichtigt</li>
<li>Autor-Informationen, Sentiment, Status</li>
<li>Response-Daten (falls vorhanden)</li>
</ul>
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn" onClick={() => setShowExportModal(false)}>
Abbrechen
</button>
<button className="btn btn-primary" onClick={handleExport} disabled={exporting}>
{exporting ? '⏳ Exportiere...' : '📥 Exportieren'}
</button>
</div>
</div>
</div>
)}
</div>
)
}