Add additional chart to dashboard and prefill some transaction creation details
Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
parent
a3ea896232
commit
6968d9cfb8
11 changed files with 218 additions and 16 deletions
59
package-lock.json
generated
59
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -5,33 +5,41 @@
|
|||
<span
|
||||
[ngClass]="{'income': getBalance() > 0, 'expense': getBalance() < 0}">{{ getBalance() / 100 | currency }}</span>
|
||||
</h2>
|
||||
<app-category-breakdown [barChartLabels]="barChartLabels" [barChartData]="barChartData">
|
||||
</app-category-breakdown>
|
||||
<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 class="dashboard-categories" [hidden]="!account">
|
||||
<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">
|
||||
<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>
|
||||
<p>Add categories to gain more insights into your income.</p>
|
||||
</a>
|
||||
</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 class="dashboard-categories" [hidden]="!account">
|
||||
<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">
|
||||
<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>
|
||||
<p>Add categories to gain more insights into your expenses.</p>
|
||||
</a>
|
||||
</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>
|
||||
<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>
|
||||
</a>
|
|
@ -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<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(
|
||||
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<string, number>();
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<div class="category-breakdown">
|
||||
<canvas baseChart
|
||||
[datasets]="barChartData"
|
||||
[options]="barChartOptions"
|
||||
[labels]="barChartLabels"
|
||||
[legend]="barChartLegend"
|
||||
[chartType]="barChartType">
|
||||
</canvas>
|
||||
</div>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
<input matInput type="text" [(ngModel)]="currentTransaction.amount" placeholder="Amount" required currencyMask>
|
||||
</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-select placeholder="Category" [(ngModel)]="currentTransaction.categoryId">
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue