diff --git a/package-lock.json b/package-lock.json index 983b290..9c3315d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -288,7 +289,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -305,7 +305,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -322,7 +321,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -339,7 +337,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -356,7 +353,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -373,7 +369,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -390,7 +385,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -407,7 +401,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -424,7 +417,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -441,7 +433,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -458,7 +449,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -475,7 +465,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -492,7 +481,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -509,7 +497,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -526,7 +513,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -543,7 +529,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -560,7 +545,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -577,7 +561,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -594,7 +577,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -611,7 +593,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -628,7 +609,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -645,7 +625,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -662,7 +641,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -679,7 +657,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -696,7 +673,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -713,7 +689,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -780,8 +755,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.52.4", @@ -794,8 +768,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.52.4", @@ -808,8 +781,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.52.4", @@ -822,8 +794,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.52.4", @@ -836,8 +807,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.52.4", @@ -850,8 +820,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.52.4", @@ -864,8 +833,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.52.4", @@ -878,8 +846,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.52.4", @@ -892,8 +859,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.52.4", @@ -906,8 +872,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { "version": "4.52.4", @@ -920,8 +885,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { "version": "4.52.4", @@ -934,8 +898,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.52.4", @@ -948,8 +911,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.52.4", @@ -962,8 +924,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.52.4", @@ -976,8 +937,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.52.4", @@ -990,8 +950,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.52.4", @@ -1004,8 +963,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-openharmony-arm64": { "version": "4.52.4", @@ -1018,8 +976,7 @@ "optional": true, "os": [ "openharmony" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.52.4", @@ -1032,8 +989,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.52.4", @@ -1046,8 +1002,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.52.4", @@ -1060,8 +1015,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.52.4", @@ -1074,8 +1028,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -1122,8 +1075,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@vitejs/plugin-react": { "version": "5.0.4", @@ -1197,6 +1149,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -1356,7 +1309,6 @@ "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -1406,7 +1358,6 @@ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.0.0" }, @@ -1429,7 +1380,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -1526,7 +1476,6 @@ } ], "license": "MIT", - "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -1578,7 +1527,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -1593,6 +1541,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -1602,6 +1551,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -1642,7 +1592,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -1720,7 +1669,6 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -1771,7 +1719,6 @@ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", - "peer": true, "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" @@ -1829,9 +1776,9 @@ } }, "node_modules/vite": { - "version": "7.1.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz", - "integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==", + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", + "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "license": "MIT", "peer": true, "dependencies": { diff --git a/package.json b/package.json index d3079e1..9a4d94c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "dev": "npm --prefix task_manager_app/src/views/todo-list run dev", "start": "concurrently \"npm run server\" \"npm run client\"", - "server": "cd task_manager_app/src && python3 -m controllers.server", + "server": "cd task_manager_app/src && python -m controllers.server", "client": "npm --prefix task_manager_app/src/views/todo-list run dev" }, "dependencies": { diff --git a/task_manager_app/docs/meeting_minutes/milestone_4/week_2.md b/task_manager_app/docs/meeting_minutes/milestone_4/week_2.md new file mode 100644 index 0000000..4387c6f --- /dev/null +++ b/task_manager_app/docs/meeting_minutes/milestone_4/week_2.md @@ -0,0 +1,70 @@ +# Feature Progress Report: +### Kylee +- Added: multiple lists +- Needs: rearrange list and rename list +### Bishakha +- Added: Due dates for tasks +- Needs: N/A +- Notes: (had issue with merge conflict, but resolved) +### Dominic +- Added: Reorder task +- Needs: N/A +- Notes: Can use functionality for reordering lists +### Simon +- Added: Edit task name +- Needs: Buttons prettier +- Notes: Edit functionality can extend to the list +### Jaxon +- Added: Priority to tasks +- Needs: task color based on priority? Sort by priority? +## +# Week 2 Planning +### Discussion: By-task vs by-role - Pros and Cons +- Dominic: By-task gives you experience with all parts of software +- Jaxon: By-task worked fine +- Kylee: By-task led to fast, productive development, but also merge conflicts with independent work. + +**Result**: Assignments this week will be by-role, not by-task. Developers will work on all parts of all tasks within their role. + +## This week's roles +Kylee and Simon are Non-developers this week. Here are their assignments: +- **Simon** + - Update UML (Use Case and Class Diagram) + - Manual Validation +- **Kylee** + - Update Github Project Board + - Select and Draft Design + +Developers to focus on their roles: +- **Dominic**: Front-end +- **Jaxon**: Backend +- **Bishakha**: Database + +## Tasks to complete +- **All developers**: Extending multiple list capabilities + - Edit name of list + - Rearrange list +- **Dominic**: Notification for a task (to level of day, not time. Also, we may not implement this. TBD based on Teams discussion) +- **Dominic**: (Optional) aesthetic changes to priority +- **Jaxon**: Delete list descripton +- **Jaxon**: Rearrange backend per Professor's instructions + + + + +### Development team deadlines: +- Wednesday: Do individual tasks (extending ) +- Wednesday-Friday: Collaborate +- Friday night / Saturday Morning: Code is stable and functional +### Non-Development team deadlines +- Saturday-Monday: Manual validation and testing (Simon) + + +### Add to scrum board: +- Hamburger menu does nothing, but should fold in and out +- Implement date -> datetime for task + + + + + \ No newline at end of file diff --git a/task_manager_app/docs/uml/Use Case Diagram.png b/task_manager_app/docs/uml/Use Case Diagram.png index 085892c..439e62a 100644 Binary files a/task_manager_app/docs/uml/Use Case Diagram.png and b/task_manager_app/docs/uml/Use Case Diagram.png differ diff --git a/task_manager_app/src/controllers/server.py b/task_manager_app/src/controllers/server.py index 163f0fa..e6120ab 100644 --- a/task_manager_app/src/controllers/server.py +++ b/task_manager_app/src/controllers/server.py @@ -12,13 +12,15 @@ list_table = ListTable(task_database) task_table = TaskTable(task_database) + class HTTPStatus(Enum): """HTTP Status Codes for API responses.""" - STATUS_OK = 200 # Success - everything worked - STATUS_CREATED = 201 # Success - a new resource was created - STATUS_BAD_REQUEST = 400 # Error - the client sent bad data - STATUS_NOT_FOUND = 404 # Error - the resource doesn't exist + STATUS_OK = 200 # Success - everything worked + STATUS_CREATED = 201 # Success - a new resource was created + STATUS_BAD_REQUEST = 400 # Error - the client sent bad data + STATUS_NOT_FOUND = 404 # Error - the resource doesn't exist + class Handler(BaseHTTPRequestHandler): """HTTP request handler for the Task Manager API.""" @@ -27,21 +29,25 @@ def do_OPTIONS(self): """Allows browsers from other websites to access this server (CORS support).""" self.send_response(200) self.send_header("Access-Control-Allow-Origin", "*") - self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + self.send_header( + "Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS" + ) self.send_header("Access-Control-Allow-Headers", "Content-Type") self.end_headers() - + def _send_json_response(self, data, status_code): """Helper function to send JSON responses.""" self.send_response(status_code) self.send_header("Content-Type", "application/json") self.send_header("Access-Control-Allow-Origin", "*") - self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + self.send_header( + "Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS" + ) self.send_header("Access-Control-Allow-Headers", "Content-Type") self.end_headers() response = json.dumps(data) self.wfile.write(response.encode("utf-8")) - + def retrieve_data(self, url): """Helper function to retrieve and parse JSON data from the request body.""" # Retrieve the size of the incoming data @@ -52,7 +58,7 @@ def retrieve_data(self, url): body_size = 0 body_bytes = self.rfile.read(body_size) body_text = body_bytes.decode("utf-8") - + # Read data and convert it to a dictionary if body_size > 0: body_bytes = self.rfile.read(body_size) @@ -62,7 +68,7 @@ def retrieve_data(self, url): except: data = {} else: - #data = {} + # data = {} data = json.loads(body_text) return data @@ -80,9 +86,12 @@ def do_GET(self): list_id = url_parts[2] if list_id is None: - self._send_json_response({"error": "list_id is required"}, HTTPStatus.STATUS_BAD_REQUEST.value) + self._send_json_response( + {"error": "list_id is required"}, + HTTPStatus.STATUS_BAD_REQUEST.value, + ) return - + # makes task list empty if none found tasks = task_table.get_all_tasks_from_list(list_id) or [] @@ -91,7 +100,7 @@ def do_GET(self): tasksJson.append(t.to_dict()) self._send_json_response(tasksJson, HTTPStatus.STATUS_OK.value) return - + # Get all lists if url == "/lists": # Retrieve all lists from the database @@ -104,14 +113,16 @@ def do_GET(self): return # Unknown route - self._send_json_response({"error": "Not found"}, HTTPStatus.STATUS_NOT_FOUND.value) - + self._send_json_response( + {"error": "Not found"}, HTTPStatus.STATUS_NOT_FOUND.value + ) + def do_POST(self): """Handle POST requests to create tasks.""" # Determine what URL was visited url = self.path - + # Retrieve data from request body data = self.retrieve_data(url) print(f"POST {url} - Received data:", data) @@ -123,10 +134,13 @@ def do_POST(self): task_name = data["name"] else: task_name = "" - + # Get due date due_date = data.get("due_date", None) - + + # Get priority (default to "medium" if not provided) + priority = data.get("priority", "medium") + if "list_id" in data: list_id = data["list_id"] else: @@ -135,24 +149,31 @@ def do_POST(self): # Check for empty name if task_name == "": # Send error - self._send_json_response({"error": "name is required"}, HTTPStatus.STATUS_BAD_REQUEST.value) + self._send_json_response( + {"error": "name is required"}, HTTPStatus.STATUS_BAD_REQUEST.value + ) return - - # create task with proper UUID - new_task = Task(task_name, due_date) - + + # create task with proper UUID and priority + new_task = Task(task_name, due_date, priority) + if list_id is None: # Send error - self._send_json_response({"error": "list_id is required"}, HTTPStatus.STATUS_BAD_REQUEST.value) + self._send_json_response( + {"error": "list_id is required"}, + HTTPStatus.STATUS_BAD_REQUEST.value, + ) return # Add task to database task_table.add_task(new_task, list_id) - + # Send success response - self._send_json_response(new_task.to_dict(), HTTPStatus.STATUS_CREATED.value) - return - + self._send_json_response( + new_task.to_dict(), HTTPStatus.STATUS_CREATED.value + ) + return + # Create a new list if url == "/addList": # Retrieve data from request body @@ -160,7 +181,9 @@ def do_POST(self): # Validate if list_name == "": - self._send_json_response({"error": "name is required"}, HTTPStatus.STATUS_BAD_REQUEST.value) + self._send_json_response( + {"error": "name is required"}, HTTPStatus.STATUS_BAD_REQUEST.value + ) return # Create the TaskList model and persist via ListTable @@ -168,27 +191,31 @@ def do_POST(self): list_table.add_list(new_list.id, new_list.name) # Respond with the created list - self._send_json_response(new_list.to_dict(), HTTPStatus.STATUS_CREATED.value) + self._send_json_response( + new_list.to_dict(), HTTPStatus.STATUS_CREATED.value + ) return - + # If we're here, we don't know what they want - self._send_json_response({"error": "Not found"}, HTTPStatus.STATUS_NOT_FOUND.value) - + self._send_json_response( + {"error": "Not found"}, HTTPStatus.STATUS_NOT_FOUND.value + ) + def do_PATCH(self): """Handle PATCH requests to update tasks.""" # Determine what URL was visited url = self.path - + # Toggle task complete/incomplete if url.startswith("/tasks/") and url.endswith("/toggleTask"): # Get task ID from URL url_parts = url.split("/") task_id = url_parts[2] - + # Get task from database task = task_table.get_task(task_id) - + # If task is found, toggle it if task: # Flip the completion status @@ -196,18 +223,20 @@ def do_PATCH(self): task.is_complete = False else: task.is_complete = True - + # Update task in database task_table.toggle_task_completion(task_id) - + # Send success response self._send_json_response(task.to_dict(), HTTPStatus.STATUS_OK.value) return else: # Task not found - self._send_json_response({"error": "Task not found"}, HTTPStatus.STATUS_NOT_FOUND.value) + self._send_json_response( + {"error": "Task not found"}, HTTPStatus.STATUS_NOT_FOUND.value + ) return - + # Update task fields (name, due_date, is_complete) if url.startswith("/tasks/") and url.count("/") == 2: # Example: /tasks/ @@ -217,7 +246,9 @@ def do_PATCH(self): # Get task from database task = task_table.get_task(task_id) if not task: - self._send_json_response({"error": "Task not found"}, HTTPStatus.STATUS_NOT_FOUND.value) + self._send_json_response( + {"error": "Task not found"}, HTTPStatus.STATUS_NOT_FOUND.value + ) return # Get updated fields from request body @@ -233,14 +264,20 @@ def do_PATCH(self): if "is_complete" in data: task.is_complete = bool(data["is_complete"]) updated = True + if "priority" in data: + task.priority = data["priority"] + updated = True if updated: task_table.update_task(task) self._send_json_response(task.to_dict(), HTTPStatus.STATUS_OK.value) else: - self._send_json_response({"error": "No valid fields to update"}, HTTPStatus.STATUS_BAD_REQUEST.value) + self._send_json_response( + {"error": "No valid fields to update"}, + HTTPStatus.STATUS_BAD_REQUEST.value, + ) return - + elif url.endswith("/reorderTasks"): data = self.retrieve_data(url) tasks = data["newTasks"] @@ -248,15 +285,17 @@ def do_PATCH(self): task_table.reorder_tasks(list_id, tasks) self._send_json_response(tasks, HTTPStatus.STATUS_OK.value) return - + # If we're here, we don't know what they want - self._send_json_response({"error": "Not found"}, HTTPStatus.STATUS_NOT_FOUND.value) - + self._send_json_response( + {"error": "Not found"}, HTTPStatus.STATUS_NOT_FOUND.value + ) + def do_DELETE(self): """Handle DELETE requests to delete tasks and lists.""" # Determine what URL was visited url = self.path - + # Delete a task if url.startswith("/tasks/"): # Get task ID from URL @@ -272,7 +311,9 @@ def do_DELETE(self): return else: # Task not found - self._send_json_response({"error": "Task not found"}, HTTPStatus.STATUS_NOT_FOUND.value) + self._send_json_response( + {"error": "Task not found"}, HTTPStatus.STATUS_NOT_FOUND.value + ) return # Delete a list (and its tasks via FK cascade, if configured) @@ -288,7 +329,9 @@ def do_DELETE(self): self._send_json_response({"deleted": True}, HTTPStatus.STATUS_OK.value) return else: - self._send_json_response({"error": "List not found"}, HTTPStatus.STATUS_NOT_FOUND.value) + self._send_json_response( + {"error": "List not found"}, HTTPStatus.STATUS_NOT_FOUND.value + ) return diff --git a/task_manager_app/src/controllers/task-controller.js b/task_manager_app/src/controllers/task-controller.js index 8d6780a..f94885d 100644 --- a/task_manager_app/src/controllers/task-controller.js +++ b/task_manager_app/src/controllers/task-controller.js @@ -1,87 +1,95 @@ - -export default class taskManager{ - constructor(){ - if (taskManager.instance){ - return taskManager.instance; - } - taskManager.instance = this; +export default class taskManager { + constructor() { + if (taskManager.instance) { + return taskManager.instance; } - async createTask(task, list_id) { - const name = typeof task === "string" ? task : (task.text || task.name || ""); - const due_date = typeof task === "object" ? (task.dueDate || task.due_date || null) : null; - console.log("Creating task:", { name, due_date, list_id }); // Add this to debug + taskManager.instance = this; + } + async createTask(task, list_id) { + const name = typeof task === "string" ? task : task.text || task.name || ""; + const due_date = + typeof task === "object" ? task.dueDate || task.due_date || null : null; + const priority = + typeof task === "object" ? task.priority || "medium" : "medium"; + console.log("Creating task:", { name, due_date, list_id }); // Add this to debug - try { - const res = await fetch("http://localhost:8000/addTask", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name, due_date, list_id }), - }); - if (!res.ok) throw new Error(`Create failed: ${res.status}`); - const data = await res.json(); - console.log("Task created:", data); - return data; - } catch (err) { - console.error(err); - throw err; - } + try { + const res = await fetch("http://localhost:8000/addTask", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, due_date, priority, list_id }), + }); + if (!res.ok) throw new Error(`Create failed: ${res.status}`); + const data = await res.json(); + console.log("Task created:", data); + return data; + } catch (err) { + console.error(err); + throw err; } + } - async deleteTask(task_id) { - try { - const res = await fetch(`http://localhost:8000/tasks/${task_id}`, { - method: "DELETE" - }); - if (!res.ok) throw new Error(`Delete failed: ${res.status}`); - // return true/false or parsed body if server returns JSON - try { return await res.json(); } catch { return { ok: true }; } - } catch (err) { - console.error(err); - throw err; - } + async deleteTask(task_id) { + try { + const res = await fetch(`http://localhost:8000/tasks/${task_id}`, { + method: "DELETE", + }); + if (!res.ok) throw new Error(`Delete failed: ${res.status}`); + // return true/false or parsed body if server returns JSON + try { + return await res.json(); + } catch { + return { ok: true }; + } + } catch (err) { + console.error(err); + throw err; } + } - async updateTask(task_id, updates) { - try { - const res = await fetch(`http://localhost:8000/tasks/${task_id}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(updates) - }); - if (!res.ok) throw new Error(`Update failed: ${res.status}`); - const data = await res.json(); - console.log("Task updated:", data); - return data; - } catch (err) { - console.error(err); - throw err; - } + async updateTask(task_id, updates) { + try { + const res = await fetch(`http://localhost:8000/tasks/${task_id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(updates), + }); + if (!res.ok) throw new Error(`Update failed: ${res.status}`); + const data = await res.json(); + console.log("Task updated:", data); + return data; + } catch (err) { + console.error(err); + throw err; } + } - async getTasks(list_id) { - try { - const res = await fetch(`http://localhost:8000/tasks/${(list_id)}`, { method: "GET"}); - if (!res.ok) throw new Error(`Fetch tasks failed: ${res.status}`); - const data = await res.json(); - return data; // expect array of { id, name, is_complete } - } catch (err) { - console.error(err); - throw err; - } + async getTasks(list_id) { + try { + const res = await fetch(`http://localhost:8000/tasks/${list_id}`, { + method: "GET", + }); + if (!res.ok) throw new Error(`Fetch tasks failed: ${res.status}`); + const data = await res.json(); + return data; // expect array of { id, name, is_complete } + } catch (err) { + console.error(err); + throw err; } - async reorderTasks(tasks, listId){ - try { - const res = await fetch(`http://localhost:8000/tasks/reorderTasks`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({newTasks: tasks, list: listId}), - }); - if (!res.ok) throw new Error(`Reordering tasks failed: ${res.status}`); - const data = await res.json(); - return data; - } catch (err) { - console.error("Request to reorder tasked failed"); - throw err; - } + } + async reorderTasks(tasks, listId) { + try { + const res = await fetch(`http://localhost:8000/reorderTasks`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ newTasks: tasks, list: listId }), + }); + if (!res.ok) throw new Error(`Reordering tasks failed: ${res.status}`); + const data = await res.json(); + return data; + } catch (err) { + console.error("Request to reorder tasked failed"); + throw err; } -} \ No newline at end of file + } +} diff --git a/task_manager_app/src/models/database.py b/task_manager_app/src/models/database.py index f4073dc..a410db2 100644 --- a/task_manager_app/src/models/database.py +++ b/task_manager_app/src/models/database.py @@ -7,8 +7,9 @@ class TaskDatabase: """Database class for managing list and task persistence in SQLite (Singleton pattern).""" + _instance = None - + def __new__(cls, database_name="database.db"): """Singleton Design Pattern: Ensure only one instance of TaskDatabase exists.""" if cls._instance is None: @@ -21,50 +22,61 @@ def __init__(self, database_name="database.db"): # Only initialize once if self._initialized: return - + # Use absolute path: put database.db in the models directory if not os.path.isabs(database_name): models_dir = Path(__file__).parent database_path = models_dir / database_name else: database_path = database_name - - self.database_connection = sqlite3.connect(str(database_path), check_same_thread=False) + + self.database_connection = sqlite3.connect( + str(database_path), check_same_thread=False + ) self.cursor = self.database_connection.cursor() self.cursor.execute("PRAGMA foreign_keys = ON") # Turn on foreign keys self._create_tables() self._initialized = True - + def _create_tables(self): """Create tables for task lists and tasks if they do not exist.""" # Table for lists - self.cursor.execute(""" + self.cursor.execute( + """ CREATE TABLE IF NOT EXISTS lists ( id TEXT PRIMARY KEY, name TEXT NOT NULL ) - """) + """ + ) # Table for tasks, each linked to a list - self.cursor.execute(""" + self.cursor.execute( + """ CREATE TABLE IF NOT EXISTS task_list ( id TEXT PRIMARY KEY, name TEXT NOT NULL, is_complete INTEGER NOT NULL DEFAULT 0, due_date TEXT, + priority TEXT DEFAULT 'medium', list_id TEXT NOT NULL, FOREIGN KEY (list_id) REFERENCES lists (id) ON DELETE CASCADE ) - """) + """ + ) # Helpful for speed when we look up tasks by list_id - self.cursor.execute("CREATE INDEX IF NOT EXISTS idx_task_list_list_id ON task_list(list_id)") + self.cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_task_list_list_id ON task_list(list_id)" + ) self.database_connection.commit() - + def close(self): """Close the database connection.""" self.database_connection.close() + + class ListTable: """This class manages the 'lists' table in the database.""" @@ -74,13 +86,17 @@ def __init__(self, task_database: "TaskDatabase"): def add_list(self, list_id: str, name: str) -> dict: """Add a new list to the list table.""" - self.cursor.execute("INSERT INTO lists (id, name) VALUES (?, ?)", (list_id, name)) + self.cursor.execute( + "INSERT INTO lists (id, name) VALUES (?, ?)", (list_id, name) + ) self.database_connection.commit() return {"id": list_id, "name": name} - + def rename_list(self, list_id: str, new_name: str) -> bool: """Change the name of a list by id. Return True if successful, False if not found.""" - self.cursor.execute("UPDATE lists SET name = ? WHERE id = ?", (new_name, list_id)) + self.cursor.execute( + "UPDATE lists SET name = ? WHERE id = ?", (new_name, list_id) + ) self.database_connection.commit() return self.cursor.rowcount > 0 @@ -108,11 +124,13 @@ def get_list(self, list_id: str) -> TaskList | None: if not row: return None - task_list = TaskList(row[1]) # name - task_list.id = row[0] # id + task_list = TaskList(row[1]) # name + task_list.id = row[0] # id return task_list - - def get_list_with_tasks(self, list_id: str, task_table: "TaskTable") -> TaskList | None: + + def get_list_with_tasks( + self, list_id: str, task_table: "TaskTable" + ) -> TaskList | None: """Return a TaskList object with all its tasks attached.""" self.cursor.execute("SELECT id, name FROM lists WHERE id = ?", (list_id,)) row = self.cursor.fetchone() @@ -127,6 +145,7 @@ def get_list_with_tasks(self, list_id: str, task_table: "TaskTable") -> TaskList task_list.tasks = task_table.get_all_tasks_from_list(list_id_db) return task_list + class TaskTable: """This class manages the 'tasks' table in the database.""" @@ -137,33 +156,41 @@ def __init__(self, task_database: "TaskDatabase"): def add_task(self, task, list_id: str): """Add a new task to the database for a specific list""" self.cursor.execute( - "INSERT INTO task_list (id, name, is_complete, due_date, list_id) VALUES (?, ?, ?, ?, ?)", - (task.id, task.name, int(task.is_complete), task.due_date, list_id) + "INSERT INTO task_list (id, name, is_complete, due_date, priority, list_id) VALUES (?, ?, ?, ?, ?, ?)", + ( + task.id, + task.name, + int(task.is_complete), + task.due_date, + task.priority, + list_id, + ), ) self.database_connection.commit() return task - + def rename_task(self, task_id: str, new_name: str) -> bool: """Change the name of a task by its unique ID.""" self.cursor.execute( - "UPDATE task_list SET name = ? WHERE id = ?", - (new_name, task_id) + "UPDATE task_list SET name = ? WHERE id = ?", (new_name, task_id) ) self.database_connection.commit() return self.cursor.rowcount > 0 - + def update_task(self, task): """Update all fields of a task in the database.""" self.cursor.execute( - "UPDATE task_list SET name = ?, due_date = ?, is_complete = ? WHERE id = ?", - (task.name, task.due_date, int(task.is_complete), task.id) + "UPDATE task_list SET name = ?, due_date = ?, is_complete = ?, priority = ? WHERE id = ?", + (task.name, task.due_date, int(task.is_complete), task.priority, task.id), ) self.database_connection.commit() return self.cursor.rowcount > 0 - + def toggle_task_completion(self, task_id: str) -> bool: """Flip the completion status of a task (complete <-> incomplete).""" - self.cursor.execute("SELECT is_complete FROM task_list WHERE id = ?", (task_id,)) + self.cursor.execute( + "SELECT is_complete FROM task_list WHERE id = ?", (task_id,) + ) row = self.cursor.fetchone() if not row: return False @@ -172,12 +199,11 @@ def toggle_task_completion(self, task_id: str) -> bool: new_state = 0 if current_state else 1 self.cursor.execute( - "UPDATE task_list SET is_complete = ? WHERE id = ?", - (new_state, task_id) + "UPDATE task_list SET is_complete = ? WHERE id = ?", (new_state, task_id) ) self.database_connection.commit() return self.cursor.rowcount > 0 - + def delete_task(self, task_id: str) -> bool: """Delete a task from the database using its unique ID.""" self.cursor.execute("DELETE FROM task_list WHERE id = ?", (task_id,)) @@ -186,22 +212,28 @@ def delete_task(self, task_id: str) -> bool: def get_task(self, task_id): """Get one task by its ID, or None if it doesnot exist.""" - self.cursor.execute("SELECT id, name, is_complete, due_date FROM task_list WHERE id = ?", (task_id,)) + self.cursor.execute( + "SELECT id, name, is_complete, due_date, priority FROM task_list WHERE id = ?", + (task_id,), + ) row = self.cursor.fetchone() if not row: return None - t = Task(row[1], row[3]) # passing due date as second argument + t = Task(row[1], row[3], row[4]) # name, due_date, priority t.id = row[0] t.is_complete = bool(row[2]) return t - + def get_all_tasks_from_list(self, list_id): """Get all tasks belonging to one list by filtering on list_id.""" - self.cursor.execute("SELECT id, name, is_complete, due_date FROM task_list WHERE list_id = ?", (list_id,)) + self.cursor.execute( + "SELECT id, name, is_complete, due_date, priority FROM task_list WHERE list_id = ?", + (list_id,), + ) rows = self.cursor.fetchall() tasks = [] for row in rows: - t = Task(row[1], row[3]) # passing due date as second argument + t = Task(row[1], row[3], row[4]) # name, due_date, priority t.id = row[0] t.is_complete = bool(row[2]) tasks.append(t) @@ -214,9 +246,10 @@ def reorder_tasks(self, list_id, tasks) -> TaskList | None: name = task.get("text", "") is_complete = task.get("completed", False) due_date = task.get("dueDate", "") + priority = task.get("priority", "medium") self.cursor.execute( - "INSERT INTO task_list (id, name, is_complete, due_date, list_id) VALUES (?, ?, ?, ?, ?)", - (task_id, name, int(is_complete), due_date, list_id) + "INSERT INTO task_list (id, name, is_complete, due_date, priority, list_id) VALUES (?, ?, ?, ?, ?, ?)", + (task_id, name, int(is_complete), due_date, priority, list_id), ) self.database_connection.commit() return self.get_all_tasks_from_list(list_id) diff --git a/task_manager_app/src/models/task.py b/task_manager_app/src/models/task.py index 0415f3f..e845d50 100644 --- a/task_manager_app/src/models/task.py +++ b/task_manager_app/src/models/task.py @@ -1,15 +1,18 @@ import uuid + class Task: """This class gives Task objects a name and a completion status.""" - def __init__(self, name: str, due_date = None): + + def __init__(self, name: str, due_date=None, priority: str = "medium"): """Initialize a new Task.""" self.id = str(uuid.uuid4()) # Set a unique identifier for each task self.name = name self.is_complete = False # Set default completion status to False self.due_date = due_date # added due date field + self.priority = priority # added priority field - def complete_task(self) -> None: + def complete_task(self) -> None: """Mark the task as complete.""" self.is_complete = True @@ -19,6 +22,6 @@ def to_dict(self): "name": self.name, "is_complete": self.is_complete, "id": self.id, - "due_date": self.due_date # included due date in dictionary + "due_date": self.due_date, # included due date in dictionary + "priority": self.priority, # included priority in dictionary } - \ No newline at end of file diff --git a/task_manager_app/src/views/todo-list/src/App.jsx b/task_manager_app/src/views/todo-list/src/App.jsx index f2f60bb..d40ce30 100644 --- a/task_manager_app/src/views/todo-list/src/App.jsx +++ b/task_manager_app/src/views/todo-list/src/App.jsx @@ -7,7 +7,16 @@ import { List, arrayMove } from "react-movable"; export default function TodoApp() { const [lists, setLists] = useState([]); const [activeList, setActiveList] = useState(); - const taskHandler = new taskManager(); + const [editingListId, setEditingListId] = useState(null); + const [listName, setListName] = useState(''); + const [tasks, setTasks] = useState([]); + const [newTaskInput, setNewTaskInput] = useState(''); + const [newListInput, setNewListInput] = useState(''); + const [newDueDate, setNewDueDate] = useState(''); + const [editingTaskId, setEditingTaskId] = useState(null); + const [editingText, setEditingText] = useState(''); + const [editingPriority, setEditingPriority] = useState('medium'); + const taskHandler = useState(() => new taskManager())[0]; useEffect(() => { console.log("Loading lists from server..."); @@ -48,7 +57,7 @@ export default function TodoApp() { // If the previously active list still exists, keep it. // Otherwise, switch to the first remaining list (or empty). setActiveList(currActive => { - if (updated.some(l => l.id === currActive.id)) return currActive; + if (currActive && updated.some(l => l.id === currActive.id)) return currActive; return updated.length > 0 ? updated[0] : null; }); @@ -59,10 +68,6 @@ export default function TodoApp() { } }; - // No boilerplate tasks, just loading from database - const [tasks, setTasks] = useState([]); - - // load tasks from server on initial mount useEffect(() => { if (!activeList) return; @@ -75,19 +80,14 @@ export default function TodoApp() { id: t.id, text: t.name, completed: !!t.is_complete, - dueDate: t.due_date || '' + dueDate: t.due_date || '', + priority: t.priority || 'medium' }))); } catch (err) { console.error("Failed to load tasks:", err); } })(); - }, [activeList]); - - const [newTaskInput, setNewTaskInput] = useState(''); - const [newListInput, setNewListInput] = useState(''); - const [newDueDate, setNewDueDate] = useState(''); - const [editingTaskId, setEditingTaskId] = useState(null); - const [editingText, setEditingText] = useState(''); + }, [activeList, taskHandler]); const addTask = () => { @@ -97,7 +97,7 @@ export default function TodoApp() { } if (newTaskInput.trim()) { - const newTask = { id: Date.now(), text: newTaskInput, completed: false, dueDate: newDueDate }; + const newTask = { id: Date.now(), text: newTaskInput, completed: false, dueDate: newDueDate, priority: 'medium' }; console.log("Adding task locally:", newTask); console.log("Active list:", activeList); // Add this line! setTasks([...tasks, newTask]); @@ -157,10 +157,29 @@ export default function TodoApp() { await taskHandler.updateTask(task.id, { name: task.text, is_complete: task.completed, - due_date: task.dueDate + due_date: task.dueDate, + priority: task.priority }); } } + + const daysFromDate = (dueDate) => { + const [year, month, day] = dueDate.split("-").map(Number); + let taskDay = new Date(year, month - 1, day); + let currentDate = new Date() + taskDay.setHours(0,0,0,0); + currentDate.setHours(0,0,0,0); + let daysAway = taskDay - currentDate + daysAway = Math.ceil(daysAway / (1000 * 60 * 60 * 24)); + console.log(daysAway) + if (daysAway > 0){ + return `: due in ${daysAway} day(s)`; + } else if (daysAway == 0){ + return `: due today`; + } else { + return `: due ${-daysAway} day(s) ago`; + } + } return (
@@ -179,27 +198,74 @@ export default function TodoApp() {
- {lists.map(list => ( -
- - -
- ))} + {/*/The async function will have to be implemented in task-controller.js*/} + { + let newLists = arrayMove(lists, oldIndex, newIndex); + setLists(newLists); + /*(async () => { + await taskHandler.reorderLists(newLists); + })();*/ + }} + renderList={({ children, props }) => ( +
+ {children} +
+ )} + renderItem={({ value: list, props }) => ( +
+ {editingListId == list.id ? + <> + setListName(e.target.value)} + /> + + : + <> + + + + + } +
+ )} + />
@@ -244,7 +310,7 @@ export default function TodoApp() { )} renderItem={({ value: task, props }) => ( -
+
+ >πŸ“ )} - - {task.dueDate ? task.dueDate : ''} + {editingTaskId !== task.id && ( + + {(task.priority || 'medium').charAt(0).toUpperCase() + (task.priority || 'medium').slice(1) } + + )} + + + {task.dueDate ? task.dueDate + daysFromDate(task.dueDate) : ''} + >πŸ—‘
)} /> diff --git a/task_manager_app/src/views/todo-list/src/index.css b/task_manager_app/src/views/todo-list/src/index.css index ed405fd..fe27e3a 100644 --- a/task_manager_app/src/views/todo-list/src/index.css +++ b/task_manager_app/src/views/todo-list/src/index.css @@ -217,6 +217,10 @@ body { gap: 12px; } +.draggable { + cursor: grab; +} + .task-item { display: flex; align-items: center; @@ -401,8 +405,7 @@ body { } .task-date { - margin-left: auto; - margin-right: 10px; + margin: 0px 10px 0px 10px; font-size: 14px; color: gray; }