Address, Config, Orders

This commit is contained in:
Juan Ferrer 2019-07-01 23:47:53 +02:00
parent 03a7734cf3
commit 73a8d045a6
23 changed files with 1299 additions and 353 deletions

View File

@ -0,0 +1,47 @@
{
"name": "Account",
"base": "PersistedModel",
"options": {
"mysql": {
"table": "account.user"
}
},
"properties": {
"id": {
"type": "number",
"required": true
},
"name": {
"type": "string",
"required": true
},
"roleFk": {
"type": "number",
"mysql": {
"columnName": "role"
}
},
"nickname": {
"type": "string"
},
"password": {
"type": "string",
"required": true
},
"active": {
"type": "boolean"
},
"email": {
"type": "string"
},
"lang": {
"type": "string"
},
"created": {
"type": "date"
},
"updated": {
"type": "date"
}
}
}

View File

@ -0,0 +1,32 @@
{
"name": "Country",
"description": "Worldwide countries",
"base": "PersistedModel",
"options": {
"mysql": {
"table": "country"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier"
},
"country": {
"type": "string",
"required": true
},
"code": {
"type": "string"
}
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -13,6 +13,10 @@ module.exports = Self => {
arg: 'typeFk', arg: 'typeFk',
type: 'Number', type: 'Number',
description: 'The item type id' description: 'The item type id'
}, {
arg: 'categoryFk',
type: 'Number',
description: 'The item category id'
}, { }, {
arg: 'search', arg: 'search',
type: 'String', type: 'String',
@ -42,7 +46,7 @@ module.exports = Self => {
} }
}); });
Self.catalog = async (dated, typeFk, search, order, limit, tagFilter) => { Self.catalog = async (dated, typeFk, categoryFk, search, order, limit, tagFilter) => {
let $ = Self.app.models; let $ = Self.app.models;
let itemIds; let itemIds;
@ -63,11 +67,27 @@ module.exports = Self => {
if (/^[0-9]+$/.test(search)) { if (/^[0-9]+$/.test(search)) {
itemIds = [parseInt(search)]; itemIds = [parseInt(search)];
} else { } else {
if (typeFk || search) { let inbounds = await $.Inbound.find({
let where = {}; fields: ['itemFk'],
where: inboundWhere
});
itemIds = toValues(inbounds, 'itemFk');
if (typeFk) if (categoryFk || typeFk || search) {
let where = {
id: {inq: itemIds}
};
if (typeFk) {
where.typeFk = typeFk; where.typeFk = typeFk;
} else if (categoryFk) {
let types = await $.ItemType.find({
fields: ['id'],
where: {categoryFk}
});
where.typeFk = {inq: toValues(types, 'id')};
}
if (search) if (search)
where.longName = {like: `%${search}%`}; where.longName = {like: `%${search}%`};
@ -79,15 +99,6 @@ module.exports = Self => {
let items = await Self.find(filter); let items = await Self.find(filter);
itemIds = items.map(i => i.id); itemIds = items.map(i => i.id);
} }
let where = Object.assign({}, inboundWhere);
if (itemIds) where.itemFk = {inq: itemIds};
let inbounds = await $.Inbound.find({
fields: ['itemFk'],
where
});
itemIds = toValues(inbounds, 'itemFk');
} }
// Applies tag filters // Applies tag filters
@ -129,89 +140,95 @@ module.exports = Self => {
// Obtains distinct tags and it's distinct values // Obtains distinct tags and it's distinct values
let tagValues = await $.ItemTag.find({ let tags = [];
fields: ['tagFk', 'value', 'intValue', 'priority'],
where: {
itemFk: {inq: itemIds},
tagFk: {nin: tagFilterIds}
},
order: 'tagFk, value'
});
let tagValueMap = toMultiMap(tagValues, 'tagFk');
for (let i = 0; i < tagItems.length; i++) {
let tagFk = tagFilter[i].tagFk;
let itemIds;
if (tagItems.length > 1) {
let siblings = tagItems.filter(v => v != tagItems[i]);
itemIds = intersect(siblings);
} else
itemIds = baseItemIds;
if (typeFk || search) {
let tagValues = await $.ItemTag.find({ let tagValues = await $.ItemTag.find({
fields: ['value', 'intValue', 'priority'], fields: ['tagFk', 'value', 'intValue', 'priority'],
where: { where: {
itemFk: {inq: itemIds}, itemFk: {inq: itemIds},
tagFk: tagFk tagFk: {nin: tagFilterIds}
}, },
order: 'value' order: 'tagFk, value'
});
let tagValueMap = toMultiMap(tagValues, 'tagFk');
for (let i = 0; i < tagItems.length; i++) {
let tagFk = tagFilter[i].tagFk;
let itemIds;
if (tagItems.length > 1) {
let siblings = tagItems.filter(v => v != tagItems[i]);
itemIds = intersect(siblings);
} else
itemIds = baseItemIds;
let tagValues = await $.ItemTag.find({
fields: ['value', 'intValue', 'priority'],
where: {
itemFk: {inq: itemIds},
tagFk: tagFk
},
order: 'value'
});
tagValueMap.set(tagFk, tagValues);
}
let tagIds = [...tagValueMap.keys()];
tags = await $.Tag.find({
fields: ['id', 'name', 'isQuantitative', 'unit'],
where: {
id: {inq: tagIds}
}
}); });
tagValueMap.set(tagFk, tagValues); for (let tag of tags) {
} let tagValues = tagValueMap.get(tag.id);
let tagIds = [...tagValueMap.keys()]; let filter = tagFilter && tagFilter.find(i => i.tagFk == tag.id);
let tags = await $.Tag.find({ filter = filter && filter.values;
fields: ['id', 'name', 'isQuantitative', 'unit'],
where: {
id: {inq: tagIds}
}
});
for (let tag of tags) { let values = toSet(tagValues, 'value');
let tagValues = tagValueMap.get(tag.id); if (Array.isArray(filter))
values = new Set([...filter, ...values]);
let filter = tagFilter && tagFilter.find(i => i.tagFk == tag.id); if (tag.isQuantitative) {
filter = filter && filter.values; let intValues = toValues(tagValues, 'intValue');
let values = toSet(tagValues, 'value'); if (filter) {
if (Array.isArray(filter)) if (filter.min) intValues.push(filter.min);
values = new Set([...filter, ...values]); if (filter.max) intValues.push(filter.max);
}
if (tag.isQuantitative) { let min = Math.min(...intValues);
let intValues = toValues(tagValues, 'intValue'); let max = Math.max(...intValues);
let dif = max - min;
let digits = new String(dif).length;
let step = Math.pow(10, digits - 1);
if (digits > 1 && step * 5 > dif) step /= 10;
if (filter) { Object.assign(tag, {
if (filter.min) intValues.push(filter.min); step,
if (filter.max) intValues.push(filter.max); min: Math.floor(min / step) * step,
max: Math.ceil(max / step) * step
});
} }
let min = Math.min(...intValues);
let max = Math.max(...intValues);
let dif = max - min;
let digits = new String(dif).length;
let step = Math.pow(10, digits - 1);
if (digits > 1 && step * 5 > dif) step /= 10;
Object.assign(tag, { Object.assign(tag, {
step, values: [...values],
min: Math.floor(min / step) * step, filter
max: Math.ceil(max / step) * step
}); });
} }
Object.assign(tag, {
values: [...values],
filter
});
} }
// Obtains items data // Obtains items data
let items = await Self.find({ let items = await Self.find({
fields: ['id', 'longName', 'subname', 'image'], fields: ['id', 'longName', 'subName', 'image'],
where: {id: {inq: itemIds}}, where: {id: {inq: itemIds}},
limit: limit,
order: order,
include: [ include: [
{ {
relation: 'tags', relation: 'tags',
@ -236,13 +253,14 @@ module.exports = Self => {
}, },
} }
} }
], ]
limit: limit,
order: order
}); });
for (let item of items) for (let item of items) {
item.inbound = item.inbounds()[0];
item.buy = item.inbound && item.inbound.buy();
item.available = sum(item.inbounds(), 'available'); item.available = sum(item.inbounds(), 'available');
}
return {items, tags}; return {items, tags};
}; };

