WIP: Save transactions and categories in Firebase

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

View file

@ -1,8 +1,8 @@
import { Component, OnInit, Input, OnDestroy } from '@angular/core';
import { 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();
}
}

View file

@ -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);
}
}

View file

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

View file

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

View file

@ -1,6 +1,7 @@
<mat-sidenav-container class="sidenav-container">
<mat-sidenav #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>

View file

@ -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();
}

View file

@ -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]
})

View file

@ -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 => {

View file

@ -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;

View file

@ -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);
});
});
}
}

View file

@ -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;

View file

@ -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;

View file

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

View file

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

View file

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

View file

@ -1,55 +1,18 @@
import { Injectable } from '@angular/core';
import { 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');

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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);
});
});
}
}

View file

@ -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() {

View file

@ -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;

View file

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

View file

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

View file

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

View file

@ -1,80 +1,27 @@
import { Injectable } from '@angular/core';
import { 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');

View file

@ -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;
}
}

View file

@ -1,4 +1,4 @@
export enum TransactionType {
INCOME,
EXPENSE
}
}

View file

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, Input, Inject } from '@angular/core';
import { Transaction } from '../transaction';
import { 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;
});
}