Compare commits

...

102 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
Lance Edgar 9bd8ddbf3a Update changelog 2020-04-24 14:02:21 -05:00
Lance Edgar af21cab150 Add initial "inventory" component, for updating row counts 2020-03-29 16:37:25 -05:00
Lance Edgar 62d3c5fc05 Misc. fixes for model-crud, e.g. redirect to parent after deleting row 2020-03-29 16:34:12 -05:00
Lance Edgar d634143e84 Avoid showing "removed" batch rows by default 2020-03-29 16:32:52 -05:00
Lance Edgar d1db1c54c4 Add support for "Create Row" button when viewing parent CRUD object
plus misc. other tweaks, for polish
2020-03-27 11:48:30 -05:00
Lance Edgar bc148f910a Misc. improvements for "delete" support in model-crud 2020-03-24 13:57:49 -05:00
Lance Edgar f68db3c97c Stop showing Buefy toast messages at bottom of screen
top is much better at least for development...
2020-03-24 13:57:19 -05:00
Lance Edgar e7a0ddb782 Let caller define row sorting for model-crud
also display a "title" for rows if set
2020-03-23 23:55:04 -05:00
Lance Edgar 36ba991bdc Add new ByjoveReceiving component 2020-03-20 14:02:07 -05:00
Lance Edgar 09852baa84 Fix pagination bug in model-crud 2020-03-20 13:53:11 -05:00
Lance Edgar 985860732d Add byjoveCurrency filter 2020-03-20 13:52:16 -05:00
Lance Edgar 0ceabeb53f Wrap "create new object" button, for padding 2020-03-13 13:16:14 -05:00
Lance Edgar 15ddce14ee Update changelog 2020-03-02 11:56:26 -06:00
Lance Edgar 68a349383c Let caller define placeholder and icon for autocomplete field 2020-03-01 17:32:37 -06:00
Lance Edgar ce70eb2b91 Add basic "quick-delete" prop for model-crud
if set, and Delete button is clicked, will emit a "delete" event instead of
navigating to a "delete" URL
2020-03-01 17:23:21 -06:00
Lance Edgar cd71754f84 Fix the "view object" link in CRUD header
depending on if object is a "row" or not
2020-03-01 17:22:57 -06:00
Lance Edgar 2f58da7df3 Add basic support for "executing" mode, in model-crud 2020-02-26 21:31:38 -06:00
Lance Edgar d1b503fd41 Allow customization of Save button for model-crud 2020-02-26 17:45:58 -06:00
Lance Edgar 3d9c31c084 Update changelog 2020-02-26 15:10:09 -06:00
Lance Edgar 3d5e0be2fe Tweak how/when Edit and Delete buttons are shown for model-crud
esp. with regard to "row" views
2020-02-23 21:09:51 -06:00
Lance Edgar a9c4548594 Make sure we never ask for page 0 when fetching model index data 2020-02-23 21:09:26 -06:00
Lance Edgar 24667ead2a Consolidate some perm check logic 2020-02-23 21:09:04 -06:00
Lance Edgar d887345138 Update changelog 2020-02-21 14:35:24 -06:00
Lance Edgar 41ccf00049 Update permissions when logging in user 2020-02-21 12:05:40 -06:00
Lance Edgar 637363909a Use empty object for default user info 2020-02-12 19:28:43 -06:00
Lance Edgar 0b4e4f9894 Add proper logout logic to menu; let caller hide root options, about link 2020-02-11 13:54:53 -06:00
Lance Edgar c17c25b4cb Change aria-role (to "menu") for main user menu 2020-02-11 13:42:47 -06:00
Lance Edgar 3c4e9567a9 Add user_is_admin flag to app store, plus logic to set/clear it 2020-02-11 13:33:54 -06:00
Lance Edgar a80a42fb56 Add session_established flag in global store 2020-02-11 11:06:11 -06:00
Lance Edgar 777f877dd5 Add ByjovePlugin for sake of $hasPerm() method
hopefully this will be home to more things over time?
2020-02-11 11:05:31 -06:00
Lance Edgar 7664f9bbb9 Delay showing the app until user info is fetched 2020-02-10 19:53:36 -06:00
Lance Edgar 5451f87f30 Remove some redirect magic, for logged-in users
that is now the custom app's responsibility
2020-02-10 17:35:45 -06:00
Lance Edgar 4501505f38 Stop including Home link in menu
caller must explicitly do that if they want it
2020-02-10 17:35:22 -06:00
Lance Edgar a1a9aafdac Stop forcing redirect to login for anonymous user
custom app should be responsible for that i think..yes?
2020-02-10 16:31:33 -06:00
Lance Edgar f0d94cf06b Let caller control whether app footer is shown 2020-02-10 16:31:19 -06:00
Lance Edgar 3d89cffa65 Let caller control whether Login and Feedback menu items are shown 2020-02-10 16:30:38 -06:00
Lance Edgar e131305902 Force redirect to login page, if settings say so
this is pretty rudimentary, probably needs to be smarter or else should maybe
just be defined within final app instead of byjove
2020-02-10 14:18:28 -06:00
Lance Edgar ac5231c657 Add icons to nav header buttons
although we probably will need a way to remove those soon..  since we don't
have a ton of space up there
2020-02-09 16:30:36 -06:00
Lance Edgar fb5e6cde46 Add page-content-wrapper to main app template 2020-02-08 21:21:39 -06:00
Lance Edgar dfc4b88ca8 Update changelog 2019-12-04 16:56:12 -06:00
Lance Edgar 7f1ef1b335 Fix bug when checking user session
must have a user before can be root
2019-12-02 13:53:23 -06:00
33 changed files with 25851 additions and 159 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,143 @@ 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.
- Add `byjoveCurrency` filter.
- Fix pagination bug in model-crud.
- Add new `ByjoveReceiving` component.
- Let caller define row sorting for model-crud.
- Stop showing Buefy toast messages at bottom of screen.
- Misc. improvements for "delete" support in model-crud.
- Add support for "Create Row" button when viewing parent CRUD object.
- Avoid showing "removed" batch rows by default.
- Misc. fixes for model-crud, e.g. redirect to parent after deleting row.
- Add initial "inventory" component, for updating row counts.
## [0.1.8] - 2020-03-02
### Changed
- Allow customization of Save button for model-crud.
- Add basic support for "executing" mode, in model-crud.
- Fix the "view object" link in CRUD header.
- Add basic "quick-delete" prop for model-crud.
- Let caller define placeholder and icon for autocomplete field.
## [0.1.7] - 2020-02-26
### Changed
- Consolidate some perm check logic.
- Make sure we never ask for page 0 when fetching model index data.
- Tweak how/when Edit and Delete buttons are shown for model-crud.
## [0.1.6] - 2020-02-21
### Changed
- Add page-content-wrapper to main app template.
- Add icons to nav header buttons.
- Let caller control whether Login and Feedback menu items are shown.
- Let caller control whether app footer is shown.
- Stop including Home link in menu.
- Remove some redirect magic, for logged-in users.
- Add `session_established` flag in global store.
- Delay showing the app until user info is fetched.
- Add `ByjovePlugin` for sake of `$hasPerm()` method.
- Add `user_is_admin` flag to app store, plus logic to set/clear it.
- Change aria-role (to "menu") for main user menu.
- Add proper logout logic to menu; let caller hide root options, about link.
- Update permissions when logging in user.
## [0.1.5] - 2019-12-04
### Changed
- Fix bug when checking user session.
## [0.1.4] - 2019-12-02
### Added
- Add app/menu support for "become / stop being root" feature.

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.4",
"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

