Skip to content

Add local reminder controls#2

Open
SloppyBobbert wants to merge 3 commits into
mainfrom
reminders-crud
Open

Add local reminder controls#2
SloppyBobbert wants to merge 3 commits into
mainfrom
reminders-crud

Conversation

@SloppyBobbert

@SloppyBobbert SloppyBobbert commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Adds basic local reminder management to the Flutter app.

This builds on the app-basics work by making the Reminders tab usable without notifications, BLE, or cloud sync.

Changes:

  • Adds local reminder add, edit, delete, and enable/disable controls.
  • Keeps reminder schedules stored locally through the existing Drift repository.
  • Adds basic validation for blank labels and invalid times.
  • Adds a widget test for the reminder add/edit/toggle/delete flow.
  • Marks the app-owned database close call as intentionally unawaited during widget disposal.
  • Updates README and mobile docs for the reminder controls and mobile test expectations.

Verified from mobile_app/dosey_app/:

  • dart format .
  • flutter analyze
  • flutter test
  • flutter build apk --debug
  • flutter build ios --debug --no-codesign
  • flutter doctor -v

Summary by CodeRabbit

  • New Features

    • Added comprehensive local reminder management. Users can now create, edit, delete, and control the enabled/disabled state of individual reminders directly within the app.
  • Documentation

    • Updated project documentation to reflect current reminder management capabilities, local app functionality, development tooling, and overall project status.
  • Tests

    • Added widget tests for reminder management workflows, including creation, editing, deletion, and toggle operations.

Copilot AI review requested due to automatic review settings June 10, 2026 19:48
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements complete local reminder management for the Flutter app, adding the ability to create, edit, delete, and toggle enabled/disabled state for reminders. The core ReminderSchedule model gains a copyWith helper, RemindersScreen is refactored from a read-only list into an interactive CRUD interface with modal forms, and comprehensive widget tests validate the full user flow.

Changes

Local Reminder CRUD Management

Layer / File(s) Summary
ReminderSchedule copyWith method
mobile_app/dosey_app/lib/core/reminders/reminder_schedule.dart
Adds copyWith(...) method to enable selective field updates when modifying reminder instances.
Reminders screen CRUD implementation
mobile_app/dosey_app/lib/features/reminders/reminders_screen.dart
Transforms RemindersScreen from static display into full reminder management: header with "Add reminder" button, _ReminderTile with enable/disable switch and edit/delete buttons, _showReminderSheet modal opener, and _ReminderSheet form widget with input validation (label non-empty, hour 0–23, minute 0–59), persistence via upsertSchedule, exception handling, and context.mounted checks.
Reminder management widget test
mobile_app/dosey_app/test/widget_test.dart
Comprehensive end-to-end test exercising empty-state, add-via-modal, save, toggle enabled/disabled, edit, delete, and return to empty-state workflows with assertions on label formatting, switch state, and UI visibility.
Documentation and consistency updates
README.md, docs/decisions.md, docs/mobile_stack.md, docs/test_plan.md, mobile_app/README.md, mobile_app/dosey_app/README.md, mobile_app/dosey_app/lib/app/dosey_app_scope.dart
README and architecture docs updated to enumerate local reminder add/edit/delete controls and enabled/disabled state; test plan adds "Current mobile checks" phase with widget test expectations; architectural decision documented for keeping reminder logic local until notification scheduling wires through app-owned interface; database disposal now wraps close() with unawaited() for consistency.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A flutter of buttons and forms now bloom,
Add, edit, toggle through each reminder's room,
Modal sheets dance with validation so bright,
Local persistence keeps the schedules just right—
Test cases bound through the UI's delight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Add local reminder controls' is fully related to and clearly summarizes the main change in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch reminders-crud

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds basic local reminder management to the Flutter app so the Reminders tab is usable without notifications/BLE/cloud sync, while keeping schedules persisted in the existing Drift repository.

Changes:

  • Implemented local reminder add/edit/delete and enable/disable UI in the Reminders tab (bottom sheet + list tiles).
  • Added ReminderSchedule.copyWith to support updates (e.g., toggling enabled state).
  • Added a widget test covering the reminder add/edit/toggle/delete flow and updated docs/READMEs to reflect the new reminder controls and mobile test checklist.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
