diff --git a/Epsilon.Abstractions/Epsilon.Abstractions.csproj b/Epsilon.Abstractions/Epsilon.Abstractions.csproj index d46fbc80..d034d6a3 100644 --- a/Epsilon.Abstractions/Epsilon.Abstractions.csproj +++ b/Epsilon.Abstractions/Epsilon.Abstractions.csproj @@ -23,5 +23,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/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.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/DecayingAverageLogic.ts b/Epsilon.Host.Frontend/src/logic/DecayingAverageLogic.ts new file mode 100644 index 00000000..186262b9 --- /dev/null +++ b/Epsilon.Host.Frontend/src/logic/DecayingAverageLogic.ts @@ -0,0 +1,199 @@ +import { + IHboIDomain, + ProfessionalSkillResult, + ProfessionalTaskResult, +} from "/@/logic/Api" + +export interface DecayingAveragePerActivity { + outcome: number + activity: number + decayingAverage: number +} + +export interface DecayingAveragePerLayer { + architectureLayer: number + layerActivities: DecayingAveragePerActivity[] +} + +export interface DecayingAveragePerSkill { + skill: number + decayingAverage: number + masteryLevel: number +} + +export class DecayingAverageLogic { + /** + * Calculate the averages for each skill type + * @param taskResults + * @param domain + * @constructor + */ + public static getAverageSkillOutcomeScores( + taskResults: ProfessionalSkillResult[], + domain: IHboIDomain + ): DecayingAveragePerSkill[] { + const listOfResults = Object.entries( + this.groupBy(taskResults, (r) => 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 @@