@ -1,12 +1,16 @@
<template>
<div class="main-container">
<!-- note, we do not show anything at all until we have user info -->
<div class="main-container" v-show="$store.state.session_established">
<slot>
<p>TODO: put your nav in the default slot!</p>
</slot>
<div class="page-content-wrapper">
<div class="page-content">
<router-view />
</div>
<footer class="footer">
</div>
<footer v-if="includeFooter"
class="footer">
<slot name="footer">
<router-link to="/about">{{ appsettings.appTitle }} {{ appsettings.version }}</router-link>
</slot>
@ -19,6 +23,10 @@ export default {
name: 'ByjoveApp',
props: {
appsettings: Object,
includeFooter: {
type: Boolean,
default: true,
},
},
beforeCreate: function() {
@ -34,22 +42,21 @@ 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_IS_ROOT', response.data.user.is_root)
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)
// if user is anonymous, and requested logout page, send to login instead
if (!response.data.user && this.$route.name == 'logout') {
this.$router.push('/login')
// also keep track of backend app settings
this.$store.commit('SET_SETTINGS', response.data.settings || {})
// if user is logged in, and requested login page, send to home instead
} else if (response.data.user && this.$route.name == 'login') {
this.$router.push('/')
}
// declare the session established
this.$store.commit('SET_SESSION_ESTABLISHED', true)
// set background color, to whatever api said
// TODO: should this be the custom app's responsibility?
if (response.data.background_color) {
document.body.style.backgroundColor = response.data.background_color
}

View file

@ -4,12 +4,16 @@
:name="name"
v-show="!selected"
v-model="autocompleteValue"
:data="data"
:placeholder="placeholder"
field="label"
:icon="icon"
: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>
@ -26,10 +30,34 @@ 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: {
type: String,
default: null,
},
icon: {
type: String,
default: null,
},
openOnFocus: {
type: Boolean,
default: false,
},
},
data() {
let selected = null
if (this.value) {
@ -39,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() {
@ -56,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)
@ -64,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

@ -1,6 +1,7 @@
<template>
<div>
<b-button type="is-primary"
icon-left="fas fa-comment"
@click="showFeedback()">
Feedback
</b-button>
@ -11,6 +12,24 @@
<section class="modal-card-body">
<p class="modal-card-title">User Feedback</p>
<br />
<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.
@ -21,6 +40,7 @@
{{ referringURL }}
</div>
</b-field>
</div>
<b-field label="Your Name"
v-show="!userUUID">
@ -35,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">
:disabled="sendIsDisabled">
Send Note
</b-button>
</div>
@ -58,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,
@ -89,16 +144,15 @@ 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',
position: 'is-bottom',
})
}, response => {
this.$buefy.toast.open({
message: "Something went wrong!",
type: 'is-danger',
position: 'is-bottom',
})
})
},

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,17 +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

