Compare commits
31 Commits
v1.0.2
...
7536f2f759
| Author | SHA1 | Date | |
|---|---|---|---|
| 7536f2f759 | |||
| 27b1a80f29 | |||
| 88ef248a33 | |||
| f718ee8483 | |||
| 01de2d0f9c | |||
| 588f215078 | |||
| 68ba7c65ce | |||
| c666f9a1c6 | |||
| f5c4b4928f | |||
| 31d4ef879b | |||
| fe7ba21061 | |||
| 90ff66223c | |||
| b7a243603c | |||
| fa26d6b301 | |||
| 6bb1bc35d7 | |||
| ead085ad26 | |||
| 74a801c6f2 | |||
| 0059095e38 | |||
| 8c72403c85 | |||
| 1a1a10c9ea | |||
| 36126acc18 | |||
| 76192e22fa | |||
| 9c2ae12012 | |||
| dcb2cd0afa | |||
| c2570cdc01 | |||
| 3c2ad5c7c6 | |||
| f6272a39b4 | |||
| 170326dd85 | |||
| 74de67de59 | |||
| b0765795b8 | |||
| 489c0d5c4f |
@@ -4,10 +4,14 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker
|
||||||
|
env:
|
||||||
|
ANDROID_HOME: /opt/android-sdk
|
||||||
|
ANDROID_SDK_ROOT: /opt/android-sdk
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -18,12 +22,67 @@ jobs:
|
|||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Install Android SDK packages
|
||||||
|
run: |
|
||||||
|
sdkmanager --licenses >/dev/null <<'EOF'
|
||||||
|
y
|
||||||
|
y
|
||||||
|
y
|
||||||
|
y
|
||||||
|
y
|
||||||
|
y
|
||||||
|
y
|
||||||
|
y
|
||||||
|
y
|
||||||
|
y
|
||||||
|
EOF
|
||||||
|
sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0"
|
||||||
|
|
||||||
|
- name: Install jq
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
SUDO=""
|
||||||
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
SUDO="sudo"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
$SUDO apt-get update
|
||||||
|
$SUDO apt-get install -y jq
|
||||||
|
elif command -v apk >/dev/null 2>&1; then
|
||||||
|
$SUDO apk add --no-cache jq
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
$SUDO dnf install -y jq
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
$SUDO yum install -y jq
|
||||||
|
else
|
||||||
|
echo "Could not find a supported package manager to install jq"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup Flutter
|
- name: Setup Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: '3.11.0'
|
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
|
|
||||||
|
- name: Trust Flutter SDK git directory
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
FLUTTER_BIN_DIR="$(dirname "$(command -v flutter)")"
|
||||||
|
FLUTTER_SDK_DIR="$(cd "$FLUTTER_BIN_DIR/.." && pwd -P)"
|
||||||
|
git config --global --add safe.directory "$FLUTTER_SDK_DIR"
|
||||||
|
if [ -n "${FLUTTER_ROOT:-}" ]; then
|
||||||
|
git config --global --add safe.directory "$FLUTTER_ROOT"
|
||||||
|
fi
|
||||||
|
# Runner-specific fallback observed in failing logs
|
||||||
|
git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.4-x64 || true
|
||||||
|
|
||||||
|
- name: Verify Android + Flutter toolchain
|
||||||
|
run: flutter doctor -v
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
@@ -48,8 +107,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup F-Droid Server Tools
|
- name: Setup F-Droid Server Tools
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
SUDO=""
|
||||||
sudo apt-get install -y fdroidserver sshpass
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
SUDO="sudo"
|
||||||
|
fi
|
||||||
|
$SUDO apt-get update
|
||||||
|
# sshpass from apt, fdroidserver via pip to get a newer androguard that
|
||||||
|
# can parse modern Flutter/AGP APKs (apt ships fdroidserver 2.2.1 which crashes)
|
||||||
|
$SUDO apt-get install -y sshpass python3-pip
|
||||||
|
pip3 install --break-system-packages --upgrade fdroidserver
|
||||||
|
|
||||||
- name: Initialize or fetch F-Droid Repository
|
- name: Initialize or fetch F-Droid Repository
|
||||||
env:
|
env:
|
||||||
@@ -66,10 +132,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Copy new APK to repo
|
- name: Copy new APK to repo
|
||||||
run: |
|
run: |
|
||||||
# The app-release.apk name should ideally include the version number
|
set -e
|
||||||
# so it doesn't overwrite older versions in the repo.
|
mkdir -p fdroid/repo
|
||||||
VERSION_TAG=${GITHUB_REF#refs/tags/} # gets 'v1.0.0'
|
|
||||||
cp build/app/outputs/flutter-apk/app-release.apk fdroid/repo/my_flutter_app_${VERSION_TAG}.apk
|
# Prefer tag name for release builds; fallback to ref name for manual runs.
|
||||||
|
REF_NAME="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}"
|
||||||
|
SAFE_REF_NAME="$(echo "$REF_NAME" | tr '/ ' '__' | tr -cd '[:alnum:]_.-')"
|
||||||
|
if [ -z "$SAFE_REF_NAME" ]; then
|
||||||
|
SAFE_REF_NAME="${GITHUB_SHA:-manual}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp build/app/outputs/flutter-apk/app-release.apk "fdroid/repo/my_flutter_app_${SAFE_REF_NAME}.apk"
|
||||||
|
|
||||||
- name: Generate F-Droid Index
|
- name: Generate F-Droid Index
|
||||||
run: |
|
run: |
|
||||||
@@ -82,5 +155,14 @@ jobs:
|
|||||||
USER: ${{ secrets.HETZNER_USER }}
|
USER: ${{ secrets.HETZNER_USER }}
|
||||||
PASS: ${{ secrets.HETZNER_PASS }}
|
PASS: ${{ secrets.HETZNER_PASS }}
|
||||||
run: |
|
run: |
|
||||||
# Use rsync to efficiently upload only the changed files (the new APK and updated index files)
|
set -euo pipefail
|
||||||
sshpass -p "$PASS" rsync -avz -e "ssh -o StrictHostKeyChecking=no" fdroid/repo/ $USER@$HOST:dev/fdroid/repo/
|
REMOTE_REPO_DIR="dev/fdroid/repo"
|
||||||
|
SCP_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=20"
|
||||||
|
|
||||||
|
# Use SCP/SFTP path only (some hosts deny SSH exec channels required by rsync/ssh).
|
||||||
|
if sshpass -p "$PASS" scp $SCP_OPTS -r fdroid/repo/. "$USER@$HOST:$REMOTE_REPO_DIR/"; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback for older SSH servers that require legacy SCP protocol.
|
||||||
|
sshpass -p "$PASS" scp -O $SCP_OPTS -r fdroid/repo/. "$USER@$HOST:$REMOTE_REPO_DIR/"
|
||||||
|
|||||||
20
.planning/MILESTONES.md
Normal file
20
.planning/MILESTONES.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Milestones
|
||||||
|
|
||||||
|
## v1.0 MVP (Shipped: 2026-03-16)
|
||||||
|
|
||||||
|
**Phases completed:** 4 phases, 13 plans
|
||||||
|
**Codebase:** 10,588 LOC Dart (7,773 lib + 2,815 test), 89 tests, 76 commits
|
||||||
|
**Timeline:** 2 days (2026-03-15 to 2026-03-16)
|
||||||
|
|
||||||
|
**Key accomplishments:**
|
||||||
|
1. Flutter project with Drift SQLite, Riverpod 3 state management, ARB localization, and calm sage & stone Material 3 theme
|
||||||
|
2. Full room CRUD with drag-and-drop reorder, icon picker, and cleanliness indicator per room card
|
||||||
|
3. Task CRUD with 11 frequency presets + custom intervals, calendar-anchored scheduling with anchor memory, and auto-calculated next due dates
|
||||||
|
4. Bundled German-language task templates for 14 room types with post-creation template picker
|
||||||
|
5. Daily plan home screen with overdue/today/tomorrow sections, animated checkbox completion, and progress tracking
|
||||||
|
6. Daily summary notification with configurable time, POST_NOTIFICATIONS permission handling, and boot receiver rescheduling
|
||||||
|
|
||||||
|
**Archive:** See `milestones/v1.0-ROADMAP.md` and `milestones/v1.0-REQUIREMENTS.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
@@ -2,30 +2,46 @@
|
|||||||
|
|
||||||
## What This Is
|
## What This Is
|
||||||
|
|
||||||
A local-first Flutter app for organizing household chores and one-time projects, built for personal/couple use on Android. Takes the room-based task scheduling model (inspired by BeTidy), strips cloud/account/social features, and wraps it in a calm, minimal Material 3 UI. Fully offline, free, privacy-respecting — all data stays on-device.
|
A local-first Flutter app for organizing household chores, built for personal/couple use on Android. Uses a room-based task scheduling model where users create rooms, add recurring tasks with frequency intervals, and the app auto-calculates the next due date after each completion. Features a daily plan home screen, bundled German-language task templates, room cleanliness indicators, and daily summary notifications. Fully offline, free, privacy-respecting — all data stays on-device.
|
||||||
|
|
||||||
## Core Value
|
## Core Value
|
||||||
|
|
||||||
Users can see what needs doing today, mark it done, and trust the app to schedule the next occurrence — without thinking about it.
|
Users can see what needs doing today, mark it done, and trust the app to schedule the next occurrence — without thinking about it.
|
||||||
|
|
||||||
|
## Current Milestone: v1.1 Calendar & Polish
|
||||||
|
|
||||||
|
**Goal:** Replace the stacked daily plan with a horizontal calendar strip UI, add task completion history, and task sorting options.
|
||||||
|
|
||||||
|
**Target features:**
|
||||||
|
- Horizontal date-strip calendar with day abbreviation + date number cards
|
||||||
|
- Month color shift for visual boundary between months
|
||||||
|
- Day-selection shows tasks in a list below the strip
|
||||||
|
- Undone tasks carry over to the next day with color accent (overdue marker)
|
||||||
|
- Task completion history log
|
||||||
|
- Additional task sorting (alphabetical, interval, effort)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Validated
|
### Validated
|
||||||
|
|
||||||
(None yet — ship to validate)
|
- Room CRUD with icons and drag-and-drop reorder — v1.0
|
||||||
|
- Task CRUD with frequency intervals and due date calculation — v1.0
|
||||||
|
- Daily plan view with overdue/today/upcoming sections — v1.0
|
||||||
|
- Task completion with auto-scheduling of next due date — v1.0
|
||||||
|
- Bundled task templates per room type (German only, 14 room types) — v1.0
|
||||||
|
- Daily summary notification with configurable time — v1.0
|
||||||
|
- Light/dark theme with calm Material 3 palette — v1.0
|
||||||
|
- Cleanliness indicator per room (based on overdue vs on-time) — v1.0
|
||||||
|
|
||||||
### Active
|
### Active
|
||||||
|
|
||||||
- [ ] Room CRUD with icons and optional photos
|
- [ ] Horizontal calendar strip home screen (replacing stacked daily plan)
|
||||||
- [ ] Task CRUD with frequency intervals and due date calculation
|
- [ ] Overdue task carry-over with visual accent
|
||||||
- [ ] Daily plan view with overdue/today/upcoming sections
|
- [ ] Task completion history log
|
||||||
- [ ] Task completion with auto-scheduling of next due date
|
- [ ] Additional task sorting (alphabetical, interval, effort)
|
||||||
- [ ] Bundled task templates per room type (German only)
|
- [ ] Data export/import (JSON) — deferred
|
||||||
- [ ] Daily summary notification
|
- [ ] English localization — deferred
|
||||||
- [ ] Light/dark theme with calm Material 3 palette
|
- [ ] Room cover photos from camera or gallery — deferred
|
||||||
- [ ] Cleanliness indicator per room (based on overdue vs on-time)
|
|
||||||
- [ ] Task sorting (due date, alphabetical, interval, effort)
|
|
||||||
- [ ] Task history (completion log per task)
|
|
||||||
|
|
||||||
### Out of Scope
|
### Out of Scope
|
||||||
|
|
||||||
@@ -34,8 +50,9 @@ Users can see what needs doing today, mark it done, and trust the app to schedul
|
|||||||
- Subscription model / in-app purchases — free forever
|
- Subscription model / in-app purchases — free forever
|
||||||
- Family profile sharing across devices — single-device app
|
- Family profile sharing across devices — single-device app
|
||||||
- Server-side infrastructure — zero backend
|
- Server-side infrastructure — zero backend
|
||||||
- Data export/import (JSON) — deferred to v1.1
|
- AI-powered task suggestions — overkill for curated templates
|
||||||
- English localization — deferred to v1.1 (ship German-only MVP)
|
- Per-task push notifications — daily summary is more effective
|
||||||
|
- Firebase or any Google cloud services — contradicts local-first design
|
||||||
- Real-time cross-device sync — potential future self-hosted feature
|
- Real-time cross-device sync — potential future self-hosted feature
|
||||||
- Tablet-optimized layout — future enhancement
|
- Tablet-optimized layout — future enhancement
|
||||||
- Statistics & insights dashboard — v2.0
|
- Statistics & insights dashboard — v2.0
|
||||||
@@ -44,11 +61,11 @@ Users can see what needs doing today, mark it done, and trust the app to schedul
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
- Inspired by BeTidy (iOS/Android household cleaning app) — taking the proven room-based model, removing cloud/social, refining the UI
|
- Shipped v1.0 MVP with 10,588 LOC Dart (7,773 lib + 2,815 test), 89 tests
|
||||||
|
- Tech stack: Flutter + Dart, Riverpod 3 + code generation, Drift 2.31 SQLite, GoRouter, flutter_local_notifications
|
||||||
|
- Inspired by BeTidy (iOS/Android household cleaning app) — room-based model, no cloud/social
|
||||||
- Built for personal use with partner on a shared Android device; may publish publicly later
|
- Built for personal use with partner on a shared Android device; may publish publicly later
|
||||||
- Code and comments in English; UI strings German-only for MVP
|
- Code and comments in English; UI strings German-only for v1.0
|
||||||
- Room photos are nice-to-have for MVP — icon-only rooms are sufficient initially
|
|
||||||
- Developer is new to Drift (SQLite ORM) — plan should account for learning curve
|
|
||||||
- Gitea (self-hosted on Hetzner) for version control; no CI/CD pipeline yet
|
- Gitea (self-hosted on Hetzner) for version control; no CI/CD pipeline yet
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
@@ -57,20 +74,23 @@ Users can see what needs doing today, mark it done, and trust the app to schedul
|
|||||||
- **Platform**: Android-first (iOS later)
|
- **Platform**: Android-first (iOS later)
|
||||||
- **Offline**: 100% offline-capable, zero network dependencies
|
- **Offline**: 100% offline-capable, zero network dependencies
|
||||||
- **Privacy**: No data leaves the device, no analytics, no tracking
|
- **Privacy**: No data leaves the device, no analytics, no tracking
|
||||||
- **Language**: German-only UI for MVP, English code/comments
|
- **Language**: German-only UI for v1.0, English code/comments
|
||||||
- **No CI**: No automated build pipeline initially
|
- **No CI**: No automated build pipeline initially
|
||||||
|
|
||||||
## Key Decisions
|
## Key Decisions
|
||||||
|
|
||||||
| Decision | Rationale | Outcome |
|
| Decision | Rationale | Outcome |
|
||||||
|----------|-----------|---------|
|
|----------|-----------|---------|
|
||||||
| Riverpod over Bloc | Modern, compile-safe, less boilerplate, Dart-native | — Pending |
|
| Riverpod 3 over Bloc | Modern, compile-safe, less boilerplate, Dart-native | Good — code generation works well, @riverpod annotation reduces boilerplate |
|
||||||
| Drift over raw sqflite | Type-safe queries, compile-time validation, migration support | — Pending |
|
| Drift over raw sqflite | Type-safe queries, compile-time validation, migration support | Good — DAOs with stream queries provide reactive UI, migration workflow established |
|
||||||
| Android-first | Primary device is Android; iOS follows | — Pending |
|
| Android-first | Primary device is Android; iOS follows | Good — no iOS-specific issues encountered |
|
||||||
| German-only MVP | Primary user language; defer localization infrastructure | — Pending |
|
| German-only MVP | Primary user language; defer localization infrastructure | Good — ARB localization infrastructure in place from Phase 1, ready for English |
|
||||||
| No CI initially | Keep scope focused on the app itself | — Pending |
|
| No CI initially | Keep scope focused on the app itself | Good — manual dart analyze + flutter test sufficient for solo dev |
|
||||||
| Calm Material 3 palette | Muted greens, warm grays, gentle blues — calm productivity, not playful | — Pending |
|
| Calm Material 3 palette | Muted greens, warm grays, gentle blues — calm productivity | Good — sage & stone theme (seed 0xFF7A9A6D) with warm charcoal dark mode |
|
||||||
| Clean Architecture | Feature-based folder structure with data/domain/presentation layers | — Pending |
|
| Clean Architecture | Feature-based folder structure with data/domain/presentation layers | Good — clear separation, easy to navigate |
|
||||||
|
| Calendar-anchored scheduling | Monthly/quarterly/yearly tasks anchor to original day-of-month with clamping | Good — handles Feb 28/31 edge cases correctly with anchor memory |
|
||||||
|
| flutter_local_notifications v21 | Standard Flutter notification package, TZ-aware scheduling | Good — inexactAllowWhileIdle avoids SCHEDULE_EXACT_ALARM complexity |
|
||||||
|
| Manual StreamProvider for drift types | riverpod_generator throws InvalidTypeException with drift Task type | Revisit — may be fixed in future riverpod_generator versions |
|
||||||
|
|
||||||
---
|
---
|
||||||
*Last updated: 2026-03-15 after initialization*
|
*Last updated: 2026-03-16 after v1.1 milestone started*
|
||||||
|
|||||||
@@ -1,102 +1,47 @@
|
|||||||
# Requirements: HouseHoldKeaper
|
# Requirements: HouseHoldKeaper
|
||||||
|
|
||||||
**Defined:** 2026-03-15
|
**Defined:** 2026-03-16
|
||||||
**Core Value:** Users can see what needs doing today, mark it done, and trust the app to schedule the next occurrence — without thinking about it.
|
**Core Value:** Users can see what needs doing today, mark it done, and trust the app to schedule the next occurrence — without thinking about it.
|
||||||
|
|
||||||
## v1 Requirements
|
## v1.1 Requirements
|
||||||
|
|
||||||
Requirements for initial release. Each maps to roadmap phases.
|
Requirements for milestone v1.1 Calendar & Polish. Each maps to roadmap phases.
|
||||||
|
|
||||||
### Room Management
|
### Calendar UI
|
||||||
|
|
||||||
- [x] **ROOM-01**: User can create a room with a name and an icon from a curated Material Icons set
|
- [x] **CAL-01**: User sees a horizontal scrollable date-strip with day abbreviation (Mo, Di...) and date number per card
|
||||||
- [x] **ROOM-02**: User can edit a room's name and icon
|
- [x] **CAL-02**: User can tap a day card to see that day's tasks in a list below the strip
|
||||||
- [x] **ROOM-03**: User can delete a room with confirmation (cascades to associated tasks)
|
- [x] **CAL-03**: User sees a subtle color shift at month boundaries for visual orientation
|
||||||
- [x] **ROOM-04**: User can reorder rooms via drag-and-drop on the rooms screen
|
- [x] **CAL-04**: Calendar strip auto-scrolls to today on app launch
|
||||||
- [x] **ROOM-05**: User can view all rooms as cards showing name, icon, due task count, and cleanliness indicator
|
- [x] **CAL-05**: Undone tasks carry over to the next day with a red/orange color accent marking them as overdue
|
||||||
|
|
||||||
### Task Management
|
### Task History
|
||||||
|
|
||||||
- [x] **TASK-01**: User can create a task within a room with name, optional description, frequency interval, and effort level
|
- [ ] **HIST-01**: Each task completion is recorded with a timestamp
|
||||||
- [x] **TASK-02**: User can edit a task's name, description, frequency interval, and effort level
|
- [ ] **HIST-02**: User can view past completion dates for any individual task
|
||||||
- [x] **TASK-03**: User can delete a task with confirmation
|
|
||||||
- [x] **TASK-04**: User can set frequency interval from: daily, every 2 days, every 3 days, weekly, biweekly, monthly, every 2 months, quarterly, every 6 months, yearly, or custom (every N days)
|
|
||||||
- [x] **TASK-05**: User can set effort level (low / medium / high) on a task
|
|
||||||
- [x] **TASK-06**: User can sort tasks within a room by due date (default sort order)
|
|
||||||
- [x] **TASK-07**: User can mark a task as done via tap or swipe, which records a completion timestamp and auto-calculates the next due date based on the interval
|
|
||||||
- [x] **TASK-08**: Overdue tasks are visually highlighted with distinct color/badge on room cards and in task lists
|
|
||||||
|
|
||||||
### Task Templates
|
### Task Sorting
|
||||||
|
|
||||||
- [x] **TMPL-01**: When creating a room, user can select from bundled German-language task templates appropriate for that room type
|
- [ ] **SORT-01**: User can sort tasks alphabetically
|
||||||
- [x] **TMPL-02**: Preset room types with templates include: Küche, Badezimmer, Schlafzimmer, Wohnzimmer, Flur, Büro, Garage, Balkon, Waschküche, Keller, Kinderzimmer, Gästezimmer, Esszimmer, Garten/Außenbereich
|
- [ ] **SORT-02**: User can sort tasks by frequency interval
|
||||||
|
- [ ] **SORT-03**: User can sort tasks by effort level
|
||||||
|
|
||||||
### Daily Plan
|
## Future Requirements
|
||||||
|
|
||||||
- [x] **PLAN-01**: User sees all tasks due today grouped by room on the daily plan screen (primary/default screen)
|
|
||||||
- [x] **PLAN-02**: Overdue tasks appear in a separate highlighted section at the top of the daily plan
|
|
||||||
- [x] **PLAN-03**: User can preview upcoming tasks (tomorrow / this week)
|
|
||||||
- [x] **PLAN-04**: User can swipe-to-complete or tap checkbox to mark tasks done directly from the daily plan view
|
|
||||||
- [x] **PLAN-05**: User sees a progress indicator showing completed vs total tasks for today (e.g. "5 of 12 tasks done")
|
|
||||||
- [x] **PLAN-06**: When no tasks are due, user sees an encouraging "all clear" empty state
|
|
||||||
|
|
||||||
### Cleanliness Indicator
|
|
||||||
|
|
||||||
- [x] **CLEAN-01**: Each room card displays a cleanliness indicator derived from the ratio of overdue tasks to total tasks in that room
|
|
||||||
|
|
||||||
### Notifications
|
|
||||||
|
|
||||||
- [x] **NOTF-01**: User receives a daily summary notification showing today's task count at a configurable time
|
|
||||||
- [x] **NOTF-02**: User can enable/disable notifications in settings
|
|
||||||
|
|
||||||
### Theme & UI
|
|
||||||
|
|
||||||
- [x] **THEME-01**: App supports light and dark themes, following the system setting by default
|
|
||||||
- [x] **THEME-02**: App uses a calm Material 3 palette with muted greens, warm grays, and gentle blues
|
|
||||||
|
|
||||||
### Foundation
|
|
||||||
|
|
||||||
- [x] **FOUND-01**: App uses Drift for local SQLite storage with proper schema migration workflow
|
|
||||||
- [x] **FOUND-02**: App uses Riverpod 3 for state management with code generation
|
|
||||||
- [x] **FOUND-03**: App uses localization infrastructure (ARB files + AppLocalizations) with German locale, even though only one language ships in v1
|
|
||||||
- [x] **FOUND-04**: Bottom navigation with tabs: Home (Daily Plan), Rooms, Settings
|
|
||||||
|
|
||||||
## v2 Requirements
|
|
||||||
|
|
||||||
Deferred to future release. Tracked but not in current roadmap.
|
Deferred to future release. Tracked but not in current roadmap.
|
||||||
|
|
||||||
### v1.1 — Near-Term
|
### Data
|
||||||
|
|
||||||
- **EXPORT-01**: User can export all data as JSON file
|
- **DATA-01**: User can export all data as JSON
|
||||||
- **EXPORT-02**: User can import data from a JSON file
|
- **DATA-02**: User can import data from JSON backup
|
||||||
- **I18N-01**: App supports English as a second language
|
|
||||||
- **PHOTO-01**: User can add a cover photo to a room from camera or gallery
|
|
||||||
- **HIST-01**: User can view a completion history log per task (scrollable timeline of completion dates)
|
|
||||||
- **SORT-01**: User can sort tasks by alphabetical order, interval length, or effort level (in addition to due date)
|
|
||||||
|
|
||||||
### v1.2 — Medium-Term
|
### Localization
|
||||||
|
|
||||||
- **PROJ-01**: User can create one-time organization projects with sub-task steps
|
- **LOC-01**: User can switch UI language to English
|
||||||
- **PROJ-02**: User can attach before/after photos to a project
|
|
||||||
- **PROF-01**: User can create named local profiles for household members
|
|
||||||
- **PROF-02**: User can assign tasks to one or more profiles
|
|
||||||
- **PROF-03**: User can enable task rotation (round-robin) for shared recurring tasks
|
|
||||||
- **PROF-04**: User can filter the daily plan view by profile ("My tasks" vs "All tasks")
|
|
||||||
- **WIDG-01**: Home screen widget showing today's due tasks and overdue count
|
|
||||||
- **CAL-01**: User can view a weekly overview with task load per day
|
|
||||||
- **CAL-02**: User can view a monthly calendar heatmap showing task density
|
|
||||||
- **VAC-01**: User can pause/freeze all task due dates during vacation and resume on return
|
|
||||||
|
|
||||||
### v2.0 — Future
|
### Rooms
|
||||||
|
|
||||||
- **STAT-01**: User can view completion rate (% on time this week/month)
|
- **ROOM-01**: User can set a cover photo for a room from camera or gallery
|
||||||
- **STAT-02**: User can view streak of consecutive days with all tasks completed
|
|
||||||
- **STAT-03**: User can view per-room health scores over time
|
|
||||||
- **ONBRD-01**: First-launch wizard walks user through creating first room and adding tasks
|
|
||||||
- **COLOR-01**: User can pick a custom accent color for the app theme
|
|
||||||
- **SYNC-01**: User can optionally sync data via self-hosted infrastructure
|
|
||||||
- **TABLET-01**: App provides a tablet-optimized layout with adaptive breakpoints
|
|
||||||
- **NOTF-03**: Optional evening nudge notification if overdue tasks remain
|
|
||||||
|
|
||||||
## Out of Scope
|
## Out of Scope
|
||||||
|
|
||||||
@@ -104,14 +49,10 @@ Explicitly excluded. Documented to prevent scope creep.
|
|||||||
|
|
||||||
| Feature | Reason |
|
| Feature | Reason |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| User accounts & cloud sync | Local-only by design — zero backend, zero data leaves device |
|
| Weekly/monthly calendar views | Overcomplicates UI — date strip is sufficient for task app |
|
||||||
| Leaderboards & points ranking | Anti-feature — gamification causes app burnout and embeds unequal labor dynamics |
|
| Drag tasks between days | Not needed — tasks auto-schedule based on frequency |
|
||||||
| Subscription model / in-app purchases | Free forever by design — paywalls are the #2 complaint in chore app reviews |
|
| Calendar sync (Google/Apple) | Contradicts local-first, offline-only design |
|
||||||
| Family profile sharing across devices | Single-device app; cross-device requires cloud infrastructure |
|
| Task statistics/charts | Deferred to v2.0 — history log is the foundation |
|
||||||
| AI-powered task suggestions | Requires network/ML; overkill for personal app with curated templates |
|
|
||||||
| Per-task push notifications | Causes notification fatigue; daily summary is more effective for habit formation |
|
|
||||||
| Focus timer / Pomodoro | Not a productivity timer app; out of domain |
|
|
||||||
| Firebase or any Google cloud services | Contradicts local-first, privacy-first design |
|
|
||||||
|
|
||||||
## Traceability
|
## Traceability
|
||||||
|
|
||||||
@@ -119,42 +60,22 @@ Which phases cover which requirements. Updated during roadmap creation.
|
|||||||
|
|
||||||
| Requirement | Phase | Status |
|
| Requirement | Phase | Status |
|
||||||
|-------------|-------|--------|
|
|-------------|-------|--------|
|
||||||
| FOUND-01 | Phase 1: Foundation | Complete |
|
| CAL-01 | Phase 5 | Complete |
|
||||||
| FOUND-02 | Phase 1: Foundation | Complete |
|
| CAL-02 | Phase 5 | Complete |
|
||||||
| FOUND-03 | Phase 1: Foundation | Complete |
|
| CAL-03 | Phase 5 | Complete |
|
||||||
| FOUND-04 | Phase 1: Foundation | Complete |
|
| CAL-04 | Phase 5 | Complete |
|
||||||
| THEME-01 | Phase 1: Foundation | Complete |
|
| CAL-05 | Phase 5 | Complete |
|
||||||
| THEME-02 | Phase 1: Foundation | Complete |
|
| HIST-01 | Phase 6 | Pending |
|
||||||
| ROOM-01 | Phase 2: Rooms and Tasks | Complete |
|
| HIST-02 | Phase 6 | Pending |
|
||||||
| ROOM-02 | Phase 2: Rooms and Tasks | Complete |
|
| SORT-01 | Phase 7 | Pending |
|
||||||
| ROOM-03 | Phase 2: Rooms and Tasks | Complete |
|
| SORT-02 | Phase 7 | Pending |
|
||||||
| ROOM-04 | Phase 2: Rooms and Tasks | Complete |
|
| SORT-03 | Phase 7 | Pending |
|
||||||
| ROOM-05 | Phase 2: Rooms and Tasks | Complete |
|
|
||||||
| TASK-01 | Phase 2: Rooms and Tasks | Complete |
|
|
||||||
| TASK-02 | Phase 2: Rooms and Tasks | Complete |
|
|
||||||
| TASK-03 | Phase 2: Rooms and Tasks | Complete |
|
|
||||||
| TASK-04 | Phase 2: Rooms and Tasks | Complete |
|
|
||||||
| TASK-05 | Phase 2: Rooms and Tasks | Complete |
|
|
||||||
| TASK-06 | Phase 2: Rooms and Tasks | Complete |
|
|
||||||
| TASK-07 | Phase 2: Rooms and Tasks | Complete |
|
|
||||||
| TASK-08 | Phase 2: Rooms and Tasks | Complete |
|
|
||||||
| TMPL-01 | Phase 2: Rooms and Tasks | Complete |
|
|
||||||
| TMPL-02 | Phase 2: Rooms and Tasks | Complete |
|
|
||||||
| PLAN-01 | Phase 3: Daily Plan and Cleanliness | Complete |
|
|
||||||
| PLAN-02 | Phase 3: Daily Plan and Cleanliness | Complete |
|
|
||||||
| PLAN-03 | Phase 3: Daily Plan and Cleanliness | Complete |
|
|
||||||
| PLAN-04 | Phase 3: Daily Plan and Cleanliness | Complete |
|
|
||||||
| PLAN-05 | Phase 3: Daily Plan and Cleanliness | Complete |
|
|
||||||
| PLAN-06 | Phase 3: Daily Plan and Cleanliness | Complete |
|
|
||||||
| CLEAN-01 | Phase 3: Daily Plan and Cleanliness | Complete |
|
|
||||||
| NOTF-01 | Phase 4: Notifications | Complete |
|
|
||||||
| NOTF-02 | Phase 4: Notifications | Complete |
|
|
||||||
|
|
||||||
**Coverage:**
|
**Coverage:**
|
||||||
- v1 requirements: 30 total
|
- v1.1 requirements: 10 total
|
||||||
- Mapped to phases: 30
|
- Mapped to phases: 10
|
||||||
- Unmapped: 0
|
- Unmapped: 0
|
||||||
|
|
||||||
---
|
---
|
||||||
*Requirements defined: 2026-03-15*
|
*Requirements defined: 2026-03-16*
|
||||||
*Last updated: 2026-03-15 after roadmap creation*
|
*Last updated: 2026-03-16 after roadmap creation (phases 5-7)*
|
||||||
|
|||||||
65
.planning/RETROSPECTIVE.md
Normal file
65
.planning/RETROSPECTIVE.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Project Retrospective
|
||||||
|
|
||||||
|
*A living document updated after each milestone. Lessons feed forward into future planning.*
|
||||||
|
|
||||||
|
## Milestone: v1.0 — MVP
|
||||||
|
|
||||||
|
**Shipped:** 2026-03-16
|
||||||
|
**Phases:** 4 | **Plans:** 13
|
||||||
|
|
||||||
|
### What Was Built
|
||||||
|
- Complete room-based household chore app with auto-scheduling task management
|
||||||
|
- Daily plan home screen with overdue/today/tomorrow sections and progress tracking
|
||||||
|
- Bundled German task templates for 14 room types
|
||||||
|
- Daily summary notifications with configurable time and Android permission handling
|
||||||
|
- 89 tests covering DAOs, scheduling logic, providers, and widget behavior
|
||||||
|
|
||||||
|
### What Worked
|
||||||
|
- Bottom-up phase structure (foundation -> data -> UI -> polish) kept each phase clean with minimal rework
|
||||||
|
- TDD approach for providers and services caught several issues early (async race conditions, API mismatches)
|
||||||
|
- Verification gates at the end of Phase 2, 3, and 4 confirmed all requirements before moving on
|
||||||
|
- Calendar-anchored scheduling with anchor memory was designed right the first time — no rework needed
|
||||||
|
- ARB localization from Phase 1 meant adding German strings was frictionless throughout
|
||||||
|
|
||||||
|
### What Was Inefficient
|
||||||
|
- riverpod_generator InvalidTypeException with drift Task type required workaround (manual StreamProvider) in 3 separate plans — should have been caught in Phase 1 research
|
||||||
|
- Some plan specifications referenced outdated API patterns (flutter_local_notifications positional parameters removed in v20+) — research needs to verify exact current API signatures
|
||||||
|
- Phase 4 plan checkboxes in ROADMAP.md weren't updated to [x] by executor — minor bookkeeping gap
|
||||||
|
|
||||||
|
### Patterns Established
|
||||||
|
- `@Riverpod(keepAlive: true)` AsyncNotifier with SharedPreferences for persistent settings (ThemeNotifier, NotificationSettingsNotifier)
|
||||||
|
- Manual StreamProvider.family/autoDispose for drift type compatibility
|
||||||
|
- DailyPlanDao innerJoin pattern for cross-table queries
|
||||||
|
- ConsumerStatefulWidget for screens with async callbacks requiring `mounted` guards
|
||||||
|
- Provider override pattern in widget tests for database isolation
|
||||||
|
|
||||||
|
### Key Lessons
|
||||||
|
1. Research phase should verify exact current package API signatures — breaking changes between major versions cause plan deviations
|
||||||
|
2. Drift + riverpod_generator type incompatibility is a known issue — plan for manual providers from the start when using drift
|
||||||
|
3. Verification gates add minimal time (~2 min) but catch integration issues — keep them for all phases
|
||||||
|
4. Progressive disclosure (AnimatedSize) is a clean pattern for conditional settings UI
|
||||||
|
|
||||||
|
### Cost Observations
|
||||||
|
- Model mix: orchestrator on opus, researchers/planners/executors/checkers on sonnet
|
||||||
|
- Total execution: ~1.3 hours for 13 plans across 4 phases
|
||||||
|
- Notable: Verification gates averaged 2 min — very efficient for the confidence they provide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Milestone Trends
|
||||||
|
|
||||||
|
### Process Evolution
|
||||||
|
|
||||||
|
| Milestone | Phases | Plans | Key Change |
|
||||||
|
|-----------|--------|-------|------------|
|
||||||
|
| v1.0 | 4 | 13 | Initial project — established all patterns |
|
||||||
|
|
||||||
|
### Cumulative Quality
|
||||||
|
|
||||||
|
| Milestone | Tests | Key Metric |
|
||||||
|
|-----------|-------|------------|
|
||||||
|
| v1.0 | 89 | dart analyze clean, 0 issues |
|
||||||
|
|
||||||
|
### Top Lessons (Verified Across Milestones)
|
||||||
|
|
||||||
|
1. (Single milestone — lessons above will be cross-validated as more milestones ship)
|
||||||
@@ -1,100 +1,76 @@
|
|||||||
# Roadmap: HouseHoldKeaper
|
# Roadmap: HouseHoldKeaper
|
||||||
|
|
||||||
## Overview
|
## Milestones
|
||||||
|
|
||||||
Four phases build the app bottom-up along its natural dependency chain. Phase 1 lays the technical foundation every subsequent phase relies on. Phase 2 delivers complete room and task management — the core scheduling loop. Phase 3 surfaces that data as the daily plan view (the primary user experience) and adds the cleanliness indicator. Phase 4 adds notifications and completes the v1 feature set. After Phase 3, the app is usable daily. After Phase 4, it is releasable.
|
- **v1.0 MVP** — Phases 1-4 (shipped 2026-03-16)
|
||||||
|
- **v1.1 Calendar & Polish** — Phases 5-7 (in progress)
|
||||||
|
|
||||||
## Phases
|
## Phases
|
||||||
|
|
||||||
**Phase Numbering:**
|
<details>
|
||||||
- Integer phases (1, 2, 3): Planned milestone work
|
<summary>v1.0 MVP (Phases 1-4) — SHIPPED 2026-03-16</summary>
|
||||||
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
|
|
||||||
|
|
||||||
Decimal phases appear between their surrounding integers in numeric order.
|
- [x] Phase 1: Foundation (2/2 plans) — completed 2026-03-15
|
||||||
|
- [x] Phase 2: Rooms and Tasks (5/5 plans) — completed 2026-03-15
|
||||||
|
- [x] Phase 3: Daily Plan and Cleanliness (3/3 plans) — completed 2026-03-16
|
||||||
|
- [x] Phase 4: Notifications (3/3 plans) — completed 2026-03-16
|
||||||
|
|
||||||
- [x] **Phase 1: Foundation** - Project scaffold, database, state management, theme, and localization infrastructure (completed 2026-03-15)
|
See `milestones/v1.0-ROADMAP.md` for full phase details.
|
||||||
- [x] **Phase 2: Rooms and Tasks** - Complete room CRUD, task CRUD with auto-scheduling, and bundled templates (completed 2026-03-15)
|
|
||||||
- [x] **Phase 3: Daily Plan and Cleanliness** - Primary daily plan screen with overdue/today/upcoming, cleanliness indicators per room (completed 2026-03-16)
|
</details>
|
||||||
- [x] **Phase 4: Notifications** - Daily summary notification with configurable time and Android permission handling (completed 2026-03-16)
|
|
||||||
|
**v1.1 Calendar & Polish (Phases 5-7):**
|
||||||
|
|
||||||
|
- [x] **Phase 5: Calendar Strip** - Replace the stacked daily plan home screen with a horizontal scrollable date-strip and day-task list (completed 2026-03-16)
|
||||||
|
- [ ] **Phase 6: Task History** - Record every task completion with a timestamp and expose a per-task history view
|
||||||
|
- [ ] **Phase 7: Task Sorting** - Add alphabetical, interval, and effort sort options to task lists
|
||||||
|
|
||||||
## Phase Details
|
## Phase Details
|
||||||
|
|
||||||
### Phase 1: Foundation
|
### Phase 5: Calendar Strip
|
||||||
**Goal**: The app compiles, opens, and enforces correct architecture patterns — ready to receive features without accumulating technical debt
|
**Goal**: Users navigate their tasks through a horizontal date-strip that replaces the stacked daily plan, seeing today's tasks by default and any day's tasks on tap
|
||||||
**Depends on**: Nothing (first phase)
|
**Depends on**: Phase 4 (v1.0 shipped — all data layer and scheduling in place)
|
||||||
**Requirements**: FOUND-01, FOUND-02, FOUND-03, FOUND-04, THEME-01, THEME-02
|
**Requirements**: CAL-01, CAL-02, CAL-03, CAL-04, CAL-05
|
||||||
**Success Criteria** (what must be TRUE):
|
**Success Criteria** (what must be TRUE):
|
||||||
1. App launches on Android without errors and shows a bottom navigation bar with Home, Rooms, and Settings tabs
|
1. The home screen shows a horizontal scrollable strip of day cards, each displaying the German day abbreviation (Mo, Di, Mi...) and the date number
|
||||||
2. Light and dark themes work correctly and follow the system setting by default, using the calm Material 3 palette (muted greens, warm grays, gentle blues)
|
2. Tapping any day card updates the task list below the strip to show that day's tasks, with the selected card visually highlighted
|
||||||
3. All UI strings are loaded from ARB localization files — no hardcoded German text in Dart code
|
3. On app launch the strip auto-scrolls so today's card is centered and selected by default
|
||||||
4. The Drift database opens on first launch with schemaVersion 1 and the migration workflow is established (drift_dev make-migrations runs without errors)
|
4. When two adjacent day cards span a month boundary, a subtle color shift or divider makes the boundary visible without extra chrome
|
||||||
5. riverpod_lint is active and flags ref.watch usage outside build() as an analysis error
|
5. Tasks that were not completed on their due date appear in subsequent days' lists with a red/orange accent marking them as overdue
|
||||||
**Plans**: 2 plans
|
**Plans:** 2/2 plans complete
|
||||||
Plans:
|
Plans:
|
||||||
- [x] 01-01-PLAN.md — Scaffold Flutter project and build core infrastructure (database, providers, theme, localization)
|
- [ ] 05-01-PLAN.md — Data layer: CalendarDao, CalendarDayState model, Riverpod providers, localization, DAO tests
|
||||||
- [x] 01-02-PLAN.md — Navigation shell, placeholder screens, Settings, and full app wiring
|
- [ ] 05-02-PLAN.md — UI: CalendarStrip, CalendarDayList, CalendarTaskRow widgets, HomeScreen replacement
|
||||||
|
|
||||||
### Phase 2: Rooms and Tasks
|
### Phase 6: Task History
|
||||||
**Goal**: Users can create and manage rooms and tasks, mark tasks done, and trust the app to schedule the next occurrence automatically
|
**Goal**: Users can see exactly when each task was completed in the past, building trust that the scheduling loop is working correctly
|
||||||
**Depends on**: Phase 1
|
**Depends on**: Phase 5
|
||||||
**Requirements**: ROOM-01, ROOM-02, ROOM-03, ROOM-04, ROOM-05, TASK-01, TASK-02, TASK-03, TASK-04, TASK-05, TASK-06, TASK-07, TASK-08, TMPL-01, TMPL-02
|
**Requirements**: HIST-01, HIST-02
|
||||||
**Success Criteria** (what must be TRUE):
|
**Success Criteria** (what must be TRUE):
|
||||||
1. User can create a room with a name and icon, edit it, reorder rooms via drag-and-drop, and delete it (with confirmation that removes all associated tasks)
|
1. Every task completion (tap done in any view) is recorded in the database with a precise timestamp — data persists across app restarts
|
||||||
2. User can create a task in a room with name, description, frequency interval (daily through yearly and custom), and effort level; tasks can be edited and deleted with confirmation
|
2. From a task's detail or context menu the user can open a history view listing all past completion dates for that task in reverse-chronological order
|
||||||
3. When creating a room, user can select from bundled German-language task templates for the chosen room type (all 14 room types covered) and they are added to the room as tasks
|
3. The history view shows a meaningful empty state if the task has never been completed
|
||||||
4. User can mark a task done (tap or swipe), which records the completion and sets the next due date correctly based on the interval
|
**Plans**: TBD
|
||||||
5. Overdue tasks are visually highlighted with a distinct color or badge on room cards and in task lists; tasks within a room are sorted by due date by default
|
|
||||||
6. Each room card shows its name, icon, count of due tasks, and cleanliness indicator
|
|
||||||
**Plans**: 5 plans
|
|
||||||
Plans:
|
|
||||||
- [x] 02-01-PLAN.md — Data layer: Drift tables, migration v1->v2, DAOs, scheduling utility, domain models, templates, tests
|
|
||||||
- [x] 02-02-PLAN.md — Room CRUD UI: 2-column card grid, room form, icon picker, drag-and-drop reorder, providers
|
|
||||||
- [x] 02-03-PLAN.md — Task CRUD UI: task list, task row with completion, task form, overdue highlighting, providers
|
|
||||||
- [x] 02-04-PLAN.md — Template selection: template picker bottom sheet, room type detection, integration with room creation
|
|
||||||
- [x] 02-05-PLAN.md — Visual and functional verification checkpoint
|
|
||||||
|
|
||||||
### Phase 3: Daily Plan and Cleanliness
|
### Phase 7: Task Sorting
|
||||||
**Goal**: Users can open the app and immediately see what needs doing today, act on tasks directly from the plan view, and see a room-level health indicator
|
**Goal**: Users can reorder task lists by the dimension most useful to them — name, how often the task recurs, or how much effort it requires
|
||||||
**Depends on**: Phase 2
|
**Depends on**: Phase 5
|
||||||
**Requirements**: PLAN-01, PLAN-02, PLAN-03, PLAN-04, PLAN-05, PLAN-06, CLEAN-01
|
**Requirements**: SORT-01, SORT-02, SORT-03
|
||||||
**Success Criteria** (what must be TRUE):
|
**Success Criteria** (what must be TRUE):
|
||||||
1. The Home tab shows today's tasks grouped by room, with a separate highlighted section at the top for overdue tasks
|
1. A sort control (dropdown, segmented button, or similar) is visible on task list screens and persists the chosen sort across app restarts
|
||||||
2. User can mark a task done directly from the daily plan view via swipe or checkbox without navigating to the room
|
2. Selecting alphabetical sort orders tasks A-Z by name within the visible list
|
||||||
3. User can see upcoming tasks (tomorrow and this week) from the daily plan screen
|
3. Selecting interval sort orders tasks from most-frequent (daily) to least-frequent (yearly/custom) intervals
|
||||||
4. A progress indicator shows completed vs total tasks for today (e.g., "5 von 12 erledigt")
|
4. Selecting effort sort orders tasks from lowest effort to highest effort level
|
||||||
5. When no tasks are due, an encouraging "all clear" empty state is shown instead of an empty list
|
**Plans**: TBD
|
||||||
6. Each room card displays a cleanliness indicator derived from the ratio of overdue tasks to total tasks in that room
|
|
||||||
**Plans**: 3 plans
|
|
||||||
Plans:
|
|
||||||
- [x] 03-01-PLAN.md — Data layer: DailyPlanDao with cross-room join query, providers, and localization keys
|
|
||||||
- [x] 03-02-PLAN.md — Daily plan UI: HomeScreen rewrite with progress card, task sections, animated completion, empty state
|
|
||||||
- [x] 03-03-PLAN.md — Visual and functional verification checkpoint
|
|
||||||
|
|
||||||
### Phase 4: Notifications
|
|
||||||
**Goal**: Users receive a daily summary notification reminding them of today's task count, and can control notification behavior from settings
|
|
||||||
**Depends on**: Phase 2
|
|
||||||
**Requirements**: NOTF-01, NOTF-02
|
|
||||||
**Success Criteria** (what must be TRUE):
|
|
||||||
1. User receives one daily notification showing the count of tasks due today, scheduled at a configurable time
|
|
||||||
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**: 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
|
## Progress
|
||||||
|
|
||||||
**Execution Order:**
|
| Phase | Milestone | Plans Complete | Status | Completed |
|
||||||
Phases execute in numeric order: 1 -> 2 -> 3 -> 4
|
|-------|-----------|----------------|--------|-----------|
|
||||||
|
| 1. Foundation | v1.0 | 2/2 | Complete | 2026-03-15 |
|
||||||
Note: Phase 4 depends on Phase 2 (needs scheduling data) but can be developed in parallel with Phase 3.
|
| 2. Rooms and Tasks | v1.0 | 5/5 | Complete | 2026-03-15 |
|
||||||
|
| 3. Daily Plan and Cleanliness | v1.0 | 3/3 | Complete | 2026-03-16 |
|
||||||
| Phase | Plans Complete | Status | Completed |
|
| 4. Notifications | v1.0 | 3/3 | Complete | 2026-03-16 |
|
||||||
|-------|----------------|--------|-----------|
|
| 5. Calendar Strip | 2/2 | Complete | 2026-03-16 | - |
|
||||||
| 1. Foundation | 2/2 | Complete | 2026-03-15 |
|
| 6. Task History | v1.1 | 0/? | Not started | - |
|
||||||
| 2. Rooms and Tasks | 5/5 | Complete | 2026-03-15 |
|
| 7. Task Sorting | v1.1 | 0/? | Not started | - |
|
||||||
| 3. Daily Plan and Cleanliness | 3/3 | Complete | 2026-03-16 |
|
|
||||||
| 4. Notifications | 3/3 | Complete | 2026-03-16 |
|
|
||||||
|
|||||||
@@ -3,128 +3,75 @@ gsd_state_version: 1.0
|
|||||||
milestone: v1.0
|
milestone: v1.0
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: executing
|
status: executing
|
||||||
stopped_at: Completed 04-03-PLAN.md (Phase 4 Verification Gate)
|
stopped_at: Completed 05-calendar-strip 05-02-PLAN.md
|
||||||
last_updated: "2026-03-16T14:20:25.850Z"
|
last_updated: "2026-03-16T20:37:30.052Z"
|
||||||
last_activity: 2026-03-16 — Completed 04-01-PLAN.md (Notification infrastructure)
|
last_activity: 2026-03-16 — Completed Phase 5 Plan 01 (CalendarDao + providers)
|
||||||
progress:
|
progress:
|
||||||
total_phases: 4
|
total_phases: 3
|
||||||
completed_phases: 4
|
completed_phases: 1
|
||||||
total_plans: 13
|
total_plans: 2
|
||||||
completed_plans: 13
|
completed_plans: 2
|
||||||
percent: 100
|
percent: 50
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project State
|
# Project State
|
||||||
|
|
||||||
## Project Reference
|
## Project Reference
|
||||||
|
|
||||||
See: .planning/PROJECT.md (updated 2026-03-15)
|
See: .planning/PROJECT.md (updated 2026-03-16)
|
||||||
|
|
||||||
**Core value:** Users can see what needs doing today, mark it done, and trust the app to schedule the next occurrence — without thinking about it.
|
**Core value:** Users can see what needs doing today, mark it done, and trust the app to schedule the next occurrence — without thinking about it.
|
||||||
**Current focus:** Phase 4: Notifications (Phase 3 complete)
|
**Current focus:** v1.1 Calendar & Polish — Phase 5: Calendar Strip
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 4 of 4 (Notifications)
|
Phase: 5 — Calendar Strip
|
||||||
Plan: 1 of 2 in current phase -- COMPLETE
|
Plan: 2/2 complete (Phase 5 done)
|
||||||
Status: Phase 4 in progress — plan 1 complete, plan 2 (Settings UI) pending
|
Status: Phase Complete
|
||||||
Last activity: 2026-03-16 — Completed 04-01-PLAN.md (Notification infrastructure)
|
Last activity: 2026-03-16 — Completed Phase 5 Plan 02 (calendar strip UI)
|
||||||
|
|
||||||
Progress: [██████████] 100%
|
```
|
||||||
|
Progress: [██████████] 100% (2/2 plans in Phase 5)
|
||||||
|
```
|
||||||
|
|
||||||
## Performance Metrics
|
## Performance Metrics
|
||||||
|
|
||||||
**Velocity:**
|
| Metric | v1.0 | v1.1 |
|
||||||
- Total plans completed: 10
|
|--------|------|------|
|
||||||
- Average duration: 6.1 min
|
| Phases | 4 | 3 planned |
|
||||||
- Total execution time: 1.0 hours
|
| Plans | 13 | TBD |
|
||||||
|
| LOC (lib) | 7,773 | TBD |
|
||||||
**By Phase:**
|
| Tests | 89 | TBD |
|
||||||
|
| Phase 05-calendar-strip P01 | 5 | 2 tasks | 10 files |
|
||||||
| Phase | Plans | Total | Avg/Plan |
|
| Phase 05-calendar-strip P02 | 8 | 3 tasks | 9 files |
|
||||||
|-------|-------|-------|----------|
|
|
||||||
| 1 - Foundation | 2 | 15 min | 7.5 min |
|
|
||||||
| 2 - Rooms and Tasks | 5 | 35 min | 7.0 min |
|
|
||||||
| 3 - Daily Plan and Cleanliness | 3 | 11 min | 3.7 min |
|
|
||||||
|
|
||||||
**Recent Trend:**
|
|
||||||
- Last 5 plans: 02-04 (3 min), 02-05 (1 min), 03-01 (5 min), 03-02 (4 min), 03-03 (2 min)
|
|
||||||
- Trend: Verification gates consistently fast (1-2 min)
|
|
||||||
|
|
||||||
*Updated after each plan completion*
|
|
||||||
| Phase 02 P01 | 8 | 2 tasks | 16 files |
|
|
||||||
| Phase 02 P02 | 11 | 2 tasks | 14 files |
|
|
||||||
| Phase 02 P03 | 12 | 2 tasks | 8 files |
|
|
||||||
| Phase 02 P04 | 3 | 2 tasks | 5 files |
|
|
||||||
| Phase 02 P05 | 1 | 1 task | 0 files |
|
|
||||||
| Phase 03 P01 | 5 | 2 tasks | 10 files |
|
|
||||||
| Phase 03 P02 | 4 | 2 tasks | 5 files |
|
|
||||||
| Phase 03 P03 | 2 | 2 tasks | 0 files |
|
|
||||||
| Phase 04-notifications P01 | 9 | 2 tasks | 11 files |
|
|
||||||
| Phase 04-notifications P02 | 5 | 2 tasks | 5 files |
|
|
||||||
| Phase 04-notifications P03 | 2 | 1 tasks | 0 files |
|
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
### Decisions
|
### Decisions
|
||||||
|
|
||||||
Decisions are logged in PROJECT.md Key Decisions table.
|
| Decision | Rationale |
|
||||||
Recent decisions affecting current work:
|
|----------|-----------|
|
||||||
|
| Calendar strip replaces daily plan home screen | v1.1 goal per PROJECT.md — not additive, the stacked overdue/today/upcoming sections are removed |
|
||||||
- [Pre-phase]: Riverpod 3.3 requires Flutter 3.41+ — verify before scaffolding
|
| Phase 5 before Phase 6 and 7 | Calendar strip is the primary UI surface; history and sorting operate within or alongside it |
|
||||||
- [Pre-phase]: All due dates stored as date-only (calendar day), not DateTime — enforce from first migration
|
| Phase 6 and 7 both depend on Phase 5 only | History and sorting are independent of each other — could execute in either order |
|
||||||
- [Pre-phase]: German-only UI for MVP; localization infrastructure (ARB + AppLocalizations) required from Phase 1 even with one locale
|
| HIST-01 and HIST-02 in same phase | Data layer (HIST-01) is only 1-2 DAO additions; grouping with the UI (HIST-02) keeps the phase coherent |
|
||||||
- [Pre-phase]: riverpod_lint must be active before any feature code — catches ref.watch outside build() at analysis time
|
| Used NotifierProvider<SelectedDateNotifier> instead of deprecated StateProvider | Riverpod 3.x removed StateProvider; NotifierProvider is the correct replacement |
|
||||||
- [Pre-phase]: drift_dev make-migrations workflow must be established in Phase 1 — recovery cost is data loss
|
| calendarDayProvider fetches overdue tasks with .first in asyncMap when isToday | Consistent with dailyPlanProvider pattern; avoids combining two streams |
|
||||||
- [01-01]: Pinned drift/drift_dev to 2.31.0 for analyzer ^9.0.0 compatibility with riverpod_generator 4.0.3
|
| watchTasksForDate sorts alphabetically by task name | Same-day tasks have no meaningful time-based order; alpha sort is deterministic and user-friendly |
|
||||||
- [01-01]: Generated Riverpod 3 provider named themeProvider (not themeNotifierProvider) per new naming convention
|
| CalendarStripController as VoidCallback holder | Avoids GlobalKey for single imperative scroll-to-today action — simpler |
|
||||||
- [Phase 01-02]: Used themeProvider (Riverpod 3 naming) instead of themeNotifierProvider referenced in plan
|
| Tests use pump()+pump(Duration) instead of pumpAndSettle() | CalendarStrip animation controllers cause pumpAndSettle timeout — fixed-duration pump steps are reliable |
|
||||||
- [02-01]: Scheduling functions are top-level pure functions with DateTime today parameter for testability
|
|
||||||
- [02-01]: Calendar-anchored intervals use anchorDay nullable field for month-clamping memory
|
|
||||||
- [02-01]: RoomWithStats computed via asyncMap on watchAllRooms stream, not a custom SQL join
|
|
||||||
- [02-01]: Templates stored as Dart const map for type safety, not JSON assets
|
|
||||||
- [02-01]: detectRoomType uses contains-based matching with alias map
|
|
||||||
- [Phase 02]: Scheduling functions are top-level pure functions with DateTime today parameter for testability
|
|
||||||
- [Phase 02]: Calendar-anchored intervals use anchorDay nullable field for month-clamping memory
|
|
||||||
- [Phase 02]: Templates stored as Dart const map for type safety, not JSON assets
|
|
||||||
- [02-02]: ReorderableBuilder<Widget> with typed onReorder callback for drag-and-drop grid
|
|
||||||
- [02-02]: Long-press context menu (bottom sheet) for edit/delete on room cards
|
|
||||||
- [02-02]: Provider override pattern in tests to decouple from database dependency
|
|
||||||
- [02-03]: tasksInRoomProvider defined as manual StreamProvider.family due to riverpod_generator InvalidTypeException with drift Task type
|
|
||||||
- [02-03]: Frequency selector uses ChoiceChip Wrap layout for 10 presets plus custom option
|
|
||||||
- [02-03]: TaskRow uses ListTile with middle-dot separator between relative date and frequency label
|
|
||||||
- [02-04]: Template picker uses StatefulWidget (not Consumer) receiving data via constructor
|
|
||||||
- [02-04]: Room creation navigates to /rooms/$roomId (context.go) instead of context.pop to show new room
|
|
||||||
- [02-04]: Calendar-anchored intervals set anchorDay to today's day-of-month; day-count intervals set null
|
|
||||||
- [02-05]: Auto-approved verification checkpoint: dart analyze clean, 59/59 tests passing, all Phase 2 code integrated
|
|
||||||
- [03-01]: DailyPlanDao uses innerJoin (not leftOuterJoin) since tasks always have a room
|
|
||||||
- [03-01]: watchCompletionsToday uses customSelect with readsFrom for proper stream invalidation
|
|
||||||
- [03-01]: dailyPlanProvider uses manual StreamProvider.autoDispose (not @riverpod) due to drift Task type issue
|
|
||||||
- [03-01]: Progress total = remaining overdue + remaining today + completedTodayCount for stable denominator
|
|
||||||
- [03-02]: Used stream-driven completion with local _completingTaskIds Set for animation instead of AnimatedList
|
|
||||||
- [03-02]: DailyPlanTaskRow is StatelessWidget (not ConsumerWidget) -- completion callback passed in from parent
|
|
||||||
- [03-02]: No-tasks empty state uses dailyPlanNoTasks key for clearer daily plan context messaging
|
|
||||||
- [03-03]: Phase 3 verification gate passed: dart analyze clean, 72/72 tests, all 7 requirements confirmed functional
|
|
||||||
- [Phase 04-01]: timezone constraint upgraded to ^0.11.0 — flutter_local_notifications v21 requires ^0.11.0, plan specified ^0.9.4
|
|
||||||
- [Phase 04-01]: flutter_local_notifications v21 uses named parameters in initialize() and zonedSchedule() — positional API removed in v20+
|
|
||||||
- [Phase 04-01]: Generated Riverpod 3 provider named notificationSettingsProvider (not notificationSettingsNotifierProvider) — consistent with themeProvider naming convention
|
|
||||||
- [Phase 04-01]: nextInstanceOf exposed as @visibleForTesting public method to enable TZ logic unit testing without native dispatch mocking
|
|
||||||
- [Phase Phase 04-02]: openNotificationSettings() not available in flutter_local_notifications v21 — simplified to informational SnackBar (no action button)
|
|
||||||
- [Phase Phase 04-02]: ConsumerStatefulWidget for SettingsScreen — async permission callbacks require mounted guards after every await
|
|
||||||
- [Phase 04-notifications]: Phase 4 verification gate passed: dart analyze --fatal-infos zero issues, 89/89 tests passing — all NOTF-01 and NOTF-02 requirements confirmed functional
|
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
None yet.
|
None.
|
||||||
|
|
||||||
### Blockers/Concerns
|
### Blockers/Concerns
|
||||||
|
|
||||||
- ~~[Research]: Recurrence policy edge cases not fully specified~~ — **RESOLVED** in 2-CONTEXT.md: calendar-anchored intervals clamp to last day with anchor memory, day-count intervals roll forward. Next due from original due date. Catch-up skips to next future date.
|
- Phase 5 complete. daily_plan_providers.dart, daily_plan_task_row.dart, and progress_card.dart are now dead code (safe to clean up in a future phase). DailyPlanDao must NOT be deleted — still used by the notification service.
|
||||||
- [Research]: Notification time configuration (user-adjustable vs hardcoded) not resolved. Decide before Phase 4 planning.
|
|
||||||
- ~~[Research]: First-launch template seeding UX (silent vs prompted) not resolved~~ — **RESOLVED** in 2-CONTEXT.md: post-creation prompt with all templates unchecked. Room type is optional, detected from name. Custom rooms skip templates entirely.
|
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-16T14:13:32.148Z
|
Last session: 2026-03-16T20:37:30.050Z
|
||||||
Stopped at: Completed 04-03-PLAN.md (Phase 4 Verification Gate)
|
Stopped at: Completed 05-calendar-strip 05-02-PLAN.md
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
Next action: `/gsd:plan-phase 5`
|
||||||
|
|||||||
169
.planning/milestones/v1.0-REQUIREMENTS.md
Normal file
169
.planning/milestones/v1.0-REQUIREMENTS.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# Requirements Archive: v1.0 MVP
|
||||||
|
|
||||||
|
**Archived:** 2026-03-16
|
||||||
|
**Status:** SHIPPED
|
||||||
|
|
||||||
|
For current requirements, see `.planning/REQUIREMENTS.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Requirements: HouseHoldKeaper
|
||||||
|
|
||||||
|
**Defined:** 2026-03-15
|
||||||
|
**Core Value:** Users can see what needs doing today, mark it done, and trust the app to schedule the next occurrence — without thinking about it.
|
||||||
|
|
||||||
|
## v1 Requirements
|
||||||
|
|
||||||
|
Requirements for initial release. Each maps to roadmap phases.
|
||||||
|
|
||||||
|
### Room Management
|
||||||
|
|
||||||
|
- [x] **ROOM-01**: User can create a room with a name and an icon from a curated Material Icons set
|
||||||
|
- [x] **ROOM-02**: User can edit a room's name and icon
|
||||||
|
- [x] **ROOM-03**: User can delete a room with confirmation (cascades to associated tasks)
|
||||||
|
- [x] **ROOM-04**: User can reorder rooms via drag-and-drop on the rooms screen
|
||||||
|
- [x] **ROOM-05**: User can view all rooms as cards showing name, icon, due task count, and cleanliness indicator
|
||||||
|
|
||||||
|
### Task Management
|
||||||
|
|
||||||
|
- [x] **TASK-01**: User can create a task within a room with name, optional description, frequency interval, and effort level
|
||||||
|
- [x] **TASK-02**: User can edit a task's name, description, frequency interval, and effort level
|
||||||
|
- [x] **TASK-03**: User can delete a task with confirmation
|
||||||
|
- [x] **TASK-04**: User can set frequency interval from: daily, every 2 days, every 3 days, weekly, biweekly, monthly, every 2 months, quarterly, every 6 months, yearly, or custom (every N days)
|
||||||
|
- [x] **TASK-05**: User can set effort level (low / medium / high) on a task
|
||||||
|
- [x] **TASK-06**: User can sort tasks within a room by due date (default sort order)
|
||||||
|
- [x] **TASK-07**: User can mark a task as done via tap or swipe, which records a completion timestamp and auto-calculates the next due date based on the interval
|
||||||
|
- [x] **TASK-08**: Overdue tasks are visually highlighted with distinct color/badge on room cards and in task lists
|
||||||
|
|
||||||
|
### Task Templates
|
||||||
|
|
||||||
|
- [x] **TMPL-01**: When creating a room, user can select from bundled German-language task templates appropriate for that room type
|
||||||
|
- [x] **TMPL-02**: Preset room types with templates include: Küche, Badezimmer, Schlafzimmer, Wohnzimmer, Flur, Büro, Garage, Balkon, Waschküche, Keller, Kinderzimmer, Gästezimmer, Esszimmer, Garten/Außenbereich
|
||||||
|
|
||||||
|
### Daily Plan
|
||||||
|
|
||||||
|
- [x] **PLAN-01**: User sees all tasks due today grouped by room on the daily plan screen (primary/default screen)
|
||||||
|
- [x] **PLAN-02**: Overdue tasks appear in a separate highlighted section at the top of the daily plan
|
||||||
|
- [x] **PLAN-03**: User can preview upcoming tasks (tomorrow / this week)
|
||||||
|
- [x] **PLAN-04**: User can swipe-to-complete or tap checkbox to mark tasks done directly from the daily plan view
|
||||||
|
- [x] **PLAN-05**: User sees a progress indicator showing completed vs total tasks for today (e.g. "5 of 12 tasks done")
|
||||||
|
- [x] **PLAN-06**: When no tasks are due, user sees an encouraging "all clear" empty state
|
||||||
|
|
||||||
|
### Cleanliness Indicator
|
||||||
|
|
||||||
|
- [x] **CLEAN-01**: Each room card displays a cleanliness indicator derived from the ratio of overdue tasks to total tasks in that room
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
- [x] **NOTF-01**: User receives a daily summary notification showing today's task count at a configurable time
|
||||||
|
- [x] **NOTF-02**: User can enable/disable notifications in settings
|
||||||
|
|
||||||
|
### Theme & UI
|
||||||
|
|
||||||
|
- [x] **THEME-01**: App supports light and dark themes, following the system setting by default
|
||||||
|
- [x] **THEME-02**: App uses a calm Material 3 palette with muted greens, warm grays, and gentle blues
|
||||||
|
|
||||||
|
### Foundation
|
||||||
|
|
||||||
|
- [x] **FOUND-01**: App uses Drift for local SQLite storage with proper schema migration workflow
|
||||||
|
- [x] **FOUND-02**: App uses Riverpod 3 for state management with code generation
|
||||||
|
- [x] **FOUND-03**: App uses localization infrastructure (ARB files + AppLocalizations) with German locale, even though only one language ships in v1
|
||||||
|
- [x] **FOUND-04**: Bottom navigation with tabs: Home (Daily Plan), Rooms, Settings
|
||||||
|
|
||||||
|
## v2 Requirements
|
||||||
|
|
||||||
|
Deferred to future release. Tracked but not in current roadmap.
|
||||||
|
|
||||||
|
### v1.1 — Near-Term
|
||||||
|
|
||||||
|
- **EXPORT-01**: User can export all data as JSON file
|
||||||
|
- **EXPORT-02**: User can import data from a JSON file
|
||||||
|
- **I18N-01**: App supports English as a second language
|
||||||
|
- **PHOTO-01**: User can add a cover photo to a room from camera or gallery
|
||||||
|
- **HIST-01**: User can view a completion history log per task (scrollable timeline of completion dates)
|
||||||
|
- **SORT-01**: User can sort tasks by alphabetical order, interval length, or effort level (in addition to due date)
|
||||||
|
|
||||||
|
### v1.2 — Medium-Term
|
||||||
|
|
||||||
|
- **PROJ-01**: User can create one-time organization projects with sub-task steps
|
||||||
|
- **PROJ-02**: User can attach before/after photos to a project
|
||||||
|
- **PROF-01**: User can create named local profiles for household members
|
||||||
|
- **PROF-02**: User can assign tasks to one or more profiles
|
||||||
|
- **PROF-03**: User can enable task rotation (round-robin) for shared recurring tasks
|
||||||
|
- **PROF-04**: User can filter the daily plan view by profile ("My tasks" vs "All tasks")
|
||||||
|
- **WIDG-01**: Home screen widget showing today's due tasks and overdue count
|
||||||
|
- **CAL-01**: User can view a weekly overview with task load per day
|
||||||
|
- **CAL-02**: User can view a monthly calendar heatmap showing task density
|
||||||
|
- **VAC-01**: User can pause/freeze all task due dates during vacation and resume on return
|
||||||
|
|
||||||
|
### v2.0 — Future
|
||||||
|
|
||||||
|
- **STAT-01**: User can view completion rate (% on time this week/month)
|
||||||
|
- **STAT-02**: User can view streak of consecutive days with all tasks completed
|
||||||
|
- **STAT-03**: User can view per-room health scores over time
|
||||||
|
- **ONBRD-01**: First-launch wizard walks user through creating first room and adding tasks
|
||||||
|
- **COLOR-01**: User can pick a custom accent color for the app theme
|
||||||
|
- **SYNC-01**: User can optionally sync data via self-hosted infrastructure
|
||||||
|
- **TABLET-01**: App provides a tablet-optimized layout with adaptive breakpoints
|
||||||
|
- **NOTF-03**: Optional evening nudge notification if overdue tasks remain
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
Explicitly excluded. Documented to prevent scope creep.
|
||||||
|
|
||||||
|
| Feature | Reason |
|
||||||
|
|---------|--------|
|
||||||
|
| User accounts & cloud sync | Local-only by design — zero backend, zero data leaves device |
|
||||||
|
| Leaderboards & points ranking | Anti-feature — gamification causes app burnout and embeds unequal labor dynamics |
|
||||||
|
| Subscription model / in-app purchases | Free forever by design — paywalls are the #2 complaint in chore app reviews |
|
||||||
|
| Family profile sharing across devices | Single-device app; cross-device requires cloud infrastructure |
|
||||||
|
| AI-powered task suggestions | Requires network/ML; overkill for personal app with curated templates |
|
||||||
|
| Per-task push notifications | Causes notification fatigue; daily summary is more effective for habit formation |
|
||||||
|
| Focus timer / Pomodoro | Not a productivity timer app; out of domain |
|
||||||
|
| Firebase or any Google cloud services | Contradicts local-first, privacy-first design |
|
||||||
|
|
||||||
|
## Traceability
|
||||||
|
|
||||||
|
Which phases cover which requirements. Updated during roadmap creation.
|
||||||
|
|
||||||
|
| Requirement | Phase | Status |
|
||||||
|
|-------------|-------|--------|
|
||||||
|
| FOUND-01 | Phase 1: Foundation | Complete |
|
||||||
|
| FOUND-02 | Phase 1: Foundation | Complete |
|
||||||
|
| FOUND-03 | Phase 1: Foundation | Complete |
|
||||||
|
| FOUND-04 | Phase 1: Foundation | Complete |
|
||||||
|
| THEME-01 | Phase 1: Foundation | Complete |
|
||||||
|
| THEME-02 | Phase 1: Foundation | Complete |
|
||||||
|
| ROOM-01 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| ROOM-02 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| ROOM-03 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| ROOM-04 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| ROOM-05 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| TASK-01 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| TASK-02 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| TASK-03 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| TASK-04 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| TASK-05 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| TASK-06 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| TASK-07 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| TASK-08 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| TMPL-01 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| TMPL-02 | Phase 2: Rooms and Tasks | Complete |
|
||||||
|
| PLAN-01 | Phase 3: Daily Plan and Cleanliness | Complete |
|
||||||
|
| PLAN-02 | Phase 3: Daily Plan and Cleanliness | Complete |
|
||||||
|
| PLAN-03 | Phase 3: Daily Plan and Cleanliness | Complete |
|
||||||
|
| PLAN-04 | Phase 3: Daily Plan and Cleanliness | Complete |
|
||||||
|
| PLAN-05 | Phase 3: Daily Plan and Cleanliness | Complete |
|
||||||
|
| PLAN-06 | Phase 3: Daily Plan and Cleanliness | Complete |
|
||||||
|
| CLEAN-01 | Phase 3: Daily Plan and Cleanliness | Complete |
|
||||||
|
| NOTF-01 | Phase 4: Notifications | Complete |
|
||||||
|
| NOTF-02 | Phase 4: Notifications | Complete |
|
||||||
|
|
||||||
|
**Coverage:**
|
||||||
|
- v1 requirements: 30 total
|
||||||
|
- Mapped to phases: 30
|
||||||
|
- Unmapped: 0
|
||||||
|
|
||||||
|
---
|
||||||
|
*Requirements defined: 2026-03-15*
|
||||||
|
*Last updated: 2026-03-15 after roadmap creation*
|
||||||
100
.planning/milestones/v1.0-ROADMAP.md
Normal file
100
.planning/milestones/v1.0-ROADMAP.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Roadmap: HouseHoldKeaper
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Four phases build the app bottom-up along its natural dependency chain. Phase 1 lays the technical foundation every subsequent phase relies on. Phase 2 delivers complete room and task management — the core scheduling loop. Phase 3 surfaces that data as the daily plan view (the primary user experience) and adds the cleanliness indicator. Phase 4 adds notifications and completes the v1 feature set. After Phase 3, the app is usable daily. After Phase 4, it is releasable.
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
**Phase Numbering:**
|
||||||
|
- Integer phases (1, 2, 3): Planned milestone work
|
||||||
|
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
|
||||||
|
|
||||||
|
Decimal phases appear between their surrounding integers in numeric order.
|
||||||
|
|
||||||
|
- [x] **Phase 1: Foundation** - Project scaffold, database, state management, theme, and localization infrastructure (completed 2026-03-15)
|
||||||
|
- [x] **Phase 2: Rooms and Tasks** - Complete room CRUD, task CRUD with auto-scheduling, and bundled templates (completed 2026-03-15)
|
||||||
|
- [x] **Phase 3: Daily Plan and Cleanliness** - Primary daily plan screen with overdue/today/upcoming, cleanliness indicators per room (completed 2026-03-16)
|
||||||
|
- [x] **Phase 4: Notifications** - Daily summary notification with configurable time and Android permission handling (completed 2026-03-16)
|
||||||
|
|
||||||
|
## Phase Details
|
||||||
|
|
||||||
|
### Phase 1: Foundation
|
||||||
|
**Goal**: The app compiles, opens, and enforces correct architecture patterns — ready to receive features without accumulating technical debt
|
||||||
|
**Depends on**: Nothing (first phase)
|
||||||
|
**Requirements**: FOUND-01, FOUND-02, FOUND-03, FOUND-04, THEME-01, THEME-02
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. App launches on Android without errors and shows a bottom navigation bar with Home, Rooms, and Settings tabs
|
||||||
|
2. Light and dark themes work correctly and follow the system setting by default, using the calm Material 3 palette (muted greens, warm grays, gentle blues)
|
||||||
|
3. All UI strings are loaded from ARB localization files — no hardcoded German text in Dart code
|
||||||
|
4. The Drift database opens on first launch with schemaVersion 1 and the migration workflow is established (drift_dev make-migrations runs without errors)
|
||||||
|
5. riverpod_lint is active and flags ref.watch usage outside build() as an analysis error
|
||||||
|
**Plans**: 2 plans
|
||||||
|
Plans:
|
||||||
|
- [x] 01-01-PLAN.md — Scaffold Flutter project and build core infrastructure (database, providers, theme, localization)
|
||||||
|
- [x] 01-02-PLAN.md — Navigation shell, placeholder screens, Settings, and full app wiring
|
||||||
|
|
||||||
|
### Phase 2: Rooms and Tasks
|
||||||
|
**Goal**: Users can create and manage rooms and tasks, mark tasks done, and trust the app to schedule the next occurrence automatically
|
||||||
|
**Depends on**: Phase 1
|
||||||
|
**Requirements**: ROOM-01, ROOM-02, ROOM-03, ROOM-04, ROOM-05, TASK-01, TASK-02, TASK-03, TASK-04, TASK-05, TASK-06, TASK-07, TASK-08, TMPL-01, TMPL-02
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. User can create a room with a name and icon, edit it, reorder rooms via drag-and-drop, and delete it (with confirmation that removes all associated tasks)
|
||||||
|
2. User can create a task in a room with name, description, frequency interval (daily through yearly and custom), and effort level; tasks can be edited and deleted with confirmation
|
||||||
|
3. When creating a room, user can select from bundled German-language task templates for the chosen room type (all 14 room types covered) and they are added to the room as tasks
|
||||||
|
4. User can mark a task done (tap or swipe), which records the completion and sets the next due date correctly based on the interval
|
||||||
|
5. Overdue tasks are visually highlighted with a distinct color or badge on room cards and in task lists; tasks within a room are sorted by due date by default
|
||||||
|
6. Each room card shows its name, icon, count of due tasks, and cleanliness indicator
|
||||||
|
**Plans**: 5 plans
|
||||||
|
Plans:
|
||||||
|
- [x] 02-01-PLAN.md — Data layer: Drift tables, migration v1->v2, DAOs, scheduling utility, domain models, templates, tests
|
||||||
|
- [x] 02-02-PLAN.md — Room CRUD UI: 2-column card grid, room form, icon picker, drag-and-drop reorder, providers
|
||||||
|
- [x] 02-03-PLAN.md — Task CRUD UI: task list, task row with completion, task form, overdue highlighting, providers
|
||||||
|
- [x] 02-04-PLAN.md — Template selection: template picker bottom sheet, room type detection, integration with room creation
|
||||||
|
- [x] 02-05-PLAN.md — Visual and functional verification checkpoint
|
||||||
|
|
||||||
|
### Phase 3: Daily Plan and Cleanliness
|
||||||
|
**Goal**: Users can open the app and immediately see what needs doing today, act on tasks directly from the plan view, and see a room-level health indicator
|
||||||
|
**Depends on**: Phase 2
|
||||||
|
**Requirements**: PLAN-01, PLAN-02, PLAN-03, PLAN-04, PLAN-05, PLAN-06, CLEAN-01
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. The Home tab shows today's tasks grouped by room, with a separate highlighted section at the top for overdue tasks
|
||||||
|
2. User can mark a task done directly from the daily plan view via swipe or checkbox without navigating to the room
|
||||||
|
3. User can see upcoming tasks (tomorrow and this week) from the daily plan screen
|
||||||
|
4. A progress indicator shows completed vs total tasks for today (e.g., "5 von 12 erledigt")
|
||||||
|
5. When no tasks are due, an encouraging "all clear" empty state is shown instead of an empty list
|
||||||
|
6. Each room card displays a cleanliness indicator derived from the ratio of overdue tasks to total tasks in that room
|
||||||
|
**Plans**: 3 plans
|
||||||
|
Plans:
|
||||||
|
- [x] 03-01-PLAN.md — Data layer: DailyPlanDao with cross-room join query, providers, and localization keys
|
||||||
|
- [x] 03-02-PLAN.md — Daily plan UI: HomeScreen rewrite with progress card, task sections, animated completion, empty state
|
||||||
|
- [x] 03-03-PLAN.md — Visual and functional verification checkpoint
|
||||||
|
|
||||||
|
### Phase 4: Notifications
|
||||||
|
**Goal**: Users receive a daily summary notification reminding them of today's task count, and can control notification behavior from settings
|
||||||
|
**Depends on**: Phase 2
|
||||||
|
**Requirements**: NOTF-01, NOTF-02
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. User receives one daily notification showing the count of tasks due today, scheduled at a configurable time
|
||||||
|
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**: 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
|
||||||
|
|
||||||
|
**Execution Order:**
|
||||||
|
Phases execute in numeric order: 1 -> 2 -> 3 -> 4
|
||||||
|
|
||||||
|
Note: Phase 4 depends on Phase 2 (needs scheduling data) but can be developed in parallel with Phase 3.
|
||||||
|
|
||||||
|
| Phase | Plans Complete | Status | Completed |
|
||||||
|
|-------|----------------|--------|-----------|
|
||||||
|
| 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 | 3/3 | Complete | 2026-03-16 |
|
||||||
262
.planning/phases/05-calendar-strip/05-01-PLAN.md
Normal file
262
.planning/phases/05-calendar-strip/05-01-PLAN.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
---
|
||||||
|
phase: 05-calendar-strip
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- lib/features/home/data/calendar_dao.dart
|
||||||
|
- lib/features/home/data/calendar_dao.g.dart
|
||||||
|
- lib/features/home/domain/calendar_models.dart
|
||||||
|
- lib/features/home/presentation/calendar_providers.dart
|
||||||
|
- lib/core/database/database.dart
|
||||||
|
- lib/core/database/database.g.dart
|
||||||
|
- lib/l10n/app_de.arb
|
||||||
|
- lib/l10n/app_localizations_de.dart
|
||||||
|
- lib/l10n/app_localizations.dart
|
||||||
|
- test/features/home/data/calendar_dao_test.dart
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CAL-02
|
||||||
|
- CAL-05
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Querying tasks for any arbitrary date returns exactly the tasks whose nextDueDate falls on that day"
|
||||||
|
- "Querying overdue tasks for today returns all tasks whose nextDueDate is strictly before today"
|
||||||
|
- "Querying a future date returns only tasks due that day, no overdue carry-over"
|
||||||
|
- "CalendarState model holds selectedDate, overdue tasks, and day tasks as separate lists"
|
||||||
|
- "Localization strings for calendar UI exist in ARB and generated files"
|
||||||
|
artifacts:
|
||||||
|
- path: "lib/features/home/data/calendar_dao.dart"
|
||||||
|
provides: "Date-parameterized task queries"
|
||||||
|
exports: ["CalendarDao"]
|
||||||
|
- path: "lib/features/home/domain/calendar_models.dart"
|
||||||
|
provides: "CalendarState and reuse of TaskWithRoom"
|
||||||
|
exports: ["CalendarState"]
|
||||||
|
- path: "lib/features/home/presentation/calendar_providers.dart"
|
||||||
|
provides: "Riverpod provider for calendar state"
|
||||||
|
exports: ["calendarProvider", "selectedDateProvider"]
|
||||||
|
- path: "test/features/home/data/calendar_dao_test.dart"
|
||||||
|
provides: "DAO unit tests"
|
||||||
|
min_lines: 50
|
||||||
|
key_links:
|
||||||
|
- from: "lib/features/home/data/calendar_dao.dart"
|
||||||
|
to: "lib/core/database/database.dart"
|
||||||
|
via: "DAO registered in @DriftDatabase annotation"
|
||||||
|
pattern: "CalendarDao"
|
||||||
|
- from: "lib/features/home/presentation/calendar_providers.dart"
|
||||||
|
to: "lib/features/home/data/calendar_dao.dart"
|
||||||
|
via: "Provider reads CalendarDao from AppDatabase"
|
||||||
|
pattern: "db\\.calendarDao"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the data layer, domain models, Riverpod providers, and localization strings for the calendar strip feature.
|
||||||
|
|
||||||
|
Purpose: The calendar strip UI (Plan 02) needs a data foundation that can answer "what tasks are due on date X?" and "what tasks are overdue relative to today?" without the old overdue/today/tomorrow bucketing. This plan builds that foundation and tests it.
|
||||||
|
|
||||||
|
Output: CalendarDao with date-parameterized queries, CalendarState model, Riverpod providers (selectedDateProvider + calendarProvider), new l10n strings, DAO unit tests.
|
||||||
|
</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/05-calendar-strip/5-CONTEXT.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||||
|
|
||||||
|
From lib/core/database/database.dart:
|
||||||
|
```dart
|
||||||
|
// Tables: Rooms, Tasks, TaskCompletions
|
||||||
|
// Existing DAOs: RoomsDao, TasksDao, DailyPlanDao
|
||||||
|
// CalendarDao must be added to the @DriftDatabase annotation daos list
|
||||||
|
// and imported at the top of database.dart
|
||||||
|
|
||||||
|
@DriftDatabase(
|
||||||
|
tables: [Rooms, Tasks, TaskCompletions],
|
||||||
|
daos: [RoomsDao, TasksDao, DailyPlanDao], // ADD CalendarDao here
|
||||||
|
)
|
||||||
|
class AppDatabase extends _$AppDatabase { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
From lib/features/home/domain/daily_plan_models.dart:
|
||||||
|
```dart
|
||||||
|
class TaskWithRoom {
|
||||||
|
final Task task;
|
||||||
|
final String roomName;
|
||||||
|
final int roomId;
|
||||||
|
const TaskWithRoom({required this.task, required this.roomName, required this.roomId});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From lib/features/home/presentation/daily_plan_providers.dart:
|
||||||
|
```dart
|
||||||
|
// Pattern to follow: StreamProvider.autoDispose, manual (not @riverpod)
|
||||||
|
// because of drift's generated Task type
|
||||||
|
final dailyPlanProvider = StreamProvider.autoDispose<DailyPlanState>((ref) {
|
||||||
|
final db = ref.watch(appDatabaseProvider);
|
||||||
|
...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
From lib/features/home/data/daily_plan_dao.dart:
|
||||||
|
```dart
|
||||||
|
// Pattern: @DriftAccessor with tables, extends DatabaseAccessor<AppDatabase>
|
||||||
|
// Uses query.watch() for reactive streams
|
||||||
|
@DriftAccessor(tables: [Tasks, Rooms, TaskCompletions])
|
||||||
|
class DailyPlanDao extends DatabaseAccessor<AppDatabase> with _$DailyPlanDaoMixin { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
From lib/core/providers/database_provider.dart:
|
||||||
|
```dart
|
||||||
|
// appDatabaseProvider gives access to the database singleton
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Create CalendarDao with date-parameterized queries and tests</name>
|
||||||
|
<files>
|
||||||
|
lib/features/home/data/calendar_dao.dart,
|
||||||
|
lib/core/database/database.dart,
|
||||||
|
test/features/home/data/calendar_dao_test.dart
|
||||||
|
</files>
|
||||||
|
<behavior>
|
||||||
|
- watchTasksForDate(date): returns tasks whose nextDueDate falls on the given calendar day (same year/month/day), joined with room name, sorted by task name alphabetically
|
||||||
|
- watchOverdueTasks(referenceDate): returns tasks whose nextDueDate is strictly before referenceDate (start of day), joined with room name, sorted by nextDueDate ascending
|
||||||
|
- watchTasksForDate for a date with no tasks returns empty list
|
||||||
|
- watchOverdueTasks returns empty when no tasks are overdue
|
||||||
|
- watchOverdueTasks does NOT include tasks due on the referenceDate itself
|
||||||
|
- watchTasksForDate for a past date returns only tasks originally due that day (does NOT include overdue carry-over)
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. Create `lib/features/home/data/calendar_dao.dart`:
|
||||||
|
- Class `CalendarDao` extends `DatabaseAccessor<AppDatabase>` with `_$CalendarDaoMixin`
|
||||||
|
- Annotated `@DriftAccessor(tables: [Tasks, Rooms, TaskCompletions])`
|
||||||
|
- `part 'calendar_dao.g.dart';`
|
||||||
|
- Method `Stream<List<TaskWithRoom>> watchTasksForDate(DateTime date)`:
|
||||||
|
Compute startOfDay and endOfDay (startOfDay + 1 day). Join tasks with rooms. Filter `tasks.nextDueDate >= startOfDay AND tasks.nextDueDate < endOfDay`. Order by `tasks.name` ascending. Map to `TaskWithRoom`.
|
||||||
|
- Method `Stream<List<TaskWithRoom>> watchOverdueTasks(DateTime referenceDate)`:
|
||||||
|
Compute startOfReferenceDay. Join tasks with rooms. Filter `tasks.nextDueDate < startOfReferenceDay`. Order by `tasks.nextDueDate` ascending. Map to `TaskWithRoom`.
|
||||||
|
- Import `daily_plan_models.dart` for `TaskWithRoom` (reuse, don't duplicate).
|
||||||
|
|
||||||
|
2. Register CalendarDao in `lib/core/database/database.dart`:
|
||||||
|
- Add import: `import '../../features/home/data/calendar_dao.dart';`
|
||||||
|
- Add `CalendarDao` to the `daos:` list in `@DriftDatabase`
|
||||||
|
|
||||||
|
3. Run `dart run build_runner build --delete-conflicting-outputs` to generate `calendar_dao.g.dart` and updated `database.g.dart`.
|
||||||
|
|
||||||
|
4. Write `test/features/home/data/calendar_dao_test.dart` following the pattern in `test/features/home/data/daily_plan_dao_test.dart`:
|
||||||
|
- Use in-memory database: `AppDatabase(NativeDatabase.memory())`
|
||||||
|
- Create test rooms in setUp
|
||||||
|
- Test group for watchTasksForDate:
|
||||||
|
- Empty when no tasks
|
||||||
|
- Returns only tasks due on the queried date (not before, not after)
|
||||||
|
- Returns tasks from multiple rooms
|
||||||
|
- Sorted alphabetically by name
|
||||||
|
- Test group for watchOverdueTasks:
|
||||||
|
- Empty when no overdue tasks
|
||||||
|
- Returns tasks due before reference date
|
||||||
|
- Does NOT include tasks due ON the reference date
|
||||||
|
- Sorted by nextDueDate ascending
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/features/home/data/calendar_dao_test.dart</automated>
|
||||||
|
</verify>
|
||||||
|
<done>CalendarDao registered in AppDatabase, both query methods return correct results for arbitrary dates, all DAO tests pass</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create CalendarState model, Riverpod providers, and localization strings</name>
|
||||||
|
<files>
|
||||||
|
lib/features/home/domain/calendar_models.dart,
|
||||||
|
lib/features/home/presentation/calendar_providers.dart,
|
||||||
|
lib/l10n/app_de.arb
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1. Create `lib/features/home/domain/calendar_models.dart`:
|
||||||
|
```dart
|
||||||
|
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
||||||
|
|
||||||
|
/// State for the calendar day view: tasks for the selected date + overdue tasks.
|
||||||
|
class CalendarDayState {
|
||||||
|
final DateTime selectedDate;
|
||||||
|
final List<TaskWithRoom> dayTasks;
|
||||||
|
final List<TaskWithRoom> overdueTasks;
|
||||||
|
|
||||||
|
const CalendarDayState({
|
||||||
|
required this.selectedDate,
|
||||||
|
required this.dayTasks,
|
||||||
|
required this.overdueTasks,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// True when viewing today and all tasks (day + overdue) have been completed
|
||||||
|
/// (lists are empty but completions exist). Determined by the UI layer.
|
||||||
|
bool get isEmpty => dayTasks.isEmpty && overdueTasks.isEmpty;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `lib/features/home/presentation/calendar_providers.dart`:
|
||||||
|
- Import Riverpod, database_provider, calendar_dao, calendar_models, daily_plan_models
|
||||||
|
- `final selectedDateProvider = StateProvider<DateTime>((ref) { final now = DateTime.now(); return DateTime(now.year, now.month, now.day); });`
|
||||||
|
This is NOT autoDispose -- the selected date persists as long as the app is alive (resets on restart naturally).
|
||||||
|
- `final calendarDayProvider = StreamProvider.autoDispose<CalendarDayState>((ref) { ... });`
|
||||||
|
Manual definition (not @riverpod) following dailyPlanProvider pattern.
|
||||||
|
Reads `selectedDateProvider` to get the current date.
|
||||||
|
Reads `appDatabaseProvider` to get the DB.
|
||||||
|
Determines if selectedDate is today: `isToday = selectedDate == DateTime(now.year, now.month, now.day)`.
|
||||||
|
Determines if selectedDate is in the future: `isFuture = selectedDate.isAfter(today)`.
|
||||||
|
Watches `db.calendarDao.watchTasksForDate(selectedDate)`.
|
||||||
|
For overdue: if `isToday`, also watch `db.calendarDao.watchOverdueTasks(selectedDate)`.
|
||||||
|
If viewing a past date or future date, overdueTasks = empty.
|
||||||
|
Per user decision: "When viewing past days: show what was due that day. When viewing future days: show only tasks due that day, no overdue carry-over."
|
||||||
|
Combine both streams using `Rx.combineLatest2` or simply use `asyncMap` on the day tasks stream and fetch overdue as a secondary query.
|
||||||
|
|
||||||
|
Implementation approach: Use the dayTasks stream as the primary, and inside asyncMap call the overdue stream's `.first` when isToday. This keeps it simple and follows the existing `dailyPlanProvider` pattern of `stream.asyncMap()`.
|
||||||
|
|
||||||
|
3. Add new l10n strings to `lib/l10n/app_de.arb` (add before the closing `}`):
|
||||||
|
- `"calendarNoTasks": "Keine Aufgaben"` — shown when a day has no tasks at all
|
||||||
|
- `"calendarAllDone": "Alles erledigt!"` — celebration when all tasks for a day are done
|
||||||
|
- `"calendarOverdueSection": "Uberfaellig"` — No, reuse existing `dailyPlanSectionOverdue` ("Uberfaellig") for the overdue section header
|
||||||
|
- `"calendarTodayButton": "Heute"` — floating today button label
|
||||||
|
|
||||||
|
Actually, we can reuse `dailyPlanSectionOverdue` for the overdue header, `dailyPlanNoTasks` for no-tasks-at-all, and `dailyPlanAllClearTitle`/`dailyPlanAllClearMessage` for celebration. The only truly new string needed is for the Today button:
|
||||||
|
- Add `"calendarTodayButton": "Heute"` to the ARB file
|
||||||
|
|
||||||
|
4. Run `flutter gen-l10n` to regenerate localization files.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter analyze --no-fatal-infos</automated>
|
||||||
|
</verify>
|
||||||
|
<done>CalendarDayState model exists with selectedDate/dayTasks/overdueTasks fields. selectedDateProvider and calendarDayProvider are defined. calendarDayProvider returns overdue tasks only when viewing today. New l10n string "calendarTodayButton" exists. No analysis errors.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `flutter test test/features/home/data/calendar_dao_test.dart` — all DAO tests pass
|
||||||
|
- `flutter analyze --no-fatal-infos` — no errors in new or modified files
|
||||||
|
- `flutter test` — full test suite still passes (existing tests not broken by database.dart changes)
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- CalendarDao is registered in AppDatabase and has two working query methods
|
||||||
|
- CalendarDayState model correctly separates day tasks from overdue tasks
|
||||||
|
- calendarDayProvider returns overdue only for today, not for past/future dates
|
||||||
|
- All existing tests still pass after database.dart modification
|
||||||
|
- New DAO tests cover core query behaviors
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/05-calendar-strip/05-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
142
.planning/phases/05-calendar-strip/05-01-SUMMARY.md
Normal file
142
.planning/phases/05-calendar-strip/05-01-SUMMARY.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
---
|
||||||
|
phase: 05-calendar-strip
|
||||||
|
plan: 01
|
||||||
|
subsystem: database
|
||||||
|
tags: [drift, riverpod, dart, flutter, localization, tdd]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires: []
|
||||||
|
provides:
|
||||||
|
- CalendarDao with watchTasksForDate and watchOverdueTasks date-parameterized queries
|
||||||
|
- CalendarDayState domain model with selectedDate/dayTasks/overdueTasks
|
||||||
|
- selectedDateProvider (NotifierProvider, persists while app is alive)
|
||||||
|
- calendarDayProvider (StreamProvider.autoDispose, overdue only for today)
|
||||||
|
- calendarTodayButton l10n string in ARB and generated dart files
|
||||||
|
- 11 DAO unit tests covering all query behaviors
|
||||||
|
affects:
|
||||||
|
- 05-calendar-strip plan 02 (calendar strip UI uses these providers and state model)
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "CalendarDao follows @DriftAccessor pattern with DatabaseAccessor<AppDatabase>"
|
||||||
|
- "Manual NotifierProvider<SelectedDateNotifier, DateTime> instead of @riverpod (Riverpod 3.x pattern)"
|
||||||
|
- "StreamProvider.autoDispose with asyncMap for combining day + overdue streams"
|
||||||
|
- "TDD: failing test commit, then implementation commit"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- lib/features/home/data/calendar_dao.dart
|
||||||
|
- lib/features/home/data/calendar_dao.g.dart
|
||||||
|
- lib/features/home/domain/calendar_models.dart
|
||||||
|
- lib/features/home/presentation/calendar_providers.dart
|
||||||
|
- test/features/home/data/calendar_dao_test.dart
|
||||||
|
modified:
|
||||||
|
- lib/core/database/database.dart
|
||||||
|
- lib/core/database/database.g.dart
|
||||||
|
- lib/l10n/app_de.arb
|
||||||
|
- lib/l10n/app_localizations.dart
|
||||||
|
- lib/l10n/app_localizations_de.dart
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Used NotifierProvider<SelectedDateNotifier, DateTime> instead of deprecated StateProvider — Riverpod 3.x removed StateProvider in favour of Notifier-based providers"
|
||||||
|
- "calendarDayProvider fetches overdue tasks with .first when isToday, keeping asyncMap pattern consistent with dailyPlanProvider"
|
||||||
|
- "watchTasksForDate sorts alphabetically by name (not by due time) — arbitrary due time on same day has no meaningful sort order"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "CalendarDao: @DriftAccessor with join + where filter + orderBy, mapped to TaskWithRoom — same shape as DailyPlanDao"
|
||||||
|
- "Manual Notifier subclass for simple value-holding state provider (not @riverpod) to avoid code gen constraints"
|
||||||
|
|
||||||
|
requirements-completed: [CAL-02, CAL-05]
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 5min
|
||||||
|
completed: 2026-03-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 5 Plan 01: Calendar Data Layer Summary
|
||||||
|
|
||||||
|
**CalendarDao with date-exact and overdue-before-date Drift queries, CalendarDayState model, Riverpod providers for selected date and day state, and "Heute" l10n string — full data foundation for the calendar strip UI**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 5 min
|
||||||
|
- **Started:** 2026-03-16T20:18:55Z
|
||||||
|
- **Completed:** 2026-03-16T20:24:12Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 10
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- CalendarDao registered in AppDatabase with two reactive Drift streams: `watchTasksForDate` (exact day, sorted by name) and `watchOverdueTasks` (strictly before reference date, sorted by due date)
|
||||||
|
- CalendarDayState domain model separating dayTasks and overdueTasks with isEmpty helper
|
||||||
|
- selectedDateProvider (NotifierProvider, keeps alive) + calendarDayProvider (StreamProvider.autoDispose) following existing Riverpod patterns
|
||||||
|
- 11 unit tests passing via TDD red-green cycle; full 100-test suite passes with no regressions
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: RED - CalendarDao tests** - `f5c4b49` (test)
|
||||||
|
2. **Task 1: GREEN - CalendarDao implementation** - `c666f9a` (feat)
|
||||||
|
3. **Task 2: CalendarDayState, providers, l10n** - `68ba7c6` (feat)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `lib/features/home/data/calendar_dao.dart` - CalendarDao with watchTasksForDate and watchOverdueTasks
|
||||||
|
- `lib/features/home/data/calendar_dao.g.dart` - Generated Drift mixin for CalendarDao
|
||||||
|
- `lib/features/home/domain/calendar_models.dart` - CalendarDayState model
|
||||||
|
- `lib/features/home/presentation/calendar_providers.dart` - selectedDateProvider and calendarDayProvider
|
||||||
|
- `test/features/home/data/calendar_dao_test.dart` - 11 DAO unit tests (TDD RED phase)
|
||||||
|
- `lib/core/database/database.dart` - Added CalendarDao import and registration in @DriftDatabase
|
||||||
|
- `lib/core/database/database.g.dart` - Regenerated with CalendarDao accessor
|
||||||
|
- `lib/l10n/app_de.arb` - Added calendarTodayButton: "Heute"
|
||||||
|
- `lib/l10n/app_localizations.dart` - Regenerated with calendarTodayButton getter
|
||||||
|
- `lib/l10n/app_localizations_de.dart` - Regenerated with calendarTodayButton implementation
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- **NotifierProvider instead of StateProvider:** Riverpod 3.x dropped `StateProvider` — replaced with `NotifierProvider<SelectedDateNotifier, DateTime>` pattern (manual, not @riverpod) to keep consistent with the codebase's non-generated providers.
|
||||||
|
- **Overdue fetched with .first inside asyncMap:** When isToday, the overdue tasks stream's first emission is awaited inside asyncMap on the day tasks stream. This avoids combining two streams and stays consistent with the `dailyPlanProvider` pattern.
|
||||||
|
- **watchTasksForDate sorts alphabetically by name:** Tasks due on the same calendar day have no meaningful relative order by time. Alphabetical name sort gives deterministic, user-friendly ordering.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 1 - Bug] StateProvider unavailable in Riverpod 3.x**
|
||||||
|
- **Found during:** Task 2 (calendar providers)
|
||||||
|
- **Issue:** Plan specified `StateProvider<DateTime>` but flutter_riverpod 3.3.1 removed StateProvider; analyzer reported `undefined_function`
|
||||||
|
- **Fix:** Replaced with `NotifierProvider<SelectedDateNotifier, DateTime>` using a minimal `Notifier` subclass with a `selectDate(DateTime)` method
|
||||||
|
- **Files modified:** lib/features/home/presentation/calendar_providers.dart
|
||||||
|
- **Verification:** `flutter analyze --no-fatal-infos` reports no issues
|
||||||
|
- **Committed in:** 68ba7c6 (Task 2 commit)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 1 auto-fixed (Rule 1 - Bug)
|
||||||
|
**Impact on plan:** Fix was required for compilation. The API surface is equivalent — consumers call `ref.watch(selectedDateProvider)` to read the date and `ref.read(selectedDateProvider.notifier).selectDate(date)` to update it. No scope creep.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
- None beyond the StateProvider API change documented above.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
None - no external service configuration required.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- CalendarDao, CalendarDayState, selectedDateProvider, and calendarDayProvider are all ready for consumption by Plan 02 (calendar strip UI)
|
||||||
|
- The `selectDate` method on SelectedDateNotifier is the correct way to update the selected date from the UI
|
||||||
|
- Existing dailyPlanProvider is unchanged — Plan 02 will decide whether to replace or retain it in the HomeScreen
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 05-calendar-strip*
|
||||||
|
*Completed: 2026-03-16*
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- FOUND: lib/features/home/data/calendar_dao.dart
|
||||||
|
- FOUND: lib/features/home/domain/calendar_models.dart
|
||||||
|
- FOUND: lib/features/home/presentation/calendar_providers.dart
|
||||||
|
- FOUND: test/features/home/data/calendar_dao_test.dart
|
||||||
|
- FOUND: .planning/phases/05-calendar-strip/05-01-SUMMARY.md
|
||||||
|
- FOUND: commit f5c4b49 (test RED phase)
|
||||||
|
- FOUND: commit c666f9a (feat GREEN phase)
|
||||||
|
- FOUND: commit 68ba7c6 (feat Task 2)
|
||||||
316
.planning/phases/05-calendar-strip/05-02-PLAN.md
Normal file
316
.planning/phases/05-calendar-strip/05-02-PLAN.md
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
---
|
||||||
|
phase: 05-calendar-strip
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: ["05-01"]
|
||||||
|
files_modified:
|
||||||
|
- lib/features/home/presentation/home_screen.dart
|
||||||
|
- lib/features/home/presentation/calendar_strip.dart
|
||||||
|
- lib/features/home/presentation/calendar_task_row.dart
|
||||||
|
- lib/features/home/presentation/calendar_day_list.dart
|
||||||
|
autonomous: false
|
||||||
|
requirements:
|
||||||
|
- CAL-01
|
||||||
|
- CAL-03
|
||||||
|
- CAL-04
|
||||||
|
- CAL-05
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Home screen shows a horizontal scrollable strip of day cards with German abbreviation (Mo, Di, Mi...) and date number"
|
||||||
|
- "Tapping a day card updates the task list below to show that day's tasks"
|
||||||
|
- "On app launch the strip auto-scrolls so today's card is centered"
|
||||||
|
- "A subtle wider gap and month label appears at month boundaries"
|
||||||
|
- "Overdue tasks appear in a separate coral-accented section when viewing today"
|
||||||
|
- "Overdue tasks do NOT appear when viewing past or future days"
|
||||||
|
- "Completing a task via checkbox triggers slide-out animation"
|
||||||
|
- "Floating Today button appears when scrolled away from today, hidden when today is visible"
|
||||||
|
- "First-run empty state (no rooms/tasks) still shows the create-room prompt"
|
||||||
|
- "Celebration state shows when all tasks for the selected day are done"
|
||||||
|
artifacts:
|
||||||
|
- path: "lib/features/home/presentation/calendar_strip.dart"
|
||||||
|
provides: "Horizontal scrollable date strip widget"
|
||||||
|
min_lines: 100
|
||||||
|
- path: "lib/features/home/presentation/calendar_day_list.dart"
|
||||||
|
provides: "Day task list with overdue section, empty, and celebration states"
|
||||||
|
min_lines: 80
|
||||||
|
- path: "lib/features/home/presentation/calendar_task_row.dart"
|
||||||
|
provides: "Task row adapted for calendar (no relative date, has room tag + checkbox)"
|
||||||
|
min_lines: 30
|
||||||
|
- path: "lib/features/home/presentation/home_screen.dart"
|
||||||
|
provides: "Rewritten HomeScreen composing strip + day list"
|
||||||
|
min_lines: 40
|
||||||
|
key_links:
|
||||||
|
- from: "lib/features/home/presentation/home_screen.dart"
|
||||||
|
to: "lib/features/home/presentation/calendar_strip.dart"
|
||||||
|
via: "HomeScreen composes CalendarStrip widget"
|
||||||
|
pattern: "CalendarStrip"
|
||||||
|
- from: "lib/features/home/presentation/home_screen.dart"
|
||||||
|
to: "lib/features/home/presentation/calendar_day_list.dart"
|
||||||
|
via: "HomeScreen composes CalendarDayList widget"
|
||||||
|
pattern: "CalendarDayList"
|
||||||
|
- from: "lib/features/home/presentation/calendar_strip.dart"
|
||||||
|
to: "lib/features/home/presentation/calendar_providers.dart"
|
||||||
|
via: "Strip reads and writes selectedDateProvider"
|
||||||
|
pattern: "selectedDateProvider"
|
||||||
|
- from: "lib/features/home/presentation/calendar_day_list.dart"
|
||||||
|
to: "lib/features/home/presentation/calendar_providers.dart"
|
||||||
|
via: "Day list watches calendarDayProvider for reactive task data"
|
||||||
|
pattern: "calendarDayProvider"
|
||||||
|
- from: "lib/features/home/presentation/calendar_day_list.dart"
|
||||||
|
to: "lib/features/tasks/presentation/task_providers.dart"
|
||||||
|
via: "Task completion uses taskActionsProvider.completeTask()"
|
||||||
|
pattern: "taskActionsProvider"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Build the complete calendar strip UI and replace the old HomeScreen with it.
|
||||||
|
|
||||||
|
Purpose: This is the user-facing deliverable of Phase 5 -- the horizontal date strip with day-task list that replaces the stacked overdue/today/tomorrow daily plan.
|
||||||
|
|
||||||
|
Output: CalendarStrip widget, CalendarDayList widget, CalendarTaskRow widget, rewritten HomeScreen that composes them.
|
||||||
|
</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/05-calendar-strip/5-CONTEXT.md
|
||||||
|
@.planning/phases/05-calendar-strip/05-01-SUMMARY.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- From Plan 01 outputs (CalendarDao, models, providers) -->
|
||||||
|
|
||||||
|
From lib/features/home/domain/calendar_models.dart:
|
||||||
|
```dart
|
||||||
|
class CalendarDayState {
|
||||||
|
final DateTime selectedDate;
|
||||||
|
final List<TaskWithRoom> dayTasks;
|
||||||
|
final List<TaskWithRoom> overdueTasks;
|
||||||
|
const CalendarDayState({required this.selectedDate, required this.dayTasks, required this.overdueTasks});
|
||||||
|
bool get isEmpty => dayTasks.isEmpty && overdueTasks.isEmpty;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From lib/features/home/presentation/calendar_providers.dart:
|
||||||
|
```dart
|
||||||
|
final selectedDateProvider = StateProvider<DateTime>(...); // read/write selected date
|
||||||
|
final calendarDayProvider = StreamProvider.autoDispose<CalendarDayState>(...); // reactive day data
|
||||||
|
```
|
||||||
|
|
||||||
|
From lib/features/home/domain/daily_plan_models.dart:
|
||||||
|
```dart
|
||||||
|
class TaskWithRoom {
|
||||||
|
final Task task;
|
||||||
|
final String roomName;
|
||||||
|
final int roomId;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From lib/features/tasks/presentation/task_providers.dart:
|
||||||
|
```dart
|
||||||
|
// Use to complete tasks:
|
||||||
|
ref.read(taskActionsProvider.notifier).completeTask(taskId);
|
||||||
|
```
|
||||||
|
|
||||||
|
From lib/core/theme/app_theme.dart:
|
||||||
|
```dart
|
||||||
|
// Seed color: Color(0xFF7A9A6D) -- sage green
|
||||||
|
// The "light sage/green tint" for day cards should derive from the theme's primary/seed
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing reusable constants:
|
||||||
|
```dart
|
||||||
|
const _overdueColor = Color(0xFFE07A5F); // warm coral for overdue
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing l10n strings to reuse:
|
||||||
|
```dart
|
||||||
|
l10n.dailyPlanSectionOverdue // "Uberfaellig"
|
||||||
|
l10n.dailyPlanNoTasks // "Noch keine Aufgaben angelegt"
|
||||||
|
l10n.dailyPlanAllClearTitle // "Alles erledigt!"
|
||||||
|
l10n.dailyPlanAllClearMessage // "Keine Aufgaben fuer heute..."
|
||||||
|
l10n.homeEmptyMessage // "Lege zuerst einen Raum an..."
|
||||||
|
l10n.homeEmptyAction // "Raum erstellen"
|
||||||
|
l10n.calendarTodayButton // "Heute" (added in Plan 01)
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Build CalendarStrip, CalendarTaskRow, CalendarDayList widgets</name>
|
||||||
|
<files>
|
||||||
|
lib/features/home/presentation/calendar_strip.dart,
|
||||||
|
lib/features/home/presentation/calendar_task_row.dart,
|
||||||
|
lib/features/home/presentation/calendar_day_list.dart
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
**CalendarStrip** (`lib/features/home/presentation/calendar_strip.dart`):
|
||||||
|
|
||||||
|
A ConsumerStatefulWidget that renders a horizontal scrollable row of day cards.
|
||||||
|
|
||||||
|
Scroll range: 90 days in the past and 90 days in the future (181 total items). This gives enough past for review and future for planning without performance concerns.
|
||||||
|
|
||||||
|
Layout:
|
||||||
|
- Uses a `ScrollController` with `initialScrollOffset` calculated to center today's card on first build.
|
||||||
|
- Each day card is a fixed-width container (~56px wide, ~72px tall). Cards show:
|
||||||
|
- Top: German day abbreviation using `DateFormat('E', 'de').format(date)` which gives "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So". Import `package:intl/intl.dart`.
|
||||||
|
- Bottom: Date number (day of month) as text.
|
||||||
|
- Card styling per user decisions:
|
||||||
|
- All cards: light sage/green tint background. Use `theme.colorScheme.primaryContainer.withValues(alpha: 0.3)` or similar to get a subtle green wash.
|
||||||
|
- Selected card: stronger green (`theme.colorScheme.primaryContainer`) and border with `theme.colorScheme.primary`. The strip scrolls to center the selected card using `animateTo()`.
|
||||||
|
- Today's card (when not selected): bold text + a small accent underline bar below the date number (2px, primary color).
|
||||||
|
- Today + selected: both treatments combined.
|
||||||
|
- Spacing: cards have 4px horizontal margin by default. At month boundaries (where card N is the last day of a month and card N+1 is the first of the next month), the gap is 16px, and a small Text widget showing the new month abbreviation (e.g., "Apr") in `theme.textTheme.labelSmall` is inserted between them.
|
||||||
|
- On tap: update `ref.read(selectedDateProvider.notifier).state = tappedDate` and animate the scroll to center the tapped card.
|
||||||
|
- Auto-scroll on init: In `initState`, after the first frame (using `WidgetsBinding.instance.addPostFrameCallback`), animate to center today's card with a 200ms duration using `Curves.easeOut`.
|
||||||
|
|
||||||
|
Controller pattern for scroll-to-today:
|
||||||
|
```dart
|
||||||
|
class CalendarStripController {
|
||||||
|
VoidCallback? _scrollToToday;
|
||||||
|
void scrollToToday() => _scrollToToday?.call();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
CalendarStrip takes `CalendarStripController controller` parameter and sets `controller._scrollToToday` in initState. Parent calls `controller.scrollToToday()` from the Today button.
|
||||||
|
|
||||||
|
Today visibility callback: Expose `onTodayVisibilityChanged(bool isVisible)`. Determine visibility by checking if today's card offset is within the viewport bounds during scroll events.
|
||||||
|
|
||||||
|
**CalendarTaskRow** (`lib/features/home/presentation/calendar_task_row.dart`):
|
||||||
|
|
||||||
|
Adapted from `DailyPlanTaskRow` but simplified per user decisions:
|
||||||
|
- Shows: task name, tappable room tag (navigates to room via `context.go('/rooms/$roomId')`), checkbox
|
||||||
|
- Does NOT show relative date (strip already communicates which day)
|
||||||
|
- Same room tag styling as DailyPlanTaskRow (secondaryContainer chip with borderRadius 4)
|
||||||
|
- Checkbox visible, onChanged triggers `onCompleted` callback
|
||||||
|
- Overdue variant: if `isOverdue` flag is true, task name text color uses `_overdueColor` for visual distinction
|
||||||
|
|
||||||
|
**CalendarDayList** (`lib/features/home/presentation/calendar_day_list.dart`):
|
||||||
|
|
||||||
|
A ConsumerStatefulWidget that shows the task list for the selected day.
|
||||||
|
|
||||||
|
Watches `calendarDayProvider`. Manages `Set<int> _completingTaskIds` for animation state.
|
||||||
|
|
||||||
|
Handles these states:
|
||||||
|
|
||||||
|
a) **Loading**: `CircularProgressIndicator` centered.
|
||||||
|
|
||||||
|
b) **Error**: Error text centered.
|
||||||
|
|
||||||
|
c) **First-run empty** (no rooms/tasks at all): Same pattern as current `_buildNoTasksState` -- checklist icon, "Noch keine Aufgaben angelegt" message, "Lege zuerst einen Raum an" subtitle, "Raum erstellen" FilledButton.tonal navigating to `/rooms`. Detect by checking if `state.isEmpty && state.totalTaskCount == 0` (requires adding `totalTaskCount` field to CalendarDayState and computing it in the provider -- see NOTE below).
|
||||||
|
|
||||||
|
d) **Empty day** (tasks exist elsewhere but not this day, and not today): show centered subtle icon (Icons.event_available) + "Keine Aufgaben" text.
|
||||||
|
|
||||||
|
e) **Celebration** (today is selected, tasks exist elsewhere, but today's tasks are all done): show celebration icon + "Alles erledigt!" title + "Keine Aufgaben fuer heute. Geniesse den Moment!" message. Compact layout (no ProgressCard).
|
||||||
|
|
||||||
|
f) **Has tasks**: Render a ListView with:
|
||||||
|
- If overdue tasks exist (only present when viewing today): Section header "Uberfaellig" in coral color (`_overdueColor`), followed by overdue CalendarTaskRow items with `isOverdue: true` and interactive checkboxes.
|
||||||
|
- Day tasks: CalendarTaskRow items with interactive checkboxes.
|
||||||
|
- Task completion: on checkbox tap, add taskId to `_completingTaskIds`, call `ref.read(taskActionsProvider.notifier).completeTask(taskId)`. Render completing tasks with the `_CompletingTaskRow` animation (SizeTransition + SlideTransition, 300ms, Curves.easeInOut) -- recreate this private widget in calendar_day_list.dart.
|
||||||
|
|
||||||
|
NOTE for executor: Plan 01 creates CalendarDayState with selectedDate, dayTasks, overdueTasks. This task needs a `totalTaskCount` int field on CalendarDayState to distinguish first-run from celebration. When implementing, add `final int totalTaskCount` to CalendarDayState in calendar_models.dart and compute it in the calendarDayProvider via a simple `SELECT COUNT(*) FROM tasks` query (one line in CalendarDao: `Future<int> getTaskCount() async { final r = await (selectOnly(tasks)..addColumns([tasks.id.count()])).getSingle(); return r.read(tasks.id.count()) ?? 0; }`).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter analyze --no-fatal-infos</automated>
|
||||||
|
</verify>
|
||||||
|
<done>CalendarStrip renders 181 day cards with German abbreviations, highlights selected/today cards, shows month boundary labels. CalendarTaskRow shows name + room tag + checkbox without relative date. CalendarDayList shows overdue section (today only), day tasks, empty states, and celebration state. All compile without analysis errors.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Replace HomeScreen with calendar composition and floating Today button</name>
|
||||||
|
<files>
|
||||||
|
lib/features/home/presentation/home_screen.dart
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Rewrite `lib/features/home/presentation/home_screen.dart` entirely. The old content (DailyPlanState, overdue/today/tomorrow sections, ProgressCard) is fully replaced.
|
||||||
|
|
||||||
|
New HomeScreen is a `ConsumerStatefulWidget`:
|
||||||
|
|
||||||
|
State fields:
|
||||||
|
- `late final CalendarStripController _stripController = CalendarStripController();`
|
||||||
|
- `bool _showTodayButton = false;`
|
||||||
|
|
||||||
|
Build method returns a Stack with:
|
||||||
|
1. A Column containing:
|
||||||
|
- `CalendarStrip(controller: _stripController, onTodayVisibilityChanged: (visible) { setState(() => _showTodayButton = !visible); })`
|
||||||
|
- `Expanded(child: CalendarDayList())`
|
||||||
|
2. Conditionally, a Positioned floating "Heute" button at bottom-center:
|
||||||
|
- `FloatingActionButton.extended` with `Icons.today` icon and `l10n.calendarTodayButton` label
|
||||||
|
- onPressed: set `selectedDateProvider` to today's date-only DateTime, call `_stripController.scrollToToday()`
|
||||||
|
|
||||||
|
Imports needed:
|
||||||
|
- `flutter/material.dart`
|
||||||
|
- `flutter_riverpod/flutter_riverpod.dart`
|
||||||
|
- `calendar_strip.dart`
|
||||||
|
- `calendar_day_list.dart`
|
||||||
|
- `calendar_providers.dart` (for selectedDateProvider)
|
||||||
|
- `app_localizations.dart`
|
||||||
|
|
||||||
|
Do NOT delete old files (`daily_plan_providers.dart`, `daily_plan_task_row.dart`, `progress_card.dart`, `daily_plan_dao.dart`). DailyPlanDao is still used by the notification service. Old presentation files become dead code -- safe to clean up in a future phase.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter analyze --no-fatal-infos && flutter test</automated>
|
||||||
|
</verify>
|
||||||
|
<done>HomeScreen renders CalendarStrip at top and CalendarDayList below. Floating Today button appears when scrolled away from today. Old overdue/today/tomorrow sections are gone. Full test suite passes. No analysis errors.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<name>Task 3: Verify calendar strip home screen visually and functionally</name>
|
||||||
|
<files>lib/features/home/presentation/home_screen.dart</files>
|
||||||
|
<action>
|
||||||
|
Human verifies the complete calendar strip experience on a running device/emulator.
|
||||||
|
|
||||||
|
Launch the app with `flutter run` (or hot-restart). Walk through all key behaviors:
|
||||||
|
1. Strip appearance: day cards with German abbreviations and date numbers
|
||||||
|
2. Today highlighting: centered, stronger green, bold + underline
|
||||||
|
3. Day selection: tap a card, task list updates
|
||||||
|
4. Month boundaries: wider gap with month label
|
||||||
|
5. Today button: appears when scrolled away, snaps back on tap
|
||||||
|
6. Overdue section: coral header on today only
|
||||||
|
7. Task completion: checkbox triggers slide-out animation
|
||||||
|
8. Empty/celebration states
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter analyze --no-fatal-infos</automated>
|
||||||
|
</verify>
|
||||||
|
<done>User has confirmed the calendar strip looks correct, day selection works, overdue behavior is right, and all states render properly.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `flutter analyze --no-fatal-infos` -- zero errors
|
||||||
|
- `flutter test` -- full test suite passes (existing + new DAO tests)
|
||||||
|
- Visual: calendar strip is horizontally scrollable with day cards
|
||||||
|
- Visual: selected day highlighted, today has bold + underline treatment
|
||||||
|
- Visual: month boundaries have wider gaps and month name labels
|
||||||
|
- Functional: tapping a day card updates the task list below
|
||||||
|
- Functional: overdue tasks appear only when viewing today
|
||||||
|
- Functional: floating Today button appears/disappears correctly
|
||||||
|
- Functional: task completion animation works
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Home screen replaced: no more stacked overdue/today/tomorrow sections
|
||||||
|
- Horizontal date strip scrolls smoothly with 181 day range
|
||||||
|
- Day cards show German abbreviations and date numbers
|
||||||
|
- Tapping a card selects it and shows that day's tasks
|
||||||
|
- Today auto-centers on launch with smooth animation
|
||||||
|
- Month boundaries visually distinct with labels
|
||||||
|
- Overdue carry-over only on today's view with coral accent
|
||||||
|
- Floating Today button for quick navigation
|
||||||
|
- Empty and celebration states work correctly
|
||||||
|
- All existing tests pass, no regressions
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/05-calendar-strip/05-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
148
.planning/phases/05-calendar-strip/05-02-SUMMARY.md
Normal file
148
.planning/phases/05-calendar-strip/05-02-SUMMARY.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
---
|
||||||
|
phase: 05-calendar-strip
|
||||||
|
plan: 02
|
||||||
|
subsystem: ui
|
||||||
|
tags: [flutter, riverpod, dart, intl, animation, calendar]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 05-calendar-strip plan 01
|
||||||
|
provides: CalendarDao, CalendarDayState, selectedDateProvider, calendarDayProvider
|
||||||
|
provides:
|
||||||
|
- CalendarStrip widget (181-day horizontal scroll, German abbreviations, month boundary labels)
|
||||||
|
- CalendarTaskRow widget (task name + room tag chip + checkbox, no relative date)
|
||||||
|
- CalendarDayList widget (loading/empty/celebration/tasks states, overdue section today-only)
|
||||||
|
- Rewritten HomeScreen composing strip + day list with floating Today button
|
||||||
|
- totalTaskCount field on CalendarDayState and getTaskCount() on CalendarDao
|
||||||
|
- Updated home screen and app shell tests for new calendar providers
|
||||||
|
affects:
|
||||||
|
- 06-task-history (uses CalendarStrip as the navigation surface)
|
||||||
|
- 07-task-sorting (task display within CalendarDayList)
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "CalendarStrip uses CalendarStripController (simple VoidCallback holder) for parent-to-child imperative scrolling"
|
||||||
|
- "CalendarDayList manages _completingTaskIds Set<int> for slide-out animation the same way as old HomeScreen"
|
||||||
|
- "Tests use tester.pump() + pump(Duration) instead of pumpAndSettle() to avoid timeout from animation controllers"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- lib/features/home/presentation/calendar_strip.dart
|
||||||
|
- lib/features/home/presentation/calendar_task_row.dart
|
||||||
|
- lib/features/home/presentation/calendar_day_list.dart
|
||||||
|
modified:
|
||||||
|
- lib/features/home/presentation/home_screen.dart
|
||||||
|
- lib/features/home/domain/calendar_models.dart
|
||||||
|
- lib/features/home/data/calendar_dao.dart
|
||||||
|
- lib/features/home/presentation/calendar_providers.dart
|
||||||
|
- test/features/home/presentation/home_screen_test.dart
|
||||||
|
- test/shell/app_shell_test.dart
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "CalendarStripController holds a VoidCallback instead of using GlobalKey — simpler for this one-direction imperative call"
|
||||||
|
- "totalTaskCount fetched via getTaskCount() inside calendarDayProvider asyncMap — avoids a third stream, consistent with existing pattern"
|
||||||
|
- "Tests use pump() + pump(Duration) instead of pumpAndSettle() — CalendarStrip's ScrollController postFrameCallback and animation controllers cause pumpAndSettle to timeout"
|
||||||
|
- "month label height always reserved with SizedBox(height:16) on non-boundary cards — prevents strip height jitter as you scroll through months"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "ImperativeController pattern: class with VoidCallback? _action; void action() => _action?.call(); widget sets _action in initState"
|
||||||
|
- "CalendarDayList state machine: first-run (totalTaskCount==0) > celebration (isToday + isEmpty + totalTaskCount>0) > emptyDay (isEmpty) > hasTasks"
|
||||||
|
|
||||||
|
requirements-completed: [CAL-01, CAL-03, CAL-04, CAL-05]
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 8min
|
||||||
|
completed: 2026-03-16
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 5 Plan 02: Calendar Strip UI Summary
|
||||||
|
|
||||||
|
**Horizontal 181-day calendar strip with German day cards, month boundaries, floating Today button, and day task list with overdue section — replaces the stacked daily-plan HomeScreen**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 8 min
|
||||||
|
- **Started:** 2026-03-16T20:27:39Z
|
||||||
|
- **Completed:** 2026-03-16T20:35:55Z
|
||||||
|
- **Tasks:** 3 (Task 3 auto-approved in auto-advance mode)
|
||||||
|
- **Files modified:** 9
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- CalendarStrip: horizontal ListView with 181 day cards (90 past + today + 90 future), German abbreviations via `DateFormat('E', 'de')`, selected card highlighted (stronger primaryContainer + border), today card with bold text + 2px accent underline, month boundary wider gap + month label, auto-scrolls to center today on init, CalendarStripController enables Today-button → strip communication
|
||||||
|
- CalendarDayList: five-state machine (loading, first-run empty, celebration, empty day, has tasks) with overdue section when viewing today, slide-out completion animation reusing the same SizeTransition + SlideTransition pattern from the old HomeScreen
|
||||||
|
- CalendarTaskRow: simplified from DailyPlanTaskRow — no relative date, name + room chip + checkbox, coral text when isOverdue
|
||||||
|
- HomeScreen rewritten: Stack with Column(CalendarStrip + Expanded(CalendarDayList)) and conditionally-visible FloatingActionButton.extended for "Heute" navigation
|
||||||
|
- Added totalTaskCount to CalendarDayState and getTaskCount() SELECT COUNT to CalendarDao for first-run vs. celebration disambiguation
|
||||||
|
- Updated 2 test files (home_screen_test.dart, app_shell_test.dart) to test new providers; test count grew from 100 to 101
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Build CalendarStrip, CalendarTaskRow, CalendarDayList widgets** - `f718ee8` (feat)
|
||||||
|
2. **Task 2: Replace HomeScreen with calendar composition** - `88ef248` (feat)
|
||||||
|
3. **Task 3: Verify calendar strip visually** - auto-approved (checkpoint:human-verify in auto-advance mode)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `lib/features/home/presentation/calendar_strip.dart` - 181-day horizontal scrollable strip with German abbreviations, today/selected highlights, month boundary labels
|
||||||
|
- `lib/features/home/presentation/calendar_task_row.dart` - Task row: name + room chip + checkbox, isOverdue coral styling, no relative date
|
||||||
|
- `lib/features/home/presentation/calendar_day_list.dart` - Day task list with 5-state machine, overdue section (today only), slide-out animation
|
||||||
|
- `lib/features/home/presentation/home_screen.dart` - Rewritten: CalendarStrip + CalendarDayList + floating Today FAB
|
||||||
|
- `lib/features/home/domain/calendar_models.dart` - Added totalTaskCount field
|
||||||
|
- `lib/features/home/data/calendar_dao.dart` - Added getTaskCount() query
|
||||||
|
- `lib/features/home/presentation/calendar_providers.dart` - calendarDayProvider now fetches and includes totalTaskCount
|
||||||
|
- `test/features/home/presentation/home_screen_test.dart` - Rewritten for CalendarDayState / calendarDayProvider
|
||||||
|
- `test/shell/app_shell_test.dart` - Updated from dailyPlanProvider to calendarDayProvider
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- **CalendarStripController as simple VoidCallback holder:** Avoids GlobalKey complexity for a single imperative scroll-to-today action; parent holds controller, widget registers its implementation in initState.
|
||||||
|
- **totalTaskCount fetched in asyncMap:** Consistent with existing calendarDayProvider asyncMap pattern; avoids a third reactive stream just for a count.
|
||||||
|
- **Tests use pump() + pump(Duration) instead of pumpAndSettle():** ScrollController's postFrameCallback animation and _completingTaskIds AnimationController keep the tester busy indefinitely; fixed-duration pump steps are reliable.
|
||||||
|
- **Month label height always reserved:** Non-boundary cards get `SizedBox(height: 16)` to match the label row height — prevents strip height from changing as you scroll across month edges.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 1 - Bug] Updated existing tests broken by the HomeScreen rewrite**
|
||||||
|
- **Found during:** Task 2 verification (flutter test)
|
||||||
|
- **Issue:** `home_screen_test.dart` and `app_shell_test.dart` both imported `dailyPlanProvider` and `DailyPlanState` and used `pumpAndSettle()`, which now times out because CalendarStrip animation controllers never settle
|
||||||
|
- **Fix:** Rewrote both test files to use `calendarDayProvider`/`CalendarDayState` and replaced `pumpAndSettle()` with `pump() + pump(Duration(milliseconds: 500))`; updated all assertions to match new UI (removed progress card / tomorrow section assertions, added strip-visible assertion)
|
||||||
|
- **Files modified:** test/features/home/presentation/home_screen_test.dart, test/shell/app_shell_test.dart
|
||||||
|
- **Verification:** `flutter test` — 101 tests all pass; `flutter analyze --no-fatal-infos` — zero issues
|
||||||
|
- **Committed in:** f718ee8 (Task 1 commit, as tests were fixed alongside widget creation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 1 auto-fixed (Rule 1 - Bug)
|
||||||
|
**Impact on plan:** Required to maintain working test suite. The new tests cover the same behaviors (empty state, overdue section, celebration, checkboxes) but against the calendar API. No scope creep.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
- None beyond the test migration documented above.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
None - no external service configuration required.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
- HomeScreen is fully replaced; CalendarStrip and CalendarDayList are composable widgets ready for Phase 6/7 integration
|
||||||
|
- The old daily_plan_providers.dart, daily_plan_task_row.dart, and progress_card.dart are now dead code; safe to clean up in a future phase
|
||||||
|
- DailyPlanDao is still used by the notification service and must NOT be deleted
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 05-calendar-strip*
|
||||||
|
*Completed: 2026-03-16*
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- FOUND: lib/features/home/presentation/calendar_strip.dart
|
||||||
|
- FOUND: lib/features/home/presentation/calendar_task_row.dart
|
||||||
|
- FOUND: lib/features/home/presentation/calendar_day_list.dart
|
||||||
|
- FOUND: lib/features/home/presentation/home_screen.dart (rewritten)
|
||||||
|
- FOUND: lib/features/home/domain/calendar_models.dart (updated)
|
||||||
|
- FOUND: lib/features/home/data/calendar_dao.dart (updated)
|
||||||
|
- FOUND: lib/features/home/presentation/calendar_providers.dart (updated)
|
||||||
|
- FOUND: .planning/phases/05-calendar-strip/05-02-SUMMARY.md
|
||||||
|
- FOUND: commit f718ee8 (Task 1)
|
||||||
|
- FOUND: commit 88ef248 (Task 2)
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import java.io.FileInputStream
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
@@ -31,25 +34,25 @@ android {
|
|||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
def keystorePropertiesFile = rootProject.file("key.properties")
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
def keystoreProperties = new Properties()
|
val keystoreProperties = Properties()
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
release {
|
create("release") {
|
||||||
keyAlias = keystoreProperties['keyAlias']
|
keyAlias = keystoreProperties.getProperty("keyAlias")
|
||||||
keyPassword = keystoreProperties['keyPassword']
|
keyPassword = keystoreProperties.getProperty("keyPassword")
|
||||||
storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
storeFile = keystoreProperties.getProperty("storeFile")?.let { file(it) }
|
||||||
storePassword = keystoreProperties['storePassword']
|
storePassword = keystoreProperties.getProperty("storePassword")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
// TODO: Add your own signing config for the release build.
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
signingConfig = signingConfigs.release
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.jlmak.household_keeper
|
package de.jeanlucmakiola.household_keeper
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
|
|||||||
import 'package:drift_flutter/drift_flutter.dart';
|
import 'package:drift_flutter/drift_flutter.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
import '../../features/home/data/calendar_dao.dart';
|
||||||
import '../../features/home/data/daily_plan_dao.dart';
|
import '../../features/home/data/daily_plan_dao.dart';
|
||||||
import '../../features/rooms/data/rooms_dao.dart';
|
import '../../features/rooms/data/rooms_dao.dart';
|
||||||
import '../../features/tasks/data/tasks_dao.dart';
|
import '../../features/tasks/data/tasks_dao.dart';
|
||||||
@@ -45,7 +46,7 @@ class TaskCompletions extends Table {
|
|||||||
|
|
||||||
@DriftDatabase(
|
@DriftDatabase(
|
||||||
tables: [Rooms, Tasks, TaskCompletions],
|
tables: [Rooms, Tasks, TaskCompletions],
|
||||||
daos: [RoomsDao, TasksDao, DailyPlanDao],
|
daos: [RoomsDao, TasksDao, DailyPlanDao, CalendarDao],
|
||||||
)
|
)
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase([QueryExecutor? executor])
|
AppDatabase([QueryExecutor? executor])
|
||||||
|
|||||||
@@ -1246,6 +1246,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
late final RoomsDao roomsDao = RoomsDao(this as AppDatabase);
|
late final RoomsDao roomsDao = RoomsDao(this as AppDatabase);
|
||||||
late final TasksDao tasksDao = TasksDao(this as AppDatabase);
|
late final TasksDao tasksDao = TasksDao(this as AppDatabase);
|
||||||
late final DailyPlanDao dailyPlanDao = DailyPlanDao(this as AppDatabase);
|
late final DailyPlanDao dailyPlanDao = DailyPlanDao(this as AppDatabase);
|
||||||
|
late final CalendarDao calendarDao = CalendarDao(this as AppDatabase);
|
||||||
@override
|
@override
|
||||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||||
|
|||||||
87
lib/features/home/data/calendar_dao.dart
Normal file
87
lib/features/home/data/calendar_dao.dart
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
import '../../../core/database/database.dart';
|
||||||
|
import '../domain/daily_plan_models.dart';
|
||||||
|
|
||||||
|
part 'calendar_dao.g.dart';
|
||||||
|
|
||||||
|
/// DAO for calendar-based task queries.
|
||||||
|
///
|
||||||
|
/// Provides date-parameterized queries to answer:
|
||||||
|
/// - "What tasks are due on date X?"
|
||||||
|
/// - "What tasks are overdue relative to today?"
|
||||||
|
@DriftAccessor(tables: [Tasks, Rooms, TaskCompletions])
|
||||||
|
class CalendarDao extends DatabaseAccessor<AppDatabase>
|
||||||
|
with _$CalendarDaoMixin {
|
||||||
|
CalendarDao(super.attachedDatabase);
|
||||||
|
|
||||||
|
/// Watch tasks whose [nextDueDate] falls on the given calendar day.
|
||||||
|
///
|
||||||
|
/// Returns tasks sorted alphabetically by name.
|
||||||
|
/// Does NOT include overdue carry-over — only tasks originally due on [date].
|
||||||
|
Stream<List<TaskWithRoom>> watchTasksForDate(DateTime date) {
|
||||||
|
final startOfDay = DateTime(date.year, date.month, date.day);
|
||||||
|
final endOfDay = startOfDay.add(const Duration(days: 1));
|
||||||
|
|
||||||
|
final query = select(tasks).join([
|
||||||
|
innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
|
||||||
|
]);
|
||||||
|
query.where(
|
||||||
|
tasks.nextDueDate.isBiggerOrEqualValue(startOfDay) &
|
||||||
|
tasks.nextDueDate.isSmallerThanValue(endOfDay),
|
||||||
|
);
|
||||||
|
query.orderBy([OrderingTerm.asc(tasks.name)]);
|
||||||
|
|
||||||
|
return query.watch().map((rows) {
|
||||||
|
return rows.map((row) {
|
||||||
|
final task = row.readTable(tasks);
|
||||||
|
final room = row.readTable(rooms);
|
||||||
|
return TaskWithRoom(
|
||||||
|
task: task,
|
||||||
|
roomName: room.name,
|
||||||
|
roomId: room.id,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the total count of tasks across all rooms and dates.
|
||||||
|
///
|
||||||
|
/// Used by the UI to distinguish first-run empty state from celebration state.
|
||||||
|
Future<int> getTaskCount() async {
|
||||||
|
final countExp = tasks.id.count();
|
||||||
|
final query = selectOnly(tasks)..addColumns([countExp]);
|
||||||
|
final result = await query.getSingle();
|
||||||
|
return result.read(countExp) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Watch tasks whose [nextDueDate] is strictly before [referenceDate].
|
||||||
|
///
|
||||||
|
/// Returns tasks sorted by [nextDueDate] ascending (oldest first).
|
||||||
|
/// Does NOT include tasks due on [referenceDate] itself.
|
||||||
|
Stream<List<TaskWithRoom>> watchOverdueTasks(DateTime referenceDate) {
|
||||||
|
final startOfReferenceDay = DateTime(
|
||||||
|
referenceDate.year,
|
||||||
|
referenceDate.month,
|
||||||
|
referenceDate.day,
|
||||||
|
);
|
||||||
|
|
||||||
|
final query = select(tasks).join([
|
||||||
|
innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
|
||||||
|
]);
|
||||||
|
query.where(tasks.nextDueDate.isSmallerThanValue(startOfReferenceDay));
|
||||||
|
query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]);
|
||||||
|
|
||||||
|
return query.watch().map((rows) {
|
||||||
|
return rows.map((row) {
|
||||||
|
final task = row.readTable(tasks);
|
||||||
|
final room = row.readTable(rooms);
|
||||||
|
return TaskWithRoom(
|
||||||
|
task: task,
|
||||||
|
roomName: room.name,
|
||||||
|
roomId: room.id,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
25
lib/features/home/data/calendar_dao.g.dart
Normal file
25
lib/features/home/data/calendar_dao.g.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'calendar_dao.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
mixin _$CalendarDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||||
|
$RoomsTable get rooms => attachedDatabase.rooms;
|
||||||
|
$TasksTable get tasks => attachedDatabase.tasks;
|
||||||
|
$TaskCompletionsTable get taskCompletions => attachedDatabase.taskCompletions;
|
||||||
|
CalendarDaoManager get managers => CalendarDaoManager(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CalendarDaoManager {
|
||||||
|
final _$CalendarDaoMixin _db;
|
||||||
|
CalendarDaoManager(this._db);
|
||||||
|
$$RoomsTableTableManager get rooms =>
|
||||||
|
$$RoomsTableTableManager(_db.attachedDatabase, _db.rooms);
|
||||||
|
$$TasksTableTableManager get tasks =>
|
||||||
|
$$TasksTableTableManager(_db.attachedDatabase, _db.tasks);
|
||||||
|
$$TaskCompletionsTableTableManager get taskCompletions =>
|
||||||
|
$$TaskCompletionsTableTableManager(
|
||||||
|
_db.attachedDatabase,
|
||||||
|
_db.taskCompletions,
|
||||||
|
);
|
||||||
|
}
|
||||||
25
lib/features/home/domain/calendar_models.dart
Normal file
25
lib/features/home/domain/calendar_models.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
||||||
|
|
||||||
|
/// State for the calendar day view: tasks for the selected date + overdue tasks.
|
||||||
|
class CalendarDayState {
|
||||||
|
final DateTime selectedDate;
|
||||||
|
final List<TaskWithRoom> dayTasks;
|
||||||
|
final List<TaskWithRoom> overdueTasks;
|
||||||
|
|
||||||
|
/// Total number of tasks in the database (across all days/rooms).
|
||||||
|
/// Used by the UI to distinguish first-run empty state (no tasks exist at all)
|
||||||
|
/// from celebration state (tasks exist but today's are all done).
|
||||||
|
final int totalTaskCount;
|
||||||
|
|
||||||
|
const CalendarDayState({
|
||||||
|
required this.selectedDate,
|
||||||
|
required this.dayTasks,
|
||||||
|
required this.overdueTasks,
|
||||||
|
required this.totalTaskCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// True when both day tasks and overdue tasks are empty.
|
||||||
|
/// Determined by the UI layer (completion state vs. no tasks at all
|
||||||
|
/// is handled in the widget based on this flag and history context).
|
||||||
|
bool get isEmpty => dayTasks.isEmpty && overdueTasks.isEmpty;
|
||||||
|
}
|
||||||
310
lib/features/home/presentation/calendar_day_list.dart
Normal file
310
lib/features/home/presentation/calendar_day_list.dart
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
||||||
|
import 'package:household_keeper/features/home/domain/calendar_models.dart';
|
||||||
|
import 'package:household_keeper/features/home/presentation/calendar_providers.dart';
|
||||||
|
import 'package:household_keeper/features/home/presentation/calendar_task_row.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/presentation/task_providers.dart';
|
||||||
|
import 'package:household_keeper/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
/// Warm coral/terracotta color for overdue section header.
|
||||||
|
const _overdueColor = Color(0xFFE07A5F);
|
||||||
|
|
||||||
|
/// Shows the task list for the selected calendar day.
|
||||||
|
///
|
||||||
|
/// Watches [calendarDayProvider] and renders one of several states:
|
||||||
|
/// - Loading spinner while data loads
|
||||||
|
/// - Error text on failure
|
||||||
|
/// - First-run empty state (no rooms/tasks at all) — prompts to create a room
|
||||||
|
/// - Empty day state (tasks exist elsewhere but not this day)
|
||||||
|
/// - Celebration state (today is selected and all tasks are done)
|
||||||
|
/// - Has-tasks state with optional overdue section (today only) and checkboxes
|
||||||
|
class CalendarDayList extends ConsumerStatefulWidget {
|
||||||
|
const CalendarDayList({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<CalendarDayList> createState() => _CalendarDayListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CalendarDayListState extends ConsumerState<CalendarDayList> {
|
||||||
|
/// Task IDs currently animating out after completion.
|
||||||
|
final Set<int> _completingTaskIds = {};
|
||||||
|
|
||||||
|
void _onTaskCompleted(int taskId) {
|
||||||
|
setState(() {
|
||||||
|
_completingTaskIds.add(taskId);
|
||||||
|
});
|
||||||
|
ref.read(taskActionsProvider.notifier).completeTask(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final dayState = ref.watch(calendarDayProvider);
|
||||||
|
|
||||||
|
return dayState.when(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (error, _) => Center(child: Text(error.toString())),
|
||||||
|
data: (state) {
|
||||||
|
// Clean up animation IDs for tasks that are no longer in the data.
|
||||||
|
_completingTaskIds.removeWhere((id) =>
|
||||||
|
!state.overdueTasks.any((t) => t.task.id == id) &&
|
||||||
|
!state.dayTasks.any((t) => t.task.id == id));
|
||||||
|
|
||||||
|
return _buildContent(context, state, l10n, theme);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(
|
||||||
|
BuildContext context,
|
||||||
|
CalendarDayState state,
|
||||||
|
AppLocalizations l10n,
|
||||||
|
ThemeData theme,
|
||||||
|
) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
final isToday = state.selectedDate == today;
|
||||||
|
|
||||||
|
// State (a): First-run empty — no tasks exist at all in the database.
|
||||||
|
if (state.isEmpty && state.totalTaskCount == 0) {
|
||||||
|
return _buildFirstRunEmpty(context, l10n, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// State (e): Celebration — today is selected and all tasks are done
|
||||||
|
// (totalTaskCount > 0 so at least some task exists somewhere, but today
|
||||||
|
// has none remaining after completion).
|
||||||
|
if (isToday && state.dayTasks.isEmpty && state.overdueTasks.isEmpty && state.totalTaskCount > 0) {
|
||||||
|
return _buildCelebration(l10n, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// State (d): Empty day — tasks exist elsewhere but not this day.
|
||||||
|
if (state.isEmpty) {
|
||||||
|
return _buildEmptyDay(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// State (f): Has tasks — render overdue section (today only) + day tasks.
|
||||||
|
return _buildTaskList(state, l10n, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// First-run: no rooms/tasks created yet.
|
||||||
|
Widget _buildFirstRunEmpty(
|
||||||
|
BuildContext context,
|
||||||
|
AppLocalizations l10n,
|
||||||
|
ThemeData theme,
|
||||||
|
) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.checklist_rounded,
|
||||||
|
size: 80,
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
l10n.dailyPlanNoTasks,
|
||||||
|
style: theme.textTheme.headlineSmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
l10n.homeEmptyMessage,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
FilledButton.tonal(
|
||||||
|
onPressed: () => context.go('/rooms'),
|
||||||
|
child: Text(l10n.homeEmptyAction),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Celebration state: today is selected and all tasks are done.
|
||||||
|
Widget _buildCelebration(AppLocalizations l10n, ThemeData theme) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.celebration_outlined,
|
||||||
|
size: 80,
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
l10n.dailyPlanAllClearTitle,
|
||||||
|
style: theme.textTheme.headlineSmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
l10n.dailyPlanAllClearMessage,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Empty day: tasks exist elsewhere but nothing scheduled for this day.
|
||||||
|
Widget _buildEmptyDay(ThemeData theme) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.event_available,
|
||||||
|
size: 48,
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Keine Aufgaben',
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Task list with optional overdue section.
|
||||||
|
Widget _buildTaskList(
|
||||||
|
CalendarDayState state,
|
||||||
|
AppLocalizations l10n,
|
||||||
|
ThemeData theme,
|
||||||
|
) {
|
||||||
|
final items = <Widget>[];
|
||||||
|
|
||||||
|
// Overdue section (today only, when overdue tasks exist).
|
||||||
|
if (state.overdueTasks.isNotEmpty) {
|
||||||
|
items.add(_buildSectionHeader(l10n.dailyPlanSectionOverdue, theme,
|
||||||
|
color: _overdueColor));
|
||||||
|
for (final tw in state.overdueTasks) {
|
||||||
|
items.add(_buildAnimatedTaskRow(tw, isOverdue: true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Day tasks section.
|
||||||
|
for (final tw in state.dayTasks) {
|
||||||
|
items.add(_buildAnimatedTaskRow(tw, isOverdue: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView(children: items);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionHeader(
|
||||||
|
String title,
|
||||||
|
ThemeData theme, {
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(color: color),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAnimatedTaskRow(TaskWithRoom tw, {required bool isOverdue}) {
|
||||||
|
final isCompleting = _completingTaskIds.contains(tw.task.id);
|
||||||
|
|
||||||
|
if (isCompleting) {
|
||||||
|
return _CompletingTaskRow(
|
||||||
|
key: ValueKey('completing-${tw.task.id}'),
|
||||||
|
taskWithRoom: tw,
|
||||||
|
isOverdue: isOverdue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CalendarTaskRow(
|
||||||
|
key: ValueKey('task-${tw.task.id}'),
|
||||||
|
taskWithRoom: tw,
|
||||||
|
isOverdue: isOverdue,
|
||||||
|
onCompleted: () => _onTaskCompleted(tw.task.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A task row that animates (slide + size) to zero height on completion.
|
||||||
|
class _CompletingTaskRow extends StatefulWidget {
|
||||||
|
const _CompletingTaskRow({
|
||||||
|
super.key,
|
||||||
|
required this.taskWithRoom,
|
||||||
|
required this.isOverdue,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TaskWithRoom taskWithRoom;
|
||||||
|
final bool isOverdue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CompletingTaskRow> createState() => _CompletingTaskRowState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CompletingTaskRowState extends State<_CompletingTaskRow>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
late final Animation<double> _sizeAnimation;
|
||||||
|
late final Animation<Offset> _slideAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
_sizeAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
_slideAnimation = Tween<Offset>(
|
||||||
|
begin: Offset.zero,
|
||||||
|
end: const Offset(1.0, 0.0),
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
_controller.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizeTransition(
|
||||||
|
sizeFactor: _sizeAnimation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: _slideAnimation,
|
||||||
|
child: CalendarTaskRow(
|
||||||
|
taskWithRoom: widget.taskWithRoom,
|
||||||
|
isOverdue: widget.isOverdue,
|
||||||
|
onCompleted: () {}, // Already completing — ignore repeat taps.
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
lib/features/home/presentation/calendar_providers.dart
Normal file
69
lib/features/home/presentation/calendar_providers.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:household_keeper/core/providers/database_provider.dart';
|
||||||
|
import 'package:household_keeper/features/home/domain/calendar_models.dart';
|
||||||
|
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
||||||
|
|
||||||
|
/// Notifier that manages the currently selected date in the calendar strip.
|
||||||
|
///
|
||||||
|
/// Defaults to today (start of day, time zeroed out).
|
||||||
|
/// NOT autoDispose — the selected date persists while the app is alive.
|
||||||
|
class SelectedDateNotifier extends Notifier<DateTime> {
|
||||||
|
@override
|
||||||
|
DateTime build() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
return DateTime(now.year, now.month, now.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the selected date (always normalized to start of day).
|
||||||
|
void selectDate(DateTime date) {
|
||||||
|
state = DateTime(date.year, date.month, date.day);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provider for the currently selected date in the calendar strip.
|
||||||
|
final selectedDateProvider =
|
||||||
|
NotifierProvider<SelectedDateNotifier, DateTime>(
|
||||||
|
SelectedDateNotifier.new,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Reactive calendar day state: tasks for the selected date + overdue tasks.
|
||||||
|
///
|
||||||
|
/// Overdue tasks are only included when the selected date is today.
|
||||||
|
/// Past and future dates show only tasks originally due on that day.
|
||||||
|
///
|
||||||
|
/// Defined manually (not @riverpod) because riverpod_generator has trouble
|
||||||
|
/// with drift's generated [Task] type. Same pattern as [dailyPlanProvider].
|
||||||
|
final calendarDayProvider =
|
||||||
|
StreamProvider.autoDispose<CalendarDayState>((ref) {
|
||||||
|
final db = ref.watch(appDatabaseProvider);
|
||||||
|
final selectedDate = ref.watch(selectedDateProvider);
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
final isToday = selectedDate == today;
|
||||||
|
|
||||||
|
final dayTasksStream = db.calendarDao.watchTasksForDate(selectedDate);
|
||||||
|
|
||||||
|
return dayTasksStream.asyncMap((dayTasks) async {
|
||||||
|
final List<TaskWithRoom> overdueTasks;
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
// When viewing today, include overdue tasks (due before today)
|
||||||
|
overdueTasks =
|
||||||
|
await db.calendarDao.watchOverdueTasks(selectedDate).first;
|
||||||
|
} else {
|
||||||
|
// Past or future dates: no overdue carry-over
|
||||||
|
overdueTasks = const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final totalTaskCount = await db.calendarDao.getTaskCount();
|
||||||
|
|
||||||
|
return CalendarDayState(
|
||||||
|
selectedDate: selectedDate,
|
||||||
|
dayTasks: dayTasks,
|
||||||
|
overdueTasks: overdueTasks,
|
||||||
|
totalTaskCount: totalTaskCount,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
348
lib/features/home/presentation/calendar_strip.dart
Normal file
348
lib/features/home/presentation/calendar_strip.dart
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import 'package:household_keeper/features/home/presentation/calendar_providers.dart';
|
||||||
|
|
||||||
|
/// Number of days in the past and future to show in the strip.
|
||||||
|
const _kPastDays = 90;
|
||||||
|
const _kFutureDays = 90;
|
||||||
|
|
||||||
|
/// Total number of day cards in the strip.
|
||||||
|
const _kTotalDays = _kPastDays + 1 + _kFutureDays;
|
||||||
|
|
||||||
|
/// Fixed card width and height for each day card.
|
||||||
|
const _kCardWidth = 56.0;
|
||||||
|
const _kCardHeight = 72.0;
|
||||||
|
|
||||||
|
/// Default horizontal margin between cards.
|
||||||
|
const _kCardMargin = 4.0;
|
||||||
|
|
||||||
|
/// Wider gap inserted at month boundaries (left side margin of the first-of-month card).
|
||||||
|
const _kMonthBoundaryGap = 16.0;
|
||||||
|
|
||||||
|
/// Controller that allows external code (e.g. the Today button) to trigger
|
||||||
|
/// a scroll-to-today animation on the strip.
|
||||||
|
class CalendarStripController {
|
||||||
|
VoidCallback? _scrollToToday;
|
||||||
|
|
||||||
|
/// Animate the strip to center today's card.
|
||||||
|
void scrollToToday() => _scrollToToday?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A horizontal scrollable strip of day cards spanning [_kPastDays] days in the
|
||||||
|
/// past and [_kFutureDays] days in the future.
|
||||||
|
///
|
||||||
|
/// Each card shows:
|
||||||
|
/// - German day abbreviation (Mo, Di, Mi, Do, Fr, Sa, So)
|
||||||
|
/// - Date number (day of month)
|
||||||
|
///
|
||||||
|
/// The selected card is highlighted and always centered.
|
||||||
|
/// Today's card uses bold text + an accent underline bar.
|
||||||
|
/// Month boundaries get a wider gap and a small month label.
|
||||||
|
class CalendarStrip extends ConsumerStatefulWidget {
|
||||||
|
const CalendarStrip({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.onTodayVisibilityChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Controller for programmatic scroll-to-today.
|
||||||
|
final CalendarStripController controller;
|
||||||
|
|
||||||
|
/// Called when today's card enters or leaves the viewport.
|
||||||
|
final ValueChanged<bool> onTodayVisibilityChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<CalendarStrip> createState() => _CalendarStripState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CalendarStripState extends ConsumerState<CalendarStrip> {
|
||||||
|
late final ScrollController _scrollController;
|
||||||
|
late final DateTime _today;
|
||||||
|
late final List<DateTime> _dates;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
_today = DateTime(now.year, now.month, now.day);
|
||||||
|
|
||||||
|
// Build the date list: _kPastDays before today, today, _kFutureDays after.
|
||||||
|
_dates = List.generate(
|
||||||
|
_kTotalDays,
|
||||||
|
(i) => _today.subtract(Duration(days: _kPastDays - i)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate initial scroll offset so today's card is centered.
|
||||||
|
_scrollController = ScrollController(
|
||||||
|
initialScrollOffset: _offsetForIndex(_kPastDays),
|
||||||
|
);
|
||||||
|
|
||||||
|
_scrollController.addListener(_onScroll);
|
||||||
|
|
||||||
|
// Register the scroll-to-today callback on the controller.
|
||||||
|
widget.controller._scrollToToday = _animateToToday;
|
||||||
|
|
||||||
|
// After first frame, animate to center today with a short delay so the
|
||||||
|
// strip has laid out its children.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
_animateToToday();
|
||||||
|
// Initial visibility check
|
||||||
|
_onScroll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.removeListener(_onScroll);
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the scroll offset that centers the card at [index].
|
||||||
|
double _offsetForIndex(int index) {
|
||||||
|
// Sum the widths of all items before [index], then subtract half the viewport
|
||||||
|
// width so the card is centered. We approximate viewport as screen width
|
||||||
|
// because we cannot access it here; we compensate in the post-frame callback.
|
||||||
|
double offset = 0;
|
||||||
|
for (int i = 0; i < index; i++) {
|
||||||
|
offset += _itemWidth(i);
|
||||||
|
}
|
||||||
|
// Center by subtracting half the card container width (will be corrected post-frame).
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the total width occupied by the item at [index], including margins
|
||||||
|
/// and any month-boundary gap on its left side.
|
||||||
|
double _itemWidth(int index) {
|
||||||
|
final date = _dates[index];
|
||||||
|
final leftMargin = _isFirstOfMonth(date) && index > 0
|
||||||
|
? _kMonthBoundaryGap
|
||||||
|
: _kCardMargin;
|
||||||
|
// Each item = leftMargin + card width + rightMargin
|
||||||
|
return leftMargin + _kCardWidth + _kCardMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isFirstOfMonth(DateTime date) => date.day == 1;
|
||||||
|
|
||||||
|
void _animateToToday() {
|
||||||
|
if (!mounted || !_scrollController.hasClients) return;
|
||||||
|
final viewportWidth = _scrollController.position.viewportDimension;
|
||||||
|
double targetOffset = 0;
|
||||||
|
for (int i = 0; i < _kPastDays; i++) {
|
||||||
|
targetOffset += _itemWidth(i);
|
||||||
|
}
|
||||||
|
// Center today's card in the viewport.
|
||||||
|
targetOffset -= (viewportWidth - _kCardWidth) / 2;
|
||||||
|
targetOffset = targetOffset.clamp(
|
||||||
|
_scrollController.position.minScrollExtent,
|
||||||
|
_scrollController.position.maxScrollExtent,
|
||||||
|
);
|
||||||
|
_scrollController.animateTo(
|
||||||
|
targetOffset,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _animateToIndex(int index) {
|
||||||
|
if (!mounted || !_scrollController.hasClients) return;
|
||||||
|
final viewportWidth = _scrollController.position.viewportDimension;
|
||||||
|
double targetOffset = 0;
|
||||||
|
for (int i = 0; i < index; i++) {
|
||||||
|
targetOffset += _itemWidth(i);
|
||||||
|
}
|
||||||
|
targetOffset -= (viewportWidth - _kCardWidth) / 2;
|
||||||
|
targetOffset = targetOffset.clamp(
|
||||||
|
_scrollController.position.minScrollExtent,
|
||||||
|
_scrollController.position.maxScrollExtent,
|
||||||
|
);
|
||||||
|
_scrollController.animateTo(
|
||||||
|
targetOffset,
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScroll() {
|
||||||
|
if (!mounted || !_scrollController.hasClients) return;
|
||||||
|
final viewportWidth = _scrollController.position.viewportDimension;
|
||||||
|
final scrollOffset = _scrollController.offset;
|
||||||
|
|
||||||
|
// Calculate the left edge of today's card.
|
||||||
|
double todayLeftEdge = 0;
|
||||||
|
for (int i = 0; i < _kPastDays; i++) {
|
||||||
|
todayLeftEdge += _itemWidth(i);
|
||||||
|
}
|
||||||
|
final todayRightEdge = todayLeftEdge + _kCardWidth;
|
||||||
|
|
||||||
|
// Today is visible if any part of the card is in the viewport.
|
||||||
|
final isVisible =
|
||||||
|
todayRightEdge > scrollOffset &&
|
||||||
|
todayLeftEdge < scrollOffset + viewportWidth;
|
||||||
|
|
||||||
|
widget.onTodayVisibilityChanged(isVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCardTapped(int index) {
|
||||||
|
final tappedDate = _dates[index];
|
||||||
|
ref.read(selectedDateProvider.notifier).selectDate(tappedDate);
|
||||||
|
_animateToIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final selectedDate = ref.watch(selectedDateProvider);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: _kCardHeight + 24, // extra height for month label
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: _kTotalDays,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final date = _dates[index];
|
||||||
|
final isToday = date == _today;
|
||||||
|
final isSelected = date == selectedDate;
|
||||||
|
final isFirstOfMonth = _isFirstOfMonth(date) && index > 0;
|
||||||
|
|
||||||
|
return _DayCardItem(
|
||||||
|
date: date,
|
||||||
|
isToday: isToday,
|
||||||
|
isSelected: isSelected,
|
||||||
|
isFirstOfMonth: isFirstOfMonth,
|
||||||
|
onTap: () => _onCardTapped(index),
|
||||||
|
theme: theme,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single day card in the calendar strip, with optional month boundary label.
|
||||||
|
class _DayCardItem extends StatelessWidget {
|
||||||
|
const _DayCardItem({
|
||||||
|
required this.date,
|
||||||
|
required this.isToday,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.isFirstOfMonth,
|
||||||
|
required this.onTap,
|
||||||
|
required this.theme,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateTime date;
|
||||||
|
final bool isToday;
|
||||||
|
final bool isSelected;
|
||||||
|
final bool isFirstOfMonth;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final ThemeData theme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final leftMargin = isFirstOfMonth ? _kMonthBoundaryGap : _kCardMargin;
|
||||||
|
|
||||||
|
// Card background color: selected gets full primaryContainer, others get
|
||||||
|
// a subtle tint of primaryContainer.
|
||||||
|
final bgColor = isSelected
|
||||||
|
? theme.colorScheme.primaryContainer
|
||||||
|
: theme.colorScheme.primaryContainer.withValues(alpha: 0.3);
|
||||||
|
|
||||||
|
// Border: selected card gets a primary color border.
|
||||||
|
final border = isSelected
|
||||||
|
? Border.all(color: theme.colorScheme.primary, width: 1.5)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Text weight: today uses bold.
|
||||||
|
final fontWeight = isToday ? FontWeight.bold : FontWeight.normal;
|
||||||
|
|
||||||
|
// Day abbreviation (German locale): Mo, Di, Mi, Do, Fr, Sa, So
|
||||||
|
final dayAbbr = DateFormat('E', 'de').format(date);
|
||||||
|
// Date number
|
||||||
|
final dayNum = date.day.toString();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Month label at boundary
|
||||||
|
if (isFirstOfMonth)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: leftMargin),
|
||||||
|
child: SizedBox(
|
||||||
|
width: _kCardWidth + _kCardMargin,
|
||||||
|
child: Text(
|
||||||
|
DateFormat('MMM', 'de').format(date),
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(height: 16), // Reserve space for month label row
|
||||||
|
|
||||||
|
// Day card
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
width: _kCardWidth,
|
||||||
|
height: _kCardHeight,
|
||||||
|
margin: EdgeInsets.only(left: leftMargin, right: _kCardMargin),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: border,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// German day abbreviation
|
||||||
|
Text(
|
||||||
|
dayAbbr,
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
fontWeight: fontWeight,
|
||||||
|
color: isSelected
|
||||||
|
? theme.colorScheme.onPrimaryContainer
|
||||||
|
: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
// Date number
|
||||||
|
Text(
|
||||||
|
dayNum,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: fontWeight,
|
||||||
|
color: isSelected
|
||||||
|
? theme.colorScheme.onPrimaryContainer
|
||||||
|
: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Today accent underline bar
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
if (isToday)
|
||||||
|
Container(
|
||||||
|
width: 20,
|
||||||
|
height: 2,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
borderRadius: BorderRadius.circular(1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
lib/features/home/presentation/calendar_task_row.dart
Normal file
69
lib/features/home/presentation/calendar_task_row.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
||||||
|
|
||||||
|
/// Warm coral/terracotta color for overdue task name text.
|
||||||
|
const _overdueColor = Color(0xFFE07A5F);
|
||||||
|
|
||||||
|
/// A task row adapted for the calendar day list.
|
||||||
|
///
|
||||||
|
/// Shows task name, a tappable room tag (navigates to room task list),
|
||||||
|
/// and an interactive checkbox. Does NOT show a relative date — the
|
||||||
|
/// calendar strip already communicates which day is selected.
|
||||||
|
///
|
||||||
|
/// When [isOverdue] is true the task name uses coral text to visually
|
||||||
|
/// distinguish overdue carry-over from today's regular tasks.
|
||||||
|
class CalendarTaskRow extends StatelessWidget {
|
||||||
|
const CalendarTaskRow({
|
||||||
|
super.key,
|
||||||
|
required this.taskWithRoom,
|
||||||
|
required this.onCompleted,
|
||||||
|
this.isOverdue = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TaskWithRoom taskWithRoom;
|
||||||
|
|
||||||
|
/// Called when the user checks the checkbox.
|
||||||
|
final VoidCallback onCompleted;
|
||||||
|
|
||||||
|
/// When true, task name is rendered in coral color.
|
||||||
|
final bool isOverdue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final task = taskWithRoom.task;
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: Checkbox(
|
||||||
|
value: false,
|
||||||
|
onChanged: (_) => onCompleted(),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
task.name,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: isOverdue ? _overdueColor : null,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
subtitle: GestureDetector(
|
||||||
|
onTap: () => context.go('/rooms/${taskWithRoom.roomId}'),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
taskWithRoom.roomName,
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
import 'package:household_keeper/features/home/presentation/calendar_day_list.dart';
|
||||||
import 'package:household_keeper/features/home/presentation/daily_plan_providers.dart';
|
import 'package:household_keeper/features/home/presentation/calendar_providers.dart';
|
||||||
import 'package:household_keeper/features/home/presentation/daily_plan_task_row.dart';
|
import 'package:household_keeper/features/home/presentation/calendar_strip.dart';
|
||||||
import 'package:household_keeper/features/home/presentation/progress_card.dart';
|
|
||||||
import 'package:household_keeper/features/tasks/presentation/task_providers.dart';
|
|
||||||
import 'package:household_keeper/l10n/app_localizations.dart';
|
import 'package:household_keeper/l10n/app_localizations.dart';
|
||||||
|
|
||||||
/// Warm coral/terracotta color for overdue section header.
|
/// The app's primary screen: a horizontal calendar strip at the top with a
|
||||||
const _overdueColor = Color(0xFFE07A5F);
|
/// day task list below.
|
||||||
|
|
||||||
/// The app's primary screen: daily plan showing what's due today,
|
|
||||||
/// overdue tasks, and a preview of tomorrow.
|
|
||||||
///
|
///
|
||||||
/// Replaces the former placeholder with a full daily workflow:
|
/// Replaces the former stacked overdue/today/tomorrow daily plan layout.
|
||||||
/// see what's due, check it off, feel progress.
|
/// Users navigate by tapping day cards to see that day's tasks.
|
||||||
class HomeScreen extends ConsumerStatefulWidget {
|
class HomeScreen extends ConsumerStatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
@@ -25,365 +19,51 @@ class HomeScreen extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeScreenState extends ConsumerState<HomeScreen> {
|
class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||||
/// Task IDs currently animating out after completion.
|
late final CalendarStripController _stripController =
|
||||||
final Set<int> _completingTaskIds = {};
|
CalendarStripController();
|
||||||
|
|
||||||
void _onTaskCompleted(int taskId) {
|
/// Whether to show the floating "Heute" button.
|
||||||
setState(() {
|
/// True when the user has scrolled away from today's card.
|
||||||
_completingTaskIds.add(taskId);
|
bool _showTodayButton = false;
|
||||||
});
|
|
||||||
ref.read(taskActionsProvider.notifier).completeTask(taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
|
||||||
final dailyPlan = ref.watch(dailyPlanProvider);
|
|
||||||
|
|
||||||
return dailyPlan.when(
|
return Stack(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
children: [
|
||||||
error: (error, _) => Center(child: Text(error.toString())),
|
Column(
|
||||||
data: (state) {
|
children: [
|
||||||
// Clean up completing IDs that are no longer in the data
|
CalendarStrip(
|
||||||
_completingTaskIds.removeWhere((id) =>
|
controller: _stripController,
|
||||||
!state.overdueTasks.any((t) => t.task.id == id) &&
|
onTodayVisibilityChanged: (visible) {
|
||||||
!state.todayTasks.any((t) => t.task.id == id));
|
setState(() => _showTodayButton = !visible);
|
||||||
|
|
||||||
return _buildDailyPlan(context, state, l10n, theme);
|
|
||||||
},
|
},
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDailyPlan(
|
|
||||||
BuildContext context,
|
|
||||||
DailyPlanState state,
|
|
||||||
AppLocalizations l10n,
|
|
||||||
ThemeData theme,
|
|
||||||
) {
|
|
||||||
// Case a: No tasks at all (user hasn't created any rooms/tasks)
|
|
||||||
if (state.totalTodayCount == 0 &&
|
|
||||||
state.tomorrowTasks.isEmpty &&
|
|
||||||
state.completedTodayCount == 0) {
|
|
||||||
return _buildNoTasksState(l10n, theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case b: All clear -- there WERE tasks today but all are done
|
|
||||||
if (state.overdueTasks.isEmpty &&
|
|
||||||
state.todayTasks.isEmpty &&
|
|
||||||
state.completedTodayCount > 0 &&
|
|
||||||
state.tomorrowTasks.isEmpty) {
|
|
||||||
return _buildAllClearState(state, l10n, theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case c: Nothing today, but stuff tomorrow -- show celebration + tomorrow
|
|
||||||
if (state.overdueTasks.isEmpty &&
|
|
||||||
state.todayTasks.isEmpty &&
|
|
||||||
state.completedTodayCount == 0 &&
|
|
||||||
state.tomorrowTasks.isNotEmpty) {
|
|
||||||
return _buildAllClearWithTomorrow(state, l10n, theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case b extended: all clear with tomorrow tasks
|
|
||||||
if (state.overdueTasks.isEmpty &&
|
|
||||||
state.todayTasks.isEmpty &&
|
|
||||||
state.completedTodayCount > 0 &&
|
|
||||||
state.tomorrowTasks.isNotEmpty) {
|
|
||||||
return _buildAllClearWithTomorrow(state, l10n, theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case d: Normal state -- tasks exist
|
|
||||||
return _buildNormalState(state, l10n, theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No tasks at all -- first-run empty state.
|
|
||||||
Widget _buildNoTasksState(AppLocalizations l10n, ThemeData theme) {
|
|
||||||
return Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.checklist_rounded,
|
|
||||||
size: 80,
|
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Text(
|
|
||||||
l10n.dailyPlanNoTasks,
|
|
||||||
style: theme.textTheme.headlineSmall,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
l10n.homeEmptyMessage,
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
FilledButton.tonal(
|
|
||||||
onPressed: () => context.go('/rooms'),
|
|
||||||
child: Text(l10n.homeEmptyAction),
|
|
||||||
),
|
),
|
||||||
|
const Expanded(child: CalendarDayList()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
if (_showTodayButton)
|
||||||
);
|
Positioned(
|
||||||
}
|
bottom: 16,
|
||||||
|
left: 0,
|
||||||
/// All tasks done, no tomorrow tasks -- celebration state.
|
right: 0,
|
||||||
Widget _buildAllClearState(
|
child: Center(
|
||||||
DailyPlanState state,
|
child: FloatingActionButton.extended(
|
||||||
AppLocalizations l10n,
|
onPressed: () {
|
||||||
ThemeData theme,
|
final now = DateTime.now();
|
||||||
) {
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
return Center(
|
ref
|
||||||
child: Padding(
|
.read(selectedDateProvider.notifier)
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
.selectDate(today);
|
||||||
child: Column(
|
_stripController.scrollToToday();
|
||||||
mainAxisSize: MainAxisSize.min,
|
},
|
||||||
children: [
|
icon: const Icon(Icons.today),
|
||||||
ProgressCard(
|
label: Text(l10n.calendarTodayButton),
|
||||||
completed: state.completedTodayCount,
|
|
||||||
total: state.totalTodayCount,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Icon(
|
|
||||||
Icons.celebration_outlined,
|
|
||||||
size: 80,
|
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Text(
|
|
||||||
l10n.dailyPlanAllClearTitle,
|
|
||||||
style: theme.textTheme.headlineSmall,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
l10n.dailyPlanAllClearMessage,
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All clear for today but tomorrow tasks exist.
|
|
||||||
Widget _buildAllClearWithTomorrow(
|
|
||||||
DailyPlanState state,
|
|
||||||
AppLocalizations l10n,
|
|
||||||
ThemeData theme,
|
|
||||||
) {
|
|
||||||
return ListView(
|
|
||||||
children: [
|
|
||||||
ProgressCard(
|
|
||||||
completed: state.completedTodayCount,
|
|
||||||
total: state.totalTodayCount,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.celebration_outlined,
|
|
||||||
size: 80,
|
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Text(
|
|
||||||
l10n.dailyPlanAllClearTitle,
|
|
||||||
style: theme.textTheme.headlineSmall,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
l10n.dailyPlanAllClearMessage,
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildTomorrowSection(state, l10n, theme),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normal state with overdue/today/tomorrow sections.
|
|
||||||
Widget _buildNormalState(
|
|
||||||
DailyPlanState state,
|
|
||||||
AppLocalizations l10n,
|
|
||||||
ThemeData theme,
|
|
||||||
) {
|
|
||||||
return ListView(
|
|
||||||
children: [
|
|
||||||
ProgressCard(
|
|
||||||
completed: state.completedTodayCount,
|
|
||||||
total: state.totalTodayCount,
|
|
||||||
),
|
|
||||||
// Overdue section (conditional)
|
|
||||||
if (state.overdueTasks.isNotEmpty) ...[
|
|
||||||
_buildSectionHeader(
|
|
||||||
l10n.dailyPlanSectionOverdue,
|
|
||||||
theme,
|
|
||||||
color: _overdueColor,
|
|
||||||
),
|
|
||||||
...state.overdueTasks.map(
|
|
||||||
(tw) => _buildAnimatedTaskRow(tw, showCheckbox: true),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
// Today section
|
|
||||||
_buildSectionHeader(
|
|
||||||
l10n.dailyPlanSectionToday,
|
|
||||||
theme,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
if (state.todayTasks.isEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
child: Text(
|
|
||||||
l10n.dailyPlanAllClearMessage,
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
...state.todayTasks.map(
|
|
||||||
(tw) => _buildAnimatedTaskRow(tw, showCheckbox: true),
|
|
||||||
),
|
|
||||||
// Tomorrow section (conditional, collapsed)
|
|
||||||
if (state.tomorrowTasks.isNotEmpty)
|
|
||||||
_buildTomorrowSection(state, l10n, theme),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSectionHeader(
|
|
||||||
String title,
|
|
||||||
ThemeData theme, {
|
|
||||||
required Color color,
|
|
||||||
}) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(color: color),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAnimatedTaskRow(
|
|
||||||
TaskWithRoom tw, {
|
|
||||||
required bool showCheckbox,
|
|
||||||
}) {
|
|
||||||
final isCompleting = _completingTaskIds.contains(tw.task.id);
|
|
||||||
|
|
||||||
if (isCompleting) {
|
|
||||||
return _CompletingTaskRow(
|
|
||||||
key: ValueKey('completing-${tw.task.id}'),
|
|
||||||
taskWithRoom: tw,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DailyPlanTaskRow(
|
|
||||||
key: ValueKey('task-${tw.task.id}'),
|
|
||||||
taskWithRoom: tw,
|
|
||||||
showCheckbox: showCheckbox,
|
|
||||||
onCompleted: () => _onTaskCompleted(tw.task.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTomorrowSection(
|
|
||||||
DailyPlanState state,
|
|
||||||
AppLocalizations l10n,
|
|
||||||
ThemeData theme,
|
|
||||||
) {
|
|
||||||
return ExpansionTile(
|
|
||||||
initiallyExpanded: false,
|
|
||||||
title: Text(
|
|
||||||
l10n.dailyPlanUpcomingCount(state.tomorrowTasks.length),
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
children: state.tomorrowTasks
|
|
||||||
.map(
|
|
||||||
(tw) => DailyPlanTaskRow(
|
|
||||||
key: ValueKey('tomorrow-${tw.task.id}'),
|
|
||||||
taskWithRoom: tw,
|
|
||||||
showCheckbox: false,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A task row that animates to zero height on completion.
|
|
||||||
class _CompletingTaskRow extends StatefulWidget {
|
|
||||||
const _CompletingTaskRow({
|
|
||||||
super.key,
|
|
||||||
required this.taskWithRoom,
|
|
||||||
});
|
|
||||||
|
|
||||||
final TaskWithRoom taskWithRoom;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_CompletingTaskRow> createState() => _CompletingTaskRowState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CompletingTaskRowState extends State<_CompletingTaskRow>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late final AnimationController _controller;
|
|
||||||
late final Animation<double> _sizeAnimation;
|
|
||||||
late final Animation<Offset> _slideAnimation;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
_sizeAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
|
||||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
|
||||||
);
|
|
||||||
_slideAnimation = Tween<Offset>(
|
|
||||||
begin: Offset.zero,
|
|
||||||
end: const Offset(1.0, 0.0),
|
|
||||||
).animate(
|
|
||||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
|
||||||
);
|
|
||||||
_controller.forward();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizeTransition(
|
|
||||||
sizeFactor: _sizeAnimation,
|
|
||||||
child: SlideTransition(
|
|
||||||
position: _slideAnimation,
|
|
||||||
child: DailyPlanTaskRow(
|
|
||||||
taskWithRoom: widget.taskWithRoom,
|
|
||||||
showCheckbox: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,5 +106,6 @@
|
|||||||
"count": { "type": "int" },
|
"count": { "type": "int" },
|
||||||
"overdue": { "type": "int" }
|
"overdue": { "type": "int" }
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"calendarTodayButton": "Heute"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -513,6 +513,12 @@ abstract class AppLocalizations {
|
|||||||
/// In de, this message translates to:
|
/// In de, this message translates to:
|
||||||
/// **'{count} Aufgaben fällig ({overdue} überfällig)'**
|
/// **'{count} Aufgaben fällig ({overdue} überfällig)'**
|
||||||
String notificationBodyWithOverdue(int count, int overdue);
|
String notificationBodyWithOverdue(int count, int overdue);
|
||||||
|
|
||||||
|
/// No description provided for @calendarTodayButton.
|
||||||
|
///
|
||||||
|
/// In de, this message translates to:
|
||||||
|
/// **'Heute'**
|
||||||
|
String get calendarTodayButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -236,4 +236,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String notificationBodyWithOverdue(int count, int overdue) {
|
String notificationBodyWithOverdue(int count, int overdue) {
|
||||||
return '$count Aufgaben fällig ($overdue überfällig)';
|
return '$count Aufgaben fällig ($overdue überfällig)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get calendarTodayButton => 'Heute';
|
||||||
}
|
}
|
||||||
|
|||||||
286
test/features/home/data/calendar_dao_test.dart
Normal file
286
test/features/home/data/calendar_dao_test.dart
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:household_keeper/core/database/database.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/domain/effort_level.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/domain/frequency.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late AppDatabase db;
|
||||||
|
late int room1Id;
|
||||||
|
late int room2Id;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
db = AppDatabase(NativeDatabase.memory());
|
||||||
|
room1Id = await db.roomsDao.insertRoom(
|
||||||
|
RoomsCompanion.insert(name: 'Kueche', iconName: 'kitchen'),
|
||||||
|
);
|
||||||
|
room2Id = await db.roomsDao.insertRoom(
|
||||||
|
RoomsCompanion.insert(name: 'Badezimmer', iconName: 'bathroom'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('CalendarDao.watchTasksForDate', () {
|
||||||
|
test('returns empty list when no tasks exist', () async {
|
||||||
|
final result = await db.calendarDao
|
||||||
|
.watchTasksForDate(DateTime(2026, 3, 16))
|
||||||
|
.first;
|
||||||
|
expect(result, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns only tasks due on the queried date', () async {
|
||||||
|
// Task due on March 16
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Abspuelen',
|
||||||
|
intervalType: IntervalType.daily,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 16, 9, 30),
|
||||||
|
));
|
||||||
|
// Task due on March 15 (should NOT appear)
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Staubsaugen',
|
||||||
|
intervalType: IntervalType.weekly,
|
||||||
|
effortLevel: EffortLevel.medium,
|
||||||
|
nextDueDate: DateTime(2026, 3, 15),
|
||||||
|
));
|
||||||
|
// Task due on March 17 (should NOT appear)
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Fenster putzen',
|
||||||
|
intervalType: IntervalType.monthly,
|
||||||
|
effortLevel: EffortLevel.high,
|
||||||
|
nextDueDate: DateTime(2026, 3, 17),
|
||||||
|
));
|
||||||
|
|
||||||
|
final result = await db.calendarDao
|
||||||
|
.watchTasksForDate(DateTime(2026, 3, 16))
|
||||||
|
.first;
|
||||||
|
expect(result.length, 1);
|
||||||
|
expect(result.first.task.name, 'Abspuelen');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns tasks from multiple rooms with correct room pairing',
|
||||||
|
() async {
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Kueche Aufgabe',
|
||||||
|
intervalType: IntervalType.daily,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 20),
|
||||||
|
));
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room2Id,
|
||||||
|
name: 'Bad Aufgabe',
|
||||||
|
intervalType: IntervalType.weekly,
|
||||||
|
effortLevel: EffortLevel.medium,
|
||||||
|
nextDueDate: DateTime(2026, 3, 20, 14),
|
||||||
|
));
|
||||||
|
|
||||||
|
final result = await db.calendarDao
|
||||||
|
.watchTasksForDate(DateTime(2026, 3, 20))
|
||||||
|
.first;
|
||||||
|
expect(result.length, 2);
|
||||||
|
|
||||||
|
final names = result.map((t) => t.task.name).toList();
|
||||||
|
expect(names, contains('Kueche Aufgabe'));
|
||||||
|
expect(names, contains('Bad Aufgabe'));
|
||||||
|
|
||||||
|
final kuecheTask =
|
||||||
|
result.firstWhere((t) => t.task.name == 'Kueche Aufgabe');
|
||||||
|
expect(kuecheTask.roomName, 'Kueche');
|
||||||
|
expect(kuecheTask.roomId, room1Id);
|
||||||
|
|
||||||
|
final badTask =
|
||||||
|
result.firstWhere((t) => t.task.name == 'Bad Aufgabe');
|
||||||
|
expect(badTask.roomName, 'Badezimmer');
|
||||||
|
expect(badTask.roomId, room2Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns tasks sorted alphabetically by name', () async {
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Zitrone putzen',
|
||||||
|
intervalType: IntervalType.daily,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 18),
|
||||||
|
));
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Abspuelen',
|
||||||
|
intervalType: IntervalType.daily,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 18, 10),
|
||||||
|
));
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room2Id,
|
||||||
|
name: 'Moppen',
|
||||||
|
intervalType: IntervalType.weekly,
|
||||||
|
effortLevel: EffortLevel.medium,
|
||||||
|
nextDueDate: DateTime(2026, 3, 18, 8),
|
||||||
|
));
|
||||||
|
|
||||||
|
final result = await db.calendarDao
|
||||||
|
.watchTasksForDate(DateTime(2026, 3, 18))
|
||||||
|
.first;
|
||||||
|
expect(result.length, 3);
|
||||||
|
expect(result[0].task.name, 'Abspuelen');
|
||||||
|
expect(result[1].task.name, 'Moppen');
|
||||||
|
expect(result[2].task.name, 'Zitrone putzen');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does NOT include overdue carry-over for past dates', () async {
|
||||||
|
// Task due on March 10 (overdue relative to March 16)
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Alte Aufgabe',
|
||||||
|
intervalType: IntervalType.monthly,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 10),
|
||||||
|
));
|
||||||
|
// Task due on March 15 (queried date)
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Richtige Aufgabe',
|
||||||
|
intervalType: IntervalType.daily,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 15),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Querying March 15 should only return the task due on March 15
|
||||||
|
final result = await db.calendarDao
|
||||||
|
.watchTasksForDate(DateTime(2026, 3, 15))
|
||||||
|
.first;
|
||||||
|
expect(result.length, 1);
|
||||||
|
expect(result.first.task.name, 'Richtige Aufgabe');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('CalendarDao.watchOverdueTasks', () {
|
||||||
|
test('returns empty list when no overdue tasks exist', () async {
|
||||||
|
final result = await db.calendarDao
|
||||||
|
.watchOverdueTasks(DateTime(2026, 3, 16))
|
||||||
|
.first;
|
||||||
|
expect(result, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns tasks whose nextDueDate is before referenceDate', () async {
|
||||||
|
// Task due March 15 — overdue relative to March 16
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Ueberfaelliges Task',
|
||||||
|
intervalType: IntervalType.daily,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 15),
|
||||||
|
));
|
||||||
|
// Task due March 10 — also overdue
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Sehr altes Task',
|
||||||
|
intervalType: IntervalType.weekly,
|
||||||
|
effortLevel: EffortLevel.medium,
|
||||||
|
nextDueDate: DateTime(2026, 3, 10),
|
||||||
|
));
|
||||||
|
|
||||||
|
final result = await db.calendarDao
|
||||||
|
.watchOverdueTasks(DateTime(2026, 3, 16))
|
||||||
|
.first;
|
||||||
|
expect(result.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does NOT include tasks due ON the referenceDate', () async {
|
||||||
|
// Task due exactly on reference date (March 16) — should NOT appear
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Heutiges Task',
|
||||||
|
intervalType: IntervalType.daily,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 16),
|
||||||
|
));
|
||||||
|
// Task due yesterday (March 15) — should appear
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Gestriges Task',
|
||||||
|
intervalType: IntervalType.daily,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 15),
|
||||||
|
));
|
||||||
|
|
||||||
|
final result = await db.calendarDao
|
||||||
|
.watchOverdueTasks(DateTime(2026, 3, 16))
|
||||||
|
.first;
|
||||||
|
expect(result.length, 1);
|
||||||
|
expect(result.first.task.name, 'Gestriges Task');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does NOT include tasks due in the future', () async {
|
||||||
|
// Future task — should NOT appear
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Zukuenftiges Task',
|
||||||
|
intervalType: IntervalType.weekly,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 20),
|
||||||
|
));
|
||||||
|
|
||||||
|
final result = await db.calendarDao
|
||||||
|
.watchOverdueTasks(DateTime(2026, 3, 16))
|
||||||
|
.first;
|
||||||
|
expect(result, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns overdue tasks sorted by nextDueDate ascending', () async {
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Neues Overdue',
|
||||||
|
intervalType: IntervalType.daily,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 15),
|
||||||
|
));
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room1Id,
|
||||||
|
name: 'Altes Overdue',
|
||||||
|
intervalType: IntervalType.monthly,
|
||||||
|
effortLevel: EffortLevel.medium,
|
||||||
|
nextDueDate: DateTime(2026, 3, 1),
|
||||||
|
));
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room2Id,
|
||||||
|
name: 'Mittleres Overdue',
|
||||||
|
intervalType: IntervalType.weekly,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 10),
|
||||||
|
));
|
||||||
|
|
||||||
|
final result = await db.calendarDao
|
||||||
|
.watchOverdueTasks(DateTime(2026, 3, 16))
|
||||||
|
.first;
|
||||||
|
expect(result.length, 3);
|
||||||
|
expect(result[0].task.name, 'Altes Overdue');
|
||||||
|
expect(result[1].task.name, 'Mittleres Overdue');
|
||||||
|
expect(result[2].task.name, 'Neues Overdue');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns overdue task with correct room pairing', () async {
|
||||||
|
await db.tasksDao.insertTask(TasksCompanion.insert(
|
||||||
|
roomId: room2Id,
|
||||||
|
name: 'Bad Overdue',
|
||||||
|
intervalType: IntervalType.weekly,
|
||||||
|
effortLevel: EffortLevel.low,
|
||||||
|
nextDueDate: DateTime(2026, 3, 14),
|
||||||
|
));
|
||||||
|
|
||||||
|
final result = await db.calendarDao
|
||||||
|
.watchOverdueTasks(DateTime(2026, 3, 16))
|
||||||
|
.first;
|
||||||
|
expect(result.length, 1);
|
||||||
|
expect(result.first.task.name, 'Bad Overdue');
|
||||||
|
expect(result.first.roomName, 'Badezimmer');
|
||||||
|
expect(result.first.roomId, room2Id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -5,12 +5,13 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
|
|
||||||
import 'package:household_keeper/core/database/database.dart';
|
import 'package:household_keeper/core/database/database.dart';
|
||||||
import 'package:household_keeper/core/router/router.dart';
|
import 'package:household_keeper/core/router/router.dart';
|
||||||
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
import 'package:household_keeper/features/home/domain/calendar_models.dart';
|
||||||
import 'package:household_keeper/features/home/presentation/daily_plan_providers.dart';
|
import 'package:household_keeper/features/home/presentation/calendar_providers.dart';
|
||||||
import 'package:household_keeper/features/rooms/presentation/room_providers.dart';
|
import 'package:household_keeper/features/rooms/presentation/room_providers.dart';
|
||||||
import 'package:household_keeper/features/tasks/domain/effort_level.dart';
|
import 'package:household_keeper/features/tasks/domain/effort_level.dart';
|
||||||
import 'package:household_keeper/features/tasks/domain/frequency.dart';
|
import 'package:household_keeper/features/tasks/domain/frequency.dart';
|
||||||
import 'package:household_keeper/l10n/app_localizations.dart';
|
import 'package:household_keeper/l10n/app_localizations.dart';
|
||||||
|
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
||||||
|
|
||||||
/// Helper to create a test [Task] with sensible defaults.
|
/// Helper to create a test [Task] with sensible defaults.
|
||||||
Task _makeTask({
|
Task _makeTask({
|
||||||
@@ -51,15 +52,16 @@ TaskWithRoom _makeTaskWithRoom({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the app with dailyPlanProvider overridden to the given state.
|
/// Build the app with calendarDayProvider overridden to the given state.
|
||||||
///
|
///
|
||||||
/// Uses [UncontrolledProviderScope] with a [ProviderContainer] to avoid
|
/// Uses [UncontrolledProviderScope] with a [ProviderContainer] to avoid
|
||||||
/// the riverpod_lint scoped_providers_should_specify_dependencies warning.
|
/// the riverpod_lint scoped_providers_should_specify_dependencies warning.
|
||||||
Widget _buildApp(DailyPlanState planState) {
|
Widget _buildApp(CalendarDayState dayState) {
|
||||||
final container = ProviderContainer(overrides: [
|
final container = ProviderContainer(overrides: [
|
||||||
dailyPlanProvider.overrideWith(
|
calendarDayProvider.overrideWith(
|
||||||
(ref) => Stream.value(planState),
|
(ref) => Stream.value(dayState),
|
||||||
),
|
),
|
||||||
|
selectedDateProvider.overrideWith(SelectedDateNotifier.new),
|
||||||
roomWithStatsListProvider.overrideWith(
|
roomWithStatsListProvider.overrideWith(
|
||||||
(ref) => Stream.value([]),
|
(ref) => Stream.value([]),
|
||||||
),
|
),
|
||||||
@@ -81,17 +83,21 @@ void main() {
|
|||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
final yesterday = today.subtract(const Duration(days: 1));
|
||||||
|
|
||||||
group('HomeScreen empty states', () {
|
group('HomeScreen empty states', () {
|
||||||
testWidgets('shows no-tasks empty state when no tasks exist at all',
|
testWidgets('shows no-tasks empty state when no tasks exist at all',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
await tester.pumpWidget(_buildApp(const DailyPlanState(
|
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||||
overdueTasks: [],
|
selectedDate: today,
|
||||||
todayTasks: [],
|
dayTasks: const [],
|
||||||
tomorrowTasks: [],
|
overdueTasks: const [],
|
||||||
completedTodayCount: 0,
|
totalTaskCount: 0,
|
||||||
totalTodayCount: 0,
|
|
||||||
)));
|
)));
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
// Should show "Noch keine Aufgaben angelegt" (dailyPlanNoTasks)
|
// Should show "Noch keine Aufgaben angelegt" (dailyPlanNoTasks)
|
||||||
expect(find.text('Noch keine Aufgaben angelegt'), findsOneWidget);
|
expect(find.text('Noch keine Aufgaben angelegt'), findsOneWidget);
|
||||||
@@ -99,58 +105,53 @@ void main() {
|
|||||||
expect(find.text('Raum erstellen'), findsOneWidget);
|
expect(find.text('Raum erstellen'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('shows all-clear state when all tasks are done',
|
testWidgets('shows celebration state when tasks exist but today is clear',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
await tester.pumpWidget(_buildApp(const DailyPlanState(
|
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||||
overdueTasks: [],
|
selectedDate: today,
|
||||||
todayTasks: [],
|
dayTasks: const [],
|
||||||
tomorrowTasks: [],
|
overdueTasks: const [],
|
||||||
completedTodayCount: 3,
|
totalTaskCount: 5, // tasks exist elsewhere
|
||||||
totalTodayCount: 3,
|
|
||||||
)));
|
)));
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
// Should show celebration empty state
|
// Should show celebration state
|
||||||
expect(find.text('Alles erledigt! \u{1F31F}'), findsOneWidget);
|
|
||||||
expect(find.byIcon(Icons.celebration_outlined), findsOneWidget);
|
expect(find.byIcon(Icons.celebration_outlined), findsOneWidget);
|
||||||
// Progress card should show 3/3
|
expect(find.text('Alles erledigt! \u{1F31F}'), findsOneWidget);
|
||||||
expect(find.text('3 von 3 erledigt'), findsOneWidget);
|
});
|
||||||
|
|
||||||
|
testWidgets('shows empty-day state for non-today date with no tasks',
|
||||||
|
(tester) async {
|
||||||
|
final tomorrow = today.add(const Duration(days: 1));
|
||||||
|
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||||
|
selectedDate: tomorrow,
|
||||||
|
dayTasks: const [],
|
||||||
|
overdueTasks: const [],
|
||||||
|
totalTaskCount: 5, // tasks exist on other days
|
||||||
|
)));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
// Should show "Keine Aufgaben" (not celebration — not today)
|
||||||
|
expect(find.text('Keine Aufgaben'), findsOneWidget);
|
||||||
|
expect(find.byIcon(Icons.event_available), findsOneWidget);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('HomeScreen normal state', () {
|
group('HomeScreen normal state', () {
|
||||||
testWidgets('shows progress card with correct counts', (tester) async {
|
testWidgets('shows overdue section when overdue tasks exist (today)',
|
||||||
final now = DateTime.now();
|
(tester) async {
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||||
|
selectedDate: today,
|
||||||
await tester.pumpWidget(_buildApp(DailyPlanState(
|
dayTasks: [
|
||||||
overdueTasks: [],
|
|
||||||
todayTasks: [
|
|
||||||
_makeTaskWithRoom(
|
_makeTaskWithRoom(
|
||||||
id: 1,
|
id: 2,
|
||||||
taskName: 'Staubsaugen',
|
taskName: 'Staubsaugen',
|
||||||
roomName: 'Wohnzimmer',
|
roomName: 'Wohnzimmer',
|
||||||
nextDueDate: today,
|
nextDueDate: today,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
tomorrowTasks: [],
|
|
||||||
completedTodayCount: 2,
|
|
||||||
totalTodayCount: 3,
|
|
||||||
)));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// Progress card should show 2/3
|
|
||||||
expect(find.text('2 von 3 erledigt'), findsOneWidget);
|
|
||||||
expect(find.byType(LinearProgressIndicator), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('shows overdue section when overdue tasks exist',
|
|
||||||
(tester) async {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
|
||||||
final yesterday = today.subtract(const Duration(days: 1));
|
|
||||||
|
|
||||||
await tester.pumpWidget(_buildApp(DailyPlanState(
|
|
||||||
overdueTasks: [
|
overdueTasks: [
|
||||||
_makeTaskWithRoom(
|
_makeTaskWithRoom(
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -159,24 +160,13 @@ void main() {
|
|||||||
nextDueDate: yesterday,
|
nextDueDate: yesterday,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
todayTasks: [
|
totalTaskCount: 2,
|
||||||
_makeTaskWithRoom(
|
|
||||||
id: 2,
|
|
||||||
taskName: 'Staubsaugen',
|
|
||||||
roomName: 'Wohnzimmer',
|
|
||||||
nextDueDate: today,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
tomorrowTasks: [],
|
|
||||||
completedTodayCount: 0,
|
|
||||||
totalTodayCount: 2,
|
|
||||||
)));
|
)));
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
// Should show overdue section header
|
// Should show overdue section header
|
||||||
expect(find.text('\u00dcberf\u00e4llig'), findsOneWidget);
|
expect(find.text('\u00dcberf\u00e4llig'), findsOneWidget);
|
||||||
// Should show today section header (may also appear as relative date)
|
|
||||||
expect(find.text('Heute'), findsAtLeast(1));
|
|
||||||
// Should show both tasks
|
// Should show both tasks
|
||||||
expect(find.text('Boden wischen'), findsOneWidget);
|
expect(find.text('Boden wischen'), findsOneWidget);
|
||||||
expect(find.text('Staubsaugen'), findsOneWidget);
|
expect(find.text('Staubsaugen'), findsOneWidget);
|
||||||
@@ -185,54 +175,37 @@ void main() {
|
|||||||
expect(find.text('Wohnzimmer'), findsOneWidget);
|
expect(find.text('Wohnzimmer'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('shows collapsed tomorrow section with count',
|
testWidgets('does not show overdue section for non-today date',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
final now = DateTime.now();
|
// On a future date, overdueTasks will be empty (calendarDayProvider
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
// only populates overdueTasks when isToday).
|
||||||
final tomorrow = today.add(const Duration(days: 1));
|
final tomorrow = today.add(const Duration(days: 1));
|
||||||
|
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||||
await tester.pumpWidget(_buildApp(DailyPlanState(
|
selectedDate: tomorrow,
|
||||||
overdueTasks: [],
|
dayTasks: [
|
||||||
todayTasks: [
|
|
||||||
_makeTaskWithRoom(
|
_makeTaskWithRoom(
|
||||||
id: 1,
|
id: 1,
|
||||||
taskName: 'Staubsaugen',
|
taskName: 'Staubsaugen',
|
||||||
roomName: 'Wohnzimmer',
|
roomName: 'Wohnzimmer',
|
||||||
nextDueDate: today,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
tomorrowTasks: [
|
|
||||||
_makeTaskWithRoom(
|
|
||||||
id: 2,
|
|
||||||
taskName: 'Fenster putzen',
|
|
||||||
roomName: 'Schlafzimmer',
|
|
||||||
nextDueDate: tomorrow,
|
|
||||||
),
|
|
||||||
_makeTaskWithRoom(
|
|
||||||
id: 3,
|
|
||||||
taskName: 'Bett beziehen',
|
|
||||||
roomName: 'Schlafzimmer',
|
|
||||||
nextDueDate: tomorrow,
|
nextDueDate: tomorrow,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
completedTodayCount: 0,
|
overdueTasks: const [], // No overdue for non-today
|
||||||
totalTodayCount: 1,
|
totalTaskCount: 1,
|
||||||
)));
|
)));
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
// Should show collapsed tomorrow section with count
|
// Should NOT show overdue section header
|
||||||
expect(find.text('Demn\u00e4chst (2)'), findsOneWidget);
|
expect(find.text('\u00dcberf\u00e4llig'), findsNothing);
|
||||||
// Tomorrow tasks should NOT be visible (collapsed by default)
|
// Should show day task
|
||||||
expect(find.text('Fenster putzen'), findsNothing);
|
expect(find.text('Staubsaugen'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('today tasks have checkboxes', (tester) async {
|
testWidgets('tasks have checkboxes', (tester) async {
|
||||||
final now = DateTime.now();
|
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
selectedDate: today,
|
||||||
|
dayTasks: [
|
||||||
await tester.pumpWidget(_buildApp(DailyPlanState(
|
|
||||||
overdueTasks: [],
|
|
||||||
todayTasks: [
|
|
||||||
_makeTaskWithRoom(
|
_makeTaskWithRoom(
|
||||||
id: 1,
|
id: 1,
|
||||||
taskName: 'Staubsaugen',
|
taskName: 'Staubsaugen',
|
||||||
@@ -240,14 +213,29 @@ void main() {
|
|||||||
nextDueDate: today,
|
nextDueDate: today,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
tomorrowTasks: [],
|
overdueTasks: const [],
|
||||||
completedTodayCount: 0,
|
totalTaskCount: 1,
|
||||||
totalTodayCount: 1,
|
|
||||||
)));
|
)));
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
// Today task should have a checkbox
|
// Task should have a checkbox
|
||||||
expect(find.byType(Checkbox), findsOneWidget);
|
expect(find.byType(Checkbox), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('calendar strip is shown', (tester) async {
|
||||||
|
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||||
|
selectedDate: today,
|
||||||
|
dayTasks: const [],
|
||||||
|
overdueTasks: const [],
|
||||||
|
totalTaskCount: 0,
|
||||||
|
)));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
// The strip is a horizontal ListView — verify it exists by finding
|
||||||
|
// ListView widgets (strip + potentially the task list).
|
||||||
|
expect(find.byType(ListView), findsWidgets);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import 'package:household_keeper/core/router/router.dart';
|
import 'package:household_keeper/core/router/router.dart';
|
||||||
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
import 'package:household_keeper/features/home/domain/calendar_models.dart';
|
||||||
import 'package:household_keeper/features/home/presentation/daily_plan_providers.dart';
|
import 'package:household_keeper/features/home/presentation/calendar_providers.dart';
|
||||||
import 'package:household_keeper/features/rooms/presentation/room_providers.dart';
|
import 'package:household_keeper/features/rooms/presentation/room_providers.dart';
|
||||||
import 'package:household_keeper/l10n/app_localizations.dart';
|
import 'package:household_keeper/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -15,6 +15,9 @@ void main() {
|
|||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
|
||||||
/// Helper to build the app with providers overridden for testing.
|
/// Helper to build the app with providers overridden for testing.
|
||||||
///
|
///
|
||||||
/// Uses [UncontrolledProviderScope] with a [ProviderContainer] to avoid
|
/// Uses [UncontrolledProviderScope] with a [ProviderContainer] to avoid
|
||||||
@@ -26,15 +29,16 @@ void main() {
|
|||||||
roomWithStatsListProvider.overrideWith(
|
roomWithStatsListProvider.overrideWith(
|
||||||
(ref) => Stream.value([]),
|
(ref) => Stream.value([]),
|
||||||
),
|
),
|
||||||
// Override daily plan to return empty state so HomeScreen
|
// Override selected date to avoid any DB access.
|
||||||
// renders without a database.
|
selectedDateProvider.overrideWith(SelectedDateNotifier.new),
|
||||||
dailyPlanProvider.overrideWith(
|
// Override calendar day provider to return empty first-run state so
|
||||||
(ref) => Stream.value(const DailyPlanState(
|
// HomeScreen renders without a database.
|
||||||
overdueTasks: [],
|
calendarDayProvider.overrideWith(
|
||||||
todayTasks: [],
|
(ref) => Stream.value(CalendarDayState(
|
||||||
tomorrowTasks: [],
|
selectedDate: today,
|
||||||
completedTodayCount: 0,
|
dayTasks: const [],
|
||||||
totalTodayCount: 0,
|
overdueTasks: const [],
|
||||||
|
totalTaskCount: 0,
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
@@ -53,7 +57,8 @@ void main() {
|
|||||||
testWidgets('renders 3 navigation destinations with correct German labels',
|
testWidgets('renders 3 navigation destinations with correct German labels',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
await tester.pumpWidget(buildApp());
|
await tester.pumpWidget(buildApp());
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
// Verify 3 NavigationDestination widgets are rendered
|
// Verify 3 NavigationDestination widgets are rendered
|
||||||
expect(find.byType(NavigationDestination), findsNWidgets(3));
|
expect(find.byType(NavigationDestination), findsNWidgets(3));
|
||||||
@@ -67,22 +72,24 @@ void main() {
|
|||||||
testWidgets('tapping a destination changes the selected tab',
|
testWidgets('tapping a destination changes the selected tab',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
await tester.pumpWidget(buildApp());
|
await tester.pumpWidget(buildApp());
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
// Initially on Home tab (index 0) -- verify home empty state is shown
|
// Initially on Home tab (index 0) -- verify home first-run empty state
|
||||||
// (dailyPlanNoTasks text from the daily plan empty state)
|
|
||||||
expect(find.text('Noch keine Aufgaben angelegt'), findsOneWidget);
|
expect(find.text('Noch keine Aufgaben angelegt'), findsOneWidget);
|
||||||
|
|
||||||
// Tap the Rooms tab (second destination)
|
// Tap the Rooms tab (second destination)
|
||||||
await tester.tap(find.text('R\u00e4ume'));
|
await tester.tap(find.text('R\u00e4ume'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
// Verify we see Rooms content now (empty state)
|
// Verify we see Rooms content now (empty state)
|
||||||
expect(find.text('Hier ist noch alles leer!'), findsOneWidget);
|
expect(find.text('Hier ist noch alles leer!'), findsOneWidget);
|
||||||
|
|
||||||
// Tap the Settings tab (third destination)
|
// Tap the Settings tab (third destination)
|
||||||
await tester.tap(find.text('Einstellungen'));
|
await tester.tap(find.text('Einstellungen'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
// Verify we see Settings content now
|
// Verify we see Settings content now
|
||||||
expect(find.text('Darstellung'), findsOneWidget);
|
expect(find.text('Darstellung'), findsOneWidget);
|
||||||
|
|||||||
Reference in New Issue
Block a user