'use server'; import { revalidatePath } from 'next/cache'; import { requireStaff } from '@/lib/admin/guard'; import { prisma } from '@/lib/prisma'; import { logStaffActivity } from '@/lib/services/staff-activity'; // AutoDJ playlist CRUD (radio_auto_dj_playlist). CMS-owned table backing the // fallback playlist the radio rotates through when no live DJ is streaming. // Faithful to AtomCMS: a flat list of tracks ordered by sort_order then title. // ── Helpers ────────────────────────────────────────────────────────────── /** Parse a FormData field into a positive BigInt id, or null when invalid. */ function parseId(raw: FormDataEntryValue | null): bigint | null { if (typeof raw !== 'string' || raw.trim() === '') return null; try { const id = BigInt(raw.trim()); return id > 0n ? id : null; } catch { return null; } } function str(raw: FormDataEntryValue | null): string { return typeof raw === 'string' ? raw : ''; } /** Checkbox/select truthiness: '1', 'true', 'on' → true. */ function bool(raw: FormDataEntryValue | null): boolean { const v = str(raw).trim().toLowerCase(); return v === '1' || v === 'true' || v === 'on'; } /** Parse a non-negative UnsignedInt, falling back to 0. */ function reqUInt(raw: FormDataEntryValue | null): number { const n = Number(str(raw).trim()); if (!Number.isFinite(n) || n < 0) return 0; return Math.trunc(n); } /** Parse an optional non-negative UnsignedInt; blank/invalid/negative → null. */ function optUInt(raw: FormDataEntryValue | null): number | null { const s = str(raw).trim(); if (s === '') return null; const n = Number(s); if (!Number.isFinite(n) || n < 0) return null; return Math.trunc(n); } // ── AutoDJ playlist CRUD (radio_auto_dj_playlist) ──────────────────────── export async function createTrack(formData: FormData): Promise { const staff = await requireStaff(); const title = str(formData.get('title')).trim().slice(0, 255); if (!title) return; const artist = str(formData.get('artist')).trim().slice(0, 255); const album = str(formData.get('album')).trim().slice(0, 255); const artworkUrl = str(formData.get('artworkUrl')).trim().slice(0, 255); const duration = optUInt(formData.get('duration')); const sortOrder = reqUInt(formData.get('sortOrder')); const isActive = bool(formData.get('isActive')); const now = new Date(); try { const created = await prisma.radioAutoDjPlaylist.create({ data: { title, artist: artist || null, album: album || null, artworkUrl: artworkUrl || null, duration, sortOrder, isActive, createdAt: now, updatedAt: now, }, }); await logStaffActivity({ staffId: staff.id, action: 'radio_autodj_create', description: `Created AutoDJ track "${title}"${artist ? ` by ${artist}` : ''}`, targetType: 'radio_auto_dj_track', targetId: Number(created.id), }); } catch { // Fail soft — DB unavailable; re-render without throwing. } revalidatePath('/admin/radio/autodj'); } export async function toggleTrack(formData: FormData): Promise { const staff = await requireStaff(); const id = parseId(formData.get('id')); if (id === null) return; // The form posts the desired next state so the toggle is idempotent. const isActive = bool(formData.get('isActive')); try { await prisma.radioAutoDjPlaylist.update({ where: { id }, data: { isActive, updatedAt: new Date() }, }); await logStaffActivity({ staffId: staff.id, action: 'radio_autodj_toggle', description: `${isActive ? 'Activated' : 'Deactivated'} AutoDJ track #${id}`, targetType: 'radio_auto_dj_track', targetId: Number(id), }); } catch { // Row may be gone; ignore. } revalidatePath('/admin/radio/autodj'); } export async function deleteTrack(formData: FormData): Promise { const staff = await requireStaff(); const id = parseId(formData.get('id')); if (id === null) return; try { await prisma.radioAutoDjPlaylist.delete({ where: { id } }); await logStaffActivity({ staffId: staff.id, action: 'radio_autodj_delete', description: `Deleted AutoDJ track #${id}`, targetType: 'radio_auto_dj_track', targetId: Number(id), }); } catch { // Already deleted; ignore. } revalidatePath('/admin/radio/autodj'); }