added vue-bootstrap

added income/expense input
added Frequency enum

Signed-off-by: Pascal Syma <pascal@syma.dev>
This commit is contained in:
2021-05-13 20:25:53 +02:00
parent f40dcc8438
commit d5146239c0
7 changed files with 483 additions and 14 deletions
+3 -3
View File
@@ -2,11 +2,11 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>expense</title> <title>expense</title>
</head> </head>
<body> <body class="py-4">
<div id="app"></div> <div id="app" class="container"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
</html> </html>
+121
View File
@@ -166,6 +166,66 @@
} }
} }
}, },
"@nuxt/opencollective": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.3.2.tgz",
"integrity": "sha512-XG7rUdXG9fcafu9KTDIYjJSkRO38EwjlKYIb5TQ/0WDbiTUTtUtgncMscKOYzfsY86kGs05pAuMOR+3Fi0aN3A==",
"requires": {
"chalk": "^4.1.0",
"consola": "^2.15.0",
"node-fetch": "^2.6.1"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"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==",
"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=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"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==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"@popperjs/core": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz",
"integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q=="
},
"@types/json5": { "@types/json5": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -2018,6 +2078,23 @@
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
"dev": true "dev": true
}, },
"bootstrap": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz",
"integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw=="
},
"bootstrap-vue": {
"version": "2.21.2",
"resolved": "https://registry.npmjs.org/bootstrap-vue/-/bootstrap-vue-2.21.2.tgz",
"integrity": "sha512-0Exe+4MZysqhZNXIKf4TzkvXaupxh9EHsoCRez0o5Dc0J7rlafayOEwql63qXv74CgZO8E4U8ugRNJko1vMvNw==",
"requires": {
"@nuxt/opencollective": "^0.3.2",
"bootstrap": ">=4.5.3 <5.0.0",
"popper.js": "^1.16.1",
"portal-vue": "^2.1.7",
"vue-functional-data-merge": "^3.1.0"
}
},
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -2901,6 +2978,11 @@
"integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
"dev": true "dev": true
}, },
"consola": {
"version": "2.15.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
"integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="
},
"console-browserify": { "console-browserify": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
@@ -5319,6 +5401,15 @@
"jest-regex-util": "^22.4.3" "jest-regex-util": "^22.4.3"
} }
}, },
"exports-loader": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/exports-loader/-/exports-loader-2.0.0.tgz",
"integrity": "sha512-k/VFrVEUmotfkk8vZ+njG5NEXpr5Ee+BonV+AYINV2hNo3o+/UB8nEuCUQk2k6IyWIoobmXoTFO0igxrQcMV4Q==",
"dev": true,
"requires": {
"source-map": "^0.6.1"
}
},
"express": { "express": {
"version": "4.17.1", "version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
@@ -7867,6 +7958,11 @@
"merge-stream": "^1.0.1" "merge-stream": "^1.0.1"
} }
}, },
"jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
},
"js-base64": { "js-base64": {
"version": "2.6.4", "version": "2.6.4",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
@@ -8707,6 +8803,11 @@
} }
} }
}, },
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"node-forge": { "node-forge": {
"version": "0.10.0", "version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
@@ -9405,6 +9506,16 @@
"integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==",
"dev": true "dev": true
}, },
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
},
"portal-vue": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/portal-vue/-/portal-vue-2.1.7.tgz",
"integrity": "sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g=="
},
"portfinder": { "portfinder": {
"version": "1.0.28", "version": "1.0.28",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
@@ -14425,6 +14536,11 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==" "integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
}, },
"vue-enums": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/vue-enums/-/vue-enums-1.0.0.tgz",
"integrity": "sha512-fCLmWRSzl6O7e9BkQ+aFdrWpvIxLlNIWswjvvCLkzsCQ4/nSCVrRd7Tr+9/651d1FGOe5v3fuPIFVP6Cjsv+0Q=="
},
"vue-eslint-parser": { "vue-eslint-parser": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz",
@@ -14456,6 +14572,11 @@
} }
} }
}, },
"vue-functional-data-merge": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
"integrity": "sha512-leT4kdJVQyeZNY1kmnS1xiUlQ9z1B/kdBFCILIjYYQDqZgLqCLa0UhjSSeRX6c3mUe6U5qYeM8LrEqkHJ1B4LA=="
},
"vue-hot-reload-api": { "vue-hot-reload-api": {
"version": "2.3.4", "version": "2.3.4",
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
+8 -1
View File
@@ -13,7 +13,13 @@
"build": "node build/build.js" "build": "node build/build.js"
}, },
"dependencies": { "dependencies": {
"vue": "^2.5.2" "@popperjs/core": "^2.9.2",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.21.2",
"jquery": "^3.6.0",
"popper.js": "^1.16.1",
"vue": "^2.5.2",
"vue-enums": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^7.1.2", "autoprefixer": "^7.1.2",
@@ -41,6 +47,7 @@
"eslint-plugin-promise": "^3.4.0", "eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1", "eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.0.0", "eslint-plugin-vue": "^4.0.0",
"exports-loader": "^2.0.0",
"extract-text-webpack-plugin": "^3.0.0", "extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4", "file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1", "friendly-errors-webpack-plugin": "^1.6.1",
+81 -10
View File
@@ -1,28 +1,99 @@
<template> <template>
<div id="app"> <div id="app" class="container">
<img src="./assets/logo.png"> <b-row class="results">
<HelloWorld/> <b-col sm="12" offset-md="1" md="10">
<b-card header="Result" title="Special title treatment">
<b-card-text>With supporting text below as a natural lead-in to additional content.</b-card-text>
<p>Income: {{ calculated.yearly.in | currency }}</p>
<p>Expenses: {{ calculated.yearly.out | currency }}</p>
<p>Profit: <span :class="calculated.yearly.sum < 0 ? 'text-danger' : 'text-success'">{{ calculated.yearly.sum | currency }}</span></p>
<!-- <b-button variant="primary" size="lg" @click="calculate">Calculate</b-button>-->
</b-card>
</b-col>
</b-row>
<b-row class="inputs">
<Input title="Income" color="success" :entries="income" />
<Input title="Expenses" color="danger" :entries="expenses" />
</b-row>
</div> </div>
</template> </template>
<script> <script>
import HelloWorld from './components/HelloWorld' import HelloWorld from './components/HelloWorld'
import Input from './components/Input'
import Frequency from './enums/Frequency'
export default { export default {
name: 'App', name: 'App',
components: { components: {
Input,
HelloWorld HelloWorld
},
data () {
return {
income: [],
expenses: [],
calculated: {
yearly: {
in: 0,
out: 0,
sum: 0
}
}
}
},
mounted () {
const storage = localStorage.getItem('expenses')
if (storage) {
try {
const storageObj = JSON.parse(storage)
if (storageObj.expenses && storageObj.income) {
this.$set(this, 'expenses', storageObj.expenses)
this.$set(this, 'income', storageObj.income)
}
} catch (e) {
}
}
this.calculate()
},
watch: {
income: {
deep: true,
handler: 'calculate'
},
expenses: {
deep: true,
handler: 'calculate'
}
},
methods: {
calculate () {
const storage = JSON.stringify({expenses: this.expenses, income: this.income})
localStorage.setItem('expenses', storage)
let calc = {
in: 0,
out: 0,
sum: 0
}
calc.in = [...this.income.map(i => Frequency.byId(i.frequency).interval * parseFloat(i.amount)), ...this.income.filter(i => i.frequency === 0).map(i => parseFloat(i.amount))].reduce((c, v) => c + v, 0)
calc.out = [...this.expenses.map(i => Frequency.byId(i.frequency).interval * parseFloat(i.amount)), ...this.expenses.filter(i => i.frequency === 0).map(i => parseFloat(i.amount))].reduce((c, v) => c + v, 0)
calc.sum = calc.in - calc.out
this.$set(this.calculated, 'yearly', calc)
}
} }
} }
</script> </script>
<style> <style>
#app { body {
font-family: 'Avenir', Helvetica, Arial, sans-serif; overflow-y: scroll;
-webkit-font-smoothing: antialiased; }
-moz-osx-font-smoothing: grayscale; .inputs {
text-align: center; margin-top: 20px;
color: #2c3e50;
margin-top: 60px;
} }
</style> </style>
+209
View File
@@ -0,0 +1,209 @@
<template>
<b-col md="6">
<b-card no-body>
<b-card-header :header-bg-variant="color" header-text-variant="white" class="container-fluid">
<b-row>
<b-col md="10">
<h3 class="w-75 p-3">{{ title }}</h3>
</b-col>
<b-col md="2" class="float-right align-self-center">
<b-button @click="add()" variant="primary" size="lg"><b-icon-plus /></b-button>
</b-col>
</b-row>
</b-card-header>
<b-card-body>
<b-table :fields="fields" :items="data" responsive="sm" v-model="currentItems">
<template #cell(index)="data">
{{ data.index + 1 }}
</template>
<template #cell(options)="data">
<b-button-group size="sm">
<b-button @click="toggleDetails(data)" v-b-tooltip.hover :title="data.detailsShowing ? 'Cancel' : 'Edit'" :variant="data.detailsShowing ? 'outline-danger' : 'primary'">
<b-icon-x-square v-if="data.detailsShowing" />
<b-icon-pencil-square v-else />
</b-button>
<b-button @click="askDelete(data)" v-b-tooltip.hover title="Remove" variant="danger"><b-icon-trash /></b-button>
</b-button-group>
</template>
<template #cell()="data">
{{ data.value }}
</template>
<template #cell(amount)="data">
{{ data.item.amount | currency }}
</template>
<template #cell(frequency)="data">
{{ $enums.Frequency.byId(data.item.frequency).name }}
</template>
<template #row-details="row">
<b-card>
<b-form @submit.prevent="save(row)">
<b-form-group
id="name-group"
label="Name:"
label-for="name">
<b-form-input
id="name"
v-model="editingCache.name"
type="text"
placeholder="Name"
required></b-form-input>
</b-form-group>
<b-form-group
id="amount-group"
label="Amount:"
label-for="amount">
<b-input-group append="€" class="mb-2 mr-sm-2 mb-sm-0">
<b-form-input
id="amount"
v-model="editingCache.amount"
type="number"
placeholder="0,00"
step="0.01"
min="0.01"
required></b-form-input>
</b-input-group>
</b-form-group>
<b-form-group
id="frequency-group"
label="Frequency:"
label-for="frequency">
<b-form-select
id="frequency"
v-model="editingCache.frequency"
type="text"
:options="$enums.Frequency.options()"
required></b-form-select>
</b-form-group>
<b-button type="submit" variant="success"><b-icon-check /> Save</b-button>
</b-form>
</b-card>
</template>
</b-table>
</b-card-body>
</b-card>
</b-col>
</template>
<script>
import Vue from 'vue'
import Frequency from '../enums/Frequency'
export default {
name: 'Input',
enums: {
Frequency
},
props: [
'title',
'color',
'entries'
],
data () {
return {
fields: [
{
key: 'index',
label: '#'
},
'name',
'amount',
'frequency',
{
key: 'options',
label: ''
}
],
data: [],
currentItems: [],
editingCache: undefined,
editing: undefined
}
},
mounted () {
this.$set(this, 'data', this.$props.entries)
},
methods: {
add () {
this.data.push({
name: '',
amount: 1.00,
frequency: 0
})
this.toggleDetails({index: this.data.length - 1, toggleDetails: () => {}})
},
askDelete (row) {
// ask before deletion
this.$bvModal.msgBoxConfirm(`Please confirm that you want to delete '${row.item.name}'?`, {
title: 'Are you sure?',
size: 'sm',
buttonSize: 'sm',
okVariant: 'danger',
okTitle: 'Yes',
cancelTitle: 'No',
footerClass: 'p-2',
hideHeaderClose: false,
centered: true
})
.then(confirmedDelete => {
if (!confirmedDelete) return
this.data.splice(row.index, 1)
})
},
save (row) {
// override active data with from cache
this.$set(this.data, row.index, this.editingCache)
this.toggleDetails(row)
},
toggleDetails (row) {
// close all open details
this.currentItems.forEach((item, i) => {
if (i === row.index) return
this.$set(item, '_showDetails', false)
})
// create (or reset) local copy for form data
if (row.detailsShowing) {
this.editingCache = undefined
} else {
this.editingCache = Vue.util.extend({}, row.item)
}
row.toggleDetails()
}
},
watch: {
data: {
deep: true,
handler () {
this.$emit('input', this.data)
}
},
entries: {
deep: true,
handler () {
this.$set(this, 'data', this.$props.entries)
}
}
}
}
</script>
<style>
/* highlight selected row */
.b-table-has-details {
background: var(--dark);
color: var(--white);
}
.b-table-has-details + tr {
background: var(--dark);
}
</style>
+36
View File
@@ -0,0 +1,36 @@
class Frequency {
constructor (name, interval) {
this.name = name
this.interval = interval
}
}
Frequency.ONCE = new Frequency('Once', 0)
Frequency.YEARLY = new Frequency('Yearly', 1)
Frequency.BIMONTHLY = new Frequency('Bi-Monthly', 6)
Frequency.MONTHLY = new Frequency('Monthly', 12)
Frequency.BIWEEKLY = new Frequency('Bi-Weekly', 26)
Frequency.WEEKLY = new Frequency('Weekly', 52)
Frequency.DAILY = new Frequency('Daily', 52 * 7)
Frequency.byId = id => {
try { return Frequency[Object.keys(Frequency)[id]] } catch (e) { return Frequency.ONCE }
}
Frequency.values = () => {
let keys = Object.keys(Frequency)
keys.splice(-3, 3)
return keys.map(k => Frequency[k])
}
Frequency.options = () => {
return Frequency.values().map((f, i) => {
return {
value: i,
text: f.name
}
})
}
export default Frequency
+25
View File
@@ -1,7 +1,32 @@
// The Vue build version to load with the `import` command // The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias. // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue' import Vue from 'vue'
import enums from 'vue-enums'
import App from './App' import App from './App'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/js/bootstrap'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
Vue.use(enums, {namespace: '$enums'})
// Make BootstrapVue available throughout your project
Vue.use(BootstrapVue)
// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)
Vue.filter('currency', function (value) {
if (typeof value !== 'number') {
try {
value = parseFloat(value)
} catch (e) {
return value
}
}
return `${value.toFixed(2)}`
})
Vue.config.productionTip = false Vue.config.productionTip = false