Finish implementing new accounts structure

Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
Billy Brawner 2019-05-04 18:29:21 -07:00
parent 7c2e58cb11
commit 8d843473e2
39 changed files with 370 additions and 343 deletions

View file

@ -0,0 +1,68 @@
.dashboard {
color: #F1F1F1;
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 1em;
}
.dashboard > div {
background: #212121;
display: inline-block;
margin: 1em;
padding: 1em;
max-width: 500px;
position: relative;
width: 100%;
}
.dashboard .dashboard-primary {
padding: 5em 1em;
text-align: center;
}
.dashboard-primary > * {
display: block;
}
.dashboard div h2, .dashboard div h3 {
margin: 0;
}
.dashboard p, .dashboard a {
color: #F1F1F1;
text-align: center;
text-decoration: none;
}
.dashboard-primary div {
bottom: 0.5em;
display: flex;
justify-content: flex-end;
left: 0.5em;
right: 0.5em;
position: absolute;
}
.dashboard .no-categories {
padding: 1em;
text-align: center;
}
.dashboard .no-categories a {
display: inline-block;
border: 1px dashed #F1F1F1;
padding: 1em;
}
.dashboard .no-categories p {
line-height: normal;
white-space: normal;
}
a.view-all {
position: absolute;
right: 0.5em;
top: 0.5em;
}

View file

@ -1,3 +1,26 @@
<p>
account-details works!
</p>
<div class="dashboard">
<div class="dashboard-primary" [hidden]="!account">
<h2 class="balance">
Current Balance: <br />
<span
[ngClass]="{'income': getBalance() > 0, 'expense': getBalance() < 0}">{{ getBalance() / 100 | currency }}</span>
</h2>
<div class="transaction-navigation">
<a mat-button routerLink="/accounts/{{ account.id }}/transactions">View Transactions</a>
</div>
</div>
<div class="dashboard-categories" [hidden]="!account">
<h3 class="categories">Categories</h3>
<a mat-button routerLink="/accounts/{{ account.id }}/categories" class="view-all" *ngIf="categories">View All</a>
<div class="no-categories" *ngIf="!categories || categories.length === 0">
<a mat-button routerLink="/accounts/{{ account.id }}/categories/new">
<mat-icon>add</mat-icon>
<p>Add categories to get more insights into your income and expenses.</p>
</a>
</div>
<app-category-list [accountId]="account.id" [categories]="categories" [categoryBalances]="categoryBalances"></app-category-list>
</div>
</div>
<a mat-fab routerLink="/accounts/{{ account.id }}/transactions/new" [hidden]="!account">
<mat-icon aria-label="Add">add</mat-icon>
</a>

View file

@ -1,4 +1,14 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, Inject } from '@angular/core';
import { Account } from '../account';
import { ACCOUNT_SERVICE, AccountService } from '../account.service';
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';
@Component({
selector: 'app-account-details',
@ -7,9 +17,76 @@ import { Component, OnInit } from '@angular/core';
})
export class AccountDetailsComponent implements OnInit {
constructor() { }
account: Account;
public transactions: Transaction[];
public categories: Category[];
categoryBalances: Map<string, number>;
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,
) { }
ngOnInit() {
this.getAccount();
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;
this.getBalance();
this.getTransactions();
this.getCategories();
});
}
getBalance(): number {
let totalBalance = 0;
if (!this.categoryBalances) {
return 0;
}
this.categoryBalances.forEach(balance => {
totalBalance += balance;
});
return totalBalance;
}
getTransactions(): void {
this.transactionService.getTransactions(this.account.id, null, 5)
.subscribe(transactions => this.transactions = <Transaction[]>transactions);
}
getCategories(): void {
this.categoryService.getCategories(this.account.id, 5).subscribe(categories => {
this.categories = categories;
for (const category of categories) {
this.getCategoryBalance(category.id).subscribe(balance => this.categoryBalances.set(category.id, balance));
}
});
}
getCategoryBalance(category: string): Observable<number> {
return Observable.create(subscriber => {
this.transactionService.getTransactions(this.account.id, category).subscribe(transactions => {
let balance = 0;
for (const transaction of transactions) {
if (transaction.type === TransactionType.INCOME) {
balance += transaction.amount;
} else {
balance -= transaction.amount;
}
}
subscriber.next(balance);
});
});
}
}

