diff --git a/client/src/App.tsx b/client/src/App.tsx
index 515bc81..5894b95 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,14 +1,15 @@
-import React from 'react';
-import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { CssBaseline, ThemeProvider, createTheme } from '@mui/material';
-import { AuthProvider } from './contexts/AuthContext';
+import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { LoginForm } from './components/auth/LoginForm';
import { RegisterForm } from './components/auth/RegisterForm';
-import { AuthLayout } from './components/layout/AuthLayout';
import { Dashboard } from './components/dashboard/Dashboard';
-import { JuniorsList } from './components/juniors/JuniorsList';
import { AddJuniorForm } from './components/juniors/AddJuniorForm';
+import { JuniorsList } from './components/juniors/JuniorsList';
+import { AuthLayout } from './components/layout/AuthLayout';
+import { AddTaskForm } from './components/tasks/AddTask';
+import { TaskDetails } from './components/tasks/TaskDetails';
import { TasksList } from './components/tasks/TasksList';
+import { AuthProvider } from './contexts/AuthContext';
// Create theme
const theme = createTheme({
@@ -110,13 +111,12 @@ function App() {
} />
} />
} />
+ } />
+ } />
{/* Redirect root to dashboard or login */}
- }
- />
+ } />
diff --git a/client/src/api/client.ts b/client/src/api/client.ts
index e9f7409..3e52f27 100644
--- a/client/src/api/client.ts
+++ b/client/src/api/client.ts
@@ -15,34 +15,32 @@ export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
- 'x-client-id': 'web-client'
- }
+ 'x-client-id': 'web-client',
+ },
});
// Add request interceptor to include current auth header
-apiClient.interceptors.request.use(config => {
+apiClient.interceptors.request.use((config) => {
config.headers.Authorization = getAuthHeader();
return config;
});
// Add response interceptor to handle errors
apiClient.interceptors.response.use(
- response => response,
- error => {
- const errorMessage = error.response?.data?.message ||
- error.response?.data?.error ||
- error.message ||
- 'An unexpected error occurred';
+ (response) => response,
+ (error) => {
+ const errorMessage =
+ error.response?.data?.message || error.response?.data?.error || error.message || 'An unexpected error occurred';
console.error('API Error:', {
status: error.response?.status,
message: errorMessage,
- data: error.response?.data
+ data: error.response?.data,
});
// Throw error with meaningful message
throw new Error(errorMessage);
- }
+ },
);
// Auth API
@@ -52,13 +50,13 @@ export const authApi = {
const cleanPhoneNumber = phoneNumber.replace(/\D/g, '');
return apiClient.post('/api/auth/register/otp', {
countryCode: countryCode.startsWith('+') ? countryCode : `+${countryCode}`,
- phoneNumber: cleanPhoneNumber
+ phoneNumber: cleanPhoneNumber,
});
},
verifyOtp: (countryCode: string, phoneNumber: string, otp: string) =>
apiClient.post('/api/auth/register/verify', { countryCode, phoneNumber, otp }),
-
+
setEmail: (email: string) => {
// Use the stored token from localStorage
const storedToken = localStorage.getItem('accessToken');
@@ -67,7 +65,7 @@ export const authApi = {
}
return apiClient.post('/api/auth/register/set-email', { email });
},
-
+
setPasscode: (passcode: string) => {
// Use the stored token from localStorage
const storedToken = localStorage.getItem('accessToken');
@@ -83,29 +81,23 @@ export const authApi = {
email,
password,
fcmToken: 'web-client-token', // Required by API
- signature: 'web-login' // Required by API
- })
+ signature: 'web-login', // Required by API
+ }),
};
// Juniors API
export const juniorsApi = {
- createJunior: (data: CreateJuniorRequest) =>
- apiClient.post('/api/juniors', data),
+ createJunior: (data: CreateJuniorRequest) => apiClient.post('/api/juniors', data),
- getJuniors: (page = 1, size = 10) =>
- apiClient.get(`/api/juniors?page=${page}&size=${size}`),
+ getJuniors: (page = 1, size = 10) => apiClient.get(`/api/juniors?page=${page}&size=${size}`),
- getJunior: (juniorId: string) =>
- apiClient.get(`/api/juniors/${juniorId}`),
+ getJunior: (juniorId: string) => apiClient.get(`/api/juniors/${juniorId}`),
- setTheme: (data: JuniorTheme) =>
- apiClient.post('/api/juniors/set-theme', data),
+ setTheme: (data: JuniorTheme) => apiClient.post('/api/juniors/set-theme', data),
- getQrCode: (juniorId: string) =>
- apiClient.get(`/api/juniors/${juniorId}/qr-code`),
+ getQrCode: (juniorId: string) => apiClient.get(`/api/juniors/${juniorId}/qr-code`),
- validateQrCode: (token: string) =>
- apiClient.get(`/api/juniors/qr-code/${token}/validate`)
+ validateQrCode: (token: string) => apiClient.get(`/api/juniors/qr-code/${token}/validate`),
};
// Document API
@@ -116,16 +108,15 @@ export const documentApi = {
formData.append('documentType', documentType);
return apiClient.post('/api/document', formData, {
headers: {
- 'Content-Type': 'multipart/form-data'
- }
+ 'Content-Type': 'multipart/form-data',
+ },
});
- }
+ },
};
// Tasks API
export const tasksApi = {
- createTask: (data: CreateTaskRequest) =>
- apiClient.post('/api/tasks', data),
+ createTask: (data: CreateTaskRequest) => apiClient.post('/api/tasks', data),
getTasks: (status: TaskStatus, page = 1, size = 10, juniorId?: string) => {
const url = new URL('/api/tasks', API_BASE_URL);
@@ -136,12 +127,11 @@ export const tasksApi = {
return apiClient.get(url.pathname + url.search);
},
- submitTask: (taskId: string, data: TaskSubmission) =>
- apiClient.patch(`/api/tasks/${taskId}/submit`, data),
+ getTaskById: (taskId: string) => apiClient.get(`/api/tasks/${taskId}`),
- approveTask: (taskId: string) =>
- apiClient.patch(`/api/tasks/${taskId}/approve`),
+ submitTask: (taskId: string, data: TaskSubmission) => apiClient.patch(`/api/tasks/${taskId}/submit`, data),
- rejectTask: (taskId: string) =>
- apiClient.patch(`/api/tasks/${taskId}/reject`)
+ approveTask: (taskId: string) => apiClient.patch(`/api/tasks/${taskId}/approve`),
+
+ rejectTask: (taskId: string) => apiClient.patch(`/api/tasks/${taskId}/reject`),
};
diff --git a/client/src/components/tasks/AddTask.tsx b/client/src/components/tasks/AddTask.tsx
new file mode 100644
index 0000000..057ff38
--- /dev/null
+++ b/client/src/components/tasks/AddTask.tsx
@@ -0,0 +1,245 @@
+import {
+ Alert,
+ Box,
+ Button,
+ Checkbox,
+ FormControl,
+ FormControlLabel,
+ Grid,
+ InputLabel,
+ MenuItem,
+ Paper,
+ Select,
+ SelectChangeEvent,
+ TextField,
+ Typography,
+} from '@mui/material';
+import { AxiosError } from 'axios';
+import React, { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { juniorsApi, tasksApi } from '../../api/client';
+import { ApiError } from '../../types/api';
+import { DocumentType } from '../../types/document';
+import { Junior, PaginatedResponse } from '../../types/junior';
+import { CreateTaskRequest } from '../../types/task';
+import { DocumentUpload } from '../document/DocumentUpload';
+
+export const AddTaskForm = () => {
+ const navigate = useNavigate();
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [formData, setFormData] = useState({
+ title: '',
+ description: '',
+ dueDate: '',
+ rewardAmount: 0,
+ isProofRequired: false,
+ juniorId: '',
+ imageId: '',
+ });
+
+ const [juniors, setJuniors] = useState([]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ console.log('Form data:', formData);
+
+ setError('');
+ setLoading(true);
+
+ try {
+ if (!formData.imageId) {
+ console.log('Proof is required but no image uploaded');
+ }
+
+ console.log('Submitting data:', formData);
+ const dataToSubmit = {
+ ...formData,
+ rewardAmount: Number(formData.rewardAmount),
+ imageId: formData.imageId,
+ };
+ await tasksApi.createTask(dataToSubmit);
+ navigate('/tasks');
+ } catch (err) {
+ console.error('Create junior error:', err);
+ if (err instanceof AxiosError && err.response?.data) {
+ const apiError = err.response.data as ApiError;
+ const messages = Array.isArray(apiError.message)
+ ? apiError.message.map((m) => `${m.field}: ${m.message}`).join('\n')
+ : apiError.message;
+ setError(messages);
+ } else {
+ setError(err instanceof Error ? err.message : 'Failed to create Task');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ console.log(name, value);
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ const fetchJuniors = async () => {
+ try {
+ const response = await juniorsApi.getJuniors(1, 50);
+ const data = response.data as PaginatedResponse;
+ setJuniors(data.data);
+ } catch (err) {
+ console.error('Failed to load juniors:', err);
+ }
+ };
+
+ const handleSelectChange = (e: SelectChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({
+ ...prev,
+ [name as string]: value,
+ }));
+ };
+
+ useEffect(() => {
+ console.log('Form data updated:', formData);
+ }, [formData]);
+
+ useEffect(() => {
+ fetchJuniors();
+ }, []);
+
+ const handleTaskImageUpload = (documentId: string) => {
+ console.log('task image ID uploaded:', documentId);
+ setFormData((prev) => ({
+ ...prev,
+ imageId: documentId,
+ }));
+ };
+
+ const handleCheckedInputChange = (e: React.ChangeEvent) => {
+ setFormData((prev) => ({
+ ...prev,
+ isProofRequired: e.target.checked,
+ }));
+ };
+
+ return (
+
+
+ Add New Task
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Junior
+
+
+
+
+
+
+ {formData.imageId && (
+
+ Task Image uploaded (ID: {formData.imageId})
+
+ )}
+
+
+
+
+
+ }
+ label="Proof Required"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/components/tasks/TaskDetails.tsx b/client/src/components/tasks/TaskDetails.tsx
new file mode 100644
index 0000000..a6ae7ba
--- /dev/null
+++ b/client/src/components/tasks/TaskDetails.tsx
@@ -0,0 +1,87 @@
+import { Box, Card, CardContent, Chip, CircularProgress, Typography } from '@mui/material';
+import { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { tasksApi } from '../../api/client';
+import { Task } from '../../types/task';
+
+export const TaskDetails = () => {
+ useNavigate();
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const statusColors = {
+ PENDING: 'warning',
+ IN_PROGRESS: 'info',
+ COMPLETED: 'success',
+ } as const;
+
+ const { taskId } = useParams();
+ if (!taskId) {
+ throw new Error('Task ID is required');
+ }
+ const [task, setTask] = useState();
+ const fetchTask = async () => {
+ try {
+ setLoading(true);
+ const response = await tasksApi.getTaskById(taskId);
+ setTask(response.data.data);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to load task');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchTask();
+ }, []);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error}
+
+ );
+ }
+
+ if (!task) {
+ return (
+
+ Task not found
+
+ );
+ }
+ console.log(task);
+
+ return (
+
+
+
+
+ {task.title}
+
+
+
+
+ Due: {new Date(task.dueDate).toLocaleDateString()}
+
+
+ {task.description}
+
+
+ Reward: ${task.rewardAmount}
+
+
+ Assigned to: {task.junior.fullName}
+
+
+
+ );
+};
diff --git a/client/src/components/tasks/TasksList.tsx b/client/src/components/tasks/TasksList.tsx
index 985e3a8..7703498 100644
--- a/client/src/components/tasks/TasksList.tsx
+++ b/client/src/components/tasks/TasksList.tsx
@@ -1,24 +1,24 @@
-import React, { useEffect, useState } from 'react';
-import {
- Box,
- Typography,
- Grid,
- Card,
- CardContent,
+import {
+ Box,
Button,
- CircularProgress,
- Pagination,
- FormControl,
- InputLabel,
- Select,
- MenuItem,
+ Card,
+ CardContent,
Chip,
- SelectChangeEvent
+ CircularProgress,
+ FormControl,
+ Grid,
+ InputLabel,
+ MenuItem,
+ Pagination,
+ Select,
+ SelectChangeEvent,
+ Typography
} from '@mui/material';
-import { tasksApi, juniorsApi } from '../../api/client';
-import { Task, TaskStatus } from '../../types/task';
-import { Junior, PaginatedResponse } from '../../types/junior';
+import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
+import { juniorsApi, tasksApi } from '../../api/client';
+import { Junior, PaginatedResponse } from '../../types/junior';
+import { Task, TaskStatus } from '../../types/task';
const statusColors = {
PENDING: 'warning',
@@ -39,7 +39,7 @@ export const TasksList = () => {
const fetchJuniors = async () => {
try {
- const response = await juniorsApi.getJuniors(1, 100);
+ const response = await juniorsApi.getJuniors(1, 50);
const data = response.data as PaginatedResponse;
setJuniors(data.data);
} catch (err) {
diff --git a/src/task/controllers/task.controller.ts b/src/task/controllers/task.controller.ts
index 715bf3c..81265cb 100644
--- a/src/task/controllers/task.controller.ts
+++ b/src/task/controllers/task.controller.ts
@@ -41,6 +41,13 @@ export class TaskController {
);
}
+ @Get(':taskId')
+ @UseGuards(AccessTokenGuard)
+ async findTask(@Param('taskId', CustomParseUUIDPipe) taskId: string, @AuthenticatedUser() user: IJwtPayload) {
+ const task = await this.taskService.findTaskForUser(taskId, user);
+ return ResponseFactory.data(new TaskResponseDto(task));
+ }
+
@Patch(':taskId/submit')
@UseGuards(RolesGuard)
@AllowedRoles(Roles.JUNIOR)
diff --git a/src/task/services/task.service.ts b/src/task/services/task.service.ts
index 1e57bab..264c47d 100644
--- a/src/task/services/task.service.ts
+++ b/src/task/services/task.service.ts
@@ -1,6 +1,7 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import moment from 'moment';
import { FindOptionsWhere } from 'typeorm';
+import { Roles } from '~/auth/enums';
import { IJwtPayload } from '~/auth/interfaces';
import { DocumentService, OciService } from '~/document/services';
import { CreateTaskRequestDto, TasksFilterOptions, TaskSubmissionRequestDto } from '../dtos/request';
@@ -35,6 +36,16 @@ export class TaskService {
return this.findTask({ id: task.id });
}
+ findTaskForUser(taskId: string, user: IJwtPayload) {
+ this.logger.log(`Finding task ${taskId} for user ${user.sub}`);
+
+ return this.findTask({
+ id: taskId,
+ assignedById: user.roles.includes(Roles.GUARDIAN) ? user.sub : undefined,
+ assignedToId: user.roles.includes(Roles.JUNIOR) ? user.sub : undefined,
+ });
+ }
+
async findTask(where: FindOptionsWhere) {
this.logger.log(`Finding task with where ${JSON.stringify(where)}`);
const task = await this.taskRepository.findTask(where);