diff --git a/backend/src/controllers/target-values.controller.ts b/backend/src/controllers/target-values.controller.ts new file mode 100644 index 0000000..05da4ac --- /dev/null +++ b/backend/src/controllers/target-values.controller.ts @@ -0,0 +1,24 @@ +import { Request, Response } from 'express'; +import TargetValuesService from '../services/target-values.service.js'; + +class TargetValuesController { + async getTargetValues(req: Request, res: Response): Promise { + try { + const targetValues = await TargetValuesService.getTargetValues(); + res.status(200).json(targetValues); + } catch (error) { + res.status(500).json(error); + } + } + + async updateTargetValues(req: Request, res: Response): Promise { + try { + const updatedTargetValues = await TargetValuesService.updateTargetValues(req.body); + res.status(200).json(updatedTargetValues); + } catch (error) { + res.status(500).json(error); + } + } +} + +export default new TargetValuesController(); diff --git a/backend/src/models/target-values.model.ts b/backend/src/models/target-values.model.ts new file mode 100644 index 0000000..dcf8a77 --- /dev/null +++ b/backend/src/models/target-values.model.ts @@ -0,0 +1,29 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../database.js'; + +class TargetValues extends Model { + public targetedRoomForImprovement!: number; + public targetedNumberOfDevelopers!: number; + public targetedPercentOfTimeSaved!: number; +} + +TargetValues.init({ + targetedRoomForImprovement: { + type: DataTypes.FLOAT, + allowNull: false, + }, + targetedNumberOfDevelopers: { + type: DataTypes.INTEGER, + allowNull: false, + }, + targetedPercentOfTimeSaved: { + type: DataTypes.FLOAT, + allowNull: false, + } +}, { + sequelize, + modelName: 'TargetValues', + timestamps: false, +}); + +export { TargetValues }; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 026c76c..908e6fc 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -1,11 +1,12 @@ import { Router, Request, Response } from 'express'; -import SurveyController from '../controllers/survey.controller.js'; +import surveyController from '../controllers/survey.controller.js'; import usageController from '../controllers/usage.controller.js'; import settingsController from '../controllers/settings.controller.js'; import setupController from '../controllers/setup.controller.js'; import SeatsController from '../controllers/seats.controller.js'; import metricsController from '../controllers/metrics.controller.js'; -import TeamsController from '../controllers/teams.controller.js'; +import teamsController from '../controllers/teams.controller.js'; +import targetValuesController from '../controllers/target-values.controller.js'; const router = Router(); @@ -13,11 +14,11 @@ router.get('/', (req: Request, res: Response) => { res.send('Hello World!'); }); -router.get('/survey', SurveyController.getAllSurveys); -router.post('/survey', SurveyController.createSurvey); -router.get('/survey/:id', SurveyController.getSurveyById); -router.put('/survey/:id', SurveyController.updateSurvey); -router.delete('/survey/:id', SurveyController.deleteSurvey); +router.get('/survey', surveyController.getAllSurveys); +router.post('/survey', surveyController.createSurvey); +router.get('/survey/:id', surveyController.getSurveyById); +router.put('/survey/:id', surveyController.updateSurvey); +router.delete('/survey/:id', surveyController.deleteSurvey); router.get('/usage', usageController.getUsage); @@ -31,8 +32,8 @@ router.get('/seats/:id', SeatsController.getSeat); // TODO - remove this route router.get('/seats/activity/highcharts', SeatsController.getActivityHighcharts); -router.get('/teams', TeamsController.getAllTeams); -router.get('/members', TeamsController.getAllMembers); +router.get('/teams', teamsController.getAllTeams); +router.get('/members', teamsController.getAllMembers); router.get('/settings', settingsController.getAllSettings); router.post('/settings', settingsController.createSettings); @@ -47,6 +48,9 @@ router.get('/setup/status', setupController.setupStatus); router.get('/setup/manifest', setupController.getManifest); router.post('/setup/existing-app', setupController.addExistingApp); +router.get('/predictive-modeling/targets', targetValuesController.getTargetValues); +router.post('/predictive-modeling/targets', targetValuesController.updateTargetValues); + router.get('*', (req: Request, res: Response) => { res.status(404).send('Route not found'); }); diff --git a/backend/src/services/target-values.service.ts b/backend/src/services/target-values.service.ts new file mode 100644 index 0000000..0f904f8 --- /dev/null +++ b/backend/src/services/target-values.service.ts @@ -0,0 +1,22 @@ +import { TargetValues } from '../models/target-values.model.js'; + +class TargetValuesService { + async getTargetValues() { + return await TargetValues.findAll(); + } + + async updateTargetValues(data: { targetedRoomForImprovement: number, targetedNumberOfDevelopers: number, targetedPercentOfTimeSaved: number }) { + const [targetValues] = await TargetValues.findOrCreate({ + where: {}, + defaults: data + }); + + if (!targetValues.isNewRecord) { + await targetValues.update(data); + } + + return targetValues; + } +} + +export default new TargetValuesService(); diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 5d93214..6678624 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -9,10 +9,10 @@ import { CopilotDashboardComponent } from './main/copilot/copilot-dashboard/dash import { CopilotValueComponent } from './main/copilot/copilot-value/value.component'; import { CopilotMetricsComponent } from './main/copilot/copilot-metrics/copilot-metrics.component'; import { CopilotSeatsComponent } from './main/copilot/copilot-seats/copilot-seats.component'; -import { CopilotCalculatorComponent } from './main/copilot/copilot-calculator/copilot-calculator.component'; import { DbLoadingComponent } from './install/db-loading/db-loading.component'; import { CopilotSurveyComponent } from './main/copilot/copilot-surveys/copilot-survey-details/copilot-survey.component'; import { CopilotSeatComponent } from './main/copilot/copilot-seats/copilot-seat/copilot-seat.component'; +import { PredictiveModelingComponent } from './main/copilot/predictive-modeling/predictive-modeling.component'; export const routes: Routes = [ { path: 'setup', component: InstallComponent }, @@ -28,13 +28,13 @@ export const routes: Routes = [ { path: 'copilot/metrics', component: CopilotMetricsComponent, title: 'Metrics' }, { path: 'copilot/seats', component: CopilotSeatsComponent, title: 'Seats' }, { path: 'copilot/seats/:id', component: CopilotSeatComponent, title: 'Seat' }, - { path: 'copilot/calculator', component: CopilotCalculatorComponent, title: 'Calculator' }, { path: 'copilot/surveys', component: CopilotSurveysComponent, title: 'Surveys' }, { path: 'copilot/surveys/new/:id', component: NewCopilotSurveyComponent, title: 'New Survey' }, { path: 'copilot/surveys/:id', component: CopilotSurveyComponent, title: 'Survey' }, + { path: 'copilot/predictive-modeling', component: PredictiveModelingComponent, title: 'Predictive Modeling' }, { path: 'settings', component: SettingsComponent, title: 'Settings' }, { path: '', redirectTo: 'copilot', pathMatch: 'full' } ] }, { path: '**', redirectTo: '' } -]; \ No newline at end of file +]; diff --git a/frontend/src/app/main/copilot/copilot-calculator/copilot-calculator.component.html b/frontend/src/app/main/copilot/copilot-calculator/copilot-calculator.component.html deleted file mode 100644 index 487a2d9..0000000 --- a/frontend/src/app/main/copilot/copilot-calculator/copilot-calculator.component.html +++ /dev/null @@ -1,6 +0,0 @@ -
- - -
\ No newline at end of file diff --git a/frontend/src/app/main/copilot/copilot-calculator/copilot-calculator.component.scss b/frontend/src/app/main/copilot/copilot-calculator/copilot-calculator.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/app/main/copilot/copilot-calculator/copilot-calculator.component.spec.ts b/frontend/src/app/main/copilot/copilot-calculator/copilot-calculator.component.spec.ts deleted file mode 100644 index b6181c4..0000000 --- a/frontend/src/app/main/copilot/copilot-calculator/copilot-calculator.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { CopilotCalculatorComponent } from './copilot-calculator.component'; - -describe('CalculatorComponent', () => { - let component: CopilotCalculatorComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CopilotCalculatorComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(CopilotCalculatorComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/main/copilot/copilot-calculator/copilot-calculator.component.ts b/frontend/src/app/main/copilot/copilot-calculator/copilot-calculator.component.ts deleted file mode 100644 index 2a93ac4..0000000 --- a/frontend/src/app/main/copilot/copilot-calculator/copilot-calculator.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-calculator', - standalone: true, - imports: [], - templateUrl: './copilot-calculator.component.html', - styleUrl: './copilot-calculator.component.scss' -}) -export class CopilotCalculatorComponent { - -} diff --git a/frontend/src/app/main/copilot/predictive-modeling/predictive-modeling.component.html b/frontend/src/app/main/copilot/predictive-modeling/predictive-modeling.component.html new file mode 100644 index 0000000..111e78a --- /dev/null +++ b/frontend/src/app/main/copilot/predictive-modeling/predictive-modeling.component.html @@ -0,0 +1,60 @@ +
+ +
+
+
+
+
+

Settings

+
+ + Developer Count + + + + Dev Cost Per Year + + + + Hours Per Year + + + + Percent Coding + + + + Percent Time Saved + + +
+
+
+

Targets

+
+ + Number of Developers + + + + Room for Improvement + + + + Time saved + + +
+
+
+
+

Calculated Fields

+ +
+
+ +
\ No newline at end of file diff --git a/frontend/src/app/main/copilot/predictive-modeling/predictive-modeling.component.scss b/frontend/src/app/main/copilot/predictive-modeling/predictive-modeling.component.scss new file mode 100644 index 0000000..ec112e0 --- /dev/null +++ b/frontend/src/app/main/copilot/predictive-modeling/predictive-modeling.component.scss @@ -0,0 +1,17 @@ +.predictive-modeling-container { + display: grid; + grid-template-columns: 1fr 1fr; // Two equal columns + gap: 20px; // Space between columns +} + +.left-column, .right-column { + width: 100%; // Full width within grid cell +} + +.settings-section, .targets-section { + margin-bottom: 20px; +} + +mat-form-field { + width: 100%; +} diff --git a/frontend/src/app/main/copilot/predictive-modeling/predictive-modeling.component.spec.ts b/frontend/src/app/main/copilot/predictive-modeling/predictive-modeling.component.spec.ts new file mode 100644 index 0000000..582252c --- /dev/null +++ b/frontend/src/app/main/copilot/predictive-modeling/predictive-modeling.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PredictiveModelingComponent } from './predictive-modeling.component'; + +describe('PredictiveModelingComponent', () => { + let component: PredictiveModelingComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PredictiveModelingComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PredictiveModelingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/main/copilot/predictive-modeling/predictive-modeling.component.ts b/frontend/src/app/main/copilot/predictive-modeling/predictive-modeling.component.ts new file mode 100644 index 0000000..c6949c3 --- /dev/null +++ b/frontend/src/app/main/copilot/predictive-modeling/predictive-modeling.component.ts @@ -0,0 +1,70 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { PredictiveModelingService } from '../../../services/predictive-modeling.service'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { AppModule } from '../../../app.module'; +import { CommonModule } from '@angular/common'; +import { SettingsHttpService } from '../../../services/settings.service'; + +@Component({ + standalone: true, + selector: 'app-predictive-modeling', + templateUrl: './predictive-modeling.component.html', + styleUrls: ['./predictive-modeling.component.scss'], + imports: [ + AppModule, + CommonModule, + ] +}) +export class PredictiveModelingComponent implements OnInit { + settingsForm = new FormGroup({ + developerCount: new FormControl({ value: '', disabled: true }), + devCostPerYear: new FormControl({ value: '', disabled: true }), + hoursPerYear: new FormControl({ value: '', disabled: true }), + percentCoding: new FormControl({ value: '', disabled: true }), + percentTimeSaved: new FormControl({ value: '', disabled: true }) + }) + targetForm = new FormGroup({ + targetedRoomForImprovement: new FormControl('', [Validators.required, Validators.min(0)]), + targetedNumberOfDevelopers: new FormControl('', [Validators.required, Validators.min(0)]), + targetedPercentOfTimeSaved: new FormControl('', [Validators.required, Validators.min(0), Validators.max(100)]), + }); + + constructor( + private predictiveModelingService: PredictiveModelingService, + private settingsService: SettingsHttpService, + private snackBar: MatSnackBar + ) { } + + ngOnInit(): void { + this.loadSettings(); + this.loadTargets(); + } + + loadSettings(): void { + this.settingsService.getAllSettings().subscribe(settings => { + this.settingsForm.patchValue({ + developerCount: settings.developerCount, + devCostPerYear: settings.devCostPerYear, + hoursPerYear: settings.hoursPerYear, + percentCoding: settings.percentCoding, + percentTimeSaved: settings.percentTimeSaved, + }); + }); + } + + loadTargets(): void { + this.predictiveModelingService.getTargets().subscribe(targets => { + this.targetForm.patchValue(targets); + }); + } + + saveTargets(): void { + const targets = this.targetForm.value; + this.predictiveModelingService.saveTargets(targets).subscribe(() => { + this.snackBar.open('Targets saved successfully', 'Close', { + duration: 2000, + }); + }); + } +} diff --git a/frontend/src/app/main/main.component.html b/frontend/src/app/main/main.component.html index 1836fec..46a6ad9 100644 --- a/frontend/src/app/main/main.component.html +++ b/frontend/src/app/main/main.component.html @@ -32,9 +32,9 @@ Surveys - + calculate - Calculator + Predictive Modeling @@ -62,4 +62,4 @@ - \ No newline at end of file + diff --git a/frontend/src/app/services/predictive-modeling.service.ts b/frontend/src/app/services/predictive-modeling.service.ts new file mode 100644 index 0000000..d94a6f8 --- /dev/null +++ b/frontend/src/app/services/predictive-modeling.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { serverUrl } from './server.service'; + +@Injectable({ + providedIn: 'root' +}) +export class PredictiveModelingService { + private apiUrl = `${serverUrl}/api/predictive-modeling`; + + constructor(private http: HttpClient) {} + + getTargets() { + return this.http.get(`${this.apiUrl}/targets`); + } + + saveTargets(targets: any) { + return this.http.post(`${this.apiUrl}/targets`, targets); + } + + getCalculatedFields() { + return this.http.get(`${this.apiUrl}/calculated-fields`); + } +} diff --git a/frontend/src/app/services/settings.service.ts b/frontend/src/app/services/settings.service.ts index fce6264..b879871 100644 --- a/frontend/src/app/services/settings.service.ts +++ b/frontend/src/app/services/settings.service.ts @@ -12,6 +12,12 @@ export interface Settings { baseUrl?: string | null; webhookProxyUrl?: string | null; webhookSecret?: string | null; + developerTotal?: string | null; + adopterCount?: string | null; + perLicenseCost?: string | null; + perDevCostPerYear?: string | null; + perDevHoursPerYear?: string | null; + percentofHoursCoding?: string | null; [key: string]: string | null | undefined; } @@ -42,4 +48,5 @@ export class SettingsHttpService { deleteSettings(name: string) { return this.http.delete(`${this.apiUrl}/${name}`); } -} \ No newline at end of file + +}