cms.c2sgmbh/prompts/youtube2.md
Martin Porwoll 77f70876f4 chore: add Claude Code config, prompts, and tenant setup scripts
- Add .claude/ configuration (agents, commands, hooks, get-shit-done workflows)
- Add prompts/ directory with development planning documents
- Add scripts/setup-tenants/ with tenant configuration
- Add docs/screenshots/
- Remove obsolete phase2.2-corrections-report.md
- Update pnpm-lock.yaml
- Update detect-secrets.sh to ignore setup.sh (env var usage, not secrets)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 10:18:05 +00:00

54 KiB
Raw Blame History

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

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

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

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

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:

// === 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

'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<CalendarVideo[]>([])
  const [channels, setChannels] = useState<Channel[]>([])
  const [selectedChannel, setSelectedChannel] = useState<string>('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<string, string> = {
      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 (
      <div className="calendar-grid">
        <div className="calendar-header">
          {['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map((day) => (
            <div key={day} className="calendar-header-cell">
              {day}
            </div>
          ))}
        </div>

        <div className="calendar-body">
          {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 (
              <div
                key={day.toISOString()}
                className={`calendar-cell ${!isSameMonth(day, currentMonth) ? 'outside-month' : ''} ${isSameDay(day, new Date()) ? 'today' : ''}`}
              >
                <div className="calendar-date">{format(day, 'd')}</div>
                <div className="calendar-videos">
                  {dayVideos.map((video) => (
                    <a
                      key={video.id}
                      href={`/admin/collections/youtube-content/${video.id}`}
                      className={`calendar-video ${video.format}`}
                      style={{ borderLeftColor: getStatusColor(video.status) }}
                      title={`${video.title} (${video.status})`}
                    >
                      <span className="video-series">{video.contentSeries}</span>
                      <span className="video-title">{video.title}</span>
                      {video.publishTime && viewType === 'publishing' && (
                        <span className="video-time">{video.publishTime}</span>
                      )}
                    </a>
                  ))}
                </div>
              </div>
            )
          })}
        </div>
      </div>
    )
  }

  return (
    <div className="content-calendar-view">
      <header className="calendar-controls">
        <h1>📅 Content Calendar</h1>

        <div className="controls-row">
          <div className="month-navigation">
            <button
              type="button"
              onClick={() => setCurrentMonth((prev) => subMonths(prev, 1))}
            >
               Vorheriger
            </button>
            <h2>{format(currentMonth, 'MMMM yyyy', { locale: de })}</h2>
            <button
              type="button"
              onClick={() => setCurrentMonth((prev) => addMonths(prev, 1))}
            >
              Nächster 
            </button>
          </div>

          <div className="filters">
            <select
              value={viewType}
              onChange={(e) => setViewType(e.target.value as 'production' | 'publishing')}
            >
              <option value="production">🎬 Produktion</option>
              <option value="publishing">📤 Veröffentlichung</option>
            </select>

            <select
              value={selectedChannel}
              onChange={(e) => setSelectedChannel(e.target.value)}
            >
              <option value="all">Alle Kanäle</option>
              {channels.map((channel) => (
                <option key={channel.id} value={channel.id}>
                  {channel.name}
                </option>
              ))}
            </select>
          </div>
        </div>

        <div className="calendar-legend">
          <div className="legend-section">
            <span className="legend-title">Format:</span>
            <span className="legend-item short">Short</span>
            <span className="legend-item longform">Longform</span>
          </div>
          <div className="legend-section">
            <span className="legend-title">Status:</span>
            <span className="legend-item" style={{ '--status-color': '#9CA3AF' } as React.CSSProperties}>
              Idee
            </span>
            <span className="legend-item" style={{ '--status-color': '#60A5FA' } as React.CSSProperties}>
              Script
            </span>
            <span className="legend-item" style={{ '--status-color': '#FBBF24' } as React.CSSProperties}>
              Produktion
            </span>
            <span className="legend-item" style={{ '--status-color': '#F97316' } as React.CSSProperties}>
              Schnitt
            </span>
            <span className="legend-item" style={{ '--status-color': '#10B981' } as React.CSSProperties}>
              Fertig
            </span>
          </div>
        </div>
      </header>

      {loading ? (
        <div className="loading">
          <div className="loading-spinner" />
          <span>Lade Kalender...</span>
        </div>
      ) : (
        renderCalendar()
      )}
    </div>
  )
}

Datei: src/app/(payload)/admin/views/youtube/content-calendar/calendar.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)

// 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

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.