cms.c2sgmbh/prompts/hub-concept.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

48 KiB
Raw Permalink Blame History

YouTube Operations Hub v2 Vollständiges Konzept

Übersicht der Erweiterungen

Neue Features:

  1. Script Editor Custom Lexical Rich-Text-Editor mit Section-Blöcken
  2. Kalenderansichten Content Calendar + Posting Calendar direkt in Payload
  3. Batch-Planung Detaillierte Produktions-Batches mit Kapazitätsplanung
  4. Monthly Goals Dashboard KPI-Tracking pro Kanal und Monat
  5. Upload Workflow Strukturierte Checklisten mit Fortschrittsanzeige

1. Script Editor Custom Lexical Blocks

1.1 Konzept

Payload 3.x verwendet Lexical als Rich-Text-Editor. Wir erstellen Custom Blocks für die Skript-Struktur:

┌─────────────────────────────────────────────────────────────┐
│  📝 Script Editor                                           │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 🎬 HOOK [0-10 Sek]                            [Edit]│   │
│  │ "Board-Meeting in 20 Minuten. Der Morgen war       │   │
│  │  Chaos, die Kinder waren anstrengend..."           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 🏷️ INTRO-IDENT [1-2 Sek]                     [Edit]│   │
│  │ [GRFI Serien-Badge einblenden]                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 📖 CONTEXT [45-60 Sek]                        [Edit]│   │
│  │ "Kennst du das? Du wachst auf mit einem Plan..."    │   │
│  │                                                      │   │
│  │ 🎥 B-Roll: Morgenroutine, Meetings                  │   │
│  │ 📝 Overlay: "System schlägt Motivation"             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 📚 TEIL 1: OUTFIT-LOGIK [2-3 Min]             [Edit]│   │
│  │ "Fangen wir mit der Basis an: dem Outfit..."        │   │
│  │                                                      │   │
│  │ 🎥 B-Roll: Outfit zeigen, 3 Kombinationen           │   │
│  │ 📝 Overlay: "3 Kombinationen = 0 Entscheidungen"    │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  [+ Section hinzufügen ▾]                                   │
│    • Hook                                                   │
│    • Intro/Ident                                            │
│    • Context                                                │
│    • Content Part                                           │
│    • Summary                                                │
│    • CTA                                                    │
│    • Outro                                                  │
│    • Disclaimer                                             │
└─────────────────────────────────────────────────────────────┘

1.2 Script Section Block Schema

// src/blocks/ScriptSection.ts

import { Block } from 'payload/types'

export const ScriptSectionBlock: Block = {
  slug: 'scriptSection',
  labels: {
    singular: 'Script Section',
    plural: 'Script Sections',
  },
  fields: [
    {
      name: 'sectionType',
      type: 'select',
      required: true,
      options: [
        { label: '🎬 Hook', value: 'hook' },
        { label: '🏷️ Intro/Ident', value: 'intro_ident' },
        { label: '📖 Context', value: 'context' },
        { label: '📚 Content Part', value: 'content_part' },
        { label: '📋 Summary', value: 'summary' },
        { label: '📢 CTA', value: 'cta' },
        { label: '🎬 Outro', value: 'outro' },
        { label: '⚠️ Disclaimer', value: 'disclaimer' },
      ],
      admin: {
        description: 'Art der Script-Section',
      },
    },
    {
      name: 'sectionTitle',
      type: 'text',
      label: 'Section-Titel',
      admin: {
        placeholder: 'z.B. "TEIL 1: OUTFIT-LOGIK"',
        condition: (data, siblingData) => 
          siblingData?.sectionType === 'content_part',
      },
    },
    {
      name: 'duration',
      type: 'text',
      label: 'Dauer',
      admin: {
        placeholder: 'z.B. "2-3 Min" oder "45-60 Sek"',
        width: '25%',
      },
    },
    {
      name: 'spokenText',
      type: 'richText',
      label: 'Gesprochener Text',
      required: true,
      admin: {
        description: 'Der Text, den Caroline spricht',
      },
    },
    {
      name: 'bRollInstructions',
      type: 'array',
      label: 'B-Roll Anweisungen',
      labels: {
        singular: 'B-Roll',
        plural: 'B-Roll Anweisungen',
      },
      fields: [
        {
          name: 'instruction',
          type: 'text',
          required: true,
          admin: {
            placeholder: 'z.B. "Outfit zeigen", "Kleiderschrank-Aufnahme"',
          },
        },
        {
          name: 'timestamp',
          type: 'text',
          admin: {
            placeholder: 'Optional: "bei 0:45"',
            width: '30%',
          },
        },
      ],
    },
    {
      name: 'textOverlays',
      type: 'array',
      label: 'Text-Overlays',
      labels: {
        singular: 'Overlay',
        plural: 'Text-Overlays',
      },
      fields: [
        {
          name: 'text',
          type: 'text',
          required: true,
          admin: {
            placeholder: 'z.B. "3 Kombinationen = 0 Entscheidungen"',
          },
        },
        {
          name: 'style',
          type: 'select',
          defaultValue: 'standard',
          options: [
            { label: 'Standard', value: 'standard' },
            { label: 'Highlight', value: 'highlight' },
            { label: 'Quote', value: 'quote' },
            { label: 'Statistik', value: 'statistic' },
            { label: 'Liste', value: 'list' },
          ],
        },
      ],
    },
    {
      name: 'visualNotes',
      type: 'textarea',
      label: 'Visuelle Notizen',
      admin: {
        placeholder: 'Zusätzliche Anweisungen für Schnitt/Grafik',
        rows: 2,
      },
    },
  ],
}

