Skip to content

Create task#41

Merged
tqha1011 merged 20 commits intomainfrom
create-task
Apr 18, 2026
Merged

Create task#41
tqha1011 merged 20 commits intomainfrom
create-task

Conversation

@Ender-Via
Copy link
Copy Markdown
Collaborator

@Ender-Via Ender-Via commented Apr 18, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Create tasks with category selection, date and time scheduling
    • Add and manage notes for individual tasks
    • Create and organize custom tags with validation
    • Filter and organize tasks by date
    • Edit and delete tasks with confirmation
    • Web app support with PWA manifest
  • Improvements

    • Enhanced error handling with user-friendly feedback
    • Updated dependencies for better performance

Ender-Via and others added 13 commits April 9, 2026 18:16
* feat(UserProfile): build screen UserProfile

# Conflicts:
#	src/lib/features/main/view/screens/main_screen.dart

* feat: switch dark/light theme

* fix: black color theme

* fix: black theme in statistics screen

* feat: add dark theme to auth screen

* feat: apply dark theme for bottom navigation bar
…tion version 1 (#31)

* feat(task): implement priority and tag selection features in task creation

* Update README.md

* Update README.md

* Update README.md

---------

Co-authored-by: Tran Quang Ha <[email protected]>
Removed commented section about main UI.
* feat(task): implement priority and tag selection features in task creation

* feat(tags): enhance tag management with custom tag creation and selection

* Update README.md

* Update README.md
* feat(core): add auth layout template, custom textfield and colors

* feat(auth): implement viewmodels for auth flow (MVVM)

* feat(auth): build complete auth UI screens (Login, Register, OTP, Passwords)

* chore(main): set LoginView as initial route

* refactor(auth) : delete .gitkeep

* chore: update dependencies and pubspec.lock

* refactor(auth): optimize registration logic, timezone handling, and form validation

* feat(auth): update UI for login, registration, and forgot password screens

* feat(tasks): update task management UI and statistics screen

* chore: update main entry point and fix widget tests

* chore: ignore devtools_options.yaml

* chore: ignore devtools_options.yaml

* style(login) : rewrite title for login view

* feat(auth): configure android deep link for supabase oauth

* refactor(ui): add social login callbacks to auth layout template

* feat(auth): update oauth methods with redirect url and signout

* feat(auth): implement AuthGate using StreamBuilder for session tracking

* feat(viewmodel): add oauth logic and improve provider lifecycle

* refactor(ui): migrate LoginView to Provider pattern

* chore(main): set AuthGate as initial route and setup provider

* feat: implement full Focus feature set

- Added Pomodoro timer with Start/Reset/Skip logic.
- Integrated local Quick Notes with Pin/Delete functionality.
- Supported image attachments in notes using image_picker.
- Added Focus settings: time duration, vibration, and ringtones.

* fix (auth) : dispose TextEditingControllers to prevent memory leaks

* refactor (alarm ) : create off alarm button  when time out

* fix: apply CodeRabbit auto-fixes

Fixed 3 file(s) based on 4 unresolved review comments.

Co-authored-by: CodeRabbit <[email protected]>

* fix(timer): prevent division by zero in progress calculation and sanitize negative settings input

* fix(timer): prevent division by zero in progress calculation and sanitize negative settings input

* fix(auth): unblock new-user login and add settings logout

* refactor(LoginScreen) : compact all items to fit in screen to help users interface easily

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <[email protected]>
#36)

Bumps [shared_preferences](https://github.com/flutter/packages/tree/main/packages/shared_preferences) from 2.5.4 to 2.5.5.
- [Commits](https://github.com/flutter/packages/commits/shared_preferences-v2.5.5/packages/shared_preferences)

---
updated-dependencies:
- dependency-name: shared_preferences
  dependency-version: 2.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat(UserProfile): build screen UserProfile

# Conflicts:
#	src/lib/features/main/view/screens/main_screen.dart

* feat: switch dark/light theme

* fix: black color theme

* fix: black theme in statistics screen

* feat: add dark theme to auth screen

* feat: apply dark theme for bottom navigation bar

* feat(RPC): update RPC to get data for heatmap

* feat(RPC): update new RPC to get data for heatmap

* feat: integrate chatbot assistant

* feat(chatbot): integrate create task, answer question for chatbot

* feat: remove mock data and get data tags and categories from supabase
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

This PR adds IDE configuration files (Visual Studio, VS Code), introduces a comprehensive task management feature set including task creation screens with state management via ChangeNotifier, task notes and custom tags support, enhanced task detail views with note management, and expands database operations through the TaskViewModel. Web platform assets and updated dependencies for calendar and AI features are also included.

Changes

Cohort / File(s) Summary
IDE & Editor Configuration
.vs/TaskManagement.slnx/v18/DocumentLayout.backup.json, .vs/TaskManagement.slnx/v18/DocumentLayout.json, .vs/VSWorkspaceState.json, .vscode/launch.json
Added Visual Studio solution layout state files, workspace configuration, and VS Code Dart/Flutter launch profiles for debugging in debug/profile/release modes.
Build & Source Exclusions
src/.gitignore
Added new .gitignore with patterns for build artifacts, logs, editor swap files, and Flutter/Dart/Android Studio caches.
Task Creation Features
src/lib/features/main/view/screens/create_task.dart, src/lib/features/tasks/view/screens/create_task_screen.dart
Introduced two task creation screens with CreateTaskProvider state management (form fields, validation, Supabase persistence). Validates task name, requires authenticated user, inserts tasks with category, date/time, and description into Supabase task table; displays snackbar feedback and navigates on success.
Task Data Models
src/lib/features/tasks/model/task_model.dart
Added NoteModel class with id, content, and pinned fields, including fromJson factory for Supabase deserialization.
Task UI & Widgets
src/lib/features/tasks/view/screens/task_detail_screen.dart, src/lib/features/tasks/view/widgets/task_widgets.dart
Enhanced TaskDetailScreen with note management (fetch/create notes via FutureBuilder), dynamic tags UI (toggleable chips), category selection, and task update/delete flows using TaskViewModel. Updated TaskCard to accept optional trailing widget for custom right-side content.
Task Business Logic
src/lib/features/tasks/viewmodel/task_viewmodel.dart
Expanded TaskViewModel with date-based filtering, custom tag management via SharedPreferences, and Supabase operations for task CRUD, note management (getNotesForTask, createNote), and tag updates.
App Configuration & Dependencies
src/lib/main.dart, src/pubspec.yaml
Added custom ErrorWidget.builder for global error UI in main. Updated shared_preferences (^2.2.2^2.5.5); added flutter_heatmap_calendar: ^1.0.5 and google_generative_ai: ^0.4.7 dependencies.
Web Platform Assets
src/web/index.html, src/web/manifest.json
Added Flutter web app entrypoint and PWA manifest declaring app metadata, icons (192x192 & 512x512 with maskable purpose), and launcher configuration.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as CreateTaskScreen
    participant Provider as CreateTaskProvider
    participant Supabase as Supabase API
    participant Feedback as SnackBar
    
    User->>UI: Fill form (name, category, date, time, description)
    User->>UI: Press "Confirm Task Creation"
    UI->>Provider: onCreateTaskPressed(context)
    
    Note over Provider: Validate task name (trimmed non-empty)
    Provider->>Provider: Check Supabase user authenticated
    Provider->>Provider: Set isLoading = true, notify listeners
    
    Provider->>Provider: Map category label to category_id
    Provider->>Provider: Combine date + start time
    
    Provider->>Supabase: INSERT into task table
    Supabase-->>Provider: Success/Error response
    
    alt Success
        Provider->>Feedback: Show "Task created successfully"
        Provider->>UI: Navigator.pop(context)
    else Failure
        Provider->>Feedback: Show error message
    end
    
    Provider->>Provider: Set isLoading = false, notify listeners
Loading
sequenceDiagram
    participant User as User
    participant UI as TaskDetailScreen
    participant ViewModel as TaskViewModel
    participant Supabase as Supabase API
    participant Dialog as AddNoteDialog
    participant Feedback as SnackBar
    
    User->>UI: View task details
    UI->>ViewModel: Load categories, tags, notes (post-frame)
    ViewModel->>Supabase: fetchTasks(), fetch categories/tags
    Supabase-->>ViewModel: Task data, categories, tags
    ViewModel-->>UI: Update state, notify listeners
    
    User->>UI: Click add note icon
    UI->>Dialog: Show _showAddNoteDialog
    User->>Dialog: Enter note content
    User->>Dialog: Confirm
    Dialog->>ViewModel: createNote(taskId, content)
    ViewModel->>Supabase: INSERT into note table
    Supabase-->>ViewModel: Success/Error
    
    ViewModel->>UI: Notify listeners
    UI->>ViewModel: getNotesForTask(taskId) [FutureBuilder]
    Supabase-->>ViewModel: Return notes list
    ViewModel-->>UI: Render notes
    
    User->>UI: Toggle tag chip
    UI->>ViewModel: updateTaskTags(taskId, newTags)
    ViewModel->>Supabase: UPDATE task table with new tags
    Supabase-->>ViewModel: Success/Error
    ViewModel-->>UI: Notify listeners
    UI->>Feedback: Show success/error message
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The changes introduce multiple new features across diverse files—two task creation flows with state management, note management with database operations, custom tag persistence, enhanced task detail views, and expanded TaskViewModel with date-based filtering and CRUD operations. The density of new logic, number of files affected, and multiple interacting components (Supabase, state providers, UI screens, view models) require careful review of validation flows, state synchronization, and database operation error handling.

Possibly related PRs

  • Implement priority and tag selection in task creation version 1 #31: Modifies task_model.dart and task_viewmodel.dart to add priority/tag models and selection state—overlapping changes in the same core task domain files.
  • Feature/user profile #37: Modifies task-related screens (create_task_screen.dart, task_detail_screen.dart) and TaskViewModel—directly related at the code level with overlapping task creation and management flows.
  • fix: error screen in main.dart #34: Modifies src/lib/main.dart to adjust app startup and error handling configuration—code-level dependency if both PRs alter global error handling or provider registration.

Poem

🐰 Hop along, dear tasks and notes so fine,
With screens that flutter and designs divine,
From Supabase queries to custom tags galore,
This project now bounces like never before! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title "Create task" is vague and generic. While task creation is implemented, the title lacks specificity about the scope—it doesn't indicate whether this is just the UI, includes database integration, adds notes/tags support, or encompasses the full feature. Use a more descriptive title like "Add task creation screen with category, date/time, and Supabase integration" or "Implement create task feature with UI and database persistence".
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 create-task

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/lib/features/auth/presentation/view/otp_verification_view.dart (1)

66-87: ⚠️ Potential issue | 🔴 Critical

Render eight OTP boxes to match the 8-digit flow.

Line 86 still creates only 6 inputs while the text and focus logic expect 8 digits, so users cannot complete an 8-digit OTP in this screen.

Suggested fix
-                    Row(
-                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                      children: List.generate(6, (index) => _buildOtpBox(index, context)),
-                    ),
+                    Row(
+                      children: List.generate(
+                        8,
+                        (index) => Expanded(
+                          child: Padding(
+                            padding: EdgeInsets.only(right: index == 7 ? 0 : 6),
+                            child: _buildOtpBox(index, context),
+                          ),
+                        ),
+                      ),
+                    ),
@@
-      width: 35, height: 48, // Thu nhỏ kích thước ô lại để nhét vừa 8 ô trên 1 dòng
+      width: double.infinity,
+      height: 48,

Also applies to: 180-182, 198-200

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/auth/presentation/view/otp_verification_view.dart` around
lines 66 - 87, The UI currently generates six OTP input boxes while the rest of
the screen (text and focus/logic) expects an 8-digit code; update every
List.generate(6, ...) that creates the OTP fields to List.generate(8, ...) so
_buildOtpBox(index, context) is called for eight boxes (also update any
hardcoded 6 occurrences around the _buildOtpBox usages at the other locations
you noted). If there is an OTP length constant or variable used elsewhere,
prefer using that (e.g., otpLength) instead of a magic number so all places stay
consistent.
src/lib/features/statistics/view/screens/statistics_screen.dart (1)

23-30: ⚠️ Potential issue | 🟡 Minor

Guard the deferred category load.

The post-frame callback can run after disposal, and categories.isEmpty still allows duplicate fetches while another screen is already loading categories.

Proposed fix
     WidgetsBinding.instance.addPostFrameCallback((_) {
+      if (!mounted) return;
       final userId = Supabase.instance.client.auth.currentUser?.id;
       if (userId != null) {
         context.read<StatisticsViewmodel>().getStatisticsData(userId);
         final categoryViewModel = context.read<CategoryViewModel>();
-        if (categoryViewModel.categories.isEmpty) {
+        if (categoryViewModel.categories.isEmpty &&
+            !categoryViewModel.isLoading) {
           categoryViewModel.loadCategories();
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/statistics/view/screens/statistics_screen.dart` around lines
23 - 30, Guard the post-frame callback against running after disposal and
against duplicate category fetches: inside the callback (the
WidgetsBinding.instance.addPostFrameCallback block) first check the widget is
still mounted (return early if not), then get the CategoryViewModel and only
call categoryViewModel.loadCategories() when
categoryViewModel.categories.isEmpty AND a loading flag is false (e.g.,
categoryViewModel.isLoading or add one like categoryViewModel.isFetching) to
prevent concurrent loads; keep the existing call to
context.read<StatisticsViewmodel>().getStatisticsData(userId) but ensure it also
runs only when mounted.
src/lib/features/statistics/view/widgets/statistics_widgets.dart (1)

319-343: ⚠️ Potential issue | 🟠 Major

Add category_id to RecentTaskModel and use actual task category in CompletedTaskCard.

RecentTaskModel currently omits category data. Every completed task displays categories.first, showing the wrong category for most tasks. Extend the RPC/statistics query to include category_id (or category object if available), then update RecentTaskModel to accept and store it. Use this value when mapping the task, falling back to the first category only if missing.

Fix TimeOfDay hour overflow when endTime is calculated.

Line 349 creates TimeOfDay(hour: task.updatedAt.hour + 1, ...). When a task's updatedAt is 23:xx, this produces an invalid TimeOfDay(hour: 24) which exceeds the valid range (0–23). Clamp or wrap the hour: hour: (task.updatedAt.hour + 1) % 24.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/statistics/view/widgets/statistics_widgets.dart` around
lines 319 - 343, RecentTaskModel is missing category data so CompletedTaskCard
always picks categoryViewModel.categories.first; extend the RPC/statistics query
to return category_id (or category object), add a category/categoryId field to
RecentTaskModel, populate it when deserializing, and update the mapping in
CompletedTaskCard where you build TaskModel (the mappedTask creation) to use
recentTask.category (or resolve its id to the correct CategoryModel) and only
fall back to fallbackCategory when that field is absent. Also fix the TimeOfDay
overflow where endTime is computed from task.updatedAt (currently
TimeOfDay(hour: task.updatedAt.hour + 1, ...)) by wrapping/clamping the hour,
e.g. use (task.updatedAt.hour + 1) % 24 (or min/max clamping) when constructing
TimeOfDay to ensure hour stays in 0–23.
♻️ Duplicate comments (2)
.vs/TaskManagement.slnx/v18/DocumentLayout.json (1)

3-68: ⚠️ Potential issue | 🟠 Major

Local path and activity metadata leaked via tracked IDE layout file.

This file persists machine-specific paths and timestamps (for example, Line 3 and Lines 41/54/67). It should not be committed. Please remove tracked .vs layout artifacts and keep .vs/ ignored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vs/TaskManagement.slnx/v18/DocumentLayout.json around lines 3 - 68, The
repo contains a tracked IDE layout JSON (DocumentLayout.json) that leaks
machine-specific keys like "WorkspaceRootPath" and timestamp fields
"WhenOpened"; remove that file from version control, add the IDE layout
directory/pattern to .gitignore (so future files aren’t tracked), and commit the
change; specifically run git rm --cached on the tracked DocumentLayout.json
entry (or equivalent) and commit the removal alongside updating .gitignore to
ignore the IDE layout files.
.vs/TaskManagement.slnx/v18/DocumentLayout.backup.json (1)

1-33: ⚠️ Potential issue | 🟠 Major

Same issue: local IDE backup layout should not be tracked.

This backup file is user-specific workspace metadata and includes absolute local paths (for example, Line 3). Please remove it from git and rely on .gitignore for .vs/.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vs/TaskManagement.slnx/v18/DocumentLayout.backup.json around lines 1 - 33,
This file contains user-specific IDE workspace metadata (e.g., the
"WorkspaceRootPath" entry) and should not be tracked; remove .vs/ layout backups
from the repo by deleting this file from Git tracking (use git rm --cached on
this DocumentLayout.backup.json or equivalent), add/ensure `.vs/` is listed in
.gitignore, and commit the change so future workspace files (including entries
like "WorkspaceRootPath") are not committed.
🟠 Major comments (24)
.vs/VSWorkspaceState.json-1-13 (1)

1-13: ⚠️ Potential issue | 🟠 Major

Do not commit .vs workspace state files.

This is local machine/editor state (for example, Line 11 selected node) and will create persistent merge noise across developers. Please remove this file from version control and ignore .vs/ in .gitignore.

Suggested cleanup
# .gitignore
+.vs/
- .vs/VSWorkspaceState.json
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vs/VSWorkspaceState.json around lines 1 - 13, The committed .vs workspace
state (VSWorkspaceState.json containing keys like "ExpandedNodes" and
"SelectedNode") is local editor state and must be removed from version control:
delete the tracked .vs/VSWorkspaceState.json from the repo, add the .vs/
directory (or .vs/) to .gitignore, remove the file from the git index (so it
stops being tracked) and commit the .gitignore and removal; ensure future
commits do not re-add VSWorkspaceState.json by verifying .gitignore contains the
.vs/ pattern.
src/web/index.html-24-24 (1)

24-24: ⚠️ Potential issue | 🟠 Major

Use apple-mobile-web-app-capable for iOS Safari/PWA support.

Line 24 currently uses mobile-web-app-capable, which is the Android/Chromium standard. The iOS-specific meta tag is apple-mobile-web-app-capable. Since this line is within the "iOS meta tags" section (alongside other apple-* prefixed tags), it should be corrected to ensure iOS home screen web apps function properly in standalone mode.

Suggested fix
-  <meta name="mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-capable" content="yes">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/index.html` at line 24, Replace the Android/Chromium meta tag name
"mobile-web-app-capable" with the iOS-specific name
"apple-mobile-web-app-capable" in the iOS meta tags section so the line that
currently sets mobile-web-app-capable to "yes" uses the apple-prefixed meta name
to enable proper iOS Safari/PWA standalone behavior.
src/lib/features/chatbot/model/chatmessage_model.dart-14-23 (1)

14-23: ⚠️ Potential issue | 🟠 Major

Make persisted chat history decoding tolerant of bad data.

jsonDecode(raw), Map<String, dynamic>.from(item), and json['isUser'] as bool? can throw for corrupted or older persisted data, which can break chat startup instead of falling back to an empty/partial history.

Suggested hardening
   factory ChatMessageModel.fromJson(Map<String, dynamic> json) {
     final parsedTimestamp =
         DateTime.tryParse(json['timestamp']?.toString() ?? '') ?? DateTime.now();
+    final rawIsUser = json['isUser'];

     return ChatMessageModel(
       text: json['text']?.toString() ?? '',
-      isUser: json['isUser'] as bool? ?? true,
+      isUser: rawIsUser is bool ? rawIsUser : true,
       timestamp: parsedTimestamp,
     );
   }
@@
   static List<ChatMessageModel> decodeList(String raw) {
-    final decoded = jsonDecode(raw);
-    if (decoded is! List) return [];
-
-    return decoded
-        .whereType<Map>()
-        .map((item) => ChatMessageModel.fromJson(Map<String, dynamic>.from(item)))
-        .toList();
+    try {
+      final decoded = jsonDecode(raw);
+      if (decoded is! List) return [];
+
+      final messages = <ChatMessageModel>[];
+      for (final item in decoded.whereType<Map>()) {
+        try {
+          messages.add(
+            ChatMessageModel.fromJson(Map<String, dynamic>.from(item)),
+          );
+        } catch (_) {
+          // Skip malformed entries instead of failing the whole history load.
+        }
+      }
+      return messages;
+    } catch (_) {
+      return [];
+    }
   }

Also applies to: 37-45

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/chatbot/model/chatmessage_model.dart` around lines 14 - 23,
The ChatMessageModel.fromJson should be made defensive so corrupted/old
persisted data doesn't throw: wrap parsing in a try/catch and fall back to sane
defaults, avoid using direct casts like json['isUser'] as bool? and Map<String,
dynamic>.from; instead tolerate null/malformed json by checking types and
coercing values (e.g., accept 'true'/'false' strings or 1/0 for isUser, parse
timestamp with DateTime.tryParse and default to DateTime.now() on failure), and
catch any exceptions to return a ChatMessageModel with safe defaults; apply the
same hardening to the corresponding alternate constructor/decoder referenced
around lines 37-45 (the other ChatMessageModel factory/decoder) so all persisted
history decoding is tolerant of bad data.
src/lib/features/category/view/widgets/category_choice_chips.dart-37-40 (1)

37-40: ⚠️ Potential issue | 🟠 Major

Choose selected label color based on chip background contrast.

Line 40 hardcodes white text for every selected category. Light category colors can make the selected chip label unreadable.

Suggested contrast-aware fix
           final category = categories[index];
           final adaptiveColor = category.color.toAdaptiveColor(context);
           final isSelected = category.id == selectedCategoryId;
+          final selectedLabelColor =
+              ThemeData.estimateBrightnessForColor(adaptiveColor) ==
+                      Brightness.dark
+                  ? Colors.white
+                  : Colors.black;
           return Padding(
@@
               selectedColor: adaptiveColor,
               labelStyle: TextStyle(
-                color: isSelected ? Colors.white : adaptiveColor,
+                color: isSelected ? selectedLabelColor : adaptiveColor,
                 fontSize: 14,
               ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/category/view/widgets/category_choice_chips.dart` around
lines 37 - 40, The selected label color is hardcoded to Colors.white which can
be unreadable on light selected chip backgrounds; update the label color logic
in the widget that builds the ChoiceChip (referenced by labelStyle,
backgroundColor, selectedColor, isSelected and adaptiveColor) to pick a
contrast-aware color when isSelected is true (e.g., use
adaptiveColor.computeLuminance() to choose Colors.white for dark backgrounds and
Colors.black for light backgrounds) while keeping the current adaptiveColor for
the unselected state.
src/lib/features/category/viewmodel/category_viewmodel.dart-22-38 (1)

22-38: ⚠️ Potential issue | 🟠 Major

Guard against stale overlapping category loads.

loadCategories() can be triggered from multiple screens. If two calls overlap, the older request can finish last and overwrite newer data/error state, and _isLoading can flip to false while another request is still running.

Suggested request-token guard
   String? _error;
   String? get error => _error;
+
+  int _loadRequestId = 0;

   Future<void> loadCategories() async {
+    final requestId = ++_loadRequestId;
     _isLoading = true;
     _error = null;
     notifyListeners();

     try {
       final data = await _repository.fetchCategories();
+      if (requestId != _loadRequestId) return;
       _categories
         ..clear()
         ..addAll(data);
     } catch (e) {
+      if (requestId != _loadRequestId) return;
       _error = e.toString();
       _categories.clear();
     } finally {
+      if (requestId != _loadRequestId) return;
       _isLoading = false;
       notifyListeners();
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/category/viewmodel/category_viewmodel.dart` around lines 22
- 38, loadCategories can suffer from race conditions when multiple overlapping
calls overwrite state; introduce a request token/id (e.g., _currentLoadId or
_loadToken) that you increment before calling _repository.fetchCategories(),
capture locally inside loadCategories(), and only apply results to _categories,
_error and set _isLoading=false if the captured token equals the latest token;
alternatively use an _activeLoadCount++/-- to keep _isLoading true while any
load remains active. Update loadCategories to increment the token (or active
count) before await, check the token after await (or decrement active count in
finally) and only mutate state when the token matches (or active count hits
zero), so older requests cannot overwrite newer results.
src/lib/features/main/view/screens/create_task.dart-72-81 (1)

72-81: ⚠️ Potential issue | 🟠 Major

Do not collect fields that are discarded on save.

The screen accepts description and endTime, but the insert only persists title/category/start timestamp. This silently loses user-entered task details; either persist these fields through the supported schema/RPC or remove the inputs until they are supported.

Also applies to: 140-145, 279-316

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/main/view/screens/create_task.dart` around lines 72 - 81,
The form collects description and endTime but the insert (via
_supabase.from('task').insert) only saves title/category/create_at, causing
silent data loss; either persist the extra fields by adding the correct column
names to the insert payload (e.g., include 'description': _description.trim()
and the proper timestamp column like 'end_at' or 'end_time':
endTime.toIso8601String() matching your DB/RPC schema) and update any RPC calls
used in _createTask/_saveTask methods, or remove/disable the description and
endTime inputs from the CreateTask screen so the UI matches what is actually
persisted (also apply the same fix to the other create/update blocks in this
file that handle task inserts/updates).
src/lib/features/main/view/screens/create_task.dart-54-61 (1)

54-61: ⚠️ Potential issue | 🟠 Major

Resolve categories from persisted data instead of hard-coded IDs.

Mapping display labels to fixed IDs can create tasks under the wrong category when database IDs differ between environments or users. Use the loaded category model/RPC result and submit the selected category’s actual ID.

Also applies to: 207-239

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/main/view/screens/create_task.dart` around lines 54 - 61,
The code currently maps labels to hard-coded IDs using categoryMapping and
assigns categoryId from _selectedCategory; instead, look up the persisted
Category model returned by your RPC/fetch (e.g., the list variable that holds
loaded categories such as fetchedCategories or categories) and find the matching
category by its display label (or unique key) and use that Category.id when
creating the task; replace the hard-coded categoryMapping and fallback logic so
creation uses the real persisted ID and add a safe fallback (e.g., use the first
category id or surface a validation error) if no match is found.
src/pubspec.yaml-47-47 (1)

47-47: ⚠️ Potential issue | 🟠 Major

Move the Gemini API key to a backend service instead of bundling it in the Flutter app.

The chatbot service loads GEMINI_API_KEY from the bundled .env asset file. Since Flutter assets are extractable from the app package, this exposes the API key to any user who inspects the app. Use a Supabase Edge Function or other backend proxy to handle Gemini API calls instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pubspec.yaml` at line 47, Remove GEMINI_API_KEY from the bundled .env
asset and stop initializing the chatbot service with a client-side Gemini key;
instead implement a backend proxy (e.g., a Supabase Edge Function) that stores
the Gemini API key securely and exposes an authenticated endpoint for chat
requests, update the client-side ChatbotService (or wherever GEMINI_API_KEY is
read) to call that backend endpoint (e.g., /edge-fn/gemini-chat) for all Gemini
interactions, and ensure build/config no longer includes google_generative_ai
credentials in assets or source so the key is never packaged with the Flutter
app.
src/lib/features/main/view/screens/main_screen.dart-38-42 (1)

38-42: ⚠️ Potential issue | 🟠 Major

useMockData: true is hard-coded in production initialization.

UserProfileViewModel(useMockData: true) is being wired into the live app shell at line 39. The constructor defaults to true, but here it is explicitly set, ensuring the profile tab always loads mock data (via _buildMockUser()) instead of real user data from UserService.fetchUserProfile(). This defeats the purpose of the UserService integration added in this PR.

Before merging, gate this behind a build flag (kDebugMode, environment variable, or --dart-define) so production builds use real data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/main/view/screens/main_screen.dart` around lines 38 - 42,
The UserProfileViewModel is being initialized with useMockData: true in the
production app shell; change the initialization of UserProfileViewModel in the
widget tree so useMockData is driven by a build-time/debug flag (e.g., use
kDebugMode or a bool from environment via bool.fromEnvironment/--dart-define)
instead of hard-coding true so that in production the constructor uses the real
UserService.fetchUserProfile(); update the create call that constructs
UserProfileViewModel (and its subsequent loadProfile()) to pass useMockData:
<flag> (or omit if defaulting appropriately) and ensure UserProfileView remains
untouched.
src/lib/main.dart-36-49 (1)

36-49: ⚠️ Potential issue | 🟠 Major

Don’t surface exception + stack trace in release builds.

ErrorWidget.builder is set unconditionally, so production users will see raw exception messages and stack traces on widget build failures. This is both a poor UX and a potential information-disclosure vector (internal paths, package names, SQL-looking messages from Supabase, etc.).

Proposed fix — gate on `kDebugMode`
-  ErrorWidget.builder = (FlutterErrorDetails details) {
-    return Material(
-      child: Container(
-        color: Colors.black87,
-        padding: const EdgeInsets.all(20),
-        child: SingleChildScrollView(
-          child: Text(
-            'Lỗi!\n\n${details.exception}\n\nStack Trace:\n${details.stack}',
-            style: const TextStyle(color: Colors.greenAccent, fontSize: 14),
-          ),
-        ),
-      ),
-    );
-  };
+  if (kDebugMode) {
+    ErrorWidget.builder = (FlutterErrorDetails details) {
+      return Material(
+        child: Container(
+          color: Colors.black87,
+          padding: const EdgeInsets.all(20),
+          child: SingleChildScrollView(
+            child: Text(
+              'Lỗi!\n\n${details.exception}\n\nStack Trace:\n${details.stack}',
+              style: const TextStyle(color: Colors.greenAccent, fontSize: 14),
+            ),
+          ),
+        ),
+      );
+    };
+  }

Requires import 'package:flutter/foundation.dart';.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/main.dart` around lines 36 - 49, ErrorWidget.builder is currently set
unconditionally and exposes raw exceptions/stack traces from FlutterErrorDetails
in production; wrap the custom ErrorWidget.builder assignment in a kDebugMode
check (importing package:flutter/foundation.dart) so the detailed error UI is
only used in debug, and provide a safe generic user-friendly fallback for
release builds (e.g., a simple error message or empty Container) to avoid
leaking internals.
src/lib/features/user/service/user_service.dart-10-24 (1)

10-24: ⚠️ Potential issue | 🟠 Major

Double-wrapped exception handling obscures the original error in debug logs.

The try/catch block at lines 10–23 wraps every thrown Exception — including the two Exception instances thrown inside the try block at lines 13 and 17 — into a generic Exception("Failed to fetch user profile: $e"). The resulting debug message becomes "Error loading profile: Exception: Failed to fetch user profile: Exception: Không tìm thấy...", burying the original exception type and losing the stack trace.

Either drop the outer try/catch entirely (let callers surface the original error) or use rethrow to preserve the trace.

Additionally, remove the stale comments on lines 7 and 9 (which claim to "simulate" and "mimic" a network delay, but the code performs real API calls without any artificial delay), and reuse the already-loaded user.id instead of redundantly calling currentUser!.id again on line 19.

Proposed fix
-  Future<UserProfileModel> fetchUserProfile() async {
-    // Mimic API call delay for smooth state switching
-    try{
-      final user = _supabase.auth.currentUser;
-      if (user == null) {
-        throw Exception("Không tìm thấy phiên đăng nhập. Hãy đăng nhập lại");
-      }
-      final response = await _supabase.rpc('get_user_profile_stats');
-      if(response == null){
-        throw Exception("Không thể lấy thông tin người dùng. Hãy thử lại sau");
-      }
-      response['id'] = _supabase.auth.currentUser!.id;
-      return UserProfileModel.fromJson(response);
-    }
-    catch(e){
-      throw Exception("Failed to fetch user profile: $e");
-    }
-  }
+  Future<UserProfileModel> fetchUserProfile() async {
+    final user = _supabase.auth.currentUser;
+    if (user == null) {
+      throw Exception('Không tìm thấy phiên đăng nhập. Hãy đăng nhập lại');
+    }
+    final response = await _supabase.rpc('get_user_profile_stats');
+    if (response == null) {
+      throw Exception('Không thể lấy thông tin người dùng. Hãy thử lại sau');
+    }
+    final map = Map<String, dynamic>.from(response as Map);
+    map['id'] = user.id;
+    return UserProfileModel.fromJson(map);
+  }

This also defensively copies the RPC response into a new Map<String, dynamic> before mutating, in case the returned map is immutable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/user/service/user_service.dart` around lines 10 - 24, The
catch block in the user profile fetch wraps and masks original exceptions;
remove the outer try/catch or replace the catch with a rethrow to preserve stack
traces, reuse the already-loaded user variable for the id (use user.id instead
of calling _supabase.auth.currentUser!.id), defensively copy the RPC result into
a new Map<String, dynamic> before mutating (so you don’t modify an immutable
response) and remove the stale "simulate"/"mimic" network delay comments; locate
changes around the _supabase usage and UserProfileModel.fromJson call to update
error handling, response copying, and id assignment.
supabase/migrations/20260417060333_chatbot_add_task_rpc.sql-46-50 (1)

46-50: ⚠️ Potential issue | 🟠 Major

Avoid returning raw database errors to clients.

SQLERRM can expose table names, constraint names, and policy details. Return a stable error code/message and log the internal error server-side.

Proposed fix
 EXCEPTION WHEN OTHERS THEN
+  RAISE LOG 'create_task_full failed for user %: %', auth.uid(), SQLERRM;
   RETURN json_build_object(
     'success', false,
-    'error', SQLERRM
+    'error', 'CREATE_TASK_FAILED'
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260417060333_chatbot_add_task_rpc.sql` around lines 46
- 50, The EXCEPTION WHEN OTHERS THEN block currently returns raw SQLERRM via
json_build_object which can leak internal schema details; change this to log the
full SQLERRM/server error internally (e.g., RAISE NOTICE/EXCEPTION to a server
log, INSERT into an audit/errors table, or call a logging function) and return a
stable, non-sensitive payload instead (e.g., json_build_object('success', false,
'code', 'INTERNAL_ERROR', 'message', 'An unexpected error occurred')). Update
the EXCEPTION WHEN OTHERS THEN handling that references SQLERRM and
json_build_object to perform internal logging and return the generic error
object.
src/lib/features/tag/view/widgets/tag_selector.dart-24-59 (1)

24-59: ⚠️ Potential issue | 🟠 Major

Pop the dialog context, not the parent context, after async work.

If the user dismisses the dialog while addCustomTag is pending, Navigator.pop(context) can pop the underlying screen. Capture the dialog context and check that it is still mounted.

Proposed fix
     showDialog(
       context: context,
-      builder: (_) => AlertDialog(
+      builder: (dialogContext) => AlertDialog(
@@
           TextButton(
-            onPressed: () => Navigator.pop(context),
+            onPressed: () => Navigator.pop(dialogContext),
             child: const Text('Huỷ'),
           ),
@@
               if (error != null) {
                 ScaffoldMessenger.of(context).showSnackBar(
@@
                 );
               } else {
-                Navigator.pop(context);
+                if (dialogContext.mounted) {
+                  Navigator.pop(dialogContext);
+                }
               }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tag/view/widgets/tag_selector.dart` around lines 24 - 59,
The dialog is popping the parent context after awaiting viewModel.addCustomTag;
update _showAddCustomDialog to capture the dialog-specific BuildContext by
changing the showDialog builder signature to (dialogContext) => AlertDialog(...)
and replace Navigator.pop(context) inside the async handler with
Navigator.of(dialogContext).pop(), but only after verifying the dialog navigator
is still mounted (e.g., if (Navigator.of(dialogContext).mounted)
Navigator.of(dialogContext).pop()); keep references to _showAddCustomDialog and
viewModel.addCustomTag to locate the changes.
supabase/migrations/20260413084521_update_user_profile_with_heatmap_rpc.sql-6-35 (1)

6-35: ⚠️ Potential issue | 🟠 Major

Clamp p_days and compare local dates consistently.

A caller can pass a huge p_days value and force a large scan. Also, the current updated_at >= (...)::DATE comparison converts a Vietnam-local date back through the DB timezone, which can drop boundary-day completions.

Proposed fix
 DECLARE
     v_user_id UUID;
     v_username TEXT;
     v_avatar TEXT;
     v_tasks_done INT;
     v_current_streak INT;
     v_heatmap_data JSON;
+    v_days INT;
 BEGIN
     v_user_id := auth.uid();
+    v_days := LEAST(GREATEST(COALESCE(p_days, 90), 1), 365);
 
     IF v_user_id IS NULL THEN
         RAISE EXCEPTION 'User not authenticated';
@@
         SELECT DATE(updated_at AT TIME ZONE 'Asia/Ho_Chi_Minh') AS task_date, COUNT(*) AS task_count
         FROM public.task
         WHERE profile_id = v_user_id AND status = 1
-          AND updated_at >= ((CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Ho_Chi_Minh')::DATE - (p_days || ' days')::INTERVAL)
+          AND DATE(updated_at AT TIME ZONE 'Asia/Ho_Chi_Minh')
+              >= (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Ho_Chi_Minh')::DATE - v_days
         GROUP BY DATE(updated_at AT TIME ZONE 'Asia/Ho_Chi_Minh')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260413084521_update_user_profile_with_heatmap_rpc.sql`
around lines 6 - 35, Clamp the incoming p_days to a safe range (e.g., min 1, max
365) and use that clamped value when building the interval; replace direct
(p_days || ' days')::INTERVAL with a sanitized integer (e.g., v_days) and use
INTERVAL format based on that value. Also compare dates in the same local
timezone by converting updated_at to 'Asia/Ho_Chi_Minh' and comparing its DATE
to (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Ho_Chi_Minh')::DATE - v_days (or
equivalent interval), so the WHERE clause that computes task_date
(DATE(updated_at AT TIME ZONE 'Asia/Ho_Chi_Minh')) is compared against a local
DATE boundary computed the same way to avoid timezone boundary drops; update
references in the heatmap query that produce v_heatmap_data and any earlier uses
of p_days accordingly.
src/lib/features/statistics/view/widgets/statistics_widgets.dart-344-351 (1)

344-351: ⚠️ Potential issue | 🟠 Major

Handle TimeOfDay rollover at 23:xx.

task.updatedAt.hour + 1 becomes 24 for tasks completed at 23:xx, which is outside Flutter's valid TimeOfDay hour range (0–23). The code at line 349 will throw a runtime error when a task is updated at this time.

Proposed fix
           onTap: () {
+            final endDateTime = task.updatedAt.add(const Duration(hours: 1));
             final mappedTask = TaskModel(
@@
               endTime: TimeOfDay(
-                hour: task.updatedAt.hour + 1,
-                minute: task.updatedAt.minute,
+                hour: endDateTime.hour,
+                minute: endDateTime.minute,
               ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/statistics/view/widgets/statistics_widgets.dart` around
lines 344 - 351, The endTime construction can produce an invalid hour (24) when
task.updatedAt.hour is 23; update the TimeOfDay creation to wrap hours into the
valid 0–23 range (e.g., compute endHour = (task.updatedAt.hour + 1) % 24 or
conditional if task.updatedAt.hour == 23 then 0) and use that endHour with
task.updatedAt.minute when building the endTime TimeOfDay to avoid runtime
errors in TimeOfDay(hour: ..., minute: ...).
src/lib/features/tasks/view/screens/home_screen.dart-105-109 (1)

105-109: ⚠️ Potential issue | 🟠 Major

Theming regression: hardcoded AppColors/Colors.black/Colors.white across the screen.

The stated intent of this PR is a theme refactor, but HomeScreen still hardcodes brand/system colors in several spots, so the screen renders poorly in dark mode and bypasses the new ColorScheme:

  • Line 109: Container(color: AppColors.primaryBlue) for the top wave — should use Theme.of(context).colorScheme.primary.
  • Lines 170, 173, 181: Icon(..., color: Colors.black) for menu/notifications/add — invisible on dark backgrounds; use colorScheme.onSurface (or onPrimary if the wave sits behind them).
  • Line 224: AppColors.grayTextcolorScheme.onSurfaceVariant.
  • Line 268: _FilterChip(color: AppColors.primaryBlue, ...) — should use colorScheme.primary.
  • Lines 389, 398: _FilterChip uses Colors.white for unselected bg / selected text — breaks in dark mode; prefer colorScheme.surface / colorScheme.onPrimary.

Once these are removed, the app_colors.dart import on Line 5 should go too.

Also applies to: 167-199, 223-224, 265-270, 385-400

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/view/screens/home_screen.dart` around lines 105 - 109,
HomeScreen still uses hardcoded AppColors/Colors (e.g., Container(color:
AppColors.primaryBlue) in the TopWaveClipper area, Icon(..., color:
Colors.black), AppColors.grayText, and _FilterChip(color:
AppColors.primaryBlue/Colors.white)), which breaks dark mode; update these
usages to read from Theme.of(context).colorScheme (use colorScheme.primary for
the wave and _FilterChip selected color, colorScheme.onPrimary for icons on the
wave, colorScheme.onSurface/onSurfaceVariant for surface text/icons, and
colorScheme.surface for chip backgrounds) and adjust _FilterChip props
accordingly so selected/unselected states use colorScheme.surface and
colorScheme.onPrimary/onSurface as appropriate; after replacing all occurrences
in HomeScreen and _FilterChip, remove the unused app_colors.dart import.
src/lib/features/tasks/view/screens/task_detail_screen.dart-220-250 (1)

220-250: ⚠️ Potential issue | 🟠 Major

Delete flow uses a popped context and ignores async failures.

Three issues on the delete path:

  1. context.read<TaskViewModel>().deleteTask(widget.task.id) is not awaited, so Navigator.pop(context) runs before the delete completes; if the RPC fails, the task is already removed from the UI stack and the error is swallowed.
  2. ScaffoldMessenger.of(context).showSnackBar(...) at Line 240 is called AFTER Navigator.pop(context) at Line 239 — the detail screen is being torn down, so the snackbar frequently shows on the wrong scaffold or not at all.
  3. No mounted check after the async work.

Await the delete, capture the messenger before popping, and show the snackbar via the parent scaffold.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/view/screens/task_detail_screen.dart` around lines 220
- 250, The delete path should await the async delete, capture the
ScaffoldMessenger before popping, and check mounted before navigating: change
the onPressed handler inside the AlertDialog's "Xóa" button to await
context.read<TaskViewModel>().deleteTask(widget.task.id) (referencing
TaskViewModel.deleteTask and widget.task.id), call final messenger =
ScaffoldMessenger.of(context) before any Navigator.pop calls, then after the
await verify if (!mounted) return; before calling Navigator.pop(context) and
Navigator.pop(ctx); use messenger.showSnackBar(...) to show success or show an
error SnackBar if the awaited delete throws (catch the exception and show a
failure message) so the UI isn't popped prematurely and snackbar targets the
correct scaffold.
src/lib/features/chatbot/services/chatbot_services.dart-72-104 (1)

72-104: ⚠️ Potential issue | 🟠 Major

Unsafe casts and unvalidated RPC inputs; catch-all hides real failures as "AI connection error".

A few concerns in the function-call branch:

  • Line 76: args['title'] as String will throw if Gemini returns null or a non-string for title. Any such failure propagates to the generic catch (e) at Line 106 and is reported to the user as Lỗi kết nối AI: …, which is misleading (it's a schema violation, not a network error).
  • Line 77: priority is clamped via ?? 1 but not range-checked. The RPC create_task_full accepts any INT4 and stores it as-is, so a model hallucination like priority: 99 silently corrupts task data. Clamp to [1, 3] before calling the RPC.
  • Line 96: dbResponse['success'] assumes the RPC response is a Map. Supabase rpc(...) returns dynamic; if the function errors out or returns null, this throws and again surfaces as a connection error.
  • Line 98: Sending a functionResponse with only {'status': 'Thành công' | 'Thất bại'} drops the RPC's error/task_id/message fields, so the model cannot tailor its reply (or apologize with a reason on failure). Forward the relevant payload.
🛡️ Proposed hardening
-          final title = args['title'] as String;
-          final priority = (args['priority'] as num?)?.toInt() ?? 1;
-          final rawTags = args['tags'] as List<dynamic>? ?? [];
-          final tags = rawTags.map((e) => e.toString()).toList();
+          final title = (args['title'] as String?)?.trim() ?? '';
+          if (title.isEmpty) {
+            return 'Mình chưa rõ tên công việc, bạn nói lại giúp nhé!';
+          }
+          final rawPriority = (args['priority'] as num?)?.toInt() ?? 1;
+          final priority = rawPriority.clamp(1, 3);
+          final rawTags = args['tags'] as List<dynamic>? ?? const [];
+          final tags = rawTags.map((e) => e.toString()).toList();
@@
-          final isSuccess = dbResponse['success'] == true;
+          final responseMap = (dbResponse is Map) ? dbResponse : const {};
+          final isSuccess = responseMap['success'] == true;
           final functionResponse = await _chatSession!.sendMessage(
             Content.functionResponse('create_task_full', {
-              'status': isSuccess ? 'Thành công' : 'Thất bại',
+              'status': isSuccess ? 'Thành công' : 'Thất bại',
+              if (!isSuccess && responseMap['error'] != null)
+                'error': responseMap['error'],
             }),
           );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/chatbot/services/chatbot_services.dart` around lines 72 -
104, In the function-call branch handling response.functionCalls (inside the
block that checks functionCall.name == 'create_task_full'), validate and safely
parse args: ensure title is a non-empty String (return a clear user-facing
validation message if missing), parse priority as int then clamp it to the
allowed range 1..3, and coerce tags to List<String> safely; then call
Supabase.instance.client.rpc with these sanitized inputs. After RPC, defensively
handle dbResponse being null or not a Map (check type and presence of keys),
extract and forward relevant RPC fields (e.g., 'success', 'task_id', 'message',
'error') into the Content.functionResponse payload instead of only {'status':
...} so the model gets full context, and ensure any thrown parsing/validation
errors are reported with an appropriate message rather than bubbled to the
generic AI-connection catch-all; update the use of
_chatSession!.sendMessage(Content.functionResponse('create_task_full', ...))
accordingly.
src/lib/features/tasks/view/screens/task_detail_screen.dart-130-157 (1)

130-157: ⚠️ Potential issue | 🟠 Major

FutureBuilder.future recreated on every rebuild.

future: context.read<TaskViewModel>().getNotesForTask(taskId) is evaluated inline, so every rebuild of the detail screen (tag toggle, category change, time picker, keyboard open/close, etc.) kicks off a new Supabase query and briefly shows CircularProgressIndicator, causing flicker and unnecessary network traffic. Store the Future in state and only refresh it in response to explicit events (e.g., after createNote succeeds).

♻️ Sketch
-class _TaskDetailScreenState extends State<TaskDetailScreen> {
+class _TaskDetailScreenState extends State<TaskDetailScreen> {
+  Future<List<NoteModel>>? _notesFuture;
@@
-    _currentTags = List.from(widget.task.tags);
+    _currentTags = List.from(widget.task.tags);
+    _notesFuture = context.read<TaskViewModel>().getNotesForTask(widget.task.id.toString());
@@
-        FutureBuilder<List<NoteModel>>(
-          future: context.read<TaskViewModel>().getNotesForTask(taskId),
+        FutureBuilder<List<NoteModel>>(
+          future: _notesFuture,

Then refresh _notesFuture after a successful createNote instead of calling bare setState(() {}).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/view/screens/task_detail_screen.dart` around lines 130
- 157, The FutureBuilder is recreating the future each rebuild because you call
context.read<TaskViewModel>().getNotesForTask(taskId) inline; introduce a State
field (e.g., Future<List<NoteModel>>? _notesFuture) in the TaskDetailScreen
State, initialize it once in initState with _notesFuture =
context.read<TaskViewModel>().getNotesForTask(taskId), and change the
FutureBuilder to use future: _notesFuture; after a successful createNote (or
other explicit refresh events) reassign _notesFuture =
context.read<TaskViewModel>().getNotesForTask(taskId) and call setState to
update—this prevents automatic refetch/flicker on unrelated rebuilds.
src/lib/features/tasks/viewmodel/task_viewmodel.dart-162-167 (1)

162-167: ⚠️ Potential issue | 🟠 Major

Clear stale tasks when there is no authenticated user.

If the session is missing, fetchTasks() returns without clearing _tasks, so a logged-out or switched user can still see the previous user’s in-memory task list.

Proposed fix
     final user = supabase.auth.currentUser;
     
-    if (user == null) return; 
+    if (user == null) {
+      _tasks.clear();
+      notifyListeners();
+      return;
+    }

Also applies to: 211-213

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 162 - 167,
When fetchTasks() finds no authenticated user
(Supabase.instance.client.auth.currentUser == null), it should clear the
in-memory task list and update observers instead of returning early; update the
fetchTasks() method to set _tasks = [] (or clear the collection) and call
notifyListeners() before returning. Apply the same change to the other fetch
path referenced in the file (the second early-return block around the later
fetchTasks logic) so that both locations clear _tasks and notifyListeners when
user == null.
src/lib/features/tasks/viewmodel/task_viewmodel.dart-169-206 (1)

169-206: ⚠️ Potential issue | 🟠 Major

Hydrate category and tags from the persisted schema.

fetchTasks() selects * and reads item['category'], but task creation writes category_id; fetched tasks will fall back to General. Tags are never populated either, so tag filtering over t.tags cannot work for persisted tasks.

Proposed direction
       final data = await supabase
           .from('task')
-          .select('*')
+          .select('''
+            *,
+            category:category_id(id, name, color_code, profile_id),
+            tags:task_tag(tag(id, name, color_code, profile_id))
+          ''')
           .eq('profile_id', user.id) 
           .order('create_at', ascending: true);

Then build CategoryModel from the joined category object and List<TagModel> from the joined tag rows instead of using placeholder category data and empty tags.

Also applies to: 259-264

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 169 - 206,
fetchTasks() currently reads item['category'] incorrectly (only category_id was
persisted) and never populates TaskModel.tags, so tasks fall back to a
placeholder CategoryModel and empty tags. Fix by updating the Supabase query in
fetchTasks() to include the joined category and tags (e.g. select with
category(*) and tags(*) or the intermediate task_tag join), then when iterating
items construct CategoryModel from the returned category object fields (id,
name, color_code, profile_id) instead of the hardcoded values and build a
List<TagModel> from the returned tag rows (mapping each tag row to TagModel) and
assign it to TaskModel.tags; keep using existing TaskModel, CategoryModel and
TagModel constructors to map the fields and preserve date/priority parsing as
before.
src/lib/features/user/viewmodel/user_profile_viewmodel.dart-16-16 (1)

16-16: ⚠️ Potential issue | 🟠 Major

Default profile loading should use real data.

useMockData defaults to true, so any production call site that omits the argument will show the hardcoded Alex Thompson profile instead of the signed-in user. Make mock mode opt-in and ensure the production provider wiring does not pass true.

Proposed fix
-  UserProfileViewModel({this.useMockData = true});
+  UserProfileViewModel({this.useMockData = false});

Also applies to: 30-34

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/user/viewmodel/user_profile_viewmodel.dart` at line 16, The
constructor default for UserProfileViewModel currently sets useMockData = true,
causing real callers to load mock profiles; change the default to false so mock
mode is opt-in (i.e., UserProfileViewModel({this.useMockData = false})), update
any related factory/constructors or providers that currently pass true to
instead opt-in explicitly, and scan the other constructor/initialization usages
referenced around the UserProfileViewModel class (the similar block at lines
~30-34) to ensure they do not default to or pass true inadvertently.
src/lib/features/tasks/view/screens/create_task_screen.dart-167-172 (1)

167-172: ⚠️ Potential issue | 🟠 Major

Remove demo defaults and dispose the controllers.

The create screen currently opens with "Team Meeting" and a sample description, which can create accidental test data. Since these controllers are owned by the state object, they should also be disposed.

Proposed fix
-  final TextEditingController _nameController = TextEditingController(
-    text: 'Team Meeting',
-  );
-  final TextEditingController _descController = TextEditingController(
-    text: 'Discuss all questions about new projects',
-  );
+  final TextEditingController _nameController = TextEditingController();
+  final TextEditingController _descController = TextEditingController();
+
+  `@override`
+  void dispose() {
+    _nameController.dispose();
+    _descController.dispose();
+    super.dispose();
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/view/screens/create_task_screen.dart` around lines 167
- 172, Remove the hardcoded demo values from the TextEditingController instances
by initializing _nameController and _descController without the text: parameter
so they start empty, and add a dispose override in the State class to call
_nameController.dispose() and _descController.dispose(); ensure the dispose
method also calls super.dispose(). These symbols to change are the
TextEditingController fields named _nameController and _descController and the
State's dispose() method for the CreateTaskScreen widget.
src/lib/features/tasks/view/screens/create_task_screen.dart-66-127 (1)

66-127: ⚠️ Potential issue | 🟠 Major

Persist all submitted task fields.

description and tags are accepted by submitTask() and passed from the UI, but only title/status/priority/profile/category/date are inserted. This drops the user’s description and selected tags on every created task.

Proposed direction
-    await _supabase.from('task').insert({
+    final insertedTask = await _supabase.from('task').insert({
       'title': taskName.trim(),
-      //'description': description.trim(), // Tui mở comment cái này ra cho ông luôn
+      'description': description.trim(),
       'status': 0,
       'priority': priorityId,
       'profile_id': user.id,
       'category_id': categoryId, // Dùng ID thực tế từ UI
       'create_at': scheduledDateTime.toIso8601String(),
-    });
+    }).select('id').single();
+
+    // Also persist `tags` into the task/tag join table used by the schema.
+    // Example:
+    // await _supabase.from('task_tag').insert(
+    //   tags.map((tag) => {'task_id': insertedTask['id'], 'tag_id': tag.id}).toList(),
+    // );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/view/screens/create_task_screen.dart` around lines 66
- 127, The insert currently omits the passed description and tags; update the
insert in submitTask(...) (the _supabase.from('task').insert call) to include
'description' (use description.trim() or null-guard if empty) and persist 'tags'
(serialize to the DB format your table expects, e.g., a JSON array or text
array—convert List<dynamic> tags to List<String> or jsonEncode(tags) as needed)
alongside the existing fields so all submitted task fields are saved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 72b404c1-009d-4cd9-8339-2fbc84279dbf

📥 Commits

Reviewing files that changed from the base of the PR and between 7b91943 and f6c381d.

⛔ Files ignored due to path filters (6)
  • src/pubspec.lock is excluded by !**/*.lock
  • src/web/favicon.png is excluded by !**/*.png
  • src/web/icons/Icon-192.png is excluded by !**/*.png
  • src/web/icons/Icon-512.png is excluded by !**/*.png
  • src/web/icons/Icon-maskable-192.png is excluded by !**/*.png
  • src/web/icons/Icon-maskable-512.png is excluded by !**/*.png
📒 Files selected for processing (72)
  • .vs/TaskManagement.slnx/v18/.wsuo
  • .vs/TaskManagement.slnx/v18/DocumentLayout.backup.json
  • .vs/TaskManagement.slnx/v18/DocumentLayout.json
  • .vs/VSWorkspaceState.json
  • .vs/slnx.sqlite
  • .vscode/launch.json
  • README.md
  • src/.gitignore
  • src/lib/core/theme/app_theme.dart
  • src/lib/core/theme/auth_layout_template.dart
  • src/lib/core/theme/custom_text_field.dart
  • src/lib/core/theme/theme_provider.dart
  • src/lib/core/utils/adaptive_color_extension.dart
  • src/lib/core/widgets/custom_input_field.dart
  • src/lib/features/auth/otp_verification_view.dart
  • src/lib/features/auth/presentation/view/forgot_password_view.dart
  • src/lib/features/auth/presentation/view/login_view.dart
  • src/lib/features/auth/presentation/view/new_password_view.dart
  • src/lib/features/auth/presentation/view/otp_verification_view.dart
  • src/lib/features/auth/presentation/view/register_view.dart
  • src/lib/features/auth/viewmodels/task_provider.dart
  • src/lib/features/category/model/category_model.dart
  • src/lib/features/category/repository/category_repository.dart
  • src/lib/features/category/view/widgets/category_choice_chips.dart
  • src/lib/features/category/viewmodel/category_viewmodel.dart
  • src/lib/features/chatbot/model/chatmessage_model.dart
  • src/lib/features/chatbot/services/chatbot_services.dart
  • src/lib/features/chatbot/view/chatbot_view.dart
  • src/lib/features/chatbot/view/widgets/bot_avatar.dart
  • src/lib/features/chatbot/view/widgets/chat_header.dart
  • src/lib/features/chatbot/view/widgets/day_separator.dart
  • src/lib/features/chatbot/view/widgets/message_composer.dart
  • src/lib/features/chatbot/view/widgets/message_tile.dart
  • src/lib/features/chatbot/view/widgets/typing_indicator.dart
  • src/lib/features/chatbot/view/widgets/user_avatar.dart
  • src/lib/features/chatbot/viewmodel/chatbot_viewmodel.dart
  • src/lib/features/main/view/screens/create_task.dart
  • src/lib/features/main/view/screens/main_screen.dart
  • src/lib/features/note/view/focus_screen.dart
  • src/lib/features/note/view/focus_widget.dart
  • src/lib/features/statistics/model/StatisticsModel.dart
  • src/lib/features/statistics/view/screens/statistics_screen.dart
  • src/lib/features/statistics/view/widgets/statistics_widgets.dart
  • src/lib/features/tag/model/tag_model.dart
  • src/lib/features/tag/repository/tag_repository.dart
  • src/lib/features/tag/view/widgets/tag_selector.dart
  • src/lib/features/tag/viewmodel/tag_viewmodel.dart
  • src/lib/features/tasks/model/task_model.dart
  • src/lib/features/tasks/view/screens/create_task_screen.dart
  • src/lib/features/tasks/view/screens/home_screen.dart
  • src/lib/features/tasks/view/screens/task_detail_screen.dart
  • src/lib/features/tasks/view/widgets/priority_selector.dart
  • src/lib/features/tasks/view/widgets/tag_selector.dart
  • src/lib/features/tasks/view/widgets/task_widgets.dart
  • src/lib/features/tasks/viewmodel/task_viewmodel.dart
  • src/lib/features/user/model/user_profile_model.dart
  • src/lib/features/user/service/user_service.dart
  • src/lib/features/user/view/user_profile_view.dart
  • src/lib/features/user/view/widgets/logout_button.dart
  • src/lib/features/user/view/widgets/profile_header.dart
  • src/lib/features/user/view/widgets/settings_list_tile.dart
  • src/lib/features/user/view/widgets/settings_section.dart
  • src/lib/features/user/view/widgets/stat_card.dart
  • src/lib/features/user/viewmodel/user_profile_viewmodel.dart
  • src/lib/main.dart
  • src/pubspec.yaml
  • src/web/index.html
  • src/web/manifest.json
  • src/web_entrypoint.dart
  • supabase/migrations/20260409084009_create_user_profile_rpc.sql
  • supabase/migrations/20260413084521_update_user_profile_with_heatmap_rpc.sql
  • supabase/migrations/20260417060333_chatbot_add_task_rpc.sql

Comment thread src/lib/features/tasks/view/screens/home_screen.dart Outdated
Comment on lines +56 to +66
void _toggleTag(TagModel tag) {
setState(() {
if (_currentTags.any((t) => t.id == tag.id)) {
_currentTags.removeWhere((t) => t.id == tag.id);
} else {
_currentTags.add(tag);
}
});
}

bool _isTagSelected(TagModel tag) => _currentTags.any((t) => t.id == tag.id);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Tag/time/description edits are silently dropped on Save.

State is tracked for tags (_currentTags), times (_startTime/_endTime) and description (_descController), and the UI lets users edit all of them — but _saveChanges only sends title and category_id:

final Map<String, dynamic> updates = {
  'title': _titleController.text.trim(),
  'category_id': _currentCategory.id,
};

So toggling a tag chip, picking a new start/end time, or editing the description appears to save (snackbar "Cập nhật thành công!") but the mutations never reach Supabase. This is data-loss from the user's perspective. Either wire the remaining fields into updates (and the task_tags join table for tag add/remove) or disable the inputs.

Also applies to: 161-183

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/view/screens/task_detail_screen.dart` around lines 56
- 66, The save routine currently only sends title and category_id; update
_saveChanges to include description (_descController.text.trim()), start/end
times (_startTime/_endTime) converted to the expected payload format, and the
tag changes derived from _currentTags; additionally, implement syncing for the
task_tags join table by computing tags to add/remove (compare original tags vs
_currentTags) and call the appropriate Supabase mutations or API methods to
insert/delete join rows (or include tag IDs in the task update if your backend
supports it). Ensure you reference the existing _currentTags, _startTime,
_endTime, _descController, and _saveChanges symbols when adding the payload and
tag-sync logic so edited tags, times, and description are actually persisted
rather than silently dropped.

Comment on lines +1 to +18
CREATE OR REPLACE FUNCTION create_task_full(
p_title TEXT,
p_priority INT4,
p_profile_id UUID,
p_tag_names TEXT[]
)
RETURNS JSON
LANGUAGE plpgsql
AS $$
DECLARE
v_task_id INT8;
v_tag_name TEXT;
v_tag_id INT8;
BEGIN

INSERT INTO task (title, priority, profile_id, status)
VALUES (p_title, p_priority, p_profile_id, 0)
RETURNING id INTO v_task_id;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don’t trust p_profile_id from the client.

Any authenticated caller can invoke this RPC with another profile ID unless every RLS policy blocks it. Derive or verify the profile ID from auth.uid() before inserting.

Proposed fix
 DECLARE
   v_task_id INT8;
   v_tag_name TEXT;
   v_tag_id INT8;
+  v_profile_id UUID;
 BEGIN
+  v_profile_id := auth.uid();
+
+  IF v_profile_id IS NULL OR p_profile_id IS DISTINCT FROM v_profile_id THEN
+    RAISE EXCEPTION 'Not authorized to create task for this profile';
+  END IF;
 
   INSERT INTO task (title, priority, profile_id, status)
-  VALUES (p_title, p_priority, p_profile_id, 0)
+  VALUES (p_title, p_priority, v_profile_id, 0)
   RETURNING id INTO v_task_id;
@@
         INSERT INTO tag (name, profile_id, color_code)
-        VALUES (v_tag_name, p_profile_id, '#6200EE')
+        VALUES (v_tag_name, v_profile_id, '#6200EE')

Also applies to: 26-34

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260417060333_chatbot_add_task_rpc.sql` around lines 1 -
18, The RPC create_task_full must not trust the client-supplied p_profile_id;
instead derive or verify the profile ID using auth.uid() before inserting.
Inside create_task_full, query the profile table (e.g., SELECT id INTO
v_profile_id FROM profile WHERE auth_uid = auth.uid()) or validate that
p_profile_id matches the profile id for auth.uid(), then use that
server-verified v_profile_id in the INSERT (replace uses of p_profile_id).
Ensure any error path returns or raises when no profile is found or the
verification fails so unprivileged callers cannot insert for another profile.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/features/tasks/view/screens/create_task_screen.dart (1)

485-500: ⚠️ Potential issue | 🟡 Minor

Remove the duplicate Description field.

The same _descController is rendered twice, which makes the form look broken and can confuse users.

Proposed fix
-                    // --- Input Desc ---
-                    CustomInputField(
-                      label: 'Description',
-                      hint: 'Enter task description',
-                      controller: _descController,
-                      maxLines: 2,
-                    ),
-                    const SizedBox(height: 25),
-
                     // Description
                     CustomInputField(
                       label: 'Description',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/view/screens/create_task_screen.dart` around lines 485
- 500, The UI renders the same Description input twice using CustomInputField
with the same _descController; remove the duplicate widget instance so only one
Description field remains (keep a single CustomInputField that uses
_descController and its surrounding spacing like the SizedBox), and verify
CreateTaskScreen (or the widget tree containing _descController) no longer
contains the duplicated block to avoid controller reuse and visual duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/features/tasks/view/screens/create_task_screen.dart`:
- Around line 66-127: The submitTask function drops the description and tags
parameters when inserting into Supabase, so include description in the task
insert and persist tags after the task is created: add 'description':
description.trim() to the payload passed to _supabase.from('task').insert,
capture the inserted task's id from the insert response, then persist tags by
inserting into your join/association (e.g., _supabase.from('task_tag').insert)
using that task id and each tag id or name; if you intend to store tags as a
JSON/array column on the task row instead, write the tags list (e.g.,
tags.map(...).toList()) into that column in the same insert. Ensure you handle
failures and keep _isLoading/notifyListeners consistent.
- Line 193: CreateTaskScreen is calling context.read<CreateTaskProvider>()
(e.g., in CreateTaskScreen where formattedDate is computed and at other
occurrences) but CreateTaskProvider is not registered, causing
ProviderNotFoundException; fix by registering the provider - either add
ChangeNotifierProvider<CreateTaskProvider>(create: (_) => CreateTaskProvider())
to your app's root MultiProvider so the provider is available app-wide, or wrap
the screen when navigating (replace MaterialPageRoute(builder: (_) =>
CreateTaskScreen()) with MaterialPageRoute(builder: (_) =>
ChangeNotifierProvider(create: (_) => CreateTaskProvider(), child:
CreateTaskScreen())) so CreateTaskProvider is in the widget tree before any
context.read()/watch() calls.

In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart`:
- Around line 147-175: fetchTasks() currently builds CategoryModel from
item['category'] string so reloaded tasks lose their real category; change data
retrieval to include the category row (or at least the category_id) and map it
into CategoryModel instead of using a fallback "General". Specifically, modify
the Supabase select to fetch the related category fields (or fetch category by
item['category_id']) and then construct CategoryModel with the real id, name,
colorCode and profileId when creating TaskModel in fetchTasks(), ensuring you
use the actual item['category_id'] and returned category columns rather than
item['category'].
- Around line 209-214: The deleteTask method returns before the task list is
refreshed; update deleteTask (and any call sites expecting completion) to await
the refresh by awaiting fetchTasks() after the delete completes (i.e., change
the call inside deleteTask to await fetchTasks()); keep the try/catch and
existing awaits on Supabase but ensure fetchTasks() is awaited so callers of
deleteTask see the updated list.
- Around line 140-154: fetchTasks currently returns early if
Supabase.auth.currentUser is null but doesn't clear previous state; update the
fetchTasks method to clear _tasks (and call notifyListeners() if this ViewModel
uses ChangeNotifier) before returning when user is null so the UI no longer
shows another user's tasks after logout/session expiry; specifically modify
fetchTasks to check user == null, then call _tasks.clear() (and
notifyListeners() / update any reactive state) and then return.
- Around line 52-72: The method addCustomTag currently calls fire-and-forget
_saveCustomTags(), causing callers to see success even if persistence fails;
change addCustomTag to be asynchronous (Future<String?>) and await
_saveCustomTags(), and make _saveCustomTags return a Future<bool> or throw on
failure; after adding the TagModel to _customTags, await _saveCustomTags(), and
if it fails remove the recently added tag from _customTags, avoid calling
notifyListeners() on failure and return an error message (or rethrow) so callers
see the persistence failure; keep references: addCustomTag, _saveCustomTags,
_customTags, notifyListeners.
- Around line 195-203: updateTask currently updates Supabase but leaves the
in-memory _tasks list stale; after the DB update in updateTask, locate the task
in the _tasks collection by matching its id (taskId), merge/patch the updated
fields from data into that task object (e.g., update title and category_id),
replace or update the entry in _tasks, and then call notifyListeners(); if the
task isn't found, fall back to fetching/refreshing tasks (or insert the patched
task) to keep local state consistent with the server.

---

Outside diff comments:
In `@src/lib/features/tasks/view/screens/create_task_screen.dart`:
- Around line 485-500: The UI renders the same Description input twice using
CustomInputField with the same _descController; remove the duplicate widget
instance so only one Description field remains (keep a single CustomInputField
that uses _descController and its surrounding spacing like the SizedBox), and
verify CreateTaskScreen (or the widget tree containing _descController) no
longer contains the duplicated block to avoid controller reuse and visual
duplication.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 88c67b93-46f0-4ccc-89d7-3e6f0b498fef

📥 Commits

Reviewing files that changed from the base of the PR and between f6c381d and a759ad7.

📒 Files selected for processing (5)
  • src/lib/features/tasks/model/task_model.dart
  • src/lib/features/tasks/view/screens/create_task_screen.dart
  • src/lib/features/tasks/view/widgets/task_widgets.dart
  • src/lib/features/tasks/viewmodel/task_viewmodel.dart
  • src/lib/main.dart
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib/main.dart
  • src/lib/features/tasks/view/widgets/task_widgets.dart

Comment on lines +66 to +127
Future<void> submitTask(
BuildContext context, {
required String taskName,
required String description,
required dynamic priority,
required List<dynamic> tags,
required int? categoryId, // Thêm tham số ID từ UI truyền vào
}) async {
if (taskName.trim().isEmpty) {
_showSnackBar(context, "Task name is required.");
return;
}

// Check xem có chọn Category chưa
if (categoryId == null) {
_showSnackBar(context, "Please select a category.");
return;
}

final user = _supabase.auth.currentUser;
if (user == null) {
_showSnackBar(context, "Session not found. Please re-authenticate.");
return;
}

_isLoading = true;
notifyListeners();

try {
// 1. Xử lý Priority ID
int priorityId = 3; // Mặc định Medium
final String priorityStr = priority.toString().toLowerCase();

if (priorityStr.contains('urgent')) {
priorityId = 1;
} else if (priorityStr.contains('high')) {
priorityId = 2;
} else if (priorityStr.contains('medium')) {
priorityId = 3;
} else if (priorityStr.contains('low')) {
priorityId = 4;
}

// 2. Xử lý thời gian
final scheduledDateTime = DateTime(
_selectedDate.year,
_selectedDate.month,
_selectedDate.day,
_startTime.hour,
_startTime.minute,
);

// 3. Insert vào Supabase
await _supabase.from('task').insert({
'title': taskName.trim(),
//'description': description.trim(), // Tui mở comment cái này ra cho ông luôn
'status': 0,
'priority': priorityId,
'profile_id': user.id,
'category_id': categoryId, // Dùng ID thực tế từ UI
'create_at': scheduledDateTime.toIso8601String(),
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Persist the description and selected tags.

description and tags are accepted from the form, but the insert payload drops them; created tasks lose user-entered details and tag selections.

Proposed fix for description persistence
     await _supabase.from('task').insert({
       'title': taskName.trim(),
-      //'description': description.trim(), // Tui mở comment cái này ra cho ông luôn
+      'description': description.trim(),
       'status': 0,
       'priority': priorityId,
       'profile_id': user.id,
       'category_id': categoryId, // Dùng ID thực tế từ UI
       'create_at': scheduledDateTime.toIso8601String(),
     });

Also persist tags here or remove the parameter until tag persistence is implemented.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<void> submitTask(
BuildContext context, {
required String taskName,
required String description,
required dynamic priority,
required List<dynamic> tags,
required int? categoryId, // Thêm tham số ID từ UI truyền vào
}) async {
if (taskName.trim().isEmpty) {
_showSnackBar(context, "Task name is required.");
return;
}
// Check xem có chọn Category chưa
if (categoryId == null) {
_showSnackBar(context, "Please select a category.");
return;
}
final user = _supabase.auth.currentUser;
if (user == null) {
_showSnackBar(context, "Session not found. Please re-authenticate.");
return;
}
_isLoading = true;
notifyListeners();
try {
// 1. Xử lý Priority ID
int priorityId = 3; // Mặc định Medium
final String priorityStr = priority.toString().toLowerCase();
if (priorityStr.contains('urgent')) {
priorityId = 1;
} else if (priorityStr.contains('high')) {
priorityId = 2;
} else if (priorityStr.contains('medium')) {
priorityId = 3;
} else if (priorityStr.contains('low')) {
priorityId = 4;
}
// 2. Xử lý thời gian
final scheduledDateTime = DateTime(
_selectedDate.year,
_selectedDate.month,
_selectedDate.day,
_startTime.hour,
_startTime.minute,
);
// 3. Insert vào Supabase
await _supabase.from('task').insert({
'title': taskName.trim(),
//'description': description.trim(), // Tui mở comment cái này ra cho ông luôn
'status': 0,
'priority': priorityId,
'profile_id': user.id,
'category_id': categoryId, // Dùng ID thực tế từ UI
'create_at': scheduledDateTime.toIso8601String(),
});
Future<void> submitTask(
BuildContext context, {
required String taskName,
required String description,
required dynamic priority,
required List<dynamic> tags,
required int? categoryId, // Thêm tham số ID từ UI truyền vào
}) async {
if (taskName.trim().isEmpty) {
_showSnackBar(context, "Task name is required.");
return;
}
// Check xem có chọn Category chưa
if (categoryId == null) {
_showSnackBar(context, "Please select a category.");
return;
}
final user = _supabase.auth.currentUser;
if (user == null) {
_showSnackBar(context, "Session not found. Please re-authenticate.");
return;
}
_isLoading = true;
notifyListeners();
try {
// 1. Xử lý Priority ID
int priorityId = 3; // Mặc định Medium
final String priorityStr = priority.toString().toLowerCase();
if (priorityStr.contains('urgent')) {
priorityId = 1;
} else if (priorityStr.contains('high')) {
priorityId = 2;
} else if (priorityStr.contains('medium')) {
priorityId = 3;
} else if (priorityStr.contains('low')) {
priorityId = 4;
}
// 2. Xử lý thời gian
final scheduledDateTime = DateTime(
_selectedDate.year,
_selectedDate.month,
_selectedDate.day,
_startTime.hour,
_startTime.minute,
);
// 3. Insert vào Supabase
await _supabase.from('task').insert({
'title': taskName.trim(),
'description': description.trim(),
'status': 0,
'priority': priorityId,
'profile_id': user.id,
'category_id': categoryId, // Dùng ID thực tế từ UI
'create_at': scheduledDateTime.toIso8601String(),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/view/screens/create_task_screen.dart` around lines 66
- 127, The submitTask function drops the description and tags parameters when
inserting into Supabase, so include description in the task insert and persist
tags after the task is created: add 'description': description.trim() to the
payload passed to _supabase.from('task').insert, capture the inserted task's id
from the insert response, then persist tags by inserting into your
join/association (e.g., _supabase.from('task_tag').insert) using that task id
and each tag id or name; if you intend to store tags as a JSON/array column on
the task row instead, write the tags list (e.g., tags.map(...).toList()) into
that column in the same insert. Ensure you handle failures and keep
_isLoading/notifyListeners consistent.

final categoryViewModel = context.watch<CategoryViewModel>();
final tagViewModel = context.watch<TagViewModel>();
String formattedDate = DateFormat('EEEE, d MMMM').format(_selectedDate);
String formattedDate = DateFormat('EEEE, d MMMM').format(context.read<CreateTaskProvider>().selectedDate);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm CreateTaskProvider is registered in the widget tree.
# Expectation: At least one ChangeNotifierProvider/CreateTaskProvider registration exists above CreateTaskScreen.
rg -n -C3 'ChangeNotifierProvider<\s*CreateTaskProvider|Provider<\s*CreateTaskProvider|create:\s*\([^)]*\)\s*=>\s*CreateTaskProvider|MultiProvider'

Repository: tqha1011/TaskManagement

Length of output: 394


🏁 Script executed:

# First, search for CreateTaskProvider definition and usages
rg -n 'CreateTaskProvider' --type dart

Repository: tqha1011/TaskManagement

Length of output: 1831


🏁 Script executed:

# Get the full MultiProvider setup from main.dart
cat -n src/lib/main.dart | head -100

Repository: tqha1011/TaskManagement

Length of output: 3694


🏁 Script executed:

# Check the specific lines in create_task_screen.dart
sed -n '185,200p' src/lib/features/tasks/view/screens/create_task_screen.dart

Repository: tqha1011/TaskManagement

Length of output: 643


🏁 Script executed:

# Search for navigation to CreateTaskScreen
rg -n 'CreateTaskScreen|Navigator.push|pushNamed.*create' --type dart -C 2

Repository: tqha1011/TaskManagement

Length of output: 6122


🏁 Script executed:

# Look for any route definitions or wrapping of CreateTaskScreen
rg -n 'ChangeNotifierProvider.*CreateTaskProvider|Provider.*CreateTaskProvider' --type dart -B2 -A2

Repository: tqha1011/TaskManagement

Length of output: 49


🏁 Script executed:

# Check if there are route definitions or named routes
fd -n 'routes\|app_routes' --type dart

Repository: tqha1011/TaskManagement

Length of output: 297


🏁 Script executed:

# Read the CreateTaskProvider definition
sed -n '28,80p' src/lib/features/tasks/view/screens/create_task_screen.dart

Repository: tqha1011/TaskManagement

Length of output: 1487


🏁 Script executed:

# Check if CreateTaskProvider is wrapped around the screen elsewhere
rg -n 'ChangeNotifierProvider.*CreateTaskProvider' src/lib/features/tasks/view/screens/ -A 5

Repository: tqha1011/TaskManagement

Length of output: 49


Register CreateTaskProvider in the widget tree before accessing it.

CreateTaskScreen reads CreateTaskProvider via context.read() at lines 193, 296–310, and 508–510, but the provider is not registered. The navigation in home_screen.dart (line 83) uses a plain MaterialPageRoute without wrapping CreateTaskProvider, and the root MultiProvider in main.dart does not include it. This will throw ProviderNotFoundException at runtime.

Either add ChangeNotifierProvider<CreateTaskProvider>(create: (_) => CreateTaskProvider()) to the root MultiProvider, or wrap CreateTaskScreen with the provider when navigating.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/view/screens/create_task_screen.dart` at line 193,
CreateTaskScreen is calling context.read<CreateTaskProvider>() (e.g., in
CreateTaskScreen where formattedDate is computed and at other occurrences) but
CreateTaskProvider is not registered, causing ProviderNotFoundException; fix by
registering the provider - either add
ChangeNotifierProvider<CreateTaskProvider>(create: (_) => CreateTaskProvider())
to your app's root MultiProvider so the provider is available app-wide, or wrap
the screen when navigating (replace MaterialPageRoute(builder: (_) =>
CreateTaskScreen()) with MaterialPageRoute(builder: (_) =>
ChangeNotifierProvider(create: (_) => CreateTaskProvider(), child:
CreateTaskScreen())) so CreateTaskProvider is in the widget tree before any
context.read()/watch() calls.

Comment on lines +52 to +72
String? addCustomTag(String name) {
name = name.trim();
if (name.isEmpty) return 'Tên tag không được để trống';
if (name.length > _maxCustomTagLength)
return 'Tối đa $_maxCustomTagLength ký tự';
if (_customTags.length >= _maxCustomTags)
return 'Tối đa $_maxCustomTags tag custom';
if (_customTags.any((t) => t.name.toLowerCase() == name.toLowerCase())) {
return 'Tag đã tồn tại';
}
_customTags.add(
TagModel(
id: DateTime.now().millisecondsSinceEpoch,
name: name,
colorCode: '#FF9800',
profileId: '',
),
);
_saveCustomTags();
notifyListeners();
return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Await custom-tag persistence before reporting success.

_saveCustomTags() is fire-and-forget, so addCustomTag() can return null and notify listeners even if SharedPreferences fails; the tag then disappears on restart.

Proposed fix
-  String? addCustomTag(String name) {
+  Future<String?> addCustomTag(String name) async {
     name = name.trim();
     if (name.isEmpty) return 'Tên tag không được để trống';
     if (name.length > _maxCustomTagLength)
       return 'Tối đa $_maxCustomTagLength ký tự';
@@
-    _saveCustomTags();
+    await _saveCustomTags();
     notifyListeners();
     return null;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String? addCustomTag(String name) {
name = name.trim();
if (name.isEmpty) return 'Tên tag không được để trống';
if (name.length > _maxCustomTagLength)
return 'Tối đa $_maxCustomTagLength ký tự';
if (_customTags.length >= _maxCustomTags)
return 'Tối đa $_maxCustomTags tag custom';
if (_customTags.any((t) => t.name.toLowerCase() == name.toLowerCase())) {
return 'Tag đã tồn tại';
}
_customTags.add(
TagModel(
id: DateTime.now().millisecondsSinceEpoch,
name: name,
colorCode: '#FF9800',
profileId: '',
),
);
_saveCustomTags();
notifyListeners();
return null;
Future<String?> addCustomTag(String name) async {
name = name.trim();
if (name.isEmpty) return 'Tên tag không được để trống';
if (name.length > _maxCustomTagLength)
return 'Tối đa $_maxCustomTagLength ký tự';
if (_customTags.length >= _maxCustomTags)
return 'Tối đa $_maxCustomTags tag custom';
if (_customTags.any((t) => t.name.toLowerCase() == name.toLowerCase())) {
return 'Tag đã tồn tại';
}
_customTags.add(
TagModel(
id: DateTime.now().millisecondsSinceEpoch,
name: name,
colorCode: '#FF9800',
profileId: '',
),
);
await _saveCustomTags();
notifyListeners();
return null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 52 - 72,
The method addCustomTag currently calls fire-and-forget _saveCustomTags(),
causing callers to see success even if persistence fails; change addCustomTag to
be asynchronous (Future<String?>) and await _saveCustomTags(), and make
_saveCustomTags return a Future<bool> or throw on failure; after adding the
TagModel to _customTags, await _saveCustomTags(), and if it fails remove the
recently added tag from _customTags, avoid calling notifyListeners() on failure
and return an error message (or rethrow) so callers see the persistence failure;
keep references: addCustomTag, _saveCustomTags, _customTags, notifyListeners.

Comment on lines +140 to +154
Future<void> fetchTasks() async {
final supabase = Supabase.instance.client;
final user = supabase.auth.currentUser;

if (user == null) return;

try {
final data = await supabase
.from('task')
.select('*')
.eq('profile_id', user.id)
.order('create_at', ascending: true);

if (data != null) {
_tasks.clear();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear task state when there is no authenticated user.

Line 144 returns without clearing _tasks, so a logout/session-expiry path can keep showing the previous user’s tasks.

Proposed fix
-    if (user == null) return; 
+    if (user == null) {
+      _tasks.clear();
+      notifyListeners();
+      return;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<void> fetchTasks() async {
final supabase = Supabase.instance.client;
final user = supabase.auth.currentUser;
if (user == null) return;
try {
final data = await supabase
.from('task')
.select('*')
.eq('profile_id', user.id)
.order('create_at', ascending: true);
if (data != null) {
_tasks.clear();
Future<void> fetchTasks() async {
final supabase = Supabase.instance.client;
final user = supabase.auth.currentUser;
if (user == null) {
_tasks.clear();
notifyListeners();
return;
}
try {
final data = await supabase
.from('task')
.select('*')
.eq('profile_id', user.id)
.order('create_at', ascending: true);
if (data != null) {
_tasks.clear();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 140 - 154,
fetchTasks currently returns early if Supabase.auth.currentUser is null but
doesn't clear previous state; update the fetchTasks method to clear _tasks (and
call notifyListeners() if this ViewModel uses ChangeNotifier) before returning
when user is null so the UI no longer shows another user's tasks after
logout/session expiry; specifically modify fetchTasks to check user == null,
then call _tasks.clear() (and notifyListeners() / update any reactive state) and
then return.

Comment on lines +147 to +175
final data = await supabase
.from('task')
.select('*')
.eq('profile_id', user.id)
.order('create_at', ascending: true);

if (data != null) {
_tasks.clear();

for (var item in data) {
// 1. Chuyển đổi Priority trực tiếp
Priority p = Priority.medium;
if (item['priority'] == 1) p = Priority.urgent;
else if (item['priority'] == 2) p = Priority.high;
else if (item['priority'] == 4) p = Priority.low;

// 2. Nhét data thẳng vào TaskModel luôn, đách cần fromJson nữa
_tasks.add(TaskModel(
id: item['id'].toString(),
title: item['title'] ?? 'Task mới',
description: item['description'] ?? '',

// CHÍNH LÀ CHỖ NÀY: Khởi tạo cục CategoryModel đàng hoàng
category: CategoryModel(
id: 0, // Nhét số 0 vào làm ID ảo
name: item['category']?.toString() ?? 'General', // Lấy tên từ database, nếu rỗng thì cho chữ General
colorCode: '#5A8DF3', // Lấy màu mặc định
profileId: '', // Bỏ trống
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve the selected category when reloading tasks.

fetchTasks() selects from task and builds the category from item['category'], but task creation stores category_id; after reload, tasks fall back to "General" with fake id 0.

Fetch/join the category row or at least map category_id consistently before creating CategoryModel.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 147 - 175,
fetchTasks() currently builds CategoryModel from item['category'] string so
reloaded tasks lose their real category; change data retrieval to include the
category row (or at least the category_id) and map it into CategoryModel instead
of using a fallback "General". Specifically, modify the Supabase select to fetch
the related category fields (or fetch category by item['category_id']) and then
construct CategoryModel with the real id, name, colorCode and profileId when
creating TaskModel in fetchTasks(), ensuring you use the actual
item['category_id'] and returned category columns rather than item['category'].

Comment on lines +195 to +203
Future<void> updateTask(dynamic taskId, Map<String, dynamic> data) async {
final _supabase = Supabase.instance.client;
try {
await _supabase
.from('task')
.update(data) // Data ở đây sẽ chứa {'title': '...', 'category_id': ...}
.eq('id', taskId);

notifyListeners(); // Để màn hình Home load lại dữ liệu mới
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Refresh or patch local state after updating a task.

updateTask() only writes to Supabase and calls notifyListeners(), but _tasks remains unchanged, so listeners rebuild with stale title/category data until another explicit fetch happens.

Proposed fix
     await _supabase
         .from('task')
         .update(data) // Data ở đây sẽ chứa {'title': '...', 'category_id': ...}
         .eq('id', taskId);
     
+    await fetchTasks();
-    notifyListeners(); // Để màn hình Home load lại dữ liệu mới
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 195 - 203,
updateTask currently updates Supabase but leaves the in-memory _tasks list
stale; after the DB update in updateTask, locate the task in the _tasks
collection by matching its id (taskId), merge/patch the updated fields from data
into that task object (e.g., update title and category_id), replace or update
the entry in _tasks, and then call notifyListeners(); if the task isn't found,
fall back to fetching/refreshing tasks (or insert the patched task) to keep
local state consistent with the server.

Comment on lines +209 to +214
Future<void> deleteTask(String taskId) async {
final supabase = Supabase.instance.client;
try {
await supabase.from('task').delete().eq('id', taskId);
// Gọi fetch lại để làm mới danh sách
fetchTasks();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Await the refresh after deletion.

deleteTask() completes before fetchTasks() finishes, so callers awaiting deletion can still observe the old list.

Proposed fix
       await supabase.from('task').delete().eq('id', taskId);
       // Gọi fetch lại để làm mới danh sách
-      fetchTasks();
+      await fetchTasks();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<void> deleteTask(String taskId) async {
final supabase = Supabase.instance.client;
try {
await supabase.from('task').delete().eq('id', taskId);
// Gọi fetch lại để làm mới danh sách
fetchTasks();
Future<void> deleteTask(String taskId) async {
final supabase = Supabase.instance.client;
try {
await supabase.from('task').delete().eq('id', taskId);
// Gọi fetch lại để làm mới danh sách
await fetchTasks();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 209 - 214,
The deleteTask method returns before the task list is refreshed; update
deleteTask (and any call sites expecting completion) to await the refresh by
awaiting fetchTasks() after the delete completes (i.e., change the call inside
deleteTask to await fetchTasks()); keep the try/catch and existing awaits on
Supabase but ensure fetchTasks() is awaited so callers of deleteTask see the
updated list.

@tqha1011 tqha1011 merged commit fa93e4a into main Apr 18, 2026
2 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.

4 participants