Compare commits
2 commits
Author | SHA1 | Date | |
---|---|---|---|
8a09ae2a15 | |||
e3e8d4805c |
136 changed files with 26 additions and 33664 deletions
|
@ -1,12 +0,0 @@
|
||||||
FROM mcr.microsoft.com/devcontainers/javascript-node:0-16-bullseye
|
|
||||||
|
|
||||||
# [Optional] Uncomment this section to install additional OS packages.
|
|
||||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|
||||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
|
||||||
|
|
||||||
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
|
||||||
# ARG EXTRA_NODE_VERSION=10
|
|
||||||
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
|
||||||
|
|
||||||
# [Optional] Uncomment if you want to install more global node modules
|
|
||||||
# RUN su node -c "npm install -g <your-package-list-here>"
|
|
|
@ -1,23 +0,0 @@
|
||||||
// Update the VARIANT arg in docker-compose.yml to pick a Node.js version
|
|
||||||
{
|
|
||||||
"name": "Twigs Web",
|
|
||||||
"dockerComposeFile": "docker-compose.yml",
|
|
||||||
"service": "app",
|
|
||||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
|
||||||
|
|
||||||
// Features to add to the dev container. More info: https://containers.dev/implementors/features.
|
|
||||||
// "features": {},
|
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
// This can be used to network with other containers or with the host.
|
|
||||||
"forwardPorts": [4200, "backend:8080"]
|
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
// "postCreateCommand": "yarn install",
|
|
||||||
|
|
||||||
// Configure tool-specific properties.
|
|
||||||
// "customizations": {},
|
|
||||||
|
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
|
||||||
// "remoteUser": "root"
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- ../..:/workspaces:cached
|
|
||||||
|
|
||||||
# Overrides default command so things don't shut down after the process ends.
|
|
||||||
command: sleep infinity
|
|
||||||
|
|
||||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
|
||||||
network_mode: service:db
|
|
||||||
|
|
||||||
# Uncomment the next line to use a non-root user for all processes.
|
|
||||||
user: node
|
|
||||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
|
||||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
|
||||||
|
|
||||||
backend:
|
|
||||||
image: ghcr.io/wbrawner/twigs-server:main
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
TWIGS_DB_HOST: db
|
|
||||||
TWIGS_DB_NAME: postgres
|
|
||||||
TWIGS_DB_USER: postgres
|
|
||||||
TWIGS_DB_PASS: postgres
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- postgres-data:/var/lib/postgresql/data
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_DB: postgres
|
|
||||||
|
|
||||||
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
|
|
||||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres-data:
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"projects": {
|
|
||||||
"default": "budget-c7da5"
|
|
||||||
}
|
|
||||||
}
|
|
20
.github/workflows/firebase-hosting-merge.yml
vendored
20
.github/workflows/firebase-hosting-merge.yml
vendored
|
@ -1,20 +0,0 @@
|
||||||
# This file was auto-generated by the Firebase CLI
|
|
||||||
# https://github.com/firebase/firebase-tools
|
|
||||||
|
|
||||||
name: Deploy to Firebase Hosting on merge
|
|
||||||
'on':
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
jobs:
|
|
||||||
build_and_deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- run: npm ci && npm run package
|
|
||||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
|
||||||
with:
|
|
||||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
|
||||||
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_BUDGET_C7DA5 }}'
|
|
||||||
channelId: live
|
|
||||||
projectId: budget-c7da5
|
|
24
.vscode/launch.json
vendored
24
.vscode/launch.json
vendored
|
@ -1,24 +0,0 @@
|
||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Launch Chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "pwa-chrome",
|
|
||||||
"url": "http://localhost:4200",
|
|
||||||
"webRoot": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ng serve",
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"runtimeExecutable": "${workspaceFolder}/node_modules/@angular/cli/bin/ng",
|
|
||||||
"runtimeArgs": [
|
|
||||||
"serve"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
144
angular.json
144
angular.json
|
@ -1,144 +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": {
|
|
||||||
"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": [],
|
|
||||||
"vendorChunk": true,
|
|
||||||
"extractLicenses": false,
|
|
||||||
"buildOptimizer": false,
|
|
||||||
"sourceMap": true,
|
|
||||||
"optimization": false,
|
|
||||||
"namedChunks": true
|
|
||||||
},
|
|
||||||
"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,
|
|
||||||
"namedChunks": false,
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultConfiguration": ""
|
|
||||||
},
|
|
||||||
"serve": {
|
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
|
||||||
"options": {
|
|
||||||
"buildTarget": "twigs:build"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"buildTarget": "twigs:build:production"
|
|
||||||
},
|
|
||||||
"codeserver": {
|
|
||||||
"buildTarget": "twigs:build:codeserver"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"extract-i18n": {
|
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
|
||||||
"options": {
|
|
||||||
"buildTarget": "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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cli": {
|
|
||||||
"analytics": "b8304464-255e-47bb-976a-7ed81af63238"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"schemaVersion": 2,
|
|
||||||
"dockerfilePath": "./Dockerfile"
|
|
||||||
}
|
|
|
@ -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,16 +0,0 @@
|
||||||
{
|
|
||||||
"hosting": {
|
|
||||||
"public": "dist/twigs",
|
|
||||||
"ignore": [
|
|
||||||
"firebase.json",
|
|
||||||
"**/.*",
|
|
||||||
"**/node_modules/**"
|
|
||||||
],
|
|
||||||
"rewrites": [
|
|
||||||
{
|
|
||||||
"source": "**",
|
|
||||||
"destination": "/index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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/**"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
29127
package-lock.json
generated
29127
package-lock.json
generated
File diff suppressed because it is too large
Load diff
54
package.json
54
package.json
|
@ -1,58 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "budget",
|
"name": "twigs",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"start": ""
|
||||||
"start": "ng serve --configuration=production --host '0.0.0.0'",
|
|
||||||
"code-server": "ng serve --configuration=codeserver --host \"0.0.0.0\" --disable-host-check --poll=2000",
|
|
||||||
"build": "ng build",
|
|
||||||
"package": "ng build --configuration=production --service-worker",
|
|
||||||
"publish": "ng build --configuration=production --service-worker && firebase deploy",
|
|
||||||
"test": "ng test",
|
|
||||||
"lint": "ng lint",
|
|
||||||
"e2e": "ng e2e",
|
|
||||||
"update": "ncu -u"
|
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^17.2.3",
|
"chart.js": "^3.7.0"
|
||||||
"@angular/cdk": "^17.2.1",
|
|
||||||
"@angular/common": "^17.2.3",
|
|
||||||
"@angular/compiler": "^17.2.3",
|
|
||||||
"@angular/core": "^17.2.3",
|
|
||||||
"@angular/forms": "^17.2.3",
|
|
||||||
"@angular/material": "^16.2.0",
|
|
||||||
"@angular/platform-browser": "^17.2.3",
|
|
||||||
"@angular/platform-browser-dynamic": "^17.2.3",
|
|
||||||
"@angular/router": "^17.2.3",
|
|
||||||
"@angular/service-worker": "^17.2.3",
|
|
||||||
"chart.js": "^3.7.0",
|
|
||||||
"core-js": "^3.20.3",
|
|
||||||
"ng2-charts": "^3.0.8",
|
|
||||||
"rxjs": "^7.5.2",
|
|
||||||
"tslib": "^2.3.1",
|
|
||||||
"zone.js": "~0.14.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^17.2.2",
|
"eslint": "^8.7.0"
|
||||||
"@angular/cli": "^17.2.2",
|
|
||||||
"@angular/compiler-cli": "^17.2.3",
|
|
||||||
"@angular/language-service": "^17.2.3",
|
|
||||||
"@types/jasmine": "~3.10.3",
|
|
||||||
"@types/jasminewd2": "^2.0.10",
|
|
||||||
"@types/node": "^17.0.10",
|
|
||||||
"eslint": "^8.7.0",
|
|
||||||
"jasmine-core": "~4.0.0",
|
|
||||||
"jasmine-spec-reporter": "~7.0.0",
|
|
||||||
"karma": "~6.3.11",
|
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
|
||||||
"karma-jasmine": "~4.0.1",
|
|
||||||
"karma-jasmine-html-reporter": "^1.7.0",
|
|
||||||
"npm-check-updates": "^15.0.1",
|
|
||||||
"protractor": "^7.0.0",
|
|
||||||
"ts-node": "~10.4.0",
|
|
||||||
"tslint": "^6.1.3",
|
|
||||||
"typescript": "5.3.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
|
|
@ -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: 'transactions', component: TransactionsComponent },
|
|
||||||
{ path: 'transactions/new', component: NewTransactionComponent },
|
|
||||||
{ path: 'transactions/:id', component: TransactionDetailsComponent },
|
|
||||||
{ path: 'categories', component: CategoriesComponent },
|
|
||||||
{ path: 'categories/new', component: NewCategoryComponent },
|
|
||||||
{ path: 'categories/:id', component: CategoryDetailsComponent },
|
|
||||||
{ path: '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,35 +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()" *ngIf="loggedIn">
|
|
||||||
<a mat-list-item routerLink="">{{ getUsername() }}</a>
|
|
||||||
<a mat-list-item routerLink="/budgets">Budgets</a>
|
|
||||||
<a mat-list-item (click)="logout()">Logout</a>
|
|
||||||
</mat-nav-list>
|
|
||||||
<mat-nav-list (click)="sidenav.close()" *ngIf="!loggedIn">
|
|
||||||
<a mat-list-item routerLink="/login">Login</a>
|
|
||||||
<a mat-list-item routerLink="/register">Register</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, waitForAsync } from '@angular/core/testing';
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
describe('AppComponent', () => {
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [
|
|
||||||
AppComponent
|
|
||||||
],
|
|
||||||
}).compileComponents();
|
|
||||||
}));
|
|
||||||
it('should create the app', waitForAsync(() => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.debugElement.componentInstance;
|
|
||||||
expect(app).toBeTruthy();
|
|
||||||
}));
|
|
||||||
it(`should have as title 'budget'`, waitForAsync(() => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.debugElement.componentInstance;
|
|
||||||
expect(app.title).toEqual('budget');
|
|
||||||
}));
|
|
||||||
it('should render title in a h1 tag', waitForAsync(() => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
fixture.detectChanges();
|
|
||||||
const compiled = fixture.debugElement.nativeElement;
|
|
||||||
expect(compiled.querySelector('h1').textContent).toContain('Welcome to budget!');
|
|
||||||
}));
|
|
||||||
});
|
|
|
@ -1,134 +0,0 @@
|
||||||
import { Component, Inject, ApplicationRef, ChangeDetectorRef, OnInit } from '@angular/core';
|
|
||||||
import { DOCUMENT, Location } from '@angular/common';
|
|
||||||
import { User } from './users/user';
|
|
||||||
import { TWIGS_SERVICE, TwigsService } from './shared/twigs.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 implements OnInit {
|
|
||||||
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 router: Router,
|
|
||||||
private appRef: ApplicationRef,
|
|
||||||
private updates: SwUpdate,
|
|
||||||
private changeDetector: ChangeDetectorRef,
|
|
||||||
private storage: Storage,
|
|
||||||
@Inject(DOCUMENT) private document: Document
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
const unauthenticatedRoutes = [
|
|
||||||
'',
|
|
||||||
'/',
|
|
||||||
'/login',
|
|
||||||
'/register'
|
|
||||||
]
|
|
||||||
let auth = this.storage.getItem('Authorization');
|
|
||||||
let userId = this.storage.getItem('userId');
|
|
||||||
let savedUser = JSON.parse(this.storage.getItem('user')) as User;
|
|
||||||
if (auth && auth.length == 255 && userId) {
|
|
||||||
if (savedUser) {
|
|
||||||
this.user.next(savedUser);
|
|
||||||
}
|
|
||||||
this.twigsService.getProfile(userId).then(fetchedUser => {
|
|
||||||
this.storage.setItem('user', JSON.stringify(fetchedUser));
|
|
||||||
this.user.next(fetchedUser);
|
|
||||||
if (unauthenticatedRoutes.indexOf(this.location.path()) != -1) {
|
|
||||||
//TODO: Save last opened budget and redirect to there instead of the main list
|
|
||||||
this.router.navigateByUrl("/budgets");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (unauthenticatedRoutes.indexOf(this.location.path()) == -1) {
|
|
||||||
this.router.navigateByUrl(`/login?redirect=${this.location.path()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updates.versionUpdates.subscribe(
|
|
||||||
event => {
|
|
||||||
if (event.type == "VERSION_READY") {
|
|
||||||
console.log('current version is', event.currentVersion);
|
|
||||||
console.log('available version is', event.latestVersion);
|
|
||||||
// TODO: Prompt user to click something to update
|
|
||||||
this.updates.activateUpdate();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
err => {
|
|
||||||
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true));
|
|
||||||
const everySixHours$ = interval(6 * 60 * 60 * 1000);
|
|
||||||
const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
|
|
||||||
everySixHoursOnceAppIsStable$.subscribe(() => this.updates.checkForUpdate());
|
|
||||||
this.user.subscribe(
|
|
||||||
user => {
|
|
||||||
if (user) {
|
|
||||||
this.loggedIn = true;
|
|
||||||
} else {
|
|
||||||
this.loggedIn = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const darkMode = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
this.handleDarkModeChanges(darkMode);
|
|
||||||
darkMode.addEventListener('change', (e => this.handleDarkModeChanges(e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
getUsername(): String {
|
|
||||||
return this.user.value.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
goBack(): void {
|
|
||||||
this.location.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
logout(): void {
|
|
||||||
this.twigsService.logout().then(_ => {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDarkModeChanges(darkMode: any) {
|
|
||||||
const themeColor = this.document.getElementsByName('theme-color')[0] as HTMLMetaElement;
|
|
||||||
let themeColorValue: string;
|
|
||||||
if (darkMode.matches) {
|
|
||||||
themeColorValue = '#333333';
|
|
||||||
} else {
|
|
||||||
themeColorValue = '#F1F1F1';
|
|
||||||
}
|
|
||||||
themeColor.content = themeColorValue;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,108 +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 { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
|
||||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
|
||||||
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
|
||||||
import { MatLegacyListModule as MatListModule } from '@angular/material/legacy-list';
|
|
||||||
import { MatLegacyProgressBarModule as MatProgressBarModule } from '@angular/material/legacy-progress-bar';
|
|
||||||
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
|
|
||||||
import { MatLegacyRadioModule as MatRadioModule } from '@angular/material/legacy-radio';
|
|
||||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
|
||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
|
||||||
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
|
|
||||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-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 { ServiceWorkerModule } from '@angular/service-worker';
|
|
||||||
import { CategoryBreakdownComponent } from './categories/category-breakdown/category-breakdown.component';
|
|
||||||
import { NgChartsModule } 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 { 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';
|
|
||||||
|
|
||||||
@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,
|
|
||||||
NgChartsModule,
|
|
||||||
MatCheckboxModule,
|
|
||||||
MatCardModule,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
|
||||||
{ provide: TWIGS_SERVICE, useClass: TwigsHttpService },
|
|
||||||
{ provide: Storage, useValue: window.localStorage },
|
|
||||||
// { provide: TWIGS_SERVICE, useClass: TwigsLocalService },
|
|
||||||
],
|
|
||||||
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 autocapitalize="words">
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field>
|
|
||||||
<textarea matInput [(ngModel)]="budget.description" placeholder="Description" autocapitalize="sentences"></textarea>
|
|
||||||
</mat-form-field>
|
|
||||||
<button mat-raised-button color="accent" (click)="save()">Save</button>
|
|
||||||
<button class="button-delete" mat-raised-button color="warn" *ngIf="!create" (click)="delete()">Delete</button>
|
|
||||||
</div>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { AddEditBudgetComponent } from './add-edit-budget.component';
|
|
||||||
|
|
||||||
describe('AddEditBudgetComponent', () => {
|
|
||||||
let component: AddEditBudgetComponent;
|
|
||||||
let fixture: ComponentFixture<AddEditBudgetComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ AddEditBudgetComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(AddEditBudgetComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,70 +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';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
|
|
||||||
@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;
|
|
||||||
@Input() create: boolean;
|
|
||||||
public users: UserPermission[];
|
|
||||||
public searchedUsers: User[] = [];
|
|
||||||
public isLoading = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private app: AppComponent,
|
|
||||||
private router: Router,
|
|
||||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
|
||||||
) {
|
|
||||||
this.app.setTitle(this.title)
|
|
||||||
this.app.setBackEnabled(true);
|
|
||||||
this.users = [new UserPermission(this.app.user.value.id, Permission.OWNER)];
|
|
||||||
}
|
|
||||||
|
|
||||||
save(): void {
|
|
||||||
let promise: Promise<Budget>;
|
|
||||||
this.isLoading = true;
|
|
||||||
if (this.create) {
|
|
||||||
// This is a new budget, save it
|
|
||||||
promise = this.twigsService.createBudget(
|
|
||||||
this.budget.id,
|
|
||||||
this.budget.name,
|
|
||||||
this.budget.description,
|
|
||||||
this.users
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// This is an existing budget, update it
|
|
||||||
promise = this.twigsService.updateBudget(this.budget.id, this.budget);
|
|
||||||
}
|
|
||||||
// TODO: Check if it was actually successful or not
|
|
||||||
promise.then(_ => {
|
|
||||||
this.app.goBack();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(): void {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.twigsService.deleteBudget(this.budget.id)
|
|
||||||
.then(() => {
|
|
||||||
this.router.navigateByUrl("/budgets");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement a search box with suggestions to add users
|
|
||||||
searchUsers(username: string) {
|
|
||||||
this.twigsService.getUsersByUsername(username).then(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': budgetBalance > 0, 'expense': budgetBalance < 0}">{{ budgetBalance / 100 | currency }}</span>
|
|
||||||
</h2>
|
|
||||||
<app-category-breakdown [barChartLabels]="barChartLabels" [barChartData]="barChartData">
|
|
||||||
</app-category-breakdown>
|
|
||||||
<div class="transaction-navigation">
|
|
||||||
<a mat-button routerLink="/transactions" [queryParams]="{budgetIds: budget.id}" *ngIf="budget">View Transactions</a>
|
|
||||||
</div>
|
|
||||||
</mat-card>
|
|
||||||
<mat-card class="dashboard-categories" [hidden]="!budget">
|
|
||||||
<h3 class="categories">Income</h3>
|
|
||||||
<a mat-button routerLink="/categories/new" [queryParams]="{budgetId: budget.id}" class="view-all" *ngIf="budget">Add Category</a>
|
|
||||||
<div class="no-categories" *ngIf="!income || income.length === 0">
|
|
||||||
<a mat-button routerLink="/categories/new" [queryParams]="{budgetId: budget.id}" *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="/categories/new" [queryParams]="{budgetId: budget.id, expense: true}" class="view-all" *ngIf="budget">Add Category</a>
|
|
||||||
<div class="no-categories" *ngIf="!expenses || expenses.length === 0">
|
|
||||||
<a mat-button routerLink="/categories/new" [queryParams]="{budgetId: budget.id, expense: true}" *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="/transactions/new" [queryParams]="{budgetId: budget.id}" *ngIf="budget">
|
|
||||||
<mat-icon aria-label="Add">add</mat-icon>
|
|
||||||
</a>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { BudgetDetailsComponent } from './budget-details.component';
|
|
||||||
|
|
||||||
describe('BudgetDetailsComponent', () => {
|
|
||||||
let component: BudgetDetailsComponent;
|
|
||||||
let fixture: ComponentFixture<BudgetDetailsComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ BudgetDetailsComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(BudgetDetailsComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,173 +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 { ChartDataset } 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 budgetBalance: number;
|
|
||||||
public transactions: Transaction[];
|
|
||||||
public expenses: Category[] = [];
|
|
||||||
public income: Category[] = [];
|
|
||||||
categoryBalances: Map<string, number>;
|
|
||||||
expectedIncome = 0;
|
|
||||||
actualIncome = 0;
|
|
||||||
expectedExpenses = 0;
|
|
||||||
actualExpenses = 0;
|
|
||||||
barChartLabels: string[] = ['Income', 'Expenses'];
|
|
||||||
barChartData: ChartDataset[] = [
|
|
||||||
{ data: [0, 0], label: 'Expected' },
|
|
||||||
{ data: [0, 0], label: 'Actual' },
|
|
||||||
];
|
|
||||||
from: Date
|
|
||||||
to: Date
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private app: AppComponent,
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
|
||||||
private router: Router,
|
|
||||||
) {
|
|
||||||
let fromStr = this.route.snapshot.queryParamMap.get('from');
|
|
||||||
if (fromStr) {
|
|
||||||
let fromDate = new Date(fromStr);
|
|
||||||
if (!isNaN(fromDate.getTime())) {
|
|
||||||
this.from = fromDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.from) {
|
|
||||||
let date = new Date();
|
|
||||||
date.setHours(0);
|
|
||||||
date.setMinutes(0);
|
|
||||||
date.setSeconds(0);
|
|
||||||
date.setMilliseconds(0);
|
|
||||||
date.setDate(1);
|
|
||||||
this.from = date;
|
|
||||||
}
|
|
||||||
|
|
||||||
let toStr = this.route.snapshot.queryParamMap.get('to');
|
|
||||||
if (toStr) {
|
|
||||||
let toDate = new Date(toStr);
|
|
||||||
if (!isNaN(toDate.getTime())) {
|
|
||||||
this.to = toDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.getBudget();
|
|
||||||
this.app.setBackEnabled(false);
|
|
||||||
this.app.setActionable(this)
|
|
||||||
this.categoryBalances = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.app.setActionable(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
getBudget() {
|
|
||||||
const id = this.route.snapshot.paramMap.get('id');
|
|
||||||
this.twigsService.getBudget(id)
|
|
||||||
.then(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(): void {
|
|
||||||
const id = this.route.snapshot.paramMap.get('id');
|
|
||||||
this.twigsService.getBudgetBalance(id, this.from, this.to)
|
|
||||||
.then(balance => {
|
|
||||||
this.budgetBalance = balance;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
.then(transactions => this.transactions = <Transaction[]>transactions);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCategories() {
|
|
||||||
const categories = await this.twigsService.getCategories(this.budget.id)
|
|
||||||
const categoryBalances = new Map<string, number>();
|
|
||||||
let categoryBalancesCount = 0;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const balance = await this.twigsService.getCategoryBalance(category.id, this.from, this.to)
|
|
||||||
console.log(balance);
|
|
||||||
if (category.expense) {
|
|
||||||
this.actualExpenses += balance * -1;
|
|
||||||
} else {
|
|
||||||
this.actualIncome += balance;
|
|
||||||
}
|
|
||||||
categoryBalances.set(category.id, balance);
|
|
||||||
if (categoryBalancesCount === categories.length - 1) {
|
|
||||||
// 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.
|
|
||||||
this.categoryBalances = categoryBalances;
|
|
||||||
this.updateBarChart();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
categoryBalancesCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { BudgetsComponent } from './budget.component';
|
|
||||||
|
|
||||||
describe('BudgetsComponent', () => {
|
|
||||||
let component: BudgetsComponent;
|
|
||||||
let fixture: ComponentFixture<BudgetsComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ BudgetsComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(BudgetsComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,52 +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()
|
|
||||||
.then(
|
|
||||||
budgets => {
|
|
||||||
console.log(budgets)
|
|
||||||
this.budgets = budgets;
|
|
||||||
this.loading = false;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.log(error)
|
|
||||||
this.loading = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { UserPermission } from '../users/user';
|
|
||||||
import { randomId } from '../shared/utils';
|
|
||||||
|
|
||||||
export class Budget {
|
|
||||||
id: string = randomId();
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
users: UserPermission[];
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
<app-add-edit-budget [title]="'Edit Budget'" [budget]="budget" [create]="false"></app-add-edit-budget>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { EditBudgetComponent } from './edit-budget.component';
|
|
||||||
|
|
||||||
describe('EditBudgetComponent', () => {
|
|
||||||
let component: EditBudgetComponent;
|
|
||||||
let fixture: ComponentFixture<EditBudgetComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
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 = this.route.snapshot.paramMap.get('id');
|
|
||||||
this.twigsService.getBudget(id)
|
|
||||||
.then(budget => {
|
|
||||||
this.budget = budget;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
<app-add-edit-budget [title]="'Add Budget'" [budget]="budget" [create]="true"></app-add-edit-budget>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { NewBudgetComponent } from './new-budget.component';
|
|
||||||
|
|
||||||
describe('NewBudgetComponent', () => {
|
|
||||||
let component: NewBudgetComponent;
|
|
||||||
let fixture: ComponentFixture<NewBudgetComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
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 { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { CategoriesComponent } from './categories.component';
|
|
||||||
|
|
||||||
describe('CategoriesComponent', () => {
|
|
||||||
let component: CategoriesComponent;
|
|
||||||
let fixture: ComponentFixture<CategoriesComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ CategoriesComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(CategoriesComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,61 +0,0 @@
|
||||||
import { Component, OnInit, Inject } from '@angular/core';
|
|
||||||
import { Category } from './category';
|
|
||||||
import { AppComponent } from '../app.component';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { TWIGS_SERVICE, TwigsService } from '../shared/twigs.service';
|
|
||||||
import { Transaction } from '../transactions/transaction';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-categories',
|
|
||||||
templateUrl: './categories.component.html',
|
|
||||||
styleUrls: ['./categories.component.css']
|
|
||||||
})
|
|
||||||
export class CategoriesComponent implements OnInit {
|
|
||||||
|
|
||||||
budgetId: string;
|
|
||||||
public categories: Category[];
|
|
||||||
public categoryBalances: Map<string, number>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private app: AppComponent,
|
|
||||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.budgetId = 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).then(categories => {
|
|
||||||
this.categories = categories;
|
|
||||||
for (const category of this.categories) {
|
|
||||||
this.getCategoryBalance(category).then(balance => this.categoryBalances.set(category.id, balance));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getCategoryBalance(category: Category): Promise<number> {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
let transactions: Transaction[]
|
|
||||||
try {
|
|
||||||
transactions = await this.twigsService.getTransactions(this.budgetId, category.id)
|
|
||||||
} catch(e) {
|
|
||||||
reject(e)
|
|
||||||
}
|
|
||||||
let balance = 0;
|
|
||||||
for (const transaction of transactions) {
|
|
||||||
if (transaction.expense) {
|
|
||||||
balance -= transaction.amount;
|
|
||||||
} else {
|
|
||||||
balance += transaction.amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve(balance);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
<div class="category-breakdown">
|
|
||||||
<canvas baseChart
|
|
||||||
[datasets]="barChartData"
|
|
||||||
[options]="barChartOptions"
|
|
||||||
[labels]="barChartLabels"
|
|
||||||
[legend]="barChartLegend"
|
|
||||||
[type]="barChartType">
|
|
||||||
</canvas>
|
|
||||||
</div>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { CategoryBreakdownComponent } from './category-breakdown.component';
|
|
||||||
|
|
||||||
describe('CategoryBreakdownComponent', () => {
|
|
||||||
let component: CategoryBreakdownComponent;
|
|
||||||
let fixture: ComponentFixture<CategoryBreakdownComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ CategoryBreakdownComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(CategoryBreakdownComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, SimpleChange, ViewChild } from '@angular/core';
|
|
||||||
import { Category } from '../category';
|
|
||||||
import { CategoriesComponent } from '../categories.component';
|
|
||||||
import { ChartConfiguration, ChartType, ChartDataset } from 'chart.js';
|
|
||||||
import { BaseChartDirective } 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: ChartConfiguration['options'] = {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
ticks: {
|
|
||||||
// beginAtZero: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
y: {}
|
|
||||||
},
|
|
||||||
indexAxis: 'y'
|
|
||||||
};
|
|
||||||
@Input() barChartLabels: string[];
|
|
||||||
@Input() barChartData: ChartDataset[] = [
|
|
||||||
{ data: [0, 0, 0, 0], label: '' },
|
|
||||||
];
|
|
||||||
barChartType: ChartType = 'bar';
|
|
||||||
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" [budgetIds]="[budgetId]" [categoryIds]="[category.id]"></app-transaction-list>
|
|
||||||
<a mat-fab routerLink="/transactions/new" [queryParams]="{budgetId: budgetId}">
|
|
||||||
<mat-icon aria-label="Add">add</mat-icon>
|
|
||||||
</a>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { CategoryDetailsComponent } from './category-details.component';
|
|
||||||
|
|
||||||
describe('CategoryDetailsComponent', () => {
|
|
||||||
let component: CategoryDetailsComponent;
|
|
||||||
let fixture: ComponentFixture<CategoryDetailsComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
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: string;
|
|
||||||
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 = this.route.snapshot.paramMap.get('id');
|
|
||||||
this.twigsService.getCategory(id)
|
|
||||||
.then(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)="save()">
|
|
||||||
<input matInput [(ngModel)]="currentCategory.title" placeholder="Name" required autocapitalize="words">
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field (keyup.enter)="save()">
|
|
||||||
<textarea matInput [(ngModel)]="currentCategory.description" placeholder="Description" autocapitalize="sentences"></textarea>
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field (keyup.enter)="save()">
|
|
||||||
<input matInput type="input" [(ngModel)]="currentCategory.amount" placeholder="Amount" required step="0.01">
|
|
||||||
</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="!create" (click)="delete()">Delete</button>
|
|
||||||
</div>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { CategoryFormComponent } from './category-form.component';
|
|
||||||
|
|
||||||
describe('CategoryFormComponent', () => {
|
|
||||||
let component: CategoryFormComponent;
|
|
||||||
let fixture: ComponentFixture<CategoryFormComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
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, 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';
|
|
||||||
import { decimalToInteger } from 'src/app/shared/utils';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-category-form',
|
|
||||||
templateUrl: './category-form.component.html',
|
|
||||||
styleUrls: ['./category-form.component.css']
|
|
||||||
})
|
|
||||||
export class CategoryFormComponent implements OnInit {
|
|
||||||
|
|
||||||
@Input() budgetId: string;
|
|
||||||
@Input() title: string;
|
|
||||||
@Input() currentCategory: Category;
|
|
||||||
@Input() create: boolean;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private app: AppComponent,
|
|
||||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.app.setBackEnabled(true);
|
|
||||||
this.app.setTitle(this.title)
|
|
||||||
}
|
|
||||||
|
|
||||||
save(): void {
|
|
||||||
let promise;
|
|
||||||
this.currentCategory.amount = decimalToInteger(String(this.currentCategory.amount))
|
|
||||||
if (this.create) {
|
|
||||||
// This is a new category, save it
|
|
||||||
promise = this.twigsService.createCategory(
|
|
||||||
this.currentCategory.id,
|
|
||||||
this.budgetId,
|
|
||||||
this.currentCategory.title,
|
|
||||||
this.currentCategory.description,
|
|
||||||
this.currentCategory.amount,
|
|
||||||
this.currentCategory.expense
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// This is an existing category, update it
|
|
||||||
const updatedCategory: Category = {
|
|
||||||
...this.currentCategory,
|
|
||||||
}
|
|
||||||
promise = this.twigsService.updateCategory(
|
|
||||||
this.currentCategory.id,
|
|
||||||
this.currentCategory
|
|
||||||
);
|
|
||||||
}
|
|
||||||
promise.then(_ => {
|
|
||||||
this.app.goBack();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(): void {
|
|
||||||
this.twigsService.deleteCategory(this.currentCategory.id).then(() => {
|
|
||||||
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="/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 { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { CategoryListComponent } from './category-list.component';
|
|
||||||
|
|
||||||
describe('CategoryListComponent', () => {
|
|
||||||
let component: CategoryListComponent;
|
|
||||||
let fixture: ComponentFixture<CategoryListComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
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<string, 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,11 +0,0 @@
|
||||||
import { randomId } from '../shared/utils';
|
|
||||||
|
|
||||||
export class Category {
|
|
||||||
id: string = randomId();
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
amount: number;
|
|
||||||
expense: boolean;
|
|
||||||
archived: boolean;
|
|
||||||
budgetId: string;
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
<app-category-form [title]="'Edit Category'" [budgetId]="budgetId" [currentCategory]="category" [create]="false"></app-category-form>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { EditCategoryComponent } from './edit-category.component';
|
|
||||||
|
|
||||||
describe('EditCategoryComponent', () => {
|
|
||||||
let component: EditCategoryComponent;
|
|
||||||
let fixture: ComponentFixture<EditCategoryComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
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: string;
|
|
||||||
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 = this.route.snapshot.paramMap.get('id');
|
|
||||||
this.twigsService.getCategory(id)
|
|
||||||
.then(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" [create]="true"></app-category-form>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { NewCategoryComponent } from './new-category.component';
|
|
||||||
|
|
||||||
describe('NewCategoryComponent', () => {
|
|
||||||
let component: NewCategoryComponent;
|
|
||||||
let fixture: ComponentFixture<NewCategoryComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ NewCategoryComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(NewCategoryComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,27 +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.queryParamMap.get('budgetId');
|
|
||||||
console.log(`Creating category for budget ${this.budgetId}`)
|
|
||||||
this.category = new Category();
|
|
||||||
// TODO: Set random color for category, improve color picker
|
|
||||||
// this.category.color =
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,221 +0,0 @@
|
||||||
export class RecurringTransaction {
|
|
||||||
id: string = '';
|
|
||||||
title: string;
|
|
||||||
description?: string = null;
|
|
||||||
frequency: Frequency;
|
|
||||||
start: Date = new Date();
|
|
||||||
end?: Date;
|
|
||||||
amount: number;
|
|
||||||
expense = true;
|
|
||||||
categoryId: string;
|
|
||||||
budgetId: string;
|
|
||||||
createdBy: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Frequency {
|
|
||||||
unit: FrequencyUnit;
|
|
||||||
count: number;
|
|
||||||
time: Time;
|
|
||||||
amount?: (void | Set<DayOfWeek> | DayOfMonth | DayOfYear);
|
|
||||||
|
|
||||||
private constructor(unit: FrequencyUnit, count: number, time: Time, amount?: (void | Set<DayOfWeek> | DayOfMonth | DayOfYear)) {
|
|
||||||
this.unit = unit;
|
|
||||||
this.count = count;
|
|
||||||
this.time = time;
|
|
||||||
this.amount = amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Daily(count: number, time: Time): Frequency {
|
|
||||||
return new Frequency(FrequencyUnit.DAILY, count, time);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Weekly(count: number, time: Time, daysOfWeek: Set<DayOfWeek>): Frequency {
|
|
||||||
return new Frequency(FrequencyUnit.WEEKLY, count, time, daysOfWeek)
|
|
||||||
}
|
|
||||||
|
|
||||||
static Monthly(count: number, time: Time, dayOfMonth: DayOfMonth): Frequency {
|
|
||||||
return new Frequency(FrequencyUnit.MONTHLY, count, time, dayOfMonth)
|
|
||||||
}
|
|
||||||
|
|
||||||
static Yearly(count: number, time: Time, dayOfYear: DayOfYear): Frequency {
|
|
||||||
return new Frequency(FrequencyUnit.YEARLY, count, time, dayOfYear)
|
|
||||||
}
|
|
||||||
|
|
||||||
static parse(s: string): Frequency {
|
|
||||||
const parts = s.split(';');
|
|
||||||
let count: number, time: Time;
|
|
||||||
switch (parts[0]) {
|
|
||||||
case 'D':
|
|
||||||
count = Number.parseInt(parts[1]);
|
|
||||||
time = Time.parse(parts[2]);
|
|
||||||
return this.Daily(count, time);
|
|
||||||
case 'W':
|
|
||||||
count = Number.parseInt(parts[1]);
|
|
||||||
time = Time.parse(parts[3]);
|
|
||||||
const daysOfWeek = new Set(parts[2].split(',').map(day => DayOfWeek[day]));
|
|
||||||
return this.Weekly(count, time, daysOfWeek);
|
|
||||||
case 'M':
|
|
||||||
count = Number.parseInt(parts[1]);
|
|
||||||
time = Time.parse(parts[3]);
|
|
||||||
const dayOfMonth = DayOfMonth.parse(parts[2]);
|
|
||||||
return this.Monthly(count, time, dayOfMonth);
|
|
||||||
case 'Y':
|
|
||||||
count = Number.parseInt(parts[1]);
|
|
||||||
time = Time.parse(parts[3]);
|
|
||||||
const dayOfYear = DayOfYear.parse(parts[2]);
|
|
||||||
return this.Yearly(count, time, dayOfYear);
|
|
||||||
default:
|
|
||||||
throw new Error(`Invalid Frequency format: ${s}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
let parts = [this.unit.toString()]
|
|
||||||
parts.push(this.count.toString())
|
|
||||||
if (this.amount) {
|
|
||||||
if (this.unit === FrequencyUnit.WEEKLY) {
|
|
||||||
parts.push(Array.from(this.amount as Set<DayOfWeek>).join(','))
|
|
||||||
} else {
|
|
||||||
parts.push(this.amount.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parts.push(this.time.toString())
|
|
||||||
return parts.join(';')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum FrequencyUnit {
|
|
||||||
DAILY = 'D',
|
|
||||||
WEEKLY = 'W',
|
|
||||||
MONTHLY = 'M',
|
|
||||||
YEARLY = 'Y',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Time {
|
|
||||||
hours: number;
|
|
||||||
minutes: number;
|
|
||||||
seconds: number;
|
|
||||||
|
|
||||||
constructor(hours: number, minutes: number, seconds: number) {
|
|
||||||
this.hours = hours;
|
|
||||||
this.minutes = minutes;
|
|
||||||
this.seconds = seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
return [
|
|
||||||
String(this.hours).padStart(2, '0'),
|
|
||||||
String(this.minutes).padStart(2, '0'),
|
|
||||||
String(this.seconds).padStart(2, '0'),
|
|
||||||
].join(':')
|
|
||||||
}
|
|
||||||
|
|
||||||
static parse(s: string): Time {
|
|
||||||
if (!s.match(/[0-9]{2}:[0-9]{2}:[0-9]{2}/)) {
|
|
||||||
throw new Error('Invalid time format. Time must be formatted as HH:mm:ss');
|
|
||||||
}
|
|
||||||
const parts = s.split(':').map(part => Number.parseInt(part));
|
|
||||||
return new Time(parts[0], parts[1], parts[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Position {
|
|
||||||
DAY = 'DAY',
|
|
||||||
FIRST = 'FIRST',
|
|
||||||
SECOND = 'SECOND',
|
|
||||||
THIRD = 'THIRD',
|
|
||||||
FOURTH = 'FOURTH',
|
|
||||||
LAST = 'LAST',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum DayOfWeek {
|
|
||||||
MONDAY = 'MONDAY',
|
|
||||||
TUESDAY = 'TUESDAY',
|
|
||||||
WEDNESDAY = 'WEDNESDAY',
|
|
||||||
THURSDAY = 'THURSDAY',
|
|
||||||
FRIDAY = 'FRIDAY',
|
|
||||||
SATURDAY = 'SATURDAY',
|
|
||||||
SUNDAY = 'SUNDAY',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DayOfMonth {
|
|
||||||
position: Position;
|
|
||||||
day: (number | DayOfWeek);
|
|
||||||
|
|
||||||
private constructor(position: Position, day: (number | DayOfWeek)) {
|
|
||||||
this.position = position;
|
|
||||||
this.day = day;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Each(day: number): DayOfMonth {
|
|
||||||
if (day < 1 || day > 31) {
|
|
||||||
throw new Error('Day must be between 1 and 31');
|
|
||||||
}
|
|
||||||
return new DayOfMonth(Position.DAY, day);
|
|
||||||
}
|
|
||||||
|
|
||||||
static PositionalDayOfWeek(position: Position, day: DayOfWeek): DayOfMonth {
|
|
||||||
if (position === Position.DAY) {
|
|
||||||
throw new Error('Use DayOfMonth.Each() to create a monthly recurring transaction on the same calendar day');
|
|
||||||
}
|
|
||||||
return new DayOfMonth(position, day)
|
|
||||||
}
|
|
||||||
|
|
||||||
static parse(s: string): DayOfMonth {
|
|
||||||
const parts = s.split('-');
|
|
||||||
const position = Position[parts[0]];
|
|
||||||
if (position === Position.DAY) {
|
|
||||||
return DayOfMonth.Each(Number.parseInt(parts[1]));
|
|
||||||
} else {
|
|
||||||
return DayOfMonth.PositionalDayOfWeek(position, DayOfWeek[parts[1]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
return `${this.position}-${this.day}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DayOfYear {
|
|
||||||
month: number;
|
|
||||||
day: number;
|
|
||||||
|
|
||||||
constructor(month: number, day: number) {
|
|
||||||
this.month = month;
|
|
||||||
this.day = day;
|
|
||||||
}
|
|
||||||
|
|
||||||
static parse(s: string): DayOfYear {
|
|
||||||
if (!s.match(/[0-9]{2}-[0-9]{2}/)) {
|
|
||||||
throw new Error(`Invalid format for DayOfYear: ${s}`)
|
|
||||||
}
|
|
||||||
const parts = s.split('-').map(part => Number.parseInt(part));
|
|
||||||
if (parts[0] < 1 || parts[0] > 12) {
|
|
||||||
throw new Error(`Invalid month for DayOfYear: ${parts[0]}`);
|
|
||||||
}
|
|
||||||
let maxDay: number;
|
|
||||||
switch (parts[0]) {
|
|
||||||
case 2:
|
|
||||||
maxDay = 29;
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
case 6:
|
|
||||||
case 9:
|
|
||||||
case 11:
|
|
||||||
maxDay = 30;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
maxDay = 31;
|
|
||||||
}
|
|
||||||
if (parts[1] < 1 || parts[1] > maxDay) {
|
|
||||||
throw new Error(`Invalid day for DayOfYear: ${parts[0]}`);
|
|
||||||
}
|
|
||||||
return new DayOfYear(parts[0], parts[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
const monthString = this.month.toString().padStart(2, '0')
|
|
||||||
const dayString = this.day.toString().padStart(2, '0')
|
|
||||||
return `${monthString}-${dayString}`
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,21 +0,0 @@
|
||||||
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpHeaders } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthInterceptor implements HttpInterceptor {
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private storage: Storage
|
|
||||||
) { }
|
|
||||||
|
|
||||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
|
||||||
let token = this.storage.getItem('Authorization')
|
|
||||||
if (!token) {
|
|
||||||
return next.handle(req);
|
|
||||||
}
|
|
||||||
let headers = req.headers;
|
|
||||||
headers = headers.append('Authorization', `Bearer ${token}`);
|
|
||||||
return next.handle(req.clone({headers: headers}));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,370 +0,0 @@
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { User, UserPermission, Permission, AuthToken } 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 { Frequency, RecurringTransaction } from '../recurringtransactions/recurringtransaction';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class TwigsHttpService implements TwigsService {
|
|
||||||
|
|
||||||
private apiUrl = environment.apiUrl;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private storage: Storage
|
|
||||||
) { }
|
|
||||||
|
|
||||||
async login(email: string, password: string): Promise<User> {
|
|
||||||
const url = new URL('/api/users/login', this.apiUrl)
|
|
||||||
const auth: AuthToken = await this.request(url, HttpMethod.POST, {
|
|
||||||
'username': email,
|
|
||||||
'password': password
|
|
||||||
});
|
|
||||||
this.storage.setItem('Authorization', auth.token);
|
|
||||||
this.storage.setItem('userId', auth.userId);
|
|
||||||
return await this.getProfile(auth.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
register(username: string, email: string, password: string): Promise<User> {
|
|
||||||
const body = {
|
|
||||||
'username': username,
|
|
||||||
'email': email,
|
|
||||||
'password': password
|
|
||||||
};
|
|
||||||
const url = new URL('/api/users/register', this.apiUrl)
|
|
||||||
return this.request<User>(url, HttpMethod.POST, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
logout(): Promise<void> {
|
|
||||||
this.storage.removeItem('Authorization');
|
|
||||||
this.storage.removeItem('userId');
|
|
||||||
return Promise.resolve()
|
|
||||||
// TODO: Implement this to revoke the token server-side as well
|
|
||||||
// return this.http.post<void>(this.apiUrl + '/login?logout', this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Budgets
|
|
||||||
getBudgets(): Promise<Budget[]> {
|
|
||||||
const url = new URL('/api/budgets', this.apiUrl)
|
|
||||||
return this.request(url, HttpMethod.GET)
|
|
||||||
}
|
|
||||||
|
|
||||||
getBudgetBalance(
|
|
||||||
id: string,
|
|
||||||
from?: Date,
|
|
||||||
to?: Date
|
|
||||||
): Promise<number> {
|
|
||||||
const url = new URL('/api/transactions/sum', this.apiUrl)
|
|
||||||
url.searchParams.set('budgetId', id)
|
|
||||||
if (from) {
|
|
||||||
url.searchParams.set('from', from.toISOString());
|
|
||||||
}
|
|
||||||
if (to) {
|
|
||||||
url.searchParams.set('to', to.toISOString());
|
|
||||||
}
|
|
||||||
return this.request(url, HttpMethod.GET).then((res: any) => res.balance)
|
|
||||||
}
|
|
||||||
|
|
||||||
getBudget(id: string): Promise<Budget> {
|
|
||||||
const url = new URL(`/api/budgets/${id}`, this.apiUrl)
|
|
||||||
return this.request(url, HttpMethod.GET)
|
|
||||||
}
|
|
||||||
|
|
||||||
createBudget(
|
|
||||||
id: string,
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
users: UserPermission[],
|
|
||||||
): Promise<Budget> {
|
|
||||||
const url = new URL('/api/budgets', this.apiUrl)
|
|
||||||
const body = {
|
|
||||||
'id': id,
|
|
||||||
'name': name,
|
|
||||||
'description': description,
|
|
||||||
'users': users.map(userPermission => {
|
|
||||||
return {
|
|
||||||
user: userPermission.user,
|
|
||||||
permission: Permission[userPermission.permission]
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
return this.request(url, HttpMethod.POST, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBudget(id: string, budget: Budget): Promise<Budget> {
|
|
||||||
const url = new URL(`/api/budgets/${id}`, this.apiUrl)
|
|
||||||
const body = {
|
|
||||||
'name': budget.name,
|
|
||||||
'description': budget.description,
|
|
||||||
'users': budget.users.map(userPermission => {
|
|
||||||
return {
|
|
||||||
user: userPermission.user,
|
|
||||||
permission: Permission[userPermission.permission]
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
return this.request(url, HttpMethod.PUT, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteBudget(id: String): Promise<void> {
|
|
||||||
const url = new URL(`/api/budgets/${id}`, this.apiUrl)
|
|
||||||
return this.request(url, HttpMethod.DELETE)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Categories
|
|
||||||
getCategories(budgetId: string, count?: number): Promise<Category[]> {
|
|
||||||
const url = new URL(`/api/categories`, this.apiUrl)
|
|
||||||
url.searchParams.set('budgetIds', budgetId)
|
|
||||||
url.searchParams.set('archived', 'false')
|
|
||||||
return this.request(url, HttpMethod.GET);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCategory(id: string): Promise<Category> {
|
|
||||||
const url = new URL(`/api/categories/${id}`, this.apiUrl)
|
|
||||||
return this.request(url, HttpMethod.GET);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCategoryBalance(
|
|
||||||
id: string,
|
|
||||||
from?: Date,
|
|
||||||
to?: Date
|
|
||||||
): Promise<number> {
|
|
||||||
const url = new URL(`/api/transactions/sum`, this.apiUrl)
|
|
||||||
url.searchParams.set('categoryId', id)
|
|
||||||
if (from) {
|
|
||||||
url.searchParams.set('from', from.toISOString());
|
|
||||||
}
|
|
||||||
if (to) {
|
|
||||||
url.searchParams.set('to', to.toISOString());
|
|
||||||
}
|
|
||||||
const res: any = await this.request(url, HttpMethod.GET);
|
|
||||||
return res.balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
createCategory(id: string, budgetId: string, name: string, description: string, amount: number, isExpense: boolean): Promise<Category> {
|
|
||||||
const url = new URL(`/api/categories`, this.apiUrl)
|
|
||||||
const body = {
|
|
||||||
'id': id,
|
|
||||||
'title': name,
|
|
||||||
'description': description,
|
|
||||||
'amount': amount,
|
|
||||||
'expense': isExpense,
|
|
||||||
'budgetId': budgetId
|
|
||||||
};
|
|
||||||
return this.request(url, HttpMethod.POST, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCategory(id: string, changes: object): Promise<Category> {
|
|
||||||
const url = new URL(`/api/categories/${id}`, this.apiUrl)
|
|
||||||
return this.request(url, HttpMethod.PUT, changes);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteCategory(id: string): Promise<void> {
|
|
||||||
const url = new URL(`/api/categories/${id}`, this.apiUrl)
|
|
||||||
return this.request(url, HttpMethod.DELETE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transactions
|
|
||||||
async getTransactions(
|
|
||||||
budgetId?: string,
|
|
||||||
categoryId?: string,
|
|
||||||
count?: number,
|
|
||||||
from?: Date,
|
|
||||||
to?: Date
|
|
||||||
): Promise<Transaction[]> {
|
|
||||||
const url = new URL(`/api/transactions`, this.apiUrl)
|
|
||||||
if (budgetId) {
|
|
||||||
url.searchParams.set('budgetIds', budgetId);
|
|
||||||
}
|
|
||||||
if (categoryId) {
|
|
||||||
url.searchParams.set('categoryIds', categoryId);
|
|
||||||
}
|
|
||||||
if (from) {
|
|
||||||
url.searchParams.set('from', from.toISOString());
|
|
||||||
}
|
|
||||||
if (to) {
|
|
||||||
url.searchParams.set('to', to.toISOString());
|
|
||||||
}
|
|
||||||
const transactions: Transaction[] = await this.request(url, HttpMethod.GET)
|
|
||||||
transactions.forEach(transaction => {
|
|
||||||
transaction.date = new Date(transaction.date);
|
|
||||||
})
|
|
||||||
return transactions
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTransaction(id: string): Promise<Transaction> {
|
|
||||||
const url = new URL(`/api/transactions/${id}`, this.apiUrl)
|
|
||||||
const transaction: Transaction = await this.request(url, HttpMethod.GET)
|
|
||||||
transaction.date = new Date(transaction.date)
|
|
||||||
return transaction
|
|
||||||
}
|
|
||||||
|
|
||||||
async createTransaction(
|
|
||||||
id: string,
|
|
||||||
budgetId: string,
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
amount: number,
|
|
||||||
date: Date,
|
|
||||||
expense: boolean,
|
|
||||||
category: string
|
|
||||||
): Promise<Transaction> {
|
|
||||||
const url = new URL(`/api/transactions`, this.apiUrl)
|
|
||||||
const body = {
|
|
||||||
'id': id,
|
|
||||||
'title': name,
|
|
||||||
'description': description,
|
|
||||||
'date': date.toISOString(),
|
|
||||||
'amount': amount,
|
|
||||||
'expense': expense,
|
|
||||||
'categoryId': category,
|
|
||||||
'budgetId': budgetId
|
|
||||||
};
|
|
||||||
const transaction: Transaction = await this.request(url, HttpMethod.POST, body)
|
|
||||||
transaction.date = new Date(transaction.date)
|
|
||||||
return transaction
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTransaction(id: string, transaction: Transaction): Promise<Transaction> {
|
|
||||||
const body: any = transaction;
|
|
||||||
body.date = transaction.date.toISOString()
|
|
||||||
const url = new URL(`/api/transactions/${id}`, this.apiUrl)
|
|
||||||
const updatedTransaction: Transaction = await this.request(url, HttpMethod.PUT, body)
|
|
||||||
updatedTransaction.date = new Date(updatedTransaction.date)
|
|
||||||
return updatedTransaction
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteTransaction(id: string): Promise<void> {
|
|
||||||
const url = new URL(`/api/transactions/${id}`, this.apiUrl)
|
|
||||||
return this.request(url, HttpMethod.DELETE)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recurring Transactions
|
|
||||||
async getRecurringTransactions(
|
|
||||||
budgetId?: string,
|
|
||||||
categoryId?: string,
|
|
||||||
count?: number,
|
|
||||||
from?: Date,
|
|
||||||
to?: Date
|
|
||||||
): Promise<RecurringTransaction[]> {
|
|
||||||
const url = new URL(`/api/recurringtransactions`, this.apiUrl)
|
|
||||||
if (budgetId) {
|
|
||||||
url.searchParams.set('budgetIds', budgetId);
|
|
||||||
}
|
|
||||||
if (categoryId) {
|
|
||||||
url.searchParams.set('categoryIds', categoryId);
|
|
||||||
}
|
|
||||||
if (from) {
|
|
||||||
url.searchParams.set('from', from.toISOString());
|
|
||||||
}
|
|
||||||
if (to) {
|
|
||||||
url.searchParams.set('to', to.toISOString());
|
|
||||||
}
|
|
||||||
const transactions: RecurringTransaction[] = await this.request(url, HttpMethod.GET)
|
|
||||||
transactions.forEach(transaction => {
|
|
||||||
transaction.frequency = Frequency.parse(transaction.frequency as any)
|
|
||||||
})
|
|
||||||
return transactions
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRecurringTransaction(id: string): Promise<RecurringTransaction> {
|
|
||||||
const url = new URL(`/api/recurringtransactions/${id}`, this.apiUrl)
|
|
||||||
const transaction: RecurringTransaction = await this.request(url, HttpMethod.GET)
|
|
||||||
transaction.frequency = Frequency.parse(transaction.frequency as any)
|
|
||||||
return transaction
|
|
||||||
}
|
|
||||||
|
|
||||||
async createRecurringTransaction(
|
|
||||||
id: string,
|
|
||||||
budgetId: string,
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
amount: number,
|
|
||||||
frequency: Frequency,
|
|
||||||
start: Date,
|
|
||||||
expense: boolean,
|
|
||||||
category: string,
|
|
||||||
end?: Date,
|
|
||||||
): Promise<RecurringTransaction> {
|
|
||||||
const url = new URL(`/api/transactions`, this.apiUrl)
|
|
||||||
const body = {
|
|
||||||
'id': id,
|
|
||||||
'title': name,
|
|
||||||
'description': description,
|
|
||||||
'frequency': frequency.toString(),
|
|
||||||
'start': start.toISOString(),
|
|
||||||
'finish': end?.toISOString(),
|
|
||||||
'amount': amount,
|
|
||||||
'expense': expense,
|
|
||||||
'categoryId': category,
|
|
||||||
'budgetId': budgetId
|
|
||||||
};
|
|
||||||
const transaction: RecurringTransaction = await this.request(url, HttpMethod.POST, body)
|
|
||||||
transaction.frequency = Frequency.parse(transaction.frequency as any)
|
|
||||||
return transaction
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateRecurringTransaction(id: string, transaction: RecurringTransaction): Promise<RecurringTransaction> {
|
|
||||||
const body: any = transaction;
|
|
||||||
body.frequency = transaction.frequency.toString()
|
|
||||||
const url = new URL(`/api/transactions/${id}`, this.apiUrl)
|
|
||||||
const updatedTransaction: RecurringTransaction = await this.request(url, HttpMethod.PUT, body)
|
|
||||||
updatedTransaction.frequency = Frequency.parse(updatedTransaction.frequency as any)
|
|
||||||
return updatedTransaction
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteRecurringTransaction(id: string): Promise<void> {
|
|
||||||
const url = new URL(`/api/recurringtransactions/${id}`, this.apiUrl)
|
|
||||||
return this.request(url, HttpMethod.DELETE)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Users
|
|
||||||
getProfile(id: string): Promise<User> {
|
|
||||||
const url = new URL(`/api/users/${id}`, this.apiUrl)
|
|
||||||
return this.request(url, HttpMethod.GET)
|
|
||||||
}
|
|
||||||
|
|
||||||
getUsersByUsername(username: string): Promise<User[]> {
|
|
||||||
return Promise.reject("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
private async request<T>(url: URL, method: HttpMethod, body?: any): Promise<T> {
|
|
||||||
const headers = {
|
|
||||||
'content-type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = this.storage.getItem('Authorization')
|
|
||||||
if (token) {
|
|
||||||
headers['authorization'] = `Bearer ${token}`
|
|
||||||
}
|
|
||||||
|
|
||||||
let jsonBody: string;
|
|
||||||
if (body) {
|
|
||||||
jsonBody = JSON.stringify(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
credentials: 'include',
|
|
||||||
headers: headers,
|
|
||||||
method: method,
|
|
||||||
body: jsonBody
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.status === 204) {
|
|
||||||
// No content
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum HttpMethod {
|
|
||||||
GET = "GET",
|
|
||||||
POST = "POST",
|
|
||||||
PUT = "PUT",
|
|
||||||
DELETE = "DELETE",
|
|
||||||
}
|
|
|
@ -1,348 +0,0 @@
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
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';
|
|
||||||
import { randomId } from '../shared/utils';
|
|
||||||
import { Frequency, RecurringTransaction } from '../recurringtransactions/recurringtransaction';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 users: User[] = [new User(randomId(), 'test', 'test@example.com')];
|
|
||||||
private budgets: Budget[] = [];
|
|
||||||
private transactions: Transaction[] = [];
|
|
||||||
private categories: Category[] = [];
|
|
||||||
|
|
||||||
// Auth
|
|
||||||
login(email: string, password: string): Promise<User> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const filteredUsers = this.users.filter(user => {
|
|
||||||
return (user.email === email || user.username === email);
|
|
||||||
});
|
|
||||||
if (filteredUsers.length !== 0) {
|
|
||||||
resolve(filteredUsers[0]);
|
|
||||||
} else {
|
|
||||||
reject('No users found');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
register(username: string, email: string, password: string): Promise<User> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const user = new User();
|
|
||||||
user.username = username;
|
|
||||||
user.email = email;
|
|
||||||
user.id = randomId();
|
|
||||||
this.users.push(user);
|
|
||||||
resolve(user);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
logout(): Promise<void> {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Budgets
|
|
||||||
getBudgets(): Promise<Budget[]> {
|
|
||||||
return Promise.resolve(this.budgets)
|
|
||||||
}
|
|
||||||
|
|
||||||
getBudgetBalance(id: string, from?: Date, to?: Date): Promise<number> {
|
|
||||||
return Promise.resolve(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
getBudget(id: string): Promise<Budget> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const budget = this.budgets.filter(it => {
|
|
||||||
return it.id === id;
|
|
||||||
})[0];
|
|
||||||
if (budget) {
|
|
||||||
resolve(budget);
|
|
||||||
} else {
|
|
||||||
reject('No budget found for given id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createBudget(
|
|
||||||
id: string,
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
users: UserPermission[],
|
|
||||||
): Promise<Budget> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const budget = new Budget();
|
|
||||||
budget.name = name;
|
|
||||||
budget.description = description;
|
|
||||||
budget.users = users;
|
|
||||||
budget.id = id;
|
|
||||||
this.budgets.push(budget);
|
|
||||||
resolve(budget);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBudget(id: string, budget: Budget): Promise<Budget> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const budget = this.budgets.filter(it => {
|
|
||||||
return it.id === id;
|
|
||||||
})[0];
|
|
||||||
if (budget) {
|
|
||||||
const index = this.budgets.indexOf(budget);
|
|
||||||
this.updateValues(
|
|
||||||
budget,
|
|
||||||
budget,
|
|
||||||
[
|
|
||||||
'name',
|
|
||||||
'description',
|
|
||||||
'users',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
this.budgets[index] = budget;
|
|
||||||
resolve(budget);
|
|
||||||
} else {
|
|
||||||
reject('No budget found for given id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteBudget(id: string): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const budget = this.budgets.filter(it => {
|
|
||||||
return budget.id === id;
|
|
||||||
})[0];
|
|
||||||
if (budget) {
|
|
||||||
const index = this.budgets.indexOf(budget);
|
|
||||||
delete this.budgets[index];
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject('No budget found for given id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Categories
|
|
||||||
getCategories(budgetId: string, count?: number): Promise<Category[]> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
resolve(this.categories.filter(category => {
|
|
||||||
return category.budgetId === budgetId;
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getCategory(id: string): Promise<Category> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
resolve(this.findById(this.categories, id));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getCategoryBalance(id: string, from?: Date, to?: Date): Promise<number> {
|
|
||||||
return Promise.resolve(20);
|
|
||||||
}
|
|
||||||
|
|
||||||
createCategory(id: string, budgetId: string, name: string, description: string, amount: number, isExpense: boolean): Promise<Category> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const category = new Category();
|
|
||||||
category.title = name;
|
|
||||||
category.description = description;
|
|
||||||
category.amount = amount;
|
|
||||||
category.expense = isExpense;
|
|
||||||
category.budgetId = budgetId;
|
|
||||||
category.id = id;
|
|
||||||
this.categories.push(category);
|
|
||||||
resolve(category);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCategory(id: string, changes: object): Promise<Category> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
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;
|
|
||||||
resolve(category);
|
|
||||||
} else {
|
|
||||||
reject('No category found for given id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteCategory(id: string): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const category = this.findById(this.categories, id);
|
|
||||||
if (category) {
|
|
||||||
const index = this.categories.indexOf(category);
|
|
||||||
delete this.transactions[index];
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject('No category found for given id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transactions
|
|
||||||
getTransactions(budgetId?: string, categoryId?: string, count?: number): Promise<Transaction[]> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
resolve(this.transactions.filter(transaction => {
|
|
||||||
let include = true;
|
|
||||||
if (budgetId) {
|
|
||||||
include = transaction.budgetId === budgetId;
|
|
||||||
}
|
|
||||||
if (include && categoryId) {
|
|
||||||
include = transaction.categoryId === categoryId;
|
|
||||||
}
|
|
||||||
return include;
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getTransaction(id: string): Promise<Transaction> {
|
|
||||||
return Promise.resolve(this.findById(this.transactions, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
createTransaction(
|
|
||||||
id: string,
|
|
||||||
budgetId: string,
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
amount: number,
|
|
||||||
date: Date,
|
|
||||||
isExpense: boolean,
|
|
||||||
category: string
|
|
||||||
): Promise<Transaction> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
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 = randomId();
|
|
||||||
this.transactions.push(transaction);
|
|
||||||
resolve(transaction);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTransaction(id: string, changes: object): Promise<Transaction> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
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;
|
|
||||||
resolve(transaction);
|
|
||||||
} else {
|
|
||||||
reject('No transaction found for given id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteTransaction(id: string): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.findById(this.transactions, id);
|
|
||||||
if (transaction) {
|
|
||||||
const index = this.transactions.indexOf(transaction);
|
|
||||||
delete this.transactions[index];
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject('No transaction found for given id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recurring Transactions
|
|
||||||
getRecurringTransactions(
|
|
||||||
budgetId?: string,
|
|
||||||
categoryId?: string,
|
|
||||||
count?: number,
|
|
||||||
from?: Date,
|
|
||||||
to?: Date
|
|
||||||
): Promise<RecurringTransaction[]> {
|
|
||||||
return Promise.reject("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
getRecurringTransaction(id: string): Promise<RecurringTransaction> {
|
|
||||||
return Promise.reject("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
createRecurringTransaction(
|
|
||||||
id: string,
|
|
||||||
budgetId: string,
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
amount: number,
|
|
||||||
frequency: Frequency,
|
|
||||||
start: Date,
|
|
||||||
expense: boolean,
|
|
||||||
category: string,
|
|
||||||
end?: Date,
|
|
||||||
): Promise<RecurringTransaction> {
|
|
||||||
return Promise.reject("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRecurringTransaction(id: string, transaction: RecurringTransaction): Promise<RecurringTransaction> {
|
|
||||||
return Promise.reject("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteRecurringTransaction(id: string): Promise<void> {
|
|
||||||
return Promise.reject("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Users
|
|
||||||
getProfile(id: string): Promise<User> {
|
|
||||||
return Promise.reject("Not yet implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
getUsersByUsername(username: string): Promise<User[]> {
|
|
||||||
return Promise.resolve(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: string): T {
|
|
||||||
return items.filter(item => {
|
|
||||||
return item['id'] === id;
|
|
||||||
})[0];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
import { InjectionToken } from '@angular/core';
|
|
||||||
import { User, UserPermission } from '../users/user';
|
|
||||||
import { Budget } from '../budgets/budget';
|
|
||||||
import { Category } from '../categories/category';
|
|
||||||
import { RecurringTransaction, Frequency } from '../recurringtransactions/recurringtransaction';
|
|
||||||
import { Transaction } from '../transactions/transaction';
|
|
||||||
|
|
||||||
export interface TwigsService {
|
|
||||||
// Auth
|
|
||||||
login(email: string, password: string): Promise<User>;
|
|
||||||
register(username: string, email: string, password: string): Promise<User>;
|
|
||||||
logout(): Promise<void>;
|
|
||||||
|
|
||||||
// Budgets
|
|
||||||
getBudgets(): Promise<Budget[]>;
|
|
||||||
getBudget(id: string): Promise<Budget>;
|
|
||||||
getBudgetBalance(id: string, from?: Date, to?: Date): Promise<number>;
|
|
||||||
createBudget(
|
|
||||||
id: string,
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
users: UserPermission[],
|
|
||||||
): Promise<Budget>;
|
|
||||||
updateBudget(id: string, budget: Budget): Promise<Budget>;
|
|
||||||
deleteBudget(id: string): Promise<void>;
|
|
||||||
|
|
||||||
// Categories
|
|
||||||
getCategories(budgetId?: string, count?: number): Promise<Category[]>;
|
|
||||||
getCategory(id: string): Promise<Category>;
|
|
||||||
getCategoryBalance(id: string, from?: Date, to?: Date): Promise<number>;
|
|
||||||
createCategory(id: string, budgetId: string, name: string, description: string, amount: number, isExpense: boolean): Promise<Category>;
|
|
||||||
updateCategory(id: string, category: Category): Promise<Category>;
|
|
||||||
deleteCategory(id: string): Promise<void>;
|
|
||||||
|
|
||||||
// Transactions
|
|
||||||
getTransactions(
|
|
||||||
budgetId?: string,
|
|
||||||
categoryId?: string,
|
|
||||||
count?: number,
|
|
||||||
from?: Date,
|
|
||||||
to?: Date
|
|
||||||
): Promise<Transaction[]>;
|
|
||||||
getTransaction(id: string): Promise<Transaction>;
|
|
||||||
createTransaction(
|
|
||||||
id: string,
|
|
||||||
budgetId: string,
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
amount: number,
|
|
||||||
date: Date,
|
|
||||||
isExpense: boolean,
|
|
||||||
category: string
|
|
||||||
): Promise<Transaction>;
|
|
||||||
updateTransaction(id: string, transaction: Transaction): Promise<Transaction>;
|
|
||||||
deleteTransaction(id: string): Promise<void>;
|
|
||||||
|
|
||||||
// Recurring Transactions
|
|
||||||
getRecurringTransactions(
|
|
||||||
budgetId?: string,
|
|
||||||
categoryId?: string,
|
|
||||||
count?: number,
|
|
||||||
from?: Date,
|
|
||||||
to?: Date
|
|
||||||
): Promise<RecurringTransaction[]>;
|
|
||||||
getRecurringTransaction(id: string): Promise<RecurringTransaction>;
|
|
||||||
createRecurringTransaction(
|
|
||||||
id: string,
|
|
||||||
budgetId: string,
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
amount: number,
|
|
||||||
frequency: Frequency,
|
|
||||||
start: Date,
|
|
||||||
expense: boolean,
|
|
||||||
category: string,
|
|
||||||
end?: Date,
|
|
||||||
): Promise<RecurringTransaction>;
|
|
||||||
updateRecurringTransaction(id: string, transaction: RecurringTransaction): Promise<RecurringTransaction>;
|
|
||||||
deleteRecurringTransaction(id: string): Promise<void>;
|
|
||||||
|
|
||||||
getProfile(id: string): Promise<User>;
|
|
||||||
getUsersByUsername(username: string): Promise<User[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export let TWIGS_SERVICE = new InjectionToken<TwigsService>('twigs.service');
|
|
|
@ -1,15 +0,0 @@
|
||||||
const CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
||||||
|
|
||||||
export function randomId(): string {
|
|
||||||
var bytes = new Uint8Array(32)
|
|
||||||
window.crypto.getRandomValues(bytes)
|
|
||||||
return Array.from(bytes, (byte) => CHARACTERS[byte % CHARACTERS.length]).join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decimalToInteger(amount: string): number {
|
|
||||||
if (amount[amount.length - 3] === "." || amount[amount.length - 3] === ",") {
|
|
||||||
return Number(amount.replace(/[,.]/g, ""))
|
|
||||||
} else {
|
|
||||||
return Number(amount + "00")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
.transaction-form {
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transaction-form * {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-radio-button {
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
|
|
@ -1,34 +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 autocapitalize="words">
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field>
|
|
||||||
<textarea matInput [(ngModel)]="currentTransaction.description" placeholder="Description" autocapitalize="sentences"></textarea>
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field>
|
|
||||||
<input matInput type="number" [(ngModel)]="currentTransaction.amount" placeholder="Amount" required step="0.01">
|
|
||||||
</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="!create" (click)="delete()">Delete</button>
|
|
||||||
</div>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { AddEditTransactionComponent } from './add-edit-transaction.component';
|
|
||||||
|
|
||||||
describe('AddEditTransactionComponent', () => {
|
|
||||||
let component: AddEditTransactionComponent;
|
|
||||||
let fixture: ComponentFixture<AddEditTransactionComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ AddEditTransactionComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(AddEditTransactionComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,107 +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 { MatLegacyRadioChange as MatRadioChange } from '@angular/material/legacy-radio';
|
|
||||||
import { decimalToInteger } from 'src/app/shared/utils';
|
|
||||||
|
|
||||||
@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: string;
|
|
||||||
@Input() create: boolean
|
|
||||||
public transactionType = TransactionType;
|
|
||||||
public categories: Category[];
|
|
||||||
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)
|
|
||||||
.then(newCategories => {
|
|
||||||
this.categories = newCategories.filter(category => category.expense === change.value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
save(): void {
|
|
||||||
let promise;
|
|
||||||
this.currentTransaction.amount = decimalToInteger(String(this.currentTransaction.amount))
|
|
||||||
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.create) {
|
|
||||||
// This is a new transaction, save it
|
|
||||||
promise = this.twigsService.createTransaction(
|
|
||||||
this.currentTransaction.id,
|
|
||||||
this.budgetId,
|
|
||||||
this.currentTransaction.title,
|
|
||||||
this.currentTransaction.description,
|
|
||||||
this.currentTransaction.amount,
|
|
||||||
this.currentTransaction.date,
|
|
||||||
this.currentTransaction.expense,
|
|
||||||
this.currentTransaction.categoryId,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// This is an existing transaction, update it
|
|
||||||
const updatedTransaction: Transaction = {
|
|
||||||
...this.currentTransaction,
|
|
||||||
}
|
|
||||||
promise = this.twigsService.updateTransaction(
|
|
||||||
this.currentTransaction.id,
|
|
||||||
updatedTransaction
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.then(() => {
|
|
||||||
this.app.goBack();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(): void {
|
|
||||||
this.twigsService.deleteTransaction(this.currentTransaction.id).then(() => {
|
|
||||||
this.app.goBack();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
<app-add-edit-transaction [budgetId]="budgetId" [title]="'Add Transaction'" [currentTransaction]="transaction" [create]="true"></app-add-edit-transaction>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { NewTransactionComponent } from './new-transaction.component';
|
|
||||||
|
|
||||||
describe('NewTransactionComponent', () => {
|
|
||||||
let component: NewTransactionComponent;
|
|
||||||
let fixture: ComponentFixture<NewTransactionComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ NewTransactionComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(NewTransactionComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { Transaction } from '../transaction';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-new-transaction',
|
|
||||||
templateUrl: './new-transaction.component.html',
|
|
||||||
styleUrls: ['./new-transaction.component.css']
|
|
||||||
})
|
|
||||||
export class NewTransactionComponent implements OnInit {
|
|
||||||
|
|
||||||
budgetId: string;
|
|
||||||
transaction: Transaction;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.budgetId = this.route.snapshot.queryParamMap.get('budgetId');
|
|
||||||
this.transaction = new Transaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
<app-add-edit-transaction [budgetId]="budgetId" [title]="'Edit Transaction'" [currentTransaction]="transaction" *ngIf="transaction" [create]="false"></app-add-edit-transaction>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { TransactionDetailsComponent } from './transaction-details.component';
|
|
||||||
|
|
||||||
describe('TransactionDetailsComponent', () => {
|
|
||||||
let component: TransactionDetailsComponent;
|
|
||||||
let fixture: ComponentFixture<TransactionDetailsComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ TransactionDetailsComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(TransactionDetailsComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { Component, OnInit, Input, Inject } from '@angular/core';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { Transaction } from '../transaction';
|
|
||||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-transaction-details',
|
|
||||||
templateUrl: './transaction-details.component.html',
|
|
||||||
styleUrls: ['./transaction-details.component.css']
|
|
||||||
})
|
|
||||||
export class TransactionDetailsComponent implements OnInit {
|
|
||||||
|
|
||||||
budgetId: string;
|
|
||||||
transaction: Transaction;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.getTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
getTransaction(): void {
|
|
||||||
const id = this.route.snapshot.paramMap.get('id');
|
|
||||||
this.twigsService.getTransaction(id)
|
|
||||||
.then(transaction => {
|
|
||||||
transaction.amount /= 100;
|
|
||||||
this.transaction = transaction;
|
|
||||||
this.budgetId = transaction.budgetId;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
.transaction-details p {
|
|
||||||
padding: 2px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transaction-description {
|
|
||||||
font-style: italic;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
<mat-nav-list *ngIf="transactions" class="transactions">
|
|
||||||
<a mat-list-item class="transaction-list-item" *ngFor="let transaction of transactions"
|
|
||||||
routerLink="/transactions/{{ transaction.id }}">
|
|
||||||
<div matLine class="transaction-list-details">
|
|
||||||
<p class="transaction-title">{{transaction.title}}</p>
|
|
||||||
<p class="transaction-description text-small" *ngIf="transaction.description">{{transaction.description }}</p>
|
|
||||||
<p matLine class="transaction-date text-small">{{ transaction.date | date }}</p>
|
|
||||||
</div>
|
|
||||||
<p class="amount" [class.expense]="transaction.expense" [class.income]="!transaction.expense">
|
|
||||||
{{ transaction.amount / 100 | currency }}
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</mat-nav-list>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { TransactionListComponent } from './transaction-list.component';
|
|
||||||
|
|
||||||
describe('TransactionListComponent', () => {
|
|
||||||
let component: TransactionListComponent;
|
|
||||||
let fixture: ComponentFixture<TransactionListComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ TransactionListComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(TransactionListComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,60 +0,0 @@
|
||||||
import { Component, OnInit, Input, Inject } from '@angular/core';
|
|
||||||
import { Transaction } from '../transaction';
|
|
||||||
import { TWIGS_SERVICE, TwigsService } from '../../shared/twigs.service';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-transaction-list',
|
|
||||||
templateUrl: './transaction-list.component.html',
|
|
||||||
styleUrls: ['./transaction-list.component.css']
|
|
||||||
})
|
|
||||||
export class TransactionListComponent implements OnInit {
|
|
||||||
|
|
||||||
@Input() budgetIds: string[];
|
|
||||||
@Input() categoryIds?: string[];
|
|
||||||
public transactions: Transaction[];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
|
||||||
private route: ActivatedRoute
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.getTransactions();
|
|
||||||
}
|
|
||||||
|
|
||||||
getTransactions(): void {
|
|
||||||
let fromStr = this.route.snapshot.queryParamMap.get('from');
|
|
||||||
var from;
|
|
||||||
if (fromStr) {
|
|
||||||
let fromDate = new Date(fromStr);
|
|
||||||
if (!isNaN(fromDate.getTime())) {
|
|
||||||
from = fromDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!from) {
|
|
||||||
let date = new Date();
|
|
||||||
date.setHours(0);
|
|
||||||
date.setMinutes(0);
|
|
||||||
date.setSeconds(0);
|
|
||||||
date.setMilliseconds(0);
|
|
||||||
date.setDate(1);
|
|
||||||
from = date;
|
|
||||||
}
|
|
||||||
|
|
||||||
let toStr = this.route.snapshot.queryParamMap.get('to');
|
|
||||||
let to: Date;
|
|
||||||
if (toStr) {
|
|
||||||
let toDate = new Date(toStr);
|
|
||||||
if (!isNaN(toDate.getTime())) {
|
|
||||||
to = toDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.twigsService.getTransactions(this.budgetIds.join(','), this.categoryIds?.join(','), null, from, to)
|
|
||||||
.then(transactions => {
|
|
||||||
this.transactions = transactions;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { randomId } from '../shared/utils';
|
|
||||||
|
|
||||||
export class Transaction {
|
|
||||||
id: string = randomId();
|
|
||||||
title: string;
|
|
||||||
description: string = null;
|
|
||||||
date: Date = new Date();
|
|
||||||
amount: number;
|
|
||||||
expense = true;
|
|
||||||
categoryId: string;
|
|
||||||
budgetId: string;
|
|
||||||
createdBy: string;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export enum TransactionType {
|
|
||||||
INCOME,
|
|
||||||
EXPENSE
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue