From 65097c3f58f14b10777123d0b511b29f866f3d0c Mon Sep 17 00:00:00 2001 From: Juan Ferrer Toribio Date: Mon, 29 Jun 2020 13:31:48 +0200 Subject: [PATCH] Refactor, bugfix, i18n --- back/.eslintignore | 1 - back/common/models/item.js | 660 +++++++++--------- back/common/models/user.js | 55 +- back/common/models/user.json | 7 +- back/common/models/vn-model.js | 58 ++ back/common/models/vn-model.json | 5 + back/common/models/zone.js | 33 + back/common/models/zone.json | 9 + back/locales/en.json | 11 + back/locales/es.json | 17 + back/package-lock.json | 71 ++ back/package.json | 2 +- back/server/boot/connector.js | 17 + back/server/config.local.json | 3 + back/server/middleware.json | 7 +- back/server/middleware/error-handler.js | 28 + back/server/model-config.json | 355 +++++----- back/server/server.js | 14 +- quasar.conf.js | 4 + src/boot/axios.js | 5 + src/components/Page.js | 2 + src/components/Portal.vue | 21 + src/i18n/en-us/index.js | 7 +- src/i18n/es-es/index.js | 75 +- src/layouts/LoginLayout.vue | 10 +- src/layouts/MainLayout.vue | 4 +- src/pages/{ => Account}/Address.vue | 2 +- src/pages/{ => Account}/Addresses.vue | 6 +- src/pages/{ => Account}/User.vue | 4 +- src/pages/{ => Admin}/AccessLog.vue | 0 src/pages/{ => Admin}/Connections.vue | 0 src/pages/{ => Admin}/Images.vue | 0 src/pages/{Admin.vue => Admin/Index.vue} | 2 +- src/pages/{ => Admin}/Items.vue | 0 src/pages/{ => Admin}/New.vue | 2 +- src/pages/{ => Admin}/News.vue | 0 src/pages/{ => Admin}/Panel.vue | 0 src/pages/{ => Admin}/Users.vue | 0 src/pages/{ => Admin}/Visits.vue | 0 src/pages/{ => Cms}/About.vue | 0 src/pages/{ => Cms}/Conditions.vue | 0 src/pages/{Index.vue => Cms/Home.vue} | 0 src/pages/{ => Login}/Login.vue | 22 +- src/pages/{ => Login}/Register.vue | 3 +- src/pages/{ => Login}/RememberPassword.vue | 1 + src/pages/{ => Login}/ResetPassword.vue | 1 + src/pages/{ => Webshop}/Catalog.vue | 8 +- src/pages/{ => Webshop}/Confirmed.vue | 4 +- .../Confirmed/View.vue} | 2 +- src/pages/{ => Webshop}/Orders.vue | 0 src/pages/{ => Webshop}/Pending.vue | 14 +- .../Pending/Checkout.vue} | 2 +- src/pages/Webshop/Pending/Configure.vue | 201 ++++++ .../Pending/Rows.vue} | 2 +- .../{Order.vue => Webshop/Pending/View.vue} | 7 +- src/router/routes.js | 68 +- 56 files changed, 1204 insertions(+), 628 deletions(-) delete mode 100644 back/.eslintignore create mode 100644 back/common/models/vn-model.js create mode 100644 back/common/models/vn-model.json create mode 100644 back/common/models/zone.js create mode 100644 back/common/models/zone.json create mode 100644 back/locales/en.json create mode 100644 back/locales/es.json create mode 100644 back/server/boot/connector.js create mode 100644 back/server/config.local.json create mode 100644 back/server/middleware/error-handler.js create mode 100644 src/components/Portal.vue rename src/pages/{ => Account}/Address.vue (99%) rename src/pages/{ => Account}/Addresses.vue (95%) rename src/pages/{ => Account}/User.vue (96%) rename src/pages/{ => Admin}/AccessLog.vue (100%) rename src/pages/{ => Admin}/Connections.vue (100%) rename src/pages/{ => Admin}/Images.vue (100%) rename src/pages/{Admin.vue => Admin/Index.vue} (81%) rename src/pages/{ => Admin}/Items.vue (100%) rename src/pages/{ => Admin}/New.vue (99%) rename src/pages/{ => Admin}/News.vue (100%) rename src/pages/{ => Admin}/Panel.vue (100%) rename src/pages/{ => Admin}/Users.vue (100%) rename src/pages/{ => Admin}/Visits.vue (100%) rename src/pages/{ => Cms}/About.vue (100%) rename src/pages/{ => Cms}/Conditions.vue (100%) rename src/pages/{Index.vue => Cms/Home.vue} (100%) rename src/pages/{ => Login}/Login.vue (83%) rename src/pages/{ => Login}/Register.vue (97%) rename src/pages/{ => Login}/RememberPassword.vue (98%) rename src/pages/{ => Login}/ResetPassword.vue (99%) rename src/pages/{ => Webshop}/Catalog.vue (99%) rename src/pages/{ => Webshop}/Confirmed.vue (96%) rename src/pages/{Ticket.vue => Webshop/Confirmed/View.vue} (87%) rename src/pages/{ => Webshop}/Orders.vue (100%) rename src/pages/{ => Webshop}/Pending.vue (85%) rename src/pages/{OrderCheckout.vue => Webshop/Pending/Checkout.vue} (99%) create mode 100644 src/pages/Webshop/Pending/Configure.vue rename src/pages/{OrderView.vue => Webshop/Pending/Rows.vue} (99%) rename src/pages/{Order.vue => Webshop/Pending/View.vue} (93%) diff --git a/back/.eslintignore b/back/.eslintignore deleted file mode 100644 index 44f3970..0000000 --- a/back/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -/client/ \ No newline at end of file diff --git a/back/common/models/item.js b/back/common/models/item.js index 81bcddf..fa93de8 100644 --- a/back/common/models/item.js +++ b/back/common/models/item.js @@ -1,368 +1,368 @@ const warehouseIds = [1, 44]; module.exports = Self => { - Self.remoteMethod('calcCatalog', { - description: 'Get the available and prices list for an item', - accessType: 'READ', - accepts: [ - { - arg: 'id', - type: 'Number', - description: 'The item id' - }, { - arg: 'dated', - type: 'Date', - description: 'The date' - }, { - arg: 'addressFk', - type: 'Number', - description: 'The address id' - }, { - arg: 'agencyModeFk', - type: 'Number', - description: 'The agency id' - } - ], - returns: { - type: ['Object'], - description: 'The item available and prices list', - root: true, - }, - http: { - path: `/:id/calcCatalog`, - verb: 'GET' - } - }); + Self.remoteMethod('calcCatalog', { + description: 'Get the available and prices list for an item', + accessType: 'READ', + accepts: [ + { + arg: 'id', + type: 'Number', + description: 'The item id' + }, { + arg: 'dated', + type: 'Date', + description: 'The date' + }, { + arg: 'addressFk', + type: 'Number', + description: 'The address id' + }, { + arg: 'agencyModeFk', + type: 'Number', + description: 'The agency id' + } + ], + returns: { + type: ['Object'], + description: 'The item available and prices list', + root: true, + }, + http: { + path: `/:id/calcCatalog`, + verb: 'GET' + } + }); - Self.calcCatalog = (id, dated, addressFk, agencyModeFk, cb) => { - Self.dataSource.connector.query( - `CALL hedera.item_calcCatalog(?, ?, ?, ?)`, - [id, dated, addressFk, agencyModeFk], - (err, res) => { - if (err) return cb(err) - return cb(null, res[0]) - } - ); + Self.calcCatalog = (id, dated, addressFk, agencyModeFk, cb) => { + Self.dataSource.connector.query( + `CALL hedera.item_calcCatalog(?, ?, ?, ?)`, + [id, dated, addressFk, agencyModeFk], + (err, res) => { + if (err) return cb(err) + return cb(null, res[0]) + } + ); + }; + + Self.remoteMethod('catalog', { + description: 'Get the catalog', + accessType: 'READ', + accepts: [ + { + arg: 'dated', + type: 'Date', + description: 'The available date' + }, { + arg: 'typeFk', + type: 'Number', + description: 'The item type id' + }, { + arg: 'categoryFk', + type: 'Number', + description: 'The item category id' + }, { + arg: 'search', + type: 'String', + description: 'The search string' + }, { + arg: 'order', + type: 'String', + description: 'The order string' + }, { + arg: 'limit', + type: 'Number', + description: 'The maximum number of registers' + }, { + arg: 'tagFilter', + type: ['Object'], + description: 'The tag filter object' + } + ], + returns: { + type: ['Object'], + description: 'The item list', + root: true, + }, + http: { + path: `/catalog`, + verb: 'GET' + } + }); + + Self.catalog = async (dated, typeFk, categoryFk, search, order, limit, tagFilter) => { + let $ = Self.app.models; + let itemIds; + + let inboundWhere = { + warehouseFk: {inq: warehouseIds}, + available: {gt: 0}, + dated: {lte: dated}, + and: [ + {or: [ + {expired: {gt: dated}}, + {expired: null} + ]} + ] }; - Self.remoteMethod('catalog', { - description: 'Get the catalog', - accessType: 'READ', - accepts: [ - { - arg: 'dated', - type: 'Date', - description: 'The available date' - }, { - arg: 'typeFk', - type: 'Number', - description: 'The item type id' - }, { - arg: 'categoryFk', - type: 'Number', - description: 'The item category id' - }, { - arg: 'search', - type: 'String', - description: 'The search string' - }, { - arg: 'order', - type: 'String', - description: 'The order string' - }, { - arg: 'limit', - type: 'Number', - description: 'The maximum number of registers' - }, { - arg: 'tagFilter', - type: ['Object'], - description: 'The tag filter object' - } - ], - returns: { - type: ['Object'], - description: 'The item list', - root: true, - }, - http: { - path: `/catalog`, - verb: 'GET' - } - }); + // Applies base filters - Self.catalog = async (dated, typeFk, categoryFk, search, order, limit, tagFilter) => { - let $ = Self.app.models; - let itemIds; + if (/^[0-9]+$/.test(search)) { + itemIds = [parseInt(search)]; + } else { + let inbounds = await $.Inbound.find({ + fields: ['itemFk'], + where: inboundWhere + }); + itemIds = toValues(inbounds, 'itemFk'); - let inboundWhere = { - warehouseFk: {inq: warehouseIds}, - available: {gt: 0}, - dated: {lte: dated}, - and: [ - {or: [ - {expired: {gt: dated}}, - {expired: null} - ]} - ] + if (categoryFk || typeFk || search) { + let where = { + id: {inq: itemIds} }; - // Applies base filters - - if (/^[0-9]+$/.test(search)) { - itemIds = [parseInt(search)]; - } else { - let inbounds = await $.Inbound.find({ - fields: ['itemFk'], - where: inboundWhere - }); - itemIds = toValues(inbounds, 'itemFk'); - - 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}%`}; - - let filter = { - fields: ['id'], - where - }; - - let items = await Self.find(filter); - itemIds = items.map(i => i.id); - } + if (typeFk) { + where.typeFk = typeFk; + } else if (categoryFk) { + let types = await $.ItemType.find({ + fields: ['id'], + where: {categoryFk} + }); + where.typeFk = {inq: toValues(types, 'id')}; } - // Applies tag filters + if (search) + where.longName = {like: `%${search}%`}; - let baseItemIds = itemIds; - let tagItems = []; - let tagFilterIds = []; + let filter = { + fields: ['id'], + where + }; - if (tagFilter && tagFilter.length) { - for (let filter of tagFilter) { - let cond; - let values = filter.values; + let items = await Self.find(filter); + itemIds = items.map(i => i.id); + } + } - if (values.length) - cond = {value: {inq: values}}; - else if (values.min && values.max) - cond = {intValue: {between: [values.min, values.max]}}; - else if (values.min) - cond = {intValue: {gte: values.min}}; - else if (values.max) - cond = {intValue: {lte: values.max}}; + // Applies tag filters - let where = { - itemFk: {inq: itemIds}, - tagFk: filter.tagFk - }; - Object.assign(where, cond); + let baseItemIds = itemIds; + let tagItems = []; + let tagFilterIds = []; - let itemTags = await $.ItemTag.find({ - fields: ['itemFk'], - where - }); - tagItems.push(toSet(itemTags, 'itemFk')); - tagFilterIds.push(filter.tagFk); - } + if (tagFilter && tagFilter.length) { + for (let filter of tagFilter) { + let cond; + let values = filter.values; - itemIds = intersect(tagItems); - } + if (values.length) + cond = {value: {inq: values}}; + else if (values.min && values.max) + cond = {intValue: {between: [values.min, values.max]}}; + else if (values.min) + cond = {intValue: {gte: values.min}}; + else if (values.max) + cond = {intValue: {lte: values.max}}; - // Obtains distinct tags and it's distinct values - - let tags = []; + let where = { + itemFk: {inq: itemIds}, + tagFk: filter.tagFk + }; + Object.assign(where, cond); - if (typeFk || search) { - 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'); + let itemTags = await $.ItemTag.find({ + fields: ['itemFk'], + where + }); + tagItems.push(toSet(itemTags, 'itemFk')); + tagFilterIds.push(filter.tagFk); + } - for (let i = 0; i < tagItems.length; i++) { - let tagFk = tagFilter[i].tagFk; - let itemIds; + itemIds = intersect(tagItems); + } - if (tagItems.length > 1) { - let siblings = tagItems.filter(v => v != tagItems[i]); - itemIds = intersect(siblings); - } else - itemIds = baseItemIds; + // Obtains distinct tags and it's distinct values + + let tags = []; - let tagValues = await $.ItemTag.find({ - fields: ['value', 'intValue', 'priority'], - where: { - itemFk: {inq: itemIds}, - tagFk: tagFk - }, - order: 'value' - }); + if (typeFk || search) { + 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'); - tagValueMap.set(tagFk, tagValues); - } + for (let i = 0; i < tagItems.length; i++) { + let tagFk = tagFilter[i].tagFk; + let itemIds; - let tagIds = [...tagValueMap.keys()]; - tags = await $.Tag.find({ - fields: ['id', 'name', 'isQuantitative', 'unit'], - where: { - id: {inq: tagIds} - } - }); + if (tagItems.length > 1) { + let siblings = tagItems.filter(v => v != tagItems[i]); + itemIds = intersect(siblings); + } else + itemIds = baseItemIds; - for (let tag of tags) { - let tagValues = tagValueMap.get(tag.id); - - let filter = tagFilter && tagFilter.find(i => i.tagFk == tag.id); - filter = filter && filter.values; - - let values = toSet(tagValues, 'value'); - if (Array.isArray(filter)) - values = new Set([...filter, ...values]); - - if (tag.isQuantitative) { - let intValues = toValues(tagValues, 'intValue'); - - if (filter) { - if (filter.min) intValues.push(filter.min); - if (filter.max) intValues.push(filter.max); - } - - 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 - }); - } - - Object.assign(tag, { - values: [...values], - filter - }); - } - } - - // Obtains items data - - let items = await Self.find({ - fields: ['id', 'longName', 'subName', 'image'], - where: {id: {inq: itemIds}}, - limit: limit, - order: order, - include: [ - { - relation: 'tags', - scope: { - fields: ['value', 'tagFk'], - where: {priority: {gt: 4}}, - order: 'priority', - include: { - relation: 'tag', - scope: {fields: ['name']} - } - } - }, { - relation: 'inbounds', - scope: { - fields: ['available', 'dated', 'tableId'], - where: inboundWhere, - order: 'dated DESC', - include: { - relation: 'buy', - scope: {fields: ['id', 'price3']} - }, - } - } - ] + let tagValues = await $.ItemTag.find({ + fields: ['value', 'intValue', 'priority'], + where: { + itemFk: {inq: itemIds}, + tagFk: tagFk + }, + order: 'value' }); - for (let item of items) { - item.inbound = item.inbounds()[0]; - item.buy = item.inbound && item.inbound.buy(); - item.available = sum(item.inbounds(), 'available'); + tagValueMap.set(tagFk, tagValues); + } + + let tagIds = [...tagValueMap.keys()]; + tags = await $.Tag.find({ + fields: ['id', 'name', 'isQuantitative', 'unit'], + where: { + id: {inq: tagIds} + } + }); + + for (let tag of tags) { + let tagValues = tagValueMap.get(tag.id); + + let filter = tagFilter && tagFilter.find(i => i.tagFk == tag.id); + filter = filter && filter.values; + + let values = toSet(tagValues, 'value'); + if (Array.isArray(filter)) + values = new Set([...filter, ...values]); + + if (tag.isQuantitative) { + let intValues = toValues(tagValues, 'intValue'); + + if (filter) { + if (filter.min) intValues.push(filter.min); + if (filter.max) intValues.push(filter.max); + } + + 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 + }); } - return {items, tags}; - }; - - // Array functions - - function sum(array, key) { - if (!Array.isArray(array)) return 0; - return array.reduce((a, c) => a + c[key], 0); + Object.assign(tag, { + values: [...values], + filter + }); + } } - function toMap(objects, key) { - let map = new Map(); - for (let object of objects) - map.set(object[key], object); - return map; - } + // Obtains items data - function toMultiMap(objects, key) { - let map = new Map(); - for (let object of objects) { - let value = map.get(object[key]); - if (!value) map.set(object[key], value = []); - value.push(object); - } - return map; - } - - function toSet(objects, key) { - let set = new Set(); - for (let object of objects) - set.add(object[key]); - return set; - } - - function toValues(objects, key) { - return [...toSet(objects, key)]; - } - - function intersect(sets) { - if (!sets.length) return []; - let array = []; - let mySets = sets.slice(0); - let firstSet = mySets.shift(); - - for (let value of firstSet) { - let isOnAll = true; - - for (let set of mySets) - if (!set.has(value)) { - isOnAll = false; - break; + let items = await Self.find({ + fields: ['id', 'longName', 'subName', 'image'], + where: {id: {inq: itemIds}}, + limit, + order, + include: [ + { + relation: 'tags', + scope: { + fields: ['value', 'tagFk'], + where: {priority: {gt: 4}}, + order: 'priority', + include: { + relation: 'tag', + scope: {fields: ['name']} } - - if (isOnAll) - array.push(value); + } + }, { + relation: 'inbounds', + scope: { + fields: ['available', 'dated', 'tableId'], + where: inboundWhere, + order: 'dated DESC', + include: { + relation: 'buy', + scope: {fields: ['id', 'price3']} + }, + } } + ] + }); - return array; + 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}; + }; }; + +// Array functions + +function sum(array, key) { + if (!Array.isArray(array)) return 0; + return array.reduce((a, c) => a + c[key], 0); +} + +function toMap(objects, key) { + let map = new Map(); + for (let object of objects) + map.set(object[key], object); + return map; +} + +function toMultiMap(objects, key) { + let map = new Map(); + for (let object of objects) { + let value = map.get(object[key]); + if (!value) map.set(object[key], value = []); + value.push(object); + } + return map; +} + +function toSet(objects, key) { + let set = new Set(); + for (let object of objects) + set.add(object[key]); + return set; +} + +function toValues(objects, key) { + return [...toSet(objects, key)]; +} + +function intersect(sets) { + if (!sets.length) return []; + let array = []; + let mySets = sets.slice(0); + let firstSet = mySets.shift(); + + for (let value of firstSet) { + let isOnAll = true; + + for (let set of mySets) + if (!set.has(value)) { + isOnAll = false; + break; + } + + if (isOnAll) + array.push(value); + } + + return array; +} diff --git a/back/common/models/user.js b/back/common/models/user.js index a6e6a77..a0b9fcc 100644 --- a/back/common/models/user.js +++ b/back/common/models/user.js @@ -2,45 +2,64 @@ const app = require('../../server/server'); const loopback = require('loopback'); const path = require('path'); -const config = { - proto: 'http', - host: 'localhost', - port: 3000, - from: 'nocontestar@verdnatura.es' - // app.dataSources.email.settings.transports[0].auth.user -}; +function getUrl() { + return app.get('rootUrl') || app.get('url'); +} + +function getFrom() { + return app.dataSources.email.settings.transports[0].auth.from; +} + +class ValidationError extends Error { + constructor(message) { + super(message); + this.name = 'ValidationError'; + this.statusCode = 422; + } +} module.exports = function (Self) { - const hostBase = `${config.proto}://${config.host}` - const urlBase = `${hostBase}:8080`; - const apiBase = `${hostBase}:3000/api`; - Self.afterRemote('create', async function(ctx, instance) { + const url = new URL(getUrl()); const options = { type: 'email', to: instance.email, - from: config.from, + from: getFrom(), subject: 'Thanks for registering', template: path.resolve(__dirname, '../../views/verify.ejs'), - redirect: `${urlBase}/#/login?emailConfirmed`, + redirect: `${getUrl()}#/login/${instance.email}?emailConfirmed`, + host: url.hostname, + port: url.port, + protocol: url.protocol.split(':')[0], user: Self }; - const res = await instance.verify(options); - console.log('> verification email sent:', res); + instance.verify(options) + .then(res => console.log('> Verification email sent:', res)); }); + Self.validatePassword = function(password) { + if (!password) { + throw new ValidationError('passwordEmpty'); + } + + const pattern = new RegExp('(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.{6,})') + if (!pattern.test(password)) { + throw new ValidationError('passwordRequeriments'); + } + }; + Self.on('resetPasswordRequest', async function(info) { const renderer = loopback.template(path.resolve(__dirname, '../../views/reset-password.ejs')); const html = renderer({ - url: `${urlBase}/#/reset-password?access_token=${info.accessToken.id}` + url: `${getUrl()}#/reset-password?access_token=${info.accessToken.id}` }); await app.models.Email.send({ to: info.email, - from: config.from, + from: getFrom(), subject: 'Password reset', html }); - console.log('> sending password reset email to:', info.email); + console.log('> Sending password reset email to:', info.email); }); }; diff --git a/back/common/models/user.json b/back/common/models/user.json index 7110845..a0831b9 100644 --- a/back/common/models/user.json +++ b/back/common/models/user.json @@ -1,10 +1,15 @@ { "name": "user", "base": "User", + "options": { + "mysql": { + "table": "salix.user" + } + }, "idInjection": true, "properties": {}, "restrictResetPasswordTokenScope": true, - "emailVerificationRequired": true, + "emailVerificationRequired": false, "validations": [], "relations": {}, "acls": [ diff --git a/back/common/models/vn-model.js b/back/common/models/vn-model.js new file mode 100644 index 0000000..e2bf54f --- /dev/null +++ b/back/common/models/vn-model.js @@ -0,0 +1,58 @@ + +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; + +module.exports = function(Self) { + Self.ParameterizedSQL = ParameterizedSQL; + + Object.assign(Self, { + setup() { + Self.super_.setup.call(this); + }, + + rewriteDbError(replaceErrFunc) { + function replaceErr(err, replaceErrFunc) { + if (Array.isArray(err)) { + let errs = []; + for (let e of err) + errs.push(replaceErrFunc(e)); + return errs; + } + return replaceErrFunc(err); + } + + function rewriteMethod(methodName) { + const realMethod = this[methodName]; + return async(data, options, cb) => { + if (options instanceof Function) { + cb = options; + options = null; + } + + try { + const result = await realMethod.call(this, data, options); + + if (cb) cb(null, result); + else return result; + } catch (err) { + let myErr = replaceErr(err, replaceErrFunc); + if (cb) cb(myErr); + else + throw myErr; + } + }; + } + + this.once('attached', () => { + this.remove = + this.deleteAll = + this.destroyAll = rewriteMethod.call(this, 'remove'); + this.upsert = rewriteMethod.call(this, 'upsert'); + this.create = rewriteMethod.call(this, 'create'); + }); + }, + + rawSql(query, params, options, cb) { + return this.dataSource.connector.executeP(query, params, options, cb); + } + }); +}; diff --git a/back/common/models/vn-model.json b/back/common/models/vn-model.json new file mode 100644 index 0000000..ab3718e --- /dev/null +++ b/back/common/models/vn-model.json @@ -0,0 +1,5 @@ +{ + "name": "VnModel", + "base": "PersistedModel", + "validateUpsert": true +} diff --git a/back/common/models/zone.js b/back/common/models/zone.js new file mode 100644 index 0000000..4ff821b --- /dev/null +++ b/back/common/models/zone.js @@ -0,0 +1,33 @@ + +module.exports = function (Self) { + Self.remoteMethod('getEventsForAddress', { + description: 'Returns delivery days for a postcode', + accepts: [ + { + arg: 'geoFk', + type: 'Number', + description: 'The geo id' + }, { + arg: 'agencyModeFk', + type: 'Number', + description: 'The agency mode id' + } + ], + returns: { + type: 'Object', + root: true + }, + http: { + path: `/getEvents`, + verb: 'GET' + } + }); + + Self.getEvents = async(geoFk, agencyModeFk) => { + let [events, exclusions] = await Self.rawSql( + `CALL zone_getEvents(?, ?)`, + [geoFk, agencyModeFk] + ); + return {events, exclusions}; + }; +}; diff --git a/back/common/models/zone.json b/back/common/models/zone.json new file mode 100644 index 0000000..83cebd5 --- /dev/null +++ b/back/common/models/zone.json @@ -0,0 +1,9 @@ +{ + "name": "Zone", + "base": "VnModel", + "options": { + "mysql": { + "table": "zone" + } + } +} diff --git a/back/locales/en.json b/back/locales/en.json new file mode 100644 index 0000000..138730d --- /dev/null +++ b/back/locales/en.json @@ -0,0 +1,11 @@ +{ + "login failed": "Login failed", + "is blank": "Cannot be blank", + "is invalid": "Invalid value", + "can't be blank": "Field cannot be blank", + "invalidData": "Invalid data", + "invalidEmail": "Invalid email", + "passwordEmpty": "Password cannot be empty", + "passwordRequeriments": "Password doesn't meet requirements", + "notUniqueEmail": "User already exists" +} \ No newline at end of file diff --git a/back/locales/es.json b/back/locales/es.json new file mode 100644 index 0000000..cab89f3 --- /dev/null +++ b/back/locales/es.json @@ -0,0 +1,17 @@ +{ + "login failed": "Usuario o contraseña incorrectos", + "is blank": "No puede estar vacío", + "is invalid": "Valor inválido", + "can't be blank": "El campo no puede estar vacío", + "invalidData": "Los datos son inválidos", + "invalidEmail": "Correo inválido", + "passwordEmpty": "La contraseña no puede estar vacía", + "passwordRequeriments": "La contraseña no cumple los requisitos", + "notUniqueEmail": "El usuario ya existe", + "could not find a model with id checkout": "could not find a model with id checkout", + "Unknown \"Order\" id \"checkout\".": "Unknown \"Order\" id \"checkout\".", + "There is no method to handle GET /Client/1437/addresses": "There is no method to handle GET /Client/1437/addresses", + "could not find a model with id configure": "could not find a model with id configure", + "Unknown \"Order\" id \"configure\".": "Unknown \"Order\" id \"configure\".", + "could not find a model with id undefined": "could not find a model with id undefined" +} \ No newline at end of file diff --git a/back/package-lock.json b/back/package-lock.json index afce5fa..8a5db78 100644 --- a/back/package-lock.json +++ b/back/package-lock.json @@ -1839,6 +1839,26 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" }, + "i18n": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/i18n/-/i18n-0.9.1.tgz", + "integrity": "sha512-ERo9WloOP2inRsJzAlzn4JDm3jvX7FW1+KB/JGXTzUVzi9Bsf4LNLXUQTMgM/aze4LNW/kvmxQX6bzg5UzqMJw==", + "requires": { + "debug": "*", + "make-plural": "^6.2.1", + "math-interval-parser": "^2.0.1", + "messageformat": "^2.3.0", + "mustache": "^4.0.1", + "sprintf-js": "^1.1.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + } + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2715,6 +2735,11 @@ } } }, + "make-plural": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-6.2.1.tgz", + "integrity": "sha512-AmkruwJ9EjvyTv6AM8MBMK3TAeOJvhgTv5YQXzF0EP2qawhpvMjDpHvsdOIIT0Vn+BB0+IogmYZ1z+Ulm/m0Fg==" + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -2723,6 +2748,11 @@ "p-defer": "^1.0.0" } }, + "math-interval-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/math-interval-parser/-/math-interval-parser-2.0.1.tgz", + "integrity": "sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA==" + }, "md5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", @@ -2758,6 +2788,42 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, + "messageformat": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-2.3.0.tgz", + "integrity": "sha512-uTzvsv0lTeQxYI2y1NPa1lItL5VRI8Gb93Y2K2ue5gBPyrbJxfDi/EYWxh2PKv5yO42AJeeqblS9MJSh/IEk4w==", + "requires": { + "make-plural": "^4.3.0", + "messageformat-formatters": "^2.0.1", + "messageformat-parser": "^4.1.2" + }, + "dependencies": { + "make-plural": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz", + "integrity": "sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA==", + "requires": { + "minimist": "^1.2.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "optional": true + } + } + }, + "messageformat-formatters": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/messageformat-formatters/-/messageformat-formatters-2.0.1.tgz", + "integrity": "sha512-E/lQRXhtHwGuiQjI7qxkLp8AHbMD5r2217XNe/SREbBlSawe0lOqsFb7rflZJmlQFSULNLIqlcjjsCPlB3m3Mg==" + }, + "messageformat-parser": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/messageformat-parser/-/messageformat-parser-4.1.3.tgz", + "integrity": "sha512-2fU3XDCanRqeOCkn7R5zW5VQHWf+T3hH65SzuqRvjatBK7r4uyFa5mEX+k6F9Bd04LVM5G4/BHBTUJsOdW7uyg==" + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -2976,6 +3042,11 @@ "safe-buffer": "^5.1.2" } }, + "mustache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.0.1.tgz", + "integrity": "sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA==" + }, "mute-stream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", diff --git a/back/package.json b/back/package.json index 1f38430..6346024 100644 --- a/back/package.json +++ b/back/package.json @@ -6,7 +6,6 @@ "node": ">=6" }, "scripts": { - "lint": "eslint .", "start": "node .", "posttest": "npm run lint && npm audit" }, @@ -15,6 +14,7 @@ "cors": "^2.5.2", "fs-extra": "^8.1.0", "helmet": "^3.22.0", + "i18n": "^0.9.1", "loopback": "^3.27.0", "loopback-boot": "^2.28.0", "loopback-component-explorer": "^6.5.1", diff --git a/back/server/boot/connector.js b/back/server/boot/connector.js new file mode 100644 index 0000000..bab7091 --- /dev/null +++ b/back/server/boot/connector.js @@ -0,0 +1,17 @@ + +module.exports = function enableAuthentication(server) { + const connector = server.dataSources.vn.connector + + connector.executeAsync = function(query, params, options = {}, cb) { + return new Promise((resolve, reject) => { + this.execute(query, params, options, (error, response) => { + if (cb) + cb(error, response); + if (error) + reject(error); + else + resolve(response); + }); + }); + } +}; diff --git a/back/server/config.local.json b/back/server/config.local.json new file mode 100644 index 0000000..dae3bb6 --- /dev/null +++ b/back/server/config.local.json @@ -0,0 +1,3 @@ +{ + "rootUrl": "http://localhost:8080" +} \ No newline at end of file diff --git a/back/server/middleware.json b/back/server/middleware.json index 8bac49d..cc0e9f3 100644 --- a/back/server/middleware.json +++ b/back/server/middleware.json @@ -43,6 +43,11 @@ "loopback#urlNotFound": {} }, "final:after": { - "strong-error-handler": {} + "./middleware/error-handler": {}, + "strong-error-handler": { + "params": { + "log": false + } + } } } diff --git a/back/server/middleware/error-handler.js b/back/server/middleware/error-handler.js new file mode 100644 index 0000000..110115d --- /dev/null +++ b/back/server/middleware/error-handler.js @@ -0,0 +1,28 @@ + +module.exports = function() { + return function(err, req, res, next) { + const statusCode = err.statusCode; + + switch(statusCode) { + case 422: // Validation error + if (err.details) { + let messages = err.details.messages; + for (let message in messages) { + let texts = messages[message]; + for (let i = 0; i < texts.length; i++) { + if (!texts[i]) continue; + texts[i] = req.__(texts[i]); + } + } + err.message = req.__('invalidData'); + break; + } + default: + if (statusCode >= 400 && statusCode < 500) { + err.message = req.__(err.message); + } + } + + next(err); + }; +}; diff --git a/back/server/model-config.json b/back/server/model-config.json index a890512..c38acd8 100644 --- a/back/server/model-config.json +++ b/back/server/model-config.json @@ -1,182 +1,181 @@ { - "_meta": { - "sources": [ - "loopback/common/models", - "loopback/server/models", - "../common/models", - "./models" - ], - "mixins": [ - "loopback/common/mixins", - "loopback/server/mixins", - "../common/mixins", - "./mixins" - ] - }, - "AccessToken": { - "dataSource": "vn", - "options": { - "mysql": { - "table": "salix.AccessToken" - } - } - }, - "ACL": { - "dataSource": "vn", - "options": { - "mysql": { - "table": "salix.ACL" - } - } - }, - "Role": { - "dataSource": "vn", - "options": { - "mysql": { - "table": "salix.Role" - } - } - }, - "RoleMapping": { - "dataSource": "vn", - "options": { - "mysql": { - "table": "salix.RoleMapping" - } - } - }, - "User": { - "dataSource": "vn", - "public": false - }, - "user": { - "dataSource": "db", - "public": true, - "options": { - "mysql": { - "table": "salix.user" + "_meta": { + "sources": [ + "loopback/common/models", + "loopback/server/models", + "../common/models", + "./models" + ], + "mixins": [ + "loopback/common/mixins", + "loopback/server/mixins", + "../common/mixins", + "./mixins" + ] + }, + "AccessToken": { + "dataSource": "vn", + "options": { + "mysql": { + "table": "salix.AccessToken" + } }, - "emailVerificationRequired": true + "relations": { + "user": { + "type": "belongsTo", + "model": "user", + "foreignKey": "userId" + } + } + }, + "ACL": { + "dataSource": "vn", + "options": { + "mysql": { + "table": "salix.ACL" + } + } + }, + "Role": { + "dataSource": "vn", + "options": { + "mysql": { + "table": "salix.Role" + } + } + }, + "RoleMapping": { + "dataSource": "vn", + "options": { + "mysql": { + "table": "salix.RoleMapping" + } + } + }, + "user": { + "dataSource": "vn" + }, + "Email": { + "dataSource": "email" + }, + "Account": { + "dataSource": "vn" + }, + "Address": { + "dataSource": "vn" + }, + "AgencyMode": { + "dataSource": "vn" + }, + "AlertLevel": { + "dataSource": "vn" + }, + "Buy": { + "dataSource": "vn" + }, + "Client": { + "dataSource": "vn" + }, + "Container": { + "dataSource": "storage" + }, + "Country": { + "dataSource": "vn" + }, + "DeliveryMethod": { + "dataSource": "vn" + }, + "Inbound": { + "dataSource": "vn" + }, + "Image": { + "dataSource": "vn" + }, + "ImageCollection": { + "dataSource": "vn" + }, + "ImageCollectionSize": { + "dataSource": "vn" + }, + "ItemCategory": { + "dataSource": "vn" + }, + "Item": { + "dataSource": "vn" + }, + "ItemTag": { + "dataSource": "vn" + }, + "ItemType": { + "dataSource": "vn" + }, + "Language": { + "dataSource": "vn" + }, + "Link": { + "dataSource": "vn" + }, + "MainAccountBank": { + "dataSource": "vn" + }, + "New": { + "dataSource": "vn" + }, + "NewTag": { + "dataSource": "vn" + }, + "OrderRow": { + "dataSource": "vn" + }, + "OrderTicket": { + "dataSource": "vn" + }, + "Order": { + "dataSource": "vn" + }, + "Province": { + "dataSource": "vn" + }, + "Sale": { + "dataSource": "vn" + }, + "State": { + "dataSource": "vn" + }, + "Tag": { + "dataSource": "vn" + }, + "Ticket": { + "dataSource": "vn" + }, + "TicketState": { + "dataSource": "vn" + }, + "TicketTracking": { + "dataSource": "vn" + }, + "UserPassword": { + "dataSource": "vn" + }, + "UserSession": { + "dataSource": "vn" + }, + "Visit": { + "dataSource": "vn" + }, + "VisitAccess": { + "dataSource": "vn" + }, + "VisitAgent": { + "dataSource": "vn" + }, + "VisitUser": { + "dataSource": "vn" + }, + "Warehouse": { + "dataSource": "vn" + }, + "Zone": { + "dataSource": "vn" } - }, - "Email": { - "dataSource": "email" - }, - "Account": { - "dataSource": "vn" - }, - "Address": { - "dataSource": "vn" - }, - "AgencyMode": { - "dataSource": "vn" - }, - "AlertLevel": { - "dataSource": "vn" - }, - "Buy": { - "dataSource": "vn" - }, - "Client": { - "dataSource": "vn" - }, - "Container": { - "dataSource": "storage" - }, - "Country": { - "dataSource": "vn" - }, - "DeliveryMethod": { - "dataSource": "vn" - }, - "Inbound": { - "dataSource": "vn" - }, - "Image": { - "dataSource": "vn" - }, - "ImageCollection": { - "dataSource": "vn" - }, - "ImageCollectionSize": { - "dataSource": "vn" - }, - "ItemCategory": { - "dataSource": "vn" - }, - "Item": { - "dataSource": "vn" - }, - "ItemTag": { - "dataSource": "vn" - }, - "ItemType": { - "dataSource": "vn" - }, - "Language": { - "dataSource": "vn" - }, - "Link": { - "dataSource": "vn" - }, - "MainAccountBank": { - "dataSource": "vn" - }, - "New": { - "dataSource": "vn" - }, - "NewTag": { - "dataSource": "vn" - }, - "OrderRow": { - "dataSource": "vn" - }, - "OrderTicket": { - "dataSource": "vn" - }, - "Order": { - "dataSource": "vn" - }, - "Province": { - "dataSource": "vn" - }, - "Sale": { - "dataSource": "vn" - }, - "State": { - "dataSource": "vn" - }, - "Tag": { - "dataSource": "vn" - }, - "Ticket": { - "dataSource": "vn" - }, - "TicketState": { - "dataSource": "vn" - }, - "TicketTracking": { - "dataSource": "vn" - }, - "UserPassword": { - "dataSource": "vn" - }, - "UserSession": { - "dataSource": "vn" - }, - "Visit": { - "dataSource": "vn" - }, - "VisitAccess": { - "dataSource": "vn" - }, - "VisitAgent": { - "dataSource": "vn" - }, - "VisitUser": { - "dataSource": "vn" - }, - "Warehouse": { - "dataSource": "vn" - } } diff --git a/back/server/server.js b/back/server/server.js index 7bfa487..db8267f 100644 --- a/back/server/server.js +++ b/back/server/server.js @@ -5,19 +5,23 @@ 'use strict'; -var loopback = require('loopback'); -var boot = require('loopback-boot'); +const loopback = require('loopback'); +const boot = require('loopback-boot'); +const i18n = require('i18n'); -var app = module.exports = loopback(); +const app = module.exports = loopback(); + +i18n.configure({directory: `${__dirname}/../locales`}); +app.use(i18n.init); app.start = function() { // start the web server return app.listen(function() { app.emit('started'); - var baseUrl = app.get('url').replace(/\/$/, ''); + const baseUrl = app.get('url').replace(/\/$/, ''); console.log('Web server listening at: %s', baseUrl); if (app.get('loopback-component-explorer')) { - var explorerPath = app.get('loopback-component-explorer').mountPath; + const explorerPath = app.get('loopback-component-explorer').mountPath; console.log('Browse your REST API at %s%s', baseUrl, explorerPath); } }); diff --git a/quasar.conf.js b/quasar.conf.js index 83e9cf7..6f0d0be 100644 --- a/quasar.conf.js +++ b/quasar.conf.js @@ -65,7 +65,11 @@ module.exports = function (ctx) { 'QSelect', 'QSeparator', 'QSlideTransition', + 'QSpace', 'QSpinner', + 'QStepper', + 'QStep', + 'QStepperNavigation', 'QTab', 'QTabs', 'QTabPanel', diff --git a/src/boot/axios.js b/src/boot/axios.js index bb9e4fa..bc0f8a5 100644 --- a/src/boot/axios.js +++ b/src/boot/axios.js @@ -7,9 +7,14 @@ export default async ({ app, Vue }) => { axios.interceptors.request.use(function (config) { const $state = Vue.prototype.$state + if ($state.user.loggedIn) { config.headers.Authorization = $state.user.token } + if (config.filter) { + if (!config.params) config.params = {} + config.params.filter = config.filter + } return config }) } diff --git a/src/components/Page.js b/src/components/Page.js index 6cde374..5dd5e91 100644 --- a/src/components/Page.js +++ b/src/components/Page.js @@ -1,10 +1,12 @@ import Toolbar from './Toolbar' +import Portal from './Portal' import LbScroll from './LbScroll' import FullImage from './FullImage' export default { components: { Toolbar, + Portal, LbScroll, FullImage }, diff --git a/src/components/Portal.vue b/src/components/Portal.vue new file mode 100644 index 0000000..358ec3b --- /dev/null +++ b/src/components/Portal.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/i18n/en-us/index.js b/src/i18n/en-us/index.js index 8cea978..96da0c1 100644 --- a/src/i18n/en-us/index.js +++ b/src/i18n/en-us/index.js @@ -75,6 +75,10 @@ export default { ] }, + // errors + internalServerError: 'Internal server error', + somethingWentWrong: 'Something went wrong', + // Layout login: 'Login', logout: 'Logout', @@ -88,12 +92,13 @@ export default { notRememberPassword: 'I don\'t remember my password', inputEmail: 'Input email', inputPassword: 'Input password', + emailConfirmedSuccessfully: 'E-mail confirmed succesfully', // register register: 'Register', fillData: 'Fill the data', notYetUser: 'You are not yet a user?', - userRegistered: 'User registered successfully', + userRegistered: 'User registered successfully, we\'ve sent you an e-mail to verify your account', repeatPasswordError: 'Passwords doesn\'t match', // recover diff --git a/src/i18n/es-es/index.js b/src/i18n/es-es/index.js index 786e4e6..160fd94 100644 --- a/src/i18n/es-es/index.js +++ b/src/i18n/es-es/index.js @@ -1,5 +1,5 @@ export default { - // global + // Global search: 'Buscar', accept: 'Aceptar', cancel: 'Cancelar', @@ -75,12 +75,15 @@ export default { ] }, - // layout + // Errors + internalServerError: 'Error interno del servidor', + somethingWentWrong: 'Algo salió mal', + + // MainLayout login: 'Iniciar sesión', logout: 'Cerrar sesión', - visitor: 'Visitante', - // login + // Login enter: 'Entrar', email: 'Correo electrónico', password: 'Contraseña', @@ -88,25 +91,26 @@ export default { notRememberPassword: 'No recuerdo mi contraseña', inputEmail: 'Introduce el correo electrónico', inputPassword: 'Introduce la contraseña', + emailConfirmedSuccessfully: 'Correo verificado correctamente', - // register - register: 'Registrarse', + // Register + register: 'Registrarme', fillData: 'Rellena los datos', notYetUser: '¿Todavía no eres usuario?', - userRegistered: 'Usuario registrado correctamente', + userRegistered: 'Usuario registrado correctamente, te hemos enviado un correo para verificar tu dirección', repeatPasswordError: 'Las contraseñas no coinciden', - // recover + // Recover rememberPassword: 'Recordar contraseña', dontWorry: '¡No te preocupes!', weSendEmail: 'Te enviaremos un correo para restablecer tu contraseña', weHaveSentEmailToRecover: 'Te hemos enviado un correo donde podrás recuperar tu contraseña', - // reset + // Reset resetPassword: 'Restaurar contraseña', passwordResetSuccessfully: 'Contraseña modificada correctamente', - // menu + // Menu home: 'Inicio', catalog: 'Catálogo', orders: 'Pedidos', @@ -128,7 +132,7 @@ export default { addresses: 'Direcciones', addressEdit: 'Editar dirección', - // home + // Home recentNews: 'Noticias recientes', startOrder: 'Empezar pedido', @@ -147,7 +151,7 @@ export default { n1InPrice: 'Nº1 en precio', ourBigVolumeAllows: 'Nuestro gran volumen nos permite ofrecerte los mejores precios', - // catalog + // Catalog more: 'Más', noItemsFound: 'No se han encontrado artículos', pleaseSetFilter: 'Por favor, establece un filtro usando el menú de la derecha', @@ -170,19 +174,19 @@ export default { siceAsc: 'Medida ascendente', sizeDesc: 'Medida descendente', - // orders + // Orders pending: 'Pendientes', confirmed: 'Confirmados', - // orders/pending + // Pending pendingConfirmtion: 'Pendientes de confirmar', noOrdersFound: 'No se han encontrado pedidos', - // orders/confirmed + // Confirmed ordersMadeAt: 'Pedidos realizados en', packages: '{n} bultos', - // order + // Order total: 'Total', confirm: 'Confirmar', delivery: 'Fecha de entrega', @@ -191,7 +195,24 @@ export default { warehouse: 'Almacén', configure: 'Configurar', - // order/checkout + // OrderRows + + rows: 'Lineas', + + // OrderConfigure + + deliveryMethod: 'Método de entrega', + pickupDate: 'Fecha de recogida', + deliveryAgency: 'Agencia de transporte', + pickupStore: 'Almacén de recogida', + deliveryAddress: 'Dirección de entrega', + optionalAddress: 'Dirección (Opcional)', + optionalAddressInfo: 'Para pedidos de recogida la dirección se utiliza como referencia cuando se dispone de varias direcciones y evitar que pedidos de tiendas diferentes se fusionen', + addressHint: 'Las fechas de entrega varian en función de la dirección', + storePickup: 'Recogida en almacén', + homeDelivery: 'Entrega a domicilio', + + // OrderCheckout checkout: 'Finalizar pedido', orderSummary: 'Resumen del pedido', accountsSummary: 'Resumen de cuentas', @@ -217,13 +238,13 @@ export default { youExceededCredit: 'Has excedido tu crédito, por favor realiza el pago para que podamos preparar tu pedido.', notes: 'Notas', - // conditions + // Conditions conditionsDesc: 'Te aseguramos que el pedido llegara a tu casa/tienda en menos de 24/48 horas (Dependiendo de en que zona te encuentres).', - // about + // About aboutDesc: 'Verdnatura te ofrece todos los servicios que necesita tu floristería.', - // connections + // Connections nConnections: '{0} connexiones', refreshRate: 'Frecuencia de actualización', lastAction: 'Última acción', @@ -231,27 +252,27 @@ export default { nSeconds: '{0} segundos', dontRefresh: 'No refrescar', - // accessLog + // AccessLog accessLog: 'Registro de accesos', - // visits + // Visits visitsCount: '{0} visitas, {1} nuevas', - // new + // New title: 'Título', image: 'Imagen', tag: 'Etiqueta', priority: 'Prioridad', text: 'Texto', - // images + // Images collection: 'Colección', updateMatchingId: 'Actualizar ítems con id coincidente', uploadAutomatically: 'Subir automáticamente', imagesUploadSuccess: 'Imágenes subidas correctamente', imagesUploadFailed: 'Algunas imágenes no se ha podido subir', - // user + // User userName: 'Nombre de usuario', nickname: 'Nombre a mostrar', language: 'Idioma', @@ -270,14 +291,14 @@ export default { passwordsDontMatch: 'Las contraseñas no coinciden', passwordChanged: '¡Contraseña modificada correctamente!', - // addresses + // 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 + // Address consignatary: 'Consignatario', street: 'Dirección', city: 'City', diff --git a/src/layouts/LoginLayout.vue b/src/layouts/LoginLayout.vue index eab4996..bd71b2e 100644 --- a/src/layouts/LoginLayout.vue +++ b/src/layouts/LoginLayout.vue @@ -1,5 +1,5 @@ + + diff --git a/src/pages/OrderView.vue b/src/pages/Webshop/Pending/Rows.vue similarity index 99% rename from src/pages/OrderView.vue rename to src/pages/Webshop/Pending/Rows.vue index f71639c..11c62ac 100644 --- a/src/pages/OrderView.vue +++ b/src/pages/Webshop/Pending/Rows.vue @@ -77,7 +77,7 @@ import Page from 'components/Page' export default { - name: 'OrderView', + name: 'OrdersPendingRows', mixins: [Page], data () { return { diff --git a/src/pages/Order.vue b/src/pages/Webshop/Pending/View.vue similarity index 93% rename from src/pages/Order.vue rename to src/pages/Webshop/Pending/View.vue index 9f069d6..0c01b0a 100644 --- a/src/pages/Order.vue +++ b/src/pages/Webshop/Pending/View.vue @@ -26,12 +26,9 @@ :label="$t('agency')" :value="order.agencyMode.name" readonly/> - import('pages/Login.vue') + path: '/login/:email?', + component: () => import('pages/Login/Login.vue') }, { name: 'register', path: '/register', - component: () => import('pages/Register.vue') + component: () => import('pages/Login/Register.vue') }, { name: 'rememberPassword', path: '/remember-password', - component: () => import('pages/RememberPassword.vue') + component: () => import('pages/Login/RememberPassword.vue') }, { name: 'resetPassword', path: '/reset-password', - component: () => import('pages/ResetPassword.vue') + component: () => import('pages/Login/ResetPassword.vue') } ] }, { @@ -29,115 +29,119 @@ const routes = [ { name: '', path: '', - component: () => import('pages/Index.vue') + component: () => import('pages/Cms/Home.vue') }, { name: 'home', path: '/home', - component: () => import('pages/Index.vue') + component: () => import('pages/Cms/Home.vue') }, { name: 'catalog', path: '/catalog/:category?/:type?', - component: () => import('pages/Catalog.vue') + component: () => import('pages/Webshop/Catalog.vue') }, { name: 'orders', path: '/orders', - component: () => import('pages/Orders.vue'), + component: () => import('pages/Webshop/Orders.vue'), children: [ { name: 'pending', path: 'pending', - component: () => import('pages/Pending.vue') + component: () => import('pages/Webshop/Pending.vue') }, { name: 'confirmed', path: 'confirmed', - component: () => import('pages/Confirmed.vue') + component: () => import('pages/Webshop/Confirmed.vue') } ] }, { name: 'order', path: '/order/:id', - component: () => import('pages/Order.vue'), + component: () => import('pages/Webshop/Pending/View.vue'), children: [ { - name: 'view', - path: 'view', - component: () => import('pages/OrderView.vue') + name: '', + path: '', + component: () => import('pages/Webshop/Pending/Rows.vue') + }, { + name: 'configure', + path: 'configure', + component: () => import('pages/Webshop/Pending/Configure.vue') }, { name: 'checkout', path: 'checkout', - component: () => import('pages/OrderCheckout.vue') + component: () => import('pages/Webshop/Pending/Checkout.vue') } ] }, { name: 'ticket', path: '/ticket/:id', - component: () => import('pages/Ticket.vue') + component: () => import('pages/Webshop/Confirmed/View.vue') }, { name: 'conditions', path: '/conditions', - component: () => import('pages/Conditions.vue') + component: () => import('pages/Cms/Conditions.vue') }, { name: 'about', path: '/about', - component: () => import('pages/About.vue') + component: () => import('pages/Cms/About.vue') }, { name: 'admin', path: '/admin', - component: () => import('pages/Admin.vue'), + component: () => import('pages/Admin/Index.vue'), children: [ { name: 'panel', path: 'panel', - component: () => import('pages/Panel.vue') + component: () => import('pages/Admin/Panel.vue') }, { name: 'users', path: 'users', - component: () => import('pages/Users.vue') + component: () => import('pages/Admin/Users.vue') }, { name: 'connections', path: 'connections', - component: () => import('pages/Connections.vue') + component: () => import('pages/Admin/Connections.vue') }, { name: 'visits', path: 'visits', - component: () => import('pages/Visits.vue') + component: () => import('pages/Admin/Visits.vue') }, { name: 'news', path: 'news', - component: () => import('pages/News.vue') + component: () => import('pages/Admin/News.vue') }, { name: 'images', path: 'images', - component: () => import('pages/Images.vue') + component: () => import('pages/Admin/Images.vue') }, { name: 'items', path: 'items', - component: () => import('pages/Items.vue') + component: () => import('pages/Admin/Items.vue') } ] }, { name: 'accessLog', path: '/access-log/:user', - component: () => import('pages/AccessLog.vue') + component: () => import('pages/Admin/AccessLog.vue') }, { name: 'newEdit', path: '/new/:id?', - component: () => import('pages/New.vue') + component: () => import('pages/Admin/New.vue') }, { name: 'user', path: '/user', - component: () => import('pages/User.vue'), + component: () => import('pages/Account/User.vue'), props: route => ({ changePassword: String(route.query.changePassword) === 'true' }) }, { name: 'addresses', path: '/addresses', - component: () => import('pages/Addresses.vue') + component: () => import('pages/Account/Addresses.vue') }, { name: 'addressEdit', path: '/address/:id?', - component: () => import('pages/Address.vue') + component: () => import('pages/Account/Address.vue') } ] }