diff --git a/README.md b/README.md
index a4e4753..2cd5439 100644
--- a/README.md
+++ b/README.md
@@ -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)
\ No newline at end of file
+
diff --git a/src/lib/features/tasks/model/task_model.dart b/src/lib/features/tasks/model/task_model.dart
index 8158b08..a6a64fb 100644
--- a/src/lib/features/tasks/model/task_model.dart
+++ b/src/lib/features/tasks/model/task_model.dart
@@ -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 tags;
TaskModel({
required this.id,
@@ -17,5 +73,7 @@ class TaskModel {
required this.startTime,
required this.endTime,
required this.date,
+ this.priority = Priority.medium,
+ this.tags = const [],
});
-}
\ No newline at end of file
+}
diff --git a/src/lib/features/tasks/view/screens/create_task_screen.dart b/src/lib/features/tasks/view/screens/create_task_screen.dart
index 43ad594..311fd00 100644
--- a/src/lib/features/tasks/view/screens/create_task_screen.dart
+++ b/src/lib/features/tasks/view/screens/create_task_screen.dart
@@ -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});
@@ -11,8 +17,12 @@ class CreateTaskScreen extends StatefulWidget {
}
class _CreateTaskScreenState extends State {
- 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);
@@ -28,15 +38,20 @@ class _CreateTaskScreenState extends State {
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),
@@ -61,17 +76,33 @@ class _CreateTaskScreenState extends State {
],
),
),
+
+ // ─── 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,
@@ -79,7 +110,12 @@ class _CreateTaskScreenState extends State {
scrollDirection: Axis.horizontal,
itemCount: 4,
itemBuilder: (context, index) {
- List categories = ['Development', 'Research', 'Design', 'Backend'];
+ List categories = [
+ 'Development',
+ 'Research',
+ 'Design',
+ 'Backend',
+ ];
bool isSelected = index == _selectedCategoryIndex;
return Padding(
padding: const EdgeInsets.only(right: 10),
@@ -112,20 +148,38 @@ class _CreateTaskScreenState extends State {
),
),
const SizedBox(height: 20),
+
+ // ─── PRIORITY SELECTOR (MỚI) ──────────────
+ const PrioritySelector(),
+ const SizedBox(height: 20),
+
+ // ─── TAG SELECTOR (MỚI) ───────────────────
+ const TagSelector(),
+ 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,
@@ -146,7 +200,7 @@ class _CreateTaskScreenState extends State {
)
],
),
- )
+ ),
],
),
Container(
@@ -160,15 +214,24 @@ class _CreateTaskScreenState extends State {
],
),
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),
+ ),
],
),
),
@@ -177,27 +240,73 @@ class _CreateTaskScreenState extends State {
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();
+ final List 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);
+ },
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)),
),
),
],
@@ -209,4 +318,4 @@ class _CreateTaskScreenState extends State {
),
);
}
-}
\ No newline at end of file
+}
diff --git a/src/lib/features/tasks/view/screens/home_screen.dart b/src/lib/features/tasks/view/screens/home_screen.dart
index 0232c40..882458d 100644
--- a/src/lib/features/tasks/view/screens/home_screen.dart
+++ b/src/lib/features/tasks/view/screens/home_screen.dart
@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
+import 'package:provider/provider.dart';
+import '../../../../core/theme/app_colors.dart';
import '../../model/task_model.dart';
+import '../../viewmodel/task_viewmodel.dart';
import '../widgets/task_widgets.dart';
import 'create_task_screen.dart';
@@ -10,46 +13,41 @@ class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
String formattedDate = DateFormat('EEEE, d MMMM').format(DateTime.now());
+ final viewModel = context.watch();
final isDark = Theme.of(context).brightness == Brightness.dark;
- // --- TẠO DỮ LIỆU GIẢ LẬP (MOCK DATA) ---
- final task1 = TaskModel(
- id: '1', // ID dùng để làm tag cho Hero Animation
- title: 'Team Meeting',
- description: 'Discuss all questions about new projects',
- category: 'Development',
- startTime: const TimeOfDay(hour: 10, minute: 0),
- endTime: const TimeOfDay(hour: 11, minute: 0),
- date: DateTime.now(),
- );
-
- final task2 = TaskModel(
- id: '2', // ID phải khác nhau
- title: 'Call the stylist',
- description: 'Agree on an evening look',
- category: 'Design',
- startTime: const TimeOfDay(hour: 11, minute: 0),
- endTime: const TimeOfDay(hour: 12, minute: 0),
- date: DateTime.now(),
- );
- // ----------------------------------------
+ // Nhóm task theo priority
+ Map> grouped = {};
+ for (var priority in Priority.values.reversed) {
+ final tasks = viewModel.tasks
+ .where((t) => t.priority == priority)
+ .toList();
+ if (tasks.isNotEmpty) grouped[priority] = tasks;
+ }
return Scaffold(
body: Stack(
children: [
Positioned(
- top: 0, left: 0, right: 0, height: 250,
+ top: 0,
+ left: 0,
+ right: 0,
+ height: 250,
child: ClipPath(
clipper: TopWaveClipper(),
- child: Container(color: Theme.of(context).colorScheme.primary),
+ child: Container(color: AppColors.primaryBlue),
),
),
SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
+ // ─── Header ───────────────────────────────────
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 10,
+ ),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -70,7 +68,9 @@ class HomeScreen extends StatelessWidget {
const SizedBox(width: 15),
const CircleAvatar(
radius: 20,
- backgroundImage: NetworkImage('https://i.pravatar.cc/150?u=user1'),
+ backgroundImage: NetworkImage(
+ 'https://i.pravatar.cc/150?u=user1',
+ ),
),
const SizedBox(width: 10),
IconButton(
@@ -87,7 +87,8 @@ class HomeScreen extends StatelessWidget {
],
),
),
- const SizedBox(height: 10),
+
+ // ─── Date Card ────────────────────────────────
Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 20),
@@ -103,7 +104,10 @@ class HomeScreen extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text('My Task', style: Theme.of(context).textTheme.headlineMedium),
+ Text(
+ 'My Task',
+ style: Theme.of(context).textTheme.headlineMedium,
+ ),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -125,8 +129,13 @@ class HomeScreen extends StatelessWidget {
scrollDirection: Axis.horizontal,
itemCount: 10,
itemBuilder: (context, index) {
- DateTime date = DateTime.now().add(Duration(days: index));
- return DateBox(date: date, isSelected: index == 0);
+ DateTime date = DateTime.now().add(
+ Duration(days: index),
+ );
+ return DateBox(
+ date: date,
+ isSelected: index == 0,
+ );
},
),
),
@@ -134,11 +143,40 @@ class HomeScreen extends StatelessWidget {
),
),
),
- const SizedBox(height: 25),
- Expanded(
- child: ListView(
- padding: const EdgeInsets.symmetric(horizontal: 20),
+ const SizedBox(height: 15),
+
+ // ─── Filter Bar ───────────────────────────────
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Row(
children: [
+ // Sort button
+ GestureDetector(
+ onTap: () => viewModel.toggleSortByPriority(),
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 7,
+ ),
+ decoration: BoxDecoration(
+ color: viewModel.sortByPriority
+ ? AppColors.primaryBlue
+ : Colors.white,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(
+ color: AppColors.primaryBlue,
+ width: 1,
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ Icons.sort,
+ size: 16,
+ color: viewModel.sortByPriority
+ ? Colors.white
+ : AppColors.primaryBlue,
// --- SỬ DỤNG MOCK DATA VÀO TASKCARD ---
TaskCard(
task: task1, // Truyền task1 vào đây
@@ -161,30 +199,138 @@ class HomeScreen extends StatelessWidget {
color: Theme.of(context).colorScheme.primary,
),
),
- )
- ],
+ const SizedBox(width: 5),
+ Text(
+ 'Sort',
+ style: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ color: viewModel.sortByPriority
+ ? Colors.white
+ : AppColors.primaryBlue,
+ ),
+ ),
+ ],
+ ),
),
),
- TaskCard(
- task: task2, // Truyền task2 vào đây
- leading: Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: isDark
- ? Theme.of(context).colorScheme.surfaceContainerHighest
- : const Color(0xFFF1F7FD),
- borderRadius: BorderRadius.circular(15),
- ),
- child: Icon(
- Icons.call_outlined,
- color: Theme.of(context).colorScheme.primary,
+ const SizedBox(width: 8),
+
+ // Filter theo priority
+ Expanded(
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ children: [
+ // Chip "All"
+ _FilterChip(
+ label: 'All',
+ isSelected: viewModel.filterPriority == null,
+ color: AppColors.primaryBlue,
+ onTap: () => viewModel.setFilterPriority(null),
+ ),
+ const SizedBox(width: 8),
+ // Chip cho từng priority
+ ...Priority.values.map(
+ (p) => Padding(
+ padding: const EdgeInsets.only(right: 8),
+ child: _FilterChip(
+ label: p.label,
+ isSelected: viewModel.filterPriority == p,
+ color: p.color,
+ onTap: () => viewModel.setFilterPriority(p),
+ ),
+ ),
+ ),
+ ],
),
),
),
- // ----------------------------------------
],
),
),
+ const SizedBox(height: 10),
+
+ // ─── Task List nhóm theo Priority ─────────────
+ Expanded(
+ child: grouped.isEmpty
+ ? _buildEmptyState()
+ : ListView(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ children: grouped.entries.map((entry) {
+ final priority = entry.key;
+ final tasks = entry.value;
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header nhóm
+ Padding(
+ padding: const EdgeInsets.only(
+ bottom: 10,
+ top: 5,
+ ),
+ child: Row(
+ mainAxisAlignment:
+ MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ Container(
+ width: 4,
+ height: 20,
+ decoration: BoxDecoration(
+ color: priority.color,
+ borderRadius:
+ BorderRadius.circular(2),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ _priorityGroupLabel(priority),
+ style: const TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ Text(
+ '${tasks.length} task',
+ style: const TextStyle(
+ fontSize: 12,
+ color: AppColors.grayText,
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // Danh sách task trong nhóm
+ ...tasks.map(
+ (task) => TaskCard(
+ task: task,
+ leading: Container(
+ padding: const EdgeInsets.all(10),
+ decoration: BoxDecoration(
+ color: priority.color.withValues(
+ alpha: 0.1,
+ ),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Icon(
+ priority.icon,
+ color: priority.color,
+ size: 22,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(height: 10),
+ ],
+ );
+ }).toList(),
+ ),
+ ),
],
),
),
@@ -192,4 +338,81 @@ class HomeScreen extends StatelessWidget {
),
);
}
-}
\ No newline at end of file
+
+ String _priorityGroupLabel(Priority priority) {
+ switch (priority) {
+ case Priority.urgent:
+ return 'Ưu tiên Khẩn cấp';
+ case Priority.high:
+ return 'Ưu tiên Cao';
+ case Priority.medium:
+ return 'Ưu tiên Trung bình';
+ case Priority.low:
+ return 'Ưu tiên Thấp';
+ }
+ }
+
+ Widget _buildEmptyState() {
+ return const Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.checklist_rounded, size: 60, color: AppColors.grayText),
+ SizedBox(height: 12),
+ Text(
+ 'Chưa có task nào',
+ style: TextStyle(
+ fontSize: 16,
+ color: AppColors.grayText,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ SizedBox(height: 4),
+ Text(
+ 'Nhấn + để tạo task mới',
+ style: TextStyle(fontSize: 13, color: AppColors.grayText),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+// ─── Filter Chip Widget ──────────────────────────────────────
+class _FilterChip extends StatelessWidget {
+ final String label;
+ final bool isSelected;
+ final Color color;
+ final VoidCallback onTap;
+
+ const _FilterChip({
+ required this.label,
+ required this.isSelected,
+ required this.color,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap,
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
+ decoration: BoxDecoration(
+ color: isSelected ? color : Colors.white,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(color: color, width: 1),
+ ),
+ child: Text(
+ label,
+ style: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ color: isSelected ? Colors.white : color,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/src/lib/features/tasks/view/widgets/priority_selector.dart b/src/lib/features/tasks/view/widgets/priority_selector.dart
new file mode 100644
index 0000000..3beee4b
--- /dev/null
+++ b/src/lib/features/tasks/view/widgets/priority_selector.dart
@@ -0,0 +1,65 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '../../model/task_model.dart';
+import '../../viewmodel/task_viewmodel.dart';
+
+class PrioritySelector extends StatelessWidget {
+ const PrioritySelector({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final viewModel = context.watch();
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Priority', style: Theme.of(context).textTheme.labelLarge),
+ const SizedBox(height: 10),
+ Row(
+ children: Priority.values.map((priority) {
+ final isSelected = viewModel.selectedPriority == priority;
+ return Expanded(
+ child: Padding(
+ padding: const EdgeInsets.only(right: 8),
+ child: GestureDetector(
+ onTap: () => viewModel.setPriority(priority),
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ padding: const EdgeInsets.symmetric(vertical: 10),
+ decoration: BoxDecoration(
+ color: isSelected
+ ? priority.color
+ : const Color(0xFFF1F7FD),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: isSelected ? priority.color : Colors.transparent,
+ ),
+ ),
+ child: Column(
+ children: [
+ Icon(
+ priority.icon,
+ color: isSelected ? Colors.white : priority.color,
+ size: 20,
+ ),
+ const SizedBox(height: 4),
+ Text(
+ priority.label,
+ style: TextStyle(
+ fontSize: 11,
+ fontWeight: FontWeight.w600,
+ color: isSelected ? Colors.white : priority.color,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ ],
+ );
+ }
+}
diff --git a/src/lib/features/tasks/view/widgets/tag_selector.dart b/src/lib/features/tasks/view/widgets/tag_selector.dart
new file mode 100644
index 0000000..0ac54be
--- /dev/null
+++ b/src/lib/features/tasks/view/widgets/tag_selector.dart
@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '../../viewmodel/task_viewmodel.dart';
+
+class TagSelector extends StatelessWidget {
+ const TagSelector({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final viewModel = context.watch();
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Tags', style: Theme.of(context).textTheme.labelLarge),
+ const SizedBox(height: 10),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: viewModel.availableTags.map((tag) {
+ final isSelected = viewModel.isTagSelected(tag);
+ return GestureDetector(
+ onTap: () => viewModel.toggleTag(tag),
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 14,
+ vertical: 8,
+ ),
+ decoration: BoxDecoration(
+ color: isSelected
+ ? tag.color
+ : tag.color.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (isSelected) ...[
+ const Icon(Icons.check, color: Colors.white, size: 14),
+ const SizedBox(width: 4),
+ ],
+ Text(
+ tag.name,
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w500,
+ color: isSelected ? Colors.white : tag.color,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ ],
+ );
+ }
+}
diff --git a/src/lib/features/tasks/viewmodel/task_viewmodel.dart b/src/lib/features/tasks/viewmodel/task_viewmodel.dart
new file mode 100644
index 0000000..0917473
--- /dev/null
+++ b/src/lib/features/tasks/viewmodel/task_viewmodel.dart
@@ -0,0 +1,106 @@
+import 'package:flutter/material.dart';
+import '../model/task_model.dart';
+
+class TaskViewModel extends ChangeNotifier {
+ // ─── Danh sách tag có sẵn
+ final List availableTags = [
+ TagModel(id: 'work', name: 'Work', color: const Color(0xFF2196F3)),
+ TagModel(id: 'study', name: 'Study', color: const Color(0xFF9C27B0)),
+ TagModel(id: 'personal', name: 'Personal', color: const Color(0xFF4CAF50)),
+ TagModel(id: 'project', name: 'Project', color: const Color(0xFFFF9800)),
+ ];
+
+ // ─── State khi đang tạo task
+ Priority _selectedPriority = Priority.medium;
+ final List _selectedTags = [];
+
+ Priority get selectedPriority => _selectedPriority;
+ List get selectedTags => List.unmodifiable(_selectedTags);
+
+ void setPriority(Priority priority) {
+ _selectedPriority = priority;
+ notifyListeners();
+ }
+
+ void toggleTag(TagModel tag) {
+ if (_selectedTags.any((t) => t.id == tag.id)) {
+ _selectedTags.removeWhere((t) => t.id == tag.id);
+ } else {
+ _selectedTags.add(tag);
+ }
+ notifyListeners();
+ }
+
+ bool isTagSelected(TagModel tag) => _selectedTags.any((t) => t.id == tag.id);
+
+ void reset() {
+ _selectedPriority = Priority.medium;
+ _selectedTags.clear();
+ notifyListeners();
+ }
+
+ // ─── Danh sách task + filter/sort ────────────────────────
+ final List _tasks = [];
+
+ List get tasks => _getFilteredAndSorted();
+
+ Priority? _filterPriority;
+ String? _filterTagId;
+ bool _sortByPriority = false;
+
+ Priority? get filterPriority => _filterPriority;
+ String? get filterTagId => _filterTagId;
+ bool get sortByPriority => _sortByPriority;
+
+ void setFilterPriority(Priority? priority) {
+ _filterPriority = priority;
+ notifyListeners();
+ }
+
+ void setFilterTag(String? tagId) {
+ _filterTagId = tagId;
+ notifyListeners();
+ }
+
+ void toggleSortByPriority() {
+ _sortByPriority = !_sortByPriority;
+ notifyListeners();
+ }
+
+ void addTask(TaskModel task) {
+ _tasks.add(task);
+ notifyListeners();
+ }
+
+ List _getFilteredAndSorted() {
+ List result = List.from(_tasks);
+
+ // Lọc theo priority
+ if (_filterPriority != null) {
+ result = result.where((t) => t.priority == _filterPriority).toList();
+ }
+
+ // Lọc theo tag
+ if (_filterTagId != null) {
+ result = result
+ .where((t) => t.tags.any((tag) => tag.id == _filterTagId))
+ .toList();
+ }
+
+ // Sắp xếp theo priority (urgent → high → medium → low)
+ if (_sortByPriority) {
+ const order = [
+ Priority.urgent,
+ Priority.high,
+ Priority.medium,
+ Priority.low,
+ ];
+ result.sort(
+ (a, b) =>
+ order.indexOf(a.priority).compareTo(order.indexOf(b.priority)),
+ );
+ }
+
+ return result;
+ }
+}
diff --git a/src/lib/main.dart b/src/lib/main.dart
index bcdb6be..c97c2fc 100644
--- a/src/lib/main.dart
+++ b/src/lib/main.dart
@@ -7,6 +7,8 @@ import 'package:task_management_app/features/main/view/screens/main_screen.dart'
import 'core/theme/app_theme.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
+import 'package:provider/provider.dart';
+import 'features/tasks/viewmodel/task_viewmodel.dart';
import 'core/theme/theme_provider.dart';
@@ -14,6 +16,7 @@ import 'core/theme/theme_provider.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
+ // Tải cấu hình từ file .env
await dotenv.load(fileName: ".env");
String supabaseUrl = dotenv.env['SUPABASE_URL'] ?? '';
@@ -23,11 +26,7 @@ Future main() async {
debugPrint('Error: SUPABASE_URL or SUPABASE_ANON_KEY is missing');
}
- // 3. Khởi tạo kết nối Supabase
- await Supabase.initialize(
- url: supabaseUrl,
- anonKey: supabaseAnonKey,
- );
+ await Supabase.initialize(url: supabaseUrl, anonKey: supabaseAnonKey);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
@@ -39,11 +38,8 @@ Future main() async {
child: const TaskApp()));
}
-// 4. Create a global variable for ViewModel to call API quickly
final supabase = Supabase.instance.client;
-
-
class TaskApp extends StatelessWidget {
const TaskApp({super.key});
@@ -59,4 +55,4 @@ class TaskApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
);
}
-}
\ No newline at end of file
+}