Separate income and expenses at category level

Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
Billy Brawner 2019-05-04 20:46:37 -07:00
parent 71da5bd9e7
commit 7587af9726
12 changed files with 89 additions and 44 deletions

View file

@ -10,15 +10,26 @@
</div>
</div>
<div class="dashboard-categories" [hidden]="!account">
<h3 class="categories">Categories</h3>
<a mat-button routerLink="/accounts/{{ account.id }}/categories" class="view-all" *ngIf="categories">View All</a>
<div class="no-categories" *ngIf="!categories || categories.length === 0">
<h3 class="categories">Income</h3>
<a mat-button routerLink="/accounts/{{ account.id }}/categories/new" class="view-all">Add Category</a>
<div class="no-categories" *ngIf="!income || income.length === 0">
<a mat-button routerLink="/accounts/{{ account.id }}/categories/new">
<mat-icon>add</mat-icon>
<p>Add categories to get more insights into your income and expenses.</p>
<p>Add categories to gain more insights into your income.</p>
</a>
</div>
<app-category-list [accountId]="account.id" [categories]="categories" [categoryBalances]="categoryBalances"></app-category-list>
<app-category-list [accountId]="account.id" [categories]="income" [categoryBalances]="categoryBalances"></app-category-list>
</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>
<div class="no-categories" *ngIf="!expenses || expenses.length === 0">
<a mat-button routerLink="/accounts/{{ account.id }}/categories/new">
<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>
</div>
<a mat-fab routerLink="/accounts/{{ account.id }}/transactions/new" [hidden]="!account">

View file

