From 3751808764e1784188d58330688b56eea457345e Mon Sep 17 00:00:00 2001 From: Matt Gunter Date: Sun, 15 Jun 2025 10:37:55 -0400 Subject: [PATCH] feat: add target recalculation functionality and UI controls for saving and resetting targets Also, fixed time savings chart to use configured targets & max values --- .../services/target-calculation-service.ts | 21 +++++----- .../services/value-modeling-explanation.md | 0 .../copilot-value-modeling.component.html | 8 ++++ .../copilot-value-modeling.component.ts | 16 +++++++- .../time-saved-chart.component.ts | 40 ++++++++++++++++++- .../src/app/services/api/targets.service.ts | 5 +++ 6 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 backend/src/services/value-modeling-explanation.md diff --git a/backend/src/services/target-calculation-service.ts b/backend/src/services/target-calculation-service.ts index d42c722b..32636725 100644 --- a/backend/src/services/target-calculation-service.ts +++ b/backend/src/services/target-calculation-service.ts @@ -702,7 +702,8 @@ RESULT: const userTimeSavings = distinctUsers.map(userId => { const userSurveys = this.surveysWeekly.filter(survey => survey.userId === userId); const totalPercent = userSurveys.reduce((sum, survey) => { - const percentTimeSaved = typeof survey.percentTimeSaved === 'number' ? survey.percentTimeSaved : 0; + // Always parse percentTimeSaved as float + const percentTimeSaved = survey.percentTimeSaved != null ? parseFloat(survey.percentTimeSaved as any) : 0; return sum + percentTimeSaved; }, 0); return totalPercent / userSurveys.length; // Average percent time saved per user @@ -711,9 +712,9 @@ RESULT: // Average across all users const avgPercentTimeSaved = userTimeSavings.reduce((sum, percent) => sum + percent, 0) / userTimeSavings.length; - // Convert settings values to numbers - const hoursPerYear = typeof this.settings.hoursPerYear === 'number' ? this.settings.hoursPerYear : 2000; - const percentCoding = typeof this.settings.percentCoding === 'number' ? this.settings.percentCoding : 50; + // Convert settings values to numbers (parse from string if needed) + const hoursPerYear = this.settings.hoursPerYear != null ? parseFloat(this.settings.hoursPerYear as any) : 2000; + const percentCoding = this.settings.percentCoding != null ? parseFloat(this.settings.percentCoding as any) : 50; // Calculate weekly hours saved based on settings and average percent const weeklyHours = hoursPerYear / 50; // Assuming 50 working weeks @@ -721,7 +722,7 @@ RESULT: const avgWeeklyTimeSaved = weeklyDevHours * (avgPercentTimeSaved / 100); // Calculate max based on settings - const maxPercentTimeSaved = typeof this.settings.percentTimeSaved === 'number' ? this.settings.percentTimeSaved : 20; + const maxPercentTimeSaved = this.settings.percentTimeSaved != null ? parseFloat(this.settings.percentTimeSaved as any) : 20; const maxWeeklyTimeSaved = weeklyDevHours * (maxPercentTimeSaved / 100); const result = { @@ -785,11 +786,11 @@ RESULT: const adoptedDevs = this.calculateAdoptedDevs().current; const weeklyTimeSavedHrs = this.calculateWeeklyTimeSavedHrs().current; - // Ensure all values are properly typed as numbers - const hoursPerYear = typeof this.settings.hoursPerYear === 'number' ? this.settings.hoursPerYear : 2000; + // Always parse settings values as numbers (from string if needed) + const hoursPerYear = this.settings.hoursPerYear != null ? parseFloat(this.settings.hoursPerYear as any) : 2000; const weeksInYear = Math.round(hoursPerYear / 40) || 50; // Calculate weeks and ensure it's a number - const devCostPerYear = typeof this.settings.devCostPerYear === 'number' ? this.settings.devCostPerYear : 0; + const devCostPerYear = this.settings.devCostPerYear != null ? parseFloat(this.settings.devCostPerYear as any) : 0; const hourlyRate = devCostPerYear > 0 ? (devCostPerYear / hoursPerYear) : 50; const annualSavings = weeklyTimeSavedHrs * weeksInYear * hourlyRate * adoptedDevs; @@ -824,8 +825,8 @@ RESULT: const adoptedDevs = this.calculateAdoptedDevs().current; const weeklyTimeSavedHrs = this.calculateWeeklyTimeSavedHrs().current; - // Convert hours per year to number - const hoursPerYear = typeof this.settings.hoursPerYear === 'number' ? this.settings.hoursPerYear : 2000; + // Always parse hours per year as number + const hoursPerYear = this.settings.hoursPerYear != null ? parseFloat(this.settings.hoursPerYear as any) : 2000; const hoursPerWeek = hoursPerYear / 50 || 40; // Default to 40 if undefined // Calculate productivity boost factor (not percentage) diff --git a/backend/src/services/value-modeling-explanation.md b/backend/src/services/value-modeling-explanation.md new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/main/copilot/copilot-value-modeling/copilot-value-modeling.component.html b/frontend/src/app/main/copilot/copilot-value-modeling/copilot-value-modeling.component.html index 1368ba70..93f145ce 100644 --- a/frontend/src/app/main/copilot/copilot-value-modeling/copilot-value-modeling.component.html +++ b/frontend/src/app/main/copilot/copilot-value-modeling/copilot-value-modeling.component.html @@ -1,6 +1,14 @@

Org Metrics

diff --git a/frontend/src/app/main/copilot/copilot-value-modeling/copilot-value-modeling.component.ts b/frontend/src/app/main/copilot/copilot-value-modeling/copilot-value-modeling.component.ts index b0a72a27..f3cb28a2 100644 --- a/frontend/src/app/main/copilot/copilot-value-modeling/copilot-value-modeling.component.ts +++ b/frontend/src/app/main/copilot/copilot-value-modeling/copilot-value-modeling.component.ts @@ -37,6 +37,7 @@ export class CopilotValueModelingComponent implements OnInit { orgDataSource: TableTarget[] = []; userDataSource: TableTarget[] = []; impactDataSource: TableTarget[] = []; + showSaveAllButton = false; private readonly _destroy$ = new Subject(); keyToNameMap: Record = { seats: 'Seats', @@ -128,7 +129,9 @@ export class CopilotValueModelingComponent implements OnInit { saveTargets() { const targets: Targets = this.transformBackToTargets(this.orgDataSource, this.userDataSource, this.impactDataSource); - this.targetsService.saveTargets(targets).subscribe(); + this.targetsService.saveTargets(targets).subscribe(() => { + this.showSaveAllButton = false; + }); } openEditDialog(target: Target) { @@ -144,6 +147,17 @@ export class CopilotValueModelingComponent implements OnInit { } }); } + + resetTargets() { + // Call the backend endpoint to recalculate targets + this.targetsService.recalculateTargets().subscribe((result: any) => { + const targets = result.targets || result; // handle both {targets, logs} and just targets + this.orgDataSource = this.transformTargets(targets.org); + this.userDataSource = this.transformTargets(targets.user); + this.impactDataSource = this.transformTargets(targets.impact); + this.showSaveAllButton = true; + }); + } } @Component({ diff --git a/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.ts b/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.ts index 8ceea2e2..9df7124e 100644 --- a/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.ts +++ b/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.ts @@ -29,7 +29,7 @@ export class TimeSavedChartComponent implements OnInit, OnChanges { text: 'Time Saved (Hrs per Week)' }, min: 0, - max: 10, + max: 10, // Will be updated dynamically labels: { format: '{value}hrs' }, @@ -45,7 +45,7 @@ export class TimeSavedChartComponent implements OnInit, OnChanges { } }], plotLines: [{ - value: 5, + value: 5, // Will be updated dynamically color: 'var(--sys-primary)', dashStyle: 'Dash', width: 2, @@ -91,6 +91,7 @@ export class TimeSavedChartComponent implements OnInit, OnChanges { this._chartOptions.yAxis = Object.assign({}, this.chartOptions?.yAxis, this._chartOptions.yAxis); this._chartOptions.tooltip = Object.assign({}, this.chartOptions?.tooltip, this._chartOptions.tooltip); this._chartOptions = Object.assign({}, this.chartOptions, this._chartOptions); + this.updateYAxisFromTargets(); } ngOnChanges() { @@ -101,6 +102,41 @@ export class TimeSavedChartComponent implements OnInit, OnChanges { }; this.updateFlag = true; } + this.updateYAxisFromTargets(); + } + + private updateYAxisFromTargets() { + if (this.targets?.user?.weeklyTimeSavedHrs) { + const targetValue = this.targets.user.weeklyTimeSavedHrs.target; + const maxValue = Math.max( + targetValue * 1.5, + this.targets.user.weeklyTimeSavedHrs.max || 10, + 10 + ); + const yAxis = { + ...this._chartOptions.yAxis, + max: maxValue, + plotLines: [{ + value: targetValue, + color: 'var(--sys-primary)', + dashStyle: 'Dash' as Highcharts.DashStyleValue, + width: 2, + label: { + text: 'Target Level', + align: 'left' as Highcharts.AlignValue, + style: { + color: 'var(--sys-primary)' + } + }, + zIndex: 2 + }] + }; + this._chartOptions = { + ...this._chartOptions, + yAxis + }; + this.updateFlag = true; + } } } diff --git a/frontend/src/app/services/api/targets.service.ts b/frontend/src/app/services/api/targets.service.ts index 86f4ea15..59ba1bd0 100644 --- a/frontend/src/app/services/api/targets.service.ts +++ b/frontend/src/app/services/api/targets.service.ts @@ -77,5 +77,10 @@ export class TargetsService { saveTargets(targets: Targets) { return this.http.post(`${this.apiUrl}`, targets); } + + recalculateTargets() { + // Calls the backend endpoint to recalculate targets + return this.http.get(`${this.apiUrl}/calculate`); + } }