14 Commits

Author SHA1 Message Date
588f215078 chore(release): improve upload script with retries, directory check, and fallback to scp 2026-03-16 21:25:00 +01:00
68ba7c65ce feat(05-01): add CalendarDayState model, Riverpod providers, and l10n strings
- CalendarDayState: selectedDate, dayTasks, overdueTasks fields with isEmpty helper
- selectedDateProvider: NotifierProvider with SelectedDateNotifier, defaults to today
- calendarDayProvider: StreamProvider.autoDispose, overdue only when viewing today
- Add calendarTodayButton l10n string ("Heute") to ARB and generated dart files
2026-03-16 21:24:07 +01:00
c666f9a1c6 feat(05-01): implement CalendarDao with date-parameterized task queries
- CalendarDao.watchTasksForDate: returns tasks due on a specific calendar day, sorted by name
- CalendarDao.watchOverdueTasks: returns tasks due strictly before reference date, sorted by due date
- Registered CalendarDao in AppDatabase @DriftDatabase annotation
- Generated calendar_dao.g.dart and updated database.g.dart
2026-03-16 21:21:07 +01:00
f5c4b4928f test(05-01): add failing tests for CalendarDao
- watchTasksForDate: empty list, date filter, multi-room, alpha sort, no overdue carry-over
- watchOverdueTasks: empty list, before-date filter, excludes reference date, sorted by date
2026-03-16 21:19:55 +01:00
31d4ef879b docs(05-calendar-strip): create phase plan 2026-03-16 21:14:59 +01:00
fe7ba21061 chore(release): add rsync to release workflow dependencies 2026-03-16 20:58:25 +01:00
90ff66223c docs: create milestone v1.1 roadmap (3 phases) 2026-03-16 20:54:40 +01:00
b7a243603c docs: define milestone v1.1 requirements 2026-03-16 20:50:20 +01:00
fa26d6b301 chore(release): install fdroidserver via pip for compatibility with modern Flutter/AGP APKs 2026-03-16 20:45:09 +01:00
6bb1bc35d7 docs: start milestone v1.1 Calendar & Polish 2026-03-16 20:44:51 +01:00
ead085ad26 fix(android): move MainActivity to correct package to fix ClassNotFoundException
The namespace/applicationId is `de.jeanlucmakiola.household_keeper` but
MainActivity was in the old `com.jlmak.household_keeper` package, causing
a crash on launch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 20:39:08 +01:00
74a801c6f2 chore(release): enhance release workflow to safely handle APK naming with ref-based fallback 2026-03-16 20:30:40 +01:00
0059095e38 chore(release): make sudo usage optional in release workflow setup steps 2026-03-16 20:14:31 +01:00
8c72403c85 chore: archive v1.0 phase directories to milestones/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 20:12:01 +01:00
60 changed files with 1291 additions and 47 deletions

View File

@@ -107,8 +107,15 @@ jobs:
- name: Setup F-Droid Server Tools
run: |
sudo apt-get update
sudo apt-get install -y fdroidserver sshpass
SUDO=""
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 rsync python3-pip
pip3 install --break-system-packages --upgrade fdroidserver
- name: Initialize or fetch F-Droid Repository
env:
@@ -125,10 +132,17 @@ jobs:
- name: Copy new APK to repo
run: |
# The app-release.apk name should ideally include the version number
# so it doesn't overwrite older versions in the 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
set -e
mkdir -p fdroid/repo
# 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
run: |
@@ -141,5 +155,26 @@ jobs:
USER: ${{ secrets.HETZNER_USER }}
PASS: ${{ secrets.HETZNER_PASS }}
run: |
# Use rsync to efficiently upload only the changed files (the new APK and updated index files)
sshpass -p "$PASS" rsync -avz -e "ssh -o StrictHostKeyChecking=no" fdroid/repo/ $USER@$HOST:dev/fdroid/repo/
set -euo pipefail
REMOTE_REPO_DIR="dev/fdroid/repo"
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=20 -o ServerAliveInterval=30 -o ServerAliveCountMax=5"
# Ensure target directory exists before upload.
sshpass -p "$PASS" ssh $SSH_OPTS "$USER@$HOST" "mkdir -p '$REMOTE_REPO_DIR'"
if sshpass -p "$PASS" ssh $SSH_OPTS "$USER@$HOST" "command -v rsync >/dev/null 2>&1"; then
ATTEMPT=1
until [ "$ATTEMPT" -gt 3 ]; do
echo "Rsync upload attempt $ATTEMPT/3"
if sshpass -p "$PASS" rsync -avz --timeout=60 -e "ssh $SSH_OPTS" fdroid/repo/ "$USER@$HOST:$REMOTE_REPO_DIR/"; then
exit 0
fi
sleep $((ATTEMPT * 5))
ATTEMPT=$((ATTEMPT + 1))
done
echo "Rsync failed after retries, falling back to scp"
else
echo "Remote rsync not found, using scp fallback"
fi
sshpass -p "$PASS" scp $SSH_OPTS -r fdroid/repo/. "$USER@$HOST:$REMOTE_REPO_DIR/"

