WIP: Integrate app with API
This commit is contained in:
parent
c5a4f19fc2
commit
09e32d1cd1
50 changed files with 743 additions and 168 deletions
37
.vscode/launch.json
vendored
Normal file
37
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "Launch index.html",
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"reAttach": true,
|
||||
"file": "${workspaceFolder}/index.html"
|
||||
},
|
||||
{
|
||||
"name": "Launch localhost",
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"reAttach": true,
|
||||
"url": "http://localhost/index.html",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"name": "Attach",
|
||||
"type": "firefox",
|
||||
"request": "attach"
|
||||
},
|
||||
{
|
||||
"name": "Launch addon",
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"reAttach": true,
|
||||
"addonType": "webExtension",
|
||||
"addonPath": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
7
src/app/account.ts
Normal file
7
src/app/account.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { IAccount } from './budget-database'
|
||||
|
||||
export class Account implements IAccount {
|
||||
id: number;
|
||||
remoteId: number;
|
||||
name: string;
|
||||
}
|
15
src/app/accounts.service.spec.ts
Normal file
15
src/app/accounts.service.spec.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { AccountsService } from './accounts.service';
|
||||
|
||||
describe('AccountsService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [AccountsService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([AccountsService], (service: AccountsService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
9
src/app/accounts.service.ts
Normal file
9
src/app/accounts.service.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AccountsService {
|
||||
|
||||
constructor() { }
|
||||
}
|
4
src/app/actionable.ts
Normal file
4
src/app/actionable.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface Actionable {
|
||||
doAction(): void;
|
||||
getActionLabel(): string;
|
||||
}
|
0
src/app/add-edit-account/add-edit-account.component.css
Normal file
0
src/app/add-edit-account/add-edit-account.component.css
Normal file
3
src/app/add-edit-account/add-edit-account.component.html
Normal file
3
src/app/add-edit-account/add-edit-account.component.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<p>
|
||||
add-edit-account works!
|
||||
</p>
|
25
src/app/add-edit-account/add-edit-account.component.spec.ts
Normal file
25
src/app/add-edit-account/add-edit-account.component.spec.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddEditAccountComponent } from './add-edit-account.component';
|
||||
|
||||
describe('AddEditAccountComponent', () => {
|
||||
let component: AddEditAccountComponent;
|
||||
let fixture: ComponentFixture<AddEditAccountComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddEditAccountComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddEditAccountComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
15
src/app/add-edit-account/add-edit-account.component.ts
Normal file
15
src/app/add-edit-account/add-edit-account.component.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-edit-account',
|
||||
templateUrl: './add-edit-account.component.html',
|
||||
styleUrls: ['./add-edit-account.component.css']
|
||||
})
|
||||
export class AddEditAccountComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -1,16 +1,3 @@
|
|||
.category-form {
|
||||
padding: 1em;
|
||||
color: #F1F1F1;
|
||||
}
|
||||
|
||||
.category-form * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
mat-radio-button {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.button-delete {
|
||||
float: right;
|
||||
}
|
||||
|
|
|
@ -1,20 +1,7 @@
|
|||
<mat-toolbar>
|
||||
<span>
|
||||
<a mat-button (click)="goBack()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
{{ title }}
|
||||
</span>
|
||||
<span class="action-item">
|
||||
<a mat-button (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">
|
||||
<div *ngIf="currentCategory" class="form category-form">
|
||||
<mat-form-field>
|
||||
<input matInput [(ngModel)]="currentCategory.name" placeholder="Name" required>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -1,31 +1,36 @@
|
|||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnDestroy } from '@angular/core';
|
||||
import { CategoryService } from '../category.service'
|
||||
import { Category } from '../category'
|
||||
import { Location } from '@angular/common';
|
||||
import { Actionable } from '../actionable';
|
||||
import { AppComponent } from '../app.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-edit-category',
|
||||
templateUrl: './add-edit-category.component.html',
|
||||
styleUrls: ['./add-edit-category.component.css']
|
||||
})
|
||||
export class AddEditCategoryComponent implements OnInit {
|
||||
export class AddEditCategoryComponent implements OnInit, Actionable, OnDestroy {
|
||||
|
||||
@Input() title: string;
|
||||
@Input() currentCategory: Category;
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
private categoryService: CategoryService,
|
||||
private location: Location
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.actionable = this;
|
||||
this.app.backEnabled = true;
|
||||
this.app.title = this.title;
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.location.back()
|
||||
ngOnDestroy() {
|
||||
this.app.actionable = null;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
doAction(): void {
|
||||
if (this.currentCategory.id) {
|
||||
// This is an existing category, update it
|
||||
this.categoryService.updateCategory(this.currentCategory);
|
||||
|
@ -33,11 +38,15 @@ export class AddEditCategoryComponent implements OnInit {
|
|||
// This is a new category, save it
|
||||
this.categoryService.saveCategory(this.currentCategory);
|
||||
}
|
||||
this.goBack()
|
||||
this.app.goBack();
|
||||
}
|
||||
|
||||
getActionLabel(): string {
|
||||
return 'Save';
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this.categoryService.deleteCategory(this.currentCategory);
|
||||
this.goBack()
|
||||
this.app.goBack();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,7 @@
|
|||
<mat-toolbar>
|
||||
<span>
|
||||
<a mat-button (click)="goBack()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
{{ title }}
|
||||
</span>
|
||||
<span class="action-item">
|
||||
<a mat-button (click)="save()">Save</a>
|
||||
</span>
|
||||
</mat-toolbar>
|
||||
<div *ngIf="!currentTransaction">
|
||||
<div [hidden]="currentTransaction">
|
||||
<p>Select a transaction from the list to view details about it or edit it.</p>
|
||||
</div>
|
||||
<div *ngIf="currentTransaction" class="transaction-form">
|
||||
<div [hidden]="!currentTransaction" class="form transaction-form">
|
||||
<mat-form-field>
|
||||
<input matInput [(ngModel)]="currentTransaction.title" placeholder="Name" required>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -1,39 +1,42 @@
|
|||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, OnDestroy } from '@angular/core';
|
||||
import { Transaction } from '../transaction'
|
||||
import { TransactionType } from '../transaction.type'
|
||||
import { TransactionService } from '../transaction.service'
|
||||
import { Location } from '@angular/common';
|
||||
import { Category } from '../category'
|
||||
import { CategoryService } from '../category.service'
|
||||
import { AppComponent } from '../app.component';
|
||||
import { Actionable } from '../actionable';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-edit-transaction',
|
||||
templateUrl: './add-edit-transaction.component.html',
|
||||
styleUrls: ['./add-edit-transaction.component.css']
|
||||
})
|
||||
export class AddEditTransactionComponent implements OnInit {
|
||||
|
||||
export class AddEditTransactionComponent implements OnInit, OnDestroy, Actionable {
|
||||
@Input() title: string;
|
||||
@Input() currentTransaction: Transaction;
|
||||
public transactionType = TransactionType;
|
||||
public selectedCategory: Category;
|
||||
public categories: Category[]
|
||||
public categories: Category[];
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
private categoryService: CategoryService,
|
||||
private transactionService: TransactionService,
|
||||
private location: Location
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getCategories()
|
||||
this.app.title = this.title;
|
||||
this.app.backEnabled = true;
|
||||
this.app.actionable = this;
|
||||
this.getCategories();
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.location.back()
|
||||
ngOnDestroy() {
|
||||
this.app.actionable = null;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
doAction(): void {
|
||||
if (this.currentTransaction.id) {
|
||||
// This is an existing transaction, update it
|
||||
this.transactionService.updateTransaction(this.currentTransaction);
|
||||
|
@ -41,15 +44,19 @@ export class AddEditTransactionComponent implements OnInit {
|
|||
// This is a new transaction, save it
|
||||
this.transactionService.saveTransaction(this.currentTransaction);
|
||||
}
|
||||
this.goBack()
|
||||
this.app.goBack();
|
||||
}
|
||||
|
||||
getActionLabel(): string {
|
||||
return 'Save';
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this.transactionService.deleteTransaction(this.currentTransaction);
|
||||
this.goBack()
|
||||
this.app.goBack();
|
||||
}
|
||||
|
||||
getCategories() {
|
||||
this.categoryService.getCategories().subscribe(categories => this.categories = categories)
|
||||
this.categoryService.getCategories().subscribe(categories => this.categories = categories);
|
||||
}
|
||||
}
|
||||
|
|
19
src/app/api.service.spec.ts
Normal file
19
src/app/api.service.spec.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { ApiService } from './api.service';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
describe('ApiService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
],
|
||||
providers: [ApiService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ApiService], (service: ApiService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
52
src/app/api.service.ts
Normal file
52
src/app/api.service.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, of } from 'rxjs';
|
||||
|
||||
const httpOptions = {
|
||||
headers: new HttpHeaders({
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
withCredentials: true,
|
||||
};
|
||||
|
||||
const host = 'http://localhost:9090';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
) { }
|
||||
|
||||
login(username: string, password: string): Observable<any> {
|
||||
return this.http.post(
|
||||
host + '/login',
|
||||
{
|
||||
'username': username,
|
||||
'password': password
|
||||
},
|
||||
httpOptions
|
||||
);
|
||||
}
|
||||
|
||||
register(username: string, email: string, password: string): Observable<any> {
|
||||
return this.http.post(
|
||||
host + '/register',
|
||||
{
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': password
|
||||
},
|
||||
httpOptions
|
||||
);
|
||||
}
|
||||
|
||||
logout(): Observable<any> {
|
||||
return this.http.get(
|
||||
host + '/logout',
|
||||
httpOptions
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,16 +7,20 @@ import { NewTransactionComponent } from './new-transaction/new-transaction.compo
|
|||
import { CategoriesComponent } from './categories/categories.component';
|
||||
import { CategoryDetailsComponent } from './category-details/category-details.component';
|
||||
import { NewCategoryComponent } from './new-category/new-category.component';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { RegisterComponent } from './register/register.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: DashboardComponent },
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{ path: 'register', component: RegisterComponent },
|
||||
{ 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({
|
||||
imports: [
|
||||
|
|
|
@ -1 +1,29 @@
|
|||
<router-outlet></router-outlet>
|
||||
<mat-sidenav-container class="sidenav-container">
|
||||
<mat-sidenav #sidenav mode="over" closed>
|
||||
<mat-nav-list (click)="sidenav.close()">
|
||||
<a mat-list-item routerLink="/accounts">Manage Accounts</a>
|
||||
<a mat-list-item *ngIf="!authService.currentUser" routerLink="/login">Login</a>
|
||||
<a mat-list-item *ngIf="!authService.currentUser" routerLink="/register">Register</a>
|
||||
<a mat-list-item *ngIf="authService.currentUser" (click)="logout()">Logout</a>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content>
|
||||
<mat-toolbar>
|
||||
<span>
|
||||
<a mat-button [hidden]="!backEnabled" (click)="goBack()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</a>
|
||||
<a mat-button [hidden]="backEnabled" (click)="sidenav.open()">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
{{ title }}
|
||||
</span>
|
||||
<span class="action-item">
|
||||
<a mat-button *ngIf="actionable" (click)="actionable.doAction()">{{ actionable.getActionLabel() }}</a>
|
||||
</span>
|
||||
</mat-toolbar>
|
||||
<router-outlet></router-outlet>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
|
@ -1,4 +1,7 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { Actionable } from './actionable';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -6,5 +9,20 @@ import { Component } from '@angular/core';
|
|||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'budget';
|
||||
public title = 'Budget';
|
||||
public backEnabled = false;
|
||||
public actionable: Actionable;
|
||||
|
||||
constructor(
|
||||
public authService: AuthService,
|
||||
private location: Location
|
||||
) { }
|
||||
|
||||
goBack(): void {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.authService.logout();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
MatProgressBarModule,
|
||||
MatSelectModule,
|
||||
MatToolbarModule,
|
||||
MatSidenavModule,
|
||||
} from '@angular/material';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
@ -29,6 +30,12 @@ import { NewCategoryComponent } from './new-category/new-category.component';
|
|||
import { CategoryListComponent } from './category-list/category-list.component';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { RegisterComponent } from './register/register.component';
|
||||
import { AddEditAccountComponent } from './add-edit-account/add-edit-account.component';
|
||||
import { EditProfileComponent } from './edit-profile/edit-profile.component';
|
||||
import { UserComponent } from './user/user.component';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -42,7 +49,12 @@ import { environment } from '../environments/environment';
|
|||
CategoryDetailsComponent,
|
||||
AddEditCategoryComponent,
|
||||
NewCategoryComponent,
|
||||
CategoryListComponent
|
||||
CategoryListComponent,
|
||||
LoginComponent,
|
||||
RegisterComponent,
|
||||
AddEditAccountComponent,
|
||||
EditProfileComponent,
|
||||
UserComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -57,9 +69,11 @@ import { environment } from '../environments/environment';
|
|||
MatProgressBarModule,
|
||||
MatSelectModule,
|
||||
MatToolbarModule,
|
||||
MatSidenavModule,
|
||||
AppRoutingModule,
|
||||
FormsModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||
HttpClientModule,
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
|
|
21
src/app/auth.service.spec.ts
Normal file
21
src/app/auth.service.spec.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
describe('AuthService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
],
|
||||
providers: [AuthService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([AuthService], (service: AuthService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
55
src/app/auth.service.ts
Normal file
55
src/app/auth.service.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { ApiService } from './api.service';
|
||||
import { User } from './user';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private currentUser: User;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private router: Router,
|
||||
) {
|
||||
console.log('AuthService constructed');
|
||||
}
|
||||
|
||||
login(user: User) {
|
||||
this.apiService.login(user.name, user.password).subscribe(
|
||||
value => {
|
||||
this.currentUser = value;
|
||||
this.router.navigate(['/']);
|
||||
},
|
||||
error => {
|
||||
console.log('Login failed');
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
register(user: User) {
|
||||
this.apiService.register(user.name, user.email, user.password).subscribe(
|
||||
value => {
|
||||
this.login(value);
|
||||
},
|
||||
error => {
|
||||
console.log('Registration failed');
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.apiService.logout().subscribe(
|
||||
value => {
|
||||
window.location.reload();
|
||||
},
|
||||
error => {
|
||||
console.log('Logout failed');
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import Dexie from 'dexie';
|
||||
import { Account } from './account'
|
||||
import { Category } from './category'
|
||||
import { Transaction } from './transaction'
|
||||
import { TransactionType } from './transaction.type';
|
||||
|
@ -10,8 +11,9 @@ import { TransactionType } from './transaction.type';
|
|||
export class BudgetDatabase extends Dexie {
|
||||
transactions: Dexie.Table<ITransaction, number>;
|
||||
categories: Dexie.Table<ICategory, number>;
|
||||
accounts: Dexie.Table<IAccount, number>;
|
||||
|
||||
constructor () {
|
||||
constructor() {
|
||||
super('BudgetDatabase')
|
||||
this.version(1).stores({
|
||||
transactions: `++id, title, description, amount, date, category, type`,
|
||||
|
@ -24,14 +26,22 @@ export class BudgetDatabase extends Dexie {
|
|||
this.version(3).stores({
|
||||
transactions: `++id, title, description, amount, date, category_id, type`,
|
||||
categories: `++id, name, amount, repeat, color`
|
||||
})
|
||||
this.version(4).stores({
|
||||
transactions: `++id, remote_id, account_id, title, description, amount, date, category_id, type`,
|
||||
categories: `++id, remote_id, account_id, name, amount, repeat, color`,
|
||||
accounts: `++id, remote_id, name`
|
||||
})
|
||||
this.transactions.mapToClass(Transaction)
|
||||
this.categories.mapToClass(Category)
|
||||
this.accounts.mapToClass(Account)
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITransaction {
|
||||
id: number;
|
||||
accountId: number;
|
||||
remoteId: number;
|
||||
title: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
|
@ -40,10 +50,18 @@ export interface ITransaction {
|
|||
type: TransactionType;
|
||||
}
|
||||
|
||||
export interface ICategory{
|
||||
export interface ICategory {
|
||||
id: number;
|
||||
accountId: number;
|
||||
remoteId: number;
|
||||
name: string;
|
||||
amount: number;
|
||||
repeat: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface IAccount {
|
||||
id: number;
|
||||
remoteId: number;
|
||||
name: string;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,3 @@
|
|||
<mat-toolbar>
|
||||
<span>
|
||||
<a mat-button (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>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { CategoryService } from '../category.service'
|
||||
import { Category } from '../category'
|
||||
import { Location } from '@angular/common';
|
||||
import { CategoryService } from '../category.service';
|
||||
import { Category } from '../category';
|
||||
import { AppComponent } from '../app.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-categories',
|
||||
|
@ -14,25 +14,22 @@ export class CategoriesComponent implements OnInit {
|
|||
public categoryBalances: Map<number, number>;
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
private categoryService: CategoryService,
|
||||
private location: Location
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.title = 'Categories';
|
||||
this.getCategories();
|
||||
this.categoryBalances = new Map();
|
||||
}
|
||||
|
||||
getCategories(): void {
|
||||
this.categoryService.getCategories().subscribe(categories => {
|
||||
this.categories = categories
|
||||
for (let category of this.categories) {
|
||||
this.categories = categories;
|
||||
for (const category of this.categories) {
|
||||
this.categoryService.getBalance(category).subscribe(balance => this.categoryBalances.set(category.id, balance))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.location.back()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import { ICategory } from './budget-database'
|
|||
|
||||
export class Category implements ICategory {
|
||||
id: number;
|
||||
accountId: number;
|
||||
remoteId: number;
|
||||
name: string;
|
||||
amount: number;
|
||||
repeat: string;
|
||||
|
|
|
@ -1,10 +1,3 @@
|
|||
<mat-toolbar>
|
||||
<!-- empty span object for spacing -->
|
||||
<span></span>
|
||||
<span>My Finances</span>
|
||||
<!-- empty span object for spacing -->
|
||||
<span></span>
|
||||
</mat-toolbar>
|
||||
<div class="dashboard">
|
||||
<div class="dashboard-primary">
|
||||
<h2 class="balance">
|
||||
|
|
|
@ -3,6 +3,8 @@ import { Transaction } from '../transaction'
|
|||
import { TransactionService } from '../transaction.service'
|
||||
import { CategoryService } from '../category.service'
|
||||
import { Category } from '../category'
|
||||
import { AppComponent } from '../app.component';
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
|
@ -17,11 +19,14 @@ export class DashboardComponent implements OnInit {
|
|||
categoryBalances: Map<number, number>;
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
private transactionService: TransactionService,
|
||||
private categoryService: CategoryService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.backEnabled = false;
|
||||
this.app.title = 'My Finances';
|
||||
this.balance = 0;
|
||||
this.getBalance();
|
||||
this.getTransactions();
|
||||
|
@ -39,10 +44,10 @@ export class DashboardComponent implements OnInit {
|
|||
|
||||
getCategories(): void {
|
||||
this.categoryService.getCategories(5).subscribe(categories => {
|
||||
this.categories = categories
|
||||
for (let category of this.categories) {
|
||||
this.categories = categories;
|
||||
for (const category of this.categories) {
|
||||
this.categoryService.getBalance(category).subscribe(balance => this.categoryBalances.set(category.id, balance))
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
0
src/app/edit-profile/edit-profile.component.css
Normal file
0
src/app/edit-profile/edit-profile.component.css
Normal file
3
src/app/edit-profile/edit-profile.component.html
Normal file
3
src/app/edit-profile/edit-profile.component.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<p>
|
||||
edit-profile works!
|
||||
</p>
|
25
src/app/edit-profile/edit-profile.component.spec.ts
Normal file
25
src/app/edit-profile/edit-profile.component.spec.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EditProfileComponent } from './edit-profile.component';
|
||||
|
||||
describe('EditProfileComponent', () => {
|
||||
let component: EditProfileComponent;
|
||||
let fixture: ComponentFixture<EditProfileComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ EditProfileComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditProfileComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
15
src/app/edit-profile/edit-profile.component.ts
Normal file
15
src/app/edit-profile/edit-profile.component.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-profile',
|
||||
templateUrl: './edit-profile.component.html',
|
||||
styleUrls: ['./edit-profile.component.css']
|
||||
})
|
||||
export class EditProfileComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
0
src/app/login/login.component.css
Normal file
0
src/app/login/login.component.css
Normal file
8
src/app/login/login.component.html
Normal file
8
src/app/login/login.component.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<div class="form login-form">
|
||||
<mat-form-field>
|
||||
<input matInput placeholder="Username" [(ngModel)]="user.name" />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput type="password" placeholder="Password" [(ngModel)]="user.password" />
|
||||
</mat-form-field>
|
||||
</div>
|
25
src/app/login/login.component.spec.ts
Normal file
25
src/app/login/login.component.spec.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoginComponent } from './login.component';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LoginComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
38
src/app/login/login.component.ts
Normal file
38
src/app/login/login.component.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { Component, OnInit, OnDestroy, OnChanges } from '@angular/core';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { User } from '../user';
|
||||
import { Actionable } from '../actionable';
|
||||
import { AppComponent } from '../app.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.css']
|
||||
})
|
||||
export class LoginComponent implements OnInit, OnDestroy, Actionable {
|
||||
|
||||
public user: User = new User();
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
private authService: AuthService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.title = 'Login';
|
||||
this.app.actionable = this;
|
||||
this.app.backEnabled = true;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.app.actionable = null;
|
||||
}
|
||||
|
||||
doAction(): void {
|
||||
this.authService.login(this.user);
|
||||
}
|
||||
|
||||
getActionLabel() {
|
||||
return 'Submit';
|
||||
}
|
||||
}
|
0
src/app/register/register.component.css
Normal file
0
src/app/register/register.component.css
Normal file
14
src/app/register/register.component.html
Normal file
14
src/app/register/register.component.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<div class="form register-form">
|
||||
<mat-form-field>
|
||||
<input matInput placeholder="Username" [(ngModel)]="user.name" />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput type="email" placeholder="Email Address" [(ngModel)]="user.email" />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput type="password" placeholder="Password" [(ngModel)]="user.password" />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput type="password" placeholder="Confirm Password" />
|
||||
</mat-form-field>
|
||||
</div>
|
25
src/app/register/register.component.spec.ts
Normal file
25
src/app/register/register.component.spec.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RegisterComponent } from './register.component';
|
||||
|
||||
describe('RegisterComponent', () => {
|
||||
let component: RegisterComponent;
|
||||
let fixture: ComponentFixture<RegisterComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ RegisterComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RegisterComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
38
src/app/register/register.component.ts
Normal file
38
src/app/register/register.component.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { User } from '../user';
|
||||
import { AppComponent } from '../app.component';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { Actionable } from '../actionable';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
templateUrl: './register.component.html',
|
||||
styleUrls: ['./register.component.css']
|
||||
})
|
||||
export class RegisterComponent implements OnInit, OnDestroy, Actionable {
|
||||
|
||||
public user: User = new User();
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
private authService: AuthService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.title = 'Login';
|
||||
this.app.actionable = this;
|
||||
this.app.backEnabled = true;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.app.actionable = null;
|
||||
}
|
||||
|
||||
doAction(): void {
|
||||
this.authService.register(this.user);
|
||||
}
|
||||
|
||||
getActionLabel() {
|
||||
return 'Submit';
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ import { TransactionType } from './transaction.type';
|
|||
|
||||
export class Transaction implements ITransaction {
|
||||
id: number;
|
||||
accountId: number;
|
||||
remoteId: number;
|
||||
title: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
|
|
|
@ -1,13 +1,3 @@
|
|||
<mat-toolbar>
|
||||
<span>
|
||||
<a mat-button (click)="goBack()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</a>
|
||||
</span>
|
||||
<span>Transactions</span>
|
||||
<!-- empty span object for spacing -->
|
||||
<span></span>
|
||||
</mat-toolbar>
|
||||
<mat-nav-list *ngIf="transactions" class="transactions">
|
||||
<a mat-list-item *ngFor="let transaction of transactions" routerLink="/transactions/{{ transaction.id }}">
|
||||
<div matLine class="list-row-one">
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ExpensesComponent } from './transactions.component';
|
||||
import { TransactionsComponent } from './transactions.component';
|
||||
|
||||
describe('ExpensesComponent', () => {
|
||||
let component: ExpensesComponent;
|
||||
let fixture: ComponentFixture<ExpensesComponent>;
|
||||
let component: TransactionsComponent;
|
||||
let fixture: ComponentFixture<TransactionsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ExpensesComponent ]
|
||||
declarations: [ TransactionsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ExpensesComponent);
|
||||
fixture = TestBed.createComponent(TransactionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
|||
import { Transaction } from '../transaction';
|
||||
import { TransactionType } from '../transaction.type';
|
||||
import { TransactionService } from '../transaction.service';
|
||||
import { Location } from '@angular/common';
|
||||
import { AppComponent } from '../app.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transactions',
|
||||
|
@ -16,21 +16,19 @@ export class TransactionsComponent implements OnInit {
|
|||
public transactions: Transaction[]
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
private transactionService: TransactionService,
|
||||
private location: Location
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getTransactions()
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.location.back()
|
||||
this.app.backEnabled = true;
|
||||
this.app.title = 'Transactions';
|
||||
this.getTransactions();
|
||||
}
|
||||
|
||||
getTransactions(): void {
|
||||
this.transactionService.getTransactions().subscribe(transactions => {
|
||||
this.transactions = transactions;
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
6
src/app/user.ts
Normal file
6
src/app/user.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export class User {
|
||||
id: number;
|
||||
name: string;
|
||||
password: string;
|
||||
email: string;
|
||||
}
|
0
src/app/user/user.component.css
Normal file
0
src/app/user/user.component.css
Normal file
3
src/app/user/user.component.html
Normal file
3
src/app/user/user.component.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<p>
|
||||
user works!
|
||||
</p>
|
25
src/app/user/user.component.spec.ts
Normal file
25
src/app/user/user.component.spec.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserComponent } from './user.component';
|
||||
|
||||
describe('UserComponent', () => {
|
||||
let component: UserComponent;
|
||||
let fixture: ComponentFixture<UserComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ UserComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UserComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
15
src/app/user/user.component.ts
Normal file
15
src/app/user/user.component.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user',
|
||||
templateUrl: './user.component.html',
|
||||
styleUrls: ['./user.component.css']
|
||||
})
|
||||
export class UserComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
/* You can add global styles to this file, and also import other style files */
|
||||
html, body {
|
||||
|
||||
html,
|
||||
body {
|
||||
background: #333333;
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
margin: 0;
|
||||
|
@ -11,6 +13,10 @@ p {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
a.mat-button[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a.mat-fab {
|
||||
position: fixed;
|
||||
right: 2em;
|
||||
|
@ -26,13 +32,15 @@ mat-toolbar {
|
|||
box-shadow: 0px 3px 3px 1px #212121;
|
||||
}
|
||||
|
||||
mat-toolbar.mat-toolbar-row, mat-toolbar.mat-toolbar-single-row {
|
||||
mat-toolbar.mat-toolbar-row,
|
||||
mat-toolbar.mat-toolbar-single-row {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
mat-toolbar span {
|
||||
display: flex;
|
||||
width: 33%;;
|
||||
width: 33%;
|
||||
;
|
||||
}
|
||||
|
||||
mat-toolbar span:nth-child(1) {
|
||||
|
@ -70,6 +78,18 @@ mat-toolbar .action-item a {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
mat-sidenav-container.mat-drawer-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
mat-sidenav-container .mat-drawer-backdrop.mat-drawer-shown {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.income {
|
||||
color: #81C784;
|
||||
}
|
||||
|
@ -78,3 +98,20 @@ mat-toolbar .action-item a {
|
|||
color: #E57373;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forms
|
||||
*/
|
||||
|
||||
.form {
|
||||
padding: 1em;
|
||||
color: #F1F1F1;
|
||||
}
|
||||
|
||||
.form .mat-form-field,
|
||||
.form .button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form mat-radio-button {
|
||||
padding-bottom: 15px;
|
||||
}
|
Loading…
Reference in a new issue