mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 22:04:10 +00:00
- 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>
1806 lines
53 KiB
Markdown
1806 lines
53 KiB
Markdown
# 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?
|