View File

@@ -8,6 +8,18 @@ A local-first Flutter app for organizing household chores, built for personal/co
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
### Validated
@@ -23,11 +35,13 @@ Users can see what needs doing today, mark it done, and trust the app to schedul
### Active
- [ ] Data export/import (JSON)
- [ ] English localization
- [ ] Room cover photos from camera or gallery
- [ ] Horizontal calendar strip home screen (replacing stacked daily plan)
- [ ] Overdue task carry-over with visual accent
- [ ] Task completion history log
- [ ] Additional task sorting (alphabetical, interval, effort)
- [ ] Data export/import (JSON) — deferred
- [ ] English localization — deferred
- [ ] Room cover photos from camera or gallery — deferred
### Out of Scope
@@ -79,4 +93,4 @@ Users can see what needs doing today, mark it done, and trust the app to schedul
| 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-16 after v1.0 milestone*
*Last updated: 2026-03-16 after v1.1 milestone started*

81
.planning/REQUIREMENTS.md Normal file
View File

@@ -0,0 +1,81 @@
# Requirements: HouseHoldKeaper
**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.
## v1.1 Requirements
Requirements for milestone v1.1 Calendar & Polish. Each maps to roadmap phases.
### Calendar UI
- [ ] **CAL-01**: User sees a horizontal scrollable date-strip with day abbreviation (Mo, Di...) and date number per card
- [ ] **CAL-02**: User can tap a day card to see that day's tasks in a list below the strip
- [ ] **CAL-03**: User sees a subtle color shift at month boundaries for visual orientation
- [ ] **CAL-04**: Calendar strip auto-scrolls to today on app launch
- [ ] **CAL-05**: Undone tasks carry over to the next day with a red/orange color accent marking them as overdue
### Task History
- [ ] **HIST-01**: Each task completion is recorded with a timestamp
- [ ] **HIST-02**: User can view past completion dates for any individual task
### Task Sorting
- [ ] **SORT-01**: User can sort tasks alphabetically
- [ ] **SORT-02**: User can sort tasks by frequency interval
- [ ] **SORT-03**: User can sort tasks by effort level
## Future Requirements
Deferred to future release. Tracked but not in current roadmap.
### Data
- **DATA-01**: User can export all data as JSON
- **DATA-02**: User can import data from JSON backup
### Localization
- **LOC-01**: User can switch UI language to English
### Rooms
- **ROOM-01**: User can set a cover photo for a room from camera or gallery
## Out of Scope
Explicitly excluded. Documented to prevent scope creep.
| Feature | Reason |
|---------|--------|
| Weekly/monthly calendar views | Overcomplicates UI — date strip is sufficient for task app |
| Drag tasks between days | Not needed — tasks auto-schedule based on frequency |
| Calendar sync (Google/Apple) | Contradicts local-first, offline-only design |
| Task statistics/charts | Deferred to v2.0 — history log is the foundation |
## Traceability
Which phases cover which requirements. Updated during roadmap creation.
| Requirement | Phase | Status |
|-------------|-------|--------|
| CAL-01 | Phase 5 | Pending |
| CAL-02 | Phase 5 | Pending |
| CAL-03 | Phase 5 | Pending |
| CAL-04 | Phase 5 | Pending |
| CAL-05 | Phase 5 | Pending |
| HIST-01 | Phase 6 | Pending |
| HIST-02 | Phase 6 | Pending |
| SORT-01 | Phase 7 | Pending |
| SORT-02 | Phase 7 | Pending |
| SORT-03 | Phase 7 | Pending |
**Coverage:**
- v1.1 requirements: 10 total
- Mapped to phases: 10
- Unmapped: 0
---
*Requirements defined: 2026-03-16*
*Last updated: 2026-03-16 after roadmap creation (phases 5-7)*

