11from fastapi import APIRouter , HTTPException , Query , Query , Depends
22from typing import Dict , Optional
3- from db .database import groups_collection , users_collection , tasks_collection
3+ from db .database import groups_collection , users_collection , tasks_collection , subteams_collection
44from db .models import User , Group , Task , Notification
55from db .schemas import users_serial , groups_serial , tasks_serial
66from bson import ObjectId # mongodb uses ObjectId to store _id
@@ -29,77 +29,113 @@ async def get_tasks(assigned_to: Optional[str] = Query(None, description="User e
2929@tasks_router .post ("/tasks/" )
3030async def create_task (task : Task ):
3131
32- # Check if user exists
33- assigned_users = [user for user in task .assigned_to if users_collection .find_one ({"email" : user })]
34- if not assigned_users :
35- raise HTTPException (status_code = 400 , detail = f"User(s) { task .assigned_to } do not exist" )
36- # Assign only valid users
37- task .assigned_to = assigned_users
38-
39- # Check if group exists
32+ # Validate group
4033 assigned_group = groups_collection .find_one ({"_id" : ObjectId (task .group )})
41-
4234 if not assigned_group :
4335 raise HTTPException (status_code = 400 , detail = f"Group { task .group } does not exist" )
4436
45- # Check if all assigned users are members of the group
46- non_members = [user for user in task .assigned_to if user not in assigned_group ["members" ]]
47-
48- if non_members :
49- raise HTTPException (status_code = 400 , detail = f"User(s) { non_members } are not members of the group" )
50-
51- # stop duplicate tasks for one user
37+ assigned_to = task .assigned_to
38+ subteam_id = None
39+
40+ if len (assigned_to ) == 1 :
41+ possible_subteam_id = assigned_to [0 ]
42+ try :
43+ subteam = subteams_collection .find_one ({"_id" : ObjectId (possible_subteam_id )})
44+ except :
45+ subteam = None
46+
47+ if subteam :
48+ subteam_id = possible_subteam_id
49+ assigned_to = subteam ["members" ] # Get members of subteam
50+
51+ # Make sure all members are part of the group
52+ non_members = [user for user in assigned_to if user not in assigned_group ["members" ]]
53+ if non_members :
54+ raise HTTPException (status_code = 400 , detail = f"User(s) { non_members } in subteam are not part of the group" )
55+
56+ # Send emails to subteam members
57+ send_assigned_task_email_subteam (
58+ subteam_id = subteam_id ,
59+ task_name = task .name ,
60+ task_description = task .description ,
61+ task_id = "pending" , # Will update after insertion
62+ group = assigned_group
63+ )
64+
65+ else :
66+ # Not a subteam, continue as individual user assignment
67+ assigned_to = [user for user in assigned_to if users_collection .find_one ({"email" : user })]
68+ if not assigned_to :
69+ raise HTTPException (status_code = 400 , detail = f"No valid users found in { task .assigned_to } " )
70+ else :
71+ # Individual users case
72+ assigned_to = [user for user in assigned_to if users_collection .find_one ({"email" : user })]
73+ if not assigned_to :
74+ raise HTTPException (status_code = 400 , detail = f"No valid users found in { task .assigned_to } " )
75+
76+ # Check duplicate task for same users
5277 existing_task = tasks_collection .find_one ({
53- "assigned_to" : task . assigned_to ,
78+ "assigned_to" : assigned_to ,
5479 "name" : task .name ,
5580 "group" : task .group
5681 })
5782 if existing_task :
58- raise HTTPException ( status_code = 400 , detail = "task already exists for this user in the group" )
83+ raise HTTPException (status_code = 400 , detail = "Task already exists for this user/subteam in the group" )
5984
60- # ensure correct status
61- valid_statuses = ["To Do" , "In Progress" , "Completed" ]
85+ # Validate task status and priority
86+ valid_statuses = ["To Do" , "In Progress" , "Completed" ]
87+ valid_priorities = ["Low" , "Medium" , "High" ]
6288 if task .status not in valid_statuses :
63- raise HTTPException (status_code = 400 , detail = f" invalid status, choose from { valid_statuses } " )
64-
65- # ensure correct priority
66- valid_priorities = ["Low" , "Medium" , "High" ]
89+ raise HTTPException (status_code = 400 , detail = f"Invalid status, choose from { valid_statuses } " )
6790 if task .priority not in valid_priorities :
68- raise HTTPException ( status_code = 400 , detail = f"invalid priority. Choose from { valid_priorities } " )
91+ raise HTTPException (status_code = 400 , detail = f"Invalid priority, choose from { valid_priorities } " )
6992
70- # insert task to database
93+ # Prepare and insert task
7194 task_data = task .dict ()
95+ task_data ["assigned_to" ] = assigned_to
96+ if subteam_id :
97+ task_data ["subteam" ] = str (subteam_id ) # Store subteam ID as string
98+
7299 new_task = tasks_collection .insert_one (task_data )
73100
74- # include new task in groups task list
101+ # Update group's task list
75102 groups_collection .update_one (
76103 {"_id" : ObjectId (task .group )},
77104 {"$push" : {"tasks" : str (new_task .inserted_id )}}
78105 )
79106
80- # send email to new user
81- for user in task .assigned_to :
82- send_assigned_task_email (user , task .name , task .description , str (new_task .inserted_id ), task .group , assigned_group ["name" ])
83- # create notification for each user
84- notification_dir = {
85- "user_email" : user ,
86- "group_id" : task .group ,
87- "notification_type" : "Task Assigned" ,
88- "content" : f"You have been assigned a new task: { task .name } " ,
89- "task_id" : str (new_task .inserted_id )
90- }
91-
92- # create notification in database
93- notification = CreateNotificationRequest (** notification_dir )
94- await create_notification (notification )
107+ # Send emails to individual users if not subteam
108+ if not subteam_id :
109+ for user in assigned_to :
110+ send_assigned_task_email (
111+ user_email = user ,
112+ task_name = task .name ,
113+ task_description = task .description ,
114+ task_id = str (new_task .inserted_id ),
115+ group_id = task .group ,
116+ group_name = assigned_group ["name" ]
117+ )
118+
119+ # Create notification for each user
120+ notification_dir = {
121+ "user_email" : user ,
122+ "group_id" : task .group ,
123+ "notification_type" : "Task Assigned" ,
124+ "content" : f"You have been assigned a new task: { task .name } " ,
125+ "task_id" : str (new_task .inserted_id )
126+ }
127+ notification = CreateNotificationRequest (** notification_dir )
128+ await create_notification (notification )
95129
96130 return {
97- "id" : str (new_task .inserted_id ),
98- "message" : "task created and assigned successfully" ,
99- "task_details" : {** task_data , "_id" : str (new_task .inserted_id )}
131+ "id" : str (new_task .inserted_id ),
132+ "message" : "Task created and assigned successfully" ,
133+ "task_details" : {** task_data , "_id" : str (new_task .inserted_id )}
100134 }
101135
102136
137+
138+
103139@tasks_router .put ("/tasks/assign/" )
104140async def assign_task (task_id : str , new_user_email : str ):
105141 # check if task exists
@@ -369,3 +405,160 @@ def send_assigned_task_email(user_email: str, task_name: str, task_description:
369405 return {"message" : "Task assignment email sent successfully" , "task_id" : task_id , "assigned_to" : user_email }
370406
371407
408+ TASK_ASSIGNMENT_EMAIL_TEMPLATE_SUBTEAM = """<!DOCTYPE html>
409+ <html>
410+ <head>
411+ <meta charset="UTF-8">
412+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
413+ <title>New Task Assigned</title>
414+ <style>
415+ body {{
416+ font-family: Arial, sans-serif;
417+ background-color: #f4f4f4;
418+ margin: 0;
419+ padding: 20px;
420+ }}
421+ .container {{
422+ background-color: #ffffff;
423+ padding: 20px;
424+ border-radius: 8px;
425+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
426+ max-width: 600px;
427+ margin: auto;
428+ text-align: center;
429+ }}
430+ .logo {{
431+ width: 60px;
432+ height: 60px;
433+ margin: 0 auto 20px;
434+ display: block;
435+ }}
436+ .header {{
437+ font-size: 24px;
438+ font-weight: bold;
439+ color: #333;
440+ }}
441+ .content {{
442+ font-size: 16px;
443+ color: #555;
444+ margin-top: 20px;
445+ }}
446+ .task-box {{
447+ border: 4px solid #a463f2;
448+ border-radius: 12px;
449+ padding: 20px;
450+ margin-top: 20px;
451+ display: inline-block;
452+ width: 100%;
453+ box-sizing: border-box;
454+ }}
455+ .task-name {{
456+ font-size: 24px;
457+ font-weight: bold;
458+ color: #333;
459+ margin-bottom: 10px;
460+ }}
461+ .task-description {{
462+ font-size: 16px;
463+ color: #555;
464+ margin-bottom: 15px;
465+ }}
466+ .instruction {{
467+ font-size: 12px;
468+ color: #666;
469+ margin-bottom: 6px;
470+ }}
471+ .button {{
472+ display: block;
473+ width: 150px;
474+ margin: 0 auto;
475+ padding: 12px;
476+ text-align: center;
477+ background-color: #a463f2;
478+ color: #FFFFFF !important;
479+ text-decoration: none;
480+ border-radius: 8px;
481+ font-size: 16px;
482+ font-weight: bold;
483+ }}
484+ .footer {{
485+ margin-top: 20px;
486+ font-size: 12px;
487+ color: #888;
488+ text-align: center;
489+ }}
490+ </style>
491+ </head>
492+ <body>
493+ <div class="container">
494+ <img class="logo" src="https://group-grade-files.s3.eu-north-1.amazonaws.com/groupgrade-assets/hexlogo.png" alt="GroupGrade Logo" />
495+ <div class="header">New Task Assigned: {task_name}</div>
496+ <div class="content">
497+ <p>Hi {user_name},</p>
498+ <p>Your subteam {subteam_name} has been assigned a new task!</p>
499+ <div class="task-box">
500+ <div class="task-name">{task_name}</div>
501+ <br>
502+ <div class="task-description">{task_description}</div>
503+ <br>
504+ <p class="instruction">Click the button below to view and start working on your task.</p>
505+ <a href="{task_link}" class="button"
506+ style="color: #FFFFFF !important; text-decoration: none !important;">
507+ View task
508+ </a>
509+ </div>
510+ <div class="footer">
511+ Need help? Contact us at <a href="mailto:support@groupgrade.com">support@groupgrade.com</a><br/>
512+ © 2025 GroupGrade. All rights reserved.
513+ </div>
514+ </div>
515+ </div>
516+ </body>
517+ </html>"""
518+
519+ def send_assigned_task_email_subteam (subteam_id : str , task_name : str , task_description : str , task_id : str , group : dict ):
520+ """
521+ Sends task assignment emails to all members of a subteam given its ID.
522+ """
523+ # Fetch subteam from DB
524+ subteam = subteams_collection .find_one ({"_id" : ObjectId (subteam_id )})
525+ if not subteam :
526+ return {"error" : f"Subteam with ID { subteam_id } not found." }
527+
528+ for user_email in subteam ["members" ]:
529+ # Validate email format
530+ if not is_valid_email (user_email ):
531+ continue # Skip invalid emails
532+
533+ # Use fallback in case user is not in DB
534+ user = users_collection .find_one ({"email" : user_email })
535+ user_email_for_link = user_email if user else "notRegistered"
536+
537+ # Generate task link
538+ task_link = f"{ BASE_URL .format (frontend_url = frontend_url_dev , group_id = group ['_id' ])} "
539+
540+ if not user :
541+ continue # Skip if user doesn't exist
542+
543+ # Format email content using subteam template
544+ email_content = TASK_ASSIGNMENT_EMAIL_TEMPLATE_SUBTEAM .format (
545+ user_name = user ["name" ],
546+ subteam_name = subteam ["team_name" ],
547+ task_name = task_name ,
548+ task_description = task_description ,
549+ group_name = group ["name" ],
550+ task_link = task_link
551+ )
552+
553+ # Send email
554+ email_sender .send_email (
555+ receipient = user_email ,
556+ email_message = email_content ,
557+ subject_line = f"New Task Assigned to Subteam '{ subteam ['team_name' ]} ' - { task_name } "
558+ )
559+
560+ return {
561+ "message" : "Subteam task notification emails sent successfully" ,
562+ "task_id" : task_id ,
563+ "subteam" : subteam ["team_name" ]
564+ }
0 commit comments