View file

@ -9,11 +9,21 @@ 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);
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);
});
});
});
}
@ -33,13 +43,14 @@ export class FirestoreAccountService implements AccountService {
name: string,
description: string,
currency: string,
members: User[],
members: string[],
): Observable<Account> {
return Observable.create(subscriber => {
firebase.firestore().collection('accounts').add({
name: name,
description: description,
members: members.map(member => member.id)
currency: currency,
members: members
}).then(docRef => {
docRef.get().then(snapshot => {
if (!snapshot) {

View file

@ -10,7 +10,7 @@ export interface AccountService {
name: string,
description: string,
currency: string,
members: User[],
members: string[],
): Observable<Account>;
updateAccount(id: string, changes: object): Observable<Account>;
deleteAccount(id: string): Observable<boolean>;

View file

@ -1,4 +1,8 @@
<mat-nav-list class="accounts">
<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 }}
@ -14,3 +18,6 @@
<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

@ -20,9 +20,13 @@ export class AccountsComponent implements OnInit {
ngOnInit() {
this.app.backEnabled = true;
this.app.title = 'Transactions';
this.app.title = 'Accounts';
this.accountService.getAccounts().subscribe(accounts => {
this.accounts = accounts;
});
}
isLoggedIn(): boolean {
return this.app.isLoggedIn();
}
}

View file

@ -5,6 +5,7 @@ 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';
@Component({
selector: 'app-add-edit-account',
@ -14,8 +15,8 @@ import { User } from 'src/app/users/user';
export class AddEditAccountComponent implements OnInit, OnDestroy, Actionable {
@Input() title: string;
@Input() account: Account;
public users: User[];
public searchedUsers: User[];
public userIds: string[] = [firebase.auth().currentUser.uid];
public searchedUsers: User[] = [];
constructor(
private app: AppComponent,
@ -45,7 +46,7 @@ export class AddEditAccountComponent implements OnInit, OnDestroy, Actionable {
this.account.name,
this.account.description,
this.account.currency,
this.users
this.userIds
);
}
// TODO: Check if it was actually successful or not

View file

@ -1,6 +1,5 @@
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 './transactions/transaction-details/transaction-details.component';
import { NewTransactionComponent } from './transactions/new-transaction/new-transaction.component';
@ -14,18 +13,18 @@ import { NewAccountComponent } from './accounts/new-account/new-account.componen
import { AccountDetailsComponent } from './accounts/account-details/account-details.component';
const routes: Routes = [
{ path: '', component: DashboardComponent },
{ path: '', component: AccountsComponent },
{ 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 },
{ path: 'categories', component: CategoriesComponent },
{ path: 'categories/new', component: NewCategoryComponent },
{ path: 'categories/:id', component: CategoryDetailsComponent },
{ 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 },
];
@NgModule({

View file

@ -2,8 +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">Manage Accounts</a>
<a mat-list-item *ngIf="isLoggedIn()" (click)="exportData()">Export Data</a>
<a mat-list-item *ngIf="isLoggedIn()" routerLink="/accounts">Accounts</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

@ -14,7 +14,6 @@ export class AppComponent {
public title = 'Budget';
public backEnabled = false;
public actionable: Actionable;
public group = 'MG3KOiuPu0Xy38O2LdhJ';
constructor(
public authService: AuthService,

View file

@ -23,7 +23,6 @@ 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 './categories/category-details/category-details.component';
import { AddEditCategoryComponent } from './categories/add-edit-category/add-edit-category.component';
@ -47,6 +46,8 @@ import { CurrencyMaskConfig, CURRENCY_MASK_CONFIG } from 'ng2-currency-mask/src/
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';
export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
align: 'left',
@ -65,7 +66,6 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
TransactionDetailsComponent,
NewTransactionComponent,
AddEditTransactionComponent,
DashboardComponent,
CategoriesComponent,
CategoryDetailsComponent,
AddEditCategoryComponent,
@ -105,6 +105,7 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
{ provide: TRANSACTION_SERVICE, useClass: TransactionServiceFirebaseFirestoreImpl },
{ provide: CATEGORY_SERVICE, useClass: CategoryServiceFirebaseFirestoreImpl },
{ provide: ACCOUNT_SERVICE, useClass: FirestoreAccountService },
{ provide: USER_SERVICE, useClass: FirestoreUserService },
],
bootstrap: [AppComponent]
})

View file

@ -3,6 +3,8 @@ 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';
@Component({
selector: 'app-add-edit-category',
@ -11,12 +13,13 @@ import { AppComponent } from 'src/app/app.component';
})
export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
@Input() accountId: string;
@Input() title: string;
@Input() currentCategory: Category;
@Input() group: string;
constructor(
private app: AppComponent,
@Inject(ACCOUNT_SERVICE) private accountService: AccountService,
@Inject(CATEGORY_SERVICE) private categoryService: CategoryService,
) { }
@ -31,14 +34,24 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
}
doAction(): void {
this.currentCategory.amount *= 100;
let observable;
if (this.currentCategory.id) {
// This is an existing category, update it
observable = this.categoryService.updateCategory(this.currentCategory.id, this.currentCategory);
observable = this.categoryService.updateCategory(
this.accountId,
this.currentCategory.id,
{
name: this.currentCategory.name,
amount: this.currentCategory.amount * 100
}
);
} else {
// This is a new category, save it
observable = this.categoryService.createCategory(this.currentCategory.name, this.currentCategory.amount, this.app.group);
observable = this.categoryService.createCategory(
this.accountId,
this.currentCategory.name,
this.currentCategory.amount
);
}
observable.subscribe(val => {
this.app.goBack();
@ -50,7 +63,7 @@ export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
}
delete(): void {
this.categoryService.deleteCategory(this.currentCategory.id);
this.categoryService.deleteCategory(this.accountId, this.currentCategory.id);
this.app.goBack();
}
}

View file

@ -1,4 +1,4 @@
<app-category-list [categories]="categories" [categoryBalances]="categoryBalances"></app-category-list>
<a mat-fab routerLink="/categories/new">
<app-category-list [accountId]="accountId" [categories]="categories" [categoryBalances]="categoryBalances"></app-category-list>
<a mat-fab routerLink="/accounts/{{ accountId }}/categories/new">
<mat-icon aria-label="Add">add</mat-icon>
</a>

View file

@ -5,6 +5,8 @@ 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 { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-categories',
@ -13,17 +15,19 @@ import { TransactionType } from '../transactions/transaction.type';
})
export class CategoriesComponent implements OnInit {
@Input() group: string;
accountId: string;
public categories: Category[];
public categoryBalances: Map<string, number>;
constructor(
private route: ActivatedRoute,
private app: AppComponent,
@Inject(CATEGORY_SERVICE) private categoryService: CategoryService,
@Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
) { }
ngOnInit() {
this.accountId = this.route.snapshot.paramMap.get('accountId');
this.app.title = 'Categories';
this.app.backEnabled = true;
this.getCategories();
@ -31,17 +35,17 @@ export class CategoriesComponent implements OnInit {
}
getCategories(): void {
this.categoryService.getCategories(this.app.group).subscribe(categories => {
this.categoryService.getCategories(this.accountId).subscribe(categories => {
this.categories = categories;
for (const category of this.categories) {
this.getCategoryBalance(category.id).subscribe(balance => this.categoryBalances.set(category.id, balance));
this.getCategoryBalance(category).subscribe(balance => this.categoryBalances.set(category.id, balance));
}
});
}
getCategoryBalance(category: string): Observable<number> {
getCategoryBalance(category: Category): Observable<number> {
return Observable.create(subscriber => {
this.transactionService.getTransactionsForCategory(category).subscribe(transactions => {
this.transactionService.getTransactions(this.accountId, category.id).subscribe(transactions => {
let balance = 0;
for (const transaction of transactions) {
if (transaction.type === TransactionType.INCOME) {

View file

@ -1 +1 @@
<app-add-edit-category [title]="'Edit Category'" [currentCategory]="category"></app-add-edit-category>
<app-add-edit-category [title]="'Edit Category'" [accountId]="accountId" [currentCategory]="category"></app-add-edit-category>

View file

@ -1,7 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, Input } from '@angular/core';
import { CategoryServiceFirebaseFirestoreImpl } from '../category.service.firestore';
import { Category } from '../category';
import { ActivatedRoute } from '@angular/router';
import { Account } from 'src/app/accounts/account';
@Component({
selector: 'app-category-details',
@ -10,6 +11,7 @@ import { ActivatedRoute } from '@angular/router';
})
export class CategoryDetailsComponent implements OnInit {
accountId: string;
category: Category;
constructor(
@ -22,8 +24,9 @@ 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(id)
this.categoryService.getCategory(this.accountId, id)
.subscribe(category => {
category.amount /= 100;
this.category = category;

View file

@ -4,7 +4,7 @@
}
</style>
<mat-nav-list class="categories">
<a mat-list-item *ngFor="let category of categories" routerLink="/categories/{{ category.id }}">
<a mat-list-item *ngFor="let category of categories" routerLink="/accounts/{{ accountId }}/categories/{{ category.id }}">
<p matLine class="category-list-title">
<span>
{{ category.name }}

View file

@ -1,5 +1,6 @@
import { Component, OnInit, Input } from '@angular/core';
import { Category } from '../category';
import { Account } from 'src/app/accounts/account';
@Component({
selector: 'app-category-list',
@ -8,6 +9,7 @@ import { Category } from '../category';
})
export class CategoryListComponent implements OnInit {
@Input() accountId: string;
@Input() categories: Category[];
@Input() categoryBalances: Map<string, number>;

View file

@ -3,6 +3,7 @@ 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'
@ -11,9 +12,9 @@ export class CategoryServiceFirebaseFirestoreImpl {
constructor() { }
getCategories(group: string, count?: number): Observable<Category[]> {
getCategories(accountId: string, count?: number): Observable<Category[]> {
return Observable.create(subscriber => {
let query = firebase.firestore().collection('categories').where('group', '==', group);
let query: any = firebase.firestore().collection('accounts').doc(accountId).collection('categories');
if (count) {
query = query.limit(count);
}
@ -26,7 +27,7 @@ export class CategoryServiceFirebaseFirestoreImpl {
const categories = [];
for (const categoryDoc of snapshot.docs) {
categories.push(Category.fromSnapshotRef(categoryDoc));
categories.push(Category.fromSnapshotRef(accountId, categoryDoc));
}
subscriber.next(categories);
}, error => {
@ -36,24 +37,23 @@ export class CategoryServiceFirebaseFirestoreImpl {
});
}
getCategory(id: string): Observable<Category> {
getCategory(accountId: string, id: string): Observable<Category> {
return Observable.create(subscriber => {
firebase.firestore().collection('categories').doc(id).onSnapshot(snapshot => {
firebase.firestore().collection('accounts').doc(accountId).collection('categories').doc(id).onSnapshot(snapshot => {
if (!snapshot.exists) {
return;
}
subscriber.next(Category.fromSnapshotRef(snapshot));
subscriber.next(Category.fromSnapshotRef(accountId, snapshot));
});
});
}
createCategory(name: string, amount: number, group: string): Observable<Category> {
createCategory(accountId: string, name: string, amount: number): Observable<Category> {
return Observable.create(subscriber => {
firebase.firestore().collection('categories').add({
firebase.firestore().collection('accounts').doc(accountId).collection('categories').add({
name: name,
amount: amount,
group: group,
amount: amount
}).then(docRef => {
if (!docRef) {
console.error('Failed to create category');
@ -64,32 +64,34 @@ export class CategoryServiceFirebaseFirestoreImpl {
subscriber.error('Unable to retrieve saved transaction data');
return;
}
subscriber.next(Category.fromSnapshotRef(snapshot));
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(id: string, changes: object): Observable<boolean> {
updateCategory(accountId: string, id: string, changes: object): Observable<boolean> {
return Observable.create(subscriber => {
firebase.firestore().collection('categories').doc(id).onSnapshot(snapshot => {
if (!snapshot.exists) {
return;
}
subscriber.next(Category.fromSnapshotRef(snapshot));
});
firebase.firestore().collection('accounts').doc(accountId).collection('categories').doc(id)
.update(changes)
.then(function () {
subscriber.next(true);
})
.catch(function () {
subscriber.next(false);
});
});
}
deleteCategory(id: string): Observable<boolean> {
deleteCategory(accountId: string, id: string): Observable<boolean> {
return Observable.create(subscriber => {
firebase.firestore().collection('categories').doc(id).delete().then(result => {
firebase.firestore().collection('accounts').doc(accountId).collection('categories').doc(id).delete().then(result => {
subscriber.next(true);
}).catch(err => {
console.error(err);

View file

@ -1,18 +1,19 @@
import { Observable } from 'rxjs';
import { Category } from './category';
import { InjectionToken } from '@angular/core';
import { Account } from '../accounts/account';
export interface CategoryService {
getCategories(group: string, count?: number): Observable<Category[]>;
getCategories(accountId: string, count?: number): Observable<Category[]>;
getCategory(id: string): Observable<Category>;
getCategory(accountId: string, id: string): Observable<Category>;
createCategory(name: string, amount: number, group: string): Observable<Category>;
createCategory(accountId: string, name: string, amount: number): Observable<Category>;
updateCategory(id: string, changes: object): Observable<boolean>;
updateCategory(accountId: string, id: string, changes: object): Observable<boolean>;
deleteCategory(id: string): Observable<boolean>;
deleteCategory(accountId: string, id: string): Observable<boolean>;
}
export let CATEGORY_SERVICE = new InjectionToken<CategoryService>('category.service');

View file

@ -1,18 +1,17 @@
export class Category {
id: string;
accountId: string;
remoteId: string;
name: string;
amount: number;
repeat: string;
color: string;
accountId: string;
static fromSnapshotRef(snapshot: firebase.firestore.DocumentSnapshot): Category {
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');
category.accountId = snapshot.get('group');
category.accountId = accountId;
return category;
}
}

View file

@ -1 +1 @@
<app-add-edit-category [title]="'Add Category'" [currentCategory]="category"></app-add-edit-category>
<app-add-edit-category [title]="'Add Category'" [accountId]="accountId" [currentCategory]="category"></app-add-edit-category>

View file

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Category } from '../category'
import { Category } from '../category';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-new-category',
@ -8,14 +9,18 @@ import { Category } from '../category'
})
export class NewCategoryComponent implements OnInit {
accountId: string;
category: Category;
constructor() { }
constructor(
private route: ActivatedRoute
) { }
ngOnInit() {
this.accountId = this.route.snapshot.paramMap.get('accountId');
this.category = new Category();
// TODO: Set random color for category, improve color picker
//this.category.color =
// this.category.color =
}
}

View file

@ -1,67 +0,0 @@
.dashboard {
color: #F1F1F1;
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 1em;
}
.dashboard > div {
background: #212121;
display: inline-block;
margin: 1em;
padding: 1em;
max-width: 500px;
position: relative;
width: 100%;
}
.dashboard .dashboard-primary {
padding: 5em 1em;
text-align: center;
}
.dashboard-primary > * {
display: block;
}
.dashboard div h2, .dashboard div h3 {
margin: 0;
}
.dashboard p, .dashboard a {
color: #F1F1F1;
text-align: center;
text-decoration: none;
}
.dashboard-primary div {
bottom: 0.5em;
display: flex;
justify-content: flex-end;
left: 0.5em;
right: 0.5em;
position: absolute;
}
.dashboard .no-categories {
padding: 1em;
text-align: center;
}
.dashboard .no-categories a {
display: inline-block;
border: 1px dashed #F1F1F1;
padding: 1em;
}
.dashboard .no-categories p {
line-height: normal;
white-space: normal;
}
a.view-all {
position: absolute;
right: 0.5em;
top: 0.5em;
}

View file

@ -1,29 +0,0 @@
<div class="dashboard">
<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>
</h2>
<div class="transaction-navigation">
<a mat-button routerLink="/transactions">View Transactions</a>
</div>
</div>
<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">
<a mat-button routerLink="/categories/new">
<mat-icon>add</mat-icon>
<p>Add categories to get more insights into your income and expenses.</p>
</a>
</div>
<app-category-list [categories]="categories" [categoryBalances]="categoryBalances"></app-category-list>
</div>
</div>
<a mat-fab routerLink="/transactions/new" *ngIf="isLoggedIn()">
<mat-icon aria-label="Add">add</mat-icon>
</a>

View file

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

View file

@ -1,79 +0,0 @@
import { Component, OnInit, Inject } from '@angular/core';
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 '../transactions/transaction.type';
import { Observable } from 'rxjs';
import { CategoryService, CATEGORY_SERVICE } from '../categories/category.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit {
public transactions: Transaction[];
public categories: Category[];
categoryBalances: Map<string, number>;
constructor(
private app: AppComponent,
@Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
@Inject(CATEGORY_SERVICE) private categoryService: CategoryService,
) { }
ngOnInit() {
this.app.backEnabled = false;
this.app.title = 'My Finances';
this.getBalance();
this.getTransactions();
this.getCategories();
this.categoryBalances = new Map();
}
getBalance(): number {
let totalBalance = 0;
if (!this.categoryBalances) {
return 0;
}
this.categoryBalances.forEach(balance => {
totalBalance += balance;
});
return totalBalance;
}
getTransactions(): void {
this.transactionService.getTransactions(this.app.group, 5).subscribe(transactions => this.transactions = <Transaction[]>transactions);
}
getCategories(): void {
this.categoryService.getCategories(this.app.group, 5).subscribe(categories => {
this.categories = categories;
for (const category of categories) {
this.getCategoryBalance(category.id).subscribe(balance => this.categoryBalances.set(category.id, balance));
}
});
}
getCategoryBalance(category: string): Observable<number> {
return Observable.create(subscriber => {
this.transactionService.getTransactionsForCategory(category).subscribe(transactions => {
let balance = 0;
for (const transaction of transactions) {
if (transaction.type === TransactionType.INCOME) {
balance += transaction.amount;
} else {
balance -= transaction.amount;
}
}
subscriber.next(balance);
});
});
}
isLoggedIn(): boolean {
return this.app.isLoggedIn();
}
}

View file

@ -1,7 +1,7 @@
<div [hidden]="currentTransaction">
<p>Select a transaction from the list to view details about it or edit it.</p>
</div>
<div [hidden]="!currentTransaction" class="form transaction-form">
<div [hidden]="!currentTransaction" *ngIf="currentTransaction" class="form transaction-form">
<mat-form-field>
<input matInput [(ngModel)]="currentTransaction.title" placeholder="Name" required>
</mat-form-field>

View file

@ -6,6 +6,7 @@ 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';
@Component({
selector: 'app-add-edit-transaction',
@ -15,9 +16,8 @@ import { CATEGORY_SERVICE, CategoryService } from 'src/app/categories/category.s
export class AddEditTransactionComponent implements OnInit, OnDestroy, Actionable {
@Input() title: string;
@Input() currentTransaction: Transaction;
@Input() group: string;
@Input() accountId: string;
public transactionType = TransactionType;
public selectedCategory: Category;
public categories: Category[];
public rawAmount: string;
@ -41,17 +41,28 @@ export class AddEditTransactionComponent implements OnInit, OnDestroy, Actionabl
doAction(): void {
// The amount will be input as a decimal value so we need to convert it
// to an integer
this.currentTransaction.amount *= 100;
let observable;
if (this.currentTransaction.id) {
// This is an existing transaction, update it
observable = this.transactionService.updateTransaction(this.currentTransaction.id, this.currentTransaction);
observable = this.transactionService.updateTransaction(
this.accountId,
this.currentTransaction.id,
{
name: this.currentTransaction.title,
description: this.currentTransaction.description,
amount: this.currentTransaction.amount * 100,
date: this.currentTransaction.date,
category: this.currentTransaction.categoryId,
isExpense: this.currentTransaction.type === TransactionType.EXPENSE
}
);
} else {
// This is a new transaction, save it
observable = this.transactionService.createTransaction(
this.accountId,
this.currentTransaction.title,
this.currentTransaction.description,
this.currentTransaction.amount,
this.currentTransaction.amount * 100,
this.currentTransaction.date,
this.currentTransaction.type === TransactionType.EXPENSE,
this.currentTransaction.categoryId,
@ -68,11 +79,12 @@ export class AddEditTransactionComponent implements OnInit, OnDestroy, Actionabl
}
delete(): void {
this.transactionService.deleteTransaction(this.currentTransaction.id);
this.app.goBack();
this.transactionService.deleteTransaction(this.accountId, this.currentTransaction.id).subscribe(() => {
this.app.goBack();
});
}
getCategories() {
this.categoryService.getCategories(this.app.group).subscribe(categories => this.categories = categories);
this.categoryService.getCategories(this.accountId).subscribe(categories => this.categories = categories);
}
}

View file

@ -1 +1 @@
<app-add-edit-transaction [title]="'Add Transaction'" [currentTransaction]="transaction"></app-add-edit-transaction>
<app-add-edit-transaction [accountId]="accountId" [title]="'Add Transaction'" [currentTransaction]="transaction"></app-add-edit-transaction>

View file

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Transaction } from '../transaction';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-new-transaction',
@ -8,12 +9,16 @@ import { Transaction } from '../transaction';
})
export class NewTransactionComponent implements OnInit {
accountId: string;
transaction: Transaction;
constructor() { }
constructor(
private route: ActivatedRoute
) { }
ngOnInit() {
this.transaction = new Transaction()
this.accountId = this.route.snapshot.paramMap.get('accountId');
this.transaction = new Transaction();
}
}

View file

@ -1 +1 @@
<app-add-edit-transaction [title]="'Edit Transaction'" [currentTransaction]="transaction"></app-add-edit-transaction>
<app-add-edit-transaction [accountId]="accountId" [title]="'Edit Transaction'" [currentTransaction]="transaction"></app-add-edit-transaction>

View file

@ -2,6 +2,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';
@Component({
selector: 'app-transaction-details',
@ -10,6 +11,7 @@ import { Transaction } from '../transaction';
})
export class TransactionDetailsComponent implements OnInit {
accountId: string;
transaction: Transaction;
constructor(
@ -22,8 +24,9 @@ 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(id)
this.transactionService.getTransaction(this.accountId, id)
.subscribe(transaction => {
transaction.amount /= 100;
this.transaction = transaction;

View file

@ -1,5 +1,4 @@
import { Injectable } from '@angular/core';
import { Observable, Subscriber } from 'rxjs';
import { Observable } from 'rxjs';
import { Transaction } from './transaction';
import * as firebase from 'firebase/app';
import 'firebase/firestore';
@ -9,47 +8,33 @@ export class TransactionServiceFirebaseFirestoreImpl implements TransactionServi
constructor() { }
getTransactionsForCategory(category: string): Observable<Transaction[]> {
const transactionsQuery = firebase.firestore().collection('transactions').where('category', '==', category);
getTransactions(accountId: string, category?: string, count?: number): Observable<Transaction[]> {
return Observable.create(subscriber => {
const transactions = [];
transactionsQuery.onSnapshot(data => {
if (!data.empty) {
data.docs.map(transaction => transactions.push(Transaction.fromSnapshotRef(transaction)));
}
subscriber.next(transactions);
});
});
}
getTransactions(group: string, count?: number): Observable<Transaction[]> {
const categoriesQuery = firebase.firestore().collection('categories').where('group', '==', group);
return Observable.create(subscriber => {
categoriesQuery.onSnapshot(querySnapshot => {
if (querySnapshot.empty) {
subscriber.error(`Unable to query categories within group ${group}`);
let transactionQuery: any = firebase.firestore().collection('accounts').doc(accountId).collection('transactions');
if (category) {
transactionQuery = transactionQuery.where('category', '==', category);
}
if (count) {
transactionQuery = transactionQuery.limit(count);
}
transactionQuery.onSnapshot(snapshot => {
if (snapshot.empty) {
subscriber.error(`Unable to query transactions within account ${accountId}`);
return;
}
const transactions = [];
querySnapshot.docs.map(categoryDoc => {
firebase.firestore().collection('transactions').where('category', '==', categoryDoc.id).get().then(results => {
if (results.empty) {
return;
}
for (const transactionDoc of results.docs) {
transactions.push(Transaction.fromSnapshotRef(transactionDoc));
}
});
snapshot.docs.forEach(transaction => {
transactions.push(Transaction.fromSnapshotRef(transaction));
});
subscriber.next(transactions);
});
});
}
getTransaction(id: string): Observable<Transaction> {
getTransaction(accountId: string, id: string): Observable<Transaction> {
return Observable.create(subscriber => {
firebase.firestore().collection('transactions').doc(id).onSnapshot(snapshot => {
firebase.firestore().collection('accounts').doc(accountId).collection('transactions').doc(id).onSnapshot(snapshot => {
if (!snapshot.exists) {
return;
}
@ -59,6 +44,7 @@ export class TransactionServiceFirebaseFirestoreImpl implements TransactionServi
}
createTransaction(
accountId: string,
name: string,
description: string,
amount: number,
@ -67,7 +53,7 @@ export class TransactionServiceFirebaseFirestoreImpl implements TransactionServi
category: string
): Observable<Transaction> {
return Observable.create(subscriber => {
firebase.firestore().collection('transactions').add({
firebase.firestore().collection('accounts').doc(accountId).collection('transactions').add({
name: name,
description: description,
date: date,
@ -89,9 +75,9 @@ export class TransactionServiceFirebaseFirestoreImpl implements TransactionServi
}
updateTransaction(id: string, changes: object): Observable<boolean> {
updateTransaction(accountId: string, id: string, changes: object): Observable<boolean> {
return Observable.create(subscriber => {
firebase.firestore().collection('transactions').doc(id).update(changes).then(result => {
firebase.firestore().collection('accounts').doc(accountId).collection('transactions').doc(id).update(changes).then(() => {
subscriber.next(true);
}).catch(err => {
subscriber.next(false);
@ -99,9 +85,9 @@ export class TransactionServiceFirebaseFirestoreImpl implements TransactionServi
});
}
deleteTransaction(id: string): Observable<boolean> {
deleteTransaction(accountId: string, id: string): Observable<boolean> {
return Observable.create(subscriber => {
firebase.firestore().collection('transactions').doc(id).delete().then(data => {
firebase.firestore().collection('accounts').doc(accountId).collection('transactions').doc(id).delete().then(data => {
subscriber.next(true);
}).catch(err => {
console.log(err);

View file

@ -1,16 +1,16 @@
import { Observable } from 'rxjs';
import { Transaction } from './transaction';
import { InjectionToken } from '@angular/core';
import { Account } from '../accounts/account';
export interface TransactionService {
getTransactions(group: string, count?: number): Observable<Transaction[]>;
getTransactions(accountId: string, categoryId?: string, count?: number): Observable<Transaction[]>;
getTransactionsForCategory(category: string, count?: number): Observable<Transaction[]>;
getTransaction(id: string): Observable<Transaction>;
getTransaction(accountId: string, id: string): Observable<Transaction>;
createTransaction(
accountId: string,
name: string,
description: string,
amount: number,
@ -19,9 +19,9 @@ export interface TransactionService {
category: string
): Observable<Transaction>;
updateTransaction(id: string, changes: object): Observable<boolean>;
updateTransaction(accountId: string, id: string, changes: object): Observable<boolean>;
deleteTransaction(id: string): Observable<boolean>;
deleteTransaction(accountId: string, id: string): Observable<boolean>;
}
export let TRANSACTION_SERVICE = new InjectionToken<TransactionService>('transaction.service');

View file

@ -5,9 +5,8 @@ import * as firebase from 'firebase/app';
export class Transaction {
id: string;
accountId: string;
remoteId: string;
title: string;
description: string;
description: string = null;
amount: number;
date: Date = new Date();
categoryId: string;

View file

@ -1,5 +1,5 @@
<mat-nav-list *ngIf="transactions" class="transactions">
<a mat-list-item *ngFor="let transaction of transactions" routerLink="/transactions/{{ transaction.id }}">
<a mat-list-item *ngFor="let transaction of transactions" routerLink="/accounts/{{ accountId }}/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="/transactions/new">
<a mat-fab routerLink="/accounts/{{ accountId }}/transactions/new">
<mat-icon aria-label="Add">add</mat-icon>
</a>

View file

@ -3,6 +3,8 @@ 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 { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-transactions',
@ -11,24 +13,26 @@ import { AppComponent } from '../app.component';
})
export class TransactionsComponent implements OnInit {
@Input() group: string;
accountId: string;
public transactionType = TransactionType;
public transactions: Transaction[];
constructor(
private route: ActivatedRoute,
private app: AppComponent,
@Inject(TRANSACTION_SERVICE) private transactionService: TransactionService,
) { }
ngOnInit() {
this.accountId = this.route.snapshot.paramMap.get('accountId');
this.app.backEnabled = true;
this.app.title = 'Transactions';
this.getTransactions();
}
getTransactions(): void {
this.transactionService.getTransactions(this.app.group).subscribe(transactions => {
this.transactionService.getTransactions(this.accountId).subscribe(transactions => {
this.transactions = transactions;
});
}