# Phase 1 Completion – Integration Prompt (Extended) ## Übersicht Dieser Prompt vervollständigt Phase 1 des Community Management Systems mit: 1. **Community Inbox View** – Custom Admin View für tägliches Community Management 2. **Mobile-Optimierte Inbox** – Vollständig responsive für Smartphone-Nutzung 3. **YouTube OAuth Flow** – Authentifizierung + Token Management 4. **Auto-Sync Cron Job** – Automatischer Kommentar-Import 5. **Rules Engine** – Automatische Regel-Ausführung bei neuen Interaktionen 6. **Export-Funktion** – PDF/Excel Reports für Analysen **Geschätzter Aufwand:** 7-9 Tage --- ## Teil 1: Community Inbox View (Mobile-Optimiert) ### 1.1 View Registration ```typescript // src/app/(payload)/admin/views/community/inbox/page.tsx import React from 'react' import { DefaultTemplate } from '@payloadcms/next/templates' import { Gutter } from '@payloadcms/ui' import { CommunityInbox } from './CommunityInbox' import './inbox.scss' export const metadata = { title: 'Community Inbox', description: 'Manage community interactions across all platforms', } export default function CommunityInboxPage() { return ( ) } ``` ### 1.2 Main Component (mit Mobile Support) ```tsx // src/app/(payload)/admin/views/community/inbox/CommunityInbox.tsx 'use client' import React, { useState, useEffect, useCallback, useRef } from 'react' import { useConfig } from '@payloadcms/ui' 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 = () => { const { config } = useConfig() // 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') const [showMobileFilters, setShowMobileFilters] = useState(false) // Export state const [showExportModal, setShowExportModal] = useState(false) const [exportFormat, setExportFormat] = useState<'pdf' | 'excel' | 'csv'>('excel') const [exportDateRange, setExportDateRange] = useState({ from: '', to: '' }) // Refs const listRef = useRef(null) const detailRef = 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]) // Sync comments const handleSync = async () => { setSyncing(true) try { const response = await fetch('/api/community/sync-comments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ analyzeWithAI: true }), }) if (response.ok) { const result = await response.json() if (isMobile) { alert(`✅ ${result.created} neue, ${result.updated} aktualisiert`) } else { alert(`Sync complete: ${result.created} new, ${result.updated} updated`) } loadInteractions() } else { throw new 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) } // Include current filters 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') } // Download file 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() if (isMobile) { 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 () => { if (!selectedInteraction) return try { const response = await fetch('/api/community/generate-reply', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ interactionId: selectedInteraction.id, }), }) if (response.ok) { const data = await response.json() setReplyText(data.reply) } } catch (error) { console.error('AI generation error:', error) } } // Select interaction (mobile-aware) 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) => { // Status-Werte gemäß CommunityInteractions.ts 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' }) } // Active filters count const activeFiltersCount = [ filters.status.length !== 3 ? 1 : 0, // Default is 3 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'} {mobileView === 'filters' && ( )} )}
{/* Mobile Stats Bar */} {mobileView === 'list' && (
{stats.new} neu {stats.urgent} urgent {stats.medical} med.
)} {/* Mobile Search */} {mobileView === 'list' && (
setFilters(prev => ({ ...prev, search: e.target.value }))} />
)} {/* Mobile Filters View */} {mobileView === 'filters' && (

Status

{['new', 'in_review', 'waiting', 'replied', 'resolved', 'archived', 'spam'].map(status => ( ))}

Priorität

{['urgent', 'high', 'normal', 'low'].map(priority => ( ))}

Sentiment

{['positive', 'neutral', 'negative', 'mixed'].map(sentiment => ( ))}

Flags

Zugewiesen an

)} {/* Mobile List View */} {mobileView === 'list' && (
{loading ? (
) : interactions.length === 0 ? (

Keine Interaktionen

) : (
    {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 && ( 🚨 )} {interaction.flags?.isFromInfluencer && ( )} {getStatusBadge(interaction.status).label}
  • ))}
)}
)} {/* Mobile Detail View */} {mobileView === 'detail' && selectedInteraction && (
{/* Author Card */}
{selectedInteraction.author?.avatarUrl && ( )}

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

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

{/* Quick Actions */}
{/* Flags Alert */} {(selectedInteraction.flags?.isMedicalQuestion || selectedInteraction.flags?.requiresEscalation) && (
{selectedInteraction.flags.isMedicalQuestion && (
⚕️ Medizinische Frage – Auf Hotline verweisen!
)} {selectedInteraction.flags.requiresEscalation && (
🚨 Eskalation erforderlich
)}
)} {/* Context */} {selectedInteraction.linkedContent && ( 📺 {selectedInteraction.linkedContent.title} )} {/* Message */}

{selectedInteraction.message}

{formatDate(selectedInteraction.publishedAt)} ❤️ {selectedInteraction.engagement?.likes || 0} 💬 {selectedInteraction.engagement?.replies || 0}
{/* AI Analysis */} {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} ))}
)}
)} {/* Previous Response */} {selectedInteraction.response?.text && (

✅ Unsere Antwort

{selectedInteraction.response.text}

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