chore: archive v1.2 phase directories to milestones/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
---
|
||||
phase: 09-task-creation-ux
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- lib/features/tasks/presentation/task_form_screen.dart
|
||||
- lib/l10n/app_de.arb
|
||||
- lib/l10n/app_localizations.dart
|
||||
- lib/l10n/app_localizations_de.dart
|
||||
autonomous: false
|
||||
requirements: [TCX-01, TCX-02, TCX-03, TCX-04]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Frequency section shows 4 shortcut chips (Taeglich, Woechentlich, Alle 2 Wochen, Monatlich) above the freeform picker"
|
||||
- "The freeform 'Every [N] [unit]' picker row is always visible — not hidden behind a Custom toggle"
|
||||
- "Tapping a shortcut chip highlights it AND populates the picker with the corresponding values"
|
||||
- "Editing the picker number or unit manually deselects any highlighted chip"
|
||||
- "Any arbitrary interval (e.g., every 5 days, every 3 weeks, every 2 months) can be entered directly in the freeform picker"
|
||||
- "Editing an existing daily task shows 'Taeglich' chip highlighted and picker showing 1/Tage"
|
||||
- "Editing an existing quarterly task (3 months) shows no chip highlighted and picker showing 3/Monate"
|
||||
- "Saving a task from the new picker produces the correct IntervalType and intervalDays values"
|
||||
artifacts:
|
||||
- path: "lib/features/tasks/presentation/task_form_screen.dart"
|
||||
provides: "Reworked frequency picker with shortcut chips + freeform picker"
|
||||
contains: "_ShortcutFrequency"
|
||||
- path: "lib/l10n/app_de.arb"
|
||||
provides: "German l10n strings for shortcut chip labels"
|
||||
contains: "frequencyShortcutDaily"
|
||||
key_links:
|
||||
- from: "lib/features/tasks/presentation/task_form_screen.dart"
|
||||
to: "lib/features/tasks/domain/frequency.dart"
|
||||
via: "IntervalType enum and FrequencyInterval for _resolveFrequency mapping"
|
||||
pattern: "IntervalType\\."
|
||||
- from: "lib/features/tasks/presentation/task_form_screen.dart"
|
||||
to: "lib/l10n/app_de.arb"
|
||||
via: "AppLocalizations for chip labels and picker labels"
|
||||
pattern: "l10n\\.frequencyShortcut"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Rework the frequency picker in TaskFormScreen from a flat grid of 10 preset chips + hidden "Custom" mode into an intuitive 4 shortcut chips + always-visible freeform "Every [N] [unit]" picker.
|
||||
|
||||
Purpose: Users should be able to set any recurring frequency intuitively — common frequencies are one tap away, custom intervals are freeform without mode switching.
|
||||
|
||||
Output: Reworked `task_form_screen.dart` with simplified state management, bidirectional chip/picker sync, correct edit-mode loading, and all existing scheduling behavior preserved.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/09-task-creation-ux/09-CONTEXT.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
From lib/features/tasks/domain/frequency.dart:
|
||||
```dart
|
||||
enum IntervalType {
|
||||
daily, // 0
|
||||
everyNDays, // 1
|
||||
weekly, // 2
|
||||
biweekly, // 3
|
||||
monthly, // 4
|
||||
everyNMonths,// 5
|
||||
quarterly, // 6
|
||||
yearly, // 7
|
||||
}
|
||||
|
||||
class FrequencyInterval {
|
||||
final IntervalType intervalType;
|
||||
final int days;
|
||||
const FrequencyInterval({required this.intervalType, this.days = 1});
|
||||
String label() { /* German label logic */ }
|
||||
static const List<FrequencyInterval> presets = [ /* 10 presets */ ];
|
||||
}
|
||||
```
|
||||
|
||||
From lib/features/tasks/presentation/task_form_screen.dart (current state to rework):
|
||||
```dart
|
||||
// Current state variables (lines 37-39):
|
||||
FrequencyInterval? _selectedPreset;
|
||||
bool _isCustomFrequency = false;
|
||||
_CustomUnit _customUnit = _CustomUnit.days;
|
||||
|
||||
// Current methods to rework:
|
||||
_buildFrequencySelector() // lines 226-277 — chip grid + conditional custom input
|
||||
_buildCustomFrequencyInput() // lines 279-326 — the "Alle [N] [unit]" row (KEEP, promote to primary)
|
||||
_loadExistingTask() // lines 55-101 — edit mode preset matching (rework for new chips)
|
||||
_resolveFrequency() // lines 378-415 — maps to IntervalType (KEEP, simplify condition)
|
||||
```
|
||||
|
||||
From lib/l10n/app_de.arb (existing frequency strings):
|
||||
```json
|
||||
"taskFormFrequencyLabel": "Wiederholung",
|
||||
"taskFormFrequencyCustom": "Benutzerdefiniert", // will be unused
|
||||
"taskFormFrequencyEvery": "Alle",
|
||||
"taskFormFrequencyUnitDays": "Tage",
|
||||
"taskFormFrequencyUnitWeeks": "Wochen",
|
||||
"taskFormFrequencyUnitMonths": "Monate"
|
||||
```
|
||||
|
||||
IMPORTANT: FrequencyInterval.presets is NOT used outside of task_form_screen.dart for selection purposes.
|
||||
template_picker_sheet.dart and task_row.dart only use FrequencyInterval constructor + .label() — they do NOT reference .presets.
|
||||
The .presets list can safely stop being used in the UI without breaking anything.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Rework frequency picker — shortcut chips + freeform picker</name>
|
||||
<files>lib/features/tasks/presentation/task_form_screen.dart, lib/l10n/app_de.arb, lib/l10n/app_localizations.dart, lib/l10n/app_localizations_de.dart</files>
|
||||
<action>
|
||||
Rework the frequency picker in `task_form_screen.dart` following the locked user decisions in 09-CONTEXT.md.
|
||||
|
||||
**Step 1 — Add l10n strings** to `app_de.arb`:
|
||||
Add 4 new keys for shortcut chip labels:
|
||||
- `"frequencyShortcutDaily": "Täglich"`
|
||||
- `"frequencyShortcutWeekly": "Wöchentlich"`
|
||||
- `"frequencyShortcutBiweekly": "Alle 2 Wochen"`
|
||||
- `"frequencyShortcutMonthly": "Monatlich"`
|
||||
|
||||
Then run `flutter gen-l10n` to regenerate `app_localizations.dart` and `app_localizations_de.dart`.
|
||||
|
||||
**Step 2 — Define shortcut enum** in `task_form_screen.dart`:
|
||||
Create a private enum `_ShortcutFrequency` with values: `daily, weekly, biweekly, monthly`.
|
||||
Add a method `toPickerValues()` returning `({int number, _CustomUnit unit})`:
|
||||
- daily → (1, days)
|
||||
- weekly → (1, weeks)
|
||||
- biweekly → (2, weeks)
|
||||
- monthly → (1, months)
|
||||
|
||||
Add a static method `fromPickerValues(int number, _CustomUnit unit)` returning `_ShortcutFrequency?`:
|
||||
- (1, days) → daily
|
||||
- (1, weeks) → weekly
|
||||
- (2, weeks) → biweekly
|
||||
- (1, months) → monthly
|
||||
- anything else → null
|
||||
|
||||
**Step 3 — Simplify state variables:**
|
||||
Remove `_selectedPreset` (FrequencyInterval?) and `_isCustomFrequency` (bool).
|
||||
Add `_activeShortcut` (_ShortcutFrequency?) — nullable, null means no chip highlighted.
|
||||
|
||||
Change `initState` default: instead of `_selectedPreset = FrequencyInterval.presets[3]`, set:
|
||||
- `_activeShortcut = _ShortcutFrequency.weekly`
|
||||
- `_customIntervalController.text = '1'` (already defaults to '2', change to '1')
|
||||
- `_customUnit = _CustomUnit.weeks`
|
||||
|
||||
**Step 4 — Rework `_buildFrequencySelector()`:**
|
||||
Replace the entire method. New structure:
|
||||
```
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Shortcut chips row (always visible)
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for each _ShortcutFrequency shortcut:
|
||||
ChoiceChip(
|
||||
label: Text(_shortcutLabel(shortcut, l10n)),
|
||||
selected: _activeShortcut == shortcut,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
final values = shortcut.toPickerValues();
|
||||
setState(() {
|
||||
_activeShortcut = shortcut;
|
||||
_customIntervalController.text = values.number.toString();
|
||||
_customUnit = values.unit;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Freeform picker row (ALWAYS visible — not conditional)
|
||||
_buildFrequencyPickerRow(l10n, theme),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
Add helper `_shortcutLabel(_ShortcutFrequency shortcut, AppLocalizations l10n)` returning the l10n string for each shortcut.
|
||||
|
||||
**Step 5 — Rename `_buildCustomFrequencyInput` to `_buildFrequencyPickerRow`:**
|
||||
The method body stays almost identical. One change: when the user edits the number field or changes the unit, recalculate `_activeShortcut`:
|
||||
- In the TextFormField's `onChanged` callback: `setState(() { _activeShortcut = _ShortcutFrequency.fromPickerValues(int.tryParse(_customIntervalController.text) ?? 1, _customUnit); })`
|
||||
- In the SegmentedButton's `onSelectionChanged`: after setting `_customUnit`, also recalculate: `_activeShortcut = _ShortcutFrequency.fromPickerValues(int.tryParse(_customIntervalController.text) ?? 1, newUnit);`
|
||||
|
||||
This ensures bidirectional sync: chip → picker and picker → chip.
|
||||
|
||||
**Step 6 — Simplify `_resolveFrequency()`:**
|
||||
Remove the `if (!_isCustomFrequency && _selectedPreset != null)` branch entirely.
|
||||
The method now ALWAYS reads from the picker values (`_customIntervalController` + `_customUnit`), since the picker is always the source of truth (shortcuts just populate it). Use the SMART mapping that matches existing DB behavior for named types:
|
||||
- 1 day → IntervalType.daily, days=1
|
||||
- N days (N>1) → IntervalType.everyNDays, days=N
|
||||
- 1 week → IntervalType.weekly, days=1
|
||||
- 2 weeks → IntervalType.biweekly, days=14
|
||||
- N weeks (N>2) → IntervalType.everyNDays, days=N*7
|
||||
- 1 month → IntervalType.monthly, days=1, anchorDay=dueDate.day
|
||||
- N months (N>1) → IntervalType.everyNMonths, days=N, anchorDay=dueDate.day
|
||||
|
||||
CRITICAL correctness note: The existing weekly preset has `days=1` (it's a named type where `intervalDays` stores 1). The old custom weeks path returns `everyNDays` with `days=N*7`. The new unified `_resolveFrequency` MUST use the named types (daily/weekly/biweekly/monthly) for their canonical values to match existing DB records. Only use everyNDays for non-canonical week counts (3+ weeks). Similarly, monthly uses `days=1` (not days=30) since it's a named type.
|
||||
|
||||
**Step 7 — Rework `_loadExistingTask()` for edit mode:**
|
||||
Replace the preset-matching loop (lines 69-78) and custom-detection logic (lines 80-98) with unified picker population:
|
||||
|
||||
```dart
|
||||
// Populate picker from stored interval
|
||||
switch (task.intervalType) {
|
||||
case IntervalType.daily:
|
||||
_customUnit = _CustomUnit.days;
|
||||
_customIntervalController.text = '1';
|
||||
case IntervalType.everyNDays:
|
||||
// Check if it's a clean week multiple
|
||||
if (task.intervalDays % 7 == 0) {
|
||||
_customUnit = _CustomUnit.weeks;
|
||||
_customIntervalController.text = (task.intervalDays ~/ 7).toString();
|
||||
} else {
|
||||
_customUnit = _CustomUnit.days;
|
||||
_customIntervalController.text = task.intervalDays.toString();
|
||||
}
|
||||
case IntervalType.weekly:
|
||||
_customUnit = _CustomUnit.weeks;
|
||||
_customIntervalController.text = '1';
|
||||
case IntervalType.biweekly:
|
||||
_customUnit = _CustomUnit.weeks;
|
||||
_customIntervalController.text = '2';
|
||||
case IntervalType.monthly:
|
||||
_customUnit = _CustomUnit.months;
|
||||
_customIntervalController.text = '1';
|
||||
case IntervalType.everyNMonths:
|
||||
_customUnit = _CustomUnit.months;
|
||||
_customIntervalController.text = task.intervalDays.toString();
|
||||
case IntervalType.quarterly:
|
||||
_customUnit = _CustomUnit.months;
|
||||
_customIntervalController.text = '3';
|
||||
case IntervalType.yearly:
|
||||
_customUnit = _CustomUnit.months;
|
||||
_customIntervalController.text = '12';
|
||||
}
|
||||
|
||||
// Detect matching shortcut chip
|
||||
_activeShortcut = _ShortcutFrequency.fromPickerValues(
|
||||
int.tryParse(_customIntervalController.text) ?? 1,
|
||||
_customUnit,
|
||||
);
|
||||
```
|
||||
|
||||
This handles ALL 8 IntervalType values correctly, including quarterly (3 months) and yearly (12 months) which have no shortcut chip but display correctly in the picker.
|
||||
|
||||
**Step 8 — Clean up unused references:**
|
||||
- Remove `_selectedPreset` field
|
||||
- Remove `_isCustomFrequency` field
|
||||
- Remove the import or reference to `FrequencyInterval.presets` in the chip-building code (the `for (final preset in FrequencyInterval.presets)` loop)
|
||||
- Keep the `taskFormFrequencyCustom` l10n key in the ARB file (do NOT delete l10n keys — they're harmless and removing requires regen)
|
||||
- Do NOT modify `frequency.dart` — the `presets` list stays for backward compatibility even though the UI no longer iterates it
|
||||
|
||||
**Verification notes for _resolveFrequency:**
|
||||
The key correctness requirement is that saving a task from the new picker produces EXACTLY the same IntervalType + intervalDays + anchorDay as the old preset path did for equivalent selections. Verify by mentally tracing:
|
||||
- Chip "Taeglich" → picker (1, days) → resolves to (daily, 1, null) -- matches old preset[0]
|
||||
- Chip "Woechentlich" → picker (1, weeks) → resolves to (weekly, 1, null) -- matches old preset[3]
|
||||
- Chip "Alle 2 Wochen" → picker (2, weeks) → resolves to (biweekly, 14, null) -- matches old preset[4]
|
||||
- Chip "Monatlich" → picker (1, months) → resolves to (monthly, 1, anchorDay) -- matches old preset[5]
|
||||
- Freeform "5 days" → picker (5, days) → resolves to (everyNDays, 5, null) -- matches old custom path
|
||||
- Freeform "3 months" → picker (3, months) → resolves to (everyNMonths, 3, anchorDay) -- matches old custom path
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter analyze --no-fatal-infos 2>&1 | tail -5 && flutter test 2>&1 | tail -10</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- The 10-chip Wrap grid is fully replaced by 4 shortcut chips + always-visible freeform picker
|
||||
- The "Benutzerdefiniert" (Custom) chip is removed — the picker is inherently freeform
|
||||
- Bidirectional sync: tapping a chip populates the picker; editing the picker recalculates chip highlight
|
||||
- `_resolveFrequency()` reads exclusively from the picker (single source of truth)
|
||||
- Edit mode correctly loads all 8 IntervalType values into the picker and highlights matching shortcut chip
|
||||
- All existing tests pass, dart analyze is clean
|
||||
- No changes to frequency.dart, no DB migration, no new screens
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 2: Verify frequency picker UX</name>
|
||||
<what-built>Reworked frequency picker with 4 shortcut chips and freeform "Every [N] [unit]" picker, replacing the old 10-chip grid + hidden Custom mode</what-built>
|
||||
<how-to-verify>
|
||||
1. Launch the app: `flutter run`
|
||||
2. Navigate to any room and tap "+" to create a new task
|
||||
3. Verify the frequency section shows:
|
||||
- Row of 4 shortcut chips: Taeglich, Woechentlich, Alle 2 Wochen, Monatlich
|
||||
- Below: always-visible freeform picker row with number field + Tage/Wochen/Monate segments
|
||||
- "Woechentlich" chip highlighted by default, picker showing "1" with "Wochen" selected
|
||||
4. Tap "Taeglich" chip — verify chip highlights and picker updates to "1" / "Tage"
|
||||
5. Tap "Monatlich" chip — verify chip highlights and picker updates to "1" / "Monate"
|
||||
6. Manually type "5" in the number field — verify all chips deselect (no shortcut matches 5 weeks)
|
||||
7. Change unit to "Tage" — verify still no chip selected (5 days is not a shortcut)
|
||||
8. Type "1" in the number field with "Tage" selected — verify "Taeglich" chip auto-highlights
|
||||
9. Save a task with "Alle 2 Wochen" shortcut, then re-open in edit mode — verify "Alle 2 Wochen" chip is highlighted and picker shows "2" / "Wochen"
|
||||
10. If you have an existing quarterly or yearly task, open it in edit mode — verify no chip highlighted, picker shows "3" / "Monate" (quarterly) or "12" / "Monate" (yearly)
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" or describe issues</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `flutter analyze --no-fatal-infos` reports zero issues
|
||||
- `flutter test` — all existing tests pass (108+)
|
||||
- Manual verification: create task with each shortcut, create task with arbitrary interval, edit existing tasks of all interval types
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. The frequency section presents 4 shortcut chips above an always-visible "Every [N] [unit]" picker (TCX-01, TCX-02)
|
||||
2. Any arbitrary interval is settable directly in the picker without a "Custom" mode (TCX-03)
|
||||
3. All 8 IntervalType values save and load correctly, including calendar-anchored monthly/quarterly/yearly with anchor memory (TCX-04)
|
||||
4. Existing tests pass without modification, dart analyze is clean
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/09-task-creation-ux/09-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
phase: 09-task-creation-ux
|
||||
plan: 01
|
||||
subsystem: ui
|
||||
tags: [flutter, dart, l10n, frequency-picker, choice-chip, segmented-button]
|
||||
|
||||
# Dependency graph
|
||||
requires: []
|
||||
provides:
|
||||
- Reworked frequency picker with 4 shortcut chips (Täglich, Wöchentlich, Alle 2 Wochen, Monatlich)
|
||||
- Always-visible freeform "Alle [N] [unit]" picker replacing hidden Custom mode
|
||||
- Bidirectional chip/picker sync via _ShortcutFrequency enum
|
||||
- Unified _resolveFrequency() reading exclusively from picker (single source of truth)
|
||||
- Edit mode loading for all 8 IntervalType values including quarterly and yearly
|
||||
affects: []
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Shortcut chip + freeform picker: ChoiceChip row above always-visible SegmentedButton picker"
|
||||
- "Bidirectional sync: chip tapped populates picker; picker edited recalculates chip highlight via fromPickerValues()"
|
||||
- "Single source of truth: _resolveFrequency() always reads from picker, never from a preset reference"
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- lib/features/tasks/presentation/task_form_screen.dart
|
||||
- lib/l10n/app_de.arb
|
||||
- lib/l10n/app_localizations.dart
|
||||
- lib/l10n/app_localizations_de.dart
|
||||
|
||||
key-decisions:
|
||||
- "Picker is single source of truth: _resolveFrequency() reads from _customIntervalController + _customUnit always"
|
||||
- "_ShortcutFrequency enum with toPickerValues() and fromPickerValues() handles bidirectional sync without manual mapping"
|
||||
- "Named IntervalTypes (daily/weekly/biweekly/monthly) used for canonical values; only everyNDays for 3+ weeks"
|
||||
- "Quarterly (3 months) and yearly (12 months) displayed correctly in picker with no chip highlighted"
|
||||
|
||||
patterns-established:
|
||||
- "Shortcut chip pattern: enum with toPickerValues() / fromPickerValues() for bidirectional picker sync"
|
||||
|
||||
requirements-completed: [TCX-01, TCX-02, TCX-03, TCX-04]
|
||||
|
||||
# Metrics
|
||||
duration: 2min
|
||||
completed: 2026-03-18
|
||||
---
|
||||
|
||||
# Phase 9 Plan 01: Task Creation UX — Frequency Picker Rework Summary
|
||||
|
||||
**4 shortcut chips (Täglich/Wöchentlich/Alle 2 Wochen/Monatlich) + always-visible freeform picker replacing the 10-chip grid with hidden Custom mode**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 2 min
|
||||
- **Started:** 2026-03-18T21:43:24Z
|
||||
- **Completed:** 2026-03-18T21:45:30Z
|
||||
- **Tasks:** 1 (+ 1 auto-approved checkpoint)
|
||||
- **Files modified:** 4
|
||||
|
||||
## Accomplishments
|
||||
- Replaced 10-chip preset grid and hidden "Benutzerdefiniert" mode with 4 shortcut chips + always-visible freeform picker
|
||||
- Implemented bidirectional sync: tapping a chip populates the picker; editing the picker recalculates chip highlight
|
||||
- Simplified _resolveFrequency() to read exclusively from the picker (single source of truth), using named IntervalTypes for canonical values
|
||||
- Edit mode correctly loads all 8 IntervalType values (daily, everyNDays, weekly, biweekly, monthly, everyNMonths, quarterly, yearly) into the picker and highlights the matching shortcut chip where applicable
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: Rework frequency picker — shortcut chips + freeform picker** - `8a0b69b` (feat)
|
||||
2. **Task 2: Verify frequency picker UX** - auto-approved (checkpoint:human-verify, auto_advance=true)
|
||||
|
||||
**Plan metadata:** (docs commit follows)
|
||||
|
||||
## Files Created/Modified
|
||||
- `lib/features/tasks/presentation/task_form_screen.dart` - Reworked frequency picker: removed _selectedPreset and _isCustomFrequency fields; added _ShortcutFrequency enum and _activeShortcut state; replaced _buildFrequencySelector() with shortcut chips + always-visible picker; renamed _buildCustomFrequencyInput to _buildFrequencyPickerRow with bidirectional sync; simplified _resolveFrequency() to picker-only
|
||||
- `lib/l10n/app_de.arb` - Added frequencyShortcutDaily/Weekly/Biweekly/Monthly keys
|
||||
- `lib/l10n/app_localizations.dart` - Regenerated to include new shortcut string getters
|
||||
- `lib/l10n/app_localizations_de.dart` - Regenerated with German translations (Täglich, Wöchentlich, Alle 2 Wochen, Monatlich)
|
||||
|
||||
## Decisions Made
|
||||
- Picker is single source of truth: _resolveFrequency() reads from _customIntervalController + _customUnit always, regardless of which chip is highlighted
|
||||
- _ShortcutFrequency enum with toPickerValues() and static fromPickerValues() cleanly handles bidirectional sync without manual if-chain mapping in each callback
|
||||
- Named IntervalTypes (daily/weekly/biweekly/monthly) used for canonical values (e.g., weekly has days=1, biweekly has days=14) matching existing DB records; only everyNDays used for 3+ weeks
|
||||
- Quarterly (3 months) and yearly (12 months) round-trip correctly: loaded as "3 Monate" / "12 Monate" in picker with no chip highlighted
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
None.
|
||||
|
||||
## User Setup Required
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Frequency picker rework complete; TaskFormScreen is ready for further UX improvements
|
||||
- All 144 existing tests pass, dart analyze is clean
|
||||
- No changes to frequency.dart, no DB migration, no new screens
|
||||
|
||||
---
|
||||
*Phase: 09-task-creation-ux*
|
||||
*Completed: 2026-03-18*
|
||||
@@ -0,0 +1,117 @@
|
||||
# Phase 9: Task Creation UX - Context
|
||||
|
||||
**Gathered:** 2026-03-18
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Rework the frequency picker from a flat grid of 10 preset ChoiceChips + hidden "Custom" mode into an intuitive "Every [N] [unit]" picker with quick-select shortcut chips. The picker is inherently freeform — no separate "Custom" mode. All existing interval types and calendar-anchored scheduling behavior must continue working. No new scheduling logic, no new DB columns, no new screens.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Picker layout
|
||||
- Shortcut chips first (compact row), then the "Every [N] [unit]" picker row below
|
||||
- Tapping a chip highlights it AND populates the picker row (bidirectional sync)
|
||||
- Editing the picker manually deselects any highlighted chip
|
||||
- Both always reflect the same value — single source of truth
|
||||
- The picker row is always visible, not hidden behind a "Custom" toggle
|
||||
|
||||
### Number input
|
||||
- Keep the existing TextFormField with digit-only filter
|
||||
- Same pattern as the current custom interval input (TextFormField + FilteringTextInputFormatter.digitsOnly)
|
||||
- Minimum value: 1 (no max limit — if someone wants every 999 days, let them)
|
||||
- Text field centered, compact width (~60px as current)
|
||||
|
||||
### Unit selector
|
||||
- Keep SegmentedButton with 3 units: Tage (days), Wochen (weeks), Monate (months)
|
||||
- No "years" unit — yearly is handled as 12 months in the picker
|
||||
- Consistent with existing SegmentedButton pattern used for effort level selector
|
||||
|
||||
### Shortcut chips
|
||||
- 4 shortcut chips: Täglich, Wöchentlich, Alle 2 Wochen, Monatlich
|
||||
- No quarterly/yearly shortcut chips — users type 3/12 months via the freeform picker
|
||||
- Drop all other presets (every 2 days, every 3 days, every 2 months, every 6 months) — the freeform picker handles arbitrary intervals naturally
|
||||
- Chips use ChoiceChip widget (existing pattern)
|
||||
|
||||
### Preset removal
|
||||
- Remove the entire `FrequencyInterval.presets` static list from being used in the UI
|
||||
- The 10-chip Wrap grid is fully replaced by 4 shortcut chips + freeform picker
|
||||
- The "Benutzerdefiniert" (Custom) chip is removed — the picker is inherently freeform
|
||||
- `_isCustomFrequency` boolean state is no longer needed
|
||||
|
||||
### Edit mode behavior
|
||||
- When editing an existing task, match the stored interval to a shortcut chip if possible
|
||||
- Daily task → highlight "Täglich" chip, picker shows "Alle 1 Tage"
|
||||
- Weekly task → highlight "Wöchentlich" chip, picker shows "Alle 1 Wochen"
|
||||
- Biweekly → highlight "Alle 2 Wochen" chip, picker shows "Alle 2 Wochen"
|
||||
- Monthly → highlight "Monatlich" chip, picker shows "Alle 1 Monate"
|
||||
- Any other interval (e.g., every 5 days, quarterly, yearly) → no chip highlighted, picker filled with correct values
|
||||
|
||||
### DB mapping
|
||||
- Shortcut chips map to existing IntervalType enum values (daily, weekly, biweekly, monthly)
|
||||
- Freeform days → IntervalType.everyNDays with the entered value
|
||||
- Freeform weeks → IntervalType.everyNDays with value * 7
|
||||
- Freeform months → IntervalType.everyNMonths with the entered value
|
||||
- Calendar-anchored behavior (anchorDay) preserved for month-based intervals
|
||||
- No changes to IntervalType enum, no new DB values, no migration needed
|
||||
|
||||
### Claude's Discretion
|
||||
- Exact chip styling and spacing within the Wrap
|
||||
- Animation/transition when syncing chip ↔ picker
|
||||
- Whether the "Alle" prefix label is part of the picker row or omitted
|
||||
- How to handle the edge case where user clears the number field (empty → treat as 1)
|
||||
- l10n string changes needed for new/modified labels
|
||||
|
||||
</decisions>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- User consistently prefers simplicity across phases ("just keep it simple" — Phase 8 pattern)
|
||||
- The key UX improvement: no more hunting through 10 chips or finding a hidden "Custom" mode — the picker is always there and always works
|
||||
- 4 common shortcuts for one-tap convenience, freeform picker for everything else
|
||||
- The current `_buildCustomFrequencyInput` method is essentially what becomes the primary picker — it already has the right structure
|
||||
|
||||
</specifics>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `_buildCustomFrequencyInput()` in `task_form_screen.dart:279-326`: Already implements "Alle [N] [Tage|Wochen|Monate]" row with TextFormField + SegmentedButton — this becomes the primary picker
|
||||
- `_CustomUnit` enum (`task_form_screen.dart:511`): Already has days/weeks/months — reuse directly
|
||||
- `_customIntervalController` (`task_form_screen.dart:35`): Already exists for the number input
|
||||
- `_resolveFrequency()` (`task_form_screen.dart:378-415`): Already handles custom unit → IntervalType mapping — core logic stays the same
|
||||
- `_loadExistingTask()` (`task_form_screen.dart:55-101`): Already has edit-mode loading logic with preset matching — needs rework for new chip set
|
||||
- `FrequencyInterval.presets` (`frequency.dart:50-61`): Static list of 10 presets — UI no longer iterates this, but the model class stays for backward compat
|
||||
|
||||
### Established Patterns
|
||||
- ChoiceChip in Wrap for multi-option selection (current frequency chips)
|
||||
- SegmentedButton for unit/level selection (effort level, custom unit)
|
||||
- TextFormField with FilteringTextInputFormatter for numeric input
|
||||
- ConsumerStatefulWidget with setState for form state management
|
||||
- German l10n strings in `app_de.arb` via `AppLocalizations`
|
||||
|
||||
### Integration Points
|
||||
- `task_form_screen.dart`: Primary file — rework `_buildFrequencySelector()` method, simplify state variables
|
||||
- `frequency.dart`: `FrequencyInterval.presets` list is no longer iterated in UI but may still be used elsewhere (templates) — check before removing
|
||||
- `app_de.arb` / `app_localizations.dart`: May need new/updated l10n keys for shortcut chip labels
|
||||
- `template_picker_sheet.dart` / `task_templates.dart`: Templates create tasks with specific IntervalType values — no changes needed since DB mapping unchanged
|
||||
|
||||
</code_context>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
None — discussion stayed within phase scope
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 09-task-creation-ux*
|
||||
*Context gathered: 2026-03-18*
|
||||
@@ -0,0 +1,161 @@
|
||||
---
|
||||
phase: 09-task-creation-ux
|
||||
verified: 2026-03-18T23:00:00Z
|
||||
status: human_needed
|
||||
score: 8/8 must-haves verified
|
||||
human_verification:
|
||||
- test: "Create new task — verify frequency section layout"
|
||||
expected: "4 shortcut chips (Taeglich, Woechentlich, Alle 2 Wochen, Monatlich) appear in a Wrap row; below them an always-visible picker row shows 'Alle [number] [Tage|Wochen|Monate]'; 'Woechentlich' chip is highlighted by default with picker showing '1' and 'Wochen' selected"
|
||||
why_human: "Visual layout and default highlight state require running the app"
|
||||
- test: "Tap each shortcut chip and verify bidirectional sync"
|
||||
expected: "Tapping 'Taeglich' highlights that chip and sets picker to '1'/'Tage'; tapping 'Monatlich' highlights that chip and sets picker to '1'/'Monate'; previously highlighted chip deselects"
|
||||
why_human: "Widget interaction and visual chip highlight state require running the app"
|
||||
- test: "Edit the number field and verify chip deselection"
|
||||
expected: "With 'Woechentlich' highlighted, typing '5' in the number field deselects all chips; changing unit to 'Tage' still shows no chip; typing '1' with 'Tage' selected auto-highlights 'Taeglich'"
|
||||
why_human: "Bidirectional sync from picker back to chip highlight requires running the app"
|
||||
- test: "Save a task using each shortcut and verify re-open in edit mode"
|
||||
expected: "Task saved with 'Alle 2 Wochen' reopens with that chip highlighted and picker showing '2'/'Wochen'; task saved with arbitrary interval (e.g. 5 days) reopens with no chip highlighted and picker showing the correct values"
|
||||
why_human: "Round-trip edit-mode loading of IntervalType values requires running the app"
|
||||
- test: "Verify quarterly and yearly tasks load with no chip highlighted"
|
||||
expected: "An existing quarterly task (IntervalType.quarterly) opens with no chip highlighted and picker showing '3'/'Monate'; a yearly task shows '12'/'Monate' with no chip"
|
||||
why_human: "Requires an existing quarterly or yearly task in the database to test against"
|
||||
---
|
||||
|
||||
# Phase 9: Task Creation UX Verification Report
|
||||
|
||||
**Phase Goal:** Users can set any recurring frequency intuitively without hunting through a grid of preset chips — common frequencies are one tap away, custom intervals are freeform
|
||||
**Verified:** 2026-03-18T23:00:00Z
|
||||
**Status:** human_needed
|
||||
**Re-verification:** No — initial verification
|
||||
|
||||
---
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 1 | Frequency section shows 4 shortcut chips (Taeglich, Woechentlich, Alle 2 Wochen, Monatlich) above the freeform picker | VERIFIED | `_buildFrequencySelector()` at line 234 iterates `_ShortcutFrequency.values` in a `Wrap` with `ChoiceChip` for each of the 4 values; `_buildFrequencyPickerRow()` is called unconditionally below the Wrap |
|
||||
| 2 | The freeform 'Every [N] [unit]' picker row is always visible — not hidden behind a Custom toggle | VERIFIED | `_buildFrequencyPickerRow(l10n, theme)` is called unconditionally at line 262 with no conditional wrapping; `_isCustomFrequency` field removed entirely |
|
||||
| 3 | Tapping a shortcut chip highlights it AND populates the picker with the corresponding values | VERIFIED | `onSelected` at line 247 calls `shortcut.toPickerValues()` and `setState` setting both `_activeShortcut = shortcut` and updating `_customIntervalController.text` + `_customUnit`; `selected: _activeShortcut == shortcut` drives the highlight |
|
||||
| 4 | Editing the picker number or unit manually deselects any highlighted chip | VERIFIED | `onChanged` in TextFormField at line 298 calls `_ShortcutFrequency.fromPickerValues(...)` — returns null for non-matching values, clearing `_activeShortcut`; `SegmentedButton.onSelectionChanged` at line 326 does the same |
|
||||
| 5 | Any arbitrary interval (e.g., every 5 days, every 3 weeks, every 2 months) can be entered directly in the freeform picker | VERIFIED | Picker is a `TextFormField` with `FilteringTextInputFormatter.digitsOnly` (no max) and a 3-segment unit selector; `_resolveFrequency()` at line 393 maps all day/week/month combinations to the correct `IntervalType` values without requiring any mode switch |
|
||||
| 6 | Editing an existing daily task shows 'Taeglich' chip highlighted and picker showing 1/Tage | VERIFIED | `_loadExistingTask()` at line 56: `case IntervalType.daily` sets `_customUnit = _CustomUnit.days` and `_customIntervalController.text = '1'`; then `_ShortcutFrequency.fromPickerValues(1, days)` returns `daily` — highlighting the chip |
|
||||
| 7 | Editing an existing quarterly task (3 months) shows no chip highlighted and picker showing 3/Monate | VERIFIED | `case IntervalType.quarterly` sets `_customUnit = _CustomUnit.months` and `_customIntervalController.text = '3'`; `fromPickerValues(3, months)` returns `null` (3 months is not a shortcut), leaving `_activeShortcut` null |
|
||||
| 8 | Saving a task from the new picker produces the correct IntervalType and intervalDays values | VERIFIED | `_resolveFrequency()` maps: 1 day → (daily, 1); N days → (everyNDays, N); 1 week → (weekly, 1); 2 weeks → (biweekly, 14); N weeks (N>2) → (everyNDays, N*7); 1 month → (monthly, 1, anchorDay); N months → (everyNMonths, N, anchorDay). Result is applied in `_onSave()` at line 423. All 144 existing tests pass. |
|
||||
|
||||
**Score:** 8/8 truths verified (automated)
|
||||
|
||||
---
|
||||
|
||||
### Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `lib/features/tasks/presentation/task_form_screen.dart` | Reworked frequency picker with shortcut chips + freeform picker | VERIFIED | File exists, 536 lines; contains `_ShortcutFrequency` enum (line 504), `_activeShortcut` state (line 37), `_buildFrequencySelector()` (line 234), `_buildFrequencyPickerRow()` (line 280), `_resolveFrequency()` (line 393), `_loadExistingTask()` (line 56) |
|
||||
| `lib/l10n/app_de.arb` | German l10n strings for shortcut chip labels | VERIFIED | Contains `frequencyShortcutDaily` ("Täglich"), `frequencyShortcutWeekly` ("Wöchentlich"), `frequencyShortcutBiweekly` ("Alle 2 Wochen"), `frequencyShortcutMonthly` ("Monatlich") at lines 51-54 |
|
||||
| `lib/l10n/app_localizations.dart` | Abstract getters for new shortcut strings | VERIFIED | `frequencyShortcutDaily`, `frequencyShortcutWeekly`, `frequencyShortcutBiweekly`, `frequencyShortcutMonthly` abstract getters present at lines 325-347 |
|
||||
| `lib/l10n/app_localizations_de.dart` | German implementations of shortcut string getters | VERIFIED | All 4 `@override` getter implementations present at lines 132-141 with correct German strings |
|
||||
|
||||
---
|
||||
|
||||
### Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|-----|--------|---------|
|
||||
| `task_form_screen.dart` | `lib/features/tasks/domain/frequency.dart` | `IntervalType` enum in `_resolveFrequency()` and `_loadExistingTask()` | WIRED | `IntervalType.` referenced at 13 sites (lines 71, 74, 83, 86, 89, 92, 95, 98, 398, 400, 403, 406, 408, 411, 413); all 8 enum values handled; `frequency.dart` imported via `../domain/frequency.dart` |
|
||||
| `task_form_screen.dart` | `lib/l10n/app_de.arb` | `AppLocalizations` for chip labels via `l10n.frequencyShortcut*` | WIRED | `l10n.frequencyShortcutDaily/Weekly/Biweekly/Monthly` called at lines 270-276 in `_shortcutLabel()`; `AppLocalizations` imported at line 10 |
|
||||
|
||||
---
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
| Requirement | Source Plan | Description | Status | Evidence |
|
||||
|-------------|------------|-------------|--------|----------|
|
||||
| TCX-01 | 09-01-PLAN.md | Frequency picker presents an intuitive "Every [N] [unit]" interface instead of a flat grid of preset chips | SATISFIED | `_buildFrequencyPickerRow()` always-visible TextFormField + SegmentedButton row replaces the old 10-chip `FrequencyInterval.presets` grid; `_isCustomFrequency` and `_selectedPreset` removed entirely |
|
||||
| TCX-02 | 09-01-PLAN.md | Common frequencies (daily, weekly, biweekly, monthly) are available as quick-select shortcuts without scrolling through all options | SATISFIED | `_ShortcutFrequency.values` iterated in a `Wrap` at lines 243-257; 4 ChoiceChips one-tap select and populate the picker |
|
||||
| TCX-03 | 09-01-PLAN.md | User can set any arbitrary interval without needing to select "Custom" first | SATISFIED | Picker is always visible; number field accepts any positive integer; no mode gate or "Custom" toggle exists in the code |
|
||||
| TCX-04 | 09-01-PLAN.md | The frequency picker preserves all existing interval types and scheduling behavior (calendar-anchored monthly/quarterly/yearly with anchor memory) | SATISFIED | `_resolveFrequency()` passes `anchorDay: _dueDate.day` for monthly and everyNMonths; `_loadExistingTask()` handles all 8 `IntervalType` values in a complete exhaustive switch; `frequency.dart` not modified; 144 tests pass |
|
||||
|
||||
No orphaned requirements found — all 4 TCX-* IDs declared in PLAN frontmatter are present in REQUIREMENTS.md and verified above.
|
||||
|
||||
---
|
||||
|
||||
### Anti-Patterns Found
|
||||
|
||||
| File | Line | Pattern | Severity | Impact |
|
||||
|------|------|---------|----------|--------|
|
||||
| None | — | — | — | — |
|
||||
|
||||
No TODOs, FIXMEs, placeholders, empty implementations, or console.log-only handlers found in any modified file.
|
||||
|
||||
**Removed code confirmed absent:**
|
||||
- `_selectedPreset` field: not found
|
||||
- `_isCustomFrequency` field: not found
|
||||
- `FrequencyInterval.presets` iteration loop: not found
|
||||
- `_buildCustomFrequencyInput` (old name): not found (correctly renamed to `_buildFrequencyPickerRow`)
|
||||
|
||||
**Backward compatibility confirmed:**
|
||||
- `frequency.dart` is unchanged; `FrequencyInterval.presets` remains in the model for `template_picker_sheet.dart` and `task_row.dart` usage
|
||||
|
||||
---
|
||||
|
||||
### Human Verification Required
|
||||
|
||||
All automated checks passed. The following items require a running app to confirm the interactive UX behavior:
|
||||
|
||||
#### 1. Frequency Section Layout
|
||||
|
||||
**Test:** Launch the app, navigate to any room, tap "+" to create a new task, scroll to the "Wiederholung" section.
|
||||
**Expected:** A row of 4 shortcut chips (Täglich, Wöchentlich, Alle 2 Wochen, Monatlich) appears in a compact Wrap; below them the always-visible "Alle [N] [Tage|Wochen|Monate]" picker row; "Wöchentlich" chip is highlighted by default; picker shows "1" with "Wochen" segment selected.
|
||||
**Why human:** Visual layout, spacing, and default highlight state require running the app.
|
||||
|
||||
#### 2. Chip-to-Picker Bidirectional Sync
|
||||
|
||||
**Test:** Tap "Täglich" — verify chip highlights and picker updates to "1"/"Tage". Tap "Monatlich" — verify chip highlights and picker updates to "1"/"Monate". Previous chip deselects.
|
||||
**Expected:** Smooth single-tap update of both chip highlight and picker values.
|
||||
**Why human:** Widget interaction and visual state transitions require running the app.
|
||||
|
||||
#### 3. Picker-to-Chip Reverse Sync
|
||||
|
||||
**Test:** With "Wöchentlich" highlighted, type "5" in the number field. Verify all chips deselect. Change unit to "Tage" — still no chip selected. Type "1" with "Tage" selected — verify "Täglich" chip auto-highlights.
|
||||
**Expected:** The picker editing recalculates chip highlight in real time.
|
||||
**Why human:** Text field onChange and SegmentedButton interaction require running the app.
|
||||
|
||||
#### 4. Round-Trip Edit Mode — Shortcut Task
|
||||
|
||||
**Test:** Create a task using "Alle 2 Wochen" shortcut. Re-open it in edit mode.
|
||||
**Expected:** "Alle 2 Wochen" chip is highlighted; picker shows "2" with "Wochen" selected.
|
||||
**Why human:** Requires saving to database and reopening to test _loadExistingTask() end-to-end.
|
||||
|
||||
#### 5. Round-Trip Edit Mode — Non-Shortcut Task
|
||||
|
||||
**Test:** Create a task with freeform "5"/"Tage". Re-open it in edit mode.
|
||||
**Expected:** No chip highlighted; picker shows "5" with "Tage" selected.
|
||||
**Why human:** Requires running the app and database round-trip.
|
||||
|
||||
#### 6. Quarterly / Yearly Task Edit Mode
|
||||
|
||||
**Test:** If an existing quarterly or yearly task is available, open it in edit mode.
|
||||
**Expected:** Quarterly task: no chip highlighted, picker shows "3"/"Monate". Yearly task: picker shows "12"/"Monate" with no chip.
|
||||
**Why human:** Requires an existing task with IntervalType.quarterly or IntervalType.yearly in the database.
|
||||
|
||||
---
|
||||
|
||||
### Static Analysis and Tests
|
||||
|
||||
- `flutter analyze --no-fatal-infos`: **No issues found** (ran 2026-03-18)
|
||||
- `flutter test`: **144/144 tests passed** (ran 2026-03-18)
|
||||
- Commit `8a0b69b` verified: feat(09-01) with correct 4-file diff (task_form_screen.dart +179/-115, app_de.arb +4, app_localizations.dart +24, app_localizations_de.dart +12)
|
||||
|
||||
---
|
||||
|
||||
### Gaps Summary
|
||||
|
||||
No gaps found. All automated must-haves are verified. The phase goal — intuitive frequency selection with shortcut chips and always-visible freeform picker — is fully implemented in the codebase. Human verification of interactive UX behavior is the only remaining item.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-03-18T23:00:00Z_
|
||||
_Verifier: Claude (gsd-verifier)_
|
||||
Reference in New Issue
Block a user