WIP: Save transactions and categories in Firebase
This commit is contained in:
parent
279d644a2e
commit
7cb97d1487
28 changed files with 467 additions and 383 deletions
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}));
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
<mat-sidenav-container class="sidenav-container">
|
||||
<mat-sidenav #sidenav mode="over" closed>
|
||||
<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 (click)="exportData()">Export Data</a>
|
||||
<a mat-list-item *ngIf="!isLoggedIn()" routerLink="/login">Login</a>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
})
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<number, number>;
|
||||
public categoryBalances: Map<string, number>;
|
||||
|
||||
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<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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<number, number>;
|
||||
@Input() categoryBalances: Map<string, number>;
|
||||
|
||||
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;
|
||||
|
|
15
src/app/category.service.firestore.spec.ts
Normal file
15
src/app/category.service.firestore.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
100
src/app/category.service.firestore.ts
Normal file
100
src/app/category.service.firestore.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}));
|
||||
});
|
|
@ -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<Category[]>;
|
||||
|
||||
getCategories(count?: number): Observable<Category[]> {
|
||||
let collection = this.db.categories.orderBy('name');
|
||||
if (count) {
|
||||
return from(collection.limit(count).toArray())
|
||||
} else {
|
||||
return from(collection.toArray())
|
||||
}
|
||||
}
|
||||
getCategory(id: string): Observable<Category>;
|
||||
|
||||
getCategory(id: number): Observable<Category> {
|
||||
return from(this.db.categories.where('id').equals(id).first())
|
||||
}
|
||||
createCategory(name: string, amount: number, group: string): Observable<Category>;
|
||||
|
||||
saveCategory(category: Category): Observable<Category> {
|
||||
this.db.categories.put(category)
|
||||
return of(category)
|
||||
}
|
||||
updateCategory(id: string, changes: object): Observable<boolean>;
|
||||
|
||||
updateCategory(category: Category): Observable<any> {
|
||||
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;
|
||||
})
|
||||
)
|
||||
}
|
||||
deleteCategory(id: string): Observable<boolean>;
|
||||
}
|
||||
|
||||
export let CATEGORY_SERVICE = new InjectionToken<CategoryService>('category.service');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="dashboard-primary">
|
||||
<h2 class="balance">
|
||||
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>
|
||||
<div class="transaction-navigation">
|
||||
<a mat-button routerLink="/transactions">View Transactions</a>
|
||||
|
|
|
@ -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<number, number>;
|
||||
categoryBalances: Map<string, number>;
|
||||
|
||||
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 = <Transaction[]> transactions);
|
||||
this.transactionService.getTransactions(this.app.group, 5).subscribe(transactions => this.transactions = <Transaction[]>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<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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
19
src/app/transaction.service.firebase.spec.ts
Normal file
19
src/app/transaction.service.firebase.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
112
src/app/transaction.service.firestore.ts
Normal file
112
src/app/transaction.service.firestore.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}));
|
||||
});
|
|
@ -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<Transaction[]>;
|
||||
|
||||
getTransactions(count?: number): Observable<ITransaction[]> {
|
||||
// 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<Transaction[]>;
|
||||
|
||||
getTransaction(id: number): 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
getTransaction(id: string): Observable<Transaction>;
|
||||
|
||||
saveTransaction(transaction: Transaction): Observable<Transaction> {
|
||||
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<Transaction>;
|
||||
|
||||
updateTransaction(transaction: Transaction): Observable<any> {
|
||||
this.db.transactions.update(transaction.id, transaction);
|
||||
return of([]);
|
||||
}
|
||||
updateTransaction(id: string, changes: object): Observable<boolean>;
|
||||
|
||||
deleteTransaction(transaction: Transaction): Observable<any> {
|
||||
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;
|
||||
})
|
||||
);
|
||||
}
|
||||
deleteTransaction(id: string): Observable<boolean>;
|
||||
}
|
||||
|
||||
export let TRANSACTION_SERVICE = new InjectionToken<TransactionService>('transaction.service');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export enum TransactionType {
|
||||
INCOME,
|
||||
EXPENSE
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue