- 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.
9.2 KiB
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:
#fffwith subtle box-shadow - Dimensions: 48×24px track, 20px circular thumb, 44px min-height row (mobile touch target)
- Uses
role="switch"andaria-checkedfor 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
- Add field
enabled: bool = TruetoChoreScheduledataclass - Add
enabled=d.get('enabled', True)tofrom_dict() - Add
'enabled': self.enabledtoto_dict()
Frontend Model — frontend/vue-app/src/common/models.ts
- Add
enabled?: booleantoChoreScheduleinterface
Backend Implementation
API — backend/api/chore_schedule_api.py
- Accept
enabledfield in the schedule create/update endpoint payload - Persist
enabledvalue through to the database
Scheduler Logic
- In
scheduleUtils.ts(isScheduledToday), returntruewhenschedule.enabled === false— a paused chore shows every day, like an unscheduled chore:if (schedule.enabled === false) return true; // paused = show always, like an unscheduled chore - In
scheduleUtils.ts(getDueTimeToday), returnnullwhenschedule.enabled === false— a paused chore has no deadline or expiry:if (schedule.enabled === false) return null; // paused = no deadline, no expiryNote: 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
- Existing
chore_schedule_updatedevent is sufficient (the payload includes the full schedule object, which will now containenabled)
Backend Tests
Unit Tests — backend/tests/test_chore_schedule_api.py
- test_create_schedule_enabled_by_default: Create a schedule without specifying
enabled; verify the persisted schedule hasenabled=True - test_create_schedule_with_enabled_false: Create a schedule with
enabled=False; verify it persists correctly - test_update_schedule_toggle_enabled: Create a schedule (enabled), update it with
enabled=False, verify the change persists; toggle back toTrue, verify again - test_enabled_field_in_get_response: Fetch a schedule via GET; verify the
enabledfield is present in the JSON response - test_enabled_invalid_value_returns_400: Pass a non-boolean
enabledvalue; verify 400 response
Model Tests
- test_chore_schedule_from_dict_defaults_enabled: Call
ChoreSchedule.from_dict({...})withoutenabled; assertenabled is True - test_chore_schedule_from_dict_enabled_false: Call
ChoreSchedule.from_dict({..., 'enabled': False}); assertenabled is False - test_chore_schedule_to_dict_includes_enabled: Create a
ChoreScheduleinstance; callto_dict(); assert'enabled'key is present with correct value
Frontend Implementation
Component — frontend/vue-app/src/components/shared/ScheduleModal.vue
Template
- Add toggle row between
ModalDialogopen and mode toggle - Wrap mode toggle + form content in
<div class="schedule-body" :class="{ disabled: !scheduleEnabled }"> - Keep action buttons (Cancel / Save) outside the dim wrapper
Script
- Add
const scheduleEnabled = ref<boolean>(props.schedule?.enabled ?? true) - Add
const origEnabled = props.schedule?.enabled ?? truefor dirty tracking - Add
if (scheduleEnabled.value !== origEnabled) return truetoisDirtycomputed - Include
enabled: scheduleEnabled.valuein bothsetChoreSchedulepayloads (days + interval modes)
CSS
- Add toggle row styles (
.schedule-toggle-row,.toggle-label) - Add toggle track/thumb styles (
.toggle-track,.toggle-track.on,.toggle-thumb) - 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
- returns true when enabled is false on a scheduled day (days mode): A paused schedule always returns
trueregardless of day - returns true when enabled is false on an unscheduled day (days mode): A paused schedule returns
trueeven on days that would normally be skipped - returns true when enabled is false on a hit day (interval mode): A paused interval schedule always returns
true - returns true when enabled is false on a non-hit day (interval mode): A paused interval schedule returns
trueeven on non-hit days - getDueTimeToday returns null when paused: A paused schedule's
getDueTimeTodayreturnsnull— no deadline, no expiry - treats enabled=undefined as enabled (backward compat): Schedules without
enabledfield behave as enabled
CSS Spec
/* ── 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
ChoreSchedulemodel hasenabled: boolfield, defaulting toTrue- API accepts and persists
enabledin create/update - Client-side scheduler:
isScheduledTodayreturnstruefor paused — chore shows every day - Client-side scheduler:
getDueTimeTodayreturnsnullfor paused — no deadline or expiry - All backend unit tests pass (28/28)
Frontend
- Toggle row renders in
ScheduleModalbetween heading and mode toggle - Toggle uses
--btn-primaryfor ON and--form-input-borderfor OFF - Form body dims with
opacity: 0.45andpointer-events: nonewhen paused enabledfield is included in save payloads for both modes- Dirty detection tracks
enabledchanges - Toggle meets 44px minimum touch target
- All frontend scheduleUtils tests pass (38/38)