Skip to content

Commit

Permalink
feat: add task performance widget (#2461)
Browse files Browse the repository at this point in the history
* feat: add task performance widget

* fix: code review
  • Loading branch information
valerydluski committed Apr 15, 2024
1 parent 6ae534e commit 32f1e2e
Show file tree
Hide file tree
Showing 12 changed files with 432 additions and 4 deletions.
119 changes: 119 additions & 0 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5271,6 +5271,55 @@ export const TaskDtoTypeEnum = {

export type TaskDtoTypeEnum = typeof TaskDtoTypeEnum[keyof typeof TaskDtoTypeEnum];

/**
*
* @export
* @interface TaskPerformanceStatsDto
*/
export interface TaskPerformanceStatsDto {
/**
* Total number of students who submitted the task
* @type {number}
* @memberof TaskPerformanceStatsDto
*/
'totalAchievement': number;
/**
* Number of students scoring between 1% and 20% of the maximum points
* @type {number}
* @memberof TaskPerformanceStatsDto
*/
'minimalAchievement': number;
/**
* Number of students scoring between 21% and 50% of the maximum points
* @type {number}
* @memberof TaskPerformanceStatsDto
*/
'lowAchievement': number;
/**
* Number of students scoring between 51% and 70% of the maximum points
* @type {number}
* @memberof TaskPerformanceStatsDto
*/
'moderateAchievement': number;
/**
* Number of students scoring between 71% and 90% of the maximum points
* @type {number}
* @memberof TaskPerformanceStatsDto
*/
'highAchievement': number;
/**
* Number of students scoring between 91% and 99% of the maximum points
* @type {number}
* @memberof TaskPerformanceStatsDto
*/
'exceptionalAchievement': number;
/**
* Number of students achieving a perfect score of 100%
* @type {number}
* @memberof TaskPerformanceStatsDto
*/
'perfectScores': number;
}
/**
*
* @export
Expand Down Expand Up @@ -8208,6 +8257,43 @@ export const CourseStatsApiAxiosParamCreator = function (configuration?: Configu



setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};

return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {number} courseId
* @param {number} taskId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getTaskPerformance: async (courseId: number, taskId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'courseId' is not null or undefined
assertParamExists('getTaskPerformance', 'courseId', courseId)
// verify required parameter 'taskId' is not null or undefined
assertParamExists('getTaskPerformance', 'taskId', taskId)
const localVarPath = `/courses/{courseId}/stats/task/{taskId}/performance`
.replace(`{${"courseId"}}`, encodeURIComponent(String(courseId)))
.replace(`{${"taskId"}}`, encodeURIComponent(String(taskId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}

const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;



setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
Expand Down Expand Up @@ -8267,6 +8353,17 @@ export const CourseStatsApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseStudentCountries(courseId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {number} courseId
* @param {number} taskId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getTaskPerformance(courseId: number, taskId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TaskPerformanceStatsDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getTaskPerformance(courseId, taskId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};

Expand Down Expand Up @@ -8313,6 +8410,16 @@ export const CourseStatsApiFactory = function (configuration?: Configuration, ba
getCourseStudentCountries(courseId: number, options?: any): AxiosPromise<CountriesStatsDto> {
return localVarFp.getCourseStudentCountries(courseId, options).then((request) => request(axios, basePath));
},
/**
*
* @param {number} courseId
* @param {number} taskId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getTaskPerformance(courseId: number, taskId: number, options?: any): AxiosPromise<TaskPerformanceStatsDto> {
return localVarFp.getTaskPerformance(courseId, taskId, options).then((request) => request(axios, basePath));
},
};
};

Expand Down Expand Up @@ -8366,6 +8473,18 @@ export class CourseStatsApi extends BaseAPI {
public getCourseStudentCountries(courseId: number, options?: AxiosRequestConfig) {
return CourseStatsApiFp(this.configuration).getCourseStudentCountries(courseId, options).then((request) => request(this.axios, this.basePath));
}

/**
*
* @param {number} courseId
* @param {number} taskId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof CourseStatsApi
*/
public getTaskPerformance(courseId: number, taskId: number, options?: AxiosRequestConfig) {
return CourseStatsApiFp(this.configuration).getTaskPerformance(courseId, taskId, options).then((request) => request(this.axios, this.basePath));
}
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Pie, PieConfig } from '@ant-design/plots';

type Props = {
data: {
type: string;
value: number;
}[];
config?: Partial<PieConfig>;
};

const DonutChart = ({ data, config = {} }: Props) => {
const pieConfig: PieConfig = {
...config,
data,
angleField: 'value',
colorField: 'type',
innerRadius: 0.6,
label: {
content: '',
},
legend: {
color: {
title: false,
position: 'right',
rowPadding: 5,
},
},
};
return <Pie {...pieConfig} />;
};

export default DonutChart;
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Datum } from '@antv/g2plot';
import { Card, Flex, Form, Image, Select, Typography } from 'antd';
import { CourseStatsApi, CourseTaskDto, TaskPerformanceStatsDto } from 'api';
import { useActiveCourseContext } from 'modules/Course/contexts';
import dynamic from 'next/dynamic';
import { useState } from 'react';
import { useAsync } from 'react-use';
import { Colors } from '../data';

