diff --git a/backend/Value-Modeling.png b/backend/Value-Modeling.png new file mode 100644 index 0000000..3be7b53 Binary files /dev/null and b/backend/Value-Modeling.png differ diff --git a/frontend/src/app/main/copilot/value-modeling/grid-object.model.ts b/frontend/src/app/main/copilot/value-modeling/grid-object.model.ts new file mode 100644 index 0000000..939bcb0 --- /dev/null +++ b/frontend/src/app/main/copilot/value-modeling/grid-object.model.ts @@ -0,0 +1,46 @@ +export interface MetricState { + seats: number; + adoptedDevs: number; + monthlyDevsReportingTimeSavings: number; + percentSeatsReportingTimeSavings: number; + percentSeatsAdopted: number; + percentMaxAdopted: number; + dailySuggestions: number; + dailyChatTurns: number; + weeklyPRSummaries: number; + weeklyTimeSaved: number; + monthlyTimeSavings: number; + annualTimeSavingsDollars: number; + productivityBoost: number; + [key: string]: number; // Index signature +} + +export interface GridObject { + current: MetricState; + target: MetricState; + max: MetricState; +} + +export function initializeGridObject(): GridObject { + const defaultMetricState: MetricState = { + seats: 0, + adoptedDevs: 0, + monthlyDevsReportingTimeSavings: 0, + percentSeatsReportingTimeSavings: 0, + percentSeatsAdopted: 0, + percentMaxAdopted: 0, + dailySuggestions: 0, + dailyChatTurns: 0, + weeklyPRSummaries: 0, + weeklyTimeSaved: 0, + monthlyTimeSavings: 0, + annualTimeSavingsDollars: 0, + productivityBoost: 0 + }; + + return { + current: { ...defaultMetricState }, + target: { ...defaultMetricState }, + max: { ...defaultMetricState } + }; +} diff --git a/frontend/src/app/main/copilot/value-modeling/value-modeling.component.html b/frontend/src/app/main/copilot/value-modeling/value-modeling.component.html index 589c824..3f407c7 100644 --- a/frontend/src/app/main/copilot/value-modeling/value-modeling.component.html +++ b/frontend/src/app/main/copilot/value-modeling/value-modeling.component.html @@ -1,13 +1,18 @@
- Adopted Users + Org-Level Metrics
@@ -21,7 +26,7 @@

Value Modeling and Targeting

- Activity Per Daily User + Per Daily User Metrics
@@ -33,98 +38,332 @@

Value Modeling and Targeting

+
+ + +
- Reported Weekly Time Savings per Dev + Org-Level Metrics -
- - -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current LevelTarget LevelMax Level
Seats + + + + + + + + + + + +
Adopted Devs + + + + + + + + + + + +
Monthly Devs reporting Time Savings + + + + + + + + + + + +
% of Seats reporting Time Savings + + + % + + + + + % + + + + + % + +
% of Seats Adopted + + + % + + + + + % + + + + + % + +
% of Max Adopted + + + % + + + + + % + + + + + % + +
+ - Productivity Boost + Per Daily User Metrics -
- - -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current LevelTarget LevelMax Level
Daily Suggestions + + + + + + + + + + + +
Daily Chat Turns + + + + + + + + + + + +
Weekly PR Summaries + + + + + + + + + + + +
Weekly Time Saved (hrs) + + + hrs + + + + + hrs + + + + + hrs + +
-
- - - - -
-
Current Level
-
Target Level
-
Max Level
- - -
Adopted Users
- - - % - - - -
Activity Per Daily User
-
- - Activity - - % - - - Time Savings - - % - -
- -
Total Time Saved
- - - - - - -
Time Saved as Dollars
- - - % - + + + Calculated Impacts + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current LevelTarget LevelMax Level
Monthly Time Savings (hrs) + + + hrs + Invalid value + + + + + hrs + Invalid value + + + + + hrs + Invalid value + +
Annual Time Savings as Dollars ($) + + + + Invalid value + + + + + + Invalid value + + + + + + Invalid value + +
Productivity or Throughput Boost (%) + + + % + Invalid value + + + + + % + Invalid value + + + + + % + Invalid value + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/main/copilot/value-modeling/value-modeling.component.scss b/frontend/src/app/main/copilot/value-modeling/value-modeling.component.scss index 779f8d8..44ed6bd 100644 --- a/frontend/src/app/main/copilot/value-modeling/value-modeling.component.scss +++ b/frontend/src/app/main/copilot/value-modeling/value-modeling.component.scss @@ -5,12 +5,21 @@ } .page-header { + display: flex; + justify-content: space-between; + align-items: center; margin-bottom: 24px; + h1 { margin: 0; font-size: 24px; font-weight: 500; } + + .button-group { + display: flex; + gap: 16px; + } } .charts-grid { @@ -55,10 +64,58 @@ } } +.metrics-table { + display: grid; + grid-template-columns: 1fr; + gap: 24px; + + mat-card { + display: grid; + grid-template-columns: 1fr; + gap: 16px; + + table { + width: 100%; + border-collapse: collapse; + + th, td { + padding: 8px; + text-align: right; /* Right justify all content */ + width: 25%; /* Ensure consistent column widths */ + } + + th { + font-weight: 500; + } + + td { + mat-form-field { + width: 100%; + } + } + } + } +} + .example-right-align { text-align: right; } +.invalid { + border-color: red; +} + +mat-form-field[disabled] { + .mat-form-field-wrapper { + background-color: #f5f5f5; + box-shadow: none; + } + + .mat-input-element { + color: #9e9e9e; + } +} + @media (max-width: 768px) { .charts-grid { grid-template-columns: 1fr; @@ -75,4 +132,8 @@ grid-template-columns: 1fr; } } + + .metrics-table { + grid-template-columns: 1fr; + } } \ No newline at end of file diff --git a/frontend/src/app/main/copilot/value-modeling/value-modeling.component.ts b/frontend/src/app/main/copilot/value-modeling/value-modeling.component.ts index c8f850e..7e5eeaa 100644 --- a/frontend/src/app/main/copilot/value-modeling/value-modeling.component.ts +++ b/frontend/src/app/main/copilot/value-modeling/value-modeling.component.ts @@ -1,34 +1,23 @@ -import { Component, OnInit } from '@angular/core'; -import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { Component, OnInit, AfterViewInit } from '@angular/core'; +import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; import { MaterialModule } from '../../../material.module'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { CommonModule, DecimalPipe } from '@angular/common'; import { HighchartsChartModule } from 'highcharts-angular'; +import { SharedModule } from '../../../shared/shared.module'; import * as Highcharts from 'highcharts'; - -interface MetricState { - adoption: string; - usage: { - activity: string; - timeSavings: string; - }; - timeSavedDollars: string; - downstreamProductivity: string; -} - -interface Metrics { - current: MetricState; - target: MetricState; - max: MetricState; -} +import { GridObject, MetricState, initializeGridObject } from './grid-object.model'; @Component({ selector: 'app-value-modeling', standalone: true, imports: [ MaterialModule, + MatSlideToggleModule, CommonModule, ReactiveFormsModule, HighchartsChartModule, + SharedModule ], providers: [ DecimalPipe @@ -36,7 +25,7 @@ interface Metrics { templateUrl: './value-modeling.component.html', styleUrl: './value-modeling.component.scss' }) -export class ValueModelingComponent implements OnInit { +export class ValueModelingComponent implements OnInit, AfterViewInit { Highcharts: typeof Highcharts = Highcharts; // chartOptions: Highcharts.Options; adoptionChartOption: Highcharts.Options = { @@ -97,9 +86,9 @@ export class ValueModelingComponent implements OnInit { series: [{ type: 'bar', data: [ - this.calculateOverallImpact('current'), - this.calculateOverallImpact('target'), - this.calculateOverallImpact('max') + // this.calculateOverallImpact('current'), + // this.calculateOverallImpact('target'), + // this.calculateOverallImpact('max') ] }], xAxis: { @@ -115,71 +104,244 @@ export class ValueModelingComponent implements OnInit { //get 7 day moving average time savings (current time saved) // set up the initial model values based on the above and the settings from the API - model = { - adoption: { - current: 0, // - target: 0, - max: 0 - }, - activity: { - current: 0, - target: 0, - max: 0 - }, - timeSavings: { - current: 0, - target: 0, - max: 0 - }, - timeSavedDollars: { - current: 0, - target: 0, - max: 0 - }, - downstreamProductivity: { - current: 0, - target: 0, - max: 0 - } - }; + form = new FormGroup({ current: new FormGroup({ - adoption: new FormControl(20), - activityLevel: new FormControl(30), - timeSavings: new FormControl(15), - timeSavedDollars: new FormControl(50000), - downstreamProductivity: new FormControl(25) + seats: new FormControl('0', [Validators.required, Validators.min(0)]), + adoptedDevs: new FormControl('0', [Validators.required, Validators.min(0)]), + monthlyDevsReportingTimeSavings: new FormControl('0', [Validators.required, Validators.min(0)]), + percentSeatsReportingTimeSavings: new FormControl('0', [Validators.required, Validators.min(0), Validators.max(100)]), + percentSeatsAdopted: new FormControl('0', [Validators.required, Validators.min(0), Validators.max(100)]), + percentMaxAdopted: new FormControl('0', [Validators.required, Validators.min(0), Validators.max(100)]), + dailySuggestions: new FormControl('0', [Validators.required, Validators.min(0)]), + dailyChatTurns: new FormControl('0', [Validators.required, Validators.min(0)]), + weeklyPRSummaries: new FormControl('0', [Validators.required, Validators.min(0)]), + weeklyTimeSaved: new FormControl('0', [Validators.required, Validators.min(0)]), + monthlyTimeSavings: new FormControl('0', [Validators.required, Validators.min(0)]), + annualTimeSavingsDollars: new FormControl('0', [Validators.required, Validators.min(0)]), + productivityBoost: new FormControl('0', [Validators.required, Validators.min(0), Validators.max(100)]) }), target: new FormGroup({ - adoption: new FormControl(60), - activityLevel: new FormControl(70), - timeSavings: new FormControl(45), - timeSavedDollars: new FormControl(150000), - downstreamProductivity: new FormControl(75) + seats: new FormControl('0', [Validators.required, Validators.min(0)]), + adoptedDevs: new FormControl('0', [Validators.required, Validators.min(0)]), + monthlyDevsReportingTimeSavings: new FormControl('0', [Validators.required, Validators.min(0)]), + percentSeatsReportingTimeSavings: new FormControl('0', [Validators.required, Validators.min(0), Validators.max(100)]), + percentSeatsAdopted: new FormControl('0', [Validators.required, Validators.min(0), Validators.max(100)]), + percentMaxAdopted: new FormControl('0', [Validators.required, Validators.min(0), Validators.max(100)]), + dailySuggestions: new FormControl('0', [Validators.required, Validators.min(0)]), + dailyChatTurns: new FormControl('0', [Validators.required, Validators.min(0)]), + weeklyPRSummaries: new FormControl('0', [Validators.required, Validators.min(0)]), + weeklyTimeSaved: new FormControl('0', [Validators.required, Validators.min(0)]), + monthlyTimeSavings: new FormControl('0', [Validators.required, Validators.min(0)]), + annualTimeSavingsDollars: new FormControl('0', [Validators.required, Validators.min(0)]), + productivityBoost: new FormControl('0', [Validators.required, Validators.min(0), Validators.max(100)]) }), max: new FormGroup({ - adoption: new FormControl(99), - activityLevel: new FormControl(99), - timeSavings: new FormControl(99), - timeSavedDollars: new FormControl(99), - downstreamProductivity: new FormControl(99) + seats: new FormControl('0', [Validators.required, Validators.min(0)]), + adoptedDevs: new FormControl('0', [Validators.required, Validators.min(0)]), + monthlyDevsReportingTimeSavings: new FormControl('0', [Validators.required, Validators.min(0)]), + percentSeatsReportingTimeSavings: new FormControl('0', [Validators.required, Validators.min(0), Validators.max(100)]), + percentSeatsAdopted: new FormControl('0', [Validators.required, Validators.min(0), Validators.max(100)]), + percentMaxAdopted: new FormControl('0', [Validators.required, Validators.min(0), Validators.max(100)]), + dailySuggestions: new FormControl('0', [Validators.required, Validators.min(0)]), + dailyChatTurns: new FormControl('0', [Validators.required, Validators.min(0)]), + weeklyPRSummaries: new FormControl('0', [Validators.required, Validators.min(0)]), + weeklyTimeSaved: new FormControl('0', [Validators.required, Validators.min(0)]), + monthlyTimeSavings: new FormControl('0', [Validators.required, Validators.min(0)]), + annualTimeSavingsDollars: new FormControl('0', [Validators.required, Validators.min(0)]), + productivityBoost: new FormControl('0', [Validators.required, Validators.min(0), Validators.max(100)]) }) }); + gridObject: GridObject = initializeGridObject(); + gridObjectSaved: GridObject = initializeGridObject(); + disableInputs = false; + constructor( private decimalPipe: DecimalPipe ) {} ngOnInit() { + console.log('Component loaded'); + console.log('ngOnInit: Initializing component'); + console.log('Initial gridObject:', this.gridObject); // this.updateChartData(); // this.form.valueChanges.subscribe(() => this.updateChartData()); + this.form.valueChanges.subscribe((values) => { + console.log('1*. Form value changed:', values); + // Example update logic for internal state + // this.model.adoption.current = values.current.adoption; + // this.model.adoption.target = values.target.adoption; + // this.model.adoption.max = values.max.adoption; + // ...existing code... + }); + } + + ngAfterViewInit() { + console.log('0. ngAfterViewInit: View initialized'); + this.makeEventListenersPassive(); + } + + private makeEventListenersPassive() { + const elements = document.querySelectorAll('.highcharts-container'); + elements.forEach(element => { + element.addEventListener('touchstart', () => {}, { passive: true }); + }); + console.log('0. makeEventListenersPassive: Event listeners set to passive'); + } + + onBlur(event: Event, level: 'current' | 'target' | 'max', field: keyof MetricState) { + const input = event.target as HTMLInputElement; + const value = parseFloat(input.value.replace(/,/g, '').replace(/[^0-9.-]+/g, '')); + this.gridObject[level][field] = isNaN(value) ? 0 : value; + console.log(`2. onBlur: Updated gridObject[${level}][${field}] to`, this.gridObject[level][field]); + this.modelCalc(); + // print out gridObject for debugging + // console.log('Updated gridObject:', this.gridObject); + } + + loadGridObject() { + // Stub: Load the gridObject from a data source + console.log('loadGridObject: Loading saved gridObject',this.gridObjectSaved); + this.gridObject = this.gridObjectSaved; + this.modelCalc(); + //this.updateFormFromGridObject(); + console.log('Loaded gridObject:', this.gridObject); + } + + saveGridObject() { + // Stub: Save the gridObject to a data source + + this.updateGridObjectFromForm(); + this.modelCalc(); + //This needs to be a deep copy. If we do a shallow copy, the gridObjectSaved will be updated whenever the gridObject is updated. + this.gridObjectSaved = JSON.parse(JSON.stringify(this.gridObject)); + console.log('7. Saved gridObject:', this.gridObjectSaved); + } + + private updateFormFromGridObject() { + //console.log('updateFormFromGridObject: Updating form from gridObject'); + this.form.patchValue({ + current: this.convertMetricStateToString(this.gridObject.current), + target: this.convertMetricStateToString(this.gridObject.target), + max: this.convertMetricStateToString(this.gridObject.max) + }); + console.log('6. Updated form values:', this.form.value); + } + + private updateGridObjectFromForm() { + //console.log('updateGridObjectFromForm: Updating gridObject from form'); + const currentFormValue = this.form.get('current')?.value || {}; + const targetFormValue = this.form.get('target')?.value || {}; + const maxFormValue = this.form.get('max')?.value || {}; + + this.gridObject.current = this.convertMetricStateToNumber(currentFormValue); + this.gridObject.target = this.convertMetricStateToNumber(targetFormValue); + this.gridObject.max = this.convertMetricStateToNumber(maxFormValue); + // print out gridObject for debugging + console.log('6. Updated gridObject from form:', this.gridObject); + } + + private convertMetricStateToString(metricState: MetricState): { [key: string]: string } { + const result: { [key: string]: string } = {}; + for (const key in metricState) { + if (metricState.hasOwnProperty(key)) { + result[key] = this.decimalPipe.transform(metricState[key], '1.0-0') || '0'; + console.log('called convertMetricStateToString:', key.toString); + } + } + return result; + } + + private convertMetricStateToNumber(metricState: { [key: string]: string }): MetricState { + const result: MetricState = { + seats: 0, + adoptedDevs: 0, + monthlyDevsReportingTimeSavings: 0, + percentSeatsReportingTimeSavings: 0, + percentSeatsAdopted: 0, + percentMaxAdopted: 0, + dailySuggestions: 0, + dailyChatTurns: 0, + weeklyPRSummaries: 0, + weeklyTimeSaved: 0, + monthlyTimeSavings: 0, + annualTimeSavingsDollars: 0, + productivityBoost: 0 + }; + for (const key in metricState) { + if (metricState.hasOwnProperty(key)) { + const value = parseFloat(metricState[key].replace(/,/g, '').replace(/[^0-9.-]+/g, '')); + result[key as keyof MetricState] = isNaN(value) ? 0 : value; + } + } + return result; } - private calculateOverallImpact(level: 'current' | 'target' | 'max'): number { - const values = this.form?.get(level)?.value; - if (!values) return 0; + modelCalc() { + try { + console.log('3. modelCalc: Calculating model'); + // 1. Calculate Max column percentages and then Impacts + this.gridObject.max.percentSeatsAdopted = this.calculatePercentage(this.gridObject.max.adoptedDevs, this.gridObject.max.seats); + this.gridObject.max.percentSeatsReportingTimeSavings = this.calculatePercentage(this.gridObject.max.monthlyDevsReportingTimeSavings, this.gridObject.max.seats); + this.gridObject.max.percentMaxAdopted = this.calculatePercentage(this.gridObject.max.adoptedDevs, this.gridObject.max.seats); + this.gridObject.max.annualTimeSavingsDollars = this.calculateAnnualTimeSavingsDollars(this.gridObject.max.weeklyTimeSaved, this.gridObject.max.adoptedDevs); + this.gridObject.max.monthlyTimeSavings = this.calculateMonthlyTimeSavings(this.gridObject.max.adoptedDevs, this.gridObject.max.weeklyTimeSaved); + this.gridObject.max.productivityBoost = this.calculateProductivityBoost(this.gridObject.max.dailySuggestions, this.gridObject.max.dailyChatTurns); - const usageAvg = (Number(values.activityLevel) + Number(values.timeSavings)) / 2; - return (Number(values.adoption) * usageAvg * Number(values.downstreamProductivity)) / 10000; + // 2. Calculate Current column percentages and then Impacts + this.gridObject.current.percentSeatsAdopted = this.calculatePercentage(this.gridObject.current.adoptedDevs, this.gridObject.current.seats); + this.gridObject.current.percentSeatsReportingTimeSavings = this.calculatePercentage(this.gridObject.current.monthlyDevsReportingTimeSavings, this.gridObject.current.seats); + this.gridObject.current.percentMaxAdopted = this.calculatePercentage(this.gridObject.current.adoptedDevs, this.gridObject.current.seats); + this.gridObject.current.annualTimeSavingsDollars = this.calculateAnnualTimeSavingsDollars(this.gridObject.current.weeklyTimeSaved, this.gridObject.current.adoptedDevs); + this.gridObject.current.monthlyTimeSavings = this.calculateMonthlyTimeSavings(this.gridObject.current.adoptedDevs, this.gridObject.current.weeklyTimeSaved); + this.gridObject.current.productivityBoost = this.calculateProductivityBoost(this.gridObject.current.dailySuggestions, this.gridObject.current.dailyChatTurns); + + // 3. Calculate Target column values (percentages and then impacts) + this.gridObject.target.percentSeatsAdopted = this.calculatePercentage(this.gridObject.target.adoptedDevs, this.gridObject.target.seats); + this.gridObject.target.percentSeatsReportingTimeSavings = this.calculatePercentage(this.gridObject.target.monthlyDevsReportingTimeSavings, this.gridObject.target.seats); + this.gridObject.target.percentMaxAdopted = this.calculatePercentage(this.gridObject.target.adoptedDevs, this.gridObject.target.seats); + this.gridObject.target.annualTimeSavingsDollars = this.calculateAnnualTimeSavingsDollars(this.gridObject.target.weeklyTimeSaved, this.gridObject.target.adoptedDevs); + this.gridObject.target.monthlyTimeSavings = this.calculateMonthlyTimeSavings(this.gridObject.target.adoptedDevs, this.gridObject.target.weeklyTimeSaved); + this.gridObject.target.productivityBoost = this.calculateProductivityBoost(this.gridObject.target.dailySuggestions, this.gridObject.target.dailyChatTurns); +// 4. Update the form values + + console.log('4. modelCalc: Updated gridObject:', this.gridObject); + this.updateFormFromGridObject(); + } catch (error) { + const errorMessage = (error instanceof Error) ? error.message : 'An unknown error occurred'; + console.error(`5. Error in ModelCalc: ${errorMessage}`); + alert(`Error in ModelCalc: ${errorMessage}`); + } } + + private calculatePercentage(numerator: number, denominator: number): number { + if (denominator === 0) { + return 0; + } + return (numerator / denominator) * 100; + } + + private calculateAnnualTimeSavingsDollars(weeklyTimeSaved: number, adoptedDevs: number): number { + const weeksInYear = 50; // TO DO: needs to come from settings + const hourlyRate = 50; // TO DO: needs to come from settings + return weeklyTimeSaved * weeksInYear * hourlyRate * adoptedDevs; + } + + private calculateProductivityBoost(dailySuggestions: number, dailyChatTurns: number): number { + return dailySuggestions + dailyChatTurns; // Example calculation + } + + private calculateMonthlyTimeSavings(adoptedDevs: number, weeklyTimeSaved: number): number { + return adoptedDevs * weeklyTimeSaved * 4; + } + + toggleInputs(disable: boolean) { + if (disable) { + this.disableInputs = true; + } else { + this.disableInputs = false; + } + console.log('disableInputs:', this.disableInputs); +} } \ No newline at end of file diff --git a/frontend/src/app/shared/directives/thousand-separator.directive.ts b/frontend/src/app/shared/directives/thousand-separator.directive.ts new file mode 100644 index 0000000..2e053ce --- /dev/null +++ b/frontend/src/app/shared/directives/thousand-separator.directive.ts @@ -0,0 +1,26 @@ +import { Directive, ElementRef, HostListener } from '@angular/core'; + +@Directive({ + selector: '[matThousandSeparator]' +}) +export class ThousandSeparatorDirective { + private regex: RegExp = new RegExp(/^\d+$/); + + constructor(private el: ElementRef) {} + + @HostListener('input', ['$event']) + onInputChange(event: Event) { + const input = event.target as HTMLInputElement; + let value = input.value.replace(/,/g, ''); + if (this.regex.test(value)) { + value = this.formatNumber(value); + input.value = value; + } else { + input.value = input.value.slice(0, -1); + } + } + + private formatNumber(value: string): string { + return value.replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } +} diff --git a/frontend/src/app/shared/pipes/currency.pipe.ts b/frontend/src/app/shared/pipes/currency.pipe.ts new file mode 100644 index 0000000..24d19ef --- /dev/null +++ b/frontend/src/app/shared/pipes/currency.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'matCurrencyPipe' +}) +export class CurrencyPipe implements PipeTransform { + transform(value: number | string): string { + if (typeof value === 'string') { + value = parseFloat(value.replace(/,/g, '')); + } + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value); + } +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts new file mode 100644 index 0000000..a7aed3f --- /dev/null +++ b/frontend/src/app/shared/shared.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ThousandSeparatorDirective } from './directives/thousand-separator.directive'; +import { CurrencyPipe } from './pipes/currency.pipe'; + +@NgModule({ + declarations: [ThousandSeparatorDirective, CurrencyPipe], + imports: [CommonModule], + exports: [ThousandSeparatorDirective, CurrencyPipe] +}) +export class SharedModule {}