Compare commits
61 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3d02e46f2a | ||
![]() |
f21a7298fe | ||
![]() |
b04fa3eb72 | ||
![]() |
32d1bd430f | ||
![]() |
b6e8b74eef | ||
![]() |
18936d9efd | ||
![]() |
86fbf8f164 | ||
![]() |
7bc9991be0 | ||
![]() |
0609fdebf7 | ||
![]() |
0f56b478fc | ||
![]() |
e6012bddb8 | ||
![]() |
4f355fc58f | ||
![]() |
95d6e41c81 | ||
![]() |
daac09d1e4 | ||
![]() |
a1bf81aa56 | ||
![]() |
8c6fe828ad | ||
![]() |
8edbe48293 | ||
![]() |
8421785aff | ||
![]() |
6fd233da87 | ||
![]() |
99fbe0e5ec | ||
![]() |
6d58eb8d35 | ||
![]() |
95afae16ee | ||
![]() |
229b0b7f2e | ||
![]() |
d778ce29e7 | ||
![]() |
4879a8e46c | ||
![]() |
abd92f503f | ||
![]() |
4572d63274 | ||
![]() |
0ffb184267 | ||
![]() |
e94383dd25 | ||
![]() |
606bf7d19e | ||
![]() |
e85fc22544 | ||
![]() |
df4cb06a49 | ||
![]() |
c1f3da15f1 | ||
![]() |
29220093dd | ||
![]() |
85c43e95e5 | ||
![]() |
10f816cba9 | ||
![]() |
4fa08c68ec | ||
![]() |
6db418ec19 | ||
![]() |
ad9024a4a2 | ||
![]() |
c4ea9758d3 | ||
![]() |
7436d414c0 | ||
![]() |
721600b12d | ||
![]() |
2a1f5764a1 | ||
![]() |
4189bdf919 | ||
![]() |
dee27b51f5 | ||
![]() |
e537d8e3a6 | ||
![]() |
3392f8cbd6 | ||
![]() |
427732bf1a | ||
![]() |
31cebd28c2 | ||
![]() |
6666857a0b | ||
![]() |
720fc6182d | ||
![]() |
a0a405f849 | ||
![]() |
cebf7fc749 | ||
![]() |
695ea388a6 | ||
![]() |
6e949eb199 | ||
![]() |
df089d2f77 | ||
![]() |
edede0f2ce | ||
![]() |
cd61210a4c | ||
![]() |
a7cb817050 | ||
![]() |
f19b1bc387 | ||
![]() |
609fef4159 |
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,8 +2,6 @@
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
89
CHANGELOG.md
89
CHANGELOG.md
|
@ -5,6 +5,95 @@ All notable changes to 'byjove' will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## [0.1.26] - 2024-06-03
|
||||||
|
### Changed
|
||||||
|
- Optionally allow decimal quantities for receiving.
|
||||||
|
|
||||||
|
## [0.1.25] - 2024-05-29
|
||||||
|
### Changed
|
||||||
|
- Show actual error text if applicable, for receiving failure.
|
||||||
|
|
||||||
|
## [0.1.24] - 2024-03-26
|
||||||
|
### Changed
|
||||||
|
- Add delay when fetching rows data for model CRUD component.
|
||||||
|
|
||||||
|
## [0.1.23] - 2023-12-26
|
||||||
|
### Changed
|
||||||
|
- Improve focus behavior for inventory count view.
|
||||||
|
- Keep row filters "raw" until encoding for actual request.
|
||||||
|
|
||||||
|
## [0.1.22] - 2023-10-23
|
||||||
|
### Changed
|
||||||
|
- Use columns instead of table, for row receiving quantities.
|
||||||
|
- Show invoice number when receiving row.
|
||||||
|
- Center receiving buttons; add "12 EA" quick receive button.
|
||||||
|
|
||||||
|
## [0.1.21] - 2023-10-06
|
||||||
|
### Changed
|
||||||
|
- Auto-trim username field in login form.
|
||||||
|
- Auto-focus case/unit field when showing inventory count page.
|
||||||
|
|
||||||
|
## [0.1.20] - 2023-09-17
|
||||||
|
### Changed
|
||||||
|
- Validate amount when adding receiving quantity.
|
||||||
|
- Show "missing" quantities for item receiving.
|
||||||
|
|
||||||
|
## [0.1.19] - 2023-08-29
|
||||||
|
### Changed
|
||||||
|
- Add support for "missing" credit in mobile receiving.
|
||||||
|
|
||||||
|
## [0.1.18] - 2023-08-08
|
||||||
|
### Changed
|
||||||
|
- Add UPC quick lookup for View Product page.
|
||||||
|
- Add slot for "default panels" in View Product.
|
||||||
|
|
||||||
|
## [0.1.17] - 2023-08-03
|
||||||
|
### Changed
|
||||||
|
- Update to use lts/gallium version for nodejs.
|
||||||
|
|
||||||
|
## [0.1.16] - 2023-01-30
|
||||||
|
### Changed
|
||||||
|
- Add Prices panel for default product view.
|
||||||
|
|
||||||
|
## [0.1.15] - 2023-01-07
|
||||||
|
### Changed
|
||||||
|
- Add basic components for Product CRUD.
|
||||||
|
- Update dependencies.
|
||||||
|
- Center logo and text for home page.
|
||||||
|
|
||||||
|
## [0.1.14] - 2022-11-16
|
||||||
|
### Changed
|
||||||
|
- Detect 404 notfound when viewing CRUD record; warn user accordingly.
|
||||||
|
- Increase spacing around "Create" button for model-index.
|
||||||
|
- Put available settings in the store; etc.
|
||||||
|
- Add components for login form and customer field.
|
||||||
|
- Rename methods provided by plugin.
|
||||||
|
- Add home page component; fix user state issues.
|
||||||
|
- Allow static data set for autocomplete component.
|
||||||
|
- Add styles, warnings template for receiving component.
|
||||||
|
- Let item-level receiving default option reflect "what's left".
|
||||||
|
|
||||||
|
## [0.1.13] - 2021-10-20
|
||||||
|
### Changed
|
||||||
|
- Allow for "native sort" params in model index.
|
||||||
|
- Allow override of "create" permission for model index.
|
||||||
|
|
||||||
|
## [0.1.12] - 2021-08-04
|
||||||
|
### Changed
|
||||||
|
- Use better class name for logo image styles.
|
||||||
|
- Allow multiple feedback recipient options.
|
||||||
|
|
||||||
|
## [0.1.11] - 2021-04-12
|
||||||
|
### Changed
|
||||||
|
- Allow "any" number for inventory cases/units, including decimal.
|
||||||
|
|
||||||
|
## [0.1.10] - 2021-02-10
|
||||||
|
### Changed
|
||||||
|
- Add `package-lock.json` back to src repo.
|
||||||
|
- Add "angle-down" icon for user menu button.
|
||||||
|
|
||||||
## [0.1.9] - 2020-04-24
|
## [0.1.9] - 2020-04-24
|
||||||
### Changed
|
### Changed
|
||||||
- Wrap "create new object" button, for padding.
|
- Wrap "create new object" button, for padding.
|
||||||
|
|
23339
package-lock.json
generated
Normal file
23339
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
19
package.json
19
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "byjove",
|
"name": "byjove",
|
||||||
"version": "0.1.9",
|
"version": "0.1.26",
|
||||||
"description": "Generic-ish app components for Vue.js frontend to Tailbone API backend",
|
"description": "Generic-ish app components for Vue.js frontend to Tailbone API backend",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"rattail",
|
"rattail",
|
||||||
|
@ -14,21 +14,22 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build --target lib --name byjove src/index.js"
|
"build": "vue-cli-service build --target lib --name byjove src/index.js",
|
||||||
|
"build-watch": "vue-cli-service build --target lib --name byjove --watch src/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^2.6.10"
|
"vue": "^2.7.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-service": "^3.0.5",
|
"@vue/cli-service": "^3.0.5",
|
||||||
"rollup": "^1.26.3",
|
"rollup": "^1.32.1",
|
||||||
"rollup-plugin-buble": "^0.19.8",
|
"rollup-plugin-buble": "^0.19.8",
|
||||||
"rollup-plugin-commonjs": "^10.1.0",
|
"rollup-plugin-commonjs": "^10.1.0",
|
||||||
"rollup-plugin-vue": "^5.1.2",
|
"rollup-plugin-vue": "^5.1.9",
|
||||||
"vue-resource": "^1.5.1",
|
"vue-resource": "^1.5.3",
|
||||||
"vue-router": "^3.1.3",
|
"vue-router": "^3.6.5",
|
||||||
"vue-template-compiler": "^2.6.10",
|
"vue-template-compiler": "^2.7.14",
|
||||||
"vuex": "^3.1.1"
|
"vuex": "^3.6.2"
|
||||||
},
|
},
|
||||||
"main": "./dist/byjove.umd.js",
|
"main": "./dist/byjove.umd.js",
|
||||||
"files": [
|
"files": [
|
||||||
|
|
|
@ -42,13 +42,16 @@ export default {
|
||||||
this.$http.get('/api/session').then(response => {
|
this.$http.get('/api/session').then(response => {
|
||||||
|
|
||||||
// let all of app know who the user is(n't)
|
// let all of app know who the user is(n't)
|
||||||
this.$store.commit('SET_USER', response.data.user)
|
this.$store.commit('SET_USER', response.data.user || {})
|
||||||
this.$store.commit('SET_USER_IS_ADMIN', response.data.user ? response.data.user.is_admin : false)
|
this.$store.commit('SET_USER_IS_ADMIN', response.data.user ? response.data.user.is_admin : false)
|
||||||
this.$store.commit('SET_USER_IS_ROOT', response.data.user ? response.data.user.is_root : false)
|
this.$store.commit('SET_USER_IS_ROOT', response.data.user ? response.data.user.is_root : false)
|
||||||
|
|
||||||
// also keep track of user's permissions
|
// also keep track of user's permissions
|
||||||
this.$store.commit('SET_PERMISSIONS', response.data.permissions)
|
this.$store.commit('SET_PERMISSIONS', response.data.permissions)
|
||||||
|
|
||||||
|
// also keep track of backend app settings
|
||||||
|
this.$store.commit('SET_SETTINGS', response.data.settings || {})
|
||||||
|
|
||||||
// declare the session established
|
// declare the session established
|
||||||
this.$store.commit('SET_SESSION_ESTABLISHED', true)
|
this.$store.commit('SET_SESSION_ESTABLISHED', true)
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,15 @@
|
||||||
v-show="!selected"
|
v-show="!selected"
|
||||||
v-model="autocompleteValue"
|
v-model="autocompleteValue"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
|
field="label"
|
||||||
:icon="icon"
|
:icon="icon"
|
||||||
:data="data"
|
:data="autocompleteData"
|
||||||
@typing="getAsyncData"
|
@typing="getAsyncData"
|
||||||
@select="selectionMade"
|
@select="selectionMade"
|
||||||
|
:open-on-focus="openOnFocus"
|
||||||
keep-first>
|
keep-first>
|
||||||
<template slot-scope="props">
|
<template #header>
|
||||||
{{ props.option.label }}
|
<slot name="header"></slot>
|
||||||
</template>
|
</template>
|
||||||
</b-autocomplete>
|
</b-autocomplete>
|
||||||
|
|
||||||
|
@ -28,7 +30,18 @@ export default {
|
||||||
name: 'ByjoveAutocomplete',
|
name: 'ByjoveAutocomplete',
|
||||||
props: {
|
props: {
|
||||||
name: String,
|
name: String,
|
||||||
serviceUrl: String,
|
serviceUrl: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
dataFilter: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
value: String,
|
value: String,
|
||||||
initialLabel: String,
|
initialLabel: String,
|
||||||
placeholder: {
|
placeholder: {
|
||||||
|
@ -39,7 +52,12 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
openOnFocus: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
let selected = null
|
let selected = null
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
|
@ -49,12 +67,30 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
data: [],
|
fetchedData: [],
|
||||||
selected: selected,
|
selected: selected,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
autocompleteValue: this.value,
|
autocompleteValue: this.value,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
autocompleteData() {
|
||||||
|
return this.serviceUrl ? this.fetchedData : this.filteredData
|
||||||
|
},
|
||||||
|
|
||||||
|
filteredData() {
|
||||||
|
if (this.dataFilter) {
|
||||||
|
return this.data.filter((option) => {
|
||||||
|
return this.dataFilter(this.autocompleteValue, option)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return this.data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
clearSelection() {
|
clearSelection() {
|
||||||
|
@ -66,6 +102,13 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO: i am not clear yet what the best pattern is for setting
|
||||||
|
// the selected option. this does not seem complete but did the
|
||||||
|
// trick for what i needed atm..?
|
||||||
|
setSelected(option) {
|
||||||
|
this.selected = option
|
||||||
|
},
|
||||||
|
|
||||||
selectionMade(option) {
|
selectionMade(option) {
|
||||||
this.selected = option
|
this.selected = option
|
||||||
this.$emit('input', option ? option.value : null)
|
this.$emit('input', option ? option.value : null)
|
||||||
|
@ -74,16 +117,22 @@ export default {
|
||||||
// TODO: buefy example uses `debounce()` here and perhaps we should too?
|
// TODO: buefy example uses `debounce()` here and perhaps we should too?
|
||||||
// https://buefy.org/documentation/autocomplete
|
// https://buefy.org/documentation/autocomplete
|
||||||
getAsyncData: function (entry) {
|
getAsyncData: function (entry) {
|
||||||
|
|
||||||
|
// we only fetch async data via URL if we have one!
|
||||||
|
if (!this.serviceUrl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.length < 3) {
|
if (entry.length < 3) {
|
||||||
this.data = []
|
this.fetchedData = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isFetching = true
|
this.isFetching = true
|
||||||
this.$http.get(this.serviceUrl, {params: {term: entry}}).then((response) => {
|
this.$http.get(this.serviceUrl, {params: {term: entry}}).then((response) => {
|
||||||
this.data = response.data
|
this.fetchedData = response.data
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
this.data = []
|
this.fetchedData = []
|
||||||
throw error
|
throw error
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isFetching = false
|
this.isFetching = false
|
||||||
|
|
162
src/components/customers/ByjoveCustomerField.vue
Normal file
162
src/components/customers/ByjoveCustomerField.vue
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
<template>
|
||||||
|
<b-field :label="label" expanded
|
||||||
|
:type="fieldType">
|
||||||
|
|
||||||
|
<byjove-autocomplete v-if="!readonly && !useDropdown"
|
||||||
|
v-model="customerUUID"
|
||||||
|
ref="autocomplete"
|
||||||
|
:service-url="autocompleteUrl"
|
||||||
|
@input="customerChanged">
|
||||||
|
</byjove-autocomplete>
|
||||||
|
|
||||||
|
<b-select v-if="!readonly && useDropdown"
|
||||||
|
v-model="customerUUID"
|
||||||
|
@input="customerChanged">
|
||||||
|
|
||||||
|
<option v-for="customer in customers"
|
||||||
|
:key="customer.uuid"
|
||||||
|
:value="customer.uuid">
|
||||||
|
{{ customer.name }}
|
||||||
|
</option>
|
||||||
|
|
||||||
|
</b-select>
|
||||||
|
|
||||||
|
<router-link v-if="readonly"
|
||||||
|
:to="`/customers/${value}`">
|
||||||
|
{{ readonlyDisplay || value }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
</b-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ByjoveAutocomplete from '../autocomplete'
|
||||||
|
export default {
|
||||||
|
name: 'ByjoveCustomerField',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ByjoveAutocomplete,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: "Customer",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
readonlyDisplay: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
type: Boolean,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
autocompleteUrl: {
|
||||||
|
type: String,
|
||||||
|
// TODO: should not hard-code the "full" api endpoint url here
|
||||||
|
default: '/api/customers/autocomplete',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
inited: false,
|
||||||
|
customerUUID: null,
|
||||||
|
customers: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
fieldType() {
|
||||||
|
if (this.required && !this.customerUUID) {
|
||||||
|
return 'is-danger'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
useDropdown() {
|
||||||
|
|
||||||
|
// use whatever caller specified, if they did so
|
||||||
|
if (this.dropdown !== null) {
|
||||||
|
return this.dropdown
|
||||||
|
}
|
||||||
|
|
||||||
|
// use setting, provided session is loaded
|
||||||
|
if (this.$store.state.session_established) {
|
||||||
|
return this.$store.state.settings['customer_field_dropdown']
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise assume false by default
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'$store.state.session_established': 'init',
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// only need to init once
|
||||||
|
if (this.inited) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// cannot init until session is established
|
||||||
|
if (!this.$store.state.session_established) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inited = true
|
||||||
|
|
||||||
|
// fetch any needed data
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchData() {
|
||||||
|
|
||||||
|
// nothing to fetch if not using dropdown
|
||||||
|
if (!this.useDropdown) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all customers for dropdown
|
||||||
|
this.$http.get('/api/customers?sort=name|asc').then(response => {
|
||||||
|
this.customers = response.data.customers
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
customerChanged(value) {
|
||||||
|
this.$emit('input', value)
|
||||||
|
},
|
||||||
|
|
||||||
|
setCustomer(customer) {
|
||||||
|
this.$refs.autocomplete.selectionMade({
|
||||||
|
value: customer.uuid,
|
||||||
|
label: customer._str,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
27
src/components/customers/index.js
Normal file
27
src/components/customers/index.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import ByjoveCustomerField from './ByjoveCustomerField.vue'
|
||||||
|
|
||||||
|
// Declare install function executed by Vue.use()
|
||||||
|
export function install(Vue) {
|
||||||
|
if (install.installed) return;
|
||||||
|
install.installed = true;
|
||||||
|
Vue.component('ByjoveCustomerField', ByjoveCustomerField);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create module definition for Vue.use()
|
||||||
|
const plugin = {
|
||||||
|
install,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-install when vue is found (eg. in browser via <script> tag)
|
||||||
|
let GlobalVue = null;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
GlobalVue = window.Vue;
|
||||||
|
} else if (typeof global !== 'undefined') {
|
||||||
|
GlobalVue = global.Vue;
|
||||||
|
}
|
||||||
|
if (GlobalVue) {
|
||||||
|
GlobalVue.use(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// To allow use as module (npm/webpack/etc.) export component
|
||||||
|
export default ByjoveCustomerField
|
|
@ -12,6 +12,24 @@
|
||||||
<section class="modal-card-body">
|
<section class="modal-card-body">
|
||||||
<p class="modal-card-title">User Feedback</p>
|
<p class="modal-card-title">User Feedback</p>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
<div v-if="hasRecipientOptions">
|
||||||
|
<p class="block">
|
||||||
|
Comments welcome! Just let us know who they should go to.
|
||||||
|
</p>
|
||||||
|
<b-field label="Who To Notify?"
|
||||||
|
:type="recipient ? null : 'is-danger'">
|
||||||
|
<b-select v-model="recipient">
|
||||||
|
<option v-for="option in allRecipientOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!hasRecipientOptions">
|
||||||
<p>
|
<p>
|
||||||
Questions, suggestions, comments, complaints, etc. regarding this
|
Questions, suggestions, comments, complaints, etc. regarding this
|
||||||
website are welcome and may be submitted below.
|
website are welcome and may be submitted below.
|
||||||
|
@ -22,6 +40,7 @@
|
||||||
{{ referringURL }}
|
{{ referringURL }}
|
||||||
</div>
|
</div>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
<b-field label="Your Name"
|
<b-field label="Your Name"
|
||||||
v-show="!userUUID">
|
v-show="!userUUID">
|
||||||
|
@ -36,12 +55,12 @@
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<b-button @click="showFeedbackDialog = false">
|
<b-button @click="cancelFeedback()">
|
||||||
Cancel
|
Cancel
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
@click="sendFeedback()"
|
@click="sendFeedback()"
|
||||||
:disabled="!message">
|
:disabled="sendIsDisabled">
|
||||||
Send Note
|
Send Note
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,29 +78,64 @@ export default {
|
||||||
url: String,
|
url: String,
|
||||||
// userUUID: String,
|
// userUUID: String,
|
||||||
// user: Object,
|
// user: Object,
|
||||||
|
recipientOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showFeedbackDialog: false,
|
showFeedbackDialog: false,
|
||||||
referringURL: null,
|
referringURL: null,
|
||||||
|
recipient: null,
|
||||||
userUUID: null,
|
userUUID: null,
|
||||||
userName: null,
|
userName: null,
|
||||||
message: null,
|
message: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
allRecipientOptions() {
|
||||||
|
if (this.recipientOptions !== null) {
|
||||||
|
return this.recipientOptions
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
hasRecipientOptions() {
|
||||||
|
return !!this.allRecipientOptions.length
|
||||||
|
},
|
||||||
|
sendIsDisabled() {
|
||||||
|
if (this.hasRecipientOptions && !this.recipient) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!this.message) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
clearForm() {
|
||||||
|
this.recipient = null
|
||||||
|
this.message = null
|
||||||
|
},
|
||||||
|
|
||||||
showFeedback() {
|
showFeedback() {
|
||||||
this.referringURL = location.href
|
this.referringURL = location.href
|
||||||
if (this.$store.state.user) {
|
if (this.$store.state.user) {
|
||||||
this.userUUID = this.$store.state.user.uuid
|
this.userUUID = this.$store.state.user.uuid
|
||||||
}
|
}
|
||||||
this.message = null
|
|
||||||
this.showFeedbackDialog = true
|
this.showFeedbackDialog = true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cancelFeedback() {
|
||||||
|
this.showFeedbackDialog = false
|
||||||
|
// this.clearForm()
|
||||||
|
},
|
||||||
|
|
||||||
sendFeedback() {
|
sendFeedback() {
|
||||||
let params = {
|
let params = {
|
||||||
|
email_key: this.recipient,
|
||||||
referrer: this.referringURL,
|
referrer: this.referringURL,
|
||||||
user: this.userUUID,
|
user: this.userUUID,
|
||||||
// user: this.user ? this.user.uuid : null,
|
// user: this.user ? this.user.uuid : null,
|
||||||
|
@ -90,6 +144,7 @@ export default {
|
||||||
}
|
}
|
||||||
this.$http.post(this.url, params).then(response => {
|
this.$http.post(this.url, params).then(response => {
|
||||||
this.showFeedbackDialog = false
|
this.showFeedbackDialog = false
|
||||||
|
this.clearForm()
|
||||||
this.$buefy.toast.open({
|
this.$buefy.toast.open({
|
||||||
message: "Thank you for your feedback!",
|
message: "Thank you for your feedback!",
|
||||||
type: 'is-success',
|
type: 'is-success',
|
||||||
|
|
55
src/components/home/ByjoveHome.vue
Normal file
55
src/components/home/ByjoveHome.vue
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<div class="has-text-centered">
|
||||||
|
<img :alt="`${appsettings.systemTitle} logo`" :src="appsettings.logo" />
|
||||||
|
<h1>Welcome to {{ appsettings.appTitle }}</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ByjoveHome',
|
||||||
|
props: {
|
||||||
|
appsettings: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
forceLogin: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
inited: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$store.state.session_established': 'init',
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
if (this.inited) return
|
||||||
|
|
||||||
|
// nothing to check, unless login is to be enforced
|
||||||
|
if (!this.forceLogin) {
|
||||||
|
this.inited = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// cannot init until session is established
|
||||||
|
if (!this.$store.state.session_established) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inited = true
|
||||||
|
|
||||||
|
// send anonymous users to login page
|
||||||
|
if (!this.$store.state.user.uuid) {
|
||||||
|
this.$router.push('/login')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
28
src/components/home/index.js
Normal file
28
src/components/home/index.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// Import vue component
|
||||||
|
import ByjoveHome from './ByjoveHome.vue'
|
||||||
|
|
||||||
|
// Declare install function executed by Vue.use()
|
||||||
|
export function install(Vue) {
|
||||||
|
if (install.installed) return;
|
||||||
|
install.installed = true;
|
||||||
|
Vue.component('ByjoveHome', ByjoveHome);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create module definition for Vue.use()
|
||||||
|
const plugin = {
|
||||||
|
install,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-install when vue is found (eg. in browser via <script> tag)
|
||||||
|
let GlobalVue = null;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
GlobalVue = window.Vue;
|
||||||
|
} else if (typeof global !== 'undefined') {
|
||||||
|
GlobalVue = global.Vue;
|
||||||
|
}
|
||||||
|
if (GlobalVue) {
|
||||||
|
GlobalVue.use(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// To allow use as module (npm/webpack/etc.) export component
|
||||||
|
export default ByjoveHome
|
|
@ -1,21 +1,32 @@
|
||||||
import ByjoveApp from './app'
|
import ByjoveApp from './app'
|
||||||
import ByjoveMenu from './menu'
|
import ByjoveMenu from './menu'
|
||||||
|
import ByjoveHome from './home'
|
||||||
import ByjoveLogo from './logo'
|
import ByjoveLogo from './logo'
|
||||||
import ByjoveFeedback from './feedback'
|
import ByjoveFeedback from './feedback'
|
||||||
|
import ByjoveLogin from './login'
|
||||||
import ByjoveAutocomplete from './autocomplete'
|
import ByjoveAutocomplete from './autocomplete'
|
||||||
import ByjoveModelIndex from './model-index'
|
import ByjoveModelIndex from './model-index'
|
||||||
import ByjoveModelCrud from './model-crud'
|
import ByjoveModelCrud from './model-crud'
|
||||||
|
import ByjoveScannerInput from './scanner-input'
|
||||||
|
import {ByjoveProducts, ByjoveProduct} from './products'
|
||||||
|
import ByjoveCustomerField from './customers'
|
||||||
import ByjoveInventory from './inventory'
|
import ByjoveInventory from './inventory'
|
||||||
import ByjoveReceiving from './receiving'
|
import ByjoveReceiving from './receiving'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ByjoveApp,
|
ByjoveApp,
|
||||||
ByjoveMenu,
|
ByjoveMenu,
|
||||||
|
ByjoveHome,
|
||||||
ByjoveLogo,
|
ByjoveLogo,
|
||||||
ByjoveFeedback,
|
ByjoveFeedback,
|
||||||
|
ByjoveLogin,
|
||||||
ByjoveAutocomplete,
|
ByjoveAutocomplete,
|
||||||
ByjoveModelIndex,
|
ByjoveModelIndex,
|
||||||
ByjoveModelCrud,
|
ByjoveModelCrud,
|
||||||
|
ByjoveScannerInput,
|
||||||
|
ByjoveProducts,
|
||||||
|
ByjoveProduct,
|
||||||
|
ByjoveCustomerField,
|
||||||
ByjoveInventory,
|
ByjoveInventory,
|
||||||
ByjoveReceiving,
|
ByjoveReceiving,
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,12 +43,16 @@
|
||||||
<b-field label="Cases" expanded>
|
<b-field label="Cases" expanded>
|
||||||
<b-input v-model="row.cases"
|
<b-input v-model="row.cases"
|
||||||
type="number"
|
type="number"
|
||||||
|
step="any"
|
||||||
|
ref="cases"
|
||||||
:disabled="!shouldAllowCases()">
|
:disabled="!shouldAllowCases()">
|
||||||
</b-input>
|
</b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field label="Units" expanded>
|
<b-field label="Units" expanded>
|
||||||
<b-input v-model="row.units"
|
<b-input v-model="row.units"
|
||||||
type="number">
|
type="number"
|
||||||
|
step="any"
|
||||||
|
ref="units">
|
||||||
</b-input>
|
</b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
@ -79,6 +83,10 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
focusCases: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
allowEdit: {
|
allowEdit: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -88,6 +96,7 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
row: {},
|
row: {},
|
||||||
|
@ -151,6 +160,14 @@ export default {
|
||||||
if (this.row.batch_executed || this.row.batch_complete) {
|
if (this.row.batch_executed || this.row.batch_complete) {
|
||||||
// cannot edit this row, so view it instead
|
// cannot edit this row, so view it instead
|
||||||
this.$router.push(this.getViewUrl())
|
this.$router.push(this.getViewUrl())
|
||||||
|
} else {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.shouldAllowCases() && this.focusCases) {
|
||||||
|
this.$refs.cases.focus()
|
||||||
|
} else {
|
||||||
|
this.$refs.units.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, response => {
|
}, response => {
|
||||||
if (response.status == 403) { // forbidden; redirect to model index
|
if (response.status == 403) { // forbidden; redirect to model index
|
||||||
|
|
117
src/components/login/ByjoveLogin.vue
Normal file
117
src/components/login/ByjoveLogin.vue
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<div class="byjove-login">
|
||||||
|
<form>
|
||||||
|
|
||||||
|
<byjove-logo centered :appsettings="appsettings">
|
||||||
|
</byjove-logo>
|
||||||
|
|
||||||
|
<b-field label="Username">
|
||||||
|
<b-input v-model.trim="username" />
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Password">
|
||||||
|
<b-input v-model="password" type="password" />
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<div v-if="loginError" style="color: red;">
|
||||||
|
{{ loginError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<b-button type="is-primary"
|
||||||
|
@click="attemptLogin()"
|
||||||
|
:disabled="loginFormDisabled"
|
||||||
|
expanded>
|
||||||
|
Login
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ByjoveLogo from '../logo'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ByjoveLogin',
|
||||||
|
components: {
|
||||||
|
ByjoveLogo,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
appsettings: Object,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
inited: false,
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
loginError: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
loginFormDisabled: function() {
|
||||||
|
if (!this.username) {
|
||||||
|
return true
|
||||||
|
} else if (!this.password) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$store.state.session_established': 'init',
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.inited) return
|
||||||
|
|
||||||
|
// cannot init until session is established
|
||||||
|
if (!this.$store.state.session_established) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inited = true
|
||||||
|
|
||||||
|
// send logged-in users to "home" page
|
||||||
|
if (this.$store.state.user.uuid) {
|
||||||
|
this.$router.push('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
attemptLogin() {
|
||||||
|
|
||||||
|
// clear any previous login error
|
||||||
|
this.loginError = null
|
||||||
|
|
||||||
|
// post credentials to login API
|
||||||
|
let creds = {
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
}
|
||||||
|
this.$http.post('/api/login', creds).then(response => {
|
||||||
|
if (response.data.ok) {
|
||||||
|
|
||||||
|
// login successful; let the app know
|
||||||
|
this.$byjoveLoginUser(response.data.user,
|
||||||
|
response.data.permissions)
|
||||||
|
|
||||||
|
// send user to "home" page
|
||||||
|
this.$router.push('/')
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// login failed
|
||||||
|
this.loginError = response.data.error;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
27
src/components/login/index.js
Normal file
27
src/components/login/index.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import ByjoveLogin from './ByjoveLogin.vue'
|
||||||
|
|
||||||
|
// Declare install function executed by Vue.use()
|
||||||
|
export function install(Vue) {
|
||||||
|
if (install.installed) return;
|
||||||
|
install.installed = true;
|
||||||
|
Vue.component('ByjoveLogin', ByjoveLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create module definition for Vue.use()
|
||||||
|
const plugin = {
|
||||||
|
install,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-install when vue is found (eg. in browser via <script> tag)
|
||||||
|
let GlobalVue = null;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
GlobalVue = window.Vue;
|
||||||
|
} else if (typeof global !== 'undefined') {
|
||||||
|
GlobalVue = global.Vue;
|
||||||
|
}
|
||||||
|
if (GlobalVue) {
|
||||||
|
GlobalVue.use(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// To allow use as module (npm/webpack/etc.) export component
|
||||||
|
export default ByjoveLogin
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="logo">
|
<div class="byjove-logo"
|
||||||
|
:style="centered ? 'text-align: center;' : null">
|
||||||
<img v-if="appsettings.logo" :alt="alternateText" :src="appsettings.logo" />
|
<img v-if="appsettings.logo" :alt="alternateText" :src="appsettings.logo" />
|
||||||
<img v-if="!appsettings.logo" :alt="alternateText" src="../../assets/logo.png" />
|
<img v-if="!appsettings.logo" :alt="alternateText" src="../../assets/logo.png" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +11,7 @@ export default {
|
||||||
name: 'ByjoveLogo',
|
name: 'ByjoveLogo',
|
||||||
props: {
|
props: {
|
||||||
appsettings: Object,
|
appsettings: Object,
|
||||||
|
centered: Boolean,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
alternateText: function() {
|
alternateText: function() {
|
||||||
|
@ -20,7 +22,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
img {
|
.byjove-logo img {
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,18 @@
|
||||||
<section>
|
<section>
|
||||||
<nav class="level is-mobile">
|
<nav class="level is-mobile">
|
||||||
|
|
||||||
<div v-if="user"
|
<div v-if="user.uuid"
|
||||||
class="level-item">
|
class="level-item">
|
||||||
|
|
||||||
<b-dropdown aria-role="menu">
|
<b-dropdown aria-role="menu">
|
||||||
<button class="button is-primary"
|
|
||||||
|
<b-button type="is-primary"
|
||||||
:class="{'is-danger': exposeRoot && user_is_root}"
|
:class="{'is-danger': exposeRoot && user_is_root}"
|
||||||
slot="trigger">
|
slot="trigger"
|
||||||
<b-icon icon="fas fa-user"></b-icon>
|
icon-left="user"
|
||||||
<span>{{ user.short_name }}</span>
|
icon-right="angle-down">
|
||||||
</button>
|
{{ user.short_name }}
|
||||||
|
</b-button>
|
||||||
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|
||||||
|
@ -41,7 +43,7 @@
|
||||||
</b-dropdown>
|
</b-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!user && showLoginButton"
|
<div v-if="!user.uuid && showLoginButton"
|
||||||
class="level-item">
|
class="level-item">
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
tag="router-link" to="/login">
|
tag="router-link" to="/login">
|
||||||
|
@ -58,7 +60,9 @@
|
||||||
|
|
||||||
<div v-if="shouldShowFeedback"
|
<div v-if="shouldShowFeedback"
|
||||||
class="level-item">
|
class="level-item">
|
||||||
<byjove-feedback :url="feedbackUrl"></byjove-feedback>
|
<byjove-feedback :url="feedbackUrl"
|
||||||
|
:recipient-options="feedbackRecipientOptions">
|
||||||
|
</byjove-feedback>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
@ -85,12 +89,16 @@ export default {
|
||||||
},
|
},
|
||||||
allowAnonymousFeedback: {
|
allowAnonymousFeedback: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: false,
|
||||||
},
|
},
|
||||||
feedbackUrl: {
|
feedbackUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '/api/feedback',
|
default: '/api/feedback',
|
||||||
},
|
},
|
||||||
|
feedbackRecipientOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
includeAboutLink: {
|
includeAboutLink: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -129,7 +137,7 @@ export default {
|
||||||
if (this.allowFeedback !== null) {
|
if (this.allowFeedback !== null) {
|
||||||
return this.allowFeedback
|
return this.allowFeedback
|
||||||
}
|
}
|
||||||
if (!this.allowAnonymousFeedback && !this.user) {
|
if (!this.allowAnonymousFeedback && !this.user.uuid) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -35,8 +35,9 @@
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|
||||||
<div v-if="showButtons"
|
<div v-if="showButtons"
|
||||||
class="buttons">
|
class="buttons" style="margin-top: 1rem;">
|
||||||
<b-button :type="getSaveButtonType()"
|
<b-button :type="getSaveButtonType()"
|
||||||
|
v-if="!hideSaveButton"
|
||||||
:icon-left="getSaveButtonIcon()"
|
:icon-left="getSaveButtonIcon()"
|
||||||
:disabled="saveDisabled"
|
:disabled="saveDisabled"
|
||||||
@click="save()">
|
@click="save()">
|
||||||
|
@ -158,6 +159,10 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
createRowPermission: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
createRowButtonText: {
|
createRowButtonText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "Create New Row",
|
default: "Create New Row",
|
||||||
|
@ -184,10 +189,10 @@ export default {
|
||||||
rowFilters: {
|
rowFilters: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: (uuid) => {
|
default: (uuid) => {
|
||||||
return JSON.stringify([
|
return [
|
||||||
{field: 'batch_uuid', op: 'eq', value: uuid},
|
{field: 'batch_uuid', op: 'eq', value: uuid},
|
||||||
{field: 'removed', op: 'eq', value: false},
|
{field: 'removed', op: 'eq', value: false},
|
||||||
])
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rowOrderBy: {
|
rowOrderBy: {
|
||||||
|
@ -214,6 +219,10 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
hideSaveButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
saveDisabled: {
|
saveDisabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -312,6 +321,11 @@ export default {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.record = {}
|
||||||
|
this.$emit('refresh', this.record)
|
||||||
|
},
|
||||||
|
|
||||||
getModelSlug() {
|
getModelSlug() {
|
||||||
if (this.modelSlug) {
|
if (this.modelSlug) {
|
||||||
return this.modelSlug
|
return this.modelSlug
|
||||||
|
@ -427,23 +441,11 @@ export default {
|
||||||
return this.getModelSlug()
|
return this.getModelSlug()
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: should use this.$hasPerm()
|
|
||||||
hasPerm(perm) {
|
|
||||||
|
|
||||||
// if user is root then assume permission
|
|
||||||
if (this.$store.state.user && this.$store.state.user.is_root) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise do true perm check for user
|
|
||||||
return this.$store.state.permissions.includes(perm)
|
|
||||||
},
|
|
||||||
|
|
||||||
hasModelPerm(perm) {
|
hasModelPerm(perm) {
|
||||||
|
|
||||||
// do normal check, but first add prefix
|
// do normal check, but first add prefix
|
||||||
let prefix = this.getModelPermissionPrefix()
|
let prefix = this.getModelPermissionPrefix()
|
||||||
return this.hasPerm(prefix + '.' + perm)
|
return this.$byjoveHasPerm(prefix + '.' + perm)
|
||||||
},
|
},
|
||||||
|
|
||||||
renderLabel(obj) {
|
renderLabel(obj) {
|
||||||
|
@ -509,28 +511,41 @@ export default {
|
||||||
this.record = response.data.data
|
this.record = response.data.data
|
||||||
this.$emit('refresh', this.record)
|
this.$emit('refresh', this.record)
|
||||||
if (this.hasRows) {
|
if (this.hasRows) {
|
||||||
|
// TODO: was seeing occasional errors when a batch
|
||||||
|
// view was loaded, and it tried to fetch rows but
|
||||||
|
// somehow the uuid was not passed along and
|
||||||
|
// server failed to build a query filter. so
|
||||||
|
// hoping the nextTick() delay fixes it..?
|
||||||
|
this.$nextTick(() => {
|
||||||
this.fetchRows(uuid)
|
this.fetchRows(uuid)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, response => {
|
}, response => {
|
||||||
if (response.status == 403) { // forbidden; redirect to model index
|
if (response.status == 403) { // forbidden
|
||||||
this.$buefy.toast.open({
|
this.$buefy.toast.open({
|
||||||
message: "You do not have permission to access that page.",
|
message: "You do not have permission to access that page.",
|
||||||
type: 'is-danger',
|
type: 'is-danger',
|
||||||
})
|
})
|
||||||
this.$router.push(this.getModelPathPrefix() + '/')
|
} else if (response.status == 404) { // notfound
|
||||||
} else {
|
this.$buefy.toast.open({
|
||||||
|
message: `The requested ${this.getModelTitle()} was not found.`,
|
||||||
|
type: 'is-danger',
|
||||||
|
})
|
||||||
|
} else { // other error
|
||||||
this.$buefy.toast.open({
|
this.$buefy.toast.open({
|
||||||
message: "Failed to fetch page data!",
|
message: "Failed to fetch page data!",
|
||||||
type: 'is-danger',
|
type: 'is-danger',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// redirect to model index
|
||||||
|
this.$router.push(this.getIndexURL())
|
||||||
})
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchRows(uuid) {
|
fetchRows(uuid) {
|
||||||
let params = {
|
let params = {
|
||||||
filters: this.rowFilters(uuid),
|
filters: JSON.stringify(this.rowFilters(uuid)),
|
||||||
orderBy: this.rowOrderBy,
|
orderBy: this.rowOrderBy,
|
||||||
ascending: this.rowOrderAscending ? 1 : 0,
|
ascending: this.rowOrderAscending ? 1 : 0,
|
||||||
}
|
}
|
||||||
|
@ -659,9 +674,15 @@ export default {
|
||||||
if (this.mode != 'viewing') {
|
if (this.mode != 'viewing') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (this.createRowPermission) {
|
||||||
|
if (!this.$byjoveHasPerm(this.createRowPermission)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (!this.hasModelPerm('create_row')) {
|
if (!this.hasModelPerm('create_row')) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="buttons"
|
<div class="buttons block"
|
||||||
v-if="allowCreate && hasModelPerm('create')">
|
v-if="allowCreate && userCanCreate()">
|
||||||
<b-button class="new-object"
|
<b-button class="new-object"
|
||||||
type="is-primary"
|
type="is-primary"
|
||||||
tag="router-link"
|
tag="router-link"
|
||||||
|
@ -56,12 +56,21 @@ export default {
|
||||||
modelPathPrefix: String,
|
modelPathPrefix: String,
|
||||||
labelRenderer: Function,
|
labelRenderer: Function,
|
||||||
apiIndexUrl: String,
|
apiIndexUrl: String,
|
||||||
apiIndexSort: Object,
|
apiIndexSort: [Array, Object],
|
||||||
apiIndexFilters: Array,
|
apiIndexFilters: Array,
|
||||||
|
nativeSort: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
wantInitialResults: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
allowCreate: {
|
allowCreate: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
createPermCheck: Function,
|
||||||
paginated: {
|
paginated: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -78,7 +87,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
if (this.wantInitialResults) {
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'apiIndexFilters' (to, from) {
|
'apiIndexFilters' (to, from) {
|
||||||
|
@ -94,8 +105,12 @@ export default {
|
||||||
fetchData() {
|
fetchData() {
|
||||||
let params = {
|
let params = {
|
||||||
filters: JSON.stringify(this.apiIndexFilters),
|
filters: JSON.stringify(this.apiIndexFilters),
|
||||||
orderBy: this.apiIndexSort.field,
|
}
|
||||||
ascending: (this.apiIndexSort.dir == 'asc') ? 1 : 0,
|
if (this.nativeSort) {
|
||||||
|
params.nativeSort = JSON.stringify(this.apiIndexSort)
|
||||||
|
} else {
|
||||||
|
params.orderBy = this.apiIndexSort.field
|
||||||
|
params.ascending = (this.apiIndexSort.dir == 'asc') ? 1 : 0
|
||||||
}
|
}
|
||||||
if (this.paginated) {
|
if (this.paginated) {
|
||||||
params.per_page = this.perPage
|
params.per_page = this.perPage
|
||||||
|
@ -111,6 +126,11 @@ export default {
|
||||||
type: 'is-danger',
|
type: 'is-danger',
|
||||||
})
|
})
|
||||||
this.$router.push('/')
|
this.$router.push('/')
|
||||||
|
} else if (response.status == 404) { // not found; display warning
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Page data not found!",
|
||||||
|
type: 'is-danger',
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$buefy.toast.open({
|
this.$buefy.toast.open({
|
||||||
message: "Failed to fetch page data!",
|
message: "Failed to fetch page data!",
|
||||||
|
@ -169,6 +189,13 @@ export default {
|
||||||
return this.getModelSlug()
|
return this.getModelSlug()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
userCanCreate() {
|
||||||
|
if (this.createPermCheck) {
|
||||||
|
return this.createPermCheck()
|
||||||
|
}
|
||||||
|
return this.hasModelPerm('create')
|
||||||
|
},
|
||||||
|
|
||||||
hasModelPerm(perm) {
|
hasModelPerm(perm) {
|
||||||
|
|
||||||
// do normal check, but first add prefix
|
// do normal check, but first add prefix
|
||||||
|
|
152
src/components/numeric-input/NumericInput.vue
Normal file
152
src/components/numeric-input/NumericInput.vue
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
<template>
|
||||||
|
<b-input :name="name"
|
||||||
|
:value="value"
|
||||||
|
ref="input"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:size="size"
|
||||||
|
:icon-pack="iconPack"
|
||||||
|
:icon="icon"
|
||||||
|
:disabled="disabled"
|
||||||
|
:custom-class="customClass"
|
||||||
|
@focus="notifyFocus"
|
||||||
|
@blur="notifyBlur"
|
||||||
|
@keydown.native="keyDown"
|
||||||
|
@input="valueChanged"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'NumericInput',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
value: [Number, String],
|
||||||
|
placeholder: String,
|
||||||
|
iconPack: String,
|
||||||
|
icon: String,
|
||||||
|
size: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
allowEnter: Boolean,
|
||||||
|
customClass: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.$refs.input.focus()
|
||||||
|
},
|
||||||
|
|
||||||
|
notifyFocus(event) {
|
||||||
|
this.$emit('focus', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
notifyBlur(event) {
|
||||||
|
this.$emit('blur', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
keyDown(event) {
|
||||||
|
// by default we only allow numeric keys, and general navigation
|
||||||
|
// keys, but we might also allow Enter key
|
||||||
|
if (!this.key_modifies(event) && !this.key_allowed(event)) {
|
||||||
|
if (!this.allowEnter || event.which != 13) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Determine if a keypress would modify the value of a textbox.
|
||||||
|
*
|
||||||
|
* Note that this implies that the keypress is also *valid* in the context of a
|
||||||
|
* numeric textbox.
|
||||||
|
*
|
||||||
|
* Returns `true` if the keypress is valid and would modify the textbox value,
|
||||||
|
* or `false` otherwise.
|
||||||
|
*/
|
||||||
|
key_modifies(event) {
|
||||||
|
|
||||||
|
if (event.which >= 48 && event.which <= 57) { // Numeric (QWERTY)
|
||||||
|
if (! event.shiftKey) { // shift key means punctuation instead of numeric
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (event.which >= 96 && event.which <= 105) { // Numeric (10-Key)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} else if (event.which == 109 || event.which == 173) { // hyphen (negative sign)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} else if (event.which == 110 || event.which == 190) { // period/decimal
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} else if (event.which == 8) { // Backspace
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} else if (event.which == 46) { // Delete
|
||||||
|
return true;
|
||||||
|
} else if (event.ctrlKey && event.which == 86) { // Ctrl+V
|
||||||
|
return true;
|
||||||
|
} else if (event.ctrlKey && event.which == 88) { // Ctrl+X
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Determine if a keypress is allowed in the context of a textbox.
|
||||||
|
*
|
||||||
|
* The purpose of this function is to let certain "special" keys (e.g. function
|
||||||
|
* and navigational keys) to pass through, so they may be processed as they
|
||||||
|
* would for a normal textbox.
|
||||||
|
*
|
||||||
|
* Note that this function does *not* check for keys which would actually
|
||||||
|
* modify the value of the textbox. It is assumed that the caller will have
|
||||||
|
* already used `key_modifies()` for that.
|
||||||
|
*
|
||||||
|
* Returns `true` if the keypress is allowed, or `false` otherwise.
|
||||||
|
*/
|
||||||
|
key_allowed(event) {
|
||||||
|
|
||||||
|
// Allow anything with modifiers (except Shift).
|
||||||
|
if (event.altKey || event.ctrlKey || event.metaKey) {
|
||||||
|
|
||||||
|
// ...but don't allow Ctrl+X or Ctrl+V
|
||||||
|
return event.which != 86 && event.which != 88;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow function keys.
|
||||||
|
if (event.which >= 112 && event.which <= 123) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow Home/End/arrow keys.
|
||||||
|
if (event.which >= 35 && event.which <= 40) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow Tab key.
|
||||||
|
if (event.which == 9) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow Escape key
|
||||||
|
if (event.which == 27) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
select() {
|
||||||
|
this.$el.children[0].select()
|
||||||
|
},
|
||||||
|
|
||||||
|
valueChanged(value) {
|
||||||
|
this.$emit('input', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
28
src/components/numeric-input/index.js
Normal file
28
src/components/numeric-input/index.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// Import vue component
|
||||||
|
import NumericInput from './NumericInput.vue'
|
||||||
|
|
||||||
|
// Declare install function executed by Vue.use()
|
||||||
|
export function install(Vue) {
|
||||||
|
if (install.installed) return;
|
||||||
|
install.installed = true;
|
||||||
|
Vue.component('NumericInput', NumericInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create module definition for Vue.use()
|
||||||
|
const plugin = {
|
||||||
|
install,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-install when vue is found (eg. in browser via <script> tag)
|
||||||
|
let GlobalVue = null;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
GlobalVue = window.Vue;
|
||||||
|
} else if (typeof global !== 'undefined') {
|
||||||
|
GlobalVue = global.Vue;
|
||||||
|
}
|
||||||
|
if (GlobalVue) {
|
||||||
|
GlobalVue.use(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// To allow use as module (npm/webpack/etc.) export component
|
||||||
|
export default NumericInput
|
200
src/components/products/ByjoveProduct.vue
Normal file
200
src/components/products/ByjoveProduct.vue
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<byjove-model-crud ref="modelCrud"
|
||||||
|
model-name="Product"
|
||||||
|
:label-renderer="renderLabel"
|
||||||
|
:allow-edit="allowEdit"
|
||||||
|
:mode="mode"
|
||||||
|
@refresh="record => { product = record }"
|
||||||
|
v-show="!scannerSubmitting"
|
||||||
|
class="block">
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column;">
|
||||||
|
|
||||||
|
<b-field label="Brand">
|
||||||
|
{{ product.brand_name }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Description">
|
||||||
|
{{ product.description }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Size">
|
||||||
|
{{ product.size }}
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img :src="product.image_url" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot name="default-panels" :product="product">
|
||||||
|
|
||||||
|
<b-collapse class="card"
|
||||||
|
animation="slide"
|
||||||
|
:open="false"
|
||||||
|
aria-id="contentIdForVendors">
|
||||||
|
|
||||||
|
<template #trigger="props">
|
||||||
|
<div class="card-header"
|
||||||
|
role="button"
|
||||||
|
aria-controls="contentIdForVendors"
|
||||||
|
:aria-expanded="props.open">
|
||||||
|
<p class="card-header-title">
|
||||||
|
Vendors
|
||||||
|
</p>
|
||||||
|
<a class="card-header-icon">
|
||||||
|
<b-icon pack="fas"
|
||||||
|
:icon="props.open ? 'angle-up' : 'angle-down'">
|
||||||
|
</b-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<b-field label="Preferred">
|
||||||
|
{{ product.vendor_name }} @ {{ product.default_unit_cost_display }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-table :data="product.costs">
|
||||||
|
<b-table-column label="Vendor"
|
||||||
|
field="vendor_name"
|
||||||
|
v-slot="props">
|
||||||
|
{{ props.row.vendor_name }}
|
||||||
|
</b-table-column>
|
||||||
|
<b-table-column label="Unit Cost"
|
||||||
|
field="unit_cost"
|
||||||
|
v-slot="props">
|
||||||
|
{{ props.row.unit_cost }}
|
||||||
|
</b-table-column>
|
||||||
|
</b-table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-collapse>
|
||||||
|
|
||||||
|
<b-collapse class="card"
|
||||||
|
animation="slide"
|
||||||
|
:open="false"
|
||||||
|
aria-id="contentIdForPrices">
|
||||||
|
|
||||||
|
<template #trigger="props">
|
||||||
|
<div class="card-header"
|
||||||
|
role="button"
|
||||||
|
aria-controls="contentIdForPrices"
|
||||||
|
:aria-expanded="props.open">
|
||||||
|
<p class="card-header-title">
|
||||||
|
Prices
|
||||||
|
</p>
|
||||||
|
<a class="card-header-icon">
|
||||||
|
<b-icon pack="fas"
|
||||||
|
:icon="props.open ? 'angle-up' : 'angle-down'">
|
||||||
|
</b-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<b-field label="Reg. Price">
|
||||||
|
{{ product.unit_price_display }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Sale Price"
|
||||||
|
v-if="product.sale_price">
|
||||||
|
{{ product.sale_price_display }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Sale Ends"
|
||||||
|
v-if="product.sale_price">
|
||||||
|
{{ product.sale_ends_display }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="TPR Price"
|
||||||
|
v-if="product.tpr_price">
|
||||||
|
{{ product.tpr_price_display }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="TPR Ends"
|
||||||
|
v-if="product.tpr_price">
|
||||||
|
{{ product.tpr_ends_display }}
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-collapse>
|
||||||
|
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<slot name="extra-panels" :product="product"></slot>
|
||||||
|
|
||||||
|
</byjove-model-crud>
|
||||||
|
|
||||||
|
<byjove-scanner-input ref="scannerInput"
|
||||||
|
class="block"
|
||||||
|
@submit="scannerSubmit">
|
||||||
|
</byjove-scanner-input>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ByjoveModelCrud from '../model-crud'
|
||||||
|
import ByjoveScannerInput from '../scanner-input'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Product',
|
||||||
|
props: {
|
||||||
|
mode: String,
|
||||||
|
allowEdit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
ByjoveModelCrud,
|
||||||
|
ByjoveScannerInput,
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
product: {},
|
||||||
|
scannerSubmitting: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
renderLabel(product) {
|
||||||
|
return product.product_key
|
||||||
|
},
|
||||||
|
|
||||||
|
scannerSubmit(entry) {
|
||||||
|
this.scannerSubmitting = true
|
||||||
|
let url = '/api/products/quick-lookup'
|
||||||
|
let params = {entry: entry}
|
||||||
|
this.$http.get(url, {params: params}).then(response => {
|
||||||
|
if (response.data.error) {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: response.data.error,
|
||||||
|
type: 'is-danger',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$refs.scannerInput.clear()
|
||||||
|
this.$refs.modelCrud.clear()
|
||||||
|
this.$router.push(`/products/${response.data.product.uuid}`)
|
||||||
|
}
|
||||||
|
this.scannerSubmitting = false
|
||||||
|
}, response => {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Unknown error!",
|
||||||
|
type: 'is-danger',
|
||||||
|
})
|
||||||
|
this.scannerSubmitting = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
55
src/components/products/ByjoveProducts.vue
Normal file
55
src/components/products/ByjoveProducts.vue
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<byjove-model-index model-name="Product"
|
||||||
|
:want-initial-results="wantInitialResults"
|
||||||
|
:allow-create="allowCreate">
|
||||||
|
|
||||||
|
<byjove-scanner-input @submit="scannerSubmit">
|
||||||
|
</byjove-scanner-input>
|
||||||
|
|
||||||
|
</byjove-model-index>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ByjoveModelIndex from '../model-index'
|
||||||
|
import ByjoveScannerInput from '../scanner-input'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ByjoveProducts',
|
||||||
|
components: {
|
||||||
|
ByjoveModelIndex,
|
||||||
|
ByjoveScannerInput,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
wantInitialResults: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
allowCreate: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
scannerSubmit(entry) {
|
||||||
|
let url = '/api/products/quick-lookup'
|
||||||
|
let params = {entry: entry}
|
||||||
|
this.$http.get(url, {params: params}).then(response => {
|
||||||
|
if (response.data.error) {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: response.data.error,
|
||||||
|
type: 'is-danger',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$router.push(`/products/${response.data.product.uuid}`)
|
||||||
|
}
|
||||||
|
}, response => {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Unknown error!",
|
||||||
|
type: 'is-danger',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
29
src/components/products/index.js
Normal file
29
src/components/products/index.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import ByjoveProducts from './ByjoveProducts'
|
||||||
|
import ByjoveProduct from './ByjoveProduct'
|
||||||
|
|
||||||
|
// Declare install function executed by Vue.use()
|
||||||
|
export function install(Vue) {
|
||||||
|
if (install.installed) return;
|
||||||
|
install.installed = true;
|
||||||
|
Vue.component('ByjoveProducts', ByjoveProducts);
|
||||||
|
Vue.component('ByjoveProduct', ByjoveProduct);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create module definition for Vue.use()
|
||||||
|
const plugin = {
|
||||||
|
install,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-install when vue is found (eg. in browser via <script> tag)
|
||||||
|
let GlobalVue = null;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
GlobalVue = window.Vue;
|
||||||
|
} else if (typeof global !== 'undefined') {
|
||||||
|
GlobalVue = global.Vue;
|
||||||
|
}
|
||||||
|
if (GlobalVue) {
|
||||||
|
GlobalVue.use(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {ByjoveProducts}
|
||||||
|
export {ByjoveProduct}
|
|
@ -25,58 +25,16 @@
|
||||||
{{ row.description }} {{ row.size }}
|
{{ row.description }} {{ row.size }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p v-if="row.invoice_number"
|
||||||
|
class="has-text-weight-bold">
|
||||||
|
Invoice # {{ row.invoice_number }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<p v-if="allowCases"
|
<p v-if="allowCases"
|
||||||
class="has-text-weight-bold">
|
class="has-text-weight-bold">
|
||||||
1 CS = {{ row.case_quantity || 1 }} {{ row.unit_uom }}
|
1 CS = {{ row.case_quantity || 1 }} {{ row.unit_uom }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table class="receiving-quantities">
|
|
||||||
<tr v-if="row.order_quantities_known">
|
|
||||||
<td>ordered</td>
|
|
||||||
<td>
|
|
||||||
<span v-if="allowCases">
|
|
||||||
{{ row.cases_ordered || 0}} /
|
|
||||||
</span>
|
|
||||||
{{ row.units_ordered || 0}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="row.order_quantities_known">
|
|
||||||
<td>shipped</td>
|
|
||||||
<td>
|
|
||||||
<span v-if="allowCases">
|
|
||||||
{{ row.cases_shipped || 0}} /
|
|
||||||
</span>
|
|
||||||
{{ row.units_shipped || 0}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>received</td>
|
|
||||||
<td>
|
|
||||||
<span v-if="allowCases">
|
|
||||||
{{ row.cases_received || 0}} /
|
|
||||||
</span>
|
|
||||||
{{ row.units_received || 0}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>damaged</td>
|
|
||||||
<td>
|
|
||||||
<span v-if="allowCases">
|
|
||||||
{{ row.cases_damaged || 0}} /
|
|
||||||
</span>
|
|
||||||
{{ row.units_damaged || 0}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="allowExpired">
|
|
||||||
<td>expired</td>
|
|
||||||
<td>
|
|
||||||
<span v-if="allowCases">
|
|
||||||
{{ row.cases_expired || 0}} /
|
|
||||||
</span>
|
|
||||||
{{ row.units_expired || 0}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -84,6 +42,56 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
|
||||||
|
<div v-if="row.order_quantities_known"
|
||||||
|
style="display: flex; gap: 0.3rem;">
|
||||||
|
<span style="flex-grow: 1;">ordered</span>
|
||||||
|
<span v-if="allowCases">{{ row.cases_ordered || 0 }} /</span>
|
||||||
|
<span>{{ row.units_ordered || 0}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="row.order_quantities_known"
|
||||||
|
style="display: flex; gap: 0.3rem;">
|
||||||
|
<span style="flex-grow: 1;">shipped</span>
|
||||||
|
<span v-if="allowCases">{{ row.cases_shipped || 0 }} /</span>
|
||||||
|
<span>{{ row.units_shipped || 0}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 0.3rem;">
|
||||||
|
<span style="flex-grow: 1;">received</span>
|
||||||
|
<span v-if="allowCases">{{ row.cases_received || 0 }} /</span>
|
||||||
|
<span>{{ row.units_received || 0}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 0.3rem;">
|
||||||
|
<span style="flex-grow: 1;">damaged</span>
|
||||||
|
<span v-if="allowCases">{{ row.cases_damaged || 0 }} /</span>
|
||||||
|
<span>{{ row.units_damaged || 0}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="allowExpired"
|
||||||
|
style="display: flex; gap: 0.3rem;">
|
||||||
|
<span style="flex-grow: 1;">expired</span>
|
||||||
|
<span v-if="allowCases">{{ row.cases_expired || 0 }} /</span>
|
||||||
|
<span>{{ row.units_expired || 0}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 0.3rem;">
|
||||||
|
<span style="flex-grow: 1;">missing</span>
|
||||||
|
<span v-if="allowCases">{{ row.cases_missing || 0 }} /</span>
|
||||||
|
<span>{{ row.units_missing || 0}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="row.received_alert"
|
<div v-if="row.received_alert"
|
||||||
class="has-text-danger">
|
class="has-text-danger">
|
||||||
<br />
|
<br />
|
||||||
|
@ -96,40 +104,50 @@
|
||||||
<p>{{ row.unexpected_alert }}</p>
|
<p>{{ row.unexpected_alert }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<slot name="warnings" :row="row"></slot>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div v-if="shouldShowQuickReceive">
|
<div v-if="shouldShowQuickReceive">
|
||||||
<slot name="quick-receive">
|
<slot name="quick-receive">
|
||||||
<!-- only show quick-receive if we have an identifiable product -->
|
<!-- only show quick-receive if we have an identifiable product -->
|
||||||
<div v-if="shouldShowQuickReceive" class="buttons">
|
<div class="buttons">
|
||||||
<b-button v-if="row.quick_receive_all"
|
<b-button v-if="row.quick_receive_all"
|
||||||
type="is-primary"
|
type="is-primary"
|
||||||
@click="addQuickAmount(row.quick_receive_quantity, row.quick_receive_uom)"
|
@click="addQuickAmount(row.quick_receive_quantity, row.quick_receive_uom)"
|
||||||
expanded>
|
expanded>
|
||||||
{{ row.quick_receive_text }}
|
{{ row.quick_receive_text }}
|
||||||
</b-button>
|
</b-button>
|
||||||
<div v-if="!row.quick_receive_all">
|
<div v-if="!row.quick_receive_all"
|
||||||
<div class="buttons"
|
style="margin: auto;">
|
||||||
v-if="allowCases">
|
<b-button v-if="allowCases"
|
||||||
<b-button type="is-primary"
|
type="is-primary"
|
||||||
@click="addQuickAmount(1, 'CS')"
|
@click="addQuickAmount(1, 'CS')"
|
||||||
expanded>
|
expanded
|
||||||
|
style="margin-bottom: 1rem;">
|
||||||
Receive 1 CS
|
Receive 1 CS
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
<div>
|
||||||
<div class="buttons has-text-centered" style="width: 100%;">
|
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
|
size="is-small"
|
||||||
@click="addQuickAmount(1, row.unit_uom)">
|
@click="addQuickAmount(1, row.unit_uom)">
|
||||||
1 {{ row.unit_uom }}
|
1 {{ row.unit_uom }}
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
|
size="is-small"
|
||||||
@click="addQuickAmount(3, row.unit_uom)">
|
@click="addQuickAmount(3, row.unit_uom)">
|
||||||
3 {{ row.unit_uom }}
|
3 {{ row.unit_uom }}
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
|
size="is-small"
|
||||||
@click="addQuickAmount(6, row.unit_uom)">
|
@click="addQuickAmount(6, row.unit_uom)">
|
||||||
6 {{ row.unit_uom }}
|
6 {{ row.unit_uom }}
|
||||||
</b-button>
|
</b-button>
|
||||||
|
<b-button type="is-primary"
|
||||||
|
size="is-small"
|
||||||
|
@click="addQuickAmount(12, row.unit_uom)">
|
||||||
|
12 {{ row.unit_uom }}
|
||||||
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -141,10 +159,13 @@
|
||||||
|
|
||||||
<b-field grouped>
|
<b-field grouped>
|
||||||
<b-field class="control">
|
<b-field class="control">
|
||||||
<b-input v-model="inputQuantity"
|
<numeric-input v-if="allowDecimalQuantities"
|
||||||
|
v-model="inputQuantity"
|
||||||
|
custom-class="receiving-quantity-input" />
|
||||||
|
<b-input v-else
|
||||||
|
v-model="inputQuantity"
|
||||||
type="number"
|
type="number"
|
||||||
custom-class="receiving-quantity-input">
|
custom-class="receiving-quantity-input" />
|
||||||
</b-input>
|
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field class="control">
|
<b-field class="control">
|
||||||
<b-radio-button v-model="inputUOM"
|
<b-radio-button v-model="inputUOM"
|
||||||
|
@ -174,10 +195,15 @@
|
||||||
<span>EXPD</span>
|
<span>EXPD</span>
|
||||||
</b-radio-button>
|
</b-radio-button>
|
||||||
<b-radio-button v-model="inputType"
|
<b-radio-button v-model="inputType"
|
||||||
native-value="mispick"
|
native-value="missing">
|
||||||
disabled>
|
<span>DNR</span>
|
||||||
<span>MSPK</span>
|
|
||||||
</b-radio-button>
|
</b-radio-button>
|
||||||
|
<!-- TODO: maybe some day... -->
|
||||||
|
<!-- <b-radio-button v-model="inputType" -->
|
||||||
|
<!-- native-value="mispick" -->
|
||||||
|
<!-- disabled> -->
|
||||||
|
<!-- <span>MSPK</span> -->
|
||||||
|
<!-- </b-radio-button> -->
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field v-if="inputType == 'expired'"
|
<b-field v-if="inputType == 'expired'"
|
||||||
|
@ -188,6 +214,7 @@
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
|
<div style="margin: auto;">
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
@click="addAmount()"
|
@click="addAmount()"
|
||||||
:disabled="addAmountDisabled">
|
:disabled="addAmountDisabled">
|
||||||
|
@ -195,9 +222,10 @@
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button type="is-warning"
|
<b-button type="is-warning"
|
||||||
@click="subtractAmount()">
|
@click="subtractAmount()">
|
||||||
Subtract Amount
|
Subtract
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="has-text-centered is-italic">OR</p>
|
<p class="has-text-centered is-italic">OR</p>
|
||||||
<br />
|
<br />
|
||||||
|
@ -221,8 +249,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import NumericInput from '../numeric-input/NumericInput.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ByjoveReceiving',
|
name: 'ByjoveReceiving',
|
||||||
|
components: {NumericInput},
|
||||||
props: {
|
props: {
|
||||||
productKey: {
|
productKey: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -232,6 +263,10 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
allowDecimalQuantities: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
allowExpired: {
|
allowExpired: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -256,11 +291,9 @@ export default {
|
||||||
},
|
},
|
||||||
shouldShowQuickReceive() {
|
shouldShowQuickReceive() {
|
||||||
if (!this.row.quick_receive) {
|
if (!this.row.quick_receive) {
|
||||||
console.log("row says not to")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (!this.row.product_uuid) {
|
if (!this.row.product_uuid) {
|
||||||
console.log("row has no product")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -284,7 +317,13 @@ export default {
|
||||||
fetch(uuid) {
|
fetch(uuid) {
|
||||||
this.$http.get(`/api/receiving-batch-row/${uuid}`).then(response => {
|
this.$http.get(`/api/receiving-batch-row/${uuid}`).then(response => {
|
||||||
this.row = response.data.data
|
this.row = response.data.data
|
||||||
|
if (this.allowCases && this.row.cases_unconfirmed) {
|
||||||
|
this.inputQuantity = this.row.cases_unconfirmed
|
||||||
|
this.inputUOM = 'CS'
|
||||||
|
} else {
|
||||||
|
this.inputQuantity = this.row.units_unconfirmed || 1
|
||||||
this.inputUOM = this.row.unit_uom
|
this.inputUOM = this.row.unit_uom
|
||||||
|
}
|
||||||
}, response => {
|
}, response => {
|
||||||
if (response.status == 403) { // forbidden; redirect to model index
|
if (response.status == 403) { // forbidden; redirect to model index
|
||||||
this.$buefy.toast.open({
|
this.$buefy.toast.open({
|
||||||
|
@ -303,6 +342,17 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
addAmount() {
|
addAmount() {
|
||||||
|
|
||||||
|
let amount = this.inputQuantity
|
||||||
|
amount = this.allowDecimalQuantities ? parseFloat(amount) : parseInt(amount)
|
||||||
|
if (!amount) {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Please specify an amount",
|
||||||
|
type: 'is-info',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let url = `/api/receiving-batch-row/${this.row.uuid}/receive`
|
let url = `/api/receiving-batch-row/${this.row.uuid}/receive`
|
||||||
let params = {
|
let params = {
|
||||||
row: this.row.uuid,
|
row: this.row.uuid,
|
||||||
|
@ -314,23 +364,23 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.inputUOM == 'CS') {
|
if (this.inputUOM == 'CS') {
|
||||||
params.cases = this.inputQuantity
|
params.cases = amount
|
||||||
} else {
|
} else {
|
||||||
params.units = this.inputQuantity
|
params.units = amount
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$http.post(url, params).then(response => {
|
this.$http.post(url, params).then(response => {
|
||||||
if (response.data.data) {
|
if (!response.data.error) {
|
||||||
this.$router.push(`/receiving/${this.row.batch_uuid}`)
|
this.$router.push(`/receiving/${this.row.batch_uuid}`)
|
||||||
} else {
|
} else {
|
||||||
this.$buefy.toast.open({
|
this.$buefy.toast.open({
|
||||||
message: response.data.error || "Failed to post receiving!",
|
message: response.data.error,
|
||||||
type: 'is-danger',
|
type: 'is-danger',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, response => {
|
}, response => {
|
||||||
this.$buefy.toast.open({
|
this.$buefy.toast.open({
|
||||||
message: "Failed to post receiving!",
|
message: "Save failed: unknown error",
|
||||||
type: 'is-danger',
|
type: 'is-danger',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -349,3 +399,11 @@ export default {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
table.receiving-quantities td {
|
||||||
|
padding: 0px 10px 0px 0px;
|
||||||
|
}
|
||||||
|
.input.receiving-quantity-input {
|
||||||
|
width: 6rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
60
src/components/scanner-input/ByjoveScannerInput.vue
Normal file
60
src/components/scanner-input/ByjoveScannerInput.vue
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<div class="scanner-input">
|
||||||
|
<b-input v-model="quickEntry"
|
||||||
|
placeholder="Scan/Enter Code"
|
||||||
|
icon="search"
|
||||||
|
@keypress.native="quickKey">
|
||||||
|
</b-input>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ByjoveScannerInput',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
quickEntry: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener('keypress', this.globalKey)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('keypress', this.globalKey)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.quickEntry = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
globalKey(event) {
|
||||||
|
if (event.target.tagName == 'BODY') {
|
||||||
|
|
||||||
|
// mimic keyboard wedge
|
||||||
|
if (event.charCode >= 48 && event.charCode <= 57) { // numeric (qwerty)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.quickEntry += event.key
|
||||||
|
})
|
||||||
|
|
||||||
|
} else if (event.keyCode == 13) { // enter
|
||||||
|
this.submitQuickEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
quickKey(event) {
|
||||||
|
if (event.keyCode == 13) { // enter
|
||||||
|
this.submitQuickEntry()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
submitQuickEntry() {
|
||||||
|
if (this.quickEntry) {
|
||||||
|
this.$emit('submit', this.quickEntry)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
28
src/components/scanner-input/index.js
Normal file
28
src/components/scanner-input/index.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// Import vue component
|
||||||
|
import ByjoveScannerInput from './ByjoveScannerInput.vue'
|
||||||
|
|
||||||
|
// Declare install function executed by Vue.use()
|
||||||
|
export function install(Vue) {
|
||||||
|
if (install.installed) return;
|
||||||
|
install.installed = true;
|
||||||
|
Vue.component('ByjoveScannerInput', ByjoveScannerInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create module definition for Vue.use()
|
||||||
|
const plugin = {
|
||||||
|
install,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-install when vue is found (eg. in browser via <script> tag)
|
||||||
|
let GlobalVue = null;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
GlobalVue = window.Vue;
|
||||||
|
} else if (typeof global !== 'undefined') {
|
||||||
|
GlobalVue = global.Vue;
|
||||||
|
}
|
||||||
|
if (GlobalVue) {
|
||||||
|
GlobalVue.use(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// To allow use as module (npm/webpack/etc.) export component
|
||||||
|
export default ByjoveScannerInput
|
|
@ -3,7 +3,7 @@ export function ByjovePlugin(Vue) {
|
||||||
Vue.mixin({
|
Vue.mixin({
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
$loginUser(user, permissions) {
|
$byjoveLoginUser(user, permissions) {
|
||||||
|
|
||||||
// put user info in app store
|
// put user info in app store
|
||||||
this.$store.commit('SET_USER', user)
|
this.$store.commit('SET_USER', user)
|
||||||
|
@ -17,15 +17,25 @@ export function ByjovePlugin(Vue) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
$logoutUser() {
|
// TODO: deprecate / remove
|
||||||
|
$loginUser(user, permission) {
|
||||||
|
this.$byjoveLoginUser(user, permission)
|
||||||
|
},
|
||||||
|
|
||||||
|
$byjoveLogoutUser() {
|
||||||
|
|
||||||
// remove user info from app store
|
// remove user info from app store
|
||||||
this.$store.commit('SET_USER', null)
|
this.$store.commit('SET_USER', {})
|
||||||
this.$store.commit('SET_USER_IS_ADMIN', false)
|
this.$store.commit('SET_USER_IS_ADMIN', false)
|
||||||
this.$store.commit('SET_USER_IS_ROOT', false)
|
this.$store.commit('SET_USER_IS_ROOT', false)
|
||||||
},
|
},
|
||||||
|
|
||||||
$hasPerm(name) {
|
// TODO: deprecate / remove
|
||||||
|
$logoutUser() {
|
||||||
|
this.$byjoveLogoutUser()
|
||||||
|
},
|
||||||
|
|
||||||
|
$byjoveHasPerm(name) {
|
||||||
|
|
||||||
// if user is root then assume permission
|
// if user is root then assume permission
|
||||||
if (this.$store.state.user_is_root) {
|
if (this.$store.state.user_is_root) {
|
||||||
|
@ -35,6 +45,11 @@ export function ByjovePlugin(Vue) {
|
||||||
// otherwise do true perm check for user
|
// otherwise do true perm check for user
|
||||||
return this.$store.state.permissions.includes(name)
|
return this.$store.state.permissions.includes(name)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO: deprecate / remove
|
||||||
|
$hasPerm(name) {
|
||||||
|
return this.$byjoveHasPerm(name)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ export let ByjoveStoreConfig = {
|
||||||
user_is_admin: false,
|
user_is_admin: false,
|
||||||
user_is_root: false,
|
user_is_root: false,
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
settings: {},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
SET_SESSION_ESTABLISHED(state, established) {
|
SET_SESSION_ESTABLISHED(state, established) {
|
||||||
|
@ -23,6 +24,9 @@ export let ByjoveStoreConfig = {
|
||||||
SET_PERMISSIONS(state, permissions) {
|
SET_PERMISSIONS(state, permissions) {
|
||||||
state.permissions = permissions
|
state.permissions = permissions
|
||||||
},
|
},
|
||||||
|
SET_SETTINGS(state, settings) {
|
||||||
|
state.settings = settings
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
// TODO: should we define the logic here, for fetching current session
|
// TODO: should we define the logic here, for fetching current session
|
||||||
|
|
Loading…
Reference in a new issue