From b79f7548082360883732eff2b3e5dc3abd201a14 Mon Sep 17 00:00:00 2001 From: Billy Brawner Date: Thu, 30 Aug 2018 17:19:39 -0500 Subject: [PATCH] Finish implementing category add/edit/list features --- .../add-edit-category.component.css | 16 ++++++ .../add-edit-category.component.html | 28 ++++++++++ .../add-edit-category.component.spec.ts | 25 +++++++++ .../add-edit-category.component.ts | 43 +++++++++++++++ src/app/app-routing.module.ts | 6 +++ src/app/app.module.ts | 14 ++++- src/app/budget-database.ts | 4 ++ src/app/categories/categories.component.css | 0 src/app/categories/categories.component.html | 14 +++++ .../categories/categories.component.spec.ts | 25 +++++++++ src/app/categories/categories.component.ts | 38 +++++++++++++ .../category-details.component.css | 0 .../category-details.component.html | 1 + .../category-details.component.spec.ts | 25 +++++++++ .../category-details.component.ts | 28 ++++++++++ .../category-list/category-list.component.css | 8 +++ .../category-list.component.html | 16 ++++++ .../category-list.component.spec.ts | 25 +++++++++ .../category-list/category-list.component.ts | 31 +++++++++++ src/app/category.service.spec.ts | 15 ++++++ src/app/category.service.ts | 54 +++++++++++++++++++ src/app/dashboard/dashboard.component.css | 42 ++++++++++++--- src/app/dashboard/dashboard.component.html | 13 ++++- src/app/dashboard/dashboard.component.ts | 15 +++++- .../new-category/new-category.component.css | 0 .../new-category/new-category.component.html | 1 + .../new-category.component.spec.ts | 25 +++++++++ .../new-category/new-category.component.ts | 21 ++++++++ src/app/transaction.service.ts | 8 +-- 29 files changed, 524 insertions(+), 17 deletions(-) create mode 100644 src/app/add-edit-category/add-edit-category.component.css create mode 100644 src/app/add-edit-category/add-edit-category.component.html create mode 100644 src/app/add-edit-category/add-edit-category.component.spec.ts create mode 100644 src/app/add-edit-category/add-edit-category.component.ts create mode 100644 src/app/categories/categories.component.css create mode 100644 src/app/categories/categories.component.html create mode 100644 src/app/categories/categories.component.spec.ts create mode 100644 src/app/categories/categories.component.ts create mode 100644 src/app/category-details/category-details.component.css create mode 100644 src/app/category-details/category-details.component.html create mode 100644 src/app/category-details/category-details.component.spec.ts create mode 100644 src/app/category-details/category-details.component.ts create mode 100644 src/app/category-list/category-list.component.css create mode 100644 src/app/category-list/category-list.component.html create mode 100644 src/app/category-list/category-list.component.spec.ts create mode 100644 src/app/category-list/category-list.component.ts create mode 100644 src/app/category.service.spec.ts create mode 100644 src/app/category.service.ts create mode 100644 src/app/new-category/new-category.component.css create mode 100644 src/app/new-category/new-category.component.html create mode 100644 src/app/new-category/new-category.component.spec.ts create mode 100644 src/app/new-category/new-category.component.ts diff --git a/src/app/add-edit-category/add-edit-category.component.css b/src/app/add-edit-category/add-edit-category.component.css new file mode 100644 index 0000000..90e6614 --- /dev/null +++ b/src/app/add-edit-category/add-edit-category.component.css @@ -0,0 +1,16 @@ +.category-form { + padding: 1em; + color: #F1F1F1; +} + +.category-form * { + display: block; +} + +mat-radio-button { + padding-bottom: 15px; +} + +.button-delete { + float: right; +} diff --git a/src/app/add-edit-category/add-edit-category.component.html b/src/app/add-edit-category/add-edit-category.component.html new file mode 100644 index 0000000..dccb537 --- /dev/null +++ b/src/app/add-edit-category/add-edit-category.component.html @@ -0,0 +1,28 @@ + + + + arrow_back + + + + {{ title }} + + + Save + + +
+

Select a category from the list to view details about it or edit it.

+
+
+ + + + + + + + + + +
diff --git a/src/app/add-edit-category/add-edit-category.component.spec.ts b/src/app/add-edit-category/add-edit-category.component.spec.ts new file mode 100644 index 0000000..b8ac206 --- /dev/null +++ b/src/app/add-edit-category/add-edit-category.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AddEditCategoryComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddEditCategoryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/add-edit-category/add-edit-category.component.ts b/src/app/add-edit-category/add-edit-category.component.ts new file mode 100644 index 0000000..0e3b43a --- /dev/null +++ b/src/app/add-edit-category/add-edit-category.component.ts @@ -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() + } +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 3e360df..b76ff9d 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -4,12 +4,18 @@ 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 { CategoriesComponent } from './categories/categories.component'; +import { CategoryDetailsComponent } from './category-details/category-details.component'; +import { NewCategoryComponent } from './new-category/new-category.component'; const routes: Routes = [ { path: '', component: DashboardComponent }, { 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 }, ] @NgModule({ diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3059888..abcaece 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -10,6 +10,7 @@ import { MatInputModule, MatListModule, MatRadioModule, + MatProgressBarModule, MatToolbarModule, } from '@angular/material'; @@ -20,6 +21,11 @@ import { TransactionDetailsComponent } from './transaction-details/transaction-d import { NewTransactionComponent } from './new-transaction/new-transaction.component'; import { AddEditTransactionComponent } from './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'; @NgModule({ declarations: [ @@ -28,7 +34,12 @@ import { DashboardComponent } from './dashboard/dashboard.component'; TransactionDetailsComponent, NewTransactionComponent, AddEditTransactionComponent, - DashboardComponent + DashboardComponent, + CategoriesComponent, + CategoryDetailsComponent, + AddEditCategoryComponent, + NewCategoryComponent, + CategoryListComponent ], imports: [ BrowserModule, @@ -40,6 +51,7 @@ import { DashboardComponent } from './dashboard/dashboard.component'; MatInputModule, MatListModule, MatRadioModule, + MatProgressBarModule, MatToolbarModule, AppRoutingModule, FormsModule diff --git a/src/app/budget-database.ts b/src/app/budget-database.ts index 57dadb7..fe8d61e 100644 --- a/src/app/budget-database.ts +++ b/src/app/budget-database.ts @@ -1,8 +1,12 @@ +import { Injectable } from '@angular/core'; import Dexie from 'dexie'; import { TransactionType } from './transaction.type'; import { Category } from './category' import { Transaction } from './transaction' +@Injectable({ + providedIn: 'root' +}) export class BudgetDatabase extends Dexie { transactions: Dexie.Table; categories: Dexie.Table; diff --git a/src/app/categories/categories.component.css b/src/app/categories/categories.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/categories/categories.component.html b/src/app/categories/categories.component.html new file mode 100644 index 0000000..9e85dc4 --- /dev/null +++ b/src/app/categories/categories.component.html @@ -0,0 +1,14 @@ + + + + arrow_back + + + Categories + + + + + + add + diff --git a/src/app/categories/categories.component.spec.ts b/src/app/categories/categories.component.spec.ts new file mode 100644 index 0000000..9e9f4ed --- /dev/null +++ b/src/app/categories/categories.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CategoriesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CategoriesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/categories/categories.component.ts b/src/app/categories/categories.component.ts new file mode 100644 index 0000000..d18f039 --- /dev/null +++ b/src/app/categories/categories.component.ts @@ -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; + + 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() + } +} diff --git a/src/app/category-details/category-details.component.css b/src/app/category-details/category-details.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/category-details/category-details.component.html b/src/app/category-details/category-details.component.html new file mode 100644 index 0000000..aece769 --- /dev/null +++ b/src/app/category-details/category-details.component.html @@ -0,0 +1 @@ + diff --git a/src/app/category-details/category-details.component.spec.ts b/src/app/category-details/category-details.component.spec.ts new file mode 100644 index 0000000..9417e34 --- /dev/null +++ b/src/app/category-details/category-details.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CategoryDetailsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CategoryDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/category-details/category-details.component.ts b/src/app/category-details/category-details.component.ts new file mode 100644 index 0000000..352d7e3 --- /dev/null +++ b/src/app/category-details/category-details.component.ts @@ -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) +} diff --git a/src/app/category-list/category-list.component.css b/src/app/category-list/category-list.component.css new file mode 100644 index 0000000..2983efc --- /dev/null +++ b/src/app/category-list/category-list.component.css @@ -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; +} diff --git a/src/app/category-list/category-list.component.html b/src/app/category-list/category-list.component.html new file mode 100644 index 0000000..6245ec3 --- /dev/null +++ b/src/app/category-list/category-list.component.html @@ -0,0 +1,16 @@ + + + +

{{category.name}}

+ + +
+
diff --git a/src/app/category-list/category-list.component.spec.ts b/src/app/category-list/category-list.component.spec.ts new file mode 100644 index 0000000..fcdbd39 --- /dev/null +++ b/src/app/category-list/category-list.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CategoryListComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CategoryListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/category-list/category-list.component.ts b/src/app/category-list/category-list.component.ts new file mode 100644 index 0000000..e9860fd --- /dev/null +++ b/src/app/category-list/category-list.component.ts @@ -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; + + 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; + } +} diff --git a/src/app/category.service.spec.ts b/src/app/category.service.spec.ts new file mode 100644 index 0000000..34cdebc --- /dev/null +++ b/src/app/category.service.spec.ts @@ -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(); + })); +}); diff --git a/src/app/category.service.ts b/src/app/category.service.ts new file mode 100644 index 0000000..caf47f5 --- /dev/null +++ b/src/app/category.service.ts @@ -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 { + if (count) { + return from(this.db.categories.toCollection().limit(count).toArray()) + } else { + return from(this.db.categories.toCollection().toArray()) + } + } + + getCategory(id: number): Observable { + return from(this.db.categories.where('id').equals(id).first()) + } + + saveCategory(category: Category): Observable { + this.db.categories.put(category) + return of(category) + } + + updateCategory(category: Category): Observable { + this.db.categories.update(category.id, category) + return of([]) + } + + deleteCategory(category: Category): Observable { + return from(this.db.categories.delete(category.id)) + } + + getBalance(category: Category): Observable { + 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; + }) + ) + } +} diff --git a/src/app/dashboard/dashboard.component.css b/src/app/dashboard/dashboard.component.css index 715a51d..ff5e9e8 100644 --- a/src/app/dashboard/dashboard.component.css +++ b/src/app/dashboard/dashboard.component.css @@ -1,33 +1,61 @@ .dashboard { + color: #F1F1F1; display: flex; + flex-wrap: wrap; justify-content: center; padding: 1em; } -.dashboard-primary { +.dashboard > div { background: #212121; - color: #F1F1F1; display: inline-block; + margin: 1em; + padding: 1em; max-width: 500px; - padding: 5em 1em; position: relative; - text-align: center; width: 100%; } +.dashboard .dashboard-primary { + padding: 5em 1em; + text-align: center; +} + .dashboard-primary > * { display: block; } -.dashboard-primary h2 { +.dashboard div h2, .dashboard div h3 { margin: 0; } +.dashboard p, .dashboard a { + color: #F1F1F1; + text-align: center; + text-decoration: none; +} + .dashboard-primary a { bottom: 1em; - color: #F1F1F1; right: 1em; position: absolute; 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; } diff --git a/src/app/dashboard/dashboard.component.html b/src/app/dashboard/dashboard.component.html index d38be98..4e22973 100644 --- a/src/app/dashboard/dashboard.component.html +++ b/src/app/dashboard/dashboard.component.html @@ -10,6 +10,17 @@

Current Balance: {{ balance | currency }}

- View Transactions >> + View Transactions >> + + diff --git a/src/app/dashboard/dashboard.component.ts b/src/app/dashboard/dashboard.component.ts index 30519a2..b3f2b7f 100644 --- a/src/app/dashboard/dashboard.component.ts +++ b/src/app/dashboard/dashboard.component.ts @@ -1,6 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { Transaction } from '../transaction' import { TransactionService } from '../transaction.service' +import { CategoryService } from '../category.service' +import { Category } from '../category' @Component({ selector: 'app-dashboard', @@ -11,14 +13,19 @@ export class DashboardComponent implements OnInit { public balance: number; public transactions: Transaction[]; + public categories: Category[]; + categoryBalances: Map; constructor( - private transactionService: TransactionService + private transactionService: TransactionService, + private categoryService: CategoryService ) { } ngOnInit() { this.getBalance(); this.getTransactions(); + this.getCategories(); + this.categoryBalances = new Map(); } getBalance(): void { @@ -26,6 +33,10 @@ export class DashboardComponent implements OnInit { } 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) } } diff --git a/src/app/new-category/new-category.component.css b/src/app/new-category/new-category.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/new-category/new-category.component.html b/src/app/new-category/new-category.component.html new file mode 100644 index 0000000..9076dfe --- /dev/null +++ b/src/app/new-category/new-category.component.html @@ -0,0 +1 @@ + diff --git a/src/app/new-category/new-category.component.spec.ts b/src/app/new-category/new-category.component.spec.ts new file mode 100644 index 0000000..e23a522 --- /dev/null +++ b/src/app/new-category/new-category.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ NewCategoryComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NewCategoryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/new-category/new-category.component.ts b/src/app/new-category/new-category.component.ts new file mode 100644 index 0000000..2bd64ad --- /dev/null +++ b/src/app/new-category/new-category.component.ts @@ -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 = + } + +} diff --git a/src/app/transaction.service.ts b/src/app/transaction.service.ts index bb4136a..175f31f 100644 --- a/src/app/transaction.service.ts +++ b/src/app/transaction.service.ts @@ -9,11 +9,7 @@ import { BudgetDatabase } from './budget-database'; }) export class TransactionService { - db: BudgetDatabase; - - constructor() { - this.db = new BudgetDatabase(); - } + constructor(private db: BudgetDatabase) { } getTransactions(count?: number): Observable { if (count) { @@ -42,7 +38,6 @@ export class TransactionService { } getBalance(): Observable { - console.log("Getting balance") let sum = 0; return from( this.db.transactions.each(function(transaction) { @@ -56,4 +51,5 @@ export class TransactionService { }) ) } + }