Files
chore/.github/specs/feat-calendar-chore/feat-calendar-schedule-refactor-02.md
Ryan Kegel a197f8e206
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m30s
feat: Refactor ScheduleModal to support interval scheduling with date input and deadline toggle
- Updated ChoreSchedule model to include anchor_date and interval_has_deadline.
- Refactored interval scheduling logic in scheduleUtils to use anchor_date.
- Introduced DateInputField component for selecting anchor dates in ScheduleModal.
- Enhanced ScheduleModal to include a stepper for interval days and a toggle for deadline.
- Updated tests for ScheduleModal and scheduleUtils to reflect new interval scheduling logic.
- Added DateInputField tests to ensure proper functionality and prop handling.
2026-02-26 15:16:46 -05:00

9.9 KiB
Raw Blame History

Feature: Daily chore scheduler refactor phase 2

Overview

Parent Feature: .github/feat-calenar-chore/feat-calendar-chore.md

Goal: UI refactor of the 'Every X Days' portion of the chore scheduler so that it is not so complicated and mobile friendly

User Story:

  • As a parent, I will be able to select an assigned chore and configure it to occur on 'Every X Days' as before, except I will be presented with a much easier to use interface.

Rules:

  • Follow instructions from .github/copilot-instructions.md

Design:

  • Do not modify 'Specific Days' pattern or UI. However, reuse code if necessary

Architecture:

  1. Shared Logic & Previewer Architecture The "Brain" remains centralized, but the output is now focused on the immediate next event to reduce cognitive clutter.

State Variables:

interval: Integer (Default: 1, Min: 1). "Frequency"

anchorDate: Date Object (Default: Today). "Start Date"

deadlineTime: String or Null (Default: null). "Deadline"

The "Next 1" Previewer Logic:

Input: anchorDate + interval.

Calculation: Result = anchorDate + (interval days).

Formatting: Returns a human-readable string (e.g., "Next occurrence: Friday, Oct 24").

Calendar Constraints:

Disable Past Dates: Any date prior to "Today" is disabled (greyed out and non-clickable) to prevent scheduling chores in the past.

  1. Mobile Specification: Bottom Sheet Calendar Design & Calendar Details Interface: A full-width monthly grid inside a slide-up panel.

Touch Targets: Each day cell is a minimum of 44x44 pixels to meet accessibility standards.

Month Navigation: Uses large left/right chevron buttons at the top of the sheet.

Visual Indicators:

Current Selection: A solid primary-colored circle.

Todays Date: A subtle outline or "dot" indicator.

Disabled Dates: 30% opacity with a "forbidden" cursor state if touched.

Architecture Gesture Control: The Bottom Sheet can be dismissed by swiping down on the "handle" at the top or tapping the dimmed backdrop.

Performance: The calendar should lazy-load months to ensure the sheet slides up instantly without lag.

The Flow When the user taps the "Starting on" row...

Then the sheet slides up. The current anchorDate is pre-selected and centered.

When the user taps a new date...

Then the sheet slides down immediately (Auto-confirm).

When the sheet closes...

Then the main UI updates the Next 1 Previewer text.

  1. PC (Desktop) Specification: Tethered Popover Calendar Design & Calendar Details Interface: A compact monthly grid (approx. 250px300px wide) that floats near the input.

Month Navigation: Small chevrons in the header. Includes a "Today" button to quickly jump back to the current month.

Day Headers: Single-letter abbreviations (S, M, T, W, T, F, S) to save space.

Hover States: As the mouse moves over valid dates, a light background highlight follows the cursor to provide immediate feedback.

Architecture Tethering: The popover is anchored to the bottom-left of the input field. If the browser window is too small, it intelligently repositions to the top-left.

Keyboard Support: * Arrow Keys: Move selection between days.

Enter: Confirm selection and close.

Esc: Close without saving changes.

Focus Management: When the popover opens, focus shifts to the calendar grid. When it closes, focus returns to the "Starting on" input.

The Flow When the user clicks the "Starting on" field...

Then the popover appears. No backdrop dimming is used.

When the user clicks a date...

Then the popover disappears.

When the user clicks anywhere outside the popover...

Then the popover closes (Cancel intent).

  1. Reusable Time Picker Reference Referenced from the 'Specific Days' design. TimePickerPopover.vue

Logic: 15-minute intervals (00, 15, 30, 45).

Mobile: Implemented via a Bottom Sheet with three scrollable columns.

PC: Implemented via a Tethered Popover with three clickable columns.

Clear Action: Both versions must include a "Clear" button to set the deadline to null (Anytime).

Data Model Changes

Backend Models

ChoreSchedule changes:

  • Remove anchor_weekday: int = 0
  • Add anchor_date: str = "" — ISO date string (e.g. "2026-02-25"). Empty string means "use today" (backward compat for old DB records).
  • Add interval_has_deadline: bool = True — when False, deadline is ignored ("Anytime").
  • Change interval_days valid range from [2, 7] to [1, 7].

