WIP: Save transactions and categories in Firebase

This commit is contained in:
Billy Brawner 2018-12-18 18:35:52 -06:00
parent 279d644a2e
commit 7cb97d1487
28 changed files with 467 additions and 383 deletions

View file

@ -1,8 +1,8 @@
import { Component, OnInit, Input, OnDestroy } from '@angular/core'; import { Component, OnInit, Input, OnDestroy, Inject } from '@angular/core';
import { CategoryService } from '../category.service';
import { Category } from '../category'; import { Category } from '../category';
import { Actionable } from '../actionable'; import { Actionable } from '../actionable';
import { AppComponent } from '../app.component'; import { AppComponent } from '../app.component';
import { CATEGORY_SERVICE, CategoryService } from '../category.service';
@Component({ @Component({
selector: 'app-add-edit-category', selector: 'app-add-edit-category',
@ -13,10 +13,11 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
@Input() title: string; @Input() title: string;
@Input() currentCategory: Category; @Input() currentCategory: Category;
@Input() group: string;
constructor( constructor(
private app: AppComponent, private app: AppComponent,
private categoryService: CategoryService, @Inject(CATEGORY_SERVICE) private categoryService: CategoryService,
) { } ) { }
ngOnInit() { ngOnInit() {
@ -31,14 +32,17 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
doAction(): void { doAction(): void {
this.currentCategory.amount *= 100; this.currentCategory.amount *= 100;
let observable;
if (this.currentCategory.id) { if (this.currentCategory.id) {
// This is an existing category, update it // This is an existing category, update it
this.categoryService.updateCategory(this.currentCategory); observable = this.categoryService.updateCategory(this.currentCategory.id, this.currentCategory);
} else { } else {
// This is a new category, save it // This is a new category, save it
this.categoryService.saveCategory(this.currentCategory); observable = this.categoryService.createCategory(this.currentCategory.name, this.currentCategory.amount, this.app.group);
} }
observable.subscribe(val => {
this.app.goBack(); this.app.goBack();
});
} }
getActionLabel(): string { getActionLabel(): string {
@ -46,7 +50,7 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
} }
delete(): void { delete(): void {
this.categoryService.deleteCategory(this.currentCategory); this.categoryService.deleteCategory(this.currentCategory.id);
this.app.goBack(); this.app.goBack();
} }
} }

View file

@ -1,11 +1,11 @@
import { Component, OnInit, Input, OnChanges, OnDestroy } from '@angular/core'; import { Component, OnInit, Input, OnChanges, OnDestroy, Inject } from '@angular/core';
import { Transaction } from '../transaction' import { Transaction } from '../transaction';
import { TransactionType } from '../transaction.type' import { TransactionType } from '../transaction.type';
import { TransactionService } from '../transaction.service' import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service';
import { Category } from '../category' import { Category } from '../category';
import { CategoryService } from '../category.service'
import { AppComponent } from '../app.component'; import { AppComponent } from '../app.component';
import { Actionable } from '../actionable'; import { Actionable } from '../actionable';
import { CATEGORY_SERVICE, CategoryService } from '../category.service';
@Component({ @Component({
selector: 'app-add-edit-transaction', selector: 'app-add-edit-transaction',
@ -15,6 +15,7 @@ import { Actionable } from '../actionable';
export class AddEditTransactionComponent implements OnInit, OnDestroy, Actionable { export class AddEditTransactionComponent implements OnInit, OnDestroy, Actionable {
@Input() title: string; @Input() title: string;
@Input() currentTransaction: Transaction; @Input() currentTransaction: Transaction;
@Input() group: string;
public transactionType = TransactionType; public transactionType = TransactionType;
public selectedCategory: Category; public selectedCategory: Category;
public categories: Category[]; public categories: Category[];
@ -22,8 +23,8 @@ export class AddEditTransactionComponent implements OnInit, OnDestroy, Actionabl
constructor( constructor(
private app: AppComponent, private app: AppComponent,
private categoryService: CategoryService, @Inject(CATEGORY_SERVICE) private categoryService: CategoryService,
private transactionService: TransactionService, @Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
) { } ) { }
ngOnInit() { ngOnInit() {
@ -41,14 +42,25 @@ export class AddEditTransactionComponent implements OnInit, OnDestroy, Actionabl
// The amount will be input as a decimal value so we need to convert it // The amount will be input as a decimal value so we need to convert it
// to an integer // to an integer
this.currentTransaction.amount *= 100; this.currentTransaction.amount *= 100;
let observable;
if (this.currentTransaction.id) { if (this.currentTransaction.id) {
// This is an existing transaction, update it // This is an existing transaction, update it
this.transactionService.updateTransaction(this.currentTransaction); observable = this.transactionService.updateTransaction(this.currentTransaction.id, this.currentTransaction);
} else { } else {
// This is a new transaction, save it // This is a new transaction, save it
this.transactionService.saveTransaction(this.currentTransaction); observable = this.transactionService.createTransaction(
this.currentTransaction.title,
this.currentTransaction.description,
this.currentTransaction.amount,
this.currentTransaction.date,
this.currentTransaction.type === TransactionType.EXPENSE,
this.currentTransaction.categoryId,
);
} }
observable.subscribe(val => {
this.app.goBack(); this.app.goBack();
});
} }
getActionLabel(): string { getActionLabel(): string {
@ -56,11 +68,11 @@ export class AddEditTransactionComponent implements OnInit, OnDestroy, Actionabl
} }
delete(): void { delete(): void {
this.transactionService.deleteTransaction(this.currentTransaction); this.transactionService.deleteTransaction(this.currentTransaction.id);
this.app.goBack(); this.app.goBack();
} }
getCategories() { getCategories() {
this.categoryService.getCategories().subscribe(categories => this.categories = categories); this.categoryService.getCategories(this.app.group).subscribe(categories => this.categories = categories);
} }
} }

View file

@ -1,19 +0,0 @@
import { TestBed, inject } from '@angular/core/testing';
import { ApiService } from './api.service';
import { HttpClientModule } from '@angular/common/http';
describe('ApiService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
],
providers: [ApiService]
});
});
it('should be created', inject([ApiService], (service: ApiService) => {
expect(service).toBeTruthy();
}));
});

View file

@ -1,99 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, of, from } from 'rxjs';
import { Transaction } from './transaction';
import * as firebase from 'firebase/app';
import 'firebase/database';
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json'
}),
withCredentials: true,
};
const host = 'http://localhost:9090';
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(
private http: HttpClient,
) { }
login(username: string, password: string): Observable<any> {
return this.http.post(
host + '/login',
{
'username': username,
'password': password
},
httpOptions
);
}
register(username: string, email: string, password: string): Observable<any> {
return this.http.post(
host + '/register',
{
'username': username,
'email': email,
'password': password
},
httpOptions
);
}
logout(): Observable<any> {
return this.http.get(
host + '/logout',
httpOptions
);
}
getTransactions(): Observable<any> {
return Observable.create(subscriber => {
const transactionsRef = firebase.database().ref('transactions');
transactionsRef.on('child_changed', data => {
if (!data.val()) {
return;
}
subscriber.next(data.val());
});
});
}
saveTransaction(transaction: Transaction): Observable<Transaction> {
return Observable.create(subscriber => {
const params = {
name: transaction.title,
amount: transaction.amount,
accountId: transaction.accountId,
categoryId: transaction.categoryId,
description: transaction.description,
date: transaction.date,
type: transaction.type,
};
if (transaction.remoteId > 0) {
params['id'] = transaction.remoteId;
}
this.http.post(
host + '/transactions',
params,
httpOptions,
).subscribe(
value => {
console.log(value);
},
error => {
console.error(error);
}
);
});
}
}

