Skip to content

Commit ae8e44c

Browse files
feat: Priority selector and tag system verson2 (#33)
* 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
1 parent 5b59d35 commit ae8e44c

4 files changed

Lines changed: 525 additions & 100 deletions

File tree

src/lib/features/tasks/view/screens/home_screen.dart

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -177,27 +177,6 @@ class HomeScreen extends StatelessWidget {
177177
color: viewModel.sortByPriority
178178
? Colors.white
179179
: AppColors.primaryBlue,
180-
// --- SỬ DỤNG MOCK DATA VÀO TASKCARD ---
181-
TaskCard(
182-
task: task1, // Truyền task1 vào đây
183-
leading: Stack(
184-
children: [
185-
const CircleAvatar(radius: 15, backgroundImage: NetworkImage('https://i.pravatar.cc/150?u=user2')),
186-
const Positioned(left: 10, child: CircleAvatar(radius: 15, backgroundImage: NetworkImage('https://i.pravatar.cc/150?u=user3'))),
187-
const Positioned(left: 20, child: CircleAvatar(radius: 15, backgroundImage: NetworkImage('https://i.pravatar.cc/150?u=user4'))),
188-
Positioned(
189-
left: 30,
190-
child: Container(
191-
padding: const EdgeInsets.all(4),
192-
decoration: BoxDecoration(
193-
color: Theme.of(context).colorScheme.surface,
194-
shape: BoxShape.circle,
195-
),
196-
child: Icon(
197-
Icons.add_rounded,
198-
size: 20,
199-
color: Theme.of(context).colorScheme.primary,
200-
),
201180
),
202181
const SizedBox(width: 5),
203182
Text(

src/lib/features/tasks/view/screens/task_detail_screen.dart

Lines changed: 196 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import 'package:flutter/material.dart';
22
import 'package:intl/intl.dart';
3+
import 'package:provider/provider.dart';
4+
import '../../../../core/theme/app_colors.dart';
35
import '../../../../core/widgets/custom_input_field.dart';
46
import '../../model/task_model.dart';
5-
import '../widgets/task_widgets.dart'; // Contains TimePickerWidget
7+
import '../../viewmodel/task_viewmodel.dart';
8+
import '../widgets/task_widgets.dart';
69

710
class TaskDetailScreen extends StatefulWidget {
811
final TaskModel task;
9-
1012
const TaskDetailScreen({super.key, required this.task});
1113

1214
@override
@@ -19,50 +21,60 @@ class _TaskDetailScreenState extends State<TaskDetailScreen> {
1921
late TimeOfDay _startTime;
2022
late TimeOfDay _endTime;
2123
late String _currentCategory;
24+
late List<TagModel> _currentTags;
2225

2326
@override
2427
void initState() {
2528
super.initState();
26-
// Initialize state variables with services from the passed task object
2729
_titleController = TextEditingController(text: widget.task.title);
2830
_descController = TextEditingController(text: widget.task.description);
2931
_startTime = widget.task.startTime;
3032
_endTime = widget.task.endTime;
3133
_currentCategory = widget.task.category;
34+
_currentTags = List.from(widget.task.tags);
3235
}
3336

3437
@override
3538
void dispose() {
36-
// Dispose controllers to prevent memory leaks
3739
_titleController.dispose();
3840
_descController.dispose();
3941
super.dispose();
4042
}
4143

44+
void _toggleTag(TagModel tag) {
45+
setState(() {
46+
if (_currentTags.any((t) => t.id == tag.id)) {
47+
_currentTags.removeWhere((t) => t.id == tag.id);
48+
} else {
49+
_currentTags.add(tag);
50+
}
51+
});
52+
}
53+
54+
bool _isTagSelected(TagModel tag) => _currentTags.any((t) => t.id == tag.id);
55+
4256
void _saveChanges() {
43-
// Update the local model
44-
// (Note: Later, this will call TaskViewModel -> TaskService -> your ASP.NET Core API to update the database)
4557
widget.task.title = _titleController.text;
4658
widget.task.description = _descController.text;
4759
widget.task.startTime = _startTime;
4860
widget.task.endTime = _endTime;
4961
widget.task.category = _currentCategory;
5062

51-
// Show success message
63+
// Lưu tags mới vào task qua ViewModel
64+
context.read<TaskViewModel>().updateTaskTags(widget.task.id, _currentTags);
65+
5266
ScaffoldMessenger.of(context).showSnackBar(
5367
SnackBar(
5468
content: const Text('Task updated successfully!'),
5569
backgroundColor: Theme.of(context).colorScheme.tertiary,
5670
),
5771
);
58-
59-
// Return to the previous screen
6072
Navigator.pop(context);
6173
}
6274

6375
@override
6476
Widget build(BuildContext context) {
65-
// Format date for display
77+
final viewModel = context.watch<TaskViewModel>();
6678
String formattedDate = DateFormat('EEEE, d MMMM').format(widget.task.date);
6779
final isDark = Theme.of(context).brightness == Brightness.dark;
6880

@@ -92,8 +104,8 @@ class _TaskDetailScreenState extends State<TaskDetailScreen> {
92104
),
93105
body: SafeArea(
94106
child: Hero(
95-
tag: 'task_card_${widget.task.id}', // Must match the Hero tag in the Home/Statistics screen
96-
child: Material( // Required inside Hero to prevent yellow underline text rendering issues
107+
tag: 'task_card_${widget.task.id}',
108+
child: Material(
97109
type: MaterialType.transparency,
98110
child: Container(
99111
margin: const EdgeInsets.all(20),
@@ -104,36 +116,51 @@ class _TaskDetailScreenState extends State<TaskDetailScreen> {
104116
? Border.all(color: Theme.of(context).colorScheme.outline)
105117
: null,
106118
boxShadow: [
107-
BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 5))
119+
BoxShadow(
120+
color: Colors.black.withValues(alpha: 0.05),
121+
blurRadius: 10,
122+
offset: const Offset(0, 5),
123+
),
108124
],
109125
),
110126
child: SingleChildScrollView(
111127
padding: const EdgeInsets.all(25.0),
112128
child: Column(
113129
crossAxisAlignment: CrossAxisAlignment.start,
114130
children: [
115-
// Input for Task Name
116-
CustomInputField(label: 'Task Name', hint: '', controller: _titleController),
131+
// Task Name
132+
CustomInputField(
133+
label: 'Task Name',
134+
hint: '',
135+
controller: _titleController,
136+
),
117137
const SizedBox(height: 20),
118138

119-
Text('Category Tag', style: Theme.of(context).textTheme.labelLarge),
139+
// Category
140+
Text(
141+
'Category Tag',
142+
style: Theme.of(context).textTheme.labelLarge,
143+
),
120144
const SizedBox(height: 10),
121-
122-
// Horizontal list of Category chips
123145
SizedBox(
124146
height: 40,
125147
child: ListView.builder(
126148
scrollDirection: Axis.horizontal,
127149
itemCount: categories.length,
128150
itemBuilder: (context, index) {
129-
bool isSelected = categories[index] == _currentCategory;
151+
bool isSelected =
152+
categories[index] == _currentCategory;
130153
return Padding(
131154
padding: const EdgeInsets.only(right: 10),
132155
child: ChoiceChip(
133156
label: Text(categories[index]),
134157
selected: isSelected,
135158
onSelected: (selected) {
136-
if (selected) setState(() => _currentCategory = categories[index]);
159+
if (selected) {
160+
setState(
161+
() => _currentCategory = categories[index],
162+
);
163+
}
137164
},
138165
backgroundColor: isDark
139166
? Theme.of(context).colorScheme.surfaceContainerHighest
@@ -161,7 +188,7 @@ class _TaskDetailScreenState extends State<TaskDetailScreen> {
161188
),
162189
const SizedBox(height: 25),
163190

164-
// Display Task Date
191+
// Date
165192
Text('Date', style: Theme.of(context).textTheme.labelLarge),
166193
const SizedBox(height: 5),
167194
Text(
@@ -174,16 +201,131 @@ class _TaskDetailScreenState extends State<TaskDetailScreen> {
174201
),
175202
const SizedBox(height: 25),
176203

177-
// Time Pickers for Start and End time
204+
// ─── Time Tags ────────────────────────────
205+
Text(
206+
'Thời gian',
207+
style: Theme.of(context).textTheme.labelLarge,
208+
),
209+
const SizedBox(height: 10),
210+
Wrap(
211+
spacing: 8,
212+
runSpacing: 8,
213+
children: viewModel.timeTags.map((tag) {
214+
final isSelected = _isTagSelected(tag);
215+
return GestureDetector(
216+
onTap: () => _toggleTag(tag),
217+
child: AnimatedContainer(
218+
duration: const Duration(milliseconds: 200),
219+
padding: const EdgeInsets.symmetric(
220+
horizontal: 14,
221+
vertical: 8,
222+
),
223+
decoration: BoxDecoration(
224+
color: isSelected
225+
? tag.color
226+
: tag.color.withValues(alpha: 0.1),
227+
borderRadius: BorderRadius.circular(20),
228+
),
229+
child: Row(
230+
mainAxisSize: MainAxisSize.min,
231+
children: [
232+
if (isSelected) ...[
233+
const Icon(
234+
Icons.check,
235+
color: Colors.white,
236+
size: 14,
237+
),
238+
const SizedBox(width: 4),
239+
],
240+
Text(
241+
tag.name,
242+
style: TextStyle(
243+
fontSize: 13,
244+
fontWeight: FontWeight.w500,
245+
color: isSelected
246+
? Colors.white
247+
: tag.color,
248+
),
249+
),
250+
],
251+
),
252+
),
253+
);
254+
}).toList(),
255+
),
256+
const SizedBox(height: 20),
257+
258+
// ─── Status Tags ──────────────────────────
259+
Text(
260+
'Trạng thái',
261+
style: Theme.of(context).textTheme.labelLarge,
262+
),
263+
const SizedBox(height: 10),
264+
Wrap(
265+
spacing: 8,
266+
runSpacing: 8,
267+
children: viewModel.statusTags.map((tag) {
268+
final isSelected = _isTagSelected(tag);
269+
return GestureDetector(
270+
onTap: () => _toggleTag(tag),
271+
child: AnimatedContainer(
272+
duration: const Duration(milliseconds: 200),
273+
padding: const EdgeInsets.symmetric(
274+
horizontal: 14,
275+
vertical: 8,
276+
),
277+
decoration: BoxDecoration(
278+
color: isSelected
279+
? tag.color
280+
: tag.color.withValues(alpha: 0.1),
281+
borderRadius: BorderRadius.circular(20),
282+
),
283+
child: Row(
284+
mainAxisSize: MainAxisSize.min,
285+
children: [
286+
if (isSelected) ...[
287+
const Icon(
288+
Icons.check,
289+
color: Colors.white,
290+
size: 14,
291+
),
292+
const SizedBox(width: 4),
293+
],
294+
Text(
295+
tag.name,
296+
style: TextStyle(
297+
fontSize: 13,
298+
fontWeight: FontWeight.w500,
299+
color: isSelected
300+
? Colors.white
301+
: tag.color,
302+
),
303+
),
304+
],
305+
),
306+
),
307+
);
308+
}).toList(),
309+
),
310+
const SizedBox(height: 25),
311+
312+
// Time Pickers
178313
Row(
179314
children: [
180315
Expanded(
181316
child: Column(
182317
crossAxisAlignment: CrossAxisAlignment.start,
183318
children: [
184-
Text('Start time', style: Theme.of(context).textTheme.labelLarge),
319+
Text(
320+
'Start time',
321+
style: Theme.of(context).textTheme.labelLarge,
322+
),
185323
const SizedBox(height: 5),
186-
TimePickerWidget(time: _startTime, onChanged: (newTime) => setState(() => _startTime = newTime)),
324+
TimePickerWidget(
325+
time: _startTime,
326+
onChanged: (t) =>
327+
setState(() => _startTime = t),
328+
),
187329
],
188330
),
189331
),
@@ -192,18 +334,29 @@ class _TaskDetailScreenState extends State<TaskDetailScreen> {
192334
child: Column(
193335
crossAxisAlignment: CrossAxisAlignment.start,
194336
children: [
195-
Text('End time', style: Theme.of(context).textTheme.labelLarge),
337+
Text(
338+
'End time',
339+
style: Theme.of(context).textTheme.labelLarge,
340+
),
196341
const SizedBox(height: 5),
197-
TimePickerWidget(time: _endTime, onChanged: (newTime) => setState(() => _endTime = newTime)),
342+
TimePickerWidget(
343+
time: _endTime,
344+
onChanged: (t) => setState(() => _endTime = t),
345+
),
198346
],
199347
),
200348
),
201349
],
202350
),
203351
const SizedBox(height: 25),
204352

205-
// Input for Description
206-
CustomInputField(label: 'Description', hint: '', controller: _descController, maxLines: 3),
353+
// Description
354+
CustomInputField(
355+
label: 'Description',
356+
hint: '',
357+
controller: _descController,
358+
maxLines: 3,
359+
),
207360
const SizedBox(height: 40),
208361

209362
// Save Button
@@ -213,11 +366,22 @@ class _TaskDetailScreenState extends State<TaskDetailScreen> {
213366
style: ElevatedButton.styleFrom(
214367
backgroundColor: Theme.of(context).colorScheme.primary,
215368
foregroundColor: Colors.white,
216-
padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 15),
217-
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
369+
padding: const EdgeInsets.symmetric(
370+
horizontal: 50,
371+
vertical: 15,
372+
),
373+
shape: RoundedRectangleBorder(
374+
borderRadius: BorderRadius.circular(15),
375+
),
218376
minimumSize: const Size(double.infinity, 50),
219377
),
220-
child: const Text('Save Changes', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
378+
child: const Text(
379+
'Save Changes',
380+
style: TextStyle(
381+
fontSize: 18,
382+
fontWeight: FontWeight.bold,
383+
),
384+
),
221385
),
222386
),
223387
],
@@ -229,4 +393,4 @@ class _TaskDetailScreenState extends State<TaskDetailScreen> {
229393
),
230394
);
231395
}
232-
}
396+
}

0 commit comments

Comments
 (0)