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:
Lance Edgar 2021-01-28 21:15:34 -06:00
parent 774221420c
commit 8c738d3ee8
27 changed files with 12577 additions and 1 deletions

3
mobile/.browserslistrc Normal file
View file

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

29
mobile/.gitignore vendored Normal file
View 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
View 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
View file

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

11346
mobile/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

26
mobile/package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

18
mobile/public/index.html Normal file
View 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
View 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>

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

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

View 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

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

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

View 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>&nbsp;
<input v-model="username" />
</div>
<br />
<div>
<label for="password">Password</label>&nbsp;
<input v-model="password" type="password" />
</div>
<br />
<div>
<button type="button" v-on:click="attemptLogin">Login</button>&nbsp;
</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>

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

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

View 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>
&nbsp; 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>

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

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

View file

@ -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
View 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
View 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()