README.md Updates top-level project summary to mention local reminder controls.
mobile_app/README.md Notes local reminder add/edit/delete + enabled state in the mobile overview.
mobile_app/dosey_app/README.md Expands app README to reflect local reminder controls and enabled state.
mobile_app/dosey_app/lib/features/reminders/reminders_screen.dart Adds reminder list tiles, bottom-sheet editor, enable/disable toggle, and delete flow.
mobile_app/dosey_app/lib/core/reminders/reminder_schedule.dart Adds copyWith for schedule updates.
mobile_app/dosey_app/lib/app/dosey_app_scope.dart Marks owned database close as intentionally unawaited during disposal.
mobile_app/dosey_app/test/widget_test.dart Adds widget test for reminder add/edit/toggle/delete flow using an in-memory database.
docs/test_plan.md Adds a “Current mobile checks” section with expected Flutter commands and widget-test coverage.
docs/mobile_stack.md Updates mobile stack doc to mention local reminder controls.
docs/decisions.md Records decision to keep reminder CRUD + enabled state local until notifications are wired behind the reminder interface.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

await reminders.upsertSchedule(
schedule.copyWith(isEnabled: value, updatedAt: DateTime.now().toUtc()),
);
} catch (error) {
Future<void> _delete(BuildContext context) async {
try {
await reminders.deleteSchedule(schedule.id);
} catch (error) {

try {
await widget.reminders.upsertSchedule(schedule);
} catch (error) {

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mobile_app/dosey_app/lib/features/reminders/reminders_screen.dart`:
- Around line 255-257: The Save button can trigger multiple concurrent _save()
calls and generate duplicate IDs; add a boolean flag (e.g., isSaving) in the
reminders screen state, have _save() immediately check and return if isSaving is
true, set isSaving = true at the start of _save() and reset it in a finally
block, and update the FilledButton's onPressed to be null (disabled) when
isSaving is true so repeated taps are ignored; ensure the ID generation in
create mode only runs when entering _save() once (after the isSaving guard) so
duplicates cannot be created.

In `@mobile_app/dosey_app/test/widget_test.dart`:
- Around line 77-127: Extend the existing testWidgets 'reminders tab adds edits
toggles and deletes reminders' to cover validation branches: after tapping 'Add
reminder' and populating fields, add a negative case where you clear the 'Label'
TextFormField, tap 'Save reminder', and assert a validation error is shown for
the label and that no reminder was saved (still findsOneWidget 'No reminders
yet.' or absence of the label); similarly add a case where you enter invalid
time values into the 'Hour'/'Minute' TextFormField(s) (e.g., '24' or '60'), tap
'Save reminder', and assert a time validation error is displayed and the
reminder is not created. Use the existing helpers/finders (TextFormField with
text 'Label' / 'Hour' / 'Minute', buttons 'Add reminder' / 'Save reminder', and
the page messages like 'No reminders yet.') to locate UI elements.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 81f23a36-7819-4727-ac01-f81ca801c770

📥 Commits

Reviewing files that changed from the base of the PR and between 0c23be9 and 5bbc4f2.

📒 Files selected for processing (10)
  • README.md
  • docs/decisions.md
  • docs/mobile_stack.md
  • docs/test_plan.md
  • mobile_app/README.md
  • mobile_app/dosey_app/README.md
  • mobile_app/dosey_app/lib/app/dosey_app_scope.dart
  • mobile_app/dosey_app/lib/core/reminders/reminder_schedule.dart
  • mobile_app/dosey_app/lib/features/reminders/reminders_screen.dart
  • mobile_app/dosey_app/test/widget_test.dart

Comment on lines +255 to +257
FilledButton(
onPressed: _save,
child: const Text('Save reminder'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent duplicate reminder creation from repeated Save taps.

_save is async, but the Save button remains enabled. Rapid taps can invoke _save more than once before dismissal, and Line 287 generates a new ID per invocation in create mode, causing duplicate persisted reminders.

Proposed fix
 class _ReminderSheetState extends State<_ReminderSheet> {
   late final TextEditingController _labelController;
   late final TextEditingController _hourController;
   late final TextEditingController _minuteController;
   late bool _isEnabled;
+  bool _isSaving = false;
   String? _errorText;
@@
               FilledButton(
-                onPressed: _save,
+                onPressed: _isSaving ? null : _save,
                 child: const Text('Save reminder'),
               ),
@@
   Future<void> _save() async {
+    if (_isSaving) return;
+    setState(() {
+      _isSaving = true;
+      _errorText = null;
+    });
+
     final label = _labelController.text.trim();
     final hour = int.tryParse(_hourController.text.trim());
     final minute = int.tryParse(_minuteController.text.trim());
@@
     if (label.isEmpty) {
-      setState(() => _errorText = 'Enter a reminder label.');
+      setState(() {
+        _errorText = 'Enter a reminder label.';
+        _isSaving = false;
+      });
       return;
     }
     if (hour == null || hour < 0 || hour > 23) {
-      setState(() => _errorText = 'Hour must be 0 through 23.');
+      setState(() {
+        _errorText = 'Hour must be 0 through 23.';
+        _isSaving = false;
+      });
       return;
     }
     if (minute == null || minute < 0 || minute > 59) {
-      setState(() => _errorText = 'Minute must be 0 through 59.');
+      setState(() {
+        _errorText = 'Minute must be 0 through 59.';
+        _isSaving = false;
+      });
       return;
     }
@@
     try {
       await widget.reminders.upsertSchedule(schedule);
     } catch (error) {
       if (!mounted) return;
-      setState(() => _errorText = 'Reminder save failed: $error');
+      setState(() {
+        _errorText = 'Reminder save failed: $error';
+        _isSaving = false;
+      });
       return;
     }
 
     if (!mounted) return;
+    setState(() => _isSaving = false);
     Navigator.of(context).pop();
   }
 }

Also applies to: 266-306

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile_app/dosey_app/lib/features/reminders/reminders_screen.dart` around
lines 255 - 257, The Save button can trigger multiple concurrent _save() calls
and generate duplicate IDs; add a boolean flag (e.g., isSaving) in the reminders
screen state, have _save() immediately check and return if isSaving is true, set
isSaving = true at the start of _save() and reset it in a finally block, and
update the FilledButton's onPressed to be null (disabled) when isSaving is true
so repeated taps are ignored; ensure the ID generation in create mode only runs
when entering _save() once (after the isSaving guard) so duplicates cannot be
created.

Comment on lines +77 to +127
testWidgets('reminders tab adds edits toggles and deletes reminders', (
WidgetTester tester,
) async {
final database = DoseyDatabase.inMemory();
addTearDown(database.close);

await tester.pumpWidget(DoseyApp(database: database));
await tester.tap(find.text('Reminders'));
await tester.pumpAndSettle();

expect(find.text('No reminders yet.'), findsOneWidget);
expect(find.text('Add reminder'), findsOneWidget);

await tester.tap(find.text('Add reminder'));
await tester.pumpAndSettle();
await tester.enterText(
find.widgetWithText(TextFormField, 'Label'),
'Vitamin D',
);
await tester.enterText(find.widgetWithText(TextFormField, 'Hour'), '8');
await tester.enterText(find.widgetWithText(TextFormField, 'Minute'), '30');
await tester.tap(find.text('Save reminder'));
await tester.pumpAndSettle();

expect(find.text('Vitamin D'), findsOneWidget);
expect(find.text('08:30'), findsOneWidget);
expect(tester.widget<Switch>(find.byType(Switch)).value, isTrue);

await tester.tap(find.byType(Switch));
await tester.pumpAndSettle();

expect(tester.widget<Switch>(find.byType(Switch)).value, isFalse);

await tester.tap(find.byTooltip('Edit reminder'));
await tester.pumpAndSettle();
await tester.enterText(
find.widgetWithText(TextFormField, 'Label'),
'Morning vitamin',
);
await tester.tap(find.text('Save reminder'));
await tester.pumpAndSettle();

expect(find.text('Vitamin D'), findsNothing);
expect(find.text('Morning vitamin'), findsOneWidget);

await tester.tap(find.byTooltip('Delete reminder'));
await tester.pumpAndSettle();

expect(find.text('Morning vitamin'), findsNothing);
expect(find.text('No reminders yet.'), findsOneWidget);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a validation-path assertion to this reminder flow test.

This test fully covers the happy path but misses the newly added validation branches (blank label / invalid time), so regressions there can slip through.

Minimal extension example
   testWidgets('reminders tab adds edits toggles and deletes reminders', (
     WidgetTester tester,
   ) async {
@@
     await tester.tap(find.text('Add reminder'));
     await tester.pumpAndSettle();
+
+    await tester.tap(find.text('Save reminder'));
+    await tester.pumpAndSettle();
+    expect(find.text('Enter a reminder label.'), findsOneWidget);
+
     await tester.enterText(
       find.widgetWithText(TextFormField, 'Label'),
       'Vitamin D',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile_app/dosey_app/test/widget_test.dart` around lines 77 - 127, Extend the
existing testWidgets 'reminders tab adds edits toggles and deletes reminders' to
cover validation branches: after tapping 'Add reminder' and populating fields,
add a negative case where you clear the 'Label' TextFormField, tap 'Save
reminder', and assert a validation error is shown for the label and that no
reminder was saved (still findsOneWidget 'No reminders yet.' or absence of the
label); similarly add a case where you enter invalid time values into the
'Hour'/'Minute' TextFormField(s) (e.g., '24' or '60'), tap 'Save reminder', and
assert a time validation error is displayed and the reminder is not created. Use
the existing helpers/finders (TextFormField with text 'Label' / 'Hour' /
'Minute', buttons 'Add reminder' / 'Save reminder', and the page messages like
'No reminders yet.') to locate UI elements.

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