Add very basic mobile app support for Theo
much more to come yet, but should be enough to launch a demo
This commit is contained in:
parent
774221420c
commit
8c738d3ee8
3
mobile/.browserslistrc
Normal file
3
mobile/.browserslistrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
29
mobile/.gitignore
vendored
Normal file
29
mobile/.gitignore
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
|
||||
# custom entries follow
|
||||
|
||||
vue.config.js
|
||||
src/appsettings.js
|
19
mobile/README.md
Normal file
19
mobile/README.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# mobile
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
mobile/babel.config.js
Normal file
5
mobile/babel.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
11346
mobile/package-lock.json
generated
Normal file
11346
mobile/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
mobile/package.json
Normal file
26
mobile/package.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "theo-mobile",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"buefy": "^0.9.4",
|
||||
"byjove": "^0.1.9",
|
||||
"core-js": "^3.6.5",
|
||||
"js-cookie": "^2.2.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-resource": "^1.5.1",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
}
|
||||
}
|
BIN
mobile/public/favicon.ico
Normal file
BIN
mobile/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
18
mobile/public/index.html
Normal file
18
mobile/public/index.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.2/css/all.css">
|
||||
<title>Theo Mobile</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but Theo Mobile doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
122
mobile/src/App.vue
Normal file
122
mobile/src/App.vue
Normal file
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<byjove-app :appsettings="appsettings">
|
||||
<app-nav></app-nav>
|
||||
<template v-slot:footer>
|
||||
<router-link to="/about">{{ appsettings.appTitle}} {{ appsettings.version }}</router-link>
|
||||
<div>
|
||||
<br />
|
||||
<a href="/">View Desktop Site</a>
|
||||
</div>
|
||||
</template>
|
||||
</byjove-app>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ByjoveApp} from 'byjove'
|
||||
import AppNav from './components/AppNav.vue'
|
||||
import appsettings from './appsettings'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {
|
||||
ByjoveApp,
|
||||
AppNav,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
appsettings: appsettings,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
/******************************
|
||||
* main app layout
|
||||
******************************/
|
||||
|
||||
#app {
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
}
|
||||
.main-container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
.page-content {
|
||||
flex: 1;
|
||||
padding: 0.5rem 1rem 0.5rem 0.5rem;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/******************************
|
||||
* general stuff
|
||||
******************************/
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/******************************
|
||||
* root-user
|
||||
******************************/
|
||||
|
||||
[role=menuitem].root-user {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
/******************************
|
||||
* model-index
|
||||
******************************/
|
||||
|
||||
.model-index .menu {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.model-index .menu-list li {
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
}
|
||||
|
||||
.model-index .menu-list li:last-child {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
/******************************
|
||||
* model-crud
|
||||
******************************/
|
||||
|
||||
.model-crud .menu {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.model-crud .menu-list li {
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
}
|
||||
|
||||
.model-crud .menu-list li:last-child {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
</style>
|
14
mobile/src/appsettings.js.dist
Normal file
14
mobile/src/appsettings.js.dist
Normal file
|
@ -0,0 +1,14 @@
|
|||
// -*- mode: js; -*-
|
||||
|
||||
import packageData from '../package.json'
|
||||
|
||||
var appsettings = {
|
||||
systemTitle: "Theo",
|
||||
appTitle: "Theo-Mobile",
|
||||
version: packageData.version,
|
||||
logo: '/tailbone/img/home_logo.png',
|
||||
production: false,
|
||||
watermark: 'url("/tailbone/img/testing.png")',
|
||||
};
|
||||
|
||||
export default appsettings;
|
BIN
mobile/src/assets/logo.png
Normal file
BIN
mobile/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
40
mobile/src/components/AppNav.vue
Normal file
40
mobile/src/components/AppNav.vue
Normal file
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<byjove-menu :appsettings="appsettings">
|
||||
|
||||
<b-dropdown-item aria-role="menuitem" has-link>
|
||||
<router-link to="/">Home</router-link>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item v-if="$hasPerm('ordering.list')"
|
||||
aria-role="menuitem"
|
||||
has-link>
|
||||
<router-link to="/ordering/">Ordering</router-link>
|
||||
</b-dropdown-item>
|
||||
|
||||
</byjove-menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ByjoveMenu} from 'byjove'
|
||||
import appsettings from '../appsettings'
|
||||
|
||||
export default {
|
||||
name: 'AppNav',
|
||||
components: {
|
||||
ByjoveMenu,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
appsettings: appsettings,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user: function() {
|
||||
return this.$store.state.user
|
||||
},
|
||||
user_is_root: function() {
|
||||
return this.$store.state.user_is_root
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
34
mobile/src/main.js
Normal file
34
mobile/src/main.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import vueResource from 'vue-resource'
|
||||
import Cookie from 'js-cookie'
|
||||
import Buefy from 'buefy'
|
||||
import 'buefy/dist/buefy.css'
|
||||
import {ByjovePlugin} from 'byjove'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
Vue.use(vueResource)
|
||||
|
||||
// the backend API will set a cookie for XSRF-TOKEN, which we will submit
|
||||
// *back* to the backend API whenever we call it from then on. we set this up
|
||||
// globally so none of our API calls actually have to mess with it
|
||||
Vue.http.interceptors.push((request, next) => {
|
||||
request.headers.set('X-XSRF-TOKEN', Cookie.get('XSRF-TOKEN'))
|
||||
next()
|
||||
})
|
||||
|
||||
Vue.use(Buefy, {
|
||||
// use FontAwesome icon pack
|
||||
defaultIconPack: 'fas',
|
||||
})
|
||||
|
||||
Vue.use(ByjovePlugin)
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
80
mobile/src/router/index.js
Normal file
80
mobile/src/router/index.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
import Login from '../views/Login.vue'
|
||||
import {OrderingBatches, OrderingBatch, OrderingBatchRow, OrderingBatchWorksheet} from '../views/ordering'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
|
||||
},
|
||||
|
||||
//////////////////////////////
|
||||
// Ordering
|
||||
//////////////////////////////
|
||||
{
|
||||
path: '/ordering/',
|
||||
name: 'ordering',
|
||||
component: OrderingBatches,
|
||||
},
|
||||
{
|
||||
path: '/ordering/new',
|
||||
name: 'ordering.new',
|
||||
component: OrderingBatch,
|
||||
props: {mode: 'creating'},
|
||||
},
|
||||
{
|
||||
path: '/ordering/:uuid',
|
||||
name: 'ordering.view',
|
||||
component: OrderingBatch,
|
||||
props: {mode: 'viewing'},
|
||||
},
|
||||
// {
|
||||
// path: '/ordering/:uuid/edit',
|
||||
// name: 'ordering.edit',
|
||||
// component: OrderingBatch,
|
||||
// props: {mode: 'editing'},
|
||||
// },
|
||||
{
|
||||
path: '/ordering/rows/:uuid',
|
||||
name: 'ordering.rows.view',
|
||||
component: OrderingBatchRow,
|
||||
props: {mode: 'viewing'},
|
||||
},
|
||||
{
|
||||
path: '/ordering/rows/:uuid/edit',
|
||||
name: 'ordering.rows.edit',
|
||||
component: OrderingBatchRow,
|
||||
props: {mode: 'editing'},
|
||||
},
|
||||
{
|
||||
path: '/ordering/:uuid/worksheet',
|
||||
name: 'ordering.worksheet',
|
||||
component: OrderingBatchWorksheet,
|
||||
},
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
7
mobile/src/store/index.js
Normal file
7
mobile/src/store/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import {ByjoveStoreConfig} from 'byjove'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store(ByjoveStoreConfig)
|
5
mobile/src/views/About.vue
Normal file
5
mobile/src/views/About.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
29
mobile/src/views/Home.vue
Normal file
29
mobile/src/views/Home.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="home-page">
|
||||
<byjove-logo :appsettings="appsettings"></byjove-logo>
|
||||
<h2>Welcome to {{ appsettings.appTitle }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appsettings from '@/appsettings'
|
||||
import {ByjoveLogo} from 'byjove'
|
||||
|
||||
export default {
|
||||
name: 'home',
|
||||
components: {
|
||||
ByjoveLogo,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
appsettings: appsettings,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
92
mobile/src/views/Login.vue
Normal file
92
mobile/src/views/Login.vue
Normal file
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<div class="login">
|
||||
|
||||
<byjove-logo :appsettings="appsettings"></byjove-logo>
|
||||
|
||||
<div v-if="loginError" style="background-color: red;">
|
||||
{{ loginError }}
|
||||
</div>
|
||||
|
||||
<form>
|
||||
<div>
|
||||
<label for="username">Username</label>
|
||||
<input v-model="username" />
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<label for="password">Password</label>
|
||||
<input v-model="password" type="password" />
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<button type="button" v-on:click="attemptLogin">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appsettings from '@/appsettings'
|
||||
import {ByjoveLogo} from 'byjove'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
components: {
|
||||
ByjoveLogo,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
appsettings: appsettings,
|
||||
username: null,
|
||||
password: null,
|
||||
loginError: null
|
||||
}
|
||||
},
|
||||
beforeCreate: function() {
|
||||
this.checkUser()
|
||||
},
|
||||
watch: {
|
||||
'$store.state.user': 'checkUser',
|
||||
},
|
||||
methods: {
|
||||
|
||||
checkUser() {
|
||||
// send logged-in users to "home" instead
|
||||
if (this.$store.state.user) {
|
||||
this.$router.push('/')
|
||||
}
|
||||
},
|
||||
|
||||
attemptLogin: function() {
|
||||
this.loginError = null;
|
||||
var creds = {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
};
|
||||
|
||||
this.$http.post('/api/login', creds).then(response => {
|
||||
if (response.data.error) {
|
||||
this.loginError = response.data.error;
|
||||
} else {
|
||||
// let byjove do login proper
|
||||
this.$loginUser(response.data.user, response.data.permissions)
|
||||
// after user logs in, show home page
|
||||
this.$router.push('/')
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login {
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
max-height: 200px;
|
||||
max-width: 300px;
|
||||
}
|
||||
</style>
|
256
mobile/src/views/ordering/OrderingBatch.vue
Normal file
256
mobile/src/views/ordering/OrderingBatch.vue
Normal file
|
@ -0,0 +1,256 @@
|
|||
<template>
|
||||
<byjove-model-crud model-name="PurchaseBatch"
|
||||
model-index-title="Ordering"
|
||||
model-title="Ordering Batch"
|
||||
model-title-plural="Ordering Batches"
|
||||
model-path-prefix="/ordering"
|
||||
model-permission-prefix="ordering"
|
||||
row-path-prefix="/ordering/rows"
|
||||
:mode="mode"
|
||||
@refresh="record => { batch = record }"
|
||||
api-index-url="/api/ordering-batches"
|
||||
api-object-url="/api/ordering-batch/"
|
||||
:header-label-renderer="renderHeaderLabel"
|
||||
:allow-edit="false"
|
||||
has-rows
|
||||
api-rows-url="/api/ordering-batch-rows"
|
||||
:row-label-renderer="renderRowLabel"
|
||||
:row-route-getter="getRowRoute"
|
||||
save-button-text="Make Ordering Batch"
|
||||
@save="save">
|
||||
|
||||
<div v-if="mode == 'creating'">
|
||||
|
||||
<b-field label="Vendor"
|
||||
:type="{'is-danger': !batch.vendor_uuid}">
|
||||
<byjove-autocomplete v-model="batch.vendor_uuid"
|
||||
service-url="/api/vendors/autocomplete">
|
||||
</byjove-autocomplete>
|
||||
</b-field>
|
||||
|
||||
<br />
|
||||
</div> <!-- creating -->
|
||||
|
||||
<div v-if="mode == 'viewing'">
|
||||
|
||||
<b-field label="Vendor">
|
||||
<span>
|
||||
{{ batch.vendor_display }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Date Ordered"
|
||||
v-if="mode != 'creating' && batch.executed">
|
||||
<span>
|
||||
{{ batch.date_ordered }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Total">
|
||||
<span>
|
||||
{{ batch.po_total_calculated_display }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Created"
|
||||
v-if="mode != 'creating' && batch.executed">
|
||||
<span>
|
||||
{{ batch.created }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Created By"
|
||||
v-if="mode != 'creating' && batch.executed">
|
||||
<span>
|
||||
{{ batch.created_by_display }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<br v-if="!batch.executed" />
|
||||
|
||||
<div v-if="batch.executed">
|
||||
|
||||
<b-field label="Executed">
|
||||
<span>
|
||||
{{ batch.executed }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Executed By">
|
||||
<span>
|
||||
{{ batch.executed_by_display }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
</div> <!-- executed -->
|
||||
|
||||
</div> <!-- viewing -->
|
||||
|
||||
<template slot="quick-entry">
|
||||
<div v-if="mode == 'viewing' && !batch.executed && !batch.complete">
|
||||
<b-input v-model="quickEntry"
|
||||
placeholder="Enter UPC"
|
||||
icon="search"
|
||||
@keypress.native="quickKey">
|
||||
</b-input>
|
||||
<br />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template slot="footer"
|
||||
v-if="mode == 'viewing' && !batch.executed">
|
||||
<br />
|
||||
<div class="buttons is-centered">
|
||||
<b-button v-if="!batch.complete"
|
||||
type="is-primary"
|
||||
@click="editWorksheet()">
|
||||
Edit as Worksheet
|
||||
</b-button>
|
||||
<b-button v-if="!batch.complete"
|
||||
type="is-primary"
|
||||
@click="markOrderingComplete()">
|
||||
Ordering is Complete!
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</byjove-model-crud>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ByjoveModelCrud, ByjoveAutocomplete} from 'byjove'
|
||||
|
||||
export default {
|
||||
name: 'OrderingBatch',
|
||||
props: {
|
||||
mode: String,
|
||||
},
|
||||
components: {
|
||||
ByjoveModelCrud,
|
||||
ByjoveAutocomplete,
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
batch: {},
|
||||
quickEntry: '',
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('keypress', this.globalKey)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('keypress', this.globalKey)
|
||||
},
|
||||
methods: {
|
||||
renderHeaderLabel(batch) {
|
||||
return batch.id_str
|
||||
},
|
||||
|
||||
renderRowLabel(row) {
|
||||
return `(${row.cases_ordered_display} / ${row.units_ordered_display}) ${row.full_description}`
|
||||
},
|
||||
|
||||
getRowRoute(row) {
|
||||
return `/ordering/rows/${row.uuid}/edit`
|
||||
},
|
||||
|
||||
save(url) {
|
||||
if (url === undefined) {
|
||||
url = '/api/ordering-batches'
|
||||
}
|
||||
let params = {
|
||||
vendor_uuid: this.batch.vendor_uuid,
|
||||
}
|
||||
this.$http.post(url, params).then(response => {
|
||||
if (response.data.data) {
|
||||
// navigate to new ordering batch
|
||||
this.$router.push('/ordering/' + response.data.data.uuid)
|
||||
} else {
|
||||
this.$buefy.toast.open({
|
||||
message: response.data.error || "Failed to save batch!",
|
||||
type: 'is-danger',
|
||||
})
|
||||
}
|
||||
}, response => {
|
||||
this.$buefy.toast.open({
|
||||
message: "Failed to save batch!",
|
||||
type: 'is-danger',
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
globalKey(event) {
|
||||
if (event.target.tagName == 'BODY') {
|
||||
|
||||
// mimic keyboard wedge
|
||||
if (event.charCode >= 48 && event.charCode <= 57) { // numeric (qwerty)
|
||||
this.$nextTick(function() {
|
||||
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) {
|
||||
return
|
||||
}
|
||||
let params = {
|
||||
batch_uuid: this.batch.uuid,
|
||||
quick_entry: this.quickEntry,
|
||||
}
|
||||
let url = '/api/ordering-batch-rows/quick-entry'
|
||||
this.$http.post(url, params).then(response => {
|
||||
if (response.data.data) {
|
||||
// let user edit row immediately
|
||||
this.$router.push(`/ordering/rows/${response.data.data.uuid}/edit`)
|
||||
} else {
|
||||
this.$buefy.toast.open({
|
||||
message: response.data.error || "Failed to post quick entry!",
|
||||
type: 'is-danger',
|
||||
})
|
||||
}
|
||||
}, response => {
|
||||
this.$buefy.toast.open({
|
||||
message: "Failed to post quick entry!",
|
||||
type: 'is-danger',
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
editWorksheet() {
|
||||
this.$router.push(`/ordering/${this.batch.uuid}/worksheet`)
|
||||
},
|
||||
|
||||
markOrderingComplete() {
|
||||
this.$http.post(`/api/ordering-batch/${this.batch.uuid}/mark-complete`).then(response => {
|
||||
if (response.data.data) {
|
||||
this.batch = response.data.data
|
||||
this.$router.push('/ordering')
|
||||
} else {
|
||||
this.$buefy.toast.open({
|
||||
message: response.data.error || "Failed to mark ordering complete!",
|
||||
type: 'is-danger',
|
||||
position: 'is-bottom',
|
||||
})
|
||||
}
|
||||
}, response => {
|
||||
this.$buefy.toast.open({
|
||||
message: "Failed to mark ordering complete!",
|
||||
type: 'is-danger',
|
||||
position: 'is-bottom',
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
127
mobile/src/views/ordering/OrderingBatchRow.vue
Normal file
127
mobile/src/views/ordering/OrderingBatchRow.vue
Normal file
|
@ -0,0 +1,127 @@
|
|||
<template>
|
||||
<byjove-model-crud model-name="OrderingBatchRow"
|
||||
model-index-title="Ordering"
|
||||
:mode="mode"
|
||||
:header-label-renderer="renderHeaderLabel"
|
||||
:parent-header-label-renderer="renderParentHeaderLabel"
|
||||
api-object-url="/api/ordering-batch-row/"
|
||||
model-path-prefix="/ordering"
|
||||
row-path-prefix="/ordering/rows"
|
||||
@refresh="record => { row = record }"
|
||||
is-row
|
||||
@save="save">
|
||||
|
||||
<b-field label="UPC">
|
||||
<span>
|
||||
{{ row.upc_pretty }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Description">
|
||||
<span>
|
||||
{{ row.description }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Cases Ordered">
|
||||
<b-input v-if="mode == 'editing'"
|
||||
v-model="row.cases_ordered"
|
||||
type="number"
|
||||
ref="casesOrdered"
|
||||
@keypress.native="handleEnter">
|
||||
</b-input>
|
||||
<span v-if="mode == 'viewing'">
|
||||
{{ row.cases_ordered_display }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Units Ordered">
|
||||
<b-input v-if="mode == 'editing'"
|
||||
v-model="row.units_ordered"
|
||||
type="number"
|
||||
ref="unitsOrdered"
|
||||
@keypress.native="handleEnter">
|
||||
</b-input>
|
||||
<span v-if="mode == 'viewing'">
|
||||
{{ row.units_ordered_display }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field label="PO Unit Cost">
|
||||
<span>
|
||||
{{ row.po_unit_cost_display }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Total">
|
||||
<span>
|
||||
{{ row.po_total_calculated_display }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Status">
|
||||
<span>
|
||||
{{ row.status_display }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
</byjove-model-crud>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ByjoveModelCrud} from 'byjove'
|
||||
|
||||
export default {
|
||||
name: 'OrderingBatchRow',
|
||||
props: {
|
||||
mode: String,
|
||||
},
|
||||
components: {
|
||||
ByjoveModelCrud,
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
row: {},
|
||||
}
|
||||
},
|
||||
// TODO: pretty sure this is too aggressive
|
||||
updated() {
|
||||
if (this.mode == 'editing') {
|
||||
this.$nextTick(function() {
|
||||
this.$refs.casesOrdered.focus()
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
renderParentHeaderLabel(row) {
|
||||
return row.batch_id_str
|
||||
},
|
||||
|
||||
renderHeaderLabel(row) {
|
||||
return row.upc_pretty
|
||||
},
|
||||
|
||||
handleEnter(event) {
|
||||
if (event.keyCode == 13) { // enter
|
||||
this.save()
|
||||
}
|
||||
},
|
||||
|
||||
save(url) {
|
||||
// we only update the order quantities
|
||||
let params = {
|
||||
cases_ordered: parseInt(this.row.cases_ordered),
|
||||
units_ordered: parseInt(this.row.units_ordered),
|
||||
}
|
||||
if (!url) {
|
||||
url = `/api/ordering-batch-row/${this.row.uuid}`
|
||||
}
|
||||
this.$http.post(url, params).then(response => {
|
||||
// go back to the purchase order
|
||||
this.$router.push('/ordering/' + response.data.data.batch_uuid)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
182
mobile/src/views/ordering/OrderingBatchWorksheet.vue
Normal file
182
mobile/src/views/ordering/OrderingBatchWorksheet.vue
Normal file
|
@ -0,0 +1,182 @@
|
|||
<template>
|
||||
<div>
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link to="/ordering">Ordering</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="`/ordering/${batch.uuid}`">
|
||||
{{ batch.id_str }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
Worksheet
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<b-field label="Vendor">
|
||||
<span>
|
||||
{{ batch.vendor_display }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Total">
|
||||
<span>
|
||||
{{ batch.po_total_calculated_display }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<div v-if="sortedDepartments">
|
||||
|
||||
<div v-for="dept in sortedDepartments"
|
||||
:key="dept.uuid">
|
||||
<h2>
|
||||
Dept. {{ dept.number }} ({{ dept.name }})
|
||||
</h2>
|
||||
<div v-for="subdept in dept.subdepartments"
|
||||
:key="subdept.uuid">
|
||||
<h3>
|
||||
Sub. {{ subdept.number }} ({{ subdept.name }})
|
||||
</h3>
|
||||
|
||||
<b-table :data="subdept.costs">
|
||||
<template slot-scope="props">
|
||||
|
||||
<b-table-column field="upc_pretty" label="UPC" sortable>
|
||||
{{ props.row.upc_pretty }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="brand_name" label="Brand" sortable>
|
||||
{{ props.row.brand_name }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="description" label="Description" sortable>
|
||||
{{ props.row.description }} {{ props.row.size }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="case_size" label="Case" numeric sortable>
|
||||
{{ props.row.case_size }} {{ props.row.uom_display }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="vendor_item_code" label="Vend. Code" sortable>
|
||||
{{ props.row.vendor_item_code }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="preferred" label="Pref." sortable>
|
||||
{{ props.row.preferred ? "X" : "" }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="unit_cost" label="Unit Cost" numeric sortable>
|
||||
{{ props.row.unit_cost_display }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="units_ordered" label="Cases / Units">
|
||||
<b-field grouped>
|
||||
<b-input v-model="props.row.cases_ordered"
|
||||
type="number"
|
||||
size="is-small">
|
||||
</b-input>
|
||||
<b-input v-model="props.row.units_ordered"
|
||||
type="number"
|
||||
size="is-small">
|
||||
</b-input>
|
||||
</b-field>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="po_total" label="PO Total" numeric sortable>
|
||||
{{ props.row.po_total_display }}
|
||||
</b-table-column>
|
||||
|
||||
</template>
|
||||
</b-table>
|
||||
<br />
|
||||
|
||||
</div> <!-- subdept -->
|
||||
</div> <!-- dept -->
|
||||
</div> <!-- if sortedDepartments -->
|
||||
|
||||
<b-loading :active="!sortedDepartments"></b-loading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'OrderingBatchWorksheet',
|
||||
data() {
|
||||
return {
|
||||
inited: false,
|
||||
batch: {},
|
||||
sortedDepartments: null,
|
||||
purchaseHistory: null,
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
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
|
||||
}
|
||||
|
||||
// is someone logged in?
|
||||
if (this.$store.state.user) {
|
||||
|
||||
// go ahead and fetch data, but only if user has permission
|
||||
if (this.$hasPerm('ordering.worksheet')) {
|
||||
this.fetchData()
|
||||
} else { // send others back to "home" page
|
||||
this.router.push('/')
|
||||
}
|
||||
|
||||
} else { // send logged-out users to "login"
|
||||
this.$router.push('/login')
|
||||
}
|
||||
|
||||
// we did it!
|
||||
this.inited = true
|
||||
},
|
||||
|
||||
fetchData(uuid) {
|
||||
if (uuid === undefined) {
|
||||
uuid = this.$route.params.uuid
|
||||
}
|
||||
this.$http.get(`/api/ordering-batch/${uuid}`).then(response => {
|
||||
this.batch = response.data.purchasebatch
|
||||
this.fetchWorksheetData(uuid)
|
||||
}, response => {
|
||||
this.$buefy.toast.open({
|
||||
message: "Failed to fetch batch data!",
|
||||
type: 'is-danger',
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchWorksheetData(uuid) {
|
||||
this.$http.get(`/api/ordering-batch/${uuid}/worksheet`).then(response => {
|
||||
this.sortedDepartments = response.data.sorted_departments
|
||||
this.purchaseHistory = response.data.history
|
||||
}, response => {
|
||||
this.$buefy.toast.open({
|
||||
message: "Failed to fetch worksheet data!",
|
||||
type: 'is-danger',
|
||||
})
|
||||
})
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
40
mobile/src/views/ordering/OrderingBatches.vue
Normal file
40
mobile/src/views/ordering/OrderingBatches.vue
Normal file
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<byjove-model-index model-name="PurchaseBatch"
|
||||
model-index-title="Ordering"
|
||||
model-title="Ordering Batch"
|
||||
model-title-plural="Ordering Batches"
|
||||
model-path-prefix="/ordering"
|
||||
model-permission-prefix="ordering"
|
||||
api-index-url="/api/ordering-batches"
|
||||
:api-index-sort="{field: 'id', dir: 'desc'}"
|
||||
:api-index-filters="filters"
|
||||
:label-renderer="renderLabel">
|
||||
</byjove-model-index>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ByjoveModelIndex} from 'byjove'
|
||||
|
||||
export default {
|
||||
name: 'OrderingBatches',
|
||||
components: {
|
||||
ByjoveModelIndex,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filters: [
|
||||
{field: 'executed', op: 'is_null'},
|
||||
{or: [
|
||||
{field: 'complete', op: 'is_null'},
|
||||
{field: 'complete', op: 'eq', value: false},
|
||||
]},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
renderLabel(batch) {
|
||||
return `(${batch.id_str}) ${batch.vendor_display}`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
11
mobile/src/views/ordering/index.js
Normal file
11
mobile/src/views/ordering/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import OrderingBatches from './OrderingBatches'
|
||||
import OrderingBatch from './OrderingBatch'
|
||||
import OrderingBatchRow from './OrderingBatchRow'
|
||||
import OrderingBatchWorksheet from './OrderingBatchWorksheet'
|
||||
|
||||
export {
|
||||
OrderingBatches,
|
||||
OrderingBatch,
|
||||
OrderingBatchRow,
|
||||
OrderingBatchWorksheet,
|
||||
}
|
11
mobile/vue.config.js.dist
Normal file
11
mobile/vue.config.js.dist
Normal file
|
@ -0,0 +1,11 @@
|
|||
// -*- mode: js; -*-
|
||||
|
||||
module.exports = {
|
||||
publicPath: '/m/',
|
||||
devServer: {
|
||||
host: '127.0.0.1',
|
||||
port: 6647,
|
||||
public: 'theo-demo.example.com',
|
||||
// clientLogLevel: 'debug',
|
||||
}
|
||||
}
|
3
setup.py
3
setup.py
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -163,6 +163,7 @@ setup(
|
|||
|
||||
'paste.app_factory': [
|
||||
'main = theo.web.app:main',
|
||||
'webapi = theo.web.webapi:main',
|
||||
],
|
||||
},
|
||||
)
|
||||
|
|
38
theo/web/api/__init__.py
Normal file
38
theo/web/api/__init__.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Theo Web API
|
||||
"""
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
# core
|
||||
config.include('tailbone.api.common')
|
||||
config.include('tailbone.api.auth')
|
||||
|
||||
# models
|
||||
config.include('tailbone.api.vendors')
|
||||
|
||||
# batches
|
||||
config.include('tailbone.api.batch.ordering')
|
41
theo/web/webapi.py
Normal file
41
theo/web/webapi.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Theo web API app
|
||||
"""
|
||||
|
||||
from tailbone import webapi as base
|
||||
|
||||
|
||||
def main(global_config, **settings):
|
||||
"""
|
||||
This function returns a Pyramid WSGI application.
|
||||
"""
|
||||
rattail_config = base.make_rattail_config(settings)
|
||||
pyramid_config = base.make_pyramid_config(settings)
|
||||
|
||||
# bring in some Theo / Tailbone
|
||||
pyramid_config.include('theo.web.subscribers')
|
||||
pyramid_config.include('theo.web.api')
|
||||
|
||||
return pyramid_config.make_wsgi_app()
|
Loading…
Reference in a new issue