Files
chore/.github/specs/feat-schedule-enable-disable.md
Ryan Kegel ef9cb01d92
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m39s
feat: add enable/disable toggle for chore scheduling in ScheduleModal
- 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.
2026-03-20 16:42:13 -04:00

9.2 KiB
Raw Blame History

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

  • Add field enabled: bool = True to ChoreSchedule dataclass
  • Add enabled=d.get('enabled', True) to from_dict()
  • Add 'enabled': self.enabled to to_dict()

Frontend Model — frontend/vue-app/src/common/models.ts

  • Add enabled?: boolean to ChoreSchedule interface

Backend Implementation

API — backend/api/chore_schedule_api.py

  • Accept enabled field in the schedule create/update endpoint payload
  • Persist enabled value through to the database

Scheduler Logic

  • In scheduleUtils.ts (isScheduledToday), return true when schedule.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), return null when schedule.enabled === false — a paused chore has no deadline or expiry:
    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

  • 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

  • test_create_schedule_enabled_by_default: Create a schedule without specifying enabled; verify the persisted schedule has enabled=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 to True, verify again
  • test_enabled_field_in_get_response: Fetch a schedule via GET; verify the enabled field is present in the JSON response
  • test_enabled_invalid_value_returns_400: Pass a non-boolean enabled value; verify 400 response

Model Tests

  • test_chore_schedule_from_dict_defaults_enabled: Call ChoreSchedule.from_dict({...}) without enabled; assert enabled is True
  • test_chore_schedule_from_dict_enabled_false: Call ChoreSchedule.from_dict({..., 'enabled': False}); assert enabled is False
  • 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

  • Add toggle row between ModalDialog open 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 ?? true for dirty tracking
  • Add if (scheduleEnabled.value !== origEnabled) return true to isDirty computed
  • Include enabled: scheduleEnabled.value in both setChoreSchedule payloads (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 true regardless of day
  • 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
  • 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 true even on non-hit days
  • getDueTimeToday returns null when paused: A paused schedule's getDueTimeToday returns null — no deadline, no expiry
  • treats enabled=undefined as enabled (backward compat): Schedules without enabled field 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

  • ChoreSchedule model has enabled: bool field, defaulting to True
  • API accepts and persists enabled in create/update
  • Client-side scheduler: isScheduledToday returns true for paused — chore shows every day
  • Client-side scheduler: getDueTimeToday returns null for paused — no deadline or expiry
  • All backend unit tests pass (28/28)

Frontend

  • Toggle row renders in ScheduleModal between heading and mode toggle
  • Toggle uses --btn-primary for ON and --form-input-border for OFF
  • Form body dims with opacity: 0.45 and pointer-events: none when paused
  • enabled field is included in save payloads for both modes
  • Dirty detection tracks enabled changes
  • Toggle meets 44px minimum touch target
  • All frontend scheduleUtils tests pass (38/38)