diff --git a/Epsilon.Abstractions/Epsilon.Abstractions.csproj b/Epsilon.Abstractions/Epsilon.Abstractions.csproj index d3a8bb24..8f641cfd 100644 --- a/Epsilon.Abstractions/Epsilon.Abstractions.csproj +++ b/Epsilon.Abstractions/Epsilon.Abstractions.csproj @@ -9,5 +9,4 @@ - diff --git a/Epsilon.Abstractions/Model/CompetenceOutcomeResult.cs b/Epsilon.Abstractions/Model/CompetenceOutcomeResult.cs new file mode 100644 index 00000000..7d6e8cc5 --- /dev/null +++ b/Epsilon.Abstractions/Model/CompetenceOutcomeResult.cs @@ -0,0 +1,7 @@ +namespace Epsilon.Abstractions.Model; + +public record CompetenceOutcomeResult( + int OutcomeId, + double Grade, + DateTime SubmittedAt +); \ No newline at end of file diff --git a/Epsilon.Abstractions/Model/CompetenceProfile.cs b/Epsilon.Abstractions/Model/CompetenceProfile.cs index 096eb4fb..1aa51940 100644 --- a/Epsilon.Abstractions/Model/CompetenceProfile.cs +++ b/Epsilon.Abstractions/Model/CompetenceProfile.cs @@ -6,7 +6,5 @@ public record CompetenceProfile( IHboIDomain HboIDomain, IEnumerable ProfessionalTaskOutcomes, IEnumerable ProfessionalSkillOutcomes, - IEnumerable Terms, - IEnumerable DecayingAveragesPerTask, - IEnumerable DecayingAveragesPerSkill + IEnumerable Terms ); \ No newline at end of file diff --git a/Epsilon.Abstractions/Model/CourseModule.cs b/Epsilon.Abstractions/Model/CourseModule.cs index 035d9b02..d7a5950b 100644 --- a/Epsilon.Abstractions/Model/CourseModule.cs +++ b/Epsilon.Abstractions/Model/CourseModule.cs @@ -4,7 +4,5 @@ public class CourseModule { public string Name { get; set; } = string.Empty; public IEnumerable Kpis { get; set; } = Enumerable.Empty(); - - public string DecayingAverage { get; set; } } } diff --git a/Epsilon.Abstractions/Model/DecayingAverage.cs b/Epsilon.Abstractions/Model/DecayingAverage.cs new file mode 100644 index 00000000..0de50cae --- /dev/null +++ b/Epsilon.Abstractions/Model/DecayingAverage.cs @@ -0,0 +1,7 @@ +namespace Epsilon.Abstractions.Model; + +public record DecayingAverage( + double Score, + int ArchitectureLayer, + int Activity +); \ No newline at end of file diff --git a/Epsilon.Abstractions/Model/DecayingAveragePerActivity.cs b/Epsilon.Abstractions/Model/DecayingAveragePerActivity.cs deleted file mode 100644 index 0c7c7645..00000000 --- a/Epsilon.Abstractions/Model/DecayingAveragePerActivity.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Epsilon.Abstractions.Model; - -public record DecayingAveragePerActivity( - int Activity, - double DecayingAverage -); \ No newline at end of file diff --git a/Epsilon.Abstractions/Model/DecayingAveragePerLayer.cs b/Epsilon.Abstractions/Model/DecayingAveragePerLayer.cs deleted file mode 100644 index 7c3d648e..00000000 --- a/Epsilon.Abstractions/Model/DecayingAveragePerLayer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Epsilon.Abstractions.Model; - -public record DecayingAveragePerLayer( - int ArchitectureLayer, - IEnumerable LayerActivities -); \ No newline at end of file diff --git a/Epsilon.Abstractions/Model/DecayingAveragePerSkill.cs b/Epsilon.Abstractions/Model/DecayingAveragePerSkill.cs deleted file mode 100644 index 90eef853..00000000 --- a/Epsilon.Abstractions/Model/DecayingAveragePerSkill.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Epsilon.Abstractions.Model; - -public record DecayingAveragePerSkill( - int Skill, - double DecayingAverage -); \ No newline at end of file diff --git a/Epsilon.Abstractions/Model/ProfessionalSkillResult.cs b/Epsilon.Abstractions/Model/ProfessionalSkillResult.cs index 8cd48934..505b4f3b 100644 --- a/Epsilon.Abstractions/Model/ProfessionalSkillResult.cs +++ b/Epsilon.Abstractions/Model/ProfessionalSkillResult.cs @@ -1,8 +1,9 @@ namespace Epsilon.Abstractions.Model; public record ProfessionalSkillResult( + int OutcomeId, int Skill, int MasteryLevel, double Grade, DateTime AssessedAt -); \ No newline at end of file +) : CompetenceOutcomeResult(OutcomeId, Grade, AssessedAt); \ No newline at end of file diff --git a/Epsilon.Abstractions/Model/ProfessionalTaskResult.cs b/Epsilon.Abstractions/Model/ProfessionalTaskResult.cs index 55998383..a83e9c83 100644 --- a/Epsilon.Abstractions/Model/ProfessionalTaskResult.cs +++ b/Epsilon.Abstractions/Model/ProfessionalTaskResult.cs @@ -1,9 +1,10 @@ namespace Epsilon.Abstractions.Model; public record ProfessionalTaskResult( + int OutcomeId, int ArchitectureLayer, int Activity, int MasteryLevel, double Grade, DateTime AssessedAt -); \ No newline at end of file +) : CompetenceOutcomeResult(OutcomeId, Grade, AssessedAt); \ No newline at end of file diff --git a/Epsilon.Canvas.Abstractions/Model/GraphQl/SubmissionsHistoriesConnectionNode.cs b/Epsilon.Canvas.Abstractions/Model/GraphQl/SubmissionsHistoriesConnectionNode.cs index 7c6ed535..c09367c5 100644 --- a/Epsilon.Canvas.Abstractions/Model/GraphQl/SubmissionsHistoriesConnectionNode.cs +++ b/Epsilon.Canvas.Abstractions/Model/GraphQl/SubmissionsHistoriesConnectionNode.cs @@ -4,5 +4,6 @@ namespace Epsilon.Canvas.Abstractions.Model.GraphQl; public record SubmissionsHistoriesConnectionNode( [property: JsonPropertyName("attempt")] int? Attempt, + [property: JsonPropertyName("submittedAt")] DateTime? SubmittedAt, [property: JsonPropertyName("rubricAssessmentsConnection")] RubricAssessmentsConnection? RubricAssessments ); \ No newline at end of file diff --git a/Epsilon.Canvas.Abstractions/Model/OutcomeResultCollection.cs b/Epsilon.Canvas.Abstractions/Model/OutcomeResultCollection.cs index 09e169c2..9cbf3ac6 100644 --- a/Epsilon.Canvas.Abstractions/Model/OutcomeResultCollection.cs +++ b/Epsilon.Canvas.Abstractions/Model/OutcomeResultCollection.cs @@ -6,20 +6,4 @@ public record OutcomeResultCollection( [property: JsonPropertyName("outcome_results")] IEnumerable OutcomeResults, [property: JsonPropertyName("linked")] OutcomeResultCollectionLink? Links -) -{ - public double GetDecayingAverage() - { - var decayingAverage = 0.0; - - foreach(var grade in OutcomeResults) - { - if (grade.Score != null) - { - decayingAverage = decayingAverage * 0.35 + grade.Score.Value * 0.65; - } - } - - return decayingAverage; - } -} \ No newline at end of file +); \ No newline at end of file diff --git a/Epsilon.Canvas/QueryConstants.cs b/Epsilon.Canvas/QueryConstants.cs index 4481e619..721f4eb5 100644 --- a/Epsilon.Canvas/QueryConstants.cs +++ b/Epsilon.Canvas/QueryConstants.cs @@ -54,6 +54,7 @@ query MyQuery { } } attempt + submittedAt } } postedAt diff --git a/Epsilon.Host.Frontend/src/components/CompetenceGraph.vue b/Epsilon.Host.Frontend/src/components/CompetenceGraph.vue index 176fac24..44185abb 100644 --- a/Epsilon.Host.Frontend/src/components/CompetenceGraph.vue +++ b/Epsilon.Host.Frontend/src/components/CompetenceGraph.vue @@ -1,18 +1,22 @@ diff --git a/Epsilon.Host.Frontend/src/components/PersonalDevelopmentGraph.vue b/Epsilon.Host.Frontend/src/components/PersonalDevelopmentGraph.vue index 61b89581..6190ea02 100644 --- a/Epsilon.Host.Frontend/src/components/PersonalDevelopmentGraph.vue +++ b/Epsilon.Host.Frontend/src/components/PersonalDevelopmentGraph.vue @@ -1,23 +1,28 @@ diff --git a/Epsilon.Host.Frontend/src/logic/Api.ts b/Epsilon.Host.Frontend/src/logic/Api.ts index 68f98af0..135ff2d9 100644 --- a/Epsilon.Host.Frontend/src/logic/Api.ts +++ b/Epsilon.Host.Frontend/src/logic/Api.ts @@ -1,6 +1,5 @@ /* eslint-disable */ /* tslint:disable */ - /* * --------------------------------------------------------------- * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## @@ -12,140 +11,132 @@ export interface Activity { /** @format int32 */ - id?: number; - name?: string | null; - color?: string | null; + id?: number + name?: string | null + color?: string | null } export interface ArchitectureLayer { /** @format int32 */ - id?: number; - name?: string | null; - shortName?: string | null; - color?: string | null; + id?: number + name?: string | null + shortName?: string | null + color?: string | null } export interface CompetenceProfile { - hboIDomain?: IHboIDomain; - professionalTaskOutcomes?: ProfessionalTaskResult[] | null; - professionalSkillOutcomes?: ProfessionalSkillResult[] | null; - terms?: EnrollmentTerm[] | null; - decayingAveragesPerTask?: DecayingAveragePerLayer[] | null; - decayingAveragesPerSkill?: DecayingAveragePerSkill[] | null; + hboIDomain?: IHboIDomain + professionalTaskOutcomes?: ProfessionalTaskResult[] | null + professionalSkillOutcomes?: ProfessionalSkillResult[] | null + terms?: EnrollmentTerm[] | null + decayingAverages?: DecayingAverage[] | null } -export interface DecayingAveragePerActivity { - /** @format int32 */ - activity?: number; +export interface DecayingAverage { /** @format double */ - decayingAverage?: number; -} - -export interface DecayingAveragePerLayer { + score?: number /** @format int32 */ - architectureLayer?: number; - layerActivities?: DecayingAveragePerActivity[] | null; -} - -export interface DecayingAveragePerSkill { + architectureLayer?: number /** @format int32 */ - skill?: number; - /** @format double */ - decayingAverage?: number; + activity?: number } export interface EnrollmentTerm { - name?: string | null; + name?: string | null /** @format date-time */ - start_at?: string | null; + start_at?: string | null /** @format date-time */ - end_at?: string | null; + end_at?: string | null } export interface IHboIDomain { - architectureLayers?: ArchitectureLayer[] | null; - activities?: Activity[] | null; - professionalSkills?: ProfessionalSkill[] | null; - masteryLevels?: MasteryLevel[] | null; + architectureLayers?: ArchitectureLayer[] | null + activities?: Activity[] | null + professionalSkills?: ProfessionalSkill[] | null + masteryLevels?: MasteryLevel[] | null } export interface MasteryLevel { /** @format int32 */ - id?: number; + id?: number /** @format int32 */ - level?: number; - color?: string | null; + level?: number + color?: string | null } export interface ProfessionalSkill { /** @format int32 */ - id?: number; - name?: string | null; - shortName?: string | null; - color?: string | null; + id?: number + name?: string | null + shortName?: string | null + color?: string | null } export interface ProfessionalSkillResult { /** @format int32 */ - skill?: number; - /** @format int32 */ - masteryLevel?: number; + outcomeId?: number /** @format double */ - grade?: number; + grade?: number /** @format date-time */ - assessedAt?: string; + assessedAt?: string + /** @format int32 */ + skill?: number + /** @format int32 */ + masteryLevel?: number } export interface ProfessionalTaskResult { /** @format int32 */ - architectureLayer?: number; - /** @format int32 */ - activity?: number; - /** @format int32 */ - masteryLevel?: number; + outcomeId?: number /** @format double */ - grade?: number; + grade?: number /** @format date-time */ - assessedAt?: string; + assessedAt?: string + /** @format int32 */ + architectureLayer?: number + /** @format int32 */ + activity?: number + /** @format int32 */ + masteryLevel?: number } -export type QueryParamsType = Record; -export type ResponseFormat = keyof Omit; +export type QueryParamsType = Record +export type ResponseFormat = keyof Omit export interface FullRequestParams extends Omit { /** set parameter to `true` for call `securityWorker` for this request */ - secure?: boolean; + secure?: boolean /** request path */ - path: string; + path: string /** content type of request body */ - type?: ContentType; + type?: ContentType /** query params */ - query?: QueryParamsType; + query?: QueryParamsType /** format of response (i.e. response.json() -> format: "json") */ - format?: ResponseFormat; + format?: ResponseFormat /** request body */ - body?: unknown; + body?: unknown /** base url */ - baseUrl?: string; + baseUrl?: string /** request cancellation token */ - cancelToken?: CancelToken; + cancelToken?: CancelToken } -export type RequestParams = Omit; +export type RequestParams = Omit export interface ApiConfig { - baseUrl?: string; - baseApiParams?: Omit; - securityWorker?: (securityData: SecurityDataType | null) => Promise | RequestParams | void; - customFetch?: typeof fetch; + baseUrl?: string + baseApiParams?: Omit + securityWorker?: (securityData: SecurityDataType | null) => Promise | RequestParams | void + customFetch?: typeof fetch } export interface HttpResponse extends Response { - data: D; - error: E; + data: D + error: E } -type CancelToken = Symbol | string | number; +type CancelToken = Symbol | string | number export enum ContentType { Json = "application/json", @@ -155,73 +146,76 @@ export enum ContentType { } export class HttpClient { - public baseUrl: string = "https://localhost:7084"; - private securityData: SecurityDataType | null = null; - private securityWorker?: ApiConfig["securityWorker"]; - private abortControllers = new Map(); - private customFetch = (...fetchParams: Parameters) => fetch(...fetchParams); + public baseUrl: string = "https://localhost:7084" + private securityData: SecurityDataType | null = null + private securityWorker?: ApiConfig["securityWorker"] + private abortControllers = new Map() + private customFetch = (...fetchParams: Parameters) => fetch(...fetchParams) private baseApiParams: RequestParams = { credentials: "same-origin", headers: {}, redirect: "follow", referrerPolicy: "no-referrer", - }; + } constructor(apiConfig: ApiConfig = {}) { - Object.assign(this, apiConfig); + Object.assign(this, apiConfig) } public setSecurityData = (data: SecurityDataType | null) => { - this.securityData = data; - }; + this.securityData = data + } protected encodeQueryParam(key: string, value: any) { - const encodedKey = encodeURIComponent(key); - return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`; + const encodedKey = encodeURIComponent(key) + return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}` } protected addQueryParam(query: QueryParamsType, key: string) { - return this.encodeQueryParam(key, query[key]); + return this.encodeQueryParam(key, query[key]) } protected addArrayQueryParam(query: QueryParamsType, key: string) { - const value = query[key]; - return value.map((v: any) => this.encodeQueryParam(key, v)).join("&"); + const value = query[key] + return value.map((v: any) => this.encodeQueryParam(key, v)).join("&") } protected toQueryString(rawQuery?: QueryParamsType): string { - const query = rawQuery || {}; - const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]); + const query = rawQuery || {} + const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]) return keys - .map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key))) - .join("&"); + .map((key) => + Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key) + ) + .join("&") } protected addQueryParams(rawQuery?: QueryParamsType): string { - const queryString = this.toQueryString(rawQuery); - return queryString ? `?${queryString}` : ""; + const queryString = this.toQueryString(rawQuery) + return queryString ? `?${queryString}` : "" } private contentFormatters: Record any> = { [ContentType.Json]: (input: any) => input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input, - [ContentType.Text]: (input: any) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input), + [ContentType.Text]: (input: any) => + input !== null && typeof input !== "string" ? JSON.stringify(input) : input, [ContentType.FormData]: (input: any) => Object.keys(input || {}).reduce((formData, key) => { - const property = input[key]; + const property = input[key] formData.append( key, property instanceof Blob ? property : typeof property === "object" && property !== null - ? JSON.stringify(property) - : `${property}`, - ); - return formData; + ? JSON.stringify(property) + : `${property}` + ) + return formData }, new FormData()), [ContentType.UrlEncoded]: (input: any) => this.toQueryString(input), - }; + } protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams { return { @@ -233,90 +227,90 @@ export class HttpClient { ...(params1.headers || {}), ...((params2 && params2.headers) || {}), }, - }; + } } protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => { if (this.abortControllers.has(cancelToken)) { - const abortController = this.abortControllers.get(cancelToken); + const abortController = this.abortControllers.get(cancelToken) if (abortController) { - return abortController.signal; + return abortController.signal } - return void 0; + return void 0 } - const abortController = new AbortController(); - this.abortControllers.set(cancelToken, abortController); - return abortController.signal; - }; + const abortController = new AbortController() + this.abortControllers.set(cancelToken, abortController) + return abortController.signal + } public abortRequest = (cancelToken: CancelToken) => { - const abortController = this.abortControllers.get(cancelToken); + const abortController = this.abortControllers.get(cancelToken) if (abortController) { - abortController.abort(); - this.abortControllers.delete(cancelToken); + abortController.abort() + this.abortControllers.delete(cancelToken) } - }; + } public request = async ({ - body, - secure, - path, - type, - query, - format, - baseUrl, - cancelToken, - ...params - }: FullRequestParams): Promise> => { + body, + secure, + path, + type, + query, + format, + baseUrl, + cancelToken, + ...params + }: FullRequestParams): Promise> => { const secureParams = ((typeof secure === "boolean" ? secure : this.baseApiParams.secure) && this.securityWorker && (await this.securityWorker(this.securityData))) || - {}; - const requestParams = this.mergeRequestParams(params, secureParams); - const queryString = query && this.toQueryString(query); - const payloadFormatter = this.contentFormatters[type || ContentType.Json]; - const responseFormat = format || requestParams.format; + {} + const requestParams = this.mergeRequestParams(params, secureParams) + const queryString = query && this.toQueryString(query) + const payloadFormatter = this.contentFormatters[type || ContentType.Json] + const responseFormat = format || requestParams.format return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, { ...requestParams, headers: { ...(requestParams.headers || {}), - ...(type && type !== ContentType.FormData ? {"Content-Type": type} : {}), + ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}), }, signal: cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal, body: typeof body === "undefined" || body === null ? null : payloadFormatter(body), }).then(async (response) => { - const r = response as HttpResponse; - r.data = null as unknown as T; - r.error = null as unknown as E; + const r = response as HttpResponse + r.data = null as unknown as T + r.error = null as unknown as E const data = !responseFormat ? r : await response[responseFormat]() - .then((data) => { - if (r.ok) { - r.data = data; - } else { - r.error = data; - } - return r; - }) - .catch((e) => { - r.error = e; - return r; - }); + .then((data) => { + if (r.ok) { + r.data = data + } else { + r.error = data + } + return r + }) + .catch((e) => { + r.error = e + return r + }) if (cancelToken) { - this.abortControllers.delete(cancelToken); + this.abortControllers.delete(cancelToken) } - if (!response.ok) throw data; - return data; - }); - }; + if (!response.ok) throw data + return data + }) + } } /** @@ -352,7 +346,7 @@ export class Api extends HttpClient extends HttpClient r.outcomeId as unknown as string) + ).map(([, j]) => { + return { + decayingAverage: this.getDecayingAverageFromOneOutcomeType(j), + skill: j.at(0)?.skill, + masteryLevel: j + .sort((a) => a.masteryLevel as never as number) + .at(0)?.masteryLevel, + } as DecayingAveragePerSkill + }) + + return domain.professionalSkills?.map((s) => { + let score = 0.0 + const filteredResults = listOfResults.filter( + (r) => r.skill === s.id + ) + filteredResults.map((result) => { + score += result.decayingAverage + }) + return { + decayingAverage: score / filteredResults.length, + skill: s.id, + masteryLevel: filteredResults + .sort((a) => a.masteryLevel as never as number) + .at(0)?.masteryLevel, + } as DecayingAveragePerSkill + }) as DecayingAveragePerSkill[] + } + + /** + * Calculate the averages for each task type divided in architecture layers. + * @param taskResults + * @param domain + * @constructor + */ + public static getAverageTaskOutcomeScores( + taskResults: ProfessionalTaskResult[], + domain: IHboIDomain + ): DecayingAveragePerLayer[] { + const canvasDecaying = this.getDecayingAverageForAllOutcomes( + taskResults, + domain + ) + return domain.architectureLayers?.map((layer) => { + return { + architectureLayer: layer.id, + layerActivities: domain.activities?.map((activity) => { + let totalScoreActivity = 0 + let totalScoreArchitectureActivity = 0 + let amountOfActivities = 0 + + //Calculate the total score from activity + canvasDecaying.map((l) => + l.layerActivities + ?.filter((la) => la.activity === activity.id) + .map( + (la) => + (totalScoreActivity += + la.decayingAverage && + amountOfActivities++) + ) + ) + + //Calculate the total score from activity inside this architecture layer + canvasDecaying + .filter((l) => l.architectureLayer === layer.id) + .map((l) => + l.layerActivities + ?.filter((la) => la.activity === activity.id) + .map( + (la) => + (totalScoreArchitectureActivity += + la.decayingAverage) + ) + ) + + return { + activity: activity.id, + decayingAverage: + ((totalScoreActivity / amountOfActivities) * + totalScoreArchitectureActivity) / + totalScoreActivity, + } as DecayingAveragePerActivity + }), + } + }) as DecayingAveragePerLayer[] + } + + /** + * Calculate average of given tasks divided in architectural layers + * @param taskResults + * @param domain + * @constructor + * @private + */ + private static getDecayingAverageForAllOutcomes( + taskResults: ProfessionalTaskResult[], + domain: IHboIDomain + ): DecayingAveragePerLayer[] { + return domain.architectureLayers?.map((l) => { + return { + architectureLayer: l.id, + layerActivities: Object.entries( + this.groupBy( + //Ensure that given results are only relined on the architecture that is currently being used. + taskResults.filter( + (layer) => layer.architectureLayer === l.id + ), + (r) => r.outcomeId as unknown as string + ) + ).map(([i, j]) => { + //From all selected outcomes calculate the decaying average, Give outcome id and activity layer. + return { + outcome: i, + activity: j.at(0)?.activity, + decayingAverage: + this.getDecayingAverageFromOneOutcomeType(j), + } + }) as unknown as DecayingAveragePerActivity[], + } + }) as DecayingAveragePerLayer[] + } + + /** + * Calculate decaying average described by Canvas: https://community.canvaslms.com/t5/Canvas-Basics-Guide/What-are-Outcomes/ta-p/75#decaying_average + * !IMPORTANT, The list of results will always have to be a list of the same outcome id. Not a list of equal activity and architecture layer. + * @param results + * @constructor + * @private + */ + private static getDecayingAverageFromOneOutcomeType( + results: ProfessionalTaskResult[] | ProfessionalSkillResult[] + ): number { + let totalGradeScore = 0.0 + + const recentResult = results.reverse().pop() + if (recentResult && recentResult.grade) { + if (results.length > 0) { + results.forEach( + (r) => (totalGradeScore += r.grade ? r.grade : 0) + ) + totalGradeScore = + (totalGradeScore / results.length) * 0.35 + + recentResult.grade * 0.65 + } else { + totalGradeScore = recentResult.grade + } + } + + return totalGradeScore + } + + private static groupBy( + arr: T[], + fn: (item: T) => number | string + ): Record { + return arr.reduce>((prev, curr) => { + const groupKey = fn(curr) + const group = prev[groupKey] || [] + group.push(curr) + return { ...prev, [groupKey]: group } + }, {}) + } +} diff --git a/Epsilon.Host.Frontend/src/views/PerformanceDashboard.vue b/Epsilon.Host.Frontend/src/views/PerformanceDashboard.vue index 3e5989d3..e487b248 100644 --- a/Epsilon.Host.Frontend/src/views/PerformanceDashboard.vue +++ b/Epsilon.Host.Frontend/src/views/PerformanceDashboard.vue @@ -4,16 +4,16 @@ :terms="data.terms" @on-term-selected="setTermFilter" /> + :data="filteredProfessionalTaskOutcomes" + :domain="data.hboIDomain" />
- + :data="filteredProfessionalTaskOutcomes" + :domain="data.hboIDomain" /> +
@@ -21,17 +21,17 @@