# YouTube Operations Hub – Payload CMS Integration Prompt

## Kontext für Claude Code

Du arbeitest auf dem Payload CMS Development Server (sv-payload, 10.10.181.100) der Complex Care Solutions GmbH. Deine Aufgabe ist die Integration eines "YouTube Operations Hub" – eines internen Tools zur Steuerung von drei YouTube-Kanälen.

### Bestehende Infrastruktur

```
Server: sv-payload (LXC 700)
IP: 10.10.181.100
Port: 3000
OS: Debian 13
Stack: Payload CMS 3.69.0, Next.js 15.5.9, PostgreSQL 17, Redis, PgBouncer
Datenbank: payload_db auf sv-postgres (10.10.181.101)
```

### Projektstruktur

```
/home/payload/payload-cms/
├── src/
│   ├── collections/           # Alle Collections
│   ├── globals/               # Globale Einstellungen
│   ├── hooks/                 # Collection Hooks (separate Dateien!)
│   ├── lib/
│   │   ├── tenantAccess.ts    # Access Control Funktionen
│   │   ├── access.ts          # Zusätzliche Access-Helfer
│   │   ├── validation.ts      # Validierungs-Hooks
│   │   └── security/          # Security-Module
│   ├── app/(payload)/api/     # Custom API Routes (Next.js App Router)
│   └── payload.config.ts      # Haupt-Konfiguration
├── .env
└── ecosystem.config.cjs       # PM2 Config
```

---

## WICHTIGE KONVENTIONEN

### 1. Multi-Tenant Architektur

Das System nutzt `@payloadcms/plugin-multi-tenant`. **Alle neuen Collections müssen:**

1. Die Access-Control-Funktionen aus `src/lib/tenantAccess.ts` verwenden:
   ```typescript
   import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'

   access: {
     read: tenantScopedPublicRead,  // Öffentlich lesbar, aber tenant-isoliert
     create: authenticatedOnly,
     update: authenticatedOnly,
     delete: authenticatedOnly,
   }
   ```

2. In `payload.config.ts` im `multiTenantPlugin` registriert werden:
   ```typescript
   multiTenantPlugin({
     collections: {
       // ... bestehende Collections
       'youtube-channels': {},
       'youtube-content': {},
       'yt-tasks': {},
       // ...
     },
   })
   ```

### 2. Lokalisierung (i18n)

Das System unterstützt Deutsch (default) und Englisch. **Alle text-basierten Felder sollten lokalisiert sein:**

```typescript
{
  name: 'title',
  type: 'text',
  required: true,
  localized: true,  // WICHTIG!
  label: 'Titel',
}
```

### 3. Users Collection

Die bestehende Users Collection nutzt `isSuperAdmin: boolean` für Super-Admin-Rechte. **Nicht überschreiben!**

Für YouTube-spezifische Rollen: Separates Array-Feld oder eigene Collection.

### 4. Bestehende Collections - KONFLIKTE VERMEIDEN

| Bestehend | Slug | Neuer Name für YouTube |
|-----------|------|------------------------|
| Videos | `videos` | `youtube-content` |
| Series | `series` | `youtube-channels.series` (Array) |

### 5. Admin Group Namen

Verwende deutsche Gruppennamen:
- `YouTube` (für alle YouTube Operations Collections)
- Nicht: "YouTube Operations"

### 6. Hooks-Struktur

Hooks gehören in **separate Dateien** unter `src/hooks/`:

```
src/hooks/
├── youtubeContent/
│   ├── createTasksOnStatusChange.ts
│   └── notifyOnApproval.ts
└── ytTasks/
    └── notifyOnAssignment.ts
```

### 7. API Routes (Next.js App Router)

API-Endpoints unter `src/app/(payload)/api/`:

```
src/app/(payload)/api/
└── youtube/
    ├── dashboard/
    │   └── route.ts
    ├── my-tasks/
    │   └── route.ts
    └── complete-task/
        └── [id]/
            └── route.ts
```

### 8. Migrationen - KRITISCH

Nach dem Erstellen neuer Collections **MUSS** die Migration die System-Tabelle erweitern:

```sql
-- Für JEDE neue Collection:
ALTER TABLE "payload_locked_documents_rels"
  ADD COLUMN IF NOT EXISTS "youtube_channels_id" integer
  REFERENCES youtube_channels(id) ON DELETE CASCADE;

CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_youtube_channels_idx"
  ON "payload_locked_documents_rels" ("youtube_channels_id");
```

### 9. Slug-Validierung

Verwende den bestehenden Slug-Validierungs-Hook:

```typescript
import { createSlugValidationHook } from '../lib/validation'

hooks: {
  beforeValidate: [
    createSlugValidationHook({ collection: 'youtube-content' }),
  ],
}
```

---

## Aufgabe

Implementiere die folgenden Collections und Funktionalitäten für den YouTube Operations Hub. Das System dient zur Verwaltung von drei YouTube-Kanälen mit unterschiedlichen Teams.

---

## 1. Rollen & Access Control

### YouTube-spezifische Rollen

Da die Users Collection `isSuperAdmin` verwendet, implementieren wir YouTube-Rollen als **separates Feld auf Users**:

```typescript
// In Users.ts hinzufügen (NICHT ersetzen!):
{
  name: 'youtubeRole',
  type: 'select',
  label: 'YouTube-Rolle',
  admin: {
    position: 'sidebar',
    description: 'Rolle im YouTube Operations Hub',
  },
  options: [
    { label: 'Kein Zugriff', value: 'none' },
    { label: 'Viewer (nur Lesen)', value: 'viewer' },
    { label: 'Editor (Schnitt)', value: 'editor' },
    { label: 'Producer (Produktion)', value: 'producer' },
    { label: 'Creator (Inhalte)', value: 'creator' },
    { label: 'Manager (Vollzugriff)', value: 'manager' },
  ],
  defaultValue: 'none',
},
{
  name: 'youtubeChannels',
  type: 'relationship',
  relationTo: 'youtube-channels',
  hasMany: true,
  label: 'YouTube-Kanäle',
  admin: {
    position: 'sidebar',
    description: 'Zugewiesene YouTube-Kanäle',
    condition: (data) => data?.youtubeRole && data.youtubeRole !== 'none',
  },
},
```

### Access Control Funktionen