View File

@ -0,0 +1,28 @@
{
"name": "Language",
"base": "PersistedModel",
"options": {
"mysql": {
"table": "hedera.language"
}
},
"properties": {
"code": {
"type": "string",
"required": true,
"id": true
},
"name": {
"type": "string",
"required": true
},
"orgName": {
"type": "string",
"required": true
},
"isActive": {
"type": "boolean",
"required": true
}
}
}

View File

@ -0,0 +1,37 @@
{
"name": "UserPassword",
"description": "User password requirements",
"base": "PersistedModel",
"options": {
"mysql": {
"table": "account.userPassword"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier"
},
"length": {
"type": "Number",
"required": true
},
"nAlpha": {
"type": "Number",
"required": true
},
"nUpper": {
"type": "Number",
"required": true
},
"nDigits": {
"type": "Number",
"required": true
},
"nPunct": {
"type": "Number",
"required": true
}
}
}

View File

@ -53,6 +53,9 @@
} }
} }
}, },
"Account": {
"dataSource": "vn"
},
"Address": { "Address": {
"dataSource": "vn" "dataSource": "vn"
}, },
@ -68,6 +71,9 @@
"Client": { "Client": {
"dataSource": "vn" "dataSource": "vn"
}, },
"Country": {
"dataSource": "vn"
},
"DeliveryMethod": { "DeliveryMethod": {
"dataSource": "vn" "dataSource": "vn"
}, },
@ -86,6 +92,9 @@
"ItemType": { "ItemType": {
"dataSource": "vn" "dataSource": "vn"
}, },
"Language": {
"dataSource": "vn"
},
"New": { "New": {
"dataSource": "vn" "dataSource": "vn"
}, },
@ -119,6 +128,9 @@
"TicketTracking": { "TicketTracking": {
"dataSource": "vn" "dataSource": "vn"
}, },
"UserPassword": {
"dataSource": "vn"
},
"Warehouse": { "Warehouse": {
"dataSource": "vn" "dataSource": "vn"
} }

View File

@ -27,42 +27,51 @@ module.exports = function (ctx) {
framework: { framework: {
components: [ components: [
'QBadge', 'QBadge',
'QLayout',
'QHeader',
'QDate',
'QDrawer',
'QPageContainer',
'QPage',
'QToolbar',
'QToolbarTitle',
'QBtn', 'QBtn',
'QIcon',
'QList',
'QInfiniteScroll',
'QItem',
'QItemSection',
'QItemLabel',
'QCarousel', 'QCarousel',
'QCarouselControl', 'QCarouselControl',
'QCarouselSlide', 'QCarouselSlide',
'QCard', 'QCard',
'QCardSection', 'QCardSection',
'QCardActions', 'QCardActions',
'QRating',
'QCheckbox', 'QCheckbox',
'QDate',
'QDialog',
'QDrawer',
'QExpansionItem',
'QHeader',
'QIcon',
'QImg',
'QInfiniteScroll',
'QInput', 'QInput',
'QItem',
'QItemSection',
'QItemLabel',
'QList',
'QLayout',
'QPageContainer',
'QPage',
'QPageSticky',
'QPopupProxy',
'QRadio',
'QRating',
'QRange', 'QRange',
'QSelect', 'QSelect',
'QSeparator', 'QSeparator',
'QSpinner', 'QSpinner',
'QTooltip', 'QTab',
'QImg', 'QTabs',
'QPageSticky', 'QTabPanel',
'QPopupProxy' 'QTabPanels',
'QRouteTab',
'QToolbar',
'QToolbarTitle',
'QTooltip'
], ],
directives: [ directives: [
'Ripple' 'Ripple',
'ClosePopup'
], ],
// Quasar plugins // Quasar plugins

View File

@ -7,7 +7,8 @@ export default async ({ app, Vue }) => {
userName: null, userName: null,
title: null, title: null,
subtitle: null, subtitle: null,
search: null search: null,
rightDrawerOpen: true
}) })
Vue.prototype.$state = state Vue.prototype.$state = state
} }

View File

@ -1 +1,10 @@
// app global css // app global css
.vn-w-md
width 30em
.vn-pp
padding 1em
@media (max-width: 960px)
.vn-pp
padding 1em 0

View File

