diff --git a/backend/controller_task.py b/backend/controller_task.py index 9aab959..0a53496 100644 --- a/backend/controller_task.py +++ b/backend/controller_task.py @@ -97,7 +97,7 @@ def edit_task_control( with db_operation() as data_cursor: data_cursor.execute( "UPDATE task SET name = ?, description = ?, due = ?, est_day = ?, " - "est_hour = ?, est_min = ?, assigner_uuid = ?, assign_uuid = ?, group_id = ?, " + "est_hour = ?, est_min = ?, assigner_uuid = ?, assign_uuid = ?, group_uuid = ?, " "recursive = ?, priority = ?, image_path = ?, completed = ? " "WHERE uuid = ?;", ( @@ -151,6 +151,14 @@ def get_user_task_control(self, user_id: str, password: str) -> dict[str, dict]: username_result = username_cursor.fetchone() if username_result: assigner_username = username_result[0] + + # Get assignee username + assignee_username = "Unknown" + with db_operation() as username_cursor: + username_cursor.execute("SELECT username FROM user WHERE uuid = ?;", (task[8],)) + username_result = username_cursor.fetchone() + if username_result: + assignee_username = username_result[0] new_task_list[task[0]] = { "name": task[1], @@ -162,17 +170,12 @@ def get_user_task_control(self, user_id: str, password: str) -> dict[str, dict]: "assigner_id": task[7], "assigner_username": assigner_username, "assign_id": task[8], + "assignee_username": assignee_username, "group_id": task[9], "completed": bool(task[10]), -<<<<<<< Updated upstream "priority": int(task[11]) if len(task) > 11 else 0, "recursive": int(task[12]) if len(task) > 12 else 0, "image_path": task[13] if len(task) > 13 else "", -======= - "priority": int(task[11]), - "recursive": int(task[12]), - "image_path": task[13], ->>>>>>> Stashed changes } return new_task_list @@ -192,10 +195,7 @@ def get_group_task_control( if not Validator().check_user_in_group(user_id=user_id, group_id=group_id): raise BackendError("Backend Error: User is not in the group", "310") with db_operation() as data_cursor: - data_cursor.execute( - "SELECT * FROM task WHERE group_id = ?;", - (group_id), - ) + data_cursor.execute("SELECT * FROM task WHERE group_uuid = ?;", (group_id,)) task_list: list[tuple] = data_cursor.fetchall() new_task_list: dict[str, dict] = {} for task in task_list: @@ -207,6 +207,14 @@ def get_group_task_control( if username_result: assigner_username = username_result[0] + # Get assignee username + assignee_username = "Unknown" + with db_operation() as username_cursor: + username_cursor.execute("SELECT username FROM user WHERE uuid = ?;", (task[8],)) + username_result = username_cursor.fetchone() + if username_result: + assignee_username = username_result[0] + new_task_list[task[0]] = { "name": task[1], "description": task[2], @@ -217,17 +225,12 @@ def get_group_task_control( "assigner_id": task[7], "assigner_username": assigner_username, "assign_id": task[8], + "assignee_username": assignee_username, "group_id": task[9], "completed": bool(task[10]), -<<<<<<< Updated upstream "priority": int(task[11]) if len(task) > 11 else 0, "recursive": int(task[12]) if len(task) > 12 else 0, "image_path": task[13] if len(task) > 13 else "", -======= - "priority": int(task[11]), - "recursive": int(task[12]), - "image_path": task[13], ->>>>>>> Stashed changes } return new_task_list @@ -249,7 +252,7 @@ def get_completed_task_control( with db_operation() as data_cursor: data_cursor.execute( "SELECT * FROM task WHERE assign_id = ? " - "OR group_id IN (SELECT group_id FROM group_user WHERE user_id = ?) " + "OR group_uuid IN (SELECT group_uuid FROM group_user WHERE user_id = ?) " "AND completed = 1;", ( group_id, @@ -279,15 +282,9 @@ def get_completed_task_control( "assign_id": task[8], "group_id": task[9], "completed": bool(task[10]), -<<<<<<< Updated upstream "priority": int(task[11]) if len(task) > 11 else 0, "recursive": int(task[12]) if len(task) > 12 else 0, "image_path": task[13] if len(task) > 13 else "", -======= - "priority": int(task[11]), - "recursive": int(task[12]), - "image_path": task[13], ->>>>>>> Stashed changes } return new_task_list diff --git a/backend/handler_task.py b/backend/handler_task.py index 6be8027..8b15349 100644 --- a/backend/handler_task.py +++ b/backend/handler_task.py @@ -86,6 +86,22 @@ def get_user_task_request(self) -> dict[str, dict[str, Any]]: user_id=user_id, password=password ) + @handle_backend_exceptions + def get_group_task_request(self) -> dict[str, dict[str, Any]]: + """Gets tasks for a specific group.""" + request_data: dict[str, Any] = extract_request_data( + request=self.user_request, + required_fields=["user_id", "group_id", "password"], + ) + user_id: str = request_data["user_id"] + group_id: str = request_data["group_id"] + password: str = request_data["password"] + return TaskController().get_group_task_control( + user_id=user_id, + group_id=group_id, + password=password + ) + @handle_backend_exceptions def get_image_request(self) -> str: """Gets the image.""" diff --git a/backend/main.py b/backend/main.py index b5b21a7..604d0de 100644 --- a/backend/main.py +++ b/backend/main.py @@ -57,7 +57,6 @@ def handle_signup() -> Response: @error_handling_decorator("login") def handle_login() -> Response: """Login a user.""" -<<<<<<< Updated upstream user_info: dict[str, str] = UserHandle(request).login_user_request() # Extract user_id and username user_id: str = user_info["user_id"] @@ -67,10 +66,6 @@ def handle_login() -> Response: # (Front end expects this and needs it to store user info) [{"error_no": "0", "message": "success", "user_id": user_id, "username": username}] ) -======= - user_id: str = UserHandle(request).login_user_request() - return jsonify([{"error_no": "0", "message": "success", "user_id": user_id}]) ->>>>>>> Stashed changes @app.route("/edit_user", methods=["POST"]) @@ -124,6 +119,14 @@ def handle_get_user_task() -> Response: return jsonify([{"error_no": "0", "message": "success", "tasks": tasks}]) +@app.route("/get_group_task", methods=["POST"]) +@error_handling_decorator("get_group_task") +def handle_get_group_task() -> Response: + """Get all tasks for a specific group.""" + tasks: dict[str, dict[str, Any]] = TaskHandle(request).get_group_task_request() + return jsonify([{"error_no": "0", "message": "success", "tasks": tasks}]) + + @app.route("/get_image", methods=["POST"]) @error_handling_decorator("get_image") def handle_get_image() -> Response: diff --git a/data/make_a_new_file_called_(data.db) b/data/make_a_new_file_called_(data.db) deleted file mode 100644 index e69de29..0000000 diff --git a/log/make_a_new_file_called_(log.txt) b/log/make_a_new_file_called_(log.txt) deleted file mode 100644 index e69de29..0000000 diff --git a/msdocs-python-flask-webapp-quickstart b/msdocs-python-flask-webapp-quickstart new file mode 160000 index 0000000..5bfb67b --- /dev/null +++ b/msdocs-python-flask-webapp-quickstart @@ -0,0 +1 @@ +Subproject commit 5bfb67bffda1a5083e33fec45861de6b55f74e57 diff --git a/roomiebuddy/lib/common/widget/appbar/appbar.dart b/roomiebuddy/lib/common/widget/appbar/appbar.dart index 43cd174..612c98a 100644 --- a/roomiebuddy/lib/common/widget/appbar/appbar.dart +++ b/roomiebuddy/lib/common/widget/appbar/appbar.dart @@ -26,6 +26,8 @@ class TAppBar extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { return AppBar( automaticallyImplyLeading: false, + elevation: 0, + backgroundColor: Theme.of(context).appBarTheme.backgroundColor, leading: showBackArrow ? IconButton(onPressed: () => Get.back(), icon: const Icon(Icons.arrow_back)) : leadingIcon != null diff --git a/roomiebuddy/lib/common/widget/search/task_search_delegate.dart b/roomiebuddy/lib/common/widget/search/task_search_delegate.dart new file mode 100644 index 0000000..f0bf36c --- /dev/null +++ b/roomiebuddy/lib/common/widget/search/task_search_delegate.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:roomiebuddy/pages/subpages/home/task_detail_page.dart'; + +class TaskSearchDelegate extends SearchDelegate { + final List> tasks; + TaskSearchDelegate({required this.tasks}); + + @override + List buildActions(BuildContext context) => + [IconButton(icon: const Icon(Icons.clear), onPressed: () => query = '')]; + + @override + Widget buildLeading(BuildContext context) => + IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => close(context, null)); + + @override + Widget buildResults(BuildContext context) { + final results = tasks + .where((task) => + task['taskName'].toLowerCase().contains(query.toLowerCase())) + .toList(); + + return ListView.builder( + itemCount: results.length, + itemBuilder: (context, index) => ListTile( + title: Text(results[index]['taskName']), + subtitle: Text('Assigned by: ${results[index]['assignedBy']}'), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => TaskDetailScreen(task: results[index]), + ), + ); + }, + ), + ); + } + + @override + Widget buildSuggestions(BuildContext context) => buildResults(context); +} \ No newline at end of file diff --git a/roomiebuddy/lib/common/widget/task/task_list_widget.dart b/roomiebuddy/lib/common/widget/task/task_list_widget.dart new file mode 100644 index 0000000..c7600a0 --- /dev/null +++ b/roomiebuddy/lib/common/widget/task/task_list_widget.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:roomiebuddy/providers/theme_provider.dart'; +import 'package:roomiebuddy/pages/subpages/home/task_detail_page.dart'; + +class TaskListWidget extends StatelessWidget { + // --- State Variables --- // + final List> allTasks; + final String? focusedGroupId; + final Set userGroupIds; + final List> roommateGroups; + final VoidCallback onTaskActionCompleted; + final bool showOnlyMyTasks; + final String? currentUserId; + + // --- Constructor --- // + + const TaskListWidget({ + super.key, + required this.allTasks, + required this.focusedGroupId, + required this.userGroupIds, + required this.roommateGroups, + required this.onTaskActionCompleted, + required this.showOnlyMyTasks, + required this.currentUserId, + }); + + @override + Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + + // Create the content to display inside the container + Widget content; + + // If there are no tasks, show a message + if (allTasks.isEmpty) { + content = Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Text( + 'No tasks!', + style: TextStyle(color: themeProvider.currentSecondaryTextColor, fontSize: 16), + textAlign: TextAlign.center, + ), + ), + ); + } else { + // Filter tasks based on the focused group ID + List> filteredTasks = allTasks.where((task) { + final String? groupId = task["group_id"] as String?; + + // Show tasks for the focused group if one is selected + if (focusedGroupId != null) { + return groupId == focusedGroupId; + } + + // If no group is focused, show tasks that either have no group ID + // or belong to any of the user's groups. + return groupId == null || groupId == "0" || userGroupIds.contains(groupId); + }).toList(); + + // Further filter by assigned user if the flag is set + if (showOnlyMyTasks && currentUserId != null) { + filteredTasks = filteredTasks.where((task) { + // Keep tasks where assignee_id matches the current user ID + return task['assignee_id'] == currentUserId; + }).toList(); + } + + // If no tasks are found with the current filter display a message + if (filteredTasks.isEmpty) { + content = Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Text( + showOnlyMyTasks + ? 'No tasks assigned specifically to you${focusedGroupId != null ? ' in this group' : ''}.' + : 'No tasks found${focusedGroupId != null ? ' for this group' : ''}.', + style: TextStyle(color: themeProvider.currentSecondaryTextColor, fontSize: 16), + textAlign: TextAlign.center, + ), + ), + ); + } else { + // If there are tasks, display them + content = ListView.builder( + shrinkWrap: true, + itemCount: filteredTasks.length, + itemBuilder: (context, index) { + final task = filteredTasks[index]; + String groupName = 'Unknown Group'; // Default group name + + // Find the group name if focusedGroupId is null (showing all groups) + if (focusedGroupId == null && task['group_id'] != null) { + final group = roommateGroups.firstWhere( + (g) => g['group_id'] == task['group_id'], + orElse: () => {'group_name': 'Unknown Group'} + ); + groupName = group['group_name'] ?? 'Unknown Group'; + } + + // Return a card for each task + return Card( + margin: EdgeInsets.only(bottom: index < filteredTasks.length - 1 ? 12 : 0), + color: themeProvider.currentBackground, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + title: Text(task['taskName'], + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: themeProvider.currentTextColor + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Show group name when displaying tasks from multiple groups (like in calendar) + if (focusedGroupId == null && task['group_id'] != null) ...[ + Text('Group: $groupName', style: TextStyle(color: themeProvider.currentSecondaryTextColor)), + const SizedBox(height: 2), + ], + // Always show assigned by + Text('Assigned by: ${task['assignedBy']}', style: TextStyle(color: themeProvider.currentSecondaryTextColor)), + + if (!showOnlyMyTasks) ...[ + const SizedBox(height: 2), + Text('For: ${task['assignedTo']}', style: TextStyle(color: themeProvider.currentSecondaryTextColor)), + ], + ], + ), + ), + ], + ), + trailing: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: task['priority'] == 'High' + ? themeProvider.errorColor.withAlpha(40) + : task['priority'] == 'Medium' + ? themeProvider.warningColor.withAlpha(40) + : themeProvider.successColor.withAlpha(40), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + task['priority'], + style: TextStyle( + color: task['priority'] == 'High' + ? themeProvider.errorColor + : task['priority'] == 'Medium' + ? themeProvider.warningColor + : themeProvider.successColor, + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ), + ), + onTap: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TaskDetailScreen(task: task), + ), + ); + if (result == true && context.mounted) { + onTaskActionCompleted(); + } + }, + ), + ); + }, + ); + } + } + + // Return the styled container with the content + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: themeProvider.currentInputFill, + borderRadius: BorderRadius.circular(8), + ), + child: content, + ); + } +} \ No newline at end of file diff --git a/roomiebuddy/lib/main.dart b/roomiebuddy/lib/main.dart index a72173b..d1c414b 100644 --- a/roomiebuddy/lib/main.dart +++ b/roomiebuddy/lib/main.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:roomiebuddy/pages/login_screen.dart'; +import 'package:roomiebuddy/pages/subpages/auth/login_screen.dart'; import 'package:roomiebuddy/providers/theme_provider.dart'; void main() { diff --git a/roomiebuddy/lib/NavScreen.dart b/roomiebuddy/lib/nav_screen.dart similarity index 91% rename from roomiebuddy/lib/NavScreen.dart rename to roomiebuddy/lib/nav_screen.dart index 33da0ee..9d9907a 100644 --- a/roomiebuddy/lib/NavScreen.dart +++ b/roomiebuddy/lib/nav_screen.dart @@ -5,7 +5,7 @@ import 'package:roomiebuddy/providers/theme_provider.dart'; import 'pages/home_page.dart'; import 'pages/calendar_page.dart'; -import 'pages/add_taskpage.dart'; +import 'pages/add_task_page.dart'; import 'pages/group_page.dart'; import 'pages/settings_page.dart'; @@ -18,11 +18,11 @@ class Navscreen extends StatefulWidget { class _NavscreenState extends State { final pages = [ - HomePage(), - CalendarPage(), - AddTaskpage(), - GroupPage(), - SettingsPage(), + const HomePage(), + const CalendarPage(), + const AddTaskpage(), + const GroupPage(), + const SettingsPage(), ]; int selectedIndex = 0; @override diff --git a/roomiebuddy/lib/pages/add_taskpage.dart b/roomiebuddy/lib/pages/add_task_page.dart similarity index 93% rename from roomiebuddy/lib/pages/add_taskpage.dart rename to roomiebuddy/lib/pages/add_task_page.dart index f6d464e..038e328 100644 --- a/roomiebuddy/lib/pages/add_taskpage.dart +++ b/roomiebuddy/lib/pages/add_task_page.dart @@ -15,7 +15,6 @@ class AddTaskpage extends StatefulWidget { } class _AddTaskpageState extends State { - // TextEditingControllers final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); @@ -70,7 +69,8 @@ class _AddTaskpageState extends State { super.dispose(); } - // Get the users id and password from the auth storage + // ------- Backend Communication Methods ------- // + Future _loadInitialData() async { setState(() => _isLoadingGroups = true); final userId = await _authStorage.getUserId(); @@ -163,112 +163,6 @@ class _AddTaskpageState extends State { } } - // For Date Picker - Future _selectDate(BuildContext context) async { - final themeProvider = Provider.of(context, listen: false); - - final DateTime? picked = await showDatePicker( - context: context, - initialDate: _selectedDate ?? DateTime.now(), - firstDate: DateTime.now().subtract(const Duration(days: 1)), - lastDate: DateTime(2101), - builder: (context, child) { - return Theme( - data: Theme.of(context).copyWith( - colorScheme: themeProvider.isDarkMode - ? ColorScheme.dark( - primary: themeProvider.themeColor, - onPrimary: themeProvider.currentTextColor, - surface: themeProvider.currentInputFill, - onSurface: themeProvider.currentTextColor, - ) - : ColorScheme.light( - primary: themeProvider.themeColor, - onPrimary: themeProvider.currentTextColor, - surface: themeProvider.currentInputFill, - onSurface: themeProvider.currentTextColor, - ), - dialogTheme: DialogTheme( - backgroundColor: themeProvider.currentInputFill, - ), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - foregroundColor: themeProvider.themeColor, - ), - ), - ), - child: child!, - ); - }, - ); - if (picked != null && picked != _selectedDate) { - setState(() { - _selectedDate = picked; - }); - } - } - - // For Time Picker - Future _selectTime(BuildContext context) async { - final themeProvider = Provider.of(context, listen: false); - - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: _selectedTime ?? TimeOfDay.now(), - builder: (context, child) { - return Theme( - data: Theme.of(context).copyWith( - colorScheme: themeProvider.isDarkMode - ? ColorScheme.dark( - primary: themeProvider.themeColor, - onPrimary: themeProvider.currentTextColor, - surface: themeProvider.currentInputFill, - onSurface: themeProvider.currentTextColor, - ) - : ColorScheme.light( - primary: themeProvider.themeColor, - onPrimary: themeProvider.currentTextColor, - surface: themeProvider.currentInputFill, - onSurface: themeProvider.currentTextColor, - ), - timePickerTheme: TimePickerThemeData( - backgroundColor: themeProvider.currentInputFill, - dialHandColor: themeProvider.themeColor, - dayPeriodColor: themeProvider.themeColor, - dayPeriodTextColor: themeProvider.currentTextColor, - ), - dialogTheme: DialogTheme( - backgroundColor: themeProvider.currentInputFill, - ), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - foregroundColor: themeProvider.themeColor, - ), - ), - ), - child: child!, - ); - }, - ); - if (picked != null && picked != _selectedTime) { - setState(() { - _selectedTime = picked; - }); - } - } - - // For Image Picker - Future _pickImage() async { - final ImagePicker picker = ImagePicker(); - final XFile? image = await picker.pickImage(source: ImageSource.gallery); - - if (image != null) { - setState(() { - _selectedImage = File(image.path); - }); - } - } - Future _saveTask() async { if (!mounted) return; final scaffoldMessenger = ScaffoldMessenger.of(context); @@ -336,11 +230,10 @@ class _AddTaskpageState extends State { }; final int recurrenceInt = recurrenceMap[_selectedRecurrence ?? 'Once'] ?? 0; - final double? dueTimestamp = dateTimeToTimestamp(_selectedDate, _selectedTime); final int priorityInt = priorityToInt(_selectedPriority); - // If the date/time is invalid, show a snackbar - if (dueTimestamp == null) { + // If the date/time is invalid (shouldn't happen with current checks, but good practice) + if (_selectedDate == null || _selectedTime == null) { scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Invalid date/time selected.'))); return; } @@ -348,21 +241,24 @@ class _AddTaskpageState extends State { setState(() => _isSaving = true); // Set saving state to true, shows a loading indicator try { - final response = await _apiService.post('/add_task', { - 'task_name': _titleController.text, - 'task_description': _descriptionController.text, - 'task_due': dueTimestamp, - 'assigner_id': _userId, - 'assign_id': _selectedMemberId!, - 'group_id': _selectedGroupId!, - 'password': _password, - 'priority': priorityInt, - 'task_est_day': estDays, - 'task_est_hour': estHours, - 'task_est_min': estMins, - 'recursive': recurrenceInt, - 'image_path': '' // TODO: Implement image upload not via path, but via file - }); + final response = await _apiService.addTask( + _titleController.text, // taskName + _descriptionController.text, // taskDescription + _selectedDate!.year, // dueYear + _selectedDate!.month, // dueMonth + _selectedDate!.day, // dueDate + _selectedTime!.hour, // dueHour + _selectedTime!.minute, // dueMin + estDays, // estDay + estHours, // estHour + estMins, // estMin + _userId, // assignerId + _selectedMemberId!, // assignId + _selectedGroupId!, // groupId + recurrenceInt, // recursive + priorityInt, // priority + _password, // password + ); if (!mounted) return; @@ -401,6 +297,114 @@ class _AddTaskpageState extends State { } } + + // ------- Date/Time/Image Picker Methods ------- // + + Future _selectDate(BuildContext context) async { + final themeProvider = Provider.of(context, listen: false); + + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedDate ?? DateTime.now(), + firstDate: DateTime.now().subtract(const Duration(days: 1)), + lastDate: DateTime(2101), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: themeProvider.isDarkMode + ? ColorScheme.dark( + primary: themeProvider.themeColor, + onPrimary: themeProvider.currentTextColor, + surface: themeProvider.currentInputFill, + onSurface: themeProvider.currentTextColor, + ) + : ColorScheme.light( + primary: themeProvider.themeColor, + onPrimary: themeProvider.currentTextColor, + surface: themeProvider.currentInputFill, + onSurface: themeProvider.currentTextColor, + ), + dialogTheme: DialogTheme( + backgroundColor: themeProvider.currentInputFill, + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: themeProvider.themeColor, + ), + ), + ), + child: child!, + ); + }, + ); + if (picked != null && picked != _selectedDate) { + setState(() { + _selectedDate = picked; + }); + } + } + + Future _selectTime(BuildContext context) async { + final themeProvider = Provider.of(context, listen: false); + + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: _selectedTime ?? TimeOfDay.now(), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: themeProvider.isDarkMode + ? ColorScheme.dark( + primary: themeProvider.themeColor, + onPrimary: themeProvider.currentTextColor, + surface: themeProvider.currentInputFill, + onSurface: themeProvider.currentTextColor, + ) + : ColorScheme.light( + primary: themeProvider.themeColor, + onPrimary: themeProvider.currentTextColor, + surface: themeProvider.currentInputFill, + onSurface: themeProvider.currentTextColor, + ), + timePickerTheme: TimePickerThemeData( + backgroundColor: themeProvider.currentInputFill, + dialHandColor: themeProvider.themeColor, + dayPeriodColor: themeProvider.themeColor, + dayPeriodTextColor: themeProvider.currentTextColor, + ), + dialogTheme: DialogTheme( + backgroundColor: themeProvider.currentInputFill, + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: themeProvider.themeColor, + ), + ), + ), + child: child!, + ); + }, + ); + if (picked != null && picked != _selectedTime) { + setState(() { + _selectedTime = picked; + }); + } + } + + Future _pickImage() async { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage(source: ImageSource.gallery); + + if (image != null) { + setState(() { + _selectedImage = File(image.path); + }); + } + } + + // ------- Main Build Method ------- // + @override Widget build(BuildContext context) { // Theme and general input styling setup @@ -433,7 +437,7 @@ class _AddTaskpageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Task Details Section - Title and Description + // ------------ Task Details Section ------------ // Text( 'Task Details', style: TextStyle( @@ -455,7 +459,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), - // Assignment Section - Group and Member Selection + // ------------ Assignment Section ------------ // Text( 'Assign To', style: TextStyle( @@ -527,7 +531,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), - // Priority Section - Task Priority Selection + // ------------ Priority Section ------------ // Text( 'Priority', style: TextStyle( @@ -558,7 +562,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), - // Due Date & Time Section - Calendar and Time Pickers + // ------------ Due Date & Time Section ------------ // Text( 'Due Date & Time', style: TextStyle( @@ -603,7 +607,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), - // Estimated Duration Section - Days, Hours, Minutes inputs + // ------------ Estimated Duration Section ------------ // Text( 'Estimated Duration', style: TextStyle( @@ -642,7 +646,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), - // Recurrence Section - Task repetition pattern + // ------------ Recurrence Section ------------ // Text( 'Recurrence', style: TextStyle( @@ -674,7 +678,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 16), - // Action Buttons - Add Photo and Save Task + // ------------ Action Buttons ------------ // Row( children: [ ElevatedButton.icon( diff --git a/roomiebuddy/lib/pages/calendar_page.dart b/roomiebuddy/lib/pages/calendar_page.dart index a88182f..a90287c 100644 --- a/roomiebuddy/lib/pages/calendar_page.dart +++ b/roomiebuddy/lib/pages/calendar_page.dart @@ -2,6 +2,11 @@ import 'package:flutter/material.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:provider/provider.dart'; import 'package:roomiebuddy/providers/theme_provider.dart'; +import 'package:roomiebuddy/services/auth_storage.dart'; +import 'package:roomiebuddy/services/api_service.dart'; +import 'package:roomiebuddy/common/widget/task/task_list_widget.dart'; +import 'package:roomiebuddy/utils/data_transformer.dart'; +import 'dart:async'; class CalendarPage extends StatefulWidget { const CalendarPage({super.key}); @@ -11,20 +16,36 @@ class CalendarPage extends StatefulWidget { } class _CalendarPageState extends State { - // CALENDAR PROPERTIES CalendarFormat _calendarFormat = CalendarFormat.month; DateTime _selectedDay = DateTime.now(); // Tracks day user has selected DateTime _focusedDay = DateTime.now(); // Tracks month period in view final DateTime _firstDay = DateTime.utc(2020, 1, 1); final DateTime _lastDay = DateTime.utc(2030, 12, 31); - // STYLE CONSTANTS + Map>> _events = {}; + bool _showOnlyMyTasks = true; + static const double _cellMargin = 2.0; static const double _cellPadding = 4.0; static const double _borderRadius = 8.0; static const double _fontSize = 14.0; - // MAIN BUILD METHOD + bool _isTaskLoading = true; + List> _roommateGroups = []; + String? _userId; + String? _password; + + final AuthStorage _authStorage = AuthStorage(); + final ApiService _apiService = ApiService(); + + @override + void initState() { + super.initState(); + _loadUserDataAndGroups(); + } + + // ------------ Main Build Method ------------ // + @override Widget build(BuildContext context) { final themeProvider = Provider.of(context); @@ -46,25 +67,233 @@ class _CalendarPageState extends State { children: [ _buildCalendar(), const SizedBox(height: 20), - Text( - 'Selected Day: ${_formatSelectedDate()}', - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Selected Day: ${_formatSelectedDate()}', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: themeProvider.currentTextColor), + ), + Tooltip( + message: _showOnlyMyTasks ? 'Show all tasks' : 'Show only my tasks', + child: IconButton( + icon: Icon( + _showOnlyMyTasks ? Icons.person : Icons.group, + color: themeProvider.currentTextColor, + ), + onPressed: () { + setState(() { + _showOnlyMyTasks = !_showOnlyMyTasks; + }); + }, + ), + ), + ], ), const SizedBox(height: 10), _buildEventsList(themeProvider), + const SizedBox(height: 10), ], ), ), ); } - // CALENDAR BUILDER METHODS + // ------------ Backend Communication Methods ------------ // + Future _loadUserDataAndGroups() async { + _userId = await _authStorage.getUserId(); + _password = await _authStorage.getPassword(); + + if (_userId != null && _password != null) { + try { + final response = await _apiService.getGroupList(_userId!, _password!); + if (mounted) { + if (response['success']) { + final groupsMap = response['data']?['groups'] as Map? ?? {}; + setState(() { + _roommateGroups = groupsMap.values.map((rawGroup) { + final group = Map.from(rawGroup as Map); + final List membersData = group['members'] ?? []; + final List> processedMembers = membersData.map((member) { + if (member is Map) { + return member; + } else if (member is Map) { + return Map.from(member); + } else { + return {'user_id': 'unknown', 'username': 'Invalid Member Data'}; + } + }).toList(); + return { + ...group, + 'members': processedMembers, + 'group_name': group['name'] ?? 'Unnamed Group' + }; + }).toList(); + }); + // After loading groups, load all tasks + await _loadAllTasks(); + } else { + setState(() => _isTaskLoading = false); // Stop loading if group fetch fails + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to load groups: ${response['message']}')), + ); + } + } + } catch (e) { + if (mounted) { + setState(() => _isTaskLoading = false); // Stop loading on error + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error loading groups: $e')), + ); + } + } + } else { + if (mounted) { + setState(() => _isTaskLoading = false); // Stop loading if not logged in + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please log in to view tasks.')), + ); + } + } + } + + Future _loadAllTasks() async { + if (!mounted || _userId == null || _password == null || _roommateGroups.isEmpty) { + setState(() => _isTaskLoading = false); // Stop loading if prerequisites aren't met + return; + } + + setState(() { + _isTaskLoading = true; // Start loading tasks + _events = {}; // Clear previous events + }); + + try { + List>> taskFutures = []; + for (var group in _roommateGroups) { + final groupId = group['group_id']; + if (groupId != null) { + taskFutures.add(_apiService.getGroupTasks(_userId!, groupId, _password!)); + } + } + + final List> responses = await Future.wait(taskFutures); + final List> allTasks = []; + final Set addedTaskIds = {}; // To handle potential duplicates if API returns same task for multiple group calls + + for (var response in responses) { + if (response['success']) { + final Map? tasksData = response['data'] as Map?; + if (tasksData != null && tasksData.containsKey('tasks')) { + final tasksMap = tasksData['tasks'] as Map; + tasksMap.forEach((taskId, taskDataRaw) { + if (!addedTaskIds.contains(taskId)) { + final taskData = taskDataRaw as Map; + // Process task data + final double? dueTimestamp = taskData['due_timestamp'] as double?; + DateTime? dueDateObject; + String? dueDateStr; + String? dueTimeStr; + if (dueTimestamp != null) { + final dateTime = DateTime.fromMillisecondsSinceEpoch((dueTimestamp * 1000).toInt()); + dueDateObject = dateTime; + const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + dueDateStr = "${monthNames[dateTime.month - 1]} ${dateTime.day}, ${dateTime.year}"; + int hour = dateTime.hour; + final int minute = dateTime.minute; + final String period = hour < 12 ? 'AM' : 'PM'; + if (hour == 0) { hour = 12; } else if (hour > 12) { hour -= 12; } + dueTimeStr = "$hour:${minute.toString().padLeft(2, '0')} $period"; + final defaultDateCheck = "${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}"; + final defaultTimeCheck = "${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}"; + if (defaultDateCheck == '2000-01-01' && defaultTimeCheck == '00:00') { + dueDateStr = null; dueTimeStr = null; + } + } + final int priorityInt = taskData['priority'] as int? ?? 0; + final String priorityStr = priorityToString(priorityInt); + final int estDay = taskData['est_day'] as int? ?? 0; + final int estHour = taskData['est_hour'] as int? ?? 0; + final int estMin = taskData['est_min'] as int? ?? 0; + List durationParts = []; + if (estDay > 0) durationParts.add('$estDay day${estDay > 1 ? 's' : ''}'); + if (estHour > 0) durationParts.add('$estHour hour${estHour > 1 ? 's' : ''}'); + if (estMin > 0) durationParts.add('$estMin min${estMin > 1 ? 's' : ''}'); + String estimatedDuration = durationParts.join(' '); + if (estimatedDuration.isEmpty) estimatedDuration = 'Not specified'; + final int recurrenceInt = taskData['recursive'] as int? ?? 0; + String recurrence = 'Does not repeat'; + switch (recurrenceInt) { + case 1: recurrence = 'Repeats Daily'; break; + case 7: recurrence = 'Repeats Weekly'; break; + case 30: recurrence = 'Repeats Monthly'; break; + } + + allTasks.add({ + "id": taskId, + "taskName": taskData["name"] ?? "No Task Name", + "assignedBy": taskData["assigner_username"] ?? taskData["assigner_id"] ?? "Unknown", + "assignedTo": taskData["assignee_username"] ?? taskData["assign_id"] ?? "Unknown", + "priority": priorityStr, + "description": taskData["description"] ?? "", + "dueDate": dueDateStr, + "dueTime": dueTimeStr, + "dueDateObject": dueDateObject, + "estimatedDuration": estimatedDuration, + "recurrence": recurrence, + "photo": taskData["image_path"], + "assignee_id": taskData["assign_id"], + "group_id": taskData["group_id"], + "completed": taskData["completed"] as bool? ?? false, + }); + addedTaskIds.add(taskId); + } + }); + } + } else { + // Handle individual task fetch failure (optional: show a message) + debugPrint('Failed to load tasks for a group: ${response['message']}'); + } + } + if (mounted) { + setState(() { + // Populate the events map + _events = {}; + for (var task in allTasks) { + if (task['dueDateObject'] != null) { + final date = task['dueDateObject'] as DateTime; + final dayOnly = DateTime.utc(date.year, date.month, date.day); + if (_events[dayOnly] == null) { + _events[dayOnly] = []; + } + _events[dayOnly]!.add(task); + } + } + }); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error fetching tasks: $e')), + ); + } + } finally { + if (mounted) { + setState(() { + _isTaskLoading = false; // Loading finished + }); + } + } + } + + // ------------ Calendar Builder Methods ------------ // Widget _buildCalendar() { return TableCalendar( firstDay: _firstDay, lastDay: _lastDay, focusedDay: _focusedDay, calendarFormat: _calendarFormat, + eventLoader: _getEventsForDay, selectedDayPredicate: (day) => isSameDay(_selectedDay, day), onDaySelected: _onDaySelected, onFormatChanged: _onFormatChanged, @@ -75,7 +304,7 @@ class _CalendarPageState extends State { ); } - // CALENDAR EVENT HANDLERS + // ------------ Calendar Event Handlers ------------ // void _onDaySelected(DateTime selectedDay, DateTime focusedDay) { setState(() { _selectedDay = selectedDay; @@ -89,7 +318,7 @@ class _CalendarPageState extends State { }); } - // CALENDAR STYLE CONFIG + // ------------ Calendar Style Config ------------ // HeaderStyle _buildHeaderStyle() { return const HeaderStyle( formatButtonVisible: true, @@ -120,6 +349,12 @@ class _CalendarPageState extends State { ), cellMargin: const EdgeInsets.all(_cellMargin), cellAlignment: Alignment.topLeft, + markersAlignment: Alignment.bottomRight, + markerDecoration: BoxDecoration( + color: themeProvider.isDarkMode ? Colors.white : Colors.black87, + shape: BoxShape.circle, + ), + markersMaxCount: 1, ); } @@ -132,7 +367,7 @@ class _CalendarPageState extends State { ); } - // DAY CELL BUILDERS + // ------------ Day Cell Builders ------------ // Widget _defaultDayBuilder(BuildContext context, DateTime day, DateTime focusedDay) { final themeProvider = Provider.of(context); return _buildBaseDayContainer( @@ -183,7 +418,7 @@ class _CalendarPageState extends State { day, decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(_borderRadius)), - border: Border.all(color: themeProvider.currentBorderColor.withOpacity(0.3), width: 0.5), + border: Border.all(color: themeProvider.currentBorderColor.withAlpha(77), width: 0.5), ), textStyle: TextStyle( fontSize: _fontSize, @@ -208,29 +443,46 @@ class _CalendarPageState extends State { ); } - // HELPER METHODS + // ------------ Helper Methods ------------ // String _formatSelectedDate() { return '${_selectedDay.month}-${_selectedDay.day}-${_selectedDay.year}'; // "MM-DD-YYYY" formatting } - // EVENT LIST WIDGETS + // Helper to get events for a specific day (normalized) + List> _getEventsForDay(DateTime day) { + final normalizedDay = DateTime.utc(day.year, day.month, day.day); + final dayEvents = _events[normalizedDay] ?? []; + + // Filter events based on _showOnlyMyTasks + if (_showOnlyMyTasks && _userId != null) { + return dayEvents.where((task) => task['assignee_id'] == _userId).toList(); + } else { + return dayEvents; + } + } + + // ------------ Event List Widgets ------------ // Widget _buildEventsList(ThemeProvider themeProvider) { + final Set userGroupIds = _roommateGroups + .map((group) => group['group_id']) + .where((id) => id != null) + .cast() + .toSet(); + + final tasksForSelectedDay = _getEventsForDay(_selectedDay); + return Expanded( - child: Container( - decoration: BoxDecoration( - color: themeProvider.currentInputFill, - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.all(12), - child: Center( - child: Text( - 'No events for this day', - style: TextStyle( - color: themeProvider.currentSecondaryTextColor, + child: _isTaskLoading + ? const Center(child: CircularProgressIndicator()) + : TaskListWidget( + allTasks: tasksForSelectedDay, + focusedGroupId: null, + userGroupIds: userGroupIds, + roommateGroups: _roommateGroups, + onTaskActionCompleted: _loadAllTasks, + showOnlyMyTasks: _showOnlyMyTasks, + currentUserId: _userId, ), - ), - ), - ), ); } } diff --git a/roomiebuddy/lib/pages/group_page.dart b/roomiebuddy/lib/pages/group_page.dart index 98980a3..7bdfd67 100644 --- a/roomiebuddy/lib/pages/group_page.dart +++ b/roomiebuddy/lib/pages/group_page.dart @@ -44,6 +44,8 @@ class _GroupPageState extends State { super.dispose(); } + // ------- Backend Communication Methods ------- // + Future _loadUserData() async { setState(() { _isLoading = true; @@ -274,6 +276,8 @@ class _GroupPageState extends State { } } + // ------- Main Build Method ------- // + @override Widget build(BuildContext context) { final themeProvider = Provider.of(context); diff --git a/roomiebuddy/lib/pages/home_page.dart b/roomiebuddy/lib/pages/home_page.dart index 6ccf2b0..13830e4 100644 --- a/roomiebuddy/lib/pages/home_page.dart +++ b/roomiebuddy/lib/pages/home_page.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'dart:convert'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:provider/provider.dart'; import '../common/widget/appbar/appbar.dart'; import '../providers/theme_provider.dart'; import 'package:roomiebuddy/services/auth_storage.dart'; import 'package:roomiebuddy/services/api_service.dart'; +import 'subpages/home/group_detail_page.dart'; +import '../common/widget/search/task_search_delegate.dart'; +import '../common/widget/task/task_list_widget.dart'; +import '../utils/data_transformer.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -16,42 +18,67 @@ class HomePage extends StatefulWidget { } class HomePageState extends State { - bool _isLoading = false; + bool _isTaskLoading = false; List> _tasks = []; List> _roommateGroups = []; + int _currentGroupIndex = 0; + bool _showOnlyMyTasks = true; final AuthStorage _authStorage = AuthStorage(); final ApiService _apiService = ApiService(); + String? _userId; String? _password; - String _userName = "User"; + String _userName = "Unknown"; @override void initState() { super.initState(); - _loadUserDataAndTasks(); + _loadUserDataAndGroups(); } - Future _loadUserDataAndTasks() async { - if (!mounted) return; - setState(() => _isLoading = true); + // --- Backend Communication Functions --- // + + Future _loadUserDataAndGroups() async { _userId = await _authStorage.getUserId(); _password = await _authStorage.getPassword(); _userName = await _authStorage.getUsername() ?? "User"; if (_userId != null && _password != null) { - await Future.wait([ - _loadTasks(_userId!, _password!), - _loadGroups(_userId!, _password!), - ]); + try { + // Load groups first + await _loadGroups(_userId!, _password!); + // After loading groups, load tasks for the initial group (if any) + if (_roommateGroups.isNotEmpty) { + final initialGroupId = _roommateGroups[_currentGroupIndex]['group_id'] ?? _roommateGroups[_currentGroupIndex]['uuid'] ?? _roommateGroups[_currentGroupIndex]['id']; + if (initialGroupId != null) { + await _loadTasksForGroup(initialGroupId); // Load tasks for the first group + } else { + // Handle case where the initial group has no ID? Set tasks empty? + if (mounted) setState(() => _tasks = []); + } + } else { + // No groups, so no group tasks to load + if (mounted) setState(() => _tasks = []); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error loading initial data: $e')), + ); + setState(() => _tasks = []); // Clear tasks on error + } + } } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Please log in to view tasks.')), ); + setState(() { + _tasks = []; // Clear tasks if not logged in + }); } - if (mounted) setState(() => _isLoading = false); } } @@ -81,6 +108,10 @@ class HomePageState extends State { 'group_name': group['name'] ?? 'Unnamed Group' }; }).toList(); + // Reset index if it's out of bounds after refresh + if (_currentGroupIndex >= _roommateGroups.length) { + _currentGroupIndex = _roommateGroups.isEmpty ? 0 : _roommateGroups.length - 1; + } }); } } else { @@ -88,6 +119,7 @@ class HomePageState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to load groups: ${response['message']}')), ); + setState(() => _roommateGroups = []); // Clear groups on failure } } } catch (e) { @@ -95,291 +127,354 @@ class HomePageState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error loading groups: $e')), ); + setState(() => _roommateGroups = []); // Clear groups on error } } } - String _getGreeting() { - final hour = DateTime.now().hour; - if (hour < 12) return "Good Morning"; - if (hour < 18) return "Good Afternoon"; - return "Good Evening"; - } + Future _loadTasksForGroup(String groupId) async { + if (!mounted || _userId == null || _password == null) return; - Future _loadTasks(String userId, String password) async { - if (!mounted) return; setState(() { - _isLoading = true; + _isTaskLoading = true; // Use the task-specific loading flag }); try { - final response = await http.post( - Uri.parse('http://10.0.2.2:5000/get_user_task'), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'user_id': userId, - 'password': password - }), - ); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body); - if (decoded is List && decoded.isNotEmpty) { - final firstItem = decoded[0]; - if (firstItem['error_no'] == '0' && firstItem['message'] is Map) { - final tasksMap = firstItem['message'] as Map; - if (mounted) { - setState(() { - _tasks = tasksMap.entries.map((entry) { - final taskId = entry.key; - final taskData = entry.value as Map; - - final double? dueTimestamp = taskData['due_timestamp'] as double?; - String? dueDateStr; - String? dueTimeStr; - if (dueTimestamp != null) { - final dateTime = DateTime.fromMillisecondsSinceEpoch((dueTimestamp * 1000).toInt()); - dueDateStr = "${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}"; - dueTimeStr = "${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}"; + // Use ApiService to get tasks for the specified group + final response = await _apiService.getGroupTasks(_userId!, groupId, _password!); + + if (response['success']) { + final Map? tasksData = response['data'] as Map?; + + if (tasksData != null && tasksData.containsKey('tasks')) { + final tasksMap = tasksData['tasks'] as Map; + + if (mounted) { + setState(() { + _tasks = tasksMap.entries.map((entry) { + final taskId = entry.key; + final taskData = entry.value as Map; + + final double? dueTimestamp = taskData['due_timestamp'] as double?; + String? dueDateStr; + String? dueTimeStr; + if (dueTimestamp != null) { + final dateTime = DateTime.fromMillisecondsSinceEpoch((dueTimestamp * 1000).toInt()); + + // Format Date: Month DD, YYYY + const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + dueDateStr = "${monthNames[dateTime.month - 1]} ${dateTime.day}, ${dateTime.year}"; + + // Format Time: H:MM AM/PM + int hour = dateTime.hour; + final int minute = dateTime.minute; + final String period = hour < 12 ? 'AM' : 'PM'; + if (hour == 0) { + hour = 12; // Midnight + } else if (hour > 12) { + hour -= 12; // Convert to 12-hour format } + dueTimeStr = "$hour:${minute.toString().padLeft(2, '0')} $period"; + + // Check for default date/time from backend (using original YYYY-MM-DD format for check) + final defaultDateCheck = "${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}"; + final defaultTimeCheck = "${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}"; + if (defaultDateCheck == '2000-01-01' && defaultTimeCheck == '00:00') { + dueDateStr = null; // Treat as unspecified + dueTimeStr = null; + } + } - final int priorityInt = taskData['priority'] as int? ?? 0; - final String priorityStr = priorityToString(priorityInt); - - return { - "id": taskId, - "taskName": taskData["name"] ?? "No Task Name", - "assignedBy": taskData["assigner_username"] ?? taskData["assigner_id"] ?? "Unknown", - "priority": priorityStr, - "description": taskData["description"] ?? "", - "dueDate": dueDateStr, - "dueTime": dueTimeStr, - "photo": taskData["image_path"], - "assignee_id": taskData["assign_id"], - "group_id": taskData["group_id"], - "completed": taskData["completed"] as bool? ?? false, - }; - }).toList(); - }); - } - } else { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to load tasks: ${firstItem['message']}')), - ); - } + final int priorityInt = taskData['priority'] as int? ?? 0; + final String priorityStr = priorityToString(priorityInt); + + // Process estimated duration + final int estDay = taskData['est_day'] as int? ?? 0; + final int estHour = taskData['est_hour'] as int? ?? 0; + final int estMin = taskData['est_min'] as int? ?? 0; + List durationParts = []; + if (estDay > 0) durationParts.add('$estDay day${estDay > 1 ? 's' : ''}'); + if (estHour > 0) durationParts.add('$estHour hour${estHour > 1 ? 's' : ''}'); + if (estMin > 0) durationParts.add('$estMin min${estMin > 1 ? 's' : ''}'); + String estimatedDuration = durationParts.join(' '); + if (estimatedDuration.isEmpty) { + estimatedDuration = 'Not specified'; + } + + // Process recurrence + final int recurrenceInt = taskData['recursive'] as int? ?? 0; + String recurrence = 'Does not repeat'; + switch (recurrenceInt) { + case 1: recurrence = 'Repeats Daily'; break; + case 7: recurrence = 'Repeats Weekly'; break; + case 30: recurrence = 'Repeats Monthly'; break; + // Add other cases if needed + } + + return { + "id": taskId, + "taskName": taskData["name"] ?? "No Task Name", + "assignedBy": taskData["assigner_username"] ?? taskData["assigner_id"] ?? "Unknown", + "assignedTo": taskData["assignee_username"] ?? taskData["assign_id"] ?? "Unknown", + "priority": priorityStr, + "description": taskData["description"] ?? "", + "dueDate": dueDateStr, + "dueTime": dueTimeStr, + "estimatedDuration": estimatedDuration, + "recurrence": recurrence, + "photo": taskData["image_path"], + "assignee_id": taskData["assign_id"], + "group_id": taskData["group_id"], + "completed": taskData["completed"] as bool? ?? false, + }; + }).toList(); + }); } } else { if (mounted) { + setState(() => _tasks = []); // Clear tasks if format is invalid ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Failed to load tasks: Invalid server response.')), + const SnackBar(content: Text('Failed to load tasks for group: Invalid data format.')), ); } } } else { if (mounted) { + setState(() => _tasks = []); // Clear tasks on failure ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to load tasks: Server error ${response.statusCode}')), + SnackBar(content: Text('Failed to load tasks for group: ${response['message']}')), ); } } } catch (e) { if (mounted) { + setState(() => _tasks = []); // Clear tasks on error ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error fetching tasks: $e')), + SnackBar(content: Text('Error fetching tasks for group: $e')), ); } } finally { if (mounted) { setState(() { - _isLoading = false; + _isTaskLoading = false; // Loading finished for this group's tasks }); } } } - String priorityToString(int priority) { - switch (priority) { - case 0: return 'Low'; - case 1: return 'Medium'; - case 2: return 'High'; - default: return 'Unknown'; - } + // --- Helper Functions --- // + + String _getGreeting() { + final hour = DateTime.now().hour; + if (hour < 12) return "Good Morning"; + if (hour < 18) return "Good Afternoon"; + return "Good Evening"; } + // --- Build Widgets --- // + @override Widget build(BuildContext context) { String userName = _userName; - String? profileImagePath; + // String? profileImagePath; final themeProvider = Provider.of(context); - return SafeArea( - child: Scaffold( - appBar: TAppBar( - title: Row( - children: [ - if (profileImagePath != null) - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.grey, width: 2), - ), - child: ClipOval( - child: Image.asset( - profileImagePath, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - Icon(Icons.person, size: 30, color: Colors.grey), - ), - ), - ) - else - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.grey, width: 2), - ), - child: Icon(Icons.person, size: 30, color: Colors.grey), + return Scaffold( + // -------------------- AppBar -------------------- // + appBar: TAppBar( + title: Row( + // Profile Image + children: [ + // COMMENTED OUT UNTIL WE GET PROFILE PIC WORKING (The warning was bothering me) + // if (profileImagePath != null) + // Container( + // width: 40, + // height: 40, + // decoration: BoxDecoration( + // shape: BoxShape.circle, + // border: Border.all(color: Colors.grey, width: 2), + // ), + // child: ClipOval( + // child: Image.asset( + // profileImagePath, + // fit: BoxFit.cover, + // errorBuilder: (context, error, stackTrace) => const Icon(Icons.person, size: 30, color: Colors.grey), + // ), + // ), + // ) + // else + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.grey, width: 2), ), - const SizedBox(width: 10), - Text( - "${_getGreeting()}, $userName", - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + child: const Icon(Icons.person, size: 30, color: Colors.grey), ), - ], - ), - actions: [ - IconButton( - icon: const Icon(Icons.search), - onPressed: () { - showSearch( - context: context, - delegate: TaskSearchDelegate(tasks: _tasks), - ); - }, + const SizedBox(width: 10), + // Greeting + Text( + "${_getGreeting()}, $userName", + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), ], ), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, vertical: 12.0), + actions: [ + // Search Button + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + showSearch( + context: context, + delegate: TaskSearchDelegate(tasks: _tasks), + ); + }, + ), + ], + ), + // -------------------- Body -------------------- // + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Roommate Groups title + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 12.0), + child: Text( + 'Groups', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), + ), + ), + // Roommate Groups carousel + Container( + color: themeProvider.themeColor, + height: 180, + child: _roommateGroups.isEmpty + ? Center( child: Text( - 'Roommate Groups', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: themeProvider.currentTextColor, - ), + 'No groups found.', + style: TextStyle(color: themeProvider.currentSecondaryTextColor), ), - ), - Container( - color: themeProvider.themeColor, - height: 180, - child: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _roommateGroups.isEmpty - ? Center( - child: Text( - 'No groups found.', - style: TextStyle(color: themeProvider.currentSecondaryTextColor), - ), - ) - : _buildGroupCarousel(), - ), - ], + ) + : _buildGroupCarousel(), + ), + + // -------------------- Leaderboard Section -------------------- // + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0), + child: Text( + 'Leaderboard', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, ), ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'My Tasks', - style: TextStyle( - fontSize: 25, - fontWeight: FontWeight.bold, - color: themeProvider.currentTextColor, + ), + const SizedBox(height: 50), + + // -------------------- Task Section -------------------- // + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 16.0, 8.0, 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Dynamic title + Text( + _showOnlyMyTasks ? 'My Tasks' : 'All Tasks', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: themeProvider.currentTextColor, + ), ), - ), + // Dynamic icon + Tooltip( + message: _showOnlyMyTasks ? 'Show all tasks' : 'Show only my tasks', + child: IconButton( + icon: Icon( + _showOnlyMyTasks ? Icons.person : Icons.group, + color: themeProvider.currentTextColor, + ), + onPressed: () { + setState(() { + _showOnlyMyTasks = !_showOnlyMyTasks; + }); + }, + ), + ), + ], ), - _isLoading - ? const Center(child: CircularProgressIndicator()) - : displayTasks(), - ], - ), + ), + + // Tasks List (in task_list_widget.dart) + _isTaskLoading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: _buildTaskList(), + ), + const SizedBox(height: 50), + ], ), ), ); } - Widget displayTasks() { - final themeProvider = Provider.of(context); - - if (_tasks.isEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Text( - 'No tasks assigned to you yet!', - style: TextStyle(color: themeProvider.currentSecondaryTextColor, fontSize: 16), - textAlign: TextAlign.center, - ), - ), - ); + Widget _buildTaskList() { + // Get the ID of the group currently in focus + String? currentFocusedGroupId; + if (_roommateGroups.isNotEmpty && _currentGroupIndex < _roommateGroups.length) { + currentFocusedGroupId = _roommateGroups[_currentGroupIndex]['group_id']; } - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _tasks.length, - itemBuilder: (context, index) { - final task = _tasks[index]; - Color priorityColor = themeProvider.currentSecondaryTextColor; - if (task['priority'] == 'Medium') { - priorityColor = themeProvider.warningColor; - } else if (task['priority'] == 'High') { - priorityColor = themeProvider.errorColor; - } - - return Card( - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - color: themeProvider.currentCardBackground, - child: ListTile( - title: Text(task['taskName'], style: TextStyle(color: themeProvider.currentTextColor)), - subtitle: Text('Assigned by: ${task['assignedBy']}', style: TextStyle(color: themeProvider.currentSecondaryTextColor)), - trailing: Text(task['priority'], - style: TextStyle(color: priorityColor, fontWeight: FontWeight.bold)), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TaskDetailScreen(task: task), - ), - ); - }, - ), - ); - }, + // Get all group IDs the user is in + final Set userGroupIds = _roommateGroups + .map((group) => group['group_id']) // iterate through all groups + .where((id) => id != null) // filter out null IDs + .cast() // cast to String + .toSet(); // convert to a set + + // --- Task List Widget --- // + return ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 300, // min height (CHANGE THIS TO MAKE DYNAMIC) + maxHeight: 300, // max height + ), + child: TaskListWidget( + allTasks: _tasks, + focusedGroupId: currentFocusedGroupId, + userGroupIds: userGroupIds, + roommateGroups: _roommateGroups, + onTaskActionCompleted: () { + if (currentFocusedGroupId != null) { + _loadTasksForGroup(currentFocusedGroupId); + } else { + // Reload groups and then tasks if group ID is missing + _loadUserDataAndGroups(); + } + }, + showOnlyMyTasks: _showOnlyMyTasks, + currentUserId: _userId, + ), ); } Widget _buildGroupCarousel() { final themeProvider = Provider.of(context); + + // No groups to display if (_roommateGroups.isEmpty) { return const Center(child: Text('No groups to display.')); } + // --- Group Carousel --- // return SizedBox( height: 140, child: CarouselSlider( @@ -387,24 +482,47 @@ class HomePageState extends State { height: 140.0, enlargeCenterPage: true, autoPlay: false, - viewportFraction: 0.6, + viewportFraction: 0.85, + initialPage: _currentGroupIndex, + onPageChanged: (index, reason) { + setState(() { + _currentGroupIndex = index; + }); + // Load tasks for the newly focused group + if (_roommateGroups.isNotEmpty && index < _roommateGroups.length) { + final newGroupId = _roommateGroups[index]['group_id']; + if (newGroupId != null) { + _loadTasksForGroup(newGroupId); + } + } + }, ), items: _roommateGroups.map((group) { - final List membersData = group['members'] ?? []; - final List memberNames = membersData - .map((member) => member['username'] as String? ?? 'Unknown') - .toList(); - return Builder( builder: (BuildContext context) { return GestureDetector( - onTap: () { - Navigator.push( + onTap: () async { + // Determine if this is the last group before navigating + final result = await Navigator.push( context, MaterialPageRoute( - builder: (_) => GroupDetailScreen(group: group), + builder: (_) => GroupDetailScreen( + group: group, + ), ), ); + + // If left group (result is true), refresh the groups list and initial tasks + if (result == true && mounted) { + _loadUserDataAndGroups(); // Reload groups and tasks for index 0 + } + // If a task within the group was updated, refresh tasks for the *current* group + else if (result == 'task_updated' && mounted) { + final currentGroupId = _roommateGroups[_currentGroupIndex]['group_id']; + if (currentGroupId != null) { + _loadTasksForGroup(currentGroupId); + } + } }, child: Container( width: MediaQuery.of(context).size.width * 0.6, @@ -424,7 +542,8 @@ class HomePageState extends State { ], ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( group['group_name'] ?? 'Unnamed Group', @@ -433,16 +552,7 @@ class HomePageState extends State { fontWeight: FontWeight.bold, color: themeProvider.currentTextColor, ), - ), - const SizedBox(height: 8), - Text( - "Members: ${memberNames.join(', ')}", - style: TextStyle( - fontSize: 16, - color: themeProvider.currentSecondaryTextColor, - ), - overflow: TextOverflow.ellipsis, - maxLines: 2, + textAlign: TextAlign.center, ), ], ), @@ -455,116 +565,3 @@ class HomePageState extends State { ); } } - -// ========================== -// EXTRA SCREENS (Appended) -// ========================== - -class TaskDetailScreen extends StatelessWidget { - final Map task; - - const TaskDetailScreen({super.key, required this.task}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(task['taskName'])), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: ListView( - children: [ - Text("Title: ${task['taskName']}", - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), - const SizedBox(height: 12), - Text("Description: ${task['description'] ?? "No description"}"), - const SizedBox(height: 12), - Text("Priority: ${task['priority']}"), - const SizedBox(height: 12), - if (task['photo'] != null) - Image.network(task['photo'], - errorBuilder: (_, __, ___) => const Text("Image failed to load")), - const SizedBox(height: 12), - Text("Due Date: ${task['dueDate'] ?? 'Not specified'}"), - Text("Time Due: ${task['dueTime'] ?? 'Not specified'}"), - ], - ), - ), - ); - } -} - -class GroupDetailScreen extends StatelessWidget { - final Map group; - - const GroupDetailScreen({super.key, required this.group}); - - @override - Widget build(BuildContext context) { - final List membersData = group['members'] ?? []; - final List memberNames = membersData - .map((member) => member['username'] as String? ?? 'Unknown') - .toList(); - - return Scaffold( - appBar: AppBar(title: Text(group['group_name'] ?? 'Group Details')), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Group Name: ${group['group_name'] ?? 'Unnamed Group'}", - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold) - ), - const SizedBox(height: 20), - const Text("Members:", style: TextStyle(fontSize: 20)), - ...memberNames.map((name) => Padding( - padding: const EdgeInsets.only(left: 8.0, top: 4.0), - child: Text("- $name"), - )), - ], - ), - ), - ); - } -} - -class TaskSearchDelegate extends SearchDelegate { - final List> tasks; - TaskSearchDelegate({required this.tasks}); - - @override - List buildActions(BuildContext context) => - [IconButton(icon: const Icon(Icons.clear), onPressed: () => query = '')]; - - @override - Widget buildLeading(BuildContext context) => - IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => close(context, null)); - - @override - Widget buildResults(BuildContext context) { - final results = tasks - .where((task) => - task['taskName'].toLowerCase().contains(query.toLowerCase())) - .toList(); - - return ListView.builder( - itemCount: results.length, - itemBuilder: (context, index) => ListTile( - title: Text(results[index]['taskName']), - subtitle: Text('Assigned by: ${results[index]['assignedBy']}'), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => TaskDetailScreen(task: results[index]), - ), - ); - }, - ), - ); - } - - @override - Widget buildSuggestions(BuildContext context) => buildResults(context); -} diff --git a/roomiebuddy/lib/pages/settings_page.dart b/roomiebuddy/lib/pages/settings_page.dart index 82ba93c..09afaa2 100644 --- a/roomiebuddy/lib/pages/settings_page.dart +++ b/roomiebuddy/lib/pages/settings_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:roomiebuddy/providers/theme_provider.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; -import 'package:roomiebuddy/pages/login_screen.dart'; +import 'package:roomiebuddy/pages/subpages/auth/login_screen.dart'; import 'package:roomiebuddy/services/auth_storage.dart'; import 'package:flutter/services.dart'; @@ -26,6 +26,8 @@ class _SettingsPageState extends State { _loadUserData(); } + // ------- Backend Communication Methods ------- // + Future _loadUserData() async { setState(() { _isLoading = true; @@ -62,6 +64,8 @@ class _SettingsPageState extends State { } } + // ------- Color Picker Method ------- // + void _openColorPicker(BuildContext context, ThemeProvider themeProvider) { Color pickerColor = themeProvider.themeColor; @@ -113,6 +117,8 @@ class _SettingsPageState extends State { ); } + // ------- Main Build Method ------- // + @override Widget build(BuildContext context) { final themeProvider = Provider.of(context); @@ -166,7 +172,7 @@ class _SettingsPageState extends State { ), margin: const EdgeInsets.only(bottom: 16.0), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), + padding: const EdgeInsets.fromLTRB(16.0, 4.0, 0.0, 4.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/roomiebuddy/lib/pages/login_screen.dart b/roomiebuddy/lib/pages/subpages/auth/login_screen.dart similarity index 98% rename from roomiebuddy/lib/pages/login_screen.dart rename to roomiebuddy/lib/pages/subpages/auth/login_screen.dart index 7ddfcbc..399879f 100644 --- a/roomiebuddy/lib/pages/login_screen.dart +++ b/roomiebuddy/lib/pages/subpages/auth/login_screen.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:email_validator/email_validator.dart'; import 'package:provider/provider.dart'; -import 'package:roomiebuddy/NavScreen.dart'; +import 'package:roomiebuddy/nav_screen.dart'; import 'package:roomiebuddy/providers/theme_provider.dart'; -import 'package:roomiebuddy/pages/signup_screen.dart'; +import 'package:roomiebuddy/pages/subpages/auth/signup_screen.dart'; import 'package:roomiebuddy/services/api_service.dart'; import 'package:roomiebuddy/services/auth_storage.dart'; diff --git a/roomiebuddy/lib/pages/signup_screen.dart b/roomiebuddy/lib/pages/subpages/auth/signup_screen.dart similarity index 99% rename from roomiebuddy/lib/pages/signup_screen.dart rename to roomiebuddy/lib/pages/subpages/auth/signup_screen.dart index 9846491..b1e8395 100644 --- a/roomiebuddy/lib/pages/signup_screen.dart +++ b/roomiebuddy/lib/pages/subpages/auth/signup_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:email_validator/email_validator.dart'; import 'package:provider/provider.dart'; import 'package:roomiebuddy/providers/theme_provider.dart'; -import 'package:roomiebuddy/pages/login_screen.dart'; +import 'package:roomiebuddy/pages/subpages/auth/login_screen.dart'; import 'package:roomiebuddy/services/api_service.dart'; class SignupScreen extends StatefulWidget { diff --git a/roomiebuddy/lib/pages/subpages/home/group_detail_page.dart b/roomiebuddy/lib/pages/subpages/home/group_detail_page.dart new file mode 100644 index 0000000..3088e5c --- /dev/null +++ b/roomiebuddy/lib/pages/subpages/home/group_detail_page.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:roomiebuddy/services/api_service.dart'; +import 'package:roomiebuddy/services/auth_storage.dart'; + +class GroupDetailScreen extends StatelessWidget { + final Map group; + + const GroupDetailScreen({ + super.key, + required this.group, + }); + + @override + // ----- Group Detail Page ----- // + Widget build(BuildContext context) { + final List membersData = group['members'] ?? []; + final List memberNames = membersData + .map((member) => member['username'] as String? ?? 'Unknown') + .toList(); + + return Scaffold( + appBar: AppBar(title: Text(group['group_name'] ?? 'Group Details')), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Group Name: ${group['group_name'] ?? 'Unnamed Group'}", + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold) + ), + const SizedBox(height: 20), + const Text("Members:", style: TextStyle(fontSize: 20)), + ...memberNames.map((name) => Padding( + padding: const EdgeInsets.only(left: 8.0, top: 4.0), + child: Text("- $name"), + )), + const SizedBox(height: 30), + Center( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + onPressed: () => _showLeaveGroupConfirmation(context), + child: const Text('Leave Group'), + ), + ), + ], + ), + ), + ); + } + + // ----- Leave Group Confirmation ----- // + void _showLeaveGroupConfirmation(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text('Leave Group'), + content: Text('Are you sure you want to leave "${group['group_name'] ?? 'this group'}"?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + _leaveGroup(context); + }, + child: const Text('Leave'), + ), + ], + ); + }, + ); + } + + // ----- Leave Group (Backend Communication) ----- // + Future _leaveGroup(BuildContext context) async { + final AuthStorage authStorage = AuthStorage(); + final ApiService apiService = ApiService(); + + try { + final userId = await authStorage.getUserId(); + final password = await authStorage.getPassword(); + + if (!context.mounted) return; + + if (userId == null || password == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please log in to leave the group.')), + ); + return; + } + + // Try to find group ID from various possible fields + final String? groupId = group['group_id'] ?? group['uuid'] ?? group['id']; + if (groupId == null) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Group ID not found.')), + ); + return; + } + + final response = await apiService.leaveGroup(userId, password, groupId); + + if (!context.mounted) return; + + // Check for success directly from the Map response + final bool isSuccess = response['success'] == true; + + if (isSuccess) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Successfully left the group.')), + ); + + // Pop to home page and signal success (true) + Navigator.pop(context, true); + } else { + // Get error message directly from the Map response + final String errorMsg = response['message'] as String? ?? 'Unknown error leaving group'; + + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to leave group: $errorMsg')), + ); + } + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error leaving group: $e')), + ); + } + } +} \ No newline at end of file diff --git a/roomiebuddy/lib/pages/subpages/home/task_detail_page.dart b/roomiebuddy/lib/pages/subpages/home/task_detail_page.dart new file mode 100644 index 0000000..09f7933 --- /dev/null +++ b/roomiebuddy/lib/pages/subpages/home/task_detail_page.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:roomiebuddy/services/api_service.dart'; +import 'package:roomiebuddy/services/auth_storage.dart'; + +class TaskDetailScreen extends StatelessWidget { + final Map task; + + const TaskDetailScreen({super.key, required this.task}); + + @override + // ----- Task Detail Page ----- // + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(task['taskName'])), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + Text("Title: ${task['taskName']}", + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + Text("Description: ${task['description']?.isNotEmpty == true ? task['description'] : 'Not specified'}"), + const SizedBox(height: 12), + Text("Priority: ${task['priority']}"), + const SizedBox(height: 12), + Text("Assigned by: ${task['assignedBy']}"), + Text("Assigned to: ${task['assignedTo']}"), + const SizedBox(height: 12), + Text("Due Date: ${task['dueDate'] ?? 'Not specified'}"), + Text("Time Due: ${task['dueTime'] ?? 'Not specified'}"), + const SizedBox(height: 12), + Text("Estimated Duration: ${task['estimatedDuration'] ?? 'Not specified'}"), + const SizedBox(height: 12), + Text("Recurrence: ${task['recurrence'] ?? 'Not specified'}"), + ], + ), + const SizedBox(height: 30), + Center( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + onPressed: () => _showDeleteConfirmation(context), + child: const Text('Delete Task'), + ), + ), + ], + ), + ), + ); + } + + // ----- Delete Task Confirmation ----- // + void _showDeleteConfirmation(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text('Delete Task'), + content: Text('Are you sure you want to delete "${task['taskName']}"? This action cannot be undone.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + _deleteTask(context); + }, + child: const Text('Delete', style: TextStyle(color: Colors.red)), + ), + ], + ); + }, + ); + } + + // ----- Delete Task (Backend Communication) ----- // + Future _deleteTask(BuildContext context) async { + final AuthStorage authStorage = AuthStorage(); + final ApiService apiService = ApiService(); + + try { + final userId = await authStorage.getUserId(); + final password = await authStorage.getPassword(); + final taskId = task['id'] as String?; + + if ((userId == null || password == null)) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please log in to delete the task.')), + ); + return; + } + if (taskId == null) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Task ID not found.')), + ); + return; + } + + final response = await apiService.deleteTask(userId, password, taskId); + + if (!context.mounted) return; + + if (response['success']) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Task deleted successfully.')), + ); + Navigator.pop(context, true); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to delete task: ${response['message']}')), + ); + } + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error deleting task: $e')), + ); + } + } +} \ No newline at end of file diff --git a/roomiebuddy/lib/pages/view_taskpage.dart b/roomiebuddy/lib/pages/view_taskpage.dart deleted file mode 100644 index 876e163..0000000 --- a/roomiebuddy/lib/pages/view_taskpage.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class ViewTaskpage extends StatefulWidget { - const ViewTaskpage({super.key}); - - @override - State createState() => _ViewTaskpageState(); -} - -class _ViewTaskpageState extends State { - @override - Widget build(BuildContext context) { - return const Scaffold(); - } -} \ No newline at end of file diff --git a/roomiebuddy/lib/providers/theme_provider.dart b/roomiebuddy/lib/providers/theme_provider.dart index b144ebc..5b9380e 100644 --- a/roomiebuddy/lib/providers/theme_provider.dart +++ b/roomiebuddy/lib/providers/theme_provider.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; class ThemeProvider extends ChangeNotifier { - bool _isDarkMode = true; // GLOBAL DARK MODE FLAG - Color _themeColor = Colors.blueGrey; // GLOBAL THEME COLOR + bool _isDarkMode = true; // Dark mode set to default + Color _themeColor = Colors.blueGrey; // Global theme color // Utility colors (Theme independent) Color get errorColor => Colors.red; @@ -32,12 +32,12 @@ class ThemeProvider extends ChangeNotifier { Color get switchInactiveTrack => Colors.grey[300]!; // Circle colors (Main background) - Color get primaryHeaderColor => _themeColor.withOpacity(0.8); - Color get primaryHeaderOverlayColor => Colors.white.withOpacity(0.1); + Color get primaryHeaderColor => _themeColor.withAlpha(204); + Color get primaryHeaderOverlayColor => Colors.white.withAlpha(26); // Calendar colors Color get calendarSelectedDayColor => _themeColor; - Color get calendarTodayColor => _themeColor.withOpacity(0.5); + Color get calendarTodayColor => _themeColor.withAlpha(128); Color get calendarWeekendTextColor => _isDarkMode ? darkTextColor : lightTextColor; Color get calendarDefaultTextColor => _isDarkMode ? darkTextColor : lightTextColor; Color get calendarSelectedDayTextColor => _isDarkMode ? darkBackground : Colors.white; @@ -63,7 +63,7 @@ class ThemeProvider extends ChangeNotifier { final secondaryTextColor = isDark ? darkTextSecondary : lightTextSecondary; final borderColor = isDark ? darkBorder : lightBorder; final inputFillColor = isDark ? darkWidgetBackground : lightInputFill; - final appBarBgColor = isDark ? _themeColor.withOpacity(0.8) : _themeColor; + final appBarBgColor = isDark ? _themeColor.withAlpha(204) : _themeColor; return ThemeData( brightness: isDark ? Brightness.dark : Brightness.light, diff --git a/roomiebuddy/lib/services/api_service.dart b/roomiebuddy/lib/services/api_service.dart index c82cd67..620a913 100644 --- a/roomiebuddy/lib/services/api_service.dart +++ b/roomiebuddy/lib/services/api_service.dart @@ -3,12 +3,12 @@ import 'package:http/http.dart' as http; class ApiService { // BASE URL - static const String baseUrl = 'http://10.0.2.2:5000'; + static const String baseUrl = 'https://msdocs-python-webapp-quickstart-rmb.azurewebsites.net'; // HTTP client final http.Client _client = http.Client(); - // Singleton pattern implC + // Singleton pattern impl static final ApiService _instance = ApiService._internal(); factory ApiService() { @@ -65,7 +65,7 @@ class ApiService { }); } - // TASK METHODS + // ------------ TASK METHODS ------------ // // Get user tasks Future> getUserTasks(String userId, String password) async { @@ -75,6 +75,15 @@ class ApiService { }); } + // Get group tasks + Future> getGroupTasks(String userId, String groupId, String password) async { + return await post('/get_group_task', { + 'user_id': userId, + 'group_id': groupId, + 'password': password, + }); + } + // Add a new task Future> addTask( String taskName, @@ -114,7 +123,20 @@ class ApiService { }); } - // GROUP METHODS + // Delete a task + Future> deleteTask( + String userId, + String password, + String taskId, + ) async { + return await post('/delete_task', { + 'user_id': userId, + 'password': password, + 'task_id': taskId, + }); + } + + // ------------ GROUP METHODS ------------ // // Get all groups for a user Future> getGroupList(String userId, String password) async { @@ -170,17 +192,6 @@ class ApiService { return _handleError(e); } } - // **** END NEW METHOD **** - - // Helper to convert priority int to string - String priorityToString(int priority) { - switch (priority) { - case 0: return 'Low'; - case 1: return 'Medium'; - case 2: return 'High'; - default: return 'Unknown'; - } - } // Create a new group Future> createGroup( @@ -223,6 +234,8 @@ class ApiService { }); } + // ------------ INVITE METHODS ------------ // + // Invite a user to a group Future> inviteToGroup( String inviterId, @@ -261,6 +274,8 @@ class ApiService { }); } + // ------------ HANDLE RESPONSE METHODS ------------ // + // Handle HTTP response Map _handleResponse(http.Response response) { if (response.statusCode >= 200 && response.statusCode < 300) { diff --git a/roomiebuddy/lib/services/auth_storage.dart b/roomiebuddy/lib/services/auth_storage.dart index 56273e5..a3d31d2 100644 --- a/roomiebuddy/lib/services/auth_storage.dart +++ b/roomiebuddy/lib/services/auth_storage.dart @@ -1,5 +1,5 @@ import 'package:shared_preferences/shared_preferences.dart'; - +import 'package:flutter/foundation.dart' show debugPrint; class AuthStorage { // Keys for SharedPreferences static const String _userIdKey = 'user_id'; @@ -26,7 +26,7 @@ class AuthStorage { await prefs.setString(_usernameKey, username); return true; } catch (e) { - print('Error storing user credentials: $e'); + debugPrint('Error storing user credentials: $e'); return false; } } @@ -37,7 +37,7 @@ class AuthStorage { final prefs = await SharedPreferences.getInstance(); return prefs.getString(_userIdKey); } catch (e) { - print('Error getting user ID: $e'); + debugPrint('Error getting user ID: $e'); return null; } } @@ -48,7 +48,7 @@ class AuthStorage { final prefs = await SharedPreferences.getInstance(); return prefs.getString(_emailKey); } catch (e) { - print('Error getting email: $e'); + debugPrint('Error getting email: $e'); return null; } } @@ -59,7 +59,7 @@ class AuthStorage { final prefs = await SharedPreferences.getInstance(); return prefs.getString(_passwordKey); } catch (e) { - print('Error getting password: $e'); + debugPrint('Error getting password: $e'); return null; } } @@ -70,7 +70,7 @@ class AuthStorage { final prefs = await SharedPreferences.getInstance(); return prefs.getString(_usernameKey); } catch (e) { - print('Error getting username: $e'); + debugPrint('Error getting username: $e'); return null; } } @@ -86,8 +86,8 @@ class AuthStorage { final password = prefs.getString(_passwordKey); final username = prefs.getString(_usernameKey); - // For development/debugging - remove or set to false in production - print("Auth check: UserId=$userId, Email=$email, Username=$username"); + // For development/debugging - REMEMBER: remove or set to false in production (proabably wont happen) + debugPrint("Auth check: UserId=$userId, Email=$email, Username=$username"); // Ensure all values exist and aren't empty return userId != null && userId.isNotEmpty && @@ -95,7 +95,7 @@ class AuthStorage { password != null && password.isNotEmpty && username != null && username.isNotEmpty; } catch (e) { - print('Error checking login status: $e'); + debugPrint('Error checking login status: $e'); return false; } } @@ -110,7 +110,7 @@ class AuthStorage { await prefs.remove(_usernameKey); return true; } catch (e) { - print('Error clearing user credentials: $e'); + debugPrint('Error clearing user credentials: $e'); return false; } } diff --git a/roomiebuddy/lib/splash_screen.dart b/roomiebuddy/lib/splash_screen.dart index 344083a..facd7ba 100644 --- a/roomiebuddy/lib/splash_screen.dart +++ b/roomiebuddy/lib/splash_screen.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -//import 'package:roomiebuddy/NavScreen.dart'; -import 'package:roomiebuddy/pages/login_screen.dart'; -// import 'package:myapp/main.dart'; +import 'package:roomiebuddy/pages/subpages/auth/login_screen.dart'; import 'package:animated_text_kit/animated_text_kit.dart'; import 'package:provider/provider.dart'; import 'package:roomiebuddy/providers/theme_provider.dart'; diff --git a/roomiebuddy/lib/utils/data_transformer.dart b/roomiebuddy/lib/utils/data_transformer.dart index dab6506..3dbfc7b 100644 --- a/roomiebuddy/lib/utils/data_transformer.dart +++ b/roomiebuddy/lib/utils/data_transformer.dart @@ -1,25 +1,3 @@ -import 'package:flutter/material.dart'; - -/// Converts a DateTime and TimeOfDay into a Unix timestamp (seconds since epoch). -/// Returns null if either date or time is null. -double? dateTimeToTimestamp(DateTime? date, TimeOfDay? time) { - if (date == null || time == null) { - return null; - } - final combinedDateTime = DateTime( - date.year, - date.month, - date.day, - time.hour, - time.minute, - ); - // Convert to seconds since epoch (Unix timestamp) - return combinedDateTime.millisecondsSinceEpoch / 1000.0; -} - -/// Converts a priority string ('Low', 'Medium', 'High') to an integer. -/// Returns 0 for 'Low', 1 for 'Medium', 2 for 'High'. -/// Returns 0 (Low) if the input is null or doesn't match. int priorityToInt(String? priority) { switch (priority) { case 'Low': @@ -29,6 +7,15 @@ int priorityToInt(String? priority) { case 'High': return 2; default: - return 0; // Default to Low if null or unknown + return 0; + } +} + +String priorityToString(int priority) { + switch (priority) { + case 0: return 'Low'; + case 1: return 'Medium'; + case 2: return 'High'; + default: return 'Unknown'; } -} \ No newline at end of file +} \ No newline at end of file