```typescript
// src/lib/youtubeAccess.ts
import type { Access, PayloadRequest } from 'payload'

interface UserWithYouTubeRole {
  id: number
  isSuperAdmin?: boolean
  youtubeRole?: 'none' | 'viewer' | 'editor' | 'producer' | 'creator' | 'manager'
  youtubeChannels?: Array<{ id: number } | number>
}

/**
 * Prüft ob User YouTube-Manager oder Super-Admin ist
 */
export const isYouTubeManager: Access = ({ req }) => {
  const user = req.user as UserWithYouTubeRole | null
  if (!user) return false
  if (user.isSuperAdmin) return true
  return user.youtubeRole === 'manager'
}

/**
 * Prüft ob User mindestens Creator-Rechte hat
 */
export const isYouTubeCreatorOrAbove: Access = ({ req }) => {
  const user = req.user as UserWithYouTubeRole | null
  if (!user) return false
  if (user.isSuperAdmin) return true
  return ['creator', 'manager'].includes(user.youtubeRole || '')
}

/**
 * Prüft ob User Zugriff auf YouTube-Content hat (mindestens Viewer)
 */
export const hasYouTubeAccess: Access = ({ req }) => {
  const user = req.user as UserWithYouTubeRole | null
  if (!user) return false
  if (user.isSuperAdmin) return true
  return user.youtubeRole !== 'none' && user.youtubeRole !== undefined
}

/**
 * Zugriff auf zugewiesene Videos oder als Manager
 */
export const canAccessAssignedContent: Access = async ({ req }) => {
  const user = req.user as UserWithYouTubeRole | null
  if (!user) return false
  if (user.isSuperAdmin) return true
  if (user.youtubeRole === 'manager') return true

  // Für andere Rollen: Nur zugewiesene Inhalte
  return {
    or: [
      { assignedTo: { equals: user.id } },
      { createdBy: { equals: user.id } },
    ],
  }
}
```

---

## 2. Collections

### 2.1 YouTubeChannels (Kanäle)

```typescript
// src/collections/YouTubeChannels.ts
import type { CollectionConfig } from 'payload'
import { authenticatedOnly } from '../lib/tenantAccess'
import { isYouTubeManager, hasYouTubeAccess } from '../lib/youtubeAccess'

export const YouTubeChannels: CollectionConfig = {
  slug: 'youtube-channels',
  labels: {
    singular: 'YouTube-Kanal',
    plural: 'YouTube-Kanäle',
  },
  admin: {
    useAsTitle: 'name',
    group: 'YouTube',
    defaultColumns: ['name', 'youtubeHandle', 'status', 'language'],
    description: 'YouTube-Kanäle und ihre Konfiguration',
  },
  access: {
    read: hasYouTubeAccess,
    create: isYouTubeManager,
    update: isYouTubeManager,
    delete: isYouTubeManager,
  },
  fields: [
    {
      name: 'name',
      type: 'text',
      required: true,
      localized: true,
      label: 'Kanalname',
      admin: {
        description: 'z.B. "BlogWoman by Caroline Porwoll"',
      },
    },
    {
      name: 'slug',
      type: 'text',
      required: true,
      unique: true,
      label: 'Slug',
      admin: {
        description: 'Interner Kurzname (z.B. "blogwoman", "corporate-de")',
      },
    },
    {
      name: 'youtubeChannelId',
      type: 'text',
      required: true,
      label: 'YouTube Channel ID',
      admin: {
        description: 'Die YouTube Channel ID (z.B. "UCxxxxxxxxxxxxx")',
      },
    },
    {
      name: 'youtubeHandle',
      type: 'text',
      label: 'YouTube Handle',
      admin: {
        description: 'z.B. "@blogwoman" oder "@zweitmeinu.ng"',
      },
    },
    {
      name: 'language',
      type: 'select',
      required: true,
      label: 'Sprache',
      options: [
        { label: 'Deutsch', value: 'de' },
        { label: 'Englisch', value: 'en' },
      ],
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'category',
      type: 'select',
      required: true,
      label: 'Kategorie',
      options: [
        { label: 'Lifestyle', value: 'lifestyle' },
        { label: 'Corporate', value: 'corporate' },
        { label: 'Business B2B', value: 'b2b' },
      ],
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'status',
      type: 'select',
      required: true,
      defaultValue: 'active',
      label: 'Status',
      options: [
        { label: 'Aktiv', value: 'active' },
        { label: 'In Planung', value: 'planned' },
        { label: 'Pausiert', value: 'paused' },
        { label: 'Archiviert', value: 'archived' },
      ],
      admin: {
        position: 'sidebar',
      },
    },
    // Branding
    {
      name: 'branding',
      type: 'group',
      label: 'Branding',
      fields: [
        {
          name: 'primaryColor',
          type: 'text',
          label: 'Primärfarbe (Hex)',
          admin: { description: 'z.B. #1278B3' },
          validate: (value: string | undefined | null) => {
            if (!value) return true
            const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
            if (!hexRegex.test(value)) {
              return 'Bitte einen gültigen Hex-Farbcode eingeben (z.B. #1278B3)'
            }
            return true
          },
        },
        {
          name: 'secondaryColor',
          type: 'text',
          label: 'Sekundärfarbe (Hex)',
          validate: (value: string | undefined | null) => {
            if (!value) return true
            const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
            if (!hexRegex.test(value)) {
              return 'Bitte einen gültigen Hex-Farbcode eingeben'
            }
            return true
          },
        },
        {
          name: 'logo',
          type: 'upload',
          relationTo: 'media',
          label: 'Logo',
        },
        {
          name: 'thumbnailTemplate',
          type: 'upload',
          relationTo: 'media',
          label: 'Thumbnail-Vorlage',
        },
      ],
    },
    // Content-Serien (inline Array statt separate Collection)
    {
      name: 'contentSeries',
      type: 'array',
      label: 'Content-Serien',
      admin: {
        description: 'Wiederkehrende Formate wie "GRFI", "Investment-Piece", etc.',
      },
      fields: [
        {
          name: 'name',
          type: 'text',
          required: true,
          localized: true,
          label: 'Name',
        },
        {
          name: 'slug',
          type: 'text',
          required: true,
          label: 'Slug',
        },
        {
          name: 'description',
          type: 'textarea',
          localized: true,
          label: 'Beschreibung',
        },
        {
          name: 'color',
          type: 'text',
          label: 'Farbe (Hex)',
          admin: { description: 'Farbe für UI' },
        },
        {
          name: 'isActive',
          type: 'checkbox',
          defaultValue: true,
          label: 'Aktiv',
        },
      ],
    },
    // Veröffentlichungsplan
    {
      name: 'publishingSchedule',
      type: 'group',
      label: 'Veröffentlichungsplan',
      fields: [
        {
          name: 'defaultDays',
          type: 'select',
          hasMany: true,
          label: 'Standard-Tage',
          options: [
            { label: 'Montag', value: 'monday' },
            { label: 'Dienstag', value: 'tuesday' },
            { label: 'Mittwoch', value: 'wednesday' },
            { label: 'Donnerstag', value: 'thursday' },
            { label: 'Freitag', value: 'friday' },
            { label: 'Samstag', value: 'saturday' },
            { label: 'Sonntag', value: 'sunday' },
          ],
        },
        {
          name: 'defaultTime',
          type: 'text',
          label: 'Standard-Uhrzeit',
          admin: { description: 'z.B. "12:00"' },
        },
        {
          name: 'shortsPerWeek',
          type: 'number',
          defaultValue: 4,
          label: 'Shorts pro Woche',
        },
        {
          name: 'longformPerWeek',
          type: 'number',
          defaultValue: 1,
          label: 'Longform pro Woche',
        },
      ],
    },
    // Metriken (via YouTube API Sync)
    {
      name: 'currentMetrics',
      type: 'group',
      label: 'Aktuelle Metriken',
      admin: {
        description: 'Automatisch via YouTube API aktualisiert',
      },
      fields: [
        { name: 'subscriberCount', type: 'number', label: 'Abonnenten', admin: { readOnly: true } },
        { name: 'totalViews', type: 'number', label: 'Gesamtaufrufe', admin: { readOnly: true } },
        { name: 'videoCount', type: 'number', label: 'Anzahl Videos', admin: { readOnly: true } },
        { name: 'lastSyncedAt', type: 'date', label: 'Letzter Sync', admin: { readOnly: true } },
      ],
    },
  ],
  timestamps: true,
}
```

