Add side navigation drawer and maintenance tracking system#10
Add side navigation drawer and maintenance tracking system#10LucaGerlich merged 3 commits intomainfrom
Conversation
Co-authored-by: LucaGerlich <39236248+LucaGerlich@users.noreply.github.com>
Co-authored-by: LucaGerlich <39236248+LucaGerlich@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a maintenance tracking system to help users monitor and schedule equipment maintenance based on shot count, elapsed time, or estimated water usage. It replaces the decorative coffee icon with a functional menu button that opens a navigation drawer containing "Bean Vault" and "Maintenance" entries.
Changes:
- Added maintenance tracking feature with support for multiple interval types (shots, days, water liters) and task types (clean, decalcify, check machine/grinder)
- Implemented navigation drawer to access Bean Vault and Maintenance screens
- Extended data model with MaintenanceTask class and related enums, including full JSON serialization support
- Enhanced CoffeeProvider with CRUD operations for maintenance tasks, total shot counting, water usage estimation, and import/export functionality
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/screens/maintenance_screen.dart | New screen implementing the maintenance UI with task list, progress tracking, and dialogs for creating/managing tasks |
| lib/screens/bean_list_screen.dart | Replaced decorative coffee icon with menu button and added navigation drawer |
| lib/providers/coffee_provider.dart | Added maintenance task state management, CRUD operations, shot/water calculation helpers, and extended import/export |
| lib/models/models.dart | Added MaintenanceTask model with enums for task types and interval types, including JSON serialization |
| README.md | Updated features list to mention maintenance tracking |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| int shotsSince = 0; | ||
| for (final bean in provider.beans) { | ||
| for (final shot in bean.shots) { | ||
| if (shot.timestamp.isAfter(task.lastCompleted!)) { | ||
| shotsSince++; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The logic for counting shots since last completion is duplicated across multiple methods. The same nested loop pattern appears in lines 214-221, 230-237, 260-266, and 274-281. This duplicated code reduces maintainability and increases the risk of bugs if the logic needs to be updated. Consider extracting this logic into a helper method in the CoffeeProvider class, such as getShotCountSince(DateTime timestamp), which can then be called from these methods.
| MaintenanceTask copyWith({ | ||
| String? name, | ||
| MaintenanceType? type, | ||
| MaintenanceIntervalType? intervalType, | ||
| int? intervalValue, | ||
| DateTime? lastCompleted, | ||
| bool? isEnabled, | ||
| }) { | ||
| return MaintenanceTask( | ||
| id: id, | ||
| name: name ?? this.name, | ||
| type: type ?? this.type, | ||
| intervalType: intervalType ?? this.intervalType, | ||
| intervalValue: intervalValue ?? this.intervalValue, | ||
| lastCompleted: lastCompleted ?? this.lastCompleted, | ||
| isEnabled: isEnabled ?? this.isEnabled, | ||
| ); |
There was a problem hiding this comment.
The copyWith method doesn't properly handle nullable DateTime fields. When setting lastCompleted to null explicitly (to clear it), the method will fall back to the original value due to the null-coalescing operator. This prevents clearing the lastCompleted field once it has been set. Consider using a pattern that distinguishes between "not provided" and "explicitly set to null", or document that this field cannot be cleared once set.
| final interval = int.tryParse(_intervalController.text); | ||
| if (interval == null || interval <= 0) { | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| const SnackBar(content: Text('Please enter a valid interval')), |
There was a problem hiding this comment.
The error message "Please enter a valid interval" is generic and doesn't clearly explain that the interval must be a positive number (greater than zero). Consider making this message more specific, such as "Please enter a positive number for the interval" to better guide users.
| const SnackBar(content: Text('Please enter a valid interval')), | |
| const SnackBar(content: Text('Please enter a positive number for the interval')), |
| int shotsSince = 0; | ||
| for (final bean in provider.beans) { | ||
| for (final shot in bean.shots) { | ||
| if (shot.timestamp.isAfter(task.lastCompleted!)) { | ||
| shotsSince++; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This code duplicates the shot counting logic from lines 214-221. Both locations iterate through all beans and shots to count occurrences after a timestamp. This should be extracted into a shared helper method to eliminate duplication and improve maintainability.
| int shotsSince = 0; | ||
| for (final bean in provider.beans) { | ||
| for (final shot in bean.shots) { | ||
| if (shot.timestamp.isAfter(task.lastCompleted!)) { | ||
| shotsSince++; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This code duplicates the shot counting logic used elsewhere in this file (lines 214-221 and 230-237). The same nested iteration pattern is repeated multiple times. Extract this into a shared helper method to reduce code duplication.
| int shotsSince = 0; | ||
| for (final bean in provider.beans) { | ||
| for (final shot in bean.shots) { | ||
| if (shot.timestamp.isAfter(task.lastCompleted!)) { | ||
| shotsSince++; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This code duplicates the shot counting logic used elsewhere in this file (lines 214-221, 230-237, and 259-266). The same nested iteration pattern is repeated multiple times. Extract this into a shared helper method to reduce code duplication.
| (e) => e.name == json['intervalType'], | ||
| orElse: () => MaintenanceIntervalType.shots, | ||
| ), | ||
| intervalValue: json['intervalValue'], |
There was a problem hiding this comment.
The fromJson method doesn't validate that intervalValue is greater than zero. If a malformed or manually edited JSON file contains an intervalValue of 0 or negative, this could cause division by zero errors when calculating progress in _calculateProgress. Add validation to ensure intervalValue is positive, similar to the validation in the add task dialog.
| lastCompleted: json['lastCompleted'] != null | ||
| ? DateTime.parse(json['lastCompleted']) |
There was a problem hiding this comment.
DateTime.parse can throw a FormatException if the JSON contains malformed datetime strings. Consider wrapping this in a try-catch block or using a helper function that returns null on parse errors to prevent crashes when importing data with corrupted timestamps.
| if (task.lastCompleted == null) { | ||
| switch (task.intervalType) { | ||
| case MaintenanceIntervalType.shots: | ||
| final totalShots = provider.getTotalShotCount(); | ||
| return '$totalShots / ${task.intervalValue} shots'; | ||
| case MaintenanceIntervalType.days: | ||
| return 'Never completed'; | ||
| case MaintenanceIntervalType.waterLiters: | ||
| final totalWater = provider.getTotalWaterUsage(); | ||
| return '${totalWater.toStringAsFixed(1)} / ${task.intervalValue} L'; | ||
| } | ||
| } | ||
|
|
||
| switch (task.intervalType) { | ||
| case MaintenanceIntervalType.shots: | ||
| int shotsSince = 0; | ||
| for (final bean in provider.beans) { | ||
| for (final shot in bean.shots) { | ||
| if (shot.timestamp.isAfter(task.lastCompleted!)) { | ||
| shotsSince++; | ||
| } | ||
| } | ||
| } | ||
| return '$shotsSince / ${task.intervalValue} shots'; | ||
|
|
||
| case MaintenanceIntervalType.days: | ||
| final daysSince = DateTime.now().difference(task.lastCompleted!).inDays; | ||
| return '$daysSince / ${task.intervalValue} days'; | ||
|
|
||
| case MaintenanceIntervalType.waterLiters: | ||
| int shotsSince = 0; | ||
| for (final bean in provider.beans) { | ||
| for (final shot in bean.shots) { | ||
| if (shot.timestamp.isAfter(task.lastCompleted!)) { | ||
| shotsSince++; | ||
| } | ||
| } | ||
| } | ||
| final waterSince = shotsSince * 0.06; | ||
| return '${waterSince.toStringAsFixed(1)} / ${task.intervalValue} L'; |
There was a problem hiding this comment.
The progress calculation iterates through all beans and shots every time this widget builds, which could be expensive if there are many shots. For shot-based and water-based intervals, this nested iteration happens in the build method. Consider caching this calculation or moving the iteration to the provider level with memoization to improve performance, especially for users with large shot histories.
| if (task.lastCompleted == null) { | |
| switch (task.intervalType) { | |
| case MaintenanceIntervalType.shots: | |
| final totalShots = provider.getTotalShotCount(); | |
| return '$totalShots / ${task.intervalValue} shots'; | |
| case MaintenanceIntervalType.days: | |
| return 'Never completed'; | |
| case MaintenanceIntervalType.waterLiters: | |
| final totalWater = provider.getTotalWaterUsage(); | |
| return '${totalWater.toStringAsFixed(1)} / ${task.intervalValue} L'; | |
| } | |
| } | |
| switch (task.intervalType) { | |
| case MaintenanceIntervalType.shots: | |
| int shotsSince = 0; | |
| for (final bean in provider.beans) { | |
| for (final shot in bean.shots) { | |
| if (shot.timestamp.isAfter(task.lastCompleted!)) { | |
| shotsSince++; | |
| } | |
| } | |
| } | |
| return '$shotsSince / ${task.intervalValue} shots'; | |
| case MaintenanceIntervalType.days: | |
| final daysSince = DateTime.now().difference(task.lastCompleted!).inDays; | |
| return '$daysSince / ${task.intervalValue} days'; | |
| case MaintenanceIntervalType.waterLiters: | |
| int shotsSince = 0; | |
| for (final bean in provider.beans) { | |
| for (final shot in bean.shots) { | |
| if (shot.timestamp.isAfter(task.lastCompleted!)) { | |
| shotsSince++; | |
| } | |
| } | |
| } | |
| final waterSince = shotsSince * 0.06; | |
| return '${waterSince.toStringAsFixed(1)} / ${task.intervalValue} L'; | |
| final progress = _calculateProgress(provider); | |
| if (task.lastCompleted == null) { | |
| switch (task.intervalType) { | |
| case MaintenanceIntervalType.shots: | |
| final usedShots = (progress * task.intervalValue).round(); | |
| return '$usedShots / ${task.intervalValue} shots'; | |
| case MaintenanceIntervalType.days: | |
| return 'Never completed'; | |
| case MaintenanceIntervalType.waterLiters: | |
| final usedWaterLiters = progress * task.intervalValue; | |
| return '${usedWaterLiters.toStringAsFixed(1)} / ${task.intervalValue} L'; | |
| } | |
| } | |
| switch (task.intervalType) { | |
| case MaintenanceIntervalType.shots: | |
| final usedShots = (progress * task.intervalValue).round(); | |
| return '$usedShots / ${task.intervalValue} shots'; | |
| case MaintenanceIntervalType.days: | |
| final daysSince = DateTime.now().difference(task.lastCompleted!).inDays; | |
| return '$daysSince / ${task.intervalValue} days'; | |
| case MaintenanceIntervalType.waterLiters: | |
| final usedWaterLiters = progress * task.intervalValue; | |
| return '${usedWaterLiters.toStringAsFixed(1)} / ${task.intervalValue} L'; |
Implements equipment maintenance tracking with automatic progress calculation based on shot count, days elapsed, or estimated water usage.
Changes
Navigation
BeanListScreenData Model (
lib/models/models.dart)State Management (
lib/providers/coffee_provider.dart)maintenanceTaskslist with CRUD operationsgetTotalShotCount()- aggregates shots across all beansgetTotalWaterUsage()- calculates based on 60ml per shotUI (
lib/screens/maintenance_screen.dart)Progress Calculation
lastCompletedtimestamplastCompletedExample: "Decalcify every 100L" triggers after ~1,667 shots.
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.