Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 9 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
# Task Management App - Life Organizer
## 📁 Chi tiết các file thay đổi

## 1. Introduction
This repository contains the source code for the **Task Management App (Life Organizer)**, developed as a project for the **SE346** course at the University of Information Technology - VNUHCM (UIT).
* **`task_model.dart`**: Thêm thuộc tính `Priority` và tích hợp `TagModel`.
* **`task_viewmodel.dart`**: Cập nhật logic xử lý trạng thái cho Provider.
* **`priority_selector.dart`**: Thêm UI Widget hỗ trợ chọn mức độ ưu tiên.
* **`tag_selector.dart`**: Thêm UI Widget hỗ trợ chọn các loại tag.
* **`create_task_screen.dart`**: Tích hợp 2 widget chọn Priority và Tag vào form tạo nhiệm vụ.
* **`main.dart`**: Đăng ký Provider mới cho hệ thống.
* **`home_screen.dart`**: Cập nhật giao diện, hiển thị danh sách task được nhóm theo mức độ ưu tiên.

Going beyond a traditional to-do list, this application is designed to be a comprehensive personal assistant. It helps users easily organize various life contexts, such as tracking utility bills, managing grocery shopping lists, building daily habits, and handling household chores efficiently.

## 2. Authors
This project is built and maintained by a dedicated team of 4 members:
* [Trần Quang Hạ](https://github.com/tqha1011)
* [Nguyễn Lê Hoàng Hảo](https://github.com/hoanghaoz)
* [Nguyễn Anh Kiệt](https://github.com/anhkietbienhoa-crypto)
* [Nguyễn Trí Kiệt](https://github.com/Ender-Via)

## 3. Tech Stack
The application is built with a focus on performance, security, and clean code principles, strictly following the **Feature-Based MVVM** architecture:
* **Frontend:** Flutter
* **Backend & Database:** Supabase (PostgreSQL, Auth, Row Level Security)
* **Code Quality & CI/CD:** GitHub Actions, SonarCloud

## 4. Documentation
For a deeper dive into our system design and development workflows, please explore the attached documentation:
* [Flutter App Architecture](documentation/architecture/flutter-architecture.md)
* [Database Schema & ERD](documentation/architecture/database-schema.md)
* [Git & Conventional Commits Guidelines](documentation/guidelines/conventional-commit.md)
<img width="464" height="195" alt="image" src="https://github.com/user-attachments/assets/7a2244b4-82e4-4354-8029-1559c6c7766e" />
62 changes: 60 additions & 2 deletions src/lib/features/tasks/model/task_model.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,69 @@
import 'package:flutter/material.dart';

// ─── Priority Enum ───────────────────────────────────────────
enum Priority { low, medium, high, urgent }

extension PriorityExtension on Priority {
String get label {
switch (this) {
case Priority.low:
return 'Low';
case Priority.medium:
return 'Medium';
case Priority.high:
return 'High';
case Priority.urgent:
return 'Urgent';
}
}

Color get color {
switch (this) {
case Priority.low:
return const Color(0xFF4CAF50); // xanh lá
case Priority.medium:
return const Color(0xFF2196F3); // xanh dương
case Priority.high:
return const Color(0xFFFF9800); // cam
case Priority.urgent:
return const Color(0xFFF44336); // đỏ
}
}

IconData get icon {
switch (this) {
case Priority.low:
return Icons.flag_outlined;
case Priority.medium:
return Icons.flag;
case Priority.high:
return Icons.flag;
case Priority.urgent:
return Icons.flag;
}
}
}

// ─── Tag Model ───────────────────────────────────────────────
class TagModel {
final String id;
final String name;
final Color color;

const TagModel({required this.id, required this.name, required this.color});
}

// ─── Task Model ──────────────────────────────────────────────
class TaskModel {
final String id; // ID duy nhất để làm Hero tag và gọi API sau này
final String id;
String title;
String description;
String category;
TimeOfDay startTime;
TimeOfDay endTime;
DateTime date;
Priority priority;
List<TagModel> tags;

TaskModel({
required this.id,
Expand All @@ -17,5 +73,7 @@ class TaskModel {
required this.startTime,
required this.endTime,
required this.date,
this.priority = Priority.medium,
this.tags = const [],
});
}
}
157 changes: 133 additions & 24 deletions src/lib/features/tasks/view/screens/create_task_screen.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/widgets/custom_input_field.dart';
import '../../model/task_model.dart';
import '../../viewmodel/task_viewmodel.dart';
import '../widgets/task_widgets.dart';
import '../widgets/priority_selector.dart';
import '../widgets/tag_selector.dart';

class CreateTaskScreen extends StatefulWidget {
const CreateTaskScreen({super.key});
Expand All @@ -11,8 +17,12 @@ class CreateTaskScreen extends StatefulWidget {
}

class _CreateTaskScreenState extends State<CreateTaskScreen> {
final TextEditingController _nameController = TextEditingController(text: 'Team Meeting');
final TextEditingController _descController = TextEditingController(text: 'Discuss all questions about new projects');
final TextEditingController _nameController = TextEditingController(
text: 'Team Meeting',
);
final TextEditingController _descController = TextEditingController(
text: 'Discuss all questions about new projects',
);
DateTime _selectedDate = DateTime.now();
TimeOfDay _startTime = const TimeOfDay(hour: 10, minute: 0);
TimeOfDay _endTime = const TimeOfDay(hour: 11, minute: 0);
Expand All @@ -28,15 +38,20 @@ class _CreateTaskScreenState extends State<CreateTaskScreen> {
body: SafeArea(
child: Column(
children: [
// ─── Header ───────────────────────────────────────
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(30), bottomRight: Radius.circular(30)),
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10, offset: const Offset(0, 5))
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
Expand All @@ -61,25 +76,46 @@ class _CreateTaskScreenState extends State<CreateTaskScreen> {
],
),
),

// ─── Body ─────────────────────────────────────────
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(25.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Create New Task', style: Theme.of(context).textTheme.headlineMedium),
Text(
'Create New Task',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 25),
CustomInputField(label: 'Task Name', hint: 'Enter task name', controller: _nameController),

// Task Name
CustomInputField(
label: 'Task Name',
hint: 'Enter task name',
controller: _nameController,
),
const SizedBox(height: 20),
Text('Select Category', style: Theme.of(context).textTheme.labelLarge),

// Category
Text(
'Select Category',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 10),
SizedBox(
height: 40,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 4,
itemBuilder: (context, index) {
List<String> categories = ['Development', 'Research', 'Design', 'Backend'];
List<String> categories = [
'Development',
'Research',
'Design',
'Backend',
];
bool isSelected = index == _selectedCategoryIndex;
return Padding(
padding: const EdgeInsets.only(right: 10),
Expand Down Expand Up @@ -112,20 +148,38 @@ class _CreateTaskScreenState extends State<CreateTaskScreen> {
),
),
const SizedBox(height: 20),

// ─── PRIORITY SELECTOR (MỚI) ──────────────
const PrioritySelector(),
const SizedBox(height: 20),

// ─── TAG SELECTOR (MỚI) ───────────────────
const TagSelector(),
Comment on lines +152 to +157
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

🧩 Analysis chain

🏁 Script executed:

# First, let's check if main.dart exists and look for TaskViewModel provision
find . -type f -name "main.dart" | head -5

Repository: tqha1011/TaskManagement

Length of output: 86


🏁 Script executed:

# Search for TaskViewModel provider setup
rg -i "taskviewmodel" --type dart | head -20

Repository: tqha1011/TaskManagement

Length of output: 857


🏁 Script executed:

# Check the create_task_screen.dart file structure
cat -n src/lib/features/tasks/view/screens/create_task_screen.dart | head -50

Repository: tqha1011/TaskManagement

Length of output: 2234


🏁 Script executed:

# Search for reset() calls in create_task_screen.dart
rg "reset" src/lib/features/tasks/view/screens/create_task_screen.dart -n

Repository: tqha1011/TaskManagement

Length of output: 115


🏁 Script executed:

# Read lines 140-160 to see context around lines 145-150
sed -n '140,160p' src/lib/features/tasks/view/screens/create_task_screen.dart

Repository: tqha1011/TaskManagement

Length of output: 843


🏁 Script executed:

# Read lines 260-295 to see the "Also applies to" section
sed -n '260,295p' src/lib/features/tasks/view/screens/create_task_screen.dart

Repository: tqha1011/TaskManagement

Length of output: 1820


🏁 Script executed:

# Check main.dart for the full provider setup and scope
cat -n src/lib/main.dart | grep -A 10 -B 5 "TaskViewModel"

Repository: tqha1011/TaskManagement

Length of output: 702


🏁 Script executed:

# Check if there's an initState() method already in _CreateTaskScreenState
sed -n '19,100p' src/lib/features/tasks/view/screens/create_task_screen.dart | grep -A 5 "initState"

Repository: tqha1011/TaskManagement

Length of output: 49


🏁 Script executed:

# Look at the full dispose/lifecycle methods
rg "dispose|initState|deactivate" src/lib/features/tasks/view/screens/create_task_screen.dart -A 3

Repository: tqha1011/TaskManagement

Length of output: 49


🏁 Script executed:

# Get more context around where reset() is called - show the full button handler
sed -n '265,300p' src/lib/features/tasks/view/screens/create_task_screen.dart

Repository: tqha1011/TaskManagement

Length of output: 1865


🏁 Script executed:

# Check TaskViewModel.reset() method to understand what it does
rg -A 10 "void reset" src/lib/features/tasks/viewmodel/task_viewmodel.dart

Repository: tqha1011/TaskManagement

Length of output: 337


🏁 Script executed:

# Check if there's any WillPopScope or onBack handling
rg "WillPopScope|onPopInvoked|pop|Navigator" src/lib/features/tasks/view/screens/create_task_screen.dart -n

Repository: tqha1011/TaskManagement

Length of output: 184


🏁 Script executed:

# Let's see the entire _CreateTaskScreenState class to understand its full structure
wc -l src/lib/features/tasks/view/screens/create_task_screen.dart

Repository: tqha1011/TaskManagement

Length of output: 130


🏁 Script executed:

# Read more of the file to see if there are any other reset patterns
sed -n '1,50p' src/lib/features/tasks/view/screens/create_task_screen.dart

Repository: tqha1011/TaskManagement

Length of output: 1884


🏁 Script executed:

# Check how the CreateTaskScreen is navigated to
rg "CreateTaskScreen" src/lib/features/tasks/view/screens/ -n

Repository: tqha1011/TaskManagement

Length of output: 667


🏁 Script executed:

# Check the back button at line 64 - what does it do exactly?
sed -n '55,75p' src/lib/features/tasks/view/screens/create_task_screen.dart

Repository: tqha1011/TaskManagement

Length of output: 926


🏁 Script executed:

# Check if PrioritySelector and TagSelector have their own state management
cat -n src/lib/features/tasks/view/widgets/priority_selector.dart | head -40

Repository: tqha1011/TaskManagement

Length of output: 1959


🏁 Script executed:

# Look for any other screens or routes that might open CreateTaskScreen
rg "CreateTaskScreen\|push\|pushNamed" src/lib/features/tasks/view/screens/home_screen.dart -B 2 -A 2

Repository: tqha1011/TaskManagement

Length of output: 49


Reset provider state when the form opens.

TaskViewModel is app-scoped in src/lib/main.dart. The back button (line 64) closes the form without calling reset(), so previously selected priority/tags persist when the form reopens. Only the successful create path (line 287) calls reset().

Suggested fix
 class _CreateTaskScreenState extends State<CreateTaskScreen> {
+  `@override`
+  void initState() {
+    super.initState();
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      if (mounted) context.read<TaskViewModel>().reset();
+    });
+  }
🤖 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 145
- 150, TaskViewModel is app-scoped so previous selections persist; call
TaskViewModel.reset() when the form opens (not only on successful create). Add a
reset invocation in the screen's entry point—e.g. in CreateTaskScreen's
initState() (or the equivalent hook where the widget is created) call
context.read<TaskViewModel>().reset() (or ref.read(TaskViewModel).reset()
depending on your provider API) so PrioritySelector and TagSelector start with a
clean state whenever the form is presented.

const SizedBox(height: 20),

// Date
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Date', style: Theme.of(context).textTheme.labelLarge),
Text(
'Date',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 5),
InkWell(
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context, initialDate: _selectedDate,
firstDate: DateTime(2000), lastDate: DateTime(2100));
if (picked != null) setState(() => _selectedDate = picked);
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() => _selectedDate = picked);
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
Expand All @@ -146,7 +200,7 @@ class _CreateTaskScreenState extends State<CreateTaskScreen> {
)
],
),
)
),
],
),
Container(
Expand All @@ -160,15 +214,24 @@ class _CreateTaskScreenState extends State<CreateTaskScreen> {
],
),
const SizedBox(height: 25),

// Time
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Start time', style: Theme.of(context).textTheme.labelLarge),
Text(
'Start time',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 5),
TimePickerWidget(time: _startTime, onChanged: (newTime) => setState(() => _startTime = newTime)),
TimePickerWidget(
time: _startTime,
onChanged: (t) =>
setState(() => _startTime = t),
),
],
),
),
Expand All @@ -177,27 +240,73 @@ class _CreateTaskScreenState extends State<CreateTaskScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('End time', style: Theme.of(context).textTheme.labelLarge),
Text(
'End time',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 5),
TimePickerWidget(time: _endTime, onChanged: (newTime) => setState(() => _endTime = newTime)),
TimePickerWidget(
time: _endTime,
onChanged: (t) => setState(() => _endTime = t),
),
],
),
),
],
),
const SizedBox(height: 25),
CustomInputField(label: 'Description', hint: 'Enter task description', controller: _descController, maxLines: 2),

