Separate income and expenses at category level
Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
parent
71da5bd9e7
commit
7587af9726
12 changed files with 89 additions and 44 deletions
|
@ -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">
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
.button-delete {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.category-form * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
mat-radio-button {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
<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">
|
||||
|
|
|
@ -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.categoryService.deleteCategory(this.accountId, this.currentCategory.id).subscribe(() => {
|
||||
this.app.goBack();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
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 (category.isExpense) {
|
||||
if (categoryBalance < 0) {
|
||||
categoryBalance = Math.abs(categoryBalance);
|
||||
} else {
|
||||
categoryBalance -= (categoryBalance * 2);
|
||||
}
|
||||
}
|
||||
|
||||
return categoryBalance / category.amount * 100;
|
||||
return categoryBalance / amount * 100;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Reference in a new issue