Compare commits

..

61 commits

Author SHA1 Message Date
Lance Edgar 3d02e46f2a Update changelog 2024-06-03 11:40:09 -05:00
Lance Edgar f21a7298fe Optionally allow decimal quantities for receiving 2024-05-31 10:58:44 -05:00
Lance Edgar b04fa3eb72 Update changelog 2024-05-29 09:43:53 -05:00
Lance Edgar 32d1bd430f Show actual error text if applicable, for receiving failure 2024-04-11 14:14:01 -05:00
Lance Edgar b6e8b74eef Update changelog 2024-03-26 12:57:49 -05:00
Lance Edgar 18936d9efd Add delay when fetching rows data for model CRUD component
hoping this actually works, but can't reproduce the problem locally so
will just have to wait and see
2023-12-29 19:38:41 -06:00
Lance Edgar 86fbf8f164 Update changelog 2023-12-26 20:22:43 -06:00
Lance Edgar 7bc9991be0 Keep row filters "raw" until encoding for actual request
so there is no need to use `JSON.stringify()` everywhere, keep that
part in just one place
2023-12-20 11:48:09 -06:00
Lance Edgar 0609fdebf7 Improve focus behavior for inventory count view 2023-12-11 13:16:59 -06:00
Lance Edgar 0f56b478fc Update changelog 2023-10-23 13:13:08 -05:00
Lance Edgar e6012bddb8 Center receiving buttons; add "12 EA" quick receive button 2023-10-19 15:16:31 -05:00
Lance Edgar 4f355fc58f Show invoice number when receiving row
also fix spacing for quantities display
2023-10-19 14:56:03 -05:00
Lance Edgar 95d6e41c81 Use columns instead of table, for row receiving quantities 2023-10-19 14:39:01 -05:00
Lance Edgar daac09d1e4 Update changelog 2023-10-06 10:02:37 -05:00
Lance Edgar a1bf81aa56 Auto-focus case/unit field when showing inventory count page 2023-10-05 13:44:08 -05:00
Lance Edgar 8c6fe828ad Auto-trim username field in login form
to avoid confusion
2023-09-18 18:45:55 -05:00
Lance Edgar 8edbe48293 Update changelog 2023-09-17 21:28:32 -05:00
Lance Edgar 8421785aff Show "missing" quantities for item receiving 2023-09-17 18:30:58 -05:00
Lance Edgar 6fd233da87 Validate amount when adding receiving quantity 2023-09-17 17:18:33 -05:00
Lance Edgar 99fbe0e5ec Update changelog 2023-08-29 22:30:10 -05:00
Lance Edgar 6d58eb8d35 Add support for "missing" credit in mobile receiving 2023-08-29 16:10:58 -05:00
Lance Edgar 95afae16ee Update changelog 2023-08-08 18:49:16 -05:00
Lance Edgar 229b0b7f2e Fix indentation 2023-08-07 18:31:38 -05:00
Lance Edgar d778ce29e7 Add slot for "default panels" in View Product
so custom apps can override the full set if needed
2023-08-07 18:31:03 -05:00
Lance Edgar 4879a8e46c Add UPC quick lookup for View Product page
and fix expand/collapse icons
2023-08-07 18:22:39 -05:00
Lance Edgar abd92f503f Update changelog 2023-08-03 23:18:08 -05:00
Lance Edgar 4572d63274 Update to use lts/gallium version for nodejs 2023-08-03 23:14:34 -05:00
Lance Edgar 0ffb184267 Update changelog 2023-01-30 21:10:47 -06:00
Lance Edgar e94383dd25 Add Prices panel for default product view 2023-01-29 18:50:07 -06:00
Lance Edgar 606bf7d19e Pass the --otp code when publishing 2023-01-07 13:01:53 -06:00
Lance Edgar e85fc22544 Update changelog 2023-01-07 12:56:53 -06:00
Lance Edgar df4cb06a49 Center logo and text for home page 2023-01-07 12:55:48 -06:00
Lance Edgar c1f3da15f1 Update dependencies 2023-01-07 12:45:23 -06:00
Lance Edgar 29220093dd Tweak default product view 2023-01-05 07:49:44 -06:00
Lance Edgar 85c43e95e5 Try this instead?
sheesh, can't figure out why this isn't working on stage server..
2023-01-04 22:04:24 -06:00
Lance Edgar 10f816cba9 Add some vue magic to product components
not entirely clear what that does, but maybe important..?
2023-01-04 21:54:58 -06:00
Lance Edgar 4fa08c68ec Add basic components for Product CRUD
more specifically at this point, for product lookup via scanner
2023-01-04 21:26:38 -06:00
Lance Edgar 6db418ec19 Update changelog 2022-11-16 10:05:09 -06:00
Lance Edgar ad9024a4a2 Let item-level receiving default option reflect "what's left" 2022-09-07 20:48:09 -05:00
Lance Edgar c4ea9758d3 Add styles, warnings template for receiving component 2022-09-07 19:55:29 -05:00
Lance Edgar 7436d414c0 Allow static data set for autocomplete component
also header template
2022-08-30 21:12:31 -05:00
Lance Edgar 721600b12d Add home page component; fix user state issues
must be sure to keep "anonymous" user represented as {} instead of
null, otherwise getting errors.  this also fixes the menu upon user
login; previously it wasn't showing most options
2022-08-26 17:56:31 -05:00
Lance Edgar 2a1f5764a1 Misc. tweaks for model-crud, customer-field 2022-08-11 00:17:34 -05:00
Lance Edgar 4189bdf919 Rename methods provided by plugin 2022-08-11 00:17:10 -05:00
Lance Edgar dee27b51f5 Add components for login form and customer field 2022-08-09 14:40:04 -05:00
Lance Edgar e537d8e3a6 Put available settings in the store; etc.
also add `build-watch` script for dev's sake