1.3 Script Editor Field in YouTube Content

// In YouTubeContent.ts

{
  name: 'script',
  type: 'blocks',
  label: 'Script',
  labels: {
    singular: 'Section',
    plural: 'Script Sections',
  },
  blocks: [ScriptSectionBlock],
  admin: {
    description: 'Strukturiertes Video-Script mit Sections',
  },
}

1.4 Script Templates

// src/collections/ScriptTemplates.ts

export const ScriptTemplates: CollectionConfig = {
  slug: 'yt-script-templates',
  labels: {
    singular: 'Script Template',
    plural: 'Script Templates',
  },
  admin: {
    group: 'YouTube',
    useAsTitle: 'name',
    defaultColumns: ['name', 'series', 'format', 'updatedAt'],
  },
  fields: [
    {
      name: 'name',
      type: 'text',
      required: true,
      localized: true,
      // z.B. "GRFI Longform Template", "M2M Short Template"
    },
    {
      name: 'series',
      type: 'text',
      required: true,
      // z.B. "GRFI", "M2M", "SPARK"
    },
    {
      name: 'format',
      type: 'select',
      required: true,
      options: [
        { label: 'Short (45-60s)', value: 'short' },
        { label: 'Longform (8-16 Min)', value: 'longform' },
      ],
    },
    {
      name: 'channel',
      type: 'relationship',
      relationTo: 'youtube-channels',
      required: true,
    },
    {
      name: 'templateSections',
      type: 'blocks',
      blocks: [ScriptSectionBlock],
      // Vorgefüllte Section-Struktur
    },
    {
      name: 'notes',
      type: 'textarea',
      localized: true,
      admin: {
        description: 'Hinweise zur Verwendung dieses Templates',
      },
    },
  ],
}

2. Kalenderansichten in Payload Admin

2.1 Custom Admin Views Struktur

src/
├── app/(payload)/admin/
│   ├── [[...segments]]/
│   │   └── page.tsx          # Standard Payload Admin
│   └── views/
│       └── youtube/
│           ├── content-calendar/
│           │   └── page.tsx   # Content/Production Calendar
│           ├── posting-calendar/
│           │   └── page.tsx   # Publishing Calendar
│           └── batch-overview/
│               └── page.tsx   # Batch Dashboard

2.2 Content Calendar View

// src/app/(payload)/admin/views/youtube/content-calendar/page.tsx

'use client'

import React, { useState, useEffect } from 'react'
import { useConfig } from '@payloadcms/ui'
import { format, startOfMonth, endOfMonth, eachDayOfInterval, 
         startOfWeek, endOfWeek, isSameMonth, isSameDay } from 'date-fns'
import { de } from 'date-fns/locale'

interface CalendarVideo {
  id: string
  title: string
  format: 'short' | 'longform'
  series: string
  status: string
  productionDate?: string
  scheduledPublishDate?: string
  channel: {
    name: string
    slug: string
  }
}

