Files
chore/.github/specs/archive/feat-calendar-chore/feat-calendar-chore.md
Ryan Kegel d7316bb00a
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m34s
feat: add chore, kindness, and penalty management components
- Implemented ChoreAssignView for assigning chores to children.
- Created ChoreConfirmDialog for confirming chore completion.
- Developed KindnessAssignView for assigning kindness acts.
- Added PenaltyAssignView for assigning penalties.
- Introduced ChoreEditView and ChoreView for editing and viewing chores.
- Created KindnessEditView and KindnessView for managing kindness acts.
- Developed PenaltyEditView and PenaltyView for managing penalties.
- Added TaskSubNav for navigation between chores, kindness acts, and penalties.
2026-02-28 11:25:56 -05:00

14 KiB
Raw Blame History

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: str
  • child_id: str
  • task_id: str
  • mode: Literal['days', 'interval']
  • For mode='days': day_configs: list[DayConfig]
    • DayConfig: { day: int (0=Sun6=Sat), hour: int, minute: int }
  • For mode='interval': interval_days: int (27), anchor_weekday: int (0=Sun6=Sat), interval_hour: int, interval_minute: int
  • Inherits id, created_at, updated_at from BaseModel

New model — backend/models/task_extension.py:

  • id: str
  • child_id: str
  • task_id: str
  • date: str — ISO date string supplied by the client, e.g. '2026-02-22'

Frontend Models

In frontend/vue-app/src/common/models.ts:

  • Add DayConfig interface: { day: number; hour: number; minute: number }
  • Add ChoreSchedule interface mirroring the Python model (mode, day_configs, interval_days, anchor_weekday, interval_hour, interval_minute)
  • Extend ChildTask wire type with:
    • schedule?: ChoreSchedule | null — raw schedule data returned from backend
    • extension_date?: string | null — ISO date of the most recent TaskExtension for 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.png outlined 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 #item slot in ParentView.vue (not inside ScrollingList).

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-inactive class: 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 LATE badge overlay appears (same absolute-positioned .pending pattern, using --item-card-bad-border color)
  • 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 LATE badge overlay (same pattern as PENDING in ChildView.vue)

Auto-expiry timers:

  • After task list is fetched or updated, for each chore where a due time exists for today and isPastTime is false, compute msUntilExpiry using scheduleUtils.msUntilExpiry(dueHour, dueMin, new Date()) and set a setTimeout
  • 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 in onUnmounted
  • 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 = today and timers are reset

Schedule Modal

  • Triggered from the Schedule kebab menu item in ParentView
  • Wraps ModalDialog with 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 (SundaySaturday)
  • Checking a day reveals an inline TimeSelector component 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 [27]

  • Weekday picker to select the anchor day (0=Sun6=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 TimeSelector applies to all occurrences

  • Save and Cancel buttons at the bottom

Time Selector

  • Custom-built TimeSelector.vue component (no PrimeVue dependency)
  • Props: modelValue: { hour: number; minute: number } (24h internally, 12h display)
  • Emits: update:modelValue
  • UI: Up/Down arrow buttons for Hour (112) 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 :root CSS 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 table chore_schedules
  • backend/db/task_extensions.py — TinyDB table task_extensions

New API file — backend/api/chore_schedule_api.py:

  • GET /child/<child_id>/task/<task_id>/schedule — returns raw ChoreSchedule; 404 if none
  • PUT /child/<child_id>/task/<task_id>/schedule — create or replace schedule; fires chore_schedule_modified SSE (operation: 'SET')
  • DELETE /child/<child_id>/task/<task_id>/schedule — remove schedule; fires chore_schedule_modified SSE (operation: 'DELETED')
  • POST /child/<child_id>/task/<task_id>/extend — body: { date: "YYYY-MM-DD" } (client's local date); inserts a TaskExtension; returns 409 if a TaskExtension already exists for this child_id + task_id + date; fires chore_time_extended SSE

Modify backend/api/child_api.pyGET /child/<id>/list-tasks:

  • For each task, look up ChoreSchedule for (child_id, task_id) and the most recent TaskExtension
  • Return schedule: ChoreSchedule | null and extension_date: string | null on 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: verify schedule and extension_date fields 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 both days and interval modes
  • getDueTimeToday(schedule: ChoreSchedule, localDate: Date): { hour: number; minute: number } | null
  • isPastTime(dueHour: number, dueMin: number, localNow: Date): boolean
  • isExtendedToday(extensionDate: string | null | undefined, localDate: Date): boolean
  • formatDueTimeLabel(hour: number, minute: number): string — returns e.g. "3:00 PM"
  • msUntilExpiry(dueHour: number, dueMin: number, localNow: Date): number
  • intervalHitsToday(anchorWeekday: number, intervalDays: number, localDate: Date): boolean

ParentView.vue:

  • Derive computed chore state per card using scheduleUtils and new Date()
  • Remove :enableEdit="true" and @edit-item from the Chores ScrollingList (keep for Penalties)
  • Add activeMenuFor = ref<string | null>(null) and capture-phase document click listener (same pattern as ChildrenListView.vue)
  • In chore #item slot: add .kebab-wrap > .kebab-btn + .kebab-menu per card with items: Edit Points, Schedule, Extend Time (conditional on isPastTime)
  • Bind .chore-inactive class when !isScheduledToday || isPastTime
  • Add TOO LATE badge overlay when isPastTime
  • Show "Due by {{ formatDueTimeLabel(...) }}" sub-text when due time exists and not yet expired
  • Manage setTimeout handles in a ref<number[]>; clear in onUnmounted and on every re-fetch
  • Listen for chore_schedule_modified and chore_time_extended SSE events → re-fetch task list and reset timers

ChildView.vue:

  • Derive computed chore state per card using scheduleUtils and new Date()
  • Filter out chores where !isScheduledToday (client-side, not server-side)
  • Add .chore-expired grayout + TOO LATE stamp when isPastTime
  • Show "Due by {{ formatDueTimeLabel(...) }}" sub-text when due time exists and not yet expired
  • Manage setTimeout handles the same way as ParentView
  • Listen for chore_schedule_modified and chore_time_extended SSE 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 TimeSelector when checked
  • Interval: number input [27], weekday picker, single TimeSelector
  • Calls setChoreSchedule() on Save; emits saved and cancelled

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, :root variables 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 mocked Date objects
  • TimeSelector.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 correct ChoreSchedule shape

Future Considerations


Acceptance Criteria (Definition of Done)

Backend

  • ChoreSchedule and TaskExtension models created with from_dict()/to_dict() serialization
  • chore_schedules and task_extensions TinyDB tables created
  • chore_schedule_api.py CRUD and extend endpoints implemented
  • GET /child/<id>/list-tasks returns schedule and extension_date on 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.ts implemented and fully unit tested, including timezone-safe mocked Date cases
  • TimeSelector.vue implemented with 15-min increments, AM/PM toggle, mobile-friendly tap targets
  • ScheduleModal.vue implemented 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
  • setTimeout auto-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_modified and chore_time_extended
  • All frontend tests pass