- Implemented TimeSelector component for selecting time with AM/PM toggle and minute/hour increment/decrement functionality. - Created ScheduleModal component for scheduling chores with options for specific days or intervals. - Added utility functions for scheduling logic in scheduleUtils.ts. - Developed comprehensive tests for TimeSelector and scheduleUtils functions to ensure correct behavior.
14 KiB
Feature: Chores can be scheduled to occur on certain calendar days or weeks and have a time during the day where they must be finished by.
Overview
Goal: Chores can be edited to only show up in child mode on particular days and during certain times. This encourages a child to perform chores on those days before a certain time of day.
User Story:
- As a parent, I will be able to select an assigned chore and configure it to occur on any combination of the days of the week. I will also be able to configure the latest possible time of day on each of those configured days that the chore must be done.
- As a child, I will not see the chore appear if it was configured for a different day of the week. I will also not be able to see a chore if the current time of day is past the configured latest time of the chore.
Rules:
- Follow instructions from .github/copilot-instructions.md
Architecture Notes
Timezone Safety
Due times represent the child's local time of day. The server must never perform time-based computations (is it today? has the time passed?) because it may be in a different timezone than the client.
All time logic runs in the browser using new Date() (the client's local clock). The backend only stores and returns raw schedule data. The frontend computes isScheduledToday, isPastTime, due_time_label, and setTimeout durations locally.
Auto-Expiry While Browser is Open
When a chore's due time passes while the browser tab is already open, the UI must update automatically without a page refresh. The frontend sets a setTimeout per chore (calculated from the due time and the current local time). When the timer fires, the chore's state is updated reactively in local component state — no re-fetch required.
Data Model Changes
Backend Models
New model — backend/models/chore_schedule.py:
id: strchild_id: strtask_id: strmode: Literal['days', 'interval']- For
mode='days':day_configs: list[DayConfig]DayConfig:{ day: int (0=Sun–6=Sat), hour: int, minute: int }
- For
mode='interval':interval_days: int(2–7),anchor_weekday: int(0=Sun–6=Sat),interval_hour: int,interval_minute: int - Inherits
id,created_at,updated_atfromBaseModel
New model — backend/models/task_extension.py:
id: strchild_id: strtask_id: strdate: str— ISO date string supplied by the client, e.g.'2026-02-22'
Frontend Models
In frontend/vue-app/src/common/models.ts:
- Add
DayConfiginterface:{ day: number; hour: number; minute: number } - Add
ChoreScheduleinterface mirroring the Python model (mode,day_configs,interval_days,anchor_weekday,interval_hour,interval_minute) - Extend
ChildTaskwire type with:schedule?: ChoreSchedule | null— raw schedule data returned from backendextension_date?: string | null— ISO date of the most recentTaskExtensionfor this child+task, if any
Frontend Design
Chore card
- Currently, when a chore is pressed on the ParentView, it shows an icon that represents an edit button. This is shown in
.github/feat-calendar-chore/feat-calendar-chore-component01.pngoutlined in red. - This icon button will be changed to a kebab menu that drops down the following:
- Edit Points — opens the existing override points modal
- Schedule — opens the Schedule Modal
- Extend Time — only visible when the chore is computed as expired (
isPastTime === true)
- The look and feel of the dropdown follows the kebab menu pattern in
ChildrenListView.vue. - Penalties and Rewards keep the current edit icon button and existing behavior.
- The kebab menu is placed per-card in the chore
#itemslot inParentView.vue(not insideScrollingList).
ParentView card states (computed client-side from item.schedule, item.extension_date, and new Date()):
- No schedule → card appears normally, no annotations
- Scheduled but wrong day → card is grayed out (
.chore-inactiveclass:opacity: 0.45; filter: grayscale(60%)) - Correct day, due time not yet passed → show
"Due by X:XX PM"sub-text on card - Correct day, due time passed (and no extension for today) → card is grayed out and a
TOO LATEbadge overlay appears (same absolute-positioned.pendingpattern, using--item-card-bad-bordercolor) - Chore always shows the kebab menu and can always be triggered by the parent regardless of state
ChildView card states (computed client-side):
- No schedule → card appears normally
- Scheduled but wrong day → hidden entirely (filtered out in component, not server-side)
- Correct day, due time not yet passed → show
"Due by X:XX PM"sub-text - Correct day, due time passed → grayed out with
TOO LATEbadge overlay (same pattern asPENDINGinChildView.vue)
Auto-expiry timers:
- After task list is fetched or updated, for each chore where a due time exists for today and
isPastTimeisfalse, computemsUntilExpiryusingscheduleUtils.msUntilExpiry(dueHour, dueMin, new Date())and set asetTimeout - When the timer fires, flip that chore's reactive state to expired locally — no re-fetch needed
- All timer handles are stored in a
ref<number[]>and cleared inonUnmounted - Timers are also cleared and reset whenever the task list is re-fetched (e.g. on SSE events)
Extend Time behavior:
- When selected, calls
extendChoreTime(childId, taskId)with the client's local ISO date - The chore resets to normal for the remainder of that day only
- After a successful extend, the local chore state is updated to reflect
extension_date = todayand timers are reset
Schedule Modal
- Triggered from the Schedule kebab menu item in ParentView
- Wraps
ModalDialogwith title"Schedule Chore"and subtitle showing the chore's name and icon - Does not use
EntityEditForm— custom layout required for the day/time matrix - Top toggle: "Specific Days" vs "Every X Days"
Specific Days mode:
- 7 checkbox rows, one per day (Sunday–Saturday)
- Checking a day reveals an inline
TimeSelectorcomponent for that day - Unchecking a day removes its time configuration
- Each checked day can have its own independent due time
Every X Days mode:
-
Number selector for interval [2–7]
-
Weekday picker to select the anchor day (0=Sun–6=Sat)
-
Anchor logic: the first occurrence is calculated as the current week's matching weekday; subsequent occurrences repeat every X days indefinitely. See
scheduleUtils.intervalHitsToday(anchorWeekday, intervalDays, localDate) -
One shared
TimeSelectorapplies to all occurrences -
Save and Cancel buttons at the bottom
Time Selector
- Custom-built
TimeSelector.vuecomponent (no PrimeVue dependency) - Props:
modelValue: { hour: number; minute: number }(24h internally, 12h display) - Emits:
update:modelValue - UI: Up/Down arrow buttons for Hour (1–12) and Minute (00, 15, 30, 45), AM/PM toggle button
- Minutes increment/decrement in 15-minute steps with boundary wrapping (e.g. 11:45 AM → 12:00 PM)
- Large tap targets for mobile devices
- Scoped CSS using
:rootCSS variables only - Modeled after the Time Only component at https://primevue.org/datepicker/#time
Backend Implementation
New DB files:
backend/db/chore_schedules.py— TinyDB tablechore_schedulesbackend/db/task_extensions.py— TinyDB tabletask_extensions
New API file — backend/api/chore_schedule_api.py:
GET /child/<child_id>/task/<task_id>/schedule— returns rawChoreSchedule; 404 if nonePUT /child/<child_id>/task/<task_id>/schedule— create or replace schedule; fireschore_schedule_modifiedSSE (operation: 'SET')DELETE /child/<child_id>/task/<task_id>/schedule— remove schedule; fireschore_schedule_modifiedSSE (operation: 'DELETED')POST /child/<child_id>/task/<task_id>/extend— body:{ date: "YYYY-MM-DD" }(client's local date); inserts aTaskExtension; returns 409 if aTaskExtensionalready exists for thischild_id + task_id + date; fireschore_time_extendedSSE
Modify backend/api/child_api.py — GET /child/<id>/list-tasks:
- For each task, look up
ChoreSchedulefor(child_id, task_id)and the most recentTaskExtension - Return
schedule: ChoreSchedule | nullandextension_date: string | nullon each task object - No filtering, no time math, no annotations — the client handles all of this
New SSE event types in backend/events/types/:
chore_schedule_modified.py— payload:child_id,task_id,operation: Literal['SET', 'DELETED']chore_time_extended.py— payload:child_id,task_id- Register both in
event_types.py
Backend Tests
test_chore_schedule_api.py: CRUD schedule endpoints, extend endpoint (409 on duplicate date, accepts client-supplied date)- Additions to
test_child_api.py: verifyscheduleandextension_datefields are returned on each task; verify no server-side time filtering or annotation occurs
Frontend Implementation
New utility — frontend/vue-app/src/common/scheduleUtils.ts:
isScheduledToday(schedule: ChoreSchedule, localDate: Date): boolean— handles bothdaysandintervalmodesgetDueTimeToday(schedule: ChoreSchedule, localDate: Date): { hour: number; minute: number } | nullisPastTime(dueHour: number, dueMin: number, localNow: Date): booleanisExtendedToday(extensionDate: string | null | undefined, localDate: Date): booleanformatDueTimeLabel(hour: number, minute: number): string— returns e.g."3:00 PM"msUntilExpiry(dueHour: number, dueMin: number, localNow: Date): numberintervalHitsToday(anchorWeekday: number, intervalDays: number, localDate: Date): boolean
ParentView.vue:
- Derive computed chore state per card using
scheduleUtilsandnew Date() - Remove
:enableEdit="true"and@edit-itemfrom the ChoresScrollingList(keep for Penalties) - Add
activeMenuFor = ref<string | null>(null)and capture-phasedocumentclick listener (same pattern asChildrenListView.vue) - In chore
#itemslot: add.kebab-wrap > .kebab-btn + .kebab-menuper card with items: Edit Points, Schedule, Extend Time (conditional onisPastTime) - Bind
.chore-inactiveclass when!isScheduledToday || isPastTime - Add
TOO LATEbadge overlay whenisPastTime - Show
"Due by {{ formatDueTimeLabel(...) }}"sub-text when due time exists and not yet expired - Manage
setTimeouthandles in aref<number[]>; clear inonUnmountedand on every re-fetch - Listen for
chore_schedule_modifiedandchore_time_extendedSSE events → re-fetch task list and reset timers
ChildView.vue:
- Derive computed chore state per card using
scheduleUtilsandnew Date() - Filter out chores where
!isScheduledToday(client-side, not server-side) - Add
.chore-expiredgrayout +TOO LATEstamp whenisPastTime - Show
"Due by {{ formatDueTimeLabel(...) }}"sub-text when due time exists and not yet expired - Manage
setTimeouthandles the same way as ParentView - Listen for
chore_schedule_modifiedandchore_time_extendedSSE events → re-fetch and reset timers
New frontend/vue-app/src/components/shared/ScheduleModal.vue:
- Wraps
ModalDialog; custom slot content for schedule form - Mode toggle (Specific Days / Every X Days)
- Specific Days: 7 checkbox rows, each reveals an inline
TimeSelectorwhen checked - Interval: number input [2–7], weekday picker, single
TimeSelector - Calls
setChoreSchedule()on Save; emitssavedandcancelled
New frontend/vue-app/src/components/shared/TimeSelector.vue:
- Props/emits as described in Frontend Design section
- Up/Down arrows for Hour and Minute (15-min increments), AM/PM toggle
- Scoped CSS,
:rootvariables only
frontend/vue-app/src/common/api.ts additions:
getChoreSchedule(childId, taskId)setChoreSchedule(childId, taskId, schedule)deleteChoreSchedule(childId, taskId)extendChoreTime(childId, taskId, localDate: string)— passes client's local ISO date in request body
Frontend Tests
scheduleUtils.ts: unit test all helpers —isScheduledToday(days mode, interval mode),intervalHitsToday,isPastTime,isExtendedToday,formatDueTimeLabel,msUntilExpiry; include timezone-agnostic cases using mockedDateobjectsTimeSelector.vue: increment/decrement, AM/PM toggle, 15-min boundary wrapping (e.g. 11:45 AM → 12:00 PM)ScheduleModal.vue: mode toggle renders correct sub-form; unchecking a day removes its time config; Save emits correctChoreScheduleshape
Future Considerations
Acceptance Criteria (Definition of Done)
Backend
ChoreScheduleandTaskExtensionmodels created withfrom_dict()/to_dict()serializationchore_schedulesandtask_extensionsTinyDB tables createdchore_schedule_api.pyCRUD and extend endpoints implementedGET /child/<id>/list-tasksreturnsscheduleandextension_dateon each task; performs no time math or filtering- Extend endpoint accepts client-supplied ISO date; returns 409 on duplicate for same date
- All new SSE events fire on every mutation
- All backend tests pass
Frontend
scheduleUtils.tsimplemented and fully unit tested, including timezone-safe mocked Date casesTimeSelector.vueimplemented with 15-min increments, AM/PM toggle, mobile-friendly tap targetsScheduleModal.vueimplemented with Specific Days and Every X Days modes- ParentView chore edit icon replaced with kebab menu (Edit Points, Schedule, Extend Time)
- ParentView chore cards: wrong day → grayed out; expired time → grayed out + TOO LATE badge
- ChildView chore cards: wrong day → hidden; expired time → grayed out + TOO LATE badge
- "Due by X:XX PM" sub-text shown on cards in both views when applicable
setTimeoutauto-expiry implemented; timers cleared on unmount and re-fetch- API helpers added to
api.ts - SSE listeners added in ParentView and ChildView for
chore_schedule_modifiedandchore_time_extended - All frontend tests pass