menu component no longer allows anonymous feedback by default

logo component gets a `centered` prop
2022-08-09 14:38:41 -05:00
Lance Edgar 3392f8cbd6 Increase spacing around "Create" button for model-index 2022-08-08 19:36:27 -05:00
Lance Edgar 427732bf1a Detect 404 notfound when viewing CRUD record; warn user accordingly
and always redirect to model index page
2022-07-26 20:48:22 -05:00
Lance Edgar 31cebd28c2 Cleanup some receiving cruft 2022-07-24 22:30:55 -05:00
Lance Edgar 6666857a0b Update changelog 2021-10-20 16:18:50 -05:00
Lance Edgar 720fc6182d Allow override of "create" permission for model index 2021-09-03 18:29:49 -05:00
Lance Edgar a0a405f849 Allow for "native sort" params in model index 2021-09-03 16:27:10 -05:00
Lance Edgar cebf7fc749 Update changelog 2021-08-04 19:48:10 -05:00
Lance Edgar 695ea388a6 Allow multiple feedback recipient options
in which case user must choose one, to send feedback
2021-08-02 18:44:29 -05:00
Lance Edgar 6e949eb199 Use better class name for logo image styles
sheesh, we were affecting *all* images i think...
2021-07-29 20:05:22 -05:00
Lance Edgar df089d2f77 Update changelog 2021-04-12 11:58:27 -05:00
Lance Edgar edede0f2ce Allow "any" number for inventory cases/units, including decimal 2021-04-12 11:35:12 -05:00
Lance Edgar cd61210a4c Bump version in package-lock 2021-04-12 11:35:02 -05:00
Lance Edgar a7cb817050 Update changelog 2021-02-10 17:53:06 -06:00
Lance Edgar f19b1bc387 Add "angle-down" icon for user menu button
make it more obvious that it's a menu button...
2021-02-10 16:49:19 -06:00
Lance Edgar 609fef4159 Add package-lock.json back to src repo 2021-02-02 19:08:22 -06:00
30 changed files with 24828 additions and 162 deletions

2
.gitignore vendored
View file

@ -2,8 +2,6 @@
node_modules
/dist
package-lock.json
# local env files
.env.local
.env.*.local

View file

@ -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/)
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
### Changed
- Wrap "create new object" button, for padding.