from_dict defaults: anchor_date defaults to "", interval_has_deadline defaults to True for backward compat with existing DB records.

Frontend Models

ChoreSchedule interface changes:

  • Remove anchor_weekday: number
  • Add anchor_date: string
  • Add interval_has_deadline: boolean

Frontend Design

  • DateInputField.vue — new shared component at frontend/vue-app/src/components/shared/DateInputField.vue

    • Props: modelValue: string (ISO date string), min?: string (ISO date, for disabling past dates), emits update:modelValue
    • Wraps a native <input type="date"> with styling matching the TimePickerPopover button: --kebab-menu-border border, --modal-bg background, --secondary text color
    • Passes min to the native input so the browser disables past dates (no custom calendar needed)
    • Fully scoped styles using CSS variables from colors.css
  • ScheduleModal.vue — "Every X Days" section fully replaced; "Specific Days" section unchanged


Backend Implementation

  • backend/models/chore_schedule.py

    • Remove anchor_weekday: int = 0
    • Add anchor_date: str = ""
    • Add interval_has_deadline: bool = True
    • Update from_dict to default new fields for backward compat
  • backend/api/chore_schedule_api.py

    • Change interval_days validation from [2, 7] to [1, 7]
    • Accept anchor_date (string, ISO format) instead of anchor_weekday
    • Accept interval_has_deadline (boolean)

Backend Tests

  • Update existing interval-mode tests to use anchor_date instead of anchor_weekday
  • Add test: interval_days: 1 is now valid (was previously rejected)
  • Add test: interval_has_deadline: false is accepted and persisted
  • Add test: old DB records without anchor_date / interval_has_deadline load with correct defaults

Frontend Implementation

  • Created frontend/vue-app/src/components/shared/DateInputField.vue
    • Props: modelValue: string (ISO date), min?: string, emits update:modelValue
    • Styled to match TimePickerPopover button (border, background, text color)
    • Passes min to native <input type="date"> to disable past dates
    • Fully scoped styles using colors.css variables
  • Refactored ScheduleModal.vue — "Every X Days" section
    • Removed anchorWeekday state; added anchorDate: ref<string> (default: today ISO) and hasDeadline: ref<boolean> (default: true)
    • Changed intervalDays min from 2 → 1
    • Replaced <input type="number"> with a / value / + stepper, capped 17, styled with Phase 1 chip/button variables
    • Replaced <select> anchor weekday with DateInputField (min = today's ISO date)
    • Replaced TimeSelector with TimePickerPopover (exact reuse from Phase 1)
    • Added "Anytime" toggle link below the deadline row; when active, hides TimePickerPopover and sets hasDeadline = false; when inactive, shows TimePickerPopover and sets hasDeadline = true
    • Added "Next occurrence: [Weekday, Mon DD]" computed label (pure frontend, Intl.DateTimeFormat): starting from anchorDate, add intervalDays days repeatedly until result ≥ today; displayed as subtle italic label beneath the form rows (same style as Phase 1's "Default (HH:MM AM/PM)" label)
    • Load logic: read schedule.anchor_date (default to today if empty), schedule.interval_has_deadline, schedule.interval_days (clamped to ≥1)
    • Save logic: write anchor_date, interval_has_deadline; always write interval_hour/interval_minute (backend ignores them when interval_has_deadline=false)
    • "Specific Days" mode left unchanged

Frontend Tests

  • DateInputField.vue: renders the formatted date value; emits update:modelValue on change; min prop prevents selection of past dates
  • ScheduleModal.vue (Every X Days): stepper clamps to 17 at both ends; "Anytime" toggle hides the time picker and sets flag; restoring deadline shows the time picker; save payload contains anchor_date, interval_has_deadline, and correct interval_days; next occurrence label updates correctly when interval or anchor date changes; loading an existing schedule restores all fields including anchor_date and interval_has_deadline

Future Considerations

  • A fully custom calendar (bottom sheet on mobile, tethered popover on desktop) could replace DateInputField in a future phase for a more polished mobile experience.
  • TimePickerPopover could similarly gain a bottom-sheet variant for mobile.

Acceptance Criteria (Definition of Done)

Backend

  • anchor_weekday removed; anchor_date (string) added with empty-string default for old records
  • interval_has_deadline (bool) added, defaults to True for old records
  • interval_days valid range updated to [1, 7]
  • All existing and new backend tests pass

Frontend

  • New DateInputField component: styled native date input, respects min, emits ISO string
  • "Every X Days" mode shows /+ stepper for interval (17), DateInputField for anchor date, TimePickerPopover for deadline
  • "Anytime" toggle clears the deadline (sets interval_has_deadline = false) and hides the time picker
  • "Next occurrence" label computes and displays the next date ≥ today based on anchor + interval
  • Past dates are disabled in the date input (via min)
  • Existing schedules load correctly — anchor_date restored, interval_has_deadline restored
  • Save payload is valid and consumed by the existing API unchanged
  • "Specific Days" mode is unchanged
  • Frontend component tests written and passing for DateInputField and the refactored ScheduleModal interval section