diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 705a49f..0f7b186 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -79,7 +79,11 @@ Plans:
2. User can enable or disable notifications from the Settings tab, and the change takes effect immediately
3. Notifications are correctly rescheduled after device reboot (RECEIVE_BOOT_COMPLETED receiver active)
4. On Android API 33+, the app requests POST_NOTIFICATIONS permission at the appropriate moment and degrades gracefully if denied
-**Plans**: TBD
+**Plans**: 3 plans
+Plans:
+- [ ] 04-01-PLAN.md — Infrastructure: packages, Android config, NotificationService, NotificationSettingsNotifier, DAO queries, timezone init, tests
+- [ ] 04-02-PLAN.md — Settings UI: Benachrichtigungen section with toggle, time picker, permission flow, scheduling wiring, tests
+- [ ] 04-03-PLAN.md — Verification gate: dart analyze + full test suite + requirement confirmation
## Progress
@@ -93,4 +97,4 @@ Note: Phase 4 depends on Phase 2 (needs scheduling data) but can be developed in
| 1. Foundation | 2/2 | Complete | 2026-03-15 |
| 2. Rooms and Tasks | 5/5 | Complete | 2026-03-15 |
| 3. Daily Plan and Cleanliness | 3/3 | Complete | 2026-03-16 |
-| 4. Notifications | 0/TBD | Not started | - |
+| 4. Notifications | 0/3 | In progress | - |
diff --git a/.planning/phases/04-notifications/04-01-PLAN.md b/.planning/phases/04-notifications/04-01-PLAN.md
new file mode 100644
index 0000000..8210c13
--- /dev/null
+++ b/.planning/phases/04-notifications/04-01-PLAN.md
@@ -0,0 +1,339 @@
+---
+phase: 04-notifications
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - pubspec.yaml
+ - android/app/build.gradle.kts
+ - android/app/src/main/AndroidManifest.xml
+ - lib/main.dart
+ - lib/core/notifications/notification_service.dart
+ - lib/core/notifications/notification_settings_notifier.dart
+ - lib/core/notifications/notification_settings_notifier.g.dart
+ - lib/features/home/data/daily_plan_dao.dart
+ - lib/features/home/data/daily_plan_dao.g.dart
+ - lib/l10n/app_de.arb
+ - test/core/notifications/notification_service_test.dart
+ - test/core/notifications/notification_settings_notifier_test.dart
+autonomous: true
+requirements:
+ - NOTF-01
+ - NOTF-02
+
+must_haves:
+ truths:
+ - "NotificationService can schedule a daily notification at a given TimeOfDay"
+ - "NotificationService can cancel all scheduled notifications"
+ - "NotificationService can request POST_NOTIFICATIONS permission"
+ - "NotificationSettingsNotifier persists enabled boolean and TimeOfDay to SharedPreferences"
+ - "NotificationSettingsNotifier loads persisted values on build"
+ - "DailyPlanDao can return a one-shot count of overdue + today tasks"
+ - "Timezone is initialized before any notification scheduling"
+ - "Android build compiles with core library desugaring enabled"
+ - "AndroidManifest has POST_NOTIFICATIONS permission, RECEIVE_BOOT_COMPLETED permission, and boot receiver"
+ artifacts:
+ - path: "lib/core/notifications/notification_service.dart"
+ provides: "Singleton wrapper around FlutterLocalNotificationsPlugin"
+ exports: ["NotificationService"]
+ - path: "lib/core/notifications/notification_settings_notifier.dart"
+ provides: "Riverpod notifier for notification enabled + time"
+ exports: ["NotificationSettings", "NotificationSettingsNotifier"]
+ - path: "test/core/notifications/notification_service_test.dart"
+ provides: "Unit tests for scheduling, cancel, permission"
+ - path: "test/core/notifications/notification_settings_notifier_test.dart"
+ provides: "Unit tests for persistence and state management"
+ key_links:
+ - from: "lib/core/notifications/notification_service.dart"
+ to: "flutter_local_notifications"
+ via: "FlutterLocalNotificationsPlugin"
+ pattern: "FlutterLocalNotificationsPlugin"
+ - from: "lib/core/notifications/notification_settings_notifier.dart"
+ to: "shared_preferences"
+ via: "SharedPreferences persistence"
+ pattern: "SharedPreferences\\.getInstance"
+ - from: "lib/main.dart"
+ to: "lib/core/notifications/notification_service.dart"
+ via: "timezone init + service initialize"
+ pattern: "NotificationService.*initialize"
+---
+
+
+Install notification packages, configure Android build system, create the NotificationService singleton and NotificationSettingsNotifier provider, add one-shot DAO query for task counts, initialize timezone in main.dart, add ARB strings, and write unit tests.
+
+Purpose: Establish the complete notification infrastructure so Plan 02 can wire it into the Settings UI.
+Output: Working notification service and settings notifier with full test coverage. Android build configuration complete.
+
+
+
+@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
+@/home/jlmak/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/04-notifications/04-CONTEXT.md
+@.planning/phases/04-notifications/04-RESEARCH.md
+
+
+
+
+From lib/core/theme/theme_provider.dart (pattern to follow for notifier):
+```dart
+@riverpod
+class ThemeNotifier extends _$ThemeNotifier {
+ @override
+ ThemeMode build() {
+ _loadPersistedThemeMode();
+ return ThemeMode.system; // sync default, then async load overrides
+ }
+ Future setThemeMode(ThemeMode mode) async { ... }
+}
+```
+
+From lib/features/home/data/daily_plan_dao.dart (DAO to extend):
+```dart
+@DriftAccessor(tables: [Tasks, Rooms, TaskCompletions])
+class DailyPlanDao extends DatabaseAccessor with _$DailyPlanDaoMixin {
+ DailyPlanDao(super.attachedDatabase);
+ Stream> watchAllTasksWithRoomName() { ... }
+ Stream watchCompletionsToday({DateTime? today}) { ... }
+}
+```
+
+From lib/main.dart (entry point to modify):
+```dart
+void main() {
+ WidgetsFlutterBinding.ensureInitialized();
+ runApp(const ProviderScope(child: App()));
+}
+```
+
+From lib/core/router/router.dart (top-level GoRouter for notification tap):
+```dart
+final router = GoRouter(
+ initialLocation: '/',
+ routes: [ StatefulShellRoute.indexedStack(...) ],
+);
+```
+
+From lib/l10n/app_de.arb (localization file, 92 existing keys):
+Last key: "dailyPlanNoTasks": "Noch keine Aufgaben angelegt"
+
+From android/app/build.gradle.kts:
+```kotlin
+android {
+ compileSdk = flutter.compileSdkVersion
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+}
+```
+
+From android/app/src/main/AndroidManifest.xml:
+No notification-related entries exist yet. Only standard Flutter activity + meta-data.
+
+
+
+
+
+
+ Task 1: Android config, packages, NotificationService, timezone init, DAO query, ARB strings
+
+ pubspec.yaml,
+ android/app/build.gradle.kts,
+ android/app/src/main/AndroidManifest.xml,
+ lib/main.dart,
+ lib/core/notifications/notification_service.dart,
+ lib/features/home/data/daily_plan_dao.dart,
+ lib/features/home/data/daily_plan_dao.g.dart,
+ lib/l10n/app_de.arb
+
+
+ 1. **Add packages** to pubspec.yaml dependencies:
+ - `flutter_local_notifications: ^21.0.0`
+ - `timezone: ^0.9.4`
+ - `flutter_timezone: ^1.0.8`
+ Run `flutter pub get`.
+
+ 2. **Configure Android build** in `android/app/build.gradle.kts`:
+ - Set `compileSdk = 35` (explicit, replacing `flutter.compileSdkVersion`)
+ - Add `isCoreLibraryDesugaringEnabled = true` inside `compileOptions`
+ - Add `coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")` in `dependencies` block
+
+ 3. **Configure AndroidManifest.xml** — add inside `` (outside ``):
+ - ``
+ - ``
+ Add inside ``:
+ - `ScheduledNotificationReceiver` with `android:exported="false"`
+ - `ScheduledNotificationBootReceiver` with `android:exported="true"` and intent-filter for BOOT_COMPLETED, MY_PACKAGE_REPLACED, QUICKBOOT_POWERON, HTC QUICKBOOT_POWERON
+ Use the exact XML from RESEARCH.md Pattern section.
+
+ 4. **Create NotificationService** at `lib/core/notifications/notification_service.dart`:
+ - Singleton pattern (factory constructor + static `_instance`)
+ - `final _plugin = FlutterLocalNotificationsPlugin();`
+ - `Future initialize()`: AndroidInitializationSettings with `@mipmap/ic_launcher`, call `_plugin.initialize(settings, onDidReceiveNotificationResponse: _onTap)`
+ - `Future requestPermission()`: resolve Android implementation, call `requestNotificationsPermission()`, return `granted ?? false`
+ - `Future scheduleDailyNotification({required TimeOfDay time, required String title, required String body})`:
+ - Call `_plugin.cancelAll()` first
+ - Compute `_nextInstanceOf(time)` as TZDateTime
+ - AndroidNotificationDetails: channelId `'daily_summary'`, channelName `'Tagliche Zusammenfassung'`, channelDescription `'Tagliche Aufgaben-Erinnerung'`, importance default, priority default
+ - Call `_plugin.zonedSchedule(0, title: title, body: body, scheduledDate: scheduledDate, details, androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, matchDateTimeComponents: DateTimeComponents.time)`
+ - `Future cancelAll()`: delegates to `_plugin.cancelAll()`
+ - `tz.TZDateTime _nextInstanceOf(TimeOfDay time)`: compute next occurrence (today if in future, tomorrow otherwise)
+ - `static void _onTap(NotificationResponse response)`: no-op for now (Plan 02 wires navigation)
+
+ 5. **Add one-shot DAO query** to `lib/features/home/data/daily_plan_dao.dart`:
+ ```dart
+ /// One-shot count of overdue + today tasks (for notification body).
+ Future getOverdueAndTodayTaskCount({DateTime? today}) async {
+ final now = today ?? DateTime.now();
+ final endOfToday = DateTime(now.year, now.month, now.day + 1);
+ final result = await (selectOnly(tasks)
+ ..addColumns([tasks.id.count()])
+ ..where(tasks.nextDueDate.isSmallerThanValue(endOfToday)))
+ .getSingle();
+ return result.read(tasks.id.count()) ?? 0;
+ }
+
+ /// One-shot count of overdue tasks only (for notification body split).
+ Future getOverdueTaskCount({DateTime? today}) async {
+ final now = today ?? DateTime.now();
+ final startOfToday = DateTime(now.year, now.month, now.day);
+ final result = await (selectOnly(tasks)
+ ..addColumns([tasks.id.count()])
+ ..where(tasks.nextDueDate.isSmallerThanValue(startOfToday)))
+ .getSingle();
+ return result.read(tasks.id.count()) ?? 0;
+ }
+ ```
+ Run `dart run build_runner build --delete-conflicting-outputs` to regenerate DAO.
+
+ 6. **Initialize timezone in main.dart** — before `NotificationService().initialize()`:
+ ```dart
+ import 'package:timezone/data/latest_all.dart' as tz;
+ import 'package:timezone/timezone.dart' as tz;
+ import 'package:flutter_timezone/flutter_timezone.dart';
+
+ void main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+ tz.initializeTimeZones();
+ final timeZoneName = await FlutterTimezone.getLocalTimezone();
+ tz.setLocalLocation(tz.getLocation(timeZoneName));
+ await NotificationService().initialize();
+ runApp(const ProviderScope(child: App()));
+ }
+ ```
+
+ 7. **Add ARB strings** to `lib/l10n/app_de.arb`:
+ - `settingsSectionNotifications`: "Benachrichtigungen"
+ - `notificationsEnabledLabel`: "Tägliche Erinnerung"
+ - `notificationsTimeLabel`: "Uhrzeit"
+ - `notificationsPermissionDeniedHint`: "Benachrichtigungen sind in den Systemeinstellungen deaktiviert. Tippe hier, um sie zu aktivieren."
+ - `notificationTitle`: "Dein Tagesplan"
+ - `notificationBody`: "{count} Aufgaben fällig" with `@notificationBody` placeholder `count: int`
+ - `notificationBodyWithOverdue`: "{count} Aufgaben fällig ({overdue} überfällig)" with `@notificationBodyWithOverdue` placeholders `count: int, overdue: int`
+
+ 8. Run `flutter pub get` and `flutter test` to confirm no regressions (expect 72 existing tests to pass).
+
+
+ cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test
+
+
+ - flutter_local_notifications, timezone, flutter_timezone in pubspec.yaml
+ - build.gradle.kts has compileSdk=35, desugaring enabled, desugar_jdk_libs dependency
+ - AndroidManifest has POST_NOTIFICATIONS, RECEIVE_BOOT_COMPLETED, both receivers
+ - NotificationService exists with initialize, requestPermission, scheduleDailyNotification, cancelAll
+ - DailyPlanDao has getOverdueAndTodayTaskCount and getOverdueTaskCount one-shot queries
+ - main.dart initializes timezone and notification service before runApp
+ - ARB file has 7 new notification-related keys
+ - All 72 existing tests still pass
+
+
+
+
+ Task 2: NotificationSettingsNotifier and unit tests
+
+ lib/core/notifications/notification_settings_notifier.dart,
+ lib/core/notifications/notification_settings_notifier.g.dart,
+ test/core/notifications/notification_settings_notifier_test.dart,
+ test/core/notifications/notification_service_test.dart
+
+
+ - Test: NotificationSettingsNotifier build() returns default state (enabled=false, time=07:00)
+ - Test: setEnabled(true) updates state.enabled to true and persists to SharedPreferences
+ - Test: setEnabled(false) updates state.enabled to false and persists to SharedPreferences
+ - Test: setTime(TimeOfDay(hour: 9, minute: 30)) updates state.time and persists hour+minute to SharedPreferences
+ - Test: After _load() with existing prefs (enabled=true, hour=8, minute=15), state reflects persisted values
+ - Test: NotificationService._nextInstanceOf returns today if time is in the future
+ - Test: NotificationService._nextInstanceOf returns tomorrow if time has passed
+
+
+ 1. **Create NotificationSettingsNotifier** at `lib/core/notifications/notification_settings_notifier.dart`:
+ - Use `@Riverpod(keepAlive: true)` annotation (NOT plain `@riverpod`) to survive tab switches
+ - `class NotificationSettings { final bool enabled; final TimeOfDay time; const NotificationSettings({required this.enabled, required this.time}); }`
+ - `class NotificationSettingsNotifier extends _$NotificationSettingsNotifier`
+ - `build()` returns `NotificationSettings(enabled: false, time: TimeOfDay(hour: 7, minute: 0))` synchronously, then calls `_load()` which async reads SharedPreferences and updates state
+ - `Future setEnabled(bool enabled)`: update state, persist `notifications_enabled` bool
+ - `Future setTime(TimeOfDay time)`: update state, persist `notifications_hour` int and `notifications_minute` int
+ - Follow exact pattern from ThemeNotifier: sync return default, async load overrides state
+ - Add `part 'notification_settings_notifier.g.dart';`
+
+ 2. Run `dart run build_runner build --delete-conflicting-outputs` to generate `.g.dart`.
+
+ 3. **Write tests** at `test/core/notifications/notification_settings_notifier_test.dart`:
+ - Use `SharedPreferences.setMockInitialValues({})` for clean state
+ - Use `SharedPreferences.setMockInitialValues({'notifications_enabled': true, 'notifications_hour': 8, 'notifications_minute': 15})` for pre-existing state
+ - Create a `ProviderContainer` with the notifier, verify default state, call `setEnabled`/`setTime`, verify state updates and SharedPreferences values
+
+ 4. **Write tests** at `test/core/notifications/notification_service_test.dart`:
+ - Test `_nextInstanceOf` logic by extracting it to a `@visibleForTesting` static method or by testing `scheduleDailyNotification` with a mock plugin
+ - Since `FlutterLocalNotificationsPlugin` dispatches to native and cannot be truly unit-tested, focus tests on:
+ a. `_nextInstanceOf` returns correct TZDateTime (make it a package-private or `@visibleForTesting` method)
+ b. Verify the service can be instantiated (singleton pattern)
+ - Initialize timezone in test setUp: `tz.initializeTimeZones(); tz.setLocalLocation(tz.getLocation('Europe/Berlin'));`
+
+ 5. Run `flutter test test/core/notifications/` to confirm new tests pass.
+ 6. Run `flutter test` to confirm all tests pass (72 existing + new).
+
+
+ cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/core/notifications/
+
+
+ - NotificationSettingsNotifier with @Riverpod(keepAlive: true) created and generated
+ - NotificationSettings data class with enabled + time fields
+ - SharedPreferences persistence for enabled, hour, minute
+ - Unit tests for default state, setEnabled, setTime, persistence load
+ - Unit tests for _nextInstanceOf timezone logic
+ - All tests pass including existing 72
+
+
+
+
+
+
+- `flutter test` — all tests pass (72 existing + new notification tests)
+- `dart analyze --fatal-infos` — no warnings or errors
+- `grep -r "flutter_local_notifications" pubspec.yaml` — package present
+- `grep -r "isCoreLibraryDesugaringEnabled" android/app/build.gradle.kts` — desugaring enabled
+- `grep -r "POST_NOTIFICATIONS" android/app/src/main/AndroidManifest.xml` — permission present
+- `grep -r "RECEIVE_BOOT_COMPLETED" android/app/src/main/AndroidManifest.xml` — permission present
+- `grep -r "ScheduledNotificationBootReceiver" android/app/src/main/AndroidManifest.xml` — receiver present
+
+
+
+- NotificationService singleton with initialize, requestPermission, scheduleDailyNotification, cancelAll
+- NotificationSettingsNotifier persists enabled + time to SharedPreferences
+- DailyPlanDao has one-shot overdue+today count queries
+- Android build configured for flutter_local_notifications v21
+- Timezone initialized in main.dart
+- All tests pass, dart analyze clean
+
+
+
diff --git a/.planning/phases/04-notifications/04-02-PLAN.md b/.planning/phases/04-notifications/04-02-PLAN.md
new file mode 100644
index 0000000..4625402
--- /dev/null
+++ b/.planning/phases/04-notifications/04-02-PLAN.md
@@ -0,0 +1,317 @@
+---
+phase: 04-notifications
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 04-01
+files_modified:
+ - lib/features/settings/presentation/settings_screen.dart
+ - lib/core/router/router.dart
+ - lib/core/notifications/notification_service.dart
+ - test/features/settings/settings_screen_test.dart
+autonomous: true
+requirements:
+ - NOTF-01
+ - NOTF-02
+
+must_haves:
+ truths:
+ - "Settings screen shows a Benachrichtigungen section between Darstellung and Uber"
+ - "SwitchListTile toggles notification enabled/disabled"
+ - "When toggle is ON, time picker row appears below with progressive disclosure animation"
+ - "When toggle is OFF, time picker row is hidden"
+ - "Tapping time row opens Material 3 showTimePicker dialog"
+ - "Toggling ON requests POST_NOTIFICATIONS permission on Android 13+"
+ - "If permission denied, toggle reverts to OFF"
+ - "If permanently denied, user is guided to system notification settings"
+ - "When enabled + time set, daily notification is scheduled with correct body from DAO query"
+ - "Skip notification scheduling when task count is 0"
+ - "Notification body shows overdue count only when overdue > 0"
+ - "Tapping notification navigates to Home tab"
+ artifacts:
+ - path: "lib/features/settings/presentation/settings_screen.dart"
+ provides: "Benachrichtigungen section with toggle and time picker"
+ contains: "SwitchListTile"
+ - path: "test/features/settings/settings_screen_test.dart"
+ provides: "Widget tests for notification settings UI"
+ key_links:
+ - from: "lib/features/settings/presentation/settings_screen.dart"
+ to: "lib/core/notifications/notification_settings_notifier.dart"
+ via: "ref.watch(notificationSettingsNotifierProvider)"
+ pattern: "notificationSettingsNotifierProvider"
+ - from: "lib/features/settings/presentation/settings_screen.dart"
+ to: "lib/core/notifications/notification_service.dart"
+ via: "NotificationService().scheduleDailyNotification"
+ pattern: "NotificationService.*schedule"
+ - from: "lib/core/router/router.dart"
+ to: "lib/core/notifications/notification_service.dart"
+ via: "notification tap navigates to /"
+ pattern: "router\\.go\\('/'\\)"
+---
+
+
+Wire the notification infrastructure into the Settings UI with permission flow, add notification scheduling on toggle/time change, implement notification tap navigation, and write widget tests.
+
+Purpose: Complete the user-facing notification feature — users can enable notifications, pick a time, and receive daily task summaries.
+Output: Fully functional notification settings with permission handling, scheduling, and navigation.
+
+
+
+@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
+@/home/jlmak/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/04-notifications/04-CONTEXT.md
+@.planning/phases/04-notifications/04-RESEARCH.md
+@.planning/phases/04-notifications/04-01-SUMMARY.md
+
+
+
+
+From lib/core/notifications/notification_service.dart (created in Plan 01):
+```dart
+class NotificationService {
+ static final NotificationService _instance = NotificationService._internal();
+ factory NotificationService() => _instance;
+
+ Future initialize() async { ... }
+ Future requestPermission() async { ... }
+ Future scheduleDailyNotification({
+ required TimeOfDay time,
+ required String title,
+ required String body,
+ }) async { ... }
+ Future cancelAll() async { ... }
+}
+```
+
+From lib/core/notifications/notification_settings_notifier.dart (created in Plan 01):
+```dart
+class NotificationSettings {
+ final bool enabled;
+ final TimeOfDay time;
+ const NotificationSettings({required this.enabled, required this.time});
+}
+
+@Riverpod(keepAlive: true)
+class NotificationSettingsNotifier extends _$NotificationSettingsNotifier {
+ NotificationSettings build() { ... }
+ Future setEnabled(bool enabled) async { ... }
+ Future setTime(TimeOfDay time) async { ... }
+}
+// Generated provider: notificationSettingsNotifierProvider
+```
+
+From lib/features/home/data/daily_plan_dao.dart (extended in Plan 01):
+```dart
+Future getOverdueAndTodayTaskCount({DateTime? today}) async { ... }
+Future getOverdueTaskCount({DateTime? today}) async { ... }
+```
+
+From lib/features/settings/presentation/settings_screen.dart (existing):
+```dart
+class SettingsScreen extends ConsumerWidget {
+ // ListView with: Darstellung section, Divider, Uber section
+}
+```
+
+From lib/core/router/router.dart (existing):
+```dart
+final router = GoRouter(initialLocation: '/', routes: [ ... ]);
+```
+
+From lib/l10n/app_de.arb (notification strings from Plan 01):
+- settingsSectionNotifications, notificationsEnabledLabel, notificationsTimeLabel
+- notificationsPermissionDeniedHint
+- notificationTitle, notificationBody, notificationBodyWithOverdue
+
+
+
+
+
+
+ Task 1: Settings UI with Benachrichtigungen section, permission flow, and notification scheduling
+
+ lib/features/settings/presentation/settings_screen.dart,
+ lib/core/router/router.dart,
+ lib/core/notifications/notification_service.dart
+
+
+ 1. **Modify SettingsScreen** (`lib/features/settings/presentation/settings_screen.dart`):
+ - Change from `ConsumerWidget` to `ConsumerStatefulWidget` (needed for async permission/scheduling logic in callbacks)
+ - Add `ref.watch(notificationSettingsNotifierProvider)` to get current `NotificationSettings`
+ - Insert new section BETWEEN the Darstellung Divider and the Uber section header:
+
+ ```
+ const Divider(indent: 16, endIndent: 16, height: 32),
+
+ // Section 2: Notifications (Benachrichtigungen)
+ Padding(
+ padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
+ child: Text(
+ l10n.settingsSectionNotifications,
+ style: theme.textTheme.titleMedium?.copyWith(
+ color: theme.colorScheme.primary,
+ ),
+ ),
+ ),
+ SwitchListTile(
+ title: Text(l10n.notificationsEnabledLabel),
+ value: notificationSettings.enabled,
+ onChanged: (value) => _onNotificationToggle(value),
+ ),
+ // Progressive disclosure: time picker only when enabled
+ AnimatedSize(
+ duration: const Duration(milliseconds: 200),
+ child: notificationSettings.enabled
+ ? ListTile(
+ title: Text(l10n.notificationsTimeLabel),
+ trailing: Text(notificationSettings.time.format(context)),
+ onTap: () => _onPickTime(),
+ )
+ : const SizedBox.shrink(),
+ ),
+
+ const Divider(indent: 16, endIndent: 16, height: 32),
+
+ // Section 3: About (Uber) — existing code, unchanged
+ ```
+
+ 2. **Implement `_onNotificationToggle(bool value)`**:
+ - If `value == true` (enabling):
+ a. Call `NotificationService().requestPermission()` — await result
+ b. If `granted == false`: check if permanently denied. On Android, this means `shouldShowRequestRationale` returns false after denial. Since we don't have `permission_handler`, use a simpler approach: if `requestPermission()` returns false, show a SnackBar with `l10n.notificationsPermissionDeniedHint` and an action that calls `openAppSettings()` (import `flutter_local_notifications` for this, or use `AppSettings.openAppSettings()`). Actually, the simplest approach: if permission denied, show a SnackBar with the hint text. Do NOT change the toggle to ON. Return early.
+ c. If `granted == true`: call `ref.read(notificationSettingsNotifierProvider.notifier).setEnabled(true)`, then schedule notification via `_scheduleNotification()`
+ - If `value == false` (disabling):
+ a. Call `ref.read(notificationSettingsNotifierProvider.notifier).setEnabled(false)`
+ b. Call `NotificationService().cancelAll()`
+
+ 3. **Implement `_scheduleNotification()`** helper:
+ - Get the database instance from the Riverpod container: `ref.read(appDatabaseProvider)` (or access DailyPlanDao directly — check how other screens access the database and follow that pattern)
+ - Query `DailyPlanDao(db).getOverdueAndTodayTaskCount()` for total count
+ - Query `DailyPlanDao(db).getOverdueTaskCount()` for overdue count
+ - If total count == 0: call `NotificationService().cancelAll()` and return (skip-on-zero per CONTEXT.md)
+ - Build notification body:
+ - If overdue > 0: use `l10n.notificationBodyWithOverdue(total, overdue)`
+ - If overdue == 0: use `l10n.notificationBody(total)`
+ - Title: `l10n.notificationTitle` (which is "Dein Tagesplan")
+ - Call `NotificationService().scheduleDailyNotification(time: settings.time, title: title, body: body)`
+
+ 4. **Implement `_onPickTime()`**:
+ - Call `showTimePicker(context: context, initialTime: currentSettings.time, initialEntryMode: TimePickerEntryMode.dial)`
+ - If picked is not null: call `ref.read(notificationSettingsNotifierProvider.notifier).setTime(picked)`, then call `_scheduleNotification()` to reschedule with new time
+
+ 5. **Wire notification tap navigation** in `lib/core/notifications/notification_service.dart`:
+ - Update `_onTap` to use the top-level `router` instance from `lib/core/router/router.dart`:
+ ```dart
+ import 'package:household_keeper/core/router/router.dart';
+ static void _onTap(NotificationResponse response) {
+ router.go('/');
+ }
+ ```
+ - This works because `router` is a top-level `final` in router.dart, accessible without BuildContext.
+
+ 6. **Handle permanently denied state** (Claude's discretion):
+ - Use a simple approach: if `requestPermission()` returns false AND the toggle was tapped:
+ - First denial: just show SnackBar with hint text
+ - Track denial in SharedPreferences (`notifications_permission_denied_once` bool)
+ - If previously denied and denied again: show SnackBar with action button "Einstellungen offnen" that navigates to system notification settings via `AndroidFlutterLocalNotificationsPlugin`'s `openNotificationSettings()` method or Android intent
+ - Alternatively (simpler): always show the same SnackBar with the hint text on denial. If the user taps it, attempt to open system settings. This avoids tracking denial state.
+ - Pick the simpler approach: SnackBar with `notificationsPermissionDeniedHint` text. No tracking needed. The SnackBar message already says "Tippe hier, um sie zu aktivieren" — make the SnackBar action open app notification settings.
+
+ 7. Run `dart analyze --fatal-infos` to ensure no warnings.
+
+
+ cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && dart analyze --fatal-infos
+
+
+ - Settings screen shows Benachrichtigungen section between Darstellung and Uber
+ - SwitchListTile toggles notification on/off
+ - Time picker row with AnimatedSize progressive disclosure
+ - showTimePicker dialog on time row tap
+ - Permission requested on toggle ON (Android 13+)
+ - Toggle reverts to OFF on permission denial with SnackBar hint
+ - Notification scheduled with task count body on enable/time change
+ - Skip scheduling on zero-task days
+ - Notification body includes overdue split when overdue > 0
+ - Tapping notification navigates to Home tab via router.go('/')
+ - dart analyze clean
+
+
+
+
+ Task 2: Widget tests for notification settings UI
+
+ test/features/settings/settings_screen_test.dart
+
+
+ - Test: Settings screen renders Benachrichtigungen section header
+ - Test: SwitchListTile displays with label "Tagliche Erinnerung" and defaults to OFF
+ - Test: When notificationSettings.enabled is true, time picker ListTile is visible
+ - Test: When notificationSettings.enabled is false, time picker ListTile is not visible
+ - Test: Time picker displays formatted time (e.g. "07:00")
+
+
+ 1. **Create or extend** `test/features/settings/settings_screen_test.dart`:
+ - Check if file exists; if so, extend it. If not, create it.
+ - Use provider overrides to inject mock NotificationSettingsNotifier state:
+ - Override `notificationSettingsNotifierProvider` with a mock/override that returns known state
+ - Also override `themeProvider` to provide ThemeMode.system (to avoid SharedPreferences issues in tests)
+
+ 2. **Write widget tests**:
+ a. "renders Benachrichtigungen section header":
+ - Pump `SettingsScreen` wrapped in `MaterialApp` with localization delegates + `ProviderScope` with overrides
+ - Verify `find.text('Benachrichtigungen')` finds one widget
+ b. "notification toggle defaults to OFF":
+ - Override notifier with `enabled: false`
+ - Verify `SwitchListTile` value is false
+ c. "time picker visible when enabled":
+ - Override notifier with `enabled: true, time: TimeOfDay(hour: 9, minute: 30)`
+ - Verify `find.text('09:30')` (or formatted equivalent) finds one widget
+ - Verify `find.text('Uhrzeit')` finds one widget
+ d. "time picker hidden when disabled":
+ - Override notifier with `enabled: false`
+ - Verify `find.text('Uhrzeit')` finds nothing
+
+ 3. Run `flutter test test/features/settings/` to confirm tests pass.
+ 4. Run `flutter test` to confirm full suite passes.
+
+
+ cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/features/settings/
+
+
+ - Widget tests exist for notification settings section rendering
+ - Tests cover: section header present, toggle default OFF, time picker visibility on/off, time display
+ - All tests pass including full suite
+
+
+
+
+
+
+- `flutter test` — all tests pass (existing + new notification + settings tests)
+- `dart analyze --fatal-infos` — no warnings or errors
+- Settings screen has Benachrichtigungen section with toggle and conditional time picker
+- Permission flow correctly handles grant, deny, and permanently denied
+- Notification schedules/cancels based on toggle and time changes
+- Notification tap opens Home tab
+
+
+
+- User can toggle notifications on/off from Settings
+- Time picker appears only when notifications are enabled
+- Permission requested contextually on toggle ON
+- Denied permission reverts toggle with helpful SnackBar
+- Notification scheduled with task count body (or skipped on zero tasks)
+- Notification tap navigates to daily plan
+- Widget tests cover key UI states
+
+
+
diff --git a/.planning/phases/04-notifications/04-03-PLAN.md b/.planning/phases/04-notifications/04-03-PLAN.md
new file mode 100644
index 0000000..753b00c
--- /dev/null
+++ b/.planning/phases/04-notifications/04-03-PLAN.md
@@ -0,0 +1,96 @@
+---
+phase: 04-notifications
+plan: 03
+type: execute
+wave: 3
+depends_on:
+ - 04-02
+files_modified: []
+autonomous: true
+requirements:
+ - NOTF-01
+ - NOTF-02
+
+must_haves:
+ truths:
+ - "dart analyze --fatal-infos passes with zero issues"
+ - "All tests pass (72 existing + new notification tests)"
+ - "NOTF-01 artifacts exist: NotificationService, DAO queries, AndroidManifest permissions, timezone init"
+ - "NOTF-02 artifacts exist: NotificationSettingsNotifier, Settings UI section, toggle, time picker"
+ - "Phase 4 requirements are fully addressed"
+ artifacts: []
+ key_links: []
+---
+
+
+Verify all Phase 4 notification work is complete, tests pass, and code is clean before marking the phase done.
+
+Purpose: Quality gate ensuring NOTF-01 and NOTF-02 are fully implemented before phase completion.
+Output: Verification confirmation with test counts and analysis results.
+
+
+
+@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
+@/home/jlmak/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/04-notifications/04-CONTEXT.md
+@.planning/phases/04-notifications/04-01-SUMMARY.md
+@.planning/phases/04-notifications/04-02-SUMMARY.md
+
+
+
+
+
+ Task 1: Run full verification suite
+
+
+ 1. Run `dart analyze --fatal-infos` — must produce zero issues.
+ 2. Run `flutter test` — must produce zero failures. Record total test count.
+ 3. Verify NOTF-01 requirements by checking file existence and content:
+ - `lib/core/notifications/notification_service.dart` exists with `scheduleDailyNotification`, `requestPermission`, `cancelAll`
+ - `lib/features/home/data/daily_plan_dao.dart` has `getOverdueAndTodayTaskCount` and `getOverdueTaskCount`
+ - `android/app/src/main/AndroidManifest.xml` has `POST_NOTIFICATIONS`, `RECEIVE_BOOT_COMPLETED`, `ScheduledNotificationBootReceiver` with `exported="true"`
+ - `android/app/build.gradle.kts` has `isCoreLibraryDesugaringEnabled = true` and `compileSdk = 35`
+ - `lib/main.dart` has timezone initialization and `NotificationService().initialize()`
+ 4. Verify NOTF-02 requirements by checking:
+ - `lib/core/notifications/notification_settings_notifier.dart` exists with `setEnabled`, `setTime`
+ - `lib/features/settings/presentation/settings_screen.dart` has `SwitchListTile` and `AnimatedSize` for notification section
+ - `lib/l10n/app_de.arb` has notification-related keys (`settingsSectionNotifications`, `notificationsEnabledLabel`, etc.)
+ 5. Verify notification tap navigation:
+ - `lib/core/notifications/notification_service.dart` `_onTap` references `router.go('/')`
+ 6. If any issues found, fix them. If all checks pass, record results.
+
+
+ cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && dart analyze --fatal-infos && flutter test
+
+
+ - dart analyze: zero issues
+ - flutter test: all tests pass (72 existing + new notification/settings tests)
+ - NOTF-01: NotificationService, DAO queries, Android config, timezone init all confirmed present and functional
+ - NOTF-02: NotificationSettingsNotifier, Settings UI section, toggle, time picker all confirmed present and functional
+ - Phase 4 verification gate passed
+
+
+
+
+
+
+- `dart analyze --fatal-infos` — zero issues
+- `flutter test` — all tests pass
+- All NOTF-01 and NOTF-02 artifacts exist and are correctly wired
+
+
+
+- Phase 4 code is clean (no analysis warnings)
+- All tests pass
+- Both requirements (NOTF-01, NOTF-02) have their artifacts present and correctly implemented
+
+
+