WIP: Port over work from twigs-nextcloud
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
71a4d8e8a5
commit
1752c5d2d0
32 changed files with 1611 additions and 219 deletions
1
.env.development
Normal file
1
.env.development
Normal file
|
@ -0,0 +1 @@
|
||||||
|
VUE_APP_API_URL=https://3000code.brawner.home
|
54
README.md
54
README.md
|
@ -1,24 +1,38 @@
|
||||||
# twigs-web
|
# Twigs
|
||||||
|
|
||||||
## Project setup
|
## Vue Migration Checklist
|
||||||
```
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and hot-reloads for development
|
_Could also be used as a testing checklist_
|
||||||
```
|
|
||||||
npm run serve
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and minifies for production
|
- [ ] Login
|
||||||
```
|
- [ ] Logout
|
||||||
npm run build
|
- [ ] 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
|
||||||
|
|
||||||
### Lints and fixes files
|
|
||||||
```
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
||||||
### Customize configuration
|
|
||||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
||||||
|
|
160
package-lock.json
generated
160
package-lock.json
generated
|
@ -1739,6 +1739,16 @@
|
||||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"cacache": {
|
"cacache": {
|
||||||
"version": "13.0.1",
|
"version": "13.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz",
|
||||||
|
@ -1765,6 +1775,34 @@
|
||||||
"unique-filename": "^1.1.1"
|
"unique-filename": "^1.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"chalk": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"find-cache-dir": {
|
"find-cache-dir": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz",
|
||||||
|
@ -1786,6 +1824,25 @@
|
||||||
"path-exists": "^4.0.0"
|
"path-exists": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"loader-utils": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"big.js": "^5.2.2",
|
||||||
|
"emojis-list": "^3.0.0",
|
||||||
|
"json5": "^2.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"locate-path": {
|
"locate-path": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
@ -1850,6 +1907,16 @@
|
||||||
"minipass": "^3.1.1"
|
"minipass": "^3.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"terser-webpack-plugin": {
|
"terser-webpack-plugin": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
|
||||||
|
@ -1866,6 +1933,18 @@
|
||||||
"terser": "^4.6.12",
|
"terser": "^4.6.12",
|
||||||
"webpack-sources": "^1.4.3"
|
"webpack-sources": "^1.4.3"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"vue-loader-v16": {
|
||||||
|
"version": "npm:vue-loader@16.0.0-rc.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-rc.1.tgz",
|
||||||
|
"integrity": "sha512-yR+BS90EOXTNieasf8ce9J3TFCpm2DGqoqdbtiwQ33hon3FyIznLX7sKavAq1VmfBnOeV6It0Htg4aniv8ph1g==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"hash-sum": "^2.0.0",
|
||||||
|
"loader-utils": "^2.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -11153,87 +11232,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue-loader-v16": {
|
|
||||||
"version": "npm:vue-loader@16.0.0-beta.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.8.tgz",
|
|
||||||
"integrity": "sha512-oouKUQWWHbSihqSD7mhymGPX1OQ4hedzAHyvm8RdyHh6m3oIvoRF+NM45i/bhNOlo8jCnuJhaSUf/6oDjv978g==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"chalk": "^4.1.0",
|
|
||||||
"hash-sum": "^2.0.0",
|
|
||||||
"loader-utils": "^2.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"color-convert": "^2.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"chalk": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"ansi-styles": "^4.1.0",
|
|
||||||
"supports-color": "^7.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"color-convert": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"color-name": "~1.1.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"color-name": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"has-flag": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"loader-utils": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"big.js": "^5.2.2",
|
|
||||||
"emojis-list": "^3.0.0",
|
|
||||||
"json5": "^2.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"supports-color": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"has-flag": "^4.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vue-router": {
|
"vue-router": {
|
||||||
"version": "3.4.6",
|
"version": "3.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.6.tgz",
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<link rel="stylesheet" href="<%= BASE_URL %>style.css">
|
||||||
<title>Twigs</title>
|
<title>Twigs</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
<strong>We're sorry but Twigs doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
37
public/style.css
Normal file
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);
|
||||||
|
}
|
35
src/App.vue
35
src/App.vue
|
@ -1,32 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div>
|
||||||
<div id="nav">
|
<RouterView />
|
||||||
<router-link to="/">Home</router-link> |
|
|
||||||
<router-link to="/about">About</router-link>
|
|
||||||
</div>
|
|
||||||
<router-view/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
<style>
|
<style>
|
||||||
#app {
|
.app-twigs {
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
flex-grow: 1;
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav {
|
.app-twigs > div {
|
||||||
padding: 30px;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
#nav a {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav a.router-link-exact-active {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
|
@ -1,58 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="hello">
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
<p>
|
|
||||||
For a guide and recipes on how to configure / customize this project,<br>
|
|
||||||
check out the
|
|
||||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
|
||||||
</p>
|
|
||||||
<h3>Installed CLI Plugins</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Essential Links</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
|
||||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
|
||||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
|
||||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
|
||||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Ecosystem</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
|
||||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
|
||||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'HelloWorld',
|
|
||||||
props: {
|
|
||||||
msg: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped>
|
|
||||||
h3 {
|
|
||||||
margin: 40px 0 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
63
src/components/ProgressBar.vue
Normal file
63
src/components/ProgressBar.vue
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress" :class="status" :style="{ width: progress + '%' }" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ProgressBar',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
invertColors: Boolean,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
progress: function() {
|
||||||
|
return Math.round((this.value / this.max) * 100)
|
||||||
|
},
|
||||||
|
status: function() {
|
||||||
|
if (this.progress <= 33) {
|
||||||
|
return this.invertColors ? 'danger' : 'good'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.progress <= 66) {
|
||||||
|
return 'warn'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.progress <= 100) {
|
||||||
|
return this.invertColors ? 'good' : 'danger'
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.progress-bar {
|
||||||
|
height: 0.5em;
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 1em;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
transition: width ease-in-out 0.5s;
|
||||||
|
}
|
||||||
|
.progress.good {
|
||||||
|
background: var(--good-color);
|
||||||
|
}
|
||||||
|
.progress.warn {
|
||||||
|
background: var(--warn-color);
|
||||||
|
}
|
||||||
|
.progress.danger {
|
||||||
|
background: var(--danger-color);
|
||||||
|
}
|
||||||
|
</style>
|
20
src/components/TwigsHome.vue
Normal file
20
src/components/TwigsHome.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<div class="twigs-home">
|
||||||
|
<h2>Welcome to Twigs!</h2>
|
||||||
|
<p>To get started, create a new budget or select one from the list to the left.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TwigsHome',
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
118
src/components/budget/BudgetDetails.vue
Normal file
118
src/components/budget/BudgetDetails.vue
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="budget" class="budget">
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-info">
|
||||||
|
<h2>{{ budget.name }}</h2>
|
||||||
|
<p>{{ budget.description }}</p>
|
||||||
|
<h3
|
||||||
|
v-if="balance"
|
||||||
|
>Balance: {{ balance.toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button @click="addTransaction()">
|
||||||
|
<span class="icon-add" /> Add Transaction
|
||||||
|
</button>
|
||||||
|
<button @click="addCategory()">
|
||||||
|
<span class="icon-add" /> Add Category
|
||||||
|
</button>
|
||||||
|
<button @click="editBudget()">Edit</button>
|
||||||
|
<button @click="deleteBudget()">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="budget-details">
|
||||||
|
<div class="card income">
|
||||||
|
<h3>Income</h3>
|
||||||
|
<CategoryList :budget-id="budget.id" :expense="false" />
|
||||||
|
</div>
|
||||||
|
<div class="card expenses">
|
||||||
|
<h3>Expenses</h3>
|
||||||
|
<CategoryList :budget-id="budget.id" :expense="true" />
|
||||||
|
</div>
|
||||||
|
<div class="card transactions">
|
||||||
|
<h3>Recent Transactions</h3>
|
||||||
|
<TransactionList :budget-id="budget.id" :limit="5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!budget">
|
||||||
|
<p>Select a budget from the list to the left to get started.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapState } from "vuex";
|
||||||
|
import CategoryList from "../category/CategoryList";
|
||||||
|
import TransactionList from "../transaction/TransactionList";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "BudgetDetails",
|
||||||
|
components: {
|
||||||
|
CategoryList,
|
||||||
|
TransactionList
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(["budgets", "currentBudget"]),
|
||||||
|
...mapGetters(["budget"]),
|
||||||
|
balance: function(state) {
|
||||||
|
if (!state.currentBudget) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return this.$store.getters.budgetBalance(state.currentBudget) / 100;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
load() {
|
||||||
|
this.$store.dispatch("budgetDetailsViewed", this.$route.params.id);
|
||||||
|
},
|
||||||
|
addTransaction() {
|
||||||
|
this.$store.dispatch("addTransactionClicked");
|
||||||
|
},
|
||||||
|
addCategory() {
|
||||||
|
this.$store.dispatch("addCategoryClicked");
|
||||||
|
},
|
||||||
|
editBudget() {
|
||||||
|
this.$store.dispatch("editBudgetClicked", this.$route.params.id);
|
||||||
|
},
|
||||||
|
deleteBudget() {
|
||||||
|
this.$store.dispatch("deleteBudgetClicked", this.$route.params.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-details {
|
||||||
|
display: grid;
|
||||||
|
justify-content: space-between;
|
||||||
|
grid-gap: 0.5em;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.income {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expenses {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transactions {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
</style>
|
90
src/components/budget/BudgetForm.vue
Normal file
90
src/components/budget/BudgetForm.vue
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="!loading" class="add-edit-budget">
|
||||||
|
<h2>{{ budget.id ? 'Edit' : 'Add' }} Budget</h2>
|
||||||
|
<input v-model="budget.name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
title="Name">
|
||||||
|
<textarea v-model="budget.description" placeholder="Description" title="Description" />
|
||||||
|
<div class="sharing">
|
||||||
|
<h3>Sharing</h3>
|
||||||
|
<input v-model="user"
|
||||||
|
type="test"
|
||||||
|
placeholde="User"
|
||||||
|
title="User"
|
||||||
|
@keyup.enter="addPermission()">
|
||||||
|
<ul v-if="budget.users" class="sharing-users">
|
||||||
|
<li v-for="userPermission in budget.users" :key="userPermission.user">
|
||||||
|
<span v-if="userPermission.user">
|
||||||
|
{{ userPermission.user }}
|
||||||
|
</span>
|
||||||
|
<span v-if="userPermission.permission">
|
||||||
|
: {{ userPermission.permission }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button @click="saveBudget()">
|
||||||
|
Save Budget
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" class="icon-loading" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BudgetForm',
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
budget: {
|
||||||
|
default: {},
|
||||||
|
type: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
saving: false,
|
||||||
|
user: undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
loading: state => state.budget === undefined || state.saving,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addPermission() {
|
||||||
|
const user = this.user
|
||||||
|
this.user = undefined
|
||||||
|
this.budget.users = this.budget.users.filter(u => u.user !== user)
|
||||||
|
this.budget.users.push({
|
||||||
|
user: user,
|
||||||
|
permission: 2,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
saveBudget() {
|
||||||
|
this.saving = true
|
||||||
|
this.$store.dispatch('budgetFormSaveClicked', this.budget)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.add-edit-budget > * {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
.radio-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-container label {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
.icon-loading {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
31
src/components/budget/BudgetList.vue
Normal file
31
src/components/budget/BudgetList.vue
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<li @click="newBudget()">New Budget</li>
|
||||||
|
<li v-for="budget in budgets" :key="budget.id">
|
||||||
|
<router-link :to="{ name: 'budgetDetails', params: { id: budget.id } }">{{ budget.name }}</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "BudgetList",
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["budgets"])
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
load: function() {
|
||||||
|
this.$store.dispatch("budgetListViewed");
|
||||||
|
},
|
||||||
|
newBudget: function() {
|
||||||
|
this.$store.dispatch("addBudgetClicked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
20
src/components/budget/EditBudget.vue
Normal file
20
src/components/budget/EditBudget.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<BudgetForm :budget="budget" />
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import BudgetForm from './BudgetForm'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditBudget',
|
||||||
|
components: {
|
||||||
|
BudgetForm,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['budget']),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('editBudgetViewed', this.$route.params.id)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
20
src/components/budget/NewBudget.vue
Normal file
20
src/components/budget/NewBudget.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<BudgetForm :budget="budget" />
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import BudgetForm from './BudgetForm'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NewBudget',
|
||||||
|
components: {
|
||||||
|
BudgetForm,
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
budget: {
|
||||||
|
users: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
66
src/components/category/CategoryDetails.vue
Normal file
66
src/components/category/CategoryDetails.vue
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="category" class="category-details">
|
||||||
|
<div class="header-info">
|
||||||
|
<h2>{{ category.title }}</h2>
|
||||||
|
<h3>
|
||||||
|
Balance: {{ balance.toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}
|
||||||
|
</h3>
|
||||||
|
<div class="actions">
|
||||||
|
<button @click="addTransaction()">
|
||||||
|
<span class="icon-add" /> Add Transaction
|
||||||
|
</button>
|
||||||
|
<button @click="editCategory()">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button @click="deleteCategory()">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>Transactions</h3>
|
||||||
|
<TransactionList :category-id="category.id" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import TransactionList from '../transaction/TransactionList'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CategoryDetails',
|
||||||
|
components: {
|
||||||
|
TransactionList,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['categories', 'currentCategory']),
|
||||||
|
category: function(state) {
|
||||||
|
if (state.categories.length === 0 || !state.currentCategory) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return state.categories.find((category) => category.id === Number.parseInt(state.currentCategory))
|
||||||
|
},
|
||||||
|
balance: function(state) {
|
||||||
|
if (!state.currentCategory) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return this.$store.getters.categoryBalance(state.currentCategory) / 100
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.load()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
load() {
|
||||||
|
this.$store.dispatch('categoryDetailsViewed', this.$route.params.id)
|
||||||
|
},
|
||||||
|
addTransaction() {
|
||||||
|
this.$store.dispatch('addTransactionClicked')
|
||||||
|
},
|
||||||
|
editCategory() {
|
||||||
|
this.$store.dispatch('editCategoryClicked', this.$route.params.id)
|
||||||
|
},
|
||||||
|
deleteCategory() {
|
||||||
|
this.$store.dispatch('deleteCategoryClicked', this.$route.params.id)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
91
src/components/category/CategoryForm.vue
Normal file
91
src/components/category/CategoryForm.vue
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="!loading" class="add-edit-category">
|
||||||
|
<h2>{{ category.id ? 'Edit' : 'Add' }} Category</h2>
|
||||||
|
<input v-model="category.title"
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
title="Name">
|
||||||
|
<input v-model.number="category.amount"
|
||||||
|
type="number"
|
||||||
|
placeholder="Amount"
|
||||||
|
title="Amount">
|
||||||
|
<div class="radio-container">
|
||||||
|
<input id="expense"
|
||||||
|
v-model="category.expense"
|
||||||
|
type="radio"
|
||||||
|
:value="true">
|
||||||
|
<label for="expense">Expense</label>
|
||||||
|
<input id="income"
|
||||||
|
v-model="category.expense"
|
||||||
|
type="radio"
|
||||||
|
:value="false">
|
||||||
|
<label for="income">Income</label>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<input v-model="category.archived" type="checkbox"/>
|
||||||
|
<label>Archived</label>
|
||||||
|
</div>
|
||||||
|
<select v-model="category.budgetId" @change="updateCategories()">
|
||||||
|
<option disabled value>
|
||||||
|
Select a budget
|
||||||
|
</option>
|
||||||
|
<option v-for="budget in budgets" :key="budget.id" :value="budget.id">
|
||||||
|
{{ budget.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button @click="saveCategory()">
|
||||||
|
Save Category
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" class="icon-loading" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CategoryForm',
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
category: {
|
||||||
|
default: () => {},
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
saving: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['budgets']),
|
||||||
|
loading: state => state.category === undefined || state.saving,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveCategory() {
|
||||||
|
this.saving = true
|
||||||
|
this.$store.dispatch('categoryFormSaveClicked', this.category)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.add-edit-category > * {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
.radio-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-container label {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
.icon-loading {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
85
src/components/category/CategoryList.vue
Normal file
85
src/components/category/CategoryList.vue
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<li v-for="category in filteredCategories" :key="category.id">
|
||||||
|
<a class="category-summary" @click="view(category.id)">
|
||||||
|
<div class="category-info">
|
||||||
|
<p class="category-name">{{ category.title }}</p>
|
||||||
|
<p class="category-balance">
|
||||||
|
{{ category.expense ? "Remaining" : "Pending" }}:
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
categoryRemainingBalance(category) / 100
|
||||||
|
).toLocaleString(undefined, {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD"
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ProgressBar
|
||||||
|
:max="category.amount"
|
||||||
|
:value="Math.abs(categoryBalance(category.id))"
|
||||||
|
:invert-colors="!category.expense" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapState } from 'vuex'
|
||||||
|
import ProgressBar from '../ProgressBar'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CategoryList',
|
||||||
|
components: {
|
||||||
|
ProgressBar,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
budgetId: {
|
||||||
|
default: 0,
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
expense: {
|
||||||
|
default: true,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['categories', 'currentCategory']),
|
||||||
|
...mapGetters(['categoryBalance', 'categoryRemainingBalance']),
|
||||||
|
filteredCategories: function(state) {
|
||||||
|
return state.categories.filter(
|
||||||
|
category => category.expense === this.expense && category.archived === false
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
view: function(id) {
|
||||||
|
this.$store.dispatch('categoryClicked', id)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.category-summary {
|
||||||
|
padding: 0.5em;
|
||||||
|
height: 4em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-summary * {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-summary:hover {
|
||||||
|
background: var(--color-background-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-summary .category-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
20
src/components/category/EditCategory.vue
Normal file
20
src/components/category/EditCategory.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<CategoryForm :category="category" />
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import CategoryForm from './CategoryForm'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditCategory',
|
||||||
|
components: {
|
||||||
|
CategoryForm,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['category']),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('editCategoryViewed', this.$route.params.id)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
21
src/components/category/NewCategory.vue
Normal file
21
src/components/category/NewCategory.vue
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<CategoryForm :category="category" />
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import CategoryForm from './CategoryForm'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NewCategory',
|
||||||
|
components: {
|
||||||
|
CategoryForm,
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
category: {
|
||||||
|
expense: true,
|
||||||
|
budgetId: this.$store.state.currentBudget,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
20
src/components/transaction/EditTransaction.vue
Normal file
20
src/components/transaction/EditTransaction.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<TransactionForm :transaction="transaction" />
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import TransactionForm from './TransactionForm'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditTransaction',
|
||||||
|
components: {
|
||||||
|
TransactionForm,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['transaction']),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('editTransactionViewed', this.$route.params.id)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
23
src/components/transaction/NewTransaction.vue
Normal file
23
src/components/transaction/NewTransaction.vue
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<TransactionForm :transaction="transaction" />
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import TransactionForm from './TransactionForm'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NewTransaction',
|
||||||
|
components: {
|
||||||
|
TransactionForm,
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
transaction: {
|
||||||
|
date: new Date(),
|
||||||
|
expense: true,
|
||||||
|
budgetId: this.$store.state.currentBudget,
|
||||||
|
categoryId: this.$store.state.currentCategory,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
69
src/components/transaction/TransactionDetails.vue
Normal file
69
src/components/transaction/TransactionDetails.vue
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="transaction" class="transaction-details">
|
||||||
|
<h2>{{ transaction.title }}</h2>
|
||||||
|
<h3
|
||||||
|
:class="transaction.expense ? 'danger' : 'good'">
|
||||||
|
{{ (transaction.amount / 100).toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }} {{ transaction.expense ? 'Expense' : 'Income' }}
|
||||||
|
</h3>
|
||||||
|
<p class="transaction-info date">
|
||||||
|
{{ new Date(transaction.date).toLocaleDateString() }}
|
||||||
|
</p>
|
||||||
|
<p class="transaction-info description">
|
||||||
|
{{ transaction.description }}
|
||||||
|
</p>
|
||||||
|
<p v-if="category" class="transaction-info category">
|
||||||
|
Category: {{ category.title }}
|
||||||
|
</p>
|
||||||
|
<p v-if="budget" class="transaction-info budget">
|
||||||
|
Budget: {{ budget.name }}
|
||||||
|
</p>
|
||||||
|
<p class="transaction-info registered-by">
|
||||||
|
Registered By:
|
||||||
|
<!-- <UserBubble
|
||||||
|
:user="transaction.createdBy"
|
||||||
|
:display-name="transaction.createdBy" /> -->
|
||||||
|
{{ new Date(transaction.createdDate).toLocaleDateString() }}
|
||||||
|
</p>
|
||||||
|
<p v-if="transaction.updatedBy" class="transaction-info updated-by">
|
||||||
|
Updated By:
|
||||||
|
<!-- <UserBubble
|
||||||
|
:user="transaction.updatedBy"
|
||||||
|
:display-name="transaction.updatedBy" /> -->
|
||||||
|
{{ new Date(transaction.updatedDate).toLocaleDateString() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TransactionDetails',
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['transaction']),
|
||||||
|
category: function() {
|
||||||
|
const transaction = this.$store.getters.transaction
|
||||||
|
if (!transaction || !transaction.categoryId) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return this.$store.getters.categories.find(category => category.id === transaction.categoryId)
|
||||||
|
},
|
||||||
|
budget: function() {
|
||||||
|
const transaction = this.$store.getters.transaction
|
||||||
|
if (!transaction || !transaction.budgetId) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return this.$store.getters.budgets.find(budget => budget.id === transaction.budgetId)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.load()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
load() {
|
||||||
|
this.$store.dispatch('transactionDetailsViewed', this.$route.params.id)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
95
src/components/transaction/TransactionForm.vue
Normal file
95
src/components/transaction/TransactionForm.vue
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="!loading" class="add-edit-transaction">
|
||||||
|
<h2>{{ transaction.id ? "Edit" : "Add" }} Transaction</h2>
|
||||||
|
<input v-model="transaction.title" type="text" placeholder="Name" title="Name" />
|
||||||
|
<textarea v-model="transaction.description" placeholder="Description" title="Description" />
|
||||||
|
<input v-model.number="transaction.amount" type="number" placeholder="Amount" title="Amount" />
|
||||||
|
<!-- <DatetimePicker :value="transaction.date" type="datetime" /> -->
|
||||||
|
<div class="radio-container">
|
||||||
|
<input id="expense" v-model="transaction.expense" type="radio" :value="true" />
|
||||||
|
<label for="expense">Expense</label>
|
||||||
|
<input id="income" v-model="transaction.expense" type="radio" :value="false" />
|
||||||
|
<label for="income">Income</label>
|
||||||
|
</div>
|
||||||
|
<select v-model="transaction.budgetId" @change="updateCategories()">
|
||||||
|
<option disabled value>Select a budget</option>
|
||||||
|
<option v-for="budget in budgets" :key="budget.id" :value="budget.id">
|
||||||
|
{{ budget.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<select v-model="transaction.categoryId">
|
||||||
|
<option disabled value>Select a category</option>
|
||||||
|
<option
|
||||||
|
v-for="category in filteredCategories"
|
||||||
|
:key="category.id"
|
||||||
|
:value="category.id"
|
||||||
|
>{{ category.title }}</option>
|
||||||
|
</select>
|
||||||
|
<button @click="saveTransaction()">Save Transaction</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" class="icon-loading" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "TransactionForm",
|
||||||
|
components: {},
|
||||||
|
props: {
|
||||||
|
transaction: {
|
||||||
|
default: () => {},
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
saving: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["budgets"]),
|
||||||
|
filteredCategories: function(state) {
|
||||||
|
return this.$store.getters.categories.filter(function(category) {
|
||||||
|
return category.budgetId === state.transaction.budgetId && category.expense === state.transaction.expense;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
loading: state => state.transaction === undefined || state.saving
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.updateCategories();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateCategories() {
|
||||||
|
if (!this.transaction) return;
|
||||||
|
this.$store.dispatch(
|
||||||
|
"addEditTransactionBudgetSelected",
|
||||||
|
this.transaction.budgetId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
saveTransaction() {
|
||||||
|
this.saving = true;
|
||||||
|
this.$store.dispatch("transactionFormSaveClicked", this.transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.add-edit-transaction > * {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
.radio-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-container label {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
.icon-loading {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
85
src/components/transaction/TransactionList.vue
Normal file
85
src/components/transaction/TransactionList.vue
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<li v-for="transaction in filteredTransactions" :key="transaction.id">
|
||||||
|
<a class="transaction" @click="view(transaction.id)">
|
||||||
|
<div class="transaction-details">
|
||||||
|
<p class="transaction-name">{{ transaction.title }}</p>
|
||||||
|
<p class="transaction-date">{{ new Date(transaction.date).toLocaleDateString() }}</p>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="transaction-amount"
|
||||||
|
:class="transaction.expense ? 'danger' : 'good'">{{ (transaction.amount / 100).toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TransactionList',
|
||||||
|
components: {},
|
||||||
|
props: {
|
||||||
|
budgetId: {
|
||||||
|
default: 0,
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
categoryId: {
|
||||||
|
default: 0,
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
default: 0,
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['transactions', 'currentTransaction']),
|
||||||
|
filteredTransactions: function(state) {
|
||||||
|
const transactions = state.transactions.filter(function(transaction) {
|
||||||
|
if (state.budgetId) {
|
||||||
|
return transaction.budgetId === state.budgetId
|
||||||
|
}
|
||||||
|
if (state.categoryId) {
|
||||||
|
return transaction.categoryId === state.categoryId
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (this.limit !== 0) {
|
||||||
|
return transactions.slice(0, this.limit)
|
||||||
|
} else {
|
||||||
|
return transactions
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
view: function(id) {
|
||||||
|
this.$store.dispatch('transactionClicked', id)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.transaction {
|
||||||
|
padding: 0.5em;
|
||||||
|
height: 4em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction * {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction:hover {
|
||||||
|
background: var(--color-background-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
55
src/components/user/Login.vue
Normal file
55
src/components/user/Login.vue
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="!loading" class="login">
|
||||||
|
<h2>Login</h2>
|
||||||
|
<input v-model="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="Username or Email"
|
||||||
|
title="Username or Email">
|
||||||
|
<input v-model="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
title="Password">
|
||||||
|
<button @click="login()">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" class="icon-loading" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Login',
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
props: [
|
||||||
|
'username',
|
||||||
|
'password'
|
||||||
|
],
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
login() {
|
||||||
|
this.loading = true
|
||||||
|
this.$store.dispatch('loginSubmitClicked', {
|
||||||
|
username: this.username,
|
||||||
|
password: this.password
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.login > * {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
.icon-loading {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
0
src/components/user/Profile.vue
Normal file
0
src/components/user/Profile.vue
Normal file
0
src/components/user/Register.vue
Normal file
0
src/components/user/Register.vue
Normal file
|
@ -1,23 +1,81 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import Home from '../views/Home.vue'
|
import TwigsHome from '../components/TwigsHome'
|
||||||
|
import Login from '../components/user/Login'
|
||||||
|
import NewBudget from '../components/budget/NewBudget'
|
||||||
|
import EditBudget from '../components/budget/EditBudget'
|
||||||
|
import BudgetDetails from '../components/budget/BudgetDetails'
|
||||||
|
import BudgetList from '../components/budget/BudgetList'
|
||||||
|
import NewCategory from '../components/category/NewCategory'
|
||||||
|
import EditCategory from '../components/category/EditCategory'
|
||||||
|
import CategoryDetails from '../components/category/CategoryDetails'
|
||||||
|
import NewTransaction from '../components/transaction/NewTransaction'
|
||||||
|
import EditTransaction from '../components/transaction/EditTransaction'
|
||||||
|
import TransactionDetails from '../components/transaction/TransactionDetails'
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Home',
|
name: 'home',
|
||||||
component: Home
|
component: TwigsHome,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/about',
|
path: '/login',
|
||||||
name: 'About',
|
name: 'login',
|
||||||
// route level code-splitting
|
component: Login,
|
||||||
// this generates a separate chunk (about.[hash].js) for this route
|
},
|
||||||
// which is lazy-loaded when the route is visited.
|
{
|
||||||
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
|
path: '/budgets',
|
||||||
}
|
name: 'budgets',
|
||||||
|
component: BudgetList,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/budgets/new',
|
||||||
|
name: 'newBudget',
|
||||||
|
component: NewBudget,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/budgets/:id',
|
||||||
|
name: 'budgetDetails',
|
||||||
|
component: BudgetDetails,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/budgets/:id/edit',
|
||||||
|
name: 'editBudget',
|
||||||
|
component: EditBudget,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/categories/new',
|
||||||
|
name: 'newCategory',
|
||||||
|
component: NewCategory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/categories/:id',
|
||||||
|
name: 'categoryDetails',
|
||||||
|
component: CategoryDetails,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/categories/:id/edit',
|
||||||
|
name: 'editCategory',
|
||||||
|
component: EditCategory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/transactions/new',
|
||||||
|
name: 'newTransaction',
|
||||||
|
component: NewTransaction,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/transactions/:id',
|
||||||
|
name: 'transactionDetails',
|
||||||
|
component: TransactionDetails,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/transactions/:id/edit',
|
||||||
|
name: 'editTransaction',
|
||||||
|
component: EditTransaction,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
|
|
|
@ -1,15 +1,399 @@
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
|
import router from '../router'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
const API_URL = process.env.VUE_APP_API_URL;
|
||||||
|
|
||||||
|
function authenticatedHeaders(authentication) {
|
||||||
|
let authorization;
|
||||||
|
if (authentication !== undefined) {
|
||||||
|
authorization = authentication
|
||||||
|
} else {
|
||||||
|
const authCookie = document.cookie.split(';')
|
||||||
|
.map(cookie => cookie.trim())
|
||||||
|
.find(cookie => cookie.indexOf('authorization') === 0);
|
||||||
|
if (authCookie) {
|
||||||
|
authorization = authCookie.slice(14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const headers = new Headers({
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
if (authorization) {
|
||||||
|
headers.append('Authorization', authorization);
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
|
user: undefined,
|
||||||
|
budgets: [],
|
||||||
|
budgetBalances: {},
|
||||||
|
currentBudget: 0,
|
||||||
|
categories: [],
|
||||||
|
categoryBalances: {},
|
||||||
|
currentCategory: 0,
|
||||||
|
transactions: [],
|
||||||
|
currentTransaction: 0,
|
||||||
},
|
},
|
||||||
mutations: {
|
getters: {
|
||||||
|
user: (state) => state.user,
|
||||||
|
budgets: (state) => state.budgets,
|
||||||
|
budget: (state) => state.budgets.find(budget => budget.id === state.currentBudget),
|
||||||
|
budgetBalance: (state) => (id) => state.budgetBalances[id],
|
||||||
|
categories: (state) => state.categories,
|
||||||
|
category: (state) => state.categories.find(category => category.id === state.currentCategory),
|
||||||
|
categoryBalance: (state) => (categoryId) => {
|
||||||
|
return state.categoryBalances[categoryId] || 0
|
||||||
|
},
|
||||||
|
categoryRemainingBalance: (state, getters) => (category) => {
|
||||||
|
const modifier = category.expense ? -1 : 1
|
||||||
|
return category.amount - (getters.categoryBalance(category.id) * modifier)
|
||||||
|
},
|
||||||
|
transactions: (state) => state.transactions,
|
||||||
|
transaction: (state) => state.transactions.find(transaction => transaction.id === state.currentTransaction),
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
loginClicked() {
|
||||||
|
router.push({ name: 'login' })
|
||||||
|
},
|
||||||
|
loginSubmitClicked({ commit }, credentials) {
|
||||||
|
const auth = `Basic ${btoa(credentials.username + ':' + credentials.password)}`.trim();
|
||||||
|
get('users/me')
|
||||||
|
.then(user => {
|
||||||
|
document.cookie = `authorization=${auth}`;
|
||||||
|
commit('setUser', user)
|
||||||
|
router.push({ name: 'budgets' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addBudgetClicked({ commit }) {
|
||||||
|
router.push({ name: 'newBudget' })
|
||||||
|
},
|
||||||
|
budgetListViewed({ commit }) {
|
||||||
|
get('budgets')
|
||||||
|
.then(budgets => {
|
||||||
|
commit('setBudgets', budgets)
|
||||||
|
budgets.forEach(budget => {
|
||||||
|
get(`budgets/${budget.id}/balance`)
|
||||||
|
.then(balance => {
|
||||||
|
commit({
|
||||||
|
type: 'setBudgetBalance',
|
||||||
|
...balance,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
budgetClicked({ commit }, budgetId) {
|
||||||
|
router.push({ name: 'budgetDetails', params: { id: budgetId } })
|
||||||
|
},
|
||||||
|
editBudgetViewed({ commit, state, getters }, budgetId) {
|
||||||
|
commit('setCurrentBudget', budgetId)
|
||||||
|
if (budgetId !== undefined && getters.budget === undefined) {
|
||||||
|
get(`${API_URL}/budgets/${budgetId}`)
|
||||||
|
.then(budgets => {
|
||||||
|
commit('setBudgets', [budgets])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
budgetFormSaveClicked({ commit }, budget) {
|
||||||
|
let request
|
||||||
|
if (budget.id) {
|
||||||
|
request = put(`budgets/${budget.id}`, budget)
|
||||||
|
} else {
|
||||||
|
request = post(`budgets`, budget)
|
||||||
|
}
|
||||||
|
request.then(budget => {
|
||||||
|
commit('addBudget', budget)
|
||||||
|
router.push({ name: 'budgetDetails', params: { id: budget.id } })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
budgetDetailsViewed({ commit, state, getters }, budgetId) {
|
||||||
|
commit('setCurrentBudget', budgetId)
|
||||||
|
if (budgetId !== undefined && getters.budget === undefined) {
|
||||||
|
fetch(`${API_URL}/budgets`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(budgets => {
|
||||||
|
commit('setBudgets', budgets)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
commit('setCategories', [])
|
||||||
|
commit('setTransactions', [])
|
||||||
|
commit('setCurrentCategory', undefined)
|
||||||
|
fetch(`${API_URL}/categories?budgetIds=${budgetId}`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(categories => {
|
||||||
|
commit('setCategories', categories)
|
||||||
|
categories.forEach(category => {
|
||||||
|
fetch(`${API_URL}/categories/${category.id}/balance`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(balance => {
|
||||||
|
commit({
|
||||||
|
type: 'setCategoryBalance',
|
||||||
|
...balance,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
fetch(`${API_URL}/transactions?budgetId=${budgetId}?count=10`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(transactions => commit('setTransactions', transactions))
|
||||||
|
},
|
||||||
|
editBudgetClicked({ commit }, budgetId) {
|
||||||
|
router.push({ name: 'editBudget', params: { id: budgetId } })
|
||||||
|
},
|
||||||
|
deleteBudgetClicked({ commit }, budgetId) {
|
||||||
|
fetch(`${API_URL}/budgets/${budgetId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: authenticatedHeaders()
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
commit('deleteBudget', budgetId)
|
||||||
|
router.push({ name: 'home' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
categoryClicked({ commit }, categoryId) {
|
||||||
|
router.push({ name: 'categoryDetails', params: { id: categoryId } })
|
||||||
|
},
|
||||||
|
addCategoryClicked({ commit }) {
|
||||||
|
router.push({ name: 'newCategory' })
|
||||||
|
},
|
||||||
|
editCategoryClicked({ commit }, categoryId) {
|
||||||
|
router.push({ name: 'editCategory', params: { id: categoryId } })
|
||||||
|
},
|
||||||
|
editCategoryViewed({ commit, state, getters }, categoryId) {
|
||||||
|
commit('setCurrentCategory', categoryId)
|
||||||
|
if (categoryId !== undefined && getters.category === undefined) {
|
||||||
|
fetch(`${API_URL}/categories/${categoryId}`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(category => {
|
||||||
|
commit('setCategories', [category])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
categoryDetailsViewed({ commit, state }, categoryId) {
|
||||||
|
commit('setCurrentCategory', categoryId)
|
||||||
|
if (state.categories.length === 0) {
|
||||||
|
fetch(`${API_URL}/categories/${categoryId}`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(category => {
|
||||||
|
commit('setCategories', [category])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fetch(`${API_URL}/transactions?categoryId=${categoryId}`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(transactions => commit('setTransactions', transactions))
|
||||||
|
},
|
||||||
|
categoryFormSaveClicked({ commit }, category) {
|
||||||
|
let request
|
||||||
|
if (category.id) {
|
||||||
|
request = put(`categories/${category.id}`, category)
|
||||||
|
} else {
|
||||||
|
request = post('categories', category)
|
||||||
|
}
|
||||||
|
request.then(category => {
|
||||||
|
commit('addCategory', category)
|
||||||
|
router.push({ name: 'categoryDetails', params: { id: category.id } })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteCategoryClicked({ commit, state }, categoryId) {
|
||||||
|
fetch(`${API_URL}/categories/${categoryId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: authenticatedHeaders(),
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
commit('setCurrentCategory', undefined)
|
||||||
|
commit('deleteCategory', categoryId)
|
||||||
|
router.push({ name: 'budgetDetails', params: { id: state.currentBudget } })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addTransactionClicked({ commit }) {
|
||||||
|
router.push({ name: 'newTransaction' })
|
||||||
|
},
|
||||||
|
editTransactionViewed({ commit, state, getters }, transactionId) {
|
||||||
|
commit('setCurrentTransaction', transactionId)
|
||||||
|
if (transactionId !== undefined && getters.transaction === undefined) {
|
||||||
|
fetch(`${API_URL}/transactions/${transactionId}`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(transactions => {
|
||||||
|
commit('setTransactions', [transactions])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addEditTransactionBudgetSelected({ commit, state }, budgetId) {
|
||||||
|
commit('setCategories', [])
|
||||||
|
if (!budgetId) return
|
||||||
|
fetch(`${API_URL}/categories?budgetId=${budgetId}`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(categories => {
|
||||||
|
commit('setCategories', categories)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
transactionFormSaveClicked({ commit }, transaction) {
|
||||||
|
let request
|
||||||
|
if (transaction.id) {
|
||||||
|
request = put(`transactions/${transaction.id}`, transaction)
|
||||||
|
} else {
|
||||||
|
request = post('transactions', transaction)
|
||||||
|
}
|
||||||
|
request.then(res => res.json())
|
||||||
|
.then(transaction => {
|
||||||
|
commit('addTransaction', transaction)
|
||||||
|
router.push({ name: 'transactionDetails', params: { id: transaction.id } })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
transactionClicked({ commit }, transactionId) {
|
||||||
|
router.push({ name: 'transactionDetails', params: { id: transactionId } })
|
||||||
|
},
|
||||||
|
transactionDetailsViewed({ commit, state }, transactionId) {
|
||||||
|
commit('setCurrentTransaction', transactionId)
|
||||||
|
|
||||||
|
if (state.transactions.length === 0) {
|
||||||
|
fetch(`${API_URL}/transactions/${transactionId}`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(transaction => {
|
||||||
|
commit('setTransactions', [transaction])
|
||||||
|
if (state.categories.length === 0) {
|
||||||
|
fetch(`${API_URL}/categories?budgetId=${transaction.budgetId}`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(categories => {
|
||||||
|
commit('setCategories', categories)
|
||||||
|
categories.forEach(category => {
|
||||||
|
fetch(`${API_URL}/categories/${category.id}/balance`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(balance => {
|
||||||
|
commit({
|
||||||
|
type: 'setCategoryBalance',
|
||||||
|
...balance,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
modules: {
|
mutations: {
|
||||||
|
setUser(state, user) {
|
||||||
|
state.user = user
|
||||||
|
},
|
||||||
|
addBudget(state, budget) {
|
||||||
|
state.budgets = [
|
||||||
|
...state.budgets.filter(b => b.id !== budget.id),
|
||||||
|
budget,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
setCurrentBudget(state, budgetId) {
|
||||||
|
state.currentBudget = Number.parseInt(budgetId)
|
||||||
|
},
|
||||||
|
setBudgetBalance(state, data) {
|
||||||
|
state.budgetBalances = {
|
||||||
|
...state.budgetBalances,
|
||||||
|
[data.budgetId]: data.balance,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setBudgets(state, budgets) {
|
||||||
|
state.budgets = budgets
|
||||||
|
},
|
||||||
|
deleteBudget(state, budget) {
|
||||||
|
state.budgets = [
|
||||||
|
...state.budgets.filter(b => b.id !== budget.id),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
addCategory(state, category) {
|
||||||
|
state.categories = [
|
||||||
|
...state.categories.filter(c => c.id !== category.id),
|
||||||
|
category,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
setCurrentCategory(state, categoryId) {
|
||||||
|
state.currentCategory = Number.parseInt(categoryId)
|
||||||
|
},
|
||||||
|
setCategories(state, data) {
|
||||||
|
state.categories = data
|
||||||
|
},
|
||||||
|
setCategoryBalance(state, data) {
|
||||||
|
state.categoryBalances = {
|
||||||
|
...state.categoryBalances,
|
||||||
|
[data.id]: data.balance,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteCategory(state, category) {
|
||||||
|
state.categories = [
|
||||||
|
...state.categories.filter(c => c.id !== category.id),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
addTransaction(state, transaction) {
|
||||||
|
state.transactions = [
|
||||||
|
...state.transactions.filter(t => t.id !== transaction.id),
|
||||||
|
transaction,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
setTransactions(state, data) {
|
||||||
|
state.transactions = data
|
||||||
|
},
|
||||||
|
setCurrentTransaction(state, transactionId) {
|
||||||
|
state.currentTransaction = Number.parseInt(transactionId)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function get(path) {
|
||||||
|
return fetch(`${API_URL}/${path}`, { headers: authenticatedHeaders() })
|
||||||
|
.then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json()
|
||||||
|
} else {
|
||||||
|
throw Error(res.statusText)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(path, body) {
|
||||||
|
return fetch(`${API_URL}/${path}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: authenticatedHeaders(),
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json()
|
||||||
|
} else {
|
||||||
|
throw Error(res.statusText)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function put(path, body) {
|
||||||
|
return fetch(`${API_URL}/${path}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: authenticatedHeaders(),
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json()
|
||||||
|
} else {
|
||||||
|
throw Error(res.statusText)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function del(path) {
|
||||||
|
return fetch(`${API_URL}/${path}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: authenticatedHeaders()
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json()
|
||||||
|
} else {
|
||||||
|
throw Error(res.statusText)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="about">
|
|
||||||
<h1>This is an about page</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,18 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="home">
|
|
||||||
<img alt="Vue logo" src="../assets/logo.png">
|
|
||||||
<HelloWorld msg="Welcome to Your Vue.js App"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// @ is an alias to /src
|
|
||||||
import HelloWorld from '@/components/HelloWorld.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'Home',
|
|
||||||
components: {
|
|
||||||
HelloWorld
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
Loading…
Reference in a new issue