Compare commits
2 commits
Author | SHA1 | Date | |
---|---|---|---|
1752c5d2d0 | |||
71a4d8e8a5 |
1
.env.development
Normal file
|
@ -0,0 +1 @@
|
|||
VUE_APP_API_URL=https://3000code.brawner.home
|
63
.gitignore
vendored
|
@ -1,44 +1,23 @@
|
|||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
*~
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# Firebase
|
||||
.firebase/
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
57
README.md
|
@ -1,27 +1,38 @@
|
|||
# Budget
|
||||
# Twigs
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.1.5.
|
||||
## Vue Migration Checklist
|
||||
|
||||
## Development server
|
||||
_Could also be used as a testing checklist_
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
- [ ] Login
|
||||
- [ ] Logout
|
||||
- [ ] Register
|
||||
- [ ] Budget list
|
||||
- [ ] Create budget
|
||||
- [ ] Budget details
|
||||
- [ ]
|
||||
- [ ] Edit budget
|
||||
- [ ] Change name
|
||||
- [ ] Change description
|
||||
- [ ] Change users/permissions
|
||||
- [ ] Delete budget
|
||||
- [ ] Category list
|
||||
- [ ] Create category
|
||||
- [ ] Category details
|
||||
- [ ] Edit category
|
||||
- [ ] Change name
|
||||
- [ ] Change description
|
||||
- [ ] Change expense/income
|
||||
- [ ] Change amount
|
||||
- [ ] Delete category
|
||||
- [ ] Transaction list
|
||||
- [ ] Create transaction
|
||||
- [ ] Transaction details
|
||||
- [ ] Edit transaction
|
||||
- [ ] Change name
|
||||
- [ ] Change description
|
||||
- [ ] Change expense/income
|
||||
- [ ] Change amount
|
||||
- [ ] Change date
|
||||
- [ ] Delete transaction
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||
|
|
162
angular.json
|
@ -1,162 +0,0 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"twigs": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"schematics": {},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"aot": true,
|
||||
"outputPath": "dist/twigs",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/browserconfig.xml",
|
||||
"src/favicon-16x16.png",
|
||||
"src/favicon-32x32.png",
|
||||
"src/favicon-96x96.png",
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
"src/manifest.json"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css",
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"serviceWorker": true
|
||||
},
|
||||
"codeserver": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.codeserver.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "twigs:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "twigs:build:production"
|
||||
},
|
||||
"codeserver": {
|
||||
"browserTarget": "twigs:build:codeserver"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "twigs:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": [],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
"src/manifest.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"twigs-e2e": {
|
||||
"root": "e2e/",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "twigs:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "twigs:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": "e2e/tsconfig.e2e.json",
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "twigs",
|
||||
"cli": {
|
||||
"analytics": "b8304464-255e-47bb-976a-7ed81af63238"
|
||||
}
|
||||
}
|
5
babel.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.e2e.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
import { AppPage } from './app.po';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getParagraphText()).toEqual('Welcome to budget!');
|
||||
});
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getParagraphText() {
|
||||
return element(by.css('app-root h1')).getText();
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
"name": "app",
|
||||
"installMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/favicon.ico",
|
||||
"/index.html",
|
||||
"/*.css",
|
||||
"/*.js"
|
||||
]
|
||||
}
|
||||
}, {
|
||||
"name": "assets",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/assets/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
10167
package-lock.json
generated
96
package.json
|
@ -1,60 +1,48 @@
|
|||
{
|
||||
"name": "budget",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "node_modules/@angular/cli/bin/ng",
|
||||
"start": "node_modules/@angular/cli/bin/ng serve --host '0.0.0.0'",
|
||||
"code-server": "node_modules/@angular/cli/bin/ng serve --configuration=codeserver --host \"0.0.0.0\" --disable-host-check --poll=2000",
|
||||
"build": "node_modules/@angular/cli/bin/ng build",
|
||||
"package": "node_modules/@angular/cli/bin/ng build --prod --service-worker",
|
||||
"publish": "node_modules/@angular/cli/bin/ng build --prod --service-worker && firebase deploy",
|
||||
"test": "node_modules/@angular/cli/bin/ng test",
|
||||
"lint": "node_modules/@angular/cli/bin/ng lint",
|
||||
"e2e": "node_modules/@angular/cli/bin/ng e2e"
|
||||
},
|
||||
"name": "twigs-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^10.0.1",
|
||||
"@angular/cdk": "^10.0.1",
|
||||
"@angular/common": "^10.0.1",
|
||||
"@angular/compiler": "^10.0.1",
|
||||
"@angular/core": "^10.0.1",
|
||||
"@angular/forms": "^10.0.1",
|
||||
"@angular/material": "^10.0.1",
|
||||
"@angular/platform-browser": "^10.0.1",
|
||||
"@angular/platform-browser-dynamic": "^10.0.1",
|
||||
"@angular/router": "^10.0.1",
|
||||
"@angular/service-worker": "^10.0.1",
|
||||
"chart.js": "^2.9.3",
|
||||
"core-js": "^2.6.11",
|
||||
"dexie": "^2.0.4",
|
||||
"ng2-charts": "^2.3.2",
|
||||
"ng2-currency-mask": "^9.0.2",
|
||||
"ngx-cookie-service": "^2.4.0",
|
||||
"rxjs": "^6.5.4",
|
||||
"tslib": "^2.0.0",
|
||||
"zone.js": "~0.10.3"
|
||||
"core-js": "^3.6.5",
|
||||
"register-service-worker": "^1.7.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1000.0",
|
||||
"@angular/cli": "^10.0.0",
|
||||
"@angular/compiler-cli": "^10.0.1",
|
||||
"@angular/language-service": "^10.0.1",
|
||||
"@types/jasmine": "^3.5.2",
|
||||
"@types/jasminewd2": "^2.0.8",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^5.1.2",
|
||||
"eslint": "^6.8.0",
|
||||
"jasmine-core": "~3.5.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~5.0.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "~3.3.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.6.2",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "3.9.5"
|
||||
}
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-pwa": "^4.5.7",
|
||||
"@vue/cli-plugin-router": "^4.5.7",
|
||||
"@vue/cli-plugin-vuex": "^4.5.7",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
|
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 675 KiB After Width: | Height: | Size: 675 KiB |
17
public/index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link rel="stylesheet" href="<%= BASE_URL %>style.css">
|
||||
<title>Twigs</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but Twigs doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
2
public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow:
|
37
public/style.css
Normal file
|
@ -0,0 +1,37 @@
|
|||
:root {
|
||||
--good-color: green;
|
||||
--warn-color: yellow;
|
||||
--danger-color: red;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
margin: 0;
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.good {
|
||||
color: var(--good-color);
|
||||
}
|
||||
|
||||
.warn {
|
||||
color: var(--warn-color);
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: var(--danger-color);
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
#
|
||||
# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 9-11
|
21
src/App.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<div>
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
components: {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.app-twigs {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.app-twigs > div {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -1,13 +0,0 @@
|
|||
import { AppRoutingModule } from './app-routing.module';
|
||||
|
||||
describe('AppRoutingModule', () => {
|
||||
let appRoutingModule: AppRoutingModule;
|
||||
|
||||
beforeEach(() => {
|
||||
appRoutingModule = new AppRoutingModule();
|
||||
});
|
||||
|
||||
it('should create an instance', () => {
|
||||
expect(appRoutingModule).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,43 +0,0 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { TransactionsComponent } from './transactions/transactions.component';
|
||||
import { TransactionDetailsComponent } from './transactions/transaction-details/transaction-details.component';
|
||||
import { NewTransactionComponent } from './transactions/new-transaction/new-transaction.component';
|
||||
import { CategoriesComponent } from './categories/categories.component';
|
||||
import { CategoryDetailsComponent } from './categories/category-details/category-details.component';
|
||||
import { NewCategoryComponent } from './categories/new-category/new-category.component';
|
||||
import { LoginComponent } from './users/login/login.component';
|
||||
import { RegisterComponent } from './users/register/register.component';
|
||||
import { BudgetsComponent } from './budgets/budget.component';
|
||||
import { NewBudgetComponent } from './budgets/new-budget/new-budget.component';
|
||||
import { EditBudgetComponent } from './budgets/edit-budget/edit-budget.component';
|
||||
import { BudgetDetailsComponent } from './budgets/budget-details/budget-details.component';
|
||||
import { EditCategoryComponent } from './categories/edit-category/edit-category.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: BudgetsComponent },
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{ path: 'register', component: RegisterComponent },
|
||||
{ path: 'budgets', component: BudgetsComponent },
|
||||
{ path: 'budgets/new', component: NewBudgetComponent },
|
||||
{ path: 'budgets/:id', component: BudgetDetailsComponent },
|
||||
{ path: 'budgets/:id/edit', component: EditBudgetComponent },
|
||||
{ path: 'budgets/:budgetId/transactions', component: TransactionsComponent },
|
||||
{ path: 'budgets/:budgetId/transactions/new', component: NewTransactionComponent },
|
||||
{ path: 'budgets/:budgetId/transactions/:id', component: TransactionDetailsComponent },
|
||||
{ path: 'budgets/:budgetId/categories', component: CategoriesComponent },
|
||||
{ path: 'budgets/:budgetId/categories/new', component: NewCategoryComponent },
|
||||
{ path: 'budgets/:budgetId/categories/:id', component: CategoryDetailsComponent },
|
||||
{ path: 'budgets/:budgetId/categories/:id/edit', component: EditCategoryComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
],
|
||||
declarations: []
|
||||
})
|
||||
export class AppRoutingModule { }
|
|
@ -1,15 +0,0 @@
|
|||
mat-toolbar {
|
||||
background-color: #fafafa;
|
||||
box-shadow: none;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
mat-toolbar {
|
||||
background-color: #303030;
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<p *ngIf="!online" class="error-offline">
|
||||
You appear to be offline. Twigs unfortunately doesn't currently support offline use at the moment though it may be implemented in a future release!
|
||||
</p>
|
||||
<mat-sidenav-container *ngIf="online" class="sidenav-container">
|
||||
<mat-sidenav #sidenav mode="over" closed>
|
||||
<mat-nav-list (click)="sidenav.close()">
|
||||
<a mat-list-item *ngIf="loggedIn" routerLink="">{{ getUsername() }}</a>
|
||||
<a mat-list-item *ngIf="loggedIn" routerLink="/budgets">Budgets</a>
|
||||
<a mat-list-item *ngIf="!loggedIn" routerLink="/login">Login</a>
|
||||
<a mat-list-item *ngIf="!loggedIn" routerLink="/register">Register</a>
|
||||
<a mat-list-item *ngIf="loggedIn" (click)="logout()">Logout</a>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content>
|
||||
<mat-toolbar>
|
||||
<span>
|
||||
<a mat-icon-button *ngIf="backEnabled" (click)="goBack()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</a>
|
||||
<a mat-icon-button *ngIf="!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,27 +0,0 @@
|
|||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
it('should create the app', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
}));
|
||||
it(`should have as title 'budget'`, async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app.title).toEqual('budget');
|
||||
}));
|
||||
it('should render title in a h1 tag', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.debugElement.nativeElement;
|
||||
expect(compiled.querySelector('h1').textContent).toContain('Welcome to budget!');
|
||||
}));
|
||||
});
|
|
@ -1,117 +0,0 @@
|
|||
import { Component, Inject, ApplicationRef, ChangeDetectorRef } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { User } from './users/user';
|
||||
import { TWIGS_SERVICE, TwigsService } from './shared/twigs.service';
|
||||
import { CookieService } from 'ngx-cookie-service';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { first, filter, map } from 'rxjs/operators';
|
||||
import { interval, concat, BehaviorSubject } from 'rxjs';
|
||||
import { Router, ActivationEnd, ActivatedRoute } from '@angular/router';
|
||||
import { Actionable, isActionable } from './shared/actionable';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
public title = 'Twigs';
|
||||
public backEnabled = false;
|
||||
public user = new BehaviorSubject<User>(null);
|
||||
public online = window.navigator.onLine;
|
||||
public currentVersion = '';
|
||||
public actionable: Actionable;
|
||||
public loggedIn = false;
|
||||
|
||||
constructor(
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
private location: Location,
|
||||
private cookieService: CookieService,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private appRef: ApplicationRef,
|
||||
private updates: SwUpdate,
|
||||
private changeDetector: ChangeDetectorRef,
|
||||
) {
|
||||
const unauthenticatedRoutes = [
|
||||
'/',
|
||||
'/login',
|
||||
'/register'
|
||||
]
|
||||
if (this.cookieService.check('Authorization')) {
|
||||
this.twigsService.getProfile().subscribe(user => {
|
||||
this.user.next(user);
|
||||
if (this.router.url == '/') {
|
||||
this.router.navigateByUrl("/budgets");
|
||||
}
|
||||
});
|
||||
} else if (unauthenticatedRoutes.indexOf(this.router.url) == -1) {
|
||||
this.router.navigateByUrl("/login");
|
||||
}
|
||||
|
||||
updates.available.subscribe(
|
||||
event => {
|
||||
console.log('current version is', event.current);
|
||||
console.log('available version is', event.available);
|
||||
// TODO: Prompt user to click something to update
|
||||
updates.activateUpdate();
|
||||
},
|
||||
err => {
|
||||
|
||||
}
|
||||
);
|
||||
updates.activated.subscribe(
|
||||
event => {
|
||||
console.log('old version was', event.previous);
|
||||
console.log('new version is', event.current);
|
||||
},
|
||||
err => {
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
const appIsStable$ = appRef.isStable.pipe(first(isStable => isStable === true));
|
||||
const everySixHours$ = interval(6 * 60 * 60 * 1000);
|
||||
const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
|
||||
everySixHoursOnceAppIsStable$.subscribe(() => updates.checkForUpdate());
|
||||
this.user.subscribe(
|
||||
user => {
|
||||
if (user) {
|
||||
this.loggedIn = true;
|
||||
} else {
|
||||
this.loggedIn = false;
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
getUsername(): String {
|
||||
return this.user.value.username;
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.twigsService.logout().subscribe(_ => {
|
||||
this.location.go('/');
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
setActionable(actionable: Actionable): void {
|
||||
this.actionable = actionable;
|
||||
this.changeDetector.detectChanges();
|
||||
}
|
||||
|
||||
setBackEnabled(enabled: boolean): void {
|
||||
this.backEnabled = enabled;
|
||||
this.changeDetector.detectChanges();
|
||||
}
|
||||
|
||||
setTitle(title: string) {
|
||||
this.title = title;
|
||||
this.changeDetector.detectChanges();
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { TransactionsComponent } from './transactions/transactions.component';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { BudgetsComponent } from './budgets/budget.component';
|
||||
import { TransactionDetailsComponent } from './transactions/transaction-details/transaction-details.component';
|
||||
import { NewTransactionComponent } from './transactions/new-transaction/new-transaction.component';
|
||||
import { AddEditTransactionComponent } from './transactions/add-edit-transaction/add-edit-transaction.component';
|
||||
import { CategoriesComponent } from './categories/categories.component';
|
||||
import { CategoryDetailsComponent } from './categories/category-details/category-details.component';
|
||||
import { CategoryFormComponent } from './categories/category-form/category-form.component';
|
||||
import { NewCategoryComponent } from './categories/new-category/new-category.component';
|
||||
import { CategoryListComponent } from './categories/category-list/category-list.component';
|
||||
import { LoginComponent } from './users/login/login.component';
|
||||
import { RegisterComponent } from './users/register/register.component';
|
||||
import { AddEditBudgetComponent } from './budgets/add-edit-budget/add-edit-budget.component';
|
||||
import { EditProfileComponent } from './users/edit-profile/edit-profile.component';
|
||||
import { UserComponent } from './users/user.component';
|
||||
import { NewBudgetComponent } from './budgets/new-budget/new-budget.component';
|
||||
import { BudgetDetailsComponent } from './budgets/budget-details/budget-details.component';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { CurrencyMaskModule, CurrencyMaskConfig, CURRENCY_MASK_CONFIG } from 'ng2-currency-mask';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { CategoryBreakdownComponent } from './categories/category-breakdown/category-breakdown.component';
|
||||
import { ChartsModule } from 'ng2-charts';
|
||||
import { TWIGS_SERVICE } from './shared/twigs.service';
|
||||
import { AuthInterceptor } from './shared/auth.interceptor';
|
||||
import { TwigsHttpService } from './shared/twigs.http.service';
|
||||
import { TwigsLocalService } from './shared/twigs.local.service';
|
||||
import { CookieService } from 'ngx-cookie-service';
|
||||
import { TransactionListComponent } from './transactions/transaction-list/transaction-list.component';
|
||||
import { EditCategoryComponent } from './categories/edit-category/edit-category.component';
|
||||
import { EditBudgetComponent } from './budgets/edit-budget/edit-budget.component';
|
||||
|
||||
export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
||||
align: 'left',
|
||||
precision: 2,
|
||||
prefix: '',
|
||||
thousands: ',',
|
||||
decimal: '.',
|
||||
suffix: '',
|
||||
allowNegative: false,
|
||||
};
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
TransactionsComponent,
|
||||
TransactionDetailsComponent,
|
||||
NewTransactionComponent,
|
||||
AddEditTransactionComponent,
|
||||
CategoriesComponent,
|
||||
CategoryDetailsComponent,
|
||||
CategoryFormComponent,
|
||||
NewCategoryComponent,
|
||||
CategoryListComponent,
|
||||
LoginComponent,
|
||||
RegisterComponent,
|
||||
AddEditBudgetComponent,
|
||||
EditProfileComponent,
|
||||
UserComponent,
|
||||
NewBudgetComponent,
|
||||
BudgetDetailsComponent,
|
||||
BudgetsComponent,
|
||||
CategoryBreakdownComponent,
|
||||
TransactionListComponent,
|
||||
EditCategoryComponent,
|
||||
EditBudgetComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatDatepickerModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
MatRadioModule,
|
||||
MatProgressBarModule,
|
||||
MatSelectModule,
|
||||
MatToolbarModule,
|
||||
MatSidenavModule,
|
||||
MatProgressSpinnerModule,
|
||||
AppRoutingModule,
|
||||
FormsModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||
HttpClientModule,
|
||||
CurrencyMaskModule,
|
||||
ChartsModule,
|
||||
MatCheckboxModule,
|
||||
MatCardModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig },
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
||||
{ provide: TWIGS_SERVICE, useClass: TwigsHttpService },
|
||||
// { provide: TWIGS_SERVICE, useClass: TwigsLocalService },
|
||||
CookieService
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
|
@ -1,14 +0,0 @@
|
|||
<mat-progress-spinner *ngIf="isLoading" mode="indeterminate" diameter="50"></mat-progress-spinner>
|
||||
<div *ngIf="!isLoading && !budget">
|
||||
<p>Select a budget from the list to view details about it or edit it.</p>
|
||||
</div>
|
||||
<div *ngIf="!isLoading && budget" class="form budget-form">
|
||||
<mat-form-field>
|
||||
<input matInput [(ngModel)]="budget.name" placeholder="Name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<textarea matInput [(ngModel)]="budget.description" placeholder="Description"></textarea>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="accent" (click)="save()">Save</button>
|
||||
<button class="button-delete" mat-raised-button color="warn" *ngIf="budget.id" (click)="delete()">Delete</button>
|
||||
</div>
|
|
@ -1,25 +0,0 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddEditBudgetComponent } from './add-edit-budget.component';
|
||||
|
||||
describe('AddEditBudgetComponent', () => {
|
||||
let component: AddEditBudgetComponent;
|
||||
let fixture: ComponentFixture<AddEditBudgetComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddEditBudgetComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddEditBudgetComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,64 +0,0 @@
|
|||
import { Component, OnInit, Input, Inject, OnDestroy } from '@angular/core';
|
||||
import { Budget } from '../budget';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { User, UserPermission, Permission } from 'src/app/users/user';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-edit-budget',
|
||||
templateUrl: './add-edit-budget.component.html',
|
||||
styleUrls: ['./add-edit-budget.component.css']
|
||||
})
|
||||
export class AddEditBudgetComponent {
|
||||
@Input() title: string;
|
||||
@Input() budget: Budget;
|
||||
public users: UserPermission[];
|
||||
public searchedUsers: User[] = [];
|
||||
public isLoading = false;
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) {
|
||||
this.app.setTitle(this.title)
|
||||
this.app.setBackEnabled(true);
|
||||
this.users = [new UserPermission(this.app.user.value, Permission.OWNER)];
|
||||
}
|
||||
|
||||
save(): void {
|
||||
let observable;
|
||||
this.isLoading = true;
|
||||
if (this.budget.id) {
|
||||
// This is an existing budget, update it
|
||||
observable = this.twigsService.updateBudget(this.budget.id, this.budget);
|
||||
} else {
|
||||
// This is a new budget, save it
|
||||
observable = this.twigsService.createBudget(
|
||||
this.budget.name,
|
||||
this.budget.description,
|
||||
this.users
|
||||
);
|
||||
}
|
||||
// TODO: Check if it was actually successful or not
|
||||
observable.subscribe(val => {
|
||||
this.app.goBack();
|
||||
});
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this.isLoading = true;
|
||||
this.twigsService.deleteBudget(this.budget.id);
|
||||
this.app.goBack();
|
||||
}
|
||||
|
||||
// TODO: Implement a search box with suggestions to add users
|
||||
searchUsers(username: string) {
|
||||
this.twigsService.getUsersByUsername(username).subscribe(users => {
|
||||
this.searchedUsers = users;
|
||||
});
|
||||
}
|
||||
|
||||
clearUserSearch() {
|
||||
this.searchedUsers = [];
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
.dashboard {
|
||||
color: #333333;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.dashboard>mat-card {
|
||||
background: #FFFFFF;
|
||||
display: inline-block;
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
max-width: 500px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.dashboard .dashboard-primary {
|
||||
padding: 5em 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dashboard-primary>* {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dashboard div h2,
|
||||
.dashboard div h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dashboard p,
|
||||
.dashboard a {
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dashboard-primary div {
|
||||
bottom: 0.5em;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
left: 0.5em;
|
||||
right: 0.5em;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.dashboard .no-categories {
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dashboard .no-categories a {
|
||||
border-color: #333333;
|
||||
display: inline-block;
|
||||
border: 1px dashed;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.dashboard .no-categories p {
|
||||
line-height: normal;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
a.view-all {
|
||||
position: absolute;
|
||||
right: 0.5em;
|
||||
top: 0.5em;
|
||||
}
|
||||
|
||||
@media (min-width: 1160px) {
|
||||
mat-card {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.category-info {
|
||||
height: 313px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 610px) {
|
||||
.dashboard {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dashboard>mat-card {
|
||||
margin: 1em auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dashboard {
|
||||
color: #F1F1F1;
|
||||
}
|
||||
|
||||
.dashboard>mat-card {
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
.dashboard p,
|
||||
.dashboard a {
|
||||
color: #F1F1F1;
|
||||
}
|
||||
|
||||
.dashboard .no-categories a {
|
||||
border-color: #F1F1F1;
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
<div class="dashboard">
|
||||
<mat-card class="dashboard-primary" [hidden]="!budget">
|
||||
<h2 class="balance">
|
||||
Current Balance: <br />
|
||||
<span
|
||||
[ngClass]="{'income': getBalance() > 0, 'expense': getBalance() < 0}">{{ getBalance() / 100 | currency }}</span>
|
||||
</h2>
|
||||
<app-category-breakdown [barChartLabels]="barChartLabels" [barChartData]="barChartData">
|
||||
</app-category-breakdown>
|
||||
<div class="transaction-navigation">
|
||||
<a mat-button routerLink="/budgets/{{ budget.id }}/transactions" *ngIf="budget">View Transactions</a>
|
||||
</div>
|
||||
</mat-card>
|
||||
<mat-card class="dashboard-categories" [hidden]="!budget">
|
||||
<h3 class="categories">Income</h3>
|
||||
<a mat-button routerLink="/budgets/{{ budget.id }}/categories/new" class="view-all" *ngIf="budget">Add Category</a>
|
||||
<div class="no-categories" *ngIf="!income || income.length === 0">
|
||||
<a mat-button routerLink="/budgets/{{ budget.id }}/categories/new" *ngIf="budget">
|
||||
<mat-icon>add</mat-icon>
|
||||
<p>Add categories to gain more insights into your income.</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="category-info" *ngIf="income && income.length > 0">
|
||||
<app-category-list [budgetId]="budget.id" [categories]="income" [categoryBalances]="categoryBalances">
|
||||
</app-category-list>
|
||||
</div>
|
||||
</mat-card>
|
||||
<mat-card class="dashboard-categories" [hidden]="!budget">
|
||||
<h3 class="categories">Expenses</h3>
|
||||
<a mat-button routerLink="/budgets/{{ budget.id }}/categories/new" class="view-all" *ngIf="budget">Add Category</a>
|
||||
<div class="no-categories" *ngIf="!expenses || expenses.length === 0">
|
||||
<a mat-button routerLink="/budgets/{{ budget.id }}/categories/new" *ngIf="budget">
|
||||
<mat-icon>add</mat-icon>
|
||||
<p>Add categories to gain more insights into your expenses.</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="category-info" *ngIf="expenses && expenses.length > 0">
|
||||
<app-category-list [budgetId]="budget.id" [categories]="expenses" [categoryBalances]="categoryBalances">
|
||||
</app-category-list>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
<a mat-fab routerLink="/budgets/{{ budget.id }}/transactions/new" *ngIf="budget">
|
||||
<mat-icon aria-label="Add">add</mat-icon>
|
||||
</a>
|
|
@ -1,25 +0,0 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BudgetDetailsComponent } from './budget-details.component';
|
||||
|
||||
describe('BudgetDetailsComponent', () => {
|
||||
let component: BudgetDetailsComponent;
|
||||
let fixture: ComponentFixture<BudgetDetailsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BudgetDetailsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BudgetDetailsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,177 +0,0 @@
|
|||
import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
|
||||
import { Budget } from '../budget';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { Transaction } from 'src/app/transactions/transaction';
|
||||
import { Category } from 'src/app/categories/category';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Label } from 'ng2-charts';
|
||||
import { ChartDataSets } from 'chart.js';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
import { Actionable } from '../../shared/actionable';
|
||||
|
||||
@Component({
|
||||
selector: 'app-budget-details',
|
||||
templateUrl: './budget-details.component.html',
|
||||
styleUrls: ['./budget-details.component.css']
|
||||
})
|
||||
export class BudgetDetailsComponent implements OnInit, OnDestroy, Actionable {
|
||||
|
||||
budget: Budget;
|
||||
public transactions: Transaction[];
|
||||
public expenses: Category[] = [];
|
||||
public income: Category[] = [];
|
||||
categoryBalances: Map<number, number>;
|
||||
expectedIncome = 0;
|
||||
actualIncome = 0;
|
||||
expectedExpenses = 0;
|
||||
actualExpenses = 0;
|
||||
barChartLabels: Label[] = ['Income', 'Expenses'];
|
||||
barChartData: ChartDataSets[] = [
|
||||
{ data: [0, 0], label: 'Expected' },
|
||||
{ data: [0, 0], label: 'Actual' },
|
||||
];
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
private route: ActivatedRoute,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getBudget();
|
||||
this.app.setBackEnabled(false);
|
||||
this.app.setActionable(this)
|
||||
this.categoryBalances = new Map();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.app.setActionable(null)
|
||||
}
|
||||
|
||||
getBudget() {
|
||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
||||
this.twigsService.getBudget(id)
|
||||
.subscribe(budget => {
|
||||
this.app.setTitle(budget.name)
|
||||
this.budget = budget;
|
||||
this.getBalance();
|
||||
this.getTransactions();
|
||||
this.getCategories();
|
||||
});
|
||||
}
|
||||
|
||||
updateBarChart() {
|
||||
const color = [0, 188, 212];
|
||||
this.barChartData = [
|
||||
{
|
||||
data: [this.expectedIncome / 100, this.expectedExpenses / 100],
|
||||
label: 'Expected',
|
||||
backgroundColor: 'rgba(241, 241, 241, 0.8)',
|
||||
borderColor: 'rgba(241, 241, 241, 0.9)',
|
||||
hoverBackgroundColor: 'rgba(241, 241, 241, 1)',
|
||||
hoverBorderColor: 'rgba(241, 241, 241, 1)',
|
||||
},
|
||||
{
|
||||
data: [this.actualIncome / 100, this.actualExpenses / 100],
|
||||
label: 'Actual',
|
||||
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.8)`,
|
||||
borderColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.9)`,
|
||||
hoverBackgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 1)`,
|
||||
hoverBorderColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 1)`
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getBalance(): number {
|
||||
let totalBalance = 0;
|
||||
if (!this.categoryBalances) {
|
||||
return 0;
|
||||
}
|
||||
this.categoryBalances.forEach(balance => {
|
||||
totalBalance += balance;
|
||||
});
|
||||
return totalBalance;
|
||||
}
|
||||
|
||||
getTransactions(): void {
|
||||
let date = new Date();
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
date.setDate(1);
|
||||
this.twigsService.getTransactions(this.budget.id, null, 5, date)
|
||||
.subscribe(transactions => this.transactions = <Transaction[]>transactions);
|
||||
}
|
||||
|
||||
getCategories(): void {
|
||||
this.twigsService.getCategories(this.budget.id).subscribe(categories => {
|
||||
const categoryBalances = new Map<number, number>();
|
||||
let categoryBalancesCount = 0;
|
||||
console.log(categories);
|
||||
for (const category of categories) {
|
||||
if (category.expense) {
|
||||
this.expenses.push(category);
|
||||
this.expectedExpenses += category.amount;
|
||||
} else {
|
||||
this.income.push(category);
|
||||
this.expectedIncome += category.amount;
|
||||
}
|
||||
this.getCategoryBalance(category.id).subscribe(
|
||||
balance => {
|
||||
console.log(balance);
|
||||
if (category.expense) {
|
||||
this.actualExpenses += balance * -1;
|
||||
} else {
|
||||
this.actualIncome += balance;
|
||||
}
|
||||
categoryBalances.set(category.id, balance);
|
||||
categoryBalancesCount++;
|
||||
},
|
||||
error => { categoryBalancesCount++; },
|
||||
() => {
|
||||
// This weird workaround is to force the OnChanges callback to be fired.
|
||||
// Angular needs the reference to the object to change in order for it to
|
||||
// work.
|
||||
if (categoryBalancesCount === categories.length) {
|
||||
this.categoryBalances = categoryBalances;
|
||||
this.updateBarChart();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCategoryBalance(category: number): Observable<number> {
|
||||
return Observable.create(subscriber => {
|
||||
let date = new Date();
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(0);
|
||||
date.setDate(1);
|
||||
this.twigsService.getTransactions(this.budget.id, category, null, date).subscribe(transactions => {
|
||||
let balance = 0;
|
||||
for (const transaction of transactions) {
|
||||
if (transaction.expense) {
|
||||
balance -= transaction.amount;
|
||||
} else {
|
||||
balance += transaction.amount;
|
||||
}
|
||||
}
|
||||
subscriber.next(balance);
|
||||
subscriber.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
doAction(): void {
|
||||
this.router.navigateByUrl(this.router.routerState.snapshot.url + "/edit")
|
||||
}
|
||||
|
||||
getActionLabel(): string {
|
||||
return "Edit";
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
.dashboard {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 1em;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.auth-button-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
justify-content: space-between;
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.auth-button-container > a {
|
||||
flex-grow: 1;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
@media all and (max-width: 400px) {
|
||||
.auth-button-container {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
<mat-progress-spinner *ngIf="loading" diameter="50" mode="indeterminate"></mat-progress-spinner>
|
||||
<div class="dashboard" *ngIf="!loading && !loggedIn">
|
||||
<h2 class="log-in">Welcome to Twigs!</h2>
|
||||
<p>To begin tracking your finances, login or create an account!</p>
|
||||
<div class="auth-button-container">
|
||||
<a routerLink="/register" mat-stroked-button color="accent">Register</a>
|
||||
<a routerLink="/login" mat-raised-button color="accent">Login</a>
|
||||
</div>
|
||||
</div>
|
||||
<mat-nav-list class="budgets" *ngIf="!loading && loggedIn">
|
||||
<a mat-list-item *ngFor="let budget of budgets" routerLink="/budgets/{{ budget.id }}">
|
||||
<p matLine class="budget-list-title">
|
||||
{{ budget.name }}
|
||||
</p>
|
||||
<p matLine class="budget-list-description">
|
||||
{{ budget.description }}
|
||||
</p>
|
||||
</a>
|
||||
</mat-nav-list>
|
||||
<div class="no-budgets" *ngIf="!loading && loggedIn && (!budgets || budgets.length === 0)">
|
||||
<a mat-button routerLink="/budgets/new">
|
||||
<mat-icon>add</mat-icon>
|
||||
<p>Add budgets to begin tracking your finances.</p>
|
||||
</a>
|
||||
</div>
|
||||
<a mat-fab routerLink="/budgets/new" *ngIf="!loading && loggedIn">
|
||||
<mat-icon aria-label="Add">add</mat-icon>
|
||||
</a>
|
|
@ -1,25 +0,0 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BudgetsComponent } from './budget.component';
|
||||
|
||||
describe('BudgetsComponent', () => {
|
||||
let component: BudgetsComponent;
|
||||
let fixture: ComponentFixture<BudgetsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BudgetsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BudgetsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,51 +0,0 @@
|
|||
import { Component, OnInit, Input, Inject, ChangeDetectorRef } from '@angular/core';
|
||||
import { AppComponent } from '../app.component';
|
||||
import { Budget } from './budget';
|
||||
import { TWIGS_SERVICE, TwigsService } from '../shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-budgets',
|
||||
templateUrl: './budget.component.html',
|
||||
styleUrls: ['./budget.component.css']
|
||||
})
|
||||
export class BudgetsComponent implements OnInit {
|
||||
|
||||
public budgets: Budget[];
|
||||
public loading = true;
|
||||
public loggedIn = false;
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.setBackEnabled(false);
|
||||
this.app.user.subscribe(
|
||||
user => {
|
||||
if (!user) {
|
||||
this.loading = false;
|
||||
this.loggedIn = false;
|
||||
this.app.setTitle('Welcome')
|
||||
return;
|
||||
}
|
||||
this.app.setTitle('Budgets')
|
||||
this.loggedIn = true;
|
||||
this.loading = true;
|
||||
this.twigsService.getBudgets().subscribe(
|
||||
budgets => {
|
||||
console.log(budgets)
|
||||
this.budgets = budgets;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
},
|
||||
error => {
|
||||
this.loading = false;
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import { UserPermission } from '../users/user';
|
||||
|
||||
export class Budget {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
users: UserPermission[];
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<app-add-edit-budget [title]="'Edit Budget'" [budget]="budget"></app-add-edit-budget>
|
|
@ -1,25 +0,0 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EditBudgetComponent } from './edit-budget.component';
|
||||
|
||||
describe('EditBudgetComponent', () => {
|
||||
let component: EditBudgetComponent;
|
||||
let fixture: ComponentFixture<EditBudgetComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ EditBudgetComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditBudgetComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,27 +0,0 @@
|
|||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TwigsService, TWIGS_SERVICE } from '../../shared/twigs.service';
|
||||
import { Budget } from '../budget';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-budget',
|
||||
templateUrl: './edit-budget.component.html',
|
||||
styleUrls: ['./edit-budget.component.css']
|
||||
})
|
||||
export class EditBudgetComponent implements OnInit {
|
||||
|
||||
budget: Budget;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
||||
this.twigsService.getBudget(id)
|
||||
.subscribe(budget => {
|
||||
this.budget = budget;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<app-add-edit-budget [title]="'Add Budget'" [budget]="budget"></app-add-edit-budget>
|
|
@ -1,25 +0,0 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NewBudgetComponent } from './new-budget.component';
|
||||
|
||||
describe('NewBudgetComponent', () => {
|
||||
let component: NewBudgetComponent;
|
||||
let fixture: ComponentFixture<NewBudgetComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ NewBudgetComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(NewBudgetComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Budget } from '../budget';
|
||||
|
||||
@Component({
|
||||
selector: 'app-new-budget',
|
||||
templateUrl: './new-budget.component.html',
|
||||
styleUrls: ['./new-budget.component.css']
|
||||
})
|
||||
export class NewBudgetComponent implements OnInit {
|
||||
|
||||
public budget: Budget;
|
||||
|
||||
constructor() {
|
||||
this.budget = new Budget();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
<app-category-list [budgetId]="budgetId" [categories]="categories" [categoryBalances]="categoryBalances"></app-category-list>
|
||||
<a mat-fab routerLink="/budgets/{{ budgetId }}/categories/new">
|
||||
<mat-icon aria-label="Add">add</mat-icon>
|
||||
</a>
|
|
@ -1,25 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
|
@ -1,59 +0,0 @@
|
|||
import { Component, OnInit, Input, Inject } from '@angular/core';
|
||||
import { Category } from './category';
|
||||
import { AppComponent } from '../app.component';
|
||||
import { Observable } from 'rxjs';
|
||||
import { TransactionType } from '../transactions/transaction.type';
|
||||
import { Budget } from '../budgets/budget';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TWIGS_SERVICE, TwigsService } from '../shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-categories',
|
||||
templateUrl: './categories.component.html',
|
||||
styleUrls: ['./categories.component.css']
|
||||
})
|
||||
export class CategoriesComponent implements OnInit {
|
||||
|
||||
budgetId: number;
|
||||
public categories: Category[];
|
||||
public categoryBalances: Map<number, number>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private app: AppComponent,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.budgetId = Number.parseInt(this.route.snapshot.paramMap.get('budgetId'));
|
||||
this.app.setTitle('Categories')
|
||||
this.app.setBackEnabled(true);
|
||||
this.getCategories();
|
||||
this.categoryBalances = new Map();
|
||||
}
|
||||
|
||||
getCategories(): void {
|
||||
this.twigsService.getCategories(this.budgetId).subscribe(categories => {
|
||||
this.categories = categories;
|
||||
for (const category of this.categories) {
|
||||
this.getCategoryBalance(category).subscribe(balance => this.categoryBalances.set(category.id, balance));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCategoryBalance(category: Category): Observable<number> {
|
||||
return Observable.create(subscriber => {
|
||||
this.twigsService.getTransactions(this.budgetId, category.id).subscribe(transactions => {
|
||||
let balance = 0;
|
||||
for (const transaction of transactions) {
|
||||
if (transaction.expense) {
|
||||
balance -= transaction.amount;
|
||||
} else {
|
||||
balance += transaction.amount;
|
||||
}
|
||||
}
|
||||
subscriber.next(balance);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<div class="category-breakdown">
|
||||
<canvas baseChart
|
||||
[datasets]="barChartData"
|
||||
[options]="barChartOptions"
|
||||
[labels]="barChartLabels"
|
||||
[legend]="barChartLegend"
|
||||
[chartType]="barChartType">
|
||||
</canvas>
|
||||
</div>
|
|
@ -1,25 +0,0 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CategoryBreakdownComponent } from './category-breakdown.component';
|
||||
|
||||
describe('CategoryBreakdownComponent', () => {
|
||||
let component: CategoryBreakdownComponent;
|
||||
let fixture: ComponentFixture<CategoryBreakdownComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CategoryBreakdownComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CategoryBreakdownComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,45 +0,0 @@
|
|||
import { Component, OnInit, Input, OnChanges, SimpleChanges, SimpleChange, ViewChild } from '@angular/core';
|
||||
import { Category } from '../category';
|
||||
import { CategoriesComponent } from '../categories.component';
|
||||
import { ChartOptions, ChartType, ChartDataSets } from 'chart.js';
|
||||
import { BaseChartDirective, Label } from 'ng2-charts';
|
||||
|
||||
@Component({
|
||||
selector: 'app-category-breakdown',
|
||||
templateUrl: './category-breakdown.component.html',
|
||||
styleUrls: ['./category-breakdown.component.css']
|
||||
})
|
||||
export class CategoryBreakdownComponent implements OnInit, OnChanges {
|
||||
barChartOptions: ChartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
xAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}], yAxes: [{}]
|
||||
},
|
||||
};
|
||||
@Input() barChartLabels: Label[];
|
||||
@Input() barChartData: ChartDataSets[] = [
|
||||
{ data: [0, 0, 0, 0], label: '' },
|
||||
];
|
||||
barChartType: ChartType = 'horizontalBar';
|
||||
barChartLegend = true;
|
||||
@ViewChild(BaseChartDirective) chart: BaseChartDirective;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() { }
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log(changes);
|
||||
if (changes.barChartLabels) {
|
||||
this.barChartLabels = changes.barChartLabels.currentValue;
|
||||
}
|
||||
if (changes.barChartData) {
|
||||
this.barChartData = changes.barChartData.currentValue;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
.category-description {
|
||||
padding: 0 1em;
|
||||
white-space: pre-wrap;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
<p class="category-description" *ngIf="category && category.description" [innerHtml]="category.description"></p>
|
||||
<app-transaction-list *ngIf="budgetId && category" [budgetId]="budgetId" [categoryId]="category.id"></app-transaction-list>
|
||||
<a mat-fab routerLink="/budgets/{{ budgetId }}/transactions/new">
|
||||
<mat-icon aria-label="Add">add</mat-icon>
|
||||
</a>
|
|
@ -1,25 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
|
@ -1,55 +0,0 @@
|
|||
import { Component, OnInit, Inject, ApplicationModule, OnDestroy } from '@angular/core';
|
||||
import { Category } from '../category';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TWIGS_SERVICE, TwigsService } from '../../shared/twigs.service';
|
||||
import { Transaction } from '../../transactions/transaction';
|
||||
import { AppComponent } from '../../app.component';
|
||||
import { Actionable } from '../../shared/actionable';
|
||||
|
||||
@Component({
|
||||
selector: 'app-category-details',
|
||||
templateUrl: './category-details.component.html',
|
||||
styleUrls: ['./category-details.component.css']
|
||||
})
|
||||
export class CategoryDetailsComponent implements OnInit, OnDestroy, Actionable {
|
||||
|
||||
budgetId: number;
|
||||
category: Category;
|
||||
public transactions: Transaction[];
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private app: AppComponent,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
doAction(): void {
|
||||
this.router.navigateByUrl(this.router.routerState.snapshot.url + "/edit")
|
||||
}
|
||||
|
||||
getActionLabel(): string {
|
||||
return "Edit";
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.app.setBackEnabled(true);
|
||||
this.app.setActionable(this)
|
||||
this.getCategory();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.app.setActionable(null)
|
||||
}
|
||||
|
||||
getCategory(): void {
|
||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
||||
this.twigsService.getCategory(id)
|
||||
.subscribe(category => {
|
||||
category.amount /= 100;
|
||||
this.app.setTitle(category.title)
|
||||
this.category = category;
|
||||
this.budgetId = category.budgetId;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
.button-delete {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.category-form * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
mat-radio-button {
|
||||
padding-bottom: 15px;
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
<div *ngIf="!currentCategory">
|
||||
<p>Select a category from the list to view details about it or edit it.</p>
|
||||
</div>
|
||||
<div *ngIf="currentCategory" class="form category-form">
|
||||
<mat-form-field (keyup.enter)="doAction()">
|
||||
<input matInput [(ngModel)]="currentCategory.title" placeholder="Name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field (keyup.enter)="doAction()">
|
||||
<textarea matInput [(ngModel)]="currentCategory.description" placeholder="Description"></textarea>
|
||||
</mat-form-field>
|
||||
<mat-form-field (keyup.enter)="doAction()">
|
||||
<input matInput type="text" [(ngModel)]="currentCategory.amount" placeholder="Amount" required currencyMask>
|
||||
</mat-form-field>
|
||||
<mat-radio-group [(ngModel)]="currentCategory.expense">
|
||||
<mat-radio-button [value]="true">Expense</mat-radio-button>
|
||||
<mat-radio-button [value]="false">Income</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<mat-checkbox *ngIf="currentCategory.id" [(ngModel)]="currentCategory.archived">Archived</mat-checkbox>
|
||||
<!--
|
||||
<mat-form-field>
|
||||
<input type="color" matInput [(ngModel)]="currentCategory.color" placeholder="Color">
|
||||
</mat-form-field>
|
||||
-->
|
||||
<button mat-raised-button color="accent" (click)="save()">Save</button>
|
||||
<button class="button-delete" mat-raised-button color="warn" *ngIf="currentCategory.id" (click)="delete()">Delete</button>
|
||||
</div>
|
|
@ -1,25 +0,0 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CategoryFormComponent } from './category-form.component';
|
||||
|
||||
describe('CategoryFormComponent', () => {
|
||||
let component: CategoryFormComponent;
|
||||
let fixture: ComponentFixture<CategoryFormComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CategoryFormComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CategoryFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,62 +0,0 @@
|
|||
import { Component, OnInit, Input, OnDestroy, Inject } from '@angular/core';
|
||||
import { Category } from '../category';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-category-form',
|
||||
templateUrl: './category-form.component.html',
|
||||
styleUrls: ['./category-form.component.css']
|
||||
})
|
||||
export class CategoryFormComponent implements OnInit {
|
||||
|
||||
@Input() budgetId: number;
|
||||
@Input() title: string;
|
||||
@Input() currentCategory: Category;
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.setBackEnabled(true);
|
||||
this.app.setTitle(this.title)
|
||||
}
|
||||
|
||||
save(): void {
|
||||
let observable;
|
||||
if (this.currentCategory.id) {
|
||||
// This is an existing category, update it
|
||||
observable = this.twigsService.updateCategory(
|
||||
this.budgetId,
|
||||
this.currentCategory.id,
|
||||
{
|
||||
name: this.currentCategory.title,
|
||||
description: this.currentCategory.description,
|
||||
amount: this.currentCategory.amount * 100,
|
||||
expense: this.currentCategory.expense,
|
||||
archived: this.currentCategory.archived
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// This is a new category, save it
|
||||
observable = this.twigsService.createCategory(
|
||||
this.budgetId,
|
||||
this.currentCategory.title,
|
||||
this.currentCategory.description,
|
||||
this.currentCategory.amount * 100,
|
||||
this.currentCategory.expense
|
||||
);
|
||||
}
|
||||
observable.subscribe(val => {
|
||||
this.app.goBack();
|
||||
});
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this.twigsService.deleteCategory(this.budgetId, this.currentCategory.id).subscribe(() => {
|
||||
this.app.goBack();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
.categories mat-progress-bar.mat-progress-bar {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
p.mat-line.category-list-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
p.mat-line.category-list-title .remaining {
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
::ng-deep .income .mat-progress-bar-fill::after {
|
||||
background-color: #81C784 !important;
|
||||
}
|
||||
|
||||
::ng-deep .expense .mat-progress-bar-fill::after {
|
||||
background-color: #E57373 !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-progress-bar-buffer {
|
||||
background-color: #F1F1F1 !important;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
::ng-deep .mat-progress-bar-buffer {
|
||||
background-color: #333333 !important;
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<mat-nav-list class="categories">
|
||||
<a mat-list-item *ngFor="let category of categories" routerLink="/budgets/{{ budgetId }}/categories/{{ category.id }}">
|
||||
<p matLine class="category-list-title">
|
||||
<span>
|
||||
{{ category.title }}
|
||||
</span>
|
||||
<span class="remaining">
|
||||
{{ getCategoryRemainingBalance(category) | currency }} remaining of {{ category.amount / 100 | currency }}
|
||||
</span>
|
||||
</p>
|
||||
<mat-progress-bar matLine color="accent" [ngClass]="{'income': !category.expense, 'expense': category.expense}" mode="determinate" #categoryProgress [attr.id]="'cat-' + category.id" value="{{ getCategoryCompletion(category) }}"></mat-progress-bar>
|
||||
</a>
|
||||
</mat-nav-list>
|
|
@ -1,25 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
|
@ -1,54 +0,0 @@
|
|||
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() budgetId: string;
|
||||
@Input() categories: Category[];
|
||||
@Input() categoryBalances: Map<number, number>;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
getCategoryRemainingBalance(category: Category): number {
|
||||
let categoryBalance = this.categoryBalances.get(category.id);
|
||||
if (!categoryBalance) {
|
||||
categoryBalance = 0;
|
||||
}
|
||||
|
||||
if (category.expense) {
|
||||
return (category.amount / 100) + (categoryBalance / 100);
|
||||
} else {
|
||||
return (category.amount / 100) - (categoryBalance / 100);
|
||||
}
|
||||
}
|
||||
|
||||
getCategoryCompletion(category: Category): number {
|
||||
const amount = category.amount > 0 ? category.amount : 1;
|
||||
|
||||
let categoryBalance = this.categoryBalances.get(category.id);
|
||||
if (!categoryBalance) {
|
||||
categoryBalance = 0;
|
||||
}
|
||||
|
||||
// Invert the negative/positive values for calculating progress
|
||||
// since the limit for a category is saved as a positive but the
|
||||
// balance is used in the calculation.
|
||||
if (category.expense) {
|
||||
if (categoryBalance < 0) {
|
||||
categoryBalance = Math.abs(categoryBalance);
|
||||
} else {
|
||||
categoryBalance -= (categoryBalance * 2);
|
||||
}
|
||||
}
|
||||
|
||||
return categoryBalance / amount * 100;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
export class Category {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
expense: boolean;
|
||||
archived: boolean;
|
||||
budgetId: number;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<app-category-form [title]="'Edit Category'" [budgetId]="budgetId" [currentCategory]="category"></app-category-form>
|
|
@ -1,25 +0,0 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EditCategoryComponent } from './edit-category.component';
|
||||
|
||||
describe('EditCategoryComponent', () => {
|
||||
let component: EditCategoryComponent;
|
||||
let fixture: ComponentFixture<EditCategoryComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ EditCategoryComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditCategoryComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { Category } from '../category';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AppComponent } from '../../app.component';
|
||||
import { TWIGS_SERVICE, TwigsService } from '../../shared/twigs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-category',
|
||||
templateUrl: './edit-category.component.html',
|
||||
styleUrls: ['./edit-category.component.css']
|
||||
})
|
||||
export class EditCategoryComponent implements OnInit {
|
||||
|
||||
budgetId: number;
|
||||
category: Category;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private app: AppComponent,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.app.setBackEnabled(true);
|
||||
this.getCategory();
|
||||
}
|
||||
|
||||
getCategory(): void {
|
||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
||||
this.twigsService.getCategory(id)
|
||||
.subscribe(category => {
|
||||
category.amount /= 100;
|
||||
this.app.setTitle(category.title)
|
||||
this.category = category;
|
||||
this.budgetId = category.budgetId;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<app-category-form [title]="'Add Category'" [budgetId]="budgetId" [currentCategory]="category"></app-category-form>
|
|
@ -1,25 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Category } from '../category';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-new-category',
|
||||
templateUrl: './new-category.component.html',
|
||||
styleUrls: ['./new-category.component.css']
|
||||
})
|
||||
export class NewCategoryComponent implements OnInit {
|
||||
|
||||
budgetId: string;
|
||||
category: Category;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.budgetId = this.route.snapshot.paramMap.get('budgetId');
|
||||
this.category = new Category();
|
||||
// TODO: Set random color for category, improve color picker
|
||||
// this.category.color =
|
||||
}
|
||||
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
export interface Actionable {
|
||||
getActionLabel(): string;
|
||||
doAction(): void;
|
||||
}
|
||||
|
||||
export function isActionable(obj: any): obj is Actionable {
|
||||
return typeof obj.prototype.getActionLabel === 'function'
|
||||
&& typeof obj.prototype.doAction === 'function'
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CookieService } from 'ngx-cookie-service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
|
||||
constructor(
|
||||
private cookieService: CookieService
|
||||
) { }
|
||||
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
if (!this.cookieService.check('Authorization')) {
|
||||
return next.handle(req);
|
||||
}
|
||||
let headers = req.headers;
|
||||
headers = headers.append('Authorization', `Basic ${this.cookieService.get('Authorization')}`);
|
||||
this.cookieService.set('Authorization', this.cookieService.get('Authorization'), 14, null, null, true);
|
||||
return next.handle(req.clone({headers: headers}));
|
||||
}
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, Subscriber } from 'rxjs';
|
||||
import { User, UserPermission, Permission } from '../users/user';
|
||||
import { TwigsService } from './twigs.service';
|
||||
import { Budget } from '../budgets/budget';
|
||||
import { Category } from '../categories/category';
|
||||
import { Transaction } from '../transactions/transaction';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { CookieService } from 'ngx-cookie-service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TwigsHttpService implements TwigsService {
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private cookieService: CookieService
|
||||
) { }
|
||||
|
||||
private options = {
|
||||
withCredentials: true
|
||||
};
|
||||
|
||||
private apiUrl = environment.apiUrl;
|
||||
|
||||
// Auth
|
||||
login(email: string, password: string): Observable<User> {
|
||||
// const params = {
|
||||
// 'username': email,
|
||||
// 'password': password
|
||||
// };
|
||||
// return this.http.post<User>(this.apiUrl + '/users/login', params, this.options);
|
||||
const credentials = btoa(`${email}:${password}`)
|
||||
this.cookieService.set('Authorization', credentials, 14, null, null, true);
|
||||
return this.getProfile();
|
||||
}
|
||||
|
||||
register(username: string, email: string, password: string): Observable<User> {
|
||||
const params = {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': password
|
||||
};
|
||||
return this.http.post<User>(this.apiUrl + '/users', params, this.options);
|
||||
}
|
||||
|
||||
logout(): Observable<void> {
|
||||
return Observable.create(emitter => {
|
||||
this.cookieService.delete('Authorization');
|
||||
emitter.next();
|
||||
emitter.complete();
|
||||
})
|
||||
// TODO: Implement this when JWT auth is implemented
|
||||
// return this.http.post<void>(this.apiUrl + '/login?logout', this.options);
|
||||
}
|
||||
|
||||
// Budgets
|
||||
getBudgets(): Observable<Budget[]> {
|
||||
return this.http.get<Budget[]>(this.apiUrl + '/budgets', this.options);
|
||||
}
|
||||
|
||||
getBudget(id: number): Observable<Budget> {
|
||||
return this.http.get<Budget>(`${this.apiUrl}/budgets/${id}`, this.options);
|
||||
}
|
||||
|
||||
createBudget(
|
||||
name: string,
|
||||
description: string,
|
||||
users: UserPermission[],
|
||||
): Observable<Budget> {
|
||||
const params = {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'users': users.map(userPermission => {
|
||||
return {
|
||||
user: userPermission.user.id,
|
||||
permission: Permission[userPermission.permission]
|
||||
};
|
||||
})
|
||||
};
|
||||
return this.http.post<Budget>(this.apiUrl + '/budgets', params, this.options);
|
||||
}
|
||||
|
||||
updateBudget(id: number, changes: object): Observable<Budget> {
|
||||
let budget = changes as Budget;
|
||||
const params = {
|
||||
'name': budget.name,
|
||||
'description': budget.description,
|
||||
'users': budget.users.map(userPermission => {
|
||||
return {
|
||||
user: userPermission.user.id,
|
||||
permission: Permission[userPermission.permission]
|
||||
};
|
||||
})
|
||||
};
|
||||
return this.http.put<Budget>(`${this.apiUrl}/budgets/${id}`, params, this.options);
|
||||
}
|
||||
|
||||
deleteBudget(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/budgets/${id}`, this.options);
|
||||
}
|
||||
|
||||
// Categories
|
||||
getCategories(budgetId: number, count?: number): Observable<Category[]> {
|
||||
const params = {
|
||||
params: new HttpParams()
|
||||
.set('budgetIds', `${budgetId}`)
|
||||
};
|
||||
return this.http.get<Category[]>(`${this.apiUrl}/categories`, Object.assign(params, this.options));
|
||||
}
|
||||
|
||||
getCategory(id: number): Observable<Category> {
|
||||
return this.http.get<Category>(`${this.apiUrl}/categories/${id}`, this.options);
|
||||
}
|
||||
|
||||
createCategory(budgetId: number, name: string, description: string, amount: number, isExpense: boolean): Observable<Category> {
|
||||
const params = {
|
||||
'title': name,
|
||||
'description': description,
|
||||
'amount': amount,
|
||||
'expense': isExpense,
|
||||
'budgetId': budgetId
|
||||
};
|
||||
return this.http.post<Category>(this.apiUrl + '/categories', params, this.options);
|
||||
}
|
||||
|
||||
updateCategory(budgetId: number, id: number, changes: object): Observable<Category> {
|
||||
return this.http.put<Category>(`${this.apiUrl}/categories/${id}`, changes, this.options);
|
||||
}
|
||||
|
||||
deleteCategory(budgetId: number, id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/categories/${id}`, this.options);
|
||||
}
|
||||
|
||||
// Transactions
|
||||
getTransactions(
|
||||
budgetId?: number,
|
||||
categoryId?: number,
|
||||
count?: number,
|
||||
from?: Date
|
||||
): Observable<Transaction[]> {
|
||||
let httpParams = new HttpParams();
|
||||
if (budgetId) {
|
||||
httpParams = httpParams.set('budgetIds', `${budgetId}`);
|
||||
}
|
||||
if (categoryId) {
|
||||
httpParams = httpParams.set('categoryIds', `${categoryId}`);
|
||||
}
|
||||
if (from) {
|
||||
httpParams = httpParams.set('from', from.toISOString());
|
||||
}
|
||||
const params = { params: httpParams };
|
||||
return this.http.get<Transaction[]>(`${this.apiUrl}/transactions`, Object.assign(params, this.options))
|
||||
.pipe(map(transactions => {
|
||||
transactions.forEach(transaction => {
|
||||
transaction.date = new Date(transaction.date);
|
||||
});
|
||||
return transactions;
|
||||
}));
|
||||
}
|
||||
|
||||
getTransaction(id: number): Observable<Transaction> {
|
||||
return this.http.get<Transaction>(`${this.apiUrl}/transactions/${id}`, this.options)
|
||||
.pipe(map(transaction => {
|
||||
transaction.date = new Date(transaction.date);
|
||||
return transaction;
|
||||
}));
|
||||
}
|
||||
|
||||
createTransaction(
|
||||
budgetId: number,
|
||||
name: string,
|
||||
description: string,
|
||||
amount: number,
|
||||
date: Date,
|
||||
expense: boolean,
|
||||
category: number
|
||||
): Observable<Transaction> {
|
||||
const params = {
|
||||
'title': name,
|
||||
'description': description,
|
||||
'date': date.toISOString(),
|
||||
'amount': amount,
|
||||
'expense': expense,
|
||||
'categoryId': category,
|
||||
'budgetId': budgetId
|
||||
};
|
||||
return this.http.post<Transaction>(this.apiUrl + '/transactions', params, this.options);
|
||||
}
|
||||
|
||||
updateTransaction(budgetId: number, id: number, changes: object): Observable<Transaction> {
|
||||
return this.http.put<Transaction>(`${this.apiUrl}/transactions/${id}`, changes, this.options);
|
||||
}
|
||||
|
||||
deleteTransaction(budgetId: number, id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/transactions/${id}`, this.options);
|
||||
}
|
||||
|
||||
// Users
|
||||
getProfile(): Observable<User> {
|
||||
return this.http.get<User>(`${this.apiUrl}/users/me`, this.options);
|
||||
}
|
||||
|
||||
getUsersByUsername(username: string): Observable<User[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.error("Not yet implemented")
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,324 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, Subscriber } from 'rxjs';
|
||||
import { User, UserPermission } from '../users/user';
|
||||
import { TwigsService } from './twigs.service';
|
||||
import { Budget } from '../budgets/budget';
|
||||
import { Category } from '../categories/category';
|
||||
import { Transaction } from '../transactions/transaction';
|
||||
|
||||
/**
|
||||
* This is intended to be a very simple implementation of the TwigsService used for testing out the UI and quickly iterating on it.
|
||||
* It may also prove useful for automated testing.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TwigsLocalService implements TwigsService {
|
||||
|
||||
constructor(
|
||||
private http: HttpClient
|
||||
) { }
|
||||
|
||||
private users: User[] = [new User(1, 'test', 'test@example.com')];
|
||||
private budgets: Budget[] = [];
|
||||
private transactions: Transaction[] = [];
|
||||
private categories: Category[] = [];
|
||||
|
||||
// Auth
|
||||
login(email: string, password: string): Observable<User> {
|
||||
return Observable.create(subscriber => {
|
||||
const filteredUsers = this.users.filter(user => {
|
||||
return (user.email === email || user.username === email);
|
||||
});
|
||||
if (filteredUsers.length !== 0) {
|
||||
subscriber.next(filteredUsers[0]);
|
||||
} else {
|
||||
subscriber.error('No users found');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
register(username: string, email: string, password: string): Observable<User> {
|
||||
return Observable.create(subscriber => {
|
||||
const user = new User();
|
||||
user.username = username;
|
||||
user.email = email;
|
||||
user.id = this.users.length + 1;
|
||||
this.users.push(user);
|
||||
subscriber.next(user);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
logout(): Observable<void> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
// Budgets
|
||||
getBudgets(): Observable<Budget[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.budgets);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
getBudget(id: number): Observable<Budget> {
|
||||
return Observable.create(subscriber => {
|
||||
const budget = this.budgets.filter(it => {
|
||||
return it.id === id;
|
||||
})[0];
|
||||
if (budget) {
|
||||
subscriber.next(budget);
|
||||
} else {
|
||||
subscriber.error('No budget found for given id');
|
||||
}
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
createBudget(
|
||||
name: string,
|
||||
description: string,
|
||||
users: UserPermission[],
|
||||
): Observable<Budget> {
|
||||
return Observable.create(subscriber => {
|
||||
const budget = new Budget();
|
||||
budget.name = name;
|
||||
budget.description = description;
|
||||
budget.users = users;
|
||||
budget.id = this.budgets.length + 1;
|
||||
this.budgets.push(budget);
|
||||
subscriber.next(budget);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
updateBudget(id: number, changes: object): Observable<Budget> {
|
||||
return Observable.create(subscriber => {
|
||||
const budget = this.budgets.filter(it => {
|
||||
return it.id === id;
|
||||
})[0];
|
||||
if (budget) {
|
||||
const index = this.budgets.indexOf(budget);
|
||||
this.updateValues(
|
||||
budget,
|
||||
changes,
|
||||
[
|
||||
'name',
|
||||
'description',
|
||||
'users',
|
||||
]
|
||||
);
|
||||
this.budgets[index] = budget;
|
||||
subscriber.next(budget);
|
||||
} else {
|
||||
subscriber.error('No budget found for given id');
|
||||
}
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
deleteBudget(id: number): Observable<void> {
|
||||
return Observable.create(subscriber => {
|
||||
const budget = this.budgets.filter(it => {
|
||||
return budget.id === id;
|
||||
})[0];
|
||||
if (budget) {
|
||||
const index = this.budgets.indexOf(budget);
|
||||
delete this.budgets[index];
|
||||
subscriber.complete();
|
||||
} else {
|
||||
subscriber.error('No budget found for given id');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Categories
|
||||
getCategories(budgetId: number, count?: number): Observable<Category[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.categories.filter(category => {
|
||||
return category.budgetId === budgetId;
|
||||
}));
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
getCategory(id: number): Observable<Category> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.findById(this.categories, id));
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
createCategory(budgetId: number, name: string, description: string, amount: number, isExpense: boolean): Observable<Category> {
|
||||
return Observable.create(subscriber => {
|
||||
const category = new Category();
|
||||
category.title = name;
|
||||
category.description = description;
|
||||
category.amount = amount;
|
||||
category.expense = isExpense;
|
||||
category.budgetId = budgetId;
|
||||
category.id = this.categories.length + 1;
|
||||
this.categories.push(category);
|
||||
subscriber.next(category);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
updateCategory(budgetId: number, id: number, changes: object): Observable<Category> {
|
||||
return Observable.create(subscriber => {
|
||||
const category = this.findById(this.categories, id);
|
||||
if (category) {
|
||||
const index = this.categories.indexOf(category);
|
||||
this.updateValues(
|
||||
category,
|
||||
changes,
|
||||
[
|
||||
'name',
|
||||
'amount',
|
||||
'isExpense',
|
||||
'budgetId',
|
||||
]
|
||||
);
|
||||
this.categories[index] = category;
|
||||
subscriber.next(category);
|
||||
} else {
|
||||
subscriber.error('No category found for given id');
|
||||
}
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
deleteCategory(budgetId: number, id: number): Observable<void> {
|
||||
return Observable.create(subscriber => {
|
||||
const category = this.findById(this.categories, id);
|
||||
if (category) {
|
||||
const index = this.categories.indexOf(category);
|
||||
delete this.transactions[index];
|
||||
subscriber.complete();
|
||||
} else {
|
||||
subscriber.error('No category found for given id');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Transactions
|
||||
getTransactions(budgetId?: number, categoryId?: number, count?: number): Observable<Transaction[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.transactions.filter(transaction => {
|
||||
let include = true;
|
||||
if (budgetId) {
|
||||
include = transaction.budgetId === budgetId;
|
||||
}
|
||||
if (include && categoryId) {
|
||||
include = transaction.categoryId === categoryId;
|
||||
}
|
||||
return include;
|
||||
}));
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
getTransaction(id: number): Observable<Transaction> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.findById(this.transactions, id));
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
createTransaction(
|
||||
budgetId: number,
|
||||
name: string,
|
||||
description: string,
|
||||
amount: number,
|
||||
date: Date,
|
||||
isExpense: boolean,
|
||||
category: number
|
||||
): Observable<Transaction> {
|
||||
return Observable.create(subscriber => {
|
||||
const transaction = new Transaction();
|
||||
transaction.title = name;
|
||||
transaction.description = description;
|
||||
transaction.amount = amount;
|
||||
transaction.date = date;
|
||||
transaction.expense = isExpense;
|
||||
transaction.categoryId = category;
|
||||
transaction.budgetId = budgetId;
|
||||
transaction.id = this.transactions.length + 1;
|
||||
this.transactions.push(transaction);
|
||||
subscriber.next(transaction);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
updateTransaction(budgetId: number, id: number, changes: object): Observable<Transaction> {
|
||||
return Observable.create(subscriber => {
|
||||
const transaction = this.findById(this.transactions, id);
|
||||
if (transaction) {
|
||||
const index = this.transactions.indexOf(transaction);
|
||||
this.updateValues(
|
||||
transaction,
|
||||
changes,
|
||||
[
|
||||
'title',
|
||||
'description',
|
||||
'date',
|
||||
'amount',
|
||||
'isExpense',
|
||||
'categoryId',
|
||||
'budgetId',
|
||||
'createdBy'
|
||||
]
|
||||
);
|
||||
this.transactions[index] = transaction;
|
||||
subscriber.next(transaction);
|
||||
} else {
|
||||
subscriber.error('No transaction found for given id');
|
||||
}
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
deleteTransaction(budgetId: number, id: number): Observable<void> {
|
||||
return Observable.create(subscriber => {
|
||||
const transaction = this.findById(this.transactions, id);
|
||||
if (transaction) {
|
||||
const index = this.transactions.indexOf(transaction);
|
||||
delete this.transactions[index];
|
||||
subscriber.complete();
|
||||
} else {
|
||||
subscriber.error('No transaction found for given id');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Users
|
||||
getProfile(): Observable<User> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.error("Not yet implemented")
|
||||
});
|
||||
}
|
||||
|
||||
getUsersByUsername(username: string): Observable<User[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.users.filter(user => user.username.indexOf(username) > -1 ));
|
||||
});
|
||||
}
|
||||
|
||||
private updateValues(old: object, changes: object, keys: string[]) {
|
||||
keys.forEach(key => {
|
||||
if (changes[key]) {
|
||||
old[key] = changes[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private findById<T>(items: T[], id: number): T {
|
||||
return items.filter(item => {
|
||||
return item['id'] === id;
|
||||
})[0];
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import { InjectionToken } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { User, UserPermission } from '../users/user';
|
||||
import { Budget } from '../budgets/budget';
|
||||
import { Category } from '../categories/category';
|
||||
import { Transaction } from '../transactions/transaction';
|
||||
|
||||
export interface TwigsService {
|
||||
// Auth
|
||||
login(email: string, password: string): Observable<User>;
|
||||
register(username: string, email: string, password: string): Observable<User>;
|
||||
logout(): Observable<void>;
|
||||
|
||||
// Budgets
|
||||
getBudgets(): Observable<Budget[]>;
|
||||
getBudget(id: number): Observable<Budget>;
|
||||
createBudget(
|
||||
name: string,
|
||||
description: string,
|
||||
users: UserPermission[],
|
||||
): Observable<Budget>;
|
||||
updateBudget(id: number, changes: object): Observable<Budget>;
|
||||
deleteBudget(id: number): Observable<void>;
|
||||
|
||||
// Categories
|
||||
getCategories(budgetId?: number, count?: number): Observable<Category[]>;
|
||||
getCategory(id: number): Observable<Category>;
|
||||
createCategory(budgetId: number, name: string, description: string, amount: number, isExpense: boolean): Observable<Category>;
|
||||
updateCategory(budgetId: number, id: number, changes: object): Observable<Category>;
|
||||
deleteCategory(budgetId: number, id: number): Observable<void>;
|
||||
|
||||
// Transactions
|
||||
getTransactions(
|
||||
budgetId?: number,
|
||||
categoryId?: number,
|
||||
count?: number,
|
||||
from?: Date
|
||||
): Observable<Transaction[]>;
|
||||
getTransaction(id: number): Observable<Transaction>;
|
||||
createTransaction(
|
||||
budgetId: number,
|
||||
name: string,
|
||||
description: string,
|
||||
amount: number,
|
||||
date: Date,
|
||||
isExpense: boolean,
|
||||
category: number
|
||||
): Observable<Transaction>;
|
||||
updateTransaction(budgetId: number, id: number, changes: object): Observable<Transaction>;
|
||||
deleteTransaction(budgetId: number, id: number): Observable<void>;
|
||||
|
||||
getProfile(): Observable<User>;
|
||||
getUsersByUsername(username: string): Observable<User[]>;
|
||||
}
|
||||
|
||||
export let TWIGS_SERVICE = new InjectionToken<TwigsService>('twigs.service');
|
|
@ -1,11 +0,0 @@
|
|||
.transaction-form {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.transaction-form * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
mat-radio-button {
|
||||
padding-bottom: 15px;
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<div [hidden]="currentTransaction">
|
||||
<p>Select a transaction from the list to view details about it or edit it.</p>
|
||||
</div>
|
||||
<div [hidden]="!currentTransaction" *ngIf="currentTransaction" class="form 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="text" [(ngModel)]="currentTransaction.amount" placeholder="Amount" required currencyMask>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput type="date" [ngModel]="transactionDate | date:'yyyy-MM-dd'" (ngModelChange)="transactionDate = $event" placeholder="Date" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput type="time" [(ngModel)]="currentTime" placeholder="Time" required>
|
||||
</mat-form-field>
|
||||
<mat-radio-group [(ngModel)]="currentTransaction.expense" (change)="updateCategories($event)">
|
||||
<mat-radio-button [value]="true">Expense</mat-radio-button>
|
||||
<mat-radio-button [value]="false">Income</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="Category" [(ngModel)]="currentTransaction.categoryId">
|
||||
<mat-option *ngFor="let category of categories" [value]="category.id">
|
||||
{{ category.title }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="accent" (click)="save()">Save</button>
|
||||
<button class="button-delete" mat-raised-button color="warn" *ngIf="currentTransaction.id" (click)="delete()">Delete</button>
|
||||
</div>
|
|
@ -1,25 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
|
@ -1,111 +0,0 @@
|
|||
import { Component, OnInit, Input, OnChanges, OnDestroy, Inject, SimpleChanges } from '@angular/core';
|
||||
import { Transaction } from '../transaction';
|
||||
import { TransactionType } from '../transaction.type';
|
||||
import { Category } from 'src/app/categories/category';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
import { MatRadioChange } from '@angular/material/radio';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-edit-transaction',
|
||||
templateUrl: './add-edit-transaction.component.html',
|
||||
styleUrls: ['./add-edit-transaction.component.css']
|
||||
})
|
||||
export class AddEditTransactionComponent implements OnInit, OnChanges {
|
||||
@Input() title: string;
|
||||
@Input() currentTransaction: Transaction;
|
||||
@Input() budgetId: number;
|
||||
public transactionType = TransactionType;
|
||||
public categories: Category[];
|
||||
public rawAmount: string;
|
||||
public currentTime: string;
|
||||
public transactionDate: string;
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.setTitle(this.title)
|
||||
this.app.setBackEnabled(true);
|
||||
let d: Date, expense: boolean;
|
||||
if (this.currentTransaction) {
|
||||
d = new Date(this.currentTransaction.date);
|
||||
expense = this.currentTransaction.expense;
|
||||
} else {
|
||||
d = new Date();
|
||||
expense = true;
|
||||
}
|
||||
this.updateCategories(new MatRadioChange(undefined, expense));
|
||||
this.transactionDate = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`
|
||||
this.currentTime = d.toTimeString().slice(0, 5);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (!changes.currentTransaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
const d = new Date(changes.currentTransaction.currentValue.date * 1000);
|
||||
this.transactionDate = d.toLocaleDateString(undefined, {year: 'numeric', month: '2-digit', day: '2-digit'});
|
||||
this.currentTime = d.toLocaleTimeString(undefined, {hour: '2-digit', hour12: false, minute: '2-digit'});
|
||||
}
|
||||
|
||||
updateCategories(change: MatRadioChange) {
|
||||
this.twigsService.getCategories(this.budgetId)
|
||||
.subscribe(newCategories => {
|
||||
this.categories = newCategories.filter(category => category.expense === change.value)
|
||||
})
|
||||
}
|
||||
|
||||
save(): void {
|
||||
// The amount will be input as a decimal value so we need to convert it
|
||||
// to an integer
|
||||
let observable;
|
||||
this.currentTransaction.date = new Date();
|
||||
const dateParts = this.transactionDate.split('-');
|
||||
this.currentTransaction.date.setFullYear(parseInt(dateParts[0], 10));
|
||||
this.currentTransaction.date.setMonth(parseInt(dateParts[1], 10) - 1);
|
||||
this.currentTransaction.date.setDate(parseInt(dateParts[2], 10));
|
||||
const timeParts = this.currentTime.split(':');
|
||||
this.currentTransaction.date.setHours(parseInt(timeParts[0], 10));
|
||||
this.currentTransaction.date.setMinutes(parseInt(timeParts[1], 10));
|
||||
if (this.currentTransaction.id) {
|
||||
// This is an existing transaction, update it
|
||||
observable = this.twigsService.updateTransaction(
|
||||
this.budgetId,
|
||||
this.currentTransaction.id,
|
||||
{
|
||||
name: this.currentTransaction.title,
|
||||
description: this.currentTransaction.description,
|
||||
amount: this.currentTransaction.amount * 100,
|
||||
date: this.currentTransaction.date,
|
||||
categoryId: this.currentTransaction.categoryId,
|
||||
expense: this.currentTransaction.expense
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// This is a new transaction, save it
|
||||
observable = this.twigsService.createTransaction(
|
||||
this.budgetId,
|
||||
this.currentTransaction.title,
|
||||
this.currentTransaction.description,
|
||||
this.currentTransaction.amount * 100,
|
||||
this.currentTransaction.date,
|
||||
this.currentTransaction.expense,
|
||||
this.currentTransaction.categoryId,
|
||||
);
|
||||
}
|
||||
|
||||
observable.subscribe(val => {
|
||||
this.app.goBack();
|
||||
});
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this.twigsService.deleteTransaction(this.budgetId, this.currentTransaction.id).subscribe(() => {
|
||||
this.app.goBack();
|
||||
});
|
||||
}
|
||||
}
|