const courseStatsApi = new CourseStatsApi();

type Props = {
tasks: CourseTaskDto[];
};

const { Text } = Typography;

const DonutChart = dynamic(() => import('../DonutChart/DonutChart'), { ssr: false });

export const TaskPerformanceCard = ({ tasks }: Props) => {
const { course } = useActiveCourseContext();

const [taskId, setTaskId] = useState<number>();

const { value: taskPerformanceStats } = useAsync(async () => {
if (taskId) {
const { data } = await courseStatsApi.getTaskPerformance(course.id, taskId);
return data;
}
}, [taskId]);

return (
<Card title="Task Performance">
<Form.Item name="courseTaskIds">
<Select
placeholder="Select tasks"
showSearch
optionFilterProp="label"
onChange={(value: number) => setTaskId(value)}
options={tasks.map(({ name, id }) => ({
label: name,
value: id,
}))}
/>
</Form.Item>
<div style={{ height: 250, width: '100%' }}>
{taskPerformanceStats?.totalAchievement ? (
<DonutChart data={getChartData(taskPerformanceStats)} config={getChartConfig()} />
) : (
<Flex vertical align="center" justify="center">
<Text>No data available for this task, please select another task.</Text>
<Image preview={false} src="/static/svg/err.svg" alt="Error 404" width={175} height={175} />
</Flex>
)}
</div>
</Card>
);
};

function getChartConfig() {
return {
color: ({ type }: Datum) => {
switch (type) {
case 'Minimal':
return Colors.Volcano;
case 'Low':
return Colors.Orange;
case 'Moderate':
return Colors.Blue;
case 'High':
return Colors.Lime;
case 'Exceptional':
return Colors.Purple;
case 'Perfect':
return Colors.Magenta;
default:
return Colors.Gray;
}
},
};
}

