import { NextRequest, NextResponse } from 'next/server'; import { PrismaClient } from '@prisma/client'; import { requireStaffAuth, StaffContext } from '@/lib/auth'; const prisma = new PrismaClient(); async function getCuratorAssignments(curatorId: number) { const [genres, specials] = await Promise.all([ prisma.curatorGenre.findMany({ where: { curatorId }, select: { genreId: true }, }), prisma.curatorSpecial.findMany({ where: { curatorId }, select: { specialId: true }, }), ]); return { genreIds: new Set(genres.map(g => g.genreId)), specialIds: new Set(specials.map(s => s.specialId)), }; } function curatorCanEditSong(context: StaffContext, song: any, assignments: { genreIds: Set; specialIds: Set }) { if (context.role === 'admin') return true; const songGenreIds = (song.genres || []).map((g: any) => g.id); const songSpecialIds = (song.specials || []) .map((s: any) => { if (s?.specialId != null) return s.specialId; if (s?.special?.id != null) return s.special.id; if (s?.id != null && s?.specialId == null && s?.special == null) return s.id; return undefined; }) .filter((id: any): id is number => typeof id === 'number'); if (songGenreIds.length === 0 && songSpecialIds.length === 0) { return true; } const hasGenre = songGenreIds.some((id: number) => assignments.genreIds.has(id)); const hasSpecial = songSpecialIds.some((id: number) => assignments.specialIds.has(id)); return hasGenre || hasSpecial; } export async function POST(request: Request) { const { error, context } = await requireStaffAuth(request as unknown as NextRequest); if (error || !context) return error!; try { const { songIds, genreToggleIds, specialToggleIds, artist, excludeFromGlobal } = await request.json(); if (!songIds || !Array.isArray(songIds) || songIds.length === 0) { return NextResponse.json({ error: 'Missing or invalid songIds array' }, { status: 400 }); } // Validate that at least one operation is requested const hasGenreToggle = genreToggleIds && Array.isArray(genreToggleIds) && genreToggleIds.length > 0; const hasSpecialToggle = specialToggleIds && Array.isArray(specialToggleIds) && specialToggleIds.length > 0; const hasArtistChange = artist !== undefined && artist !== null && artist.trim() !== ''; const hasExcludeGlobalChange = excludeFromGlobal !== undefined; if (!hasGenreToggle && !hasSpecialToggle && !hasArtistChange && !hasExcludeGlobalChange) { return NextResponse.json({ error: 'No update operations specified' }, { status: 400 }); } // Validate artist if provided if (hasArtistChange && artist.trim() === '') { return NextResponse.json({ error: 'Artist cannot be empty' }, { status: 400 }); } // Validate excludeFromGlobal permission if (hasExcludeGlobalChange && context.role === 'curator' && !context.curator.isGlobalCurator) { return NextResponse.json( { error: 'Forbidden: Only global curators or admins can change global playlist flag' }, { status: 403 } ); } let assignments: { genreIds: Set; specialIds: Set } | null = null; if (context.role === 'curator') { assignments = await getCuratorAssignments(context.curator.id); // Validate genre/special toggles are within curator's assignments if (hasGenreToggle) { const invalidGenre = genreToggleIds.some((id: number) => !assignments.genreIds.has(id)); if (invalidGenre) { return NextResponse.json( { error: 'Curators may only toggle their own genres' }, { status: 403 } ); } } if (hasSpecialToggle) { const invalidSpecial = specialToggleIds.some((id: number) => !assignments.specialIds.has(id)); if (invalidSpecial) { return NextResponse.json( { error: 'Curators may only toggle their own specials' }, { status: 403 } ); } } } // Load all songs with relations for permission checks const songs = await prisma.song.findMany({ where: { id: { in: songIds.map((id: any) => Number(id)) } }, include: { genres: true, specials: { include: { special: true } }, }, }); if (songs.length === 0) { return NextResponse.json({ error: 'No songs found' }, { status: 404 }); } // Filter songs that can be edited const editableSongs = context.role === 'admin' ? songs : songs.filter(song => curatorCanEditSong(context, song, assignments!)); if (editableSongs.length === 0) { return NextResponse.json( { error: 'No songs can be edited with current permissions' }, { status: 403 } ); } const results = { total: songIds.length, processed: editableSongs.length, skipped: songs.length - editableSongs.length, success: 0, errors: [] as Array<{ songId: number; error: string }>, }; // Process each song in a transaction for (const song of editableSongs) { try { await prisma.$transaction(async (tx) => { const updateData: any = {}; // Handle artist change if (hasArtistChange) { updateData.artist = artist.trim(); } // Handle excludeFromGlobal change if (hasExcludeGlobalChange) { updateData.excludeFromGlobal = excludeFromGlobal; } // Handle genre toggles if (hasGenreToggle) { const currentGenreIds = song.genres.map(g => g.id); const genreIdsToToggle = genreToggleIds as number[]; // Determine which genres to add/remove const genresToAdd = genreIdsToToggle.filter(id => !currentGenreIds.includes(id)); const genresToRemove = genreIdsToToggle.filter(id => currentGenreIds.includes(id)); // For curators, preserve genres they can't manage let finalGenreIds: number[]; if (context.role === 'curator') { const fixedGenreIds = currentGenreIds.filter(gid => !assignments!.genreIds.has(gid)); const managedGenreIds = currentGenreIds .filter(gid => assignments!.genreIds.has(gid) && !genresToRemove.includes(gid)) .concat(genresToAdd); finalGenreIds = Array.from(new Set([...fixedGenreIds, ...managedGenreIds])); } else { const newGenreIds = currentGenreIds .filter(id => !genresToRemove.includes(id)) .concat(genresToAdd); finalGenreIds = Array.from(new Set(newGenreIds)); } updateData.genres = { set: finalGenreIds.map(gId => ({ id: gId })) }; } // Update song basic data if (Object.keys(updateData).length > 0) { await tx.song.update({ where: { id: song.id }, data: updateData, }); } // Handle special toggles if (hasSpecialToggle) { const currentSpecials = await tx.specialSong.findMany({ where: { songId: song.id } }); const currentSpecialIds = currentSpecials.map(ss => ss.specialId); const specialIdsToToggle = specialToggleIds as number[]; // Determine which specials to add/remove const specialsToAdd = specialIdsToToggle.filter(id => !currentSpecialIds.includes(id)); const specialsToRemove = specialIdsToToggle.filter(id => currentSpecialIds.includes(id)); // For curators, preserve specials they can't manage let finalSpecialIds: number[]; if (context.role === 'curator') { const fixedSpecialIds = currentSpecialIds.filter(sid => !assignments!.specialIds.has(sid)); const managedSpecialIds = currentSpecialIds .filter(sid => assignments!.specialIds.has(sid) && !specialsToRemove.includes(sid)) .concat(specialsToAdd); finalSpecialIds = Array.from(new Set([...fixedSpecialIds, ...managedSpecialIds])); } else { const newSpecialIds = currentSpecialIds .filter(id => !specialsToRemove.includes(id)) .concat(specialsToAdd); finalSpecialIds = Array.from(new Set(newSpecialIds)); } // Delete removed specials const toDelete = currentSpecialIds.filter(sid => !finalSpecialIds.includes(sid)); if (toDelete.length > 0) { await tx.specialSong.deleteMany({ where: { songId: song.id, specialId: { in: toDelete } } }); } // Add new specials const toAdd = finalSpecialIds.filter(sid => !currentSpecialIds.includes(sid)); if (toAdd.length > 0) { await tx.specialSong.createMany({ data: toAdd.map(specialId => ({ songId: song.id, specialId, startTime: 0 })) }); } } }); results.success++; } catch (error: any) { results.errors.push({ songId: song.id, error: error.message || 'Unknown error' }); } } return NextResponse.json(results); } catch (error) { console.error('Error in batch update:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } }