# YouTube Operations Hub v2 – Vollständiges Konzept ## Übersicht der Erweiterungen ### Neue Features: 1. **Script Editor** – Custom Lexical Rich-Text-Editor mit Section-Blöcken 2. **Kalenderansichten** – Content Calendar + Posting Calendar direkt in Payload 3. **Batch-Planung** – Detaillierte Produktions-Batches mit Kapazitätsplanung 4. **Monthly Goals Dashboard** – KPI-Tracking pro Kanal und Monat 5. **Upload Workflow** – Strukturierte Checklisten mit Fortschrittsanzeige --- ## 1. Script Editor – Custom Lexical Blocks ### 1.1 Konzept Payload 3.x verwendet Lexical als Rich-Text-Editor. Wir erstellen Custom Blocks für die Skript-Struktur: ``` ┌─────────────────────────────────────────────────────────────┐ │ 📝 Script Editor │ ├─────────────────────────────────────────────────────────────┤ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 🎬 HOOK [0-10 Sek] [Edit]│ │ │ │ "Board-Meeting in 20 Minuten. Der Morgen war │ │ │ │ Chaos, die Kinder waren anstrengend..." │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 🏷️ INTRO-IDENT [1-2 Sek] [Edit]│ │ │ │ [GRFI Serien-Badge einblenden] │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 📖 CONTEXT [45-60 Sek] [Edit]│ │ │ │ "Kennst du das? Du wachst auf mit einem Plan..." │ │ │ │ │ │ │ │ 🎥 B-Roll: Morgenroutine, Meetings │ │ │ │ 📝 Overlay: "System schlägt Motivation" │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 📚 TEIL 1: OUTFIT-LOGIK [2-3 Min] [Edit]│ │ │ │ "Fangen wir mit der Basis an: dem Outfit..." │ │ │ │ │ │ │ │ 🎥 B-Roll: Outfit zeigen, 3 Kombinationen │ │ │ │ 📝 Overlay: "3 Kombinationen = 0 Entscheidungen" │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ [+ Section hinzufügen ▾] │ │ • Hook │ │ • Intro/Ident │ │ • Context │ │ • Content Part │ │ • Summary │ │ • CTA │ │ • Outro │ │ • Disclaimer │ └─────────────────────────────────────────────────────────────┘ ``` ### 1.2 Script Section Block Schema ```typescript // src/blocks/ScriptSection.ts import { Block } from 'payload/types' export const ScriptSectionBlock: Block = { slug: 'scriptSection', labels: { singular: 'Script Section', plural: 'Script Sections', }, fields: [ { name: 'sectionType', type: 'select', required: true, options: [ { label: '🎬 Hook', value: 'hook' }, { label: '🏷️ Intro/Ident', value: 'intro_ident' }, { label: '📖 Context', value: 'context' }, { label: '📚 Content Part', value: 'content_part' }, { label: '📋 Summary', value: 'summary' }, { label: '📢 CTA', value: 'cta' }, { label: '🎬 Outro', value: 'outro' }, { label: '⚠️ Disclaimer', value: 'disclaimer' }, ], admin: { description: 'Art der Script-Section', }, }, { name: 'sectionTitle', type: 'text', label: 'Section-Titel', admin: { placeholder: 'z.B. "TEIL 1: OUTFIT-LOGIK"', condition: (data, siblingData) => siblingData?.sectionType === 'content_part', }, }, { name: 'duration', type: 'text', label: 'Dauer', admin: { placeholder: 'z.B. "2-3 Min" oder "45-60 Sek"', width: '25%', }, }, { name: 'spokenText', type: 'richText', label: 'Gesprochener Text', required: true, admin: { description: 'Der Text, den Caroline spricht', }, }, { name: 'bRollInstructions', type: 'array', label: 'B-Roll Anweisungen', labels: { singular: 'B-Roll', plural: 'B-Roll Anweisungen', }, fields: [ { name: 'instruction', type: 'text', required: true, admin: { placeholder: 'z.B. "Outfit zeigen", "Kleiderschrank-Aufnahme"', }, }, { name: 'timestamp', type: 'text', admin: { placeholder: 'Optional: "bei 0:45"', width: '30%', }, }, ], }, { name: 'textOverlays', type: 'array', label: 'Text-Overlays', labels: { singular: 'Overlay', plural: 'Text-Overlays', }, fields: [ { name: 'text', type: 'text', required: true, admin: { placeholder: 'z.B. "3 Kombinationen = 0 Entscheidungen"', }, }, { name: 'style', type: 'select', defaultValue: 'standard', options: [ { label: 'Standard', value: 'standard' }, { label: 'Highlight', value: 'highlight' }, { label: 'Quote', value: 'quote' }, { label: 'Statistik', value: 'statistic' }, { label: 'Liste', value: 'list' }, ], }, ], }, { name: 'visualNotes', type: 'textarea', label: 'Visuelle Notizen', admin: { placeholder: 'Zusätzliche Anweisungen für Schnitt/Grafik', rows: 2, }, }, ], } ``` ### 1.3 Script Editor Field in YouTube Content ```typescript // In YouTubeContent.ts { name: 'script', type: 'blocks', label: 'Script', labels: { singular: 'Section', plural: 'Script Sections', }, blocks: [ScriptSectionBlock], admin: { description: 'Strukturiertes Video-Script mit Sections', }, } ``` ### 1.4 Script Templates ```typescript // src/collections/ScriptTemplates.ts export const ScriptTemplates: CollectionConfig = { slug: 'yt-script-templates', labels: { singular: 'Script Template', plural: 'Script Templates', }, admin: { group: 'YouTube', useAsTitle: 'name', defaultColumns: ['name', 'series', 'format', 'updatedAt'], }, fields: [ { name: 'name', type: 'text', required: true, localized: true, // z.B. "GRFI Longform Template", "M2M Short Template" }, { name: 'series', type: 'text', required: true, // z.B. "GRFI", "M2M", "SPARK" }, { name: 'format', type: 'select', required: true, options: [ { label: 'Short (45-60s)', value: 'short' }, { label: 'Longform (8-16 Min)', value: 'longform' }, ], }, { name: 'channel', type: 'relationship', relationTo: 'youtube-channels', required: true, }, { name: 'templateSections', type: 'blocks', blocks: [ScriptSectionBlock], // Vorgefüllte Section-Struktur }, { name: 'notes', type: 'textarea', localized: true, admin: { description: 'Hinweise zur Verwendung dieses Templates', }, }, ], } ``` --- ## 2. Kalenderansichten in Payload Admin ### 2.1 Custom Admin Views Struktur ``` src/ ├── app/(payload)/admin/ │ ├── [[...segments]]/ │ │ └── page.tsx # Standard Payload Admin │ └── views/ │ └── youtube/ │ ├── content-calendar/ │ │ └── page.tsx # Content/Production Calendar │ ├── posting-calendar/ │ │ └── page.tsx # Publishing Calendar │ └── batch-overview/ │ └── page.tsx # Batch Dashboard ``` ### 2.2 Content Calendar View ```typescript // src/app/(payload)/admin/views/youtube/content-calendar/page.tsx 'use client' import React, { useState, useEffect } from 'react' import { useConfig } from '@payloadcms/ui' import { format, startOfMonth, endOfMonth, eachDayOfInterval, startOfWeek, endOfWeek, isSameMonth, isSameDay } from 'date-fns' import { de } from 'date-fns/locale' interface CalendarVideo { id: string title: string format: 'short' | 'longform' series: string status: string productionDate?: string scheduledPublishDate?: string channel: { name: string slug: string } } export default function ContentCalendarView() { const [currentMonth, setCurrentMonth] = useState(new Date()) const [videos, setVideos] = useState([]) const [selectedChannel, setSelectedChannel] = useState('all') const [viewType, setViewType] = useState<'production' | 'publishing'>('production') const [loading, setLoading] = useState(true) useEffect(() => { fetchVideos() }, [currentMonth, selectedChannel]) const fetchVideos = async () => { setLoading(true) const start = startOfMonth(currentMonth) const end = endOfMonth(currentMonth) const dateField = viewType === 'production' ? 'productionDate' : 'scheduledPublishDate' let query = `where[${dateField}][greater_than_equal]=${start.toISOString()}&where[${dateField}][less_than_equal]=${end.toISOString()}` if (selectedChannel !== 'all') { query += `&where[channel][equals]=${selectedChannel}` } const res = await fetch(`/api/youtube-content?${query}&depth=1&limit=100`) const data = await res.json() setVideos(data.docs) setLoading(false) } const renderCalendar = () => { const monthStart = startOfMonth(currentMonth) const monthEnd = endOfMonth(currentMonth) const calendarStart = startOfWeek(monthStart, { locale: de }) const calendarEnd = endOfWeek(monthEnd, { locale: de }) const days = eachDayOfInterval({ start: calendarStart, end: calendarEnd }) return (
{/* Header */}
{['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map(day => (
{day}
))}
{/* Days */}
{days.map(day => { const dateField = viewType === 'production' ? 'productionDate' : 'scheduledPublishDate' const dayVideos = videos.filter(v => v[dateField] && isSameDay(new Date(v[dateField]), day) ) return (
{format(day, 'd')}
{dayVideos.map(video => ( {video.series} {video.title} ))}
) })}
) } return (

📅 Content Calendar

{format(currentMonth, 'MMMM yyyy', { locale: de })}

{/* Legend */}
Short Longform Idee In Produktion Fertig Veröffentlicht
{loading ? (
Lade Kalender...
) : ( renderCalendar() )}
) } ``` ### 2.3 Calendar Styles ```css /* src/app/(payload)/admin/views/youtube/calendar.css */ .content-calendar-view { padding: 20px; max-width: 1400px; margin: 0 auto; } .calendar-controls { margin-bottom: 24px; } .controls-row { display: flex; justify-content: space-between; align-items: center; margin-top: 16px; } .month-navigation { display: flex; align-items: center; gap: 16px; } .month-navigation button { padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; background: white; cursor: pointer; } .month-navigation h2 { min-width: 200px; text-align: center; } .filters { display: flex; gap: 12px; } .filters select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; } .calendar-legend { display: flex; gap: 16px; margin-top: 16px; padding: 12px; background: #f5f5f5; border-radius: 4px; } .legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; } .legend-item::before { content: ''; width: 12px; height: 12px; border-radius: 2px; } .legend-item.short::before { background: #3B82F6; } .legend-item.longform::before { background: #8B5CF6; } .legend-item.status-idea::before { background: #9CA3AF; } .legend-item.status-production::before { background: #F59E0B; } .legend-item.status-ready::before { background: #10B981; } .legend-item.status-published::before { background: #059669; } /* Calendar Grid */ .calendar-grid { border: 1px solid #ddd; border-radius: 8px; overflow: hidden; } .calendar-header { display: grid; grid-template-columns: repeat(7, 1fr); background: #f5f5f5; border-bottom: 1px solid #ddd; } .calendar-header-cell { padding: 12px; text-align: center; font-weight: 600; font-size: 12px; text-transform: uppercase; } .calendar-body { display: grid; grid-template-columns: repeat(7, 1fr); } .calendar-cell { min-height: 120px; border-right: 1px solid #eee; border-bottom: 1px solid #eee; padding: 8px; } .calendar-cell:nth-child(7n) { border-right: none; } .calendar-cell.outside-month { background: #fafafa; opacity: 0.5; } .calendar-date { font-weight: 600; font-size: 14px; margin-bottom: 8px; } .calendar-videos { display: flex; flex-direction: column; gap: 4px; } .calendar-video { display: block; padding: 4px 8px; border-radius: 4px; font-size: 11px; text-decoration: none; color: white; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .calendar-video.short { background: #3B82F6; } .calendar-video.longform { background: #8B5CF6; } .calendar-video .video-series { font-weight: 600; margin-right: 4px; } .calendar-video:hover { opacity: 0.9; transform: scale(1.02); } /* Status Colors als Border */ .calendar-video.idea { border-left: 3px solid #9CA3AF; } .calendar-video.script_draft { border-left: 3px solid #F59E0B; } .calendar-video.in_production { border-left: 3px solid #F59E0B; } .calendar-video.ready { border-left: 3px solid #10B981; } .calendar-video.published { border-left: 3px solid #059669; } ``` ### 2.4 Admin Navigation erweitern ```typescript // payload.config.ts - Admin Navigation export default buildConfig({ admin: { components: { // Custom Navigation mit YouTube Views Nav: '/components/CustomNav', }, }, // ... }) ``` ```typescript // src/components/CustomNav.tsx 'use client' import { NavGroup, Nav as DefaultNav } from '@payloadcms/ui' import Link from 'next/link' export const CustomNav = () => { return ( ) } ``` --- ## 3. Detaillierte Batch-Planung ### 3.1 Batch Collection ```typescript // src/collections/YtBatches.ts import { CollectionConfig } from 'payload/types' import { isYouTubeManager, isYouTubeCreatorOrAbove } from '../lib/youtubeAccess' export const YtBatches: CollectionConfig = { slug: 'yt-batches', labels: { singular: 'Production Batch', plural: 'Production Batches', }, admin: { group: 'YouTube', useAsTitle: 'name', defaultColumns: ['name', 'channel', 'status', 'productionStart', 'progress'], listSearchableFields: ['name'], }, access: { read: isYouTubeCreatorOrAbove, create: isYouTubeManager, update: isYouTubeCreatorOrAbove, delete: isYouTubeManager, }, fields: [ // === BASIC INFO === { type: 'row', fields: [ { name: 'name', type: 'text', required: true, admin: { placeholder: 'z.B. "Januar Woche 1" oder "Batch 1"', width: '50%', }, }, { name: 'channel', type: 'relationship', relationTo: 'youtube-channels', required: true, admin: { width: '50%', }, }, ], }, // === ZEITRAUM === { name: 'productionPeriod', type: 'group', label: 'Produktionszeitraum', fields: [ { type: 'row', fields: [ { name: 'start', type: 'date', required: true, label: 'Start', admin: { width: '50%', date: { pickerAppearance: 'dayOnly', displayFormat: 'dd.MM.yyyy', }, }, }, { name: 'end', type: 'date', required: true, label: 'Ende', admin: { width: '50%', date: { pickerAppearance: 'dayOnly', displayFormat: 'dd.MM.yyyy', }, }, }, ], }, { name: 'shootDays', type: 'array', label: 'Drehtage', labels: { singular: 'Drehtag', plural: 'Drehtage', }, fields: [ { type: 'row', fields: [ { name: 'date', type: 'date', required: true, admin: { width: '30%', date: { pickerAppearance: 'dayOnly', }, }, }, { name: 'location', type: 'select', options: [ { label: '🏠 Home Studio', value: 'home' }, { label: '🏢 Office', value: 'office' }, { label: '🚗 Unterwegs', value: 'mobile' }, { label: '📍 Extern', value: 'external' }, ], admin: { width: '25%' }, }, { name: 'duration', type: 'select', options: [ { label: '2 Stunden', value: '2h' }, { label: '4 Stunden (Halbtag)', value: '4h' }, { label: '8 Stunden (Ganztag)', value: '8h' }, ], admin: { width: '25%' }, }, { name: 'notes', type: 'text', admin: { width: '20%', placeholder: 'Notizen', }, }, ], }, ], }, ], }, // === CONTENT TARGETS === { name: 'targets', type: 'group', label: 'Content-Ziele', fields: [ { type: 'row', fields: [ { name: 'shortsTarget', type: 'number', label: 'Shorts (Ziel)', required: true, defaultValue: 7, admin: { width: '25%' }, }, { name: 'longformsTarget', type: 'number', label: 'Longforms (Ziel)', required: true, defaultValue: 3, admin: { width: '25%' }, }, { name: 'totalTarget', type: 'number', label: 'Gesamt (Ziel)', admin: { width: '25%', readOnly: true, description: 'Automatisch berechnet', }, hooks: { beforeChange: [ ({ siblingData }) => { return (siblingData.shortsTarget || 0) + (siblingData.longformsTarget || 0) }, ], }, }, { name: 'bufferDays', type: 'number', label: 'Puffer (Tage)', defaultValue: 3, admin: { width: '25%', description: 'Tage zwischen Produktion und Publish', }, }, ], }, ], }, // === SERIEN-VERTEILUNG === { name: 'seriesDistribution', type: 'array', label: 'Serien-Verteilung', admin: { description: 'Welche Serien in diesem Batch produziert werden', }, fields: [ { type: 'row', fields: [ { name: 'series', type: 'text', required: true, admin: { width: '30%', placeholder: 'z.B. GRFI', }, }, { name: 'shortsCount', type: 'number', label: 'Shorts', defaultValue: 0, admin: { width: '20%' }, }, { name: 'longformsCount', type: 'number', label: 'Longforms', defaultValue: 0, admin: { width: '20%' }, }, { name: 'priority', type: 'select', options: [ { label: '🔴 Hoch', value: 'high' }, { label: '🟡 Normal', value: 'normal' }, { label: '🟢 Niedrig', value: 'low' }, ], defaultValue: 'normal', admin: { width: '30%' }, }, ], }, ], }, // === STATUS & PROGRESS === { name: 'status', type: 'select', required: true, defaultValue: 'planning', options: [ { label: '📝 Planung', value: 'planning' }, { label: '✍️ Scripts', value: 'scripting' }, { label: '🎬 Produktion', value: 'production' }, { label: '✂️ Schnitt', value: 'editing' }, { label: '✅ Review', value: 'review' }, { label: '📤 Upload-Ready', value: 'ready' }, { label: '🎉 Veröffentlicht', value: 'published' }, ], admin: { position: 'sidebar', }, }, // Virtuelle Felder für Progress (berechnet via Hook) { name: 'progress', type: 'group', label: 'Fortschritt', admin: { position: 'sidebar', readOnly: true, }, fields: [ { name: 'shortsCompleted', type: 'number', label: 'Shorts fertig', admin: { readOnly: true }, }, { name: 'longformsCompleted', type: 'number', label: 'Longforms fertig', admin: { readOnly: true }, }, { name: 'percentage', type: 'number', label: 'Gesamt %', admin: { readOnly: true }, }, ], }, // === TEAM & RESOURCES === { name: 'team', type: 'group', label: 'Team', fields: [ { type: 'row', fields: [ { name: 'producer', type: 'relationship', relationTo: 'users', label: 'Producer', admin: { width: '33%' }, }, { name: 'editor', type: 'relationship', relationTo: 'users', label: 'Editor', admin: { width: '33%' }, }, { name: 'reviewer', type: 'relationship', relationTo: 'users', label: 'Reviewer', admin: { width: '33%' }, }, ], }, ], }, // === NOTES === { name: 'notes', type: 'textarea', label: 'Notizen', admin: { rows: 4, }, }, ], // === HOOKS === hooks: { afterRead: [ // Progress aus verknüpften Videos berechnen async ({ doc, req }) => { if (!doc?.id) return doc const videos = await req.payload.find({ collection: 'youtube-content', where: { productionBatch: { equals: doc.id }, }, limit: 100, }) const shorts = videos.docs.filter(v => v.format === 'short') const longforms = videos.docs.filter(v => v.format === 'longform') const completedStatuses = ['approved', 'upload_scheduled', 'published', 'tracked'] const shortsCompleted = shorts.filter(v => completedStatuses.includes(v.status) ).length const longformsCompleted = longforms.filter(v => completedStatuses.includes(v.status) ).length const totalTarget = (doc.targets?.shortsTarget || 0) + (doc.targets?.longformsTarget || 0) const totalCompleted = shortsCompleted + longformsCompleted const percentage = totalTarget > 0 ? Math.round((totalCompleted / totalTarget) * 100) : 0 return { ...doc, progress: { shortsCompleted, longformsCompleted, percentage, }, } }, ], }, } ``` ### 3.2 Batch Overview Dashboard ```typescript // src/app/(payload)/admin/views/youtube/batch-overview/page.tsx 'use client' import React, { useState, useEffect } from 'react' import { format } from 'date-fns' import { de } from 'date-fns/locale' interface Batch { id: string name: string channel: { name: string; slug: string } status: string productionPeriod: { start: string end: string } targets: { shortsTarget: number longformsTarget: number totalTarget: number } progress: { shortsCompleted: number longformsCompleted: number percentage: number } } export default function BatchOverviewView() { const [batches, setBatches] = useState([]) const [selectedChannel, setSelectedChannel] = useState('all') const [loading, setLoading] = useState(true) useEffect(() => { fetchBatches() }, [selectedChannel]) const fetchBatches = async () => { setLoading(true) let query = 'depth=1&limit=50&sort=-productionPeriod.start' if (selectedChannel !== 'all') { query += `&where[channel][equals]=${selectedChannel}` } const res = await fetch(`/api/yt-batches?${query}`) const data = await res.json() setBatches(data.docs) setLoading(false) } const getStatusColor = (status: string) => { const colors: Record = { planning: '#9CA3AF', scripting: '#60A5FA', production: '#FBBF24', editing: '#F97316', review: '#A78BFA', ready: '#34D399', published: '#10B981', } return colors[status] || '#9CA3AF' } const getStatusLabel = (status: string) => { const labels: Record = { planning: '📝 Planung', scripting: '✍️ Scripts', production: '🎬 Produktion', editing: '✂️ Schnitt', review: '✅ Review', ready: '📤 Ready', published: '🎉 Published', } return labels[status] || status } return (

📦 Batch Overview

+ Neuer Batch
{loading ? (
Lade Batches...
) : ( ) } ``` --- ## 4. Monthly Goals Collection ```typescript // src/collections/YtMonthlyGoals.ts import { CollectionConfig } from 'payload/types' export const YtMonthlyGoals: CollectionConfig = { slug: 'yt-monthly-goals', labels: { singular: 'Monthly Goal', plural: 'Monthly Goals', }, admin: { group: 'YouTube', useAsTitle: 'displayTitle', defaultColumns: ['channel', 'month', 'status', 'updatedAt'], }, fields: [ { name: 'channel', type: 'relationship', relationTo: 'youtube-channels', required: true, }, { name: 'month', type: 'date', required: true, admin: { date: { pickerAppearance: 'monthOnly', displayFormat: 'MMMM yyyy', }, }, }, { name: 'displayTitle', type: 'text', admin: { hidden: true, }, hooks: { beforeChange: [ async ({ siblingData, req }) => { if (siblingData.channel && siblingData.month) { const channel = await req.payload.findByID({ collection: 'youtube-channels', id: siblingData.channel, }) const monthStr = new Date(siblingData.month).toLocaleDateString('de-DE', { month: 'long', year: 'numeric', }) return `${channel?.name} - ${monthStr}` } return 'Neues Monatsziel' }, ], }, }, // === CONTENT GOALS === { name: 'contentGoals', type: 'group', label: 'Content-Ziele', fields: [ { type: 'row', fields: [ { name: 'longformsTarget', type: 'number', label: 'Longforms', defaultValue: 12, admin: { width: '25%' }, }, { name: 'longformsCurrent', type: 'number', label: 'Aktuell', defaultValue: 0, admin: { width: '25%' }, }, { name: 'shortsTarget', type: 'number', label: 'Shorts', defaultValue: 28, admin: { width: '25%' }, }, { name: 'shortsCurrent', type: 'number', label: 'Aktuell', defaultValue: 0, admin: { width: '25%' }, }, ], }, ], }, // === AUDIENCE GOALS === { name: 'audienceGoals', type: 'group', label: 'Audience-Ziele', fields: [ { type: 'row', fields: [ { name: 'subscribersTarget', type: 'number', label: 'Neue Abos (Ziel)', admin: { width: '25%' }, }, { name: 'subscribersCurrent', type: 'number', label: 'Aktuell', admin: { width: '25%' }, }, { name: 'viewsTarget', type: 'number', label: 'Views (Ziel)', admin: { width: '25%' }, }, { name: 'viewsCurrent', type: 'number', label: 'Aktuell', admin: { width: '25%' }, }, ], }, ], }, // === ENGAGEMENT GOALS === { name: 'engagementGoals', type: 'group', label: 'Engagement-Ziele', fields: [ { type: 'row', fields: [ { name: 'avgCtrTarget', type: 'text', label: 'Ø CTR (Ziel)', admin: { width: '25%', placeholder: 'z.B. ">4%"', }, }, { name: 'avgCtrCurrent', type: 'text', label: 'Aktuell', admin: { width: '25%' }, }, { name: 'avgRetentionTarget', type: 'text', label: 'Ø Retention (Ziel)', admin: { width: '25%', placeholder: 'z.B. ">50%"', }, }, { name: 'avgRetentionCurrent', type: 'text', label: 'Aktuell', admin: { width: '25%' }, }, ], }, ], }, // === BUSINESS GOALS === { name: 'businessGoals', type: 'group', label: 'Business-Ziele', fields: [ { type: 'row', fields: [ { name: 'newsletterSignupsTarget', type: 'number', label: 'Newsletter-Anmeldungen', admin: { width: '25%' }, }, { name: 'newsletterSignupsCurrent', type: 'number', label: 'Aktuell', admin: { width: '25%' }, }, { name: 'affiliateClicksTarget', type: 'number', label: 'Affiliate-Klicks', admin: { width: '25%' }, }, { name: 'affiliateClicksCurrent', type: 'number', label: 'Aktuell', admin: { width: '25%' }, }, ], }, ], }, // === CUSTOM GOALS === { name: 'customGoals', type: 'array', label: 'Weitere Ziele', fields: [ { type: 'row', fields: [ { name: 'metric', type: 'text', required: true, admin: { width: '40%', placeholder: 'z.B. "SPARK Videos"', }, }, { name: 'target', type: 'text', required: true, admin: { width: '20%', placeholder: 'Ziel', }, }, { name: 'current', type: 'text', admin: { width: '20%', placeholder: 'Aktuell', }, }, { name: 'status', type: 'select', options: [ { label: '🟢 On Track', value: 'on_track' }, { label: '🟡 At Risk', value: 'at_risk' }, { label: '✅ Achieved', value: 'achieved' }, { label: '❌ Missed', value: 'missed' }, ], admin: { width: '20%' }, }, ], }, ], }, { name: 'notes', type: 'textarea', label: 'Notizen / Learnings', }, ], } ``` --- ## 5. Erweiterte YouTube Content Collection ```typescript // Erweiterungen für YouTubeContent.ts // Neue Felder hinzufügen: // === PRODUKTION === { name: 'productionBatch', type: 'relationship', relationTo: 'yt-batches', label: 'Produktions-Batch', admin: { position: 'sidebar', }, }, { name: 'productionWeek', type: 'number', label: 'Produktionswoche', min: 1, max: 52, admin: { position: 'sidebar', }, }, { name: 'calendarWeek', type: 'number', label: 'Kalenderwoche', min: 1, max: 52, admin: { position: 'sidebar', }, }, { name: 'productionDate', type: 'date', label: 'Produktionsdatum', admin: { date: { pickerAppearance: 'dayOnly', displayFormat: 'dd.MM.yyyy', }, }, }, { name: 'targetDuration', type: 'text', label: 'Ziel-Dauer', admin: { placeholder: 'z.B. "8-12 Min" oder "45-58s"', }, }, { name: 'bRollNotes', type: 'textarea', label: 'B-Roll / Setting Notizen', admin: { rows: 2, }, }, // === POSTING === { name: 'publishTime', type: 'text', label: 'Posting-Uhrzeit', admin: { placeholder: 'z.B. "07:00" oder "17:00"', }, }, { name: 'thumbnailText', type: 'text', label: 'Thumbnail-Text', admin: { placeholder: 'z.B. "BOARD READY | 7 MIN"', }, }, { name: 'ctaType', type: 'select', label: 'CTA-Typ', options: [ { label: 'Link in Bio', value: 'link_in_bio' }, { label: 'Newsletter', value: 'newsletter' }, { label: 'Longform verlinken', value: 'longform_link' }, { label: 'Benutzerdefiniert', value: 'custom' }, ], }, { name: 'ctaDetail', type: 'text', label: 'CTA-Detail', admin: { placeholder: 'z.B. "GRFI-Checkliste" oder "CPW-Rechner"', condition: (data) => data?.ctaType, }, }, // === SCRIPT (Block-basiert) === { name: 'script', type: 'blocks', label: 'Script', labels: { singular: 'Section', plural: 'Script Sections', }, blocks: [ScriptSectionBlock], }, // === UPLOAD CHECKLIST === { name: 'uploadChecklist', type: 'array', label: 'Upload-Checkliste', admin: { condition: (data) => ['approved', 'upload_scheduled', 'published'].includes(data?.status), }, fields: [ { type: 'row', fields: [ { name: 'step', type: 'text', required: true, admin: { width: '40%' }, }, { name: 'completed', type: 'checkbox', label: '✓', admin: { width: '10%' }, }, { name: 'completedAt', type: 'date', admin: { width: '25%', readOnly: true, }, }, { name: 'completedBy', type: 'relationship', relationTo: 'users', admin: { width: '25%', readOnly: true, }, }, ], }, ], }, // === DISCLAIMERS === { name: 'disclaimers', type: 'array', label: 'Disclaimers', fields: [ { type: 'row', fields: [ { name: 'type', type: 'select', required: true, options: [ { label: '⚠️ Medizinisch', value: 'medical' }, { label: '⚖️ Rechtlich', value: 'legal' }, { label: '🔗 Affiliate', value: 'affiliate' }, { label: '🤝 Sponsored', value: 'sponsored' }, ], admin: { width: '25%' }, }, { name: 'text', type: 'text', admin: { width: '50%' }, }, { name: 'placement', type: 'select', options: [ { label: 'Gesprochen', value: 'spoken' }, { label: 'Text-Overlay', value: 'overlay' }, { label: 'Beschreibung', value: 'description' }, { label: 'Überall', value: 'all' }, ], admin: { width: '25%' }, }, ], }, ], }, ``` --- ## 6. Zusammenfassung der neuen Collections | Collection | Slug | Zweck | |------------|------|-------| | YouTube Channels | `youtube-channels` | Kanäle (existiert) | | YouTube Content | `youtube-content` | Videos + Script (erweitert) | | YT Tasks | `yt-tasks` | Aufgaben (existiert) | | YT Notifications | `yt-notifications` | Benachrichtigungen (existiert) | | **YT Batches** | `yt-batches` | **NEU: Produktions-Batches** | | **YT Monthly Goals** | `yt-monthly-goals` | **NEU: Monatsziele** | | **YT Script Templates** | `yt-script-templates` | **NEU: Skript-Vorlagen** | | **YT Checklist Templates** | `yt-checklist-templates` | **NEU: Upload-Checklisten** | --- ## 7. Custom Admin Views | View | Pfad | Zweck | |------|------|-------| | Content Calendar | `/admin/views/youtube/content-calendar` | Produktions- & Publishing-Kalender | | Batch Overview | `/admin/views/youtube/batch-overview` | Dashboard aller Batches | | Monthly Goals | `/admin/views/youtube/monthly-goals` | KPI-Dashboard | --- ## 8. Migrations ```typescript // src/migrations/20260112_200000_youtube_ops_v2.ts import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/db-postgres' export async function up({ payload }: MigrateUpArgs): Promise { // 1. Neue Enums await payload.db.drizzle.execute(` CREATE TYPE enum_yt_batches_status AS ENUM ( 'planning', 'scripting', 'production', 'editing', 'review', 'ready', 'published' ); CREATE TYPE enum_script_section_type AS ENUM ( 'hook', 'intro_ident', 'context', 'content_part', 'summary', 'cta', 'outro', 'disclaimer' ); CREATE TYPE enum_cta_type AS ENUM ( 'link_in_bio', 'newsletter', 'longform_link', 'custom' ); CREATE TYPE enum_disclaimer_type AS ENUM ( 'medical', 'legal', 'affiliate', 'sponsored' ); CREATE TYPE enum_disclaimer_placement AS ENUM ( 'spoken', 'overlay', 'description', 'all' ); `); // 2. Neue Tabellen // ... (vollständige Migration) } export async function down({ payload }: MigrateDownArgs): Promise { // Rollback } ``` --- ## Nächste Schritte 1. **Claude Code Integration Prompt erstellen** mit allen Collections, Blocks und Views 2. **Seed-Daten** mit BlogWoman Januar Content als Beispiel 3. **CSS Styles** für Calendar und Dashboard Views 4. **Lexical Custom Block** für Script Section Editor Soll ich jetzt den vollständigen **Claude Code Integration Prompt** erstellen?