# 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)