@ -5,6 +5,16 @@ export default {
password: 'Password', password: 'Password',
remember: 'Don not close session', remember: 'Don not close session',
search: 'Search', search: 'Search',
accept: 'Accept',
cancel: 'Cancel',
save: 'Save',
delete: 'Delete',
add: 'Add',
create: 'create',
dataSaved: 'Data saved!',
noDataFound: 'No data found',
areYouSureDelete: 'Are you sure you want to delete?',
fieldIsRequired: 'Field is required',
date: { date: {
days: [ days: [
'Sunday', 'Sunday',
@ -64,9 +74,14 @@ export default {
orders: 'Orders', orders: 'Orders',
conditions: 'Conditions', conditions: 'Conditions',
about: 'About us', about: 'About us',
config: 'Configuration',
user: 'User',
addresses: 'Addresses',
addressEdit: 'Edit address',
register: 'Register', register: 'Register',
// Home // Home
recentNews: 'Recent news',
startOrder: 'Start order', startOrder: 'Start order',
weThinkForYou: 'We think for you', weThinkForYou: 'We think for you',
@ -86,6 +101,8 @@ export default {
// Catalog // Catalog
more: 'More', more: 'More',
noItemsFound: 'No items found',
pleaseSetFilter: 'Please, set a filter using the right menu',
buy: 'Buy', buy: 'Buy',
deleteFilter: 'Delete filter', deleteFilter: 'Delete filter',
viewMore: 'View more', viewMore: 'View more',
@ -101,6 +118,7 @@ export default {
relevancy: 'Relevancy', relevancy: 'Relevancy',
priceAsc: 'Acending price', priceAsc: 'Acending price',
priceDesc: 'Descending price', priceDesc: 'Descending price',
novelty: 'Novelty',
available: 'Available', available: 'Available',
siceAsc: 'Ascending size', siceAsc: 'Ascending size',
sizeDesc: 'Descencing size', sizeDesc: 'Descencing size',
@ -109,6 +127,7 @@ export default {
ordersMadeAt: 'Orders made at', ordersMadeAt: 'Orders made at',
pendingConfirmtion: 'Pending confirmation', pendingConfirmtion: 'Pending confirmation',
noOrdersFound: 'No orders found', noOrdersFound: 'No orders found',
pending: 'Pending',
confirmed: 'Confirmed', confirmed: 'Confirmed',
packages: '{n} packages', packages: '{n} packages',
@ -118,10 +137,45 @@ export default {
// About // About
aboutDesc: 'Verdnatura offers all services for your florist.', aboutDesc: 'Verdnatura offers all services for your florist.',
// Config
userName: 'Username',
nickname: 'Nickname',
language: 'Language',
receiveInvoiceByEmail: 'Receive invoices by email',
passwordRequirements: 'Password requirements',
charsLong: '{0} characters long',
alphabeticChars: '{0} alphabetic chars',
upperLetters: '{0} uppercase letters',
digits: '{0} digits',
simbols: '{0} symbols. Ex: $%&.',
oldPassword: 'Old password',
newPassword: 'New password',
repeatPassword: 'Repeat password',
changePassword: 'Change password',
passwordCannotBeEmpty: 'Password cannot be empty',
passwordsDontMatch: 'Passwords do not match',
passwordChanged: 'Password changed successfully!',
// Addresses
setAsDefault: 'Set as default',
addressSetAsDefault: 'Address set as default',
addressRemoved: 'Address removed',
areYouSureDeleteAddress: 'Are you sure you want to delete the address?',
addressCreated: 'Address created successfully!',
// Address
consignatary: 'Consignatary',
street: 'Street',
city: 'City',
postalCode: 'Postal code',
province: 'Province',
country: 'Country',
phone: 'Phone',
mobile: 'Mobile',
// Register // Register
registerAsNew: 'Registrarse como nuevo usuario', registerAsNew: 'Registrarse como nuevo usuario',
notYetUser: 'You are not yet a user, register now and start enjoying everything that Verdnatura offers you.', notYetUser: 'You are not yet a user, register now and start enjoying everything that Verdnatura offers you.',
repeatPassword: 'Repeat password',
receiveOffers: 'Receive offers and promotions by e-mail', receiveOffers: 'Receive offers and promotions by e-mail',
userRegistered: 'User registered successfully' userRegistered: 'User registered successfully'
} }

View File

@ -5,6 +5,16 @@ export default {
password: 'Contraseña', password: 'Contraseña',
remember: 'No cerrar sesión', remember: 'No cerrar sesión',
search: 'Buscar', search: 'Buscar',
accept: 'Aceptar',
cancel: 'Cancelar',
save: 'Guardar',
delete: 'Borrar',
add: 'Añadir',
create: 'Crear',
dataSaved: '¡Datos guardados!',
noDataFound: 'No se han encontrado datos',
areYouSureDelete: '¿Seguro que quieres eliminar?',
fieldIsRequired: 'Campo requerido',
date: { date: {
days: [ days: [
'Domingo', 'Domingo',
@ -65,9 +75,14 @@ export default {
orders: 'Pedidos', orders: 'Pedidos',
conditions: 'Condiciones', conditions: 'Condiciones',
about: 'Sobre nosotros', about: 'Sobre nosotros',
config: 'Configuración',
user: 'Usuario',
addresses: 'Direcciones',
addressEdit: 'Editar dirección',
register: 'Registrarse', register: 'Registrarse',
// Home // Home
recentNews: 'Noticias recientes',
startOrder: 'Empezar pedido', startOrder: 'Empezar pedido',
weThinkForYou: 'Pensamos para tí', weThinkForYou: 'Pensamos para tí',
@ -87,6 +102,8 @@ export default {
// Catalog // Catalog
more: 'Más', more: 'Más',
noItemsFound: 'No se han encontrado artículos',
pleaseSetFilter: 'Por favor, establece un filtro usando el menú de la derecha',
buy: 'Comprar', buy: 'Comprar',
deleteFilter: 'Eliminar filtro', deleteFilter: 'Eliminar filtro',
viewMore: 'Ver más', viewMore: 'Ver más',
@ -102,6 +119,7 @@ export default {
relevancy: 'Relevancia', relevancy: 'Relevancia',
priceAsc: 'Precio ascendente', priceAsc: 'Precio ascendente',
priceDesc: 'Precio descendente', priceDesc: 'Precio descendente',
novelty: 'Novedad',
available: 'Disponible', available: 'Disponible',
siceAsc: 'Medida ascendente', siceAsc: 'Medida ascendente',
sizeDesc: 'Medida descendente', sizeDesc: 'Medida descendente',
@ -110,6 +128,7 @@ export default {
ordersMadeAt: 'Pedidos realizados en', ordersMadeAt: 'Pedidos realizados en',
pendingConfirmtion: 'Pendientes de confirmar', pendingConfirmtion: 'Pendientes de confirmar',
noOrdersFound: 'No se han encontrado pedidos', noOrdersFound: 'No se han encontrado pedidos',
pending: 'Pendientes',
confirmed: 'Confirmados', confirmed: 'Confirmados',
packages: '{n} bultos', packages: '{n} bultos',
@ -119,10 +138,45 @@ export default {
// About // About
aboutDesc: 'Verdnatura te ofrece todos los servicios que necesita tu floristería.', aboutDesc: 'Verdnatura te ofrece todos los servicios que necesita tu floristería.',
// Config
userName: 'Nombre de usuario',
nickname: 'Nombre a mostrar',
language: 'Idioma',
receiveInvoiceByEmail: 'Recibir facturas por correo eletrónico',
passwordRequirements: 'Requisitos de contraseña',
charsLong: '{0} carácteres de longitud',
alphabeticChars: '{0} carácteres alfabéticos',
upperLetters: '{0} letras mayúsculas',
digits: '{0} dígitos',
simbols: '{0} símbolos. Ej: $%&.',
oldPassword: 'Antigua contraseña',
newPassword: 'Nueva contraseña',
repeatPassword: 'Repetir contraseña',
changePassword: 'Cambiar contraseña',
passwordCannotBeEmpty: 'La contraseña no puede estar vacía',
passwordsDontMatch: 'Las contraseñas no coinciden',
passwordChanged: '¡Contraseña modificada correctamente!',
// Addresses
setAsDefault: 'Establecer como predeterminada',
addressSetAsDefault: 'Dirección establecida como predeterminada',
addressRemoved: 'Dirección eliminada',
areYouSureDeleteAddress: '¿Seguro que quieres eliminar la dirección?',
addressCreated: '¡Dirección creada correctamente!',
// Address
consignatary: 'Consignatario',
street: 'Dirección',
city: 'City',
postalCode: 'Código postal',
province: 'Provincia',
country: 'País',
phone: 'Teléfono',
mobile: 'Móvil',
// Register // Register
registerAsNew: 'Registrarse como nuevo usuario', registerAsNew: 'Registrarse como nuevo usuario',
notYetUser: '¿Todavía no eres usuari@?, registrate y empieza a disfrutar de todo lo que Verdnatura puede ofrecerte.', notYetUser: '¿Todavía no eres usuari@?, registrate y empieza a disfrutar de todo lo que Verdnatura puede ofrecerte.',
repeatPassword: 'Repetir contraseña',
receiveOffers: 'Recibir ofertas y promociones por correo electrónico', receiveOffers: 'Recibir ofertas y promociones por correo electrónico',
userRegistered: 'Usuario registrado correctamente' userRegistered: 'Usuario registrado correctamente'
} }

View File

@ -20,6 +20,7 @@
<q-checkbox v-model="remember" :label="$t('remember')" /> <q-checkbox v-model="remember" :label="$t('remember')" />
</q-card-section> </q-card-section>
<q-card-actions class="justify-center"> <q-card-actions class="justify-center">
<q-btn flat :label="$t('register')" to="/register" />
<q-btn flat :label="$t('enter')" @click="login()" /> <q-btn flat :label="$t('enter')" @click="login()" />
</q-card-actions> </q-card-actions>
</q-card> </q-card>

View File

@ -19,36 +19,50 @@
{{$state.subtitle}} {{$state.subtitle}}
</div> </div>
</q-toolbar-title> </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="$state.search === ''" name="search" />
<q-icon v-else name="clear" class="cursor-pointer" @click="$state.search = ''" />
</template>
</q-input>
<q-btn flat
v-if="$router.currentRoute.name == 'catalog'"
class="q-ml-md"
:label="$t('configureOrder')"
:to="{name: 'orders'}"
/>
<q-btn flat <q-btn flat
v-if="!$state.userId" v-if="!$state.userId"
class="q-ml-md" class="q-ml-md"
:label="$t('login')" :label="$t('login')"
:to="{name: 'login'}" to="/login"
/> />
<q-input
v-if="$router.currentRoute.name == 'catalog'"
v-model="$state.search"
debounce="500"
class="q-mr-sm"
dark
dense
standout>
<template v-slot:append>
<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
v-if="$router.currentRoute.name == 'catalog'"
@click="$state.rightDrawerOpen = !$state.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
v-model="leftDrawerOpen" v-model="leftDrawerOpen"
content-class="bg-grey-2" content-class="bg-grey-2"
behavior="mobile"
elevated elevated
> overlay>
<div class="q-pa-md shadow-1 q-mb-md bg-grey-10" style="color: white;"> <div class="q-pa-md shadow-1 q-mb-md bg-grey-10" style="color: white;">
<img src="statics/logo-dark.svg" alt="Verdnatura" class="logo q-mb-md"/> <img src="statics/logo-dark.svg" alt="Verdnatura" class="logo q-mb-md"/>
<div class="row items-center full-width justify-between"> <div class="row items-center full-width justify-between">
@ -82,11 +96,21 @@
<q-item-label>{{$t('about')}}</q-item-label> <q-item-label>{{$t('about')}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item clickable :to="{name: 'register'}"> <q-expansion-item
<q-item-section> expand-icon-toggle
<q-item-label>{{$t('register')}}</q-item-label> expand-separator
</q-item-section> :label="$t('config')">
</q-item> <q-item clickable :to="{name: 'config'}">
<q-item-section>
<q-item-label>{{$t('user')}}</q-item-label>
</q-item-section>
</q-item>
<q-item clickable :to="{name: 'addresses'}">
<q-item-section>
<q-item-label>{{$t('addresses')}}</q-item-label>
</q-item-section>
</q-item>
</q-expansion-item>
</q-list> </q-list>
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>
@ -123,7 +147,7 @@ export default {
openURL, openURL,
logout () { logout () {
localStorage.removeItem('token') localStorage.removeItem('token')
this.$router.push('login') this.$router.push('/login')
} }
} }
} }

171
src/pages/Address.vue Normal file
View File

@ -0,0 +1,171 @@
<template>
<div class="vn-pp row justify-center">
<q-card class="vn-w-md">
<q-card-section class="q-gutter-md">
<q-input v-model="address.nickname" :label="$t('consignatary')" />
</q-card-section>
<q-card-section class="q-gutter-md">
<q-input v-model="address.street" :label="$t('street')" />
</q-card-section>
<q-card-section class="q-gutter-md">
<q-input v-model="address.city" :label="$t('city')" />
</q-card-section>
<q-card-section class="q-gutter-md">
<q-input v-model="address.postalCode" :label="$t('postalCode')" />
</q-card-section>
<q-card-section class="q-gutter-md">
<q-select
v-model="country"
:label="$t('country')"
:options="countries"
option-value="id"
option-label="country"/>
</q-card-section>
<q-card-section class="q-gutter-md">
<q-select
v-model="province"
:label="$t('province')"
:options="provinces"
option-value="id"
option-label="name"/>
</q-card-section>
<q-card-section class="q-gutter-md">
<q-input v-model="address.phone" :label="$t('phone')" />
</q-card-section>
<q-card-actions class="justify-center">
<q-btn
:label="$t('cancel')"
to="/addresses"
flat/>
<q-btn
v-if="addressId"
:label="$t('save')"
@click="onSave"
flat/>
<q-btn
v-if="!addressId"
:label="$t('create')"
@click="onSave"
flat/>
</q-card-actions>
</q-card>
</div>
</template>
<style lang="stylus" scoped>
</style>
<script>
export default {
name: 'Address',
data () {
return {
address: {},
addressId: null,
provinces: null,
countries: null,
province: null,
country: null
}
},
mounted () {
let filter = {
fields: ['id', 'country']
}
this.$axios.get('Countries', { params: { filter } })
.then(res => (this.countries = res.data))
this.loadAddress()
},
watch: {
address () {
this.updateCountry()
},
countries () {
this.updateCountry()
},
provinces () {
this.updateProvince()
},
country () {
this.loadProvinces()
},
province (value) {
if (!this.address.provinceFk) return
this.address.provinceFk = value && value.id
},
'this.$route.params.address': function () {
this.loadAddress()
}
},
methods: {
loadAddress () {
this.addressId = this.$route.params.address
this.address = {}
if (this.addressId) {
let filter = {
fields: [
'id',
'nickname',
'street',
'city',
'postalCode',
'provinceFk',
'phone'
],
include: {
relation: 'province',
scope: {
fields: ['countryFk']
}
}
}
this.$axios.get(`Addresses/${this.addressId}`, { params: { filter } })
.then(res => (this.address = res.data))
} else {
this.address = {
clientFk: this.$state.userId
}
}
},
loadProvinces () {
this.provinces = null
if (!this.country) return
let filter = {
fields: ['id', 'name'],
where: { countryFk: this.country.id }
}
this.$axios.get('Provinces', { params: { filter } })
.then(res => (this.provinces = res.data))
},
updateCountry () {
if (!this.countries || !this.address.province) return
this.country = this.countries.find(
i => i.id === this.address.province.countryFk)
},
updateProvince () {
if (!this.provinces) return
let province = this.provinces.find(
i => i.id === this.address.provinceFk)
if (province) this.province = province
},
onSave () {
if (this.addressId) {
this.$axios.patch(`Addresses/${this.addressId}`, this.address)
.then(res => {
this.$q.notify(this.$t('dataSaved'))
this.$router.push('/addresses')
})
} else {
this.$axios.post(`Addresses`, this.address)
.then(res => {
this.$q.notify(this.$t('addressCreated'))
this.$router.push('/addresses')
})
}
}
}
}
</script>

141
src/pages/Addresses.vue Normal file
View File

@ -0,0 +1,141 @@
<template>
<div class="vn-pp row justify-center">
<q-card class="vn-w-md">
<q-list bordered separator>
<q-item v-if="addresses && !addresses.length">
{{$t('noDataFound')}}
</q-item>
<q-item
v-for="(address, index) in addresses"
:key="address.id"
:to="`/address/${address.id}`"
clickable
v-ripple>
<q-item-section side top>
<q-radio
v-model="defaultAddress"
:val="address.id"
:title="$t('setAsDefault')"/>
</q-item-section>
<q-item-section>
<q-item-label class="ellipsis">
{{address.nickname}}
</q-item-label>
<q-item-label class="ellipsis" caption>
{{address.street}}
</q-item-label>
<q-item-label class="ellipsis" caption>
{{address.postalCode}} {{address.city}}
</q-item-label>
</q-item-section>
<q-item-section side top>
<q-btn
icon="delete"
@click="deleteAddress(index, $event)"
:title="$t('delete')"
size="12px"
flat
dense
round/>
</q-item-section>
</q-item>
</q-list>
</q-card>
<q-dialog v-model="confirm" persistent>
<q-card>
<q-card-section class="row items-center">
{{$t('areYouSureDeleteAddress')}}
</q-card-section>
<q-card-actions align="right">
<q-btn
:label="$t('cancel')"
v-close-popup
flat
color="primary"/>
<q-btn
:label="$t('accept')"
@click="confirmDeletion(deleteIndex)"
v-close-popup
flat
color="primary"/>
</q-card-actions>
</q-card>
</q-dialog>
<q-page-sticky position="bottom-right" :offset="[18, 18]">
<q-btn
fab
icon="add"
color="accent"
to="/address"
:title="$t('add')"
/>
</q-page-sticky>
</div>
</template>
<style lang="stylus" scoped>
</style>
<script>
export default {
name: 'Addresses',
data () {
return {
addresses: null,
defaultAddress: null,
confirm: false
}
},
mounted () {
let filter = {
fields: [
'id',
'nickname',
'street',
'city',
'postalCode'
],
where: {
clientFk: this.$state.userId,
isActive: true
},
order: 'nickname'
}
this.$axios.get('Addresses', { params: { filter } })
.then(res => (this.addresses = res.data))
filter = {
fields: ['defaultAddressFk']
}
this.$axios.get(`Clients/${this.$state.userId}`, { params: { filter } })
.then(res => (this.defaultAddress = res.data.defaultAddressFk))
},
watch: {
defaultAddress (value, oldValue) {
if (!oldValue) return
let data = { defaultAddressFk: value }
this.$axios.patch(`Clients/${this.$state.userId}`, data)
.then(res => {
this.$q.notify(this.$t('addressSetAsDefault'))
})
}
},
methods: {
deleteAddress (index, event) {
event.preventDefault()
event.stopPropagation()
this.confirm = true
this.deleteIndex = index
},
confirmDeletion (index) {
let address = this.addresses[index]
let data = { isActive: false }
this.$axios.patch(`Addresses/${address.id}`, data)
.then(res => {
this.addresses.splice(index, 1)
this.$q.notify(this.$t('addressRemoved'))
})
}
}
}
</script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div style="padding-bottom: 5em;">
<q-drawer <q-drawer
v-model="rightDrawerOpen" v-model="$state.rightDrawerOpen"
content-class="bg-grey-2" content-class="bg-grey-2"
side="right" side="right"
elevated> elevated>
@ -14,7 +14,7 @@
name="cancel" name="cancel"
class="cursor-pointer" class="cursor-pointer"
:title="$t('deleteFilter')" :title="$t('deleteFilter')"
:to="{params: {category: null}}" @click="$router.push({params: {category: null}})"
/> />
</div> </div>
<div> <div>
@ -25,7 +25,7 @@
:class="{active: category == cat.id}" :class="{active: category == cat.id}"
:key="cat.id" :key="cat.id"
:title="cat.name" :title="cat.name"
:to="{params: {category: cat.id}}"> :to="{params: {category: cat.id, type: null}}">
<img <img
class="category-img" class="category-img"
:src="`statics/category/${cat.id}.svg`"> :src="`statics/category/${cat.id}.svg`">
@ -40,6 +40,7 @@
clearable clearable
:label="$t('family')" :label="$t('family')"
@filter="filterType" @filter="filterType"
@input="$router.push({params: {type: type && type.id}})"
/> />
</div> </div>
<q-separator /> <q-separator />
@ -66,14 +67,14 @@
</div> </div>
<q-separator /> <q-separator />
<div class="q-pa-md" <div class="q-pa-md"
v-if="type || search"> v-if="typeId || search">
<div class="q-mb-md" <div class="q-mb-md"
v-for="tag in tags" v-for="tag in tags"
:key="tag.uid"> :key="tag.uid">
<div class="q-mb-xs text-caption text-grey-7"> <div class="q-mb-xs text-caption text-grey-7">
{{tag.name}} {{tag.name}}
<q-icon <q-icon
v-if="tag.filter.length || tag.filter.min || tag.filter.max" v-if="tag.hasFilter"
style="font-size: 1.3em;" style="font-size: 1.3em;"
name="cancel" name="cancel"
:title="$t('deleteFilter')" :title="$t('deleteFilter')"
@ -90,7 +91,7 @@
:dense="true" :dense="true"
:val="value" :val="value"
:label="value" :label="value"
@input="loadItems" @input="onCheck(tag)"
/> />
</div> </div>
<div v-if="tag.values.length > tag.showCount"> <div v-if="tag.values.length > tag.showCount">
@ -118,8 +119,9 @@
:min="tag.min" :min="tag.min"
:max="tag.max" :max="tag.max"
:step="tag.step" :step="tag.step"
@input="loadItemsDelayed" :color="tag.hasFilter ? 'primary' : 'grey-6'"
@change="loadItems" @input="onRangeChange(tag, true)"
@change="onRangeChange(tag)"
label-always label-always
markers markers
snap snap
@ -139,29 +141,45 @@
color="primary" color="primary"
size="50px"> size="50px">
</q-spinner> </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 <q-card
class="my-card" class="my-card"
v-for="item in items" v-for="item in items"
:key="item.id"> :key="item.id">
<img :src="`${$imageBase}/catalog/200x200/${item.image}`" /> <img :src="`${$imageBase}/catalog/200x200/${item.image}`" />
<q-card-section> <q-card-section>
<div class="name text-subtitle1">{{item.longName}}</div> <div class="name text-subtitle1">
<div class="text-uppercase text-subtitle1 text-grey-7"> {{item.longName}}
</div>
<div class="sub-name text-uppercase text-subtitle1 text-grey-7 ellipsize q-pt-xs">
{{item.subName}} {{item.subName}}
</div> </div>
</q-card-section> <div class="tags q-pt-xs">
<q-card-section class="tags"> <div v-for="tag in item.tags" :key="tag.tagFk">
<div v-for="tag in item.tags" :key="tag.tagFk"> <span class="text-grey-7">{{tag.tag.name}}</span> {{tag.value}}
<span class="text-grey-7">{{tag.tag.name}}</span> {{tag.value}} </div>
</div> </div>
</q-card-section> </q-card-section>
<q-card-actions class="actions justify-between"> <q-card-actions class="actions justify-between">
<div class="q-pl-sm"> <div class="q-pl-sm">
<span class="available bg-green text-white">{{item.available}}</span> <span class="available bg-green text-white">{{item.available}}</span>
{{$t('from')}} {{$t('from')}}
<span class="price">{{item.inbounds[0].buy && item.inbounds[0].buy.price3 | currency}}</span> <span class="price">{{item.buy && item.buy.price3 | currency}}</span>
</div> </div>
<q-btn flat>{{$t('buy')}}</q-btn> <q-btn
icon="add_shopping_cart"
:title="$t('buy')"
flat>
</q-btn>
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</div> </div>
@ -171,16 +189,27 @@
</div> </div>
</template> </template>
</q-infinite-scroll> </q-infinite-scroll>
<q-page-sticky position="bottom-right" :offset="[18, 18]">
<q-btn
fab
icon="shopping_basket"
color="accent"
:to="{name: 'orders'}"
:title="$t('orders')"
/>
</q-page-sticky>
</div> </div>
</template> </template>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.my-card .my-card
width 100% width 100%
max-width 21em max-width 17.5em
height 38em height 32.5em
overflow hidden overflow hidden
.name .name, .sub-name
line-height 1.3em
.ellipsize
white-space nowrap white-space nowrap
overflow hidden overflow hidden
text-overflow ellipsis text-overflow ellipsis
@ -194,7 +223,7 @@
.category-img .category-img
height 3.5em height 3.5em
.tags .tags
max-height 6.2em max-height 4.6em
overflow hidden overflow hidden
.available .available
padding .15em padding .15em
@ -223,26 +252,35 @@ export default {
date: date.formatDate(new Date(), 'YYYY/MM/DD'), date: date.formatDate(new Date(), 'YYYY/MM/DD'),
category: null, category: null,
categories: [], categories: [],
tags: [],
type: null, type: null,
typeId: null,
types: [], types: [],
orgTypes: [], orgTypes: [],
tags: [],
isLoading: false, isLoading: false,
items: null, items: null,
rightDrawerOpen: this.$q.platform.is.desktop,
pageSize: 24,
limit: null, limit: null,
pageSize: 30,
maxTags: 5, maxTags: 5,
disableScroll: true, disableScroll: true,
order: { order: {
label: this.$t('name'), label: this.$t('relevancy'),
value: 'name' value: 'relevancy DESC, longName'
}, },
orderOptions: [ orderOptions: [
{ {
label: this.$t('relevancy'),
value: 'relevancy DESC, longName'
}, {
label: this.$t('name'), label: this.$t('name'),
value: 'longName' value: 'longName'
}, /* { }, {
label: this.$t('siceAsc'),
value: 'size ASC'
}, {
label: this.$t('sizeDesc'),
value: 'size DESC'
} /* {
label: this.$t('priceAsc'), label: this.$t('priceAsc'),
value: 'price ASC' value: 'price ASC'
}, { }, {
@ -251,71 +289,92 @@ export default {
}, { }, {
label: this.$t('available'), label: this.$t('available'),
value: 'available' value: 'available'
}, */ {
label: this.$t('siceAsc'),
value: 'size ASC'
}, { }, {
label: this.$t('sizeDesc'), label: this.$t('novelty'),
value: 'size DESC' value: 'dated DESC'
} } */
] ]
} }
}, },
mounted () { mounted () {
this.$axios.get('ItemCategories') this.$axios.get('ItemCategories')
.then(res => (this.categories = res.data)) .then(res => (this.categories = res.data))
this.onRouteChange(this.$route)
}, },
beforeDestroy () { beforeDestroy () {
this.clearTimeoutAndRequest() this.clearTimeoutAndRequest()
}, },
beforeRouteUpdate (to, from, next) {
this.onRouteChange(to)
next()
},
watch: { watch: {
categories () {
this.refreshTitle()
},
orgTypes () {
this.refreshTitle()
},
order () { order () {
this.loadItems() this.loadItems()
}, },
date () { date () {
this.loadItems() this.loadItems()
}, },
type (type) { category (value) {
this.$router.push({ this.orgTypes = []
params: {
category: this.category, if (value) {
type: type && type.id let filter = {
where: { categoryFk: value },
order: 'order DESC, name'
} }
}) this.$axios.get('ItemTypes', { params: { filter } })
this.$state.subtitle = type && type.name .then(res => (this.orgTypes = res.data))
this.$state.search = '' }
this.loadItems()
}, },
'$state.search': function (value) { '$state.search': function (value) {
if (!value) this.loadItems() let location = { params: this.$route.params }
else this.loadItemsDelayed() if (value) location.query = { search: value }
}, this.$router.push(location)
'$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: 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: { methods: {
loadItemsDelayed () { onRouteChange (route) {
this.clearTimeoutAndRequest() let { category, type } = route.params
this.timeout = setTimeout(() => this.loadItems(), 500)
category = parseInt(category) || null
type = parseInt(type) || null
this.category = category
this.typeId = category ? type : null
this.search = route.query.search || ''
this.$state.search = this.search
this.tags = []
this.refreshTitle()
this.loadItems()
},
refreshTitle () {
let title = this.$t(this.$router.currentRoute.name)
let titleColor
let subtitle
if (this.category) {
let category = this.categories.find(i => i.id === this.category) || {}
title = category.name
titleColor = `#${category.color}`
}
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
}
Object.assign(this.$state, { title, titleColor, subtitle })
}, },
clearTimeoutAndRequest () { clearTimeoutAndRequest () {
if (this.timeout) { if (this.timeout) {
@ -327,34 +386,36 @@ export default {
this.source = null this.source = null
} }
}, },
loadItemsDelayed () {
this.clearTimeoutAndRequest()
this.timeout = setTimeout(() => this.loadItems(), 500)
},
loadItems () { loadItems () {
this.items = [] this.items = null
this.isLoading = true this.isLoading = true
this.limit = this.pageSize this.limit = this.pageSize
this.disableScroll = false this.disableScroll = false
this.loadItemsBase() this.loadItemsBase()
.finally(() => { .finally(() => (this.isLoading = false))
this.isLoading = false
})
}, },
onLoad (index, done) { onLoad (index, done) {
if (this.isLoading) return done() if (this.isLoading) return done()
this.limit += this.pageSize this.limit += this.pageSize
this.loadItemsBase() this.loadItemsBase()
.finally(() => done()) .finally(done)
}, },
loadItemsBase () { loadItemsBase () {
this.clearTimeoutAndRequest() this.clearTimeoutAndRequest()
if (!this.type) { if (!(this.category || this.typeId || this.search)) {
this.tags = []
return Promise.resolve(true) return Promise.resolve(true)
} }
let typeFk
let tagFilter = [] let tagFilter = []
for (let tag of this.tags) { for (let tag of this.tags) {
if (this.hasFilters(tag)) { if (tag.hasFilter) {
tagFilter.push({ tagFilter.push({
tagFk: tag.id, tagFk: tag.id,
values: tag.filter values: tag.filter
@ -362,19 +423,16 @@ export default {
} }
} }
if (this.type) {
typeFk = this.type.id
}
this.source = CancelToken.source() this.source = CancelToken.source()
let params = { let params = {
dated: this.date, dated: this.date,
typeFk: typeFk, typeFk: this.typeId,
search: this.$state.search, categoryFk: this.category,
search: this.search,
order: this.order.value, order: this.order.value,
tagFilter: tagFilter, limit: this.limit,
limit: this.limit tagFilter
} }
let config = { let config = {
params, params,
@ -382,14 +440,20 @@ export default {
} }
return this.$axios.get('Items/catalog', config) return this.$axios.get('Items/catalog', config)
.then(res => this.onItemsGet(res)) .then(res => this.onItemsGet(res))
.catch(err => { if (!err.__CANCEL__) throw err }) .catch(err => this.onItemsError(err))
.finally(() => (this.cancel = null)) .finally(() => (this.cancel = null))
}, },
onItemsError (err) {
if (err.__CANCEL__) return
this.disableScroll = true
throw err
},
onItemsGet (res) { onItemsGet (res) {
for (let tag of res.data.tags) { for (let tag of res.data.tags) {
tag.uid = this.uid++ tag.uid = this.uid++
if (tag.filter) { if (tag.filter) {
tag.hasFilter = true
tag.useRange = tag.useRange =
tag.filter.max || tag.filter.max ||
tag.filter.min tag.filter.min
@ -412,11 +476,23 @@ export default {
this.tags = res.data.tags this.tags = res.data.tags
this.disableScroll = this.items.length < this.limit 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) { resetTagFilter (tag) {
tag.hasFilter = false
if (tag.useRange) { if (tag.useRange) {
tag.filter = { tag.filter = {
min: null, min: tag.min,
max: null max: tag.max
} }
} else { } else {
tag.filter = [] tag.filter = []
@ -426,10 +502,6 @@ export default {
this.resetTagFilter(tag) this.resetTagFilter(tag)
this.loadItems() this.loadItems()
}, },
hasFilters (tag) {
let filter = tag.filter
return filter.length || filter.min || filter.max
},
filterType (val, update) { filterType (val, update) {
if (val === '') { if (val === '') {
update(() => { this.types = this.orgTypes }) update(() => { this.types = this.orgTypes })

164
src/pages/Config.vue Normal file
View File

@ -0,0 +1,164 @@
<template>
<div class="vn-pp row justify-center">
<q-card class="vn-w-md">
<q-card-section class="q-gutter-md">
<q-input
v-model="user.name"
:label="$t('userName')"
readonly/>
</q-card-section>
<q-card-section class="q-gutter-md">
<q-input v-model="user.email" :label="$t('email')" />
</q-card-section>
<q-card-section class="q-gutter-md">
<q-input v-model="user.nickname" :label="$t('nickname')" />
</q-card-section>
<q-card-section class="q-gutter-md">
<q-select
v-model="user.lang"
:label="$t('language')"
:options="languages"
option-value="id"
option-label="name" />
</q-card-section>
<q-card-section class="q-gutter-md">
<q-checkbox v-model="receiveInvoices" :label="$t('receiveInvoiceByEmail')" />
</q-card-section>
<q-card-actions class="justify-center">
<q-btn flat :label="$t('changePassword')" :to="{ query: { changePassword: true } }"/>
<q-btn flat :label="$t('save')" @click="onSave"/>
</q-card-actions>
</q-card>
<q-dialog v-model="changePassword" persistent>
<q-card style="width: 25em;">
<q-card-section>
<q-input
v-model="oldPassword"
:label="$t('oldPassword')"
type="password"
:rules="[val => !!val || $t('fieldIsRequired')]"
autofocus
dense/>
<q-input
v-model="newPassword"
:label="$t('newPassword')"
type="password"
:rules="[val => !!val || $t('passwordCannotBeEmpty')]"
dense>
<template v-slot:append>
<q-icon name="warning">
<q-tooltip>
<div>
{{$t('passwordRequirements')}}
</div>
<ul class="q-pl-md">
<li>{{$t('charsLong', [requirements.length])}}</li>
<li>{{$t('alphabeticChars', [requirements.nAlpha])}}</li>
<li>{{$t('upperLetters', [requirements.nUpper])}}</li>
<li>{{$t('digits', [requirements.nDigits])}}</li>
<li>{{$t('simbols', [requirements.nPunct])}}</li>
</ul>
</q-tooltip>
</q-icon>
</template>
</q-input>
<q-input
v-model="repeatPassword"
:label="$t('repeatPassword')"
type="password"
:rules="[val => val === newPassword || $t('passwordsDontMatch')]"
dense/>
</q-card-section>
<q-card-actions align="right">
<q-btn
:label="$t('cancel')"
to="/config"
flat
color="primary"/>
<q-btn
:label="$t('accept')"
@click="onChangePassword()"
flat
color="primary"/>
</q-card-actions>
</q-card>
</q-dialog>
</div>
</template>
<style lang="stylus" scoped>
</style>
<script>
export default {
name: 'Config',
props: {
changePassword: {
type: Boolean,
required: false
}
},
data () {
return {
user: {},
receiveInvoices: false,
languages: null,
requirements: {},
oldPassword: '',
newPassword: '',
repeatPassword: ''
}
},
mounted () {
let filter = {
fields: [
'id',
'name',
'nickname',
'lang',
'email'
]
}
this.$axios.get(`Accounts/${this.$state.userId}`, { params: { filter } })
.then(res => (this.user = res.data))
filter = {
fields: [
'code',
'name',
'orgName'
],
where: { isActive: true }
}
this.$axios.get(`Languages`, { params: { filter } })
.then(res => (this.languages = res.data))
filter = {
fields: [
'id',
'length',
'nAlpha',
'nUpper',
'nDigits',
'nPunct'
]
}
this.$axios.get(`UserPasswords`, { params: { filter } })
.then(res => (this.requirements = res.data[0]))
},
methods: {
onChangePassword: function () {
this.$q.notify(this.$t('passwordChanged'))
this.$router.push('/config')
this.oldPassword = ''
this.newPassword = ''
this.repeatPassword = ''
},
onSave () {
this.$axios.patch(`Accounts/${this.$state.userId}`, this.user)
.then(res => (this.$q.notify(this.$t('dataSaved'))))
}
}
}
</script>

97
src/pages/Confirmed.vue Normal file
View File

@ -0,0 +1,97 @@
<template>
<div class="my-list q-pa-md">
<q-card>
<q-card-section>
<q-select v-model="yearFilter" :options="years" :label="$t('ordersMadeAt')" />
</q-card-section>
</q-card>
<q-card class="q-mt-md">
<q-list bordered separator>
<q-item v-if="!tickets.length">
{{$t('noOrdersFound')}}
</q-item>
<q-item
v-for="ticket in tickets"
:key="ticket.id"
clickable
v-ripple>
<q-item-section>
<q-item-label>{{formatDate(ticket.landed)}}</q-item-label>
<q-item-label caption>#{{ticket.id}}</q-item-label>
<q-item-label caption>{{ticket.address.nickname}}</q-item-label>
<q-item-label caption>{{ticket.address.city}}</q-item-label>
</q-item-section>
<q-item-section side top>
<q-item-label>
485.50
</q-item-label>
<q-badge
v-if="ticket.state"
color="teal"
:label="ticket.state.state.name"/>
<q-item-label v-if="ticket.packages" caption>
{{$t('packages', {n: ticket.packages})}}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-card>
</div>
</template>
<style lang="stylus" scoped>
.my-list
max-width 30em
margin auto
</style>
<script>
import { date } from 'quasar'
export default {
name: 'Orders',
data () {
return {
yearFilter: null,
years: [],
tickets: []
}
},
mounted () {
let now = new Date()
let curYear = now.getFullYear()
for (let i = 0; i <= 5; i++) {
this.years.push(curYear - i)
}
this.yearFilter = curYear
},
watch: {
yearFilter (year) {
let start = new Date(year, 0)
let end = new Date(year + 1, 0)
let params = { filter: {
where: {
clientFk: this.$state.userId,
landed: { between: [start, end] }
},
include: [
'address',
{ state: 'state' }
],
order: 'landed DESC'
} }
this.$axios.get('Tickets', { params })
.then(res => (this.tickets = res.data))
}
},
methods: {
formatDate (value) {
return date.formatDate(value, 'ddd, MMMM Do', this.$t('date'))
}
}
}
</script>

View File

@ -29,6 +29,7 @@
</div> </div>
</q-carousel-slide> </q-carousel-slide>
</q-carousel> </q-carousel>
<h2 class="text-center text-grey-7">{{$t('recentNews')}}</h2>
<div class="q-pa-sm row items-start"> <div class="q-pa-sm row items-start">
<div <div
class="new-card q-pa-sm" class="new-card q-pa-sm"
@ -48,7 +49,7 @@
<q-page-sticky position="bottom-right" :offset="[18, 18]"> <q-page-sticky position="bottom-right" :offset="[18, 18]">
<q-btn <q-btn
fab fab
icon="shopping_basket" icon="add_shopping_cart"
color="accent" color="accent"
:to="{name: 'catalog'}" :to="{name: 'catalog'}"
:title="$t('startOrder')" :title="$t('startOrder')"
@ -113,7 +114,6 @@ export default {
name: 'PageIndex', name: 'PageIndex',
data () { data () {
return { return {
slideIndex: 0,
slide: null, slide: null,
slides: [ slides: [
{ {
@ -154,13 +154,14 @@ export default {
this.$axios.get('News', { params }) this.$axios.get('News', { params })
.then(res => (this.news = res.data)) .then(res => (this.news = res.data))
this.slide = this.slides[this.slideIndex].image this.slide = this.slides[0].image
this.interval = setInterval(() => { this.interval = setInterval(() => {
this.slideIndex = (this.slideIndex + 1) % this.slides.length let index = this.slides.findIndex(i => i.image === this.slide)
this.slide = this.slides[this.slideIndex].image let nextIndex = (index + 1) % this.slides.length
this.slide = this.slides[nextIndex].image
}, 8000) }, 8000)
}, },
destroyed () { beforeDestroy () {
clearInterval(this.interval) clearInterval(this.interval)
} }
} }

View File

@ -1,69 +1,16 @@
<template> <template>
<div class="my-list q-pa-md"> <div>
<q-card> <q-tabs
<q-list bordered separator> inline-label
<q-item-label header>{{$t('pendingConfirmtion')}}</q-item-label> class="bg-secondary text-white shadow-2">
<q-item v-if="!orders.length"> <q-route-tab to="/orders/pending" icon="av_timer" :label="$t('pending')" />
{{$t('noOrdersFound')}} <q-route-tab to="/orders/confirmed" icon="check" :label="$t('confirmed')" />
</q-item> </q-tabs>
<q-item <router-view></router-view>
v-for="order in orders"
:key="order.id"
clickable
v-ripple>
<q-item-section>
<q-item-label>{{formatDate(order.landed)}}</q-item-label>
<q-item-label caption>#{{order.id}}</q-item-label>
<q-item-label caption>{{order.address.nickname}}</q-item-label>
<q-item-label caption>{{order.address.city}}</q-item-label>
</q-item-section>
<q-item-section side top>
485.50
</q-item-section>
</q-item>
</q-list>
</q-card>
<q-card class="q-mt-md">
<q-card-section>
<q-select v-model="yearFilter" :options="years" :label="$t('ordersMadeAt')" />
</q-card-section>
</q-card>
<q-card class="q-mt-md">
<q-list bordered separator>
<q-item-label header>{{$t('confirmed')}}</q-item-label>
<q-item v-if="!tickets.length">
{{$t('noOrdersFound')}}
</q-item>
<q-item
v-for="ticket in tickets"
:key="ticket.id"
clickable
v-ripple>
<q-item-section>
<q-item-label>{{formatDate(ticket.landed)}}</q-item-label>
<q-item-label caption>#{{ticket.id}}</q-item-label>
<q-item-label caption>{{ticket.address.nickname}}</q-item-label>
<q-item-label caption>{{ticket.address.city}}</q-item-label>
</q-item-section>
<q-item-section side top>
<q-item-label>
485.50
</q-item-label>
<q-badge
v-if="ticket.state"
color="teal"
:label="ticket.state.state.name"/>
<q-item-label v-if="ticket.packages" caption>
{{$t('packages', {n: ticket.packages})}}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-card>
<q-page-sticky position="bottom-right" :offset="[18, 18]"> <q-page-sticky position="bottom-right" :offset="[18, 18]">
<q-btn <q-btn
fab fab
icon="shopping_basket" icon="add_shopping_cart"
color="accent" color="accent"
:to="{name: 'catalog'}" :to="{name: 'catalog'}"
:title="$t('startOrder')" :title="$t('startOrder')"
@ -72,72 +19,12 @@
</div> </div>
</template> </template>
<style lang="stylus" scoped>
.my-list
max-width 30em
margin auto
</style>
<script> <script>
import { date } from 'quasar'
export default { export default {
name: 'Orders', name: 'Orders',
data () {
return {
yearFilter: null,
years: [],
orders: [],
tickets: []
}
},
mounted () { mounted () {
let params = { filter: { this.$router.replace('/orders/pending')
where: {
clientFk: this.$state.userId,
isConfirmed: false
},
include: 'address',
order: 'created DESC',
limit: 20
} }
this.$axios.get('Orders', { params })
.then(res => (this.orders = res.data))
let now = new Date()
let curYear = now.getFullYear()
for (let i = 0; i <= 5; i++) {
this.years.push(curYear - i)
}
this.yearFilter = curYear
},
watch: {
yearFilter (year) {
let start = new Date(year, 0)
let end = new Date(year + 1, 0)
let params = { filter: {
where: {
clientFk: this.userId,
landed: { between: [start, end] }
},
include: [
'address',
{ state: 'state' }
],
order: 'landed DESC'
} }
this.$axios.get('Tickets', { params })
.then(res => (this.tickets = res.data))
}
},
methods: {
formatDate (value) {
return date.formatDate(value, 'ddd, MMMM Do', this.$t('date'))
}
} }
} }
</script> </script>

64
src/pages/Pending.vue Normal file
View File

@ -0,0 +1,64 @@
<template>
<div class="my-list q-pa-md">
<q-card>
<q-list bordered separator>
<q-item v-if="!orders.length">
{{$t('noOrdersFound')}}
</q-item>
<q-item
v-for="order in orders"
:key="order.id"
clickable
v-ripple>
<q-item-section>
<q-item-label>{{formatDate(order.landed)}}</q-item-label>
<q-item-label caption>#{{order.id}}</q-item-label>
<q-item-label caption>{{order.address.nickname}}</q-item-label>
<q-item-label caption>{{order.address.city}}</q-item-label>
</q-item-section>
<q-item-section side top>
485.50
</q-item-section>
</q-item>
</q-list>
</q-card>
</div>
</template>
<style lang="stylus" scoped>
.my-list
max-width 30em
margin auto
</style>
<script>
import { date } from 'quasar'
export default {
name: 'Orders',
data () {
return {
orders: []
}
},
mounted () {
let params = { filter: {
where: {
clientFk: this.$state.userId,
isConfirmed: false
},
include: 'address',
order: 'created DESC',
limit: 20
} }
this.$axios.get('Orders', { params })
.then(res => (this.orders = res.data))
},
methods: {
formatDate (value) {
return date.formatDate(value, 'ddd, MMMM Do', this.$t('date'))
}
}
}
</script>

View File

@ -23,14 +23,13 @@ export default function (/* { store, ssrContext } */) {
}) })
Router.afterEach((to, from) => { Router.afterEach((to, from) => {
if (from.name === to.name) return
let app = Router.app let app = Router.app
let $state = app.$state Object.assign(app.$state, {
title: app.$t(to.name),
if (from.name !== to.name) { titleColor: null,
$state.title = app.$t(to.name) subtitle: null
$state.titleColor = null })
$state.subtitle = null
}
}) })
return Router return Router

View File

@ -19,7 +19,16 @@ const routes = [
}, { }, {
name: 'orders', name: 'orders',
path: '/orders', path: '/orders',
component: () => import('pages/Orders.vue') component: () => import('pages/Orders.vue'),
children: [
{
path: 'pending',
component: () => import('pages/Pending.vue')
}, {
path: 'confirmed',
component: () => import('pages/Confirmed.vue')
}
]
}, { }, {
name: 'conditions', name: 'conditions',
path: '/conditions', path: '/conditions',
@ -32,6 +41,21 @@ const routes = [
name: 'register', name: 'register',
path: '/register', path: '/register',
component: () => import('pages/Register.vue') component: () => import('pages/Register.vue')
}, {
name: 'config',
path: '/config',
component: () => import('pages/Config.vue'),
props: route => ({
changePassword: String(route.query.changePassword) === 'true'
})
}, {
name: 'addresses',
path: '/addresses',
component: () => import('pages/Addresses.vue')
}, {
name: 'addressEdit',
path: '/address/:address?',
component: () => import('pages/Address.vue')
} }
] ]
}, { }, {