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

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