mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 19:44:12 +00:00
- Add .claude/ configuration (agents, commands, hooks, get-shit-done workflows) - Add prompts/ directory with development planning documents - Add scripts/setup-tenants/ with tenant configuration - Add docs/screenshots/ - Remove obsolete phase2.2-corrections-report.md - Update pnpm-lock.yaml - Update detect-secrets.sh to ignore setup.sh (env var usage, not secrets) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2240 lines
60 KiB
Markdown
2240 lines
60 KiB
Markdown
# Phase 1: Community Core – Detailplan
|
||
|
||
## Übersicht
|
||
|
||
**Ziel:** Community Management direkt im Hub mit YouTube API Integration und Vorbereitung für LinkedIn/Instagram/Facebook.
|
||
|
||
**Voraussetzung:** YouTube Operations Hub muss implementiert sein (YouTubeChannels, YouTubeContent Collections).
|
||
|
||
**Kern-Features:**
|
||
1. Community Inbox (Unified View)
|
||
2. YouTube Comments Sync (Direkte API)
|
||
3. Response Templates mit Variablen
|
||
4. Medical Flag System (CCS-spezifisch)
|
||
5. AI-gestützte Sentiment-Analyse (Claude API)
|
||
6. Multi-Platform Grundstruktur
|
||
|
||
---
|
||
|
||
## Architektur Phase 1
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ COMMUNITY CORE │
|
||
├─────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||
│ │ COMMUNITY INBOX VIEW │ │
|
||
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
||
│ │ │ YouTube │ │ LinkedIn │ │Instagram │ │ Facebook │ │ │
|
||
│ │ │ ✓ │ │ (prep) │ │ (prep) │ │ (prep) │ │ │
|
||
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
|
||
│ └────────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||
│ │ INTEGRATION LAYER │ │
|
||
│ │ │ │
|
||
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │ │
|
||
│ │ │ YouTube Data │ │ Claude AI │ │ Notification │ │ │
|
||
│ │ │ API v3 │ │ (Sentiment) │ │ Service │ │ │
|
||
│ │ └─────────────────┘ └─────────────────┘ └────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ Platform Adapters (Prepared for Phase 2) │ │ │
|
||
│ │ │ • YouTubeAdapter ✓ • LinkedInAdapter (stub) │ │ │
|
||
│ │ │ • InstagramAdapter (stub) • FacebookAdapter (stub) │ │ │
|
||
│ │ └─────────────────────────────────────────────────────────┘ │ │
|
||
│ └────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||
│ │ PAYLOAD COLLECTIONS │ │
|
||
│ │ │ │
|
||
│ │ • social-platforms • community-interactions │ │
|
||
│ │ • social-accounts • community-templates │ │
|
||
│ │ • community-rules │ │
|
||
│ └────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Teil 1: Access Control erweitern
|
||
|
||
### 1.0 Community Access Functions
|
||
|
||
```typescript
|
||
// src/lib/communityAccess.ts
|
||
|
||
import type { Access } from 'payload'
|
||
|
||
interface UserWithRoles {
|
||
id: number
|
||
isSuperAdmin?: boolean
|
||
is_super_admin?: boolean
|
||
youtubeRole?: 'none' | 'viewer' | 'editor' | 'producer' | 'creator' | 'manager'
|
||
youtube_role?: 'none' | 'viewer' | 'editor' | 'producer' | 'creator' | 'manager'
|
||
communityRole?: 'none' | 'viewer' | 'moderator' | 'manager'
|
||
community_role?: 'none' | 'viewer' | 'moderator' | 'manager'
|
||
}
|
||
|
||
const checkIsSuperAdmin = (user: UserWithRoles | null): boolean => {
|
||
if (!user) return false
|
||
return Boolean(user.isSuperAdmin || user.is_super_admin)
|
||
}
|
||
|
||
const getCommunityRole = (user: UserWithRoles | null): string | undefined => {
|
||
if (!user) return undefined
|
||
return user.communityRole || user.community_role
|
||
}
|
||
|
||
const getYouTubeRole = (user: UserWithRoles | null): string | undefined => {
|
||
if (!user) return undefined
|
||
return user.youtubeRole || user.youtube_role
|
||
}
|
||
|
||
/**
|
||
* Prüft ob User Community-Manager oder Super-Admin ist
|
||
*/
|
||
export const isCommunityManager: Access = ({ req }) => {
|
||
const user = req.user as UserWithRoles | null
|
||
if (!user) return false
|
||
if (checkIsSuperAdmin(user)) return true
|
||
// YouTube-Manager haben auch Community-Zugriff
|
||
if (getYouTubeRole(user) === 'manager') return true
|
||
return getCommunityRole(user) === 'manager'
|
||
}
|
||
|
||
/**
|
||
* Prüft ob User mindestens Moderator-Rechte hat
|
||
*/
|
||
export const isCommunityModeratorOrAbove: Access = ({ req }) => {
|
||
const user = req.user as UserWithRoles | null
|
||
if (!user) return false
|
||
if (checkIsSuperAdmin(user)) return true
|
||
if (['manager', 'creator'].includes(getYouTubeRole(user) || '')) return true
|
||
return ['moderator', 'manager'].includes(getCommunityRole(user) || '')
|
||
}
|
||
|
||
/**
|
||
* Prüft ob User Zugriff auf Community-Features hat (mindestens Viewer)
|
||
*/
|
||
export const hasCommunityAccess: Access = ({ req }) => {
|
||
const user = req.user as UserWithRoles | null
|
||
if (!user) return false
|
||
if (checkIsSuperAdmin(user)) return true
|
||
// YouTube-Zugriff impliziert Community-Lesezugriff
|
||
const ytRole = getYouTubeRole(user)
|
||
if (ytRole && ytRole !== 'none') return true
|
||
const commRole = getCommunityRole(user)
|
||
return commRole !== 'none' && commRole !== undefined
|
||
}
|
||
|
||
/**
|
||
* Zugriff auf zugewiesene Interactions
|
||
*/
|
||
export const canAccessAssignedInteractions: Access = ({ req }) => {
|
||
const user = req.user as UserWithRoles | null
|
||
if (!user) return false
|
||
if (checkIsSuperAdmin(user)) return true
|
||
if (getYouTubeRole(user) === 'manager') return true
|
||
if (getCommunityRole(user) === 'manager') return true
|
||
|
||
// Für andere Rollen: Nur zugewiesene Interactions
|
||
return {
|
||
or: [
|
||
{ assignedTo: { equals: user.id } },
|
||
{ 'response.sentBy': { equals: user.id } },
|
||
],
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Teil 2: Collections
|
||
|
||
### 2.1 Social Platforms (`social-platforms`)
|
||
|
||
```typescript
|
||
// src/collections/SocialPlatforms.ts
|
||
|
||
import type { CollectionConfig } from 'payload'
|
||
import { isCommunityManager, hasCommunityAccess } from '../lib/communityAccess'
|
||
|
||
export const SocialPlatforms: CollectionConfig = {
|
||
slug: 'social-platforms',
|
||
labels: {
|
||
singular: 'Social Platform',
|
||
plural: 'Social Platforms',
|
||
},
|
||
admin: {
|
||
group: 'Community',
|
||
useAsTitle: 'name',
|
||
defaultColumns: ['name', 'slug', 'isActive', 'apiStatus'],
|
||
},
|
||
access: {
|
||
read: hasCommunityAccess,
|
||
create: isCommunityManager,
|
||
update: isCommunityManager,
|
||
delete: isCommunityManager,
|
||
},
|
||
fields: [
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'name',
|
||
type: 'text',
|
||
required: true,
|
||
label: 'Name',
|
||
admin: { width: '50%' },
|
||
},
|
||
{
|
||
name: 'slug',
|
||
type: 'text',
|
||
required: true,
|
||
unique: true,
|
||
label: 'Slug',
|
||
admin: { width: '50%' },
|
||
},
|
||
],
|
||
},
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'icon',
|
||
type: 'text',
|
||
label: 'Icon (Emoji)',
|
||
admin: {
|
||
width: '25%',
|
||
placeholder: '📺',
|
||
},
|
||
},
|
||
{
|
||
name: 'color',
|
||
type: 'text',
|
||
label: 'Brand Color',
|
||
admin: {
|
||
width: '25%',
|
||
placeholder: '#FF0000',
|
||
},
|
||
},
|
||
{
|
||
name: 'isActive',
|
||
type: 'checkbox',
|
||
label: 'Aktiv',
|
||
defaultValue: true,
|
||
admin: { width: '25%' },
|
||
},
|
||
{
|
||
name: 'apiStatus',
|
||
type: 'select',
|
||
label: 'API Status',
|
||
options: [
|
||
{ label: 'Verbunden', value: 'connected' },
|
||
{ label: 'Eingeschränkt', value: 'limited' },
|
||
{ label: 'Nicht verbunden', value: 'disconnected' },
|
||
{ label: 'In Entwicklung', value: 'development' },
|
||
],
|
||
defaultValue: 'disconnected',
|
||
admin: { width: '25%' },
|
||
},
|
||
],
|
||
},
|
||
|
||
// API Configuration
|
||
{
|
||
name: 'apiConfig',
|
||
type: 'group',
|
||
label: 'API Konfiguration',
|
||
admin: {
|
||
condition: (data) => data?.isActive,
|
||
},
|
||
fields: [
|
||
{
|
||
name: 'apiType',
|
||
type: 'select',
|
||
label: 'API Type',
|
||
options: [
|
||
{ label: 'YouTube Data API v3', value: 'youtube_v3' },
|
||
{ label: 'LinkedIn API', value: 'linkedin' },
|
||
{ label: 'Instagram Graph API', value: 'instagram_graph' },
|
||
{ label: 'Facebook Graph API', value: 'facebook_graph' },
|
||
{ label: 'Custom/Webhook', value: 'custom' },
|
||
],
|
||
},
|
||
{
|
||
name: 'baseUrl',
|
||
type: 'text',
|
||
label: 'Base URL',
|
||
admin: {
|
||
placeholder: 'https://www.googleapis.com/youtube/v3',
|
||
},
|
||
},
|
||
{
|
||
name: 'authType',
|
||
type: 'select',
|
||
label: 'Auth Type',
|
||
options: [
|
||
{ label: 'OAuth 2.0', value: 'oauth2' },
|
||
{ label: 'API Key', value: 'api_key' },
|
||
{ label: 'Bearer Token', value: 'bearer' },
|
||
],
|
||
},
|
||
{
|
||
name: 'scopes',
|
||
type: 'array',
|
||
label: 'OAuth Scopes',
|
||
admin: {
|
||
condition: (data, siblingData) => siblingData?.authType === 'oauth2',
|
||
},
|
||
fields: [
|
||
{ name: 'scope', type: 'text', label: 'Scope' },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
|
||
// Interaction Types für diese Plattform
|
||
{
|
||
name: 'interactionTypes',
|
||
type: 'array',
|
||
label: 'Interaction Types',
|
||
fields: [
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'type',
|
||
type: 'text',
|
||
required: true,
|
||
label: 'Type',
|
||
admin: {
|
||
width: '30%',
|
||
placeholder: 'comment',
|
||
},
|
||
},
|
||
{
|
||
name: 'label',
|
||
type: 'text',
|
||
required: true,
|
||
label: 'Label',
|
||
admin: {
|
||
width: '30%',
|
||
placeholder: 'Kommentar',
|
||
},
|
||
},
|
||
{
|
||
name: 'icon',
|
||
type: 'text',
|
||
label: 'Icon',
|
||
admin: {
|
||
width: '20%',
|
||
placeholder: '💬',
|
||
},
|
||
},
|
||
{
|
||
name: 'canReply',
|
||
type: 'checkbox',
|
||
label: 'Reply möglich',
|
||
defaultValue: true,
|
||
admin: { width: '20%' },
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
|
||
// Rate Limits
|
||
{
|
||
name: 'rateLimits',
|
||
type: 'group',
|
||
label: 'Rate Limits',
|
||
fields: [
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'requestsPerMinute',
|
||
type: 'number',
|
||
label: 'Requests/Minute',
|
||
admin: { width: '33%' },
|
||
},
|
||
{
|
||
name: 'requestsPerDay',
|
||
type: 'number',
|
||
label: 'Requests/Tag',
|
||
admin: { width: '33%' },
|
||
},
|
||
{
|
||
name: 'quotaUnitsPerDay',
|
||
type: 'number',
|
||
label: 'Quota Units/Tag',
|
||
admin: {
|
||
width: '33%',
|
||
description: 'YouTube: 10.000/Tag',
|
||
},
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
timestamps: true,
|
||
}
|
||
```
|
||
|
||
### 2.2 Social Accounts (`social-accounts`)
|
||
|
||
```typescript
|
||
// src/collections/SocialAccounts.ts
|
||
|
||
import type { CollectionConfig } from 'payload'
|
||
import { isCommunityManager, hasCommunityAccess } from '../lib/communityAccess'
|
||
|
||
export const SocialAccounts: CollectionConfig = {
|
||
slug: 'social-accounts',
|
||
labels: {
|
||
singular: 'Social Account',
|
||
plural: 'Social Accounts',
|
||
},
|
||
admin: {
|
||
group: 'Community',
|
||
useAsTitle: 'displayName',
|
||
defaultColumns: ['displayName', 'platform', 'linkedChannel', 'isActive'],
|
||
},
|
||
access: {
|
||
read: hasCommunityAccess,
|
||
create: isCommunityManager,
|
||
update: isCommunityManager,
|
||
delete: isCommunityManager,
|
||
},
|
||
fields: [
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'platform',
|
||
type: 'relationship',
|
||
relationTo: 'social-platforms',
|
||
required: true,
|
||
label: 'Plattform',
|
||
admin: { width: '50%' },
|
||
},
|
||
{
|
||
name: 'linkedChannel',
|
||
type: 'relationship',
|
||
relationTo: 'youtube-channels',
|
||
label: 'Verknüpfter YouTube-Kanal',
|
||
admin: {
|
||
width: '50%',
|
||
description: 'Für Zuordnung zu Brand/Kanal',
|
||
},
|
||
},
|
||
],
|
||
},
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'displayName',
|
||
type: 'text',
|
||
required: true,
|
||
label: 'Anzeigename',
|
||
admin: {
|
||
width: '50%',
|
||
placeholder: 'BlogWoman YouTube',
|
||
},
|
||
},
|
||
{
|
||
name: 'accountHandle',
|
||
type: 'text',
|
||
label: 'Handle / Username',
|
||
admin: {
|
||
width: '50%',
|
||
placeholder: '@blogwoman',
|
||
},
|
||
},
|
||
],
|
||
},
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'externalId',
|
||
type: 'text',
|
||
label: 'Platform Account ID',
|
||
admin: {
|
||
width: '50%',
|
||
description: 'YouTube Channel ID, LinkedIn URN, etc.',
|
||
},
|
||
},
|
||
{
|
||
name: 'accountUrl',
|
||
type: 'text',
|
||
label: 'Account URL',
|
||
admin: { width: '50%' },
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'isActive',
|
||
type: 'checkbox',
|
||
label: 'Aktiv',
|
||
defaultValue: true,
|
||
admin: {
|
||
position: 'sidebar',
|
||
},
|
||
},
|
||
|
||
// OAuth Credentials (verschlüsselt speichern!)
|
||
{
|
||
name: 'credentials',
|
||
type: 'group',
|
||
label: 'API Credentials',
|
||
admin: {
|
||
description: 'Sensible Daten – nur für Admins sichtbar',
|
||
condition: (data, siblingData, { user }) =>
|
||
Boolean((user as any)?.isSuperAdmin || (user as any)?.is_super_admin),
|
||
},
|
||
fields: [
|
||
{
|
||
name: 'accessToken',
|
||
type: 'text',
|
||
label: 'Access Token',
|
||
admin: {
|
||
description: 'OAuth Access Token',
|
||
},
|
||
},
|
||
{
|
||
name: 'refreshToken',
|
||
type: 'text',
|
||
label: 'Refresh Token',
|
||
},
|
||
{
|
||
name: 'tokenExpiresAt',
|
||
type: 'date',
|
||
label: 'Token Ablauf',
|
||
},
|
||
{
|
||
name: 'apiKey',
|
||
type: 'text',
|
||
label: 'API Key',
|
||
admin: {
|
||
description: 'Für API-Key basierte Auth',
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// Stats (periodisch aktualisiert)
|
||
{
|
||
name: 'stats',
|
||
type: 'group',
|
||
label: 'Account Stats',
|
||
admin: {
|
||
position: 'sidebar',
|
||
},
|
||
fields: [
|
||
{
|
||
name: 'followers',
|
||
type: 'number',
|
||
label: 'Followers/Subscribers',
|
||
admin: { readOnly: true },
|
||
},
|
||
{
|
||
name: 'totalPosts',
|
||
type: 'number',
|
||
label: 'Total Posts/Videos',
|
||
admin: { readOnly: true },
|
||
},
|
||
{
|
||
name: 'lastSyncedAt',
|
||
type: 'date',
|
||
label: 'Letzter Sync',
|
||
admin: { readOnly: true },
|
||
},
|
||
],
|
||
},
|
||
|
||
// Sync Settings
|
||
{
|
||
name: 'syncSettings',
|
||
type: 'group',
|
||
label: 'Sync Settings',
|
||
fields: [
|
||
{
|
||
name: 'autoSyncEnabled',
|
||
type: 'checkbox',
|
||
label: 'Auto-Sync aktiviert',
|
||
defaultValue: true,
|
||
},
|
||
{
|
||
name: 'syncIntervalMinutes',
|
||
type: 'number',
|
||
label: 'Sync-Intervall (Minuten)',
|
||
defaultValue: 15,
|
||
min: 5,
|
||
max: 1440,
|
||
},
|
||
{
|
||
name: 'syncComments',
|
||
type: 'checkbox',
|
||
label: 'Kommentare synchronisieren',
|
||
defaultValue: true,
|
||
},
|
||
{
|
||
name: 'syncDMs',
|
||
type: 'checkbox',
|
||
label: 'DMs synchronisieren',
|
||
defaultValue: false,
|
||
admin: {
|
||
description: 'Nicht alle Plattformen unterstützen DM-API',
|
||
},
|
||
},
|
||
],
|
||
},
|
||
],
|
||
timestamps: true,
|
||
}
|
||
```
|
||
|
||
### 2.3 Community Interactions (`community-interactions`)
|
||
|
||
```typescript
|
||
// src/collections/CommunityInteractions.ts
|
||
|
||
import type { CollectionConfig } from 'payload'
|
||
import {
|
||
hasCommunityAccess,
|
||
isCommunityModeratorOrAbove,
|
||
isCommunityManager,
|
||
canAccessAssignedInteractions
|
||
} from '../lib/communityAccess'
|
||
|
||
export const CommunityInteractions: CollectionConfig = {
|
||
slug: 'community-interactions',
|
||
labels: {
|
||
singular: 'Interaction',
|
||
plural: 'Interactions',
|
||
},
|
||
admin: {
|
||
group: 'Community',
|
||
defaultColumns: ['platform', 'type', 'authorName', '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
|
||
console.log(`🚨 Urgent interaction: ${doc.id}`)
|
||
}
|
||
},
|
||
],
|
||
},
|
||
timestamps: true,
|
||
}
|
||
```
|
||
|
||
### 2.4 Community Templates (`community-templates`)
|
||
|
||
```typescript
|
||
// src/collections/CommunityTemplates.ts
|
||
|
||
import type { CollectionConfig } from 'payload'
|
||
import { isCommunityManager, isCommunityModeratorOrAbove, hasCommunityAccess } from '../lib/communityAccess'
|
||
|
||
export const CommunityTemplates: CollectionConfig = {
|
||
slug: 'community-templates',
|
||
labels: {
|
||
singular: 'Response Template',
|
||
plural: 'Response Templates',
|
||
},
|
||
admin: {
|
||
group: 'Community',
|
||
useAsTitle: 'name',
|
||
defaultColumns: ['name', 'category', 'channel', 'usageCount'],
|
||
},
|
||
access: {
|
||
read: hasCommunityAccess,
|
||
create: isCommunityModeratorOrAbove,
|
||
update: isCommunityModeratorOrAbove,
|
||
delete: isCommunityManager,
|
||
},
|
||
fields: [
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'name',
|
||
type: 'text',
|
||
required: true,
|
||
localized: true,
|
||
label: 'Name',
|
||
admin: { width: '50%' },
|
||
},
|
||
{
|
||
name: 'category',
|
||
type: 'select',
|
||
required: true,
|
||
label: 'Kategorie',
|
||
options: [
|
||
{ label: 'Danke', value: 'thank_you' },
|
||
{ label: 'Frage beantworten', value: 'question_answer' },
|
||
{ label: 'Hotline-Verweis', value: 'redirect_hotline' },
|
||
{ label: 'Medizinischer Disclaimer', value: 'medical_disclaimer' },
|
||
{ label: 'Produkt-Info', value: 'product_info' },
|
||
{ label: 'Content-Verweis', value: 'content_reference' },
|
||
{ label: 'Follow-up', value: 'follow_up' },
|
||
{ label: 'Negatives Feedback', value: 'negative_feedback' },
|
||
{ label: 'Spam-Antwort', value: 'spam_response' },
|
||
{ label: 'Begrüßung', value: 'welcome' },
|
||
],
|
||
admin: { width: '50%' },
|
||
},
|
||
],
|
||
},
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'channel',
|
||
type: 'relationship',
|
||
relationTo: 'youtube-channels',
|
||
label: 'Kanal (optional)',
|
||
admin: {
|
||
width: '50%',
|
||
description: 'Leer = für alle Kanäle',
|
||
},
|
||
},
|
||
{
|
||
name: 'platforms',
|
||
type: 'relationship',
|
||
relationTo: 'social-platforms',
|
||
hasMany: true,
|
||
label: 'Plattformen',
|
||
admin: {
|
||
width: '50%',
|
||
description: 'Leer = für alle Plattformen',
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// Template Text mit Variablen
|
||
{
|
||
name: 'template',
|
||
type: 'textarea',
|
||
required: true,
|
||
localized: true,
|
||
label: 'Template Text',
|
||
admin: {
|
||
rows: 6,
|
||
description: 'Variablen: {{author_name}}, {{video_title}}, {{channel_name}}, {{hotline_number}}',
|
||
},
|
||
},
|
||
|
||
// Verfügbare Variablen
|
||
{
|
||
name: 'variables',
|
||
type: 'array',
|
||
label: 'Verfügbare Variablen',
|
||
admin: {
|
||
description: 'Dokumentation der Variablen in diesem Template',
|
||
},
|
||
fields: [
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'variable',
|
||
type: 'text',
|
||
required: true,
|
||
label: 'Variable',
|
||
admin: {
|
||
width: '30%',
|
||
placeholder: '{{author_name}}',
|
||
},
|
||
},
|
||
{
|
||
name: 'description',
|
||
type: 'text',
|
||
label: 'Beschreibung',
|
||
admin: {
|
||
width: '50%',
|
||
placeholder: 'Name des Kommentar-Autors',
|
||
},
|
||
},
|
||
{
|
||
name: 'defaultValue',
|
||
type: 'text',
|
||
label: 'Fallback',
|
||
admin: {
|
||
width: '20%',
|
||
},
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
|
||
// Auto-Suggest Keywords
|
||
{
|
||
name: 'autoSuggestKeywords',
|
||
type: 'array',
|
||
label: 'Auto-Suggest Keywords',
|
||
admin: {
|
||
description: 'Bei diesen Keywords wird das Template vorgeschlagen',
|
||
},
|
||
fields: [
|
||
{
|
||
name: 'keyword',
|
||
type: 'text',
|
||
required: true,
|
||
label: 'Keyword',
|
||
},
|
||
],
|
||
},
|
||
|
||
// Flags
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'requiresReview',
|
||
type: 'checkbox',
|
||
label: 'Review erforderlich',
|
||
admin: {
|
||
width: '33%',
|
||
description: 'Für medizinische Antworten',
|
||
},
|
||
},
|
||
{
|
||
name: 'isActive',
|
||
type: 'checkbox',
|
||
label: 'Aktiv',
|
||
defaultValue: true,
|
||
admin: { width: '33%' },
|
||
},
|
||
{
|
||
name: 'usageCount',
|
||
type: 'number',
|
||
label: 'Verwendungen',
|
||
defaultValue: 0,
|
||
admin: {
|
||
width: '33%',
|
||
readOnly: true,
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// Beispiel-Output
|
||
{
|
||
name: 'exampleOutput',
|
||
type: 'textarea',
|
||
label: 'Beispiel-Output',
|
||
admin: {
|
||
rows: 3,
|
||
description: 'So sieht die Antwort mit ausgefüllten Variablen aus',
|
||
},
|
||
},
|
||
],
|
||
timestamps: true,
|
||
}
|
||
```
|
||
|
||
### 2.5 Community Rules (`community-rules`)
|
||
|
||
```typescript
|
||
// src/collections/CommunityRules.ts
|
||
|
||
import type { CollectionConfig } from 'payload'
|
||
import { isCommunityManager, hasCommunityAccess } from '../lib/communityAccess'
|
||
|
||
export const CommunityRules: CollectionConfig = {
|
||
slug: 'community-rules',
|
||
labels: {
|
||
singular: 'Community Rule',
|
||
plural: 'Community Rules',
|
||
},
|
||
admin: {
|
||
group: 'Community',
|
||
useAsTitle: 'name',
|
||
defaultColumns: ['name', 'trigger.type', 'isActive', 'priority'],
|
||
},
|
||
access: {
|
||
read: hasCommunityAccess,
|
||
create: isCommunityManager,
|
||
update: isCommunityManager,
|
||
delete: isCommunityManager,
|
||
},
|
||
fields: [
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'name',
|
||
type: 'text',
|
||
required: true,
|
||
label: 'Name',
|
||
admin: { width: '50%' },
|
||
},
|
||
{
|
||
name: 'priority',
|
||
type: 'number',
|
||
required: true,
|
||
defaultValue: 100,
|
||
label: 'Priorität',
|
||
admin: {
|
||
width: '25%',
|
||
description: 'Niedrigere Zahl = höhere Priorität',
|
||
},
|
||
},
|
||
{
|
||
name: 'isActive',
|
||
type: 'checkbox',
|
||
label: 'Aktiv',
|
||
defaultValue: true,
|
||
admin: { width: '25%' },
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'description',
|
||
type: 'textarea',
|
||
label: 'Beschreibung',
|
||
admin: { rows: 2 },
|
||
},
|
||
|
||
// Scope
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'channel',
|
||
type: 'relationship',
|
||
relationTo: 'youtube-channels',
|
||
label: 'Kanal (optional)',
|
||
admin: {
|
||
width: '50%',
|
||
description: 'Leer = alle Kanäle',
|
||
},
|
||
},
|
||
{
|
||
name: 'platforms',
|
||
type: 'relationship',
|
||
relationTo: 'social-platforms',
|
||
hasMany: true,
|
||
label: 'Plattformen',
|
||
admin: {
|
||
width: '50%',
|
||
description: 'Leer = alle Plattformen',
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// Trigger
|
||
{
|
||
name: 'trigger',
|
||
type: 'group',
|
||
label: 'Trigger',
|
||
fields: [
|
||
{
|
||
name: 'type',
|
||
type: 'select',
|
||
required: true,
|
||
label: 'Trigger-Typ',
|
||
options: [
|
||
{ label: 'Keyword Match', value: 'keyword' },
|
||
{ label: 'Sentiment', value: 'sentiment' },
|
||
{ label: 'Frage erkannt', value: 'question_detected' },
|
||
{ label: 'Medizinisch erkannt', value: 'medical_detected' },
|
||
{ label: 'Influencer', value: 'influencer' },
|
||
{ label: 'Alle neuen', value: 'all_new' },
|
||
{ label: 'Enthält Link', value: 'contains_link' },
|
||
{ label: 'Enthält Email', value: 'contains_email' },
|
||
],
|
||
},
|
||
{
|
||
name: 'keywords',
|
||
type: 'array',
|
||
label: 'Keywords',
|
||
admin: {
|
||
condition: (data, siblingData) => siblingData?.type === 'keyword',
|
||
},
|
||
fields: [
|
||
{
|
||
name: 'keyword',
|
||
type: 'text',
|
||
required: true,
|
||
label: 'Keyword',
|
||
},
|
||
{
|
||
name: 'matchType',
|
||
type: 'select',
|
||
label: 'Match-Typ',
|
||
options: [
|
||
{ label: 'Enthält', value: 'contains' },
|
||
{ label: 'Exakt', value: 'exact' },
|
||
{ label: 'Regex', value: 'regex' },
|
||
],
|
||
defaultValue: 'contains',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'sentimentValues',
|
||
type: 'select',
|
||
hasMany: true,
|
||
label: 'Sentiment-Werte',
|
||
options: [
|
||
{ label: 'Positiv', value: 'positive' },
|
||
{ label: 'Negativ', value: 'negative' },
|
||
{ label: 'Neutral', value: 'neutral' },
|
||
{ label: 'Frage', value: 'question' },
|
||
],
|
||
admin: {
|
||
condition: (data, siblingData) => siblingData?.type === 'sentiment',
|
||
},
|
||
},
|
||
{
|
||
name: 'influencerMinFollowers',
|
||
type: 'number',
|
||
label: 'Min. Follower',
|
||
defaultValue: 10000,
|
||
admin: {
|
||
condition: (data, siblingData) => siblingData?.type === 'influencer',
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// Actions
|
||
{
|
||
name: 'actions',
|
||
type: 'array',
|
||
label: 'Aktionen',
|
||
required: true,
|
||
minRows: 1,
|
||
fields: [
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'action',
|
||
type: 'select',
|
||
required: true,
|
||
label: 'Aktion',
|
||
options: [
|
||
{ label: 'Priorität setzen', value: 'set_priority' },
|
||
{ label: 'Zuweisen', value: 'assign_to' },
|
||
{ label: 'Flag setzen', value: 'set_flag' },
|
||
{ label: 'Template vorschlagen', value: 'suggest_template' },
|
||
{ label: 'Notification senden', value: 'send_notification' },
|
||
{ label: 'Medical Flag', value: 'flag_medical' },
|
||
{ label: 'Eskalieren', value: 'escalate' },
|
||
{ label: 'Als Spam markieren', value: 'mark_spam' },
|
||
{ label: 'Deadline setzen', value: 'set_deadline' },
|
||
],
|
||
admin: { width: '40%' },
|
||
},
|
||
{
|
||
name: 'value',
|
||
type: 'text',
|
||
label: 'Wert',
|
||
admin: {
|
||
width: '40%',
|
||
description: 'Priority: urgent/high/normal/low, Deadline: Stunden',
|
||
},
|
||
},
|
||
{
|
||
name: 'targetUser',
|
||
type: 'relationship',
|
||
relationTo: 'users',
|
||
label: 'User',
|
||
admin: {
|
||
width: '20%',
|
||
condition: (data, siblingData) =>
|
||
['assign_to', 'send_notification'].includes(siblingData?.action || ''),
|
||
},
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'targetTemplate',
|
||
type: 'relationship',
|
||
relationTo: 'community-templates',
|
||
label: 'Template',
|
||
admin: {
|
||
condition: (data, siblingData) => siblingData?.action === 'suggest_template',
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// Stats
|
||
{
|
||
name: 'stats',
|
||
type: 'group',
|
||
label: 'Statistiken',
|
||
fields: [
|
||
{
|
||
type: 'row',
|
||
fields: [
|
||
{
|
||
name: 'timesTriggered',
|
||
type: 'number',
|
||
label: 'Ausgelöst',
|
||
defaultValue: 0,
|
||
admin: { width: '50%', readOnly: true },
|
||
},
|
||
{
|
||
name: 'lastTriggeredAt',
|
||
type: 'date',
|
||
label: 'Zuletzt ausgelöst',
|
||
admin: { width: '50%', readOnly: true },
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
timestamps: true,
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Teil 3: Users Collection erweitern
|
||
|
||
### 3.1 Community-Rolle zu Users hinzufügen
|
||
|
||
```typescript
|
||
// Ergänzung in src/collections/Users.ts (zu den bestehenden YouTube-Feldern)
|
||
|
||
{
|
||
name: 'communityRole',
|
||
type: 'select',
|
||
label: 'Community-Rolle',
|
||
defaultValue: 'none',
|
||
options: [
|
||
{ label: 'Keine', value: 'none' },
|
||
{ label: 'Viewer', value: 'viewer' },
|
||
{ label: 'Moderator', value: 'moderator' },
|
||
{ label: 'Manager', value: 'manager' },
|
||
],
|
||
admin: {
|
||
position: 'sidebar',
|
||
description: 'Zugriff auf Community Management Features',
|
||
},
|
||
},
|
||
```
|
||
|
||
---
|
||
|
||
## Teil 4: YouTube API Integration
|
||
|
||
### 4.1 YouTube API Client
|
||
|
||
```typescript
|
||
// src/lib/integrations/youtube/YouTubeClient.ts
|
||
|
||
import { google, youtube_v3 } from 'googleapis'
|
||
import type { Payload } from 'payload'
|
||
|
||
interface YouTubeCredentials {
|
||
clientId: string
|
||
clientSecret: string
|
||
accessToken: string
|
||
refreshToken: string
|
||
}
|
||
|
||
interface CommentThread {
|
||
id: string
|
||
snippet: {
|
||
videoId: string
|
||
topLevelComment: {
|
||
id: string
|
||
snippet: {
|
||
textDisplay: string
|
||
textOriginal: string
|
||
authorDisplayName: string
|
||
authorProfileImageUrl: string
|
||
authorChannelUrl: string
|
||
authorChannelId: { value: string }
|
||
likeCount: number
|
||
publishedAt: string
|
||
updatedAt: string
|
||
}
|
||
}
|
||
totalReplyCount: number
|
||
}
|
||
}
|
||
|
||
export class YouTubeClient {
|
||
private youtube: youtube_v3.Youtube
|
||
private oauth2Client: any
|
||
private payload: Payload
|
||
|
||
constructor(credentials: YouTubeCredentials, payload: Payload) {
|
||
this.payload = payload
|
||
|
||
this.oauth2Client = new google.auth.OAuth2(
|
||
credentials.clientId,
|
||
credentials.clientSecret,
|
||
process.env.YOUTUBE_REDIRECT_URI
|
||
)
|
||
|
||
this.oauth2Client.setCredentials({
|
||
access_token: credentials.accessToken,
|
||
refresh_token: credentials.refreshToken,
|
||
})
|
||
|
||
this.youtube = google.youtube({
|
||
version: 'v3',
|
||
auth: this.oauth2Client,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Alle Kommentar-Threads eines Videos abrufen
|
||
*/
|
||
async getVideoComments(
|
||
videoId: string,
|
||
pageToken?: string,
|
||
maxResults: number = 100
|
||
): Promise<{
|
||
comments: CommentThread[]
|
||
nextPageToken?: string
|
||
}> {
|
||
try {
|
||
const response = await this.youtube.commentThreads.list({
|
||
part: ['snippet', 'replies'],
|
||
videoId: videoId,
|
||
maxResults: maxResults,
|
||
pageToken: pageToken,
|
||
order: 'time',
|
||
textFormat: 'plainText',
|
||
})
|
||
|
||
return {
|
||
comments: response.data.items as CommentThread[],
|
||
nextPageToken: response.data.nextPageToken || undefined,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching comments:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Alle Kommentare eines Kanals abrufen (alle Videos)
|
||
*/
|
||
async getChannelComments(
|
||
channelId: string,
|
||
publishedAfter?: Date,
|
||
maxResults: number = 100
|
||
): Promise<CommentThread[]> {
|
||
try {
|
||
const params: any = {
|
||
part: ['snippet', 'replies'],
|
||
allThreadsRelatedToChannelId: channelId,
|
||
maxResults: maxResults,
|
||
order: 'time',
|
||
textFormat: 'plainText',
|
||
}
|
||
|
||
if (publishedAfter) {
|
||
params.publishedAfter = publishedAfter.toISOString()
|
||
}
|
||
|
||
const response = await this.youtube.commentThreads.list(params)
|
||
return response.data.items as CommentThread[]
|
||
} catch (error) {
|
||
console.error('Error fetching channel comments:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Auf einen Kommentar antworten
|
||
*/
|
||
async replyToComment(parentCommentId: string, text: string): Promise<string> {
|
||
try {
|
||
const response = await this.youtube.comments.insert({
|
||
part: ['snippet'],
|
||
requestBody: {
|
||
snippet: {
|
||
parentId: parentCommentId,
|
||
textOriginal: text,
|
||
},
|
||
},
|
||
})
|
||
|
||
return response.data.id!
|
||
} catch (error) {
|
||
console.error('Error replying to comment:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Kommentar als Spam markieren
|
||
*/
|
||
async markAsSpam(commentId: string): Promise<void> {
|
||
try {
|
||
await this.youtube.comments.setModerationStatus({
|
||
id: [commentId],
|
||
moderationStatus: 'rejected',
|
||
})
|
||
} catch (error) {
|
||
console.error('Error marking as spam:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Kommentar löschen
|
||
*/
|
||
async deleteComment(commentId: string): Promise<void> {
|
||
try {
|
||
await this.youtube.comments.delete({
|
||
id: commentId,
|
||
})
|
||
} catch (error) {
|
||
console.error('Error deleting comment:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Access Token erneuern
|
||
*/
|
||
async refreshAccessToken(): Promise<{
|
||
accessToken: string
|
||
expiresAt: Date
|
||
}> {
|
||
try {
|
||
const { credentials } = await this.oauth2Client.refreshAccessToken()
|
||
|
||
return {
|
||
accessToken: credentials.access_token!,
|
||
expiresAt: new Date(credentials.expiry_date!),
|
||
}
|
||
} catch (error) {
|
||
console.error('Error refreshing token:', error)
|
||
throw error
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.2 Claude Analysis Service
|
||
|
||
```typescript
|
||
// src/lib/integrations/claude/ClaudeAnalysisService.ts
|
||
|
||
import Anthropic from '@anthropic-ai/sdk'
|
||
|
||
interface CommentAnalysis {
|
||
sentiment: 'positive' | 'neutral' | 'negative' | 'question' | 'gratitude' | 'frustration'
|
||
sentimentScore: number
|
||
confidence: number
|
||
topics: string[]
|
||
language: string
|
||
isMedicalQuestion: boolean
|
||
requiresEscalation: boolean
|
||
isSpam: boolean
|
||
suggestedReply?: string
|
||
}
|
||
|
||
export class ClaudeAnalysisService {
|
||
private client: Anthropic
|
||
|
||
constructor() {
|
||
this.client = new Anthropic({
|
||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Kommentar analysieren
|
||
*/
|
||
async analyzeComment(message: string): Promise<CommentAnalysis> {
|
||
const systemPrompt = `Du bist ein Analyse-Assistent für YouTube-Kommentare eines deutschen Healthcare-Unternehmens (Medizinische Zweitmeinung) und eines Lifestyle-Kanals.
|
||
|
||
Analysiere den Kommentar und gib ein JSON-Objekt zurück mit:
|
||
- sentiment: "positive", "neutral", "negative", "question", "gratitude", "frustration"
|
||
- sentimentScore: Zahl von -1 (sehr negativ) bis 1 (sehr positiv)
|
||
- confidence: Konfidenz der Analyse 0-100
|
||
- topics: Array von erkannten Themen (max 3)
|
||
- language: ISO-639-1 Sprachcode (z.B. "de", "en")
|
||
- isMedicalQuestion: true wenn es um medizinische Fragen/Gesundheit geht
|
||
- requiresEscalation: true wenn dringend/kritisch/negativ oder Beschwerden
|
||
- isSpam: true wenn Spam/Werbung/Bot
|
||
- suggestedReply: Kurzer Antwortvorschlag auf Deutsch (optional, nur wenn sinnvoll)
|
||
|
||
WICHTIG für isMedicalQuestion:
|
||
- Fragen zu Intensivmedizin, Diagnosen, Behandlungen, Medikamenten = true
|
||
- Fragen zu Angehörigen von Patienten = true
|
||
- Allgemeine Lifestyle-Fragen (Mode, Zeitmanagement) = false
|
||
|
||
Antworte NUR mit dem JSON-Objekt, kein anderer Text.`
|
||
|
||
try {
|
||
const response = await this.client.messages.create({
|
||
model: 'claude-3-haiku-20240307',
|
||
max_tokens: 500,
|
||
system: systemPrompt,
|
||
messages: [
|
||
{
|
||
role: 'user',
|
||
content: `Analysiere diesen Kommentar:\n\n"${message}"`,
|
||
},
|
||
],
|
||
})
|
||
|
||
const content = response.content[0]
|
||
if (content.type !== 'text') {
|
||
throw new Error('Unexpected response type')
|
||
}
|
||
|
||
const analysis = JSON.parse(content.text) as CommentAnalysis
|
||
return analysis
|
||
} catch (error) {
|
||
console.error('Claude analysis error:', error)
|
||
|
||
// Fallback bei Fehler
|
||
return {
|
||
sentiment: 'neutral',
|
||
sentimentScore: 0,
|
||
confidence: 0,
|
||
topics: [],
|
||
language: 'de',
|
||
isMedicalQuestion: false,
|
||
requiresEscalation: false,
|
||
isSpam: false,
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Antwort-Vorschlag generieren
|
||
*/
|
||
async generateReply(
|
||
comment: string,
|
||
context: {
|
||
videoTitle: string
|
||
channelName: string
|
||
isBusinessChannel: boolean
|
||
template?: string
|
||
}
|
||
): Promise<string> {
|
||
const systemPrompt = `Du bist ein Community-Manager für ${context.channelName}.
|
||
${context.isBusinessChannel
|
||
? 'Dies ist ein Healthcare-Kanal für medizinische Zweitmeinungen. Antworten müssen professionell sein und dürfen keine medizinischen Ratschläge geben. Bei medizinischen Fragen immer auf die Hotline verweisen.'
|
||
: 'Dies ist ein Lifestyle-Kanal für berufstätige Mütter. Antworten sollten warm, persönlich und hilfreich sein.'
|
||
}
|
||
|
||
Erstelle eine passende, kurze Antwort auf den Kommentar.
|
||
- Maximal 2-3 Sätze
|
||
- Persönlich und authentisch
|
||
- Auf Deutsch
|
||
${context.template ? `\nVerwende dieses Template als Basis:\n${context.template}` : ''}
|
||
|
||
Antworte NUR mit dem Antworttext, kein anderer Text.`
|
||
|
||
try {
|
||
const response = await this.client.messages.create({
|
||
model: 'claude-3-haiku-20240307',
|
||
max_tokens: 200,
|
||
system: systemPrompt,
|
||
messages: [
|
||
{
|
||
role: 'user',
|
||
content: `Video: "${context.videoTitle}"\n\nKommentar: "${comment}"\n\nErstelle eine Antwort:`,
|
||
},
|
||
],
|
||
})
|
||
|
||
const content = response.content[0]
|
||
if (content.type !== 'text') {
|
||
throw new Error('Unexpected response type')
|
||
}
|
||
|
||
return content.text.trim()
|
||
} catch (error) {
|
||
console.error('Claude reply generation error:', error)
|
||
throw error
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Teil 5: API Endpoints
|
||
|
||
### 5.1 Comments Sync Endpoint
|
||
|
||
```typescript
|
||
// src/app/(payload)/api/community/sync-comments/route.ts
|
||
|
||
import { NextRequest, NextResponse } from 'next/server'
|
||
import { getPayload } from 'payload'
|
||
import config from '@payload-config'
|
||
import { CommentsSyncService } from '@/lib/integrations/youtube/CommentsSyncService'
|
||
|
||
export async function POST(req: NextRequest) {
|
||
try {
|
||
const payload = await getPayload({ config })
|
||
const body = await req.json()
|
||
|
||
const { socialAccountId, sinceDate, maxComments, analyzeWithAI } = body
|
||
|
||
if (!socialAccountId) {
|
||
return NextResponse.json(
|
||
{ error: 'socialAccountId required' },
|
||
{ status: 400 }
|
||
)
|
||
}
|
||
|
||
const syncService = new CommentsSyncService(payload)
|
||
|
||
const result = await syncService.syncComments({
|
||
socialAccountId,
|
||
sinceDate: sinceDate ? new Date(sinceDate) : undefined,
|
||
maxComments: maxComments || 100,
|
||
analyzeWithAI: analyzeWithAI ?? true,
|
||
})
|
||
|
||
return NextResponse.json(result)
|
||
} catch (error: any) {
|
||
console.error('Sync error:', error)
|
||
return NextResponse.json(
|
||
{ error: error.message },
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.2 Reply Endpoint
|
||
|
||
```typescript
|
||
// src/app/(payload)/api/community/reply/route.ts
|
||
|
||
import { NextRequest, NextResponse } from 'next/server'
|
||
import { getPayload } from 'payload'
|
||
import config from '@payload-config'
|
||
import { YouTubeClient } from '@/lib/integrations/youtube/YouTubeClient'
|
||
|
||
export async function POST(req: NextRequest) {
|
||
try {
|
||
const payload = await getPayload({ config })
|
||
const body = await req.json()
|
||
|
||
const { interactionId, replyText, templateId } = body
|
||
|
||
if (!interactionId || !replyText) {
|
||
return NextResponse.json(
|
||
{ error: 'interactionId and replyText required' },
|
||
{ status: 400 }
|
||
)
|
||
}
|
||
|
||
// 1. Interaction laden
|
||
const interaction = await payload.findByID({
|
||
collection: 'community-interactions',
|
||
id: interactionId,
|
||
depth: 2,
|
||
})
|
||
|
||
if (!interaction) {
|
||
return NextResponse.json(
|
||
{ error: 'Interaction not found' },
|
||
{ status: 404 }
|
||
)
|
||
}
|
||
|
||
// 2. YouTube Client initialisieren
|
||
const account = interaction.socialAccount as any
|
||
const youtubeClient = new YouTubeClient({
|
||
clientId: process.env.YOUTUBE_CLIENT_ID!,
|
||
clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
|
||
accessToken: account.credentials.accessToken,
|
||
refreshToken: account.credentials.refreshToken,
|
||
}, payload)
|
||
|
||
// 3. Reply senden
|
||
const replyId = await youtubeClient.replyToComment(
|
||
interaction.externalId,
|
||
replyText
|
||
)
|
||
|
||
// 4. Interaction aktualisieren
|
||
await payload.update({
|
||
collection: 'community-interactions',
|
||
id: interactionId,
|
||
data: {
|
||
status: 'replied',
|
||
response: {
|
||
text: replyText,
|
||
usedTemplate: templateId || null,
|
||
sentAt: new Date().toISOString(),
|
||
externalReplyId: replyId,
|
||
},
|
||
},
|
||
})
|
||
|
||
// 5. Template Usage Counter erhöhen
|
||
if (templateId) {
|
||
const template = await payload.findByID({
|
||
collection: 'community-templates',
|
||
id: templateId,
|
||
})
|
||
await payload.update({
|
||
collection: 'community-templates',
|
||
id: templateId,
|
||
data: {
|
||
usageCount: ((template as any).usageCount || 0) + 1,
|
||
},
|
||
})
|
||
}
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
replyId,
|
||
})
|
||
} catch (error: any) {
|
||
console.error('Reply error:', error)
|
||
return NextResponse.json(
|
||
{ error: error.message },
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Teil 6: Environment Variables
|
||
|
||
```bash
|
||
# .env - Neue Variablen für Community Phase 1
|
||
|
||
# YouTube API (Google Cloud Console)
|
||
YOUTUBE_CLIENT_ID=your-client-id.apps.googleusercontent.com
|
||
YOUTUBE_CLIENT_SECRET=your-client-secret
|
||
YOUTUBE_REDIRECT_URI=https://pl.porwoll.tech/api/youtube/callback
|
||
|
||
# Claude API (Anthropic) - bereits vorhanden für andere Features
|
||
ANTHROPIC_API_KEY=sk-ant-api03-...
|
||
|
||
# Optional: Für spätere Phasen vorbereitet
|
||
LINKEDIN_CLIENT_ID=
|
||
LINKEDIN_CLIENT_SECRET=
|
||
INSTAGRAM_ACCESS_TOKEN=
|
||
FACEBOOK_ACCESS_TOKEN=
|
||
```
|
||
|
||
---
|
||
|
||
## Teil 7: payload.config.ts Ergänzungen
|
||
|
||
```typescript
|
||
// payload.config.ts - Ergänzungen für Community Phase 1
|
||
|
||
import { SocialPlatforms } from './collections/SocialPlatforms'
|
||
import { SocialAccounts } from './collections/SocialAccounts'
|
||
import { CommunityInteractions } from './collections/CommunityInteractions'
|
||
import { CommunityTemplates } from './collections/CommunityTemplates'
|
||
import { CommunityRules } from './collections/CommunityRules'
|
||
|
||
export default buildConfig({
|
||
// ...
|
||
collections: [
|
||
// Existing YouTube Collections
|
||
YouTubeChannels,
|
||
YouTubeContent,
|
||
YtSeries,
|
||
YtTasks,
|
||
YtNotifications,
|
||
YtBatches,
|
||
YtMonthlyGoals,
|
||
YtScriptTemplates,
|
||
YtChecklistTemplates,
|
||
|
||
// NEW: Community Collections
|
||
SocialPlatforms,
|
||
SocialAccounts,
|
||
CommunityInteractions,
|
||
CommunityTemplates,
|
||
CommunityRules,
|
||
],
|
||
// ...
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## Teil 8: Migration
|
||
|
||
Die Migration muss folgende Tabellen erstellen:
|
||
- `social_platforms` + `social_platforms_interaction_types` + `social_platforms_api_config_scopes`
|
||
- `social_accounts`
|
||
- `community_interactions` + `community_interactions_attachments` + `community_interactions_analysis_topics`
|
||
- `community_templates` + `community_templates_locales` + `community_templates_variables` + `community_templates_auto_suggest_keywords`
|
||
- `community_rules` + `community_rules_trigger_keywords` + `community_rules_actions`
|
||
- `payload_locked_documents_rels` erweitern um alle neuen Collection-IDs
|
||
|
||
**WICHTIG:** Siehe `CLAUDE.md` Abschnitt "KRITISCH: Neue Collections hinzufügen" für das korrekte Migrations-Muster.
|
||
|
||
---
|
||
|
||
## Zusammenfassung
|
||
|
||
### Neue Collections (5)
|
||
1. `social-platforms` – Plattform-Definitionen
|
||
2. `social-accounts` – Account-Verknüpfungen
|
||
3. `community-interactions` – Alle Kommentare/DMs
|
||
4. `community-templates` – Antwort-Vorlagen
|
||
5. `community-rules` – Auto-Regeln
|
||
|
||
### Neue Lib-Dateien (3)
|
||
1. `src/lib/communityAccess.ts` – Access Control
|
||
2. `src/lib/integrations/youtube/YouTubeClient.ts` – YouTube API
|
||
3. `src/lib/integrations/claude/ClaudeAnalysisService.ts` – AI Analyse
|
||
|
||
### API Endpoints (2)
|
||
1. `POST /api/community/sync-comments` – Manueller Sync
|
||
2. `POST /api/community/reply` – Antwort senden
|
||
|
||
### User-Feld (1)
|
||
1. `communityRole` in Users Collection
|
||
|
||
### Abhängigkeiten zu bestehendem Code
|
||
- `youtube-channels` Collection (linkedChannel Referenz)
|
||
- `youtube-content` Collection (linkedContent Referenz)
|
||
- `users` Collection (assignedTo, sentBy Referenzen)
|
||
- Bestehende YouTube-Access-Rollen werden berücksichtigt
|