diff --git a/client/src/api/api.ts b/client/src/api/api.ts index e61bce1a9..cef2f4e56 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -961,6 +961,49 @@ export interface CountryStatDto { */ 'count': number; } +/** + * + * @export + * @interface CourseAggregateStatsDto + */ +export interface CourseAggregateStatsDto { + /** + * + * @type {CountriesStatsDto} + * @memberof CourseAggregateStatsDto + */ + 'studentsCountries': CountriesStatsDto; + /** + * + * @type {CourseStatsDto} + * @memberof CourseAggregateStatsDto + */ + 'studentsStats': CourseStatsDto; + /** + * + * @type {CountriesStatsDto} + * @memberof CourseAggregateStatsDto + */ + 'mentorsCountries': CountriesStatsDto; + /** + * + * @type {CourseMentorsStatsDto} + * @memberof CourseAggregateStatsDto + */ + 'mentorsStats': CourseMentorsStatsDto; + /** + * + * @type {Array} + * @memberof CourseAggregateStatsDto + */ + 'courseTasks': Array; + /** + * + * @type {CountriesStatsDto} + * @memberof CourseAggregateStatsDto + */ + 'studentsCertificatesCountries': CountriesStatsDto; +} /** * * @export @@ -1476,49 +1519,6 @@ export interface CourseStatsDto { */ 'eligibleForCertificationCount': number; } -/** - * - * @export - * @interface CoursesStatsDto - */ -export interface CoursesStatsDto { - /** - * - * @type {CountriesStatsDto} - * @memberof CoursesStatsDto - */ - 'studentsCountries': CountriesStatsDto; - /** - * - * @type {CourseStatsDto} - * @memberof CourseStatsDto - */ - 'studentsStats': CourseStatsDto; - /** - * - * @type {CountriesStatsDto} - * @memberof CoursesStatsDto - */ - 'mentorsCountries': CountriesStatsDto; - /** - * - * @type {CourseMentorsStatsDto} - * @memberof CoursesStatsDto - */ - 'mentorsStats': CourseMentorsStatsDto; - /** - * - * @type {CourseTaskDto[]} - * @memberof CoursesStatsDto - */ - 'courseTasks': CourseTaskDto[]; - /** - * - * @type {CountriesStatsDto} - * @memberof CoursesStatsDto - */ - 'studentsCertificatesCountries': CountriesStatsDto; -} /** * * @export @@ -10174,47 +10174,6 @@ export const CourseStatsApiAxiosParamCreator = function (configuration?: Configu options: localVarRequestOptions, }; }, - /** - * - * @param {number[]} ids - * @param {number} year - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getCoursesStats: async (ids: number[] = [], year?: number, options: AxiosRequestConfig = {}): Promise => { - const localVarUrlObj = new URL('/courses/aggregate/stats', DUMMY_BASE_URL); - let baseOptions; - - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { - method: 'GET', - ...baseOptions, - ...options - }; - - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - if (ids.length > 0) { - localVarQueryParameter.ids = ids - } - - if (year) { - localVarQueryParameter.year = year; - } - - 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 @@ -10272,6 +10231,49 @@ 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 {Array} ids List of course IDs + * @param {number} year Year for which stats are fetched + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCoursesStats: async (ids: Array, year: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'ids' is not null or undefined + assertParamExists('getCoursesStats', 'ids', ids) + // verify required parameter 'year' is not null or undefined + assertParamExists('getCoursesStats', 'year', year) + const localVarPath = `/courses/aggregate/stats`; + // 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; + + if (ids) { + localVarQueryParameter['ids'] = ids; + } + + if (year !== undefined) { + localVarQueryParameter['year'] = year; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -10358,17 +10360,6 @@ export const CourseStatsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseStats(courseId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, - /** - * - * @param {number[]} ids - * @param {number} year - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getCoursesStats(ids: number[], year: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getCoursesStats( ids, year, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * * @param {number} courseId @@ -10389,6 +10380,17 @@ export const CourseStatsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseStudentCountries(courseId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {Array} ids List of course IDs + * @param {number} year Year for which stats are fetched + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getCoursesStats(ids: Array, year: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getCoursesStats(ids, year, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {number} courseId @@ -10455,6 +10457,16 @@ export const CourseStatsApiFactory = function (configuration?: Configuration, ba getCourseStudentCountries(courseId: number, options?: any): AxiosPromise { return localVarFp.getCourseStudentCountries(courseId, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {Array} ids List of course IDs + * @param {number} year Year for which stats are fetched + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCoursesStats(ids: Array, year: number, options?: any): AxiosPromise { + return localVarFp.getCoursesStats(ids, year, options).then((request) => request(axios, basePath)); + }, /** * * @param {number} courseId @@ -10509,15 +10521,14 @@ export class CourseStatsApi extends BaseAPI { } /** - * - * @param {number[]} ids - * @param {number} year + * + * @param {number} courseId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseStatsApi */ - public getCoursesStats(ids: number[], year: number, options?: AxiosRequestConfig) { - return CourseStatsApiFp(this.configuration).getCoursesStats( ids, year, options).then((request) => request(this.axios, this.basePath)); + public getCourseStudentCertificatesCountries(courseId: number, options?: AxiosRequestConfig) { + return CourseStatsApiFp(this.configuration).getCourseStudentCertificatesCountries(courseId, options).then((request) => request(this.axios, this.basePath)); } /** @@ -10527,19 +10538,20 @@ export class CourseStatsApi extends BaseAPI { * @throws {RequiredError} * @memberof CourseStatsApi */ - public getCourseStudentCertificatesCountries(courseId: number, options?: AxiosRequestConfig) { - return CourseStatsApiFp(this.configuration).getCourseStudentCertificatesCountries(courseId, options).then((request) => request(this.axios, this.basePath)); + public getCourseStudentCountries(courseId: number, options?: AxiosRequestConfig) { + return CourseStatsApiFp(this.configuration).getCourseStudentCountries(courseId, options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {number} courseId + * @param {Array} ids List of course IDs + * @param {number} year Year for which stats are fetched * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof CourseStatsApi */ - public getCourseStudentCountries(courseId: number, options?: AxiosRequestConfig) { - return CourseStatsApiFp(this.configuration).getCourseStudentCountries(courseId, options).then((request) => request(this.axios, this.basePath)); + public getCoursesStats(ids: Array, year: number, options?: AxiosRequestConfig) { + return CourseStatsApiFp(this.configuration).getCoursesStats(ids, year, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/client/src/api/index.ts b/client/src/api/index.ts index 746ee393a..75deac1e9 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -1,5 +1,5 @@ /* tslint:disable */ - +/* eslint-disable */ /** * * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) diff --git a/client/src/modules/CourseStatistics/components/StatCards/StatCards.tsx b/client/src/modules/CourseStatistics/components/StatCards/StatCards.tsx index 88c972a3c..f602d8ba4 100644 --- a/client/src/modules/CourseStatistics/components/StatCards/StatCards.tsx +++ b/client/src/modules/CourseStatistics/components/StatCards/StatCards.tsx @@ -1,4 +1,4 @@ -import { CoursesStatsDto } from '@client/api'; +import { CourseAggregateStatsDto } from '@client/api'; import { StudentsCountriesCard } from '@client/modules/CourseStatistics/components/StudentsCountriesCard'; import { StudentsStatsCard } from '@client/modules/CourseStatistics/components/StudentsStatsCard'; import { MentorsCountriesCard } from '@client/modules/CourseStatistics/components/MentorsCountriesCard/MentorsCountriesCard'; @@ -12,7 +12,7 @@ import Masonry from 'react-masonry-css'; import css from 'styled-jsx/css'; type StatCardsProps = { - coursesData?: CoursesStatsDto; + coursesData?: CourseAggregateStatsDto; }; const gapSize = 24; diff --git a/client/src/modules/CourseStatistics/hooks/useCourseStats.tsx b/client/src/modules/CourseStatistics/hooks/useCourseStats.tsx index 30bf0dca1..a5c34560c 100644 --- a/client/src/modules/CourseStatistics/hooks/useCourseStats.tsx +++ b/client/src/modules/CourseStatistics/hooks/useCourseStats.tsx @@ -1,5 +1,5 @@ import { useMessage } from '@client/hooks'; -import { CoursesStatsDto, CourseStatsApi } from '@client/api'; +import { CourseAggregateStatsDto, CourseStatsApi } from '@client/api'; import { useRequest } from 'ahooks'; const courseStatsApi = new CourseStatsApi(); @@ -9,30 +9,25 @@ type CourseStatsParams = { year?: number; }; -async function fetchCourseStats({ ids = [], year = 0 }: CourseStatsParams): Promise { +async function fetchCourseStats({ ids = [], year = 0 }: CourseStatsParams): Promise { try { const { data } = await courseStatsApi.getCoursesStats(ids, year); return data; - } catch { - console.error("Couldn't get course(s) stats"); + } catch (err) { + console.error("Couldn't get course(s) stats", err); + throw err; } } export function useCoursesStats({ ids, year }: CourseStatsParams) { const { message } = useMessage(); - const service = async () => { - if (!ids?.length && !year) { - return; - } - return fetchCourseStats({ ids, year }); - }; - - const { data, loading } = useRequest(service, { + const { data, loading } = useRequest(() => fetchCourseStats({ ids, year }), { + ready: Boolean((ids && ids.length) || year), refreshDeps: [ids, year], retryCount: 3, onError: () => { - message.error("Can't load courses data. Please try latter."); + message.error("Can't load courses data. Please try later."); }, }); diff --git a/nestjs/src/courses/stats/course-stats.controller.ts b/nestjs/src/courses/stats/course-stats.controller.ts index 04ed250eb..f0de4bdf6 100644 --- a/nestjs/src/courses/stats/course-stats.controller.ts +++ b/nestjs/src/courses/stats/course-stats.controller.ts @@ -11,7 +11,7 @@ import { UseInterceptors, } from '@nestjs/common'; import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager'; -import { ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { CourseGuard, CurrentRequest, DefaultGuard } from '../../auth'; import { ONE_HOUR_CACHE_TTL } from '../../constants'; import { CourseAccessService } from '../course-access.service'; @@ -37,11 +37,23 @@ export class CourseStatsController { @CacheTTL(ONE_HOUR_CACHE_TTL) @UseInterceptors(CacheInterceptor) @ApiOperation({ operationId: 'getCoursesStats' }) + @ApiQuery({ + name: 'ids', + required: true, + type: [Number], + description: 'List of course IDs', + }) + @ApiQuery({ + name: 'year', + required: true, + type: Number, + description: 'Year for which stats are fetched', + }) @ApiOkResponse({ type: CourseAggregateStatsDto }) public async getCoursesStats( @Req() req: CurrentRequest, - @Query('ids', new ParseArrayPipe({ items: Number, optional: true })) ids: number[], - @Query('year', new ParseIntPipe({ optional: true })) year: number, + @Query('ids', new ParseArrayPipe({ items: Number })) ids: number[], + @Query('year', ParseIntPipe) year: number, ) { const allowedCourseIds = await this.courseAccessService.getUserAllowedCourseIds(req.user, ids, year); const data = await this.courseStatsService.getCoursesStats(allowedCourseIds); diff --git a/nestjs/src/courses/stats/dto/course-stats.dto.ts b/nestjs/src/courses/stats/dto/course-stats.dto.ts index db303ee43..1375dbef0 100644 --- a/nestjs/src/courses/stats/dto/course-stats.dto.ts +++ b/nestjs/src/courses/stats/dto/course-stats.dto.ts @@ -63,7 +63,7 @@ export class CourseAggregateStatsDto { @ApiProperty() mentorsStats: CourseMentorsStatsDto; - @ApiProperty() + @ApiProperty({ type: () => [CourseTaskDto] }) courseTasks: CourseTaskDto[]; @ApiProperty() diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json index 902ebc754..0291cb370 100644 --- a/nestjs/src/spec.json +++ b/nestjs/src/spec.json @@ -765,6 +765,35 @@ "tags": ["courses tasks"] } }, + "/courses/aggregate/stats": { + "get": { + "operationId": "getCoursesStats", + "parameters": [ + { + "name": "ids", + "required": true, + "in": "query", + "description": "List of course IDs", + "schema": { "type": "array", "items": { "type": "number" } } + }, + { + "name": "year", + "required": true, + "in": "query", + "description": "Year for which stats are fetched", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CourseAggregateStatsDto" } } } + } + }, + "summary": "", + "tags": ["course stats"] + } + }, "/courses/{courseId}/stats": { "get": { "operationId": "getCourseStats", @@ -3723,6 +3752,16 @@ "properties": { "deadlineInHours": { "type": "number" } }, "required": ["deadlineInHours"] }, + "CountryStatDto": { + "type": "object", + "properties": { "countryName": { "type": "string" }, "count": { "type": "number" } }, + "required": ["countryName", "count"] + }, + "CountriesStatsDto": { + "type": "object", + "properties": { "countries": { "type": "array", "items": { "$ref": "#/components/schemas/CountryStatDto" } } }, + "required": ["countries"] + }, "CourseStatsDto": { "type": "object", "properties": { @@ -3749,15 +3788,24 @@ }, "required": ["mentorsActiveCount", "mentorsTotalCount", "epamMentorsCount"] }, - "CountryStatDto": { + "CourseAggregateStatsDto": { "type": "object", - "properties": { "countryName": { "type": "string" }, "count": { "type": "number" } }, - "required": ["countryName", "count"] - }, - "CountriesStatsDto": { - "type": "object", - "properties": { "countries": { "type": "array", "items": { "$ref": "#/components/schemas/CountryStatDto" } } }, - "required": ["countries"] + "properties": { + "studentsCountries": { "$ref": "#/components/schemas/CountriesStatsDto" }, + "studentsStats": { "$ref": "#/components/schemas/CourseStatsDto" }, + "mentorsCountries": { "$ref": "#/components/schemas/CountriesStatsDto" }, + "mentorsStats": { "$ref": "#/components/schemas/CourseMentorsStatsDto" }, + "courseTasks": { "type": "array", "items": { "$ref": "#/components/schemas/CourseTaskDto" } }, + "studentsCertificatesCountries": { "$ref": "#/components/schemas/CountriesStatsDto" } + }, + "required": [ + "studentsCountries", + "studentsStats", + "mentorsCountries", + "mentorsStats", + "courseTasks", + "studentsCertificatesCountries" + ] }, "TaskPerformanceStatsDto": { "type": "object",