16 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 09-task-creation-ux | 01 | execute | 1 |
|
false |
|
|
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.
<execution_context> @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/09-task-creation-ux/09-CONTEXT.mdFrom lib/features/tasks/domain/frequency.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):
// 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):
"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.
Task 1: Rework frequency picker — shortcut chips + freeform picker lib/features/tasks/presentation/task_form_screen.dart, lib/l10n/app_de.arb, lib/l10n/app_localizations.dart, lib/l10n/app_localizations_de.dart 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
onChangedcallback: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:
// 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
_selectedPresetfield - Remove
_isCustomFrequencyfield - Remove the import or reference to
FrequencyInterval.presetsin the chip-building code (thefor (final preset in FrequencyInterval.presets)loop) - Keep the
taskFormFrequencyCustoml10n key in the ARB file (do NOT delete l10n keys — they're harmless and removing requires regen) - Do NOT modify
frequency.dart— thepresetslist 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 cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter analyze --no-fatal-infos 2>&1 | tail -5 && flutter test 2>&1 | tail -10
- 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
<success_criteria>
- The frequency section presents 4 shortcut chips above an always-visible "Every [N] [unit]" picker (TCX-01, TCX-02)
- Any arbitrary interval is settable directly in the picker without a "Custom" mode (TCX-03)
- All 8 IntervalType values save and load correctly, including calendar-anchored monthly/quarterly/yearly with anchor memory (TCX-04)
- Existing tests pass without modification, dart analyze is clean </success_criteria>