### 2.2 YouTubeContent (Content Pipeline)

```typescript
// src/collections/YouTubeContent.ts
import type { CollectionConfig } from 'payload'
import { hasYouTubeAccess, isYouTubeCreatorOrAbove, isYouTubeManager, canAccessAssignedContent } from '../lib/youtubeAccess'
import { createSlugValidationHook } from '../lib/validation'

export const YouTubeContent: CollectionConfig = {
  slug: 'youtube-content',
  labels: {
    singular: 'YouTube-Video',
    plural: 'YouTube-Videos',
  },
  admin: {
    useAsTitle: 'title',
    group: 'YouTube',
    defaultColumns: ['title', 'channel', 'status', 'format', 'scheduledPublishDate', 'assignedTo'],
    listSearchableFields: ['title', 'description'],
    description: 'Content-Pipeline für YouTube-Videos',
  },
  access: {
    read: canAccessAssignedContent,
    create: isYouTubeCreatorOrAbove,
    update: canAccessAssignedContent,
    delete: isYouTubeManager,
  },
  hooks: {
    beforeValidate: [
      createSlugValidationHook({ collection: 'youtube-content' }),
    ],
    afterChange: [
      // Hook wird in separater Datei definiert
      // async ({ doc, previousDoc, req, operation }) => { ... }
    ],
  },
  fields: [
    // === GRUNDDATEN ===
    {
      name: 'title',
      type: 'text',
      required: true,
      localized: true,
      label: 'Titel',
      maxLength: 100,
      admin: {
        description: 'Max. 60 Zeichen für YouTube empfohlen',
      },
    },
    {
      name: 'slug',
      type: 'text',
      required: true,
      label: 'Slug',
      admin: {
        description: 'URL-freundlicher Name',
      },
    },
    {
      name: 'channel',
      type: 'relationship',
      relationTo: 'youtube-channels',
      required: true,
      label: 'Kanal',
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'contentSeries',
      type: 'text',
      label: 'Serie',
      admin: {
        description: 'Slug der Serie aus dem Kanal (z.B. "grfi", "investment-piece")',
        position: 'sidebar',
      },
    },
    {
      name: 'format',
      type: 'select',
      required: true,
      label: 'Format',
      options: [
        { label: 'Short (< 60s)', value: 'short' },
        { label: 'Longform', value: 'longform' },
        { label: 'Premiere/Live', value: 'premiere' },
      ],
      admin: {
        position: 'sidebar',
      },
    },

    // === STATUS & WORKFLOW ===
    {
      name: 'status',
      type: 'select',
      required: true,
      defaultValue: 'idea',
      label: 'Status',
      options: [
        { label: 'Idee', value: 'idea' },
        { label: 'Skript in Arbeit', value: 'script_draft' },
        { label: 'Skript Review', value: 'script_review' },
        { label: 'Skript freigegeben', value: 'script_approved' },
        { label: 'Dreh geplant', value: 'shoot_scheduled' },
        { label: 'Gedreht', value: 'shot' },
        { label: 'Rohschnitt', value: 'rough_cut' },
        { label: 'Feinschnitt', value: 'fine_cut' },
        { label: 'Final Review', value: 'final_review' },
        { label: 'Freigegeben', value: 'approved' },
        { label: 'Upload geplant', value: 'upload_scheduled' },
        { label: 'Live', value: 'published' },
        { label: 'Performance getrackt', value: 'tracked' },
        { label: 'Verworfen', value: 'discarded' },
      ],
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'priority',
      type: 'select',
      defaultValue: 'normal',
      label: 'Priorität',
      options: [
        { label: 'Dringend', value: 'urgent' },
        { label: 'Hoch', value: 'high' },
        { label: 'Normal', value: 'normal' },
        { label: 'Niedrig', value: 'low' },
      ],
      admin: {
        position: 'sidebar',
      },
    },

    // === ZUWEISUNG ===
    {
      name: 'assignedTo',
      type: 'relationship',
      relationTo: 'users',
      label: 'Zugewiesen an',
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'createdBy',
      type: 'relationship',
      relationTo: 'users',
      label: 'Erstellt von',
      admin: {
        readOnly: true,
        position: 'sidebar',
      },
    },

    // === TABS ===
    {
      type: 'tabs',
      tabs: [
        {
          label: 'Inhalt',
          fields: [
            {
              name: 'description',
              type: 'textarea',
              localized: true,
              label: 'Beschreibung / Konzept',
            },
            {
              name: 'hook',
              type: 'text',
              localized: true,
              label: 'Hook (erste 3 Sekunden)',
              admin: {
                description: 'Was sagt/zeigt der Creator in den ersten 3 Sekunden?',
              },
            },
            {
              name: 'keyPoints',
              type: 'array',
              label: 'Kernpunkte',
              fields: [
                { name: 'point', type: 'text', localized: true, label: 'Punkt' },
              ],
            },
            {
              name: 'callToAction',
              type: 'text',
              localized: true,
              label: 'Call-to-Action',
            },
            {
              name: 'scriptUrl',
              type: 'text',
              label: 'Skript-Link (Google Docs)',
            },
            {
              name: 'scriptContent',
              type: 'richText',
              localized: true,
              label: 'Skript (optional inline)',
            },
          ],
        },
        {
          label: 'Termine',
          fields: [
            {
              name: 'shootDate',
              type: 'date',
              label: 'Dreh-Datum',
              admin: {
                date: { pickerAppearance: 'dayAndTime' },
              },
            },
            {
              name: 'editDeadline',
              type: 'date',
              label: 'Schnitt-Deadline',
            },
            {
              name: 'reviewDeadline',
              type: 'date',
              label: 'Review-Deadline',
            },
            {
              name: 'scheduledPublishDate',
              type: 'date',
              label: 'Geplantes Veröffentlichungsdatum',
              admin: {
                date: { pickerAppearance: 'dayAndTime' },
              },
            },
            {
              name: 'actualPublishDate',
              type: 'date',
              label: 'Tatsächliches Veröffentlichungsdatum',
              admin: { readOnly: true },
            },
          ],
        },
        {
          label: 'Dateien',
          fields: [
            {
              name: 'thumbnail',
              type: 'upload',
              relationTo: 'media',
              label: 'Thumbnail',
            },
            {
              name: 'thumbnailAlt',
              type: 'upload',
              relationTo: 'media',
              label: 'Thumbnail (A/B-Test)',
            },
            {
              name: 'videoFile',
              type: 'upload',
              relationTo: 'media',
              label: 'Video-Datei (Final)',
            },
            {
              name: 'rawFootage',
              type: 'array',
              label: 'Rohmaterial',
              fields: [
                { name: 'file', type: 'upload', relationTo: 'media', label: 'Datei' },
                { name: 'description', type: 'text', label: 'Beschreibung' },
              ],
            },
          ],
        },
        {
          label: 'Freigaben',
          fields: [
            {
              name: 'approvals',
              type: 'group',
              label: 'Freigaben',
              fields: [
                {
                  name: 'scriptApproval',
                  type: 'group',
                  label: 'Skript-Freigabe',
                  fields: [
                    { name: 'approved', type: 'checkbox', label: 'Freigegeben' },
                    { name: 'approvedBy', type: 'relationship', relationTo: 'users', label: 'Von' },
                    { name: 'approvedAt', type: 'date', label: 'Am' },
                    { name: 'notes', type: 'textarea', localized: true, label: 'Anmerkungen' },
                  ],
                },
                {
                  name: 'medicalApproval',
                  type: 'group',
                  label: 'Medizinische Freigabe',
                  admin: {
                    description: 'Nur für Corporate-Kanäle relevant',
                  },
                  fields: [
                    { name: 'required', type: 'checkbox', label: 'Erforderlich', defaultValue: false },
                    { name: 'approved', type: 'checkbox', label: 'Freigegeben' },
                    { name: 'approvedBy', type: 'relationship', relationTo: 'users', label: 'Von' },
                    { name: 'approvedAt', type: 'date', label: 'Am' },
                    { name: 'notes', type: 'textarea', localized: true, label: 'Anmerkungen' },
                  ],
                },
                {
                  name: 'legalApproval',
                  type: 'group',
                  label: 'Rechtliche Freigabe',
                  fields: [
                    { name: 'approved', type: 'checkbox', label: 'Freigegeben' },
                    { name: 'approvedBy', type: 'relationship', relationTo: 'users', label: 'Von' },
                    { name: 'approvedAt', type: 'date', label: 'Am' },
                    { name: 'disclaimerIncluded', type: 'checkbox', label: 'Disclaimer enthalten' },
                    { name: 'notes', type: 'textarea', localized: true, label: 'Anmerkungen' },
                  ],
                },
                {
                  name: 'finalApproval',
                  type: 'group',
                  label: 'Finale Freigabe',
                  fields: [
                    { name: 'approved', type: 'checkbox', label: 'Freigegeben' },
                    { name: 'approvedBy', type: 'relationship', relationTo: 'users', label: 'Von' },
                    { name: 'approvedAt', type: 'date', label: 'Am' },
                    { name: 'notes', type: 'textarea', localized: true, label: 'Anmerkungen' },
                  ],
                },
              ],
            },
          ],
        },
        {
          label: 'YouTube',
          fields: [
            {
              name: 'youtube',
              type: 'group',
              label: 'YouTube-Daten',
              fields: [
                { name: 'videoId', type: 'text', label: 'YouTube Video ID', admin: { readOnly: true } },
                { name: 'url', type: 'text', label: 'YouTube URL', admin: { readOnly: true } },
                {
                  name: 'metadata',
                  type: 'group',
                  label: 'Metadaten für Upload',
                  fields: [
                    { name: 'youtubeTitle', type: 'text', localized: true, label: 'YouTube-Titel', maxLength: 100 },
                    { name: 'youtubeDescription', type: 'textarea', localized: true, label: 'YouTube-Beschreibung' },
                    {
                      name: 'tags',
                      type: 'array',
                      label: 'Tags',
                      fields: [{ name: 'tag', type: 'text', label: 'Tag' }],
                    },
                    {
                      name: 'visibility',
                      type: 'select',
                      label: 'Sichtbarkeit',
                      options: [
                        { label: 'Öffentlich', value: 'public' },
                        { label: 'Nicht gelistet', value: 'unlisted' },
                        { label: 'Privat', value: 'private' },
                      ],
                      defaultValue: 'private',
                    },
                    { name: 'chapters', type: 'textarea', label: 'Kapitelmarker' },
                    { name: 'pinnedComment', type: 'textarea', localized: true, label: 'Gepinnter Kommentar' },
                  ],
                },
              ],
            },
          ],
        },
        {
          label: 'Performance',
          fields: [
            {
              name: 'performance',
              type: 'group',
              label: 'Performance-Metriken',
              admin: {
                description: 'Automatisch via YouTube API aktualisiert',
              },
              fields: [
                { name: 'views', type: 'number', label: 'Aufrufe', admin: { readOnly: true } },
                { name: 'likes', type: 'number', label: 'Likes', admin: { readOnly: true } },
                { name: 'comments', type: 'number', label: 'Kommentare', admin: { readOnly: true } },
                { name: 'shares', type: 'number', label: 'Shares', admin: { readOnly: true } },
                { name: 'watchTimeMinutes', type: 'number', label: 'Wiedergabezeit (Min)', admin: { readOnly: true } },
                { name: 'avgViewDuration', type: 'number', label: 'Ø Wiedergabedauer (Sek)', admin: { readOnly: true } },
                { name: 'avgViewPercentage', type: 'number', label: 'Ø Wiedergabe (%)', admin: { readOnly: true } },
                { name: 'ctr', type: 'number', label: 'CTR (%)', admin: { readOnly: true } },
                { name: 'impressions', type: 'number', label: 'Impressionen', admin: { readOnly: true } },
                { name: 'subscribersGained', type: 'number', label: 'Neue Abos', admin: { readOnly: true } },
                { name: 'lastSyncedAt', type: 'date', label: 'Letzter Sync', admin: { readOnly: true } },
              ],
            },
          ],
        },
      ],
    },

    // === INTERNE NOTIZEN ===
    {
      name: 'internalNotes',
      type: 'richText',
      label: 'Interne Notizen',
      admin: {
        description: 'Nur für das Team sichtbar',
      },
    },
  ],
  timestamps: true,
}
```