export default function ContentCalendarView() {
  const [currentMonth, setCurrentMonth] = useState(new Date())
  const [videos, setVideos] = useState<CalendarVideo[]>([])
  const [selectedChannel, setSelectedChannel] = useState<string>('all')
  const [viewType, setViewType] = useState<'production' | 'publishing'>('production')
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetchVideos()
  }, [currentMonth, selectedChannel])

  const fetchVideos = async () => {
    setLoading(true)
    const start = startOfMonth(currentMonth)
    const end = endOfMonth(currentMonth)
    
    const dateField = viewType === 'production' ? 'productionDate' : 'scheduledPublishDate'
    
    let query = `where[${dateField}][greater_than_equal]=${start.toISOString()}&where[${dateField}][less_than_equal]=${end.toISOString()}`
    
    if (selectedChannel !== 'all') {
      query += `&where[channel][equals]=${selectedChannel}`
    }
    
    const res = await fetch(`/api/youtube-content?${query}&depth=1&limit=100`)
    const data = await res.json()
    setVideos(data.docs)
    setLoading(false)
  }

  const renderCalendar = () => {
    const monthStart = startOfMonth(currentMonth)
    const monthEnd = endOfMonth(currentMonth)
    const calendarStart = startOfWeek(monthStart, { locale: de })
    const calendarEnd = endOfWeek(monthEnd, { locale: de })
    
    const days = eachDayOfInterval({ start: calendarStart, end: calendarEnd })
    
    return (
      <div className="calendar-grid">
        {/* Header */}
        <div className="calendar-header">
          {['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map(day => (
            <div key={day} className="calendar-header-cell">{day}</div>
          ))}
        </div>
        
        {/* Days */}
        <div className="calendar-body">
          {days.map(day => {
            const dateField = viewType === 'production' ? 'productionDate' : 'scheduledPublishDate'
            const dayVideos = videos.filter(v => 
              v[dateField] && isSameDay(new Date(v[dateField]), day)
            )
            
            return (
              <div 
                key={day.toISOString()}
                className={`calendar-cell ${!isSameMonth(day, currentMonth) ? 'outside-month' : ''}`}
              >
                <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} ${video.status}`}
                    >
                      <span className="video-series">{video.series}</span>
                      <span className="video-title">{video.title}</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 onClick={() => setCurrentMonth(prev => 
              new Date(prev.getFullYear(), prev.getMonth() - 1)
            )}>
               Vorheriger
            </button>
            <h2>{format(currentMonth, 'MMMM yyyy', { locale: de })}</h2>
            <button onClick={() => setCurrentMonth(prev => 
              new Date(prev.getFullYear(), prev.getMonth() + 1)
            )}>
              Nächster 
            </button>
          </div>
          
          <div className="filters">
            <select 
              value={viewType} 
              onChange={e => setViewType(e.target.value as any)}
            >
              <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>
              <option value="blogwoman">BlogWoman</option>
              <option value="corporate-de">Corporate DE</option>
              <option value="business-en">Business EN</option>
            </select>
          </div>
        </div>
        
        {/* Legend */}
        <div className="calendar-legend">
          <span className="legend-item short">Short</span>
          <span className="legend-item longform">Longform</span>
          <span className="legend-item status-idea">Idee</span>
          <span className="legend-item status-production">In Produktion</span>
          <span className="legend-item status-ready">Fertig</span>
          <span className="legend-item status-published">Veröffentlicht</span>
        </div>
      </header>
      
      {loading ? (
        <div className="loading">Lade Kalender...</div>
      ) : (
        renderCalendar()
      )}
    </div>
  )
}

2.3 Calendar Styles

/* src/app/(payload)/admin/views/youtube/calendar.css */

.content-calendar-view {
  padding: 20px;
  max-width: 1400px;
  margin: 0 auto;
}

.calendar-controls {
  margin-bottom: 24px;
}

.controls-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 16px;
}

.month-navigation {
  display: flex;
  align-items: center;
  gap: 16px;
}

.month-navigation button {
  padding: 8px 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: white;
  cursor: pointer;
}

.month-navigation h2 {
  min-width: 200px;
  text-align: center;
}

.filters {
  display: flex;
  gap: 12px;
}

.filters select {
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.calendar-legend {
  display: flex;
  gap: 16px;
  margin-top: 16px;
  padding: 12px;
  background: #f5f5f5;
  border-radius: 4px;
}

.legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
}

.legend-item::before {
  content: '';
  width: 12px;
  height: 12px;
  border-radius: 2px;
}

.legend-item.short::before { background: #3B82F6; }
.legend-item.longform::before { background: #8B5CF6; }
.legend-item.status-idea::before { background: #9CA3AF; }
.legend-item.status-production::before { background: #F59E0B; }
.legend-item.status-ready::before { background: #10B981; }
.legend-item.status-published::before { background: #059669; }

/* Calendar Grid */
.calendar-grid {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
}

.calendar-header {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  background: #f5f5f5;
  border-bottom: 1px solid #ddd;
}

.calendar-header-cell {
  padding: 12px;
  text-align: center;
  font-weight: 600;
  font-size: 12px;
  text-transform: uppercase;
}

.calendar-body {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
}

.calendar-cell {
  min-height: 120px;
  border-right: 1px solid #eee;
  border-bottom: 1px solid #eee;
  padding: 8px;
}

.calendar-cell:nth-child(7n) {
  border-right: none;
}

.calendar-cell.outside-month {
  background: #fafafa;
  opacity: 0.5;
}

.calendar-date {
  font-weight: 600;
  font-size: 14px;
  margin-bottom: 8px;
}

.calendar-videos {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.calendar-video {
  display: block;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 11px;
  text-decoration: none;
  color: white;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.calendar-video.short {
  background: #3B82F6;
}

.calendar-video.longform {
  background: #8B5CF6;
}

.calendar-video .video-series {
  font-weight: 600;
  margin-right: 4px;
}

.calendar-video:hover {
  opacity: 0.9;
  transform: scale(1.02);
}

/* Status Colors als Border */
.calendar-video.idea { border-left: 3px solid #9CA3AF; }
.calendar-video.script_draft { border-left: 3px solid #F59E0B; }
.calendar-video.in_production { border-left: 3px solid #F59E0B; }
.calendar-video.ready { border-left: 3px solid #10B981; }
.calendar-video.published { border-left: 3px solid #059669; }

2.4 Admin Navigation erweitern

// payload.config.ts - Admin Navigation

export default buildConfig({
  admin: {
    components: {
      // Custom Navigation mit YouTube Views
      Nav: '/components/CustomNav',
    },
  },
  // ...
})
// src/components/CustomNav.tsx

'use client'

import { NavGroup, Nav as DefaultNav } from '@payloadcms/ui'
import Link from 'next/link'

export const CustomNav = () => {
  return (
    <nav>
      <DefaultNav />
      
      {/* YouTube Views */}
      <NavGroup label="YouTube Views">
        <Link href="/admin/views/youtube/content-calendar">
          📅 Content Calendar
        </Link>
        <Link href="/admin/views/youtube/posting-calendar">
          📤 Posting Calendar
        </Link>
        <Link href="/admin/views/youtube/batch-overview">
          📦 Batch Overview
        </Link>
        <Link href="/admin/views/youtube/monthly-goals">
          🎯 Monthly Goals
        </Link>
      </NavGroup>
    </nav>
  )
}

3. Detaillierte Batch-Planung

3.1 Batch Collection

// src/collections/YtBatches.ts

import { CollectionConfig } from 'payload/types'
import { isYouTubeManager, isYouTubeCreatorOrAbove } from '../lib/youtubeAccess'

export const YtBatches: CollectionConfig = {
  slug: 'yt-batches',
  labels: {
    singular: 'Production Batch',
    plural: 'Production Batches',
  },
  admin: {
    group: 'YouTube',
    useAsTitle: 'name',
    defaultColumns: ['name', 'channel', 'status', 'productionStart', 'progress'],
    listSearchableFields: ['name'],
  },
  access: {
    read: isYouTubeCreatorOrAbove,
    create: isYouTubeManager,
    update: isYouTubeCreatorOrAbove,
    delete: isYouTubeManager,
  },
  fields: [
    // === BASIC INFO ===
    {
      type: 'row',
      fields: [
        {
          name: 'name',
          type: 'text',
          required: true,
          admin: {
            placeholder: 'z.B. "Januar Woche 1" oder "Batch 1"',
            width: '50%',
          },
        },
        {
          name: 'channel',
          type: 'relationship',
          relationTo: 'youtube-channels',
          required: true,
          admin: {
            width: '50%',
          },
        },
      ],
    },
    
    // === ZEITRAUM ===
    {
      name: 'productionPeriod',
      type: 'group',
      label: 'Produktionszeitraum',
      fields: [
        {
          type: 'row',
          fields: [
            {
              name: 'start',
              type: 'date',
              required: true,
              label: 'Start',
              admin: {
                width: '50%',
                date: {
                  pickerAppearance: 'dayOnly',
                  displayFormat: 'dd.MM.yyyy',
                },
              },
            },
            {
              name: 'end',
              type: 'date',
              required: true,
              label: 'Ende',
              admin: {
                width: '50%',
                date: {
                  pickerAppearance: 'dayOnly',
                  displayFormat: 'dd.MM.yyyy',
                },
              },
            },
          ],
        },
        {
          name: 'shootDays',
          type: 'array',
          label: 'Drehtage',
          labels: {
            singular: 'Drehtag',
            plural: 'Drehtage',
          },
          fields: [
            {
              type: 'row',
              fields: [
                {
                  name: 'date',
                  type: 'date',
                  required: true,
                  admin: {
                    width: '30%',
                    date: {
                      pickerAppearance: 'dayOnly',
                    },
                  },
                },
                {
                  name: 'location',
                  type: 'select',
                  options: [
                    { label: '🏠 Home Studio', value: 'home' },
                    { label: '🏢 Office', value: 'office' },
                    { label: '🚗 Unterwegs', value: 'mobile' },
                    { label: '📍 Extern', value: 'external' },
                  ],
                  admin: { width: '25%' },
                },
                {
                  name: 'duration',
                  type: 'select',
                  options: [
                    { label: '2 Stunden', value: '2h' },
                    { label: '4 Stunden (Halbtag)', value: '4h' },
                    { label: '8 Stunden (Ganztag)', value: '8h' },
                  ],
                  admin: { width: '25%' },
                },
                {
                  name: 'notes',
                  type: 'text',
                  admin: { 
                    width: '20%',
                    placeholder: 'Notizen',
                  },
                },
              ],
            },
          ],
        },
      ],
    },
    
    // === CONTENT TARGETS ===
    {
      name: 'targets',
      type: 'group',
      label: 'Content-Ziele',
      fields: [
        {
          type: 'row',
          fields: [
            {
              name: 'shortsTarget',
              type: 'number',
              label: 'Shorts (Ziel)',
              required: true,
              defaultValue: 7,
              admin: { width: '25%' },
            },
            {
              name: 'longformsTarget',
              type: 'number',
              label: 'Longforms (Ziel)',
              required: true,
              defaultValue: 3,
              admin: { width: '25%' },
            },
            {
              name: 'totalTarget',
              type: 'number',
              label: 'Gesamt (Ziel)',
              admin: {
                width: '25%',
                readOnly: true,
                description: 'Automatisch berechnet',
              },
              hooks: {
                beforeChange: [
                  ({ siblingData }) => {
                    return (siblingData.shortsTarget || 0) + (siblingData.longformsTarget || 0)
                  },
                ],
              },
            },
            {
              name: 'bufferDays',
              type: 'number',
              label: 'Puffer (Tage)',
              defaultValue: 3,
              admin: { 
                width: '25%',
                description: 'Tage zwischen Produktion und Publish',
              },
            },
          ],
        },
      ],
    },
    
    // === SERIEN-VERTEILUNG ===
    {
      name: 'seriesDistribution',
      type: 'array',
      label: 'Serien-Verteilung',
      admin: {
        description: 'Welche Serien in diesem Batch produziert werden',
      },
      fields: [
        {
          type: 'row',
          fields: [
            {
              name: 'series',
              type: 'text',
              required: true,
              admin: { 
                width: '30%',
                placeholder: 'z.B. GRFI',
              },
            },
            {
              name: 'shortsCount',
              type: 'number',
              label: 'Shorts',
              defaultValue: 0,
              admin: { width: '20%' },
            },
            {
              name: 'longformsCount',
              type: 'number',
              label: 'Longforms',
              defaultValue: 0,
              admin: { width: '20%' },
            },
            {
              name: 'priority',
              type: 'select',
              options: [
                { label: '🔴 Hoch', value: 'high' },
                { label: '🟡 Normal', value: 'normal' },
                { label: '🟢 Niedrig', value: 'low' },
              ],
              defaultValue: 'normal',
              admin: { width: '30%' },
            },
          ],
        },
      ],
    },
    
    // === STATUS & PROGRESS ===
    {
      name: 'status',
      type: 'select',
      required: true,
      defaultValue: 'planning',
      options: [
        { label: '📝 Planung', value: 'planning' },
        { label: '✍️ Scripts', value: 'scripting' },
        { label: '🎬 Produktion', value: 'production' },
        { label: '✂️ Schnitt', value: 'editing' },
        { label: '✅ Review', value: 'review' },
        { label: '📤 Upload-Ready', value: 'ready' },
        { label: '🎉 Veröffentlicht', value: 'published' },
      ],
      admin: {
        position: 'sidebar',
      },
    },
    
    // Virtuelle Felder für Progress (berechnet via Hook)
    {
      name: 'progress',
      type: 'group',
      label: 'Fortschritt',
      admin: {
        position: 'sidebar',
        readOnly: true,
      },
      fields: [
        {
          name: 'shortsCompleted',
          type: 'number',
          label: 'Shorts fertig',
          admin: { readOnly: true },
        },
        {
          name: 'longformsCompleted',
          type: 'number',
          label: 'Longforms fertig',
          admin: { readOnly: true },
        },
        {
          name: 'percentage',
          type: 'number',
          label: 'Gesamt %',
          admin: { readOnly: true },
        },
      ],
    },
    
    // === TEAM & RESOURCES ===
    {
      name: 'team',
      type: 'group',
      label: 'Team',
      fields: [
        {
          type: 'row',
          fields: [
            {
              name: 'producer',
              type: 'relationship',
              relationTo: 'users',
              label: 'Producer',
              admin: { width: '33%' },
            },
            {
              name: 'editor',
              type: 'relationship',
              relationTo: 'users',
              label: 'Editor',
              admin: { width: '33%' },
            },
            {
              name: 'reviewer',
              type: 'relationship',
              relationTo: 'users',
              label: 'Reviewer',
              admin: { width: '33%' },
            },
          ],
        },
      ],
    },
    
    // === NOTES ===
    {
      name: 'notes',
      type: 'textarea',
      label: 'Notizen',
      admin: {
        rows: 4,
      },
    },
  ],
  
  // === HOOKS ===
  hooks: {
    afterRead: [
      // Progress aus verknüpften Videos berechnen
      async ({ doc, req }) => {
        if (!doc?.id) return doc
        
        const videos = await req.payload.find({
          collection: 'youtube-content',
          where: {
            productionBatch: { equals: doc.id },
          },
          limit: 100,
        })
        
        const shorts = videos.docs.filter(v => v.format === 'short')
        const longforms = videos.docs.filter(v => v.format === 'longform')
        
        const completedStatuses = ['approved', 'upload_scheduled', 'published', 'tracked']
        
        const shortsCompleted = shorts.filter(v => 
          completedStatuses.includes(v.status)
        ).length
        
        const longformsCompleted = longforms.filter(v => 
          completedStatuses.includes(v.status)
        ).length
        
        const totalTarget = (doc.targets?.shortsTarget || 0) + (doc.targets?.longformsTarget || 0)
        const totalCompleted = shortsCompleted + longformsCompleted
        const percentage = totalTarget > 0 ? Math.round((totalCompleted / totalTarget) * 100) : 0
        
        return {
          ...doc,
          progress: {
            shortsCompleted,
            longformsCompleted,
            percentage,
          },
        }
      },
    ],
  },
}

3.2 Batch Overview Dashboard

// src/app/(payload)/admin/views/youtube/batch-overview/page.tsx

'use client'

import React, { useState, useEffect } from 'react'
import { format } from 'date-fns'
import { de } from 'date-fns/locale'

interface Batch {
  id: string
  name: string
  channel: { name: string; slug: string }
  status: string
  productionPeriod: {
    start: string
    end: string
  }
  targets: {
    shortsTarget: number
    longformsTarget: number
    totalTarget: number
  }
  progress: {
    shortsCompleted: number
    longformsCompleted: number
    percentage: number
  }
}

export default function BatchOverviewView() {
  const [batches, setBatches] = useState<Batch[]>([])
  const [selectedChannel, setSelectedChannel] = useState('all')
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetchBatches()
  }, [selectedChannel])

  const fetchBatches = async () => {
    setLoading(true)
    let query = 'depth=1&limit=50&sort=-productionPeriod.start'
    
    if (selectedChannel !== 'all') {
      query += `&where[channel][equals]=${selectedChannel}`
    }
    
    const res = await fetch(`/api/yt-batches?${query}`)
    const data = await res.json()
    setBatches(data.docs)
    setLoading(false)
  }

  const getStatusColor = (status: string) => {
    const colors: Record<string, string> = {
      planning: '#9CA3AF',
      scripting: '#60A5FA',
      production: '#FBBF24',
      editing: '#F97316',
      review: '#A78BFA',
      ready: '#34D399',
      published: '#10B981',
    }
    return colors[status] || '#9CA3AF'
  }

  const getStatusLabel = (status: string) => {
    const labels: Record<string, string> = {
      planning: '📝 Planung',
      scripting: '✍️ Scripts',
      production: '🎬 Produktion',
      editing: '✂️ Schnitt',
      review: '✅ Review',
      ready: '📤 Ready',
      published: '🎉 Published',
    }
    return labels[status] || status
  }

  return (
    <div className="batch-overview">
      <header>
        <h1>📦 Batch Overview</h1>
        
        <div className="controls">
          <select 
            value={selectedChannel}
            onChange={e => setSelectedChannel(e.target.value)}
          >
            <option value="all">Alle Kanäle</option>
            <option value="blogwoman">BlogWoman</option>
            <option value="corporate-de">Corporate DE</option>
            <option value="business-en">Business EN</option>
          </select>
          
          <a href="/admin/collections/yt-batches/create" className="btn-primary">
            + Neuer Batch
          </a>
        </div>
      </header>

      {loading ? (
        <div className="loading">Lade Batches...</div>
      ) : (
        <div className="batch-grid">
          {batches.map(batch => (
            <a 
              key={batch.id} 
              href={`/admin/collections/yt-batches/${batch.id}`}
              className="batch-card"
            >
              <div className="batch-header">
                <h3>{batch.name}</h3>
                <span 
                  className="status-badge"
                  style={{ backgroundColor: getStatusColor(batch.status) }}
                >
                  {getStatusLabel(batch.status)}
                </span>
              </div>
              
              <div className="batch-channel">
                {batch.channel?.name}
              </div>
              
              <div className="batch-dates">
                📅 {format(new Date(batch.productionPeriod.start), 'dd.MM.', { locale: de })} 
                - {format(new Date(batch.productionPeriod.end), 'dd.MM.yyyy', { locale: de })}
              </div>
              
              <div className="batch-progress">
                <div className="progress-bar">
                  <div 
                    className="progress-fill"
                    style={{ width: `${batch.progress?.percentage || 0}%` }}
                  />
                </div>
                <span className="progress-text">
                  {batch.progress?.percentage || 0}%
                </span>
              </div>
              
              <div className="batch-counts">
                <div className="count">
                  <span className="count-value">
                    {batch.progress?.shortsCompleted || 0}/{batch.targets?.shortsTarget || 0}
                  </span>
                  <span className="count-label">Shorts</span>
                </div>
                <div className="count">
                  <span className="count-value">
                    {batch.progress?.longformsCompleted || 0}/{batch.targets?.longformsTarget || 0}
                  </span>
                  <span className="count-label">Longforms</span>
                </div>
              </div>
            </a>
          ))}
        </div>
      )}
    </div>
  )
}

4. Monthly Goals Collection

// src/collections/YtMonthlyGoals.ts

import { CollectionConfig } from 'payload/types'

export const YtMonthlyGoals: CollectionConfig = {
  slug: 'yt-monthly-goals',
  labels: {
    singular: 'Monthly Goal',
    plural: 'Monthly Goals',
  },
  admin: {
    group: 'YouTube',
    useAsTitle: 'displayTitle',
    defaultColumns: ['channel', 'month', 'status', 'updatedAt'],
  },
  fields: [
    {
      name: 'channel',
      type: 'relationship',
      relationTo: 'youtube-channels',
      required: true,
    },
    {
      name: 'month',
      type: 'date',
      required: true,
      admin: {
        date: {
          pickerAppearance: 'monthOnly',
          displayFormat: 'MMMM yyyy',
        },
      },
    },
    {
      name: 'displayTitle',
      type: 'text',
      admin: {
        hidden: true,
      },
      hooks: {
        beforeChange: [
          async ({ siblingData, req }) => {
            if (siblingData.channel && siblingData.month) {
              const channel = await req.payload.findByID({
                collection: 'youtube-channels',
                id: siblingData.channel,
              })
              const monthStr = new Date(siblingData.month).toLocaleDateString('de-DE', {
                month: 'long',
                year: 'numeric',
              })
              return `${channel?.name} - ${monthStr}`
            }
            return 'Neues Monatsziel'
          },
        ],
      },
    },
    
    // === CONTENT GOALS ===
    {
      name: 'contentGoals',
      type: 'group',
      label: 'Content-Ziele',
      fields: [
        {
          type: 'row',
          fields: [
            {
              name: 'longformsTarget',
              type: 'number',
              label: 'Longforms',
              defaultValue: 12,
              admin: { width: '25%' },
            },
            {
              name: 'longformsCurrent',
              type: 'number',
              label: 'Aktuell',
              defaultValue: 0,
              admin: { width: '25%' },
            },
            {
              name: 'shortsTarget',
              type: 'number',
              label: 'Shorts',
              defaultValue: 28,
              admin: { width: '25%' },
            },
            {
              name: 'shortsCurrent',
              type: 'number',
              label: 'Aktuell',
              defaultValue: 0,
              admin: { width: '25%' },
            },
          ],
        },
      ],
    },
    
    // === AUDIENCE GOALS ===
    {
      name: 'audienceGoals',
      type: 'group',
      label: 'Audience-Ziele',
      fields: [
        {
          type: 'row',
          fields: [
            {
              name: 'subscribersTarget',
              type: 'number',
              label: 'Neue Abos (Ziel)',
              admin: { width: '25%' },
            },
            {
              name: 'subscribersCurrent',
              type: 'number',
              label: 'Aktuell',
              admin: { width: '25%' },
            },
            {
              name: 'viewsTarget',
              type: 'number',
              label: 'Views (Ziel)',
              admin: { width: '25%' },
            },
            {
              name: 'viewsCurrent',
              type: 'number',
              label: 'Aktuell',
              admin: { width: '25%' },
            },
          ],
        },
      ],
    },
    
    // === ENGAGEMENT GOALS ===
    {
      name: 'engagementGoals',
      type: 'group',
      label: 'Engagement-Ziele',
      fields: [
        {
          type: 'row',
          fields: [
            {
              name: 'avgCtrTarget',
              type: 'text',
              label: 'Ø CTR (Ziel)',
              admin: { 
                width: '25%',
                placeholder: 'z.B. ">4%"',
              },
            },
            {
              name: 'avgCtrCurrent',
              type: 'text',
              label: 'Aktuell',
              admin: { width: '25%' },
            },
            {
              name: 'avgRetentionTarget',
              type: 'text',
              label: 'Ø Retention (Ziel)',
              admin: { 
                width: '25%',
                placeholder: 'z.B. ">50%"',
              },
            },
            {
              name: 'avgRetentionCurrent',
              type: 'text',
              label: 'Aktuell',
              admin: { width: '25%' },
            },
          ],
        },
      ],
    },
    
    // === BUSINESS GOALS ===
    {
      name: 'businessGoals',
      type: 'group',
      label: 'Business-Ziele',
      fields: [
        {
          type: 'row',
          fields: [
            {
              name: 'newsletterSignupsTarget',
              type: 'number',
              label: 'Newsletter-Anmeldungen',
              admin: { width: '25%' },
            },
            {
              name: 'newsletterSignupsCurrent',
              type: 'number',
              label: 'Aktuell',
              admin: { width: '25%' },
            },
            {
              name: 'affiliateClicksTarget',
              type: 'number',
              label: 'Affiliate-Klicks',
              admin: { width: '25%' },
            },
            {
              name: 'affiliateClicksCurrent',
              type: 'number',
              label: 'Aktuell',
              admin: { width: '25%' },
            },
          ],
        },
      ],
    },
    
    // === CUSTOM GOALS ===
    {
      name: 'customGoals',
      type: 'array',
      label: 'Weitere Ziele',
      fields: [
        {
          type: 'row',
          fields: [
            {
              name: 'metric',
              type: 'text',
              required: true,
              admin: { 
                width: '40%',
                placeholder: 'z.B. "SPARK Videos"',
              },
            },
            {
              name: 'target',
              type: 'text',
              required: true,
              admin: { 
                width: '20%',
                placeholder: 'Ziel',
              },
            },
            {
              name: 'current',
              type: 'text',
              admin: { 
                width: '20%',
                placeholder: 'Aktuell',
              },
            },
            {
              name: 'status',
              type: 'select',
              options: [
                { label: '🟢 On Track', value: 'on_track' },
                { label: '🟡 At Risk', value: 'at_risk' },
                { label: '✅ Achieved', value: 'achieved' },
                { label: '❌ Missed', value: 'missed' },
              ],
              admin: { width: '20%' },
            },
          ],
        },
      ],
    },
    
    {
      name: 'notes',
      type: 'textarea',
      label: 'Notizen / Learnings',
    },
  ],
}

5. Erweiterte YouTube Content Collection

// Erweiterungen für YouTubeContent.ts

// Neue Felder hinzufügen:

// === PRODUKTION ===
{
  name: 'productionBatch',
  type: 'relationship',
  relationTo: 'yt-batches',
  label: 'Produktions-Batch',
  admin: {
    position: 'sidebar',
  },
},
{
  name: 'productionWeek',
  type: 'number',
  label: 'Produktionswoche',
  min: 1,
  max: 52,
  admin: {
    position: 'sidebar',
  },
},
{
  name: 'calendarWeek',
  type: 'number',
  label: 'Kalenderwoche',
  min: 1,
  max: 52,
  admin: {
    position: 'sidebar',
  },
},
{
  name: 'productionDate',
  type: 'date',
  label: 'Produktionsdatum',
  admin: {
    date: {
      pickerAppearance: 'dayOnly',
      displayFormat: 'dd.MM.yyyy',
    },
  },
},
{
  name: 'targetDuration',
  type: 'text',
  label: 'Ziel-Dauer',
  admin: {
    placeholder: 'z.B. "8-12 Min" oder "45-58s"',
  },
},
{
  name: 'bRollNotes',
  type: 'textarea',
  label: 'B-Roll / Setting Notizen',
  admin: {
    rows: 2,
  },
},

// === POSTING ===
{
  name: 'publishTime',
  type: 'text',
  label: 'Posting-Uhrzeit',
  admin: {
    placeholder: 'z.B. "07:00" oder "17:00"',
  },
},
{
  name: 'thumbnailText',
  type: 'text',
  label: 'Thumbnail-Text',
  admin: {
    placeholder: 'z.B. "BOARD READY | 7 MIN"',
  },
},
{
  name: 'ctaType',
  type: 'select',
  label: 'CTA-Typ',
  options: [
    { label: 'Link in Bio', value: 'link_in_bio' },
    { label: 'Newsletter', value: 'newsletter' },
    { label: 'Longform verlinken', value: 'longform_link' },
    { label: 'Benutzerdefiniert', value: 'custom' },
  ],
},
{
  name: 'ctaDetail',
  type: 'text',
  label: 'CTA-Detail',
  admin: {
    placeholder: 'z.B. "GRFI-Checkliste" oder "CPW-Rechner"',
    condition: (data) => data?.ctaType,
  },
},

// === SCRIPT (Block-basiert) ===
{
  name: 'script',
  type: 'blocks',
  label: 'Script',
  labels: {
    singular: 'Section',
    plural: 'Script Sections',
  },
  blocks: [ScriptSectionBlock],
},

// === UPLOAD CHECKLIST ===
{
  name: 'uploadChecklist',
  type: 'array',
  label: 'Upload-Checkliste',
  admin: {
    condition: (data) => ['approved', 'upload_scheduled', 'published'].includes(data?.status),
  },
  fields: [
    {
      type: 'row',
      fields: [
        {
          name: 'step',
          type: 'text',
          required: true,
          admin: { width: '40%' },
        },
        {
          name: 'completed',
          type: 'checkbox',
          label: '✓',
          admin: { width: '10%' },
        },
        {
          name: 'completedAt',
          type: 'date',
          admin: { 
            width: '25%',
            readOnly: true,
          },
        },
        {
          name: 'completedBy',
          type: 'relationship',
          relationTo: 'users',
          admin: { 
            width: '25%',
            readOnly: true,
          },
        },
      ],
    },
  ],
},

// === DISCLAIMERS ===
{
  name: 'disclaimers',
  type: 'array',
  label: 'Disclaimers',
  fields: [
    {
      type: 'row',
      fields: [
        {
          name: 'type',
          type: 'select',
          required: true,
          options: [
            { label: '⚠️ Medizinisch', value: 'medical' },
            { label: '⚖️ Rechtlich', value: 'legal' },
            { label: '🔗 Affiliate', value: 'affiliate' },
            { label: '🤝 Sponsored', value: 'sponsored' },
          ],
          admin: { width: '25%' },
        },
        {
          name: 'text',
          type: 'text',
          admin: { width: '50%' },
        },
        {
          name: 'placement',
          type: 'select',
          options: [
            { label: 'Gesprochen', value: 'spoken' },
            { label: 'Text-Overlay', value: 'overlay' },
            { label: 'Beschreibung', value: 'description' },
            { label: 'Überall', value: 'all' },
          ],
          admin: { width: '25%' },
        },
      ],
    },
  ],
},

6. Zusammenfassung der neuen Collections

Collection Slug Zweck
YouTube Channels youtube-channels Kanäle (existiert)
YouTube Content youtube-content Videos + Script (erweitert)
YT Tasks yt-tasks Aufgaben (existiert)
YT Notifications yt-notifications Benachrichtigungen (existiert)
YT Batches yt-batches NEU: Produktions-Batches
YT Monthly Goals yt-monthly-goals NEU: Monatsziele
YT Script Templates yt-script-templates NEU: Skript-Vorlagen
YT Checklist Templates yt-checklist-templates NEU: Upload-Checklisten

7. Custom Admin Views

View Pfad Zweck
Content Calendar /admin/views/youtube/content-calendar Produktions- & Publishing-Kalender
Batch Overview /admin/views/youtube/batch-overview Dashboard aller Batches
Monthly Goals /admin/views/youtube/monthly-goals KPI-Dashboard

8. Migrations

// src/migrations/20260112_200000_youtube_ops_v2.ts

import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/db-postgres'

export async function up({ payload }: MigrateUpArgs): Promise<void> {
  // 1. Neue Enums
  await payload.db.drizzle.execute(`
    CREATE TYPE enum_yt_batches_status AS ENUM (
      'planning', 'scripting', 'production', 'editing', 'review', 'ready', 'published'
    );
    
    CREATE TYPE enum_script_section_type AS ENUM (
      'hook', 'intro_ident', 'context', 'content_part', 'summary', 'cta', 'outro', 'disclaimer'
    );
    
    CREATE TYPE enum_cta_type AS ENUM (
      'link_in_bio', 'newsletter', 'longform_link', 'custom'
    );
    
    CREATE TYPE enum_disclaimer_type AS ENUM (
      'medical', 'legal', 'affiliate', 'sponsored'
    );
    
    CREATE TYPE enum_disclaimer_placement AS ENUM (
      'spoken', 'overlay', 'description', 'all'
    );
  `);

  // 2. Neue Tabellen
  // ... (vollständige Migration)
}

export async function down({ payload }: MigrateDownArgs): Promise<void> {
  // Rollback
}

Nächste Schritte

  1. Claude Code Integration Prompt erstellen mit allen Collections, Blocks und Views
  2. Seed-Daten mit BlogWoman Januar Content als Beispiel
  3. CSS Styles für Calendar und Dashboard Views
  4. Lexical Custom Block für Script Section Editor

Soll ich jetzt den vollständigen Claude Code Integration Prompt erstellen?