Compare commits
14 Commits
v1.0
...
588f215078
| Author | SHA1 | Date | |
|---|---|---|---|
| 588f215078 | |||
| 68ba7c65ce | |||
| c666f9a1c6 | |||
| f5c4b4928f | |||
| 31d4ef879b | |||
| fe7ba21061 | |||
| 90ff66223c | |||
| b7a243603c | |||
| fa26d6b301 | |||
| 6bb1bc35d7 | |||
| ead085ad26 | |||
| 74a801c6f2 | |||
| 0059095e38 | |||
| 8c72403c85 |
@@ -107,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 rsync 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:
|
||||||
@@ -125,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: |
|
||||||
@@ -141,5 +155,26 @@ 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"
|
||||||
|
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/"
|
||||||
|
|||||||
@@ -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.
|
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
|
||||||
@@ -23,11 +35,13 @@ Users can see what needs doing today, mark it done, and trust the app to schedul
|
|||||||
|
|
||||||
### Active
|
### Active
|
||||||
|
|
||||||
- [ ] Data export/import (JSON)
|
- [ ] Horizontal calendar strip home screen (replacing stacked daily plan)
|
||||||
- [ ] English localization
|
- [ ] Overdue task carry-over with visual accent
|
||||||
- [ ] Room cover photos from camera or gallery
|
|
||||||
- [ ] Task completion history log
|
- [ ] Task completion history log
|
||||||
- [ ] Additional task sorting (alphabetical, interval, effort)
|
- [ ] 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
|
### 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 |
|
| 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
81
.planning/REQUIREMENTS.md
Normal 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)*
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
## Milestones
|
## Milestones
|
||||||
|
|
||||||
- **v1.0 MVP** — Phases 1-4 (shipped 2026-03-16)
|
- **v1.0 MVP** — Phases 1-4 (shipped 2026-03-16)
|
||||||
|
- **v1.1 Calendar & Polish** — Phases 5-7 (in progress)
|
||||||
|
|
||||||
## Phases
|
## Phases
|
||||||
|
|
||||||
@@ -18,6 +19,50 @@ See `milestones/v1.0-ROADMAP.md` for full phase details.
|
|||||||
|
|
||||||
</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
|
## Progress
|
||||||
|
|
||||||
| Phase | Milestone | Plans Complete | Status | Completed |
|
| 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 |
|
| 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 |
|
| 3. Daily Plan and Cleanliness | v1.0 | 3/3 | Complete | 2026-03-16 |
|
||||||
| 4. Notifications | 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 | - |
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
---
|
---
|
||||||
gsd_state_version: 1.0
|
gsd_state_version: 1.0
|
||||||
milestone: v1.0
|
milestone: v1.1
|
||||||
milestone_name: MVP
|
milestone_name: Calendar & Polish
|
||||||
status: shipped
|
status: ready
|
||||||
stopped_at: v1.0 milestone complete
|
stopped_at: Roadmap created — ready for Phase 5
|
||||||
last_updated: "2026-03-16T20:00:00.000Z"
|
last_updated: "2026-03-16T21:00:00.000Z"
|
||||||
last_activity: 2026-03-16 — v1.0 MVP milestone shipped
|
last_activity: 2026-03-16 — Roadmap created for v1.1 (phases 5-7)
|
||||||
progress:
|
progress:
|
||||||
total_phases: 4
|
total_phases: 3
|
||||||
completed_phases: 4
|
completed_phases: 0
|
||||||
total_plans: 13
|
total_plans: 0
|
||||||
completed_plans: 13
|
completed_plans: 0
|
||||||
percent: 100
|
percent: 0
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project State
|
# Project State
|
||||||
@@ -21,37 +21,38 @@ progress:
|
|||||||
See: .planning/PROJECT.md (updated 2026-03-16)
|
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:** v1.0 shipped — planning next milestone
|
**Current focus:** v1.1 Calendar & Polish — Phase 5: Calendar Strip
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v1.0 MVP — SHIPPED 2026-03-16
|
Phase: 5 — Calendar Strip
|
||||||
Status: All 4 phases complete, 13/13 plans executed, 89 tests passing
|
Plan: Not started
|
||||||
Next: `/gsd:new-milestone` for v1.1
|
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
|
## Performance Metrics
|
||||||
|
|
||||||
**Velocity:**
|
| Metric | v1.0 | v1.1 |
|
||||||
- Total plans completed: 13
|
|--------|------|------|
|
||||||
- Total execution time: ~1.3 hours
|
| Phases | 4 | 3 planned |
|
||||||
- Average duration: ~6 min/plan
|
| Plans | 13 | TBD |
|
||||||
|
| LOC (lib) | 7,773 | TBD |
|
||||||
**By Phase:**
|
| Tests | 89 | TBD |
|
||||||
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
### Decisions
|
### 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
|
### Pending Todos
|
||||||
|
|
||||||
@@ -59,10 +60,12 @@ None.
|
|||||||
|
|
||||||
### Blockers/Concerns
|
### 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
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-16
|
Last session: 2026-03-16
|
||||||
Stopped at: v1.0 milestone complete
|
Stopped at: Roadmap created, ready for Phase 5 planning
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
Next action: `/gsd:plan-phase 5`
|
||||||
|
|||||||
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>
|
||||||
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>
|
||||||
@@ -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?>>();
|
||||||
|
|||||||
77
lib/features/home/data/calendar_dao.dart
Normal file
77
lib/features/home/data/calendar_dao.dart
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
19
lib/features/home/domain/calendar_models.dart
Normal file
19
lib/features/home/domain/calendar_models.dart
Normal 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;
|
||||||
|
}
|
||||||
66
lib/features/home/presentation/calendar_providers.dart
Normal file
66
lib/features/home/presentation/calendar_providers.dart
Normal 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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user