diff --git a/package-lock.json b/package-lock.json index b4a73c2..7e4cdc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1171,6 +1171,11 @@ "semver-intersect": "1.4.0" } }, + "@types/chart.js": { + "version": "2.7.52", + "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.7.52.tgz", + "integrity": "sha512-h4Md9c0FYPoqHyPeo3sG+wBIDFGz6GubulKUopsmFkSSW2ieyI2phjlj+FjqzTwhrWwR9dbw/HlCW3axj+tWug==" + }, "@types/jasmine": { "version": "2.8.16", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.16.tgz", @@ -2626,6 +2631,39 @@ "integrity": "sha1-5upnvSR+EHESmDt6sEee02KAAIE=", "dev": true }, + "chart.js": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.8.0.tgz", + "integrity": "sha512-Di3wUL4BFvqI5FB5K26aQ+hvWh8wnP9A3DWGvXHVkO13D3DSnaSsdZx29cXlEsYKVkn1E2az+ZYFS4t0zi8x0w==", + "requires": { + "chartjs-color": "2.3.0", + "moment": "2.24.0" + } + }, + "chartjs-color": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.3.0.tgz", + "integrity": "sha512-hEvVheqczsoHD+fZ+tfPUE+1+RbV6b+eksp2LwAhwRTVXEjCSEavvk+Hg3H6SZfGlPh/UfmWKGIvZbtobOEm3g==", + "requires": { + "chartjs-color-string": "0.6.0", + "color-convert": "0.5.3" + }, + "dependencies": { + "color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" + } + } + }, + "chartjs-color-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", + "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", + "requires": { + "color-name": "1.1.1" + } + }, "chokidar": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", @@ -2862,8 +2900,7 @@ "color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", - "dev": true + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" }, "colors": { "version": "1.1.2", @@ -7989,8 +8026,7 @@ "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, "lodash._isnative": { "version": "2.4.1", @@ -8572,6 +8608,11 @@ "minimist": "0.0.8" } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "morgan": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", @@ -8687,6 +8728,16 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, + "ng2-charts": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-2.2.3.tgz", + "integrity": "sha512-Kxj2bewn537xGFVkR7AgDmfqV+YH4hIL4R36EjlUI9WCWnphzY+VKZGX+D+usXd8e+znuqly+sbGHjddLxupUA==", + "requires": { + "@types/chart.js": "2.7.52", + "lodash": "4.17.11", + "tslib": "1.9.3" + } + }, "ng2-currency-mask": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ng2-currency-mask/-/ng2-currency-mask-5.3.1.tgz", diff --git a/package.json b/package.json index 21b0034..6d3a2ff 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "start": "ng serve", + "start": "ng serve --host '0.0.0.0'", "build": "ng build", "publish": "ng build --prod --service-worker && firebase deploy", "test": "ng test", @@ -25,10 +25,12 @@ "@angular/pwa": "^0.7.5", "@angular/router": "^7.2.14", "@angular/service-worker": "^7.2.14", + "chart.js": "^2.8.0", "core-js": "^2.6.5", "dexie": "^2.0.4", "firebase": "^5.11.1", "hammerjs": "^2.0.8", + "ng2-charts": "^2.2.3", "ng2-currency-mask": "^5.3.1", "rxjs": "^6.5.1", "zone.js": "^0.8.29" diff --git a/src/app/accounts/account-details/account-details.component.html b/src/app/accounts/account-details/account-details.component.html index 44a0fec..4ea2079 100644 --- a/src/app/accounts/account-details/account-details.component.html +++ b/src/app/accounts/account-details/account-details.component.html @@ -5,33 +5,41 @@ {{ getBalance() / 100 | currency }} + +
- View Transactions + View Transactions

Income

- Add Category + Add Category
- + add

Add categories to gain more insights into your income.

- +
+ + +

Expenses

- Add Category + Add Category
- + add

Add categories to gain more insights into your expenses.

