diff --git a/backend/controller_group.py b/backend/controller_group.py index dc18973..ed715a4 100644 --- a/backend/controller_group.py +++ b/backend/controller_group.py @@ -208,6 +208,57 @@ def get_group_control( } return groups + def get_group_members_control( + self, + user_id: str, + group_id: str, + password: str, + ) -> list[dict]: + """Gets all members of a specific group.""" + if not Validator().check_user_exists(user_id): + raise BackendError("Backend Error: User does not exist", "304") + if not Validator().check_password(user_id, password): + raise BackendError("Backend Error: Password is incorrect", "305") + if not Validator().check_group_exists(group_id): + raise BackendError("Backend Error: Group does not exist", "306") + if not Validator().check_user_in_group(user_id, group_id): + raise BackendError("Backend Error: User not in group", "308") + + with db_operation() as data_cursor: + # Get all users in the group + data_cursor.execute( + """ + SELECT user_id + FROM group_user + WHERE group_id = ? + """, + (group_id,), + ) + user_ids = [row[0] for row in data_cursor.fetchall()] + + if not user_ids: + return [] + + # Get user information for each member + placeholders = ",".join("?" for _ in user_ids) + data_cursor.execute( + f""" + SELECT uuid, username, email + FROM user + WHERE uuid IN ({placeholders}) + """, + user_ids, + ) + members_data = data_cursor.fetchall() + + # Create a list of member dictionaries + members = [ + {"user_id": member_id, "username": member_name, "email": member_email} + for member_id, member_name, member_email in members_data + ] + + return members + if __name__ == "__main__": print("This is a module and should not be run directly.") diff --git a/backend/controller_invite.py b/backend/controller_invite.py index e34eeed..92ed60d 100644 --- a/backend/controller_invite.py +++ b/backend/controller_invite.py @@ -43,7 +43,7 @@ def create_invite_control( with db_operation() as data_cursor: data_cursor.execute( "INSERT INTO group_invites VALUES (?, ?, ?, ?, ?);", - (invite_id, group_id, invitee_id, inviter_id, day_created), + (invite_id, group_id, inviter_id, invitee_id, day_created), ) return invite_id @@ -59,37 +59,59 @@ def get_pending_control( raise BackendError("Backend Error: Password is incorrect", "305") invites: dict[str, dict] = {} with db_operation() as data_cursor: + # 1. Fetch all raw invite data data_cursor.execute( - ( - "SELECT invite_id, inviter_id group_id, created_at " - "FROM group_invites WHERE invitee_id = ?;" - ), + "SELECT invite_id, inviter_id, group_id " + "FROM group_invites WHERE invitee_id = ?;", (user_id,), ) invites_data: list[tuple] = data_cursor.fetchall() - for invite_item in invites_data: - invite_id: str = invite_item[0] - inviter_id: str = invite_item[1] - group_id: str = invite_item[2] - created_at: str = invite_item[3] + + if not invites_data: + return {} # Return early if no invites + + # 2. Collect unique IDs + group_ids = {item[2] for item in invites_data} + inviter_ids = {item[1] for item in invites_data} + + # 3. Fetch group names + group_names: dict[str, str] = {} + if group_ids: + group_placeholders = ",".join("?" * len(group_ids)) data_cursor.execute( - "SELECT name FROM task_group WHERE uuid = ?;", - (group_id), + f"SELECT uuid, name FROM task_group WHERE uuid IN ({group_placeholders});", + list(group_ids), ) - group_name: str = data_cursor.fetchone()[0] + group_names = dict(data_cursor.fetchall()) + + # 4. Fetch inviter names + inviter_names: dict[str, str] = {} + if inviter_ids: + inviter_placeholders = ",".join("?" * len(inviter_ids)) data_cursor.execute( - "SELECT username FROM user WHERE uuid = ?;", - (inviter_id), + f"SELECT uuid, username FROM user WHERE uuid IN ({inviter_placeholders});", + list(inviter_ids), ) - inviter_name: str = data_cursor.fetchone()[0] + inviter_names = dict(data_cursor.fetchall()) + + # 5. Build the final result dictionary + for invite_item in invites_data: + invite_id: str = invite_item[0] + inviter_id: str = invite_item[1] + group_id: str = invite_item[2] + + # Look up names from the fetched dictionaries + group_name: str = group_names.get(group_id, "Unknown Group") + inviter_name: str = inviter_names.get(inviter_id, "Unknown User") + invites[invite_id] = { "invite_id": invite_id, "group_id": group_id, "group_name": group_name, "inviter_id": inviter_id, "inviter_name": inviter_name, - "created_at": created_at, } + return invites def sent_invite_control( diff --git a/backend/controller_task.py b/backend/controller_task.py index bb4e362..9aab959 100644 --- a/backend/controller_task.py +++ b/backend/controller_task.py @@ -83,9 +83,7 @@ def edit_task_control( request_data["group_id"] ): raise BackendError("Backend Error: Group does not exist", "306") - if Validator().check_password( - user_id=request_data["assigner_id"], password=request_data["password"] - ): + if not Validator().check_password(user_id=assigner_id, password=password): raise BackendError("Backend Error: Password is incorrect", "305") image_path = "TODO CHANGE HERE" task_due: float = datetime( @@ -99,8 +97,8 @@ 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 = ? " - "recursive = ?, priority = ?, image_path = ? completed = ? " + "est_hour = ?, est_min = ?, assigner_uuid = ?, assign_uuid = ?, group_id = ?, " + "recursive = ?, priority = ?, image_path = ?, completed = ? " "WHERE uuid = ?;", ( request_data["task_name"], @@ -146,17 +144,35 @@ def get_user_task_control(self, user_id: str, password: str) -> dict[str, dict]: task_list: list[tuple] = data_cursor.fetchall() new_task_list: dict[str, dict] = {} for task in task_list: + # Get assigner username + assigner_username = "Unknown" + with db_operation() as username_cursor: + username_cursor.execute("SELECT username FROM user WHERE uuid = ?;", (task[7],)) + username_result = username_cursor.fetchone() + if username_result: + assigner_username = username_result[0] + new_task_list[task[0]] = { "name": task[1], "description": task[2], - "due": datetime.fromtimestamp(float(task[3])), + "due_timestamp": float(task[3]), "est_day": int(task[4]), "est_hour": int(task[5]), "est_min": int(task[6]), "assigner_id": task[7], + "assigner_username": assigner_username, "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 @@ -183,17 +199,35 @@ def get_group_task_control( task_list: list[tuple] = data_cursor.fetchall() new_task_list: dict[str, dict] = {} for task in task_list: + # Get assigner username + assigner_username = "Unknown" + with db_operation() as username_cursor: + username_cursor.execute("SELECT username FROM user WHERE uuid = ?;", (task[7],)) + username_result = username_cursor.fetchone() + if username_result: + assigner_username = username_result[0] + new_task_list[task[0]] = { "name": task[1], "description": task[2], - "due": datetime.fromtimestamp(float(task[3])), + "due_timestamp": float(task[3]), "est_day": int(task[4]), "est_hour": int(task[5]), "est_min": int(task[6]), "assigner_id": task[7], + "assigner_username": assigner_username, "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 @@ -225,17 +259,35 @@ def get_completed_task_control( task_list: list[tuple] = data_cursor.fetchall() new_task_list: dict[str, dict] = {} for task in task_list: + # Get assigner username + assigner_username = "Unknown" + with db_operation() as username_cursor: + username_cursor.execute("SELECT username FROM user WHERE uuid = ?;", (task[7],)) + username_result = username_cursor.fetchone() + if username_result: + assigner_username = username_result[0] + new_task_list[task[0]] = { "name": task[1], "description": task[2], - "due": datetime.fromtimestamp(float(task[3])), + "due_timestamp": float(task[3]), "est_day": int(task[4]), "est_hour": int(task[5]), "est_min": int(task[6]), "assigner_id": task[7], + "assigner_username": assigner_username, "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 @@ -243,7 +295,6 @@ def toggle_complete_task_control( self, task_id: str, user_id: str, - group_id: str, password: str, completed: int, ) -> None: @@ -252,10 +303,6 @@ def toggle_complete_task_control( raise BackendError("Backend Error: User does not exist", "304") if not Validator().check_password(user_id=user_id, password=password): raise BackendError("Backend Error: Password is incorrect", "305") - if not Validator().check_group_exists(group_id=group_id): - raise BackendError("Backend Error: Group does not exist", "306") - 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") if not Validator().check_task_exists(task_id=task_id): raise BackendError("Backend Error: Task does not exist", "309") with db_operation() as data_cursor: @@ -267,6 +314,12 @@ def toggle_complete_task_control( ), ) + def get_image_control(self, image_path: str) -> str: + """This will get the image.""" + if not exists(image_path): + raise BackendError("Backend Error: Image does not exist", "313") + return image_path + if __name__ == "__main__": print("This module is not intended to be run directly.") diff --git a/backend/controller_user.py b/backend/controller_user.py index 38a1626..f2a6f11 100644 --- a/backend/controller_user.py +++ b/backend/controller_user.py @@ -36,17 +36,20 @@ def login_user_control( self, email: str, password: str, - ) -> str: + ) -> dict[str, str]: """This will login a user.""" - if Validator().check_login(email=email, password=password): + if not Validator().check_login(email=email, password=password): raise BackendError("Backend Error: Email or Password is incorrect", "303") with db_operation() as data_cursor: data_cursor.execute( - "SELECT uuid FROM user WHERE email = ?;", + "SELECT uuid, username FROM user WHERE email = ?;", (email,), ) - user_id = data_cursor.fetchone()[0] - return user_id + user_data = data_cursor.fetchone() + if user_data: + return {"user_id": user_data[0], "username": user_data[1]} + else: + raise BackendError("Backend Error: User not found after successful check", "500") def edit_user_control( self, diff --git a/backend/handler_group.py b/backend/handler_group.py index 2d59b69..8760328 100644 --- a/backend/handler_group.py +++ b/backend/handler_group.py @@ -76,6 +76,20 @@ def delete_group_request(self) -> None: user_id=user_id, group_id=group_id, password=password ) + @handle_backend_exceptions + def get_group_members_request(self) -> list[dict]: + """Get all members of a specific group.""" + request_data = extract_request_data( + request=self.user_request, + required_fields=["user_id", "group_id", "password"], + ) + user_id = request_data["user_id"] + group_id = request_data["group_id"] + password = request_data["password"] + return GroupController().get_group_members_control( + user_id=user_id, group_id=group_id, password=password + ) + if __name__ == "__main__": print("This is a module and should not be run directly.") diff --git a/backend/handler_invite.py b/backend/handler_invite.py index 81efa70..042f7de 100644 --- a/backend/handler_invite.py +++ b/backend/handler_invite.py @@ -6,7 +6,7 @@ from flask import Request from error import BackendError, handle_backend_exceptions from controller_invite import InviteController -from utils import extract_request_data +from utils import extract_request_data, db_operation class InviteHandle: @@ -51,21 +51,40 @@ def respond_invite_request(self) -> None: request=self.user_request, required_fields=[ "user_id", - "group_id", + "invite_id", + "status", "password", - "accept", ], ) user_id: str = request_data["user_id"] - group_id: str = request_data["group_id"] + invite_id: str = request_data["invite_id"] password: str = request_data["password"] - accept: bool = request_data["accept"] + status: str = request_data["status"] + + # Get group_id from invite_id + group_id = self._get_group_id_from_invite(invite_id, user_id) + + # Convert status to boolean accept + accept = status.lower() == "accepted" + return InviteController().respond_invite_control( user_id=user_id, group_id=group_id, password=password, accept=accept, ) + + def _get_group_id_from_invite(self, invite_id: str, user_id: str) -> str: + """Get group_id from invite_id.""" + with db_operation() as data_cursor: + data_cursor.execute( + "SELECT group_id FROM group_invites WHERE invite_id = ? AND invitee_id = ?;", + (invite_id, user_id), + ) + result = data_cursor.fetchone() + if not result: + raise BackendError("Backend Error: Invite does not exist", "308") + return result[0] @handle_backend_exceptions def get_pending_request(self) -> dict[str, dict[str, Any]]: diff --git a/backend/handler_task.py b/backend/handler_task.py index a5b4c35..6be8027 100644 --- a/backend/handler_task.py +++ b/backend/handler_task.py @@ -86,6 +86,16 @@ def get_user_task_request(self) -> dict[str, dict[str, Any]]: user_id=user_id, password=password ) + @handle_backend_exceptions + def get_image_request(self) -> str: + """Gets the image.""" + request_data: dict[str, Any] = extract_request_data( + request=self.user_request, + required_fields=["image_path"], + ) + image_path: str = request_data["image_path"] + return TaskController().get_image_control(image_path=image_path) + if __name__ == "__main__": print("This is a module and should not be run directly.") diff --git a/backend/handler_user.py b/backend/handler_user.py index 6c8fcaf..7d12331 100644 --- a/backend/handler_user.py +++ b/backend/handler_user.py @@ -41,7 +41,7 @@ def add_user_request(self) -> str: return user_id @handle_backend_exceptions - def login_user_request(self) -> str: + def login_user_request(self) -> dict[str, str]: """ "Logs in a user.""" request_data: dict[str, Any] = extract_request_data( request=self.user_request, @@ -52,7 +52,8 @@ def login_user_request(self) -> str: ) email: str = request_data["email"] password: str = request_data["password"] - return UserController().login_user_control(email=email, password=password) + user_info: dict[str, str] = UserController().login_user_control(email=email, password=password) + return user_info @handle_backend_exceptions def edit_user_request(self) -> None: diff --git a/backend/main.py b/backend/main.py index 262a9db..b5b21a7 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,7 +4,7 @@ # from os.path import join from typing import Any -from flask import Flask, request, jsonify, Response +from flask import Flask, request, jsonify, Response, send_file # from werkzeug.utils import secure_filename from validator import Validator @@ -57,10 +57,20 @@ def handle_signup() -> Response: @error_handling_decorator("login") def handle_login() -> Response: """Login a user.""" - user_id: str = UserHandle(request).login_user_request() +<<<<<<< Updated upstream + user_info: dict[str, str] = UserHandle(request).login_user_request() + # Extract user_id and username + user_id: str = user_info["user_id"] + username: str = user_info["username"] return jsonify( - [{"error_no": "0", "message": "success", "user_id": user_id}] + # Include both user_id and username in the 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"]) @@ -111,7 +121,14 @@ def handle_delete_task() -> Response: def handle_get_user_task() -> Response: """Get all tasks for a user.""" tasks: dict[str, dict[str, Any]] = TaskHandle(request).get_user_task_request() - return jsonify([{"error_no": "0", "message": tasks}]) + 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: + """Get an image.""" + return send_file(TaskHandle(request).get_image_request(), mimetype="image/jpeg") # ----- Group Handlers ---- @@ -122,7 +139,7 @@ def handle_get_user_task() -> Response: def handle_get_group_list() -> Response: """Get all groups for a user.""" groups: dict[str, dict[str, Any]] = GroupHandle(request).get_group_list_request() - return jsonify([{"error_no": "0", "message": "success", "group_id": groups}]) + return jsonify([{"error_no": "0", "message": "success", "groups": groups}]) @app.route("/create_group", methods=["POST"]) @@ -149,6 +166,14 @@ def handle_delete_group() -> Response: return jsonify([{"error_no": "0", "message": "success"}]) +@app.route("/get_group_members", methods=["POST"]) +@error_handling_decorator("get_group_members") +def handle_get_group_members() -> Response: + """Get all members of a specific group.""" + members = GroupHandle(request).get_group_members_request() + return jsonify([{"error_no": "0", "message": "success", "members": members}]) + + # ----- Invite Handlers ---- diff --git a/backend_new.zip b/backend_new.zip deleted file mode 100644 index d5fcfdf..0000000 Binary files a/backend_new.zip and /dev/null differ diff --git a/documentation/backend_error_code.txt b/documentation/backend_error_code.txt index 726f438..48904be 100644 --- a/documentation/backend_error_code.txt +++ b/documentation/backend_error_code.txt @@ -59,4 +59,12 @@ sqlite3 error codes 310: "User is not in the group" The user is not in the group. Please check the group id and try again. 311: "Invitee already has an invite" - The invitee already has an invite. Please check the group id and try again. \ No newline at end of file +<<<<<<< Updated upstream + The invitee already has an invite. Please check the group id and try again. +======= + The invitee already has an invite. Please check the group id and try again. +312: "invalid file type" + The file type is not supported. Please check the file type and try again. +313: "File not found" + The file was not found. Please check the file id and try again. +>>>>>>> Stashed changes diff --git a/roomiebuddy/.metadata b/roomiebuddy/.metadata index d044da8..01965b7 100644 --- a/roomiebuddy/.metadata +++ b/roomiebuddy/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668" + revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf" channel: "stable" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf - platform: android - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - - platform: ios - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - - platform: linux - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - - platform: macos - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - - platform: web - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - - platform: windows - create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 - base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf # User provided section diff --git a/roomiebuddy/lib/pages/add_taskpage.dart b/roomiebuddy/lib/pages/add_taskpage.dart index badda59..f6d464e 100644 --- a/roomiebuddy/lib/pages/add_taskpage.dart +++ b/roomiebuddy/lib/pages/add_taskpage.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'dart:convert'; -import 'dart:io'; // Import dart:io for File +import 'dart:io'; import 'package:provider/provider.dart'; import 'package:roomiebuddy/providers/theme_provider.dart'; import 'package:roomiebuddy/utils/data_transformer.dart'; import 'package:roomiebuddy/services/api_service.dart'; import 'package:roomiebuddy/services/auth_storage.dart'; -import 'package:image_picker/image_picker.dart'; // Import image_picker +import 'package:image_picker/image_picker.dart'; class AddTaskpage extends StatefulWidget { const AddTaskpage({super.key}); @@ -17,26 +15,30 @@ class AddTaskpage extends StatefulWidget { } class _AddTaskpageState extends State { + + // TextEditingControllers final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); + final TextEditingController _estDaysController = TextEditingController(); + final TextEditingController _estHoursController = TextEditingController(); + final TextEditingController _estMinsController = TextEditingController(); + + // Task variables String? _selectedGroupId; String? _selectedMemberId; String? _selectedPriority; DateTime? _selectedDate; TimeOfDay? _selectedTime; - final TextEditingController _estDaysController = TextEditingController(); - final TextEditingController _estHoursController = TextEditingController(); - final TextEditingController _estMinsController = TextEditingController(); String? _selectedRecurrence = 'Once'; - - // Add state variable for selected image File? _selectedImage; + // For storing users groups and members in selected group List> _userGroups = []; List> _groupMembers = []; bool _isLoadingGroups = false; bool _isLoadingMembers = false; bool _isSaving = false; + bool _initialDataLoaded = false; String _userId = ""; String _password = ""; @@ -47,10 +49,30 @@ class _AddTaskpageState extends State { @override void initState() { super.initState(); - _loadInitialData(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_initialDataLoaded) { + _loadInitialData(); + _initialDataLoaded = true; + } + } + + @override + void dispose() { + _titleController.dispose(); + _descriptionController.dispose(); + _estDaysController.dispose(); + _estHoursController.dispose(); + _estMinsController.dispose(); + super.dispose(); + } + + // Get the users id and password from the auth storage Future _loadInitialData() async { + setState(() => _isLoadingGroups = true); final userId = await _authStorage.getUserId(); final password = await _authStorage.getPassword(); if (userId != null && password != null) { @@ -58,23 +80,26 @@ class _AddTaskpageState extends State { _userId = userId; _password = password; }); - _loadUserGroups(); + await _loadUserGroups(); + setState(() => _isLoadingGroups = false); } else { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Error: User not logged in.')), - ); - Navigator.of(context).pop(); - } + setState(() => _isLoadingGroups = false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Error: User not logged in.')), + ); + Navigator.of(context).pop(); + } } } Future _loadUserGroups() async { setState(() => _isLoadingGroups = true); + try { final response = await _apiService.getGroupList(_userId, _password); - if (response['success'] && mounted) { - final groupsMap = response['data']?['message'] as Map? ?? {}; + if (response['success']) { + final groupsMap = response['data']?['groups'] as Map? ?? {}; setState(() { _userGroups = groupsMap.values.map((group) => group as Map).toList(); }); @@ -92,23 +117,37 @@ class _AddTaskpageState extends State { ); } } finally { - if (mounted) { - setState(() => _isLoadingGroups = false); - } + setState(() => _isLoadingGroups = false); } } Future _loadGroupMembers(String groupId) async { setState(() => _isLoadingMembers = true); + try { final response = await _apiService.getGroupMembers(_userId, groupId, _password); - if (response['success'] && mounted) { - setState(() { - _groupMembers = List>.from(response['members']); - }); + if (response['success']) { + final members = response['members']; + if (members is List) { + setState(() { + _groupMembers = List>.from(members.map((member) { + if (member is Map) { + return member; + } else { + return Map.from(member as Map); + } + })); + }); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to load members: Invalid response format')), + ); + } + } } else { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to load members: ${response['message']}')), ); } @@ -120,12 +159,11 @@ class _AddTaskpageState extends State { ); } } finally { - if (mounted) { - setState(() => _isLoadingMembers = false); - } + setState(() => _isLoadingMembers = false); } } + // For Date Picker Future _selectDate(BuildContext context) async { final themeProvider = Provider.of(context, listen: false); @@ -170,6 +208,7 @@ class _AddTaskpageState extends State { } } + // For Time Picker Future _selectTime(BuildContext context) async { final themeProvider = Provider.of(context, listen: false); @@ -218,10 +257,9 @@ class _AddTaskpageState extends State { } } - // Function to pick an image + // For Image Picker Future _pickImage() async { final ImagePicker picker = ImagePicker(); - // Pick an image from the gallery final XFile? image = await picker.pickImage(source: ImageSource.gallery); if (image != null) { @@ -232,17 +270,19 @@ class _AddTaskpageState extends State { } Future _saveTask() async { + if (!mounted) return; final scaffoldMessenger = ScaffoldMessenger.of(context); - // --- Show SnackBar if image selected but upload not implemented --- + // TODO: Implement image upload if (_selectedImage != null) { scaffoldMessenger.showSnackBar( const SnackBar(content: Text('Image selected, but backend upload is not yet implemented.')), ); - // Optionally, you might want to return here if upload is mandatory - // return; } + // Ensure we have all the required fields + // If the user has not entered certain fields, show a snackbar telling them to enter the next missing field + if (_titleController.text.isEmpty) { scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Please enter a task title.'))); return; @@ -267,28 +307,45 @@ class _AddTaskpageState extends State { scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Please select a due time.'))); return; } + + // Validate that est time inputs contain valid integers + if (_estDaysController.text.isNotEmpty && int.tryParse(_estDaysController.text) == null) { + scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Days must be an integer value.'))); + return; + } + if (_estHoursController.text.isNotEmpty && int.tryParse(_estHoursController.text) == null) { + scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Hours must be an integer value.'))); + return; + } + if (_estMinsController.text.isNotEmpty && int.tryParse(_estMinsController.text) == null) { + scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Minutes must be an integer value.'))); + return; + } + // Convert est time inputs from objects to ints final int estDays = int.tryParse(_estDaysController.text) ?? 0; final int estHours = int.tryParse(_estHoursController.text) ?? 0; final int estMins = int.tryParse(_estMinsController.text) ?? 0; + // Map the recurrence value to an int for backend final Map recurrenceMap = { 'Once': 0, 'Daily': 1, 'Weekly': 2, 'Monthly': 3, }; - final int recurrenceInt = recurrenceMap[_selectedRecurrence ?? 'Once'] ?? 0; + 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) { scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Invalid date/time selected.'))); return; } - setState(() => _isSaving = true); + setState(() => _isSaving = true); // Set saving state to true, shows a loading indicator try { final response = await _apiService.post('/add_task', { @@ -304,13 +361,16 @@ class _AddTaskpageState extends State { 'task_est_hour': estHours, 'task_est_min': estMins, 'recursive': recurrenceInt, - 'image_path': '' + 'image_path': '' // TODO: Implement image upload not via path, but via file }); - if (response['success'] && mounted) { + if (!mounted) return; + + if (response['success']) { scaffoldMessenger.showSnackBar( const SnackBar(content: Text('Task added successfully!'), duration: Duration(seconds: 2)), ); + // Clear all the fields if save was successful setState(() { _titleController.clear(); _descriptionController.clear(); @@ -324,41 +384,26 @@ class _AddTaskpageState extends State { _estHoursController.clear(); _estMinsController.clear(); _selectedRecurrence = 'Once'; - // Clear selected image _selectedImage = null; }); } else { - if (mounted) { - scaffoldMessenger.showSnackBar( - SnackBar(content: Text('Failed to add task: ${response['message']}')), - ); - } + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Failed to add task: ${response['message']}')), + ); } } catch (e) { - if (mounted) { - scaffoldMessenger.showSnackBar( - SnackBar(content: Text('Error adding task: $e')), - ); - } + if (!mounted) return; + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Error adding task: $e')), + ); } finally { - if (mounted) { - setState(() => _isSaving = false); - } + setState(() => _isSaving = false); } } - @override - void dispose() { - _titleController.dispose(); - _descriptionController.dispose(); - _estDaysController.dispose(); - _estHoursController.dispose(); - _estMinsController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { + // Theme and general input styling setup final themeProvider = Provider.of(context); final inputDecoration = InputDecoration( border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), @@ -388,6 +433,7 @@ class _AddTaskpageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Task Details Section - Title and Description Text( 'Task Details', style: TextStyle( @@ -409,6 +455,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), + // Assignment Section - Group and Member Selection Text( 'Assign To', style: TextStyle( @@ -480,6 +527,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), + // Priority Section - Task Priority Selection Text( 'Priority', style: TextStyle( @@ -510,6 +558,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), + // Due Date & Time Section - Calendar and Time Pickers Text( 'Due Date & Time', style: TextStyle( @@ -554,6 +603,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), + // Estimated Duration Section - Days, Hours, Minutes inputs Text( 'Estimated Duration', style: TextStyle( @@ -592,6 +642,7 @@ class _AddTaskpageState extends State { ), const SizedBox(height: 12), + // Recurrence Section - Task repetition pattern Text( 'Recurrence', style: TextStyle( @@ -621,25 +672,26 @@ class _AddTaskpageState extends State { }); }, ), - const SizedBox(height: 16), // Adjusted spacing slightly + const SizedBox(height: 16), + // Action Buttons - Add Photo and Save Task Row( children: [ ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: themeProvider.themeColor.withOpacity(0.8), + backgroundColor: themeProvider.themeColor, foregroundColor: themeProvider.currentTextColor, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), ), - onPressed: _pickImage, // Call _pickImage on press + onPressed: _pickImage, // Change icon based on whether an image is selected icon: Icon( _selectedImage == null ? Icons.add_a_photo : Icons.check_circle_outline, size: 18, color: _selectedImage == null ? themeProvider.currentTextColor - : Colors.green, // Indicate success with green check + : Colors.green, // Indicate image uploaded with green check ), label: const Text('Add Photo'), ), diff --git a/roomiebuddy/lib/pages/group_page.dart b/roomiebuddy/lib/pages/group_page.dart index cd3a237..98980a3 100644 --- a/roomiebuddy/lib/pages/group_page.dart +++ b/roomiebuddy/lib/pages/group_page.dart @@ -12,22 +12,24 @@ class GroupPage extends StatefulWidget { } class _GroupPageState extends State { + // Text editing controllers final _userIdController = TextEditingController(); final _createGroupNameController = TextEditingController(); - - final ApiService _apiService = ApiService(); - final AuthStorage _authStorage = AuthStorage(); - - final Map _loadingInvites = {}; + // User data variables + String? _selectedGroupId; String _userId = ""; String _password = ""; - List> _userGroups = []; - String? _selectedGroupId; - List> _pendingInvites = []; String _errorMessage = ""; bool _isLoading = true; bool _isSubmitting = false; + List> _userGroups = []; + List> _pendingInvites = []; + final Map _loadingInvites = {}; + + // Services + final ApiService _apiService = ApiService(); + final AuthStorage _authStorage = AuthStorage(); @override void initState() { @@ -53,7 +55,7 @@ class _GroupPageState extends State { final userId = await _authStorage.getUserId(); final password = await _authStorage.getPassword(); - // Ensure necessary credentials exist + // Ensure credentials exist if (userId == null || password == null) { setState(() { _isLoading = false; @@ -71,7 +73,7 @@ class _GroupPageState extends State { _errorMessage = "Failed to load groups: ${groupsResponse['message']}"; }); } else { - final groups = groupsResponse['message'] as Map; + final groups = groupsResponse['data']?['groups'] as Map? ?? {}; _userGroups = groups.values.map((group) => group as Map).toList(); _selectedGroupId = null; } @@ -80,10 +82,12 @@ class _GroupPageState extends State { final invitesResponse = await _apiService.getPendingInvites(userId, password); if (!invitesResponse['success']) { setState(() { - _errorMessage = "Failed to load invites: ${invitesResponse['message']}"; + _errorMessage = _errorMessage.isNotEmpty + ? "$_errorMessage\nFailed to load invites: ${invitesResponse['message']}" + : "Failed to load invites: ${invitesResponse['message']}"; }); } else { - final invites = invitesResponse['message'] as Map; + final invites = invitesResponse['data']?['invites'] as Map? ?? {}; _pendingInvites = invites.values.map((invite) => invite as Map).toList(); } } catch (e) { @@ -133,7 +137,7 @@ class _GroupPageState extends State { try { final groupsResponse = await _apiService.getGroupList(_userId, _password); if (groupsResponse['success']) { - final groups = groupsResponse['message'] as Map; + final groups = groupsResponse['data']?['groups'] as Map? ?? {}; if(mounted){ setState(() { _userGroups = groups.values.map((group) => group as Map).toList(); @@ -260,7 +264,7 @@ class _GroupPageState extends State { try { final groupsResponse = await _apiService.getGroupList(_userId, _password); if (groupsResponse['success']) { - final groups = groupsResponse['message'] as Map; + final groups = groupsResponse['data']?['groups'] as Map? ?? {}; setState(() { _userGroups = groups.values.map((group) => group as Map).toList(); }); diff --git a/roomiebuddy/lib/pages/home_page.dart b/roomiebuddy/lib/pages/home_page.dart index 380bb36..6ccf2b0 100644 --- a/roomiebuddy/lib/pages/home_page.dart +++ b/roomiebuddy/lib/pages/home_page.dart @@ -6,6 +6,7 @@ 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'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -15,18 +16,12 @@ class HomePage extends StatefulWidget { } class HomePageState extends State { - final String _selectedCategory = 'Today'; bool _isLoading = false; List> _tasks = []; - TextEditingController _searchController = TextEditingController(); - - final List> roommateGroups = [ - {"groupName": "Room 101", "members": ["Alice", "Bob", "Charlie"]}, - {"groupName": "Kitchen Crew", "members": ["Dana", "Eli"]}, - {"groupName": "Laundry Legends", "members": ["Fred", "Gina", "Harry"]}, - ]; + List> _roommateGroups = []; final AuthStorage _authStorage = AuthStorage(); + final ApiService _apiService = ApiService(); String? _userId; String? _password; String _userName = "User"; @@ -46,9 +41,11 @@ class HomePageState extends State { _userName = await _authStorage.getUsername() ?? "User"; if (_userId != null && _password != null) { - await fetchTasks(_userId!, _password!); + await Future.wait([ + _loadTasks(_userId!, _password!), + _loadGroups(_userId!, _password!), + ]); } else { - print("User not logged in."); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Please log in to view tasks.')), @@ -58,6 +55,50 @@ class HomePageState extends State { } } + Future _loadGroups(String userId, String password) async { + try { + final response = await _apiService.getGroupList(userId, password); + if (response['success']) { + final groupsMap = response['data']?['groups'] as Map? ?? {}; + if (mounted) { + 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(); + }); + } + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to load groups: ${response['message']}')), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error loading groups: $e')), + ); + } + } + } + String _getGreeting() { final hour = DateTime.now().hour; if (hour < 12) return "Good Morning"; @@ -65,7 +106,7 @@ class HomePageState extends State { return "Good Evening"; } - Future fetchTasks(String userId, String password) async { + Future _loadTasks(String userId, String password) async { if (!mounted) return; setState(() { _isLoading = true; @@ -108,7 +149,7 @@ class HomePageState extends State { return { "id": taskId, "taskName": taskData["name"] ?? "No Task Name", - "assignedBy": taskData["assigner_id"] ?? "Unknown", + "assignedBy": taskData["assigner_username"] ?? taskData["assigner_id"] ?? "Unknown", "priority": priorityStr, "description": taskData["description"] ?? "", "dueDate": dueDateStr, @@ -122,7 +163,6 @@ class HomePageState extends State { }); } } else { - print('Backend error: ${firstItem['message']}'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to load tasks: ${firstItem['message']}')), @@ -130,7 +170,6 @@ class HomePageState extends State { } } } else { - print('Unexpected response format from /get_user_task'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Failed to load tasks: Invalid server response.')), @@ -138,7 +177,6 @@ class HomePageState extends State { } } } else { - print('HTTP error ${response.statusCode}'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to load tasks: Server error ${response.statusCode}')), @@ -146,7 +184,6 @@ class HomePageState extends State { } } } catch (e) { - print('Error fetching tasks: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error fetching tasks: $e')), @@ -232,8 +269,7 @@ class HomePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - color: themeProvider.themeColor, - padding: const EdgeInsets.only(bottom: 24), + color: Theme.of(context).scaffoldBackgroundColor, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -250,9 +286,18 @@ class HomePageState extends State { ), ), Container( - color: Colors.white.withOpacity(0.05), + color: themeProvider.themeColor, height: 180, - child: _buildGroupCarousel(), + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _roommateGroups.isEmpty + ? Center( + child: Text( + 'No groups found.', + style: TextStyle(color: themeProvider.currentSecondaryTextColor), + ), + ) + : _buildGroupCarousel(), ), ], ), @@ -331,6 +376,10 @@ class HomePageState extends State { Widget _buildGroupCarousel() { final themeProvider = Provider.of(context); + if (_roommateGroups.isEmpty) { + return const Center(child: Text('No groups to display.')); + } + return SizedBox( height: 140, child: CarouselSlider( @@ -338,9 +387,14 @@ class HomePageState extends State { height: 140.0, enlargeCenterPage: true, autoPlay: false, - viewportFraction: 0.85, + viewportFraction: 0.6, ), - items: roommateGroups.map((group) { + 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( @@ -353,13 +407,15 @@ class HomePageState extends State { ); }, child: Container( + width: MediaQuery.of(context).size.width * 0.6, + height: 120, margin: const EdgeInsets.symmetric(horizontal: 5), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: themeProvider.currentCardBackground, borderRadius: BorderRadius.circular(15), border: Border.all(color: themeProvider.themeColor), - boxShadow: [ + boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 5, @@ -371,7 +427,7 @@ class HomePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - group['groupName'], + group['group_name'] ?? 'Unnamed Group', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -380,11 +436,13 @@ class HomePageState extends State { ), const SizedBox(height: 8), Text( - "Members: ${group['members'].join(', ')}", + "Members: ${memberNames.join(', ')}", style: TextStyle( fontSize: 16, color: themeProvider.currentSecondaryTextColor, ), + overflow: TextOverflow.ellipsis, + maxLines: 2, ), ], ), @@ -442,18 +500,28 @@ class GroupDetailScreen extends StatelessWidget { @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['groupName'])), + 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['groupName']}", - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + Text( + "Group Name: ${group['group_name'] ?? 'Unnamed Group'}", + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold) + ), const SizedBox(height: 20), - Text("Members:", style: const TextStyle(fontSize: 20)), - ...List.from(group['members'].map((m) => Text("- $m"))), + const Text("Members:", style: TextStyle(fontSize: 20)), + ...memberNames.map((name) => Padding( + padding: const EdgeInsets.only(left: 8.0, top: 4.0), + child: Text("- $name"), + )), ], ), ), diff --git a/roomiebuddy/lib/pages/login_screen.dart b/roomiebuddy/lib/pages/login_screen.dart index d5033d5..7ddfcbc 100644 --- a/roomiebuddy/lib/pages/login_screen.dart +++ b/roomiebuddy/lib/pages/login_screen.dart @@ -125,9 +125,8 @@ class _LoginScreenState extends State { // Navigate to main screen _navigateToMainScreen(); } else { - // Show error message setState(() { - _errorMessage = 'Incorrect email/password or account does not exist'; + _errorMessage = result['message'] ?? 'An unknown error occurred.'; }); } } catch (e) { diff --git a/roomiebuddy/lib/pages/signup_screen.dart b/roomiebuddy/lib/pages/signup_screen.dart index 06ae098..9846491 100644 --- a/roomiebuddy/lib/pages/signup_screen.dart +++ b/roomiebuddy/lib/pages/signup_screen.dart @@ -246,7 +246,6 @@ class _SignupScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Registration successful! Please login.'), - backgroundColor: Colors.green, ), ); diff --git a/roomiebuddy/lib/pages/view_taskpage.dart b/roomiebuddy/lib/pages/view_taskpage.dart index 3a83e87..876e163 100644 --- a/roomiebuddy/lib/pages/view_taskpage.dart +++ b/roomiebuddy/lib/pages/view_taskpage.dart @@ -10,6 +10,6 @@ class ViewTaskpage extends StatefulWidget { class _ViewTaskpageState extends State { @override Widget build(BuildContext context) { - return Scaffold(); + return const Scaffold(); } } \ No newline at end of file diff --git a/roomiebuddy/lib/services/api_service.dart b/roomiebuddy/lib/services/api_service.dart index 4aad97b..c82cd67 100644 --- a/roomiebuddy/lib/services/api_service.dart +++ b/roomiebuddy/lib/services/api_service.dart @@ -126,10 +126,6 @@ class ApiService { // **** NEW METHOD: Get members of a specific group **** Future> getGroupMembers(String userId, String groupId, String password) async { - // Note: The backend returns { error_no: 0, message: success, members: [...] } - // The _handleResponse expects a list as the top-level JSON object. - // We need to adjust how we handle this specific response or generalize _handleResponse. - // For now, we'll make a direct call and parse differently. try { final response = await _client.post( Uri.parse('$baseUrl/get_group_members'), @@ -142,17 +138,26 @@ class ApiService { ).timeout(const Duration(seconds: 10)); if (response.statusCode >= 200 && response.statusCode < 300) { - Map responseData = jsonDecode(response.body); - if (responseData['error_no'] == '0') { - return { - 'success': true, - 'members': responseData['members'] ?? [], // Return members list - 'message': responseData['message'], - }; + // The response is a JSON array with a single object + List responseList = jsonDecode(response.body); + if (responseList.isNotEmpty) { + Map firstItem = responseList[0]; + if (firstItem['error_no'] == '0') { + return { + 'success': true, + 'members': firstItem['members'] ?? [], // Return members list + 'message': firstItem['message'], + }; + } else { + return { + 'success': false, + 'message': firstItem['message'] ?? 'Unknown backend error', + }; + } } else { return { 'success': false, - 'message': responseData['message'] ?? 'Unknown backend error', + 'message': 'Empty response from server', }; } } else { @@ -225,7 +230,7 @@ class ApiService { String groupId, String password ) async { - return await post('/invite_to_group', { + return await post('/create_invite', { 'inviter_id': inviterId, 'invitee_id': inviteeId, 'group_id': groupId, @@ -235,7 +240,7 @@ class ApiService { // Get pending group invites for a user Future> getPendingInvites(String userId, String password) async { - return await post('/get_pending_invites', { + return await post('/get_pending', { 'user_id': userId, 'password': password, }); @@ -248,7 +253,7 @@ class ApiService { String status, String password ) async { - return await post('/respond_to_invite', { + return await post('/respond_invite', { 'user_id': userId, 'invite_id': inviteId, 'status': status, // 'accepted' or 'rejected'