Finish transaction persistence with basic CRUD ops

This commit is contained in:
William Brawner 2018-08-29 19:20:49 -05:00
parent d678eecddd
commit b1fa5c021d
19 changed files with 267 additions and 126 deletions

1
.gitignore vendored
View file

@ -37,3 +37,4 @@ testem.log
# System Files # System Files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
*.swp

5
package-lock.json generated
View file

@ -2508,6 +2508,11 @@
"integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=", "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=",
"dev": true "dev": true
}, },
"dexie": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/dexie/-/dexie-2.0.4.tgz",
"integrity": "sha512-aQ/s1U2wHxwBKRrt2Z/mwFNHMQWhESerFsMYzE+5P5OsIe5o1kgpFMWkzKTtkvkyyEni6mWr/T4HUJuY9xIHLA=="
},
"di": { "di": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",

View file

@ -23,6 +23,7 @@
"@angular/platform-browser-dynamic": "^6.1.0", "@angular/platform-browser-dynamic": "^6.1.0",
"@angular/router": "^6.1.0", "@angular/router": "^6.1.0",
"core-js": "^2.5.4", "core-js": "^2.5.4",
"dexie": "^2.0.4",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"rxjs": "^6.0.0", "rxjs": "^6.0.0",
"zone.js": "~0.8.26" "zone.js": "~0.8.26"

View file

@ -0,0 +1,12 @@
.transaction-form {
padding: 1em;
color: #F1F1F1;
}
.transaction-form * {
display: block;
}
mat-radio-button {
padding-bottom: 15px;
}

View file

@ -0,0 +1,39 @@
<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="!currentTransaction">
<p>Select a transaction from the list to view details about it or edit it.</p>
</div>
<div *ngIf="currentTransaction" class="transaction-form">
<mat-form-field>
<input matInput [(ngModel)]="currentTransaction.title" placeholder="Name" required>
</mat-form-field>
<mat-form-field>
<textarea matInput [(ngModel)]="currentTransaction.description" placeholder="Description"></textarea>
</mat-form-field>
<mat-form-field>
<input matInput type="number" [(ngModel)]="currentTransaction.amount" placeholder="Amount" required>
</mat-form-field>
<mat-form-field>
<input matInput type="date" [(ngModel)]="currentTransaction.date" placeholder="Date" required>
<!--
<input matInput [matDatePicker]="transactionDatePicker" [(ngModel)]="currentTransaction.date" placeholder="Date" required>
<mat-datepicker #transactionDatePicker></mat-datepicker>
-->
</mat-form-field>
<mat-radio-group [(ngModel)]="currentTransaction.type">
<mat-radio-button [value]="transactionType.EXPENSE">Expense</mat-radio-button>
<mat-radio-button [value]="transactionType.INCOME">Income</mat-radio-button>
</mat-radio-group>
<button mat-button *ngIf="currentTransaction.id" (click)="delete()">Delete</button>
</div>

View file

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

View file

@ -0,0 +1,45 @@
import { Component, OnInit, Input } from '@angular/core';
import { Transaction } from '../transaction'
import { TransactionType } from '../transaction.type'
import { TransactionService } from '../transaction.service'
import { Location } from '@angular/common';
@Component({
selector: 'app-add-edit-transaction',
templateUrl: './add-edit-transaction.component.html',
styleUrls: ['./add-edit-transaction.component.css']
})
export class AddEditTransactionComponent implements OnInit {
@Input() title: string;
@Input() currentTransaction: Transaction;
public transactionType = TransactionType;
constructor(
private transactionService: TransactionService,
private location: Location
) { }
ngOnInit() {
}
goBack(): void {
this.location.back()
}
save(): void {
if (this.currentTransaction.id) {
// This is an existing transaction, update it
this.transactionService.updateTransaction(this.currentTransaction);
} else {
// This is a new transaction, save it
this.transactionService.saveTransaction(this.currentTransaction);
}
this.goBack()
}
delete(): void {
this.transactionService.deleteTransaction(this.currentTransaction);
this.goBack()
}
}

View file

@ -18,13 +18,15 @@ import { TransactionsComponent } from './transactions/transactions.component';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { TransactionDetailsComponent } from './transaction-details/transaction-details.component'; import { TransactionDetailsComponent } from './transaction-details/transaction-details.component';
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';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
TransactionsComponent, TransactionsComponent,
TransactionDetailsComponent, TransactionDetailsComponent,
NewTransactionComponent NewTransactionComponent,
AddEditTransactionComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View file

@ -0,0 +1,37 @@
import Dexie from 'dexie';
import { TransactionType } from './transaction.type';
import { Category } from './category'
import { Transaction } from './transaction'
export class BudgetDatabase extends Dexie {
transactions: Dexie.Table<ITransaction, number>;
categories: Dexie.Table<ICategory, number>;
constructor () {
super('BudgetDatabase')
this.version(1).stores({
transactions: `++id, title, description, amount, date, category, type`,
categories: `++id, name, amount, repeat, color`
})
this.transactions.mapToClass(Transaction)
this.categories.mapToClass(Category)
}
}
export interface ITransaction {
id: number;
title: string;
description: string;
amount: number;
date: Date;
category: ICategory;
type: TransactionType;
}
export interface ICategory{
id: number;
name: string;
amount: number;
repeat: string;
color: string;
}

9
src/app/category.ts Normal file
View file

@ -0,0 +1,9 @@
import { ICategory } from './budget-database'
export class Category implements ICategory {
id: number;
name: string;
amount: number;
repeat: string;
color: string;
}

View file

@ -1,34 +1 @@
<mat-toolbar> <app-add-edit-transaction [title]="'Add Transaction'" [currentTransaction]="transaction"></app-add-edit-transaction>
<span>
<button (click)="goBack()">
<mat-icon>arrow_back</mat-icon>
</button>
Add Transaction
</span>
<span>
<a (click)="save()">Save</a>
</span>
</mat-toolbar>
<div class="transaction-form">
<mat-form-field>
<input matInput [(ngModel)]="transaction.title" placeholder="Name" required>
</mat-form-field>
<mat-form-field>
<textarea matInput [(ngModel)]="transaction.description" placeholder="Description"></textarea>
</mat-form-field>
<mat-form-field>
<input matInput type="number" [(ngModel)]="transaction.amount" placeholder="Amount" required>
</mat-form-field>
<mat-form-field>
<input matInput type="date" [(ngModel)]="transaction.date" placeholder="Date" required>
<!--
<input matInput [matDatePicker]="transactionDatePicker" [(ngModel)]="transaction.date" placeholder="Date" required>
<mat-datepicker #transactionDatePicker></mat-datepicker>
-->
</mat-form-field>
<mat-radio-group [(ngModel)]="transaction.type">
<mat-radio-button [value]="transactionType.EXPENSE">Expense</mat-radio-button>
<mat-radio-button [value]="transactionType.INCOME">Income</mat-radio-button>
</mat-radio-group>
</div>

View file

@ -1,8 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { TransactionService } from '../transaction.service'
import { Transaction } from '../transaction' import { Transaction } from '../transaction'
import { TransactionType } from '../transaction.type'
import { Location } from '@angular/common';
@Component({ @Component({
selector: 'app-new-transaction', selector: 'app-new-transaction',
@ -11,19 +8,12 @@ import { Location } from '@angular/common';
}) })
export class NewTransactionComponent implements OnInit { export class NewTransactionComponent implements OnInit {
public transaction = new Transaction() transaction: Transaction;
public transactionType = TransactionType;
constructor( constructor() { }
private transactionService: TransactionService,
private location: Location
) { }
ngOnInit() { ngOnInit() {
this.transaction = new Transaction()
} }
save(): void {
this.transactionService.saveTransaction(this.transaction);
this.location.back()
}
} }

View file

@ -1,8 +1 @@
<div *ngIf="transaction; then transactionBlock else noTransactionBlock"> <app-add-edit-transaction [title]="'Edit Transaction'" [currentTransaction]="transaction"></app-add-edit-transaction>
</div>
<ng-template #transactionBlock>
transaction-details works!
</ng-template>
<ng-template #noTransactionBlock>
transaction-details works!
</ng-template>

View file

@ -1,4 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, Input } from '@angular/core';
import { TransactionService } from '../transaction.service'
import { ActivatedRoute } from '@angular/router';
import { Transaction } from '../transaction' import { Transaction } from '../transaction'
@Component({ @Component({
@ -8,11 +10,20 @@ import { Transaction } from '../transaction'
}) })
export class TransactionDetailsComponent implements OnInit { export class TransactionDetailsComponent implements OnInit {
public transaction: Transaction; transaction: Transaction;
constructor() { } constructor(
private route: ActivatedRoute,
private transactionService: TransactionService
) { }
ngOnInit() { ngOnInit() {
this.getTransaction()
} }
getTransaction(): void {
const id = +this.route.snapshot.paramMap.get('id')
this.transactionService.getTransaction(id)
.subscribe(transaction => this.transaction = transaction)
}
} }

