11import 'package:flutter/material.dart' ;
22import 'package:intl/intl.dart' ;
3+ import 'package:provider/provider.dart' ;
4+ import '../../../../core/theme/app_colors.dart' ;
35import '../../../../core/widgets/custom_input_field.dart' ;
46import '../../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
710class 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