Skip to content

Add side navigation drawer and maintenance tracking system#10

Merged
LucaGerlich merged 3 commits intomainfrom
copilot/add-side-navigation-menu
Feb 6, 2026
Merged

Add side navigation drawer and maintenance tracking system#10
LucaGerlich merged 3 commits intomainfrom
copilot/add-side-navigation-menu

Conversation

Copy link
Contributor

Copilot AI commented Feb 5, 2026

Implements equipment maintenance tracking with automatic progress calculation based on shot count, days elapsed, or estimated water usage.

Changes

Navigation

  • Replaced decorative coffee icon with functional menu button in BeanListScreen
  • Added navigation drawer with Bean Vault and Maintenance entries

Data Model (lib/models/models.dart)

enum MaintenanceIntervalType { shots, days, waterLiters }

class MaintenanceTask {
  final String id;
  final MaintenanceType type;  // clean, decalcify, checkMachine, checkGrinder
  final MaintenanceIntervalType intervalType;
  final int intervalValue;
  final DateTime? lastCompleted;
  // ...
}

State Management (lib/providers/coffee_provider.dart)

  • Added maintenanceTasks list with CRUD operations
  • getTotalShotCount() - aggregates shots across all beans
  • getTotalWaterUsage() - calculates based on 60ml per shot
  • Extended export/import to include maintenance data

UI (lib/screens/maintenance_screen.dart)

  • Task list with progress bars showing completion percentage
  • Visual overdue indicators (red bars, badges) when progress ≥ 100%
  • Dialog for creating tasks with type and interval configuration
  • One-tap completion to reset counters

Progress Calculation

  • Shots: Counts shots after lastCompleted timestamp
  • Days: Computes days since lastCompleted
  • Water: Counts shots × 0.06L (standard espresso water usage)

Example: "Decalcify every 100L" triggers after ~1,667 shots.

Original prompt

Right now the top right corner has a coffee icon. Please make this icon a menu button and add a side navigation. The beans vault is one navigation point in the menu and also the first entry. The second Navigation point will be maintenance. Maintenance will be a completely new feature where the user can set reminders to clean, decalcify or check machine and grinder. Add notifications and let the user set cycles when maintenance is nessesary. Maintenance intervals can be based on shots(calculate the sum of all saved shots in the app), Days or estimated water useage. For example a machine should be decalcifyed every 100 liters of filtered water. Guess how many liters are going through a machine for an espresso shot and calculate the water usage based on the shots.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits February 5, 2026 00:40
Co-authored-by: LucaGerlich <39236248+LucaGerlich@users.noreply.github.com>
Co-authored-by: LucaGerlich <39236248+LucaGerlich@users.noreply.github.com>
Copilot AI changed the title [WIP] Add side navigation menu with maintenance feature Add side navigation drawer and maintenance tracking system Feb 5, 2026
Copilot AI requested a review from LucaGerlich February 5, 2026 00:45
@LucaGerlich LucaGerlich marked this pull request as ready for review February 5, 2026 00:59
Copilot AI review requested due to automatic review settings February 5, 2026 00:59
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +214 to +221
int shotsSince = 0;
for (final bean in provider.beans) {
for (final shot in bean.shots) {
if (shot.timestamp.isAfter(task.lastCompleted!)) {
shotsSince++;
}
}
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +83
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,
);
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
final interval = int.tryParse(_intervalController.text);
if (interval == null || interval <= 0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter a valid interval')),
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const SnackBar(content: Text('Please enter a valid interval')),
const SnackBar(content: Text('Please enter a positive number for the interval')),

Copilot uses AI. Check for mistakes.
Comment on lines +230 to +237
int shotsSince = 0;
for (final bean in provider.beans) {
for (final shot in bean.shots) {
if (shot.timestamp.isAfter(task.lastCompleted!)) {
shotsSince++;
}
}
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +259 to +266
int shotsSince = 0;
for (final bean in provider.beans) {
for (final shot in bean.shots) {
if (shot.timestamp.isAfter(task.lastCompleted!)) {
shotsSince++;
}
}
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +274 to +281
int shotsSince = 0;
for (final bean in provider.beans) {
for (final shot in bean.shots) {
if (shot.timestamp.isAfter(task.lastCompleted!)) {
shotsSince++;
}
}
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
(e) => e.name == json['intervalType'],
orElse: () => MaintenanceIntervalType.shots,
),
intervalValue: json['intervalValue'],
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +61
lastCompleted: json['lastCompleted'] != null
? DateTime.parse(json['lastCompleted'])
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +283
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';
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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';

Copilot uses AI. Check for mistakes.
@LucaGerlich LucaGerlich merged commit 66990a8 into main Feb 6, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants