0
1
Fork 0

#4922 Catalog & fixes

This commit is contained in:
Juan Ferrer 2023-01-16 08:32:48 +01:00
parent 0234e14c6b
commit 6458d8db5e
12 changed files with 825 additions and 12 deletions

View File

@ -59,5 +59,27 @@ export default {
'Nov', 'Nov',
'Dec' 'Dec'
] ]
} },
// menu
home: 'Home',
catalog: 'Catalog',
orders: 'Orders',
order: 'Pending order',
ticket: 'Order',
conditions: 'Conditions',
about: 'About us',
admin: 'Administration',
panel: 'Control panel',
users: 'Users',
connections: 'Connections',
visits: 'Visits',
news: 'News',
newEdit: 'Edit new',
images: 'Images',
items: 'Items',
config: 'Configuration',
user: 'User',
addresses: 'Addresses',
addressEdit: 'Edit address'
} }

View File

@ -59,5 +59,27 @@ export default {
'Nov', 'Nov',
'Dic' 'Dic'
] ]
} },
// Menu
home: 'Inicio',
catalog: 'Catálogo',
orders: 'Pedidos',
order: 'Pedido pendiente',
ticket: 'Pedido',
conditions: 'Condiciones',
about: 'Sobre nosotros',
admin: 'Administración',
panel: 'Panel de control',
users: 'Usuarios',
connections: 'Conexiones',
visits: 'Visitas',
news: 'Noticias',
newEdit: 'Editar noticia',
images: 'Imágenes',
items: 'Artículos',
config: 'Configuración',
user: 'Usuario',
addresses: 'Direcciones',
addressEdit: 'Editar dirección'
} }

View File

@ -43,7 +43,11 @@ export class ResultSet {
const result = this.fetch() const result = this.fetch()
if (result !== null) { if (result !== null) {
if (result.data instanceof Array) { return new Result(result) } else { return true } if (result.data instanceof Array) {
return new Result(result)
} else {
return true
}
} }
return null return null
@ -73,7 +77,9 @@ export class ResultSet {
const result = this.fetch() const result = this.fetch()
if (result !== null && if (result !== null &&
result.data instanceof Array) { return result.data } result.data instanceof Array) {
return result.data
}
return null return null
} }
@ -104,7 +110,9 @@ export class ResultSet {
result.data.length > 0) { result.data.length > 0) {
const object = result.data[0] const object = result.data[0]
const row = new Array(result.columns.length) const row = new Array(result.columns.length)
for (let i = 0; i < row.length; i++) { row[i] = object[result.columns[i].name] } for (let i = 0; i < row.length; i++) {
row[i] = object[result.columns[i].name]
}
return row return row
} }

View File

@ -179,8 +179,6 @@ export class JsonConnection extends VnObject {
} }
if (error) { if (error) {
if (error.exception === 'SessionExpired') { this.clearToken() }
this.emit('error', error) this.emit('error', error)
reject(error) reject(error)
} else { resolve(data) } } else { resolve(data) }

View File