View file

@ -1,6 +1,7 @@
<mat-sidenav-container class="sidenav-container"> <mat-sidenav-container class="sidenav-container">
<mat-sidenav #sidenav mode="over" closed> <mat-sidenav #sidenav mode="over" closed>
<mat-nav-list (click)="sidenav.close()"> <mat-nav-list (click)="sidenav.close()">
<a mat-list-item *ngIf="isLoggedIn()" routerLink="">{{ getUsername() }}</a>
<a mat-list-item routerLink="/accounts">Manage Accounts</a> <a mat-list-item routerLink="/accounts">Manage Accounts</a>
<a mat-list-item (click)="exportData()">Export Data</a> <a mat-list-item (click)="exportData()">Export Data</a>
<a mat-list-item *ngIf="!isLoggedIn()" routerLink="/login">Login</a> <a mat-list-item *ngIf="!isLoggedIn()" routerLink="/login">Login</a>

View file

@ -15,6 +15,7 @@ export class AppComponent {
public title = 'Budget'; public title = 'Budget';
public backEnabled = false; public backEnabled = false;
public actionable: Actionable; public actionable: Actionable;
public group = 'MG3KOiuPu0Xy38O2LdhJ';
constructor( constructor(
public authService: AuthService, public authService: AuthService,
@ -32,6 +33,10 @@ export class AppComponent {
firebase.initializeApp(config); firebase.initializeApp(config);
} }
getUsername(): String {
return firebase.auth().currentUser.email;
}
goBack(): void { goBack(): void {
this.location.back(); this.location.back();
} }

View file

@ -38,7 +38,10 @@ import { UserComponent } from './user/user.component';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { CurrencyMaskModule } from 'ng2-currency-mask'; import { CurrencyMaskModule } from 'ng2-currency-mask';
import { CurrencyMaskConfig, CURRENCY_MASK_CONFIG } from 'ng2-currency-mask/src/currency-mask.config'; import { CurrencyMaskConfig, CURRENCY_MASK_CONFIG } from 'ng2-currency-mask/src/currency-mask.config';
import * as firebase from 'firebase/app'; import { TransactionServiceFirebaseFirestoreImpl } from './transaction.service.firestore';
import { TRANSACTION_SERVICE } from './transaction.service';
import { CATEGORY_SERVICE } from './category.service';
import { CategoryServiceFirebaseFirestoreImpl } from './category.service.firestore';
export const CustomCurrencyMaskConfig: CurrencyMaskConfig = { export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
align: 'left', align: 'left',
@ -91,6 +94,8 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
], ],
providers: [ providers: [
{ provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig }, { provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig },
{ provide: TRANSACTION_SERVICE, useClass: TransactionServiceFirebaseFirestoreImpl },
{ provide: CATEGORY_SERVICE, useClass: CategoryServiceFirebaseFirestoreImpl },
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View file

@ -1,5 +1,4 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ApiService } from './api.service';
import { User } from './user'; import { User } from './user';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import * as firebase from 'firebase/app'; import * as firebase from 'firebase/app';
@ -10,7 +9,6 @@ import * as firebase from 'firebase/app';
export class AuthService { export class AuthService {
constructor( constructor(
private apiService: ApiService,
private router: Router, private router: Router,
) { } ) { }
@ -23,17 +21,14 @@ export class AuthService {
}); });
} }
// register(user: User) { register(email: string, password: string) {
// this.apiService.register(user.name, user.email, user.password).subscribe( firebase.auth().createUserWithEmailAndPassword(email, password).then(value => {
// value => { this.router.navigate(['/']);
// this.login(value); }).catch(err => {
// }, console.log('Login failed');
// error => { console.log(err);
// console.log('Registration failed'); });
// console.log(error); }
// }
// );
// }
logout() { logout() {
firebase.auth().signOut().then(value => { firebase.auth().signOut().then(value => {

View file

@ -67,21 +67,21 @@ export class BudgetDatabase extends Dexie {
} }
export interface ITransaction { export interface ITransaction {
id: number; id: string;
accountId: number; accountId: string;
remoteId: number; remoteId: string;
title: string; title: string;
description: string; description: string;
amount: number; amount: number;
date: Date; date: Date;
categoryId: number; categoryId: string;
type: TransactionType; type: TransactionType;
} }
export interface ICategory { export interface ICategory {
id: number; id: string;
accountId: number; accountId: string;
remoteId: number; remoteId: string;
name: string; name: string;
amount: number; amount: number;
repeat: string; repeat: string;

View file

@ -1,7 +1,10 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, Input, Inject } from '@angular/core';
import { CategoryService } from '../category.service'; import { CategoryService, CATEGORY_SERVICE } from '../category.service';
import { Category } from '../category'; import { Category } from '../category';
import { AppComponent } from '../app.component'; import { AppComponent } from '../app.component';
import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service';
import { Observable } from 'rxjs';
import { TransactionType } from '../transaction.type';
@Component({ @Component({
selector: 'app-categories', selector: 'app-categories',
@ -10,12 +13,14 @@ import { AppComponent } from '../app.component';
}) })
export class CategoriesComponent implements OnInit { export class CategoriesComponent implements OnInit {
@Input() group: string;
public categories: Category[]; public categories: Category[];
public categoryBalances: Map<number, number>; public categoryBalances: Map<string, number>;
constructor( constructor(
private app: AppComponent, private app: AppComponent,
private categoryService: CategoryService, @Inject(CATEGORY_SERVICE) private categoryService: CategoryService,
@Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
) { } ) { }
ngOnInit() { ngOnInit() {
@ -26,11 +31,27 @@ export class CategoriesComponent implements OnInit {
} }
getCategories(): void { getCategories(): void {
this.categoryService.getCategories().subscribe(categories => { this.categoryService.getCategories(this.app.group).subscribe(categories => {
this.categories = categories; this.categories = categories;
for (const category of this.categories) { for (const category of this.categories) {
this.categoryService.getBalance(category).subscribe(balance => this.categoryBalances.set(category.id, balance)) this.getCategoryBalance(category.id).subscribe(balance => this.categoryBalances.set(category.id, balance));
} }
}); });
} }
getCategoryBalance(category: string): Observable<number> {
return Observable.create(subscriber => {
this.transactionService.getTransactionsForCategory(category).subscribe(transactions => {
let balance = 0;
for (const transaction of transactions) {
if (transaction.type === TransactionType.INCOME) {
balance += transaction.amount;
} else {
balance -= transaction.amount;
}
}
subscriber.next(balance);
});
});
}
} }

View file

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { CategoryService } from '../category.service' import { CategoryServiceFirebaseFirestoreImpl } from '../category.service.firestore';
import { Category } from '../category' import { Category } from '../category';
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router';
@Component({ @Component({
selector: 'app-category-details', selector: 'app-category-details',
@ -14,15 +14,15 @@ export class CategoryDetailsComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private categoryService: CategoryService private categoryService: CategoryServiceFirebaseFirestoreImpl
) { } ) { }
ngOnInit() { ngOnInit() {
this.getCategory() this.getCategory();
} }
getCategory(): void { getCategory(): void {
const id = +this.route.snapshot.paramMap.get('id') const id = this.route.snapshot.paramMap.get('id');
this.categoryService.getCategory(id) this.categoryService.getCategory(id)
.subscribe(category => { .subscribe(category => {
category.amount /= 100; category.amount /= 100;

View file

@ -1,5 +1,5 @@
import { Component, OnInit, Input } from '@angular/core'; import { Component, OnInit, Input } from '@angular/core';
import { Category } from '../category' import { Category } from '../category';
@Component({ @Component({
selector: 'app-category-list', selector: 'app-category-list',
@ -9,7 +9,7 @@ import { Category } from '../category'
export class CategoryListComponent implements OnInit { export class CategoryListComponent implements OnInit {
@Input() categories: Category[]; @Input() categories: Category[];
@Input() categoryBalances: Map<number, number>; @Input() categoryBalances: Map<string, number>;
constructor() { } constructor() { }
@ -30,7 +30,7 @@ export class CategoryListComponent implements OnInit {
return 0; return 0;
} }
let categoryBalance = this.categoryBalances.get(category.id) let categoryBalance = this.categoryBalances.get(category.id);
if (!categoryBalance) { if (!categoryBalance) {
categoryBalance = 0; categoryBalance = 0;
} }
@ -39,9 +39,9 @@ export class CategoryListComponent implements OnInit {
// since the limit for a category is saved as a positive but the // since the limit for a category is saved as a positive but the
// balance is used in the calculation. // balance is used in the calculation.
if (categoryBalance < 0) { if (categoryBalance < 0) {
categoryBalance = Math.abs(categoryBalance) categoryBalance = Math.abs(categoryBalance);
} else { } else {
categoryBalance -= (categoryBalance * 2) categoryBalance -= (categoryBalance * 2);
} }
return categoryBalance / category.amount * 100; return categoryBalance / category.amount * 100;

View file

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { CategoryServiceFirebaseFirestoreImpl } from './category.service.firestore';
describe('CategoryServiceFirebaseFirestoreImpl', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [CategoryServiceFirebaseFirestoreImpl]
});
});
it('should be created', inject([CategoryServiceFirebaseFirestoreImpl], (service: CategoryServiceFirebaseFirestoreImpl) => {
expect(service).toBeTruthy();
}));
});

View file

@ -0,0 +1,100 @@
import { Injectable } from '@angular/core';
import { of, Observable, from } from 'rxjs';
import { Category } from './category';
import * as firebase from 'firebase/app';
import 'firebase/firestore';
@Injectable({
providedIn: 'root'
})
export class CategoryServiceFirebaseFirestoreImpl {
constructor() { }
getCategories(group: string, count?: number): Observable<Category[]> {
return Observable.create(subscriber => {
let query = firebase.firestore().collection('categories').where('group', '==', group);
if (count) {
query = query.limit(count);
}
query.onSnapshot(snapshot => {
if (snapshot.empty) {
console.log('Got back empty snapshot for categories');
subscriber.error('Got back empty snapshot for categories');
return;
}
const categories = [];
for (const categoryDoc of snapshot.docs) {
categories.push(Category.fromSnapshotRef(categoryDoc));
}
subscriber.next(categories);
}, error => {
console.error('Got an error while getting categories');
console.error(error);
});
});
}
getCategory(id: string): Observable<Category> {
return Observable.create(subscriber => {
firebase.firestore().collection('categories').doc(id).onSnapshot(snapshot => {
if (!snapshot.exists) {
return;
}
subscriber.next(Category.fromSnapshotRef(snapshot));
});
});
}
createCategory(name: string, amount: number, group: string): Observable<Category> {
return Observable.create(subscriber => {
firebase.firestore().collection('categories').add({
name: name,
amount: amount,
group: group,
}).then(docRef => {
if (!docRef) {
console.error('Failed to create category');
subscriber.error('Failed to create category');
}
docRef.get().then(snapshot => {
if (!snapshot) {
subscriber.error('Unable to retrieve saved transaction data');
return;
}
subscriber.next(Category.fromSnapshotRef(snapshot));
}).catch(err => {
console.error(err);
});
}).catch(err => {
console.error(err);
subscriber.error(err);
});
});
}
updateCategory(id: string, changes: object): Observable<boolean> {
return Observable.create(subscriber => {
firebase.firestore().collection('categories').doc(id).onSnapshot(snapshot => {
if (!snapshot.exists) {
return;
}
subscriber.next(Category.fromSnapshotRef(snapshot));
});
});
}
deleteCategory(id: string): Observable<boolean> {
return Observable.create(subscriber => {
firebase.firestore().collection('categories').doc(id).delete().then(result => {
subscriber.next(true);
}).catch(err => {
console.error(err);
subscriber.next(false);
});
});
}
}

View file

@ -1,15 +0,0 @@
import { TestBed, inject } from '@angular/core/testing';
import { CategoryService } from './category.service';
describe('CategoryService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [CategoryService]
});
});
it('should be created', inject([CategoryService], (service: CategoryService) => {
expect(service).toBeTruthy();
}));
});

View file

@ -1,55 +1,18 @@
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs';
import { of, Observable, from } from 'rxjs'; import { Category } from './category';
import { BudgetDatabase } from './budget-database'; import { InjectionToken } from '@angular/core';
import { TransactionType } from './transaction.type'
import { Category } from './category'
@Injectable({ export interface CategoryService {
providedIn: 'root'
})
export class CategoryService {
constructor(private db: BudgetDatabase) { } getCategories(group: string, count?: number): Observable<Category[]>;
getCategories(count?: number): Observable<Category[]> { getCategory(id: string): Observable<Category>;
let collection = this.db.categories.orderBy('name');
if (count) {
return from(collection.limit(count).toArray())
} else {
return from(collection.toArray())
}
}
getCategory(id: number): Observable<Category> { createCategory(name: string, amount: number, group: string): Observable<Category>;
return from(this.db.categories.where('id').equals(id).first())
}
saveCategory(category: Category): Observable<Category> { updateCategory(id: string, changes: object): Observable<boolean>;
this.db.categories.put(category)
return of(category)
}
updateCategory(category: Category): Observable<any> { deleteCategory(id: string): Observable<boolean>;
this.db.categories.update(category.id, category)
return of([])
}
deleteCategory(category: Category): Observable<any> {
return from(this.db.categories.delete(category.id))
}
getBalance(category: Category): Observable<number> {
let sum = 0;
return from(
this.db.transactions.filter(transaction => transaction.categoryId === category.id).each(function (transaction) {
if (transaction.type === TransactionType.INCOME) {
sum += transaction.amount
} else {
sum -= transaction.amount
}
}).then(function () {
return sum;
})
)
}
} }
export let CATEGORY_SERVICE = new InjectionToken<CategoryService>('category.service');

View file

@ -1,11 +1,20 @@
import { ICategory } from './budget-database' import { ICategory } from './budget-database';
export class Category implements ICategory { export class Category implements ICategory {
id: number; id: string;
accountId: number; accountId: string;
remoteId: number; remoteId: string;
name: string; name: string;
amount: number; amount: number;
repeat: string; repeat: string;
color: string; color: string;
static fromSnapshotRef(snapshot: firebase.firestore.DocumentSnapshot): Category {
const category = new Category();
category.id = snapshot.id;
category.name = snapshot.get('name');
category.amount = snapshot.get('amount');
category.accountId = snapshot.get('group');
return category;
}
} }

View file

@ -2,7 +2,7 @@
<div class="dashboard-primary"> <div class="dashboard-primary">
<h2 class="balance"> <h2 class="balance">
Current Balance: <br /> Current Balance: <br />
<span [ngClass]="{'income': balance > 0, 'expense': balance < 0}" >{{ balance / 100 | currency }}</span> <span [ngClass]="{'income': getBalance() > 0, 'expense': getBalance() < 0}" >{{ getBalance() / 100 | currency }}</span>
</h2> </h2>
<div class="transaction-navigation"> <div class="transaction-navigation">
<a mat-button routerLink="/transactions">View Transactions</a> <a mat-button routerLink="/transactions">View Transactions</a>

View file

@ -1,10 +1,11 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, Inject } from '@angular/core';
import { Transaction } from '../transaction' import { Transaction } from '../transaction';
import { TransactionService } from '../transaction.service' import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service';
import { CategoryService } from '../category.service' import { Category } from '../category';
import { Category } from '../category'
import { AppComponent } from '../app.component'; import { AppComponent } from '../app.component';
import { AuthService } from '../auth.service'; import { TransactionType } from '../transaction.type';
import { Observable } from 'rxjs';
import { CategoryService, CATEGORY_SERVICE } from '../category.service';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
@ -13,41 +14,62 @@ import { AuthService } from '../auth.service';
}) })
export class DashboardComponent implements OnInit { export class DashboardComponent implements OnInit {
public balance: number;
public transactions: Transaction[]; public transactions: Transaction[];
public categories: Category[]; public categories: Category[];
categoryBalances: Map<number, number>; categoryBalances: Map<string, number>;
constructor( constructor(
private app: AppComponent, private app: AppComponent,
private transactionService: TransactionService, @Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
private categoryService: CategoryService @Inject(CATEGORY_SERVICE) private categoryService: CategoryService,
) { } ) { }
ngOnInit() { ngOnInit() {
this.app.backEnabled = false; this.app.backEnabled = false;
this.app.title = 'My Finances'; this.app.title = 'My Finances';
this.balance = 0;
this.getBalance(); this.getBalance();
this.getTransactions(); this.getTransactions();
this.getCategories(); this.getCategories();
this.categoryBalances = new Map(); this.categoryBalances = new Map();
} }
getBalance(): void { getBalance(): number {
this.transactionService.getBalance().subscribe(balance => this.balance = balance) let totalBalance = 0;
if (!this.categoryBalances) {
return 0;
}
this.categoryBalances.forEach(balance => {
totalBalance += balance;
});
return totalBalance;
} }
getTransactions(): void { getTransactions(): void {
this.transactionService.getTransactions(5).subscribe(transactions => this.transactions = <Transaction[]> transactions); this.transactionService.getTransactions(this.app.group, 5).subscribe(transactions => this.transactions = <Transaction[]>transactions);
} }
getCategories(): void { getCategories(): void {
this.categoryService.getCategories(5).subscribe(categories => { this.categoryService.getCategories(this.app.group, 5).subscribe(categories => {
this.categories = categories; this.categories = categories;
for (const category of this.categories) { for (const category of categories) {
this.categoryService.getBalance(category).subscribe(balance => this.categoryBalances.set(category.id, balance)) this.getCategoryBalance(category.id).subscribe(balance => this.categoryBalances.set(category.id, balance));
} }
}); });
} }
getCategoryBalance(category: string): Observable<number> {
return Observable.create(subscriber => {
this.transactionService.getTransactionsForCategory(category).subscribe(transactions => {
let balance = 0;
for (const transaction of transactions) {
if (transaction.type === TransactionType.INCOME) {
balance += transaction.amount;
} else {
balance -= transaction.amount;
}
}
subscriber.next(balance);
});
});
}
} }

View file

@ -34,7 +34,7 @@ export class RegisterComponent implements OnInit, OnDestroy, Actionable {
alert('Passwords don\'t match'); alert('Passwords don\'t match');
return; return;
} }
this.authService.register(this.user); this.authService.register(this.user.email, this.user.password);
} }
getActionLabel() { getActionLabel() {

View file

@ -1,7 +1,7 @@
import { Component, OnInit, Input } from '@angular/core'; import { Component, OnInit, Input, Inject } from '@angular/core';
import { TransactionService } from '../transaction.service' import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Transaction } from '../transaction' import { Transaction } from '../transaction';
@Component({ @Component({
selector: 'app-transaction-details', selector: 'app-transaction-details',
@ -14,15 +14,15 @@ export class TransactionDetailsComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private transactionService: TransactionService @Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
) { } ) { }
ngOnInit() { ngOnInit() {
this.getTransaction() this.getTransaction();
} }
getTransaction(): void { getTransaction(): void {
const id = +this.route.snapshot.paramMap.get('id') const id = this.route.snapshot.paramMap.get('id');
this.transactionService.getTransaction(id) this.transactionService.getTransaction(id)
.subscribe(transaction => { .subscribe(transaction => {
transaction.amount /= 100; transaction.amount /= 100;

View file

@ -0,0 +1,19 @@
import { TestBed, inject } from '@angular/core/testing';
import { TransactionServiceFirebaseFirestoreImpl } from './transaction.service.firestore';
import { HttpClientModule } from '@angular/common/http';
describe('TransactionServiceFirebaseFirestoreImpl', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
],
providers: [TransactionServiceFirebaseFirestoreImpl]
});
});
it('should be created', inject([TransactionServiceFirebaseFirestoreImpl], (service: TransactionServiceFirebaseFirestoreImpl) => {
expect(service).toBeTruthy();
}));
});

View file

@ -0,0 +1,112 @@
import { Injectable } from '@angular/core';
import { Observable, Subscriber } from 'rxjs';
import { Transaction } from './transaction';
import * as firebase from 'firebase/app';
import 'firebase/firestore';
import { TransactionService } from './transaction.service';
export class TransactionServiceFirebaseFirestoreImpl implements TransactionService {
constructor() { }
getTransactionsForCategory(category: string): Observable<Transaction[]> {
const transactionsQuery = firebase.firestore().collection('transactions').where('category', '==', category);
return Observable.create(subscriber => {
const transactions = [];
transactionsQuery.onSnapshot(data => {
if (!data.empty) {
data.docs.map(transaction => transactions.push(Transaction.fromSnapshotRef(transaction)));
}
subscriber.next(transactions);
});
});
}
getTransactions(group: string, count?: number): Observable<Transaction[]> {
const categoriesQuery = firebase.firestore().collection('categories').where('group', '==', group);
return Observable.create(subscriber => {
categoriesQuery.onSnapshot(querySnapshot => {
if (querySnapshot.empty) {
subscriber.error(`Unable to query categories within group ${group}`);
return;
}
const transactions = [];
querySnapshot.docs.map(categoryDoc => {
firebase.firestore().collection('transactions').where('category', '==', categoryDoc.id).get().then(results => {
if (results.empty) {
return;
}
for (const transactionDoc of results.docs) {
transactions.push(Transaction.fromSnapshotRef(transactionDoc));
}
});
});
subscriber.next(transactions);
});
});
}
getTransaction(id: string): Observable<Transaction> {
return Observable.create(subscriber => {
firebase.firestore().collection('transactions').doc(id).onSnapshot(snapshot => {
if (!snapshot.exists) {
return;
}
subscriber.next(Transaction.fromSnapshotRef(snapshot));
});
});
}
createTransaction(
name: string,
description: string,
amount: number,
date: Date,
isExpense: boolean,
category: string
): Observable<Transaction> {
return Observable.create(subscriber => {
firebase.firestore().collection('transactions').add({
name: name,
description: description,
date: date,
amount: amount,
category: category,
isExpense: isExpense,
}).then(docRef => {
docRef.get().then(snapshot => {
if (!snapshot) {
subscriber.console.error('Unable to retrieve saved transaction data');
return;
}
subscriber.next(Transaction.fromSnapshotRef(snapshot));
});
}).catch(err => {
subscriber.error(err);
});
});
}
updateTransaction(id: string, changes: object): Observable<boolean> {
return Observable.create(subscriber => {
firebase.firestore().collection('transactions').doc(id).update(changes).then(result => {
subscriber.next(true);
}).catch(err => {
subscriber.next(false);
});
});
}
deleteTransaction(id: string): Observable<boolean> {
return Observable.create(subscriber => {
firebase.firestore().collection('transactions').doc(id).delete().then(data => {
subscriber.next(true);
}).catch(err => {
console.log(err);
subscriber.next(false);
});
});
}
}

View file

@ -1,15 +0,0 @@
import { TestBed, inject } from '@angular/core/testing';
import { TransactionService } from './transaction.service';
describe('TransactionService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [TransactionService]
});
});
it('should be created', inject([TransactionService], (service: TransactionService) => {
expect(service).toBeTruthy();
}));
});

View file

@ -1,80 +1,27 @@
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs';
import { of, Observable, from } from 'rxjs';
import { Transaction } from './transaction'; import { Transaction } from './transaction';
import { TransactionType } from './transaction.type'; import { InjectionToken } from '@angular/core';
import { BudgetDatabase, ITransaction } from './budget-database';
import { AuthService } from './auth.service';
import { ApiService } from './api.service';
import * as firebase from 'firebase/app';
@Injectable({ export interface TransactionService {
providedIn: 'root'
})
export class TransactionService {
constructor( getTransactions(group: string, count?: number): Observable<Transaction[]>;
private db: BudgetDatabase,
private authService: AuthService,
private apiService: ApiService,
) { }
getTransactions(count?: number): Observable<ITransaction[]> { getTransactionsForCategory(category: string, count?: number): Observable<Transaction[]>;
// Check if we have a currently logged in user
if (!firebase.auth().currentUser) {
if (count) {
return from(this.db.transactions.orderBy('date').reverse().limit(count).toArray());
} else {
return from(this.db.transactions.orderBy('date').reverse().toArray());
}
}
return this.apiService.getTransactions();
}
getTransaction(id: number): Observable<Transaction> { getTransaction(id: string): Observable<Transaction>;
return Observable.create(subscriber => {
this.db.transactions.where('id').equals(id).first().then(transaction => {
if (!transaction) {
subscriber.error();
subscriber.complete();
return;
}
(transaction as Transaction).loadCategory(this.db);
(transaction as Transaction).loadAccount(this.db);
subscriber.next(transaction);
});
});
}
saveTransaction(transaction: Transaction): Observable<Transaction> { createTransaction(
this.db.transactions.put(transaction); name: string,
if (auth().currentUser) { description: string,
return this.apiService.saveTransaction(transaction); amount: number,
} else { date: Date,
return of(transaction); isExpense: boolean,
} category: string
} ): Observable<Transaction>;
updateTransaction(transaction: Transaction): Observable<any> { updateTransaction(id: string, changes: object): Observable<boolean>;
this.db.transactions.update(transaction.id, transaction);
return of([]);
}
deleteTransaction(transaction: Transaction): Observable<any> { deleteTransaction(id: string): Observable<boolean>;
return from(this.db.transactions.delete(transaction.id));
}
getBalance(): Observable<number> {
let sum = 0;
return from(
this.db.transactions.each(function (transaction) {
if (transaction.type === TransactionType.INCOME) {
sum += transaction.amount;
} else {
sum -= transaction.amount;
}
}).then(function () {
return sum;
})
);
}
} }
export let TRANSACTION_SERVICE = new InjectionToken<TransactionService>('transaction.service');

View file

@ -1,29 +1,30 @@
import { ITransaction, ICategory, BudgetDatabase, IAccount } from './budget-database'; import { ITransaction } from './budget-database';
import { Category } from './category'; import { Category } from './category';
import { TransactionType } from './transaction.type'; import { TransactionType } from './transaction.type';
import * as firebase from 'firebase/app';
export class Transaction implements ITransaction { export class Transaction implements ITransaction {
id: number; id: string;
accountId: number; accountId: string;
remoteId: number; remoteId: string;
title: string; title: string;
description: string; description: string;
amount: number; amount: number;
date: Date = new Date(); date: Date = new Date();
categoryId: number; categoryId: string;
type: TransactionType = TransactionType.EXPENSE; type: TransactionType = TransactionType.EXPENSE;
category: ICategory; category: Category;
account: IAccount; account: Account;
loadCategory(db: BudgetDatabase) { static fromSnapshotRef(snapshot: firebase.firestore.DocumentSnapshot): Transaction {
db.categories.where('id').equals(this.categoryId).first().then(category => { const transaction = new Transaction();
this.category = category; transaction.id = snapshot.id;
}); transaction.title = snapshot.get('name');
} transaction.description = snapshot.get('description');
transaction.amount = snapshot.get('amount');
loadAccount(db: BudgetDatabase) { transaction.categoryId = snapshot.get('category');
db.accounts.where('id').equals(this.accountId).first().then(account => { transaction.date = snapshot.get('date');
this.account = account; transaction.type = snapshot.get('isExpense') ? TransactionType.EXPENSE : TransactionType.INCOME;
}); return transaction;
} }
} }

View file

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, Input, Inject } from '@angular/core';
import { Transaction } from '../transaction'; import { Transaction } from '../transaction';
import { TransactionType } from '../transaction.type'; import { TransactionType } from '../transaction.type';
import { TransactionService } from '../transaction.service'; import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service';
import { AppComponent } from '../app.component'; import { AppComponent } from '../app.component';
@Component({ @Component({
@ -11,13 +11,14 @@ import { AppComponent } from '../app.component';
}) })
export class TransactionsComponent implements OnInit { export class TransactionsComponent implements OnInit {
@Input() group: string;
public transactionType = TransactionType; public transactionType = TransactionType;
public transactions: Transaction[] public transactions: Transaction[];
constructor( constructor(
private app: AppComponent, private app: AppComponent,
private transactionService: TransactionService, @Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
) { } ) { }
ngOnInit() { ngOnInit() {
@ -27,7 +28,7 @@ export class TransactionsComponent implements OnInit {
} }
getTransactions(): void { getTransactions(): void {
this.transactionService.getTransactions().subscribe(transactions => { this.transactionService.getTransactions(this.app.group).subscribe(transactions => {
this.transactions = transactions; this.transactions = transactions;
}); });
} }