/** * Presence pattern editor — a pushed screen like the override editor. * * Adds a pattern to the child's collection (POST) or edits one in place * (PATCH with only the changed fields). The weekday numbering sent to the * API stays Monday=0 .. Sunday=6; only the display is Sunday-first. */ import { useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react"; import type { AdminChild } from "../api/definitions"; import { createPresencePattern, updatePresencePattern, type PatternKind, type PatternWeeks, type PresencePattern, type PresencePatternBody, } from "../api/presence"; import { DISPLAY_WEEKDAYS, WEEKDAY_SHORT, dayNumber, emptyPattern, isoFromDayNumber, resizePattern, togglePatternDay, } from "./cycle"; import { anchorLabel, shortDate } from "./format"; import { ChevronLeftGlyph } from "./glyphs"; import { errorMessage, inertOutside, isIsoDate, trapTab } from "./OverrideEditor"; import "./OverrideEditor.css"; export const CYCLE_LENGTH_CHOICES = [1, 2, 3, 4]; const WEEKDAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; /** The fields of `next` that differ from the stored pattern. */ export function changedFields( stored: PresencePatternBody, next: PresencePatternBody, ): Partial { const changes: Partial = {}; if (next.name !== stored.name) changes.name = next.name; if (next.kind !== stored.kind) changes.kind = next.kind; if (next.cycle_length_weeks !== stored.cycle_length_weeks) { changes.cycle_length_weeks = next.cycle_length_weeks; } if (next.anchor_date !== stored.anchor_date) changes.anchor_date = next.anchor_date; const samePattern = Array.from({ length: next.cycle_length_weeks }, (_, week) => week).every( (week) => JSON.stringify([...(stored.pattern[week] ?? [])].sort()) === JSON.stringify([...(next.pattern[week] ?? [])].sort()), ); if (!samePattern || changes.cycle_length_weeks !== undefined) changes.pattern = next.pattern; return changes; } export interface PatternEditorProps { child: AdminChild; /** The pattern being edited, or null to add a new one. */ pattern: PresencePattern | null; /** "YYYY-MM-DD"; a new pattern's anchor starts here. */ today: string; onCancel: () => void; onSaved: (pattern: PresencePattern) => void; } export default function PatternEditor({ child, pattern, today, onCancel, onSaved }: PatternEditorProps) { const [name, setName] = useState(pattern?.name ?? ""); const [kind, setKind] = useState(pattern?.kind ?? "away"); const [cycleLength, setCycleLength] = useState(pattern?.cycle_length_weeks ?? 1); const [anchor, setAnchor] = useState(pattern?.anchor_date ?? today); const [weeks, setWeeks] = useState(pattern?.pattern ?? emptyPattern(1)); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const screenRef = useRef(null); const titleRef = useRef(null); useEffect(() => { titleRef.current?.focus(); return screenRef.current ? inertOutside(screenRef.current) : undefined; }, []); function onKeyDown(event: ReactKeyboardEvent) { if (event.key === "Escape") { event.preventDefault(); if (!saving) onCancel(); return; } trapTab(event, screenRef.current); } const body: PresencePatternBody = { name: name.trim(), kind, cycle_length_weeks: cycleLength, anchor_date: anchor, pattern: resizePattern(weeks, cycleLength), }; const changes = pattern ? changedFields(pattern, body) : body; // A pattern covering no day is valid (e.g. a migrated all-absent home schedule). const canSave = body.name !== "" && isIsoDate(anchor) && Object.keys(changes).length > 0 && !saving; function save() { if (!canSave) return; setSaving(true); setError(null); const request = pattern ? updatePresencePattern(pattern.id, changes) : createPresencePattern(child.id, body); request.then(onSaved).catch((err: unknown) => { setError(errorMessage(err, "The pattern could not be saved. Check your connection and try again.")); setSaving(false); }); } const title = pattern ? "Edit pattern" : "New pattern"; return (

{title}

Repeating presence for {child.display_name}

Covered days are
{(["away", "home"] as PatternKind[]).map((value) => ( ))}
{Array.from({ length: cycleLength }, (_, week) => { const days = body.pattern[week]; const from = isIsoDate(anchor) ? shortDate(isoFromDayNumber(dayNumber(anchor) + week * 7)) : null; return (
Week {week + 1} {from ? ` · from ${from}` : ""}
{DISPLAY_WEEKDAYS.map((weekday) => ( ))}
); })}

{isIsoDate(anchor) ? `Week 1 starts ${anchorLabel(anchor)}; the cycle repeats from there.` : "Choose an anchor date."}

{error ? (

{error}

) : null}
); }