function getChartData(taskPerformanceStats: TaskPerformanceStatsDto) {
return [
{ type: 'Minimal', value: taskPerformanceStats.minimalAchievement },
{ type: 'Low', value: taskPerformanceStats.lowAchievement },
{ type: 'Moderate', value: taskPerformanceStats.moderateAchievement },
{ type: 'High', value: taskPerformanceStats.highAchievement },
{ type: 'Exceptional', value: taskPerformanceStats.exceptionalAchievement },
{ type: 'Perfect', value: taskPerformanceStats.perfectScores },
].filter(({ value }) => value > 0);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TaskPerformanceCard } from './TaskPerformanceCard';
3 changes: 3 additions & 0 deletions client/src/modules/AdminDashboard/components/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ export enum Colors {
Blue = '#1677ff',
Volcano = '#fa541c',
Lime = '#a0d911',
Orange = '#fa8c16',
Gray = '#d9d9d9',
Magenta = '#c41d7f',
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import { message } from 'antd';
import { CourseStatsApi } from 'api';
import { CourseStatsApi, CoursesTasksApi } from 'api';
import { useAsync } from 'react-use';

const courseStatsApi = new CourseStatsApi();
const coursesTasksApi = new CoursesTasksApi();

export function useCourseStats(courseId: number) {
return useAsync(async () => {
try {
const [studentsCountries, studentsStats, mentorsCountries, mentorsStats] = await Promise.all([
const [studentsCountries, studentsStats, mentorsCountries, mentorsStats, courseTasks] = await Promise.all([
courseStatsApi.getCourseStudentCountries(courseId),
courseStatsApi.getCourseStats(courseId),
courseStatsApi.getCourseMentorCountries(courseId),
courseStatsApi.getCourseMentors(courseId),
coursesTasksApi.getCourseTasks(courseId),
]);
return {
studentsCountries: studentsCountries.data,
studentsStats: studentsStats.data,
mentorsCountries: mentorsCountries.data,
mentorsStats: mentorsStats.data,
courseTasks: courseTasks.data,
};
} catch (error) {
message.error('Something went wrong, please try to reload the page later');
Expand Down
5 changes: 5 additions & 0 deletions client/src/modules/AdminDashboard/pages/AdminDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { EpamMentorsStatsCard } from '../components/EpamMentorsStatsCard';
import { StudentsWithMentorsCard } from '../components/StudentsWithMentorsCard';
import { StudentsWithCertificateCard } from '../components/StudentsWithCertificateCard';
import { StudentsEligibleForCertificationCard } from '../components/StudentsEligibleForCertificationCard';
import { TaskPerformanceCard } from '../components/TaskPerformanceCard';

const gapSize = 24;

Expand Down Expand Up @@ -65,6 +66,10 @@ function AdminDashboard() {
title: 'StudentsEligibleForCertificationCard',
component: <StudentsEligibleForCertificationCard studentsStats={stats.studentsStats} />,
},
stats?.courseTasks && {
title: 'taskPerformanceCard',
component: <TaskPerformanceCard tasks={stats.courseTasks} />,
},
].filter(Boolean);

return (
Expand Down
18 changes: 17 additions & 1 deletion nestjs/src/courses/stats/course-stats.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from '..
import { ONE_HOUR_CACHE_TTL } from '../../constants';
import { CourseAccessService } from '../course-access.service';
import { CourseStatsService } from './course-stats.service';
import { CourseStatsDto, CountriesStatsDto, CourseMentorsStatsDto } from './dto';
import { CourseStatsDto, CountriesStatsDto, CourseMentorsStatsDto, TaskPerformanceStatsDto } from './dto';
import { CourseRole } from '@entities/index';

@Controller('courses/:courseId/stats')
Expand Down Expand Up @@ -77,4 +77,20 @@ export class CourseStatsController {
const data = await this.courseStatsService.getStudentCountries(courseId);
return data;
}

@Get('/task/:taskId/performance')
@CacheTTL(ONE_HOUR_CACHE_TTL)
@UseInterceptors(CacheInterceptor)
@UseGuards(RoleGuard)
@ApiOperation({ operationId: 'getTaskPerformance' })
@ApiOkResponse({ type: TaskPerformanceStatsDto })
@ApiBadRequestResponse()
@RequiredRoles([CourseRole.Manager, CourseRole.Supervisor, Role.Admin, CourseRole.Dementor], true)
public async getTaskPerformance(
@Param('courseId', ParseIntPipe) _courseId: number,
@Param('taskId', ParseIntPipe) taskId: number,
) {
const stats = await this.courseStatsService.getTaskPerformance(taskId);
return new TaskPerformanceStatsDto(stats);
}
}
Loading

0 comments on commit 32f1e2e

Please sign in to comment.