### 2.3 YtTasks (Aufgaben)

```typescript
// src/collections/YtTasks.ts
import type { CollectionConfig } from 'payload'
import { hasYouTubeAccess, isYouTubeManager, canAccessAssignedContent } from '../lib/youtubeAccess'

export const YtTasks: CollectionConfig = {
  slug: 'yt-tasks',
  labels: {
    singular: 'YouTube-Aufgabe',
    plural: 'YouTube-Aufgaben',
  },
  admin: {
    useAsTitle: 'title',
    group: 'YouTube',
    defaultColumns: ['title', 'video', 'assignedTo', 'status', 'dueDate', 'priority'],
    listSearchableFields: ['title'],
    description: 'Aufgaben für die Video-Produktion',
  },
  access: {
    read: canAccessAssignedContent,
    create: isYouTubeManager,
    update: canAccessAssignedContent,
    delete: isYouTubeManager,
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
      localized: true,
      label: 'Aufgabe',
    },
    {
      name: 'description',
      type: 'textarea',
      localized: true,
      label: 'Beschreibung',
    },
    {
      name: 'video',
      type: 'relationship',
      relationTo: 'youtube-content',
      label: 'Zugehöriges Video',
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'channel',
      type: 'relationship',
      relationTo: 'youtube-channels',
      label: 'Kanal',
      admin: {
        position: 'sidebar',
        description: 'Wird automatisch vom Video übernommen',
      },
    },
    {
      name: 'taskType',
      type: 'select',
      required: true,
      label: 'Aufgabentyp',
      options: [
        { label: 'Skript schreiben', value: 'script_write' },
        { label: 'Skript reviewen', value: 'script_review' },
        { label: 'Dreh vorbereiten', value: 'shoot_prep' },
        { label: 'Drehen', value: 'shoot' },
        { label: 'Schneiden', value: 'edit' },
        { label: 'Grafiken erstellen', value: 'graphics' },
        { label: 'Thumbnail erstellen', value: 'thumbnail' },
        { label: 'Review/Freigabe', value: 'review' },
        { label: 'Hochladen', value: 'upload' },
        { label: 'Performance tracken', value: 'track' },
        { label: 'Kommentare beantworten', value: 'comments' },
        { label: 'Sonstiges', value: 'other' },
      ],
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'status',
      type: 'select',
      required: true,
      defaultValue: 'todo',
      label: 'Status',
      options: [
        { label: 'Offen', value: 'todo' },
        { label: 'In Arbeit', value: 'in_progress' },
        { label: 'Blockiert', value: 'blocked' },
        { label: 'Wartet auf Review', value: 'waiting_review' },
        { label: 'Erledigt', value: 'done' },
        { label: 'Abgebrochen', value: 'cancelled' },
      ],
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'priority',
      type: 'select',
      defaultValue: 'normal',
      label: 'Priorität',
      options: [
        { label: 'Dringend', value: 'urgent' },
        { label: 'Hoch', value: 'high' },
        { label: 'Normal', value: 'normal' },
        { label: 'Niedrig', value: 'low' },
      ],
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'assignedTo',
      type: 'relationship',
      relationTo: 'users',
      required: true,
      label: 'Zugewiesen an',
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'dueDate',
      type: 'date',
      label: 'Fälligkeitsdatum',
      admin: {
        position: 'sidebar',
        date: { pickerAppearance: 'dayAndTime' },
      },
    },
    {
      name: 'completedAt',
      type: 'date',
      label: 'Abgeschlossen am',
      admin: {
        readOnly: true,
        position: 'sidebar',
      },
    },
    {
      name: 'completedBy',
      type: 'relationship',
      relationTo: 'users',
      label: 'Abgeschlossen von',
      admin: { readOnly: true },
    },
    {
      name: 'blockedReason',
      type: 'text',
      localized: true,
      label: 'Grund für Blockierung',
      admin: {
        condition: (data) => data?.status === 'blocked',
      },
    },
    {
      name: 'attachments',
      type: 'array',
      label: 'Anhänge',
      fields: [
        { name: 'file', type: 'upload', relationTo: 'media', label: 'Datei' },
        { name: 'note', type: 'text', label: 'Notiz' },
      ],
    },
    {
      name: 'comments',
      type: 'array',
      label: 'Kommentare',
      fields: [
        { name: 'author', type: 'relationship', relationTo: 'users', label: 'Autor' },
        { name: 'content', type: 'textarea', label: 'Inhalt' },
        { name: 'createdAt', type: 'date', label: 'Erstellt am' },
      ],
    },
  ],
  timestamps: true,
  hooks: {
    beforeChange: [
      async ({ data, originalDoc, req }) => {
        if (!data) return data

        // Setze completedAt wenn Status auf "done" wechselt
        if (data.status === 'done' && originalDoc?.status !== 'done') {
          data.completedAt = new Date().toISOString()
          data.completedBy = req.user?.id
        }

        // Setze Channel automatisch vom Video
        if (data.video && !data.channel) {
          const video = await req.payload.findByID({
            collection: 'youtube-content',
            id: typeof data.video === 'object' ? data.video.id : data.video,
            depth: 0,
          })
          if (video?.channel) {
            data.channel = typeof video.channel === 'object' ? video.channel.id : video.channel
          }
        }

        return data
      },
    ],
  },
}
```

