Dev #1
@@ -8,7 +8,7 @@
|
|||||||
hmToMinutes,
|
hmToMinutes,
|
||||||
isOvernight,
|
isOvernight,
|
||||||
toHm,
|
toHm,
|
||||||
validateBlock,
|
validateBlockAgainstSchedule,
|
||||||
type BlockSibling
|
type BlockSibling
|
||||||
} from '$lib/schedule-utils';
|
} from '$lib/schedule-utils';
|
||||||
|
|
||||||
@@ -25,10 +25,13 @@
|
|||||||
open = false,
|
open = false,
|
||||||
mode = 'create',
|
mode = 'create',
|
||||||
blockId,
|
blockId,
|
||||||
|
day = null,
|
||||||
|
licenseKeyId = '',
|
||||||
initialStartMinutes = 9 * 60,
|
initialStartMinutes = 9 * 60,
|
||||||
initialEndMinutes = 10 * 60,
|
initialEndMinutes = 10 * 60,
|
||||||
initialProclamationIds = [],
|
initialProclamationIds = [],
|
||||||
initialDynamicAudio = true,
|
initialDynamicAudio = true,
|
||||||
|
scheduleType = 'daily' as 'daily' | 'weekly' | null,
|
||||||
categories = [],
|
categories = [],
|
||||||
groups = [],
|
groups = [],
|
||||||
proclamations = [],
|
proclamations = [],
|
||||||
@@ -42,10 +45,13 @@
|
|||||||
open?: boolean;
|
open?: boolean;
|
||||||
mode?: 'create' | 'edit';
|
mode?: 'create' | 'edit';
|
||||||
blockId?: string;
|
blockId?: string;
|
||||||
|
day?: string | null;
|
||||||
|
licenseKeyId?: string;
|
||||||
initialStartMinutes?: number;
|
initialStartMinutes?: number;
|
||||||
initialEndMinutes?: number;
|
initialEndMinutes?: number;
|
||||||
initialProclamationIds?: string[];
|
initialProclamationIds?: string[];
|
||||||
initialDynamicAudio?: boolean;
|
initialDynamicAudio?: boolean;
|
||||||
|
scheduleType?: 'daily' | 'weekly' | null;
|
||||||
categories?: BrowserCategory[];
|
categories?: BrowserCategory[];
|
||||||
groups?: BrowserGroup[];
|
groups?: BrowserGroup[];
|
||||||
proclamations?: BrowserProclamation[];
|
proclamations?: BrowserProclamation[];
|
||||||
@@ -105,10 +111,11 @@
|
|||||||
|
|
||||||
function validate(): string | null {
|
function validate(): string | null {
|
||||||
if (!startValid || !endValid) return 'Enter a valid start and end time.';
|
if (!startValid || !endValid) return 'Enter a valid start and end time.';
|
||||||
const result = validateBlock(
|
if (scheduleType === null) return 'Set a schedule type first.';
|
||||||
{ startMinutes, endMinutes, proclamationIds: selectedIds },
|
const result = validateBlockAgainstSchedule(
|
||||||
siblings,
|
scheduleType,
|
||||||
{ isCreate: mode === 'create' }
|
{ day, startMinutes, endMinutes, proclamationIds: selectedIds },
|
||||||
|
siblings
|
||||||
);
|
);
|
||||||
return result.ok ? null : result.error;
|
return result.ok ? null : result.error;
|
||||||
}
|
}
|
||||||
@@ -193,6 +200,10 @@
|
|||||||
{#if mode === 'edit' && blockId}
|
{#if mode === 'edit' && blockId}
|
||||||
<input type="hidden" name="id" value={blockId} />
|
<input type="hidden" name="id" value={blockId} />
|
||||||
{/if}
|
{/if}
|
||||||
|
<input type="hidden" name="licenseKeyId" value={licenseKeyId} />
|
||||||
|
{#if day}
|
||||||
|
<input type="hidden" name="day" value={day} />
|
||||||
|
{/if}
|
||||||
<input type="hidden" name="startMinutes" value={startMinutes} />
|
<input type="hidden" name="startMinutes" value={startMinutes} />
|
||||||
<input type="hidden" name="endMinutes" value={endMinutes} />
|
<input type="hidden" name="endMinutes" value={endMinutes} />
|
||||||
<input type="hidden" name="proclamationIds" value={JSON.stringify(selectedIds)} />
|
<input type="hidden" name="proclamationIds" value={JSON.stringify(selectedIds)} />
|
||||||
@@ -280,7 +291,7 @@
|
|||||||
<h3 class="text-sm font-semibold text-slate-800 dark:text-slate-200">Review</h3>
|
<h3 class="text-sm font-semibold text-slate-800 dark:text-slate-200">Review</h3>
|
||||||
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
|
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
|
||||||
{formatTime(startMinutes)} – {formatTime(endMinutes)}{overnight
|
{formatTime(startMinutes)} – {formatTime(endMinutes)}{overnight
|
||||||
? ' (next day)'
|
? ' (ends next day)'
|
||||||
: ''}
|
: ''}
|
||||||
</p>
|
</p>
|
||||||
{#if selectedTitles.length === 0}
|
{#if selectedTitles.length === 0}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ describe('BlockModal', () => {
|
|||||||
it('marks an overnight block in the review', async () => {
|
it('marks an overnight block in the review', async () => {
|
||||||
mount({ initialStartMinutes: 21 * 60, initialEndMinutes: 9 * 60 });
|
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 () => {
|
it('rejects an overlapping time range with the specific error', async () => {
|
||||||
|
|||||||
@@ -10,6 +10,16 @@
|
|||||||
|
|
||||||
type ScheduleBlockView = {
|
type ScheduleBlockView = {
|
||||||
id: string;
|
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;
|
startMinutes: number;
|
||||||
endMinutes: number;
|
endMinutes: number;
|
||||||
dynamicAudio: boolean;
|
dynamicAudio: boolean;
|
||||||
@@ -18,12 +28,14 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
blocks = [],
|
blocks = [],
|
||||||
|
label,
|
||||||
onCreate,
|
onCreate,
|
||||||
onEdit
|
onEdit
|
||||||
}: {
|
}: {
|
||||||
blocks?: ScheduleBlockView[];
|
blocks?: ScheduleBlockView[];
|
||||||
|
label?: string;
|
||||||
onCreate?: (startMinutes: number, endMinutes: number) => void;
|
onCreate?: (startMinutes: number, endMinutes: number) => void;
|
||||||
onEdit?: (block: ScheduleBlockView) => void;
|
onEdit?: (block: OpenBlock) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const hourMarks = Array.from({ length: 24 }, (_, i) => i);
|
const hourMarks = Array.from({ length: 24 }, (_, i) => i);
|
||||||
@@ -97,6 +109,10 @@
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
{#if label}
|
||||||
|
<p class="text-sm font-semibold text-slate-700 dark:text-slate-200">{label}</p>
|
||||||
|
{/if}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="relative h-[720px] w-14 shrink-0" aria-hidden="true">
|
<div class="relative h-[720px] w-14 shrink-0" aria-hidden="true">
|
||||||
{#each hourMarks as hour (hour)}
|
{#each hourMarks as hour (hour)}
|
||||||
@@ -140,7 +156,7 @@
|
|||||||
onpointerdown={(event) => event.stopPropagation()}
|
onpointerdown={(event) => event.stopPropagation()}
|
||||||
onclick={(event) => {
|
onclick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onEdit?.(view.block);
|
onEdit?.({ ...view.block, day: view.block.day ?? null });
|
||||||
}}
|
}}
|
||||||
aria-label={`Block ${formatTime(view.block.startMinutes)} to ${formatTime(view.block.endMinutes)}`}
|
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"
|
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"
|
||||||
@@ -171,3 +187,4 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export function validateSchedule(type: ScheduleType, blocks: ValidationBlock[]):
|
|||||||
}
|
}
|
||||||
const overlap = findScheduleOverlap(type, blocks);
|
const overlap = findScheduleOverlap(type, blocks);
|
||||||
if (overlap) {
|
if (overlap) {
|
||||||
return { ok: false, error: overlapError(overlap.a, overlap.b) };
|
return { ok: false, error: overlapError(overlap.a) };
|
||||||
}
|
}
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
@@ -195,12 +195,12 @@ export function validateBlockAgainstSchedule(
|
|||||||
if (!shape.ok) return shape;
|
if (!shape.ok) return shape;
|
||||||
const overlap = findScheduleOverlap(type, [...siblings, candidate]);
|
const overlap = findScheduleOverlap(type, [...siblings, candidate]);
|
||||||
if (overlap) {
|
if (overlap) {
|
||||||
return { ok: false, error: overlapError(overlap.a, overlap.b) };
|
return { ok: false, error: overlapError(overlap.a) };
|
||||||
}
|
}
|
||||||
return { ok: true };
|
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.`;
|
return `This overlaps with your ${formatTime(a.startMinutes)}–${formatTime(a.endMinutes)} block.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,109 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
import DaySchedule from '$lib/components/schedule/DaySchedule.svelte';
|
import DaySchedule from '$lib/components/schedule/DaySchedule.svelte';
|
||||||
import BlockModal from '$lib/components/schedule/BlockModal.svelte';
|
import BlockModal from '$lib/components/schedule/BlockModal.svelte';
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
|
const WEEK_DAYS: { key: string; label: string }[] = [
|
||||||
|
{ key: 'mon', label: 'Monday' },
|
||||||
|
{ key: 'tue', label: 'Tuesday' },
|
||||||
|
{ key: 'wed', label: 'Wednesday' },
|
||||||
|
{ key: 'thu', label: 'Thursday' },
|
||||||
|
{ key: 'fri', label: 'Friday' },
|
||||||
|
{ key: 'sat', label: 'Saturday' },
|
||||||
|
{ key: 'sun', label: 'Sunday' }
|
||||||
|
];
|
||||||
|
|
||||||
|
type BlockView = {
|
||||||
|
id: string;
|
||||||
|
day: string | null;
|
||||||
|
startMinutes: number;
|
||||||
|
endMinutes: number;
|
||||||
|
dynamicAudio: boolean;
|
||||||
|
proclamations: { id: string; name: string; categoryId: number; groupId: number | null }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Local mirrors so the UI updates after actions. They reset whenever the
|
||||||
|
// selected system (and therefore the loaded schedule) changes.
|
||||||
|
let type = $state<'daily' | 'weekly' | null>(null);
|
||||||
|
let blocks = $state<BlockView[]>([]);
|
||||||
|
let lastLicenseId = $state<string | null>(null);
|
||||||
|
$effect(() => {
|
||||||
|
if (data.selectedLicenseId !== lastLicenseId) {
|
||||||
|
lastLicenseId = data.selectedLicenseId;
|
||||||
|
type = data.type;
|
||||||
|
blocks = data.blocks;
|
||||||
|
showTypeConfirm = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let typePending = $state<'daily' | 'weekly' | null>(null);
|
||||||
|
let showTypeConfirm = $state(false);
|
||||||
|
let successMessage = $state('');
|
||||||
|
let errorMessage = $state('');
|
||||||
|
|
||||||
let modalOpen = $state(false);
|
let modalOpen = $state(false);
|
||||||
let modalMode = $state<'create' | 'edit'>('create');
|
let modalMode = $state<'create' | 'edit'>('create');
|
||||||
|
let modalDay = $state<string | null>(null);
|
||||||
let editingId = $state<string | undefined>(undefined);
|
let editingId = $state<string | undefined>(undefined);
|
||||||
let initialStart = $state(9 * 60);
|
let initialStart = $state(9 * 60);
|
||||||
let initialEnd = $state(10 * 60);
|
let initialEnd = $state(10 * 60);
|
||||||
let initialIds = $state<string[]>([]);
|
let initialIds = $state<string[]>([]);
|
||||||
let initialDynamicAudio = $state(false);
|
let initialDynamicAudio = $state(true);
|
||||||
let successMessage = $state('');
|
|
||||||
|
let typeForm: HTMLFormElement | undefined = $state();
|
||||||
let closeTimer: ReturnType<typeof setTimeout> | undefined;
|
let closeTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
function openCreate(startMinutes: number, endMinutes: number) {
|
const selectedLicense = $derived(
|
||||||
|
data.licenses.find((l) => l.id === data.selectedLicenseId) ?? null
|
||||||
|
);
|
||||||
|
const licenseLabel = $derived(
|
||||||
|
selectedLicense ? (selectedLicense.systemName ?? selectedLicense.key) : 'No systems'
|
||||||
|
);
|
||||||
|
const dailyBlocks = $derived(blocks.filter((b) => b.day === null));
|
||||||
|
const hasBlocks = $derived(blocks.length > 0);
|
||||||
|
|
||||||
|
function blocksForDay(day: string): BlockView[] {
|
||||||
|
return blocks.filter((b) => b.day === day);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flash(message: string) {
|
||||||
|
successMessage = message;
|
||||||
|
clearTimeout(closeTimer);
|
||||||
|
closeTimer = setTimeout(() => (successMessage = ''), 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSystem(licenseKeyId: string) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if (licenseKeyId) url.searchParams.set('license', licenseKeyId);
|
||||||
|
else url.searchParams.delete('license');
|
||||||
|
window.location.href = url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestType(next: 'daily' | 'weekly' | null) {
|
||||||
|
if (next === type) return;
|
||||||
|
if (hasBlocks && type !== null && next !== null) {
|
||||||
|
typePending = next;
|
||||||
|
showTypeConfirm = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyType(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyType(next: 'daily' | 'weekly' | null) {
|
||||||
|
showTypeConfirm = false;
|
||||||
|
const form = typeForm;
|
||||||
|
if (!form) return;
|
||||||
|
(form.querySelector('input[name="type"]') as HTMLInputElement).value = next ?? '';
|
||||||
|
(form.querySelector('input[name="confirm"]') as HTMLInputElement).value = 'true';
|
||||||
|
form.requestSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate(day: string | null, startMinutes: number, endMinutes: number) {
|
||||||
modalMode = 'create';
|
modalMode = 'create';
|
||||||
editingId = undefined;
|
editingId = undefined;
|
||||||
|
modalDay = day;
|
||||||
initialStart = startMinutes;
|
initialStart = startMinutes;
|
||||||
initialEnd = endMinutes;
|
initialEnd = endMinutes;
|
||||||
initialIds = [];
|
initialIds = [];
|
||||||
@@ -27,6 +113,7 @@
|
|||||||
|
|
||||||
function openEdit(block: {
|
function openEdit(block: {
|
||||||
id: string;
|
id: string;
|
||||||
|
day: string | null;
|
||||||
startMinutes: number;
|
startMinutes: number;
|
||||||
endMinutes: number;
|
endMinutes: number;
|
||||||
dynamicAudio: boolean;
|
dynamicAudio: boolean;
|
||||||
@@ -34,9 +121,10 @@
|
|||||||
}) {
|
}) {
|
||||||
modalMode = 'edit';
|
modalMode = 'edit';
|
||||||
editingId = block.id;
|
editingId = block.id;
|
||||||
|
modalDay = block.day;
|
||||||
initialStart = block.startMinutes;
|
initialStart = block.startMinutes;
|
||||||
initialEnd = block.endMinutes;
|
initialEnd = block.endMinutes;
|
||||||
initialIds = block.proclamations.map((proclamation) => proclamation.id);
|
initialIds = block.proclamations.map((p) => p.id);
|
||||||
initialDynamicAudio = block.dynamicAudio;
|
initialDynamicAudio = block.dynamicAudio;
|
||||||
modalOpen = true;
|
modalOpen = true;
|
||||||
}
|
}
|
||||||
@@ -47,12 +135,17 @@
|
|||||||
|
|
||||||
function handleSaved() {
|
function handleSaved() {
|
||||||
modalOpen = false;
|
modalOpen = false;
|
||||||
successMessage = modalMode === 'edit' ? 'Block saved.' : 'Block created.';
|
flash(modalMode === 'edit' ? 'Block saved.' : 'Block created.');
|
||||||
clearTimeout(closeTimer);
|
|
||||||
closeTimer = setTimeout(() => (successMessage = ''), 1500);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if typeForm === undefined}{/if}
|
||||||
|
<form method="POST" action="?/setType" use:enhance bind:this={typeForm} class="hidden">
|
||||||
|
<input type="hidden" name="licenseKeyId" value={data.selectedLicenseId ?? ''} />
|
||||||
|
<input type="hidden" name="type" value="" />
|
||||||
|
<input type="hidden" name="confirm" value="false" />
|
||||||
|
</form>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-wrap items-end justify-between gap-3">
|
<div class="flex flex-wrap items-end justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -60,14 +153,23 @@
|
|||||||
Schedule
|
Schedule
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mt-1 text-sm text-slate-500 sm:text-base dark:text-slate-400">
|
<p class="mt-1 text-sm text-slate-500 sm:text-base dark:text-slate-400">
|
||||||
Build your daily listening schedule. Up to 4 blocks per day.
|
Build a listening schedule for a system. Up to 4 blocks per day.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span
|
{#if data.licenses.length > 0}
|
||||||
class="rounded-full bg-slate-100 px-3 py-1 text-sm font-medium text-slate-600 dark:bg-slate-800 dark:text-slate-300"
|
<label class="flex items-center gap-2 text-sm">
|
||||||
|
<span class="text-slate-500 dark:text-slate-400">System</span>
|
||||||
|
<select
|
||||||
|
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 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"
|
||||||
|
value={data.selectedLicenseId ?? ''}
|
||||||
|
onchange={(e) => selectSystem(e.currentTarget.value)}
|
||||||
>
|
>
|
||||||
{data.blocks.length} of 4 blocks
|
{#each data.licenses as license (license.id)}
|
||||||
</span>
|
<option value={license.id}>{license.systemName ?? license.key}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if successMessage}
|
{#if successMessage}
|
||||||
@@ -77,15 +179,114 @@
|
|||||||
{successMessage}
|
{successMessage}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if errorMessage}
|
||||||
<div class="mt-6">
|
<div
|
||||||
<DaySchedule blocks={data.blocks} onCreate={openCreate} onEdit={openEdit} />
|
class="mt-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600 dark:border-red-900 dark:bg-red-950/50 dark:text-red-400"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{errorMessage}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !selectedLicense}
|
||||||
|
<div
|
||||||
|
class="mt-6 rounded-2xl border border-dashed border-slate-300 bg-white p-8 text-center dark:border-slate-700 dark:bg-slate-900"
|
||||||
|
>
|
||||||
|
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
You don't have any systems yet. A system appears here once a Kiosk validates its License
|
||||||
|
Key.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- System + mode -->
|
||||||
|
<div class="mt-6 flex flex-wrap items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">System</span>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
{licenseLabel}
|
||||||
|
{#if !selectedLicense.systemName}
|
||||||
|
<span class="text-slate-400 dark:text-slate-500">(unnamed system)</span>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">Schedule type</span>
|
||||||
|
<div
|
||||||
|
class="mt-1 inline-flex overflow-hidden rounded-md border border-slate-300 dark:border-slate-700"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-1.5 text-sm {type === 'daily'
|
||||||
|
? 'bg-indigo-600 text-white'
|
||||||
|
: 'bg-white text-slate-700 dark:bg-slate-900 dark:text-slate-300'}"
|
||||||
|
onclick={() => requestType('daily')}>Daily</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-1.5 text-sm {type === 'weekly'
|
||||||
|
? 'bg-indigo-600 text-white'
|
||||||
|
: 'bg-white text-slate-700 dark:bg-slate-900 dark:text-slate-300'}"
|
||||||
|
onclick={() => requestType('weekly')}>Weekly</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{#if type !== null}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ml-2 text-sm text-red-600 hover:underline dark:text-red-400"
|
||||||
|
onclick={() => requestType(null)}>Delete schedule</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if type === null}
|
||||||
|
<div
|
||||||
|
class="mt-6 rounded-2xl border border-dashed border-slate-300 bg-white p-8 text-center dark:border-slate-700 dark:bg-slate-900"
|
||||||
|
>
|
||||||
|
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
No schedule. Choose Daily or Weekly to start.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mt-4 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-300"
|
||||||
|
>
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
{:else if type === 'daily'}
|
||||||
|
<div class="mt-6">
|
||||||
|
<DaySchedule
|
||||||
|
blocks={dailyBlocks}
|
||||||
|
onCreate={(s, e) => openCreate(null, s, e)}
|
||||||
|
onEdit={openEdit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="mt-6 flex flex-col gap-6">
|
||||||
|
{#each WEEK_DAYS as day (day.key)}
|
||||||
|
<DaySchedule
|
||||||
|
blocks={blocksForDay(day.key)}
|
||||||
|
label={day.label}
|
||||||
|
onCreate={(s, e) => openCreate(day.key, s, e)}
|
||||||
|
onEdit={openEdit}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<p class="mt-4 text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
<BlockModal
|
<BlockModal
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
mode={modalMode}
|
mode={modalMode}
|
||||||
blockId={editingId}
|
blockId={editingId}
|
||||||
|
day={modalDay}
|
||||||
|
licenseKeyId={data.selectedLicenseId ?? ''}
|
||||||
|
scheduleType={type}
|
||||||
initialStartMinutes={initialStart}
|
initialStartMinutes={initialStart}
|
||||||
initialEndMinutes={initialEnd}
|
initialEndMinutes={initialEnd}
|
||||||
initialProclamationIds={initialIds}
|
initialProclamationIds={initialIds}
|
||||||
@@ -93,8 +294,39 @@
|
|||||||
categories={data.categories}
|
categories={data.categories}
|
||||||
groups={data.groups}
|
groups={data.groups}
|
||||||
proclamations={data.proclamations}
|
proclamations={data.proclamations}
|
||||||
existingBlocks={data.blocks}
|
existingBlocks={blocks}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
onSaved={handleSaved}
|
onSaved={handleSaved}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if showTypeConfirm}
|
||||||
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 p-4">
|
||||||
|
<div
|
||||||
|
class="relative z-10 w-full max-w-md rounded-2xl border border-slate-200 bg-white p-6 shadow-xl dark:border-slate-800 dark:bg-slate-900"
|
||||||
|
>
|
||||||
|
<h2 class="text-lg font-bold text-slate-900 dark:text-slate-100">Change schedule type?</h2>
|
||||||
|
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
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}
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300"
|
||||||
|
onclick={() => (showTypeConfirm = false)}>Cancel</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"
|
||||||
|
onclick={() => applyType(typePending)}>Continue</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user