Add additional chart to dashboard and prefill some transaction creation details

Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
Billy Brawner 2019-05-11 18:57:50 -07:00
parent a3ea896232
commit 6968d9cfb8
11 changed files with 218 additions and 16 deletions

59
package-lock.json generated
View file

@ -1171,6 +1171,11 @@
"semver-intersect": "1.4.0" "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": { "@types/jasmine": {
"version": "2.8.16", "version": "2.8.16",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.16.tgz", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.16.tgz",
@ -2626,6 +2631,39 @@
"integrity": "sha1-5upnvSR+EHESmDt6sEee02KAAIE=", "integrity": "sha1-5upnvSR+EHESmDt6sEee02KAAIE=",
"dev": true "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": { "chokidar": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
@ -2862,8 +2900,7 @@
"color-name": { "color-name": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok="
"dev": true
}, },
"colors": { "colors": {
"version": "1.1.2", "version": "1.1.2",
@ -7989,8 +8026,7 @@
"lodash": { "lodash": {
"version": "4.17.11", "version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
"dev": true
}, },
"lodash._isnative": { "lodash._isnative": {
"version": "2.4.1", "version": "2.4.1",
@ -8572,6 +8608,11 @@
"minimist": "0.0.8" "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": { "morgan": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz",
@ -8687,6 +8728,16 @@
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
"dev": true "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": { "ng2-currency-mask": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/ng2-currency-mask/-/ng2-currency-mask-5.3.1.tgz", "resolved": "https://registry.npmjs.org/ng2-currency-mask/-/ng2-currency-mask-5.3.1.tgz",

View file

@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve --host '0.0.0.0'",
"build": "ng build", "build": "ng build",
"publish": "ng build --prod --service-worker && firebase deploy", "publish": "ng build --prod --service-worker && firebase deploy",
"test": "ng test", "test": "ng test",
@ -25,10 +25,12 @@
"@angular/pwa": "^0.7.5", "@angular/pwa": "^0.7.5",
"@angular/router": "^7.2.14", "@angular/router": "^7.2.14",
"@angular/service-worker": "^7.2.14", "@angular/service-worker": "^7.2.14",
"chart.js": "^2.8.0",
"core-js": "^2.6.5", "core-js": "^2.6.5",
"dexie": "^2.0.4", "dexie": "^2.0.4",
"firebase": "^5.11.1", "firebase": "^5.11.1",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"ng2-charts": "^2.2.3",
"ng2-currency-mask": "^5.3.1", "ng2-currency-mask": "^5.3.1",
"rxjs": "^6.5.1", "rxjs": "^6.5.1",
"zone.js": "^0.8.29" "zone.js": "^0.8.29"

View file

@ -5,33 +5,41 @@
<span <span
[ngClass]="{'income': getBalance() > 0, 'expense': getBalance() < 0}">{{ getBalance() / 100 | currency }}</span> [ngClass]="{'income': getBalance() > 0, 'expense': getBalance() < 0}">{{ getBalance() / 100 | currency }}</span>
</h2> </h2>
<app-category-breakdown [barChartLabels]="barChartLabels" [barChartData]="barChartData">
</app-category-breakdown>
<div class="transaction-navigation"> <div class="transaction-navigation">
<a mat-button routerLink="/accounts/{{ account.id }}/transactions">View Transactions</a> <a mat-button routerLink="/accounts/{{ account.id }}/transactions" *ngIf="account">View Transactions</a>
</div> </div>
</div> </div>
<div class="dashboard-categories" [hidden]="!account"> <div class="dashboard-categories" [hidden]="!account">
<h3 class="categories">Income</h3> <h3 class="categories">Income</h3>
<a mat-button routerLink="/accounts/{{ account.id }}/categories/new" class="view-all">Add Category</a> <a mat-button routerLink="/accounts/{{ account.id }}/categories/new" class="view-all" *ngIf="account">Add Category</a>
<div class="no-categories" *ngIf="!income || income.length === 0"> <div class="no-categories" *ngIf="!income || income.length === 0">
<a mat-button routerLink="/accounts/{{ account.id }}/categories/new"> <a mat-button routerLink="/accounts/{{ account.id }}/categories/new" *ngIf="account">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
<p>Add categories to gain more insights into your income.</p> <p>Add categories to gain more insights into your income.</p>
</a> </a>
</div> </div>
<app-category-list [accountId]="account.id" [categories]="income" [categoryBalances]="categoryBalances"></app-category-list> <div class="category-info" *ngIf="income && income.length > 0">
<app-category-list [accountId]="account.id" [categories]="income" [categoryBalances]="categoryBalances">
</app-category-list>
</div>
</div> </div>
<div class="dashboard-categories" [hidden]="!account"> <div class="dashboard-categories" [hidden]="!account">
<h3 class="categories">Expenses</h3> <h3 class="categories">Expenses</h3>
<a mat-button routerLink="/accounts/{{ account.id }}/categories/new" class="view-all">Add Category</a> <a mat-button routerLink="/accounts/{{ account.id }}/categories/new" class="view-all" *ngIf="account">Add Category</a>
<div class="no-categories" *ngIf="!expenses || expenses.length === 0"> <div class="no-categories" *ngIf="!expenses || expenses.length === 0">
<a mat-button routerLink="/accounts/{{ account.id }}/categories/new"> <a mat-button routerLink="/accounts/{{ account.id }}/categories/new" *ngIf="account">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
<p>Add categories to gain more insights into your expenses.</p> <p>Add categories to gain more insights into your expenses.</p>
</a> </a>
</div> </div>
<app-category-list [accountId]="account.id" [categories]="expenses" [categoryBalances]="categoryBalances"></app-category-list> <div class="category-info" *ngIf="expenses && expenses.length > 0">
<app-category-list [accountId]="account.id" [categories]="expenses" [categoryBalances]="categoryBalances">
</app-category-list>
</div>
</div> </div>
</div> </div>
<a mat-fab routerLink="/accounts/{{ account.id }}/transactions/new" [hidden]="!account"> <a mat-fab routerLink="/accounts/{{ account.id }}/transactions/new" *ngIf="account">
<mat-icon aria-label="Add">add</mat-icon> <mat-icon aria-label="Add">add</mat-icon>
</a> </a>

View file

@ -9,6 +9,8 @@ import { Observable } from 'rxjs';
import { TransactionType } from 'src/app/transactions/transaction.type'; import { TransactionType } from 'src/app/transactions/transaction.type';
import { TRANSACTION_SERVICE, TransactionService } from 'src/app/transactions/transaction.service'; import { TRANSACTION_SERVICE, TransactionService } from 'src/app/transactions/transaction.service';
import { CATEGORY_SERVICE, CategoryService } from 'src/app/categories/category.service'; import { CATEGORY_SERVICE, CategoryService } from 'src/app/categories/category.service';
import { Label } from 'ng2-charts';
import { ChartDataSets } from 'chart.js';
@Component({ @Component({
selector: 'app-account-details', selector: 'app-account-details',
@ -22,6 +24,15 @@ export class AccountDetailsComponent implements OnInit {
public expenses: Category[] = []; public expenses: Category[] = [];
public income: Category[] = []; public income: Category[] = [];
categoryBalances: Map<string, number>; categoryBalances: Map<string, number>;
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( constructor(
private app: AppComponent, 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 { getBalance(): number {
let totalBalance = 0; let totalBalance = 0;
@ -68,13 +100,38 @@ export class AccountDetailsComponent implements OnInit {
getCategories(): void { getCategories(): void {
this.categoryService.getCategories(this.account.id).subscribe(categories => { this.categoryService.getCategories(this.account.id).subscribe(categories => {
const categoryBalances = new Map<string, number>();
let categoryBalancesCount = 0;
for (const category of categories) { for (const category of categories) {
if (category.isExpense) { if (category.isExpense) {
this.expenses.push(category); this.expenses.push(category);
this.expectedExpenses += category.amount;
} else { } else {
this.income.push(category); 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.next(balance);
subscriber.complete();
}); });
}); });
} }

View file

@ -48,6 +48,8 @@ import { ACCOUNT_SERVICE } from './accounts/account.service';
import { FirestoreAccountService } from './accounts/account.service.firestore'; import { FirestoreAccountService } from './accounts/account.service.firestore';
import { USER_SERVICE } from './users/user.service'; import { USER_SERVICE } from './users/user.service';
import { FirestoreUserService } from './users/user.service.firestore'; 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 = { export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
align: 'left', align: 'left',
@ -79,6 +81,7 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
NewAccountComponent, NewAccountComponent,
AccountDetailsComponent, AccountDetailsComponent,
AccountsComponent, AccountsComponent,
CategoryBreakdownComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -99,6 +102,7 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }), ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
HttpClientModule, HttpClientModule,
CurrencyMaskModule, CurrencyMaskModule,
ChartsModule,
], ],
providers: [ providers: [
{ provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig }, { provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig },

View file

@ -0,0 +1,9 @@
<div class="category-breakdown">
<canvas baseChart
[datasets]="barChartData"
[options]="barChartOptions"
[labels]="barChartLabels"
[legend]="barChartLegend"
[chartType]="barChartType">
</canvas>
</div>

View file

@ -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<CategoryBreakdownComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CategoryBreakdownComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CategoryBreakdownComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -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;
}
}
}

View file

@ -12,7 +12,7 @@
<input matInput type="text" [(ngModel)]="currentTransaction.amount" placeholder="Amount" required currencyMask> <input matInput type="text" [(ngModel)]="currentTransaction.amount" placeholder="Amount" required currencyMask>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<input matInput type="date" [(ngModel)]="currentTransaction.date" placeholder="Date" required> <input matInput type="date" [ngModel]="currentTransaction.date | date:'yyyy-MM-dd'" (ngModelChange)="currentTransaction.date = $event" placeholder="Date" required>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-select placeholder="Category" [(ngModel)]="currentTransaction.categoryId"> <mat-select placeholder="Category" [(ngModel)]="currentTransaction.categoryId">

View file

@ -7,7 +7,7 @@ export class Transaction {
accountId: string; accountId: string;
title: string; title: string;
description: string = null; description: string = null;
amount: number; amount = 0;
date: Date = new Date(); date: Date = new Date();
categoryId: string; categoryId: string;
type: TransactionType = TransactionType.EXPENSE; type: TransactionType = TransactionType.EXPENSE;