@ -19,7 +19,8 @@ export class AccountDetailsComponent implements OnInit {
account: Account;
public transactions: Transaction[];
public categories: Category[];
public expenses: Category[] = [];
public income: Category[] = [];
categoryBalances: Map<string, number>;
constructor(
@ -66,9 +67,13 @@ export class AccountDetailsComponent implements OnInit {
}
getCategories(): void {
this.categoryService.getCategories(this.account.id, 5).subscribe(categories => {
this.categories = categories;
this.categoryService.getCategories(this.account.id).subscribe(categories => {
for (const category of categories) {
if (category.isExpense) {
this.expenses.push(category);
} else {
this.income.push(category);
}
this.getCategoryBalance(category.id).subscribe(balance => this.categoryBalances.set(category.id, balance));
}
});

View file

@ -1,3 +1,11 @@
.button-delete {
float: right;
}
.category-form * {
display: block;
}
mat-radio-button {
padding-bottom: 15px;
}

View file

@ -2,16 +2,20 @@
<p>Select a category from the list to view details about it or edit it.</p>
</div>
<div *ngIf="currentCategory" class="form category-form">
<mat-form-field>
<mat-form-field (keyup.enter)="doAction()">
<input matInput [(ngModel)]="currentCategory.name" placeholder="Name" required>
</mat-form-field>
<mat-form-field>
<mat-form-field (keyup.enter)="doAction()">
<input matInput type="text" [(ngModel)]="currentCategory.amount" placeholder="Amount" required currencyMask>
</mat-form-field>
<mat-radio-group [(ngModel)]="currentCategory.isExpense">
<mat-radio-button [value]="true">Expense</mat-radio-button>
<mat-radio-button [value]="false">Income</mat-radio-button>
</mat-radio-group>
<!--
<mat-form-field>
<input type="color" matInput [(ngModel)]="currentCategory.color" placeholder="Color">
</mat-form-field>
-->
<button class="button-delete" mat-button color="warn" *ngIf="currentCategory.id" (click)="delete()">Delete</button>
</div>
</div>

View file

@ -42,7 +42,8 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
this.currentCategory.id,
{
name: this.currentCategory.name,
amount: this.currentCategory.amount * 100
amount: this.currentCategory.amount * 100,
isExpense: this.currentCategory.isExpense
}
);
} else {
@ -50,7 +51,8 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
observable = this.categoryService.createCategory(
this.accountId,
this.currentCategory.name,
this.currentCategory.amount * 100
this.currentCategory.amount * 100,
this.currentCategory.isExpense
);
}
observable.subscribe(val => {
@ -63,7 +65,8 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
}
delete(): void {
this.categoryService.deleteCategory(this.accountId, this.currentCategory.id);
this.app.goBack();
this.categoryService.deleteCategory(this.accountId, this.currentCategory.id).subscribe(() => {
this.app.goBack();
});
}
}

View file

@ -16,3 +16,15 @@ p.mat-line.category-list-title .remaining {
font-size: 0.9em;
font-style: italic;
}
::ng-deep .income .mat-progress-bar-fill::after {
background-color: #81C784 !important;
}
::ng-deep .expense .mat-progress-bar-fill::after {
background-color: #E57373 !important;
}
::ng-deep .mat-progress-bar-buffer {
background-color: #333333 !important;
}

View file

@ -1,8 +1,3 @@
<style>
.categories mat-progress-bar div.mat-progress-bar-element.mat-progress-bar-buffer {
background-color: #BDBDBD !important;
}
</style>
<mat-nav-list class="categories">
<a mat-list-item *ngFor="let category of categories" routerLink="/accounts/{{ accountId }}/categories/{{ category.id }}">
<p matLine class="category-list-title">
@ -10,9 +5,9 @@
{{ category.name }}
</span>
<span class="remaining">
{{ getCategoryRemainingBalance(category) | currency }} remaining
{{ getCategoryRemainingBalance(category) | currency }} remaining of {{ category.amount / 100 | currency }}
</span>
</p>
<mat-progress-bar matLine color="accent" mode="determinate" #categoryProgress [attr.id]="'cat-' + category.id" value="{{ getCategoryCompletion(category) }}"></mat-progress-bar>
<mat-progress-bar matLine color="accent" [ngClass]="{'income': !category.isExpense, 'expense': category.isExpense}" mode="determinate" #categoryProgress [attr.id]="'cat-' + category.id" value="{{ getCategoryCompletion(category) }}"></mat-progress-bar>
</a>
</mat-nav-list>

View file

@ -1,6 +1,5 @@
import { Component, OnInit, Input } from '@angular/core';
import { Category } from '../category';
import { Account } from 'src/app/accounts/account';
@Component({
selector: 'app-category-list',
@ -24,13 +23,15 @@ export class CategoryListComponent implements OnInit {
categoryBalance = 0;
}
return (category.amount / 100) + (categoryBalance / 100);
if (category.isExpense) {
return (category.amount / 100) + (categoryBalance / 100);
} else {
return (category.amount / 100) - (categoryBalance / 100);
}
}
getCategoryCompletion(category: Category): number {
if (category.amount <= 0) {
return 0;
}
const amount = category.amount > 0 ? category.amount : 1;
let categoryBalance = this.categoryBalances.get(category.id);
if (!categoryBalance) {
@ -40,12 +41,14 @@ export class CategoryListComponent implements OnInit {
// Invert the negative/positive values for calculating progress
// since the limit for a category is saved as a positive but the
// balance is used in the calculation.
if (categoryBalance < 0) {
categoryBalance = Math.abs(categoryBalance);
} else {
categoryBalance -= (categoryBalance * 2);
if (category.isExpense) {
if (categoryBalance < 0) {
categoryBalance = Math.abs(categoryBalance);
} else {
categoryBalance -= (categoryBalance * 2);
}
}
return categoryBalance / category.amount * 100;
return categoryBalance / amount * 100;
}
}

View file

@ -14,7 +14,7 @@ export class CategoryServiceFirebaseFirestoreImpl {
getCategories(accountId: string, count?: number): Observable<Category[]> {
return Observable.create(subscriber => {
let query: any = firebase.firestore().collection('accounts').doc(accountId).collection('categories');
let query: any = firebase.firestore().collection('accounts').doc(accountId).collection('categories').orderBy('name');
if (count) {
query = query.limit(count);
}
@ -49,11 +49,12 @@ export class CategoryServiceFirebaseFirestoreImpl {
});
}
createCategory(accountId: string, name: string, amount: number): Observable<Category> {
createCategory(accountId: string, name: string, amount: number, isExpense: boolean): Observable<Category> {
return Observable.create(subscriber => {
firebase.firestore().collection('accounts').doc(accountId).collection('categories').add({
name: name,
amount: amount
amount: amount,
isExpense: isExpense
}).then(docRef => {
if (!docRef) {
console.error('Failed to create category');

View file

@ -9,7 +9,7 @@ export interface CategoryService {
getCategory(accountId: string, id: string): Observable<Category>;
createCategory(accountId: string, name: string, amount: number): Observable<Category>;
createCategory(accountId: string, name: string, amount: number, isExpense: boolean): Observable<Category>;
updateCategory(accountId: string, id: string, changes: object): Observable<boolean>;

View file

@ -2,8 +2,7 @@ export class Category {
id: string;
name: string;
amount: number;
repeat: string;
color: string;
isExpense: boolean;
accountId: string;
static fromSnapshotRef(accountId: string, snapshot: firebase.firestore.DocumentSnapshot): Category {
@ -11,6 +10,11 @@ export class Category {
category.id = snapshot.id;
category.name = snapshot.get('name');
category.amount = snapshot.get('amount');
let isExpense = snapshot.get('isExpense');
if (isExpense === undefined) {
isExpense = true;
}
category.isExpense = isExpense;
category.accountId = accountId;
return category;
}

View file

@ -10,7 +10,11 @@ export class TransactionServiceFirebaseFirestoreImpl implements TransactionServi
getTransactions(accountId: string, category?: string, count?: number): Observable<Transaction[]> {
return Observable.create(subscriber => {
let transactionQuery: any = firebase.firestore().collection('accounts').doc(accountId).collection('transactions');
let transactionQuery: any = firebase.firestore()
.collection('accounts')
.doc(accountId)
.collection('transactions')
.orderBy('date', 'desc');
if (category) {
transactionQuery = transactionQuery.where('category', '==', category);
}
@ -18,11 +22,6 @@ export class TransactionServiceFirebaseFirestoreImpl implements TransactionServi
transactionQuery = transactionQuery.limit(count);
}
transactionQuery.onSnapshot(snapshot => {
if (snapshot.empty) {
subscriber.error(`Unable to query transactions within account ${accountId}`);
return;
}
const transactions = [];
snapshot.docs.forEach(transaction => {
transactions.push(Transaction.fromSnapshotRef(transaction));