// Description
CustomInputField(
label: 'Description',
hint: 'Enter task description',
controller: _descController,
maxLines: 2,
),
const SizedBox(height: 40),

// ─── Create Button ────────────────────────
Center(
child: ElevatedButton(
onPressed: () {},
onPressed: () {
final viewModel = context.read<TaskViewModel>();
final List<String> categories = [
'Development',
'Research',
'Design',
'Backend',
];
final newTask = TaskModel(
id: DateTime.now().millisecondsSinceEpoch
.toString(),
title: _nameController.text,
description: _descController.text,
category: categories[_selectedCategoryIndex],
startTime: _startTime,
endTime: _endTime,
date: _selectedDate,
priority: viewModel.selectedPriority,
tags: List.from(viewModel.selectedTags),
);
viewModel.addTask(newTask);
viewModel.reset();
Navigator.pop(context);
},
Comment on lines +271 to +294
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

Validate the time range before creating the task.

Nothing prevents endTime from being earlier than or equal to startTime, so this can save impossible tasks. Guard it before addTask(...) and show feedback instead of creating the item.

Suggested fix
                         onPressed: () {
                           final viewModel = context.read<TaskViewModel>();
+                          final startMinutes =
+                              _startTime.hour * 60 + _startTime.minute;
+                          final endMinutes =
+                              _endTime.hour * 60 + _endTime.minute;
+                          if (endMinutes <= startMinutes) {
+                            ScaffoldMessenger.of(context).showSnackBar(
+                              const SnackBar(
+                                content: Text('End time must be after start time'),
+                              ),
+                            );
+                            return;
+                          }
                           final List<String> categories = [
                             'Development',
                             'Research',
                             'Design',
                             'Backend',
📝 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
onPressed: () {
final viewModel = context.read<TaskViewModel>();
final List<String> categories = [
'Development',
'Research',
'Design',
'Backend',
];
final newTask = TaskModel(
id: DateTime.now().millisecondsSinceEpoch
.toString(),
title: _nameController.text,
description: _descController.text,
category: categories[_selectedCategoryIndex],
startTime: _startTime,
endTime: _endTime,
date: _selectedDate,
priority: viewModel.selectedPriority,
tags: List.from(viewModel.selectedTags),
);
viewModel.addTask(newTask);
viewModel.reset();
Navigator.pop(context);
},
onPressed: () {
final viewModel = context.read<TaskViewModel>();
final startMinutes =
_startTime.hour * 60 + _startTime.minute;
final endMinutes =
_endTime.hour * 60 + _endTime.minute;
if (endMinutes <= startMinutes) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('End time must be after start time'),
),
);
return;
}
final List<String> categories = [
'Development',
'Research',
'Design',
'Backend',
];
final newTask = TaskModel(
id: DateTime.now().millisecondsSinceEpoch
.toString(),
title: _nameController.text,
description: _descController.text,
category: categories[_selectedCategoryIndex],
startTime: _startTime,
endTime: _endTime,
date: _selectedDate,
priority: viewModel.selectedPriority,
tags: List.from(viewModel.selectedTags),
);
viewModel.addTask(newTask);
viewModel.reset();
Navigator.pop(context);
},
🤖 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 266
- 289, Before calling viewModel.addTask(...) in the onPressed handler, validate
that _endTime is after _startTime (not equal or earlier); if the check fails, do
not call viewModel.addTask or viewModel.reset, and instead show user feedback
(e.g., ScaffoldMessenger.of(context).showSnackBar or similar) explaining the
time range is invalid. Update the onPressed block in create_task_screen.dart
around the existing onPressed closure so it checks _startTime and _endTime,
returns early on invalid range, and only constructs TaskModel and calls
TaskViewModel.addTask/reset when the times are valid.

style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 100, vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
padding: const EdgeInsets.symmetric(
horizontal: 100,
vertical: 15,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: const Text(
'Create Task',
style: TextStyle(fontSize: 18),
),
child: const Text('Create Task', style: TextStyle(fontSize: 18)),
),
),
],
Expand All @@ -209,4 +318,4 @@ class _CreateTaskScreenState extends State<CreateTaskScreen> {
),
);
}
}
}
Loading
Loading