WIP: Implement accounts

Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
Billy Brawner 2019-05-04 10:58:47 -07:00
parent 7cb97d1487
commit e7067744d9
93 changed files with 4779 additions and 3643 deletions

7700
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,46 +11,46 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^6.1.4", "@angular/animations": "^7.2.14",
"@angular/cdk": "^6.4.6", "@angular/cdk": "^7.3.7",
"@angular/common": "^6.1.0", "@angular/common": "^7.2.14",
"@angular/compiler": "^6.1.0", "@angular/compiler": "^7.2.14",
"@angular/core": "^6.1.0", "@angular/core": "^7.2.14",
"@angular/forms": "^6.1.0", "@angular/forms": "^7.2.14",
"@angular/http": "^6.1.0", "@angular/http": "^7.2.14",
"@angular/material": "^6.4.6", "@angular/material": "^7.3.7",
"@angular/platform-browser": "^6.1.0", "@angular/platform-browser": "^7.2.14",
"@angular/platform-browser-dynamic": "^6.1.0", "@angular/platform-browser-dynamic": "^7.2.14",
"@angular/pwa": "^0.7.5", "@angular/pwa": "^0.7.5",
"@angular/router": "^6.1.0", "@angular/router": "^7.2.14",
"@angular/service-worker": "^6.1.0", "@angular/service-worker": "^7.2.14",
"core-js": "^2.5.4", "core-js": "^2.6.5",
"dexie": "^2.0.4", "dexie": "^2.0.4",
"firebase": "^5.5.8", "firebase": "^5.11.1",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"ng2-currency-mask": "^5.3.1", "ng2-currency-mask": "^5.3.1",
"rxjs": "^6.0.0", "rxjs": "^6.5.1",
"zone.js": "~0.8.26" "zone.js": "^0.8.29"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~0.7.0", "@angular-devkit/build-angular": "^0.13.8",
"@angular/cli": "~6.1.5", "@angular/cli": "~6.1.5",
"@angular/compiler-cli": "^6.1.0", "@angular/compiler-cli": "^7.2.14",
"@angular/language-service": "^6.1.0", "@angular/language-service": "^7.2.14",
"@types/jasmine": "~2.8.6", "@types/jasmine": "^2.8.16",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "^2.0.6",
"@types/node": "~8.9.4", "@types/node": "~8.9.4",
"codelyzer": "~4.2.1", "codelyzer": "~4.2.1",
"jasmine-core": "~2.99.1", "jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1", "jasmine-spec-reporter": "~4.2.1",
"karma": "~1.7.1", "karma": "~1.7.1",
"karma-chrome-launcher": "~2.2.0", "karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.0", "karma-coverage-istanbul-reporter": "^2.0.5",
"karma-jasmine": "~1.1.1", "karma-jasmine": "~1.1.1",
"karma-jasmine-html-reporter": "^0.2.2", "karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0", "protractor": "^5.4.2",
"ts-node": "~5.0.1", "ts-node": "~5.0.1",
"tslint": "~5.9.1", "tslint": "~5.9.1",
"typescript": "~2.7.2" "typescript": "~3.2.4"
} }
} }

View file

@ -1,9 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AccountService {
constructor() { }
}

View file

@ -1,7 +0,0 @@
import { IAccount } from './budget-database'
export class Account implements IAccount {
id: number;
remoteId: number;
name: string;
}

View file

@ -0,0 +1,3 @@
<p>
account-details works!
</p>

View file

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AccountDetailsComponent } from './account-details.component';
describe('AccountDetailsComponent', () => {
let component: AccountDetailsComponent;
let fixture: ComponentFixture<AccountDetailsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AccountDetailsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AccountDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-account-details',
templateUrl: './account-details.component.html',
styleUrls: ['./account-details.component.css']
})
export class AccountDetailsComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View file

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

View file

