Compare commits
2 commits
Author | SHA1 | Date | |
---|---|---|---|
1752c5d2d0 | |||
71a4d8e8a5 |
|
@ -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
.env.development
Normal file
|
@ -0,0 +1 @@
|
||||||
|
VUE_APP_API_URL=https://3000code.brawner.home
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"projects": {
|
|
||||||
"default": "budget-c7da5"
|
|
||||||
}
|
|
||||||
}
|
|
22
.github/workflows/docker-image.yml
vendored
|
@ -1,22 +0,0 @@
|
||||||
name: Publish Docker image
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: main
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
push_to_registry:
|
|
||||||
name: Push Docker image to GitHub Packages
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Push to GitHub Packages
|
|
||||||
uses: docker/build-push-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
repository: ${{ github.repository }}/${{ github.event.repository.name }}
|
|
||||||
registry: docker.pkg.github.com
|
|
||||||
tag_with_ref: true
|
|
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
|
|
54
.github/workflows/gh-pages.yml
vendored
|
@ -1,54 +0,0 @@
|
||||||
name: Deploy to GitHub Pages
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
|
||||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
|
||||||
concurrency:
|
|
||||||
group: "pages"
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Build job
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Setup Pages
|
|
||||||
uses: actions/configure-pages@v4
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 'lts/*'
|
|
||||||
- name: Install dependencies with npm
|
|
||||||
run: npm ci
|
|
||||||
- name: Build with NPM
|
|
||||||
run: npm run package
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@v3
|
|
||||||
with:
|
|
||||||
path: dist/twigs
|
|
||||||
|
|
||||||
# Deployment job
|
|
||||||
deploy:
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
steps:
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
65
.gitignore
vendored
|
@ -1,46 +1,23 @@
|
||||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# compiled output
|
|
||||||
/dist
|
|
||||||
/tmp
|
|
||||||
/out-tsc
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
|
|
||||||
# IDEs and editors
|
|
||||||
/.idea
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
.c9/
|
|
||||||
*.launch
|
|
||||||
.settings/
|
|
||||||
*.sublime-workspace
|
|
||||||
|
|
||||||
# IDE - VSCode
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
|
|
||||||
# misc
|
|
||||||
/.angular/cache
|
|
||||||
/.sass-cache
|
|
||||||
/connect.lock
|
|
||||||
/coverage
|
|
||||||
/libpeerconnection.log
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
testem.log
|
|
||||||
/typings
|
|
||||||
|
|
||||||
# System Files
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
node_modules
|
||||||
*.swp
|
/dist
|
||||||
*~
|
|
||||||
|
|
||||||
# Firebase
|
|
||||||
.firebase/
|
# local env files
|
||||||
.angular/
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
7
.vscode/launch.json
vendored
|
@ -4,13 +4,6 @@
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
|
||||||
"name": "Launch Chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "pwa-chrome",
|
|
||||||
"url": "http://localhost:4200",
|
|
||||||
"webRoot": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "ng serve",
|
"name": "ng serve",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
|
|
12
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"database.connections": [
|
||||||
|
{
|
||||||
|
"type": "mysql",
|
||||||
|
"name": "root@captain.intra.wbrawner.com (MySql)",
|
||||||
|
"host": "captain.intra.wbrawner.com:3306",
|
||||||
|
"username": "root",
|
||||||
|
"database": null,
|
||||||
|
"password": "U7YE8YsmES8LHB2B39WXNjTQk4d48LzQEZG3cj6wSb2fgeRLEYtrrqTwiqAhrpR3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:lts as builder
|
FROM node:latest as builder
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN npm install && \
|
RUN npm install && \
|
||||||
|
|
21
LICENSE
|
@ -1,21 +0,0 @@
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2021 William Brawner
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
79
README.md
|
@ -1,49 +1,38 @@
|
||||||
# Twigs Web Client
|
# Twigs
|
||||||
|
|
||||||
# IMPORTANT: This repository is no longer maintained. The web version of Twigs has been replaced with a server-rendered implementation in the [backend repository](https://github.com/wbrawner/twigs)
|
## Vue Migration Checklist
|
||||||
|
|
||||||
Twigs is an open source budgeting app aimed at people who need to share a budget. This project serves as the web front end, and is powered by Angular. The main back end project can be found at [wbrawner/twigs-server](https://github.com/wbrawner/twigs-server)
|
_Could also be used as a testing checklist_
|
||||||
|
|
||||||
## Building
|
- [ ] Login
|
||||||
|
- [ ] Logout
|
||||||
|
- [ ] Register
|
||||||
|
- [ ] Budget list
|
||||||
|
- [ ] Create budget
|
||||||
|
- [ ] Budget details
|
||||||
|
- [ ]
|
||||||
|
- [ ] Edit budget
|
||||||
|
- [ ] Change name
|
||||||
|
- [ ] Change description
|
||||||
|
- [ ] Change users/permissions
|
||||||
|
- [ ] Delete budget
|
||||||
|
- [ ] Category list
|
||||||
|
- [ ] Create category
|
||||||
|
- [ ] Category details
|
||||||
|
- [ ] Edit category
|
||||||
|
- [ ] Change name
|
||||||
|
- [ ] Change description
|
||||||
|
- [ ] Change expense/income
|
||||||
|
- [ ] Change amount
|
||||||
|
- [ ] Delete category
|
||||||
|
- [ ] Transaction list
|
||||||
|
- [ ] Create transaction
|
||||||
|
- [ ] Transaction details
|
||||||
|
- [ ] Edit transaction
|
||||||
|
- [ ] Change name
|
||||||
|
- [ ] Change description
|
||||||
|
- [ ] Change expense/income
|
||||||
|
- [ ] Change amount
|
||||||
|
- [ ] Change date
|
||||||
|
- [ ] Delete transaction
|
||||||
|
|
||||||
You'll need NodeJS and NPM, then run
|
|
||||||
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
If you would like to tinker with the site and have it hot reload, then run
|
|
||||||
|
|
||||||
npm run start
|
|
||||||
|
|
||||||
The app will be available at http://localhost:4200
|
|
||||||
|
|
||||||
## Self-hosting
|
|
||||||
|
|
||||||
Eventually the plan is to ship this web app within the JAR for the server, but for now you'll need to run them separately. Before you build the app, be sure to change the `apiUrl` value in [src/environments/environment.prod.ts](src/environments/environment.prod.ts). Then you can run the following command to get an optimized version of the build for production deployments
|
|
||||||
|
|
||||||
npm run package
|
|
||||||
|
|
||||||
This will output the app in a folder called `dist/twigs`, which you can then serve directly with Apache or Nginx or any static file server.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
```
|
|
||||||
Copyright (c) 2021 William Brawner
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
```
|
|
||||||
|
|
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"
|
|
||||||
}
|
|
||||||
}
|
|
5
babel.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,28 +0,0 @@
|
||||||
// Protractor configuration file, see link for more information
|
|
||||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
|
||||||
|
|
||||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
|
||||||
|
|
||||||
exports.config = {
|
|
||||||
allScriptsTimeout: 11000,
|
|
||||||
specs: [
|
|
||||||
'./src/**/*.e2e-spec.ts'
|
|
||||||
],
|
|
||||||
capabilities: {
|
|
||||||
'browserName': 'chrome'
|
|
||||||
},
|
|
||||||
directConnect: true,
|
|
||||||
baseUrl: 'http://localhost:4200/',
|
|
||||||
framework: 'jasmine',
|
|
||||||
jasmineNodeOpts: {
|
|
||||||
showColors: true,
|
|
||||||
defaultTimeoutInterval: 30000,
|
|
||||||
print: function() {}
|
|
||||||
},
|
|
||||||
onPrepare() {
|
|
||||||
require('ts-node').register({
|
|
||||||
project: require('path').join(__dirname, './tsconfig.e2e.json')
|
|
||||||
});
|
|
||||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { AppPage } from './app.po';
|
|
||||||
|
|
||||||
describe('workspace-project App', () => {
|
|
||||||
let page: AppPage;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
page = new AppPage();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display welcome message', () => {
|
|
||||||
page.navigateTo();
|
|
||||||
expect(page.getParagraphText()).toEqual('Welcome to budget!');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { browser, by, element } from 'protractor';
|
|
||||||
|
|
||||||
export class AppPage {
|
|
||||||
navigateTo() {
|
|
||||||
return browser.get('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
getParagraphText() {
|
|
||||||
return element(by.css('app-root h1')).getText();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../out-tsc/app",
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"types": [
|
|
||||||
"jasmine",
|
|
||||||
"jasminewd2",
|
|
||||||
"node"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,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/**"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
36731
package-lock.json
generated
94
package.json
|
@ -1,58 +1,48 @@
|
||||||
{
|
{
|
||||||
"name": "budget",
|
"name": "twigs-web",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"scripts": {
|
|
||||||
"ng": "ng",
|
|
||||||
"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,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^17.2.3",
|
"core-js": "^3.6.5",
|
||||||
"@angular/cdk": "^17.2.1",
|
"register-service-worker": "^1.7.1",
|
||||||
"@angular/common": "^17.2.3",
|
"vue": "^2.6.11",
|
||||||
"@angular/compiler": "^17.2.3",
|
"vue-router": "^3.2.0",
|
||||||
"@angular/core": "^17.2.3",
|
"vuex": "^3.4.0"
|
||||||
"@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",
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
"@angular/cli": "^17.2.2",
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
"@angular/compiler-cli": "^17.2.3",
|
"@vue/cli-plugin-pwa": "^4.5.7",
|
||||||
"@angular/language-service": "^17.2.3",
|
"@vue/cli-plugin-router": "^4.5.7",
|
||||||
"@types/jasmine": "~3.10.3",
|
"@vue/cli-plugin-vuex": "^4.5.7",
|
||||||
"@types/jasminewd2": "^2.0.10",
|
"@vue/cli-service": "~4.5.0",
|
||||||
"@types/node": "^17.0.10",
|
"babel-eslint": "^10.1.0",
|
||||||
"eslint": "^8.7.0",
|
"eslint": "^6.7.2",
|
||||||
"jasmine-core": "~4.0.0",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
"jasmine-spec-reporter": "~7.0.0",
|
"vue-template-compiler": "^2.6.11"
|
||||||
"karma": "~6.3.11",
|
},
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"eslintConfig": {
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
"root": true,
|
||||||
"karma-jasmine": "~4.0.1",
|
"env": {
|
||||||
"karma-jasmine-html-reporter": "^1.7.0",
|
"node": true
|
||||||
"npm-check-updates": "^15.0.1",
|
},
|
||||||
"protractor": "^7.0.0",
|
"extends": [
|
||||||
"ts-node": "~10.4.0",
|
"plugin:vue/essential",
|
||||||
"tslint": "^6.1.3",
|
"eslint:recommended"
|
||||||
"typescript": "5.3.3"
|
],
|
||||||
}
|
"parserOptions": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
},
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead"
|
||||||
|
]
|
||||||
}
|
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 675 KiB After Width: | Height: | Size: 675 KiB |
17
public/index.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<link rel="stylesheet" href="<%= BASE_URL %>style.css">
|
||||||
|
<title>Twigs</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but Twigs doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
2
public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
37
public/style.css
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
:root {
|
||||||
|
--good-color: green;
|
||||||
|
--warn-color: yellow;
|
||||||
|
--danger-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.good {
|
||||||
|
color: var(--good-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn {
|
||||||
|
color: var(--warn-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
|
|
||||||
# For additional information regarding the format and rule options, please see:
|
|
||||||
# https://github.com/browserslist/browserslist#queries
|
|
||||||
#
|
|
||||||
# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
|
|
||||||
|
|
||||||
> 0.5%
|
|
||||||
last 2 versions
|
|
||||||
Firefox ESR
|
|
||||||
not dead
|
|
||||||
not IE 9-11
|
|
21
src/App.vue
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.app-twigs {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-twigs > div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,13 +0,0 @@
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
|
||||||
|
|
||||||
describe('AppRoutingModule', () => {
|
|
||||||
let appRoutingModule: AppRoutingModule;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
appRoutingModule = new AppRoutingModule();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create an instance', () => {
|
|
||||||
expect(appRoutingModule).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { TransactionsComponent } from './transactions/transactions.component';
|
|
||||||
import { TransactionDetailsComponent } from './transactions/transaction-details/transaction-details.component';
|
|
||||||
import { NewTransactionComponent } from './transactions/new-transaction/new-transaction.component';
|
|
||||||
import { CategoriesComponent } from './categories/categories.component';
|
|
||||||
import { CategoryDetailsComponent } from './categories/category-details/category-details.component';
|
|
||||||
import { NewCategoryComponent } from './categories/new-category/new-category.component';
|
|
||||||
import { LoginComponent } from './users/login/login.component';
|
|
||||||
import { RegisterComponent } from './users/register/register.component';
|
|
||||||
import { BudgetsComponent } from './budgets/budget.component';
|
|
||||||
import { NewBudgetComponent } from './budgets/new-budget/new-budget.component';
|
|
||||||
import { EditBudgetComponent } from './budgets/edit-budget/edit-budget.component';
|
|
||||||
import { BudgetDetailsComponent } from './budgets/budget-details/budget-details.component';
|
|
||||||
import { EditCategoryComponent } from './categories/edit-category/edit-category.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{ path: '', component: BudgetsComponent },
|
|
||||||
{ path: 'login', component: LoginComponent },
|
|
||||||
{ path: 'register', component: RegisterComponent },
|
|
||||||
{ path: 'budgets', component: BudgetsComponent },
|
|
||||||
{ path: 'budgets/new', component: NewBudgetComponent },
|
|
||||||
{ path: 'budgets/:id', component: BudgetDetailsComponent },
|
|
||||||
{ path: 'budgets/:id/edit', component: EditBudgetComponent },
|
|
||||||
{ path: '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>
|
|