### 2.4 YtNotifications (Benachrichtigungen)

```typescript
// src/collections/YtNotifications.ts
import type { CollectionConfig, Access } from 'payload'

const canAccessOwnNotifications: Access = ({ req }) => {
  const user = req.user
  if (!user) return false
  if (user.isSuperAdmin) return true
  return { recipient: { equals: user.id } }
}

export const YtNotifications: CollectionConfig = {
  slug: 'yt-notifications',
  labels: {
    singular: 'Benachrichtigung',
    plural: 'Benachrichtigungen',
  },
  admin: {
    group: 'YouTube',
    defaultColumns: ['title', 'recipient', 'type', 'read', 'createdAt'],
    description: 'Benachrichtigungen für YouTube Operations',
  },
  access: {
    read: canAccessOwnNotifications,
    create: () => true, // System kann erstellen
    update: canAccessOwnNotifications,
    delete: ({ req }) => !!req.user?.isSuperAdmin,
  },
  fields: [
    {
      name: 'recipient',
      type: 'relationship',
      relationTo: 'users',
      required: true,
      label: 'Empfänger',
      index: true,
    },
    {
      name: 'type',
      type: 'select',
      required: true,
      label: 'Typ',
      options: [
        { label: 'Neue Aufgabe', value: 'task_assigned' },
        { label: 'Aufgabe fällig', value: 'task_due' },
        { label: 'Aufgabe überfällig', value: 'task_overdue' },
        { label: 'Freigabe erforderlich', value: 'approval_required' },
        { label: 'Freigabe erteilt', value: 'approved' },
        { label: 'Freigabe abgelehnt', value: 'rejected' },
        { label: 'Video veröffentlicht', value: 'video_published' },
        { label: 'Kommentar', value: 'comment' },
        { label: 'Erwähnung', value: 'mention' },
        { label: 'System', value: 'system' },
      ],
    },
    {
      name: 'title',
      type: 'text',
      required: true,
      localized: true,
      label: 'Titel',
    },
    {
      name: 'message',
      type: 'textarea',
      localized: true,
      label: 'Nachricht',
    },
    {
      name: 'link',
      type: 'text',
      label: 'Link',
      admin: {
        description: 'Relativer Pfad zum relevanten Element',
      },
    },
    {
      name: 'relatedVideo',
      type: 'relationship',
      relationTo: 'youtube-content',
      label: 'Video',
    },
    {
      name: 'relatedTask',
      type: 'relationship',
      relationTo: 'yt-tasks',
      label: 'Aufgabe',
    },
    {
      name: 'read',
      type: 'checkbox',
      defaultValue: false,
      label: 'Gelesen',
    },
    {
      name: 'readAt',
      type: 'date',
      label: 'Gelesen am',
    },
    {
      name: 'emailSent',
      type: 'checkbox',
      defaultValue: false,
      label: 'E-Mail gesendet',
    },
  ],
  timestamps: true,
}
```