View File

@@ -3,6 +3,7 @@
## Milestones
- **v1.0 MVP** — Phases 1-4 (shipped 2026-03-16)
- **v1.1 Calendar & Polish** — Phases 5-7 (in progress)
## Phases
@@ -18,6 +19,50 @@ See `milestones/v1.0-ROADMAP.md` for full phase details.
</details>
**v1.1 Calendar & Polish (Phases 5-7):**
- [ ] **Phase 5: Calendar Strip** - Replace the stacked daily plan home screen with a horizontal scrollable date-strip and day-task list
- [ ] **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 5: Calendar Strip
**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**: Phase 4 (v1.0 shipped — all data layer and scheduling in place)
**Requirements**: CAL-01, CAL-02, CAL-03, CAL-04, CAL-05
**Success Criteria** (what must be TRUE):
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. Tapping any day card updates the task list below the strip to show that day's tasks, with the selected card visually highlighted
3. On app launch the strip auto-scrolls so today's card is centered and selected by default
4. When two adjacent day cards span a month boundary, a subtle color shift or divider makes the boundary visible without extra chrome
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:
- [ ] 05-01-PLAN.md — Data layer: CalendarDao, CalendarDayState model, Riverpod providers, localization, DAO tests
- [ ] 05-02-PLAN.md — UI: CalendarStrip, CalendarDayList, CalendarTaskRow widgets, HomeScreen replacement
### Phase 6: Task History
**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 5
**Requirements**: HIST-01, HIST-02
**Success Criteria** (what must be TRUE):
1. Every task completion (tap done in any view) is recorded in the database with a precise timestamp — data persists across app restarts
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. The history view shows a meaningful empty state if the task has never been completed
**Plans**: TBD
### Phase 7: Task Sorting
**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 5
**Requirements**: SORT-01, SORT-02, SORT-03
**Success Criteria** (what must be TRUE):
1. A sort control (dropdown, segmented button, or similar) is visible on task list screens and persists the chosen sort across app restarts
2. Selecting alphabetical sort orders tasks A-Z by name within the visible list
3. Selecting interval sort orders tasks from most-frequent (daily) to least-frequent (yearly/custom) intervals
4. Selecting effort sort orders tasks from lowest effort to highest effort level
**Plans**: TBD
## Progress
| Phase | Milestone | Plans Complete | Status | Completed |
@@ -26,3 +71,6 @@ See `milestones/v1.0-ROADMAP.md` for full phase details.
| 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 |
| 4. Notifications | v1.0 | 3/3 | Complete | 2026-03-16 |
| 5. Calendar Strip | v1.1 | 0/2 | Planned | - |
| 6. Task History | v1.1 | 0/? | Not started | - |
| 7. Task Sorting | v1.1 | 0/? | Not started | - |

View File

