Skip to content

Commit

Permalink
feat: add certificates countries widget (#2498)
Browse files Browse the repository at this point in the history
  • Loading branch information
valerydluski authored Jun 28, 2024
1 parent ba2dccc commit 716d2ed
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 1 deletion.
63 changes: 63 additions & 0 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8337,6 +8337,39 @@ 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 {*} [options] Override http request option.
* @throws {RequiredError}
*/
getCourseStudentCertificatesCountries: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'courseId' is not null or undefined
assertParamExists('getCourseStudentCertificatesCountries', 'courseId', courseId)
const localVarPath = `/courses/{courseId}/stats/students/certificates/countries`
.replace(`{${"courseId"}}`, encodeURIComponent(String(courseId)));
// 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 @@ -8456,6 +8489,16 @@ export const CourseStatsApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseStats(courseId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {number} courseId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getCourseStudentCertificatesCountries(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CountriesStatsDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseStudentCertificatesCountries(courseId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {number} courseId
Expand Down Expand Up @@ -8514,6 +8557,15 @@ export const CourseStatsApiFactory = function (configuration?: Configuration, ba
getCourseStats(courseId: number, options?: any): AxiosPromise<CourseStatsDto> {
return localVarFp.getCourseStats(courseId, options).then((request) => request(axios, basePath));
},
/**
*
* @param {number} courseId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getCourseStudentCertificatesCountries(courseId: number, options?: any): AxiosPromise<CountriesStatsDto> {
return localVarFp.getCourseStudentCertificatesCountries(courseId, options).then((request) => request(axios, basePath));
},
/**
*
* @param {number} courseId
Expand Down Expand Up @@ -8576,6 +8628,17 @@ export class CourseStatsApi extends BaseAPI {
return CourseStatsApiFp(this.configuration).getCourseStats(courseId, options).then((request) => request(this.axios, this.basePath));
}

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

/**
*
* @param {number} courseId
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Card } from 'antd';
import { CountriesStatsDto } from 'api';
import { Colors } from 'modules/CourseStatistics/data';
import dynamic from 'next/dynamic';

type Props = {
studentsCertificatesCountriesStats: CountriesStatsDto;
certificatesCount: number;
};

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

export const StudentsCertificatesCountriesCard = ({ studentsCertificatesCountriesStats, certificatesCount }: Props) => {
const { countries } = studentsCertificatesCountriesStats;
return (
<Card title="Certificates Countries">
<div style={{ height: 350, width: '100%' }}>
<CountriesChart
data={countries}
activeCount={certificatesCount}
xAxisTitle={'Number of Certificates'}
color={Colors.Lime}
/>
</div>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { StudentsCertificatesCountriesCard } from './StudentsCertificatesCountriesCard';
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,29 @@ const coursesTasksApi = new CoursesTasksApi();
export function useCourseStats(courseId: number) {
return useAsync(async () => {
try {
const [studentsCountries, studentsStats, mentorsCountries, mentorsStats, courseTasks] = await Promise.all([
const [
studentsCountries,
studentsStats,
mentorsCountries,
mentorsStats,
courseTasks,
studentsCertificatesCountries,
] = await Promise.all([
courseStatsApi.getCourseStudentCountries(courseId),
courseStatsApi.getCourseStats(courseId),
courseStatsApi.getCourseMentorCountries(courseId),
courseStatsApi.getCourseMentors(courseId),
coursesTasksApi.getCourseTasks(courseId),
courseStatsApi.getCourseStudentCertificatesCountries(courseId),
]);

return {
studentsCountries: studentsCountries.data,
studentsStats: studentsStats.data,
mentorsCountries: mentorsCountries.data,
mentorsStats: mentorsStats.data,
courseTasks: courseTasks.data,
studentsCertificatesCountries: studentsCertificatesCountries.data,
};
} catch (error) {
message.error('Something went wrong, please try to reload the page later');
Expand Down
11 changes: 11 additions & 0 deletions client/src/modules/CourseStatistics/pages/CourseStatistics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { StudentsWithMentorsCard } from '../components/StudentsWithMentorsCard';
import { StudentsWithCertificateCard } from '../components/StudentsWithCertificateCard';
import { StudentsEligibleForCertificationCard } from '../components/StudentsEligibleForCertificationCard';
import { TaskPerformanceCard } from '../components/TaskPerformanceCard';
import { StudentsCertificatesCountriesCard } from '../components/StudentsCertificatesCountriesCard';

const gapSize = 24;

Expand Down Expand Up @@ -70,6 +71,16 @@ function CourseStatistic() {
title: 'taskPerformanceCard',
component: <TaskPerformanceCard tasks={stats.courseTasks} />,
},
stats?.studentsCertificatesCountries &&
stats.studentsStats.certifiedStudentsCount && {
title: 'studentsCertificatesCountriesCard',
component: (
<StudentsCertificatesCountriesCard
studentsCertificatesCountriesStats={stats.studentsCertificatesCountries}
certificatesCount={stats.studentsStats.certifiedStudentsCount}
/>
),
},
].filter(Boolean);

return (
Expand Down
14 changes: 14 additions & 0 deletions nestjs/src/courses/stats/course-stats.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ export class CourseStatsController {
return data;
}

@Get('/students/certificates/countries')
@CacheTTL(ONE_HOUR_CACHE_TTL)
@UseInterceptors(CacheInterceptor)
@UseGuards(DefaultGuard, CourseGuard)
@ApiOperation({ operationId: 'getCourseStudentCertificatesCountries' })
@ApiOkResponse({ type: CountriesStatsDto })
@ApiBadRequestResponse()
public async getStudentsWithCertificatesCountries(
@Param('courseId', ParseIntPipe) courseId: number,
): Promise<CountriesStatsDto> {
const data = await this.courseStatsService.getStudentsWithCertificatesCountries(courseId);
return data;
}

@Get('/task/:taskId/performance')
@CacheTTL(ONE_HOUR_CACHE_TTL)
@UseInterceptors(CacheInterceptor)
Expand Down
21 changes: 21 additions & 0 deletions nestjs/src/courses/stats/course-stats.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,27 @@ export class CourseStatsService {
return this.getCountries(courseId, this.studentRepository);
}

public async getStudentsWithCertificatesCountries(courseId: number): Promise<{ countries: CountryStatDto[] }> {
const countries = await this.studentRepository
.createQueryBuilder('student')
.leftJoin('student.user', 'user')
.leftJoin(Certificate, 'certificate', 'certificate.studentId = student.id')
.select('user.countryName', 'countryName')
.addSelect('COUNT(DISTINCT student.id)', 'count')
.where('student.courseId = :courseId', { courseId })
.andWhere('certificate.publicId IS NOT NULL')
.groupBy('user.countryName')
.orderBy('COUNT(DISTINCT student.id)', 'DESC')
.getRawMany<CountryStatDto>();

return {
countries: countries.map(country => ({
countryName: country.countryName,
count: Number(country.count),
})),
};
}

private async getCountries(
courseId: number,
repository: Repository<Mentor | Student>,
Expand Down
15 changes: 15 additions & 0 deletions nestjs/src/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,21 @@
"tags": ["course stats"]
}
},
"/courses/{courseId}/stats/students/certificates/countries": {
"get": {
"operationId": "getCourseStudentCertificatesCountries",
"summary": "",
"parameters": [{ "name": "courseId", "required": true, "in": "path", "schema": { "type": "number" } }],
"responses": {
"200": {
"description": "",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/CountriesStatsDto" } } }
},
"400": { "description": "" }
},
"tags": ["course stats"]
}
},
"/courses/{courseId}/stats/task/{taskId}/performance": {
"get": {
"operationId": "getTaskPerformance",
Expand Down

0 comments on commit 716d2ed

Please sign in to comment.