Portal: fix schedule type toggle, daily rendering, overnight spill, and mobile grid

This commit is contained in:
NeilClack
2026-09-29 15:22:02 -05:00
parent cc4aa8717e
commit 220ff92e78
5 changed files with 229 additions and 67 deletions
@@ -1,6 +1,5 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import type { SubmitFunction } from '@sveltejs/kit';
import type { PageProps } from './$types';
import WeekSchedule from '$lib/components/schedule/WeekSchedule.svelte';
@@ -27,19 +26,10 @@
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;
}
});
// Render straight from the server data. Every action invalidates and reloads
// it, so the view stays in sync without a local mirror that can go stale.
const type = $derived(data.type);
const blocks: BlockView[] = $derived(data.blocks);
let typePending = $state<'daily' | 'weekly' | null>(null);
let showTypeConfirm = $state(false);
let successMessage = $state('');
@@ -104,9 +94,8 @@
applyType(next);
}
// Submit the hidden ?/setType form. The enhance callback updates the local
// view immediately (the server then prefills/clears as needed) and refreshes
// the per-system flags without a full navigation.
// Submit the hidden ?/setType form. The enhance callback reloads the server
// data (prefilling/clearing Blocks as needed) without a full navigation.
function applyType(next: 'daily' | 'weekly' | null) {
showTypeConfirm = false;
errorMessage = '';
@@ -124,10 +113,9 @@
(result.data as { error?: string })?.error ?? 'Could not change the schedule type.';
return;
}
// Show the new type straight away; invalidateAll() then loads the
// server's blocks (e.g. daily→weekly prefill) and hasSchedule flags.
// update() applies the action result and reloads all data (it calls
// invalidateAll by default), pulling the new type, blocks and flags.
await update();
await invalidateAll();
};
};
@@ -206,7 +194,6 @@
copyOpen = false;
flash(`Copied to ${targets.join(', ')}.`);
await update();
await invalidateAll();
};
};
</script>
@@ -0,0 +1,67 @@
import { page } from 'vitest/browser';
import { describe, it, expect, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
import type { PageProps } from './$types';
vi.mock('$app/forms', () => ({
enhance: () => () => {}
}));
import Page from './+page.svelte';
const LICENSE = {
id: 'lk1',
key: 'AAAA-BBBB-CCCC-DDDD',
systemName: 'Living Room',
active: true,
hasSchedule: false
};
const block = {
id: 'b1',
day: null as string | null,
startMinutes: 6 * 60,
endMinutes: 8 * 60,
dynamicAudio: true,
proclamations: [{ id: 'p1', name: 'Peace', categoryId: 1, groupId: 1 }]
};
function makeData(overrides: Partial<PageProps['data']> = {}): PageProps['data'] {
return {
userRoleId: null,
licenses: [LICENSE],
selectedLicenseId: LICENSE.id,
type: null,
blocks: [],
categories: [{ id: 1, name: 'Physical' }],
groups: [{ id: 1, name: 'General' }],
proclamations: [{ id: 'p1', name: 'Peace', categoryId: 1, groupId: 1 }],
...overrides
};
}
function mountPage(data: PageProps['data']) {
return render(Page, { params: {}, data, form: null });
}
describe('schedule page', () => {
it('renders a schedule loaded for the selected system (same system, new data)', async () => {
const result = mountPage(makeData());
await expect
.element(page.getByText('No schedule. Choose Daily or Weekly to start.'))
.toBeInTheDocument();
// The server reloads `data` after an action while the selected system id
// stays the same; the view must follow the new data rather than a stale
// local mirror.
await result.rerender({ data: makeData({ type: 'daily', blocks: [block] }) });
await expect.element(page.getByText('Every day')).toBeInTheDocument();
await expect.element(page.getByText('Peace')).toBeInTheDocument();
await expect.element(page.getByLabelText('Block 6:00 AM to 8:00 AM')).toBeInTheDocument();
await expect
.element(page.getByText('No schedule. Choose Daily or Weekly to start.'))
.not.toBeInTheDocument();
});
});