WIP: Implement accounts
Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
parent
7cb97d1487
commit
e7067744d9
93 changed files with 4779 additions and 3643 deletions
7690
package-lock.json
generated
7690
package-lock.json
generated
File diff suppressed because it is too large
Load diff
48
package.json
48
package.json
|
@ -11,46 +11,46 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^6.1.4",
|
||||
"@angular/cdk": "^6.4.6",
|
||||
"@angular/common": "^6.1.0",
|
||||
"@angular/compiler": "^6.1.0",
|
||||
"@angular/core": "^6.1.0",
|
||||
"@angular/forms": "^6.1.0",
|
||||
"@angular/http": "^6.1.0",
|
||||
"@angular/material": "^6.4.6",
|
||||
"@angular/platform-browser": "^6.1.0",
|
||||
"@angular/platform-browser-dynamic": "^6.1.0",
|
||||
"@angular/animations": "^7.2.14",
|
||||
"@angular/cdk": "^7.3.7",
|
||||
"@angular/common": "^7.2.14",
|
||||
"@angular/compiler": "^7.2.14",
|
||||
"@angular/core": "^7.2.14",
|
||||
"@angular/forms": "^7.2.14",
|
||||
"@angular/http": "^7.2.14",
|
||||
"@angular/material": "^7.3.7",
|
||||
"@angular/platform-browser": "^7.2.14",
|
||||
"@angular/platform-browser-dynamic": "^7.2.14",
|
||||
"@angular/pwa": "^0.7.5",
|
||||
"@angular/router": "^6.1.0",
|
||||
"@angular/service-worker": "^6.1.0",
|
||||
"core-js": "^2.5.4",
|
||||
"@angular/router": "^7.2.14",
|
||||
"@angular/service-worker": "^7.2.14",
|
||||
"core-js": "^2.6.5",
|
||||
"dexie": "^2.0.4",
|
||||
"firebase": "^5.5.8",
|
||||
"firebase": "^5.11.1",
|
||||
"hammerjs": "^2.0.8",
|
||||
"ng2-currency-mask": "^5.3.1",
|
||||
"rxjs": "^6.0.0",
|
||||
"zone.js": "~0.8.26"
|
||||
"rxjs": "^6.5.1",
|
||||
"zone.js": "^0.8.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.7.0",
|
||||
"@angular-devkit/build-angular": "^0.13.8",
|
||||
"@angular/cli": "~6.1.5",
|
||||
"@angular/compiler-cli": "^6.1.0",
|
||||
"@angular/language-service": "^6.1.0",
|
||||
"@types/jasmine": "~2.8.6",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@angular/compiler-cli": "^7.2.14",
|
||||
"@angular/language-service": "^7.2.14",
|
||||
"@types/jasmine": "^2.8.16",
|
||||
"@types/jasminewd2": "^2.0.6",
|
||||
"@types/node": "~8.9.4",
|
||||
"codelyzer": "~4.2.1",
|
||||
"jasmine-core": "~2.99.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~1.7.1",
|
||||
"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-html-reporter": "^0.2.2",
|
||||
"protractor": "~5.4.0",
|
||||
"protractor": "^5.4.2",
|
||||
"ts-node": "~5.0.1",
|
||||
"tslint": "~5.9.1",
|
||||
"typescript": "~2.7.2"
|
||||
"typescript": "~3.2.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AccountService {
|
||||
|
||||
constructor() { }
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { IAccount } from './budget-database'
|
||||
|
||||
export class Account implements IAccount {
|
||||
id: number;
|
||||
remoteId: number;
|
||||
name: string;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<p>
|
||||
account-details works!
|
||||
</p>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { AccountService } from './account.service';
|
||||
import { FirestoreAccountService } from './account.service.firestore';
|
||||
|
||||
describe('AccountService', () => {
|
||||
beforeEach(() => {
|
||||
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();
|
||||
}));
|
||||
});
|
77
src/app/accounts/account.service.firestore.ts
Normal file
77
src/app/accounts/account.service.firestore.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
19
src/app/accounts/account.service.ts
Normal file
19
src/app/accounts/account.service.ts
Normal 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');
|
17
src/app/accounts/account.ts
Normal file
17
src/app/accounts/account.ts
Normal 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;
|
||||
}
|
||||
}
|
16
src/app/accounts/accounts.component.html
Normal file
16
src/app/accounts/accounts.component.html
Normal 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>
|
25
src/app/accounts/accounts.component.spec.ts
Normal file
25
src/app/accounts/accounts.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
28
src/app/accounts/accounts.component.ts
Normal file
28
src/app/accounts/accounts.component.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 = [];
|
||||
}
|
||||
}
|
3
src/app/accounts/new-account/new-account.component.html
Normal file
3
src/app/accounts/new-account/new-account.component.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<p>
|
||||
new-account works!
|
||||
</p>
|
25
src/app/accounts/new-account/new-account.component.spec.ts
Normal file
25
src/app/accounts/new-account/new-account.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
15
src/app/accounts/new-account/new-account.component.ts
Normal file
15
src/app/accounts/new-account/new-account.component.ts
Normal 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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
<p>
|
||||
add-edit-account works!
|
||||
</p>
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -2,18 +2,24 @@ import { NgModule } from '@angular/core';
|
|||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { TransactionsComponent } from './transactions/transactions.component';
|
||||
import { TransactionDetailsComponent } from './transaction-details/transaction-details.component';
|
||||
import { NewTransactionComponent } from './new-transaction/new-transaction.component';
|
||||
import { TransactionDetailsComponent } from './transactions/transaction-details/transaction-details.component';
|
||||
import { NewTransactionComponent } from './transactions/new-transaction/new-transaction.component';
|
||||
import { CategoriesComponent } from './categories/categories.component';
|
||||
import { CategoryDetailsComponent } from './category-details/category-details.component';
|
||||
import { NewCategoryComponent } from './new-category/new-category.component';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { RegisterComponent } from './register/register.component';
|
||||
import { CategoryDetailsComponent } from './categories/category-details/category-details.component';
|
||||
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';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: DashboardComponent },
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{ path: 'register', component: RegisterComponent },
|
||||
{ path: 'accounts', component: AccountsComponent },
|
||||
{ path: 'accounts/new', component: NewAccountComponent },
|
||||
{ path: 'accounts/:id', component: AccountDetailsComponent },
|
||||
{ path: 'transactions', component: TransactionsComponent },
|
||||
{ path: 'transactions/new', component: NewTransactionComponent },
|
||||
{ path: 'transactions/:id', component: TransactionDetailsComponent },
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<mat-sidenav #sidenav mode="over" closed>
|
||||
<mat-nav-list (click)="sidenav.close()">
|
||||
<a mat-list-item *ngIf="isLoggedIn()" routerLink="">{{ getUsername() }}</a>
|
||||
<a mat-list-item routerLink="/accounts">Manage Accounts</a>
|
||||
<a mat-list-item (click)="exportData()">Export Data</a>
|
||||
<a mat-list-item *ngIf="isLoggedIn()" routerLink="/accounts">Manage Accounts</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="/register">Register</a>
|
||||
<a mat-list-item *ngIf="isLoggedIn()" (click)="logout()">Logout</a>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { Actionable } from './actionable';
|
||||
import { AuthService } from './auth.service';
|
||||
import { BudgetDatabase } from './budget-database';
|
||||
import { AuthService } from './users/auth.service';
|
||||
import * as firebase from 'firebase/app';
|
||||
import 'firebase/auth';
|
||||
|
||||
|
@ -20,7 +19,6 @@ export class AppComponent {
|
|||
constructor(
|
||||
public authService: AuthService,
|
||||
private location: Location,
|
||||
private db: BudgetDatabase,
|
||||
) {
|
||||
const config = {
|
||||
apiKey: 'AIzaSyALYI-ILmLV8NBNXE3DLF9yf1Z5Pp-Y1Mk',
|
||||
|
@ -45,22 +43,6 @@ export class AppComponent {
|
|||
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() {
|
||||
return firebase.auth().currentUser != null;
|
||||
}
|
||||
|
|
|
@ -19,29 +19,34 @@ import {
|
|||
import { AppComponent } from './app.component';
|
||||
import { TransactionsComponent } from './transactions/transactions.component';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { TransactionDetailsComponent } from './transaction-details/transaction-details.component';
|
||||
import { NewTransactionComponent } from './new-transaction/new-transaction.component';
|
||||
import { AddEditTransactionComponent } from './add-edit-transaction/add-edit-transaction.component';
|
||||
import { AccountsComponent } from './accounts/accounts.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';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { CategoriesComponent } from './categories/categories.component';
|
||||
import { CategoryDetailsComponent } from './category-details/category-details.component';
|
||||
import { AddEditCategoryComponent } from './add-edit-category/add-edit-category.component';
|
||||
import { NewCategoryComponent } from './new-category/new-category.component';
|
||||
import { CategoryListComponent } from './category-list/category-list.component';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { RegisterComponent } from './register/register.component';
|
||||
import { AddEditAccountComponent } from './add-edit-account/add-edit-account.component';
|
||||
import { EditProfileComponent } from './edit-profile/edit-profile.component';
|
||||
import { UserComponent } from './user/user.component';
|
||||
import { CategoryDetailsComponent } from './categories/category-details/category-details.component';
|
||||
import { AddEditCategoryComponent } from './categories/add-edit-category/add-edit-category.component';
|
||||
import { NewCategoryComponent } from './categories/new-category/new-category.component';
|
||||
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 { 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 { 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 { TransactionServiceFirebaseFirestoreImpl } from './transaction.service.firestore';
|
||||
import { TRANSACTION_SERVICE } from './transaction.service';
|
||||
import { CATEGORY_SERVICE } from './category.service';
|
||||
import { CategoryServiceFirebaseFirestoreImpl } from './category.service.firestore';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { ACCOUNT_SERVICE } from './accounts/account.service';
|
||||
import { FirestoreAccountService } from './accounts/account.service.firestore';
|
||||
|
||||
export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
||||
align: 'left',
|
||||
|
@ -71,6 +76,9 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
|||
AddEditAccountComponent,
|
||||
EditProfileComponent,
|
||||
UserComponent,
|
||||
NewAccountComponent,
|
||||
AccountDetailsComponent,
|
||||
AccountsComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -96,6 +104,7 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
|||
{ provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig },
|
||||
{ provide: TRANSACTION_SERVICE, useClass: TransactionServiceFirebaseFirestoreImpl },
|
||||
{ provide: CATEGORY_SERVICE, useClass: CategoryServiceFirebaseFirestoreImpl },
|
||||
{ provide: ACCOUNT_SERVICE, useClass: FirestoreAccountService },
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import { Component, OnInit, Input, OnDestroy, Inject } from '@angular/core';
|
||||
import { Category } from '../category';
|
||||
import { Actionable } from '../actionable';
|
||||
import { AppComponent } from '../app.component';
|
||||
import { CATEGORY_SERVICE, CategoryService } from '../category.service';
|
||||
import { Actionable } from 'src/app/actionable';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-edit-category',
|
|
@ -1,10 +1,10 @@
|
|||
import { Component, OnInit, Input, Inject } from '@angular/core';
|
||||
import { CategoryService, CATEGORY_SERVICE } from '../category.service';
|
||||
import { Category } from '../category';
|
||||
import { CategoryService, CATEGORY_SERVICE } from './category.service';
|
||||
import { Category } from './category';
|
||||
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 { TransactionType } from '../transaction.type';
|
||||
import { TransactionType } from '../transactions/transaction.type';
|
||||
|
||||
@Component({
|
||||
selector: 'app-categories',
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { ICategory } from './budget-database';
|
||||
|
||||
export class Category implements ICategory {
|
||||
export class Category {
|
||||
id: string;
|
||||
accountId: string;
|
||||
remoteId: string;
|
|
@ -1,14 +1,18 @@
|
|||
<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">
|
||||
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>
|
||||
<div class="transaction-navigation">
|
||||
<a mat-button routerLink="/transactions">View Transactions</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-categories">
|
||||
<div class="dashboard-categories" *ngIf="isLoggedIn()">
|
||||
<h3 class="categories">Categories</h3>
|
||||
<a mat-button routerLink="/categories" class="view-all" *ngIf="categories">View All</a>
|
||||
<div class="no-categories" *ngIf="!categories || categories.length === 0">
|
||||
|
@ -20,6 +24,6 @@
|
|||
<app-category-list [categories]="categories" [categoryBalances]="categoryBalances"></app-category-list>
|
||||
</div>
|
||||
</div>
|
||||
<a mat-fab routerLink="/transactions/new">
|
||||
<a mat-fab routerLink="/transactions/new" *ngIf="isLoggedIn()">
|
||||
<mat-icon aria-label="Add">add</mat-icon>
|
||||
</a>
|
|
@ -1,11 +1,11 @@
|
|||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { Transaction } from '../transaction';
|
||||
import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service';
|
||||
import { Category } from '../category';
|
||||
import { Transaction } from '../transactions/transaction';
|
||||
import { TransactionService, TRANSACTION_SERVICE } from '../transactions/transaction.service';
|
||||
import { Category } from '../categories/category';
|
||||
import { AppComponent } from '../app.component';
|
||||
import { TransactionType } from '../transaction.type';
|
||||
import { TransactionType } from '../transactions/transaction.type';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CategoryService, CATEGORY_SERVICE } from '../category.service';
|
||||
import { CategoryService, CATEGORY_SERVICE } from '../categories/category.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
|
@ -72,4 +72,8 @@ export class DashboardComponent implements OnInit {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return this.app.isLoggedIn();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@ import { Component, OnInit, Input, OnChanges, OnDestroy, Inject } from '@angular
|
|||
import { Transaction } from '../transaction';
|
||||
import { TransactionType } from '../transaction.type';
|
||||
import { TransactionService, TRANSACTION_SERVICE } from '../transaction.service';
|
||||
import { Category } from '../category';
|
||||
import { AppComponent } from '../app.component';
|
||||
import { Actionable } from '../actionable';
|
||||
import { CATEGORY_SERVICE, CategoryService } from '../category.service';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-edit-transaction',
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Transaction } from '../transaction'
|
||||
import { Transaction } from '../transaction';
|
||||
|
||||
@Component({
|
||||
selector: 'app-new-transaction',
|
|
@ -1,9 +1,8 @@
|
|||
import { ITransaction } from './budget-database';
|
||||
import { Category } from './category';
|
||||
import { Category } from '../categories/category';
|
||||
import { TransactionType } from './transaction.type';
|
||||
import * as firebase from 'firebase/app';
|
||||
|
||||
export class Transaction implements ITransaction {
|
||||
export class Transaction {
|
||||
id: string;
|
||||
accountId: string;
|
||||
remoteId: string;
|
|
@ -1,7 +1,7 @@
|
|||
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 { Transaction } from './transaction';
|
||||
import { TransactionType } from './transaction.type';
|
||||
import { TransactionService, TRANSACTION_SERVICE } from './transaction.service';
|
||||
import { AppComponent } from '../app.component';
|
||||
|
||||
@Component({
|
||||
|
|
0
src/app/users/login/login.component.css
Normal file
0
src/app/users/login/login.component.css
Normal file
|
@ -1,8 +1,8 @@
|
|||
import { Component, OnInit, OnDestroy, OnChanges } from '@angular/core';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { User } from '../user';
|
||||
import { Actionable } from '../actionable';
|
||||
import { AppComponent } from '../app.component';
|
||||
import { Actionable } from 'src/app/actionable';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
0
src/app/users/register/register.component.css
Normal file
0
src/app/users/register/register.component.css
Normal file
|
@ -1,8 +1,8 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { User } from '../user';
|
||||
import { AppComponent } from '../app.component';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { Actionable } from '../actionable';
|
||||
import { Actionable } from 'src/app/actionable';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register',
|
0
src/app/users/user.component.css
Normal file
0
src/app/users/user.component.css
Normal file
15
src/app/users/user.service.spec.ts
Normal file
15
src/app/users/user.service.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
9
src/app/users/user.service.ts
Normal file
9
src/app/users/user.service.ts
Normal 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');
|
Loading…
Reference in a new issue