// Schedule service layer. Owns the database orchestration and enforcement of // the Block business rules (at most 4 per user, at least one Proclamation, // no overnight ranges, no overlaps). Pure validation lives in // `$lib/schedule-utils` so the client modal can reuse it. import { prisma } from '$lib/db'; import { validateBlock, type BlockInput } from '$lib/schedule-utils'; export { MAX_BLOCKS, MINUTES_PER_DAY, SNAP_MINUTES, MINUTE_STEP, formatTime, toHm, snapMinutes, findOverlap, validateBlock } from '$lib/schedule-utils'; export type { BlockInput, BlockSibling, ValidationResult } from '$lib/schedule-utils'; export type BlockMutationResult = { ok: true; id?: string } | { ok: false; error: string }; function uniqueIds(ids: string[]): string[] { return [...new Set(ids.filter((id) => typeof id === 'string' && id.length > 0))]; } /** Ensure the user has a Schedule row, creating the singleton on first use. */ export async function getOrCreateSchedule(userId: string) { const existing = await prisma.schedule.findUnique({ where: { userId } }); if (existing) return existing; return prisma.schedule.create({ data: { userId } }); } /** * The user's Blocks with their selected Proclamations (and frequencies for * canonical-id mapping), ordered by start time. */ export async function listBlocks(userId: string) { return prisma.block.findMany({ where: { schedule: { userId } }, orderBy: { startMinutes: 'asc' }, include: { proclamations: { orderBy: { sortOrder: 'asc' }, include: { proclamation: { include: { solfeggioFrequency: true } } } } } }); } export async function createBlock(userId: string, input: BlockInput): Promise { const schedule = await getOrCreateSchedule(userId); const siblings = await prisma.block.findMany({ where: { scheduleId: schedule.id }, select: { id: true, startMinutes: true, endMinutes: true } }); const validation = validateBlock(input, siblings, { isCreate: true }); if (!validation.ok) return validation; const proclamationIds = uniqueIds(input.proclamationIds); const block = await prisma.block.create({ data: { scheduleId: schedule.id, startMinutes: input.startMinutes, endMinutes: input.endMinutes, dynamicAudio: input.dynamicAudio === true, proclamations: { create: proclamationIds.map((proclamationId, index) => ({ proclamationId, sortOrder: index })) } } }); return { ok: true, id: block.id }; } export async function updateBlock( userId: string, blockId: string, input: BlockInput ): Promise { const block = await prisma.block.findFirst({ where: { id: blockId, schedule: { userId } }, select: { id: true, scheduleId: true } }); if (!block) return { ok: false, error: 'Block not found.' }; const siblings = await prisma.block.findMany({ where: { scheduleId: block.scheduleId, id: { not: blockId } }, select: { id: true, startMinutes: true, endMinutes: true } }); const validation = validateBlock(input, siblings); if (!validation.ok) return validation; const proclamationIds = uniqueIds(input.proclamationIds); await prisma.block.update({ where: { id: blockId }, data: { startMinutes: input.startMinutes, endMinutes: input.endMinutes, dynamicAudio: input.dynamicAudio === true, proclamations: { deleteMany: {}, create: proclamationIds.map((proclamationId, index) => ({ proclamationId, sortOrder: index })) } } }); return { ok: true, id: blockId }; } export async function deleteBlock(userId: string, blockId: string): Promise { const block = await prisma.block.findFirst({ where: { id: blockId, schedule: { userId } }, select: { id: true } }); if (!block) return { ok: false, error: 'Block not found.' }; await prisma.block.delete({ where: { id: blockId } }); return { ok: true, id: blockId }; }