@ -0,0 +1,217 @@
<template>
<div class="inventory">
<nav class="breadcrumb">
<ul>
<li>
<router-link :to="getIndexUrl()">
Inventory
</router-link>
</li>
<li>
<router-link :to="getBatchUrl()">
{{ row.batch_id_str }}
</router-link>
</li>
<li>
&nbsp; {{ row[productKey] }}
</li>
</ul>
</nav>
<div style="display: flex;">
<div style="flex-grow: 1;">
<p class="has-text-weight-bold">
{{ row.brand_name }}
</p>
<p class="has-text-weight-bold">
{{ row.description }} {{ row.size }}
</p>
<p class="has-text-weight-bold">
<br />
1 CS = {{ row.case_quantity || 1 }} {{ row.unit_uom }}
</p>
</div>
<div>
<img :src="row.image_url" />
</div>
</div>
<b-field grouped>
<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"
step="any"
ref="units">
</b-input>
</b-field>
</b-field>
<div class="buttons">
<b-button type="is-primary"
icon-left="save"
@click="saveCounts()">
Save Counts
</b-button>
<b-button tag="router-link"
:to="`/inventory/rows/${row.uuid}`">
Cancel
</b-button>
</div>
</div>
</template>
<script>
export default {
name: 'ByjoveInventory',
props: {
productKey: {
type: String,
default: 'upc_pretty',
},
allowCases: {
type: Boolean,
default: true,
},
focusCases: {
type: Boolean,
default: false,
},
allowEdit: {
type: Boolean,
default: false,
},
allowDelete: {
type: Boolean,
default: false,
},
},
data() {
return {
row: {},
}
},
created() {
this.fetch(this.$route.params.uuid)
},
methods: {
getIndexUrl() {
return '/inventory/'
},
getBatchUrl() {
return `/inventory/${this.row.batch_uuid}`
},
getViewUrl() {
return `/inventory/rows/${this.row.uuid}`
},
getDeleteUrl() {
return `/inventory/rows/${this.row.uuid}/delete`
},
getApiUrl(uuid) {
if (!uuid) {
uuid = this.row.uuid
}
return `/api/inventory-batch-row/${uuid}`
},
shouldAllowCases() {
if (!this.allowCases) {
return false
}
return this.row.allow_cases
},
shouldAllowEdit() {
if (!this.allowEdit) {
return false
}
return true
},
shouldAllowDelete() {
if (!this.allowDelete) {
return false
}
return true
},
fetch(uuid) {
let url = this.getApiUrl(uuid)
this.$http.get(url).then(response => {
this.row = response.data.data
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
this.$buefy.toast.open({
message: "You do not have permission to access that page.",
type: 'is-danger',
})
this.$router.push(this.getIndexUrl())
} else {
this.$buefy.toast.open({
message: "Failed to fetch page data!",
type: 'is-danger',
})
}
})
},
saveCounts() {
let url = this.getApiUrl()
let params = {
row: this.row.uuid,
units: this.row.units,
}
if (this.shouldAllowCases()) {
params.cases = this.row.cases
}
this.$http.post(url, params).then(response => {
if (response.data.data) {
// return to batch view
this.$router.push(this.getBatchUrl())
} else {
this.$buefy.toast.open({
message: response.data.error || "Failed to save counts!",
type: 'is-danger',
})
}
}, response => {
this.$buefy.toast.open({
message: "Failed to save counts (server error)!",
type: 'is-danger',
})
})
},
},
}
</script>

View file

@ -0,0 +1,28 @@
// Import vue component
import ByjoveInventory from './ByjoveInventory.vue'
// Declare install function executed by Vue.use()
export function install(Vue) {
if (install.installed) return;
install.installed = true;
Vue.component('ByjoveInventory', ByjoveInventory);
}
// 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 ByjoveInventory

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

