Large rewrite to consolidate services and integrate with Spring backend
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
c4626c369f
commit
4d96740fc7
73 changed files with 909 additions and 909 deletions
41
package-lock.json
generated
41
package-lock.json
generated
|
@ -7908,7 +7908,8 @@
|
|||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
|
@ -7929,12 +7930,14 @@
|
|||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
|
@ -7949,17 +7952,20 @@
|
|||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
|
@ -8076,7 +8082,8 @@
|
|||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
|
@ -8088,6 +8095,7 @@
|
|||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
|
@ -8102,6 +8110,7 @@
|
|||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
|
@ -8109,12 +8118,14 @@
|
|||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
|
@ -8133,6 +8144,7 @@
|
|||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
|
@ -8220,7 +8232,8 @@
|
|||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
|
@ -8232,6 +8245,7 @@
|
|||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
|
@ -8317,7 +8331,8 @@
|
|||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
|
@ -8353,6 +8368,7 @@
|
|||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
@ -8372,6 +8388,7 @@
|
|||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
|
@ -8415,12 +8432,14 @@
|
|||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"scripts": {
|
||||
"ng": "node_modules/@angular/cli/bin/ng",
|
||||
"start": "node_modules/@angular/cli/bin/ng serve --host '0.0.0.0'",
|
||||
"code-server": "node_modules/@angular/cli/bin/ng serve --host \"0.0.0.0\" --disable-host-check --base-href /angular/",
|
||||
"build": "node_modules/@angular/cli/bin/ng build",
|
||||
"package": "node_modules/@angular/cli/bin/ng build --prod --service-worker",
|
||||
"publish": "node_modules/@angular/cli/bin/ng build --prod --service-worker && firebase deploy",
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { AccountService } from './account.service';
|
||||
import { FirestoreAccountService } from './account.service.firestore';
|
||||
|
||||
describe('AccountService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [FirestoreAccountService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([FirestoreAccountService], (service: AccountService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -1,88 +0,0 @@
|
|||
import { Observable } from 'rxjs';
|
||||
import { AccountService } from './account.service';
|
||||
import * as firebase from 'firebase/app';
|
||||
import 'firebase/firestore';
|
||||
import { Account } from './account';
|
||||
import { User } from '../users/user';
|
||||
|
||||
export class FirestoreAccountService implements AccountService {
|
||||
getAccounts(): Observable<Account[]> {
|
||||
return Observable.create(subscriber => {
|
||||
const accounts = [];
|
||||
firebase.auth().onAuthStateChanged(user => {
|
||||
if (user == null) { return; }
|
||||
firebase.firestore().collection('accounts')
|
||||
.orderBy('name')
|
||||
.where('members', 'array-contains', user.uid)
|
||||
.onSnapshot(
|
||||
data => {
|
||||
if (!data.empty) {
|
||||
data.docs.map(account => accounts.push(Account.fromSnapshotRef(account)));
|
||||
}
|
||||
subscriber.next(accounts);
|
||||
},
|
||||
error => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAccount(id: string): Observable<Account> {
|
||||
return Observable.create(subscriber => {
|
||||
firebase.firestore().collection('accounts').doc(id).onSnapshot(snapshot => {
|
||||
if (!snapshot.exists) {
|
||||
return;
|
||||
}
|
||||
subscriber.next(Account.fromSnapshotRef(snapshot));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createAccount(
|
||||
name: string,
|
||||
description: string,
|
||||
currency: string,
|
||||
members: string[],
|
||||
): Observable<Account> {
|
||||
return Observable.create(subscriber => {
|
||||
firebase.firestore().collection('accounts').add({
|
||||
name: name,
|
||||
description: description,
|
||||
currency: currency,
|
||||
members: members
|
||||
}).then(docRef => {
|
||||
docRef.get().then(snapshot => {
|
||||
if (!snapshot) {
|
||||
subscriber.console.error('Unable to retrieve saved account data');
|
||||
return;
|
||||
}
|
||||
subscriber.next(Account.fromSnapshotRef(snapshot));
|
||||
});
|
||||
}).catch(err => {
|
||||
subscriber.error(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateAccount(id: string, changes: object): Observable<Account> {
|
||||
return Observable.create(subscriber => {
|
||||
firebase.firestore().collection('accounts').doc(id).update(changes).then(result => {
|
||||
subscriber.next(true);
|
||||
}).catch(err => {
|
||||
subscriber.next(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteAccount(id: string): Observable<boolean> {
|
||||
return Observable.create(subscriber => {
|
||||
firebase.firestore().collection('accounts').doc(id).delete().then(data => {
|
||||
subscriber.next(true);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
subscriber.next(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import { InjectionToken } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { User } from '../users/user';
|
||||
import { Account } from './account';
|
||||
|
||||
export interface AccountService {
|
||||
getAccounts(): Observable<Account[]>;
|
||||
getAccount(id: string): Observable<Account>;
|
||||
createAccount(
|
||||
name: string,
|
||||
description: string,
|
||||
currency: string,
|
||||
members: string[],
|
||||
): Observable<Account>;
|
||||
updateAccount(id: string, changes: object): Observable<Account>;
|
||||
deleteAccount(id: string): Observable<boolean>;
|
||||
}
|
||||
|
||||
export let ACCOUNT_SERVICE = new InjectionToken<AccountService>('account.service');
|
|
@ -1,17 +0,0 @@
|
|||
import * as firebase from 'firebase/app';
|
||||
|
||||
export class Account {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
currency: string;
|
||||
|
||||
static fromSnapshotRef(snapshot: firebase.firestore.DocumentSnapshot): Account {
|
||||
const account = new Account();
|
||||
account.id = snapshot.id;
|
||||
account.name = snapshot.get('name');
|
||||
account.description = snapshot.get('description');
|
||||
account.currency = snapshot.get('currency');
|
||||
return account;
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
<div class="dashboard" *ngIf="!isLoggedIn()">
|
||||
<h2 class="log-in">Get started</h2>
|
||||
<p>To begin tracking your finances, <a routerLink="/login">login</a> or <a routerLink="/register">create an account</a>!</p>
|
||||
</div>
|
||||
<mat-nav-list class="accounts" *ngIf="isLoggedIn()">
|
||||
<a mat-list-item *ngFor="let account of accounts" routerLink="/accounts/{{ account.id }}">
|
||||
<p matLine class="account-list-title">
|
||||
{{ account.name }}
|
||||
</p>
|
||||
<p matLine class="account-list-description">
|
||||
{{ account.description }}
|
||||
</p>
|
||||
</a>
|
||||
</mat-nav-list>
|
||||
<div class="no-accounts" *ngIf="!accounts || accounts.length === 0">
|
||||
<a mat-button routerLink="/accounts/new">
|
||||
<mat-icon>add</mat-icon>
|
||||
<p>Add accounts to begin tracking your budget.</p>
|
||||
</a>
|
||||
</div>
|
||||
<a mat-fab routerLink="/accounts/new">
|
||||
<mat-icon aria-label="Add">add</mat-icon>
|
||||
</a>
|
|
@ -1,32 +0,0 @@
|
|||
import { Component, OnInit, Input, Inject } from '@angular/core';
|
||||
import { AppComponent } from '../app.component';
|
||||
import { ACCOUNT_SERVICE, AccountService } from './account.service';
|
||||
import { Account } from './account';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accounts',
|
||||
templateUrl: './accounts.component.html',
|
||||
styleUrls: ['./accounts.component.css']
|
||||
})
|
||||
export class AccountsComponent implements OnInit {
|
||||
|
||||
@Input() accountId: string;
|
||||
public accounts: Account[];
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
@Inject(ACCOUNT_SERVICE) private accountService: AccountService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.backEnabled = true;
|
||||
this.app.title = 'Accounts';
|
||||
this.accountService.getAccounts().subscribe(accounts => {
|
||||
this.accounts = accounts;
|
||||
});
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return this.app.isLoggedIn();
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
<div [hidden]="account">
|
||||
<p>Select an account from the list to view details about it or edit it.</p>
|
||||
</div>
|
||||
<div [hidden]="!account" class="form account-form">
|
||||
<mat-form-field>
|
||||
<input matInput [(ngModel)]="account.name" placeholder="Name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<textarea matInput [(ngModel)]="account.description" placeholder="Description"></textarea>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput type="text" [(ngModel)]="account.currency" placeholder="Currency" required>
|
||||
</mat-form-field>
|
||||
<button class="button-delete" mat-button color="warn" *ngIf="account.id" (click)="delete()">Delete</button>
|
||||
</div>
|
|
@ -1 +0,0 @@
|
|||
<app-add-edit-account [title]="'Add Account'" [account]="account"></app-add-edit-account>
|
|
@ -1,20 +0,0 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Account } from '../account';
|
||||
|
||||
@Component({
|
||||
selector: 'app-new-account',
|
||||
templateUrl: './new-account.component.html',
|
||||
styleUrls: ['./new-account.component.css']
|
||||
})
|
||||
export class NewAccountComponent implements OnInit {
|
||||
|
||||
public account: Account;
|
||||
|
||||
constructor() {
|
||||
this.account = new Account();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -8,23 +8,23 @@ import { CategoryDetailsComponent } from './categories/category-details/category
|
|||
import { NewCategoryComponent } from './categories/new-category/new-category.component';
|
||||
import { LoginComponent } from './users/login/login.component';
|
||||
import { RegisterComponent } from './users/register/register.component';
|
||||
import { AccountsComponent } from './accounts/accounts.component';
|
||||
import { NewAccountComponent } from './accounts/new-account/new-account.component';
|
||||
import { AccountDetailsComponent } from './accounts/account-details/account-details.component';
|
||||
import { BudgetsComponent } from './budgets/budget.component';
|
||||
import { NewBudgetComponent } from './budgets/new-budget/new-budget.component';
|
||||
import { BudgetDetailsComponent } from './budgets/budget-details/budget-details.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: AccountsComponent },
|
||||
{ path: '', component: BudgetsComponent },
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{ path: 'register', component: RegisterComponent },
|
||||
{ path: 'accounts', component: AccountsComponent },
|
||||
{ path: 'accounts/new', component: NewAccountComponent },
|
||||
{ path: 'accounts/:id', component: AccountDetailsComponent },
|
||||
{ path: 'accounts/:accountId/transactions', component: TransactionsComponent },
|
||||
{ path: 'accounts/:accountId/transactions/new', component: NewTransactionComponent },
|
||||
{ path: 'accounts/:accountId/transactions/:id', component: TransactionDetailsComponent },
|
||||
{ path: 'accounts/:accountId/categories', component: CategoriesComponent },
|
||||
{ path: 'accounts/:accountId/categories/new', component: NewCategoryComponent },
|
||||
{ path: 'accounts/:accountId/categories/:id', component: CategoryDetailsComponent },
|
||||
{ path: 'budgets', component: BudgetsComponent },
|
||||
{ path: 'budgets/new', component: NewBudgetComponent },
|
||||
{ path: 'budgets/:id', component: BudgetDetailsComponent },
|
||||
{ path: 'budgets/:budgetId/transactions', component: TransactionsComponent },
|
||||
{ path: 'budgets/:budgetId/transactions/new', component: NewTransactionComponent },
|
||||
{ path: 'budgets/:budgetId/transactions/:id', component: TransactionDetailsComponent },
|
||||
{ path: 'budgets/:budgetId/categories', component: CategoriesComponent },
|
||||
{ path: 'budgets/:budgetId/categories/new', component: NewCategoryComponent },
|
||||
{ path: 'budgets/:budgetId/categories/:id', component: CategoryDetailsComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<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 *ngIf="isLoggedIn()" routerLink="/accounts">Accounts</a>
|
||||
<a mat-list-item *ngIf="isLoggedIn()" routerLink="/budgets">Budgets</a>
|
||||
<a mat-list-item *ngIf="!isLoggedIn()" routerLink="/login">Login</a>
|
||||
<a mat-list-item *ngIf="!isLoggedIn()" routerLink="/register">Register</a>
|
||||
<a mat-list-item *ngIf="isLoggedIn()" (click)="logout()">Logout</a>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { Actionable } from './actionable';
|
||||
import { AuthService } from './users/auth.service';
|
||||
import * as firebase from 'firebase/app';
|
||||
import 'firebase/auth';
|
||||
import { User } from './users/user';
|
||||
import { TWIGS_SERVICE, TwigsService } from './shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -11,27 +10,18 @@ import 'firebase/auth';
|
|||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
public title = 'Budget';
|
||||
public title = 'Twigs';
|
||||
public backEnabled = false;
|
||||
public actionable: Actionable;
|
||||
public user: User;
|
||||
|
||||
constructor(
|
||||
public authService: AuthService,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
private location: Location,
|
||||
) {
|
||||
const config = {
|
||||
apiKey: 'AIzaSyALYI-ILmLV8NBNXE3DLF9yf1Z5Pp-Y1Mk',
|
||||
authDomain: 'budget-c7da5.firebaseapp.com',
|
||||
databaseURL: 'https://budget-c7da5.firebaseio.com',
|
||||
projectId: 'budget-c7da5',
|
||||
storageBucket: 'budget-c7da5.appspot.com',
|
||||
messagingSenderId: '527070722499'
|
||||
};
|
||||
firebase.initializeApp(config);
|
||||
}
|
||||
) { }
|
||||
|
||||
getUsername(): String {
|
||||
return firebase.auth().currentUser.email;
|
||||
return this.user.username;
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
|
@ -39,10 +29,10 @@ export class AppComponent {
|
|||
}
|
||||
|
||||
logout(): void {
|
||||
this.authService.logout();
|
||||
this.twigsService.logout();
|
||||
}
|
||||
|
||||
isLoggedIn() {
|
||||
return firebase.auth().currentUser != null;
|
||||
return this.user != null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
import { AppComponent } from './app.component';
|
||||
import { TransactionsComponent } from './transactions/transactions.component';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AccountsComponent } from './accounts/accounts.component';
|
||||
import { BudgetsComponent } from './budgets/budget.component';
|
||||
import { TransactionDetailsComponent } from './transactions/transaction-details/transaction-details.component';
|
||||
import { NewTransactionComponent } from './transactions/new-transaction/new-transaction.component';
|
||||
import { AddEditTransactionComponent } from './transactions/add-edit-transaction/add-edit-transaction.component';
|
||||
|
@ -30,26 +30,21 @@ import { NewCategoryComponent } from './categories/new-category/new-category.com
|
|||
import { CategoryListComponent } from './categories/category-list/category-list.component';
|
||||
import { LoginComponent } from './users/login/login.component';
|
||||
import { RegisterComponent } from './users/register/register.component';
|
||||
import { AddEditAccountComponent } from './accounts/add-edit-account/add-edit-account.component';
|
||||
import { AddEditBudgetComponent } from './budgets/add-edit-budget/add-edit-budget.component';
|
||||
import { EditProfileComponent } from './users/edit-profile/edit-profile.component';
|
||||
import { UserComponent } from './users/user.component';
|
||||
import { NewAccountComponent } from './accounts/new-account/new-account.component';
|
||||
import { AccountDetailsComponent } from './accounts/account-details/account-details.component';
|
||||
import { NewBudgetComponent } from './budgets/new-budget/new-budget.component';
|
||||
import { BudgetDetailsComponent } from './budgets/budget-details/budget-details.component';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { TRANSACTION_SERVICE } from './transactions/transaction.service';
|
||||
import { TransactionServiceFirebaseFirestoreImpl } from './transactions/transaction.service.firestore';
|
||||
import { CATEGORY_SERVICE } from './categories/category.service';
|
||||
import { CategoryServiceFirebaseFirestoreImpl } from './categories/category.service.firestore';
|
||||
import { CurrencyMaskModule } from 'ng2-currency-mask';
|
||||
import { CurrencyMaskConfig, CURRENCY_MASK_CONFIG } from 'ng2-currency-mask/src/currency-mask.config';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { ACCOUNT_SERVICE } from './accounts/account.service';
|
||||
import { FirestoreAccountService } from './accounts/account.service.firestore';
|
||||
import { USER_SERVICE } from './users/user.service';
|
||||
import { FirestoreUserService } from './users/user.service.firestore';
|
||||
import { CategoryBreakdownComponent } from './categories/category-breakdown/category-breakdown.component';
|
||||
import { ChartsModule } from 'ng2-charts';
|
||||
import { TWIGS_SERVICE } from './shared/twigs.service';
|
||||
import { TwigsHttpService } from './shared/twigs.http.service';
|
||||
import { TwigsLocalService } from './shared/twigs.local.service';
|
||||
|
||||
export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
||||
align: 'left',
|
||||
|
@ -75,12 +70,12 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
|||
CategoryListComponent,
|
||||
LoginComponent,
|
||||
RegisterComponent,
|
||||
AddEditAccountComponent,
|
||||
AddEditBudgetComponent,
|
||||
EditProfileComponent,
|
||||
UserComponent,
|
||||
NewAccountComponent,
|
||||
AccountDetailsComponent,
|
||||
AccountsComponent,
|
||||
NewBudgetComponent,
|
||||
BudgetDetailsComponent,
|
||||
BudgetsComponent,
|
||||
CategoryBreakdownComponent,
|
||||
],
|
||||
imports: [
|
||||
|
@ -106,10 +101,8 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
|||
],
|
||||
providers: [
|
||||
{ provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig },
|
||||
{ provide: TRANSACTION_SERVICE, useClass: TransactionServiceFirebaseFirestoreImpl },
|
||||
{ provide: CATEGORY_SERVICE, useClass: CategoryServiceFirebaseFirestoreImpl },
|
||||
{ provide: ACCOUNT_SERVICE, useClass: FirestoreAccountService },
|
||||
{ provide: USER_SERVICE, useClass: FirestoreUserService },
|
||||
{ provide: TWIGS_SERVICE, useClass: TwigsHttpService },
|
||||
// { provide: TWIGS_SERVICE, useClass: TwigsLocalService },
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<div [hidden]="budget">
|
||||
<p>Select a budget from the list to view details about it or edit it.</p>
|
||||
</div>
|
||||
<div [hidden]="!budget" class="form budget-form">
|
||||
<mat-form-field>
|
||||
<input matInput [(ngModel)]="budget.name" placeholder="Name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<textarea matInput [(ngModel)]="budget.description" placeholder="Description"></textarea>
|
||||
</mat-form-field>
|
||||
<button class="button-delete" mat-button color="warn" *ngIf="budget.id" (click)="delete()">Delete</button>
|
||||
</div>
|
|
@ -1,20 +1,20 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AccountDetailsComponent } from './account-details.component';
|
||||
import { AddEditBudgetComponent } from './add-edit-budget.component';
|
||||
|
||||
describe('AccountDetailsComponent', () => {
|
||||
let component: AccountDetailsComponent;
|
||||
let fixture: ComponentFixture<AccountDetailsComponent>;
|
||||
describe('AddEditBudgetComponent', () => {
|
||||
let component: AddEditBudgetComponent;
|
||||
let fixture: ComponentFixture<AddEditBudgetComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AccountDetailsComponent ]
|
||||
declarations: [ AddEditBudgetComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AccountDetailsComponent);
|
||||
fixture = TestBed.createComponent(AddEditBudgetComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
|
@ -1,31 +1,29 @@
|
|||
import { Component, OnInit, Input, Inject, OnDestroy } from '@angular/core';
|
||||
import { Account } from '../account';
|
||||
import { ACCOUNT_SERVICE, AccountService } from '../account.service';
|
||||
import { Budget } from '../budget';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { Actionable } from 'src/app/actionable';
|
||||
import { UserService, USER_SERVICE } from 'src/app/users/user.service';
|
||||
import { User } from 'src/app/users/user';
|
||||
import * as firebase from 'firebase';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-edit-account',
|
||||
templateUrl: './add-edit-account.component.html',
|
||||
styleUrls: ['./add-edit-account.component.css']
|
||||
selector: 'app-add-edit-budget',
|
||||
templateUrl: './add-edit-budget.component.html',
|
||||
styleUrls: ['./add-edit-budget.component.css']
|
||||
})
|
||||
export class AddEditAccountComponent implements OnInit, OnDestroy, Actionable {
|
||||
export class AddEditBudgetComponent implements OnInit, OnDestroy, Actionable {
|
||||
@Input() title: string;
|
||||
@Input() account: Account;
|
||||
public userIds: string[] = [firebase.auth().currentUser.uid];
|
||||
@Input() budget: Budget;
|
||||
public userIds: number[];
|
||||
public searchedUsers: User[] = [];
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
@Inject(ACCOUNT_SERVICE) private accountService: AccountService,
|
||||
@Inject(USER_SERVICE) private userService: UserService,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) {
|
||||
this.app.title = this.title;
|
||||
this.app.backEnabled = true;
|
||||
this.app.actionable = this;
|
||||
this.userIds = [this.app.user.id];
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -37,15 +35,14 @@ export class AddEditAccountComponent implements OnInit, OnDestroy, Actionable {
|
|||
|
||||
doAction(): void {
|
||||
let observable;
|
||||
if (this.account.id) {
|
||||
if (this.budget.id) {
|
||||
// This is an existing transaction, update it
|
||||
observable = this.accountService.updateAccount(this.account.id, this.account);
|
||||
observable = this.twigsService.updateBudget(this.budget.id, this.budget);
|
||||
} else {
|
||||
// This is a new transaction, save it
|
||||
observable = this.accountService.createAccount(
|
||||
this.account.name,
|
||||
this.account.description,
|
||||
this.account.currency,
|
||||
observable = this.twigsService.createBudget(
|
||||
this.budget.name,
|
||||
this.budget.description,
|
||||
this.userIds
|
||||
);
|
||||
}
|
||||
|
@ -60,13 +57,13 @@ export class AddEditAccountComponent implements OnInit, OnDestroy, Actionable {
|
|||
}
|
||||
|
||||
delete(): void {
|
||||
this.accountService.deleteAccount(this.account.id);
|
||||
this.twigsService.deleteBudget(this.budget.id);
|
||||
this.app.goBack();
|
||||
}
|
||||
|
||||
// TODO: Implement a search box with suggestions to add users
|
||||
searchUsers(username: string) {
|
||||
this.userService.getUsersByUsername(username).subscribe(users => {
|
||||
this.twigsService.getUsersByUsername(username).subscribe(users => {
|
||||
this.searchedUsers = users;
|
||||
});
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<div class="dashboard">
|
||||
<div class="dashboard-primary" [hidden]="!account">
|
||||
<div class="dashboard-primary" [hidden]="!budget">
|
||||
<h2 class="balance">
|
||||
Current Balance: <br />
|
||||
<span
|
||||
|
@ -8,38 +8,38 @@
|
|||
<app-category-breakdown [barChartLabels]="barChartLabels" [barChartData]="barChartData">
|
||||
</app-category-breakdown>
|
||||
<div class="transaction-navigation">
|
||||
<a mat-button routerLink="/accounts/{{ account.id }}/transactions" *ngIf="account">View Transactions</a>
|
||||
<a mat-button routerLink="/budgets/{{ budget.id }}/transactions" *ngIf="budget">View Transactions</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-categories" [hidden]="!account">
|
||||
<div class="dashboard-categories" [hidden]="!budget">
|
||||
<h3 class="categories">Income</h3>
|
||||
<a mat-button routerLink="/accounts/{{ account.id }}/categories/new" class="view-all" *ngIf="account">Add Category</a>
|
||||
<a mat-button routerLink="/budgets/{{ budget.id }}/categories/new" class="view-all" *ngIf="budget">Add Category</a>
|
||||
<div class="no-categories" *ngIf="!income || income.length === 0">
|
||||
<a mat-button routerLink="/accounts/{{ account.id }}/categories/new" *ngIf="account">
|
||||
<a mat-button routerLink="/budgets/{{ budget.id }}/categories/new" *ngIf="budget">
|
||||
<mat-icon>add</mat-icon>
|
||||
<p>Add categories to gain more insights into your income.</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="category-info" *ngIf="income && income.length > 0">
|
||||
<app-category-list [accountId]="account.id" [categories]="income" [categoryBalances]="categoryBalances">
|
||||
<app-category-list [budgetId]="budget.id" [categories]="income" [categoryBalances]="categoryBalances">
|
||||
</app-category-list>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-categories" [hidden]="!account">
|
||||
<div class="dashboard-categories" [hidden]="!budget">
|
||||
<h3 class="categories">Expenses</h3>
|
||||
<a mat-button routerLink="/accounts/{{ account.id }}/categories/new" class="view-all" *ngIf="account">Add Category</a>
|
||||
<a mat-button routerLink="/budgets/{{ budget.id }}/categories/new" class="view-all" *ngIf="budget">Add Category</a>
|
||||
<div class="no-categories" *ngIf="!expenses || expenses.length === 0">
|
||||
<a mat-button routerLink="/accounts/{{ account.id }}/categories/new" *ngIf="account">
|
||||
<a mat-button routerLink="/budgets/{{ budget.id }}/categories/new" *ngIf="budget">
|
||||
<mat-icon>add</mat-icon>
|
||||
<p>Add categories to gain more insights into your expenses.</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="category-info" *ngIf="expenses && expenses.length > 0">
|
||||
<app-category-list [accountId]="account.id" [categories]="expenses" [categoryBalances]="categoryBalances">
|
||||
<app-category-list [budgetId]="budget.id" [categories]="expenses" [categoryBalances]="categoryBalances">
|
||||
</app-category-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a mat-fab routerLink="/accounts/{{ account.id }}/transactions/new" *ngIf="account">
|
||||
<a mat-fab routerLink="/budgets/{{ budget.id }}/transactions/new" *ngIf="budget">
|
||||
<mat-icon aria-label="Add">add</mat-icon>
|
||||
</a>
|
|
@ -1,20 +1,20 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddEditAccountComponent } from './add-edit-account.component';
|
||||
import { BudgetDetailsComponent } from './budget-details.component';
|
||||
|
||||
describe('AddEditAccountComponent', () => {
|
||||
let component: AddEditAccountComponent;
|
||||
let fixture: ComponentFixture<AddEditAccountComponent>;
|
||||
describe('BudgetDetailsComponent', () => {
|
||||
let component: BudgetDetailsComponent;
|
||||
let fixture: ComponentFixture<BudgetDetailsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddEditAccountComponent ]
|
||||
declarations: [ BudgetDetailsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddEditAccountComponent);
|
||||
fixture = TestBed.createComponent(BudgetDetailsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
|
@ -1,29 +1,26 @@
|
|||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { Account } from '../account';
|
||||
import { ACCOUNT_SERVICE, AccountService } from '../account.service';
|
||||
import { Budget } from '../budget';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { Transaction } from 'src/app/transactions/transaction';
|
||||
import { Category } from 'src/app/categories/category';
|
||||
import { Observable } from 'rxjs';
|
||||
import { TransactionType } from 'src/app/transactions/transaction.type';
|
||||
import { TRANSACTION_SERVICE, TransactionService } from 'src/app/transactions/transaction.service';
|
||||
import { CATEGORY_SERVICE, CategoryService } from 'src/app/categories/category.service';
|
||||
import { Label } from 'ng2-charts';
|
||||
import { ChartDataSets } from 'chart.js';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account-details',
|
||||
templateUrl: './account-details.component.html',
|
||||
styleUrls: ['./account-details.component.css']
|
||||
selector: 'app-budget-details',
|
||||
templateUrl: './budget-details.component.html',
|
||||
styleUrls: ['./budget-details.component.css']
|
||||
})
|
||||
export class AccountDetailsComponent implements OnInit {
|
||||
export class BudgetDetailsComponent implements OnInit {
|
||||
|
||||
account: Account;
|
||||
budget: Budget;
|
||||
public transactions: Transaction[];
|
||||
public expenses: Category[] = [];
|
||||
public income: Category[] = [];
|
||||
categoryBalances: Map<string, number>;
|
||||
categoryBalances: Map<number, number>;
|
||||
expectedIncome = 0;
|
||||
actualIncome = 0;
|
||||
expectedExpenses = 0;
|
||||
|
@ -37,23 +34,21 @@ export class AccountDetailsComponent implements OnInit {
|
|||
constructor(
|
||||
private app: AppComponent,
|
||||
private route: ActivatedRoute,
|
||||
@Inject(ACCOUNT_SERVICE) private accountService: AccountService,
|
||||
@Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
|
||||
@Inject(CATEGORY_SERVICE) private categoryService: CategoryService,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getAccount();
|
||||
this.getBudget();
|
||||
this.app.backEnabled = false;
|
||||
this.categoryBalances = new Map();
|
||||
}
|
||||
|
||||
getAccount() {
|
||||
const id = this.route.snapshot.paramMap.get('id');
|
||||
this.accountService.getAccount(id)
|
||||
.subscribe(account => {
|
||||
this.app.title = account.name;
|
||||
this.account = account;
|
||||
getBudget() {
|
||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
||||
this.twigsService.getBudget(id)
|
||||
.subscribe(budget => {
|
||||
this.app.title = Budget.name;
|
||||
this.budget = budget;
|
||||
this.getBalance();
|
||||
this.getTransactions();
|
||||
this.getCategories();
|
||||
|
@ -94,16 +89,17 @@ export class AccountDetailsComponent implements OnInit {
|
|||
}
|
||||
|
||||
getTransactions(): void {
|
||||
this.transactionService.getTransactions(this.account.id, null, 5)
|
||||
this.twigsService.getTransactions(this.budget.id, null, 5)
|
||||
.subscribe(transactions => this.transactions = <Transaction[]>transactions);
|
||||
}
|
||||
|
||||
getCategories(): void {
|
||||
this.categoryService.getCategories(this.account.id).subscribe(categories => {
|
||||
const categoryBalances = new Map<string, number>();
|
||||
this.twigsService.getCategories(this.budget.id).subscribe(categories => {
|
||||
const categoryBalances = new Map<number, number>();
|
||||
let categoryBalancesCount = 0;
|
||||
console.log(categories);
|
||||
for (const category of categories) {
|
||||
if (category.isExpense) {
|
||||
if (category.expense) {
|
||||
this.expenses.push(category);
|
||||
this.expectedExpenses += category.amount;
|
||||
} else {
|
||||
|
@ -113,7 +109,7 @@ export class AccountDetailsComponent implements OnInit {
|
|||
this.getCategoryBalance(category.id).subscribe(
|
||||
balance => {
|
||||
console.log(balance);
|
||||
if (category.isExpense) {
|
||||
if (category.expense) {
|
||||
this.actualExpenses += balance * -1;
|
||||
} else {
|
||||
this.actualIncome += balance;
|
||||
|
@ -136,15 +132,15 @@ export class AccountDetailsComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
getCategoryBalance(category: string): Observable<number> {
|
||||
getCategoryBalance(category: number): Observable<number> {
|
||||
return Observable.create(subscriber => {
|
||||
this.transactionService.getTransactions(this.account.id, category).subscribe(transactions => {
|
||||
this.twigsService.getTransactions(this.budget.id, category).subscribe(transactions => {
|
||||
let balance = 0;
|
||||
for (const transaction of transactions) {
|
||||
if (transaction.type === TransactionType.INCOME) {
|
||||
balance += transaction.amount;
|
||||
} else {
|
||||
if (transaction.isExpense) {
|
||||
balance -= transaction.amount;
|
||||
} else {
|
||||
balance += transaction.amount;
|
||||
}
|
||||
}
|
||||
subscriber.next(balance);
|
23
src/app/budgets/budget.component.html
Normal file
23
src/app/budgets/budget.component.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
<div class="dashboard" *ngIf="!isLoggedIn()">
|
||||
<h2 class="log-in">Get started</h2>
|
||||
<p>To begin tracking your finances, <a routerLink="/login">login</a> or <a routerLink="/register">create an account</a>!</p>
|
||||
</div>
|
||||
<mat-nav-list class="budgets" *ngIf="isLoggedIn()">
|
||||
<a mat-list-item *ngFor="let budget of budgets" routerLink="/budgets/{{ budget.id }}">
|
||||
<p matLine class="budget-list-title">
|
||||
{{ budget.name }}
|
||||
</p>
|
||||
<p matLine class="budget-list-description">
|
||||
{{ budget.description }}
|
||||
</p>
|
||||
</a>
|
||||
</mat-nav-list>
|
||||
<div class="no-budgets" *ngIf="!budgets || budgets.length === 0">
|
||||
<a mat-button routerLink="/budgets/new">
|
||||
<mat-icon>add</mat-icon>
|
||||
<p>Add budgets to begin tracking your finances.</p>
|
||||
</a>
|
||||
</div>
|
||||
<a mat-fab routerLink="/budgets/new">
|
||||
<mat-icon aria-label="Add">add</mat-icon>
|
||||
</a>
|
|
@ -1,20 +1,20 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AccountsComponent } from './accounts.component';
|
||||
import { BudgetsComponent } from './budget.component';
|
||||
|
||||
describe('AccountsComponent', () => {
|
||||
let component: AccountsComponent;
|
||||
let fixture: ComponentFixture<AccountsComponent>;
|
||||
describe('BudgetsComponent', () => {
|
||||
let component: BudgetsComponent;
|
||||
let fixture: ComponentFixture<BudgetsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AccountsComponent ]
|
||||
declarations: [ BudgetsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AccountsComponent);
|
||||
fixture = TestBed.createComponent(BudgetsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
32
src/app/budgets/budget.component.ts
Normal file
32
src/app/budgets/budget.component.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { Component, OnInit, Input, Inject, ChangeDetectorRef } from '@angular/core';
|
||||
import { AppComponent } from '../app.component';
|
||||
import { Budget } from './budget';
|
||||
import { TWIGS_SERVICE, TwigsService } from '../shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-budgets',
|
||||
templateUrl: './budget.component.html',
|
||||
styleUrls: ['./budget.component.css']
|
||||
})
|
||||
export class BudgetsComponent implements OnInit {
|
||||
|
||||
@Input() budgetId: string;
|
||||
public budgets: Budget[];
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.backEnabled = this.isLoggedIn();
|
||||
this.app.title = 'Budgets';
|
||||
this.twigsService.getBudgets().subscribe(budgets => {
|
||||
this.budgets = budgets;
|
||||
});
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return this.app.isLoggedIn();
|
||||
}
|
||||
}
|
8
src/app/budgets/budget.ts
Normal file
8
src/app/budgets/budget.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { User } from '../users/user';
|
||||
|
||||
export class Budget {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
users: User[];
|
||||
}
|
1
src/app/budgets/new-budget/new-budget.component.html
Normal file
1
src/app/budgets/new-budget/new-budget.component.html
Normal file
|
@ -0,0 +1 @@
|
|||
<app-add-edit-budget [title]="'Add Budget'" [budget]="budget"></app-add-edit-budget>
|
|
@ -1,20 +1,20 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NewAccountComponent } from './new-account.component';
|
||||
import { NewBudgetComponent } from './new-budget.component';
|
||||
|
||||
describe('NewAccountComponent', () => {
|
||||
let component: NewAccountComponent;
|
||||
let fixture: ComponentFixture<NewAccountComponent>;
|
||||
describe('NewBudgetComponent', () => {
|
||||
let component: NewBudgetComponent;
|
||||
let fixture: ComponentFixture<NewBudgetComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ NewAccountComponent ]
|
||||
declarations: [ NewBudgetComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(NewAccountComponent);
|
||||
fixture = TestBed.createComponent(NewBudgetComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
20
src/app/budgets/new-budget/new-budget.component.ts
Normal file
20
src/app/budgets/new-budget/new-budget.component.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Budget } from '../budget';
|
||||
|
||||
@Component({
|
||||
selector: 'app-new-budget',
|
||||
templateUrl: './new-budget.component.html',
|
||||
styleUrls: ['./new-budget.component.css']
|
||||
})
|
||||
export class NewBudgetComponent implements OnInit {
|
||||
|
||||
public budget: Budget;
|
||||
|
||||
constructor() {
|
||||
this.budget = new Budget();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -3,12 +3,12 @@
|
|||
</div>
|
||||
<div *ngIf="currentCategory" class="form category-form">
|
||||
<mat-form-field (keyup.enter)="doAction()">
|
||||
<input matInput [(ngModel)]="currentCategory.name" placeholder="Name" required>
|
||||
<input matInput [(ngModel)]="currentCategory.title" placeholder="Name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field (keyup.enter)="doAction()">
|
||||
<input matInput type="text" [(ngModel)]="currentCategory.amount" placeholder="Amount" required currencyMask>
|
||||
</mat-form-field>
|
||||
<mat-radio-group [(ngModel)]="currentCategory.isExpense">
|
||||
<mat-radio-group [(ngModel)]="currentCategory.expense">
|
||||
<mat-radio-button [value]="true">Expense</mat-radio-button>
|
||||
<mat-radio-button [value]="false">Income</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { Component, OnInit, Input, OnDestroy, Inject } from '@angular/core';
|
||||
import { Category } from '../category';
|
||||
import { CATEGORY_SERVICE, CategoryService } from '../category.service';
|
||||
import { Actionable } from 'src/app/actionable';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { Account } from 'src/app/accounts/account';
|
||||
import { ACCOUNT_SERVICE, AccountService } from 'src/app/accounts/account.service';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-edit-category',
|
||||
|
@ -13,14 +11,13 @@ import { ACCOUNT_SERVICE, AccountService } from 'src/app/accounts/account.servic
|
|||
})
|
||||
export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
|
||||
|
||||
@Input() accountId: string;
|
||||
@Input() budgetId: number;
|
||||
@Input() title: string;
|
||||
@Input() currentCategory: Category;
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
@Inject(ACCOUNT_SERVICE) private accountService: AccountService,
|
||||
@Inject(CATEGORY_SERVICE) private categoryService: CategoryService,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -37,22 +34,22 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
|
|||
let observable;
|
||||
if (this.currentCategory.id) {
|
||||
// This is an existing category, update it
|
||||
observable = this.categoryService.updateCategory(
|
||||
this.accountId,
|
||||
observable = this.twigsService.updateCategory(
|
||||
this.budgetId,
|
||||
this.currentCategory.id,
|
||||
{
|
||||
name: this.currentCategory.name,
|
||||
name: this.currentCategory.title,
|
||||
amount: this.currentCategory.amount * 100,
|
||||
isExpense: this.currentCategory.isExpense
|
||||
expense: this.currentCategory.expense
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// This is a new category, save it
|
||||
observable = this.categoryService.createCategory(
|
||||
this.accountId,
|
||||
this.currentCategory.name,
|
||||
observable = this.twigsService.createCategory(
|
||||
this.budgetId,
|
||||
this.currentCategory.title,
|
||||
this.currentCategory.amount * 100,
|
||||
this.currentCategory.isExpense
|
||||
this.currentCategory.expense
|
||||
);
|
||||
}
|
||||
observable.subscribe(val => {
|
||||
|
@ -65,7 +62,7 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
|
|||
}
|
||||
|
||||
delete(): void {
|
||||
this.categoryService.deleteCategory(this.accountId, this.currentCategory.id).subscribe(() => {
|
||||
this.twigsService.deleteCategory(this.budgetId, this.currentCategory.id).subscribe(() => {
|
||||
this.app.goBack();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<app-category-list [accountId]="accountId" [categories]="categories" [categoryBalances]="categoryBalances"></app-category-list>
|
||||
<a mat-fab routerLink="/accounts/{{ accountId }}/categories/new">
|
||||
<app-category-list [budgetId]="budgetId" [categories]="categories" [categoryBalances]="categoryBalances"></app-category-list>
|
||||
<a mat-fab routerLink="/budgets/{{ budgetId }}/categories/new">
|
||||
<mat-icon aria-label="Add">add</mat-icon>
|
||||
</a>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
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 '../transactions/transaction.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { TransactionType } from '../transactions/transaction.type';
|
||||
import { Account } from '../accounts/account';
|
||||
import { Budget } from '../budgets/budget';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TWIGS_SERVICE, TwigsService } from '../shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-categories',
|
||||
|
@ -15,19 +14,18 @@ import { ActivatedRoute } from '@angular/router';
|
|||
})
|
||||
export class CategoriesComponent implements OnInit {
|
||||
|
||||
accountId: string;
|
||||
budgetId: number;
|
||||
public categories: Category[];
|
||||
public categoryBalances: Map<string, number>;
|
||||
public categoryBalances: Map<number, number>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private app: AppComponent,
|
||||
@Inject(CATEGORY_SERVICE) private categoryService: CategoryService,
|
||||
@Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.accountId = this.route.snapshot.paramMap.get('accountId');
|
||||
this.budgetId = Number.parseInt(this.route.snapshot.paramMap.get('budgetId'));
|
||||
this.app.title = 'Categories';
|
||||
this.app.backEnabled = true;
|
||||
this.getCategories();
|
||||
|
@ -35,7 +33,7 @@ export class CategoriesComponent implements OnInit {
|
|||
}
|
||||
|
||||
getCategories(): void {
|
||||
this.categoryService.getCategories(this.accountId).subscribe(categories => {
|
||||
this.twigsService.getCategories(this.budgetId).subscribe(categories => {
|
||||
this.categories = categories;
|
||||
for (const category of this.categories) {
|
||||
this.getCategoryBalance(category).subscribe(balance => this.categoryBalances.set(category.id, balance));
|
||||
|
@ -45,13 +43,13 @@ export class CategoriesComponent implements OnInit {
|
|||
|
||||
getCategoryBalance(category: Category): Observable<number> {
|
||||
return Observable.create(subscriber => {
|
||||
this.transactionService.getTransactions(this.accountId, category.id).subscribe(transactions => {
|
||||
this.twigsService.getTransactions(this.budgetId, category.id).subscribe(transactions => {
|
||||
let balance = 0;
|
||||
for (const transaction of transactions) {
|
||||
if (transaction.type === TransactionType.INCOME) {
|
||||
balance += transaction.amount;
|
||||
} else {
|
||||
if (transaction.isExpense) {
|
||||
balance -= transaction.amount;
|
||||
} else {
|
||||
balance += transaction.amount;
|
||||
}
|
||||
}
|
||||
subscriber.next(balance);
|
||||
|
|
|
@ -1 +1 @@
|
|||
<app-add-edit-category [title]="'Edit Category'" [accountId]="accountId" [currentCategory]="category"></app-add-edit-category>
|
||||
<app-add-edit-category [title]="'Edit Category'" [budgetId]="budgetId" [currentCategory]="category"></app-add-edit-category>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { CategoryServiceFirebaseFirestoreImpl } from '../category.service.firestore';
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { Category } from '../category';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Account } from 'src/app/accounts/account';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-category-details',
|
||||
|
@ -11,12 +10,12 @@ import { Account } from 'src/app/accounts/account';
|
|||
})
|
||||
export class CategoryDetailsComponent implements OnInit {
|
||||
|
||||
accountId: string;
|
||||
budgetId: number;
|
||||
category: Category;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private categoryService: CategoryServiceFirebaseFirestoreImpl
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -24,9 +23,8 @@ export class CategoryDetailsComponent implements OnInit {
|
|||
}
|
||||
|
||||
getCategory(): void {
|
||||
this.accountId = this.route.snapshot.paramMap.get('accountId');
|
||||
const id = this.route.snapshot.paramMap.get('id');
|
||||
this.categoryService.getCategory(this.accountId, id)
|
||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
||||
this.twigsService.getCategory(id)
|
||||
.subscribe(category => {
|
||||
category.amount /= 100;
|
||||
this.category = category;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<mat-nav-list class="categories">
|
||||
<a mat-list-item *ngFor="let category of categories" routerLink="/accounts/{{ accountId }}/categories/{{ category.id }}">
|
||||
<a mat-list-item *ngFor="let category of categories" routerLink="/budgets/{{ budgetId }}/categories/{{ category.id }}">
|
||||
<p matLine class="category-list-title">
|
||||
<span>
|
||||
{{ category.name }}
|
||||
{{ category.title }}
|
||||
</span>
|
||||
<span class="remaining">
|
||||
{{ getCategoryRemainingBalance(category) | currency }} remaining of {{ category.amount / 100 | currency }}
|
||||
</span>
|
||||
</p>
|
||||
<mat-progress-bar matLine color="accent" [ngClass]="{'income': !category.isExpense, 'expense': category.isExpense}" mode="determinate" #categoryProgress [attr.id]="'cat-' + category.id" value="{{ getCategoryCompletion(category) }}"></mat-progress-bar>
|
||||
<mat-progress-bar matLine color="accent" [ngClass]="{'income': !category.expense, 'expense': category.expense}" mode="determinate" #categoryProgress [attr.id]="'cat-' + category.id" value="{{ getCategoryCompletion(category) }}"></mat-progress-bar>
|
||||
</a>
|
||||
</mat-nav-list>
|
||||
|
|
|
@ -8,9 +8,9 @@ import { Category } from '../category';
|
|||
})
|
||||
export class CategoryListComponent implements OnInit {
|
||||
|
||||
@Input() accountId: string;
|
||||
@Input() budgetId: string;
|
||||
@Input() categories: Category[];
|
||||
@Input() categoryBalances: Map<string, number>;
|
||||
@Input() categoryBalances: Map<number, number>;
|
||||
|
||||
constructor() { }
|
||||
|
||||
|
@ -23,7 +23,7 @@ export class CategoryListComponent implements OnInit {
|
|||
categoryBalance = 0;
|
||||
}
|
||||
|
||||
if (category.isExpense) {
|
||||
if (category.expense) {
|
||||
return (category.amount / 100) + (categoryBalance / 100);
|
||||
} else {
|
||||
return (category.amount / 100) - (categoryBalance / 100);
|
||||
|
@ -41,7 +41,7 @@ export class CategoryListComponent implements OnInit {
|
|||
// Invert the negative/positive values for calculating progress
|
||||
// since the limit for a category is saved as a positive but the
|
||||
// balance is used in the calculation.
|
||||
if (category.isExpense) {
|
||||
if (category.expense) {
|
||||
if (categoryBalance < 0) {
|
||||
categoryBalance = Math.abs(categoryBalance);
|
||||
} else {
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
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();
|
||||
}));
|
||||
});
|
|
@ -1,103 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { of, Observable, from } from 'rxjs';
|
||||
import { Category } from './category';
|
||||
import * as firebase from 'firebase/app';
|
||||
import 'firebase/firestore';
|
||||
import { Account } from '../accounts/account';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CategoryServiceFirebaseFirestoreImpl {
|
||||
|
||||
constructor() { }
|
||||
|
||||
getCategories(accountId: string, count?: number): Observable<Category[]> {
|
||||
return Observable.create(subscriber => {
|
||||
let query: any = firebase.firestore().collection('accounts').doc(accountId).collection('categories').orderBy('name');
|
||||
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(accountId, categoryDoc));
|
||||
}
|
||||
subscriber.next(categories);
|
||||
}, error => {
|
||||
console.error('Got an error while getting categories');
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getCategory(accountId: string, id: string): Observable<Category> {
|
||||
return Observable.create(subscriber => {
|
||||
firebase.firestore().collection('accounts').doc(accountId).collection('categories').doc(id).onSnapshot(snapshot => {
|
||||
if (!snapshot.exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
subscriber.next(Category.fromSnapshotRef(accountId, snapshot));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createCategory(accountId: string, name: string, amount: number, isExpense: boolean): Observable<Category> {
|
||||
return Observable.create(subscriber => {
|
||||
firebase.firestore().collection('accounts').doc(accountId).collection('categories').add({
|
||||
name: name,
|
||||
amount: amount,
|
||||
isExpense: isExpense
|
||||
}).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(accountId, snapshot));
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('Failed to create new category: ');
|
||||
console.error(err);
|
||||
subscriber.error(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateCategory(accountId: string, id: string, changes: object): Observable<boolean> {
|
||||
return Observable.create(subscriber => {
|
||||
firebase.firestore().collection('accounts').doc(accountId).collection('categories').doc(id)
|
||||
.update(changes)
|
||||
.then(function () {
|
||||
subscriber.next(true);
|
||||
})
|
||||
.catch(function () {
|
||||
subscriber.next(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteCategory(accountId: string, id: string): Observable<boolean> {
|
||||
return Observable.create(subscriber => {
|
||||
firebase.firestore().collection('accounts').doc(accountId).collection('categories').doc(id).delete().then(result => {
|
||||
subscriber.next(true);
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
subscriber.next(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import { Observable } from 'rxjs';
|
||||
import { Category } from './category';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { Account } from '../accounts/account';
|
||||
|
||||
export interface CategoryService {
|
||||
|
||||
getCategories(accountId: string, count?: number): Observable<Category[]>;
|
||||
|
||||
getCategory(accountId: string, id: string): Observable<Category>;
|
||||
|
||||
createCategory(accountId: string, name: string, amount: number, isExpense: boolean): Observable<Category>;
|
||||
|
||||
updateCategory(accountId: string, id: string, changes: object): Observable<boolean>;
|
||||
|
||||
deleteCategory(accountId: string, id: string): Observable<boolean>;
|
||||
}
|
||||
|
||||
export let CATEGORY_SERVICE = new InjectionToken<CategoryService>('category.service');
|
|
@ -1,21 +1,7 @@
|
|||
export class Category {
|
||||
id: string;
|
||||
name: string;
|
||||
id: number;
|
||||
title: string;
|
||||
amount: number;
|
||||
isExpense: boolean;
|
||||
accountId: string;
|
||||
|
||||
static fromSnapshotRef(accountId: string, snapshot: firebase.firestore.DocumentSnapshot): Category {
|
||||
const category = new Category();
|
||||
category.id = snapshot.id;
|
||||
category.name = snapshot.get('name');
|
||||
category.amount = snapshot.get('amount');
|
||||
let isExpense = snapshot.get('isExpense');
|
||||
if (isExpense === undefined) {
|
||||
isExpense = true;
|
||||
}
|
||||
category.isExpense = isExpense;
|
||||
category.accountId = accountId;
|
||||
return category;
|
||||
}
|
||||
expense: boolean;
|
||||
budgetId: number;
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
<app-add-edit-category [title]="'Add Category'" [accountId]="accountId" [currentCategory]="category"></app-add-edit-category>
|
||||
<app-add-edit-category [title]="'Add Category'" [budgetId]="budgetId" [currentCategory]="category"></app-add-edit-category>
|
||||
|
|
|
@ -9,7 +9,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||
})
|
||||
export class NewCategoryComponent implements OnInit {
|
||||
|
||||
accountId: string;
|
||||
budgetId: string;
|
||||
category: Category;
|
||||
|
||||
constructor(
|
||||
|
@ -17,7 +17,7 @@ export class NewCategoryComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.accountId = this.route.snapshot.paramMap.get('accountId');
|
||||
this.budgetId = this.route.snapshot.paramMap.get('budgetId');
|
||||
this.category = new Category();
|
||||
// TODO: Set random color for category, improve color picker
|
||||
// this.category.color =
|
||||
|
|
164
src/app/shared/twigs.http.service.ts
Normal file
164
src/app/shared/twigs.http.service.ts
Normal file
|
@ -0,0 +1,164 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, Subscriber } from 'rxjs';
|
||||
import { User } from '../users/user';
|
||||
import { TwigsService } from './twigs.service';
|
||||
import { Budget } from '../budgets/budget';
|
||||
import { Category } from '../categories/category';
|
||||
import { Transaction } from '../transactions/transaction';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TwigsHttpService implements TwigsService {
|
||||
|
||||
constructor(
|
||||
private http: HttpClient
|
||||
) { }
|
||||
|
||||
private options = {
|
||||
headers: new HttpHeaders({
|
||||
'Origin': window.location.href.split('/')[2]
|
||||
}),
|
||||
withCredentials: true
|
||||
};
|
||||
|
||||
// TODO: Set this up in environment variables
|
||||
// private apiUrl = 'https://budget-api.intra.wbrawner.com';
|
||||
// private apiUrl = 'https://code.brawner.home/spring';
|
||||
private apiUrl = 'http://localhost:8080';
|
||||
|
||||
// Auth
|
||||
login(email: string, password: string): Observable<User> {
|
||||
const params = {
|
||||
'username': email,
|
||||
'password': password
|
||||
};
|
||||
return this.http.post<User>(this.apiUrl + '/users/login', params, this.options);
|
||||
}
|
||||
|
||||
register(username: string, email: string, password: string): Observable<User> {
|
||||
const params = {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': password
|
||||
};
|
||||
return this.http.post<User>(this.apiUrl + '/users/new', params, this.options);
|
||||
}
|
||||
|
||||
logout(): Observable<void> {
|
||||
return Observable.throw('Not Implemented');
|
||||
}
|
||||
|
||||
// Budgets
|
||||
getBudgets(): Observable<Budget[]> {
|
||||
return this.http.get<Budget[]>(this.apiUrl + '/budgets', this.options);
|
||||
}
|
||||
|
||||
getBudget(id: number): Observable<Budget> {
|
||||
return this.http.get<Budget>(`${this.apiUrl}/budgets/${id}`, this.options);
|
||||
}
|
||||
|
||||
createBudget(
|
||||
name: string,
|
||||
description: string,
|
||||
userIds: number[],
|
||||
): Observable<Budget> {
|
||||
const params = {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'userIds': userIds
|
||||
};
|
||||
return this.http.post<Budget>(this.apiUrl + '/budgets/new', params, this.options);
|
||||
}
|
||||
|
||||
updateBudget(id: number, changes: object): Observable<Budget> {
|
||||
return this.http.put<Budget>(`${this.apiUrl}/budgets/${id}`, changes, this.options);
|
||||
}
|
||||
|
||||
deleteBudget(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/budgets/${id}`, this.options);
|
||||
}
|
||||
|
||||
// Categories
|
||||
getCategories(budgetId: number, count?: number): Observable<Category[]> {
|
||||
const params = {
|
||||
'budgetIds': [budgetId]
|
||||
};
|
||||
return this.http.get<Category[]>(`${this.apiUrl}/categories`, Object.assign(params, this.options));
|
||||
}
|
||||
|
||||
getCategory(id: number): Observable<Category> {
|
||||
return this.http.get<Category>(`${this.apiUrl}/categories/${id}`, this.options);
|
||||
}
|
||||
|
||||
createCategory(budgetId: number, name: string, amount: number, isExpense: boolean): Observable<Category> {
|
||||
const params = {
|
||||
'title': name,
|
||||
'amount': amount,
|
||||
'expense': isExpense,
|
||||
'budgetId': budgetId
|
||||
};
|
||||
return this.http.post<Category>(this.apiUrl + '/categories/new', params, this.options);
|
||||
}
|
||||
|
||||
updateCategory(budgetId: number, id: number, changes: object): Observable<Category> {
|
||||
return this.http.put<Category>(`${this.apiUrl}/categories/${id}`, changes, this.options);
|
||||
}
|
||||
|
||||
deleteCategory(budgetId: number, id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/categories/${id}`, this.options);
|
||||
}
|
||||
|
||||
// Transactions
|
||||
getTransactions(budgetId?: number, categoryId?: number, count?: number): Observable<Transaction[]> {
|
||||
const params = {};
|
||||
if (budgetId) {
|
||||
params['budgetId'] = budgetId;
|
||||
}
|
||||
if (categoryId) {
|
||||
params['categoryId'] = categoryId;
|
||||
}
|
||||
return this.http.get<Transaction[]>(`${this.apiUrl}/transactions`, Object.assign(params, this.options));
|
||||
}
|
||||
|
||||
getTransaction(id: number): Observable<Transaction> {
|
||||
return this.http.get<Transaction>(`${this.apiUrl}/transactions/${id}`, this.options);
|
||||
}
|
||||
|
||||
createTransaction(
|
||||
budgetId: number,
|
||||
name: string,
|
||||
description: string,
|
||||
amount: number,
|
||||
date: Date,
|
||||
isExpense: boolean,
|
||||
category: number
|
||||
): Observable<Transaction> {
|
||||
const params = {
|
||||
'title': name,
|
||||
'description': description,
|
||||
'date': date,
|
||||
'amount': amount,
|
||||
'expense': isExpense,
|
||||
'categoryId': category,
|
||||
'budgetId': budgetId
|
||||
};
|
||||
return this.http.post<Transaction>(this.apiUrl + '/transactions/new', params, this.options);
|
||||
}
|
||||
|
||||
updateTransaction(budgetId: number, id: number, changes: object): Observable<Transaction> {
|
||||
return this.http.put<Transaction>(`${this.apiUrl}/transactions/${id}`, changes, this.options);
|
||||
}
|
||||
|
||||
deleteTransaction(budgetId: number, id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/transactions/${id}`, this.options);
|
||||
}
|
||||
|
||||
// Users
|
||||
getUsersByUsername(username: string): Observable<User[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.error("Not yet implemented")
|
||||
});
|
||||
}
|
||||
}
|
323
src/app/shared/twigs.local.service.ts
Normal file
323
src/app/shared/twigs.local.service.ts
Normal file
|
@ -0,0 +1,323 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, Subscriber } from 'rxjs';
|
||||
import { User } from '../users/user';
|
||||
import { TwigsService } from './twigs.service';
|
||||
import { Budget } from '../budgets/budget';
|
||||
import { Category } from '../categories/category';
|
||||
import { Transaction } from '../transactions/transaction';
|
||||
|
||||
/**
|
||||
* This is intended to be a very simple implementation of the TwigsService used for testing out the UI and quickly iterating on it.
|
||||
* It may also prove useful for automated testing.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TwigsLocalService implements TwigsService {
|
||||
|
||||
constructor(
|
||||
private http: HttpClient
|
||||
) { }
|
||||
|
||||
private users: User[] = [new User(1, 'test', 'test@example.com')];
|
||||
private budgets: Budget[] = [];
|
||||
private transactions: Transaction[] = [];
|
||||
private categories: Category[] = [];
|
||||
|
||||
// Auth
|
||||
login(email: string, password: string): Observable<User> {
|
||||
return Observable.create(subscriber => {
|
||||
const filteredUsers = this.users.filter(user => {
|
||||
return (user.email === email || user.username === email);
|
||||
});
|
||||
if (filteredUsers.length !== 0) {
|
||||
subscriber.next(filteredUsers[0]);
|
||||
} else {
|
||||
subscriber.error('No users found');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
register(username: string, email: string, password: string): Observable<User> {
|
||||
return Observable.create(subscriber => {
|
||||
const user = new User();
|
||||
user.username = username;
|
||||
user.email = email;
|
||||
user.id = this.users.length + 1;
|
||||
this.users.push(user);
|
||||
subscriber.next(user);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
logout(): Observable<void> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
// Budgets
|
||||
getBudgets(): Observable<Budget[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.budgets);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
getBudget(id: number): Observable<Budget> {
|
||||
return Observable.create(subscriber => {
|
||||
const budget = this.budgets.filter(it => {
|
||||
return it.id === id;
|
||||
})[0];
|
||||
if (budget) {
|
||||
subscriber.next(budget);
|
||||
} else {
|
||||
subscriber.error('No budget found for given id');
|
||||
}
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
createBudget(
|
||||
name: string,
|
||||
description: string,
|
||||
userIds: number[],
|
||||
): Observable<Budget> {
|
||||
return Observable.create(subscriber => {
|
||||
const budget = new Budget();
|
||||
budget.name = name;
|
||||
budget.description = description;
|
||||
budget.users = this.users.filter(user => {
|
||||
return userIds.indexOf(user.id) > -1;
|
||||
});
|
||||
budget.id = this.budgets.length + 1;
|
||||
this.budgets.push(budget);
|
||||
subscriber.next(budget);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
updateBudget(id: number, changes: object): Observable<Budget> {
|
||||
return Observable.create(subscriber => {
|
||||
const budget = this.budgets.filter(it => {
|
||||
return it.id === id;
|
||||
})[0];
|
||||
if (budget) {
|
||||
const index = this.budgets.indexOf(budget);
|
||||
this.updateValues(
|
||||
budget,
|
||||
changes,
|
||||
[
|
||||
'name',
|
||||
'description',
|
||||
]
|
||||
);
|
||||
if (changes['userIds']) {
|
||||
budget.users = this.users.filter(user => {
|
||||
return changes['userIds'].indexOf(user.id) > -1;
|
||||
});
|
||||
}
|
||||
this.budgets[index] = budget;
|
||||
subscriber.next(budget);
|
||||
} else {
|
||||
subscriber.error('No budget found for given id');
|
||||
}
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
deleteBudget(id: number): Observable<void> {
|
||||
return Observable.create(subscriber => {
|
||||
const budget = this.budgets.filter(it => {
|
||||
return budget.id === id;
|
||||
})[0];
|
||||
if (budget) {
|
||||
const index = this.budgets.indexOf(budget);
|
||||
delete this.budgets[index];
|
||||
subscriber.complete();
|
||||
} else {
|
||||
subscriber.error('No budget found for given id');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Categories
|
||||
getCategories(budgetId: number, count?: number): Observable<Category[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.categories.filter(category => {
|
||||
return category.budgetId === budgetId;
|
||||
}));
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
getCategory(id: number): Observable<Category> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.findById(this.categories, id));
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
createCategory(budgetId: number, name: string, amount: number, isExpense: boolean): Observable<Category> {
|
||||
return Observable.create(subscriber => {
|
||||
const category = new Category();
|
||||
category.title = name;
|
||||
category.amount = amount;
|
||||
category.expense = isExpense;
|
||||
category.budgetId = budgetId;
|
||||
category.id = this.categories.length + 1;
|
||||
this.categories.push(category);
|
||||
subscriber.next(category);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
updateCategory(budgetId: number, id: number, changes: object): Observable<Category> {
|
||||
return Observable.create(subscriber => {
|
||||
const category = this.findById(this.categories, id);
|
||||
if (category) {
|
||||
const index = this.categories.indexOf(category);
|
||||
this.updateValues(
|
||||
category,
|
||||
changes,
|
||||
[
|
||||
'name',
|
||||
'amount',
|
||||
'isExpense',
|
||||
'budgetId',
|
||||
]
|
||||
);
|
||||
this.categories[index] = category;
|
||||
subscriber.next(category);
|
||||
} else {
|
||||
subscriber.error('No category found for given id');
|
||||
}
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
deleteCategory(budgetId: number, id: number): Observable<void> {
|
||||
return Observable.create(subscriber => {
|
||||
const category = this.findById(this.categories, id);
|
||||
if (category) {
|
||||
const index = this.categories.indexOf(category);
|
||||
delete this.transactions[index];
|
||||
subscriber.complete();
|
||||
} else {
|
||||
subscriber.error('No category found for given id');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Transactions
|
||||
getTransactions(budgetId?: number, categoryId?: number, count?: number): Observable<Transaction[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.transactions.filter(transaction => {
|
||||
let include = true;
|
||||
if (budgetId) {
|
||||
include = transaction.budgetId === budgetId;
|
||||
}
|
||||
if (include && categoryId) {
|
||||
include = transaction.categoryId === categoryId;
|
||||
}
|
||||
return include;
|
||||
}));
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
getTransaction(id: number): Observable<Transaction> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.findById(this.transactions, id));
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
createTransaction(
|
||||
budgetId: number,
|
||||
name: string,
|
||||
description: string,
|
||||
amount: number,
|
||||
date: Date,
|
||||
isExpense: boolean,
|
||||
category: number
|
||||
): Observable<Transaction> {
|
||||
return Observable.create(subscriber => {
|
||||
const transaction = new Transaction();
|
||||
transaction.title = name;
|
||||
transaction.description = description;
|
||||
transaction.amount = amount;
|
||||
transaction.date = date;
|
||||
transaction.isExpense = isExpense;
|
||||
transaction.categoryId = category;
|
||||
transaction.budgetId = budgetId;
|
||||
transaction.id = this.transactions.length + 1;
|
||||
this.transactions.push(transaction);
|
||||
subscriber.next(transaction);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
updateTransaction(budgetId: number, id: number, changes: object): Observable<Transaction> {
|
||||
return Observable.create(subscriber => {
|
||||
const transaction = this.findById(this.transactions, id);
|
||||
if (transaction) {
|
||||
const index = this.transactions.indexOf(transaction);
|
||||
this.updateValues(
|
||||
transaction,
|
||||
changes,
|
||||
[
|
||||
'title',
|
||||
'description',
|
||||
'date',
|
||||
'amount',
|
||||
'isExpense',
|
||||
'categoryId',
|
||||
'budgetId',
|
||||
'createdBy'
|
||||
]
|
||||
);
|
||||
this.transactions[index] = transaction;
|
||||
subscriber.next(transaction);
|
||||
} else {
|
||||
subscriber.error('No transaction found for given id');
|
||||
}
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
deleteTransaction(budgetId: number, id: number): Observable<void> {
|
||||
return Observable.create(subscriber => {
|
||||
const transaction = this.findById(this.transactions, id);
|
||||
if (transaction) {
|
||||
const index = this.transactions.indexOf(transaction);
|
||||
delete this.transactions[index];
|
||||
subscriber.complete();
|
||||
} else {
|
||||
subscriber.error('No transaction found for given id');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Users
|
||||
getUsersByUsername(username: string): Observable<User[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.users.filter(user => user.username.indexOf(username) > -1 ));
|
||||
});
|
||||
}
|
||||
|
||||
private updateValues(old: object, changes: object, keys: string[]) {
|
||||
keys.forEach(key => {
|
||||
if (changes[key]) {
|
||||
old[key] = changes[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private findById<T>(items: T[], id: number): T {
|
||||
return items.filter(item => {
|
||||
return item['id'] === id;
|
||||
})[0];
|
||||
}
|
||||
}
|
50
src/app/shared/twigs.service.ts
Normal file
50
src/app/shared/twigs.service.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { InjectionToken } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { User } from '../users/user';
|
||||
import { Budget } from '../budgets/budget';
|
||||
import { Category } from '../categories/category';
|
||||
import { Transaction } from '../transactions/transaction';
|
||||
|
||||
export interface TwigsService {
|
||||
// Auth
|
||||
login(email: string, password: string): Observable<User>;
|
||||
register(username: string, email: string, password: string): Observable<User>;
|
||||
logout(): Observable<void>;
|
||||
|
||||
// Budgets
|
||||
getBudgets(): Observable<Budget[]>;
|
||||
getBudget(id: number): Observable<Budget>;
|
||||
createBudget(
|
||||
name: string,
|
||||
description: string,
|
||||
userIds: number[],
|
||||
): Observable<Budget>;
|
||||
updateBudget(id: number, changes: object): Observable<Budget>;
|
||||
deleteBudget(id: number): Observable<void>;
|
||||
|
||||
// Categories
|
||||
getCategories(budgetId?: number, count?: number): Observable<Category[]>;
|
||||
getCategory(id: number): Observable<Category>;
|
||||
createCategory(budgetId: number, name: string, amount: number, isExpense: boolean): Observable<Category>;
|
||||
updateCategory(budgetId: number, id: number, changes: object): Observable<Category>;
|
||||
deleteCategory(budgetId: number, id: number): Observable<void>;
|
||||
|
||||
// Transactions
|
||||
getTransactions(budgetId?: number, categoryId?: number, count?: number): Observable<Transaction[]>;
|
||||
getTransaction(id: number): Observable<Transaction>;
|
||||
createTransaction(
|
||||
budgetId: number,
|
||||
name: string,
|
||||
description: string,
|
||||
amount: number,
|
||||
date: Date,
|
||||
isExpense: boolean,
|
||||
category: number
|
||||
): Observable<Transaction>;
|
||||
updateTransaction(budgetId: number, id: number, changes: object): Observable<Transaction>;
|
||||
deleteTransaction(budgetId: number, id: number): Observable<void>;
|
||||
|
||||
getUsersByUsername(username: string): Observable<User[]>;
|
||||
}
|
||||
|
||||
export let TWIGS_SERVICE = new InjectionToken<TwigsService>('twigs.service');
|
|
@ -20,7 +20,7 @@
|
|||
<mat-form-field>
|
||||
<mat-select placeholder="Category" [(ngModel)]="currentTransaction.categoryId">
|
||||
<mat-option *ngFor="let category of categories" [value]="category.id">
|
||||
{{ category.name }}
|
||||
{{ category.title }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import { Component, OnInit, Input, OnChanges, OnDestroy, Inject, SimpleChanges } from '@angular/core';
|
||||
import { Transaction } from '../transaction';
|
||||
import { TransactionType } from '../transaction.type';
|
||||
import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service';
|
||||
import { Category } from 'src/app/categories/category';
|
||||
import { Actionable } from 'src/app/actionable';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { CATEGORY_SERVICE, CategoryService } from 'src/app/categories/category.service';
|
||||
import { Account } from 'src/app/accounts/account';
|
||||
import { Time } from '@angular/common';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-edit-transaction',
|
||||
|
@ -17,7 +14,7 @@ import { Time } from '@angular/common';
|
|||
export class AddEditTransactionComponent implements OnInit, OnChanges, OnDestroy, Actionable {
|
||||
@Input() title: string;
|
||||
@Input() currentTransaction: Transaction;
|
||||
@Input() accountId: string;
|
||||
@Input() budgetId: number;
|
||||
public transactionType = TransactionType;
|
||||
public categories: Category[];
|
||||
public rawAmount: string;
|
||||
|
@ -26,8 +23,7 @@ export class AddEditTransactionComponent implements OnInit, OnChanges, OnDestroy
|
|||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
@Inject(CATEGORY_SERVICE) private categoryService: CategoryService,
|
||||
@Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -72,8 +68,8 @@ export class AddEditTransactionComponent implements OnInit, OnChanges, OnDestroy
|
|||
this.currentTransaction.date.setMinutes(parseInt(timeParts[1], 10));
|
||||
if (this.currentTransaction.id) {
|
||||
// This is an existing transaction, update it
|
||||
observable = this.transactionService.updateTransaction(
|
||||
this.accountId,
|
||||
observable = this.twigsService.updateTransaction(
|
||||
this.budgetId,
|
||||
this.currentTransaction.id,
|
||||
{
|
||||
name: this.currentTransaction.title,
|
||||
|
@ -81,18 +77,18 @@ export class AddEditTransactionComponent implements OnInit, OnChanges, OnDestroy
|
|||
amount: this.currentTransaction.amount * 100,
|
||||
date: this.currentTransaction.date,
|
||||
category: this.currentTransaction.categoryId,
|
||||
isExpense: this.currentTransaction.type === TransactionType.EXPENSE
|
||||
isExpense: this.currentTransaction.isExpense
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// This is a new transaction, save it
|
||||
observable = this.transactionService.createTransaction(
|
||||
this.accountId,
|
||||
observable = this.twigsService.createTransaction(
|
||||
this.budgetId,
|
||||
this.currentTransaction.title,
|
||||
this.currentTransaction.description,
|
||||
this.currentTransaction.amount * 100,
|
||||
this.currentTransaction.date,
|
||||
this.currentTransaction.type === TransactionType.EXPENSE,
|
||||
this.currentTransaction.isExpense,
|
||||
this.currentTransaction.categoryId,
|
||||
);
|
||||
}
|
||||
|
@ -107,12 +103,12 @@ export class AddEditTransactionComponent implements OnInit, OnChanges, OnDestroy
|
|||
}
|
||||
|
||||
delete(): void {
|
||||
this.transactionService.deleteTransaction(this.accountId, this.currentTransaction.id).subscribe(() => {
|
||||
this.twigsService.deleteTransaction(this.budgetId, this.currentTransaction.id).subscribe(() => {
|
||||
this.app.goBack();
|
||||
});
|
||||
}
|
||||
|
||||
getCategories() {
|
||||
this.categoryService.getCategories(this.accountId).subscribe(categories => this.categories = categories);
|
||||
this.twigsService.getCategories(this.budgetId).subscribe(categories => this.categories = categories);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
<app-add-edit-transaction [accountId]="accountId" [title]="'Add Transaction'" [currentTransaction]="transaction"></app-add-edit-transaction>
|
||||
<app-add-edit-transaction [budgetId]="budgetId" [title]="'Add Transaction'" [currentTransaction]="transaction"></app-add-edit-transaction>
|
||||
|
|
|
@ -9,7 +9,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||
})
|
||||
export class NewTransactionComponent implements OnInit {
|
||||
|
||||
accountId: string;
|
||||
budgetId: string;
|
||||
transaction: Transaction;
|
||||
|
||||
constructor(
|
||||
|
@ -17,7 +17,7 @@ export class NewTransactionComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.accountId = this.route.snapshot.paramMap.get('accountId');
|
||||
this.budgetId = this.route.snapshot.paramMap.get('budgetId');
|
||||
this.transaction = new Transaction();
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
<app-add-edit-transaction [accountId]="accountId" [title]="'Edit Transaction'" [currentTransaction]="transaction" *ngIf="transaction"></app-add-edit-transaction>
|
||||
<app-add-edit-transaction [budgetId]="budgetId" [title]="'Edit Transaction'" [currentTransaction]="transaction" *ngIf="transaction"></app-add-edit-transaction>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
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 { Account } from 'src/app/accounts/account';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transaction-details',
|
||||
|
@ -11,12 +10,12 @@ import { Account } from 'src/app/accounts/account';
|
|||
})
|
||||
export class TransactionDetailsComponent implements OnInit {
|
||||
|
||||
accountId: string;
|
||||
budgetId: number;
|
||||
transaction: Transaction;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -24,9 +23,8 @@ export class TransactionDetailsComponent implements OnInit {
|
|||
}
|
||||
|
||||
getTransaction(): void {
|
||||
this.accountId = this.route.snapshot.paramMap.get('accountId');
|
||||
const id = this.route.snapshot.paramMap.get('id');
|
||||
this.transactionService.getTransaction(this.accountId, id)
|
||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
||||
this.twigsService.getTransaction(id)
|
||||
.subscribe(transaction => {
|
||||
transaction.amount /= 100;
|
||||
this.transaction = transaction;
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
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();
|
||||
}));
|
||||
});
|
|
@ -1,97 +0,0 @@
|
|||
import { Observable } 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() { }
|
||||
|
||||
getTransactions(accountId: string, category?: string, count?: number): Observable<Transaction[]> {
|
||||
return Observable.create(subscriber => {
|
||||
let transactionQuery: any = firebase.firestore()
|
||||
.collection('accounts')
|
||||
.doc(accountId)
|
||||
.collection('transactions')
|
||||
.orderBy('date', 'desc');
|
||||
if (category) {
|
||||
transactionQuery = transactionQuery.where('category', '==', category);
|
||||
}
|
||||
if (count) {
|
||||
transactionQuery = transactionQuery.limit(count);
|
||||
}
|
||||
transactionQuery.onSnapshot(snapshot => {
|
||||
const transactions = [];
|
||||
snapshot.docs.forEach(transaction => {
|
||||
transactions.push(Transaction.fromSnapshotRef(transaction));
|
||||
});
|
||||
subscriber.next(transactions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getTransaction(accountId: string, id: string): Observable<Transaction> {
|
||||
return Observable.create(subscriber => {
|
||||
firebase.firestore().collection('accounts').doc(accountId).collection('transactions').doc(id).onSnapshot(snapshot => {
|
||||
if (!snapshot.exists) {
|
||||
return;
|
||||
}
|
||||
subscriber.next(Transaction.fromSnapshotRef(snapshot));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createTransaction(
|
||||
accountId: string,
|
||||
name: string,
|
||||
description: string,
|
||||
amount: number,
|
||||
date: Date,
|
||||
isExpense: boolean,
|
||||
category: string
|
||||
): Observable<Transaction> {
|
||||
return Observable.create(subscriber => {
|
||||
firebase.firestore().collection('accounts').doc(accountId).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(accountId: string, id: string, changes: object): Observable<boolean> {
|
||||
return Observable.create(subscriber => {
|
||||
firebase.firestore().collection('accounts').doc(accountId).collection('transactions').doc(id).update(changes).then(() => {
|
||||
subscriber.next(true);
|
||||
}).catch(err => {
|
||||
subscriber.next(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteTransaction(accountId: string, id: string): Observable<boolean> {
|
||||
return Observable.create(subscriber => {
|
||||
firebase.firestore().collection('accounts').doc(accountId).collection('transactions').doc(id).delete().then(data => {
|
||||
subscriber.next(true);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
subscriber.next(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import { Observable } from 'rxjs';
|
||||
import { Transaction } from './transaction';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { Account } from '../accounts/account';
|
||||
|
||||
export interface TransactionService {
|
||||
|
||||
getTransactions(accountId: string, categoryId?: string, count?: number): Observable<Transaction[]>;
|
||||
|
||||
getTransaction(accountId: string, id: string): Observable<Transaction>;
|
||||
|
||||
createTransaction(
|
||||
accountId: string,
|
||||
name: string,
|
||||
description: string,
|
||||
amount: number,
|
||||
date: Date,
|
||||
isExpense: boolean,
|
||||
category: string
|
||||
): Observable<Transaction>;
|
||||
|
||||
updateTransaction(accountId: string, id: string, changes: object): Observable<boolean>;
|
||||
|
||||
deleteTransaction(accountId: string, id: string): Observable<boolean>;
|
||||
}
|
||||
|
||||
export let TRANSACTION_SERVICE = new InjectionToken<TransactionService>('transaction.service');
|
|
@ -1,28 +1,11 @@
|
|||
import { Category } from '../categories/category';
|
||||
import { TransactionType } from './transaction.type';
|
||||
import * as firebase from 'firebase/app';
|
||||
|
||||
export class Transaction {
|
||||
id: string;
|
||||
accountId: string;
|
||||
id: number;
|
||||
title: string;
|
||||
description: string = null;
|
||||
amount = 0;
|
||||
date: Date = new Date();
|
||||
categoryId: string;
|
||||
type: TransactionType = TransactionType.EXPENSE;
|
||||
category: Category;
|
||||
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;
|
||||
}
|
||||
amount: number;
|
||||
isExpense: boolean;
|
||||
categoryId: number;
|
||||
budgetId: number;
|
||||
createdBy: number;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<mat-nav-list *ngIf="transactions" class="transactions">
|
||||
<a mat-list-item *ngFor="let transaction of transactions" routerLink="/accounts/{{ accountId }}/transactions/{{ transaction.id }}">
|
||||
<a mat-list-item *ngFor="let transaction of transactions" routerLink="/budgets/{{ budgetId }}/transactions/{{ transaction.id }}">
|
||||
<div matLine class="list-row-one">
|
||||
<p>{{transaction.title}}</p>
|
||||
<p class="amount" [class.expense]="transaction.type === transactionType.EXPENSE" [class.income]="transaction.type === transactionType.INCOME">
|
||||
|
@ -9,6 +9,6 @@
|
|||
<p matLine class="text-small">{{ transaction.date | date }}</p>
|
||||
</a>
|
||||
</mat-nav-list>
|
||||
<a mat-fab routerLink="/accounts/{{ accountId }}/transactions/new">
|
||||
<a mat-fab routerLink="/budgets/{{ budgetId }}/transactions/new">
|
||||
<mat-icon aria-label="Add">add</mat-icon>
|
||||
</a>
|
|
@ -1,10 +1,10 @@
|
|||
import { Component, OnInit, Input, Inject } from '@angular/core';
|
||||
import { Transaction } from './transaction';
|
||||
import { TransactionType } from './transaction.type';
|
||||
import { TransactionService, TRANSACTION_SERVICE } from './transaction.service';
|
||||
import { AppComponent } from '../app.component';
|
||||
import { Account } from '../accounts/account';
|
||||
import { Budget } from '../budgets/budget';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TWIGS_SERVICE, TwigsService } from '../shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transactions',
|
||||
|
@ -13,7 +13,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||
})
|
||||
export class TransactionsComponent implements OnInit {
|
||||
|
||||
accountId: string;
|
||||
budgetId: number;
|
||||
public transactionType = TransactionType;
|
||||
|
||||
public transactions: Transaction[];
|
||||
|
@ -21,18 +21,18 @@ export class TransactionsComponent implements OnInit {
|
|||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private app: AppComponent,
|
||||
@Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.accountId = this.route.snapshot.paramMap.get('accountId');
|
||||
this.budgetId = Number.parseInt(this.route.snapshot.paramMap.get('budgetId'));
|
||||
this.app.backEnabled = true;
|
||||
this.app.title = 'Transactions';
|
||||
this.getTransactions();
|
||||
}
|
||||
|
||||
getTransactions(): void {
|
||||
this.transactionService.getTransactions(this.accountId).subscribe(transactions => {
|
||||
this.twigsService.getTransactions(this.budgetId).subscribe(transactions => {
|
||||
this.transactions = transactions;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
describe('AuthService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
],
|
||||
providers: [AuthService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([AuthService], (service: AuthService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { User } from './user';
|
||||
import { Router } from '@angular/router';
|
||||
import * as firebase from 'firebase/app';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
login(email: string, password: string) {
|
||||
firebase.auth().signInWithEmailAndPassword(email, password).then(value => {
|
||||
this.router.navigate(['/']);
|
||||
}).catch(err => {
|
||||
console.log('Login failed');
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
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 => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -3,6 +3,6 @@
|
|||
<input matInput placeholder="Email" [(ngModel)]="email" />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput type="password" placeholder="Password" [(ngModel)]="password" />
|
||||
<input matInput type="password" placeholder="Password" [(ngModel)]="password" (keyup.enter)="doAction()"/>
|
||||
</mat-form-field>
|
||||
</div>
|
|
@ -1,8 +1,9 @@
|
|||
import { Component, OnInit, OnDestroy, OnChanges } from '@angular/core';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { Component, OnInit, OnDestroy, Inject, ChangeDetectorRef } from '@angular/core';
|
||||
import { TwigsService, TWIGS_SERVICE } from '../../shared/twigs.service';
|
||||
import { User } from '../user';
|
||||
import { Actionable } from 'src/app/actionable';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
|
@ -16,7 +17,8 @@ export class LoginComponent implements OnInit, OnDestroy, Actionable {
|
|||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
private authService: AuthService,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -30,7 +32,15 @@ export class LoginComponent implements OnInit, OnDestroy, Actionable {
|
|||
}
|
||||
|
||||
doAction(): void {
|
||||
this.authService.login(this.email, this.password);
|
||||
this.twigsService.login(this.email, this.password)
|
||||
.subscribe(user => {
|
||||
this.app.user = user;
|
||||
this.router.navigate(['/'])
|
||||
},
|
||||
error => {
|
||||
console.error(error)
|
||||
alert("Login failed. Please verify you have the correct credentials");
|
||||
})
|
||||
}
|
||||
|
||||
getActionLabel() {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<div class="form register-form">
|
||||
<mat-form-field>
|
||||
<input matInput placeholder="Username" [(ngModel)]="user.name" />
|
||||
<input matInput placeholder="Username" [(ngModel)]="username" />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput type="email" placeholder="Email Address" [(ngModel)]="user.email" />
|
||||
<input matInput type="email" placeholder="Email Address" [(ngModel)]="email" />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput type="password" placeholder="Password" [(ngModel)]="user.password" />
|
||||
<input matInput type="password" placeholder="Password" [(ngModel)]="password" />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput type="password" placeholder="Confirm Password" [(ngModel)]="confirmedPassword" />
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { User } from '../user';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
|
||||
import { TwigsService, TWIGS_SERVICE } from '../../shared/twigs.service';
|
||||
import { Actionable } from 'src/app/actionable';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
|
@ -11,16 +11,19 @@ import { AppComponent } from 'src/app/app.component';
|
|||
})
|
||||
export class RegisterComponent implements OnInit, OnDestroy, Actionable {
|
||||
|
||||
public user: User = new User();
|
||||
public username: string;
|
||||
public email: string;
|
||||
public password: string;
|
||||
public confirmedPassword: string;
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
private authService: AuthService,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.title = 'Login';
|
||||
this.app.title = 'Register';
|
||||
this.app.actionable = this;
|
||||
this.app.backEnabled = true;
|
||||
}
|
||||
|
@ -30,11 +33,17 @@ export class RegisterComponent implements OnInit, OnDestroy, Actionable {
|
|||
}
|
||||
|
||||
doAction(): void {
|
||||
if (this.user.password !== this.confirmedPassword) {
|
||||
if (this.password !== this.confirmedPassword) {
|
||||
alert('Passwords don\'t match');
|
||||
return;
|
||||
}
|
||||
this.authService.register(this.user.email, this.user.password);
|
||||
this.twigsService.register(this.username, this.email, this.password).subscribe(user => {
|
||||
console.log(user);
|
||||
this.router.navigate(['/'])
|
||||
}, error => {
|
||||
console.error(error);
|
||||
alert("Registration failed!")
|
||||
})
|
||||
}
|
||||
|
||||
getActionLabel() {
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { UserService } from './user.service';
|
||||
import { FirestoreUserService } from './user.service.firestore';
|
||||
|
||||
describe('UserService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [FirestoreUserService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([FirestoreUserService], (service: UserService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
import { InjectionToken } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { User } from './user';
|
||||
import { UserService } from './user.service';
|
||||
import * as firebase from 'firebase/app';
|
||||
import 'firebase/firestore';
|
||||
|
||||
export class FirestoreUserService implements UserService {
|
||||
getUsersByUsername(username: string): Observable<User[]> {
|
||||
return Observable.create(subscriber => {
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { InjectionToken } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { User } from './user';
|
||||
|
||||
export interface UserService {
|
||||
getUsersByUsername(username: string): Observable<User[]>;
|
||||
}
|
||||
|
||||
export let USER_SERVICE = new InjectionToken<UserService>('user.service');
|
|
@ -1,6 +1,11 @@
|
|||
export class User {
|
||||
id: number;
|
||||
name: string;
|
||||
password: string;
|
||||
username: string;
|
||||
email: string;
|
||||
|
||||
constructor(id?: number, username?: string, email?: string) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,15 +3,15 @@
|
|||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Budget</title>
|
||||
<title>Twigs</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
|
||||
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#81c784">
|
||||
<meta name="apple-mobile-web-app-title" content="Budget">
|
||||
<meta name="application-name" content="Budget">
|
||||
<meta name="apple-mobile-web-app-title" content="Twigs">
|
||||
<meta name="application-name" content="Twigs">
|
||||
<meta name="msapplication-TileColor" content="#81c784">
|
||||
<meta name="theme-color" content="#212121">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/** IE10 and IE11 requires the following for the Reflect API. */
|
||||
// import 'core-js/es6/reflect';
|
||||
import 'core-js/es6/reflect';
|
||||
|
||||
|
||||
/** Evergreen browsers require these. **/
|
||||
|
|
Loading…
Reference in a new issue