View file

@ -1,41 +1,39 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { of, Observable } from 'rxjs'; import { of, Observable, from } from 'rxjs';
import { Transaction } from './transaction'; import { Transaction } from './transaction';
import { TransactionType } from './transaction.type'; import { TransactionType } from './transaction.type';
import { BudgetDatabase } from './budget-database';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class TransactionService { export class TransactionService {
transactions: Transaction[] = [
{id: 0, amount: Math.random() * 100, date: new Date(), description: "Spent some money", title: "An Expense", type: TransactionType.EXPENSE, categoryId: 0},
{id: 0, amount: Math.random() * 100, date: new Date(), description: "Earned some money", title: "Some Income", type: TransactionType.INCOME, categoryId: 0},
{id: 0, amount: Math.random() * 100, date: new Date(), description: "Spent some money", title: "An Expense", type: TransactionType.EXPENSE, categoryId: 0},
{id: 0, amount: Math.random() * 100, date: new Date(), description: "Earned some money", title: "Some Income", type: TransactionType.INCOME, categoryId: 0},
{id: 0, amount: Math.random() * 100, date: new Date(), description: "Spent some money", title: "An Expense", type: TransactionType.EXPENSE, categoryId: 0},
{id: 0, amount: Math.random() * 100, date: new Date(), description: "Earned some money", title: "Some Income", type: TransactionType.INCOME, categoryId: 0},
{id: 0, amount: Math.random() * 100, date: new Date(), description: "Spent some money", title: "An Expense", type: TransactionType.EXPENSE, categoryId: 0},
{id: 0, amount: Math.random() * 100, date: new Date(), description: "Earned some money", title: "Some Income", type: TransactionType.INCOME, categoryId: 0},
{id: 0, amount: Math.random() * 100, date: new Date(), description: "Spent some money", title: "An Expense", type: TransactionType.EXPENSE, categoryId: 0},
{id: 0, amount: Math.random() * 100, date: new Date(), description: "Earned some money", title: "Some Income", type: TransactionType.INCOME, categoryId: 0},
]
constructor() { } db: BudgetDatabase;
constructor() {
this.db = new BudgetDatabase();
}
getTransactions(): Observable<Transaction[]> { getTransactions(): Observable<Transaction[]> {
return of(this.transactions) return from(this.db.transactions.toCollection().toArray())
}
getTransaction(id: number): Observable<Transaction> {
return from(this.db.transactions.where('id').equals(id).first())
} }
saveTransaction(transaction: Transaction): Observable<Transaction> { saveTransaction(transaction: Transaction): Observable<Transaction> {
// TODO: Replace this with a DB save method this.db.transactions.put(transaction)
var newId = 0;
for (let transaction of this.transactions) {
if (transaction.id > newId) {
newId = transaction.id + 1;
}
}
transaction.id = newId;
this.transactions.push(transaction)
return of(transaction) return of(transaction)
} }
updateTransaction(transaction: Transaction): Observable<any> {
this.db.transactions.update(transaction.id, transaction)
return of([])
}
deleteTransaction(transaction: Transaction): Observable<any> {
return from(this.db.transactions.delete(transaction.id))
}
} }

