diff --git a/back/common/models/account.json b/back/common/models/account.json new file mode 100644 index 0000000..180c5be --- /dev/null +++ b/back/common/models/account.json @@ -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" + } + } +} diff --git a/back/common/models/country.json b/back/common/models/country.json new file mode 100644 index 0000000..a27fe96 --- /dev/null +++ b/back/common/models/country.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/back/common/models/item.js b/back/common/models/item.js index f67f221..10f690b 100644 --- a/back/common/models/item.js +++ b/back/common/models/item.js @@ -13,6 +13,10 @@ module.exports = Self => { arg: 'typeFk', type: 'Number', description: 'The item type id' + }, { + arg: 'categoryFk', + type: 'Number', + description: 'The item category id' }, { arg: 'search', 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 itemIds; @@ -63,11 +67,27 @@ module.exports = Self => { if (/^[0-9]+$/.test(search)) { itemIds = [parseInt(search)]; } else { - if (typeFk || search) { - let where = {}; + let inbounds = await $.Inbound.find({ + fields: ['itemFk'], + where: inboundWhere + }); + itemIds = toValues(inbounds, 'itemFk'); - if (typeFk) + if (categoryFk || typeFk || search) { + let where = { + id: {inq: itemIds} + }; + + if (typeFk) { where.typeFk = typeFk; + } else if (categoryFk) { + let types = await $.ItemType.find({ + fields: ['id'], + where: {categoryFk} + }); + where.typeFk = {inq: toValues(types, 'id')}; + } + if (search) where.longName = {like: `%${search}%`}; @@ -79,15 +99,6 @@ module.exports = Self => { let items = await Self.find(filter); 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 @@ -128,90 +139,96 @@ module.exports = Self => { } // Obtains distinct tags and it's distinct values + + let tags = []; - let tagValues = await $.ItemTag.find({ - 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({ - fields: ['value', 'intValue', 'priority'], + fields: ['tagFk', 'value', 'intValue', 'priority'], where: { 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 tags = await $.Tag.find({ - fields: ['id', 'name', 'isQuantitative', 'unit'], - where: { - id: {inq: tagIds} - } - }); + let filter = tagFilter && tagFilter.find(i => i.tagFk == tag.id); + filter = filter && filter.values; - for (let tag of tags) { - let tagValues = tagValueMap.get(tag.id); + let values = toSet(tagValues, 'value'); + if (Array.isArray(filter)) + values = new Set([...filter, ...values]); - let filter = tagFilter && tagFilter.find(i => i.tagFk == tag.id); - filter = filter && filter.values; + if (tag.isQuantitative) { + let intValues = toValues(tagValues, 'intValue'); - let values = toSet(tagValues, 'value'); - if (Array.isArray(filter)) - values = new Set([...filter, ...values]); + if (filter) { + if (filter.min) intValues.push(filter.min); + if (filter.max) intValues.push(filter.max); + } - if (tag.isQuantitative) { - let intValues = toValues(tagValues, 'intValue'); + 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; - if (filter) { - if (filter.min) intValues.push(filter.min); - if (filter.max) intValues.push(filter.max); + Object.assign(tag, { + step, + 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, { - step, - min: Math.floor(min / step) * step, - max: Math.ceil(max / step) * step + values: [...values], + filter }); } - - Object.assign(tag, { - values: [...values], - filter - }); } // Obtains items data let items = await Self.find({ - fields: ['id', 'longName', 'subname', 'image'], + fields: ['id', 'longName', 'subName', 'image'], where: {id: {inq: itemIds}}, + limit: limit, + order: order, include: [ { 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'); + } return {items, tags}; }; diff --git a/back/common/models/language.json b/back/common/models/language.json new file mode 100644 index 0000000..2bf6c08 --- /dev/null +++ b/back/common/models/language.json @@ -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 + } + } +} diff --git a/back/common/models/user-password.json b/back/common/models/user-password.json new file mode 100644 index 0000000..26169f1 --- /dev/null +++ b/back/common/models/user-password.json @@ -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 + } + } +} diff --git a/back/server/model-config.json b/back/server/model-config.json index 4ebac96..33f9376 100644 --- a/back/server/model-config.json +++ b/back/server/model-config.json @@ -53,6 +53,9 @@ } } }, + "Account": { + "dataSource": "vn" + }, "Address": { "dataSource": "vn" }, @@ -68,6 +71,9 @@ "Client": { "dataSource": "vn" }, + "Country": { + "dataSource": "vn" + }, "DeliveryMethod": { "dataSource": "vn" }, @@ -86,6 +92,9 @@ "ItemType": { "dataSource": "vn" }, + "Language": { + "dataSource": "vn" + }, "New": { "dataSource": "vn" }, @@ -119,6 +128,9 @@ "TicketTracking": { "dataSource": "vn" }, + "UserPassword": { + "dataSource": "vn" + }, "Warehouse": { "dataSource": "vn" } diff --git a/quasar.conf.js b/quasar.conf.js index 225de66..fca7e7e 100644 --- a/quasar.conf.js +++ b/quasar.conf.js @@ -27,42 +27,51 @@ module.exports = function (ctx) { framework: { components: [ 'QBadge', - 'QLayout', - 'QHeader', - 'QDate', - 'QDrawer', - 'QPageContainer', - 'QPage', - 'QToolbar', - 'QToolbarTitle', 'QBtn', - 'QIcon', - 'QList', - 'QInfiniteScroll', - 'QItem', - 'QItemSection', - 'QItemLabel', 'QCarousel', 'QCarouselControl', 'QCarouselSlide', 'QCard', 'QCardSection', 'QCardActions', - 'QRating', 'QCheckbox', + 'QDate', + 'QDialog', + 'QDrawer', + 'QExpansionItem', + 'QHeader', + 'QIcon', + 'QImg', + 'QInfiniteScroll', 'QInput', + 'QItem', + 'QItemSection', + 'QItemLabel', + 'QList', + 'QLayout', + 'QPageContainer', + 'QPage', + 'QPageSticky', + 'QPopupProxy', + 'QRadio', + 'QRating', 'QRange', 'QSelect', 'QSeparator', 'QSpinner', - 'QTooltip', - 'QImg', - 'QPageSticky', - 'QPopupProxy' + 'QTab', + 'QTabs', + 'QTabPanel', + 'QTabPanels', + 'QRouteTab', + 'QToolbar', + 'QToolbarTitle', + 'QTooltip' ], directives: [ - 'Ripple' + 'Ripple', + 'ClosePopup' ], // Quasar plugins diff --git a/src/boot/state.js b/src/boot/state.js index 4d93542..9377b79 100644 --- a/src/boot/state.js +++ b/src/boot/state.js @@ -7,7 +7,8 @@ export default async ({ app, Vue }) => { userName: null, title: null, subtitle: null, - search: null + search: null, + rightDrawerOpen: true }) Vue.prototype.$state = state } diff --git a/src/css/app.styl b/src/css/app.styl index e3e5a1e..3d13c3c 100644 --- a/src/css/app.styl +++ b/src/css/app.styl @@ -1 +1,10 @@ // app global css + +.vn-w-md + width 30em +.vn-pp + padding 1em + +@media (max-width: 960px) + .vn-pp + padding 1em 0 diff --git a/src/i18n/en-us/index.js b/src/i18n/en-us/index.js index 8b81bbd..d1a7e58 100644 --- a/src/i18n/en-us/index.js +++ b/src/i18n/en-us/index.js @@ -5,6 +5,16 @@ export default { password: 'Password', remember: 'Don not close session', 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: { days: [ 'Sunday', @@ -64,9 +74,14 @@ export default { orders: 'Orders', conditions: 'Conditions', about: 'About us', + config: 'Configuration', + user: 'User', + addresses: 'Addresses', + addressEdit: 'Edit address', register: 'Register', // Home + recentNews: 'Recent news', startOrder: 'Start order', weThinkForYou: 'We think for you', @@ -86,6 +101,8 @@ export default { // Catalog more: 'More', + noItemsFound: 'No items found', + pleaseSetFilter: 'Please, set a filter using the right menu', buy: 'Buy', deleteFilter: 'Delete filter', viewMore: 'View more', @@ -101,6 +118,7 @@ export default { relevancy: 'Relevancy', priceAsc: 'Acending price', priceDesc: 'Descending price', + novelty: 'Novelty', available: 'Available', siceAsc: 'Ascending size', sizeDesc: 'Descencing size', @@ -109,6 +127,7 @@ export default { ordersMadeAt: 'Orders made at', pendingConfirmtion: 'Pending confirmation', noOrdersFound: 'No orders found', + pending: 'Pending', confirmed: 'Confirmed', packages: '{n} packages', @@ -118,10 +137,45 @@ export default { // About 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 registerAsNew: 'Registrarse como nuevo usuario', 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', userRegistered: 'User registered successfully' } diff --git a/src/i18n/es-es/index.js b/src/i18n/es-es/index.js index 332075a..37528e3 100644 --- a/src/i18n/es-es/index.js +++ b/src/i18n/es-es/index.js @@ -5,6 +5,16 @@ export default { password: 'Contraseña', remember: 'No cerrar sesión', 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: { days: [ 'Domingo', @@ -65,9 +75,14 @@ export default { orders: 'Pedidos', conditions: 'Condiciones', about: 'Sobre nosotros', + config: 'Configuración', + user: 'Usuario', + addresses: 'Direcciones', + addressEdit: 'Editar dirección', register: 'Registrarse', // Home + recentNews: 'Noticias recientes', startOrder: 'Empezar pedido', weThinkForYou: 'Pensamos para tí', @@ -87,6 +102,8 @@ export default { // Catalog more: 'Más', + noItemsFound: 'No se han encontrado artículos', + pleaseSetFilter: 'Por favor, establece un filtro usando el menú de la derecha', buy: 'Comprar', deleteFilter: 'Eliminar filtro', viewMore: 'Ver más', @@ -102,6 +119,7 @@ export default { relevancy: 'Relevancia', priceAsc: 'Precio ascendente', priceDesc: 'Precio descendente', + novelty: 'Novedad', available: 'Disponible', siceAsc: 'Medida ascendente', sizeDesc: 'Medida descendente', @@ -110,6 +128,7 @@ export default { ordersMadeAt: 'Pedidos realizados en', pendingConfirmtion: 'Pendientes de confirmar', noOrdersFound: 'No se han encontrado pedidos', + pending: 'Pendientes', confirmed: 'Confirmados', packages: '{n} bultos', @@ -119,10 +138,45 @@ export default { // About 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 registerAsNew: 'Registrarse como nuevo usuario', 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', userRegistered: 'Usuario registrado correctamente' } diff --git a/src/layouts/Login.vue b/src/layouts/Login.vue index a8cd8c9..d899cbf 100644 --- a/src/layouts/Login.vue +++ b/src/layouts/Login.vue @@ -20,6 +20,7 @@ + diff --git a/src/layouts/MyLayout.vue b/src/layouts/MyLayout.vue index 7c7e1f3..8377357 100644 --- a/src/layouts/MyLayout.vue +++ b/src/layouts/MyLayout.vue @@ -19,36 +19,50 @@ {{$state.subtitle}} - - - - + + + + + + + overlay>
@@ -82,11 +96,21 @@ {{$t('about')}} - - - {{$t('register')}} - - + + + + {{$t('user')}} + + + + + {{$t('addresses')}} + + + @@ -123,7 +147,7 @@ export default { openURL, logout () { localStorage.removeItem('token') - this.$router.push('login') + this.$router.push('/login') } } } diff --git a/src/pages/Address.vue b/src/pages/Address.vue new file mode 100644 index 0000000..5d23ed8 --- /dev/null +++ b/src/pages/Address.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/src/pages/Addresses.vue b/src/pages/Addresses.vue new file mode 100644 index 0000000..29a030d --- /dev/null +++ b/src/pages/Addresses.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/src/pages/Catalog.vue b/src/pages/Catalog.vue index 69f0192..87d527a 100644 --- a/src/pages/Catalog.vue +++ b/src/pages/Catalog.vue @@ -1,7 +1,7 @@ + + +
+ + diff --git a/src/pages/Confirmed.vue b/src/pages/Confirmed.vue new file mode 100644 index 0000000..bccdd0d --- /dev/null +++ b/src/pages/Confirmed.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/src/pages/Index.vue b/src/pages/Index.vue index 7c0c5a9..ecb3a46 100644 --- a/src/pages/Index.vue +++ b/src/pages/Index.vue @@ -29,6 +29,7 @@
+