@ -1,60 +1,68 @@
<template>
<section>
<nav class="level is-mobile">
<div class="level-item">
<b-dropdown v-if="user"
aria-role="list">
<button class="button is-primary"
:class="{'is-danger': user_is_root}"
slot="trigger">
<span>{{ user.short_name }}</span>
<!-- <b-icon icon="menu-down"></b-icon> -->
</button>
<div v-if="user.uuid"
class="level-item">
<b-dropdown-item aria-role="listitem" has-link>
<router-link to="/">Home</router-link>
</b-dropdown-item>
<b-dropdown aria-role="menu">
<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>
<b-dropdown-item v-if="user.is_admin && !user_is_root"
aria-role="listitem"
<b-dropdown-item v-if="exposeRoot && $store.state.user_is_admin && !user_is_root"
aria-role="menuitem"
has-link
class="root-user">
<a href="#" @click.prevent="becomeRoot()">Become root</a>
</b-dropdown-item>
<b-dropdown-item v-if="user_is_root"
aria-role="listitem"
<b-dropdown-item v-if="exposeRoot && user_is_root"
aria-role="menuitem"
has-link
class="root-user">
<a href="#" @click.prevent="stopRoot()">Stop being root</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem" has-link>
<router-link to="/logout">Logout</router-link>
<b-dropdown-item aria-role="menuitem" has-link>
<a href="#" @click.prevent="logout()">Logout</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem" has-link>
<b-dropdown-item v-if="includeAboutLink"
aria-role="menuitem" has-link>
<router-link to="/about">About</router-link>
</b-dropdown-item>
</b-dropdown>
</div>
<b-button v-if="!user"
type="is-primary"
<div v-if="!user.uuid && showLoginButton"
class="level-item">
<b-button type="is-primary"
tag="router-link" to="/login">
Login
<b-icon icon="fas fa-user"></b-icon>
<span>Login</span>
</b-button>
</div>
<div class="level-item">
<slot name="header">
{{ title }}
</slot>
</div>
<div class="level-item">
<byjove-feedback :url="feedbackUrl"></byjove-feedback>
<div v-if="shouldShowFeedback"
class="level-item">
<byjove-feedback :url="feedbackUrl"
:recipient-options="feedbackRecipientOptions">
</byjove-feedback>
</div>
</nav>
@ -67,10 +75,38 @@ export default {
name: 'ByjoveMenu',
props: {
appsettings: Object,
showLoginButton: {
type: Boolean,
default: true,
},
logoutUrl: {
type: String,
default: '/api/logout',
},
allowFeedback: {
type: Boolean,
default: null,
},
allowAnonymousFeedback: {
type: Boolean,
default: false,
},
feedbackUrl: {
type: String,
default: '/api/feedback',
},
feedbackRecipientOptions: {
type: Array,
default: null,
},
includeAboutLink: {
type: Boolean,
default: true,
},
exposeRoot: {
type: Boolean,
default: true,
},
becomeRootUrl: {
type: String,
default: '/api/become-root',
@ -97,10 +133,36 @@ export default {
user_is_root: function() {
return this.$store.state.user_is_root
},
shouldShowFeedback: function() {
if (this.allowFeedback !== null) {
return this.allowFeedback
}
if (!this.allowAnonymousFeedback && !this.user.uuid) {
return false
}
return true
},
},
methods: {
logout() {
// submit user logout request to backend
this.$http.post(this.logoutUrl).then(response => {
// do logout proper, within our app
this.$logoutUser()
// re-fetch session to get fresh XSRF token cookie; our http
// interceptor will handle the rest of that for us. once we
// have token we show the login form again
this.$http.get('/api/session').then(response => {
this.$router.push('/login')
})
})
},
becomeRoot() {
if (this.user_is_root || !this.user.is_admin) {
if (this.user_is_root || !this.$store.state.user_is_admin) {
return
}
this.$http.post(this.becomeRootUrl).then(response => {
@ -108,13 +170,11 @@ export default {
this.$buefy.toast.open({
message: "You have been elevated to 'root' and now have full system access",
type: 'is-success',
position: 'is-bottom',
})
}, response => {
this.$buefy.toast.open({
message: "Something went wrong!",
type: 'is-danger',
position: 'is-bottom',
})
})
@ -128,13 +188,11 @@ export default {
this.$buefy.toast.open({
message: "Your normal system access has been restored",
type: 'is-success',
position: 'is-bottom',
})
}, response => {
this.$buefy.toast.open({
message: "Something went wrong!",
type: 'is-danger',
position: 'is-bottom',
})
})
},

View file

@ -6,7 +6,7 @@
<router-link :to="getModelPathPrefix() + '/'">{{ getModelIndexTitle() }}</router-link>
</li>
<li v-if="isRow">
<router-link :to="getModelPathPrefix() + '/' + record._parent_uuid">{{ renderParentHeaderLabel(record) }}</router-link>
<router-link :to="getParentUrl()">{{ renderParentHeaderLabel(record) }}</router-link>
<!-- &nbsp; {{ renderParentHeaderLabel(record) }} -->
</li>
<li v-if="mode == 'creating'">
@ -15,12 +15,17 @@
<li v-if="mode == 'viewing'">
&nbsp; {{ renderHeaderLabel(record) }}
</li>
<li v-if="mode == 'editing' || mode == 'deleting'">
<router-link :to="getModelPathPrefix() + '/' + record.uuid">{{ renderHeaderLabel(record) }}</router-link>
<li v-if="mode == 'editing' || mode == 'executing' || mode == 'deleting'">
<router-link :to="getViewURL()">
{{ renderHeaderLabel(record) }}
</router-link>
</li>
<li v-if="mode == 'editing'">
&nbsp; Edit
</li>
<li v-if="mode == 'executing'">
&nbsp; Execute
</li>
<li v-if="mode == 'deleting'">
&nbsp; Delete
</li>
@ -29,35 +34,75 @@
<slot></slot>
<b-button v-if="allowEdit && mode == 'viewing' && hasModelPerm('edit')"
type="is-primary"
tag="router-link"
:to="getModelPathPrefix() + '/' + record.uuid + '/edit'">
Edit This
</b-button>
<div v-if="showButtons"
class="buttons">
<b-button type="is-primary"
class="buttons" style="margin-top: 1rem;">
<b-button :type="getSaveButtonType()"
v-if="!hideSaveButton"
:icon-left="getSaveButtonIcon()"
:disabled="saveDisabled"
@click="save()">
Save
{{ getSaveButtonText() }}
</b-button>
<b-button v-if="mode == 'creating'"
tag="router-link"
:to="getModelPathPrefix() + '/'">
<b-button tag="router-link"
:to="getCancelURL()">
Cancel
</b-button>
<b-button v-if="mode == 'editing'"
</div>
<div v-if="shouldAllowEdit() || shouldAllowDelete()" class="buttons">
<br /><br />
<b-button v-if="shouldAllowEdit()"
type="is-primary"
icon-left="edit"
tag="router-link"
:to="getModelPathPrefix() + '/' + record.uuid">
Cancel
:to="getEditURL()">
Edit this {{ getModelTitle() }}
</b-button>
<b-button v-if="shouldAllowDelete() && !quickDelete"
type="is-danger"
icon-left="trash"
tag="router-link"
:to="getDeleteURL()">
Delete this {{ getModelTitle() }}
</b-button>
<b-button v-if="shouldAllowDelete() && quickDelete"
type="is-danger"
icon-left="trash"
@click="$emit('delete')">
Delete this {{ getModelTitle() }}
</b-button>
</div>
<slot name="quick-entry"></slot>
<div v-if="hasRows && mode == 'viewing'">
<div v-if="rowsTitle && shouldAllowCreateRow()"
style="display: flex;">
<h2 style="flex-grow: 1;">{{ rowsTitle }}</h2>
<b-button type="is-primary"
tag="router-link"
:to="getCreateRowButtonUrl()"
icon-left="plus">
{{ createRowButtonText }}
</b-button>
</div>
<h2 v-if="rowsTitle && !shouldAllowCreateRow()">
{{ rowsTitle }}
</h2>
<b-button v-if="shouldAllowCreateRow() && !rowsTitle"
type="is-primary"
tag="router-link"
:to="getCreateRowButtonUrl()"
icon-left="plus">
{{ createRowButtonText }}
</b-button>
<slot name="row-filters"></slot>
<b-menu>
<b-menu-list>
@ -75,9 +120,7 @@
:total="rowData.total"
:current.sync="rowPage"
:per-page="rowsPerPage"
@change="changeRowPagination"
>
<!-- icon-pack="fas" -->
@change="changeRowPagination">
</b-pagination>
</div>
@ -108,6 +151,26 @@ export default {
type: Boolean,
default: false,
},
rowsTitle: {
type: String,
default: null,
},
allowCreateRow: {
type: Boolean,
default: false,
},
createRowPermission: {
type: String,
default: null,
},
createRowButtonText: {
type: String,
default: "Create New Row",
},
createRowButtonUrl: {
type: String,
default: null,
},
rowsPaginated: {
type: Boolean,
default: true,
@ -126,21 +189,56 @@ export default {
rowFilters: {
type: Function,
default: (uuid) => {
return JSON.stringify([{field: 'batch_uuid', op: 'eq', value: uuid}])
return [
{field: 'batch_uuid', op: 'eq', value: uuid},
{field: 'removed', op: 'eq', value: false},
]
},
},
rowOrderBy: {
type: String,
default: 'modified',
},
rowOrderAscending: {
type: Boolean,
default: false,
},
allowEdit: {
type: Boolean,
default: true,
},
allowDelete: {
type: Boolean,
default: false,
},
quickDelete: {
type: Boolean,
default: false,
},
hideButtons: {
type: Boolean,
default: false,
},
hideSaveButton: {
type: Boolean,
default: false,
},
saveDisabled: {
type: Boolean,
default: false,
},
saveButtonText: {
type: String,
default: null,
},
saveButtonIcon: {
type: String,
default: null,
},
cancelUrl: {
type: String,
default: null,
},
},
data: function() {
return {
@ -161,6 +259,9 @@ export default {
if (this.mode == 'editing') {
return true
}
if (this.mode == 'deleting') {
return true
}
return false
},
},
@ -173,7 +274,6 @@ export default {
this.$buefy.toast.open({
message: "You do not have permission to access that page.",
type: 'is-danger',
position: 'is-bottom',
})
this.$router.push(this.getModelPathPrefix() + '/')
}
@ -208,7 +308,6 @@ export default {
// this.$buefy.toast.open({
// message: "You do not have permission to access that page.",
// type: 'is-danger',
// position: 'is-bottom',
// })
// this.$router.push(this.getModelPathPrefix() + '/')
// return
@ -222,6 +321,11 @@ export default {
methods: {
clear() {
this.record = {}
this.$emit('refresh', this.record)
},
getModelSlug() {
if (this.modelSlug) {
return this.modelSlug
@ -271,6 +375,37 @@ export default {
return '/' + this.getModelSlug() + '/rows'
},
getIndexURL() {
return `${this.getModelPathPrefix()}/`
},
getParentUrl() {
if (this.isRow) {
return `${this.getModelPathPrefix()}/${this.record._parent_uuid}`
}
},
getViewURL() {
if (this.isRow) {
return `${this.getRowPathPrefix()}/${this.record.uuid}`
}
return `${this.getModelPathPrefix()}/${this.record.uuid}`
},
getEditURL() {
if (this.isRow) {
return `${this.getRowPathPrefix()}/${this.record.uuid}/edit`
}
return `${this.getModelPathPrefix()}/${this.record.uuid}/edit`
},
getDeleteURL() {
if (this.isRow) {
return `${this.getRowPathPrefix()}/${this.record.uuid}/delete`
}
return `${this.getModelPathPrefix()}/${this.record.uuid}/delete`
},
getApiIndexUrl() {
if (this.apiIndexUrl) {
return this.apiIndexUrl
@ -278,7 +413,7 @@ export default {
return '/api/' + this.getModelSlug()
},
getApiObjectUrl() {
getApiObjectUrlBase() {
if (this.apiObjectUrl) {
return this.apiObjectUrl
}
@ -287,6 +422,14 @@ export default {
return url.slice(0, -1) + '/'
},
getApiObjectUrl(uuid) {
let url = this.getApiObjectUrlBase()
if (uuid) {
url += uuid
}
return url
},
getApiRowsUrl() {
return this.apiRowsUrl
},
@ -298,22 +441,11 @@ export default {
return this.getModelSlug()
},
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) {
@ -344,37 +476,78 @@ export default {
return row._str
},
getSaveButtonIcon() {
if (this.saveButtonIcon) {
return this.saveButtonIcon
}
if (this.mode == 'deleting') {
return 'trash'
}
return 'save'
},
getSaveButtonText() {
if (this.saveButtonText) {
return this.saveButtonText
}
if (this.mode == 'deleting') {
return "Delete This Forever!"
}
if (this.mode == 'creating') {
return `Create ${this.getModelTitle()}`
}
return "Save Data"
},
getSaveButtonType() {
if (this.mode == 'deleting') {
return 'is-danger'
}
return 'is-primary'
},
fetch(uuid) {
this.$http.get(this.getApiObjectUrl() + uuid).then(response => {
this.$http.get(this.getApiObjectUrl(uuid)).then(response => {
this.record = response.data.data
this.$emit('refresh', this.record)
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)
})
}
}, 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',
position: 'is-bottom',
})
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',
position: 'is-bottom',
})
}
// redirect to model index
this.$router.push(this.getIndexURL())
})
},
fetchRows(uuid) {
let params = {
filters: this.rowFilters(uuid),
orderBy: 'modified',
ascending: 0,
filters: JSON.stringify(this.rowFilters(uuid)),
orderBy: this.rowOrderBy,
ascending: this.rowOrderAscending ? 1 : 0,
}
if (this.rowsPaginated) {
params.per_page = this.rowsPerPage
@ -382,20 +555,22 @@ export default {
}
this.$http.get(this.getApiRowsUrl(), {params: params}).then(response => {
this.rowData = response.data
// TODO: for some reason rowPage is getting reset to 0, maybe by
// the b-pagination component? for now this is our workaround
if (!this.rowPage) {
this.rowPage = 1
}
}, response => {
if (response.status == 403) { // forbidden; redirect to home page
this.$buefy.toast.open({
message: "You do not have permission to access that page.",
type: 'is-danger',
position: 'is-bottom',
})
this.$router.push('/')
} else {
this.$buefy.toast.open({
message: "Failed to fetch page data!",
type: 'is-danger',
position: 'is-bottom',
})
}
})
@ -412,13 +587,111 @@ export default {
this.fetchRows(this.record.uuid)
},
shouldAllowEdit() {
if (!this.allowEdit) {
return false
}
if (this.mode != 'viewing') {
return false
}
if (!this.hasModelPerm('edit')) {
return false
}
return true
},
shouldAllowDelete() {
if (!this.allowDelete) {
return false
}
if (this.mode != 'viewing') {
return false
}
if (!this.hasModelPerm('delete')) {
return false
}
return true
},
getCancelURL() {
if (this.cancelUrl) {
return this.cancelUrl
}
if (this.mode == 'creating') {
return this.getIndexURL()
}
return this.getViewURL()
},
save() {
let url = this.getApiIndexUrl()
if (this.mode != 'creating') {
url = this.getApiObjectUrl() + this.record.uuid
if (this.mode == 'deleting') {
this.confirmDelete()
return
}
let url
if (this.mode == 'creating') {
url = this.getApiIndexUrl()
} else {
url = this.getApiObjectUrl(this.record.uuid)
}
this.$emit('save', url)
},
confirmDelete() {
let modelTitle = this.getModelTitle()
this.$buefy.dialog.confirm({
title: `Delete ${modelTitle}`,
message: `Are you sure you wish to delete this ${modelTitle}?`,
hasIcon: true,
type: 'is-danger',
confirmText: `Delete ${modelTitle}`,
cancelText: "Whoops, nevermind",
onConfirm: this.deleteObject,
})
},
deleteObject() {
let url = this.getApiObjectUrl(this.record.uuid)
this.$http.delete(url).then(response => {
if (this.isRow) {
this.$router.push(this.getParentUrl())
} else {
this.$router.push(this.getIndexURL())
}
}, response => {
this.$buefy.toast.open({
message: "Failed to delete the record!",
type: 'is-danger',
})
})
},
shouldAllowCreateRow() {
if (!this.allowCreateRow) {
return false
}
if (this.mode != 'viewing') {
return false
}
if (this.createRowPermission) {
if (!this.$byjoveHasPerm(this.createRowPermission)) {
return false
}
} else {
if (!this.hasModelPerm('create_row')) {
return false
}
}
return true
},
getCreateRowButtonUrl() {
if (this.createRowButtonUrl) {
return this.createRowButtonUrl
}
return this.getApiRowsUrl()
},
},
}
</script>

View file

@ -7,13 +7,16 @@
</ul>
</nav>
<b-button v-if="allowCreate && hasModelPerm('create')"
class="new-object"
<div class="buttons block"
v-if="allowCreate && userCanCreate()">
<b-button class="new-object"
type="is-primary"
tag="router-link"
:to="getModelPathPrefix() + '/new'">
:to="getModelPathPrefix() + '/new'"
icon-left="plus">
New {{ getModelTitle() }}
</b-button>
</div>
<slot></slot>
@ -53,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,
@ -75,7 +87,9 @@ export default {
}
},
mounted() {
if (this.wantInitialResults) {
this.fetchData()
}
},
watch: {
'apiIndexFilters' (to, from) {
@ -91,12 +105,16 @@ 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
params.page = this.page
params.page = this.page || 1
}
this.$http.get(this.getApiIndexUrl(), {params: params}).then(response => {
this.data = response.data
@ -106,14 +124,17 @@ export default {
this.$buefy.toast.open({
message: "You do not have permission to access that page.",
type: 'is-danger',
position: 'is-bottom',
})
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!",
type: 'is-danger',
position: 'is-bottom',
})
}
})
@ -168,22 +189,18 @@ export default {
return this.getModelSlug()
},
hasPerm(perm) {
// if user is root then assume permission
if (this.$store.state.user && this.$store.state.user.is_root) {
return true
userCanCreate() {
if (this.createPermCheck) {
return this.createPermCheck()
}
// otherwise do true perm check for user
return this.$store.state.permissions.includes(perm)
return this.hasModelPerm('create')
},
hasModelPerm(perm) {
// do normal check, but first add prefix
let prefix = this.getModelPermissionPrefix()
return this.hasPerm(prefix + '.' + perm)
return this.$hasPerm(prefix + '.' + perm)
},
renderLabel(obj) {

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

@ -0,0 +1,409 @@
<template>
<div class="receiving">
<nav class="breadcrumb">
<ul>
<li>
<router-link :to="getIndexURL()">Receiving</router-link>
</li>
<li>
<router-link :to="getBatchURL()">{{ row.batch_id_str }}</router-link>
</li>
<li>
&nbsp; {{ row[productKey] }}
</li>
</ul>
</nav>
<div style="display: flex;">
<div style="flex-grow: 1;">
<p class="has-text-weight-bold">
{{ row.brand_name }}
</p>
<p class="has-text-weight-bold">
{{ 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>
</div>
<div>
<img :src="row.image_url" />
</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 />
<p>{{ row.received_alert }}</p>
</div>
<div v-if="row.unexpected_alert"
class="has-text-danger">
<br />
<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 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"
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>
</slot> <!-- quick-receive -->
<p class="has-text-centered is-italic">OR</p>
<br />
</div>
<b-field grouped>
<b-field class="control">
<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-field>
<b-field class="control">
<b-radio-button v-model="inputUOM"
native-value="CS"
:disabled="!allowCases">
<span>CS</span>
</b-radio-button>
<b-radio-button v-model="inputUOM"
:native-value="row.unit_uom">
<span>{{ row.unit_uom }}</span>
</b-radio-button>
</b-field>
</b-field>
<b-field>
<b-radio-button v-model="inputType"
native-value="received">
<span>RCVD</span>
</b-radio-button>
<b-radio-button v-model="inputType"
native-value="damaged">
<span>DMGD</span>
</b-radio-button>
<b-radio-button v-model="inputType"
native-value="expired"
:disabled="!allowExpired">
<span>EXPD</span>
</b-radio-button>
<b-radio-button v-model="inputType"
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'"
label="Expiration Date">
<b-input v-model="expirationDate"
type="date">
</b-input>
</b-field>
<div class="buttons">
<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>
<br />
<div class="buttons">
<b-button type="is-primary"
tag="router-link"
:to="`/receiving/rows/${row.uuid}`">
View this Row
</b-button>
<b-button type="is-primary"
disabled>
Edit this Row
</b-button>
<b-button type="is-danger"
disabled>
Delete this Row
</b-button>
</div>
</div>
</template>
<script>
import NumericInput from '../numeric-input/NumericInput.vue'
export default {
name: 'ByjoveReceiving',
components: {NumericInput},
props: {
productKey: {
type: String,
default: 'upc',
},
allowCases: {
type: Boolean,
default: true,
},
allowDecimalQuantities: {
type: Boolean,
default: false,
},
allowExpired: {
type: Boolean,
default: true,
},
},
data() {
return {
row: {},
inputQuantity: 1,
inputUOM: null,
inputType: 'received',
expirationDate: null,
}
},
computed: {
addAmountDisabled: function() {
if (this.inputType == 'expired' && !this.expirationDate) {
return true
}
return false
},
shouldShowQuickReceive() {
if (!this.row.quick_receive) {
return false
}
if (!this.row.product_uuid) {
return false
}
return true
},
},
mounted() {
this.fetch(this.$route.params.uuid)
},
methods: {
getIndexURL() {
return '/receiving/'
},
getBatchURL() {
return `/receiving/${this.row.batch_uuid}`
},
fetch(uuid) {
this.$http.get(`/api/receiving-batch-row/${uuid}`).then(response => {
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
}
}, response => {
if (response.status == 403) { // forbidden; redirect to model index
this.$buefy.toast.open({
message: "You do not have permission to access that page.",
type: 'is-danger',
})
this.$router.push(this.getIndexURL())
} else {
this.$buefy.toast.open({
message: "Failed to fetch page data!",
type: 'is-danger',
})
}
})
},
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,
mode: this.inputType,
cases: null,
units: null,
expiration_date: this.expirationDate,
quick_receive: false,
}
if (this.inputUOM == 'CS') {
params.cases = amount
} else {
params.units = amount
}
this.$http.post(url, params).then(response => {
if (!response.data.error) {
this.$router.push(`/receiving/${this.row.batch_uuid}`)
} else {
this.$buefy.toast.open({
message: response.data.error,
type: 'is-danger',
})
}
}, response => {
this.$buefy.toast.open({
message: "Save failed: unknown error",
type: 'is-danger',
})
})
},
addQuickAmount(quantity, uom) {
this.inputQuantity = quantity
this.inputUOM = uom
this.addAmount()
},
subtractAmount() {
this.inputQuantity = -this.inputQuantity
this.addAmount()
},
},
}
</script>
<style>
table.receiving-quantities td {
padding: 0px 10px 0px 0px;
}
.input.receiving-quantity-input {
width: 6rem;
}
</style>

View file

@ -0,0 +1,28 @@
// Import vue component
import ByjoveReceiving from './ByjoveReceiving.vue'
// Declare install function executed by Vue.use()
export function install(Vue) {
if (install.installed) return;
install.installed = true;
Vue.component('ByjoveReceiving', ByjoveReceiving);
}
// 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 ByjoveReceiving

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

@ -1,4 +1,5 @@
export * from './plugin'
export * from './components'
export {ByjoveStoreConfig} from './store'

68
src/plugin.js Normal file
View file

@ -0,0 +1,68 @@
export function ByjovePlugin(Vue) {
Vue.mixin({
methods: {
$byjoveLoginUser(user, permissions) {
// put user info in app store
this.$store.commit('SET_USER', user)
this.$store.commit('SET_USER_IS_ADMIN', user.is_admin)
// note, this should already be false
// this.$store.commit('SET_USER_IS_ROOT', false)
// maybe update permissions
if (permissions !== undefined) {
this.$store.commit('SET_PERMISSIONS', permissions)
}
},
// TODO: deprecate / remove
$loginUser(user, permission) {
this.$byjoveLoginUser(user, permission)
},
$byjoveLogoutUser() {
// remove user info from app store
this.$store.commit('SET_USER', {})
this.$store.commit('SET_USER_IS_ADMIN', false)
this.$store.commit('SET_USER_IS_ROOT', false)
},
// TODO: deprecate / remove
$logoutUser() {
this.$byjoveLogoutUser()
},
$byjoveHasPerm(name) {
// if user is root then assume permission
if (this.$store.state.user_is_root) {
return true
}
// otherwise do true perm check for user
return this.$store.state.permissions.includes(name)
},
// TODO: deprecate / remove
$hasPerm(name) {
return this.$byjoveHasPerm(name)
},
}
})
Vue.filter('byjoveCurrency', function (value) {
if (typeof value !== 'number') {
return value
}
var formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
// TODO: make this configurable?
minimumFractionDigits: 2,
})
return formatter.format(value)
})
}

View file

@ -1,20 +1,32 @@
export let ByjoveStoreConfig = {
state: {
user: null,
user_is_root: null,
session_established: false,
user: {},
user_is_admin: false,
user_is_root: false,
permissions: [],
settings: {},
},
mutations: {
SET_SESSION_ESTABLISHED(state, established) {
state.session_established = established
},
SET_USER(state, user) {
state.user = user
},
SET_USER_IS_ADMIN(state, is_admin) {
state.user_is_admin = is_admin
},
SET_USER_IS_ROOT(state, is_root) {
state.user_is_root = is_root
},
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))