132 lines
3.8 KiB
TypeScript
132 lines
3.8 KiB
TypeScript
// 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<BlockMutationResult> {
|
|
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<BlockMutationResult> {
|
|
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<BlockMutationResult> {
|
|
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 };
|
|
}
|