Portal: 12-hour schedule times and dynamicAudio default on

Use TimeInput for Block start/end and show the day timeline axis with
AM/PM, so no 24-hour time is displayed in the schedule UI. New Blocks now
default dynamicAudio to true in the modal and server-side when the field
is absent; existing stored Blocks are untouched.
This commit is contained in:
NeilClack
2026-09-29 08:53:22 -05:00
parent 35558b0652
commit 179df1e252
6 changed files with 113 additions and 153 deletions
+28 -2
View File
@@ -90,7 +90,7 @@ describe('createBlock', () => {
scheduleId: 's1',
startMinutes: validInput.startMinutes,
endMinutes: validInput.endMinutes,
dynamicAudio: false,
dynamicAudio: true,
proclamations: {
create: [
{ proclamationId: 'p1', sortOrder: 0 },
@@ -101,6 +101,32 @@ describe('createBlock', () => {
});
});
it('defaults dynamic audio on when the field is absent', async () => {
blockFindMany.mockResolvedValue([]);
blockCreate.mockResolvedValue({ id: 'new-block' });
await createBlock('u1', validInput);
expect(blockCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ dynamicAudio: true })
})
);
});
it('persists dynamic audio off when the field is explicitly false', async () => {
blockFindMany.mockResolvedValue([]);
blockCreate.mockResolvedValue({ id: 'new-block' });
await createBlock('u1', { ...validInput, dynamicAudio: false });
expect(blockCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ dynamicAudio: false })
})
);
});
it('persists the dynamic audio flag', async () => {
blockFindMany.mockResolvedValue([]);
blockCreate.mockResolvedValue({ id: 'new-block' });
@@ -168,7 +194,7 @@ describe('updateBlock', () => {
data: {
startMinutes: validInput.startMinutes,
endMinutes: validInput.endMinutes,
dynamicAudio: false,
dynamicAudio: true,
proclamations: {
deleteMany: {},
create: [{ proclamationId: 'p1', sortOrder: 0 }]
+10 -2
View File
@@ -25,6 +25,14 @@ function uniqueIds(ids: string[]): string[] {
return [...new Set(ids.filter((id) => typeof id === 'string' && id.length > 0))];
}
/**
* New Blocks default `dynamicAudio` to true when the field is absent. Existing
* stored Blocks are never rewritten by this default.
*/
function dynamicAudioOrDefault(input: BlockInput): boolean {
return input.dynamicAudio !== false;
}
/** 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 } });
@@ -67,7 +75,7 @@ export async function createBlock(userId: string, input: BlockInput): Promise<Bl
scheduleId: schedule.id,
startMinutes: input.startMinutes,
endMinutes: input.endMinutes,
dynamicAudio: input.dynamicAudio === true,
dynamicAudio: dynamicAudioOrDefault(input),
proclamations: {
create: proclamationIds.map((proclamationId, index) => ({
proclamationId,
@@ -105,7 +113,7 @@ export async function updateBlock(
data: {
startMinutes: input.startMinutes,
endMinutes: input.endMinutes,
dynamicAudio: input.dynamicAudio === true,
dynamicAudio: dynamicAudioOrDefault(input),
proclamations: {
deleteMany: {},
create: proclamationIds.map((proclamationId, index) => ({