@@ -1,17 +1,17 @@
---
gsd_state_version: 1.0
milestone: v1.0
milestone_name: MVP
status: shipped
stopped_at: v1.0 milestone complete
last_updated: "2026-03-16T20:00:00.000Z"
last_activity: 2026-03-16 — v1.0 MVP milestone shipped
milestone: v1.1
milestone_name: Calendar & Polish
status: ready
stopped_at: Roadmap created — ready for Phase 5
last_updated: "2026-03-16T21:00:00.000Z"
last_activity: 2026-03-16 — Roadmap created for v1.1 (phases 5-7)
progress:
total_phases: 4
completed_phases: 4
total_plans: 13
completed_plans: 13
percent: 100
total_phases: 3
completed_phases: 0
total_plans: 0
completed_plans: 0
percent: 0
---
# Project State
@@ -21,37 +21,38 @@ progress:
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.
**Current focus:** v1.0 shipped — planning next milestone
**Current focus:** v1.1 Calendar & Polish — Phase 5: Calendar Strip
## Current Position
Milestone: v1.0 MVP — SHIPPED 2026-03-16
Status: All 4 phases complete, 13/13 plans executed, 89 tests passing
Next: `/gsd:new-milestone` for v1.1
Phase: 5 — Calendar Strip
Plan: Not started
Status: Ready to plan Phase 5
Last activity: 2026-03-16 — Roadmap for v1.1 written (phases 5-7)
Progress: [##########] 100%
```
Progress: [ ░░░░░░░░░░░░░░░░░░░░ ] 0% (0/3 phases)
```
## Performance Metrics
**Velocity:**
- Total plans completed: 13
- Total execution time: ~1.3 hours
- Average duration: ~6 min/plan
**By Phase:**
| Phase | Plans | Total | Avg/Plan |
|-------|-------|-------|----------|
| 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 |
| 4 - Notifications | 3 | 16 min | 5.3 min |
| Metric | v1.0 | v1.1 |
|--------|------|------|
| Phases | 4 | 3 planned |
| Plans | 13 | TBD |
| LOC (lib) | 7,773 | TBD |
| Tests | 89 | TBD |
## Accumulated Context
### Decisions
All v1.0 decisions are recorded in PROJECT.md Key Decisions table with outcomes.
| Decision | Rationale |
|----------|-----------|
| Calendar strip replaces daily plan home screen | v1.1 goal per PROJECT.md — not additive, the stacked overdue/today/upcoming sections are removed |
| Phase 5 before Phase 6 and 7 | Calendar strip is the primary UI surface; history and sorting operate within or alongside it |
| Phase 6 and 7 both depend on Phase 5 only | History and sorting are independent of each other — could execute in either order |
| 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 |
### Pending Todos
@@ -59,10 +60,12 @@ None.
### Blockers/Concerns
None — all v1.0 blockers resolved.
- The existing HomeScreen (daily plan with overdue/today/upcoming) will be replaced entirely in Phase 5. Verify no other screen references the daily plan provider before deleting it, or migrate references.
- CAL-05 (overdue carry-over with color accent) requires a query that returns tasks by their original due date relative to a selected day — confirm the existing DailyPlanDao can be adapted or a new CalendarDao is needed.
## Session Continuity
Last session: 2026-03-16
Stopped at: v1.0 milestone complete
Stopped at: Roadmap created, ready for Phase 5 planning
Resume file: None
Next action: `/gsd:plan-phase 5`

View 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>

View 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>

View File

@@ -1,4 +1,4 @@
package com.jlmak.household_keeper
package de.jeanlucmakiola.household_keeper
import io.flutter.embedding.android.FlutterActivity

View File

@@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.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/rooms/data/rooms_dao.dart';
import '../../features/tasks/data/tasks_dao.dart';
@@ -45,7 +46,7 @@ class TaskCompletions extends Table {
@DriftDatabase(
tables: [Rooms, Tasks, TaskCompletions],
daos: [RoomsDao, TasksDao, DailyPlanDao],
daos: [RoomsDao, TasksDao, DailyPlanDao, CalendarDao],
)
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor])

View File

@@ -1246,6 +1246,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
late final RoomsDao roomsDao = RoomsDao(this as AppDatabase);
late final TasksDao tasksDao = TasksDao(this as AppDatabase);
late final DailyPlanDao dailyPlanDao = DailyPlanDao(this as AppDatabase);
late final CalendarDao calendarDao = CalendarDao(this as AppDatabase);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();

View File

@@ -0,0 +1,77 @@
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();
});
}
/// 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();
});
}
}

View 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,
);
}

View File

@@ -0,0 +1,19 @@
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 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;
}

View File

@@ -0,0 +1,66 @@
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 [];
}
return CalendarDayState(
selectedDate: selectedDate,
dayTasks: dayTasks,
overdueTasks: overdueTasks,
);
});
});

View File

@@ -106,5 +106,6 @@
"count": { "type": "int" },
"overdue": { "type": "int" }
}
}
},
"calendarTodayButton": "Heute"
}

View File

@@ -513,6 +513,12 @@ abstract class AppLocalizations {
/// In de, this message translates to:
/// **'{count} Aufgaben fällig ({overdue} überfällig)'**
String notificationBodyWithOverdue(int count, int overdue);
/// No description provided for @calendarTodayButton.
///
/// In de, this message translates to:
/// **'Heute'**
String get calendarTodayButton;
}
class _AppLocalizationsDelegate

View File

@@ -236,4 +236,7 @@ class AppLocalizationsDe extends AppLocalizations {
String notificationBodyWithOverdue(int count, int overdue) {
return '$count Aufgaben fällig ($overdue überfällig)';
}
@override
String get calendarTodayButton => 'Heute';
}

View 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);
});
});
}