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 { 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);
|
||||||
}
|
}
|
||||||
this.app.goBack();
|
observable.subscribe(val => {
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.app.goBack();
|
|
||||||
|
observable.subscribe(val => {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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-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>
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
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 { 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');
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
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 { 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');
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export enum TransactionType {
|
export enum TransactionType {
|
||||||
INCOME,
|
INCOME,
|
||||||
EXPENSE
|
EXPENSE
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue