fix/calendar-no-child #7

Merged
iamdoubz merged 2 commits from published-events into main 2026-11-06 14:42:59 -06:00
4 changed files with 95 additions and 2 deletions

No files matched your search

+12 -1
View File
@@ -5,6 +5,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
## [Unreleased]
## [0.0.5] - 2026-11-06
### Added
- A published scan's page lists its events, and you can edit or delete them there like on the Calendar tab. Events with no child picked are on no calendar, so they're marked "Not on any calendar" there, with a note to pick a child.
### Changed
- A new source starts with every child ticked, like a new calendar. Before, it started with none, and its events showed on no calendar.
## [0.0.4] - 2026-11-06
### Added
@@ -56,7 +66,8 @@ First test release: the Phase 1 MVP.
- Runs with plain Python or as a Docker image, with SQLite and a `backup` command.
- Release workflow that pushes the image to the Gitea container registry: stable releases tag `X.Y.Z`, `X.Y`, `X` and `latest`; pre-releases tag `X.Y.Z` and `dev`.
[Unreleased]: https://git.dou.bet/iamdoubz/parentcalendar/compare/v0.0.4...main
[Unreleased]: https://git.dou.bet/iamdoubz/parentcalendar/compare/v0.0.5...main
[0.0.5]: https://git.dou.bet/iamdoubz/parentcalendar/compare/v0.0.4...v0.0.5
[0.0.4]: https://git.dou.bet/iamdoubz/parentcalendar/compare/v0.0.3...v0.0.4
[0.0.3]: https://git.dou.bet/iamdoubz/parentcalendar/compare/v0.0.2...v0.0.3
[0.0.2]: https://git.dou.bet/iamdoubz/parentcalendar/compare/v0.0.1...v0.0.2
+80
View File
@@ -0,0 +1,80 @@
<script lang="ts">
import CalendarDays from '@lucide/svelte/icons/calendar-days';
import TriangleAlert from '@lucide/svelte/icons/triangle-alert';
import { api, KINDS, type EventValues, type Scan } from '../lib/api';
import { app, toast } from '../lib/state.svelte';
import { whenLabel } from '../lib/dates';
import { KIND_ICONS } from '../lib/icons';
import EventEditor from './EventEditor.svelte';
// A published scan's events, editable like on the Calendar tab. An event with no child is on no calendar,
// so this is the only place it can be found and fixed.
let { scan = $bindable() }: { scan: Scan } = $props();
let editing = $state<number | null>(null);
const events = $derived(scan.events.filter((e) => e.status === 'confirmed'));
const untagged = $derived(events.filter((e) => !e.child_ids.length).length);
const children = $derived(new Map(app.household!.children.map((c) => [c.id, c])));
async function run(work: () => Promise<unknown>, done: string) {
try {
await work();
editing = null;
scan = await api<Scan>(`/scans/${scan.id}`);
toast(done);
} catch (e: any) {
toast(e.message);
}
}
const save = (id: number) => (v: EventValues) => run(() => api(`/events/${id}`, { method: 'PATCH', json: v }), 'Saved');
const remove = (id: number) => () => run(() => api(`/events/${id}`, { method: 'DELETE' }), 'Deleted');
</script>
{#if events.length}
<section class="section fix">
<h2 class="section-title">
<CalendarDays size={16} aria-hidden="true" />Fix the events
<span class="count">{events.length} {events.length === 1 ? 'event' : 'events'}</span>
</h2>
{#if untagged}
<div class="alert warn" role="status">
<TriangleAlert size={18} aria-hidden="true" />
<span>{untagged === 1 ? '1 event has' : `${untagged} events have`} no child picked, so {untagged === 1 ? "it isn't" : "they aren't"} on any calendar. Tap one and pick a child.</span>
</div>
{:else}
<p class="hint">Tap an event to change it. Changes show on the Calendar tab as soon as you save.</p>
{/if}
<ul class="card-list">
{#each events as e (e.id)}
{@const KindIcon = KIND_ICONS[e.kind]}
{@const none = !e.child_ids.length}
<li class="event" class:none style:--child={children.get(e.child_ids[0])?.color ?? 'var(--warn)'}>
<button class="item" onclick={() => (editing = editing === e.id ? null : e.id)} aria-expanded={editing === e.id}>
<span class="kind"><KindIcon size={18} aria-hidden="true" /></span>
<span class="grow">
<span class="title">{e.title}</span>
<span class="meta">{whenLabel(e)} · {KINDS[e.kind]}</span>
{#if none}<span class="meta warn"><TriangleAlert size={12} aria-hidden="true" /> Not on any calendar</span>{/if}
</span>
</button>
{#if editing === e.id}
<EventEditor event={e} onsave={save(e.id)} oncancel={() => (editing = null)} ondelete={remove(e.id)} />
{/if}
</li>
{/each}
</ul>
</section>
{/if}
<style>
.fix { margin-top: var(--s6); }
/* Same rows as the Calendar list. */
.event { background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--child); border-radius: var(--r-lg); padding: var(--s1) var(--s2); box-shadow: var(--shadow-1); }
.event.none { border-left-style: dashed; }
.item { width: 100%; min-height: 56px; justify-content: flex-start; gap: var(--s3); padding: var(--s1); border: 0; background: none; text-align: left; white-space: normal; font-weight: 400; }
.item:hover:not(:disabled) { background: none; }
.item:active:not(:disabled) { transform: none; }
.kind { display: grid; place-items: center; width: 36px; height: 36px; flex: none; border-radius: var(--r-md); background: var(--surface-2); color: var(--text-2); }
.title { display: block; font-weight: 650; color: var(--text); }
.meta { display: block; font-size: var(--fs-sm); color: var(--text-2); }
.meta.warn { display: flex; align-items: center; gap: var(--s1); color: var(--warn); font-weight: 600; }
</style>
+2
View File
@@ -16,6 +16,7 @@
import PageHeader from '../lib/ui/PageHeader.svelte';
import StatusBadge from './StatusBadge.svelte';
import MenuDays from './MenuDays.svelte';
import ScanEvents from './ScanEvents.svelte';
import Review from './Review.svelte';
let { id }: { id: number } = $props();
@@ -163,6 +164,7 @@
{:else}
<p class="hint photos-gone">The photo was deleted after the retention period.</p>
{/if}
<ScanEvents bind:scan />
{#if scan.menu_items.length}<div class="menu-fix"><MenuDays bind:scan /></div>{/if}
{:else}
<Review bind:scan />
+1 -1
View File
@@ -56,7 +56,7 @@
source = { name: s.name ?? '', layout_type: s.layout_type ?? 'month_grid', double_read: s.double_read ?? false,
ignore_text: (s.ignore_labels ?? []).join(', '), applies_filter: s.applies_filter ?? '',
prompt_notes: s.prompt_notes ?? '', default_calendar_id: s.default_calendar_id ?? null,
child_ids: [...(s.child_ids ?? [])] };
child_ids: [...(s.child_ids ?? h.children.map((k) => k.id))] };
}
}