feat: add chore, kindness, and penalty management components
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m34s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m34s
- 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.
This commit is contained in:
BIN
.github/specs/archive/feat-calendar-chore/feat-calendar-chore-component01.png
vendored
Normal file
BIN
.github/specs/archive/feat-calendar-chore/feat-calendar-chore-component01.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
270
.github/specs/archive/feat-calendar-chore/feat-calendar-chore.md
vendored
Normal file
270
.github/specs/archive/feat-calendar-chore/feat-calendar-chore.md
vendored
Normal file
@@ -0,0 +1,270 @@
|
||||
# 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=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_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 (Sunday–Saturday)
|
||||
- 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 [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 `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 (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 `: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.py` — `GET /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
|
||||
|
||||
- [x] `test_chore_schedule_api.py`: CRUD schedule endpoints, extend endpoint (409 on duplicate date, accepts client-supplied date)
|
||||
- [x] 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 [2–7], 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
|
||||
|
||||
- [x] `scheduleUtils.ts`: unit test all helpers — `isScheduledToday` (days mode, interval mode), `intervalHitsToday`, `isPastTime`, `isExtendedToday`, `formatDueTimeLabel`, `msUntilExpiry`; include timezone-agnostic cases using mocked `Date` objects
|
||||
- [x] `TimeSelector.vue`: increment/decrement, AM/PM toggle, 15-min boundary wrapping (e.g. 11:45 AM → 12:00 PM)
|
||||
- [x] `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
|
||||
|
||||
- [x] `ChoreSchedule` and `TaskExtension` models created with `from_dict()`/`to_dict()` serialization
|
||||
- [x] `chore_schedules` and `task_extensions` TinyDB tables created
|
||||
- [x] `chore_schedule_api.py` CRUD and extend endpoints implemented
|
||||
- [x] `GET /child/<id>/list-tasks` returns `schedule` and `extension_date` on each task; performs no time math or filtering
|
||||
- [x] Extend endpoint accepts client-supplied ISO date; returns 409 on duplicate for same date
|
||||
- [x] All new SSE events fire on every mutation
|
||||
- [x] All backend tests pass
|
||||
|
||||
### Frontend
|
||||
|
||||
- [x] `scheduleUtils.ts` implemented and fully unit tested, including timezone-safe mocked Date cases
|
||||
- [x] `TimeSelector.vue` implemented with 15-min increments, AM/PM toggle, mobile-friendly tap targets
|
||||
- [x] `ScheduleModal.vue` implemented with Specific Days and Every X Days modes
|
||||
- [x] ParentView chore edit icon replaced with kebab menu (Edit Points, Schedule, Extend Time)
|
||||
- [x] ParentView chore cards: wrong day → grayed out; expired time → grayed out + TOO LATE badge
|
||||
- [x] ChildView chore cards: wrong day → hidden; expired time → grayed out + TOO LATE badge
|
||||
- [x] "Due by X:XX PM" sub-text shown on cards in both views when applicable
|
||||
- [x] `setTimeout` auto-expiry implemented; timers cleared on unmount and re-fetch
|
||||
- [x] API helpers added to `api.ts`
|
||||
- [x] SSE listeners added in ParentView and ChildView for `chore_schedule_modified` and `chore_time_extended`
|
||||
- [x] All frontend tests pass
|
||||
182
.github/specs/archive/feat-calendar-chore/feat-calendar-schedule-refactor-01.md
vendored
Normal file
182
.github/specs/archive/feat-calendar-chore/feat-calendar-schedule-refactor-01.md
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
# Feature: Daily chore scheduler refactor phase 1
|
||||
|
||||
## Overview
|
||||
|
||||
**Parent Feature:** .github/feat-calenar-chore/feat-calendar-chore.md
|
||||
|
||||
**Goal:** UI refactor of the 'Specific Days' portion of the chore scheduler so that it is not so complicated.
|
||||
|
||||
**User Story:**
|
||||
|
||||
- As a parent, I will be able to select an assigned chore and configure it to occur on 'Specific Days' as before, except I will be presented with a much easier to use interface.
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Follow instructions from .github/copilot-instructions.md
|
||||
|
||||
**Design:**
|
||||
|
||||
- Keep the UI for 'Every X Days' the same for now, this will change in phase 2
|
||||
- Remove days of the week, time selectors, and checkboxes
|
||||
- Follow at 'Default Time' pattern with optional time for expiry
|
||||
|
||||
**Architecture:**
|
||||
|
||||
1. The Design Logic
|
||||
The interface shifts from a list of tasks to a set of rules.
|
||||
|
||||
- The "Base" State: You see 7 day chips (Su, Mo, Tu, We, Th, Fr, Sa) and one "Default Deadline" box.
|
||||
- The "Active" State: Days you click become "Active."
|
||||
- The "Silent" State: Any day not clicked is ignored by the system.
|
||||
|
||||
2. The Architecture
|
||||
Think of the system as having two layers of memory:
|
||||
|
||||
- The Global Layer: This holds the "Master Time" (e.g., 8:00 AM).
|
||||
- The Exception Layer: This is an empty list that only fills up if you explicitly say a day is "special."
|
||||
- The Merge Logic: When the system saves, it looks at each selected day. It asks: "Does this day have a special time in the Exception Layer? No? Okay, then use the Master Time."
|
||||
|
||||
3. The "When/Then" Flow
|
||||
Here is exactly how the interaction feels for the user:
|
||||
|
||||
- Step A: Establishing the Routine
|
||||
When you click Monday, Wednesday, and Friday...
|
||||
Then those days highlight, and the "Default Deadline" box becomes active.
|
||||
When you set that box to 8:00 AM...
|
||||
Then the system internally marks all three days as "8:00 AM."
|
||||
|
||||
- Step B: Adding a Day ()
|
||||
When you suddenly decide to add Sunday...
|
||||
Then Sunday highlights and automatically adopts the 8:00 AM deadline.
|
||||
- Step C: Breaking the Routine
|
||||
When you click "Set different time" and choose Sunday...
|
||||
Then a new, specific time box appears just for Sunday.
|
||||
When you change Sunday to 11:00 AM...
|
||||
Then the system "unhooks" Sunday from the Master Time. Sunday is now an Exception.
|
||||
- Step D: Changing the Master Time
|
||||
When you later change the "Default Deadline" from 8:00 AM to 9:00 AM...
|
||||
Then Monday, Wednesday, and Friday all update to 9:00 AM automatically.
|
||||
But Sunday stays at 11:00 AM because it is locked as an exception.
|
||||
|
||||
Instead of treating all 7 days as individual, equal data points, we treat them as a Group that follows a Rule.
|
||||
|
||||
- The Group: The days you selected (e.g., Mon, Wed, Fri, Sun).
|
||||
- The Rule: The "Default Deadline" that applies to the whole group (e.g., 8:00 AM).
|
||||
- The Exception: A specific day that breaks the rule (e.g., "Actually, make Sunday 11:00 AM").
|
||||
|
||||
**Time Selector Design:**
|
||||
We might need to create a new time selector or just add an additional time selector component
|
||||
|
||||
1. The "Columnar" Picker
|
||||
This popover is split into three distinct columns: Hours, Minutes, and AM/PM.
|
||||
When you click the time box...
|
||||
Then a small panel opens with three narrow, scrollable columns.
|
||||
The Logic: The "Minutes" column only contains four options: :00, :15, :30, :45.
|
||||
The Flow: The user's eye scans horizontally. "8" → "30" → "PM".
|
||||
|
||||
**The "High-Level" Combined Flow:**
|
||||
Selection: User clicks Monday.
|
||||
Trigger: User clicks the "Deadline" box.
|
||||
The Picker: The Columnar Picker pops up.
|
||||
The Snap: The user clicks "8" in the first column and "00" in the second.
|
||||
The Result: The box now displays "08:00 AM."
|
||||
|
||||
The "Auto-Apply" Flow
|
||||
When the user clicks a value (e.g., "AM" or "PM"), the selection is registered immediately.
|
||||
When the user clicks anywhere outside the popover, it closes automatically.
|
||||
Then the main UI updates to show the new time.
|
||||
|
||||
---
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
### Backend Models
|
||||
|
||||
`ChoreSchedule` gains two new fields (persisted in TinyDB, returned in API responses):
|
||||
|
||||
- `default_hour: int = 8` — the master deadline hour for `mode='days'`
|
||||
- `default_minute: int = 0` — the master deadline minute for `mode='days'`
|
||||
|
||||
`from_dict` defaults both to `8` / `0` for backwards compatibility with existing DB records.
|
||||
|
||||
### Frontend Models
|
||||
|
||||
`ChoreSchedule` interface gains two optional fields (optional for backwards compat with old API responses):
|
||||
|
||||
- `default_hour?: number`
|
||||
- `default_minute?: number`
|
||||
|
||||
---
|
||||
|
||||
## Frontend Design
|
||||
|
||||
- `TimePickerPopover.vue` — new shared component at `frontend/vue-app/src/components/shared/TimePickerPopover.vue`
|
||||
- `ScheduleModal.vue` — "Specific Days" section fully replaced; "Every X Days" section unchanged
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
No backend implementation required for phase 1.
|
||||
|
||||
---
|
||||
|
||||
## Backend Tests
|
||||
|
||||
- [x] No backend changes required in phase 1
|
||||
|
||||
---
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
- [x] Created `frontend/vue-app/src/components/shared/TimePickerPopover.vue`
|
||||
- Props: `modelValue: { hour: number, minute: number }`, emits `update:modelValue`
|
||||
- Displays formatted time as a clickable button (e.g. `08:00 AM`)
|
||||
- Opens a columnar popover with three columns: Hour (1–12), Minute (:00/:15/:30/:45), AM/PM
|
||||
- Clicking any column value updates the model immediately
|
||||
- Closes on outside click via `mousedown` document listener
|
||||
- Fully scoped styles using CSS variables from `colors.css`
|
||||
- [x] Refactored `ScheduleModal.vue` — "Specific Days" section
|
||||
- Replaced 7 checkbox rows + per-row `TimeSelector` with chip-based design
|
||||
- **Day chips row**: 7 short chips (Su Mo Tu We Th Fr Sa) — click to toggle active/inactive
|
||||
- **Default Deadline row**: shown when ≥1 day selected; single `TimePickerPopover` sets the master time for all non-exception days
|
||||
- **Selected day list**: one row per active day (sorted Sun→Sat); each row shows:
|
||||
- Day name
|
||||
- If no exception: italic "Default (HH:MM AM/PM)" label + "Set different time" link
|
||||
- If exception set: a `TimePickerPopover` for that day's override + "Reset to default" link
|
||||
- State: `selectedDays: Set<number>`, `defaultTime: TimeValue`, `exceptions: Map<number, TimeValue>`
|
||||
- Load logic: first `day_config` entry sets `defaultTime`; entries differing from it populate `exceptions`
|
||||
- Save logic: iterates `selectedDays`, applies exception time or falls back to `defaultTime` → `DayConfig[]`
|
||||
- "Every X Days" mode left unchanged
|
||||
- Validation: unchanged (`selectedDays.size > 0`)
|
||||
|
||||
---
|
||||
|
||||
## Frontend Tests
|
||||
|
||||
- [ ] `TimePickerPopover.vue`: renders formatted time, opens/closes popover, selecting hour/minute/period emits correct value, closes on outside click
|
||||
- [ ] `ScheduleModal.vue` (Specific Days): chip toggles add/remove from selected set; removing a day also removes its exception; setting a different time creates an exception; resetting removes exception; changing default time does not override exceptions; save payload shape matches `DayConfig[]` with correct times; loading an existing mixed-time schedule restores chips, defaultTime, and exceptions correctly
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria (Definition of Done)
|
||||
|
||||
### Backend
|
||||
|
||||
- [x] No backend changes required — existing `DayConfig { day, hour, minute }` model fully supports the new UI
|
||||
|
||||
### Frontend
|
||||
|
||||
- [x] "Specific Days" mode shows 7 day chips instead of checkboxes
|
||||
- [x] Selecting chips shows a single "Default Deadline" time picker
|
||||
- [x] Selected day list shows each active day with either its default label or an exception time picker
|
||||
- [x] "Set different time" link creates a per-day exception that overrides the default
|
||||
- [x] "Reset to default" link removes the exception and the day reverts to the master time
|
||||
- [x] Changing the Default Deadline updates all non-exception days (by using `defaultTime` at save time)
|
||||
- [x] "Every X Days" mode is unchanged
|
||||
- [x] Existing schedules load correctly (first entry = default, differing times = exceptions)
|
||||
- [x] Save payload is valid `DayConfig[]` consumed by the existing API unchanged
|
||||
- [x] New `TimePickerPopover` component: columnar Hour/Minute/AMPM picker, closes on outside click
|
||||
- [ ] Frontend component tests written and passing for `TimePickerPopover` and the refactored `ScheduleModal`
|
||||
238
.github/specs/archive/feat-calendar-chore/feat-calendar-schedule-refactor-02.md
vendored
Normal file
238
.github/specs/archive/feat-calendar-chore/feat-calendar-schedule-refactor-02.md
vendored
Normal file
@@ -0,0 +1,238 @@
|
||||
# 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.
|
||||
|
||||
2. 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.
|
||||
|
||||
Today’s 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.
|
||||
|
||||
3. PC (Desktop) Specification: Tethered Popover Calendar
|
||||
Design & Calendar Details
|
||||
Interface: A compact monthly grid (approx. 250px–300px 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).
|
||||
|
||||
4. 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
|
||||
|
||||
- [x] Update existing interval-mode tests to use `anchor_date` instead of `anchor_weekday`
|
||||
- [x] Add test: `interval_days: 1` is now valid (was previously rejected)
|
||||
- [x] Add test: `interval_has_deadline: false` is accepted and persisted
|
||||
- [x] Add test: old DB records without `anchor_date` / `interval_has_deadline` load with correct defaults
|
||||
|
||||
---
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
- [x] 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
|
||||
- [x] 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 1–7, 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
|
||||
|
||||
- [x] `DateInputField.vue`: renders the formatted date value; emits `update:modelValue` on change; `min` prop prevents selection of past dates
|
||||
- [x] `ScheduleModal.vue` (Every X Days): stepper clamps to 1–7 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
|
||||
|
||||
- [x] `anchor_weekday` removed; `anchor_date` (string) added with empty-string default for old records
|
||||
- [x] `interval_has_deadline` (bool) added, defaults to `True` for old records
|
||||
- [x] `interval_days` valid range updated to `[1, 7]`
|
||||
- [x] All existing and new backend tests pass
|
||||
|
||||
### Frontend
|
||||
|
||||
- [x] New `DateInputField` component: styled native date input, respects `min`, emits ISO string
|
||||
- [x] "Every X Days" mode shows `−`/`+` stepper for interval (1–7), `DateInputField` for anchor date, `TimePickerPopover` for deadline
|
||||
- [x] "Anytime" toggle clears the deadline (sets `interval_has_deadline = false`) and hides the time picker
|
||||
- [x] "Next occurrence" label computes and displays the next date ≥ today based on anchor + interval
|
||||
- [x] Past dates are disabled in the date input (via `min`)
|
||||
- [x] Existing schedules load correctly — `anchor_date` restored, `interval_has_deadline` restored
|
||||
- [x] Save payload is valid and consumed by the existing API unchanged
|
||||
- [x] "Specific Days" mode is unchanged
|
||||
- [x] Frontend component tests written and passing for `DateInputField` and the refactored `ScheduleModal` interval section
|
||||
Reference in New Issue
Block a user