cms.c2sgmbh/src/collections/CommunityInteractions.ts
Martin Porwoll 74b251edea feat(Community): add Community Inbox View, Rules Engine, and YouTube OAuth
Community Management Phase 1 completion:
- Add Community Inbox admin view with filters, stats, and reply functionality
- Add Rules Engine service for automated interaction processing
- Add YouTube OAuth flow (auth, callback, token refresh)
- Add Comment Sync cron job (every 15 minutes)
- Add Community Export API (PDF/Excel/CSV)
- Fix database schema for community_rules hasMany fields
- Fix access control in communityAccess.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 16:26:08 +00:00

567 lines
14 KiB
TypeScript

// src/collections/CommunityInteractions.ts
import type { CollectionConfig } from 'payload'
import {
isCommunityModeratorOrAbove,
isCommunityManager,
canAccessAssignedInteractions,
} from '../lib/communityAccess'
/**
* CommunityInteractions Collection
*
* Speichert alle Social Media Interaktionen (Kommentare, DMs, Mentions).
* Teil des Community Management Systems.
*/
export const CommunityInteractions: CollectionConfig = {
slug: 'community-interactions',
labels: {
singular: 'Interaction',
plural: 'Interactions',
},
admin: {
group: 'Community',
defaultColumns: ['platform', 'type', 'author.name', 'status', 'priority', 'createdAt'],
listSearchableFields: ['author.name', 'author.handle', 'message'],
pagination: {
defaultLimit: 50,
},
},
access: {
read: canAccessAssignedInteractions,
create: isCommunityModeratorOrAbove,
update: canAccessAssignedInteractions,
delete: isCommunityManager,
},
fields: [
// === SOURCE ===
{
type: 'row',
fields: [
{
name: 'platform',
type: 'relationship',
relationTo: 'social-platforms',
required: true,
label: 'Plattform',
admin: { width: '33%' },
},
{
name: 'socialAccount',
type: 'relationship',
relationTo: 'social-accounts',
required: true,
label: 'Account',
admin: { width: '33%' },
},
{
name: 'linkedContent',
type: 'relationship',
relationTo: 'youtube-content',
label: 'Verknüpfter Content',
admin: { width: '33%' },
},
],
},
// === INTERACTION TYPE ===
{
type: 'row',
fields: [
{
name: 'type',
type: 'select',
required: true,
label: 'Typ',
options: [
{ label: 'Kommentar', value: 'comment' },
{ label: 'Antwort', value: 'reply' },
{ label: 'Direktnachricht', value: 'dm' },
{ label: 'Erwähnung', value: 'mention' },
{ label: 'Bewertung', value: 'review' },
{ label: 'Frage', value: 'question' },
],
admin: { width: '50%' },
},
{
name: 'externalId',
type: 'text',
label: 'External ID',
required: true,
unique: true,
index: true,
admin: {
width: '50%',
description: 'YouTube Comment ID, etc.',
},
},
],
},
// === PARENT (für Threads) ===
{
name: 'parentInteraction',
type: 'relationship',
relationTo: 'community-interactions',
label: 'Parent (bei Replies)',
admin: {
condition: (data) => data?.type === 'reply',
},
},
// === AUTHOR INFO ===
{
name: 'author',
type: 'group',
label: 'Author',
fields: [
{
type: 'row',
fields: [
{
name: 'name',
type: 'text',
label: 'Name',
admin: { width: '50%' },
},
{
name: 'handle',
type: 'text',
label: 'Handle',
admin: { width: '50%' },
},
],
},
{
type: 'row',
fields: [
{
name: 'profileUrl',
type: 'text',
label: 'Profile URL',
admin: { width: '50%' },
},
{
name: 'avatarUrl',
type: 'text',
label: 'Avatar URL',
admin: { width: '50%' },
},
],
},
{
type: 'row',
fields: [
{
name: 'isVerified',
type: 'checkbox',
label: 'Verifiziert',
admin: { width: '25%' },
},
{
name: 'isSubscriber',
type: 'checkbox',
label: 'Subscriber/Follower',
admin: { width: '25%' },
},
{
name: 'isMember',
type: 'checkbox',
label: 'Channel Member',
admin: { width: '25%' },
},
{
name: 'subscriberCount',
type: 'number',
label: 'Ihre Subscriber',
admin: { width: '25%' },
},
],
},
],
},
// === MESSAGE CONTENT ===
{
name: 'message',
type: 'textarea',
label: 'Nachricht',
required: true,
admin: {
rows: 4,
},
},
{
name: 'messageHtml',
type: 'textarea',
label: 'Original HTML',
admin: {
rows: 2,
description: 'Falls Plattform HTML liefert',
},
},
{
name: 'attachments',
type: 'array',
label: 'Attachments',
fields: [
{
type: 'row',
fields: [
{
name: 'type',
type: 'select',
label: 'Typ',
options: [
{ label: 'Bild', value: 'image' },
{ label: 'Video', value: 'video' },
{ label: 'Link', value: 'link' },
{ label: 'Sticker', value: 'sticker' },
],
admin: { width: '30%' },
},
{
name: 'url',
type: 'text',
label: 'URL',
admin: { width: '70%' },
},
],
},
],
},
{
name: 'publishedAt',
type: 'date',
label: 'Veröffentlicht am',
required: true,
admin: {
date: {
pickerAppearance: 'dayAndTime',
},
},
},
// === AI ANALYSIS ===
{
name: 'analysis',
type: 'group',
label: 'AI Analyse',
admin: {
description: 'Automatisch via Claude API',
},
fields: [
{
type: 'row',
fields: [
{
name: 'sentiment',
type: 'select',
label: 'Sentiment',
options: [
{ label: 'Positiv', value: 'positive' },
{ label: 'Neutral', value: 'neutral' },
{ label: 'Negativ', value: 'negative' },
{ label: 'Frage', value: 'question' },
{ label: 'Dankbarkeit', value: 'gratitude' },
{ label: 'Frustration', value: 'frustration' },
],
admin: { width: '33%' },
},
{
name: 'sentimentScore',
type: 'number',
label: 'Score (-1 bis 1)',
min: -1,
max: 1,
admin: { width: '33%' },
},
{
name: 'confidence',
type: 'number',
label: 'Confidence %',
min: 0,
max: 100,
admin: { width: '33%' },
},
],
},
{
name: 'topics',
type: 'array',
label: 'Erkannte Themen',
fields: [
{
name: 'topic',
type: 'text',
label: 'Thema',
},
],
},
{
name: 'language',
type: 'text',
label: 'Sprache',
admin: {
placeholder: 'de',
},
},
{
name: 'suggestedTemplate',
type: 'relationship',
relationTo: 'community-templates',
label: 'Vorgeschlagenes Template',
},
{
name: 'suggestedReply',
type: 'textarea',
label: 'AI-generierter Antwortvorschlag',
},
{
name: 'analyzedAt',
type: 'date',
label: 'Analysiert am',
},
],
},
// === FLAGS ===
{
name: 'flags',
type: 'group',
label: 'Flags',
fields: [
{
type: 'row',
fields: [
{
name: 'isMedicalQuestion',
type: 'checkbox',
label: 'Medizinische Frage',
admin: {
width: '25%',
description: 'Erfordert ärztliche Review',
},
},
{
name: 'requiresEscalation',
type: 'checkbox',
label: 'Eskalation nötig',
admin: { width: '25%' },
},
{
name: 'isSpam',
type: 'checkbox',
label: 'Spam',
admin: { width: '25%' },
},
{
name: 'isFromInfluencer',
type: 'checkbox',
label: 'Influencer',
admin: {
width: '25%',
description: '>10k Follower',
},
},
],
},
],
},
// === WORKFLOW ===
{
name: 'status',
type: 'select',
required: true,
defaultValue: 'new',
index: true,
label: 'Status',
options: [
{ label: 'Neu', value: 'new' },
{ label: 'In Review', value: 'in_review' },
{ label: 'Warten auf Info', value: 'waiting' },
{ label: 'Beantwortet', value: 'replied' },
{ label: 'Erledigt', value: 'resolved' },
{ label: 'Archiviert', value: 'archived' },
{ label: 'Spam', value: 'spam' },
],
admin: {
position: 'sidebar',
},
},
{
name: 'priority',
type: 'select',
required: true,
defaultValue: 'normal',
index: true,
label: 'Priorität',
options: [
{ label: 'Urgent', value: 'urgent' },
{ label: 'Hoch', value: 'high' },
{ label: 'Normal', value: 'normal' },
{ label: 'Niedrig', value: 'low' },
],
admin: {
position: 'sidebar',
},
},
{
name: 'assignedTo',
type: 'relationship',
relationTo: 'users',
label: 'Zugewiesen an',
admin: {
position: 'sidebar',
},
},
{
name: 'responseDeadline',
type: 'date',
label: 'Antwort-Deadline',
admin: {
position: 'sidebar',
date: {
pickerAppearance: 'dayAndTime',
},
},
},
// === OUR RESPONSE ===
{
name: 'response',
type: 'group',
label: 'Unsere Antwort',
fields: [
{
name: 'text',
type: 'textarea',
label: 'Antwort-Text',
admin: { rows: 4 },
},
{
name: 'usedTemplate',
type: 'relationship',
relationTo: 'community-templates',
label: 'Verwendetes Template',
},
{
name: 'sentAt',
type: 'date',
label: 'Gesendet am',
},
{
name: 'sentBy',
type: 'relationship',
relationTo: 'users',
label: 'Gesendet von',
},
{
name: 'externalReplyId',
type: 'text',
label: 'Reply ID (extern)',
},
],
},
// === ENGAGEMENT (Platform-spezifisch) ===
{
name: 'engagement',
type: 'group',
label: 'Engagement',
admin: {
description: 'Wird beim Sync aktualisiert',
},
fields: [
{
type: 'row',
fields: [
{
name: 'likes',
type: 'number',
label: 'Likes',
admin: { width: '25%' },
},
{
name: 'replies',
type: 'number',
label: 'Replies',
admin: { width: '25%' },
},
{
name: 'isHearted',
type: 'checkbox',
label: 'Creator Heart',
admin: { width: '25%' },
},
{
name: 'isPinned',
type: 'checkbox',
label: 'Angepinnt',
admin: { width: '25%' },
},
],
},
],
},
// === INTERNAL NOTES ===
{
name: 'internalNotes',
type: 'textarea',
label: 'Interne Notizen',
admin: {
rows: 2,
description: 'Nur für Team sichtbar',
},
},
],
// === HOOKS ===
hooks: {
beforeChange: [
// Auto-set priority based on flags
async ({ data, operation }) => {
if (!data) return data
if (operation === 'create' || !data.priority) {
if (data?.flags?.isMedicalQuestion) {
data.priority = 'high'
}
if (data?.flags?.requiresEscalation) {
data.priority = 'urgent'
}
if (data?.flags?.isFromInfluencer) {
data.priority = data.priority === 'urgent' ? 'urgent' : 'high'
}
}
return data
},
],
afterChange: [
// Send notification for urgent items
async ({ doc, operation }) => {
if (operation === 'create' && doc.priority === 'urgent') {
// TODO: Notification Logic via YtNotifications
console.log(`🚨 Urgent interaction: ${doc.id}`)
}
},
// Run Rules Engine on new interactions
async ({ doc, operation, req }) => {
if (operation === 'create') {
try {
const { RulesEngine } = await import('@/lib/services/RulesEngine')
const rulesEngine = new RulesEngine(req.payload)
const result = await rulesEngine.evaluateRules(doc.id)
if (result.appliedRules.length > 0) {
console.log(`[RulesEngine] Applied ${result.appliedRules.length} rules to interaction ${doc.id}:`, result.appliedRules)
}
} catch (error) {
console.error('[RulesEngine] Error evaluating rules:', error)
}
}
},
],
},
timestamps: true,
}