---

## 3. Hooks (Separate Dateien)

### 3.1 Auto-Task-Erstellung

```typescript
// src/hooks/youtubeContent/createTasksOnStatusChange.ts
import type { CollectionAfterChangeHook } from 'payload'

interface TaskTemplate {
  title: string
  type: string
  assignRole: string
}

const TASK_TEMPLATES: Record<string, TaskTemplate[]> = {
  script_draft: [
    { title: 'Skript schreiben', type: 'script_write', assignRole: 'creator' },
  ],
  script_review: [
    { title: 'Skript reviewen', type: 'script_review', assignRole: 'manager' },
  ],
  script_approved: [
    { title: 'Dreh vorbereiten', type: 'shoot_prep', assignRole: 'producer' },
  ],
  shoot_scheduled: [
    { title: 'Dreh durchführen', type: 'shoot', assignRole: 'producer' },
  ],
  shot: [
    { title: 'Rohschnitt erstellen', type: 'edit', assignRole: 'editor' },
  ],
  rough_cut: [
    { title: 'Feinschnitt erstellen', type: 'edit', assignRole: 'editor' },
    { title: 'Grafiken erstellen', type: 'graphics', assignRole: 'editor' },
  ],
  fine_cut: [
    { title: 'Thumbnail erstellen', type: 'thumbnail', assignRole: 'editor' },
    { title: 'Final Review', type: 'review', assignRole: 'manager' },
  ],
  approved: [
    { title: 'Video hochladen', type: 'upload', assignRole: 'manager' },
  ],
  published: [
    { title: 'Performance nach 24h tracken', type: 'track', assignRole: 'manager' },
    { title: 'Kommentare beantworten', type: 'comments', assignRole: 'creator' },
  ],
}

interface UserWithYouTubeRole {
  id: number
  youtubeRole?: string
}

export const createTasksOnStatusChange: CollectionAfterChangeHook = async ({
  doc,
  previousDoc,
  req,
  operation,
}) => {
  // Nur bei Updates und wenn sich der Status geändert hat
  if (operation !== 'update' || doc.status === previousDoc?.status) {
    return doc
  }

  const tasks = TASK_TEMPLATES[doc.status]
  if (!tasks || tasks.length === 0) {
    return doc
  }

  // User mit der entsprechenden YouTube-Rolle finden
  const getUserByYouTubeRole = async (role: string): Promise<number | null> => {
    // Mapping: Task-Rolle -> YouTube-Rolle
    const roleMapping: Record<string, string[]> = {
      creator: ['creator', 'manager'],
      producer: ['producer', 'manager'],
      editor: ['editor', 'manager'],
      manager: ['manager'],
    }

    const allowedRoles = roleMapping[role] || ['manager']

    // Wenn Video einem User zugewiesen ist, prüfe dessen Rolle
    if (doc.assignedTo) {
      const assignedUser = await req.payload.findByID({
        collection: 'users',
        id: typeof doc.assignedTo === 'object' ? doc.assignedTo.id : doc.assignedTo,
        depth: 0,
      }) as UserWithYouTubeRole | null

      if (assignedUser?.youtubeRole && allowedRoles.includes(assignedUser.youtubeRole)) {
        return assignedUser.id
      }
    }

    // Sonst: Ersten User mit passender Rolle finden
    const users = await req.payload.find({
      collection: 'users',
      where: {
        youtubeRole: { in: allowedRoles },
      },
      limit: 1,
      depth: 0,
    })

    return users.docs[0]?.id || null
  }

  // Tasks erstellen
  for (const taskTemplate of tasks) {
    const assignedUserId = await getUserByYouTubeRole(taskTemplate.assignRole)

    if (assignedUserId) {
      const channelId = typeof doc.channel === 'object' ? doc.channel.id : doc.channel

      await req.payload.create({
        collection: 'yt-tasks',
        data: {
          title: `${taskTemplate.title}: ${doc.title}`,
          video: doc.id,
          channel: channelId,
          taskType: taskTemplate.type,
          status: 'todo',
          priority: doc.priority || 'normal',
          assignedTo: assignedUserId,
          dueDate: doc.scheduledPublishDate
            ? new Date(new Date(doc.scheduledPublishDate).getTime() - 2 * 24 * 60 * 60 * 1000).toISOString()
            : undefined,
        },
      })
    }
  }

  return doc
}
```

