# YouTube Operations Hub v2 – Claude Code Integration Prompt ## Projektkontext Du arbeitest am **YouTube Operations Hub v2** für Complex Care Solutions (CCS). Dies ist eine Erweiterung des bestehenden YouTube-Moduls in Payload CMS 3.x. **Server:** sv-payload (10.10.181.100) **Projekt-Pfad:** `/var/www/payload-main` (oder aktueller Projektpfad) **Datenbank:** PostgreSQL 17 auf sv-postgres (10.10.181.101) **Tech Stack:** Payload CMS 3.x, Next.js 15, TypeScript, Drizzle ORM --- ## Aufgabe Erweitere das bestehende YouTube Operations Hub um: 1. **Script Editor** – Custom Lexical Blocks für strukturierte Video-Skripte 2. **Kalenderansichten** – Content Calendar + Batch Overview als Custom Admin Views 3. **Batch-Planung** – Neue Collection für detaillierte Produktions-Batches 4. **Monthly Goals** – KPI-Tracking pro Kanal und Monat 5. **Erweiterte YouTube Content Felder** – Produktions- und Posting-Informationen --- ## Teil 1: Script Section Block ### Datei: `src/blocks/ScriptSection.ts` ```typescript import { Block } from 'payload' export const ScriptSectionBlock: Block = { slug: 'scriptSection', labels: { singular: { de: 'Script Section', en: 'Script Section' }, plural: { de: 'Script Sections', en: 'Script Sections' }, }, imageURL: '/assets/script-section-icon.svg', imageAltText: 'Script Section Block', fields: [ { type: 'row', 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: { width: '50%', }, }, { name: 'duration', type: 'text', label: { de: 'Dauer', en: 'Duration' }, admin: { width: '25%', placeholder: 'z.B. "2-3 Min"', }, }, { name: 'sectionTitle', type: 'text', label: { de: 'Section-Titel', en: 'Section Title' }, admin: { width: '25%', placeholder: 'z.B. "TEIL 1: OUTFIT"', condition: (data, siblingData) => siblingData?.sectionType === 'content_part', }, }, ], }, { name: 'spokenText', type: 'richText', label: { de: 'Gesprochener Text', en: 'Spoken Text' }, required: true, localized: true, }, { name: 'bRollInstructions', type: 'array', label: { de: 'B-Roll Anweisungen', en: 'B-Roll Instructions' }, labels: { singular: { de: 'B-Roll', en: 'B-Roll' }, plural: { de: 'B-Roll Anweisungen', en: 'B-Roll Instructions' }, }, fields: [ { type: 'row', fields: [ { name: 'instruction', type: 'text', required: true, admin: { width: '70%', placeholder: 'z.B. "Outfit zeigen", "Kleiderschrank-Aufnahme"', }, }, { name: 'timestamp', type: 'text', admin: { width: '30%', placeholder: 'Optional: "bei 0:45"', }, }, ], }, ], admin: { initCollapsed: true, }, }, { name: 'textOverlays', type: 'array', label: { de: 'Text-Overlays', en: 'Text Overlays' }, labels: { singular: { de: 'Overlay', en: 'Overlay' }, plural: { de: 'Text-Overlays', en: 'Text Overlays' }, }, fields: [ { type: 'row', fields: [ { name: 'text', type: 'text', required: true, admin: { width: '70%', 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' }, ], admin: { width: '30%' }, }, ], }, ], admin: { initCollapsed: true, }, }, { name: 'visualNotes', type: 'textarea', label: { de: 'Visuelle Notizen', en: 'Visual Notes' }, admin: { rows: 2, placeholder: 'Zusätzliche Anweisungen für Schnitt/Grafik', }, }, ], } ``` --- ## Teil 2: Neue Collections ### Datei: `src/collections/youtube/YtBatches.ts` ```typescript import type { CollectionConfig } from 'payload' import { isYouTubeManager, isYouTubeCreatorOrAbove, hasYouTubeAccess } from '../../lib/youtubeAccess' export const YtBatches: CollectionConfig = { slug: 'yt-batches', labels: { singular: { de: 'Production Batch', en: 'Production Batch' }, plural: { de: 'Production Batches', en: 'Production Batches' }, }, admin: { group: 'YouTube', useAsTitle: 'name', defaultColumns: ['name', 'channel', 'status', 'productionPeriod.start', 'progress.percentage'], listSearchableFields: ['name'], }, access: { read: hasYouTubeAccess, create: isYouTubeManager, update: isYouTubeCreatorOrAbove, delete: isYouTubeManager, }, fields: [ // === BASIC INFO === { type: 'row', fields: [ { name: 'name', type: 'text', required: true, label: { de: 'Batch-Name', en: 'Batch Name' }, admin: { placeholder: 'z.B. "Januar Woche 1" oder "Batch 1"', width: '50%', }, }, { name: 'channel', type: 'relationship', relationTo: 'youtube-channels', required: true, label: { de: 'Kanal', en: 'Channel' }, admin: { width: '50%', }, }, ], }, // === PRODUKTIONSZEITRAUM === { name: 'productionPeriod', type: 'group', label: { de: 'Produktionszeitraum', en: 'Production Period' }, fields: [ { type: 'row', fields: [ { name: 'start', type: 'date', required: true, label: { de: 'Start', en: 'Start' }, admin: { width: '50%', date: { pickerAppearance: 'dayOnly', displayFormat: 'dd.MM.yyyy', }, }, }, { name: 'end', type: 'date', required: true, label: { de: 'Ende', en: 'End' }, admin: { width: '50%', date: { pickerAppearance: 'dayOnly', displayFormat: 'dd.MM.yyyy', }, }, }, ], }, { name: 'shootDays', type: 'array', label: { de: 'Drehtage', en: 'Shoot Days' }, labels: { singular: { de: 'Drehtag', en: 'Shoot Day' }, plural: { de: 'Drehtage', en: 'Shoot Days' }, }, fields: [ { type: 'row', fields: [ { name: 'date', type: 'date', required: true, admin: { width: '25%', date: { pickerAppearance: 'dayOnly' }, }, }, { name: 'location', type: 'select', label: { de: 'Location', en: 'Location' }, options: [ { label: { de: '🏠 Home Studio', en: '🏠 Home Studio' }, value: 'home' }, { label: { de: '🏢 Office', en: '🏢 Office' }, value: 'office' }, { label: { de: '🚗 Unterwegs', en: '🚗 On the Go' }, value: 'mobile' }, { label: { de: '📍 Extern', en: '📍 External' }, value: 'external' }, ], admin: { width: '25%' }, }, { name: 'duration', type: 'select', label: { de: 'Dauer', en: 'Duration' }, options: [ { label: '2h', value: '2h' }, { label: '4h (Halbtag)', value: '4h' }, { label: '8h (Ganztag)', value: '8h' }, ], admin: { width: '25%' }, }, { name: 'notes', type: 'text', label: { de: 'Notizen', en: 'Notes' }, admin: { width: '25%', placeholder: 'Notizen', }, }, ], }, ], admin: { initCollapsed: true, }, }, ], }, // === CONTENT TARGETS === { name: 'targets', type: 'group', label: { de: 'Content-Ziele', en: 'Content Targets' }, fields: [ { type: 'row', fields: [ { name: 'shortsTarget', type: 'number', label: { de: 'Shorts (Ziel)', en: 'Shorts (Target)' }, required: true, defaultValue: 7, min: 0, admin: { width: '25%' }, }, { name: 'longformsTarget', type: 'number', label: { de: 'Longforms (Ziel)', en: 'Longforms (Target)' }, required: true, defaultValue: 3, min: 0, admin: { width: '25%' }, }, { name: 'totalTarget', type: 'number', label: { de: 'Gesamt', en: 'Total' }, admin: { width: '25%', readOnly: true, }, }, { name: 'bufferDays', type: 'number', label: { de: 'Puffer (Tage)', en: 'Buffer (Days)' }, defaultValue: 3, min: 0, admin: { width: '25%', description: { de: 'Tage zwischen Produktion und Publish', en: 'Days between production and publish' }, }, }, ], }, ], }, // === SERIEN-VERTEILUNG === { name: 'seriesDistribution', type: 'array', label: { de: 'Serien-Verteilung', en: 'Series Distribution' }, admin: { description: { de: 'Welche Serien in diesem Batch produziert werden', en: 'Which series are produced in this batch' }, }, fields: [ { type: 'row', fields: [ { name: 'series', type: 'text', required: true, label: { de: 'Serie', en: 'Series' }, admin: { width: '30%', placeholder: 'z.B. GRFI', }, }, { name: 'shortsCount', type: 'number', label: 'Shorts', defaultValue: 0, min: 0, admin: { width: '20%' }, }, { name: 'longformsCount', type: 'number', label: 'Longforms', defaultValue: 0, min: 0, admin: { width: '20%' }, }, { name: 'priority', type: 'select', label: { de: 'Priorität', en: 'Priority' }, options: [ { label: '🔴 Hoch', value: 'high' }, { label: '🟡 Normal', value: 'normal' }, { label: '🟢 Niedrig', value: 'low' }, ], defaultValue: 'normal', admin: { width: '30%' }, }, ], }, ], }, // === STATUS === { name: 'status', type: 'select', required: true, defaultValue: 'planning', label: { de: 'Status', en: 'Status' }, options: [ { label: { de: '📝 Planung', en: '📝 Planning' }, value: 'planning' }, { label: { de: '✍️ Scripts', en: '✍️ Scripts' }, value: 'scripting' }, { label: { de: '🎬 Produktion', en: '🎬 Production' }, value: 'production' }, { label: { de: '✂️ Schnitt', en: '✂️ Editing' }, value: 'editing' }, { label: { de: '✅ Review', en: '✅ Review' }, value: 'review' }, { label: { de: '📤 Upload-Ready', en: '📤 Upload-Ready' }, value: 'ready' }, { label: { de: '🎉 Veröffentlicht', en: '🎉 Published' }, value: 'published' }, ], admin: { position: 'sidebar', }, }, // === PROGRESS (berechnet) === { name: 'progress', type: 'group', label: { de: 'Fortschritt', en: 'Progress' }, admin: { position: 'sidebar', readOnly: true, }, fields: [ { name: 'shortsCompleted', type: 'number', label: { de: 'Shorts fertig', en: 'Shorts Completed' }, admin: { readOnly: true }, }, { name: 'longformsCompleted', type: 'number', label: { de: 'Longforms fertig', en: 'Longforms Completed' }, admin: { readOnly: true }, }, { name: 'percentage', type: 'number', label: { de: 'Gesamt %', en: 'Total %' }, admin: { readOnly: true }, }, ], }, // === TEAM === { name: 'team', type: 'group', label: { de: 'Team', en: '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%' }, }, ], }, ], }, // === NOTIZEN === { name: 'notes', type: 'textarea', label: { de: 'Notizen', en: 'Notes' }, localized: true, admin: { rows: 4, }, }, ], // === HOOKS === hooks: { beforeChange: [ // Total Target berechnen ({ data }) => { if (data?.targets) { data.targets.totalTarget = (data.targets.shortsTarget || 0) + (data.targets.longformsTarget || 0) } return data }, ], afterRead: [ // Progress aus verknüpften Videos berechnen async ({ doc, req }) => { if (!doc?.id) return doc try { const videos = await req.payload.find({ collection: 'youtube-content', where: { productionBatch: { equals: doc.id }, }, limit: 100, depth: 0, }) const shorts = videos.docs.filter((v: any) => v.format === 'short') const longforms = videos.docs.filter((v: any) => v.format === 'longform') const completedStatuses = ['approved', 'upload_scheduled', 'published', 'tracked'] const shortsCompleted = shorts.filter((v: any) => completedStatuses.includes(v.status) ).length const longformsCompleted = longforms.filter((v: any) => 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, }, } } catch (error) { console.error('Error calculating batch progress:', error) return doc } }, ], }, } ``` ### Datei: `src/collections/youtube/YtMonthlyGoals.ts` ```typescript import type { CollectionConfig } from 'payload' import { isYouTubeManager, hasYouTubeAccess } from '../../lib/youtubeAccess' export const YtMonthlyGoals: CollectionConfig = { slug: 'yt-monthly-goals', labels: { singular: { de: 'Monatsziel', en: 'Monthly Goal' }, plural: { de: 'Monatsziele', en: 'Monthly Goals' }, }, admin: { group: 'YouTube', useAsTitle: 'displayTitle', defaultColumns: ['displayTitle', 'channel', 'month', 'updatedAt'], }, access: { read: hasYouTubeAccess, create: isYouTubeManager, update: isYouTubeManager, delete: isYouTubeManager, }, fields: [ { type: 'row', fields: [ { name: 'channel', type: 'relationship', relationTo: 'youtube-channels', required: true, label: { de: 'Kanal', en: 'Channel' }, admin: { width: '50%' }, }, { name: 'month', type: 'date', required: true, label: { de: 'Monat', en: 'Month' }, admin: { width: '50%', date: { pickerAppearance: 'monthOnly', displayFormat: 'MMMM yyyy', }, }, }, ], }, { name: 'displayTitle', type: 'text', admin: { hidden: true }, hooks: { beforeChange: [ async ({ siblingData, req }) => { if (siblingData.channel && siblingData.month) { try { const channelId = typeof siblingData.channel === 'object' ? siblingData.channel.id : siblingData.channel const channel = await req.payload.findByID({ collection: 'youtube-channels', id: channelId, }) const monthStr = new Date(siblingData.month).toLocaleDateString('de-DE', { month: 'long', year: 'numeric', }) return `${channel?.name || 'Kanal'} - ${monthStr}` } catch { return 'Neues Monatsziel' } } return 'Neues Monatsziel' }, ], }, }, // === CONTENT GOALS === { name: 'contentGoals', type: 'group', label: { de: 'Content-Ziele', en: 'Content Goals' }, fields: [ { type: 'row', fields: [ { name: 'longformsTarget', type: 'number', label: { de: 'Longforms (Ziel)', en: 'Longforms (Target)' }, defaultValue: 12, admin: { width: '25%' }, }, { name: 'longformsCurrent', type: 'number', label: { de: 'Aktuell', en: 'Current' }, defaultValue: 0, admin: { width: '25%' }, }, { name: 'shortsTarget', type: 'number', label: { de: 'Shorts (Ziel)', en: 'Shorts (Target)' }, defaultValue: 28, admin: { width: '25%' }, }, { name: 'shortsCurrent', type: 'number', label: { de: 'Aktuell', en: 'Current' }, defaultValue: 0, admin: { width: '25%' }, }, ], }, ], }, // === AUDIENCE GOALS === { name: 'audienceGoals', type: 'group', label: { de: 'Audience-Ziele', en: 'Audience Goals' }, fields: [ { type: 'row', fields: [ { name: 'subscribersTarget', type: 'number', label: { de: 'Neue Abos (Ziel)', en: 'New Subs (Target)' }, admin: { width: '25%' }, }, { name: 'subscribersCurrent', type: 'number', label: { de: 'Aktuell', en: 'Current' }, admin: { width: '25%' }, }, { name: 'viewsTarget', type: 'number', label: { de: 'Views (Ziel)', en: 'Views (Target)' }, admin: { width: '25%' }, }, { name: 'viewsCurrent', type: 'number', label: { de: 'Aktuell', en: 'Current' }, admin: { width: '25%' }, }, ], }, ], }, // === ENGAGEMENT GOALS === { name: 'engagementGoals', type: 'group', label: { de: 'Engagement-Ziele', en: 'Engagement Goals' }, fields: [ { type: 'row', fields: [ { name: 'avgCtrTarget', type: 'text', label: { de: 'Ø CTR (Ziel)', en: 'Avg CTR (Target)' }, admin: { width: '25%', placeholder: 'z.B. ">4%"', }, }, { name: 'avgCtrCurrent', type: 'text', label: { de: 'Aktuell', en: 'Current' }, admin: { width: '25%' }, }, { name: 'avgRetentionTarget', type: 'text', label: { de: 'Ø Retention (Ziel)', en: 'Avg Retention (Target)' }, admin: { width: '25%', placeholder: 'z.B. ">50%"', }, }, { name: 'avgRetentionCurrent', type: 'text', label: { de: 'Aktuell', en: 'Current' }, admin: { width: '25%' }, }, ], }, ], }, // === BUSINESS GOALS === { name: 'businessGoals', type: 'group', label: { de: 'Business-Ziele', en: 'Business Goals' }, fields: [ { type: 'row', fields: [ { name: 'newsletterSignupsTarget', type: 'number', label: { de: 'Newsletter-Anmeldungen (Ziel)', en: 'Newsletter Signups (Target)' }, admin: { width: '25%' }, }, { name: 'newsletterSignupsCurrent', type: 'number', label: { de: 'Aktuell', en: 'Current' }, admin: { width: '25%' }, }, { name: 'affiliateRevenueTarget', type: 'number', label: { de: 'Affiliate-Umsatz € (Ziel)', en: 'Affiliate Revenue € (Target)' }, admin: { width: '25%' }, }, { name: 'affiliateRevenueCurrent', type: 'number', label: { de: 'Aktuell', en: 'Current' }, admin: { width: '25%' }, }, ], }, ], }, // === CUSTOM GOALS === { name: 'customGoals', type: 'array', label: { de: 'Weitere Ziele', en: 'Custom Goals' }, fields: [ { type: 'row', fields: [ { name: 'metric', type: 'text', required: true, label: { de: 'Metrik', en: 'Metric' }, admin: { width: '40%', placeholder: 'z.B. "SPARK Videos"', }, }, { name: 'target', type: 'text', required: true, label: { de: 'Ziel', en: 'Target' }, admin: { width: '20%', placeholder: 'Ziel', }, }, { name: 'current', type: 'text', label: { de: 'Aktuell', en: 'Current' }, admin: { width: '20%', placeholder: 'Aktuell', }, }, { name: 'status', type: 'select', label: 'Status', 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: { de: 'Notizen / Learnings', en: 'Notes / Learnings' }, localized: true, }, ], } ``` ### Datei: `src/collections/youtube/YtScriptTemplates.ts` ```typescript import type { CollectionConfig } from 'payload' import { ScriptSectionBlock } from '../../blocks/ScriptSection' import { isYouTubeManager, hasYouTubeAccess } from '../../lib/youtubeAccess' export const YtScriptTemplates: CollectionConfig = { slug: 'yt-script-templates', labels: { singular: { de: 'Script Template', en: 'Script Template' }, plural: { de: 'Script Templates', en: 'Script Templates' }, }, admin: { group: 'YouTube', useAsTitle: 'name', defaultColumns: ['name', 'series', 'format', 'channel', 'updatedAt'], }, access: { read: hasYouTubeAccess, create: isYouTubeManager, update: isYouTubeManager, delete: isYouTubeManager, }, fields: [ { type: 'row', fields: [ { name: 'name', type: 'text', required: true, localized: true, label: { de: 'Template-Name', en: 'Template Name' }, admin: { width: '50%', placeholder: 'z.B. "GRFI Longform Template"', }, }, { name: 'channel', type: 'relationship', relationTo: 'youtube-channels', required: true, label: { de: 'Kanal', en: 'Channel' }, admin: { width: '50%' }, }, ], }, { type: 'row', fields: [ { name: 'series', type: 'text', required: true, label: { de: 'Serie', en: 'Series' }, admin: { width: '50%', placeholder: 'z.B. "GRFI", "M2M", "SPARK"', }, }, { name: 'format', type: 'select', required: true, label: { de: 'Format', en: 'Format' }, options: [ { label: 'Short (45-60s)', value: 'short' }, { label: 'Longform (8-16 Min)', value: 'longform' }, ], admin: { width: '50%' }, }, ], }, { name: 'description', type: 'textarea', label: { de: 'Beschreibung', en: 'Description' }, localized: true, admin: { rows: 2, description: { de: 'Hinweise zur Verwendung dieses Templates', en: 'Usage notes for this template' }, }, }, { name: 'templateSections', type: 'blocks', label: { de: 'Template Sections', en: 'Template Sections' }, blocks: [ScriptSectionBlock], }, ], } ``` --- ## Teil 3: Erweiterung YouTube Content Collection ### Datei: `src/collections/youtube/YouTubeContent.ts` (Erweiterungen) Füge diese Felder zur bestehenden YouTubeContent Collection hinzu: ```typescript // === NEUE IMPORTS === import { ScriptSectionBlock } from '../../blocks/ScriptSection' // === NEUE FELDER (nach den bestehenden Feldern einfügen) === // PRODUKTION TAB - Neue Felder { name: 'productionBatch', type: 'relationship', relationTo: 'yt-batches', label: { de: 'Produktions-Batch', en: 'Production Batch' }, admin: { position: 'sidebar', }, }, { type: 'row', fields: [ { name: 'productionWeek', type: 'number', label: { de: 'Produktionswoche', en: 'Production Week' }, min: 1, max: 52, admin: { width: '50%' }, }, { name: 'calendarWeek', type: 'number', label: { de: 'Kalenderwoche', en: 'Calendar Week' }, min: 1, max: 52, admin: { width: '50%' }, }, ], }, { name: 'productionDate', type: 'date', label: { de: 'Produktionsdatum', en: 'Production Date' }, admin: { date: { pickerAppearance: 'dayOnly', displayFormat: 'dd.MM.yyyy', }, }, }, { name: 'targetDuration', type: 'text', label: { de: 'Ziel-Dauer', en: 'Target Duration' }, admin: { placeholder: 'z.B. "8-12 Min" oder "45-58s"', }, }, { name: 'bRollNotes', type: 'textarea', label: { de: 'B-Roll / Setting Notizen', en: 'B-Roll / Setting Notes' }, localized: true, admin: { rows: 2, }, }, // POSTING TAB - Neue Felder { name: 'publishTime', type: 'text', label: { de: 'Posting-Uhrzeit', en: 'Publish Time' }, admin: { placeholder: 'z.B. "07:00" oder "17:00"', }, }, { name: 'thumbnailText', type: 'text', label: { de: 'Thumbnail-Text', en: 'Thumbnail Text' }, admin: { placeholder: 'z.B. "BOARD READY | 7 MIN"', }, }, { type: 'row', fields: [ { name: 'ctaType', type: 'select', label: { de: 'CTA-Typ', en: 'CTA Type' }, options: [ { label: 'Link in Bio', value: 'link_in_bio' }, { label: 'Newsletter', value: 'newsletter' }, { label: 'Longform verlinken', value: 'longform_link' }, { label: 'Benutzerdefiniert', value: 'custom' }, ], admin: { width: '50%' }, }, { name: 'ctaDetail', type: 'text', label: { de: 'CTA-Detail', en: 'CTA Detail' }, admin: { width: '50%', placeholder: 'z.B. "GRFI-Checkliste"', condition: (data) => !!data?.ctaType, }, }, ], }, // SCRIPT TAB (neu) { name: 'script', type: 'blocks', label: { de: 'Script', en: 'Script' }, labels: { singular: { de: 'Section', en: 'Section' }, plural: { de: 'Script Sections', en: 'Script Sections' }, }, blocks: [ScriptSectionBlock], admin: { description: { de: 'Strukturiertes Video-Script mit Sections', en: 'Structured video script with sections' }, }, }, // UPLOAD CHECKLIST { name: 'uploadChecklist', type: 'array', label: { de: 'Upload-Checkliste', en: 'Upload Checklist' }, admin: { condition: (data) => ['approved', 'upload_scheduled', 'published', 'tracked'].includes(data?.status), }, fields: [ { type: 'row', fields: [ { name: 'step', type: 'text', required: true, label: { de: 'Schritt', en: 'Step' }, admin: { width: '50%' }, }, { name: 'completed', type: 'checkbox', label: '✓', admin: { width: '10%' }, }, { name: 'completedAt', type: 'date', label: { de: 'Erledigt am', en: 'Completed At' }, admin: { width: '20%', readOnly: true, }, }, { name: 'completedBy', type: 'relationship', relationTo: 'users', label: { de: 'Von', en: 'By' }, admin: { width: '20%', readOnly: true, }, }, ], }, ], }, // DISCLAIMERS { name: 'disclaimers', type: 'array', label: { de: 'Disclaimers', en: 'Disclaimers' }, fields: [ { type: 'row', fields: [ { name: 'type', type: 'select', required: true, label: { de: 'Typ', en: 'Type' }, options: [ { label: '⚠️ Medizinisch', value: 'medical' }, { label: '⚖️ Rechtlich', value: 'legal' }, { label: '🔗 Affiliate', value: 'affiliate' }, { label: '🤝 Sponsored', value: 'sponsored' }, ], admin: { width: '25%' }, }, { name: 'text', type: 'text', label: { de: 'Text', en: 'Text' }, localized: true, admin: { width: '50%' }, }, { name: 'placement', type: 'select', label: { de: 'Platzierung', en: 'Placement' }, options: [ { label: { de: 'Gesprochen', en: 'Spoken' }, value: 'spoken' }, { label: 'Text-Overlay', value: 'overlay' }, { label: { de: 'Beschreibung', en: 'Description' }, value: 'description' }, { label: { de: 'Überall', en: 'All' }, value: 'all' }, ], admin: { width: '25%' }, }, ], }, ], }, ``` --- ## Teil 4: Custom Admin Views ### Datei: `src/app/(payload)/admin/views/youtube/content-calendar/page.tsx` ```tsx 'use client' import React, { useState, useEffect, useCallback } from 'react' import { format, startOfMonth, endOfMonth, eachDayOfInterval, startOfWeek, endOfWeek, isSameMonth, isSameDay, addMonths, subMonths, } from 'date-fns' import { de } from 'date-fns/locale' import './calendar.scss' interface CalendarVideo { id: string title: string format: 'short' | 'longform' | 'premiere' contentSeries: string status: string productionDate?: string scheduledPublishDate?: string publishTime?: string channel: { id: string name: string slug: string } } interface Channel { id: string name: string slug: string } export default function ContentCalendarView() { const [currentMonth, setCurrentMonth] = useState(new Date()) const [videos, setVideos] = useState([]) const [channels, setChannels] = useState([]) const [selectedChannel, setSelectedChannel] = useState('all') const [viewType, setViewType] = useState<'production' | 'publishing'>('production') const [loading, setLoading] = useState(true) const fetchChannels = useCallback(async () => { try { const res = await fetch('/api/youtube-channels?limit=50') const data = await res.json() setChannels(data.docs || []) } catch (error) { console.error('Error fetching channels:', error) } }, []) const fetchVideos = useCallback(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()}&depth=1&limit=200` if (selectedChannel !== 'all') { query += `&where[channel][equals]=${selectedChannel}` } try { const res = await fetch(`/api/youtube-content?${query}`) const data = await res.json() setVideos(data.docs || []) } catch (error) { console.error('Error fetching videos:', error) } setLoading(false) }, [currentMonth, selectedChannel, viewType]) useEffect(() => { fetchChannels() }, [fetchChannels]) useEffect(() => { fetchVideos() }, [fetchVideos]) const getStatusColor = (status: string): string => { const colors: Record = { idea: '#9CA3AF', script_draft: '#60A5FA', script_review: '#60A5FA', script_approved: '#34D399', shoot_scheduled: '#FBBF24', shot: '#FBBF24', rough_cut: '#F97316', fine_cut: '#F97316', final_review: '#A78BFA', approved: '#34D399', upload_scheduled: '#34D399', published: '#10B981', tracked: '#059669', } return colors[status] || '#9CA3AF' } const renderCalendar = () => { const monthStart = startOfMonth(currentMonth) const monthEnd = endOfMonth(currentMonth) const calendarStart = startOfWeek(monthStart, { locale: de, weekStartsOn: 1 }) const calendarEnd = endOfWeek(monthEnd, { locale: de, weekStartsOn: 1 }) const days = eachDayOfInterval({ start: calendarStart, end: calendarEnd }) const dateField = viewType === 'production' ? 'productionDate' : 'scheduledPublishDate' return (
{['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map((day) => (
{day}
))}
{days.map((day) => { const dayVideos = videos.filter( (v) => v[dateField as keyof CalendarVideo] && isSameDay(new Date(v[dateField as keyof CalendarVideo] as string), day) ) return ( ) })}
) } return (

📅 Content Calendar

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

Format: Short Longform
Status: Idee Script Produktion Schnitt Fertig
{loading ? (
Lade Kalender...
) : ( renderCalendar() )}
) } ``` ### Datei: `src/app/(payload)/admin/views/youtube/content-calendar/calendar.scss` ```scss .content-calendar-view { padding: 20px; max-width: 1600px; margin: 0 auto; font-family: var(--font-body); } .calendar-controls { margin-bottom: 24px; h1 { margin: 0 0 16px 0; font-size: 24px; font-weight: 600; } } .controls-row { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; } .month-navigation { display: flex; align-items: center; gap: 16px; button { padding: 8px 16px; border: 1px solid var(--theme-elevation-150); border-radius: 4px; background: var(--theme-elevation-50); cursor: pointer; font-size: 14px; transition: all 0.2s; &:hover { background: var(--theme-elevation-100); } } h2 { min-width: 200px; text-align: center; margin: 0; font-size: 18px; font-weight: 600; } } .filters { display: flex; gap: 12px; select { padding: 8px 12px; border: 1px solid var(--theme-elevation-150); border-radius: 4px; background: var(--theme-elevation-50); font-size: 14px; cursor: pointer; } } .calendar-legend { display: flex; gap: 24px; margin-top: 16px; padding: 12px 16px; background: var(--theme-elevation-50); border-radius: 8px; flex-wrap: wrap; .legend-section { display: flex; align-items: center; gap: 12px; } .legend-title { font-weight: 600; font-size: 12px; text-transform: uppercase; color: var(--theme-elevation-500); } .legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; &::before { content: ''; width: 12px; height: 12px; border-radius: 2px; background: var(--status-color, #9CA3AF); } &.short::before { background: #3B82F6; } &.longform::before { background: #8B5CF6; } } } // Calendar Grid .calendar-grid { border: 1px solid var(--theme-elevation-150); border-radius: 8px; overflow: hidden; background: var(--theme-elevation-0); } .calendar-header { display: grid; grid-template-columns: repeat(7, 1fr); background: var(--theme-elevation-100); border-bottom: 1px solid var(--theme-elevation-150); } .calendar-header-cell { padding: 12px; text-align: center; font-weight: 600; font-size: 12px; text-transform: uppercase; color: var(--theme-elevation-600); } .calendar-body { display: grid; grid-template-columns: repeat(7, 1fr); } .calendar-cell { min-height: 140px; border-right: 1px solid var(--theme-elevation-100); border-bottom: 1px solid var(--theme-elevation-100); padding: 8px; background: var(--theme-elevation-0); transition: background 0.2s; &:nth-child(7n) { border-right: none; } &.outside-month { background: var(--theme-elevation-50); opacity: 0.6; } &.today { background: rgba(59, 130, 246, 0.05); .calendar-date { background: #3B82F6; color: white; border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; } } &:hover { background: var(--theme-elevation-50); } } .calendar-date { font-weight: 600; font-size: 14px; margin-bottom: 8px; color: var(--theme-elevation-800); } .calendar-videos { display: flex; flex-direction: column; gap: 4px; } .calendar-video { display: flex; flex-direction: column; padding: 6px 8px; border-radius: 4px; font-size: 11px; text-decoration: none; color: white; border-left: 3px solid; transition: transform 0.2s, opacity 0.2s; &.short { background: #3B82F6; } &.longform { background: #8B5CF6; } &.premiere { background: #EC4899; } &:hover { opacity: 0.9; transform: translateX(2px); } .video-series { font-weight: 700; font-size: 10px; text-transform: uppercase; opacity: 0.9; } .video-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; } .video-time { font-size: 10px; opacity: 0.8; margin-top: 2px; } } // Loading .loading { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px; gap: 16px; color: var(--theme-elevation-500); .loading-spinner { width: 32px; height: 32px; border: 3px solid var(--theme-elevation-150); border-top-color: #3B82F6; border-radius: 50%; animation: spin 1s linear infinite; } } @keyframes spin { to { transform: rotate(360deg); } } // Responsive @media (max-width: 1200px) { .calendar-cell { min-height: 120px; } .calendar-video { padding: 4px 6px; .video-title { display: none; } } } @media (max-width: 768px) { .controls-row { flex-direction: column; align-items: stretch; } .month-navigation { justify-content: center; } .filters { justify-content: center; } .calendar-cell { min-height: 80px; padding: 4px; } .calendar-date { font-size: 12px; } .calendar-video { .video-series { font-size: 9px; } } } ``` --- ## Teil 5: Payload Config Update ### Datei: `payload.config.ts` (Erweiterungen) ```typescript // Neue Imports import { YtBatches } from './collections/youtube/YtBatches' import { YtMonthlyGoals } from './collections/youtube/YtMonthlyGoals' import { YtScriptTemplates } from './collections/youtube/YtScriptTemplates' // In der collections Array hinzufügen: collections: [ // ... bestehende Collections YtBatches, YtMonthlyGoals, YtScriptTemplates, ], // Admin Config für Custom Views admin: { // ... bestehende Config components: { views: { // Custom YouTube Views YouTubeContentCalendar: { Component: '/app/(payload)/admin/views/youtube/content-calendar/page', path: '/youtube/calendar', }, }, }, }, ``` --- ## Teil 6: Seed-Daten (BlogWoman Januar) ### Datei: `src/seed/blogwoman-januar.ts` ```typescript import type { Payload } from 'payload' export async function seedBlogWomanJanuar(payload: Payload) { console.log('🌱 Seeding BlogWoman Januar Content...') // 1. BlogWoman Kanal finden const channels = await payload.find({ collection: 'youtube-channels', where: { slug: { equals: 'blogwoman' } }, limit: 1, }) let blogwomanChannel = channels.docs[0] if (!blogwomanChannel) { // Kanal erstellen falls nicht vorhanden blogwomanChannel = await payload.create({ collection: 'youtube-channels', data: { name: 'BlogWoman by Caroline Porwoll', slug: 'blogwoman', language: 'de', category: 'lifestyle', status: 'active', contentSeries: [ { name: 'GRFI', slug: 'grfi', description: 'Get Ready For Impact', color: '#3B82F6', isActive: true }, { name: 'Investment-Piece', slug: 'investment', description: 'Cost-per-Wear Analysen', color: '#8B5CF6', isActive: true }, { name: 'Pleasure P&L', slug: 'pleasure-pl', description: 'ROI auf Genuss', color: '#EC4899', isActive: true }, { name: 'M2M', slug: 'm2m', description: 'Meeting to Mom Mode', color: '#F59E0B', isActive: true }, { name: 'SPARK', slug: 'spark', description: 'Die Flamme', color: '#EF4444', isActive: true }, { name: 'Regeneration', slug: 'regeneration', description: 'Energie ohne Kitsch', color: '#10B981', isActive: true }, { name: 'Decision-Proof', slug: 'decision-proof', description: 'Regeln statt Willenskraft', color: '#6366F1', isActive: true }, { name: 'Sunday Reset', slug: 'sunday-reset', description: 'Wochenplanung', color: '#14B8A6', isActive: true }, ], publishingSchedule: { defaultDays: ['sunday', 'wednesday', 'saturday'], defaultTime: '07:00', shortsPerWeek: 7, longformPerWeek: 3, }, }, }) console.log('✅ BlogWoman Kanal erstellt') } // 2. Januar Batch erstellen const januarBatch1 = await payload.create({ collection: 'yt-batches', data: { name: 'Januar Woche 1', channel: blogwomanChannel.id, productionPeriod: { start: '2026-01-06', end: '2026-01-10', }, shootDays: [ { date: '2026-01-06', location: 'home', duration: '4h', notes: 'Longforms + Shorts' }, { date: '2026-01-08', location: 'home', duration: '4h', notes: 'SPARK + M2M' }, ], targets: { shortsTarget: 7, longformsTarget: 3, totalTarget: 10, bufferDays: 3, }, seriesDistribution: [ { series: 'GRFI', shortsCount: 2, longformsCount: 1, priority: 'high' }, { series: 'Investment-Piece', shortsCount: 1, longformsCount: 1, priority: 'high' }, { series: 'SPARK', shortsCount: 1, longformsCount: 1, priority: 'high' }, { series: 'M2M', shortsCount: 1, longformsCount: 0, priority: 'normal' }, { series: 'Regeneration', shortsCount: 1, longformsCount: 0, priority: 'normal' }, { series: 'Decision-Proof', shortsCount: 1, longformsCount: 0, priority: 'normal' }, ], status: 'planning', }, }) console.log('✅ Januar Batch 1 erstellt') // 3. Beispiel-Content erstellen const exampleVideos = [ { title: '7 Minuten: Boardroom-ready (mein komplettes System)', contentSeries: 'GRFI', format: 'longform', status: 'idea', productionBatch: januarBatch1.id, productionDate: '2026-01-06', scheduledPublishDate: '2026-01-12', publishTime: '10:00', targetDuration: '8-12 Min', hook: 'Board-Meeting in 20 Minuten. Der Morgen war Chaos, die Kinder waren anstrengend, der Kaffee kalt. Aber das Meeting muss sitzen. So werde ich in 7 Minuten ready.', thumbnailText: 'BOARD READY | 7 MIN', ctaType: 'newsletter', ctaDetail: 'GRFI-Checkliste', bRollNotes: 'Kleiderschrank, Spiegel, Badezimmer, 3 Outfit-Kombinationen zeigen', }, { title: '200€ Blazer: Cost-per-Wear nach 2 Jahren', contentSeries: 'Investment-Piece', format: 'longform', status: 'idea', productionBatch: januarBatch1.id, productionDate: '2026-01-07', scheduledPublishDate: '2026-01-15', publishTime: '17:00', targetDuration: '10-14 Min', hook: '200 Euro für einen Blazer. Klingt viel? Ich hab die Rechnung gemacht. Nach 2 Jahren. Mit echten Zahlen.', thumbnailText: '200€ | DIE RECHNUNG', ctaType: 'newsletter', ctaDetail: 'CPW-Rechner', bRollNotes: 'Blazer in 3 Kontexten: Board-Meeting, Office, Abend', }, { title: 'Call endet in 5 Min – Kita-Abholung in 20', contentSeries: 'M2M', format: 'short', status: 'idea', productionBatch: januarBatch1.id, productionDate: '2026-01-06', scheduledPublishDate: '2026-01-13', publishTime: '07:00', targetDuration: '45-58s', hook: 'Video-Call endet in 5 Minuten. Kita-Abholung in 20. So switch ich.', thumbnailText: 'BUSINESS → MAMA', ctaType: 'link_in_bio', bRollNotes: 'Business → Casual Transformation', }, ] for (const video of exampleVideos) { await payload.create({ collection: 'youtube-content', data: { ...video, channel: blogwomanChannel.id, priority: 'normal', }, }) } console.log(`✅ ${exampleVideos.length} Beispiel-Videos erstellt`) // 4. Monatsziele erstellen await payload.create({ collection: 'yt-monthly-goals', data: { channel: blogwomanChannel.id, month: '2026-01-01', contentGoals: { longformsTarget: 12, longformsCurrent: 0, shortsTarget: 28, shortsCurrent: 0, }, audienceGoals: { subscribersTarget: 500, subscribersCurrent: 0, viewsTarget: 50000, viewsCurrent: 0, }, engagementGoals: { avgCtrTarget: '>4%', avgRetentionTarget: '>50%', }, businessGoals: { newsletterSignupsTarget: 500, newsletterSignupsCurrent: 0, }, customGoals: [ { metric: 'SPARK Videos', target: '6', current: '0', status: 'on_track' }, { metric: 'Shorts Retention', target: '>60%', status: 'on_track' }, ], }, }) console.log('✅ Januar Monatsziele erstellt') console.log('🎉 BlogWoman Januar Seeding abgeschlossen!') } ``` --- ## Ausführungsreihenfolge 1. **Block erstellen:** `src/blocks/ScriptSection.ts` 2. **Collections erstellen:** - `src/collections/youtube/YtBatches.ts` - `src/collections/youtube/YtMonthlyGoals.ts` - `src/collections/youtube/YtScriptTemplates.ts` 3. **YouTubeContent erweitern:** Neue Felder hinzufügen 4. **payload.config.ts aktualisieren:** Neue Collections registrieren 5. **Custom Views erstellen:** - `src/app/(payload)/admin/views/youtube/content-calendar/page.tsx` - `src/app/(payload)/admin/views/youtube/content-calendar/calendar.scss` 6. **Migration erstellen:** `npx payload migrate:create youtube_ops_v2` 7. **Migration ausführen:** `npx payload migrate` 8. **Seed ausführen:** Optional für Beispieldaten --- ## Testschritte 1. [ ] Server starten: `npm run dev` 2. [ ] Neue Collections in Admin UI sichtbar unter "YouTube" 3. [ ] Production Batch erstellen 4. [ ] Video mit Script Sections erstellen 5. [ ] Content Calendar View aufrufen: `/admin/youtube/calendar` 6. [ ] Monthly Goals erstellen und prüfen --- ## Hinweise - **TypeScript Errors:** Bei Type-Fehlern `npm run generate:types` ausführen - **Migration Errors:** Datenbank-Schema prüfen, ggf. Reset mit `npx payload migrate:reset` - **SCSS kompilieren:** Payload kompiliert SCSS automatisch - **Lokalisierung:** Alle user-facing Texte sind DE/EN lokalisiert Bei Fragen oder Problemen: Logs prüfen und Fehler melden.