Finish implementing category add/edit/list features

This commit is contained in:
William Brawner 2018-08-30 17:19:39 -05:00
parent f8b33cb67e
commit b79f754808
29 changed files with 524 additions and 17 deletions

View file

@ -0,0 +1,16 @@
.category-form {
padding: 1em;
color: #F1F1F1;
}
.category-form * {
display: block;
}
mat-radio-button {
padding-bottom: 15px;
}
.button-delete {
float: right;
}

View file

@ -0,0 +1,28 @@
<mat-toolbar>
<span>
<a (click)="goBack()">
<mat-icon>arrow_back</mat-icon>
</a>
</span>
<span>
{{ title }}
</span>
<span class="action-item">
<a (click)="save()">Save</a>
</span>
</mat-toolbar>
<div *ngIf="!currentCategory">
<p>Select a category from the list to view details about it or edit it.</p>
</div>
<div *ngIf="currentCategory" class="category-form">
<mat-form-field>
<input matInput [(ngModel)]="currentCategory.name" placeholder="Name" required>
</mat-form-field>
<mat-form-field>
<input matInput type="number" [(ngModel)]="currentCategory.amount" placeholder="Amount" required>
</mat-form-field>
<mat-form-field>
<input type="color" matInput [(ngModel)]="currentCategory.color" placeholder="Color">
</mat-form-field>
<button class="button-delete" mat-button color="warn" *ngIf="currentCategory.id" (click)="delete()">Delete</button>
</div>

View file

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

View file

@ -0,0 +1,43 @@
import { Component, OnInit, Input } from '@angular/core';
import { CategoryService } from '../category.service'
import { Category } from '../category'
import { Location } from '@angular/common';
@Component({
selector: 'app-add-edit-category',
templateUrl: './add-edit-category.component.html',
styleUrls: ['./add-edit-category.component.css']
})
export class AddEditCategoryComponent implements OnInit {
@Input() title: string;
@Input() currentCategory: Category;
constructor(
private categoryService: CategoryService,
private location: Location
) { }
ngOnInit() {
}
goBack(): void {
this.location.back()
}
save(): void {
if (this.currentCategory.id) {
// This is an existing category, update it
this.categoryService.updateCategory(this.currentCategory);
} else {
// This is a new category, save it
this.categoryService.saveCategory(this.currentCategory);
}
this.goBack()
}
delete(): void {
this.categoryService.deleteCategory(this.currentCategory);
this.goBack()
}
}

View file

