diff --git a/src/lib/components/schedule/BlockModal.svelte b/src/lib/components/schedule/BlockModal.svelte index 305e61a..ea2c5e7 100644 --- a/src/lib/components/schedule/BlockModal.svelte +++ b/src/lib/components/schedule/BlockModal.svelte @@ -56,12 +56,15 @@ let step = $state<'details' | 'browse'>('details'); let startHour = $state('9'); let startMinute = $state('0'); + let startPeriod = $state<'AM' | 'PM'>('AM'); let endHour = $state('10'); let endMinute = $state('0'); + let endPeriod = $state<'AM' | 'PM'>('AM'); let selectedIds = $state([]); let error = $state(''); - const hourOptions = Array.from({ length: 24 }, (_, i) => String(i)); + const hourOptions = Array.from({ length: 12 }, (_, i) => String(i + 1)); + const periodOptions: ('AM' | 'PM')[] = ['AM', 'PM']; function minuteOptions(current: string): string[] { const base = Array.from({ length: 60 / MINUTE_STEP }, (_, i) => String(i * MINUTE_STEP)); @@ -69,20 +72,38 @@ return [...base, current].sort((a, b) => Number(a) - Number(b)); } + function to12Hour(totalMinutes: number): { hour: number; minute: number; period: 'AM' | 'PM' } { + const hours24 = Math.floor(totalMinutes / 60) % 24; + return { + hour: hours24 % 12 === 0 ? 12 : hours24 % 12, + minute: totalMinutes % 60, + period: hours24 < 12 ? 'AM' : 'PM' + }; + } + + function to24Hour(hour12: string, minute: string, period: 'AM' | 'PM'): number { + const hours = (Number(hour12) % 12) + (period === 'PM' ? 12 : 0); + return (hours * 60 + Number(minute)) | 0; + } + // Re-seed the fields whenever the modal is (re)opened. $effect(() => { if (!open) return; - startHour = String(Math.floor(initialStartMinutes / 60)); - startMinute = String(initialStartMinutes % 60); - endHour = String(Math.floor(initialEndMinutes / 60)); - endMinute = String(initialEndMinutes % 60); + const start = to12Hour(initialStartMinutes); + startHour = String(start.hour); + startMinute = String(start.minute); + startPeriod = start.period; + const end = to12Hour(initialEndMinutes); + endHour = String(end.hour); + endMinute = String(end.minute); + endPeriod = end.period; selectedIds = [...initialProclamationIds]; error = ''; step = 'details'; }); - const startMinutes = $derived((Number(startHour) * 60 + Number(startMinute)) | 0); - const endMinutes = $derived((Number(endHour) * 60 + Number(endMinute)) | 0); + const startMinutes = $derived(to24Hour(startHour, startMinute, startPeriod)); + const endMinutes = $derived(to24Hour(endHour, endMinute, endPeriod)); const overnight = $derived( startMinutes !== endMinutes && isOvernight({ startMinutes, endMinutes }) ); @@ -213,9 +234,7 @@ class="rounded-md border border-slate-300 bg-white px-2 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100" > {#each hourOptions as hour (hour)} - + {/each} : @@ -230,6 +249,15 @@ {/each} + @@ -244,9 +272,7 @@ class="rounded-md border border-slate-300 bg-white px-2 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100" > {#each hourOptions as hour (hour)} - + {/each} : @@ -261,6 +287,15 @@ {/each} + diff --git a/src/lib/components/schedule/BlockModal.svelte.spec.ts b/src/lib/components/schedule/BlockModal.svelte.spec.ts index 30ef6a0..60a411b 100644 --- a/src/lib/components/schedule/BlockModal.svelte.spec.ts +++ b/src/lib/components/schedule/BlockModal.svelte.spec.ts @@ -67,6 +67,32 @@ describe('BlockModal', () => { await expect.element(page.getByRole('combobox', { name: /^End minute$/ })).toHaveValue('15'); }); + it('uses a 12-hour clock with an AM/PM select for each time', async () => { + mount({ initialStartMinutes: 21 * 60, initialEndMinutes: 0 }); + + await expect.element(page.getByRole('combobox', { name: /^Start hour$/ })).toHaveValue('9'); + await expect.element(page.getByRole('combobox', { name: /^Start period$/ })).toHaveValue('PM'); + await expect.element(page.getByRole('combobox', { name: /^End hour$/ })).toHaveValue('12'); + await expect.element(page.getByRole('combobox', { name: /^End period$/ })).toHaveValue('AM'); + + // No 24-hour option exists any more. + await expect + .element(page.getByRole('combobox', { name: /^Start hour$/ })) + .not.toHaveValue('21'); + }); + + it('recomputes the time when the AM/PM switch changes', async () => { + mount({ initialStartMinutes: 9 * 60, initialEndMinutes: 10 * 60 }); + + const period = (await page + .getByRole('combobox', { name: /^End period$/ }) + .element()) as HTMLSelectElement; + period.value = 'PM'; + period.dispatchEvent(new Event('change', { bubbles: true })); + + await expect.element(page.getByText('9:00 AM – 10:00 PM')).toBeInTheDocument(); + }); + it('shows the review times and selected proclamation titles', async () => { mount();