### 3.2 Benachrichtigungs-Hook

```typescript
// src/hooks/ytTasks/notifyOnAssignment.ts
import type { CollectionAfterChangeHook } from 'payload'

export const notifyOnAssignment: CollectionAfterChangeHook = async ({
  doc,
  previousDoc,
  req,
  operation,
}) => {
  // Neue Zuweisung oder geänderte Zuweisung
  const previousAssignedTo = previousDoc?.assignedTo
    ? (typeof previousDoc.assignedTo === 'object' ? previousDoc.assignedTo.id : previousDoc.assignedTo)
    : null
  const currentAssignedTo = doc.assignedTo
    ? (typeof doc.assignedTo === 'object' ? doc.assignedTo.id : doc.assignedTo)
    : null

  const isNewAssignment = operation === 'create' || previousAssignedTo !== currentAssignedTo

  if (isNewAssignment && currentAssignedTo) {
    await req.payload.create({
      collection: 'yt-notifications',
      data: {
        recipient: currentAssignedTo,
        type: 'task_assigned',
        title: `Neue Aufgabe: ${doc.title}`,
        message: doc.dueDate
          ? `Fällig am ${new Date(doc.dueDate).toLocaleDateString('de-DE')}`
          : 'Keine Deadline gesetzt',
        link: `/admin/collections/yt-tasks/${doc.id}`,
        relatedTask: doc.id,
        relatedVideo: doc.video
          ? (typeof doc.video === 'object' ? doc.video.id : doc.video)
          : undefined,
      },
    })
  }

  return doc
}
```

---

## 4. API Routes (Next.js App Router)

### 4.1 Dashboard Overview

```typescript
// src/app/(payload)/api/youtube/dashboard/route.ts
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextRequest, NextResponse } from 'next/server'

interface UserWithYouTubeRole {
  id: number
  isSuperAdmin?: boolean
  youtubeRole?: string
}

export async function GET(req: NextRequest) {
  try {
    const payload = await getPayload({ config })
    const { user } = await payload.auth({ headers: req.headers })

    if (!user) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    const typedUser = user as UserWithYouTubeRole

    // Kanäle abrufen
    const channels = await payload.find({
      collection: 'youtube-channels',
      where: { status: { equals: 'active' } },
      depth: 0,
    })

    // Pipeline-Status zählen
    const [idea, scriptDraft, review, production, editing, ready, published] = await Promise.all([
      payload.count({ collection: 'youtube-content', where: { status: { equals: 'idea' } } }),
      payload.count({ collection: 'youtube-content', where: { status: { equals: 'script_draft' } } }),
      payload.count({ collection: 'youtube-content', where: { status: { in: ['script_review', 'final_review'] } } }),
      payload.count({ collection: 'youtube-content', where: { status: { in: ['shoot_scheduled', 'shot'] } } }),
      payload.count({ collection: 'youtube-content', where: { status: { in: ['rough_cut', 'fine_cut'] } } }),
      payload.count({ collection: 'youtube-content', where: { status: { equals: 'approved' } } }),
      payload.count({ collection: 'youtube-content', where: { status: { equals: 'published' } } }),
    ])

    // Ausstehende Freigaben für Manager
    const pendingApprovals = typedUser.youtubeRole === 'manager' || typedUser.isSuperAdmin
      ? await payload.find({
          collection: 'youtube-content',
          where: {
            or: [
              { status: { equals: 'script_review' } },
              { status: { equals: 'final_review' } },
            ],
          },
          limit: 10,
          depth: 1,
        })
      : { docs: [] }

    // Überfällige Tasks
    const overdueTasks = await payload.find({
      collection: 'yt-tasks',
      where: {
        and: [
          { status: { not_in: ['done', 'cancelled'] } },
          { dueDate: { less_than: new Date().toISOString() } },
        ],
      },
      depth: 1,
    })

    // Videos diese Woche geplant
    const weekStart = new Date()
    weekStart.setHours(0, 0, 0, 0)
    const weekEnd = new Date(weekStart)
    weekEnd.setDate(weekEnd.getDate() + 7)

    const thisWeekVideos = await payload.find({
      collection: 'youtube-content',
      where: {
        scheduledPublishDate: {
          greater_than_equal: weekStart.toISOString(),
          less_than: weekEnd.toISOString(),
        },
      },
      sort: 'scheduledPublishDate',
      depth: 1,
    })

    // Offene Tasks zählen
    const tasksTotal = await payload.count({
      collection: 'yt-tasks',
      where: { status: { not_equals: 'done' } },
    })

    return NextResponse.json({
      channels: channels.docs,
      pipeline: {
        idea: idea.totalDocs,
        script: scriptDraft.totalDocs,
        review: review.totalDocs,
        production: production.totalDocs,
        editing: editing.totalDocs,
        ready: ready.totalDocs,
        published: published.totalDocs,
      },
      pendingApprovals: pendingApprovals.docs,
      overdueTasks: overdueTasks.docs,
      thisWeekVideos: thisWeekVideos.docs,
      tasksTotal: tasksTotal.totalDocs,
    })
  } catch (error) {
    console.error('[YouTube Dashboard] Error:', error)
    return NextResponse.json(
      { error: error instanceof Error ? error.message : 'Unknown error' },
      { status: 500 }
    )
  }
}
```

### 4.2 My Tasks

```typescript
// src/app/(payload)/api/youtube/my-tasks/route.ts
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextRequest, NextResponse } from 'next/server'

export async function GET(req: NextRequest) {
  try {
    const payload = await getPayload({ config })
    const { user } = await payload.auth({ headers: req.headers })

    if (!user) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    const tasks = await payload.find({
      collection: 'yt-tasks',
      where: {
        and: [
          { assignedTo: { equals: user.id } },
          { status: { not_in: ['done', 'cancelled'] } },
        ],
      },
      sort: 'dueDate',
      depth: 2, // Video und Channel laden
    })

    return NextResponse.json({
      tasks: tasks.docs,
      total: tasks.totalDocs,
    })
  } catch (error) {
    console.error('[YouTube My-Tasks] Error:', error)
    return NextResponse.json(
      { error: error instanceof Error ? error.message : 'Unknown error' },
      { status: 500 }
    )
  }
}
```

### 4.3 Complete Task