@ -4,12 +4,18 @@ 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 './transaction-details/transaction-details.component';
import { NewTransactionComponent } from './new-transaction/new-transaction.component'; import { NewTransactionComponent } from './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';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: DashboardComponent }, { path: '', component: DashboardComponent },
{ 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 },
{ path: 'categories', component: CategoriesComponent },
{ path: 'categories/new', component: NewCategoryComponent },
{ path: 'categories/:id', component: CategoryDetailsComponent },
] ]
@NgModule({ @NgModule({

View file

@ -10,6 +10,7 @@ import {
MatInputModule, MatInputModule,
MatListModule, MatListModule,
MatRadioModule, MatRadioModule,
MatProgressBarModule,
MatToolbarModule, MatToolbarModule,
} from '@angular/material'; } from '@angular/material';
@ -20,6 +21,11 @@ import { TransactionDetailsComponent } from './transaction-details/transaction-d
import { NewTransactionComponent } from './new-transaction/new-transaction.component'; import { NewTransactionComponent } from './new-transaction/new-transaction.component';
import { AddEditTransactionComponent } from './add-edit-transaction/add-edit-transaction.component'; import { AddEditTransactionComponent } from './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 { 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';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -28,7 +34,12 @@ import { DashboardComponent } from './dashboard/dashboard.component';
TransactionDetailsComponent, TransactionDetailsComponent,
NewTransactionComponent, NewTransactionComponent,
AddEditTransactionComponent, AddEditTransactionComponent,
DashboardComponent DashboardComponent,
CategoriesComponent,
CategoryDetailsComponent,
AddEditCategoryComponent,
NewCategoryComponent,
CategoryListComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -40,6 +51,7 @@ import { DashboardComponent } from './dashboard/dashboard.component';
MatInputModule, MatInputModule,
MatListModule, MatListModule,
MatRadioModule, MatRadioModule,
MatProgressBarModule,
MatToolbarModule, MatToolbarModule,
AppRoutingModule, AppRoutingModule,
FormsModule FormsModule

View file

@ -1,8 +1,12 @@
import { Injectable } from '@angular/core';
import Dexie from 'dexie'; import Dexie from 'dexie';
import { TransactionType } from './transaction.type'; import { TransactionType } from './transaction.type';
import { Category } from './category' import { Category } from './category'
import { Transaction } from './transaction' import { Transaction } from './transaction'
@Injectable({
providedIn: 'root'
})
export class BudgetDatabase extends Dexie { export class BudgetDatabase extends Dexie {
transactions: Dexie.Table<ITransaction, number>; transactions: Dexie.Table<ITransaction, number>;
categories: Dexie.Table<ICategory, number>; categories: Dexie.Table<ICategory, number>;

View file

@ -0,0 +1,14 @@
<mat-toolbar>
<span>
<a (click)="goBack()">
<mat-icon>arrow_back</mat-icon>
</a>
</span>
<span>Categories</span>
<!-- empty span object for spacing -->
<span></span>
</mat-toolbar>
<app-category-list [categories]="categories" [categoryBalances]="categoryBalances"></app-category-list>
<a mat-fab routerLink="/categories/new">
<mat-icon aria-label="Add">add</mat-icon>
</a>

View file

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

View file

@ -0,0 +1,38 @@
import { Component, OnInit } from '@angular/core';
import { CategoryService } from '../category.service'
import { Category } from '../category'
import { Location } from '@angular/common';
@Component({
selector: 'app-categories',
templateUrl: './categories.component.html',
styleUrls: ['./categories.component.css']
})
export class CategoriesComponent implements OnInit {
public categories: Category[];
private categoryBalances: Map<number, number>;
constructor(
private categoryService: CategoryService,
private location: Location
) { }
ngOnInit() {
this.getCategories();
this.categoryBalances = new Map();
}
getCategories(): void {
this.categoryService.getCategories().subscribe(categories => {
this.categories = categories
for (let category of this.categories) {
this.categoryService.getBalance(category).subscribe(balance => this.categoryBalances.set(category.id, balance))
}
})
}
goBack(): void {
this.location.back()
}
}

View file

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

View file

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

View file

@ -0,0 +1,28 @@
import { Component, OnInit } from '@angular/core';
import { CategoryService } from '../category.service'
import { Category } from '../category'
import { ActivatedRoute } from '@angular/router'
@Component({
selector: 'app-category-details',
templateUrl: './category-details.component.html',
styleUrls: ['./category-details.component.css']
})
export class CategoryDetailsComponent implements OnInit {
category: Category;
constructor(
private route: ActivatedRoute,
private categoryService: CategoryService
) { }
ngOnInit() {
this.getCategory()
}
getCategory(): void {
const id = +this.route.snapshot.paramMap.get('id')
this.categoryService.getCategory(id)
.subscribe(category => this.category = category)
}

View file

@ -0,0 +1,8 @@
.categories mat-progress-bar.mat-progress-bar {
background-color: #BDBDBD;
margin-top: 0.5em;
}
::ng-deep .mat-progress-bar-buffer {
background-color: #BDBDBD;
}

View file

@ -0,0 +1,16 @@
<style>
.categories mat-progress-bar div.mat-progress-bar-element.mat-progress-bar-buffer {
background-color: #BDBDBD !important;
}
</style>
<mat-nav-list class="categories">
<a mat-list-item *ngFor="let category of categories" routerLink="/categories/{{ category.id }}">
<p matLine>{{category.name}}</p>
<style>
::ng-deep .mat-progress-bar-fill::after {
background-color: "{{ category.color }}";
}
</style>
<mat-progress-bar matLine mode="determinate" value="{{ getCategoryCompletion(category) }}"></mat-progress-bar>
</a>
</mat-nav-list>

View file

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

View file

@ -0,0 +1,31 @@
import { Component, OnInit, Input } from '@angular/core';
import { Category } from '../category'
@Component({
selector: 'app-category-list',
templateUrl: './category-list.component.html',
styleUrls: ['./category-list.component.css']
})
export class CategoryListComponent implements OnInit {
@Input() categories: Category[];
@Input() categoryBalances: Map<number, number>;
constructor() { }
ngOnInit() {
}
getCategoryCompletion(category: Category): number {
if (category.amount <= 0) {
return 0;
}
let categoryBalance = this.categoryBalances.get(category.id)
if (!categoryBalance) {
categoryBalance = 0
}
return categoryBalance / category.amount;
}
}

View file

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

View file

@ -0,0 +1,54 @@
import { Injectable } from '@angular/core';
import { of, Observable, from } from 'rxjs';
import { BudgetDatabase } from './budget-database';
import { TransactionType } from './transaction.type'
import { Category } from './category'
@Injectable({
providedIn: 'root'
})
export class CategoryService {
constructor(private db: BudgetDatabase) { }
getCategories(count?: number): Observable<Category[]> {
if (count) {
return from(this.db.categories.toCollection().limit(count).toArray())
} else {
return from(this.db.categories.toCollection().toArray())
}
}
getCategory(id: number): Observable<Category> {
return from(this.db.categories.where('id').equals(id).first())
}
saveCategory(category: Category): Observable<Category> {
this.db.categories.put(category)
return of(category)
}
updateCategory(category: Category): Observable<any> {
this.db.categories.update(category.id, category)
return of([])
}
deleteCategory(category: Category): Observable<any> {
return from(this.db.categories.delete(category.id))
}
getBalance(category: Category): Observable<number> {
let sum = 0;
return from(
this.db.transactions.filter(transaction => transaction.category === category).each(function(transaction) {
if (transaction.type === TransactionType.INCOME) {
sum += transaction.amount
} else {
sum -= transaction.amount
}
}).then(function() {
return sum;
})
)
}
}

View file

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

View file

@ -10,6 +10,17 @@
<h2 class="balance"> <h2 class="balance">
Current Balance: <span [ngClass]="{'income': balance > 0, 'expense': balance < 0}" >{{ balance | currency }}</span> Current Balance: <span [ngClass]="{'income': balance > 0, 'expense': balance < 0}" >{{ balance | currency }}</span>
</h2> </h2>
<a routerLink="/transactions">View Transactions >></a> <a routerLink="/transactions">View Transactions &gt;&gt;</a>
</div>
<div class="dashboard-categories">
<h3 class="categories">Categories</h3>
<a routerLink="/categories" class="view-all" *ngIf="categories">View All</a>
<div class="no-categories" *ngIf="!categories">
<a 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>
</div> </div>

View file

@ -1,6 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Transaction } from '../transaction' import { Transaction } from '../transaction'
import { TransactionService } from '../transaction.service' import { TransactionService } from '../transaction.service'
import { CategoryService } from '../category.service'
import { Category } from '../category'
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
@ -11,14 +13,19 @@ export class DashboardComponent implements OnInit {
public balance: number; public balance: number;
public transactions: Transaction[]; public transactions: Transaction[];
public categories: Category[];
categoryBalances: Map<number, number>;
constructor( constructor(
private transactionService: TransactionService private transactionService: TransactionService,
private categoryService: CategoryService
) { } ) { }
ngOnInit() { ngOnInit() {
this.getBalance(); this.getBalance();
this.getTransactions(); this.getTransactions();
this.getCategories();
this.categoryBalances = new Map();
} }
getBalance(): void { getBalance(): void {
@ -26,6 +33,10 @@ export class DashboardComponent implements OnInit {
} }
getTransactions(): void { getTransactions(): void {
this.transactionService.getTransactions().subscribe(transactions => this.transactions = transactions) this.transactionService.getTransactions(5).subscribe(transactions => this.transactions = transactions)
}
getCategories(): void {
this.categoryService.getCategories(5).subscribe(categories => this.categories = categories)
} }
} }

View file

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

View file

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

View file

@ -0,0 +1,21 @@
import { Component, OnInit } from '@angular/core';
import { Category } from '../category'
@Component({
selector: 'app-new-category',
templateUrl: './new-category.component.html',
styleUrls: ['./new-category.component.css']
})
export class NewCategoryComponent implements OnInit {
category: Category;
constructor() { }
ngOnInit() {
this.category = new Category();
// TODO: Set random color for category, improve color picker
//this.category.color =
}
}

View file

@ -9,11 +9,7 @@ import { BudgetDatabase } from './budget-database';
}) })
export class TransactionService { export class TransactionService {
db: BudgetDatabase; constructor(private db: BudgetDatabase) { }
constructor() {
this.db = new BudgetDatabase();
}
getTransactions(count?: number): Observable<Transaction[]> { getTransactions(count?: number): Observable<Transaction[]> {
if (count) { if (count) {
@ -42,7 +38,6 @@ export class TransactionService {
} }
getBalance(): Observable<number> { getBalance(): Observable<number> {
console.log("Getting balance")
let sum = 0; let sum = 0;
return from( return from(
this.db.transactions.each(function(transaction) { this.db.transactions.each(function(transaction) {
@ -56,4 +51,5 @@ export class TransactionService {
}) })
) )
} }
} }