Catalog improvements, global state

This commit is contained in:
Juan Ferrer 2019-06-26 13:19:03 +02:00
parent d4fdddc898
commit 03a7734cf3
17 changed files with 1507 additions and 113 deletions

View File

@ -0,0 +1,31 @@
{
"name": "Buy",
"base": "PersistedModel",
"options": {
"mysql": {
"table": "buy"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier"
},
"itemFk": {
"type": "Number",
"required": true
},
"price3": {
"type": "Number",
"required": true
}
},
"relations": {
"item": {
"type": "belongsTo",
"model": "Item",
"foreignKey": "itemFk"
}
}
}

View File

@ -40,6 +40,11 @@
"model": "Item",
"foreignKey": "itemFk"
},
"buy": {
"type": "belongsTo",
"model": "Buy",
"foreignKey": "tableId"
},
"warehouse": {
"type": "belongsTo",
"model": "Warehouse",

View File

@ -20,6 +20,9 @@
},
"icon": {
"type": "String"
},
"color": {
"type": "String"
}
},
"relations": {

View File

@ -208,27 +208,32 @@ module.exports = Self => {
}
// Obtains items data
/*
let inbounds = await $.Inbound.find({
fields: ['itemFk', 'available', 'dated'],
include: 'item',
where: Object.assign(
{itemFk: {inq: itemIds}},
inboundWhere
)
});
*/
let items = await Self.find({
fields: ['id', 'longName', 'subname', 'image'],
where: {id: {inq: itemIds}},
include: [
{
relation: 'tags',
scope: {include: 'tag'}
scope: {
fields: ['value', 'tagFk'],
where: {priority: {gt: 4}},
order: 'priority',
include: {
relation: 'tag',
scope: {fields: ['name']}
}
}
}, {
relation: 'inbounds',
scope: {
fields: ['available', 'dated'],
where: inboundWhere
fields: ['available', 'dated', 'tableId'],
where: inboundWhere,
order: 'dated DESC',
include: {
relation: 'buy',
scope: {fields: ['id', 'price3']}
},
}
}
],
@ -236,9 +241,19 @@ module.exports = Self => {
order: order
});
for (let item of items)
item.available = sum(item.inbounds(), 'available');
return {items, tags};
};
// Array functions
function sum(array, key) {
if (!Array.isArray(array)) return 0;
return array.reduce((a, c) => a + c[key], 0);
}
function toMap(objects, key) {
let map = new Map();
for (let object of objects)

View File

@ -62,6 +62,9 @@
"AlertLevel": {
"dataSource": "vn"
},
"Buy": {
"dataSource": "vn"
},
"Client": {
"dataSource": "vn"
},

1232
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,8 @@
"scripts": {
"lint": "eslint --ext .js,.vue src",
"test": "echo \"No test specified\" && exit 0",
"dev": "quasar dev"
"dev": "quasar dev",
"back": "nodemon --inspect --watch back back/server/server.js"
},
"dependencies": {
"@quasar/extras": "^1.1.2",
@ -25,6 +26,7 @@
"eslint": "^5.16.0",
"eslint-loader": "^2.1.1",
"eslint-plugin-vue": "^5.0.0",
"nodemon": "^1.19.1",
"strip-ansi": "=3.0.1"
},
"engines": {

View File

@ -6,7 +6,8 @@ module.exports = function (ctx) {
// --> boot files are part of "main.js"
boot: [
'i18n',
'axios'
'axios',
'state'
],
css: [
@ -54,6 +55,7 @@ module.exports = function (ctx) {
'QSelect',
'QSeparator',
'QSpinner',
'QTooltip',
'QImg',
'QPageSticky',
'QPopupProxy'

View File

@ -3,5 +3,4 @@ import axios from 'axios'
export default async ({ Vue }) => {
Vue.prototype.$axios = axios
axios.defaults.baseURL = `//${location.hostname}:3000/api/`
Vue.prototype.$imageBase = '//verdnatura.es/vn-image-data'
}

13
src/boot/state.js Normal file
View File

@ -0,0 +1,13 @@
export default async ({ app, Vue }) => {
Vue.prototype.$imageBase = '//verdnatura.es/vn-image-data'
let state = Vue.observable({
userId: 1437,
userName: null,
title: null,
subtitle: null,
search: null
})
Vue.prototype.$state = state
}

View File

@ -87,9 +87,10 @@ export default {
// Catalog
more: 'More',
buy: 'Buy',
deleteFilter: 'Delete filter',
viewMore: 'View more',
viewLess: 'View less',
availableFromPrice: '{0} desde {1}€',
from: 'from',
deliveryDate: 'Delivery date',
configureOrder: 'Configure order',
categories: 'Categories',

View File

@ -88,9 +88,10 @@ export default {
// Catalog
more: 'Más',
buy: 'Comprar',
deleteFilter: 'Eliminar filtro',
viewMore: 'Ver más',
viewLess: 'Ver menos',
availableFromPrice: '{0} desde {1}€',
from: 'desde',
deliveryDate: 'Fecha de entrega',
configureOrder: 'Configurar pedido',
categories: 'Categorias',

View File

@ -1,7 +1,7 @@
<template>
<q-layout view="lHh LpR fFf" class="bg-grey-3">
<q-header elevated>
<q-toolbar>
<q-toolbar :style="{background: $state.titleColor}">
<q-btn
flat
dense
@ -11,16 +11,37 @@
>
<q-icon name="menu" />
</q-btn>
<q-toolbar-title>
{{$t($router.currentRoute.name)}}
</q-toolbar-title><!--
<q-input dark dense standout v-model="search">
<q-toolbar-title class="title">
{{$state.title}}
<div
v-if="$state.subtitle"
class="subtitle text-caption">
{{$state.subtitle}}
</div>
</q-toolbar-title>
<q-input
v-if="$router.currentRoute.name == 'catalog'"
dark
dense
standout
v-model="$state.search">
<template v-slot:append>
<q-icon v-if="search === ''" name="search" />
<q-icon v-else name="clear" class="cursor-pointer" @click="search = ''" />
<q-icon v-if="$state.search === ''" name="search" />
<q-icon v-else name="clear" class="cursor-pointer" @click="$state.search = ''" />
</template>
</q-input>-->
<q-btn flat class="q-ml-md" :label="$t('login')" :to="{name: 'login'}" />
</q-input>
<q-btn flat
v-if="$router.currentRoute.name == 'catalog'"
class="q-ml-md"
:label="$t('configureOrder')"
:to="{name: 'orders'}"
/>
<q-btn flat
v-if="!$state.userId"
class="q-ml-md"
:label="$t('login')"
:to="{name: 'login'}"
/>
</q-toolbar>
</q-header>
<q-drawer
@ -74,6 +95,19 @@
</q-layout>
</template>
<style lang="stylus" scoped>
.logo
height: 2.5em;
display: block;
.title
line-height 1.2em
.subtitle
line-height 1.2em
.q-toolbar
transition-property background
transition-duration 200ms
</style>
<script>
import { openURL } from 'quasar'
@ -94,10 +128,3 @@ export default {
}
}
</script>
<style scoped>
.logo {
height: 2.5em;
display: block;
}
</style>

View File

@ -6,14 +6,15 @@
side="right"
elevated>
<div class="q-pa-md">
<div style="color: #555;" class="q-mb-xs">
<div class="q-mb-xs text-caption text-grey-7">
{{$t('category')}}
<q-icon
v-if="category"
style="font-size: 1.3em;"
name="cancel"
class="cursor-pointer"
@click="category = null"
:title="$t('deleteFilter')"
:to="{params: {category: null}}"
/>
</div>
<div>
@ -24,7 +25,7 @@
:class="{active: category == cat.id}"
:key="cat.id"
:title="cat.name"
@click="category = cat.id">
:to="{params: {category: cat.id}}">
<img
class="category-img"
:src="`statics/category/${cat.id}.svg`">
@ -43,17 +44,6 @@
</div>
<q-separator />
<div class="q-pa-md">
<q-input
type="search"
clearable
:label="$t('search')"
stack-label
v-model="searchTmp"
@change="search = searchTmp">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
<q-select
v-model="order"
input-debounce="0"
@ -73,11 +63,6 @@
</q-icon>
</template>
</q-input>
<q-btn
color="accent"
style="width: 100%"
:label="$t('configureOrder')"
/>
</div>
<q-separator />
<div class="q-pa-md"
@ -85,12 +70,13 @@
<div class="q-mb-md"
v-for="tag in tags"
:key="tag.uid">
<div class="q-mb-xs text-grey-7">
<div class="q-mb-xs text-caption text-grey-7">
{{tag.name}}
<q-icon
v-if="tag.filter.length || tag.filter.min || tag.filter.max"
style="font-size: 1.3em;"
name="cancel"
:title="$t('deleteFilter')"
class="cursor-pointer"
@click="onResetTagFilterClick(tag)"
/>
@ -104,7 +90,7 @@
:dense="true"
:val="value"
:label="value"
@input="loadItems()"
@input="loadItems"
/>
</div>
<div v-if="tag.values.length > tag.showCount">
@ -124,25 +110,29 @@
</span>
</div>
</div>
<q-range
class="q-mt-lg"
v-if="tag.useRange"
v-model="tag.filter"
:min="tag.min"
:max="tag.max"
:step="tag.step"
@change="loadItems()"
label-always
markers
snap
/>
<div class="q-mx-md">
<q-range
class="q-mt-lg"
v-if="tag.useRange"
v-model="tag.filter"
:min="tag.min"
:max="tag.max"
:step="tag.step"
@input="loadItemsDelayed"
@change="loadItems"
label-always
markers
snap
/>
</div>
</div>
</div>
</q-drawer>
<q-infinite-scroll
@load="onLoad"
scroll-taget="html"
:offset="500">
:offset="800"
:disable="disableScroll">
<div class="q-pa-md row justify-center q-gutter-md">
<q-spinner
v-if="isLoading"
@ -153,24 +143,24 @@
class="my-card"
v-for="item in items"
:key="item.id">
<img :src="imageUrl + item.image" />
<img :src="`${$imageBase}/catalog/200x200/${item.image}`" />
<q-card-section>
<div class="name text-h6">{{item.longName}}</div>
<div class="name text-subtitle1">{{item.longName}}</div>
<div class="text-uppercase text-subtitle1 text-grey-7">
{{item.subName}}
</div>
</q-card-section>
<q-card-section>
<div><span class="text-grey-7">{{item.tag5}}</span> {{item.value5}}</div>
<div><span class="text-grey-7">{{item.tag6}}</span> {{item.value6}}</div>
<div><span class="text-grey-7">{{item.tag7}}</span> {{item.value7}}</div>
<div><span class="text-grey-7">{{item.tag8}}</span> {{item.value8}}</div>
<q-card-section class="tags">
<div v-for="tag in item.tags" :key="tag.tagFk">
<span class="text-grey-7">{{tag.tag.name}}</span> {{tag.value}}
</div>
</q-card-section>
<q-card-section class="text-right text-subtitle1">
{{$t('availableFromPrice', [30, 1.5])}}
</q-card-section>
<q-card-actions class="justify-end">
<q-btn flat>{{$t('more')}}</q-btn>
<q-card-actions class="actions justify-between">
<div class="q-pl-sm">
<span class="available bg-green text-white">{{item.available}}</span>
{{$t('from')}}
<span class="price">{{item.inbounds[0].buy && item.inbounds[0].buy.price3 | currency}}</span>
</div>
<q-btn flat>{{$t('buy')}}</q-btn>
</q-card-actions>
</q-card>
@ -188,6 +178,8 @@
.my-card
width 100%
max-width 21em
height 38em
overflow hidden
.name
white-space nowrap
overflow hidden
@ -201,6 +193,19 @@
background: rgba(0, 0, 0, .08)
.category-img
height 3.5em
.tags
max-height 6.2em
overflow hidden
.available
padding .15em
border-radius .2em
font-size 1.3em
.price
font-size 1.3em
.actions
position absolute
bottom 0
width 100%
</style>
<script>
@ -214,21 +219,21 @@ export default {
data () {
return {
uid: 0,
searchTmp: null,
search: null,
date: date.formatDate(new Date(), 'YYYY/MM/DD'),
category: null,
categories: [],
tags: [],
type: null,
types: [],
orgTypes: [],
isLoading: false,
imageUrl: 'https://verdnatura.es/vn-image-data/catalog/200x200/',
items: null,
rightDrawerOpen: this.$q.platform.is.desktop,
pageSize: 9,
pageSize: 24,
limit: null,
maxTags: 5,
disableScroll: true,
order: {
label: this.$t('name'),
value: 'name'
@ -237,7 +242,7 @@ export default {
{
label: this.$t('name'),
value: 'longName'
}, {
}, /* {
label: this.$t('priceAsc'),
value: 'price ASC'
}, {
@ -246,65 +251,105 @@ export default {
}, {
label: this.$t('available'),
value: 'available'
}, {
}, */ {
label: this.$t('siceAsc'),
value: 'size ASC'
}, {
label: this.$t('sizeDesc'),
value: 'size DESC'
}
],
tags: [],
tagFilter: [],
prevFilters: {}
]
}
},
mounted () {
this.$axios.get('ItemCategories')
.then(res => (this.categories = res.data))
this.loadItems()
},
beforeDestroy () {
this.clearTimeoutAndRequest()
},
watch: {
type () {
this.search = null
this.searchTmp = null
this.loadItems()
},
order () {
this.loadItems()
},
search () {
this.loadItems()
},
date () {
this.loadItems()
},
'$route.params.type': function (type) {
this.type = { id: type }
type (type) {
this.$router.push({
params: {
category: this.category,
type: type && type.id
}
})
this.$state.subtitle = type && type.name
this.$state.search = ''
this.loadItems()
},
category (category) {
'$state.search': function (value) {
if (!value) this.loadItems()
else this.loadItemsDelayed()
},
'$route.params.category': function (categoryId) {
let category = this.categories.find(i => i.id === categoryId)
if (category) {
this.$state.title = category.name
this.$state.titleColor = `#${category.color}`
} else {
this.$state.title = this.$t(this.$router.currentRoute.name)
this.$state.titleColor = null
}
this.category = categoryId
this.type = null
let params = { filter: { where: { categoryFk: category } } }
let params = { filter: { where: { categoryFk: categoryId } } }
this.$axios.get('ItemTypes', { params })
.then(res => (this.orgTypes = res.data))
},
'$route.params.type': function (type) {
if (!this.type) this.type = { id: type }
}
},
methods: {
loadItemsDelayed () {
this.clearTimeoutAndRequest()
this.timeout = setTimeout(() => this.loadItems(), 500)
},
clearTimeoutAndRequest () {
if (this.timeout) {
clearTimeout(this.timeout)
this.timeout = null
}
if (this.source) {
this.source.cancel()
this.source = null
}
},
loadItems () {
this.items = []
this.isLoading = true
this.limit = this.pageSize
this.disableScroll = false
this.loadItemsBase()
.finally(() => (this.isLoading = false))
.finally(() => {
this.isLoading = false
})
},
onLoad (index, done) {
if (this.isLoading) return done()
this.limit += this.pageSize
console.log('onLoad')
this.loadItemsBase()
.finally(() => done())
},
loadItemsBase () {
this.clearTimeoutAndRequest()
if (!this.type) {
return Promise.resolve(true)
}
let typeFk
let tagFilter = []
@ -321,13 +366,12 @@ export default {
typeFk = this.type.id
}
if (this.source) this.source.cancel()
this.source = CancelToken.source()
let params = {
dated: this.date,
typeFk: typeFk,
search: this.search,
search: this.$state.search,
order: this.order.value,
tagFilter: tagFilter,
limit: this.limit
@ -338,18 +382,23 @@ export default {
}
return this.$axios.get('Items/catalog', config)
.then(res => this.onItemsGet(res))
.catch(err => { if (!err.__CANCEL__) throw err })
.finally(() => (this.cancel = null))
},
onItemsGet (res) {
for (let tag of res.data.tags) {
tag.uid = this.uid++
tag.useRange = tag.isQuantitative &&
tag.values.length > this.maxTags &&
(!tag.filter || !tag.filter.length)
if (!tag.filter) {
if (tag.filter) {
tag.useRange =
tag.filter.max ||
tag.filter.min
} else {
tag.useRange = tag.isQuantitative &&
tag.values.length > this.maxTags
this.resetTagFilter(tag)
}
if (tag.values) {
tag.initialCount = this.maxTags
if (Array.isArray(tag.filter)) {
@ -361,6 +410,7 @@ export default {
this.items = res.data.items
this.tags = res.data.tags
this.disableScroll = this.items.length < this.limit
},
resetTagFilter (tag) {
if (tag.useRange) {

View File

@ -87,7 +87,6 @@ export default {
data () {
return {
yearFilter: null,
userId: 1437,
years: [],
orders: [],
tickets: []
@ -96,7 +95,7 @@ export default {
mounted () {
let params = { filter: {
where: {
clientFk: this.userId,
clientFk: this.$state.userId,
isConfirmed: false
},
include: 'address',

View File

@ -22,5 +22,16 @@ export default function (/* { store, ssrContext } */) {
base: process.env.VUE_ROUTER_BASE
})
Router.afterEach((to, from) => {
let app = Router.app
let $state = app.$state
if (from.name !== to.name) {
$state.title = app.$t(to.name)
$state.titleColor = null
$state.subtitle = null
}
})
return Router
}

View File

@ -14,7 +14,7 @@ const routes = [
component: () => import('pages/Index.vue')
}, {
name: 'catalog',
path: '/catalog/:type?',
path: '/catalog/:category?/:type?',
component: () => import('pages/Catalog.vue')
}, {
name: 'orders',