diff --git a/backend/app/models/Task.py b/backend/app/models/Task.py index 11b9ece..6b787a7 100644 --- a/backend/app/models/Task.py +++ b/backend/app/models/Task.py @@ -15,6 +15,7 @@ class TaskType(str, PyEnum): VOLUNTEER_APP_REVIEW = "volunteer_app_review" PROFILE_UPDATE = "profile_update" MATCHING = "matching" + USER_OPT_OUT = "user_opt_out" class TaskPriority(str, PyEnum): diff --git a/backend/app/schemas/task.py b/backend/app/schemas/task.py index 7f4a238..a09979f 100644 --- a/backend/app/schemas/task.py +++ b/backend/app/schemas/task.py @@ -20,6 +20,7 @@ class TaskType(str, Enum): VOLUNTEER_APP_REVIEW = "volunteer_app_review" PROFILE_UPDATE = "profile_update" MATCHING = "matching" + USER_OPT_OUT = "user_opt_out" class TaskPriority(str, Enum): diff --git a/backend/app/services/implementations/user_service.py b/backend/app/services/implementations/user_service.py index 40ad27c..afc8b9e 100644 --- a/backend/app/services/implementations/user_service.py +++ b/backend/app/services/implementations/user_service.py @@ -1,4 +1,5 @@ import logging +from datetime import datetime from typing import List from uuid import UUID @@ -18,6 +19,9 @@ RankingPreference, Role, Task, + TaskPriority, + TaskStatus, + TaskType, Treatment, User, UserData, @@ -234,6 +238,19 @@ async def soft_delete_user_by_id(self, user_id: str): raise HTTPException(status_code=404, detail="User not found") db_user.active = False + + # Create a User Opt Out task for admin visibility (start and end date = day of opt-out) + opt_out_time = datetime.utcnow() + opt_out_task = Task( + participant_id=db_user.id, + type=TaskType.USER_OPT_OUT, + priority=TaskPriority.NO_STATUS, + status=TaskStatus.PENDING, + start_date=opt_out_time, + end_date=opt_out_time, + ) + self.db.add(opt_out_task) + self.db.commit() except ValueError: raise HTTPException(status_code=400, detail="Invalid user ID format") diff --git a/frontend/src/APIClients/taskAPIClient.ts b/frontend/src/APIClients/taskAPIClient.ts index cacd934..b558f06 100644 --- a/frontend/src/APIClients/taskAPIClient.ts +++ b/frontend/src/APIClients/taskAPIClient.ts @@ -6,7 +6,12 @@ export interface BackendTask { participantName: string | null; participantEmail: string | null; participantRoleId: number | null; - type: 'intake_form_review' | 'volunteer_app_review' | 'profile_update' | 'matching'; + type: + | 'intake_form_review' + | 'volunteer_app_review' + | 'profile_update' + | 'matching' + | 'user_opt_out'; priority: 'no_status' | 'low' | 'medium' | 'high'; status: 'pending' | 'in_progress' | 'completed'; assigneeId: string | null; @@ -26,7 +31,12 @@ export interface TaskListResponse { export interface UpdateTaskRequest { participantId?: string; - type?: 'intake_form_review' | 'volunteer_app_review' | 'profile_update' | 'matching'; + type?: + | 'intake_form_review' + | 'volunteer_app_review' + | 'profile_update' + | 'matching' + | 'user_opt_out'; priority?: 'no_status' | 'low' | 'medium' | 'high'; status?: 'pending' | 'in_progress' | 'completed'; assigneeId?: string | null; diff --git a/frontend/src/pages/admin/tasks.tsx b/frontend/src/pages/admin/tasks.tsx index e9093a5..6829c3e 100644 --- a/frontend/src/pages/admin/tasks.tsx +++ b/frontend/src/pages/admin/tasks.tsx @@ -82,6 +82,7 @@ const mapAPITaskToFrontend = ( volunteer_app_review: 'Ranking / Secondary App Review', profile_update: 'Profile Update', matching: 'Matching', + user_opt_out: 'User Opt Out', }; // Map backend priority to frontend priority @@ -98,6 +99,7 @@ const mapAPITaskToFrontend = ( volunteer_app_review: 'secondary_app', profile_update: 'profile_updates', matching: 'matching_requests', + user_opt_out: 'user_opt_outs', }; // Format dates from ISO to DD/MM/YY @@ -132,12 +134,12 @@ const mapAPITaskToFrontend = ( startDate: formatDate(apiTask.startDate), endDate: apiTask.endDate ? formatDate(apiTask.endDate) : formatDate(apiTask.startDate), priority: priorityMap[apiTask.priority] || 'Add status', - type: typeMap[apiTask.type] || 'Intake Form Review', + type: typeMap[apiTask.type] ?? 'Intake Form Review', assignee: assignee?.name, completed: apiTask.status === 'completed', userType, - category: categoryMap[apiTask.type] || 'intake_screening', - description: apiTask.description || `Task for ${typeMap[apiTask.type]}`, + category: categoryMap[apiTask.type] ?? 'intake_screening', + description: apiTask.description ?? `Task for ${typeMap[apiTask.type] ?? 'Intake Form Review'}`, }; }; diff --git a/frontend/src/types/adminTypes.ts b/frontend/src/types/adminTypes.ts index 5a5d398..efb397c 100644 --- a/frontend/src/types/adminTypes.ts +++ b/frontend/src/types/adminTypes.ts @@ -2,14 +2,24 @@ export interface Task { id: string; name: string; participantId?: string; - type: 'Intake Form Review' | 'Ranking / Secondary App Review' | 'Matching' | 'Profile Update'; + type: + | 'Intake Form Review' + | 'Ranking / Secondary App Review' + | 'Matching' + | 'Profile Update' + | 'User Opt Out'; startDate: string; endDate: string; priority: 'High' | 'Medium' | 'Low' | 'Add status'; assignee?: string; completed: boolean; userType: 'Participant' | 'Volunteer'; - category: 'intake_screening' | 'secondary_app' | 'matching_requests' | 'profile_updates'; + category: + | 'intake_screening' + | 'secondary_app' + | 'matching_requests' + | 'profile_updates' + | 'user_opt_outs'; description?: string; } @@ -32,6 +42,7 @@ export const categoryLabels: Record = { secondary_app: 'Review secondary application / ranking forms', matching_requests: 'Participants requesting a match', profile_updates: 'User profile updates', + user_opt_outs: 'User opt outs', }; export const taskCategories: TaskCategory[] = [ @@ -59,4 +70,10 @@ export const taskCategories: TaskCategory[] = [ categoryKey: 'profile_updates', bgColor: '#EEEEEC', }, + { + id: '5', + name: 'User opt outs', + categoryKey: 'user_opt_outs', + bgColor: 'rgba(200, 200, 200, 0.4)', + }, ]; diff --git a/frontend/src/utils/taskHelpers.ts b/frontend/src/utils/taskHelpers.ts index f48264a..571dd54 100644 --- a/frontend/src/utils/taskHelpers.ts +++ b/frontend/src/utils/taskHelpers.ts @@ -8,6 +8,7 @@ export const getTypeColor = (type: string): { bg: string; color: string } => { 'Ranking / Secondary App Review': { bg: COLORS.bgTealLight, color: COLORS.teal }, Matching: { bg: COLORS.bgPinkLight, color: COLORS.red }, 'Profile Update': { bg: COLORS.bgGrayLight, color: COLORS.gray700 }, + 'User Opt Out': { bg: 'rgba(200, 200, 200, 0.4)', color: COLORS.gray700 }, }; return typeColors[type] || { bg: COLORS.bgGrayLight, color: COLORS.gray700 }; }; @@ -28,6 +29,7 @@ export const getCategoryColor = (categoryKey: string): string => { secondary_app: COLORS.bgTealLight, matching_requests: COLORS.bgPinkLight, profile_updates: COLORS.bgGrayLight, + user_opt_outs: 'rgba(200, 200, 200, 0.4)', }; return categoryColors[categoryKey] || COLORS.bgGrayLight; }; diff --git a/frontend/src/utils/taskLinkHelpers.ts b/frontend/src/utils/taskLinkHelpers.ts index 8e5045d..83588fa 100644 --- a/frontend/src/utils/taskLinkHelpers.ts +++ b/frontend/src/utils/taskLinkHelpers.ts @@ -20,6 +20,8 @@ export const getParticipantLink = (task: Task): string => { return `${baseUrl}?tab=profile`; } else if (task.type === 'Intake Form Review' || task.type === 'Ranking / Secondary App Review') { return `${baseUrl}?tab=forms`; + } else if (task.type === 'User Opt Out') { + return baseUrl; } else { // Default: profile tab return baseUrl;