mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-07-10 23:27:33 +00:00
feat: add find task by id endpoint
This commit is contained in:
@ -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 { 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 { LoginForm } from './components/auth/LoginForm';
|
||||||
import { RegisterForm } from './components/auth/RegisterForm';
|
import { RegisterForm } from './components/auth/RegisterForm';
|
||||||
import { AuthLayout } from './components/layout/AuthLayout';
|
|
||||||
import { Dashboard } from './components/dashboard/Dashboard';
|
import { Dashboard } from './components/dashboard/Dashboard';
|
||||||
import { JuniorsList } from './components/juniors/JuniorsList';
|
|
||||||
import { AddJuniorForm } from './components/juniors/AddJuniorForm';
|
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 { TasksList } from './components/tasks/TasksList';
|
||||||
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
|
|
||||||
// Create theme
|
// Create theme
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
@ -110,13 +111,12 @@ function App() {
|
|||||||
<Route path="/juniors" element={<JuniorsList />} />
|
<Route path="/juniors" element={<JuniorsList />} />
|
||||||
<Route path="/juniors/new" element={<AddJuniorForm />} />
|
<Route path="/juniors/new" element={<AddJuniorForm />} />
|
||||||
<Route path="/tasks" element={<TasksList />} />
|
<Route path="/tasks" element={<TasksList />} />
|
||||||
|
<Route path="/tasks/new" element={<AddTaskForm />} />
|
||||||
|
<Route path="/tasks/:taskId" element={<TaskDetails />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Redirect root to dashboard or login */}
|
{/* Redirect root to dashboard or login */}
|
||||||
<Route
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
path="/"
|
|
||||||
element={<Navigate to="/dashboard" replace />}
|
|
||||||
/>
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
@ -15,34 +15,32 @@ export const apiClient = axios.create({
|
|||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-client-id': 'web-client'
|
'x-client-id': 'web-client',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add request interceptor to include current auth header
|
// Add request interceptor to include current auth header
|
||||||
apiClient.interceptors.request.use(config => {
|
apiClient.interceptors.request.use((config) => {
|
||||||
config.headers.Authorization = getAuthHeader();
|
config.headers.Authorization = getAuthHeader();
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add response interceptor to handle errors
|
// Add response interceptor to handle errors
|
||||||
apiClient.interceptors.response.use(
|
apiClient.interceptors.response.use(
|
||||||
response => response,
|
(response) => response,
|
||||||
error => {
|
(error) => {
|
||||||
const errorMessage = error.response?.data?.message ||
|
const errorMessage =
|
||||||
error.response?.data?.error ||
|
error.response?.data?.message || error.response?.data?.error || error.message || 'An unexpected error occurred';
|
||||||
error.message ||
|
|
||||||
'An unexpected error occurred';
|
|
||||||
|
|
||||||
console.error('API Error:', {
|
console.error('API Error:', {
|
||||||
status: error.response?.status,
|
status: error.response?.status,
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
data: error.response?.data
|
data: error.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Throw error with meaningful message
|
// Throw error with meaningful message
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auth API
|
// Auth API
|
||||||
@ -52,13 +50,13 @@ export const authApi = {
|
|||||||
const cleanPhoneNumber = phoneNumber.replace(/\D/g, '');
|
const cleanPhoneNumber = phoneNumber.replace(/\D/g, '');
|
||||||
return apiClient.post('/api/auth/register/otp', {
|
return apiClient.post('/api/auth/register/otp', {
|
||||||
countryCode: countryCode.startsWith('+') ? countryCode : `+${countryCode}`,
|
countryCode: countryCode.startsWith('+') ? countryCode : `+${countryCode}`,
|
||||||
phoneNumber: cleanPhoneNumber
|
phoneNumber: cleanPhoneNumber,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
verifyOtp: (countryCode: string, phoneNumber: string, otp: string) =>
|
verifyOtp: (countryCode: string, phoneNumber: string, otp: string) =>
|
||||||
apiClient.post('/api/auth/register/verify', { countryCode, phoneNumber, otp }),
|
apiClient.post('/api/auth/register/verify', { countryCode, phoneNumber, otp }),
|
||||||
|
|
||||||
setEmail: (email: string) => {
|
setEmail: (email: string) => {
|
||||||
// Use the stored token from localStorage
|
// Use the stored token from localStorage
|
||||||
const storedToken = localStorage.getItem('accessToken');
|
const storedToken = localStorage.getItem('accessToken');
|
||||||
@ -67,7 +65,7 @@ export const authApi = {
|
|||||||
}
|
}
|
||||||
return apiClient.post('/api/auth/register/set-email', { email });
|
return apiClient.post('/api/auth/register/set-email', { email });
|
||||||
},
|
},
|
||||||
|
|
||||||
setPasscode: (passcode: string) => {
|
setPasscode: (passcode: string) => {
|
||||||
// Use the stored token from localStorage
|
// Use the stored token from localStorage
|
||||||
const storedToken = localStorage.getItem('accessToken');
|
const storedToken = localStorage.getItem('accessToken');
|
||||||
@ -83,29 +81,23 @@ export const authApi = {
|
|||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
fcmToken: 'web-client-token', // Required by API
|
fcmToken: 'web-client-token', // Required by API
|
||||||
signature: 'web-login' // Required by API
|
signature: 'web-login', // Required by API
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Juniors API
|
// Juniors API
|
||||||
export const juniorsApi = {
|
export const juniorsApi = {
|
||||||
createJunior: (data: CreateJuniorRequest) =>
|
createJunior: (data: CreateJuniorRequest) => apiClient.post('/api/juniors', data),
|
||||||
apiClient.post('/api/juniors', data),
|
|
||||||
|
|
||||||
getJuniors: (page = 1, size = 10) =>
|
getJuniors: (page = 1, size = 10) => apiClient.get(`/api/juniors?page=${page}&size=${size}`),
|
||||||
apiClient.get(`/api/juniors?page=${page}&size=${size}`),
|
|
||||||
|
|
||||||
getJunior: (juniorId: string) =>
|
getJunior: (juniorId: string) => apiClient.get(`/api/juniors/${juniorId}`),
|
||||||
apiClient.get(`/api/juniors/${juniorId}`),
|
|
||||||
|
|
||||||
setTheme: (data: JuniorTheme) =>
|
setTheme: (data: JuniorTheme) => apiClient.post('/api/juniors/set-theme', data),
|
||||||
apiClient.post('/api/juniors/set-theme', data),
|
|
||||||
|
|
||||||
getQrCode: (juniorId: string) =>
|
getQrCode: (juniorId: string) => apiClient.get(`/api/juniors/${juniorId}/qr-code`),
|
||||||
apiClient.get(`/api/juniors/${juniorId}/qr-code`),
|
|
||||||
|
|
||||||
validateQrCode: (token: string) =>
|
validateQrCode: (token: string) => apiClient.get(`/api/juniors/qr-code/${token}/validate`),
|
||||||
apiClient.get(`/api/juniors/qr-code/${token}/validate`)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Document API
|
// Document API
|
||||||
@ -116,16 +108,15 @@ export const documentApi = {
|
|||||||
formData.append('documentType', documentType);
|
formData.append('documentType', documentType);
|
||||||
return apiClient.post('/api/document', formData, {
|
return apiClient.post('/api/document', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tasks API
|
// Tasks API
|
||||||
export const tasksApi = {
|
export const tasksApi = {
|
||||||
createTask: (data: CreateTaskRequest) =>
|
createTask: (data: CreateTaskRequest) => apiClient.post('/api/tasks', data),
|
||||||
apiClient.post('/api/tasks', data),
|
|
||||||
|
|
||||||
getTasks: (status: TaskStatus, page = 1, size = 10, juniorId?: string) => {
|
getTasks: (status: TaskStatus, page = 1, size = 10, juniorId?: string) => {
|
||||||
const url = new URL('/api/tasks', API_BASE_URL);
|
const url = new URL('/api/tasks', API_BASE_URL);
|
||||||
@ -136,12 +127,11 @@ export const tasksApi = {
|
|||||||
return apiClient.get(url.pathname + url.search);
|
return apiClient.get(url.pathname + url.search);
|
||||||
},
|
},
|
||||||
|
|
||||||
submitTask: (taskId: string, data: TaskSubmission) =>
|
getTaskById: (taskId: string) => apiClient.get(`/api/tasks/${taskId}`),
|
||||||
apiClient.patch(`/api/tasks/${taskId}/submit`, data),
|
|
||||||
|
|
||||||
approveTask: (taskId: string) =>
|
submitTask: (taskId: string, data: TaskSubmission) => apiClient.patch(`/api/tasks/${taskId}/submit`, data),
|
||||||
apiClient.patch(`/api/tasks/${taskId}/approve`),
|
|
||||||
|
|
||||||
rejectTask: (taskId: string) =>
|
approveTask: (taskId: string) => apiClient.patch(`/api/tasks/${taskId}/approve`),
|
||||||
apiClient.patch(`/api/tasks/${taskId}/reject`)
|
|
||||||
|
rejectTask: (taskId: string) => apiClient.patch(`/api/tasks/${taskId}/reject`),
|
||||||
};
|
};
|
||||||
|
245
client/src/components/tasks/AddTask.tsx
Normal file
245
client/src/components/tasks/AddTask.tsx
Normal file
@ -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<CreateTaskRequest>({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
dueDate: '',
|
||||||
|
rewardAmount: 0,
|
||||||
|
isProofRequired: false,
|
||||||
|
juniorId: '',
|
||||||
|
imageId: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [juniors, setJuniors] = useState<Junior[]>([]);
|
||||||
|
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
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<Junior>;
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
isProofRequired: e.target.checked,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box p={3}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Add New Task
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Paper sx={{ p: 3, maxWidth: 600, mx: 'auto' }}>
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ mb: 3, whiteSpace: 'pre-line' }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box component="form" onSubmit={handleSubmit}>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={12} sm={12}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Title"
|
||||||
|
name="title"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Task Title"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={12}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Description"
|
||||||
|
name="description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Task Description"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={12}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Due Date"
|
||||||
|
name="dueDate"
|
||||||
|
type="date"
|
||||||
|
value={formData.dueDate}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={12}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Reward Amount"
|
||||||
|
name="rewardAmount"
|
||||||
|
type="number"
|
||||||
|
value={formData.rewardAmount}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Junior</InputLabel>
|
||||||
|
<Select name="juniorId" value={formData.juniorId} label="Junior" onChange={handleSelectChange}>
|
||||||
|
<MenuItem value="">Select Junior</MenuItem>
|
||||||
|
{juniors.map((junior) => (
|
||||||
|
<MenuItem key={junior.id} value={junior.id}>
|
||||||
|
{junior.fullName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={12}>
|
||||||
|
<DocumentUpload
|
||||||
|
documentType={DocumentType.PASSPORT}
|
||||||
|
label="Upload Task Image"
|
||||||
|
onUploadSuccess={handleTaskImageUpload}
|
||||||
|
/>
|
||||||
|
{formData.imageId && (
|
||||||
|
<Typography variant="caption" color="success.main" sx={{ mt: 1, display: 'block' }}>
|
||||||
|
Task Image uploaded (ID: {formData.imageId})
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox checked={formData.isProofRequired} onChange={handleCheckedInputChange} color="primary" />
|
||||||
|
}
|
||||||
|
label="Proof Required"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', gap: 2, justifyContent: 'flex-end' }}>
|
||||||
|
<Button variant="outlined" onClick={() => navigate('/juniors')}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="contained" disabled={loading}>
|
||||||
|
{loading ? 'Adding...' : 'Add Task'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
87
client/src/components/tasks/TaskDetails.tsx
Normal file
87
client/src/components/tasks/TaskDetails.tsx
Normal file
@ -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<Task>();
|
||||||
|
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 (
|
||||||
|
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Box p={3}>
|
||||||
|
<Typography color="error">{error}</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
return (
|
||||||
|
<Box p={3}>
|
||||||
|
<Typography color="error">Task not found</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(task);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{task.title}
|
||||||
|
</Typography>
|
||||||
|
<Chip label={task.status} color={statusColors[task.status]} size="small" />
|
||||||
|
</Box>
|
||||||
|
<Typography color="textSecondary" gutterBottom>
|
||||||
|
Due: {new Date(task.dueDate).toLocaleDateString()}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
{task.description}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="primary" gutterBottom>
|
||||||
|
Reward: ${task.rewardAmount}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Assigned to: {task.junior.fullName}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
@ -1,24 +1,24 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import {
|
||||||
import {
|
Box,
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Grid,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
Button,
|
Button,
|
||||||
CircularProgress,
|
Card,
|
||||||
Pagination,
|
CardContent,
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
Select,
|
|
||||||
MenuItem,
|
|
||||||
Chip,
|
Chip,
|
||||||
SelectChangeEvent
|
CircularProgress,
|
||||||
|
FormControl,
|
||||||
|
Grid,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Pagination,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
Typography
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { tasksApi, juniorsApi } from '../../api/client';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Task, TaskStatus } from '../../types/task';
|
|
||||||
import { Junior, PaginatedResponse } from '../../types/junior';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 = {
|
const statusColors = {
|
||||||
PENDING: 'warning',
|
PENDING: 'warning',
|
||||||
@ -39,7 +39,7 @@ export const TasksList = () => {
|
|||||||
|
|
||||||
const fetchJuniors = async () => {
|
const fetchJuniors = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await juniorsApi.getJuniors(1, 100);
|
const response = await juniorsApi.getJuniors(1, 50);
|
||||||
const data = response.data as PaginatedResponse<Junior>;
|
const data = response.data as PaginatedResponse<Junior>;
|
||||||
setJuniors(data.data);
|
setJuniors(data.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -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')
|
@Patch(':taskId/submit')
|
||||||
@UseGuards(RolesGuard)
|
@UseGuards(RolesGuard)
|
||||||
@AllowedRoles(Roles.JUNIOR)
|
@AllowedRoles(Roles.JUNIOR)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { FindOptionsWhere } from 'typeorm';
|
import { FindOptionsWhere } from 'typeorm';
|
||||||
|
import { Roles } from '~/auth/enums';
|
||||||
import { IJwtPayload } from '~/auth/interfaces';
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
import { DocumentService, OciService } from '~/document/services';
|
import { DocumentService, OciService } from '~/document/services';
|
||||||
import { CreateTaskRequestDto, TasksFilterOptions, TaskSubmissionRequestDto } from '../dtos/request';
|
import { CreateTaskRequestDto, TasksFilterOptions, TaskSubmissionRequestDto } from '../dtos/request';
|
||||||
@ -35,6 +36,16 @@ export class TaskService {
|
|||||||
return this.findTask({ id: task.id });
|
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<Task>) {
|
async findTask(where: FindOptionsWhere<Task>) {
|
||||||
this.logger.log(`Finding task with where ${JSON.stringify(where)}`);
|
this.logger.log(`Finding task with where ${JSON.stringify(where)}`);
|
||||||
const task = await this.taskRepository.findTask(where);
|
const task = await this.taskRepository.findTask(where);
|
||||||
|
Reference in New Issue
Block a user