cms.c2sgmbh/src/collections/YtTasks.ts
Martin Porwoll 3294fbb506 feat(YouTube): add YouTube Operations Hub with YtSeries collection
Complete YouTube content management system:
- YouTubeChannels: Channel management with branding and metrics
- YouTubeContent: Video pipeline with workflow, approvals, scheduling
- YtSeries: Dedicated series management per channel (NEW)
- YtBatches: Production batch tracking with targets and progress
- YtTasks: Task management with notifications
- YtNotifications: User notification system
- YtMonthlyGoals: Monthly production goals per channel
- YtScriptTemplates: Reusable script templates
- YtChecklistTemplates: Checklist templates for workflows

Features:
- Role-based access (YouTubeManager, YouTubeCreator, YouTubeViewer)
- Auto-task generation on status changes
- Series relationship with channel-based filtering
- API endpoints for dashboard, tasks, and task completion
- German/English localization support

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 14:54:40 +00:00

219 lines
5.8 KiB
TypeScript

// src/collections/YtTasks.ts
import type { CollectionConfig } from 'payload'
import { isYouTubeManager, canAccessAssignedContent } from '../lib/youtubeAccess'
import { notifyOnAssignment } from '../hooks/ytTasks/notifyOnAssignment'
/**
* YtTasks Collection
*
* Aufgabenverwaltung für die Video-Produktion.
* Tasks werden automatisch bei Statuswechsel erstellt.
* Teil des YouTube Operations Hub.
*/
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: {
afterChange: [notifyOnAssignment],
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) {
try {
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
}
} catch (error) {
console.error('[YtTasks] Error fetching video for channel:', error)
}
}
return data
},
],
},
}