Files
HouseHoldKeaper/.planning/phases/09-task-creation-ux/09-01-PLAN.md

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
lib/features/tasks/presentation/task_form_screen.dart
lib/l10n/app_de.arb
lib/l10n/app_localizations.dart
lib/l10n/app_localizations_de.dart
false
TCX-01
TCX-02
TCX-03
TCX-04
truths artifacts key_links
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
path provides contains
lib/features/tasks/presentation/task_form_screen.dart Reworked frequency picker with shortcut chips + freeform picker _ShortcutFrequency
path provides contains
lib/l10n/app_de.arb German l10n strings for shortcut chip labels frequencyShortcutDaily
from to via pattern
lib/features/tasks/presentation/task_form_screen.dart lib/features/tasks/domain/frequency.dart IntervalType enum and FrequencyInterval for _resolveFrequency mapping IntervalType.
from to via pattern
lib/features/tasks/presentation/task_form_screen.dart lib/l10n/app_de.arb AppLocalizations for chip labels and picker labels l10n.frequencyShortcut
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.

<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.md

From 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 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:

// 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 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
Task 2: Verify frequency picker UX Reworked frequency picker with 4 shortcut chips and freeform "Every [N] [unit]" picker, replacing the old 10-chip grid + hidden Custom mode 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) Type "approved" or describe issues - `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

<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>
After completion, create `.planning/phases/09-task-creation-ux/09-01-SUMMARY.md`