@ -10,10 +10,24 @@
aria-label="Menu" aria-label="Menu"
@click="toggleLeftDrawer"/> @click="toggleLeftDrawer"/>
<q-toolbar-title> <q-toolbar-title>
Home {{$app.title}}
<div
v-if="$app.subtitle"
class="subtitle text-caption">
{{$app.subtitle}}
</div>
</q-toolbar-title> </q-toolbar-title>
<div id="actions" ref="actions"> <div id="actions" ref="actions">
</div> </div>
<q-btn
v-if="$app.useRightDrawer"
@click="$app.rightDrawerOpen = !$app.rightDrawerOpen"
aria-label="Menu"
flat
dense
round>
<q-icon name="menu"/>
</q-btn>
</q-toolbar> </q-toolbar>
</q-header> </q-header>
<q-drawer <q-drawer
@ -133,6 +147,10 @@
.q-page-container > * { .q-page-container > * {
padding: 16px; padding: 16px;
} }
#actions > div {
display: flex;
align-items: center;
}
@include mobile { @include mobile {
#actions > div { #actions > div {
.q-btn { .q-btn {

View File

@ -71,4 +71,10 @@ export default {
startOrder: Start order startOrder: Start order
es-ES: es-ES:
startOrder: Empezar pedido startOrder: Empezar pedido
ca-ES:
startOrder: Començar comanda
fr-FR:
startOrder: Lancer commande
pt-PT:
startOrder: Comece uma encomenda
</i18n> </i18n>

View File

@ -0,0 +1,668 @@
<template>
<Teleport :to="$actions">
<q-input
:placeholder="$t('search')"
v-model="search"
debounce="500"
class="search q-mr-sm"
rounded
dark
dense
standout>
<template v-slot:prepend>
<q-icon
v-if="search === ''"
name="search"
/>
<q-icon
v-else
name="clear"
class="cursor-pointer"
@click="search = ''"
/>
</template>
</q-input>
<q-btn
:icon="$t(viewMode == 'list' ? 'view_list' : 'grid_on')"
:label="$t(viewMode == 'list' ? 'listView' : 'gridView')"
@click="onViewModeClick()"
rounded
no-caps/>
</Teleport>
<div style="padding-bottom: 5em;">
<q-drawer
v-model="$app.rightDrawerOpen"
side="right"
:width="250">
<div class="q-pa-md">
<div class="basket-info">
<p>{{date(new Date())}}</p>
<p>
{{$t('warehouse')}}
{{'Algemesi'}}
</p>
<q-btn
flat
rounded
no-caps>
{{$t('modify')}}
</q-btn>
</div>
<div class="q-mt-md">
<div class="q-mb-xs text-grey-7">
{{$t('category')}}
<q-icon
v-if="category"
style="font-size: 1.3em;"
name="cancel"
class="cursor-pointer"
:title="$t('deleteFilter')"
@click="$router.push({params: {category: null}})"
/>
</div>
<div class="categories">
<q-btn
flat
round
class="category q-pa-sm"
v-for="cat in categories"
:class="{active: category == cat.id}"
:key="cat.id"
:title="cat.name"
:to="{params: {category: cat.id, type: null}}">
<img :src="`statics/category/${cat.code}.svg`">
</q-btn>
</div>
</div>
<div class="q-mt-md"
v-if="category || search">
<div class="q-mb-xs text-grey-7">
{{$t('filterBy')}}
</div>
<q-select
v-model="type"
option-value="id"
option-label="name"
:options="types"
:disable="!category"
clearable
:label="$t('family')"
@filter="filterType"
@input="$router.push({params: {type: type && type.id}})"
/>
<q-select
v-model="order"
input-debounce="0"
:options="orderOptions"
:label="$t('orderBy')"
/>
</div>
</div>
<div class="q-pa-md"
v-if="typeId || search">
<div class="q-mb-md"
v-for="tag in tags"
:key="tag.uid">
<div class="q-mb-xs text-caption text-grey-7">
{{tag.name}}
<q-icon
v-if="tag.hasFilter"
style="font-size: 1.3em;"
name="cancel"
:title="$t('deleteFilter')"
class="cursor-pointer"
@click="onResetTagFilterClick(tag)"
/>
</div>
<div v-if="!tag.useRange">
<div
v-for="value in tag.values.slice(0, tag.showCount)"
:key="value">
<q-checkbox
v-model="tag.filter"
:dense="true"
:val="value"
:label="value"
@input="onCheck(tag)"
/>
</div>
<div v-if="tag.values.length > tag.showCount">
<span
class="cursor-pointer text-blue"
@click="tag.showCount = Infinity">
<q-icon name="keyboard_arrow_down" />
{{$t('viewMore')}}
</span>
</div>
<div v-if="tag.showCount == Infinity">
<span
class="cursor-pointer text-blue"
@click="tag.showCount = tag.initialCount">
<q-icon name="keyboard_arrow_up" />
{{$t('viewLess')}}
</span>
</div>
</div>
<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"
:color="tag.hasFilter ? 'primary' : 'grey-6'"
@input="onRangeChange(tag, true)"
@change="onRangeChange(tag)"
label-always
markers
snap
/>
</div>
</div>
</div>
</q-drawer>
<q-infinite-scroll
@load="onLoad"
scroll-taget="html"
:offset="800"
:disable="disableScroll">
<div class="q-pa-md row justify-center q-gutter-md">
<q-spinner
v-if="isLoading"
color="primary"
size="50px">
</q-spinner>
<div
v-if="items && !items.length"
class="text-subtitle1 text-grey-7 q-pa-md">
{{$t('noItemsFound')}}
</div>
<div
v-if="!items && !isLoading"
class="text-subtitle1 text-grey-7 q-pa-md">
{{$t('pleaseSetFilter')}}
</div>
<q-card
class="my-card"
v-for="item in items"
:key="item.id">
<img :src="`${$imageBase}/catalog/200x200/${item.image}`" />
<q-card-section>
<div class="name text-subtitle1">
{{item.longName}}
</div>
<div class="sub-name text-uppercase text-subtitle1 text-grey-7 ellipsize q-pt-xs">
{{item.subName}}
</div>
<div class="tags q-pt-xs">
<div v-for="tag in item.tags" :key="tag.tagFk">
<span class="text-grey-7">{{tag.tag.name}}</span> {{tag.value}}
</div>
</div>
</q-card-section>
<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">{{currency(item.buy?.price3)}}</span>
</div>
<q-btn
icon="add_shopping_cart"
:title="$t('buy')"
@click="showItem(item)"
flat>
</q-btn>
</q-card-actions>
</q-card>
</div>
<template v-slot:loading>
<div class="row justify-center q-my-md">
<q-spinner color="primary" name="dots" size="40px" />
</div>
</template>
</q-infinite-scroll>
<q-dialog v-model="showItemDialog">
<q-card style="width: 25em;">
<q-img
:src="`${$imageBase}/catalog/200x200/${item.image}`"
:ratio="5/3">
<div class="absolute-bottom text-center q-pa-xs">
<div class="text-subtitle1">
{{item.longName}}
</div>
</div>
</q-img>
<q-card-section>
<div class="text-uppercase text-subtitle1 text-grey-7 ellipsize">
{{item.subName}}
</div>
<div class="text-grey-7">
#{{item.id}}
</div>
</q-card-section>
<q-card-section>
<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-actions align="right">
<q-btn
@click="showItemDialog = false"
flat>
{{$t('cancel')}}
</q-btn>
<q-btn
@click="showItemDialog = false"
flat>
{{$t('accept')}}
</q-btn>
</q-card-actions>
</q-card>
</q-dialog>
<q-page-sticky>
<q-btn
fab
to="/ecomerce/basket"
icon="shopping_cart"
color="accent"
:title="$t('shoppingCart')"/>
</q-page-sticky>
</div>
</template>
<style lang="scss" scoped>
.search {
max-width: 250px;
}
.basket-info {
background-color: #8cc63f;
color: white;
padding: 17px 28px;
border-radius: 7px;
text-align: center;
& > p {
margin: 0;
padding: .4em 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.categories {
margin: 0 auto;
width: 220px;
.category {
width: 55px;
&.active {
background: rgba(0, 0, 0, .08);
}
& > img {
height: 40px;
width: 40px;
}
}
}
.tags {
max-height: 4.6em;
overflow: hidden;
}
.available {
padding: .15em;
border-radius: .2em;
font-size: 1.3em;
}
.price {
font-size: 1.3em;
}
.my-card {
width: 100%;
max-width: 17.5em;
height: 32.5em;
overflow: hidden;
.name, .sub-name {
line-height: 1.3em;
}
.ellipsize {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.description {
height: 40px;
overflow: hidden;
}
}
</style>
<script>
import { date, currency } from 'src/lib/filters.js'
import { date as qdate } from 'quasar'
import axios from 'axios'
const CancelToken = axios.CancelToken
export default {
name: 'HederaCatalog',
data () {
return {
uid: 0,
search: '',
orderDate: qdate.formatDate(new Date(), 'YYYY/MM/DD'),
category: null,
categories: [],
type: null,
typeId: null,
types: [],
orgTypes: [],
item: {},
showItemDialog: false,
tags: [],
isLoading: false,
items: null,
limit: null,
pageSize: 30,
maxTags: 5,
disableScroll: true,
viewMode: 'list',
order: {
label: this.$t('relevancy'),
value: 'relevancy DESC, longName'
},
orderOptions: [
{
label: this.$t('relevancy'),
value: 'relevancy DESC, longName'
}, {
label: this.$t('name'),
value: 'longName'
}, {
label: this.$t('siceAsc'),
value: 'size ASC'
}, {
label: this.$t('sizeDesc'),
value: 'size DESC'
}, {
label: this.$t('priceAsc'),
value: 'price ASC'
}, {
label: this.$t('priceDesc'),
value: 'price DESC'
}, {
label: this.$t('available'),
value: 'available'
}
]
}
},
created () {
this.$app.useRightDrawer = true
},
async mounted () {
this.categories = await this.$jApi.query(
`SELECT c.id, l.name, c.color, c.code
FROM vn.itemCategory c
JOIN vn.itemCategoryL10n l ON l.id = c.id
WHERE c.display
ORDER BY display`
)
this.onRouteChange(this.$route)
},
beforeUnmount () {
this.clearTimeoutAndRequest()
},
beforeRouteUpdate (to, from, next) {
this.onRouteChange(to)
next()
},
watch: {
categories () {
this.refreshTitle()
},
orgTypes () {
this.refreshTitle()
},
order () {
this.loadItems()
},
date () {
this.loadItems()
},
async category (value) {
this.orgTypes = []
if (!value) return
const res = await this.$jApi.execQuery(
`CALL myBasket_getAvailable;
SELECT DISTINCT t.id, l.name
FROM vn.item i
JOIN vn.itemType t ON t.id = i.typeFk
JOIN tmp.itemAvailable a ON a.id = i.id
JOIN vn.itemTypeL10n l ON l.id = t.id
WHERE t.\`order\` >= 0
AND t.categoryFk = #category
ORDER BY t.\`order\`, l.name;
DROP TEMPORARY TABLE tmp.itemAvailable;`,
{ category: value }
)
res.fetch()
this.orgTypes = res.fetchData()
},
search (value) {
const location = { params: this.$route.params }
if (value) location.query = { search: value }
this.$router.push(location)
}
},
methods: {
date,
currency,
onViewModeClick () {
this.viewMode = this.viewMode === 'list' ? 'grid' : 'list'
},
onRouteChange (route) {
let { category, type } = route.params
category = parseInt(category) || null
type = parseInt(type) || null
this.category = category
this.typeId = category ? type : null
this.search = route.query.search || ''
this.tags = []
this.refreshTitle()
this.loadItems()
},
refreshTitle () {
let title = this.$t(this.$router.currentRoute.value.name)
let subtitle
if (this.category) {
const category = this.categories.find(i => i.id === this.category) || {}
title = category.name
}
if (this.typeId) {
this.type = this.orgTypes.find(i => i.id === this.typeId)
subtitle = title
title = this.type && this.type.name
} else {
this.type = null
}
this.$app.$patch({ title, subtitle })
},
clearTimeoutAndRequest () {
if (this.timeout) {
clearTimeout(this.timeout)
this.timeout = null
}
if (this.source) {
this.source.cancel()
this.source = null
}
},
loadItemsDelayed () {
this.clearTimeoutAndRequest()
this.timeout = setTimeout(() => this.loadItems(), 500)
},
loadItems () {
this.items = null
this.isLoading = true
this.limit = this.pageSize
this.disableScroll = false
this.isLoading = false
// this.loadItemsBase().finally(() => (this.isLoading = false))
},
onLoad (index, done) {
if (this.isLoading) return done()
this.limit += this.pageSize
done()
// this.loadItemsBase().finally(done)
},
loadItemsBase () {
this.clearTimeoutAndRequest()
if (!(this.category || this.typeId || this.search)) {
this.tags = []
return Promise.resolve(true)
}
const tagFilter = []
for (const tag of this.tags) {
if (tag.hasFilter) {
tagFilter.push({
tagFk: tag.id,
values: tag.filter
})
}
}
this.source = CancelToken.source()
const params = {
dated: this.orderDate,
typeFk: this.typeId,
categoryFk: this.category,
search: this.search,
order: this.order.value,
limit: this.limit,
tagFilter
}
const config = {
params,
cancelToken: this.source.token
}
return this.$axios.get('Items/catalog', config)
.then(res => this.onItemsGet(res))
.catch(err => this.onItemsError(err))
.finally(() => (this.cancel = null))
},
onItemsError (err) {
if (err.__CANCEL__) return
this.disableScroll = true
throw err
},
onItemsGet (res) {
for (const tag of res.data.tags) {
tag.uid = this.uid++
if (tag.filter) {
tag.hasFilter = true
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)) {
tag.initialCount = Math.max(tag.initialCount, tag.filter.length)
}
tag.showCount = tag.initialCount
}
}
this.items = res.data.items
this.tags = res.data.tags
this.disableScroll = this.items.length < this.limit
},
onRangeChange (tag, delay) {
tag.hasFilter = true
if (!delay) this.loadItems()
else this.loadItemsDelayed()
},
onCheck (tag) {
tag.hasFilter = tag.filter.length > 0
this.loadItems()
},
resetTagFilter (tag) {
tag.hasFilter = false
if (tag.useRange) {
tag.filter = {
min: tag.min,
max: tag.max
}
} else {
tag.filter = []
}
},
onResetTagFilterClick (tag) {
this.resetTagFilter(tag)
this.loadItems()
},
filterType (val, update) {
if (val === '') {
update(() => { this.types = this.orgTypes })
} else {
update(() => {
const needle = val.toLowerCase()
this.types = this.orgTypes.filter(type =>
type.name.toLowerCase().indexOf(needle) > -1)
})
}
},
showItem (item) {
this.item = item
this.showItemDialog = true
const conf = this.$state.catalogConfig
const params = {
dated: this.orderDate,
addressFk: conf.addressFk,
agencyModeFk: conf.agencyModeFk
}
this.$axios.get(`Items/${item.id}/calcCatalog`, { params })
.then(res => (this.lots = res.data))
}
}
}
</script>
<i18n lang="yaml">
es-ES:
gridView: Vista de rejilla
listView: Vista de lista
shoppingCart: Cesta de la compra
warehouse: Almacén
agency: Agencia
modify: Modificar
category: Categoría
deleteFilter: Quitar filtro
filterBy: Filtrar por
family: Familia
orderBy: Ordernar por
pleaseSetFilter: Elige un filtro en el menú de la derecha
search: Buscar
</i18n>

View File

@ -144,9 +144,58 @@ export default {
noOrdersFound: No orders found noOrdersFound: No orders found
makePayment: Make payment makePayment: Make payment
shoppingCart: Shopping cart shoppingCart: Shopping cart
balance: 'Balance:'
paymentInfo: >-
The amount shown is your slope (negative) or favorable balance today, it
disregards future orders. For get your order shipped, this amount must be
equal to or greater than 0. If you want to make a down payment, click the
payment button, delete the suggested amount and enter the amount you want.
es-ES: es-ES:
startOrder: Empezar pedido startOrder: Empezar pedido
noOrdersFound: No se encontrado pedidos noOrdersFound: No se encontrado pedidos
makePayment: Realizar pago makePayment: Realizar pago
shoppingCart: Cesta de la compra shoppingCart: Cesta de la compra
balance: 'Saldo:'
paymentInfo: >-
La cantidad mostrada es tu saldo pendiente (negativa) o favorable a día de
hoy, no tiene en cuenta pedidos del futuro. Para que tu pedido sea enviado,
esta cantidad debe ser igual o mayor que 0. Si quieres realizar una entrega a
cuenta, pulsa el botón de pago, borra la cantidad sugerida e introduce la
cantidad que desees.
ca-ES:
startOrder: Començar encàrrec
noOrdersFound: No s'han trobat comandes
makePayment: Realitzar pagament
shoppingCart: Cistella de la compra
balance: 'Saldo:'
paymentInfo: >-
La quantitat mostrada és el teu saldo pendent (negatiu) o favorable a dia
d'avui, no en compte comandes del futur. Perquè la teva comanda sigui
enviat, aquesta quantitat ha de ser igual o més gran que 0. Si vols fer un
lliurament a compte, prem el botó de pagament, esborra la quantitat suggerida
e introdueix la quantitat que vulguis.
fr-FR:
startOrder: Acheter
noOrdersFound: Aucune commande trouvée
makePayment: Effectuer un paiement
shoppingCart: Panier
balance: 'Balance:'
paymentInfo: >-
Le montant indiqué est votre pente (négative) ou balance favorable
aujourd'hui, ne tient pas compte pour les commandes futures. Obtenir votre
commande est expédiée, ce montant doit être égal ou supérieur à 0. Si vous
voulez faire un versement, le montant suggéré effacé et entrez le montant que
vous souhaitez.
pt-PT:
startOrder: Iniciar encomenda
noOrdersFound: Nenhum pedido encontrado
makePayment: Realizar pagamento
shoppingCart: Cesta da compra
balance: 'Saldo:'
paymentInfo: >-
A quantidade mostrada é seu saldo pendente (negativo) ou favorável a dia de
hoje, não se vincula a pedidos futuros. Para que seu pedido seja enviado, esta
quantidade deve ser igual ou superior a 0. Se queres realizar um depósito à
conta, clique no botão de pagamento, apague a quantidade sugerida e introduza
a quantidade que deseje.
</i18n> </i18n>

View File

@ -1,4 +1,5 @@
import { route } from 'quasar/wrappers' import { route } from 'quasar/wrappers'
import { appStore } from 'stores/app'
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router' import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
import routes from './routes' import routes from './routes'
@ -26,5 +27,16 @@ export default route(function (/* { store, ssrContext } */) {
history: createHistory(process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE) history: createHistory(process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE)
}) })
Router.afterEach((to, from) => {
if (from.name === to.name) return
const app = appStore()
app.$patch({
title: window.i18n.t(to.name || 'home'),
subtitle: null,
useRightDrawer: false,
rightDrawerOpen: true
})
})
return Router return Router
}) })

View File

@ -42,6 +42,10 @@ const routes = [
name: 'invoices', name: 'invoices',
path: '/ecomerce/invoices', path: '/ecomerce/invoices',
component: () => import('pages/Ecomerce/Invoices.vue') component: () => import('pages/Ecomerce/Invoices.vue')
}, {
name: 'catalog',
path: '/ecomerce/catalog/:category?/:type?',
component: () => import('pages/Ecomerce/Catalog.vue')
} }
] ]
}, },

View File

@ -3,7 +3,11 @@ import { jApi } from 'boot/axios'
export const appStore = defineStore('hedera', { export const appStore = defineStore('hedera', {
state: () => ({ state: () => ({
imageUrl: '' title: null,
subtitle: null,
imageUrl: '',
useRightDrawer: false,
rightDrawerOpen: false
}), }),
actions: { actions: {

View File

@ -4,8 +4,8 @@ import { api, jApi } from 'boot/axios'
export const userStore = defineStore('user', { export const userStore = defineStore('user', {
state: () => { state: () => {
const token = const token =
localStorage.getItem('vnToken') || sessionStorage.getItem('vnToken') ||
sessionStorage.getItem('vnToken') localStorage.getItem('vnToken')
return { return {
token, token,
@ -38,7 +38,9 @@ export const userStore = defineStore('user', {
async logout () { async logout () {
if (this.token != null) { if (this.token != null) {
await api.post('Accounts/logout') try {
await api.post('Accounts/logout')
} catch (e) {}
localStorage.removeItem('vnToken') localStorage.removeItem('vnToken')
sessionStorage.removeItem('vnToken') sessionStorage.removeItem('vnToken')
} }