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 hourMarks as hour (hour)}
-
- {formatTime(hour * 60)}
-
- {/each}
-
-
-
(drag = null)}
- >
- {#each hourMarks as hour (hour)}
-
- {/each}
-
- {#if preview}
-
- {/if}
-
- {#each blockViews as view (view.block.id)}
- {#each view.segments as segment, segmentIndex (`${view.block.id}-${segmentIndex}`)}
-
event.stopPropagation()}
- onclick={(event) => {
- event.stopPropagation();
- onEdit?.(view.block);
- }}
- aria-label={`Block ${formatTime(view.block.startMinutes)} to ${formatTime(view.block.endMinutes)}`}
- class="absolute inset-x-1 overflow-hidden rounded-md border border-indigo-300 bg-indigo-50 px-2 py-1 text-left text-xs text-indigo-900 shadow-sm transition hover:bg-indigo-100 dark:border-indigo-800 dark:bg-indigo-950/60 dark:text-indigo-200 dark:hover:bg-indigo-900"
- style={`top:${segment.top}%;height:${segment.height}%`}
+
+ {#if label}
+
{label}
+ {/if}
+
+
+ {#each hourMarks as hour (hour)}
+
- {#if segmentIndex === 0}
-
- {formatTime(view.block.startMinutes)} – {formatTime(
- view.block.endMinutes
- )}{view.overnight ? ' (next day)' : ''}
-
- {:else}
- ↳ continues
- {/if}
-
- {view.block.proclamations.map((p) => p.name).join(', ') || 'No proclamations'}
-
-
+ {formatTime(hour * 60)}
+
{/each}
- {/each}
+
- {#if blocks.length === 0}
-
- Click or drag to add a block.
-
- {/if}
+
(drag = null)}
+ >
+ {#each hourMarks as hour (hour)}
+
+ {/each}
+
+ {#if preview}
+
+ {/if}
+
+ {#each blockViews as view (view.block.id)}
+ {#each view.segments as segment, segmentIndex (`${view.block.id}-${segmentIndex}`)}
+
event.stopPropagation()}
+ onclick={(event) => {
+ event.stopPropagation();
+ onEdit?.({ ...view.block, day: view.block.day ?? null });
+ }}
+ aria-label={`Block ${formatTime(view.block.startMinutes)} to ${formatTime(view.block.endMinutes)}`}
+ class="absolute inset-x-1 overflow-hidden rounded-md border border-indigo-300 bg-indigo-50 px-2 py-1 text-left text-xs text-indigo-900 shadow-sm transition hover:bg-indigo-100 dark:border-indigo-800 dark:bg-indigo-950/60 dark:text-indigo-200 dark:hover:bg-indigo-900"
+ style={`top:${segment.top}%;height:${segment.height}%`}
+ >
+ {#if segmentIndex === 0}
+
+ {formatTime(view.block.startMinutes)} – {formatTime(
+ view.block.endMinutes
+ )}{view.overnight ? ' (next day)' : ''}
+
+ {:else}
+ ↳ continues
+ {/if}
+
+ {view.block.proclamations.map((p) => p.name).join(', ') || 'No proclamations'}
+
+
+ {/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}
+
+ System
+ selectSystem(e.currentTarget.value)}
+ >
+ {#each data.licenses as license (license.id)}
+ {license.systemName ?? license.key}
+ {/each}
+
+
+ {/if}
{#if successMessage}
@@ -77,24 +179,154 @@
{successMessage}
{/if}
+ {#if errorMessage}
+
+ {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
+
+ requestType('daily')}>Daily
+ requestType('weekly')}>Weekly
+
+ {#if type !== null}
+
requestType(null)}>Delete schedule
+ {/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}
+
+
+ (showTypeConfirm = false)}>Cancel
+ applyType(typePending)}>Continue
+
+
+
+{/if}