23339
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "byjove",
"version": "0.1.9",
"version": "0.1.26",
"description": "Generic-ish app components for Vue.js frontend to Tailbone API backend",
"keywords": [
"rattail",
@ -14,21 +14,22 @@
},
"scripts": {
"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": {
"vue": "^2.6.10"
"vue": "^2.7.14"
},
"devDependencies": {
"@vue/cli-service": "^3.0.5",
"rollup": "^1.26.3",
"rollup": "^1.32.1",
"rollup-plugin-buble": "^0.19.8",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-vue": "^5.1.2",
"vue-resource": "^1.5.1",
"vue-router": "^3.1.3",
"vue-template-compiler": "^2.6.10",
"vuex": "^3.1.1"
"rollup-plugin-vue": "^5.1.9",
"vue-resource": "^1.5.3",
"vue-router": "^3.6.5",
"vue-template-compiler": "^2.7.14",
"vuex": "^3.6.2"
},
"main": "./dist/byjove.umd.js",
"files": [

View file

@ -42,13 +42,16 @@ export default {
this.$http.get('/api/session').then(response => {
// 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_ROOT', response.data.user ? response.data.user.is_root : false)
// also keep track of user's 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
this.$store.commit('SET_SESSION_ESTABLISHED', true)

View file

@ -5,13 +5,15 @@
v-show="!selected"
v-model="autocompleteValue"
:placeholder="placeholder"
field="label"
:icon="icon"
:data="data"
:data="autocompleteData"
@typing="getAsyncData"
@select="selectionMade"
:open-on-focus="openOnFocus"
keep-first>
<template slot-scope="props">
{{ props.option.label }}
<template #header>
<slot name="header"></slot>
</template>
</b-autocomplete>
@ -28,7 +30,18 @@ export default {
name: 'ByjoveAutocomplete',
props: {
name: String,
serviceUrl: String,
serviceUrl: {
type: String,
default: null,
},
data: {
type: Array,
default: null,
},
dataFilter: {
type: Function,
default: null,
},
value: String,
initialLabel: String,
placeholder: {
@ -39,7 +52,12 @@ export default {
type: String,
default: null,
},
openOnFocus: {
type: Boolean,
default: false,
},
},
data() {
let selected = null
if (this.value) {
@ -49,12 +67,30 @@ export default {
}
}
return {
data: [],
fetchedData: [],
selected: selected,
isFetching: false,
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: {
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) {
this.selected = option
this.$emit('input', option ? option.value : null)
@ -74,16 +117,22 @@ export default {
// TODO: buefy example uses `debounce()` here and perhaps we should too?
// https://buefy.org/documentation/autocomplete
getAsyncData: function (entry) {
// we only fetch async data via URL if we have one!
if (!this.serviceUrl) {
return
}
if (entry.length < 3) {
this.data = []
this.fetchedData = []
return
}
this.isFetching = true
this.$http.get(this.serviceUrl, {params: {term: entry}}).then((response) => {
this.data = response.data
this.fetchedData = response.data
}).catch((error) => {
this.data = []
this.fetchedData = []
throw error
}).finally(() => {
this.isFetching = false

View 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>

View 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

View file

@ -12,16 +12,35 @@
<section class="modal-card-body">
<p class="modal-card-title">User Feedback</p>
<br />
<p>
Questions, suggestions, comments, complaints, etc. regarding this
website are welcome and may be submitted below.
</p>
<b-field label="Referring URL">
<div class="control">
{{ referringURL }}
</div>
</b-field>
<div v-if="hasRecipientOptions">
<p class="block">
Comments welcome!&nbsp; 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>
Questions, suggestions, comments, complaints, etc. regarding this
website are welcome and may be submitted below.
</p>
<b-field label="Referring URL">
<div class="control">
{{ referringURL }}
</div>
</b-field>
</div>
<b-field label="Your Name"
v-show="!userUUID">
@ -36,12 +55,12 @@
</b-field>
<div class="buttons">
<b-button @click="showFeedbackDialog = false">
<b-button @click="cancelFeedback()">
Cancel
</b-button>
<b-button type="is-primary"
@click="sendFeedback()"
:disabled="!message">
@click="sendFeedback()"
:disabled="sendIsDisabled">
Send Note
</b-button>
</div>
@ -59,29 +78,64 @@ export default {
url: String,
// userUUID: String,
// user: Object,
recipientOptions: {
type: Array,
default: null,
},
},
data() {
return {
showFeedbackDialog: false,
referringURL: null,
recipient: null,
userUUID: null,
userName: 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: {
clearForm() {
this.recipient = null
this.message = null
},
showFeedback() {
this.referringURL = location.href
if (this.$store.state.user) {
this.userUUID = this.$store.state.user.uuid
}
this.message = null
this.showFeedbackDialog = true
},
cancelFeedback() {
this.showFeedbackDialog = false
// this.clearForm()
},
sendFeedback() {
let params = {
email_key: this.recipient,
referrer: this.referringURL,
user: this.userUUID,
// user: this.user ? this.user.uuid : null,
@ -90,6 +144,7 @@ export default {
}
this.$http.post(this.url, params).then(response => {
this.showFeedbackDialog = false
this.clearForm()
this.$buefy.toast.open({
message: "Thank you for your feedback!",
type: 'is-success',

View 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>

View 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

View file

@ -1,21 +1,32 @@
import ByjoveApp from './app'
import ByjoveMenu from './menu'
import ByjoveHome from './home'
import ByjoveLogo from './logo'
import ByjoveFeedback from './feedback'
import ByjoveLogin from './login'
import ByjoveAutocomplete from './autocomplete'
import ByjoveModelIndex from './model-index'
import ByjoveModelCrud from './model-crud'
import ByjoveScannerInput from './scanner-input'
import {ByjoveProducts, ByjoveProduct} from './products'
import ByjoveCustomerField from './customers'
import ByjoveInventory from './inventory'
import ByjoveReceiving from './receiving'
export {
ByjoveApp,
ByjoveMenu,
ByjoveHome,
ByjoveLogo,
ByjoveFeedback,
ByjoveLogin,
ByjoveAutocomplete,
ByjoveModelIndex,
ByjoveModelCrud,
ByjoveScannerInput,
ByjoveProducts,
ByjoveProduct,
ByjoveCustomerField,
ByjoveInventory,
ByjoveReceiving,
}

View file

@ -43,12 +43,16 @@
<b-field label="Cases" expanded>
<b-input v-model="row.cases"
type="number"
step="any"
ref="cases"
:disabled="!shouldAllowCases()">
</b-input>
</b-field>
<b-field label="Units" expanded>
<b-input v-model="row.units"
type="number">
type="number"
step="any"
ref="units">
</b-input>
</b-field>
</b-field>
@ -79,6 +83,10 @@ export default {
type: Boolean,
default: true,
},
focusCases: {
type: Boolean,
default: false,
},
allowEdit: {
type: Boolean,
default: false,
@ -88,6 +96,7 @@ export default {
default: false,
},
},
data() {
return {
row: {},
@ -151,6 +160,14 @@ export default {
if (this.row.batch_executed || this.row.batch_complete) {
// cannot edit this row, so view it instead
this.$router.push(this.getViewUrl())
} else {
this.$nextTick(() => {
if (this.shouldAllowCases() && this.focusCases) {
this.$refs.cases.focus()
} else {
this.$refs.units.focus()
}
})
}
}, response => {
if (response.status == 403) { // forbidden; redirect to model index

View 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>

View 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

View file

@ -1,5 +1,6 @@
<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="../../assets/logo.png" />
</div>
@ -10,6 +11,7 @@ export default {
name: 'ByjoveLogo',
props: {
appsettings: Object,
centered: Boolean,
},
computed: {
alternateText: function() {
@ -20,7 +22,7 @@ export default {
</script>
<style scoped>
img {
.byjove-logo img {
max-height: 200px;
max-width: 300px;
}

View file

@ -2,16 +2,18 @@
<section>
<nav class="level is-mobile">
<div v-if="user"
<div v-if="user.uuid"
class="level-item">
<b-dropdown aria-role="menu">
<button class="button is-primary"
:class="{'is-danger': exposeRoot && user_is_root}"
slot="trigger">
<b-icon icon="fas fa-user"></b-icon>
<span>{{ user.short_name }}</span>
</button>
<b-button type="is-primary"
:class="{'is-danger': exposeRoot && user_is_root}"
slot="trigger"
icon-left="user"
icon-right="angle-down">
{{ user.short_name }}
</b-button>
<slot></slot>
@ -41,7 +43,7 @@
</b-dropdown>
</div>
<div v-if="!user && showLoginButton"
<div v-if="!user.uuid && showLoginButton"
class="level-item">
<b-button type="is-primary"
tag="router-link" to="/login">
@ -58,7 +60,9 @@
<div v-if="shouldShowFeedback"
class="level-item">
<byjove-feedback :url="feedbackUrl"></byjove-feedback>
<byjove-feedback :url="feedbackUrl"
:recipient-options="feedbackRecipientOptions">
</byjove-feedback>
</div>
</nav>
@ -85,12 +89,16 @@ export default {
},
allowAnonymousFeedback: {
type: Boolean,
default: true,
default: false,
},
feedbackUrl: {
type: String,
default: '/api/feedback',
},
feedbackRecipientOptions: {
type: Array,
default: null,
},
includeAboutLink: {
type: Boolean,
default: true,
@ -129,7 +137,7 @@ export default {
if (this.allowFeedback !== null) {
return this.allowFeedback
}
if (!this.allowAnonymousFeedback && !this.user) {
if (!this.allowAnonymousFeedback && !this.user.uuid) {
return false
}
return true

View file

@ -35,8 +35,9 @@
<slot></slot>
<div v-if="showButtons"
class="buttons">
class="buttons" style="margin-top: 1rem;">
<b-button :type="getSaveButtonType()"
v-if="!hideSaveButton"
:icon-left="getSaveButtonIcon()"
:disabled="saveDisabled"
@click="save()">
@ -158,6 +159,10 @@ export default {
type: Boolean,
default: false,
},
createRowPermission: {
type: String,
default: null,
},
createRowButtonText: {
type: String,
default: "Create New Row",
@ -184,10 +189,10 @@ export default {
rowFilters: {
type: Function,
default: (uuid) => {
return JSON.stringify([
return [
{field: 'batch_uuid', op: 'eq', value: uuid},
{field: 'removed', op: 'eq', value: false},
])
]
},
},
rowOrderBy: {
@ -214,6 +219,10 @@ export default {
type: Boolean,
default: false,
},
hideSaveButton: {
type: Boolean,
default: false,
},
saveDisabled: {
type: Boolean,
default: false,
@ -312,6 +321,11 @@ export default {
methods: {
clear() {
this.record = {}
this.$emit('refresh', this.record)
},
getModelSlug() {
if (this.modelSlug) {
return this.modelSlug
@ -427,23 +441,11 @@ export default {
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) {
// do normal check, but first add prefix
let prefix = this.getModelPermissionPrefix()
return this.hasPerm(prefix + '.' + perm)
return this.$byjoveHasPerm(prefix + '.' + perm)
},
renderLabel(obj) {
@ -509,28 +511,41 @@ export default {
this.record = response.data.data
this.$emit('refresh', this.record)
if (this.hasRows) {
this.fetchRows(uuid)
// 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)
})
}
}, response => {
if (response.status == 403) { // forbidden; redirect to model index
if (response.status == 403) { // forbidden
this.$buefy.toast.open({
message: "You do not have permission to access that page.",
type: 'is-danger',
})
this.$router.push(this.getModelPathPrefix() + '/')
} else {
} else if (response.status == 404) { // notfound
this.$buefy.toast.open({
message: `The requested ${this.getModelTitle()} was not found.`,
type: 'is-danger',
})
} else { // other error
this.$buefy.toast.open({
message: "Failed to fetch page data!",
type: 'is-danger',
})
}
// redirect to model index
this.$router.push(this.getIndexURL())
})
},
fetchRows(uuid) {
let params = {
filters: this.rowFilters(uuid),
filters: JSON.stringify(this.rowFilters(uuid)),
orderBy: this.rowOrderBy,
ascending: this.rowOrderAscending ? 1 : 0,
}
@ -659,8 +674,14 @@ export default {
if (this.mode != 'viewing') {
return false
}
if (!this.hasModelPerm('create_row')) {
return false
if (this.createRowPermission) {
if (!this.$byjoveHasPerm(this.createRowPermission)) {
return false
}
} else {
if (!this.hasModelPerm('create_row')) {
return false
}
}
return true
},

View file

@ -7,8 +7,8 @@
</ul>
</nav>
<div class="buttons"
v-if="allowCreate && hasModelPerm('create')">
<div class="buttons block"
v-if="allowCreate && userCanCreate()">
<b-button class="new-object"
type="is-primary"
tag="router-link"
@ -56,12 +56,21 @@ export default {
modelPathPrefix: String,
labelRenderer: Function,
apiIndexUrl: String,
apiIndexSort: Object,
apiIndexSort: [Array, Object],
apiIndexFilters: Array,
nativeSort: {
type: Boolean,
default: false,
},
wantInitialResults: {
type: Boolean,
default: true,
},
allowCreate: {
type: Boolean,
default: true,
},
createPermCheck: Function,
paginated: {
type: Boolean,
default: true,
@ -78,7 +87,9 @@ export default {
}
},
mounted() {
this.fetchData()
if (this.wantInitialResults) {
this.fetchData()
}
},
watch: {
'apiIndexFilters' (to, from) {
@ -94,8 +105,12 @@ export default {
fetchData() {
let params = {
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) {
params.per_page = this.perPage
@ -111,6 +126,11 @@ export default {
type: 'is-danger',
})
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 {
this.$buefy.toast.open({
message: "Failed to fetch page data!",
@ -169,6 +189,13 @@ export default {
return this.getModelSlug()
},
userCanCreate() {
if (this.createPermCheck) {
return this.createPermCheck()
}
return this.hasModelPerm('create')
},
hasModelPerm(perm) {
// do normal check, but first add prefix

View 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>

View 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

View 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>

View 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>

View 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}

View file

@ -25,58 +25,16 @@
{{ row.description }} {{ row.size }}
</p>
<p v-if="row.invoice_number"
class="has-text-weight-bold">
Invoice # {{ row.invoice_number }}
</p>
<p v-if="allowCases"
class="has-text-weight-bold">
1 CS = {{ row.case_quantity || 1 }} {{ row.unit_uom }}
</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>
@ -84,6 +42,56 @@
</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"
class="has-text-danger">
<br />
@ -96,40 +104,50 @@
<p>{{ row.unexpected_alert }}</p>
</div>
<slot name="warnings" :row="row"></slot>
<br />
<div v-if="shouldShowQuickReceive">
<slot name="quick-receive">
<!-- 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"
type="is-primary"
@click="addQuickAmount(row.quick_receive_quantity, row.quick_receive_uom)"
expanded>
{{ row.quick_receive_text }}
</b-button>
<div v-if="!row.quick_receive_all">
<div class="buttons"
v-if="allowCases">
<b-button type="is-primary"
@click="addQuickAmount(1, 'CS')"
expanded>
Receive 1 CS
</b-button>
</div>
<div class="buttons has-text-centered" style="width: 100%;">
<div v-if="!row.quick_receive_all"
style="margin: auto;">
<b-button v-if="allowCases"
type="is-primary"
@click="addQuickAmount(1, 'CS')"
expanded
style="margin-bottom: 1rem;">
Receive 1 CS
</b-button>
<div>
<b-button type="is-primary"
size="is-small"
@click="addQuickAmount(1, row.unit_uom)">
1 {{ row.unit_uom }}
</b-button>
<b-button type="is-primary"
size="is-small"
@click="addQuickAmount(3, row.unit_uom)">
3 {{ row.unit_uom }}
</b-button>
<b-button type="is-primary"
size="is-small"
@click="addQuickAmount(6, row.unit_uom)">
6 {{ row.unit_uom }}
</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>
@ -141,10 +159,13 @@
<b-field grouped>
<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"
custom-class="receiving-quantity-input">
</b-input>
custom-class="receiving-quantity-input" />
</b-field>
<b-field class="control">
<b-radio-button v-model="inputUOM"
@ -174,10 +195,15 @@
<span>EXPD</span>
</b-radio-button>
<b-radio-button v-model="inputType"
native-value="mispick"
disabled>
<span>MSPK</span>
native-value="missing">
<span>DNR</span>
</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 v-if="inputType == 'expired'"
@ -188,15 +214,17 @@
</b-field>
<div class="buttons">
<b-button type="is-primary"
@click="addAmount()"
:disabled="addAmountDisabled">
Add Amount
</b-button>
<b-button type="is-warning"
@click="subtractAmount()">
Subtract Amount
</b-button>
<div style="margin: auto;">
<b-button type="is-primary"
@click="addAmount()"
:disabled="addAmountDisabled">
Add Amount
</b-button>
<b-button type="is-warning"
@click="subtractAmount()">
Subtract
</b-button>
</div>
</div>
<p class="has-text-centered is-italic">OR</p>
@ -221,8 +249,11 @@
</template>
<script>
import NumericInput from '../numeric-input/NumericInput.vue'
export default {
name: 'ByjoveReceiving',
components: {NumericInput},
props: {
productKey: {
type: String,
@ -232,6 +263,10 @@ export default {
type: Boolean,
default: true,
},
allowDecimalQuantities: {
type: Boolean,
default: false,
},
allowExpired: {
type: Boolean,
default: true,
@ -256,11 +291,9 @@ export default {
},
shouldShowQuickReceive() {
if (!this.row.quick_receive) {
console.log("row says not to")
return false
}
if (!this.row.product_uuid) {
console.log("row has no product")
return false
}
return true
@ -284,7 +317,13 @@ export default {
fetch(uuid) {
this.$http.get(`/api/receiving-batch-row/${uuid}`).then(response => {
this.row = response.data.data
this.inputUOM = this.row.unit_uom
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
}
}, response => {
if (response.status == 403) { // forbidden; redirect to model index
this.$buefy.toast.open({
@ -303,6 +342,17 @@ export default {
},
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 params = {
row: this.row.uuid,
@ -314,23 +364,23 @@ export default {
}
if (this.inputUOM == 'CS') {
params.cases = this.inputQuantity
params.cases = amount
} else {
params.units = this.inputQuantity
params.units = amount
}
this.$http.post(url, params).then(response => {
if (response.data.data) {
if (!response.data.error) {
this.$router.push(`/receiving/${this.row.batch_uuid}`)
} else {
this.$buefy.toast.open({
message: response.data.error || "Failed to post receiving!",
message: response.data.error,
type: 'is-danger',
})
}
}, response => {
this.$buefy.toast.open({
message: "Failed to post receiving!",
message: "Save failed: unknown error",
type: 'is-danger',
})
})
@ -349,3 +399,11 @@ export default {
},
}
</script>
<style>
table.receiving-quantities td {
padding: 0px 10px 0px 0px;
}
.input.receiving-quantity-input {
width: 6rem;
}
</style>

View 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>

View 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

View file

@ -3,7 +3,7 @@ export function ByjovePlugin(Vue) {
Vue.mixin({
methods: {
$loginUser(user, permissions) {
$byjoveLoginUser(user, permissions) {
// put user info in app store
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
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_ROOT', false)
},
$hasPerm(name) {
// TODO: deprecate / remove
$logoutUser() {
this.$byjoveLogoutUser()
},
$byjoveHasPerm(name) {
// if user is root then assume permission
if (this.$store.state.user_is_root) {
@ -35,6 +45,11 @@ export function ByjovePlugin(Vue) {
// otherwise do true perm check for user
return this.$store.state.permissions.includes(name)
},
// TODO: deprecate / remove
$hasPerm(name) {
return this.$byjoveHasPerm(name)
},
}
})

View file

@ -6,6 +6,7 @@ export let ByjoveStoreConfig = {
user_is_admin: false,
user_is_root: false,
permissions: [],
settings: {},
},
mutations: {
SET_SESSION_ESTABLISHED(state, established) {
@ -23,6 +24,9 @@ export let ByjoveStoreConfig = {
SET_PERMISSIONS(state, permissions) {
state.permissions = permissions
},
SET_SETTINGS(state, settings) {
state.settings = settings
},
},
actions: {
// TODO: should we define the logic here, for fetching current session

View file

@ -13,4 +13,5 @@ def release(c):
"""
c.run("find . -name '*~' -delete")
c.run('npm run build')
c.run('npm publish')
otp = c.run('otp npm').stdout.strip()
c.run('npm publish --otp {}'.format(otp))