View file

@ -1,11 +1,13 @@
import { ITransaction } from './budget-database'
import { ICategory } from './budget-database'
import { TransactionType } from './transaction.type'; import { TransactionType } from './transaction.type';
export class Transaction { export class Transaction implements ITransaction {
id: number; id: number;
title: string; title: string;
description: string; description: string;
amount: number; amount: number;
date: Date = new Date(); date: Date = new Date();
categoryId: number; category: ICategory;
type: TransactionType = TransactionType.EXPENSE; type: TransactionType = TransactionType.EXPENSE;
} }

View file

@ -1,5 +1,5 @@
p { mat-toolbar {
margin: 0; justify-content: center;
} }
.amount.income { .amount.income {
@ -15,8 +15,3 @@ p {
justify-content: space-between; justify-content: space-between;
} }
a.mat-fab {
position: fixed;
right: 2em;
bottom: 2em;
}

View file

@ -1,14 +1,14 @@
<mat-toolbar>Transactions</mat-toolbar> <mat-toolbar>Transactions</mat-toolbar>
<mat-list *ngIf="transactions" class="transactions"> <mat-nav-list *ngIf="transactions" class="transactions">
<mat-list-item *ngFor="let transaction of transactions"> <a mat-list-item *ngFor="let transaction of transactions" routerLink="/transactions/{{ transaction.id }}">
<div matLine class="list-row-one"> <div matLine class="list-row-one">
<p>{{transaction.title}}</p> <p>{{transaction.title}}</p>
<p class="amount" [class.expense]="transaction.type === transactionType.EXPENSE" [class.income]="transaction.type === transactionType.INCOME">{{ <p class="amount" [class.expense]="transaction.type === transactionType.EXPENSE" [class.income]="transaction.type === transactionType.INCOME">{{
transaction.amount | currency }}</p> transaction.amount | currency }}</p>
</div> </div>
<p matLine class="text-small">{{ transaction.date | date }}</p> <p matLine class="text-small">{{ transaction.date | date }}</p>
</mat-list-item>
</mat-list>
<a mat-fab routerLink="/transactions/new">
<mat-icon aria-label="Add">add</mat-icon>
</a> </a>
</mat-nav-list>
<a mat-fab routerLink="/transactions/new">
<mat-icon aria-label="Add">add</mat-icon>
</a>

View file

@ -6,6 +6,16 @@ html, body {
padding: 0; padding: 0;
} }
p {
margin: 0;
}
a.mat-fab {
position: fixed;
right: 2em;
bottom: 2em;
}
.text-small { .text-small {
font-size: 1em; font-size: 1em;
color: #BDBDBD color: #BDBDBD
@ -13,22 +23,21 @@ html, body {
.mat-toolbar { .mat-toolbar {
box-shadow: 0px 3px 3px 1px #212121; box-shadow: 0px 3px 3px 1px #212121;
display: flex;
justify-content: space-between;
} }
/* .page-title { mat-toolbar mat-icon {
background: #333333; vertical-align: middle;
color: #F1f1f1; cursor: pointer;
position: fixed;
left: 0;
top: 0;
right: 0;
z-index: 99;
padding: 1em;
} }
.page-title h1 { mat-toolbar .action-item {
margin: 0; font-weight: normal;
cursor: pointer;
font-size: 0.9em;
}
mat-toolbar .action-item a {
vertical-align: middle;
} }
.content {
padding-top: 68px;
} */