Large rewrite to consolidate services and integrate with Spring backend

Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
William Brawner 2020-02-18 19:53:05 -07:00
parent c4626c369f
commit 4d96740fc7
73 changed files with 909 additions and 909 deletions

41
package-lock.json generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
<app-add-edit-account [title]="'Add Account'" [account]="account"></app-add-edit-account>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

View file

@ -0,0 +1,8 @@
import { User } from '../users/user';
export class Budget {
id: number;
name: string;
description: string;
users: User[];
}

View file

@ -0,0 +1 @@
<app-add-edit-budget [title]="'Add Budget'" [budget]="budget"></app-add-edit-budget>

View file

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

View 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() {
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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")
});
}
}

View 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];
}
}

View 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');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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. **/