- +
+ + +
- + add \ No newline at end of file diff --git a/src/app/accounts/account-details/account-details.component.ts b/src/app/accounts/account-details/account-details.component.ts index e631487..904d4ac 100644 --- a/src/app/accounts/account-details/account-details.component.ts +++ b/src/app/accounts/account-details/account-details.component.ts @@ -9,6 +9,8 @@ import { Observable } from 'rxjs'; import { TransactionType } from 'src/app/transactions/transaction.type'; import { TRANSACTION_SERVICE, TransactionService } from 'src/app/transactions/transaction.service'; import { CATEGORY_SERVICE, CategoryService } from 'src/app/categories/category.service'; +import { Label } from 'ng2-charts'; +import { ChartDataSets } from 'chart.js'; @Component({ selector: 'app-account-details', @@ -22,6 +24,15 @@ export class AccountDetailsComponent implements OnInit { public expenses: Category[] = []; public income: Category[] = []; categoryBalances: Map; + expectedIncome = 0; + actualIncome = 0; + expectedExpenses = 0; + actualExpenses = 0; + barChartLabels: Label[] = ['Income', 'Expenses']; + barChartData: ChartDataSets[] = [ + { data: [0, 0], label: 'Expected' }, + { data: [0, 0], label: 'Actual' }, + ]; constructor( private app: AppComponent, @@ -49,6 +60,27 @@ export class AccountDetailsComponent implements OnInit { }); } + updateBarChart() { + const color = [0, 188, 212]; + this.barChartData = [ + { + data: [this.expectedIncome / 100, this.expectedExpenses / 100], + label: 'Expected', + backgroundColor: 'rgba(241, 241, 241, 0.8)', + borderColor: 'rgba(241, 241, 241, 0.9)', + hoverBackgroundColor: 'rgba(241, 241, 241, 1)', + hoverBorderColor: 'rgba(241, 241, 241, 1)', + }, + { + data: [this.actualIncome / 100, this.actualExpenses / 100], + label: 'Actual', + backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.8)`, + borderColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.9)`, + hoverBackgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 1)`, + hoverBorderColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 1)` + } + ]; + } getBalance(): number { let totalBalance = 0; @@ -68,13 +100,38 @@ export class AccountDetailsComponent implements OnInit { getCategories(): void { this.categoryService.getCategories(this.account.id).subscribe(categories => { + const categoryBalances = new Map(); + let categoryBalancesCount = 0; for (const category of categories) { if (category.isExpense) { this.expenses.push(category); + this.expectedExpenses += category.amount; } else { this.income.push(category); + this.expectedIncome += category.amount; } - this.getCategoryBalance(category.id).subscribe(balance => this.categoryBalances.set(category.id, balance)); + this.getCategoryBalance(category.id).subscribe( + balance => { + console.log(balance); + if (category.isExpense) { + this.actualExpenses += balance * -1; + } else { + this.actualIncome += balance; + } + categoryBalances.set(category.id, balance); + categoryBalancesCount++; + }, + error => { categoryBalancesCount++; }, + () => { + // This weird workaround is to force the OnChanges callback to be fired. + // Angular needs the reference to the object to change in order for it to + // work. + if (categoryBalancesCount === categories.length) { + this.categoryBalances = categoryBalances; + this.updateBarChart(); + } + } + ); } }); } @@ -91,6 +148,7 @@ export class AccountDetailsComponent implements OnInit { } } subscriber.next(balance); + subscriber.complete(); }); }); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 934dd8c..2fa9511 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -48,6 +48,8 @@ import { ACCOUNT_SERVICE } from './accounts/account.service'; import { FirestoreAccountService } from './accounts/account.service.firestore'; import { USER_SERVICE } from './users/user.service'; import { FirestoreUserService } from './users/user.service.firestore'; +import { CategoryBreakdownComponent } from './categories/category-breakdown/category-breakdown.component'; +import { ChartsModule } from 'ng2-charts'; export const CustomCurrencyMaskConfig: CurrencyMaskConfig = { align: 'left', @@ -79,6 +81,7 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = { NewAccountComponent, AccountDetailsComponent, AccountsComponent, + CategoryBreakdownComponent, ], imports: [ BrowserModule, @@ -99,6 +102,7 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = { ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }), HttpClientModule, CurrencyMaskModule, + ChartsModule, ], providers: [ { provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig }, diff --git a/src/app/categories/category-breakdown/category-breakdown.component.css b/src/app/categories/category-breakdown/category-breakdown.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/categories/category-breakdown/category-breakdown.component.html b/src/app/categories/category-breakdown/category-breakdown.component.html new file mode 100644 index 0000000..bb9be6b --- /dev/null +++ b/src/app/categories/category-breakdown/category-breakdown.component.html @@ -0,0 +1,9 @@ +
+ + +
\ No newline at end of file diff --git a/src/app/categories/category-breakdown/category-breakdown.component.spec.ts b/src/app/categories/category-breakdown/category-breakdown.component.spec.ts new file mode 100644 index 0000000..104d707 --- /dev/null +++ b/src/app/categories/category-breakdown/category-breakdown.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CategoryBreakdownComponent } from './category-breakdown.component'; + +describe('CategoryBreakdownComponent', () => { + let component: CategoryBreakdownComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CategoryBreakdownComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CategoryBreakdownComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/categories/category-breakdown/category-breakdown.component.ts b/src/app/categories/category-breakdown/category-breakdown.component.ts new file mode 100644 index 0000000..c58b6dd --- /dev/null +++ b/src/app/categories/category-breakdown/category-breakdown.component.ts @@ -0,0 +1,45 @@ +import { Component, OnInit, Input, OnChanges, SimpleChanges, SimpleChange, ViewChild } from '@angular/core'; +import { Category } from '../category'; +import { CategoriesComponent } from '../categories.component'; +import { ChartOptions, ChartType, ChartDataSets } from 'chart.js'; +import { BaseChartDirective, Label } from 'ng2-charts'; + +@Component({ + selector: 'app-category-breakdown', + templateUrl: './category-breakdown.component.html', + styleUrls: ['./category-breakdown.component.css'] +}) +export class CategoryBreakdownComponent implements OnInit, OnChanges { + barChartOptions: ChartOptions = { + responsive: true, + maintainAspectRatio: false, + scales: { + xAxes: [{ + ticks: { + beginAtZero: true + } + }], yAxes: [{}] + }, + }; + @Input() barChartLabels: Label[]; + @Input() barChartData: ChartDataSets[] = [ + { data: [0, 0, 0, 0], label: '' }, + ]; + barChartType: ChartType = 'horizontalBar'; + barChartLegend = true; + @ViewChild(BaseChartDirective) chart: BaseChartDirective; + + constructor() { } + + ngOnInit() { } + + ngOnChanges(changes: SimpleChanges): void { + console.log(changes); + if (changes.barChartLabels) { + this.barChartLabels = changes.barChartLabels.currentValue; + } + if (changes.barChartData) { + this.barChartData = changes.barChartData.currentValue; + } + } +} diff --git a/src/app/transactions/add-edit-transaction/add-edit-transaction.component.html b/src/app/transactions/add-edit-transaction/add-edit-transaction.component.html index ebe3553..33bc786 100644 --- a/src/app/transactions/add-edit-transaction/add-edit-transaction.component.html +++ b/src/app/transactions/add-edit-transaction/add-edit-transaction.component.html @@ -12,7 +12,7 @@ - + diff --git a/src/app/transactions/transaction.ts b/src/app/transactions/transaction.ts index 8ee42e1..cef2fa3 100644 --- a/src/app/transactions/transaction.ts +++ b/src/app/transactions/transaction.ts @@ -7,7 +7,7 @@ export class Transaction { accountId: string; title: string; description: string = null; - amount: number; + amount = 0; date: Date = new Date(); categoryId: string; type: TransactionType = TransactionType.EXPENSE;