```typescript
// src/app/(payload)/api/youtube/complete-task/[id]/route.ts
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextRequest, NextResponse } from 'next/server'

interface RouteParams {
  params: Promise<{ id: string }>
}

export async function POST(req: NextRequest, { params }: RouteParams) {
  try {
    const { id: taskId } = await params
    const payload = await getPayload({ config })
    const { user } = await payload.auth({ headers: req.headers })

    if (!user || !taskId) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    const task = await payload.findByID({
      collection: 'yt-tasks',
      id: taskId,
      depth: 0,
    })

    if (!task) {
      return NextResponse.json({ error: 'Task not found' }, { status: 404 })
    }

    // Prüfen ob User berechtigt ist
    const assignedId = typeof task.assignedTo === 'object'
      ? task.assignedTo.id
      : task.assignedTo

    if (!user.isSuperAdmin && assignedId !== user.id) {
      return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
    }

    const updated = await payload.update({
      collection: 'yt-tasks',
      id: taskId,
      data: {
        status: 'done',
        completedAt: new Date().toISOString(),
        completedBy: user.id,
      },
    })

    return NextResponse.json({ success: true, task: updated })
  } catch (error) {
    console.error('[YouTube Complete-Task] Error:', error)
    return NextResponse.json(
      { error: error instanceof Error ? error.message : 'Unknown error' },
      { status: 500 }
    )
  }
}
```

---

## 5. Payload Config Integration

```typescript
// In src/payload.config.ts hinzufügen:

// Imports
import { YouTubeChannels } from './collections/YouTubeChannels'
import { YouTubeContent } from './collections/YouTubeContent'
import { YtTasks } from './collections/YtTasks'
import { YtNotifications } from './collections/YtNotifications'

// In collections Array:
collections: [
  // ... bestehende Collections

  // YouTube Operations Hub
  YouTubeChannels,
  YouTubeContent,
  YtTasks,
  YtNotifications,
],

// In multiTenantPlugin collections:
multiTenantPlugin({
  collections: {
    // ... bestehende Collections

    // YouTube Operations (NICHT tenant-scoped, da internes Tool)
    // Falls doch tenant-scoped gewünscht:
    // 'youtube-channels': {},
    // 'youtube-content': {},
    // 'yt-tasks': {},
    // 'yt-notifications': {},
  },
}),
```

---

## 6. Migration erstellen

Nach dem Erstellen der Collections:

```bash
cd /home/payload/payload-cms

# Migration erstellen
pnpm payload migrate:create

# Migration manuell erweitern (KRITISCH!):
```

Die generierte Migration unter `src/migrations/` muss erweitert werden:

```typescript
// In der up() Funktion NACH den CREATE TABLE Statements:

// System-Tabelle für Document Locking erweitern
await payload.db.drizzle.execute(sql`
  ALTER TABLE "payload_locked_documents_rels"
    ADD COLUMN IF NOT EXISTS "youtube_channels_id" integer
    REFERENCES youtube_channels(id) ON DELETE CASCADE;

  ALTER TABLE "payload_locked_documents_rels"
    ADD COLUMN IF NOT EXISTS "youtube_content_id" integer
    REFERENCES youtube_content(id) ON DELETE CASCADE;

  ALTER TABLE "payload_locked_documents_rels"
    ADD COLUMN IF NOT EXISTS "yt_tasks_id" integer
    REFERENCES yt_tasks(id) ON DELETE CASCADE;

  ALTER TABLE "payload_locked_documents_rels"
    ADD COLUMN IF NOT EXISTS "yt_notifications_id" integer
    REFERENCES yt_notifications(id) ON DELETE CASCADE;

  CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_youtube_channels_idx"
    ON "payload_locked_documents_rels" ("youtube_channels_id");

  CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_youtube_content_idx"
    ON "payload_locked_documents_rels" ("youtube_content_id");

  CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_yt_tasks_idx"
    ON "payload_locked_documents_rels" ("yt_tasks_id");

  CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_yt_notifications_idx"
    ON "payload_locked_documents_rels" ("yt_notifications_id");
`);
```

Dann Migration ausführen:

```bash
# Direkte DB-Verbindung für Migrationen (umgeht PgBouncer)
./scripts/db-direct.sh migrate

# Oder standard:
pnpm payload migrate
```

---

## 7. Entwicklungsschritte

### Phase 1: Basis-Setup
1. `src/lib/youtubeAccess.ts` erstellen
2. Users Collection erweitern (youtubeRole, youtubeChannels)
3. YouTubeChannels Collection erstellen
4. YouTubeContent Collection erstellen
5. YtTasks Collection erstellen
6. YtNotifications Collection erstellen
7. `payload.config.ts` aktualisieren
8. Migration erstellen und System-Tabellen erweitern
9. `pnpm payload generate:importmap`
10. `pnpm build`

### Phase 2: Hooks
1. `src/hooks/youtubeContent/createTasksOnStatusChange.ts`
2. `src/hooks/ytTasks/notifyOnAssignment.ts`
3. Hooks in Collections einbinden

### Phase 3: API Routes
1. `src/app/(payload)/api/youtube/dashboard/route.ts`
2. `src/app/(payload)/api/youtube/my-tasks/route.ts`
3. `src/app/(payload)/api/youtube/complete-task/[id]/route.ts`

### Phase 4: Test & Deployment
1. `pm2 restart payload`
2. Admin-UI testen unter https://pl.porwoll.tech/admin
3. API-Endpoints testen
4. Access Control mit verschiedenen Rollen testen

---

## 8. Wichtige Befehle

```bash
# Entwicklung
pnpm dev

# Build
pnpm build

# Migration erstellen
pnpm payload migrate:create

# Migration ausführen (direkt, umgeht PgBouncer)
./scripts/db-direct.sh migrate

# ImportMap generieren (nach Plugin/Collection-Änderungen)
pnpm payload generate:importmap

# PM2 Neustart
pm2 restart payload

# Logs
pm2 logs payload
```

---

## Zusammenfassung

Dieser Prompt beschreibt die Integration eines YouTube Operations Hub mit:

- **4 neue Collections**: YouTubeChannels, YouTubeContent, YtTasks, YtNotifications
- **Keine Konflikte** mit bestehenden Collections (Videos, Series)
- **Korrekte Access Control** via `src/lib/youtubeAccess.ts`
- **Lokalisierung** für alle text-basierten Felder
- **Hooks in separaten Dateien** unter `src/hooks/`
- **API Routes im Next.js App Router Format**
- **Vollständige Migrationsanforderungen** inkl. System-Tabellen
- **Deutsche Admin-Gruppen** ("YouTube")

Die Integration respektiert die bestehende Multi-Tenant-Architektur und alle Konventionen des Payload CMS Projekts.