@ -0,0 +1,77 @@
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.firestore().collection('accounts').onSnapshot(data => {
if (!data.empty) {
data.docs.map(account => accounts.push(Account.fromSnapshotRef(account)));
}
subscriber.next(accounts);
});
});
}
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: User[],
): Observable<Account> {
return Observable.create(subscriber => {
firebase.firestore().collection('accounts').add({
name: name,
description: description,
members: members.map(member => member.id)
}).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

@ -0,0 +1,19 @@
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: User[],
): 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

@ -0,0 +1,17 @@
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

@ -0,0 +1,16 @@
<mat-nav-list class="accounts">
<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>

View file

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AccountsComponent } from './accounts.component';
describe('AccountsComponent', () => {
let component: AccountsComponent;
let fixture: ComponentFixture<AccountsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AccountsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AccountsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,28 @@
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 = 'Transactions';
this.accountService.getAccounts().subscribe(accounts => {
this.accounts = accounts;
});
}
}

View file

@ -0,0 +1,15 @@
<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

@ -0,0 +1,75 @@
import { Component, OnInit, Input, Inject, OnDestroy } from '@angular/core';
import { Account } from '../account';
import { ACCOUNT_SERVICE, AccountService } from '../account.service';
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';
@Component({
selector: 'app-add-edit-account',
templateUrl: './add-edit-account.component.html',
styleUrls: ['./add-edit-account.component.css']
})
export class AddEditAccountComponent implements OnInit, OnDestroy, Actionable {
@Input() title: string;
@Input() account: Account;
public users: User[];
public searchedUsers: User[];
constructor(
private app: AppComponent,
@Inject(ACCOUNT_SERVICE) private accountService: AccountService,
@Inject(USER_SERVICE) private userService: UserService,
) {
this.app.title = this.title;
this.app.backEnabled = true;
this.app.actionable = this;
}
ngOnInit() {
}
ngOnDestroy(): void {
this.app.actionable = null;
}
doAction(): void {
let observable;
if (this.account.id) {
// This is an existing transaction, update it
observable = this.accountService.updateAccount(this.account.id, this.account);
} else {
// This is a new transaction, save it
observable = this.accountService.createAccount(
this.account.name,
this.account.description,
this.account.currency,
this.users
);
}
// TODO: Check if it was actually successful or not
observable.subscribe(val => {
this.app.goBack();
});
}
getActionLabel(): string {
return 'Save';
}
delete(): void {
this.accountService.deleteAccount(this.account.id);
this.app.goBack();
}
searchUsers(username: string) {
this.userService.getUsersByUsername(username).subscribe(users => {
this.searchedUsers = users;
});
}
clearUserSearch() {
this.searchedUsers = [];
}
}

View file

@ -0,0 +1,3 @@
<p>
new-account works!
</p>

View file

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NewAccountComponent } from './new-account.component';
describe('NewAccountComponent', () => {
let component: NewAccountComponent;
let fixture: ComponentFixture<NewAccountComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ NewAccountComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NewAccountComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-new-account',
templateUrl: './new-account.component.html',
styleUrls: ['./new-account.component.css']
})
export class NewAccountComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View file

@ -1,3 +0,0 @@
<p>
add-edit-account works!
</p>

View file

@ -1,15 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-add-edit-account',
templateUrl: './add-edit-account.component.html',
styleUrls: ['./add-edit-account.component.css']
})
export class AddEditAccountComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View file

@ -2,18 +2,24 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component'; import { DashboardComponent } from './dashboard/dashboard.component';
import { TransactionsComponent } from './transactions/transactions.component'; import { TransactionsComponent } from './transactions/transactions.component';
import { TransactionDetailsComponent } from './transaction-details/transaction-details.component'; import { TransactionDetailsComponent } from './transactions/transaction-details/transaction-details.component';
import { NewTransactionComponent } from './new-transaction/new-transaction.component'; import { NewTransactionComponent } from './transactions/new-transaction/new-transaction.component';
import { CategoriesComponent } from './categories/categories.component'; import { CategoriesComponent } from './categories/categories.component';
import { CategoryDetailsComponent } from './category-details/category-details.component'; import { CategoryDetailsComponent } from './categories/category-details/category-details.component';
import { NewCategoryComponent } from './new-category/new-category.component'; import { NewCategoryComponent } from './categories/new-category/new-category.component';
import { LoginComponent } from './login/login.component'; import { LoginComponent } from './users/login/login.component';
import { RegisterComponent } from './register/register.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';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: DashboardComponent }, { path: '', component: DashboardComponent },
{ path: 'login', component: LoginComponent }, { path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent }, { path: 'register', component: RegisterComponent },
{ path: 'accounts', component: AccountsComponent },
{ path: 'accounts/new', component: NewAccountComponent },
{ path: 'accounts/:id', component: AccountDetailsComponent },
{ path: 'transactions', component: TransactionsComponent }, { path: 'transactions', component: TransactionsComponent },
{ path: 'transactions/new', component: NewTransactionComponent }, { path: 'transactions/new', component: NewTransactionComponent },
{ path: 'transactions/:id', component: TransactionDetailsComponent }, { path: 'transactions/:id', component: TransactionDetailsComponent },

