cms.c2sgmbh/prompts/youtube2-corrected.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

1806 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# YouTube Operations Hub v2 Claude Code Integration Prompt (Korrigiert)
## Projektkontext
Du arbeitest am **YouTube Operations Hub v2** für Complex Care Solutions (CCS). Dies ist eine Erweiterung des bestehenden YouTube-Moduls in Payload CMS 3.x.
**Server:** sv-payload (10.10.181.100)
**Projekt-Pfad:** `/home/payload/payload-cms`
**Datenbank:** PostgreSQL 17 auf sv-postgres (10.10.181.101)
**Tech Stack:** Payload CMS 3.69.0, Next.js 15.5.9, TypeScript, Drizzle ORM
**Package Manager:** pnpm
---
## Aufgabe
Erweitere das bestehende YouTube Operations Hub um:
1. **Script Section Block** Block für strukturierte Video-Skripte
2. **Batch-Planung** Neue Collection für detaillierte Produktions-Batches
3. **Monthly Goals** KPI-Tracking pro Kanal und Monat
4. **Script Templates** Wiederverwendbare Skript-Vorlagen
5. **Checklist Templates** Wiederverwendbare Upload-Checklisten
6. **Erweiterte YouTube Content Felder** Produktions- und Posting-Informationen
> **Hinweis:** Custom Admin Views (Kalender) sind derzeit nicht möglich wegen eines path-to-regexp Bugs in Payload 3.x. Diese werden in einem späteren Update implementiert (siehe `prompts/youtube3-admin-views.md`).
---
## Teil 1: Script Section Block
### Datei: `src/blocks/ScriptSectionBlock.ts`
```typescript
import type { Block } from 'payload'
export const ScriptSectionBlock: Block = {
slug: 'script-section',
labels: {
singular: { de: 'Script Section', en: 'Script Section' },
plural: { de: 'Script Sections', en: 'Script Sections' },
},
fields: [
{
type: 'row',
fields: [
{
name: 'sectionType',
type: 'select',
required: true,
label: { de: 'Section-Typ', en: 'Section Type' },
options: [
{ label: 'Hook', value: 'hook' },
{ label: 'Intro/Ident', value: 'intro_ident' },
{ label: 'Context', value: 'context' },
{ label: 'Content Part', value: 'content_part' },
{ label: 'Summary', value: 'summary' },
{ label: 'CTA', value: 'cta' },
{ label: 'Outro', value: 'outro' },
{ label: 'Disclaimer', value: 'disclaimer' },
],
admin: {
width: '50%',
},
},
{
name: 'duration',
type: 'text',
label: { de: 'Dauer', en: 'Duration' },
admin: {
width: '25%',
placeholder: 'z.B. "2-3 Min"',
},
},
{
name: 'sectionTitle',
type: 'text',
label: { de: 'Section-Titel', en: 'Section Title' },
admin: {
width: '25%',
placeholder: 'z.B. "TEIL 1: OUTFIT"',
condition: (data, siblingData) => siblingData?.sectionType === 'content_part',
},
},
],
},
{
name: 'spokenText',
type: 'richText',
label: { de: 'Gesprochener Text', en: 'Spoken Text' },
required: true,
localized: true,
},
{
name: 'bRollInstructions',
type: 'array',
label: { de: 'B-Roll Anweisungen', en: 'B-Roll Instructions' },
labels: {
singular: { de: 'B-Roll', en: 'B-Roll' },
plural: { de: 'B-Roll Anweisungen', en: 'B-Roll Instructions' },
},
fields: [
{
type: 'row',
fields: [
{
name: 'instruction',
type: 'text',
required: true,
label: { de: 'Anweisung', en: 'Instruction' },
admin: {
width: '70%',
placeholder: 'z.B. "Outfit zeigen", "Kleiderschrank-Aufnahme"',
},
},
{
name: 'timestamp',
type: 'text',
label: { de: 'Zeitpunkt', en: 'Timestamp' },
admin: {
width: '30%',
placeholder: 'Optional: "bei 0:45"',
},
},
],
},
],
admin: {
initCollapsed: true,
},
},
{
name: 'textOverlays',
type: 'array',
label: { de: 'Text-Overlays', en: 'Text Overlays' },
labels: {
singular: { de: 'Overlay', en: 'Overlay' },
plural: { de: 'Text-Overlays', en: 'Text Overlays' },
},
fields: [
{
type: 'row',
fields: [
{
name: 'text',
type: 'text',
required: true,
label: { de: 'Text', en: 'Text' },
admin: {
width: '70%',
placeholder: 'z.B. "3 Kombinationen = 0 Entscheidungen"',
},
},
{
name: 'style',
type: 'select',
label: { de: 'Stil', en: 'Style' },
defaultValue: 'standard',
options: [
{ label: 'Standard', value: 'standard' },
{ label: 'Highlight', value: 'highlight' },
{ label: 'Quote', value: 'quote' },
{ label: 'Statistik', value: 'statistic' },
{ label: 'Liste', value: 'list' },
],
admin: { width: '30%' },
},
],
},
],
admin: {
initCollapsed: true,
},
},
{
name: 'visualNotes',
type: 'textarea',
label: { de: 'Visuelle Notizen', en: 'Visual Notes' },
admin: {
rows: 2,
placeholder: 'Zusätzliche Anweisungen für Schnitt/Grafik',
},
},
],
}
```
---
## Teil 2: Neue Collections
### Datei: `src/collections/YtBatches.ts`
```typescript
import type { CollectionConfig } from 'payload'
import { isYouTubeManager, isYouTubeCreatorOrAbove, hasYouTubeAccess } from '../lib/youtubeAccess'
export const YtBatches: CollectionConfig = {
slug: 'yt-batches',
labels: {
singular: { de: 'Production Batch', en: 'Production Batch' },
plural: { de: 'Production Batches', en: 'Production Batches' },
},
admin: {
group: 'YouTube',
useAsTitle: 'name',
defaultColumns: ['name', 'channel', 'status', 'productionPeriod.start', 'updatedAt'],
listSearchableFields: ['name'],
},
access: {
read: hasYouTubeAccess,
create: isYouTubeManager,
update: isYouTubeCreatorOrAbove,
delete: isYouTubeManager,
},
fields: [
// === BASIC INFO ===
{
type: 'row',
fields: [
{
name: 'name',
type: 'text',
required: true,
label: { de: 'Batch-Name', en: 'Batch Name' },
admin: {
placeholder: 'z.B. "Januar Woche 1" oder "Batch 1"',
width: '50%',
},
},
{
name: 'channel',
type: 'relationship',
relationTo: 'youtube-channels',
required: true,
label: { de: 'Kanal', en: 'Channel' },
admin: {
width: '50%',
},
},
],
},
// === PRODUKTIONSZEITRAUM ===
{
name: 'productionPeriod',
type: 'group',
label: { de: 'Produktionszeitraum', en: 'Production Period' },
fields: [
{
type: 'row',
fields: [
{
name: 'start',
type: 'date',
required: true,
label: { de: 'Start', en: 'Start' },
admin: {
width: '50%',
date: {
pickerAppearance: 'dayOnly',
displayFormat: 'dd.MM.yyyy',
},
},
},
{
name: 'end',
type: 'date',
required: true,
label: { de: 'Ende', en: 'End' },
admin: {
width: '50%',
date: {
pickerAppearance: 'dayOnly',
displayFormat: 'dd.MM.yyyy',
},
},
},
],
},
{
name: 'shootDays',
type: 'array',
label: { de: 'Drehtage', en: 'Shoot Days' },
labels: {
singular: { de: 'Drehtag', en: 'Shoot Day' },
plural: { de: 'Drehtage', en: 'Shoot Days' },
},
fields: [
{
type: 'row',
fields: [
{
name: 'date',
type: 'date',
required: true,
label: { de: 'Datum', en: 'Date' },
admin: {
width: '25%',
date: { pickerAppearance: 'dayOnly' },
},
},
{
name: 'location',
type: 'select',
label: { de: 'Location', en: 'Location' },
options: [
{ label: 'Home Studio', value: 'home' },
{ label: 'Office', value: 'office' },
{ label: { de: 'Unterwegs', en: 'On the Go' }, value: 'mobile' },
{ label: 'Extern', value: 'external' },
],
admin: { width: '25%' },
},
{
name: 'duration',
type: 'select',
label: { de: 'Dauer', en: 'Duration' },
options: [
{ label: '2h', value: '2h' },
{ label: '4h (Halbtag)', value: '4h' },
{ label: '8h (Ganztag)', value: '8h' },
],
admin: { width: '25%' },
},
{
name: 'notes',
type: 'text',
label: { de: 'Notizen', en: 'Notes' },
admin: {
width: '25%',
placeholder: 'Notizen',
},
},
],
},
],
admin: {
initCollapsed: true,
},
},
],
},
// === CONTENT TARGETS ===
{
name: 'targets',
type: 'group',
label: { de: 'Content-Ziele', en: 'Content Targets' },
fields: [
{
type: 'row',
fields: [
{
name: 'shortsTarget',
type: 'number',
label: { de: 'Shorts (Ziel)', en: 'Shorts (Target)' },
required: true,
defaultValue: 7,
min: 0,
admin: { width: '25%' },
},
{
name: 'longformsTarget',
type: 'number',
label: { de: 'Longforms (Ziel)', en: 'Longforms (Target)' },
required: true,
defaultValue: 3,
min: 0,
admin: { width: '25%' },
},
{
name: 'totalTarget',
type: 'number',
label: { de: 'Gesamt', en: 'Total' },
admin: {
width: '25%',
readOnly: true,
},
},
{
name: 'bufferDays',
type: 'number',
label: { de: 'Puffer (Tage)', en: 'Buffer (Days)' },
defaultValue: 3,
min: 0,
admin: {
width: '25%',
description: { de: 'Tage zwischen Produktion und Publish', en: 'Days between production and publish' },
},
},
],
},
],
},
// === SERIEN-VERTEILUNG ===
{
name: 'seriesDistribution',
type: 'array',
label: { de: 'Serien-Verteilung', en: 'Series Distribution' },
admin: {
description: { de: 'Welche Serien in diesem Batch produziert werden', en: 'Which series are produced in this batch' },
},
fields: [
{
type: 'row',
fields: [
{
name: 'series',
type: 'text',
required: true,
label: { de: 'Serie', en: 'Series' },
admin: {
width: '30%',
placeholder: 'z.B. GRFI',
},
},
{
name: 'shortsCount',
type: 'number',
label: 'Shorts',
defaultValue: 0,
min: 0,
admin: { width: '20%' },
},
{
name: 'longformsCount',
type: 'number',
label: 'Longforms',
defaultValue: 0,
min: 0,
admin: { width: '20%' },
},
{
name: 'priority',
type: 'select',
label: { de: 'Priorität', en: 'Priority' },
options: [
{ label: { de: 'Hoch', en: 'High' }, value: 'high' },
{ label: 'Normal', value: 'normal' },
{ label: { de: 'Niedrig', en: 'Low' }, value: 'low' },
],
defaultValue: 'normal',
admin: { width: '30%' },
},
],
},
],
},
// === STATUS ===
{
name: 'status',
type: 'select',
required: true,
defaultValue: 'planning',
label: { de: 'Status', en: 'Status' },
options: [
{ label: { de: 'Planung', en: 'Planning' }, value: 'planning' },
{ label: 'Scripts', value: 'scripting' },
{ label: { de: 'Produktion', en: 'Production' }, value: 'production' },
{ label: { de: 'Schnitt', en: 'Editing' }, value: 'editing' },
{ label: 'Review', value: 'review' },
{ label: 'Upload-Ready', value: 'ready' },
{ label: { de: 'Veröffentlicht', en: 'Published' }, value: 'published' },
],
admin: {
position: 'sidebar',
},
},
// === PROGRESS (berechnet) ===
{
name: 'progress',
type: 'group',
label: { de: 'Fortschritt', en: 'Progress' },
admin: {
position: 'sidebar',
},
fields: [
{
name: 'shortsCompleted',
type: 'number',
label: { de: 'Shorts fertig', en: 'Shorts Completed' },
defaultValue: 0,
admin: { readOnly: true },
},
{
name: 'longformsCompleted',
type: 'number',
label: { de: 'Longforms fertig', en: 'Longforms Completed' },
defaultValue: 0,
admin: { readOnly: true },
},
{
name: 'percentage',
type: 'number',
label: { de: 'Gesamt %', en: 'Total %' },
defaultValue: 0,
admin: { readOnly: true },
},
],
},
// === TEAM ===
{
name: 'team',
type: 'group',
label: { de: 'Team', en: 'Team' },
fields: [
{
type: 'row',
fields: [
{
name: 'producer',
type: 'relationship',
relationTo: 'users',
label: 'Producer',
admin: { width: '33%' },
},
{
name: 'editor',
type: 'relationship',
relationTo: 'users',
label: 'Editor',
admin: { width: '33%' },
},
{
name: 'reviewer',
type: 'relationship',
relationTo: 'users',
label: 'Reviewer',
admin: { width: '33%' },
},
],
},
],
},
// === NOTIZEN ===
{
name: 'notes',
type: 'textarea',
label: { de: 'Notizen', en: 'Notes' },
localized: true,
admin: {
rows: 4,
},
},
],
// === HOOKS ===
hooks: {
beforeChange: [
// Total Target berechnen
({ data }) => {
if (data?.targets) {
data.targets.totalTarget = (data.targets.shortsTarget || 0) + (data.targets.longformsTarget || 0)
}
return data
},
],
afterRead: [
// Progress aus verknüpften Videos berechnen
async ({ doc, req }) => {
if (!doc?.id) return doc
try {
const videos = await req.payload.find({
collection: 'youtube-content',
where: {
productionBatch: { equals: doc.id },
},
limit: 100,
depth: 0,
})
const shorts = videos.docs.filter((v: { format?: string }) => v.format === 'short')
const longforms = videos.docs.filter((v: { format?: string }) => v.format === 'longform')
const completedStatuses = ['approved', 'upload_scheduled', 'published', 'tracked']
const shortsCompleted = shorts.filter((v: { status?: string }) =>
completedStatuses.includes(v.status || '')
).length
const longformsCompleted = longforms.filter((v: { status?: string }) =>
completedStatuses.includes(v.status || '')
).length
const totalTarget = (doc.targets?.shortsTarget || 0) + (doc.targets?.longformsTarget || 0)
const totalCompleted = shortsCompleted + longformsCompleted
const percentage = totalTarget > 0 ? Math.round((totalCompleted / totalTarget) * 100) : 0
return {
...doc,
progress: {
shortsCompleted,
longformsCompleted,
percentage,
},
}
} catch (error) {
console.error('Error calculating batch progress:', error)
return doc
}
},
],
},
timestamps: true,
}
```
### Datei: `src/collections/YtMonthlyGoals.ts`
```typescript
import type { CollectionConfig } from 'payload'
import { isYouTubeManager, hasYouTubeAccess } from '../lib/youtubeAccess'
export const YtMonthlyGoals: CollectionConfig = {
slug: 'yt-monthly-goals',
labels: {
singular: { de: 'Monatsziel', en: 'Monthly Goal' },
plural: { de: 'Monatsziele', en: 'Monthly Goals' },
},
admin: {
group: 'YouTube',
useAsTitle: 'displayTitle',
defaultColumns: ['displayTitle', 'channel', 'month', 'updatedAt'],
},
access: {
read: hasYouTubeAccess,
create: isYouTubeManager,
update: isYouTubeManager,
delete: isYouTubeManager,
},
fields: [
{
type: 'row',
fields: [
{
name: 'channel',
type: 'relationship',
relationTo: 'youtube-channels',
required: true,
label: { de: 'Kanal', en: 'Channel' },
admin: { width: '50%' },
},
{
name: 'month',
type: 'date',
required: true,
label: { de: 'Monat', en: 'Month' },
admin: {
width: '50%',
date: {
pickerAppearance: 'monthOnly',
displayFormat: 'MMMM yyyy',
},
},
},
],
},
{
name: 'displayTitle',
type: 'text',
admin: { hidden: true },
hooks: {
beforeChange: [
async ({ siblingData, req }) => {
if (siblingData.channel && siblingData.month) {
try {
const channelId = typeof siblingData.channel === 'object'
? siblingData.channel.id
: siblingData.channel
const channel = await req.payload.findByID({
collection: 'youtube-channels',
id: channelId,
})
const monthStr = new Date(siblingData.month).toLocaleDateString('de-DE', {
month: 'long',
year: 'numeric',
})
return `${channel?.name || 'Kanal'} - ${monthStr}`
} catch {
return 'Neues Monatsziel'
}
}
return 'Neues Monatsziel'
},
],
},
},
// === CONTENT GOALS ===
{
name: 'contentGoals',
type: 'group',
label: { de: 'Content-Ziele', en: 'Content Goals' },
fields: [
{
type: 'row',
fields: [
{
name: 'longformsTarget',
type: 'number',
label: { de: 'Longforms (Ziel)', en: 'Longforms (Target)' },
defaultValue: 12,
admin: { width: '25%' },
},
{
name: 'longformsCurrent',
type: 'number',
label: { de: 'Aktuell', en: 'Current' },
defaultValue: 0,
admin: { width: '25%' },
},
{
name: 'shortsTarget',
type: 'number',
label: { de: 'Shorts (Ziel)', en: 'Shorts (Target)' },
defaultValue: 28,
admin: { width: '25%' },
},
{
name: 'shortsCurrent',
type: 'number',
label: { de: 'Aktuell', en: 'Current' },
defaultValue: 0,
admin: { width: '25%' },
},
],
},
],
},
// === AUDIENCE GOALS ===
{
name: 'audienceGoals',
type: 'group',
label: { de: 'Audience-Ziele', en: 'Audience Goals' },
fields: [
{
type: 'row',
fields: [
{
name: 'subscribersTarget',
type: 'number',
label: { de: 'Neue Abos (Ziel)', en: 'New Subs (Target)' },
admin: { width: '25%' },
},
{
name: 'subscribersCurrent',
type: 'number',
label: { de: 'Aktuell', en: 'Current' },
admin: { width: '25%' },
},
{
name: 'viewsTarget',
type: 'number',
label: { de: 'Views (Ziel)', en: 'Views (Target)' },
admin: { width: '25%' },
},
{
name: 'viewsCurrent',
type: 'number',
label: { de: 'Aktuell', en: 'Current' },
admin: { width: '25%' },
},
],
},
],
},
// === ENGAGEMENT GOALS ===
{
name: 'engagementGoals',
type: 'group',
label: { de: 'Engagement-Ziele', en: 'Engagement Goals' },
fields: [
{
type: 'row',
fields: [
{
name: 'avgCtrTarget',
type: 'text',
label: { de: 'Ø CTR (Ziel)', en: 'Avg CTR (Target)' },
admin: {
width: '25%',
placeholder: 'z.B. ">4%"',
},
},
{
name: 'avgCtrCurrent',
type: 'text',
label: { de: 'Aktuell', en: 'Current' },
admin: { width: '25%' },
},
{
name: 'avgRetentionTarget',
type: 'text',
label: { de: 'Ø Retention (Ziel)', en: 'Avg Retention (Target)' },
admin: {
width: '25%',
placeholder: 'z.B. ">50%"',
},
},
{
name: 'avgRetentionCurrent',
type: 'text',
label: { de: 'Aktuell', en: 'Current' },
admin: { width: '25%' },
},
],
},
],
},
// === BUSINESS GOALS ===
{
name: 'businessGoals',
type: 'group',
label: { de: 'Business-Ziele', en: 'Business Goals' },
fields: [
{
type: 'row',
fields: [
{
name: 'newsletterSignupsTarget',
type: 'number',
label: { de: 'Newsletter-Anmeldungen (Ziel)', en: 'Newsletter Signups (Target)' },
admin: { width: '25%' },
},
{
name: 'newsletterSignupsCurrent',
type: 'number',
label: { de: 'Aktuell', en: 'Current' },
admin: { width: '25%' },
},
{
name: 'affiliateRevenueTarget',
type: 'number',
label: { de: 'Affiliate-Umsatz (Ziel)', en: 'Affiliate Revenue (Target)' },
admin: { width: '25%' },
},
{
name: 'affiliateRevenueCurrent',
type: 'number',
label: { de: 'Aktuell', en: 'Current' },
admin: { width: '25%' },
},
],
},
],
},
// === CUSTOM GOALS ===
{
name: 'customGoals',
type: 'array',
label: { de: 'Weitere Ziele', en: 'Custom Goals' },
fields: [
{
type: 'row',
fields: [
{
name: 'metric',
type: 'text',
required: true,
label: { de: 'Metrik', en: 'Metric' },
admin: {
width: '40%',
placeholder: 'z.B. "SPARK Videos"',
},
},
{
name: 'target',
type: 'text',
required: true,
label: { de: 'Ziel', en: 'Target' },
admin: {
width: '20%',
placeholder: 'Ziel',
},
},
{
name: 'current',
type: 'text',
label: { de: 'Aktuell', en: 'Current' },
admin: {
width: '20%',
placeholder: 'Aktuell',
},
},
{
name: 'status',
type: 'select',
label: 'Status',
options: [
{ label: 'On Track', value: 'on_track' },
{ label: 'At Risk', value: 'at_risk' },
{ label: 'Achieved', value: 'achieved' },
{ label: 'Missed', value: 'missed' },
],
admin: { width: '20%' },
},
],
},
],
},
{
name: 'notes',
type: 'textarea',
label: { de: 'Notizen / Learnings', en: 'Notes / Learnings' },
localized: true,
},
],
timestamps: true,
}
```
### Datei: `src/collections/YtScriptTemplates.ts`
```typescript
import type { CollectionConfig } from 'payload'
import { ScriptSectionBlock } from '../blocks/ScriptSectionBlock'
import { isYouTubeManager, hasYouTubeAccess } from '../lib/youtubeAccess'
export const YtScriptTemplates: CollectionConfig = {
slug: 'yt-script-templates',
labels: {
singular: { de: 'Script Template', en: 'Script Template' },
plural: { de: 'Script Templates', en: 'Script Templates' },
},
admin: {
group: 'YouTube',
useAsTitle: 'name',
defaultColumns: ['name', 'series', 'format', 'channel', 'updatedAt'],
},
access: {
read: hasYouTubeAccess,
create: isYouTubeManager,
update: isYouTubeManager,
delete: isYouTubeManager,
},
fields: [
{
type: 'row',
fields: [
{
name: 'name',
type: 'text',
required: true,
localized: true,
label: { de: 'Template-Name', en: 'Template Name' },
admin: {
width: '50%',
placeholder: 'z.B. "GRFI Longform Template"',
},
},
{
name: 'channel',
type: 'relationship',
relationTo: 'youtube-channels',
required: true,
label: { de: 'Kanal', en: 'Channel' },
admin: { width: '50%' },
},
],
},
{
type: 'row',
fields: [
{
name: 'series',
type: 'text',
required: true,
label: { de: 'Serie', en: 'Series' },
admin: {
width: '50%',
placeholder: 'z.B. "GRFI", "M2M", "SPARK"',
},
},
{
name: 'format',
type: 'select',
required: true,
label: { de: 'Format', en: 'Format' },
options: [
{ label: 'Short (45-60s)', value: 'short' },
{ label: 'Longform (8-16 Min)', value: 'longform' },
],
admin: { width: '50%' },
},
],
},
{
name: 'description',
type: 'textarea',
label: { de: 'Beschreibung', en: 'Description' },
localized: true,
admin: {
rows: 2,
description: { de: 'Hinweise zur Verwendung dieses Templates', en: 'Usage notes for this template' },
},
},
{
name: 'templateSections',
type: 'blocks',
label: { de: 'Template Sections', en: 'Template Sections' },
blocks: [ScriptSectionBlock],
},
],
timestamps: true,
}
```
### Datei: `src/collections/YtChecklistTemplates.ts`
```typescript
import type { CollectionConfig } from 'payload'
import { isYouTubeManager, hasYouTubeAccess } from '../lib/youtubeAccess'
export const YtChecklistTemplates: CollectionConfig = {
slug: 'yt-checklist-templates',
labels: {
singular: { de: 'Checklisten-Vorlage', en: 'Checklist Template' },
plural: { de: 'Checklisten-Vorlagen', en: 'Checklist Templates' },
},
admin: {
group: 'YouTube',
useAsTitle: 'name',
defaultColumns: ['name', 'type', 'channel', 'updatedAt'],
description: { de: 'Wiederverwendbare Checklisten für Upload und Produktion', en: 'Reusable checklists for upload and production' },
},
access: {
read: hasYouTubeAccess,
create: isYouTubeManager,
update: isYouTubeManager,
delete: isYouTubeManager,
},
fields: [
{
type: 'row',
fields: [
{
name: 'name',
type: 'text',
required: true,
localized: true,
label: { de: 'Vorlagen-Name', en: 'Template Name' },
admin: {
width: '50%',
placeholder: 'z.B. "Standard Upload Checklist"',
},
},
{
name: 'channel',
type: 'relationship',
relationTo: 'youtube-channels',
label: { de: 'Kanal', en: 'Channel' },
admin: {
width: '50%',
description: { de: 'Optional: Kanal-spezifische Vorlage', en: 'Optional: Channel-specific template' },
},
},
],
},
{
type: 'row',
fields: [
{
name: 'type',
type: 'select',
required: true,
label: { de: 'Typ', en: 'Type' },
options: [
{ label: { de: 'Upload-Checkliste', en: 'Upload Checklist' }, value: 'upload' },
{ label: { de: 'Produktions-Checkliste', en: 'Production Checklist' }, value: 'production' },
{ label: { de: 'Review-Checkliste', en: 'Review Checklist' }, value: 'review' },
{ label: { de: 'Post-Publish-Checkliste', en: 'Post-Publish Checklist' }, value: 'post_publish' },
],
admin: { width: '50%' },
},
{
name: 'format',
type: 'select',
label: { de: 'Format', en: 'Format' },
options: [
{ label: { de: 'Alle Formate', en: 'All Formats' }, value: 'all' },
{ label: 'Short', value: 'short' },
{ label: 'Longform', value: 'longform' },
],
defaultValue: 'all',
admin: { width: '50%' },
},
],
},
{
name: 'description',
type: 'textarea',
label: { de: 'Beschreibung', en: 'Description' },
localized: true,
admin: {
rows: 2,
description: { de: 'Wann diese Checkliste verwendet werden soll', en: 'When to use this checklist' },
},
},
{
name: 'items',
type: 'array',
required: true,
label: { de: 'Checklisten-Punkte', en: 'Checklist Items' },
labels: {
singular: { de: 'Punkt', en: 'Item' },
plural: { de: 'Punkte', en: 'Items' },
},
minRows: 1,
fields: [
{
type: 'row',
fields: [
{
name: 'order',
type: 'number',
label: { de: 'Reihenfolge', en: 'Order' },
min: 1,
admin: { width: '15%' },
},
{
name: 'task',
type: 'text',
required: true,
localized: true,
label: { de: 'Aufgabe', en: 'Task' },
admin: {
width: '55%',
placeholder: 'z.B. "Thumbnail hochladen"',
},
},
{
name: 'category',
type: 'select',
label: { de: 'Kategorie', en: 'Category' },
options: [
{ label: 'Metadaten', value: 'metadata' },
{ label: 'Assets', value: 'assets' },
{ label: 'SEO', value: 'seo' },
{ label: 'Community', value: 'community' },
{ label: { de: 'Rechtliches', en: 'Legal' }, value: 'legal' },
{ label: { de: 'Sonstiges', en: 'Other' }, value: 'other' },
],
admin: { width: '30%' },
},
],
},
{
name: 'details',
type: 'textarea',
localized: true,
label: { de: 'Details/Hinweise', en: 'Details/Notes' },
admin: {
rows: 2,
placeholder: 'Zusätzliche Anweisungen oder Hinweise',
},
},
{
name: 'isRequired',
type: 'checkbox',
label: { de: 'Pflichtfeld', en: 'Required' },
defaultValue: true,
},
],
},
{
name: 'isDefault',
type: 'checkbox',
label: { de: 'Standard-Vorlage', en: 'Default Template' },
admin: {
position: 'sidebar',
description: { de: 'Wird automatisch für neue Videos verwendet', en: 'Automatically used for new videos' },
},
},
{
name: 'isActive',
type: 'checkbox',
label: { de: 'Aktiv', en: 'Active' },
defaultValue: true,
admin: {
position: 'sidebar',
},
},
],
timestamps: true,
}
```
---
## Teil 3: Erweiterung YouTube Content Collection
### Datei: `src/collections/YouTubeContent.ts`
Füge folgende Änderungen zur bestehenden YouTubeContent Collection hinzu:
**1. Import am Anfang der Datei hinzufügen:**
```typescript
import { ScriptSectionBlock } from '../blocks/ScriptSectionBlock'
```
**2. Neue Felder in der Sidebar (nach `createdBy` bei ca. Zeile 187):**
```typescript
{
name: 'productionBatch',
type: 'relationship',
relationTo: 'yt-batches',
label: { de: 'Produktions-Batch', en: 'Production Batch' },
admin: {
position: 'sidebar',
},
},
```
**3. Neuen Tab "Produktion" hinzufügen (im `tabs` Array nach dem Tab "Termine"):**
```typescript
{
label: { de: 'Produktion', en: 'Production' },
fields: [
{
type: 'row',
fields: [
{
name: 'productionWeek',
type: 'number',
label: { de: 'Produktionswoche', en: 'Production Week' },
min: 1,
max: 52,
admin: { width: '50%' },
},
{
name: 'calendarWeek',
type: 'number',
label: { de: 'Kalenderwoche', en: 'Calendar Week' },
min: 1,
max: 52,
admin: { width: '50%' },
},
],
},
{
name: 'productionDate',
type: 'date',
label: { de: 'Produktionsdatum', en: 'Production Date' },
admin: {
date: {
pickerAppearance: 'dayOnly',
displayFormat: 'dd.MM.yyyy',
},
},
},
{
name: 'targetDuration',
type: 'text',
label: { de: 'Ziel-Dauer', en: 'Target Duration' },
admin: {
placeholder: 'z.B. "8-12 Min" oder "45-58s"',
},
},
{
name: 'bRollNotes',
type: 'textarea',
label: { de: 'B-Roll / Setting Notizen', en: 'B-Roll / Setting Notes' },
localized: true,
admin: {
rows: 3,
},
},
],
},
```
**4. Neuen Tab "Script" hinzufügen (nach dem Tab "Produktion"):**
```typescript
{
label: 'Script',
fields: [
{
name: 'script',
type: 'blocks',
label: { de: 'Script Sections', en: 'Script Sections' },
labels: {
singular: { de: 'Section', en: 'Section' },
plural: { de: 'Script Sections', en: 'Script Sections' },
},
blocks: [ScriptSectionBlock],
admin: {
description: { de: 'Strukturiertes Video-Script mit Sections', en: 'Structured video script with sections' },
},
},
],
},
```
**5. Neuen Tab "Posting" hinzufügen (nach dem Tab "Script"):**
```typescript
{
label: 'Posting',
fields: [
{
name: 'publishTime',
type: 'text',
label: { de: 'Posting-Uhrzeit', en: 'Publish Time' },
admin: {
placeholder: 'z.B. "07:00" oder "17:00"',
},
},
{
name: 'thumbnailText',
type: 'text',
label: { de: 'Thumbnail-Text', en: 'Thumbnail Text' },
admin: {
placeholder: 'z.B. "BOARD READY | 7 MIN"',
},
},
{
type: 'row',
fields: [
{
name: 'ctaType',
type: 'select',
label: { de: 'CTA-Typ', en: 'CTA Type' },
options: [
{ label: 'Link in Bio', value: 'link_in_bio' },
{ label: 'Newsletter', value: 'newsletter' },
{ label: { de: 'Longform verlinken', en: 'Link Longform' }, value: 'longform_link' },
{ label: { de: 'Benutzerdefiniert', en: 'Custom' }, value: 'custom' },
],
admin: { width: '50%' },
},
{
name: 'ctaDetail',
type: 'text',
label: { de: 'CTA-Detail', en: 'CTA Detail' },
admin: {
width: '50%',
placeholder: 'z.B. "GRFI-Checkliste"',
condition: (data) => !!data?.ctaType,
},
},
],
},
{
name: 'uploadChecklist',
type: 'array',
label: { de: 'Upload-Checkliste', en: 'Upload Checklist' },
fields: [
{
type: 'row',
fields: [
{
name: 'step',
type: 'text',
required: true,
label: { de: 'Schritt', en: 'Step' },
admin: { width: '60%' },
},
{
name: 'completed',
type: 'checkbox',
label: { de: 'Erledigt', en: 'Done' },
admin: { width: '20%' },
},
{
name: 'completedAt',
type: 'date',
label: { de: 'Am', en: 'At' },
admin: {
width: '20%',
readOnly: true,
},
},
],
},
],
},
{
name: 'disclaimers',
type: 'array',
label: { de: 'Disclaimers', en: 'Disclaimers' },
fields: [
{
type: 'row',
fields: [
{
name: 'type',
type: 'select',
required: true,
label: { de: 'Typ', en: 'Type' },
options: [
{ label: { de: 'Medizinisch', en: 'Medical' }, value: 'medical' },
{ label: { de: 'Rechtlich', en: 'Legal' }, value: 'legal' },
{ label: 'Affiliate', value: 'affiliate' },
{ label: 'Sponsored', value: 'sponsored' },
],
admin: { width: '25%' },
},
{
name: 'text',
type: 'text',
label: { de: 'Text', en: 'Text' },
localized: true,
admin: { width: '50%' },
},
{
name: 'placement',
type: 'select',
label: { de: 'Platzierung', en: 'Placement' },
options: [
{ label: { de: 'Gesprochen', en: 'Spoken' }, value: 'spoken' },
{ label: 'Text-Overlay', value: 'overlay' },
{ label: { de: 'Beschreibung', en: 'Description' }, value: 'description' },
{ label: { de: 'Überall', en: 'All' }, value: 'all' },
],
admin: { width: '25%' },
},
],
},
],
},
],
},
```
---
## Teil 4: Payload Config Update
### Datei: `src/payload.config.ts`
**1. Neue Imports hinzufügen (nach den bestehenden YouTube Imports bei ca. Zeile 75):**
```typescript
// YouTube Operations Hub Collections - Erweitert
import { YtBatches } from './collections/YtBatches'
import { YtMonthlyGoals } from './collections/YtMonthlyGoals'
import { YtScriptTemplates } from './collections/YtScriptTemplates'
import { YtChecklistTemplates } from './collections/YtChecklistTemplates'
```
**2. Collections zum Array hinzufügen (nach YtNotifications):**
```typescript
collections: [
// ... bestehende Collections
YtBatches,
YtMonthlyGoals,
YtScriptTemplates,
YtChecklistTemplates,
],
```
---
## Teil 5: Migration erstellen
### Wichtige Hinweise zur Migration
Die Migration muss folgende Tabellen erstellen:
**Haupt-Tabellen:**
- `yt_batches`
- `yt_monthly_goals`
- `yt_script_templates`
- `yt_checklist_templates`
**Array-Tabellen (werden von Payload automatisch erstellt, aber prüfen!):**
- `yt_batches_production_period_shoot_days`
- `yt_batches_series_distribution`
- `yt_checklist_templates_items`
- `yt_monthly_goals_custom_goals`
- `yt_script_templates_template_sections`
- `youtube_content_script` (für ScriptSectionBlock)
- `youtube_content_upload_checklist`
- `youtube_content_disclaimers`
**System-Tabelle MUSS erweitert werden:**
```sql
-- KRITISCH: Diese Spalten zur System-Tabelle hinzufügen
ALTER TABLE "payload_locked_documents_rels"
ADD COLUMN IF NOT EXISTS "yt_batches_id" integer REFERENCES yt_batches(id) ON DELETE CASCADE;
ALTER TABLE "payload_locked_documents_rels"
ADD COLUMN IF NOT EXISTS "yt_monthly_goals_id" integer REFERENCES yt_monthly_goals(id) ON DELETE CASCADE;
ALTER TABLE "payload_locked_documents_rels"
ADD COLUMN IF NOT EXISTS "yt_script_templates_id" integer REFERENCES yt_script_templates(id) ON DELETE CASCADE;
ALTER TABLE "payload_locked_documents_rels"
ADD COLUMN IF NOT EXISTS "yt_checklist_templates_id" integer REFERENCES yt_checklist_templates(id) ON DELETE CASCADE;
-- Indexes
CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_yt_batches_idx"
ON "payload_locked_documents_rels" ("yt_batches_id");
CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_yt_monthly_goals_idx"
ON "payload_locked_documents_rels" ("yt_monthly_goals_id");
CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_yt_script_templates_idx"
ON "payload_locked_documents_rels" ("yt_script_templates_id");
CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_yt_checklist_templates_idx"
ON "payload_locked_documents_rels" ("yt_checklist_templates_id");
```
### Migration erstellen und ausführen
```bash
# 1. Migration erstellen
pnpm payload migrate:create youtube_ops_v2
# 2. Migration-Datei prüfen und ggf. anpassen
# Datei: src/migrations/[timestamp]_youtube_ops_v2.ts
# 3. Migration ausführen (direkte DB-Verbindung empfohlen)
./scripts/db-direct.sh migrate
# 4. Build erstellen
pnpm build
# 5. PM2 neu starten
pm2 restart payload
```
---
## Teil 6: Seed-Daten (BlogWoman Januar) - Optional
### Datei: `src/seed/blogwoman-januar.ts`
```typescript
import type { Payload } from 'payload'
export async function seedBlogWomanJanuar(payload: Payload) {
console.log('Seeding BlogWoman Januar Content...')
// 1. BlogWoman Kanal finden oder erstellen
const channels = await payload.find({
collection: 'youtube-channels',
where: { slug: { equals: 'blogwoman' } },
limit: 1,
})
let blogwomanChannel = channels.docs[0]
if (!blogwomanChannel) {
// Kanal erstellen (mit allen Pflichtfeldern!)
blogwomanChannel = await payload.create({
collection: 'youtube-channels',
data: {
name: 'BlogWoman by Caroline Porwoll',
slug: 'blogwoman',
youtubeChannelId: 'UCxxxxxxxxxxxxxxxxxx', // PFLICHT - echte ID eintragen!
youtubeHandle: '@blogwoman',
language: 'de',
category: 'lifestyle',
status: 'active',
contentSeries: [
{ name: 'GRFI', slug: 'grfi', description: 'Get Ready For Impact', color: '#3B82F6', isActive: true },
{ name: 'Investment-Piece', slug: 'investment', description: 'Cost-per-Wear Analysen', color: '#8B5CF6', isActive: true },
{ name: 'Pleasure P&L', slug: 'pleasure-pl', description: 'ROI auf Genuss', color: '#EC4899', isActive: true },
{ name: 'M2M', slug: 'm2m', description: 'Meeting to Mom Mode', color: '#F59E0B', isActive: true },
{ name: 'SPARK', slug: 'spark', description: 'Die Flamme', color: '#EF4444', isActive: true },
{ name: 'Regeneration', slug: 'regeneration', description: 'Energie ohne Kitsch', color: '#10B981', isActive: true },
{ name: 'Decision-Proof', slug: 'decision-proof', description: 'Regeln statt Willenskraft', color: '#6366F1', isActive: true },
{ name: 'Sunday Reset', slug: 'sunday-reset', description: 'Wochenplanung', color: '#14B8A6', isActive: true },
],
publishingSchedule: {
defaultDays: ['sunday', 'wednesday', 'saturday'],
defaultTime: '07:00',
shortsPerWeek: 7,
longformPerWeek: 3,
},
},
})
console.log('BlogWoman Kanal erstellt')
}
// 2. Januar Batch erstellen
const januarBatch1 = await payload.create({
collection: 'yt-batches',
data: {
name: 'Januar Woche 1',
channel: blogwomanChannel.id,
productionPeriod: {
start: '2026-01-06',
end: '2026-01-10',
},
shootDays: [
{ date: '2026-01-06', location: 'home', duration: '4h', notes: 'Longforms + Shorts' },
{ date: '2026-01-08', location: 'home', duration: '4h', notes: 'SPARK + M2M' },
],
targets: {
shortsTarget: 7,
longformsTarget: 3,
bufferDays: 3,
},
seriesDistribution: [
{ series: 'GRFI', shortsCount: 2, longformsCount: 1, priority: 'high' },
{ series: 'Investment-Piece', shortsCount: 1, longformsCount: 1, priority: 'high' },
{ series: 'SPARK', shortsCount: 1, longformsCount: 1, priority: 'high' },
{ series: 'M2M', shortsCount: 1, longformsCount: 0, priority: 'normal' },
{ series: 'Regeneration', shortsCount: 1, longformsCount: 0, priority: 'normal' },
{ series: 'Decision-Proof', shortsCount: 1, longformsCount: 0, priority: 'normal' },
],
status: 'planning',
},
})
console.log('Januar Batch 1 erstellt')
// 3. Beispiel-Content erstellen
const exampleVideos = [
{
title: '7 Minuten: Boardroom-ready (mein komplettes System)',
contentSeries: 'grfi',
format: 'longform' as const,
status: 'idea' as const,
productionBatch: januarBatch1.id,
productionDate: '2026-01-06',
scheduledPublishDate: '2026-01-12',
publishTime: '10:00',
targetDuration: '8-12 Min',
hook: 'Board-Meeting in 20 Minuten. Der Morgen war Chaos, die Kinder waren anstrengend, der Kaffee kalt. Aber das Meeting muss sitzen. So werde ich in 7 Minuten ready.',
thumbnailText: 'BOARD READY | 7 MIN',
ctaType: 'newsletter' as const,
ctaDetail: 'GRFI-Checkliste',
bRollNotes: 'Kleiderschrank, Spiegel, Badezimmer, 3 Outfit-Kombinationen zeigen',
},
{
title: '200 Euro Blazer: Cost-per-Wear nach 2 Jahren',
contentSeries: 'investment',
format: 'longform' as const,
status: 'idea' as const,
productionBatch: januarBatch1.id,
productionDate: '2026-01-07',
scheduledPublishDate: '2026-01-15',
publishTime: '17:00',
targetDuration: '10-14 Min',
hook: '200 Euro für einen Blazer. Klingt viel? Ich hab die Rechnung gemacht. Nach 2 Jahren. Mit echten Zahlen.',
thumbnailText: '200 EURO | DIE RECHNUNG',
ctaType: 'newsletter' as const,
ctaDetail: 'CPW-Rechner',
bRollNotes: 'Blazer in 3 Kontexten: Board-Meeting, Office, Abend',
},
{
title: 'Call endet in 5 Min - Kita-Abholung in 20',
contentSeries: 'm2m',
format: 'short' as const,
status: 'idea' as const,
productionBatch: januarBatch1.id,
productionDate: '2026-01-06',
scheduledPublishDate: '2026-01-13',
publishTime: '07:00',
targetDuration: '45-58s',
hook: 'Video-Call endet in 5 Minuten. Kita-Abholung in 20. So switch ich.',
thumbnailText: 'BUSINESS zu MAMA',
ctaType: 'link_in_bio' as const,
bRollNotes: 'Business zu Casual Transformation',
},
]
for (const video of exampleVideos) {
await payload.create({
collection: 'youtube-content',
data: {
...video,
channel: blogwomanChannel.id,
priority: 'normal',
slug: video.title
.toLowerCase()
.replace(/[äöüß]/g, (char: string) => {
const map: Record<string, string> = { ä: 'ae', ö: 'oe', ü: 'ue', ß: 'ss' }
return map[char] || char
})
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, ''),
},
})
}
console.log(`${exampleVideos.length} Beispiel-Videos erstellt`)
// 4. Monatsziele erstellen
await payload.create({
collection: 'yt-monthly-goals',
data: {
channel: blogwomanChannel.id,
month: '2026-01-01',
contentGoals: {
longformsTarget: 12,
longformsCurrent: 0,
shortsTarget: 28,
shortsCurrent: 0,
},
audienceGoals: {
subscribersTarget: 500,
subscribersCurrent: 0,
viewsTarget: 50000,
viewsCurrent: 0,
},
engagementGoals: {
avgCtrTarget: '>4%',
avgRetentionTarget: '>50%',
},
businessGoals: {
newsletterSignupsTarget: 500,
newsletterSignupsCurrent: 0,
},
customGoals: [
{ metric: 'SPARK Videos', target: '6', current: '0', status: 'on_track' },
{ metric: 'Shorts Retention', target: '>60%', status: 'on_track' },
],
},
})
console.log('Januar Monatsziele erstellt')
console.log('BlogWoman Januar Seeding abgeschlossen!')
}
```
---
## Ausführungsreihenfolge
1. **Block erstellen:** `src/blocks/ScriptSectionBlock.ts`
2. **Collections erstellen:**
- `src/collections/YtBatches.ts`
- `src/collections/YtMonthlyGoals.ts`
- `src/collections/YtScriptTemplates.ts`
3. **YouTubeContent.ts erweitern:** Import + neue Felder/Tabs
4. **payload.config.ts aktualisieren:** Imports + Collections Array
5. **Migration erstellen:** `pnpm payload migrate:create youtube_ops_v2`
6. **Migration prüfen und anpassen:**
- Array-Tabellen vorhanden?
- `payload_locked_documents_rels` erweitert?
7. **Migration ausführen:** `./scripts/db-direct.sh migrate`
8. **Build:** `pnpm build`
9. **PM2 neustarten:** `pm2 restart payload`
10. **Testen:** Collections im Admin Panel prüfen
---
## Testschritte
1. [ ] Server starten und Admin Panel öffnen
2. [ ] Neue Collections unter "YouTube" sichtbar:
- Production Batches
- Monatsziele
- Script Templates
- Checklisten-Vorlagen
3. [ ] Production Batch erstellen und mit Kanal verknüpfen
4. [ ] Checklisten-Vorlage erstellen (z.B. "Standard Upload Checklist")
5. [ ] YouTube-Video erstellen mit:
- Production Batch Zuweisung
- Script Sections
- Upload-Checkliste
6. [ ] Monthly Goal für aktuellen Monat erstellen
6. [ ] Script Template erstellen und bei Video verwenden
---
## Bekannte Einschränkungen
- **Custom Admin Views:** Kalender-Ansicht nicht möglich wegen path-to-regexp Bug in Payload 3.x
- **SCSS:** Payload 3.x verwendet CSS Modules, keine SCSS-Kompilierung
- **PgBouncer:** Für Migrationen `./scripts/db-direct.sh` verwenden (umgeht Transaction-Mode)
---
## Troubleshooting
**"relation does not exist" Fehler:**
- Migration wurde nicht korrekt ausgeführt
- Array-Tabellen fehlen
- `payload_locked_documents_rels` nicht erweitert
- Lösung: Migration-Datei prüfen und erneut ausführen
**TypeScript-Fehler:**
```bash
pnpm payload generate:types
```
**Collection nicht sichtbar:**
- Import in `payload.config.ts` prüfen
- Collection im `collections` Array vorhanden?
- Build neu erstellen: `pnpm build`
**Hook-Fehler bei Progress-Berechnung:**
- `productionBatch` Feld in YouTubeContent vorhanden?
- Migration für neue Felder ausgeführt?