feat: add enable/disable toggle for chore scheduling in ScheduleModal
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m39s

- Introduced a toggle button to enable or disable chores in the scheduler.
- The toggle will be available in both types of schedulers.
- Added UI design proposal and considerations for mobile dimensions.
- Included E2E tests to ensure toggle state persistence and correct behavior in child view.
This commit is contained in:
2026-03-20 16:42:13 -04:00
parent db6e0a7ce8
commit ef9cb01d92
45 changed files with 3543 additions and 232 deletions

73
.github/agents/ui-planner.agent.md vendored Normal file
View File

@@ -0,0 +1,73 @@
---
name: ui-planner
description: Expert UI/UX design review and creative alternative layouts for Vue 3 components. Use when asking for design critiques, layout ideas, visual polish, animation suggestions, or CSS improvements on existing .vue files.
argument-hint: review this design
tools:
- read
- search
- edit
- vscode
- web
- todo
---
You are a Senior UI/UX Architect specializing in clean, highly reactive web applications. Your goal is to provide expert design critiques and creative layout alternatives that are technically compatible with Vue 3 and this project's conventions.
## 🏗 Project Context
This is the **Reward** app — a family chore/reward tracker. Vue 3 (Composition API / `<script setup lang="ts">`) frontend. Key files:
- Theme tokens: `frontend/vue-app/src/assets/colors.css`**always read this first** when reviewing a component.
- Layout wrappers: `ParentLayout` (admin views) and `ChildLayout` (child dashboard views).
- All `.vue` files use `<style scoped>`. Child component styling uses `:deep()` selectors.
- File order within `.vue` files: `<template>`, then `<script>`, then `<style>`.
### Available Design Tokens (from `colors.css`)
**Brand:** `--primary` (#667eea), `--secondary` (#7257b3), `--accent` (#cbd5e1)
**Header:** `--header-bg` (gradient primary → secondary)
**Buttons:** `--btn-primary`, `--btn-primary-hover`, `--btn-secondary`, `--btn-secondary-hover`, `--btn-secondary-text`, `--btn-danger`, `--btn-danger-hover`, `--btn-green`, `--btn-green-hover`
**List items:** `--list-item-bg`, `--list-item-bg-good` (#8dabfd), `--list-item-bg-bad` (#f98a8a), `--list-item-bg-reward` (#4ed271), `--list-item-border-good`, `--list-item-border-bad`, `--list-item-border-reward`
**Forms:** `--form-bg`, `--form-shadow`, `--form-heading`, `--form-label`, `--form-input-bg`, `--form-input-border`
**Modals/Cards:** `--modal-bg`, `--modal-shadow`, `--card-bg`, `--card-shadow`, `--card-title`
**Feedback:** `--error`, `--error-bg`, `--loading-color`, `--list-loading-color`
**FAB:** `--fab-bg`, `--fab-hover-bg`, `--fab-active-bg`
## 🛠 Technical Constraints
- **No utility frameworks.** No Tailwind, no Bootstrap.
- **Layout:** Flexbox and CSS Grid only.
- **Animations:** Vue `<Transition>` / `<TransitionGroup>` for state changes; `@keyframes` for loaders.
- **Theming:** Only use `var(--token)` from `colors.css` for any color, shadow, or spacing token. Never hardcode hex values.
- **Component-first:** Consider how changes affect child component slots and reusability.
---
## Workflow
When a component is shared, always **read the file** plus `colors.css` before responding.
### Step 1 — Design Audit
Critique the current UI concisely:
1. **Visual Hierarchy:** Does the most important information catch the eye first?
2. **State Clarity:** Are loading / empty / pending / success / error states visually distinct?
3. **Consistency:** Does spacing, typography, and color usage feel cohesive with the rest of the app?
### Step 2 — Creative Exploration
Offer a range of design directions — not a fixed number, but varied **perspectives**:
- **Refinement:** Polish what exists for better clarity or polish.
- **Layout Shift:** Reorganize spatial relationships of elements.
- **Visual Metaphor:** New ways to represent status (icons vs. text vs. color-coding vs. progress indicators).
- **Interactivity:** Transitions, micro-animations, or hover states that make the UI feel alive.
### Step 3 — Implementation Spec
For any direction the user wants to pursue, produce a complete spec:
- **Template:** Semantic HTML using existing project components and slot patterns.
- **CSS:** Complete `<style scoped>` rules using only `var(--token)` design tokens.
- **Logic:** TypeScript / Vue reactivity changes needed (computed props, refs, transitions).

View File

@@ -0,0 +1,242 @@
# Feature: Chore Schedule Enable/Disable Toggle
## Overview
**Goal:** Allow parents to pause and resume a chore schedule without deleting it. A paused schedule retains all its configuration (days, intervals, deadlines) but acts as if the chore had no schedule — it shows every day with no deadline or expiry — until re-enabled.
**User Story:**
As a parent, I want to temporarily pause a chore's schedule so my child still sees it every day but it loses its deadline constraints, without losing the schedule configuration I've set up.
**Rules:**
follow .github/copilot-instructions.md
---
## UI Design
### Toggle Placement
A dedicated row between the `ModalDialog` heading and the mode toggle ("Specific Days" / "Every X Days"):
```
┌────────────────────────────────────┐
│ 🛏️ Schedule Chore │ heading (unchanged)
│ Make your bed │
│ │
│ Schedule ━━━━━━━━━━━━━━━● ON │ ← new toggle row
│ │
│ [Specific Days] [Every X Days] │ mode toggle (unchanged)
│ ... │
└────────────────────────────────────┘
```
- Adds ~44px of height; does not modify `ModalDialog.vue`.
- Sits above all config, reflecting its role as the highest-priority decision ("Is this schedule even active?").
### Toggle Widget
Clean slider (no text inside the track) + external label:
- **ON:** Track = `var(--btn-primary)` (#667eea), label = "Enabled"
- **OFF:** Track = `var(--form-input-border)` (#cbd5e1), label = "Paused"
- **Thumb:** `#fff` with subtle box-shadow
- **Dimensions:** 48×24px track, 20px circular thumb, 44px min-height row (mobile touch target)
- Uses `role="switch"` and `aria-checked` for accessibility.
### Dim-on-Disable
When the toggle is OFF, the form body (mode toggle + all form fields) receives `opacity: 0.45; pointer-events: none`. The action buttons (Cancel / Save) remain fully interactive so the user can save the "paused" state.
### Color Rationale
Green (`--btn-green`) is reserved for "reward" semantics in this app. Brand blue (`--btn-primary`) is the correct choice for a generic on/off state, matching the mode toggle, day chips, and primary CTA button.
---
## Data Model Changes
### Backend Model — `backend/models/chore_schedule.py`
- [x] Add field `enabled: bool = True` to `ChoreSchedule` dataclass
- [x] Add `enabled=d.get('enabled', True)` to `from_dict()`
- [x] Add `'enabled': self.enabled` to `to_dict()`
### Frontend Model — `frontend/vue-app/src/common/models.ts`
- [x] Add `enabled?: boolean` to `ChoreSchedule` interface
---
## Backend Implementation
### API — `backend/api/chore_schedule_api.py`
- [x] Accept `enabled` field in the schedule create/update endpoint payload
- [x] Persist `enabled` value through to the database
### Scheduler Logic
- [x] In `scheduleUtils.ts` (`isScheduledToday`), return `true` when `schedule.enabled === false` — a paused chore shows every day, like an unscheduled chore:
```typescript
if (schedule.enabled === false) return true; // paused = show always, like an unscheduled chore
```
- [x] In `scheduleUtils.ts` (`getDueTimeToday`), return `null` when `schedule.enabled === false` — a paused chore has no deadline or expiry:
```typescript
if (schedule.enabled === false) return null; // paused = no deadline, no expiry
```
> Note: Scheduling is evaluated client-side. The original spec referenced Python; this was updated to reflect the actual architecture.
> Note: Behavior was revised from "hide chore" to "show always, no deadline" — paused acts like the chore has no schedule at all.
### SSE Events
- [x] Existing `chore_schedule_updated` event is sufficient (the payload includes the full schedule object, which will now contain `enabled`)
---
## Backend Tests
### Unit Tests — `backend/tests/test_chore_schedule_api.py`
- [x] **test_create_schedule_enabled_by_default**: Create a schedule without specifying `enabled`; verify the persisted schedule has `enabled=True`
- [x] **test_create_schedule_with_enabled_false**: Create a schedule with `enabled=False`; verify it persists correctly
- [x] **test_update_schedule_toggle_enabled**: Create a schedule (enabled), update it with `enabled=False`, verify the change persists; toggle back to `True`, verify again
- [x] **test_enabled_field_in_get_response**: Fetch a schedule via GET; verify the `enabled` field is present in the JSON response
- [x] **test_enabled_invalid_value_returns_400**: Pass a non-boolean `enabled` value; verify 400 response
### Model Tests
- [x] **test_chore_schedule_from_dict_defaults_enabled**: Call `ChoreSchedule.from_dict({...})` without `enabled`; assert `enabled is True`
- [x] **test_chore_schedule_from_dict_enabled_false**: Call `ChoreSchedule.from_dict({..., 'enabled': False})`; assert `enabled is False`
- [x] **test_chore_schedule_to_dict_includes_enabled**: Create a `ChoreSchedule` instance; call `to_dict()`; assert `'enabled'` key is present with correct value
---
## Frontend Implementation
### Component — `frontend/vue-app/src/components/shared/ScheduleModal.vue`
#### Template
- [x] Add toggle row between `ModalDialog` open and mode toggle
- [x] Wrap mode toggle + form content in `<div class="schedule-body" :class="{ disabled: !scheduleEnabled }">`
- [x] Keep action buttons (Cancel / Save) outside the dim wrapper
#### Script
- [x] Add `const scheduleEnabled = ref<boolean>(props.schedule?.enabled ?? true)`
- [x] Add `const origEnabled = props.schedule?.enabled ?? true` for dirty tracking
- [x] Add `if (scheduleEnabled.value !== origEnabled) return true` to `isDirty` computed
- [x] Include `enabled: scheduleEnabled.value` in both `setChoreSchedule` payloads (days + interval modes)
#### CSS
- [x] Add toggle row styles (`.schedule-toggle-row`, `.toggle-label`)
- [x] Add toggle track/thumb styles (`.toggle-track`, `.toggle-track.on`, `.toggle-thumb`)
- [x] Add dim wrapper styles (`.schedule-body`, `.schedule-body.disabled`)
### Design Tokens — `frontend/vue-app/src/assets/colors.css` (optional)
- [ ] Optionally add reusable toggle tokens (not needed — existing tokens used directly)
---
## Frontend Tests
### scheduleUtils Tests — `frontend/vue-app/src/__tests__/scheduleUtils.spec.ts`
- [x] **returns true when enabled is false on a scheduled day (days mode)**: A paused schedule always returns `true` regardless of day
- [x] **returns true when enabled is false on an unscheduled day (days mode)**: A paused schedule returns `true` even on days that would normally be skipped
- [x] **returns true when enabled is false on a hit day (interval mode)**: A paused interval schedule always returns `true`
- [x] **returns true when enabled is false on a non-hit day (interval mode)**: A paused interval schedule returns `true` even on non-hit days
- [x] **getDueTimeToday returns null when paused**: A paused schedule's `getDueTimeToday` returns `null` — no deadline, no expiry
- [x] **treats enabled=undefined as enabled (backward compat)**: Schedules without `enabled` field behave as enabled
---
## CSS Spec
```css
/* ── Enable/Disable Toggle ────────────────────── */
.schedule-toggle-row {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.6rem;
margin-bottom: 1rem;
min-height: 44px;
}
.toggle-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--form-label, #444);
user-select: none;
}
.toggle-track {
position: relative;
width: 48px;
height: 24px;
border-radius: 999px;
border: none;
background: var(--form-input-border, #cbd5e1);
cursor: pointer;
transition: background 0.2s ease;
padding: 0;
flex-shrink: 0;
}
.toggle-track.on {
background: var(--btn-primary, #667eea);
}
.toggle-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
transition: transform 0.2s ease;
}
.toggle-track.on .toggle-thumb {
transform: translateX(24px);
}
/* ── Dim wrapper when paused ──────────────────── */
.schedule-body {
transition: opacity 0.2s ease;
}
.schedule-body.disabled {
opacity: 0.45;
pointer-events: none;
}
```
---
## Acceptance Criteria (Definition of Done)
### Backend
- [x] `ChoreSchedule` model has `enabled: bool` field, defaulting to `True`
- [x] API accepts and persists `enabled` in create/update
- [x] Client-side scheduler: `isScheduledToday` returns `true` for paused — chore shows every day
- [x] Client-side scheduler: `getDueTimeToday` returns `null` for paused — no deadline or expiry
- [x] All backend unit tests pass (28/28)
### Frontend
- [x] Toggle row renders in `ScheduleModal` between heading and mode toggle
- [x] Toggle uses `--btn-primary` for ON and `--form-input-border` for OFF
- [x] Form body dims with `opacity: 0.45` and `pointer-events: none` when paused
- [x] `enabled` field is included in save payloads for both modes
- [x] Dirty detection tracks `enabled` changes
- [x] Toggle meets 44px minimum touch target
- [x] All frontend scheduleUtils tests pass (38/38)