diff --git a/src/app/add-edit-category/add-edit-category.component.ts b/src/app/add-edit-category/add-edit-category.component.ts index 75b434c..1fc3c98 100644 --- a/src/app/add-edit-category/add-edit-category.component.ts +++ b/src/app/add-edit-category/add-edit-category.component.ts @@ -1,8 +1,8 @@ -import { Component, OnInit, Input, OnDestroy } from '@angular/core'; -import { CategoryService } from '../category.service'; +import { Component, OnInit, Input, OnDestroy, Inject } from '@angular/core'; import { Category } from '../category'; import { Actionable } from '../actionable'; import { AppComponent } from '../app.component'; +import { CATEGORY_SERVICE, CategoryService } from '../category.service'; @Component({ selector: 'app-add-edit-category', @@ -13,10 +13,11 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy { @Input() title: string; @Input() currentCategory: Category; + @Input() group: string; constructor( private app: AppComponent, - private categoryService: CategoryService, + @Inject(CATEGORY_SERVICE) private categoryService: CategoryService, ) { } ngOnInit() { @@ -31,14 +32,17 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy { doAction(): void { this.currentCategory.amount *= 100; + let observable; if (this.currentCategory.id) { // This is an existing category, update it - this.categoryService.updateCategory(this.currentCategory); + observable = this.categoryService.updateCategory(this.currentCategory.id, this.currentCategory); } else { // 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); } - this.app.goBack(); + observable.subscribe(val => { + this.app.goBack(); + }); } getActionLabel(): string { @@ -46,7 +50,7 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy { } delete(): void { - this.categoryService.deleteCategory(this.currentCategory); + this.categoryService.deleteCategory(this.currentCategory.id); this.app.goBack(); } } diff --git a/src/app/add-edit-transaction/add-edit-transaction.component.ts b/src/app/add-edit-transaction/add-edit-transaction.component.ts index f8b7bea..84ed2f1 100644 --- a/src/app/add-edit-transaction/add-edit-transaction.component.ts +++ b/src/app/add-edit-transaction/add-edit-transaction.component.ts @@ -1,11 +1,11 @@ -import { Component, OnInit, Input, OnChanges, OnDestroy } from '@angular/core'; -import { Transaction } from '../transaction' -import { TransactionType } from '../transaction.type' -import { TransactionService } from '../transaction.service' -import { Category } from '../category' -import { CategoryService } from '../category.service' +import { Component, OnInit, Input, OnChanges, OnDestroy, Inject } from '@angular/core'; +import { Transaction } from '../transaction'; +import { TransactionType } from '../transaction.type'; +import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service'; +import { Category } from '../category'; import { AppComponent } from '../app.component'; import { Actionable } from '../actionable'; +import { CATEGORY_SERVICE, CategoryService } from '../category.service'; @Component({ selector: 'app-add-edit-transaction', @@ -15,6 +15,7 @@ import { Actionable } from '../actionable'; export class AddEditTransactionComponent implements OnInit, OnDestroy, Actionable { @Input() title: string; @Input() currentTransaction: Transaction; + @Input() group: string; public transactionType = TransactionType; public selectedCategory: Category; public categories: Category[]; @@ -22,8 +23,8 @@ export class AddEditTransactionComponent implements OnInit, OnDestroy, Actionabl constructor( private app: AppComponent, - private categoryService: CategoryService, - private transactionService: TransactionService, + @Inject(CATEGORY_SERVICE) private categoryService: CategoryService, + @Inject(TRANSACTION_SERVICE) private transactionService: TransactionService, ) { } 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 // to an integer this.currentTransaction.amount *= 100; + let observable; if (this.currentTransaction.id) { // This is an existing transaction, update it - this.transactionService.updateTransaction(this.currentTransaction); + observable = this.transactionService.updateTransaction(this.currentTransaction.id, this.currentTransaction); } else { // 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, + ); } - this.app.goBack(); + + observable.subscribe(val => { + this.app.goBack(); + }); } getActionLabel(): string { @@ -56,11 +68,11 @@ export class AddEditTransactionComponent implements OnInit, OnDestroy, Actionabl } delete(): void { - this.transactionService.deleteTransaction(this.currentTransaction); + this.transactionService.deleteTransaction(this.currentTransaction.id); this.app.goBack(); } getCategories() { - this.categoryService.getCategories().subscribe(categories => this.categories = categories); + this.categoryService.getCategories(this.app.group).subscribe(categories => this.categories = categories); } } diff --git a/src/app/api.service.spec.ts b/src/app/api.service.spec.ts deleted file mode 100644 index 7fac290..0000000 --- a/src/app/api.service.spec.ts +++ /dev/null @@ -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(); - })); -}); diff --git a/src/app/api.service.ts b/src/app/api.service.ts deleted file mode 100644 index 3c886b1..0000000 --- a/src/app/api.service.ts +++ /dev/null @@ -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 { - return this.http.post( - host + '/login', - { - 'username': username, - 'password': password - }, - httpOptions - ); - } - - register(username: string, email: string, password: string): Observable { - return this.http.post( - host + '/register', - { - 'username': username, - 'email': email, - 'password': password - }, - httpOptions - ); - } - - logout(): Observable { - return this.http.get( - host + '/logout', - httpOptions - ); - } - - getTransactions(): Observable { - 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 { - 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); - } - ); - }); - } -} diff --git a/src/app/app.component.html b/src/app/app.component.html index 04381e4..01fb7ec 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,6 +1,7 @@ + {{ getUsername() }} Manage Accounts Export Data Login diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 81234bb..056cd3c 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -15,6 +15,7 @@ export class AppComponent { public title = 'Budget'; public backEnabled = false; public actionable: Actionable; + public group = 'MG3KOiuPu0Xy38O2LdhJ'; constructor( public authService: AuthService, @@ -32,6 +33,10 @@ export class AppComponent { firebase.initializeApp(config); } + getUsername(): String { + return firebase.auth().currentUser.email; + } + goBack(): void { this.location.back(); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8573844..b0b6a54 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -38,7 +38,10 @@ import { UserComponent } from './user/user.component'; import { HttpClientModule } from '@angular/common/http'; import { CurrencyMaskModule } from 'ng2-currency-mask'; 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 = { align: 'left', @@ -91,6 +94,8 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = { ], providers: [ { provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig }, + { provide: TRANSACTION_SERVICE, useClass: TransactionServiceFirebaseFirestoreImpl }, + { provide: CATEGORY_SERVICE, useClass: CategoryServiceFirebaseFirestoreImpl }, ], bootstrap: [AppComponent] }) diff --git a/src/app/auth.service.ts b/src/app/auth.service.ts index f1dfe4e..6ce72fe 100644 --- a/src/app/auth.service.ts +++ b/src/app/auth.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { ApiService } from './api.service'; import { User } from './user'; import { Router } from '@angular/router'; import * as firebase from 'firebase/app'; @@ -10,7 +9,6 @@ import * as firebase from 'firebase/app'; export class AuthService { constructor( - private apiService: ApiService, private router: Router, ) { } @@ -23,17 +21,14 @@ export class AuthService { }); } - // register(user: User) { - // this.apiService.register(user.name, user.email, user.password).subscribe( - // value => { - // this.login(value); - // }, - // error => { - // console.log('Registration failed'); - // console.log(error); - // } - // ); - // } + register(email: string, password: string) { + firebase.auth().createUserWithEmailAndPassword(email, password).then(value => { + this.router.navigate(['/']); + }).catch(err => { + console.log('Login failed'); + console.log(err); + }); + } logout() { firebase.auth().signOut().then(value => { diff --git a/src/app/budget-database.ts b/src/app/budget-database.ts index 0c8ed1a..2cf011b 100644 --- a/src/app/budget-database.ts +++ b/src/app/budget-database.ts @@ -67,21 +67,21 @@ export class BudgetDatabase extends Dexie { } export interface ITransaction { - id: number; - accountId: number; - remoteId: number; + id: string; + accountId: string; + remoteId: string; title: string; description: string; amount: number; date: Date; - categoryId: number; + categoryId: string; type: TransactionType; } export interface ICategory { - id: number; - accountId: number; - remoteId: number; + id: string; + accountId: string; + remoteId: string; name: string; amount: number; repeat: string; diff --git a/src/app/categories/categories.component.ts b/src/app/categories/categories.component.ts index 776ad58..d7dda16 100644 --- a/src/app/categories/categories.component.ts +++ b/src/app/categories/categories.component.ts @@ -1,7 +1,10 @@ -import { Component, OnInit } from '@angular/core'; -import { CategoryService } from '../category.service'; +import { Component, OnInit, Input, Inject } from '@angular/core'; +import { CategoryService, CATEGORY_SERVICE } from '../category.service'; import { Category } from '../category'; import { AppComponent } from '../app.component'; +import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service'; +import { Observable } from 'rxjs'; +import { TransactionType } from '../transaction.type'; @Component({ selector: 'app-categories', @@ -10,12 +13,14 @@ import { AppComponent } from '../app.component'; }) export class CategoriesComponent implements OnInit { + @Input() group: string; public categories: Category[]; - public categoryBalances: Map; + public categoryBalances: Map; constructor( private app: AppComponent, - private categoryService: CategoryService, + @Inject(CATEGORY_SERVICE) private categoryService: CategoryService, + @Inject(TRANSACTION_SERVICE) private transactionService: TransactionService, ) { } ngOnInit() { @@ -26,11 +31,27 @@ export class CategoriesComponent implements OnInit { } getCategories(): void { - this.categoryService.getCategories().subscribe(categories => { + this.categoryService.getCategories(this.app.group).subscribe(categories => { this.categories = 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 { + 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); + }); + }); + } } diff --git a/src/app/category-details/category-details.component.ts b/src/app/category-details/category-details.component.ts index 5d9014e..b0cbd87 100644 --- a/src/app/category-details/category-details.component.ts +++ b/src/app/category-details/category-details.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; -import { CategoryService } from '../category.service' -import { Category } from '../category' -import { ActivatedRoute } from '@angular/router' +import { CategoryServiceFirebaseFirestoreImpl } from '../category.service.firestore'; +import { Category } from '../category'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-category-details', @@ -14,15 +14,15 @@ export class CategoryDetailsComponent implements OnInit { constructor( private route: ActivatedRoute, - private categoryService: CategoryService + private categoryService: CategoryServiceFirebaseFirestoreImpl ) { } ngOnInit() { - this.getCategory() + this.getCategory(); } getCategory(): void { - const id = +this.route.snapshot.paramMap.get('id') + const id = this.route.snapshot.paramMap.get('id'); this.categoryService.getCategory(id) .subscribe(category => { category.amount /= 100; diff --git a/src/app/category-list/category-list.component.ts b/src/app/category-list/category-list.component.ts index e6b99d9..c541afe 100644 --- a/src/app/category-list/category-list.component.ts +++ b/src/app/category-list/category-list.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, Input } from '@angular/core'; -import { Category } from '../category' +import { Category } from '../category'; @Component({ selector: 'app-category-list', @@ -9,7 +9,7 @@ import { Category } from '../category' export class CategoryListComponent implements OnInit { @Input() categories: Category[]; - @Input() categoryBalances: Map; + @Input() categoryBalances: Map; constructor() { } @@ -30,7 +30,7 @@ export class CategoryListComponent implements OnInit { return 0; } - let categoryBalance = this.categoryBalances.get(category.id) + let categoryBalance = this.categoryBalances.get(category.id); if (!categoryBalance) { categoryBalance = 0; } @@ -39,9 +39,9 @@ export class CategoryListComponent implements OnInit { // 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) + categoryBalance = Math.abs(categoryBalance); } else { - categoryBalance -= (categoryBalance * 2) + categoryBalance -= (categoryBalance * 2); } return categoryBalance / category.amount * 100; diff --git a/src/app/category.service.firestore.spec.ts b/src/app/category.service.firestore.spec.ts new file mode 100644 index 0000000..66704a3 --- /dev/null +++ b/src/app/category.service.firestore.spec.ts @@ -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(); + })); +}); diff --git a/src/app/category.service.firestore.ts b/src/app/category.service.firestore.ts new file mode 100644 index 0000000..1da7893 --- /dev/null +++ b/src/app/category.service.firestore.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + return Observable.create(subscriber => { + firebase.firestore().collection('categories').doc(id).delete().then(result => { + subscriber.next(true); + }).catch(err => { + console.error(err); + subscriber.next(false); + }); + }); + } +} diff --git a/src/app/category.service.spec.ts b/src/app/category.service.spec.ts deleted file mode 100644 index 34cdebc..0000000 --- a/src/app/category.service.spec.ts +++ /dev/null @@ -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(); - })); -}); diff --git a/src/app/category.service.ts b/src/app/category.service.ts index e5fff18..dc88712 100644 --- a/src/app/category.service.ts +++ b/src/app/category.service.ts @@ -1,55 +1,18 @@ -import { Injectable } from '@angular/core'; -import { of, Observable, from } from 'rxjs'; -import { BudgetDatabase } from './budget-database'; -import { TransactionType } from './transaction.type' -import { Category } from './category' +import { Observable } from 'rxjs'; +import { Category } from './category'; +import { InjectionToken } from '@angular/core'; -@Injectable({ - providedIn: 'root' -}) -export class CategoryService { +export interface CategoryService { - constructor(private db: BudgetDatabase) { } + getCategories(group: string, count?: number): Observable; - getCategories(count?: number): Observable { - let collection = this.db.categories.orderBy('name'); - if (count) { - return from(collection.limit(count).toArray()) - } else { - return from(collection.toArray()) - } - } + getCategory(id: string): Observable; - getCategory(id: number): Observable { - return from(this.db.categories.where('id').equals(id).first()) - } + createCategory(name: string, amount: number, group: string): Observable; - saveCategory(category: Category): Observable { - this.db.categories.put(category) - return of(category) - } + updateCategory(id: string, changes: object): Observable; - updateCategory(category: Category): Observable { - this.db.categories.update(category.id, category) - return of([]) - } - - deleteCategory(category: Category): Observable { - return from(this.db.categories.delete(category.id)) - } - - getBalance(category: Category): Observable { - 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; - }) - ) - } + deleteCategory(id: string): Observable; } + +export let CATEGORY_SERVICE = new InjectionToken('category.service'); diff --git a/src/app/category.ts b/src/app/category.ts index 5cb8aaa..b434944 100644 --- a/src/app/category.ts +++ b/src/app/category.ts @@ -1,11 +1,20 @@ -import { ICategory } from './budget-database' +import { ICategory } from './budget-database'; export class Category implements ICategory { - id: number; - accountId: number; - remoteId: number; + id: string; + accountId: string; + remoteId: string; name: string; amount: number; repeat: 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; + } } diff --git a/src/app/dashboard/dashboard.component.html b/src/app/dashboard/dashboard.component.html index 22ad49a..5600741 100644 --- a/src/app/dashboard/dashboard.component.html +++ b/src/app/dashboard/dashboard.component.html @@ -2,7 +2,7 @@

Current Balance:
- {{ balance / 100 | currency }} + {{ getBalance() / 100 | currency }}

View Transactions diff --git a/src/app/dashboard/dashboard.component.ts b/src/app/dashboard/dashboard.component.ts index bf599e1..753c511 100644 --- a/src/app/dashboard/dashboard.component.ts +++ b/src/app/dashboard/dashboard.component.ts @@ -1,10 +1,11 @@ -import { Component, OnInit } from '@angular/core'; -import { Transaction } from '../transaction' -import { TransactionService } from '../transaction.service' -import { CategoryService } from '../category.service' -import { Category } from '../category' +import { Component, OnInit, Inject } from '@angular/core'; +import { Transaction } from '../transaction'; +import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service'; +import { Category } from '../category'; 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({ selector: 'app-dashboard', @@ -13,41 +14,62 @@ import { AuthService } from '../auth.service'; }) export class DashboardComponent implements OnInit { - public balance: number; public transactions: Transaction[]; public categories: Category[]; - categoryBalances: Map; + categoryBalances: Map; constructor( private app: AppComponent, - private transactionService: TransactionService, - private categoryService: CategoryService + @Inject(TRANSACTION_SERVICE) private transactionService: TransactionService, + @Inject(CATEGORY_SERVICE) private categoryService: CategoryService, ) { } ngOnInit() { this.app.backEnabled = false; this.app.title = 'My Finances'; - this.balance = 0; this.getBalance(); this.getTransactions(); this.getCategories(); this.categoryBalances = new Map(); } - getBalance(): void { - this.transactionService.getBalance().subscribe(balance => this.balance = balance) + getBalance(): number { + let totalBalance = 0; + if (!this.categoryBalances) { + return 0; + } + this.categoryBalances.forEach(balance => { + totalBalance += balance; + }); + return totalBalance; } getTransactions(): void { - this.transactionService.getTransactions(5).subscribe(transactions => this.transactions = transactions); + this.transactionService.getTransactions(this.app.group, 5).subscribe(transactions => this.transactions = transactions); } getCategories(): void { - this.categoryService.getCategories(5).subscribe(categories => { + this.categoryService.getCategories(this.app.group, 5).subscribe(categories => { this.categories = categories; - for (const category of this.categories) { - this.categoryService.getBalance(category).subscribe(balance => this.categoryBalances.set(category.id, balance)) + for (const category of categories) { + this.getCategoryBalance(category.id).subscribe(balance => this.categoryBalances.set(category.id, balance)); } }); } + + getCategoryBalance(category: string): Observable { + 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); + }); + }); + } } diff --git a/src/app/register/register.component.ts b/src/app/register/register.component.ts index 356ca4e..ae788f0 100644 --- a/src/app/register/register.component.ts +++ b/src/app/register/register.component.ts @@ -34,7 +34,7 @@ export class RegisterComponent implements OnInit, OnDestroy, Actionable { alert('Passwords don\'t match'); return; } - this.authService.register(this.user); + this.authService.register(this.user.email, this.user.password); } getActionLabel() { diff --git a/src/app/transaction-details/transaction-details.component.ts b/src/app/transaction-details/transaction-details.component.ts index 978373e..51f5cd8 100644 --- a/src/app/transaction-details/transaction-details.component.ts +++ b/src/app/transaction-details/transaction-details.component.ts @@ -1,7 +1,7 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { TransactionService } from '../transaction.service' +import { Component, OnInit, Input, Inject } from '@angular/core'; +import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service'; import { ActivatedRoute } from '@angular/router'; -import { Transaction } from '../transaction' +import { Transaction } from '../transaction'; @Component({ selector: 'app-transaction-details', @@ -14,15 +14,15 @@ export class TransactionDetailsComponent implements OnInit { constructor( private route: ActivatedRoute, - private transactionService: TransactionService + @Inject(TRANSACTION_SERVICE) private transactionService: TransactionService, ) { } ngOnInit() { - this.getTransaction() + this.getTransaction(); } getTransaction(): void { - const id = +this.route.snapshot.paramMap.get('id') + const id = this.route.snapshot.paramMap.get('id'); this.transactionService.getTransaction(id) .subscribe(transaction => { transaction.amount /= 100; diff --git a/src/app/transaction.service.firebase.spec.ts b/src/app/transaction.service.firebase.spec.ts new file mode 100644 index 0000000..871939f --- /dev/null +++ b/src/app/transaction.service.firebase.spec.ts @@ -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(); + })); +}); diff --git a/src/app/transaction.service.firestore.ts b/src/app/transaction.service.firestore.ts new file mode 100644 index 0000000..2f08c16 --- /dev/null +++ b/src/app/transaction.service.firestore.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + return Observable.create(subscriber => { + firebase.firestore().collection('transactions').doc(id).delete().then(data => { + subscriber.next(true); + }).catch(err => { + console.log(err); + subscriber.next(false); + }); + }); + } +} diff --git a/src/app/transaction.service.spec.ts b/src/app/transaction.service.spec.ts deleted file mode 100644 index 43a784b..0000000 --- a/src/app/transaction.service.spec.ts +++ /dev/null @@ -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(); - })); -}); diff --git a/src/app/transaction.service.ts b/src/app/transaction.service.ts index f8f0a0e..255a98c 100644 --- a/src/app/transaction.service.ts +++ b/src/app/transaction.service.ts @@ -1,80 +1,27 @@ -import { Injectable } from '@angular/core'; -import { of, Observable, from } from 'rxjs'; +import { Observable } from 'rxjs'; import { Transaction } from './transaction'; -import { TransactionType } from './transaction.type'; -import { BudgetDatabase, ITransaction } from './budget-database'; -import { AuthService } from './auth.service'; -import { ApiService } from './api.service'; -import * as firebase from 'firebase/app'; +import { InjectionToken } from '@angular/core'; -@Injectable({ - providedIn: 'root' -}) -export class TransactionService { +export interface TransactionService { - constructor( - private db: BudgetDatabase, - private authService: AuthService, - private apiService: ApiService, - ) { } + getTransactions(group: string, count?: number): Observable; - getTransactions(count?: number): Observable { - // 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(); - } + getTransactionsForCategory(category: string, count?: number): Observable; - getTransaction(id: number): Observable { - 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); - }); - }); - } + getTransaction(id: string): Observable; - saveTransaction(transaction: Transaction): Observable { - this.db.transactions.put(transaction); - if (auth().currentUser) { - return this.apiService.saveTransaction(transaction); - } else { - return of(transaction); - } - } + createTransaction( + name: string, + description: string, + amount: number, + date: Date, + isExpense: boolean, + category: string + ): Observable; - updateTransaction(transaction: Transaction): Observable { - this.db.transactions.update(transaction.id, transaction); - return of([]); - } + updateTransaction(id: string, changes: object): Observable; - deleteTransaction(transaction: Transaction): Observable { - return from(this.db.transactions.delete(transaction.id)); - } - - getBalance(): Observable { - 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; - }) - ); - } + deleteTransaction(id: string): Observable; } + +export let TRANSACTION_SERVICE = new InjectionToken('transaction.service'); diff --git a/src/app/transaction.ts b/src/app/transaction.ts index 7b0067b..fe05805 100644 --- a/src/app/transaction.ts +++ b/src/app/transaction.ts @@ -1,29 +1,30 @@ -import { ITransaction, ICategory, BudgetDatabase, IAccount } from './budget-database'; +import { ITransaction } from './budget-database'; import { Category } from './category'; import { TransactionType } from './transaction.type'; +import * as firebase from 'firebase/app'; export class Transaction implements ITransaction { - id: number; - accountId: number; - remoteId: number; - title: string; - description: string; - amount: number; - date: Date = new Date(); - categoryId: number; - type: TransactionType = TransactionType.EXPENSE; - category: ICategory; - account: IAccount; + id: string; + accountId: string; + remoteId: string; + title: string; + description: string; + amount: number; + date: Date = new Date(); + categoryId: string; + type: TransactionType = TransactionType.EXPENSE; + category: Category; + account: Account; - loadCategory(db: BudgetDatabase) { - db.categories.where('id').equals(this.categoryId).first().then(category => { - this.category = category; - }); - } - - loadAccount(db: BudgetDatabase) { - db.accounts.where('id').equals(this.accountId).first().then(account => { - this.account = account; - }); - } + static fromSnapshotRef(snapshot: firebase.firestore.DocumentSnapshot): Transaction { + const transaction = new Transaction(); + transaction.id = snapshot.id; + transaction.title = snapshot.get('name'); + transaction.description = snapshot.get('description'); + transaction.amount = snapshot.get('amount'); + transaction.categoryId = snapshot.get('category'); + transaction.date = snapshot.get('date'); + transaction.type = snapshot.get('isExpense') ? TransactionType.EXPENSE : TransactionType.INCOME; + return transaction; + } } diff --git a/src/app/transaction.type.ts b/src/app/transaction.type.ts index d3722d8..3f138d3 100644 --- a/src/app/transaction.type.ts +++ b/src/app/transaction.type.ts @@ -1,4 +1,4 @@ export enum TransactionType { INCOME, EXPENSE -} \ No newline at end of file +} diff --git a/src/app/transactions/transactions.component.ts b/src/app/transactions/transactions.component.ts index 8018a0b..7e2db72 100644 --- a/src/app/transactions/transactions.component.ts +++ b/src/app/transactions/transactions.component.ts @@ -1,7 +1,7 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, Input, Inject } from '@angular/core'; import { Transaction } from '../transaction'; import { TransactionType } from '../transaction.type'; -import { TransactionService } from '../transaction.service'; +import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service'; import { AppComponent } from '../app.component'; @Component({ @@ -11,13 +11,14 @@ import { AppComponent } from '../app.component'; }) export class TransactionsComponent implements OnInit { + @Input() group: string; public transactionType = TransactionType; - public transactions: Transaction[] + public transactions: Transaction[]; constructor( private app: AppComponent, - private transactionService: TransactionService, + @Inject(TRANSACTION_SERVICE) private transactionService: TransactionService, ) { } ngOnInit() { @@ -27,7 +28,7 @@ export class TransactionsComponent implements OnInit { } getTransactions(): void { - this.transactionService.getTransactions().subscribe(transactions => { + this.transactionService.getTransactions(this.app.group).subscribe(transactions => { this.transactions = transactions; }); }