# YouTube Operations Hub v2 – Claude Code Integration Prompt (Korrigiert) ## 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:** `/home/payload/payload-cms` **Datenbank:** PostgreSQL 17 auf sv-postgres (10.10.181.101) **Tech Stack:** Payload CMS 3.69.0, Next.js 15.5.9, TypeScript, Drizzle ORM **Package Manager:** pnpm --- ## Aufgabe Erweitere das bestehende YouTube Operations Hub um: 1. **Script Section Block** – Block für strukturierte Video-Skripte 2. **Batch-Planung** – Neue Collection für detaillierte Produktions-Batches 3. **Monthly Goals** – KPI-Tracking pro Kanal und Monat 4. **Script Templates** – Wiederverwendbare Skript-Vorlagen 5. **Checklist Templates** – Wiederverwendbare Upload-Checklisten 6. **Erweiterte YouTube Content Felder** – Produktions- und Posting-Informationen > **Hinweis:** Custom Admin Views (Kalender) sind derzeit nicht möglich wegen eines path-to-regexp Bugs in Payload 3.x. Diese werden in einem späteren Update implementiert (siehe `prompts/youtube3-admin-views.md`). --- ## Teil 1: Script Section Block ### Datei: `src/blocks/ScriptSectionBlock.ts` ```typescript import type { Block } from 'payload' export const ScriptSectionBlock: Block = { slug: 'script-section', labels: { singular: { de: 'Script Section', en: 'Script Section' }, plural: { de: 'Script Sections', en: 'Script Sections' }, }, fields: [ { type: 'row', fields: [ { name: 'sectionType', type: 'select', required: true, label: { de: 'Section-Typ', en: 'Section Type' }, 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, label: { de: 'Anweisung', en: 'Instruction' }, admin: { width: '70%', placeholder: 'z.B. "Outfit zeigen", "Kleiderschrank-Aufnahme"', }, }, { name: 'timestamp', type: 'text', label: { de: 'Zeitpunkt', en: 'Timestamp' }, 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, label: { de: 'Text', en: 'Text' }, admin: { width: '70%', placeholder: 'z.B. "3 Kombinationen = 0 Entscheidungen"', }, }, { name: 'style', type: 'select', label: { de: 'Stil', en: 'Style' }, 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/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', 'updatedAt'], 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, label: { de: 'Datum', en: 'Date' }, admin: { width: '25%', date: { pickerAppearance: 'dayOnly' }, }, }, { name: 'location', type: 'select', label: { de: 'Location', en: 'Location' }, options: [ { label: 'Home Studio', value: 'home' }, { label: 'Office', value: 'office' }, { label: { de: 'Unterwegs', en: 'On the Go' }, value: 'mobile' }, { label: 'Extern', 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: { de: 'Hoch', en: 'High' }, value: 'high' }, { label: 'Normal', value: 'normal' }, { label: { de: 'Niedrig', en: 'Low' }, 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: 'Scripts', value: 'scripting' }, { label: { de: 'Produktion', en: 'Production' }, value: 'production' }, { label: { de: 'Schnitt', en: 'Editing' }, value: 'editing' }, { label: 'Review', value: 'review' }, { label: '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', }, fields: [ { name: 'shortsCompleted', type: 'number', label: { de: 'Shorts fertig', en: 'Shorts Completed' }, defaultValue: 0, admin: { readOnly: true }, }, { name: 'longformsCompleted', type: 'number', label: { de: 'Longforms fertig', en: 'Longforms Completed' }, defaultValue: 0, admin: { readOnly: true }, }, { name: 'percentage', type: 'number', label: { de: 'Gesamt %', en: 'Total %' }, defaultValue: 0, 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: { format?: string }) => v.format === 'short') const longforms = videos.docs.filter((v: { format?: string }) => v.format === 'longform') const completedStatuses = ['approved', 'upload_scheduled', 'published', 'tracked'] const shortsCompleted = shorts.filter((v: { status?: string }) => completedStatuses.includes(v.status || '') ).length const longformsCompleted = longforms.filter((v: { status?: string }) => 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 } }, ], }, timestamps: true, } ``` ### Datei: `src/collections/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, }, ], timestamps: true, } ``` ### Datei: `src/collections/YtScriptTemplates.ts` ```typescript import type { CollectionConfig } from 'payload' import { ScriptSectionBlock } from '../blocks/ScriptSectionBlock' 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], }, ], timestamps: true, } ``` ### Datei: `src/collections/YtChecklistTemplates.ts` ```typescript import type { CollectionConfig } from 'payload' import { isYouTubeManager, hasYouTubeAccess } from '../lib/youtubeAccess' export const YtChecklistTemplates: CollectionConfig = { slug: 'yt-checklist-templates', labels: { singular: { de: 'Checklisten-Vorlage', en: 'Checklist Template' }, plural: { de: 'Checklisten-Vorlagen', en: 'Checklist Templates' }, }, admin: { group: 'YouTube', useAsTitle: 'name', defaultColumns: ['name', 'type', 'channel', 'updatedAt'], description: { de: 'Wiederverwendbare Checklisten für Upload und Produktion', en: 'Reusable checklists for upload and production' }, }, access: { read: hasYouTubeAccess, create: isYouTubeManager, update: isYouTubeManager, delete: isYouTubeManager, }, fields: [ { type: 'row', fields: [ { name: 'name', type: 'text', required: true, localized: true, label: { de: 'Vorlagen-Name', en: 'Template Name' }, admin: { width: '50%', placeholder: 'z.B. "Standard Upload Checklist"', }, }, { name: 'channel', type: 'relationship', relationTo: 'youtube-channels', label: { de: 'Kanal', en: 'Channel' }, admin: { width: '50%', description: { de: 'Optional: Kanal-spezifische Vorlage', en: 'Optional: Channel-specific template' }, }, }, ], }, { type: 'row', fields: [ { name: 'type', type: 'select', required: true, label: { de: 'Typ', en: 'Type' }, options: [ { label: { de: 'Upload-Checkliste', en: 'Upload Checklist' }, value: 'upload' }, { label: { de: 'Produktions-Checkliste', en: 'Production Checklist' }, value: 'production' }, { label: { de: 'Review-Checkliste', en: 'Review Checklist' }, value: 'review' }, { label: { de: 'Post-Publish-Checkliste', en: 'Post-Publish Checklist' }, value: 'post_publish' }, ], admin: { width: '50%' }, }, { name: 'format', type: 'select', label: { de: 'Format', en: 'Format' }, options: [ { label: { de: 'Alle Formate', en: 'All Formats' }, value: 'all' }, { label: 'Short', value: 'short' }, { label: 'Longform', value: 'longform' }, ], defaultValue: 'all', admin: { width: '50%' }, }, ], }, { name: 'description', type: 'textarea', label: { de: 'Beschreibung', en: 'Description' }, localized: true, admin: { rows: 2, description: { de: 'Wann diese Checkliste verwendet werden soll', en: 'When to use this checklist' }, }, }, { name: 'items', type: 'array', required: true, label: { de: 'Checklisten-Punkte', en: 'Checklist Items' }, labels: { singular: { de: 'Punkt', en: 'Item' }, plural: { de: 'Punkte', en: 'Items' }, }, minRows: 1, fields: [ { type: 'row', fields: [ { name: 'order', type: 'number', label: { de: 'Reihenfolge', en: 'Order' }, min: 1, admin: { width: '15%' }, }, { name: 'task', type: 'text', required: true, localized: true, label: { de: 'Aufgabe', en: 'Task' }, admin: { width: '55%', placeholder: 'z.B. "Thumbnail hochladen"', }, }, { name: 'category', type: 'select', label: { de: 'Kategorie', en: 'Category' }, options: [ { label: 'Metadaten', value: 'metadata' }, { label: 'Assets', value: 'assets' }, { label: 'SEO', value: 'seo' }, { label: 'Community', value: 'community' }, { label: { de: 'Rechtliches', en: 'Legal' }, value: 'legal' }, { label: { de: 'Sonstiges', en: 'Other' }, value: 'other' }, ], admin: { width: '30%' }, }, ], }, { name: 'details', type: 'textarea', localized: true, label: { de: 'Details/Hinweise', en: 'Details/Notes' }, admin: { rows: 2, placeholder: 'Zusätzliche Anweisungen oder Hinweise', }, }, { name: 'isRequired', type: 'checkbox', label: { de: 'Pflichtfeld', en: 'Required' }, defaultValue: true, }, ], }, { name: 'isDefault', type: 'checkbox', label: { de: 'Standard-Vorlage', en: 'Default Template' }, admin: { position: 'sidebar', description: { de: 'Wird automatisch für neue Videos verwendet', en: 'Automatically used for new videos' }, }, }, { name: 'isActive', type: 'checkbox', label: { de: 'Aktiv', en: 'Active' }, defaultValue: true, admin: { position: 'sidebar', }, }, ], timestamps: true, } ``` --- ## Teil 3: Erweiterung YouTube Content Collection ### Datei: `src/collections/YouTubeContent.ts` Füge folgende Änderungen zur bestehenden YouTubeContent Collection hinzu: **1. Import am Anfang der Datei hinzufügen:** ```typescript import { ScriptSectionBlock } from '../blocks/ScriptSectionBlock' ``` **2. Neue Felder in der Sidebar (nach `createdBy` bei ca. Zeile 187):** ```typescript { name: 'productionBatch', type: 'relationship', relationTo: 'yt-batches', label: { de: 'Produktions-Batch', en: 'Production Batch' }, admin: { position: 'sidebar', }, }, ``` **3. Neuen Tab "Produktion" hinzufügen (im `tabs` Array nach dem Tab "Termine"):** ```typescript { label: { de: 'Produktion', en: 'Production' }, fields: [ { 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: 3, }, }, ], }, ``` **4. Neuen Tab "Script" hinzufügen (nach dem Tab "Produktion"):** ```typescript { label: 'Script', fields: [ { name: 'script', type: 'blocks', label: { de: 'Script Sections', en: 'Script Sections' }, 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' }, }, }, ], }, ``` **5. Neuen Tab "Posting" hinzufügen (nach dem Tab "Script"):** ```typescript { label: 'Posting', fields: [ { 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: { de: 'Longform verlinken', en: 'Link Longform' }, value: 'longform_link' }, { label: { de: 'Benutzerdefiniert', en: 'Custom' }, 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, }, }, ], }, { name: 'uploadChecklist', type: 'array', label: { de: 'Upload-Checkliste', en: 'Upload Checklist' }, fields: [ { type: 'row', fields: [ { name: 'step', type: 'text', required: true, label: { de: 'Schritt', en: 'Step' }, admin: { width: '60%' }, }, { name: 'completed', type: 'checkbox', label: { de: 'Erledigt', en: 'Done' }, admin: { width: '20%' }, }, { name: 'completedAt', type: 'date', label: { de: 'Am', en: 'At' }, admin: { width: '20%', readOnly: true, }, }, ], }, ], }, { 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: { de: 'Medizinisch', en: 'Medical' }, value: 'medical' }, { label: { de: 'Rechtlich', en: 'Legal' }, 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: Payload Config Update ### Datei: `src/payload.config.ts` **1. Neue Imports hinzufügen (nach den bestehenden YouTube Imports bei ca. Zeile 75):** ```typescript // YouTube Operations Hub Collections - Erweitert import { YtBatches } from './collections/YtBatches' import { YtMonthlyGoals } from './collections/YtMonthlyGoals' import { YtScriptTemplates } from './collections/YtScriptTemplates' import { YtChecklistTemplates } from './collections/YtChecklistTemplates' ``` **2. Collections zum Array hinzufügen (nach YtNotifications):** ```typescript collections: [ // ... bestehende Collections YtBatches, YtMonthlyGoals, YtScriptTemplates, YtChecklistTemplates, ], ``` --- ## Teil 5: Migration erstellen ### Wichtige Hinweise zur Migration Die Migration muss folgende Tabellen erstellen: **Haupt-Tabellen:** - `yt_batches` - `yt_monthly_goals` - `yt_script_templates` - `yt_checklist_templates` **Array-Tabellen (werden von Payload automatisch erstellt, aber prüfen!):** - `yt_batches_production_period_shoot_days` - `yt_batches_series_distribution` - `yt_checklist_templates_items` - `yt_monthly_goals_custom_goals` - `yt_script_templates_template_sections` - `youtube_content_script` (für ScriptSectionBlock) - `youtube_content_upload_checklist` - `youtube_content_disclaimers` **System-Tabelle MUSS erweitert werden:** ```sql -- KRITISCH: Diese Spalten zur System-Tabelle hinzufügen ALTER TABLE "payload_locked_documents_rels" ADD COLUMN IF NOT EXISTS "yt_batches_id" integer REFERENCES yt_batches(id) ON DELETE CASCADE; ALTER TABLE "payload_locked_documents_rels" ADD COLUMN IF NOT EXISTS "yt_monthly_goals_id" integer REFERENCES yt_monthly_goals(id) ON DELETE CASCADE; ALTER TABLE "payload_locked_documents_rels" ADD COLUMN IF NOT EXISTS "yt_script_templates_id" integer REFERENCES yt_script_templates(id) ON DELETE CASCADE; ALTER TABLE "payload_locked_documents_rels" ADD COLUMN IF NOT EXISTS "yt_checklist_templates_id" integer REFERENCES yt_checklist_templates(id) ON DELETE CASCADE; -- Indexes CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_yt_batches_idx" ON "payload_locked_documents_rels" ("yt_batches_id"); CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_yt_monthly_goals_idx" ON "payload_locked_documents_rels" ("yt_monthly_goals_id"); CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_yt_script_templates_idx" ON "payload_locked_documents_rels" ("yt_script_templates_id"); CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_yt_checklist_templates_idx" ON "payload_locked_documents_rels" ("yt_checklist_templates_id"); ``` ### Migration erstellen und ausführen ```bash # 1. Migration erstellen pnpm payload migrate:create youtube_ops_v2 # 2. Migration-Datei prüfen und ggf. anpassen # Datei: src/migrations/[timestamp]_youtube_ops_v2.ts # 3. Migration ausführen (direkte DB-Verbindung empfohlen) ./scripts/db-direct.sh migrate # 4. Build erstellen pnpm build # 5. PM2 neu starten pm2 restart payload ``` --- ## Teil 6: Seed-Daten (BlogWoman Januar) - Optional ### 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 oder erstellen const channels = await payload.find({ collection: 'youtube-channels', where: { slug: { equals: 'blogwoman' } }, limit: 1, }) let blogwomanChannel = channels.docs[0] if (!blogwomanChannel) { // Kanal erstellen (mit allen Pflichtfeldern!) blogwomanChannel = await payload.create({ collection: 'youtube-channels', data: { name: 'BlogWoman by Caroline Porwoll', slug: 'blogwoman', youtubeChannelId: 'UCxxxxxxxxxxxxxxxxxx', // PFLICHT - echte ID eintragen! youtubeHandle: '@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, 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' as const, status: 'idea' as const, 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' as const, ctaDetail: 'GRFI-Checkliste', bRollNotes: 'Kleiderschrank, Spiegel, Badezimmer, 3 Outfit-Kombinationen zeigen', }, { title: '200 Euro Blazer: Cost-per-Wear nach 2 Jahren', contentSeries: 'investment', format: 'longform' as const, status: 'idea' as const, 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 EURO | DIE RECHNUNG', ctaType: 'newsletter' as const, 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' as const, status: 'idea' as const, 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 zu MAMA', ctaType: 'link_in_bio' as const, bRollNotes: 'Business zu Casual Transformation', }, ] for (const video of exampleVideos) { await payload.create({ collection: 'youtube-content', data: { ...video, channel: blogwomanChannel.id, priority: 'normal', slug: video.title .toLowerCase() .replace(/[äöüß]/g, (char: string) => { const map: Record = { ä: 'ae', ö: 'oe', ü: 'ue', ß: 'ss' } return map[char] || char }) .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''), }, }) } 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/ScriptSectionBlock.ts` 2. **Collections erstellen:** - `src/collections/YtBatches.ts` - `src/collections/YtMonthlyGoals.ts` - `src/collections/YtScriptTemplates.ts` 3. **YouTubeContent.ts erweitern:** Import + neue Felder/Tabs 4. **payload.config.ts aktualisieren:** Imports + Collections Array 5. **Migration erstellen:** `pnpm payload migrate:create youtube_ops_v2` 6. **Migration prüfen und anpassen:** - Array-Tabellen vorhanden? - `payload_locked_documents_rels` erweitert? 7. **Migration ausführen:** `./scripts/db-direct.sh migrate` 8. **Build:** `pnpm build` 9. **PM2 neustarten:** `pm2 restart payload` 10. **Testen:** Collections im Admin Panel prüfen --- ## Testschritte 1. [ ] Server starten und Admin Panel öffnen 2. [ ] Neue Collections unter "YouTube" sichtbar: - Production Batches - Monatsziele - Script Templates - Checklisten-Vorlagen 3. [ ] Production Batch erstellen und mit Kanal verknüpfen 4. [ ] Checklisten-Vorlage erstellen (z.B. "Standard Upload Checklist") 5. [ ] YouTube-Video erstellen mit: - Production Batch Zuweisung - Script Sections - Upload-Checkliste 6. [ ] Monthly Goal für aktuellen Monat erstellen 6. [ ] Script Template erstellen und bei Video verwenden --- ## Bekannte Einschränkungen - **Custom Admin Views:** Kalender-Ansicht nicht möglich wegen path-to-regexp Bug in Payload 3.x - **SCSS:** Payload 3.x verwendet CSS Modules, keine SCSS-Kompilierung - **PgBouncer:** Für Migrationen `./scripts/db-direct.sh` verwenden (umgeht Transaction-Mode) --- ## Troubleshooting **"relation does not exist" Fehler:** - Migration wurde nicht korrekt ausgeführt - Array-Tabellen fehlen - `payload_locked_documents_rels` nicht erweitert - Lösung: Migration-Datei prüfen und erneut ausführen **TypeScript-Fehler:** ```bash pnpm payload generate:types ``` **Collection nicht sichtbar:** - Import in `payload.config.ts` prüfen - Collection im `collections` Array vorhanden? - Build neu erstellen: `pnpm build` **Hook-Fehler bei Progress-Berechnung:** - `productionBatch` Feld in YouTubeContent vorhanden? - Migration für neue Felder ausgeführt?