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:
162
.github/specs/archive/bugs-1.0.5-001.md
vendored
Normal file
162
.github/specs/archive/bugs-1.0.5-001.md
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
# Bug List
|
||||
|
||||
**Feature Bugs:** .github/specs/feat-calendar-chore/feat-calendar-chore.md
|
||||
|
||||
## Bugs
|
||||
|
||||
### Kabab menu icon should only appear when the item is 'selected'
|
||||
|
||||
When a chore is 'selected' (clicked on for the first time) the kebab menu should show. The kebab menu on the chore should hide again if anywhere else is clicked except the item or it's kebab menu
|
||||
This is similar to how the 'edit' icon appears on penalties and rewards.
|
||||
|
||||
**Fix — `ParentView.vue`:**
|
||||
|
||||
- Add `selectedChoreId = ref<string | null>(null)` alongside `activeMenuFor`
|
||||
- In the chore card-click handler, set `selectedChoreId.value = item.id`; clicking the same card again deselects it
|
||||
- In `onDocClick` (the capture-phase outside-click handler), also clear `selectedChoreId.value = null` when clicking outside both the card and the kebab wrap
|
||||
- Change the `.kebab-btn` from always-rendered to `v-show="selectedChoreId === item.id"`
|
||||
|
||||
#### Additional Test Cases With This Fix
|
||||
|
||||
- [x] Kebab button is not visible before a chore card is clicked
|
||||
- [x] Kebab button becomes visible after a chore card is clicked
|
||||
- [ ] Kebab button hides when clicking outside the card and kebab area _(integration only — covered by onDocClick logic, not unit-testable)_
|
||||
- [x] Kebab button hides when a different card is clicked
|
||||
|
||||
### User Test Findings:
|
||||
|
||||
- chore-inactive::before applies an inset and background that is too dark. It should more like a gray disabled state.
|
||||
- an inactive chore shows a dark kebab menu icon, so it is very hard to see since it is too dark.
|
||||
|
||||
**Fix applied:**
|
||||
|
||||
- `::before` overlay changed from `rgba(0,0,0,0.45)` → `rgba(160,160,160,0.45)` and grayscale `60%` → `80%` for a lighter, more neutral gray disabled appearance
|
||||
- Added `:deep(.chore-inactive) .kebab-btn { color: rgba(255,255,255,0.85) }` so the kebab icon remains visible against the gray overlay
|
||||
|
||||
---
|
||||
|
||||
### In the scheduler, the cancel button should look like others
|
||||
|
||||
The cancel button doesn't follow current css design rules. It should look like other cancel buttons
|
||||
Incorrect cancel button: https://git.ryankegel.com/ryan/chore/attachments/a4a3a0eb-0acc-481d-8794-4d666f214e0a
|
||||
Correct cancel button: https://git.ryankegel.com/ryan/chore/attachments/63033ee6-35e8-4dd3-8be4-bce4a8c576fc
|
||||
|
||||
**Fix — `ScheduleModal.vue`:**
|
||||
|
||||
- Change `<button class="btn-secondary"` → `<button class="btn btn-secondary"` to use the global `styles.css` button classes
|
||||
- Remove the local `.btn-secondary` and `.btn-secondary:disabled` CSS blocks from `<style scoped>`
|
||||
|
||||
#### Additional Test Cases With This Fix
|
||||
|
||||
- []
|
||||
|
||||
### User Test Findings:
|
||||
|
||||
Fix confirmed
|
||||
|
||||
---
|
||||
|
||||
### Chore scheduler needs to be modal so that it cannot be clicked out of
|
||||
|
||||
The chore scheduler model should not disappear when clicked outside of. It should only disappear with either Save or Cancel clicked.
|
||||
|
||||
**Fix — `ScheduleModal.vue`:**
|
||||
|
||||
- Remove `@backdrop-click="$emit('cancelled')"` from the `<ModalDialog>` opening tag
|
||||
- The modal will then only close via the Save or Cancel buttons
|
||||
|
||||
#### Additional Test Cases With This Fix
|
||||
|
||||
- [x] Clicking outside the ScheduleModal backdrop does not dismiss it
|
||||
- [x] Clicking Cancel dismisses the modal
|
||||
- [x] Clicking Save dismisses the modal
|
||||
|
||||
### User Test Findings:
|
||||
|
||||
Fix confirmed
|
||||
|
||||
---
|
||||
|
||||
### In parent view, a chore that is not on the current day has it's kebab menu 'grayed out', it should not be
|
||||
|
||||
The dropdown menu from the chore kebab menu should not inherit the disabled color of the item card. It gives the impression that the menu is disabled.
|
||||
Reference of grayed out menu: https://git.ryankegel.com/ryan/chore/attachments/0ac50dae-9b60-4cf7-a9f4-c980525d72f8
|
||||
|
||||
**Fix — `ParentView.vue` and `ChildView.vue`:**
|
||||
CSS `opacity` on a parent is always inherited and cannot be overridden by children. Replace the direct `opacity + filter` on `.chore-inactive` with a pseudo-element overlay instead, so the `.chore-stamp` and `.chore-kebab-wrap` can sit above it at full opacity:
|
||||
|
||||
- Change `:deep(.chore-inactive)` from `opacity: 0.45; filter: grayscale(60%)` to use `position: relative` only
|
||||
- Add `:deep(.chore-inactive::before)` pseudo-element: `position: absolute; inset: 0; background: rgba(0,0,0,0.45); filter: grayscale(60%); z-index: 1; pointer-events: none; border-radius: inherit`
|
||||
- Give `.chore-stamp` `z-index: 3` (above the overlay)
|
||||
- Give `.chore-kebab-wrap` `z-index: 2` (above the overlay, below the stamp)
|
||||
|
||||
#### Additional Test Cases With This Fix
|
||||
|
||||
- []
|
||||
|
||||
### User Test Findings:
|
||||
|
||||
Fix confirmed
|
||||
|
||||
---
|
||||
|
||||
### Change the due by to be bigger
|
||||
|
||||
In both parent and child view, when a chore is scheduled and has a due time, I see the text 'Due by' in parent mode, and just the time in child mode.
|
||||
In both parent and child view, the text needs to be bigger: the due-label should be 0.85 rem font size.
|
||||
In child mode, the time a chore is due has a dark color that is hard to see. It should be the same color that is in parent view.
|
||||
|
||||
**Fix — `ParentView.vue`:**
|
||||
|
||||
- Change `.due-label { font-size: 0.72rem }` → `font-size: 0.85rem`
|
||||
|
||||
**Fix — `ChildView.vue`:**
|
||||
|
||||
- Change `.due-label { font-size: 0.72rem }` → `font-size: 0.85rem`
|
||||
- Change `.due-label` color from `var(--due-label-color, #aaa)` → `var(--item-points-color, #ffd166)` to match parent view
|
||||
- Change `choreDueLabel()` to return `"Due by " + formatDueTimeLabel(...)` instead of just the time string
|
||||
|
||||
#### Additional Test Cases With This Fix
|
||||
|
||||
- [x] `choreDueLabel()` in ChildView returns `"Due by 3:00 PM"` format (not just `"3:00 PM"`)
|
||||
|
||||
### User Test Findings:
|
||||
|
||||
The text size is good, but I want to change the text color to red. Create a new color in colors.css called --text-bad-color, it should be #ef4444
|
||||
|
||||
**Fix applied:**
|
||||
|
||||
- Added `--text-bad-color: #ef4444` to `colors.css`
|
||||
- Updated `.due-label` color in both `ParentView.vue` and `ChildView.vue` to use `var(--text-bad-color, #ef4444)`
|
||||
|
||||
---
|
||||
|
||||
### In parent mode, when a chore is too late. The banner shouldn't be 'grayed' out. Same in child mode - color should also be red in the banner
|
||||
|
||||
When a chore is too late, the 'TOO LATE' banner is also grayed out but it should not be. Also the font color for 'TOO LATE' needs to be consistent. Make both colors red according to colors.css
|
||||
|
||||
**Fix — `ChildView.vue`:**
|
||||
|
||||
- The TOO LATE `<span class="pending">` uses the `.pending` class which is styled green (`--pending-block-color`). Change it to `<span class="chore-stamp">` to match ParentView's class
|
||||
- Add `.chore-stamp` CSS to ChildView matching ParentView's definition: `color: var(--btn-danger, #ef4444)`
|
||||
- The grayout issue is resolved by the pseudo-element overlay fix in Bug 4 above — `.chore-stamp` at `z-index: 3` sits above the overlay at full opacity
|
||||
|
||||
**Fix — `ParentView.vue`:**
|
||||
|
||||
- No color change needed (already `color: var(--btn-danger, #ef4444)`)
|
||||
- The grayout issue is resolved by the pseudo-element overlay fix in Bug 4 above
|
||||
|
||||
#### Additional Test Cases With This Fix
|
||||
|
||||
- []
|
||||
|
||||
### User Test Findings:
|
||||
|
||||
chore-stamp background seems too dark, change it to rgba(34,34,34,0.65). Also give it text-shadow: var(--item-points-shadow)
|
||||
the TOO LATE text should not be using color --btn-danger. It should use the newly created color --text-bad-color
|
||||
|
||||
**Fix applied:**
|
||||
|
||||
- `.chore-stamp` background changed from `rgba(34,34,43,0.85)` → `rgba(34,34,34,0.65)` in both `ParentView.vue` and `ChildView.vue`
|
||||
- Added `text-shadow: var(--item-points-shadow)` to `.chore-stamp` in both files
|
||||
- Color changed from `var(--btn-danger, #ef4444)` → `var(--text-bad-color, #ef4444)` in both files
|
||||
125
.github/specs/archive/bugs-1.0.5-002.md
vendored
Normal file
125
.github/specs/archive/bugs-1.0.5-002.md
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
# Bug List
|
||||
|
||||
**Feature Bugs:** .github/specs/feat-calendar-chore/feat-calendar-chore.md
|
||||
|
||||
## Bugs
|
||||
|
||||
### When rescheduling an extended time chore, the extended time should be reset(removed)
|
||||
|
||||
When a chore has had it's time extended, but later edited through the scheduler and saved, the extended time for this chore should be reset. (A previous too late chore would again be too late if the current time is past the newly changed schedule)
|
||||
|
||||
**Root cause:** The PUT handler in `chore_schedule_api.py` saved the new schedule but never deleted the `TaskExtension` record for that child+task pair.
|
||||
|
||||
**Fix:**
|
||||
|
||||
- `backend/db/task_extensions.py` — Added `delete_extension_for_child_task(child_id, task_id)` that removes the `TaskExtension` matching both child and task IDs.
|
||||
- `backend/api/chore_schedule_api.py` — Imported the new function and called it immediately before `upsert_schedule(schedule)` in the PUT handler.
|
||||
|
||||
#### Additional Test Cases With This Fix
|
||||
|
||||
- []
|
||||
|
||||
### User Test Findings:
|
||||
|
||||
This flow produces a bug
|
||||
|
||||
1. A chore that is 'too late' (scheduled expiry time is 8:00am, but the current time is 3pm)
|
||||
2. Extend the time on that chore causes the 'too late' to disappear. The chore is now valid again.
|
||||
3. Enter the scheduler on that chore and set the expiry time to 9am (when it is 3pm in real time) the chore stays active -- I expect the chore to go back to the 'too late' state since the extended time was reset again. It should revert back to 'too late'
|
||||
|
||||
**Analysis:** The backend fix (deleting the extension on PUT) is correct. Step 3 failing is a side effect of Bug 3 (Extend Time not refreshing the UI) — the frontend was in a stale state after step 2, so step 3 was operating on stale item data. Once Bug 3 is fixed (direct refresh in `doExtendTime`), step 2 reliably updates the UI, and step 3's `onScheduleSaved → refresh()` then correctly re-evaluates `isChoreExpired` against the fresh data (no `extension_date`, 9am schedule → TOO LATE). No additional frontend changes needed beyond the backend deletion fix.
|
||||
|
||||
---
|
||||
|
||||
### Extend time is cut off of chore kebab menu
|
||||
|
||||
The dropdown for the kebab menu does not extend far enough to show all the items. It should show all the items.
|
||||
https://git.ryankegel.com/ryan/chore/attachments/951592da-29a2-4cca-912e-9b160eb2f19c
|
||||
|
||||
**Root cause:** The `.kebab-menu` is `position: absolute` inside `.chore-kebab-wrap`, which lives inside a `ScrollingList` card. The scroll wrapper has `overflow-y: hidden`, which clips any absolutely positioned content that extends below the card boundary. CSS cannot set `overflow-x: auto` and `overflow-y: visible` simultaneously — the browser coerces `visible` to `auto`.
|
||||
|
||||
**Fix:** (`frontend/vue-app/src/components/child/ParentView.vue`)
|
||||
|
||||
- Added `menuPosition = ref({ top: 0, left: 0 })` and `kebabBtnRefs = ref<Map<string, HTMLElement>>(new Map())`.
|
||||
- Added a `:ref` callback on `.kebab-btn` to register/unregister each button element keyed by `item.id`.
|
||||
- `openChoreMenu` now captures `getBoundingClientRect()` on the trigger button and stores `{ top: rect.bottom, left: rect.right - 140 }` into `menuPosition`.
|
||||
- Wrapped `.kebab-menu` in `<Teleport to="body">` and switched it to `position: fixed` with inline `:style` driven by `menuPosition`. `z-index` raised to `9999`.
|
||||
- `onDocClick` required no changes — it already checks `.kebab-menu` via `composedPath()`, which traverses the real DOM regardless of teleport destination.
|
||||
|
||||
#### Additional Test Cases With This Fix
|
||||
|
||||
- []
|
||||
|
||||
### User Test Findings:
|
||||
|
||||
Fix confirmed
|
||||
|
||||
---
|
||||
|
||||
### When a chore has passed it's time or when an time expired chore is extended, it should be updated right away in all UIs.
|
||||
|
||||
An SSE event should happen when a chore has been extended or expired so that the UI updates. Currently, the user has to refresh to see this change.
|
||||
|
||||
**Root cause:** In `ParentView.vue`, three call sites called `resetExpiryTimers()` synchronously — before `refresh()` resolved — so new timers were set against stale item data. `ChildView.vue` already delayed the call with `setTimeout(..., 300)` correctly.
|
||||
|
||||
**Fix:** (`frontend/vue-app/src/components/child/ParentView.vue`)
|
||||
|
||||
- In `handleChoreScheduleModified`, `handleChoreTimeExtended`, and `onScheduleSaved`: replaced `resetExpiryTimers()` with `setTimeout(() => resetExpiryTimers(), 300)` to match the existing correct pattern in `ChildView.vue`.
|
||||
|
||||
#### Additional Test Cases With This Fix
|
||||
|
||||
- []
|
||||
|
||||
### User Test Findings:
|
||||
|
||||
I'm still not seeing the chore update back to active state when 'Extend time' is selected from the menu. I have to refresh the page to see the change.
|
||||
|
||||
**Root cause of remaining failure:** `doExtendTime` posted to the API and relied entirely on the SSE `chore_time_extended` event to trigger a refresh. If SSE delivery is delayed or the event is missed, the UI never updates. The previous fix (timer delay) only corrected expiry timer scheduling, not the extend-time response path.
|
||||
|
||||
**Additional fix:** (`frontend/vue-app/src/components/child/ParentView.vue`)
|
||||
|
||||
- In `doExtendTime`: after a successful API response, call `childChoreListRef.value?.refresh()` and `setTimeout(() => resetExpiryTimers(), 300)` directly. Added an early `return` on error to prevent a refresh on failure.
|
||||
|
||||
---
|
||||
|
||||
### On the Every X Days scheduler, the every [x] input box and day dropbox should be on the same line
|
||||
|
||||
Currently they are on seperate lines.
|
||||
|
||||
**Root cause:** `.interval-row` and `.interval-time-row` shared a combined CSS rule that included `flex-wrap: wrap`, causing the interval input and day select to wrap onto a new line at the modal's narrow width.
|
||||
|
||||
**Fix:** (`frontend/vue-app/src/components/shared/ScheduleModal.vue`)
|
||||
|
||||
- Split the combined `.interval-row, .interval-time-row` rule into two separate rules. Removed `flex-wrap: wrap` from `.interval-row` only; kept it on `.interval-time-row`.
|
||||
|
||||
#### Additional Test Cases With This Fix
|
||||
|
||||
- []
|
||||
|
||||
### User Test Findings:
|
||||
|
||||
Fix confirmed
|
||||
|
||||
---
|
||||
|
||||
### In the chore scheduler, the time selector needs to be much smaller
|
||||
|
||||
The time selector should have a smaller font size, borders, up/down arrows. A font size of 1rem should be fine.
|
||||
|
||||
**Root cause:** `.time-value` had `font-size: 1.4rem` and `.time-separator` had `font-size: 1.6rem`. All button/value boxes were `2.5rem` wide/tall. A `@media (max-width: 480px)` block made them even larger on small screens.
|
||||
|
||||
**Fix:** (`frontend/vue-app/src/components/shared/TimeSelector.vue`)
|
||||
|
||||
- `.arrow-btn` and `.time-value`: width/height `2.5rem` → `1.8rem`; arrow font `0.85rem` → `0.75rem`.
|
||||
- `.time-value`: `font-size: 1.4rem` → `1rem`.
|
||||
- `.time-separator`: `font-size: 1.6rem` → `1rem`.
|
||||
- `.ampm-btn`: width `3rem` → `2.2rem`; height `2.5rem` → `1.8rem`; font `0.95rem` → `0.8rem`.
|
||||
- Removed the `@media (max-width: 480px)` block that was enlarging all sizes on mobile.
|
||||
|
||||
#### Additional Test Cases With This Fix
|
||||
|
||||
- [x]
|
||||
|
||||
### User Test Findings:
|
||||
|
||||
Fix confirmed
|
||||
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