{{$t('recentNews')}}

(this.news = res.data)) - this.slide = this.slides[this.slideIndex].image + this.slide = this.slides[0].image this.interval = setInterval(() => { - this.slideIndex = (this.slideIndex + 1) % this.slides.length - this.slide = this.slides[this.slideIndex].image + let index = this.slides.findIndex(i => i.image === this.slide) + let nextIndex = (index + 1) % this.slides.length + this.slide = this.slides[nextIndex].image }, 8000) }, - destroyed () { + beforeDestroy () { clearInterval(this.interval) } } diff --git a/src/pages/Orders.vue b/src/pages/Orders.vue index ea833bf..2795396 100644 --- a/src/pages/Orders.vue +++ b/src/pages/Orders.vue @@ -1,69 +1,16 @@ - - diff --git a/src/pages/Pending.vue b/src/pages/Pending.vue new file mode 100644 index 0000000..0aebc08 --- /dev/null +++ b/src/pages/Pending.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/src/router/index.js b/src/router/index.js index d2f4dde..aeab423 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -23,14 +23,13 @@ export default function (/* { store, ssrContext } */) { }) Router.afterEach((to, from) => { + if (from.name === to.name) return 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 - } + Object.assign(app.$state, { + title: app.$t(to.name), + titleColor: null, + subtitle: null + }) }) return Router diff --git a/src/router/routes.js b/src/router/routes.js index 7db3c5f..b5ffa5f 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -19,7 +19,16 @@ const routes = [ }, { name: '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', path: '/conditions', @@ -32,6 +41,21 @@ const routes = [ name: 'register', path: '/register', 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') } ] }, {