Skip to content

Commit a49a25c

Browse files
committed
feat: Add project selection to task management and enhance API integration
1 parent cdc1946 commit a49a25c

5 files changed

Lines changed: 80 additions & 5 deletions

File tree

src/components/apis/projects.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export const projectsApi = {
3737
return response.data;
3838
},
3939

40+
// Dedicated endpoint for UI selects; ensures we always hit the production host
41+
getProjectsForSelect: async (): Promise<Project[]> => {
42+
const response = await axiosInstance.get<Project[]>('https://flowboard-backend.azurewebsites.net/api/projects');
43+
return response.data;
44+
},
45+
4046
getProjectById: async (projectId: string): Promise<Project> => {
4147
const response = await axiosInstance.get<Project>(`/api/projects/${projectId}`);
4248
return response.data;

src/components/apis/tasks.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import axiosInstance from './axiosInstance';
33
export interface CreateTaskData {
44
category?: string; // API uses 'category'
55
categoryId?: string; // Keep for backward compatibility
6+
projectId?: string; // Add projectId for tasks
67
assignedTo: string;
78
title: string;
89
description: string;
@@ -16,6 +17,7 @@ export interface CreateTaskData {
1617

1718
export interface TaskResponse {
1819
_id: string;
20+
projectId?: string;
1921
category?: string; // API uses 'category' instead of 'categoryId'
2022
categoryId?: string; // Keep for backward compatibility
2123
assignedTo: string;
@@ -40,6 +42,7 @@ export const tasksApi = {
4042
// Transform the data to match API expectations
4143
const payload = {
4244
...taskData,
45+
projectId: taskData.projectId || undefined,
4346
// Use 'category' for API, fallback to categoryId
4447
category: taskData.category || taskData.categoryId,
4548
categoryId: undefined, // Remove categoryId from payload
@@ -72,6 +75,7 @@ export const tasksApi = {
7275
updateTask: async (taskId: string, taskData: Partial<CreateTaskData>): Promise<TaskResponse> => {
7376
const payload = {
7477
...taskData,
78+
projectId: taskData.projectId || undefined,
7579
// Use 'category' for API, fallback to categoryId
7680
category: taskData.category || taskData.categoryId,
7781
categoryId: undefined, // Remove categoryId from payload
@@ -89,6 +93,8 @@ export const tasksApi = {
8993
priority: string;
9094
status: string;
9195
category: string;
96+
categoryId: string;
97+
projectId: string;
9298
startDate: string;
9399
endDate: string;
94100
assignedTo: string;
@@ -97,9 +103,17 @@ export const tasksApi = {
97103
// Only include fields that are provided
98104
if (updates.title !== undefined) payload.title = updates.title;
99105
if (updates.description !== undefined) payload.description = updates.description;
100-
if (updates.priority !== undefined) payload.priority = updates.priority;
101-
if (updates.status !== undefined) payload.status = updates.status.toLowerCase();
102-
if (updates.category !== undefined) payload.category = updates.category;
106+
if (updates.priority !== undefined && updates.priority !== '') payload.priority = updates.priority;
107+
if (updates.status !== undefined && updates.status !== '') payload.status = updates.status.toLowerCase();
108+
if (updates.categoryId !== undefined && updates.categoryId !== '') {
109+
(payload as any).categoryId = updates.categoryId;
110+
payload.category = updates.categoryId; // include non-ID alias too
111+
} else if (updates.category !== undefined && updates.category !== '') {
112+
payload.category = updates.category;
113+
// Include categoryId alias in case backend expects categoryId for partial updates
114+
(payload as any).categoryId = updates.category;
115+
}
116+
if (updates.projectId !== undefined && updates.projectId !== '') payload.projectId = updates.projectId;
103117
if (updates.startDate !== undefined) payload.startDate = updates.startDate;
104118
if (updates.endDate !== undefined) payload.endDate = updates.endDate;
105119
if (updates.assignedTo !== undefined) payload.assignedTo = updates.assignedTo;

src/components/dialogs/TaskDialog.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@fluentui/react-icons';
1313
import { useEffect, useRef, useState, type CSSProperties } from 'react';
1414
import type { User } from '../apis/auth';
15+
import type { Project } from '../apis/projects';
1516
import { mainLayoutStyles } from '../styles/Styles';
1617

1718

@@ -28,13 +29,17 @@ export interface TaskDialogProps {
2829
assignedTo: string;
2930
createdBy: string;
3031
category: string; // Changed from categoryId to match API
32+
projectId?: string | null;
3133
comments?: string;
3234
};
3335
onInputChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
3436
onSubmit: (e: React.FormEvent) => void;
3537
avatars?: Array<{ name: string; image?: string }>;
3638
onAssignClick?: () => void;
3739
assignableUsers?: User[];
40+
projects?: Project[];
41+
isLoadingProjects?: boolean;
42+
projectsError?: string | null;
3843
isLoadingAssignableUsers?: boolean;
3944
assignableUsersError?: string | null;
4045
currentUser?: User | null;
@@ -58,7 +63,7 @@ export interface TaskDialogProps {
5863
taskId?: string;
5964
}
6065
export default function TaskDialog({ open, onOpenChange, form, onInputChange, onSubmit,
61-
onAssignClick, onDeleteClick, isSubmitting = false, submitError, dialogMode = 'add', createdByUser, comments = [], taskId, onAddComment, isAddingComment = false, commentError = null, assignableUsers = [], isLoadingAssignableUsers = false, assignableUsersError = null, currentUser = null }: TaskDialogProps) {
66+
onAssignClick, onDeleteClick, isSubmitting = false, submitError, dialogMode = 'add', createdByUser, comments = [], taskId, onAddComment, isAddingComment = false, commentError = null, assignableUsers = [], isLoadingAssignableUsers = false, assignableUsersError = null, projects = [], isLoadingProjects = false, projectsError = null, currentUser = null }: TaskDialogProps) {
6267
const [editingTitle, setEditingTitle] = useState(false);
6368
const inputRef = useRef<HTMLInputElement>(null);
6469
const [newComment, setNewComment] = useState('');
@@ -288,6 +293,19 @@ export default function TaskDialog({ open, onOpenChange, form, onInputChange, on
288293
</Field>
289294
{/* Row 4: Category, Status, Priority */}
290295
<div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
296+
<Field label="Project" style={{ flex: 1 }}>
297+
<Select name="projectId" value={form.projectId || ''} onChange={onInputChange} disabled={isLoadingProjects || projects.length === 0}>
298+
<option value="">{isLoadingProjects ? 'Loading projects…' : 'Select project'}</option>
299+
{!isLoadingProjects && projects.map(p => (
300+
<option key={p.id} value={p.id}>{p.projectName}</option>
301+
))}
302+
</Select>
303+
{projectsError && (
304+
<span style={{ color: tokens.colorPaletteRedForeground3, fontSize: tokens.fontSizeBase100 }}>
305+
{projectsError}
306+
</span>
307+
)}
308+
</Field>
291309
<Field label="Category" style={{ flex: 1 }}>
292310
<Select name="category" value={form.category} onChange={onInputChange}>
293311
<option value="">Select category</option>

src/pages/user/MyTasks.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { mainLayoutStyles } from '../../components/styles/Styles'
44
import TaskDialog from '../../components/dialogs/TaskDialog'
55
import { useUser } from '../../hooks/useUser'
66
import { tasksApi } from '../../components/apis/tasks'
7+
import { projectsApi, type Project } from '../../components/apis/projects'
78
import { usersApi } from '../../components/apis/users'
89
import type { User } from '../../components/apis/auth'
910
import type { Task } from '../../types/MyTasksTypes'
1011
import type { CreateTaskData } from '../../components/apis/tasks'
11-
import { useState, useRef } from 'react'
12+
import { useState, useRef, useEffect } from 'react'
1213
import type { ChangeEvent, FormEvent } from 'react'
1314

1415

@@ -34,8 +35,12 @@ export default function MyTasks() {
3435
assignedTo: user?.id || '',
3536
createdBy: user?.id || '',
3637
category: '',
38+
projectId: '',
3739
comments: '',
3840
});
41+
const [projects, setProjects] = useState<Project[]>([]);
42+
const [isLoadingProjects, setIsLoadingProjects] = useState(false);
43+
const [projectsError, setProjectsError] = useState<string | null>(null);
3944
const [isSubmitting, setIsSubmitting] = useState(false);
4045
const [submitError, setSubmitError] = useState<string | null>(null);
4146
const [assignableUsers, setAssignableUsers] = useState<User[]>([]);
@@ -57,11 +62,13 @@ export default function MyTasks() {
5762
assignedTo: user?.id || '',
5863
createdBy: user?.id || '',
5964
category: '',
65+
projectId: '',
6066
comments: '',
6167
});
6268
setOpen(true);
6369
setSelectedTask(null);
6470
fetchAssignableUsers(undefined);
71+
fetchProjects();
6572
}
6673

6774
// Called when DataGrid row is clicked
@@ -79,10 +86,12 @@ export default function MyTasks() {
7986
assignedTo: task.assignedTo || '',
8087
createdBy: task.createdBy || '',
8188
category: task.category || task.categoryId || '',
89+
projectId: (task as any).projectId || '',
8290
comments: '',
8391
});
8492
setOpen(true);
8593
fetchAssignableUsers(task.assignedTo);
94+
fetchProjects();
8695
}
8796

8897
async function fetchAssignableUsers(assignedToId?: string) {
@@ -110,6 +119,9 @@ export default function MyTasks() {
110119
async function handleFieldUpdate(fieldName: string, value: string) {
111120
if (!editingTaskId) return;
112121

122+
// If the new value is empty, skip calling the backend to avoid 'No valid updatable fields provided.'
123+
if (value === '') return;
124+
113125
try {
114126
await tasksApi.patchTask(editingTaskId, { [fieldName]: value } as Partial<CreateTaskData>);
115127
// refresh data grid
@@ -144,6 +156,7 @@ export default function MyTasks() {
144156
try {
145157
const taskData = {
146158
category: form.category,
159+
projectId: form.projectId,
147160
assignedTo: form.assignedTo || user?.id || '',
148161
title: form.title,
149162
description: form.description,
@@ -172,6 +185,7 @@ export default function MyTasks() {
172185
try {
173186
await tasksApi.updateTask(editingTaskId, {
174187
category: form.category,
188+
projectId: form.projectId,
175189
assignedTo: form.assignedTo,
176190
title: form.title,
177191
description: form.description,
@@ -193,6 +207,25 @@ export default function MyTasks() {
193207
}
194208
}
195209

210+
async function fetchProjects() {
211+
setIsLoadingProjects(true);
212+
setProjectsError(null);
213+
try {
214+
const fetched = await projectsApi.getProjectsForSelect();
215+
const normalized = fetched.map(p => ({ ...(p as any), id: (p as any).id || (p as any)._id }));
216+
setProjects(normalized as Project[]);
217+
} catch (err: unknown) {
218+
setProjectsError(err instanceof Error ? err.message : 'Unable to load projects.');
219+
} finally {
220+
setIsLoadingProjects(false);
221+
}
222+
}
223+
224+
// Preload projects on mount for faster dropdown display
225+
useEffect(() => {
226+
fetchProjects();
227+
}, [user?.id]);
228+
196229
async function handleDeleteTask() {
197230
if (!editingTaskId) return;
198231
const confirmDelete = window.confirm('Are you sure you want to delete this task?');
@@ -263,6 +296,9 @@ export default function MyTasks() {
263296
isLoadingAssignableUsers={isLoadingAssignableUsers}
264297
assignableUsersError={assignableUsersError}
265298
currentUser={user}
299+
projects={projects}
300+
isLoadingProjects={isLoadingProjects}
301+
projectsError={projectsError}
266302
/>
267303
</>
268304
)

src/types/MyTasksTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { User } from '../components/apis/auth';
22

33
export type Task = {
44
_id: string;
5+
projectId?: string;
56
category?: string; // API uses 'category' instead of 'categoryId'
67
categoryId?: string; // Keep for backward compatibility
78
assignedTo: string;

0 commit comments

Comments
 (0)