Skip to content

Commit fa93e4a

Browse files
Ender-Viatqha1011anhkietbienhoa-cryptohoanghaozcoderabbitai[bot]
authored
Feature(Task): Create/Update/Delete task (#41)
* feat: add create task screen and fix home screen provider errors * Feature/user profile (#30) * 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(priority task):Implement priority and tag selection in task creation 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]> * Remove comment about main UI in home_screen.dart Removed commented section about main UI. * Update README.md (#32) * 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 * fix: error screen in main.dart (#34) * Enhance authentication UI and implement Focus feature set (#35) * 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]> * build(deps)(deps): bump shared_preferences from 2.5.4 to 2.5.5 in /src (#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> * Feature/user profile (#37) * 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 * fixed codes, added save delete and create tasks and notes * Delete .vs/TaskManagement.slnx/v18/.wsuo * Delete .vs directory * Delete .vscode directory * Delete src/.gitignore * Delete src/lib/features/auth/viewmodels/task_provider.dart * Delete src/web_entrypoint.dart --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: Tran Quang Ha <[email protected]> Co-authored-by: Nguyễn Anh Kiệt <[email protected]> Co-authored-by: Nguyễn Lê Hoàng Hảo <[email protected]> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 7b91943 commit fa93e4a

16 files changed

Lines changed: 1211 additions & 175 deletions
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:provider/provider.dart';
3+
import 'package:intl/intl.dart';
4+
import '../../../../core/theme/app_colors.dart';
5+
import 'package:supabase_flutter/supabase_flutter.dart';
6+
7+
class CreateTaskProvider extends ChangeNotifier {
8+
final _supabase = Supabase.instance.client;
9+
10+
// Form state variables
11+
String _taskName = "";
12+
String _selectedCategory = "Development";
13+
DateTime _selectedDate = DateTime.now();
14+
TimeOfDay _startTime = TimeOfDay(hour: 10, minute: 0);
15+
TimeOfDay _endTime = TimeOfDay(hour: 11, minute: 0);
16+
String _description = "";
17+
bool _isLoading = false;
18+
19+
// Getters for UI consumption
20+
String get taskName => _taskName;
21+
String get selectedCategory => _selectedCategory;
22+
DateTime get selectedDate => _selectedDate;
23+
TimeOfDay get startTime => _startTime;
24+
TimeOfDay get endTime => _endTime;
25+
String get description => _description;
26+
bool get isLoading => _isLoading;
27+
28+
// State update methods
29+
void setTaskName(String value) { _taskName = value; notifyListeners(); }
30+
void setCategory(String value) { _selectedCategory = value; notifyListeners(); }
31+
void setDate(DateTime value) { _selectedDate = value; notifyListeners(); }
32+
void setStartTime(TimeOfDay value) { _startTime = value; notifyListeners(); }
33+
void setEndTime(TimeOfDay value) { _endTime = value; notifyListeners(); }
34+
void setDescription(String value) { _description = value; notifyListeners(); }
35+
36+
/// Validates input and persists the new task to Supabase
37+
Future<void> onCreateTaskPressed(BuildContext context) async {
38+
39+
if (_taskName.trim().isEmpty) {
40+
_showSnackBar(context, "Task name is required.");
41+
return;
42+
}
43+
44+
final user = _supabase.auth.currentUser;
45+
if (user == null) {
46+
_showSnackBar(context, "Session not found. Please re-authenticate.");
47+
return;
48+
}
49+
50+
_isLoading = true;
51+
notifyListeners();
52+
53+
try {
54+
// Map category labels to specific database IDs based on the provided schema
55+
final categoryMapping = {
56+
"Development": 1,
57+
"Research": 2,
58+
"Design": 3,
59+
"Backend": 4,
60+
};
61+
int categoryId = categoryMapping[_selectedCategory] ?? 1;
62+
63+
// Construct a unified timestamp from selected date and start time
64+
final scheduledDateTime = DateTime(
65+
_selectedDate.year,
66+
_selectedDate.month,
67+
_selectedDate.day,
68+
_startTime.hour,
69+
_startTime.minute,
70+
);
71+
72+
// Execute insert operation.
73+
// Note: 'description' is omitted as it does not exist in the current 'task' table schema.
74+
await _supabase.from('task').insert({
75+
'title': _taskName.trim(),
76+
'status': 0,
77+
'priority': 1,
78+
'profile_id': user.id,
79+
'category_id': categoryId,
80+
'create_at': scheduledDateTime.toIso8601String(),
81+
});
82+
83+
if (context.mounted) {
84+
_showSnackBar(context, "Task created successfully.");
85+
Navigator.pop(context);
86+
}
87+
} catch (e) {
88+
debugPrint("Data Persistence Error: $e");
89+
if (context.mounted) {
90+
_showSnackBar(context, "Database synchronization failed.");
91+
}
92+
} finally {
93+
_isLoading = false;
94+
notifyListeners();
95+
}
96+
}
97+
98+
void _showSnackBar(BuildContext context, String message) {
99+
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
100+
}
101+
}
102+
103+
class CreateTaskScreen extends StatelessWidget {
104+
const CreateTaskScreen({super.key});
105+
106+
@override
107+
Widget build(BuildContext context) {
108+
return Scaffold(
109+
backgroundColor: AppColors.background,
110+
body: SafeArea(
111+
bottom: false,
112+
child: Column(
113+
children: [
114+
_buildHeader(context),
115+
Expanded(
116+
child: Container(
117+
width: double.infinity,
118+
decoration: const BoxDecoration(
119+
color: Colors.white,
120+
borderRadius: BorderRadius.vertical(top: Radius.circular(40)),
121+
),
122+
child: SingleChildScrollView(
123+
padding: const EdgeInsets.all(30),
124+
child: Column(
125+
crossAxisAlignment: CrossAxisAlignment.start,
126+
children: [
127+
_buildSectionTitle("Task Name"),
128+
_buildInputField(
129+
hint: "Enter task name...",
130+
onChanged: (v) => context.read<CreateTaskProvider>().setTaskName(v),
131+
),
132+
const SizedBox(height: 25),
133+
_buildSectionTitle("Select Category"),
134+
_buildCategorySelector(),
135+
const SizedBox(height: 25),
136+
_buildDateSelector(context),
137+
const SizedBox(height: 25),
138+
_buildTimeSelectors(context),
139+
const SizedBox(height: 25),
140+
_buildSectionTitle("Description"),
141+
_buildInputField(
142+
hint: "Add supplementary notes...",
143+
maxLines: 4,
144+
onChanged: (v) => context.read<CreateTaskProvider>().setDescription(v),
145+
),
146+
const SizedBox(height: 40),
147+
_buildCreateButton(context),
148+
const SizedBox(height: 30),
149+
],
150+
),
151+
),
152+
),
153+
),
154+
],
155+
),
156+
),
157+
);
158+
}
159+
160+
Widget _buildHeader(BuildContext context) {
161+
return Padding(
162+
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 15),
163+
child: Row(
164+
children: [
165+
IconButton(
166+
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20),
167+
onPressed: () => Navigator.pop(context),
168+
),
169+
const Expanded(
170+
child: Center(
171+
child: Text(
172+
"New Task",
173+
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black),
174+
),
175+
),
176+
),
177+
const SizedBox(width: 48),
178+
],
179+
),
180+
);
181+
}
182+
183+
Widget _buildSectionTitle(String title) {
184+
return Padding(
185+
padding: const EdgeInsets.only(bottom: 10),
186+
child: Text(
187+
title,
188+
style: const TextStyle(fontSize: 16, color: AppColors.primaryBlue, fontWeight: FontWeight.bold)
189+
),
190+
);
191+
}
192+
193+
Widget _buildInputField({required String hint, int maxLines = 1, Function(String)? onChanged}) {
194+
return TextField(
195+
maxLines: maxLines,
196+
onChanged: onChanged,
197+
style: const TextStyle(fontSize: 18, color: Colors.black),
198+
decoration: InputDecoration(
199+
hintText: hint,
200+
hintStyle: TextStyle(color: AppColors.textDark.withOpacity(0.4), fontSize: 18),
201+
enabledBorder: const UnderlineInputBorder(borderSide: BorderSide(color: Colors.black12)),
202+
focusedBorder: const UnderlineInputBorder(borderSide: BorderSide(color: AppColors.primaryBlue)),
203+
),
204+
);
205+
}
206+
207+
Widget _buildCategorySelector() {
208+
return Consumer<CreateTaskProvider>(
209+
builder: (context, provider, child) {
210+
final categories = ["Development", "Research", "Design", "Backend"];
211+
return SingleChildScrollView(
212+
scrollDirection: Axis.horizontal,
213+
child: Row(
214+
children: categories.map((cat) {
215+
final isSelected = cat == provider.selectedCategory;
216+
return GestureDetector(
217+
onTap: () => provider.setCategory(cat),
218+
child: Container(
219+
margin: const EdgeInsets.only(right: 15),
220+
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
221+
decoration: BoxDecoration(
222+
color: isSelected ? AppColors.primaryBlue : AppColors.inputBackground,
223+
borderRadius: BorderRadius.circular(10),
224+
),
225+
child: Text(
226+
cat,
227+
style: TextStyle(
228+
color: isSelected ? Colors.white : AppColors.textLightBlue,
229+
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
230+
),
231+
),
232+
),
233+
);
234+
}).toList(),
235+
),
236+
);
237+
},
238+
);
239+
}
240+
241+
Widget _buildDateSelector(BuildContext context) {
242+
return Consumer<CreateTaskProvider>(
243+
builder: (context, provider, child) {
244+
final formattedDate = DateFormat('EEEE, d MMMM').format(provider.selectedDate);
245+
return Column(
246+
crossAxisAlignment: CrossAxisAlignment.start,
247+
children: [
248+
_buildSectionTitle("Date"),
249+
Row(
250+
children: [
251+
Expanded(
252+
child: Text(formattedDate, style: const TextStyle(color: Colors.black, fontSize: 18)),
253+
),
254+
GestureDetector(
255+
onTap: () async {
256+
final picked = await showDatePicker(
257+
context: context,
258+
initialDate: provider.selectedDate,
259+
firstDate: DateTime.now(),
260+
lastDate: DateTime(2100),
261+
);
262+
if (picked != null) provider.setDate(picked);
263+
},
264+
child: Container(
265+
padding: const EdgeInsets.all(12),
266+
decoration: BoxDecoration(color: AppColors.primaryBlue, borderRadius: BorderRadius.circular(12)),
267+
child: const Icon(Icons.calendar_month, color: Colors.white, size: 20),
268+
),
269+
)
270+
],
271+
),
272+
const Divider(color: Colors.black12),
273+
],
274+
);
275+
},
276+
);
277+
}
278+
279+
Widget _buildTimeSelectors(BuildContext context) {
280+
return Row(
281+
children: [
282+
Expanded(child: _timeField(context, "Start Time", true)),
283+
const SizedBox(width: 30),
284+
Expanded(child: _timeField(context, "End Time", false)),
285+
],
286+
);
287+
}
288+
289+
Widget _timeField(BuildContext context, String title, bool isStart) {
290+
return Consumer<CreateTaskProvider>(
291+
builder: (context, provider, child) {
292+
final time = isStart ? provider.startTime : provider.endTime;
293+
return Column(
294+
crossAxisAlignment: CrossAxisAlignment.start,
295+
children: [
296+
_buildSectionTitle(title),
297+
GestureDetector(
298+
onTap: () async {
299+
final picked = await showTimePicker(context: context, initialTime: time);
300+
if (picked != null) {
301+
isStart ? provider.setStartTime(picked) : provider.setEndTime(picked);
302+
}
303+
},
304+
child: Row(
305+
children: [
306+
Text(time.format(context), style: const TextStyle(color: Colors.black, fontSize: 18)),
307+
const Icon(Icons.keyboard_arrow_down, color: AppColors.primaryBlue, size: 18),
308+
],
309+
),
310+
),
311+
const Divider(color: Colors.black12),
312+
],
313+
);
314+
},
315+
);
316+
}
317+
318+
Widget _buildCreateButton(BuildContext context) {
319+
final provider = context.watch<CreateTaskProvider>();
320+
return Center(
321+
child: SizedBox(
322+
width: double.infinity,
323+
height: 55,
324+
child: ElevatedButton(
325+
onPressed: provider.isLoading ? null : () => provider.onCreateTaskPressed(context),
326+
style: ElevatedButton.styleFrom(
327+
backgroundColor: AppColors.primaryBlue,
328+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
329+
),
330+
child: provider.isLoading
331+
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
332+
: const Text("Confirm Task Creation", style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
333+
),
334+
),
335+
);
336+
}
337+
}

src/lib/features/tasks/model/task_model.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,23 @@ class TaskModel {
7171
this.tags = const [],
7272
});
7373
}
74+
75+
class NoteModel {
76+
final int id;
77+
final String content;
78+
final bool pinned;
79+
80+
NoteModel({
81+
required this.id,
82+
required this.content,
83+
this.pinned = false,
84+
});
85+
86+
factory NoteModel.fromJson(Map<String, dynamic> json) {
87+
return NoteModel(
88+
id: json['id'],
89+
content: json['content'] ?? '',
90+
pinned: json['pinned'] ?? false,
91+
);
92+
}
93+
}

0 commit comments

Comments
 (0)