feat(youtube): add team capacity planning API

Adds a capacity calculator utility and API endpoint that computes
workload utilization for team members with YouTube roles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2026-02-14 13:49:55 +00:00
parent 2ea42ca404
commit d8118528db
3 changed files with 282 additions and 0 deletions

View file

@ -0,0 +1,95 @@
// src/app/(payload)/api/youtube/capacity/route.ts
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextRequest, NextResponse } from 'next/server'
import { calculateCapacity, type CapacityInput } from '@/lib/youtube/capacity-calculator'
interface UserWithYouTubeRole {
id: number
email?: string
isSuperAdmin?: boolean
youtubeRole?: string
}
const DEFAULT_HOURS_PER_TASK = 2
const DEFAULT_AVAILABLE_HOURS_PER_WEEK = 40
/**
* GET /api/youtube/capacity
*
* Returns team capacity data for all users with a YouTube role.
* Calculates utilization based on active tasks and estimated hours.
*/
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 })
}
// Find all users with a YouTube role
const users = await payload.find({
collection: 'users',
where: {
youtubeRole: { exists: true },
},
limit: 50,
depth: 0,
})
const capacities = await Promise.all(
users.docs.map(async (u) => {
const typedUser = u as unknown as UserWithYouTubeRole
// Count active tasks
const tasks = await payload.count({
collection: 'yt-tasks',
where: {
assignedTo: { equals: typedUser.id },
status: { in: ['todo', 'in_progress'] },
},
})
// Count videos in pipeline (exclude terminal and initial states)
const videos = await payload.count({
collection: 'youtube-content',
where: {
assignedTo: { equals: typedUser.id },
status: { not_in: ['published', 'tracked', 'discarded', 'idea'] },
},
})
const input: CapacityInput = {
userId: typedUser.id,
userName: typedUser.email ?? `User ${typedUser.id}`,
activeTasks: tasks.totalDocs,
estimatedHours: tasks.totalDocs * DEFAULT_HOURS_PER_TASK,
videosInPipeline: videos.totalDocs,
availableHoursPerWeek: DEFAULT_AVAILABLE_HOURS_PER_WEEK,
}
return calculateCapacity(input)
}),
)
return NextResponse.json({
success: true,
team: capacities,
summary: {
totalMembers: capacities.length,
overloaded: capacities.filter((c) => c.status === 'red').length,
atCapacity: capacities.filter((c) => c.status === 'yellow').length,
available: capacities.filter((c) => c.status === 'green').length,
},
})
} catch (error) {
console.error('[Capacity API] Error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 },
)
}
}

View file

@ -0,0 +1,44 @@
/**
* Team Capacity Calculator
*
* Pure utility for calculating team member workload utilization
* and capacity status used by the capacity API route.
*/
export interface CapacityInput {
userId: number
userName: string
activeTasks: number
estimatedHours: number
videosInPipeline: number
availableHoursPerWeek: number
}
export interface CapacityResult {
userId: number
userName: string
activeTasks: number
estimatedHours: number
videosInPipeline: number
availableHoursPerWeek: number
utilization: number // 0-100+
status: 'green' | 'yellow' | 'red'
}
function calculateCapacity(input: CapacityInput): CapacityResult {
const utilization =
input.availableHoursPerWeek > 0
? Math.round((input.estimatedHours / input.availableHoursPerWeek) * 100)
: 0
let status: CapacityResult['status'] = 'green'
if (utilization > 90) {
status = 'red'
} else if (utilization > 70) {
status = 'yellow'
}
return { ...input, utilization, status }
}
export { calculateCapacity }

View file

@ -0,0 +1,143 @@
/**
* Team Capacity Calculator Unit Tests
*
* Tests utilization calculation and status thresholds
* used by the capacity API route.
*/
import { describe, it, expect } from 'vitest'
import { calculateCapacity, type CapacityInput } from '@/lib/youtube/capacity-calculator'
describe('Capacity Calculator', () => {
it('should calculate utilization percentage', () => {
const input: CapacityInput = {
userId: 1,
userName: 'Max',
activeTasks: 5,
estimatedHours: 20,
videosInPipeline: 3,
availableHoursPerWeek: 40,
}
const result = calculateCapacity(input)
expect(result.utilization).toBe(50)
expect(result.status).toBe('green')
})
it('should flag overloaded team members as red', () => {
const input: CapacityInput = {
userId: 2,
userName: 'Anna',
activeTasks: 10,
estimatedHours: 38,
videosInPipeline: 8,
availableHoursPerWeek: 40,
}
const result = calculateCapacity(input)
expect(result.utilization).toBe(95)
expect(result.status).toBe('red')
})
it('should flag busy team members as yellow', () => {
const input: CapacityInput = {
userId: 3,
userName: 'Lisa',
activeTasks: 8,
estimatedHours: 32,
videosInPipeline: 5,
availableHoursPerWeek: 40,
}
const result = calculateCapacity(input)
expect(result.utilization).toBe(80)
expect(result.status).toBe('yellow')
})
it('should handle zero available hours', () => {
const input: CapacityInput = {
userId: 4,
userName: 'Tom',
activeTasks: 3,
estimatedHours: 10,
videosInPipeline: 2,
availableHoursPerWeek: 0,
}
const result = calculateCapacity(input)
expect(result.utilization).toBe(0)
expect(result.status).toBe('green')
})
it('should preserve all input fields in the result', () => {
const input: CapacityInput = {
userId: 5,
userName: 'Sara',
activeTasks: 4,
estimatedHours: 16,
videosInPipeline: 2,
availableHoursPerWeek: 40,
}
const result = calculateCapacity(input)
expect(result.userId).toBe(5)
expect(result.userName).toBe('Sara')
expect(result.activeTasks).toBe(4)
expect(result.estimatedHours).toBe(16)
expect(result.videosInPipeline).toBe(2)
expect(result.availableHoursPerWeek).toBe(40)
})
it('should return red for utilization over 100%', () => {
const input: CapacityInput = {
userId: 6,
userName: 'Overloaded',
activeTasks: 30,
estimatedHours: 60,
videosInPipeline: 10,
availableHoursPerWeek: 40,
}
const result = calculateCapacity(input)
expect(result.utilization).toBe(150)
expect(result.status).toBe('red')
})
it('should return green at exactly 70% utilization', () => {
const input: CapacityInput = {
userId: 7,
userName: 'Boundary70',
activeTasks: 7,
estimatedHours: 28,
videosInPipeline: 3,
availableHoursPerWeek: 40,
}
const result = calculateCapacity(input)
expect(result.utilization).toBe(70)
expect(result.status).toBe('green')
})
it('should return yellow at exactly 90% utilization', () => {
const input: CapacityInput = {
userId: 8,
userName: 'Boundary90',
activeTasks: 9,
estimatedHours: 36,
videosInPipeline: 4,
availableHoursPerWeek: 40,
}
const result = calculateCapacity(input)
expect(result.utilization).toBe(90)
expect(result.status).toBe('yellow')
})
})