View file

@ -2,8 +2,8 @@
<mat-sidenav #sidenav mode="over" closed> <mat-sidenav #sidenav mode="over" closed>
<mat-nav-list (click)="sidenav.close()"> <mat-nav-list (click)="sidenav.close()">
<a mat-list-item *ngIf="isLoggedIn()" routerLink="">{{ getUsername() }}</a> <a mat-list-item *ngIf="isLoggedIn()" routerLink="">{{ getUsername() }}</a>
<a mat-list-item routerLink="/accounts">Manage Accounts</a> <a mat-list-item *ngIf="isLoggedIn()" routerLink="/accounts">Manage Accounts</a>
<a mat-list-item (click)="exportData()">Export Data</a> <a mat-list-item *ngIf="isLoggedIn()" (click)="exportData()">Export Data</a>
<a mat-list-item *ngIf="!isLoggedIn()" routerLink="/login">Login</a> <a mat-list-item *ngIf="!isLoggedIn()" routerLink="/login">Login</a>
<a mat-list-item *ngIf="!isLoggedIn()" routerLink="/register">Register</a> <a mat-list-item *ngIf="!isLoggedIn()" routerLink="/register">Register</a>
<a mat-list-item *ngIf="isLoggedIn()" (click)="logout()">Logout</a> <a mat-list-item *ngIf="isLoggedIn()" (click)="logout()">Logout</a>

View file

