diff --git a/src/lib/components/schedule/BlockModal.svelte b/src/lib/components/schedule/BlockModal.svelte index 239e0c0..d2cb4f1 100644 --- a/src/lib/components/schedule/BlockModal.svelte +++ b/src/lib/components/schedule/BlockModal.svelte @@ -8,7 +8,7 @@ hmToMinutes, isOvernight, toHm, - validateBlock, + validateBlockAgainstSchedule, type BlockSibling } from '$lib/schedule-utils'; @@ -25,10 +25,13 @@ open = false, mode = 'create', blockId, + day = null, + licenseKeyId = '', initialStartMinutes = 9 * 60, initialEndMinutes = 10 * 60, initialProclamationIds = [], initialDynamicAudio = true, + scheduleType = 'daily' as 'daily' | 'weekly' | null, categories = [], groups = [], proclamations = [], @@ -42,10 +45,13 @@ open?: boolean; mode?: 'create' | 'edit'; blockId?: string; + day?: string | null; + licenseKeyId?: string; initialStartMinutes?: number; initialEndMinutes?: number; initialProclamationIds?: string[]; initialDynamicAudio?: boolean; + scheduleType?: 'daily' | 'weekly' | null; categories?: BrowserCategory[]; groups?: BrowserGroup[]; proclamations?: BrowserProclamation[]; @@ -105,10 +111,11 @@ function validate(): string | null { if (!startValid || !endValid) return 'Enter a valid start and end time.'; - const result = validateBlock( - { startMinutes, endMinutes, proclamationIds: selectedIds }, - siblings, - { isCreate: mode === 'create' } + if (scheduleType === null) return 'Set a schedule type first.'; + const result = validateBlockAgainstSchedule( + scheduleType, + { day, startMinutes, endMinutes, proclamationIds: selectedIds }, + siblings ); return result.ok ? null : result.error; } @@ -193,6 +200,10 @@ {#if mode === 'edit' && blockId} {/if} + + {#if day} + + {/if} @@ -280,7 +291,7 @@

Review

{formatTime(startMinutes)} – {formatTime(endMinutes)}{overnight - ? ' (next day)' + ? ' (ends next day)' : ''}

{#if selectedTitles.length === 0} diff --git a/src/lib/components/schedule/BlockModal.svelte.spec.ts b/src/lib/components/schedule/BlockModal.svelte.spec.ts index 79fb486..a07ce81 100644 --- a/src/lib/components/schedule/BlockModal.svelte.spec.ts +++ b/src/lib/components/schedule/BlockModal.svelte.spec.ts @@ -100,7 +100,7 @@ describe('BlockModal', () => { it('marks an overnight block in the review', async () => { mount({ initialStartMinutes: 21 * 60, initialEndMinutes: 9 * 60 }); - await expect.element(page.getByText('9:00 PM – 9:00 AM (next day)')).toBeInTheDocument(); + await expect.element(page.getByText('9:00 PM – 9:00 AM (ends next day)')).toBeInTheDocument(); }); it('rejects an overlapping time range with the specific error', async () => { diff --git a/src/lib/components/schedule/DaySchedule.svelte b/src/lib/components/schedule/DaySchedule.svelte index f8e9c1c..144cfcf 100644 --- a/src/lib/components/schedule/DaySchedule.svelte +++ b/src/lib/components/schedule/DaySchedule.svelte @@ -10,6 +10,16 @@ type ScheduleBlockView = { id: string; + day?: string | null; + startMinutes: number; + endMinutes: number; + dynamicAudio: boolean; + proclamations: { id: string; name: string }[]; + }; + + type OpenBlock = { + id: string; + day: string | null; startMinutes: number; endMinutes: number; dynamicAudio: boolean; @@ -18,12 +28,14 @@ let { blocks = [], + label, onCreate, onEdit }: { blocks?: ScheduleBlockView[]; + label?: string; onCreate?: (startMinutes: number, endMinutes: number) => void; - onEdit?: (block: ScheduleBlockView) => void; + onEdit?: (block: OpenBlock) => void; } = $props(); const hourMarks = Array.from({ length: 24 }, (_, i) => i); @@ -97,77 +109,82 @@ ); -
- - - {/each} - {/each} +
- {#if blocks.length === 0} -

- Click or drag to add a block. -

- {/if} + diff --git a/src/lib/schedule-utils.ts b/src/lib/schedule-utils.ts index 8738873..9fa2682 100644 --- a/src/lib/schedule-utils.ts +++ b/src/lib/schedule-utils.ts @@ -177,7 +177,7 @@ export function validateSchedule(type: ScheduleType, blocks: ValidationBlock[]): } const overlap = findScheduleOverlap(type, blocks); if (overlap) { - return { ok: false, error: overlapError(overlap.a, overlap.b) }; + return { ok: false, error: overlapError(overlap.a) }; } return { ok: true }; } @@ -195,12 +195,12 @@ export function validateBlockAgainstSchedule( if (!shape.ok) return shape; const overlap = findScheduleOverlap(type, [...siblings, candidate]); if (overlap) { - return { ok: false, error: overlapError(overlap.a, overlap.b) }; + return { ok: false, error: overlapError(overlap.a) }; } return { ok: true }; } -function overlapError(a: ValidationBlock, b: ValidationBlock): string { +function overlapError(a: ValidationBlock): string { return `This overlaps with your ${formatTime(a.startMinutes)}–${formatTime(a.endMinutes)} block.`; } diff --git a/src/routes/(protected)/(user)/schedule/+page.svelte b/src/routes/(protected)/(user)/schedule/+page.svelte index 0ecc0df..f9981e2 100644 --- a/src/routes/(protected)/(user)/schedule/+page.svelte +++ b/src/routes/(protected)/(user)/schedule/+page.svelte @@ -1,23 +1,109 @@ +{#if typeForm === undefined}{/if} + +
@@ -60,14 +153,23 @@ Schedule

- Build your daily listening schedule. Up to 4 blocks per day. + Build a listening schedule for a system. Up to 4 blocks per day.

- - {data.blocks.length} of 4 blocks - + {#if data.licenses.length > 0} + + {/if}
{#if successMessage} @@ -77,24 +179,154 @@ {successMessage}
{/if} + {#if errorMessage} + + {/if} -
- -
+ {#if !selectedLicense} +
+

+ You don't have any systems yet. A system appears here once a Kiosk validates its License + Key. +

+
+ {:else} + +
+
+ System +

+ {licenseLabel} + {#if !selectedLicense.systemName} + (unnamed system) + {/if} +

+
+
+ Schedule type +
+ + +
+ {#if type !== null} + + {/if} +
+
- + {#if type === null} +
+

+ No schedule. Choose Daily or Weekly to start. +

+
+
+ Times run in the Kiosk's own timezone ({selectedLicense.systemName ?? selectedLicense.key} + sets its own timezone). They are not tied to your browser's timezone. +
+ {:else if type === 'daily'} +
+ openCreate(null, s, e)} + onEdit={openEdit} + /> +
+ {:else} +
+ {#each WEEK_DAYS as day (day.key)} + openCreate(day.key, s, e)} + onEdit={openEdit} + /> + {/each} +
+ {/if} + +

+ Gaps between blocks are fine: the Kiosk plays its locally selected Proclamations during a gap. + Times run in the Kiosk's own timezone ({selectedLicense.systemName ?? selectedLicense.key}), + not your browser's. +

+ + + {/if} + +{#if showTypeConfirm} +
+
+

Change schedule type?

+

+ Switching type replaces the current blocks. + {#if typePending === 'weekly'} + The Daily blocks will be copied to all seven days. + {:else} + The existing blocks will be cleared. + {/if} +

+
+ + +
+
+
+{/if}