@ -1,8 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { Actionable } from './actionable'; import { Actionable } from './actionable';
import { AuthService } from './auth.service'; import { AuthService } from './users/auth.service';
import { BudgetDatabase } from './budget-database';
import * as firebase from 'firebase/app'; import * as firebase from 'firebase/app';
import 'firebase/auth'; import 'firebase/auth';
@ -20,7 +19,6 @@ export class AppComponent {
constructor( constructor(
public authService: AuthService, public authService: AuthService,
private location: Location, private location: Location,
private db: BudgetDatabase,
) { ) {
const config = { const config = {
apiKey: 'AIzaSyALYI-ILmLV8NBNXE3DLF9yf1Z5Pp-Y1Mk', apiKey: 'AIzaSyALYI-ILmLV8NBNXE3DLF9yf1Z5Pp-Y1Mk',
@ -45,22 +43,6 @@ export class AppComponent {
this.authService.logout(); this.authService.logout();
} }
exportData(): void {
this.db.export().subscribe(data => {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.target = '_blank';
a.download = 'budget.json';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
});
}
isLoggedIn() { isLoggedIn() {
return firebase.auth().currentUser != null; return firebase.auth().currentUser != null;
} }

View file

@ -19,29 +19,34 @@ import {
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { TransactionsComponent } from './transactions/transactions.component'; import { TransactionsComponent } from './transactions/transactions.component';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { TransactionDetailsComponent } from './transaction-details/transaction-details.component'; import { AccountsComponent } from './accounts/accounts.component';
import { NewTransactionComponent } from './new-transaction/new-transaction.component'; import { TransactionDetailsComponent } from './transactions/transaction-details/transaction-details.component';
import { AddEditTransactionComponent } from './add-edit-transaction/add-edit-transaction.component'; import { NewTransactionComponent } from './transactions/new-transaction/new-transaction.component';
import { AddEditTransactionComponent } from './transactions/add-edit-transaction/add-edit-transaction.component';
import { DashboardComponent } from './dashboard/dashboard.component'; import { DashboardComponent } from './dashboard/dashboard.component';
import { CategoriesComponent } from './categories/categories.component'; import { CategoriesComponent } from './categories/categories.component';
import { CategoryDetailsComponent } from './category-details/category-details.component'; import { CategoryDetailsComponent } from './categories/category-details/category-details.component';
import { AddEditCategoryComponent } from './add-edit-category/add-edit-category.component'; import { AddEditCategoryComponent } from './categories/add-edit-category/add-edit-category.component';
import { NewCategoryComponent } from './new-category/new-category.component'; import { NewCategoryComponent } from './categories/new-category/new-category.component';
import { CategoryListComponent } from './category-list/category-list.component'; import { CategoryListComponent } from './categories/category-list/category-list.component';
import { ServiceWorkerModule } from '@angular/service-worker'; import { LoginComponent } from './users/login/login.component';
import { environment } from '../environments/environment'; import { RegisterComponent } from './users/register/register.component';
import { LoginComponent } from './login/login.component'; import { AddEditAccountComponent } from './accounts/add-edit-account/add-edit-account.component';
import { RegisterComponent } from './register/register.component'; import { EditProfileComponent } from './users/edit-profile/edit-profile.component';
import { AddEditAccountComponent } from './add-edit-account/add-edit-account.component'; import { UserComponent } from './users/user.component';
import { EditProfileComponent } from './edit-profile/edit-profile.component'; import { NewAccountComponent } from './accounts/new-account/new-account.component';
import { UserComponent } from './user/user.component'; import { AccountDetailsComponent } from './accounts/account-details/account-details.component';
import { environment } from 'src/environments/environment';
import { HttpClientModule } from '@angular/common/http'; 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 { CurrencyMaskModule } from 'ng2-currency-mask';
import { CurrencyMaskConfig, CURRENCY_MASK_CONFIG } from 'ng2-currency-mask/src/currency-mask.config'; import { CurrencyMaskConfig, CURRENCY_MASK_CONFIG } from 'ng2-currency-mask/src/currency-mask.config';
import { TransactionServiceFirebaseFirestoreImpl } from './transaction.service.firestore'; import { ServiceWorkerModule } from '@angular/service-worker';
import { TRANSACTION_SERVICE } from './transaction.service'; import { ACCOUNT_SERVICE } from './accounts/account.service';
import { CATEGORY_SERVICE } from './category.service'; import { FirestoreAccountService } from './accounts/account.service.firestore';
import { CategoryServiceFirebaseFirestoreImpl } from './category.service.firestore';
export const CustomCurrencyMaskConfig: CurrencyMaskConfig = { export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
align: 'left', align: 'left',
@ -71,6 +76,9 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
AddEditAccountComponent, AddEditAccountComponent,
EditProfileComponent, EditProfileComponent,
UserComponent, UserComponent,
NewAccountComponent,
AccountDetailsComponent,
AccountsComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -96,6 +104,7 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
{ provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig }, { provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig },
{ provide: TRANSACTION_SERVICE, useClass: TransactionServiceFirebaseFirestoreImpl }, { provide: TRANSACTION_SERVICE, useClass: TransactionServiceFirebaseFirestoreImpl },
{ provide: CATEGORY_SERVICE, useClass: CategoryServiceFirebaseFirestoreImpl }, { provide: CATEGORY_SERVICE, useClass: CategoryServiceFirebaseFirestoreImpl },
{ provide: ACCOUNT_SERVICE, useClass: FirestoreAccountService },
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View file

@ -1,95 +0,0 @@
import { Injectable } from '@angular/core';
import Dexie from 'dexie';
import { Account } from './account'
import { Category } from './category'
import { Transaction } from './transaction'
import { TransactionType } from './transaction.type';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class BudgetDatabase extends Dexie {
transactions: Dexie.Table<ITransaction, number>;
categories: Dexie.Table<ICategory, number>;
accounts: Dexie.Table<IAccount, number>;
constructor() {
super('BudgetDatabase')
this.version(1).stores({
transactions: `++id, title, description, amount, date, category, type`,
categories: `++id, name, amount, repeat, color`
});
this.version(2).stores({
transactions: `++id, title, description, amount, date, category_id, type`,
categories: `++id, name, amount, repeat, color, type`
});
this.version(3).stores({
transactions: `++id, title, description, amount, date, category_id, type`,
categories: `++id, name, amount, repeat, color`
});
this.version(4).stores({
transactions: `++id, remote_id, account_id, title, description, amount, date, category_id, type`,
categories: `++id, remote_id, account_id, name, amount, repeat, color`,
accounts: `++id, remote_id, name`
}).upgrade(dbTransaction => {
// Since the server stores amounts as integers, we need to modify the locally stored
// values to also use integers
return dbTransaction.tables.transactions.toCollection().modify(transaction => {
transaction.amount *= 100;
}).then(count => {
dbTransaction.tables.categories.toCollection().modify(category => {
category.amount *= 100;
});
});
});
this.version(5).stores({
transactions: `++id, &remote_id, account_id, title, description, amount, date, category_id, type`,
categories: `++id, &remote_id, account_id, name, amount, repeat, color`,
accounts: `++id, &remote_id, name`
});
this.transactions.mapToClass(Transaction);
this.categories.mapToClass(Category);
this.accounts.mapToClass(Account);
}
export(): Observable<Array<any>> {
const db = this;
return Observable.create(observer => {
const dump = {};
db.tables.forEach(table => {
dump[table.name] = table.toArray();
});
observer.next(dump);
observer.complete();
});
}
}
export interface ITransaction {
id: string;
accountId: string;
remoteId: string;
title: string;
description: string;
amount: number;
date: Date;
categoryId: string;
type: TransactionType;
}
export interface ICategory {
id: string;
accountId: string;
remoteId: string;
name: string;
amount: number;
repeat: string;
color: string;
}
export interface IAccount {
id: number;
remoteId: number;
name: string;
}

View file

@ -1,8 +1,8 @@
import { Component, OnInit, Input, OnDestroy, Inject } from '@angular/core'; import { Component, OnInit, Input, OnDestroy, Inject } from '@angular/core';
import { Category } from '../category'; import { Category } from '../category';
import { Actionable } from '../actionable';
import { AppComponent } from '../app.component';
import { CATEGORY_SERVICE, CategoryService } from '../category.service'; import { CATEGORY_SERVICE, CategoryService } from '../category.service';
import { Actionable } from 'src/app/actionable';
import { AppComponent } from 'src/app/app.component';
@Component({ @Component({
selector: 'app-add-edit-category', selector: 'app-add-edit-category',

View file

@ -1,10 +1,10 @@
import { Component, OnInit, Input, Inject } from '@angular/core'; import { Component, OnInit, Input, Inject } from '@angular/core';
import { CategoryService, CATEGORY_SERVICE } from '../category.service'; import { CategoryService, CATEGORY_SERVICE } from './category.service';
import { Category } from '../category'; import { Category } from './category';
import { AppComponent } from '../app.component'; import { AppComponent } from '../app.component';
import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service'; import { TransactionService, TRANSACTION_SERVICE } from '../transactions/transaction.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { TransactionType } from '../transaction.type'; import { TransactionType } from '../transactions/transaction.type';
@Component({ @Component({
selector: 'app-categories', selector: 'app-categories',

View file

@ -1,6 +1,4 @@
import { ICategory } from './budget-database'; export class Category {
export class Category implements ICategory {
id: string; id: string;
accountId: string; accountId: string;
remoteId: string; remoteId: string;

View file

@ -1,14 +1,18 @@
<div class="dashboard"> <div class="dashboard">
<div class="dashboard-primary"> <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>
<div class="dashboard-primary" *ngIf="isLoggedIn()">
<h2 class="balance"> <h2 class="balance">
Current Balance: <br /> Current Balance: <br />
<span [ngClass]="{'income': getBalance() > 0, 'expense': getBalance() < 0}" >{{ getBalance() / 100 | currency }}</span> <span [ngClass]="{'income': getBalance() > 0, 'expense': getBalance() < 0}">{{ getBalance() / 100 | currency }}</span>
</h2> </h2>
<div class="transaction-navigation"> <div class="transaction-navigation">
<a mat-button routerLink="/transactions">View Transactions</a> <a mat-button routerLink="/transactions">View Transactions</a>
</div> </div>
</div> </div>
<div class="dashboard-categories"> <div class="dashboard-categories" *ngIf="isLoggedIn()">
<h3 class="categories">Categories</h3> <h3 class="categories">Categories</h3>
<a mat-button routerLink="/categories" class="view-all" *ngIf="categories">View All</a> <a mat-button routerLink="/categories" class="view-all" *ngIf="categories">View All</a>
<div class="no-categories" *ngIf="!categories || categories.length === 0"> <div class="no-categories" *ngIf="!categories || categories.length === 0">
@ -20,6 +24,6 @@
<app-category-list [categories]="categories" [categoryBalances]="categoryBalances"></app-category-list> <app-category-list [categories]="categories" [categoryBalances]="categoryBalances"></app-category-list>
</div> </div>
</div> </div>
<a mat-fab routerLink="/transactions/new"> <a mat-fab routerLink="/transactions/new" *ngIf="isLoggedIn()">
<mat-icon aria-label="Add">add</mat-icon> <mat-icon aria-label="Add">add</mat-icon>
</a> </a>

View file

@ -1,11 +1,11 @@
import { Component, OnInit, Inject } from '@angular/core'; import { Component, OnInit, Inject } from '@angular/core';
import { Transaction } from '../transaction'; import { Transaction } from '../transactions/transaction';
import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service'; import { TransactionService, TRANSACTION_SERVICE } from '../transactions/transaction.service';
import { Category } from '../category'; import { Category } from '../categories/category';
import { AppComponent } from '../app.component'; import { AppComponent } from '../app.component';
import { TransactionType } from '../transaction.type'; import { TransactionType } from '../transactions/transaction.type';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { CategoryService, CATEGORY_SERVICE } from '../category.service'; import { CategoryService, CATEGORY_SERVICE } from '../categories/category.service';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
@ -72,4 +72,8 @@ export class DashboardComponent implements OnInit {
}); });
}); });
} }
isLoggedIn(): boolean {
return this.app.isLoggedIn();
}
} }

View file

@ -2,10 +2,10 @@ import { Component, OnInit, Input, OnChanges, OnDestroy, Inject } from '@angular
import { Transaction } from '../transaction'; import { Transaction } from '../transaction';
import { TransactionType } from '../transaction.type'; import { TransactionType } from '../transaction.type';
import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service'; import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service';
import { Category } from '../category'; import { Category } from 'src/app/categories/category';
import { AppComponent } from '../app.component'; import { Actionable } from 'src/app/actionable';
import { Actionable } from '../actionable'; import { AppComponent } from 'src/app/app.component';
import { CATEGORY_SERVICE, CategoryService } from '../category.service'; import { CATEGORY_SERVICE, CategoryService } from 'src/app/categories/category.service';
@Component({ @Component({
selector: 'app-add-edit-transaction', selector: 'app-add-edit-transaction',

View file

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Transaction } from '../transaction' import { Transaction } from '../transaction';
@Component({ @Component({
selector: 'app-new-transaction', selector: 'app-new-transaction',

View file

@ -1,9 +1,8 @@
import { ITransaction } from './budget-database'; import { Category } from '../categories/category';
import { Category } from './category';
import { TransactionType } from './transaction.type'; import { TransactionType } from './transaction.type';
import * as firebase from 'firebase/app'; import * as firebase from 'firebase/app';
export class Transaction implements ITransaction { export class Transaction {
id: string; id: string;
accountId: string; accountId: string;
remoteId: string; remoteId: string;

View file

@ -1,7 +1,7 @@
import { Component, OnInit, Input, Inject } from '@angular/core'; import { Component, OnInit, Input, Inject } from '@angular/core';
import { Transaction } from '../transaction'; import { Transaction } from './transaction';
import { TransactionType } from '../transaction.type'; import { TransactionType } from './transaction.type';
import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service'; import { TransactionService, TRANSACTION_SERVICE } from './transaction.service';
import { AppComponent } from '../app.component'; import { AppComponent } from '../app.component';
@Component({ @Component({

View file

View file

@ -1,8 +1,8 @@
import { Component, OnInit, OnDestroy, OnChanges } from '@angular/core'; import { Component, OnInit, OnDestroy, OnChanges } from '@angular/core';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
import { User } from '../user'; import { User } from '../user';
import { Actionable } from '../actionable'; import { Actionable } from 'src/app/actionable';
import { AppComponent } from '../app.component'; import { AppComponent } from 'src/app/app.component';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',

View file

@ -1,8 +1,8 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { User } from '../user'; import { User } from '../user';
import { AppComponent } from '../app.component';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
import { Actionable } from '../actionable'; import { Actionable } from 'src/app/actionable';
import { AppComponent } from 'src/app/app.component';
@Component({ @Component({
selector: 'app-register', selector: 'app-register',

View file

View file

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

View file

@ -0,0 +1,9 @@
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');