Refactor, bugfix, i18n

This commit is contained in:
Juan Ferrer 2020-06-29 13:31:48 +02:00
parent 85a2b667c2
commit 65097c3f58
56 changed files with 1204 additions and 628 deletions

View File

@ -1 +0,0 @@
/client/

View File

@ -1,368 +1,368 @@
const warehouseIds = [1, 44]; const warehouseIds = [1, 44];
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('calcCatalog', { Self.remoteMethod('calcCatalog', {
description: 'Get the available and prices list for an item', description: 'Get the available and prices list for an item',
accessType: 'READ', accessType: 'READ',
accepts: [ accepts: [
{ {
arg: 'id', arg: 'id',
type: 'Number', type: 'Number',
description: 'The item id' description: 'The item id'
}, { }, {
arg: 'dated', arg: 'dated',
type: 'Date', type: 'Date',
description: 'The date' description: 'The date'
}, { }, {
arg: 'addressFk', arg: 'addressFk',
type: 'Number', type: 'Number',
description: 'The address id' description: 'The address id'
}, { }, {
arg: 'agencyModeFk', arg: 'agencyModeFk',
type: 'Number', type: 'Number',
description: 'The agency id' description: 'The agency id'
} }
], ],
returns: { returns: {
type: ['Object'], type: ['Object'],
description: 'The item available and prices list', description: 'The item available and prices list',
root: true, root: true,
}, },
http: { http: {
path: `/:id/calcCatalog`, path: `/:id/calcCatalog`,
verb: 'GET' verb: 'GET'
} }
}); });
Self.calcCatalog = (id, dated, addressFk, agencyModeFk, cb) => { Self.calcCatalog = (id, dated, addressFk, agencyModeFk, cb) => {
Self.dataSource.connector.query( Self.dataSource.connector.query(
`CALL hedera.item_calcCatalog(?, ?, ?, ?)`, `CALL hedera.item_calcCatalog(?, ?, ?, ?)`,
[id, dated, addressFk, agencyModeFk], [id, dated, addressFk, agencyModeFk],
(err, res) => { (err, res) => {
if (err) return cb(err) if (err) return cb(err)
return cb(null, res[0]) 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', { // Applies base filters
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) => { if (/^[0-9]+$/.test(search)) {
let $ = Self.app.models; itemIds = [parseInt(search)];
let itemIds; } else {
let inbounds = await $.Inbound.find({
fields: ['itemFk'],
where: inboundWhere
});
itemIds = toValues(inbounds, 'itemFk');
let inboundWhere = { if (categoryFk || typeFk || search) {
warehouseFk: {inq: warehouseIds}, let where = {
available: {gt: 0}, id: {inq: itemIds}
dated: {lte: dated},
and: [
{or: [
{expired: {gt: dated}},
{expired: null}
]}
]
}; };
// Applies base filters if (typeFk) {
where.typeFk = typeFk;
if (/^[0-9]+$/.test(search)) { } else if (categoryFk) {
itemIds = [parseInt(search)]; let types = await $.ItemType.find({
} else { fields: ['id'],
let inbounds = await $.Inbound.find({ where: {categoryFk}
fields: ['itemFk'], });
where: inboundWhere where.typeFk = {inq: toValues(types, 'id')};
});
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);
}
} }
// Applies tag filters if (search)
where.longName = {like: `%${search}%`};
let baseItemIds = itemIds; let filter = {
let tagItems = []; fields: ['id'],
let tagFilterIds = []; where
};
if (tagFilter && tagFilter.length) { let items = await Self.find(filter);
for (let filter of tagFilter) { itemIds = items.map(i => i.id);
let cond; }
let values = filter.values; }
if (values.length) // Applies tag filters
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}};
let where = { let baseItemIds = itemIds;
itemFk: {inq: itemIds}, let tagItems = [];
tagFk: filter.tagFk let tagFilterIds = [];
};
Object.assign(where, cond);
let itemTags = await $.ItemTag.find({ if (tagFilter && tagFilter.length) {
fields: ['itemFk'], for (let filter of tagFilter) {
where let cond;
}); let values = filter.values;
tagItems.push(toSet(itemTags, 'itemFk'));
tagFilterIds.push(filter.tagFk);
}
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 where = {
itemFk: {inq: itemIds},
tagFk: filter.tagFk
};
Object.assign(where, cond);
let tags = []; let itemTags = await $.ItemTag.find({
fields: ['itemFk'],
where
});
tagItems.push(toSet(itemTags, 'itemFk'));
tagFilterIds.push(filter.tagFk);
}
if (typeFk || search) { itemIds = intersect(tagItems);
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++) { // Obtains distinct tags and it's distinct values
let tagFk = tagFilter[i].tagFk;
let itemIds;
if (tagItems.length > 1) { let tags = [];
let siblings = tagItems.filter(v => v != tagItems[i]);
itemIds = intersect(siblings);
} else
itemIds = baseItemIds;
let tagValues = await $.ItemTag.find({ if (typeFk || search) {
fields: ['value', 'intValue', 'priority'], let tagValues = await $.ItemTag.find({
where: { fields: ['tagFk', 'value', 'intValue', 'priority'],
itemFk: {inq: itemIds}, where: {
tagFk: tagFk itemFk: {inq: itemIds},
}, tagFk: {nin: tagFilterIds}
order: 'value' },
}); 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()]; if (tagItems.length > 1) {
tags = await $.Tag.find({ let siblings = tagItems.filter(v => v != tagItems[i]);
fields: ['id', 'name', 'isQuantitative', 'unit'], itemIds = intersect(siblings);
where: { } else
id: {inq: tagIds} itemIds = baseItemIds;
}
});
for (let tag of tags) { let tagValues = await $.ItemTag.find({
let tagValues = tagValueMap.get(tag.id); fields: ['value', 'intValue', 'priority'],
where: {
let filter = tagFilter && tagFilter.find(i => i.tagFk == tag.id); itemFk: {inq: itemIds},
filter = filter && filter.values; tagFk: tagFk
},
let values = toSet(tagValues, 'value'); order: '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']}
},
}
}
]
}); });
for (let item of items) { tagValueMap.set(tagFk, tagValues);
item.inbound = item.inbounds()[0]; }
item.buy = item.inbound && item.inbound.buy();
item.available = sum(item.inbounds(), 'available'); 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}; Object.assign(tag, {
}; values: [...values],
filter
// 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) { // Obtains items data
let map = new Map();
for (let object of objects)
map.set(object[key], object);
return map;
}
function toMultiMap(objects, key) { let items = await Self.find({
let map = new Map(); fields: ['id', 'longName', 'subName', 'image'],
for (let object of objects) { where: {id: {inq: itemIds}},
let value = map.get(object[key]); limit,
if (!value) map.set(object[key], value = []); order,
value.push(object); include: [
} {
return map; relation: 'tags',
} scope: {
fields: ['value', 'tagFk'],
function toSet(objects, key) { where: {priority: {gt: 4}},
let set = new Set(); order: 'priority',
for (let object of objects) include: {
set.add(object[key]); relation: 'tag',
return set; scope: {fields: ['name']}
}
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); 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;
}

View File

@ -2,45 +2,64 @@ const app = require('../../server/server');
const loopback = require('loopback'); const loopback = require('loopback');
const path = require('path'); const path = require('path');
const config = { function getUrl() {
proto: 'http', return app.get('rootUrl') || app.get('url');
host: 'localhost', }
port: 3000,
from: 'nocontestar@verdnatura.es' function getFrom() {
// app.dataSources.email.settings.transports[0].auth.user 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) { 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) { Self.afterRemote('create', async function(ctx, instance) {
const url = new URL(getUrl());
const options = { const options = {
type: 'email', type: 'email',
to: instance.email, to: instance.email,
from: config.from, from: getFrom(),
subject: 'Thanks for registering', subject: 'Thanks for registering',
template: path.resolve(__dirname, '../../views/verify.ejs'), 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 user: Self
}; };
const res = await instance.verify(options); instance.verify(options)
console.log('> verification email sent:', res); .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) { Self.on('resetPasswordRequest', async function(info) {
const renderer = loopback.template(path.resolve(__dirname, '../../views/reset-password.ejs')); const renderer = loopback.template(path.resolve(__dirname, '../../views/reset-password.ejs'));
const html = renderer({ 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({ await app.models.Email.send({
to: info.email, to: info.email,
from: config.from, from: getFrom(),
subject: 'Password reset', subject: 'Password reset',
html html
}); });
console.log('> sending password reset email to:', info.email); console.log('> Sending password reset email to:', info.email);
}); });
}; };

View File

@ -1,10 +1,15 @@
{ {
"name": "user", "name": "user",
"base": "User", "base": "User",
"options": {
"mysql": {
"table": "salix.user"
}
},
"idInjection": true, "idInjection": true,
"properties": {}, "properties": {},
"restrictResetPasswordTokenScope": true, "restrictResetPasswordTokenScope": true,
"emailVerificationRequired": true, "emailVerificationRequired": false,
"validations": [], "validations": [],
"relations": {}, "relations": {},
"acls": [ "acls": [

View File

@ -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);
}
});
};

View File

@ -0,0 +1,5 @@
{
"name": "VnModel",
"base": "PersistedModel",
"validateUpsert": true
}

View File

@ -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};
};
};

View File

@ -0,0 +1,9 @@
{
"name": "Zone",
"base": "VnModel",
"options": {
"mysql": {
"table": "zone"
}
}
}

11
back/locales/en.json Normal file
View File

@ -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"
}

17
back/locales/es.json Normal file
View File

@ -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"
}

71
back/package-lock.json generated
View File

@ -1839,6 +1839,26 @@
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
"integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" "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": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "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": { "map-age-cleaner": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
@ -2723,6 +2748,11 @@
"p-defer": "^1.0.0" "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": { "md5": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", "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", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" "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": { "methods": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@ -2976,6 +3042,11 @@
"safe-buffer": "^5.1.2" "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": { "mute-stream": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz",

View File

@ -6,7 +6,6 @@
"node": ">=6" "node": ">=6"
}, },
"scripts": { "scripts": {
"lint": "eslint .",
"start": "node .", "start": "node .",
"posttest": "npm run lint && npm audit" "posttest": "npm run lint && npm audit"
}, },
@ -15,6 +14,7 @@
"cors": "^2.5.2", "cors": "^2.5.2",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"helmet": "^3.22.0", "helmet": "^3.22.0",
"i18n": "^0.9.1",
"loopback": "^3.27.0", "loopback": "^3.27.0",
"loopback-boot": "^2.28.0", "loopback-boot": "^2.28.0",
"loopback-component-explorer": "^6.5.1", "loopback-component-explorer": "^6.5.1",

View File

@ -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);
});
});
}
};

View File

@ -0,0 +1,3 @@
{
"rootUrl": "http://localhost:8080"
}

View File

@ -43,6 +43,11 @@
"loopback#urlNotFound": {} "loopback#urlNotFound": {}
}, },
"final:after": { "final:after": {
"strong-error-handler": {} "./middleware/error-handler": {},
"strong-error-handler": {
"params": {
"log": false
}
}
} }
} }

View File

@ -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);
};
};

View File

@ -1,182 +1,181 @@
{ {
"_meta": { "_meta": {
"sources": [ "sources": [
"loopback/common/models", "loopback/common/models",
"loopback/server/models", "loopback/server/models",
"../common/models", "../common/models",
"./models" "./models"
], ],
"mixins": [ "mixins": [
"loopback/common/mixins", "loopback/common/mixins",
"loopback/server/mixins", "loopback/server/mixins",
"../common/mixins", "../common/mixins",
"./mixins" "./mixins"
] ]
}, },
"AccessToken": { "AccessToken": {
"dataSource": "vn", "dataSource": "vn",
"options": { "options": {
"mysql": { "mysql": {
"table": "salix.AccessToken" "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"
}, },
"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"
}
} }

View File

@ -5,19 +5,23 @@
'use strict'; 'use strict';
var loopback = require('loopback'); const loopback = require('loopback');
var boot = require('loopback-boot'); 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() { app.start = function() {
// start the web server // start the web server
return app.listen(function() { return app.listen(function() {
app.emit('started'); app.emit('started');
var baseUrl = app.get('url').replace(/\/$/, ''); const baseUrl = app.get('url').replace(/\/$/, '');
console.log('Web server listening at: %s', baseUrl); console.log('Web server listening at: %s', baseUrl);
if (app.get('loopback-component-explorer')) { 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); console.log('Browse your REST API at %s%s', baseUrl, explorerPath);
} }
}); });

View File

@ -65,7 +65,11 @@ module.exports = function (ctx) {
'QSelect', 'QSelect',
'QSeparator', 'QSeparator',
'QSlideTransition', 'QSlideTransition',
'QSpace',
'QSpinner', 'QSpinner',
'QStepper',
'QStep',
'QStepperNavigation',
'QTab', 'QTab',
'QTabs', 'QTabs',
'QTabPanel', 'QTabPanel',

View File

@ -7,9 +7,14 @@ export default async ({ app, Vue }) => {
axios.interceptors.request.use(function (config) { axios.interceptors.request.use(function (config) {
const $state = Vue.prototype.$state const $state = Vue.prototype.$state
if ($state.user.loggedIn) { if ($state.user.loggedIn) {
config.headers.Authorization = $state.user.token config.headers.Authorization = $state.user.token
} }
if (config.filter) {
if (!config.params) config.params = {}
config.params.filter = config.filter
}
return config return config
}) })
} }

View File

@ -1,10 +1,12 @@
import Toolbar from './Toolbar' import Toolbar from './Toolbar'
import Portal from './Portal'
import LbScroll from './LbScroll' import LbScroll from './LbScroll'
import FullImage from './FullImage' import FullImage from './FullImage'
export default { export default {
components: { components: {
Toolbar, Toolbar,
Portal,
LbScroll, LbScroll,
FullImage FullImage
}, },

21
src/components/Portal.vue Normal file
View File

@ -0,0 +1,21 @@
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'VnPortal',
props: {
place: String
},
mounted () {
this.placeEl = this.$state.layout.$refs[this.place]
this.placeEl.appendChild(this.$el)
},
beforeDestroy () {
this.placeEl.removeChild(this.$el)
}
}
</script>

View File

@ -75,6 +75,10 @@ export default {
] ]
}, },
// errors
internalServerError: 'Internal server error',
somethingWentWrong: 'Something went wrong',
// Layout // Layout
login: 'Login', login: 'Login',
logout: 'Logout', logout: 'Logout',
@ -88,12 +92,13 @@ export default {
notRememberPassword: 'I don\'t remember my password', notRememberPassword: 'I don\'t remember my password',
inputEmail: 'Input email', inputEmail: 'Input email',
inputPassword: 'Input password', inputPassword: 'Input password',
emailConfirmedSuccessfully: 'E-mail confirmed succesfully',
// register // register
register: 'Register', register: 'Register',
fillData: 'Fill the data', fillData: 'Fill the data',
notYetUser: 'You are not yet a user?', 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', repeatPasswordError: 'Passwords doesn\'t match',
// recover // recover

View File

@ -1,5 +1,5 @@
export default { export default {
// global // Global
search: 'Buscar', search: 'Buscar',
accept: 'Aceptar', accept: 'Aceptar',
cancel: 'Cancelar', cancel: 'Cancelar',
@ -75,12 +75,15 @@ export default {
] ]
}, },
// layout // Errors
internalServerError: 'Error interno del servidor',
somethingWentWrong: 'Algo salió mal',
// MainLayout
login: 'Iniciar sesión', login: 'Iniciar sesión',
logout: 'Cerrar sesión', logout: 'Cerrar sesión',
visitor: 'Visitante',
// login // Login
enter: 'Entrar', enter: 'Entrar',
email: 'Correo electrónico', email: 'Correo electrónico',
password: 'Contraseña', password: 'Contraseña',
@ -88,25 +91,26 @@ export default {
notRememberPassword: 'No recuerdo mi contraseña', notRememberPassword: 'No recuerdo mi contraseña',
inputEmail: 'Introduce el correo electrónico', inputEmail: 'Introduce el correo electrónico',
inputPassword: 'Introduce la contraseña', inputPassword: 'Introduce la contraseña',
emailConfirmedSuccessfully: 'Correo verificado correctamente',
// register // Register
register: 'Registrarse', register: 'Registrarme',
fillData: 'Rellena los datos', fillData: 'Rellena los datos',
notYetUser: '¿Todavía no eres usuario?', 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', repeatPasswordError: 'Las contraseñas no coinciden',
// recover // Recover
rememberPassword: 'Recordar contraseña', rememberPassword: 'Recordar contraseña',
dontWorry: '¡No te preocupes!', dontWorry: '¡No te preocupes!',
weSendEmail: 'Te enviaremos un correo para restablecer tu contraseña', weSendEmail: 'Te enviaremos un correo para restablecer tu contraseña',
weHaveSentEmailToRecover: 'Te hemos enviado un correo donde podrás recuperar tu contraseña', weHaveSentEmailToRecover: 'Te hemos enviado un correo donde podrás recuperar tu contraseña',
// reset // Reset
resetPassword: 'Restaurar contraseña', resetPassword: 'Restaurar contraseña',
passwordResetSuccessfully: 'Contraseña modificada correctamente', passwordResetSuccessfully: 'Contraseña modificada correctamente',
// menu // Menu
home: 'Inicio', home: 'Inicio',
catalog: 'Catálogo', catalog: 'Catálogo',
orders: 'Pedidos', orders: 'Pedidos',
@ -128,7 +132,7 @@ export default {
addresses: 'Direcciones', addresses: 'Direcciones',
addressEdit: 'Editar dirección', addressEdit: 'Editar dirección',
// home // Home
recentNews: 'Noticias recientes', recentNews: 'Noticias recientes',
startOrder: 'Empezar pedido', startOrder: 'Empezar pedido',
@ -147,7 +151,7 @@ export default {
n1InPrice: 'Nº1 en precio', n1InPrice: 'Nº1 en precio',
ourBigVolumeAllows: 'Nuestro gran volumen nos permite ofrecerte los mejores precios', ourBigVolumeAllows: 'Nuestro gran volumen nos permite ofrecerte los mejores precios',
// catalog // Catalog
more: 'Más', more: 'Más',
noItemsFound: 'No se han encontrado artículos', noItemsFound: 'No se han encontrado artículos',
pleaseSetFilter: 'Por favor, establece un filtro usando el menú de la derecha', pleaseSetFilter: 'Por favor, establece un filtro usando el menú de la derecha',
@ -170,19 +174,19 @@ export default {
siceAsc: 'Medida ascendente', siceAsc: 'Medida ascendente',
sizeDesc: 'Medida descendente', sizeDesc: 'Medida descendente',
// orders // Orders
pending: 'Pendientes', pending: 'Pendientes',
confirmed: 'Confirmados', confirmed: 'Confirmados',
// orders/pending // Pending
pendingConfirmtion: 'Pendientes de confirmar', pendingConfirmtion: 'Pendientes de confirmar',
noOrdersFound: 'No se han encontrado pedidos', noOrdersFound: 'No se han encontrado pedidos',
// orders/confirmed // Confirmed
ordersMadeAt: 'Pedidos realizados en', ordersMadeAt: 'Pedidos realizados en',
packages: '{n} bultos', packages: '{n} bultos',
// order // Order
total: 'Total', total: 'Total',
confirm: 'Confirmar', confirm: 'Confirmar',
delivery: 'Fecha de entrega', delivery: 'Fecha de entrega',
@ -191,7 +195,24 @@ export default {
warehouse: 'Almacén', warehouse: 'Almacén',
configure: 'Configurar', 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', checkout: 'Finalizar pedido',
orderSummary: 'Resumen del pedido', orderSummary: 'Resumen del pedido',
accountsSummary: 'Resumen de cuentas', 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.', youExceededCredit: 'Has excedido tu crédito, por favor realiza el pago para que podamos preparar tu pedido.',
notes: 'Notas', 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).', 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.', aboutDesc: 'Verdnatura te ofrece todos los servicios que necesita tu floristería.',
// connections // Connections
nConnections: '{0} connexiones', nConnections: '{0} connexiones',
refreshRate: 'Frecuencia de actualización', refreshRate: 'Frecuencia de actualización',
lastAction: 'Última acción', lastAction: 'Última acción',
@ -231,27 +252,27 @@ export default {
nSeconds: '{0} segundos', nSeconds: '{0} segundos',
dontRefresh: 'No refrescar', dontRefresh: 'No refrescar',
// accessLog // AccessLog
accessLog: 'Registro de accesos', accessLog: 'Registro de accesos',
// visits // Visits
visitsCount: '{0} visitas, {1} nuevas', visitsCount: '{0} visitas, {1} nuevas',
// new // New
title: 'Título', title: 'Título',
image: 'Imagen', image: 'Imagen',
tag: 'Etiqueta', tag: 'Etiqueta',
priority: 'Prioridad', priority: 'Prioridad',
text: 'Texto', text: 'Texto',
// images // Images
collection: 'Colección', collection: 'Colección',
updateMatchingId: 'Actualizar ítems con id coincidente', updateMatchingId: 'Actualizar ítems con id coincidente',
uploadAutomatically: 'Subir automáticamente', uploadAutomatically: 'Subir automáticamente',
imagesUploadSuccess: 'Imágenes subidas correctamente', imagesUploadSuccess: 'Imágenes subidas correctamente',
imagesUploadFailed: 'Algunas imágenes no se ha podido subir', imagesUploadFailed: 'Algunas imágenes no se ha podido subir',
// user // User
userName: 'Nombre de usuario', userName: 'Nombre de usuario',
nickname: 'Nombre a mostrar', nickname: 'Nombre a mostrar',
language: 'Idioma', language: 'Idioma',
@ -270,14 +291,14 @@ export default {
passwordsDontMatch: 'Las contraseñas no coinciden', passwordsDontMatch: 'Las contraseñas no coinciden',
passwordChanged: '¡Contraseña modificada correctamente!', passwordChanged: '¡Contraseña modificada correctamente!',
// addresses // Addresses
setAsDefault: 'Establecer como predeterminada', setAsDefault: 'Establecer como predeterminada',
addressSetAsDefault: 'Dirección establecida como predeterminada', addressSetAsDefault: 'Dirección establecida como predeterminada',
addressRemoved: 'Dirección eliminada', addressRemoved: 'Dirección eliminada',
areYouSureDeleteAddress: '¿Seguro que quieres eliminar la dirección?', areYouSureDeleteAddress: '¿Seguro que quieres eliminar la dirección?',
addressCreated: '¡Dirección creada correctamente!', addressCreated: '¡Dirección creada correctamente!',
// address // Address
consignatary: 'Consignatario', consignatary: 'Consignatario',
street: 'Dirección', street: 'Dirección',
city: 'City', city: 'City',

View File

@ -1,5 +1,5 @@
<template> <template>
<q-layout id="bg" class="fullscreen row justify-center items-center layout-view scroll bg-primary"> <q-layout id="bg" class="fullscreen row justify-center items-center layout-view scroll">
<q-card class="q-pa-md row items-center justify-center"> <q-card class="q-pa-md row items-center justify-center">
<transition name="slide-right"> <transition name="slide-right">
<router-view class="child-view"/> <router-view class="child-view"/>
@ -9,6 +9,14 @@
</template> </template>
<style lang="stylus" scoped> <style lang="stylus" scoped>
#bg
background: repeating-linear-gradient(
45deg,
#f18d1a,
#f18d1a 20px,
$primary 20px,
$primary 40px
);
.q-card .q-card
border-radius 0 border-radius 0
width 600px width 600px

View File

@ -18,13 +18,15 @@
{{$state.subtitle}} {{$state.subtitle}}
</div> </div>
</q-toolbar-title> </q-toolbar-title>
<div ref="top"/>
<q-space/>
<div ref="toolbar"/>
<q-btn flat <q-btn flat
v-if="!$state.user.loggedIn" v-if="!$state.user.loggedIn"
class="q-ml-md" class="q-ml-md"
:label="$t('login')" :label="$t('login')"
to="/login" to="/login"
/> />
<div ref="toolbar"/>
<q-btn <q-btn
v-if="$state.useRightDrawer" v-if="$state.useRightDrawer"
@click="$state.rightDrawerOpen = !$state.rightDrawerOpen" @click="$state.rightDrawerOpen = !$state.rightDrawerOpen"

View File

@ -117,7 +117,7 @@ export default {
.then(res => (this.address = res.data)) .then(res => (this.address = res.data))
} else { } else {
this.address = { this.address = {
clientFk: this.$state.userId clientFk: this.$state.user.id
} }
} }
}, },

View File

@ -95,7 +95,7 @@ export default {
'postalCode' 'postalCode'
], ],
where: { where: {
clientFk: this.$state.userId, clientFk: this.$state.user.id,
isActive: true isActive: true
}, },
order: 'nickname' order: 'nickname'
@ -106,14 +106,14 @@ export default {
filter = { filter = {
fields: ['defaultAddressFk'] fields: ['defaultAddressFk']
} }
this.$axios.get(`Clients/${this.$state.userId}`, { params: { filter } }) this.$axios.get(`Clients/${this.$state.user.id}`, { params: { filter } })
.then(res => (this.defaultAddress = res.data.defaultAddressFk)) .then(res => (this.defaultAddress = res.data.defaultAddressFk))
}, },
watch: { watch: {
defaultAddress (value, oldValue) { defaultAddress (value, oldValue) {
if (!oldValue) return if (!oldValue) return
let data = { defaultAddressFk: value } let data = { defaultAddressFk: value }
this.$axios.patch(`Clients/${this.$state.userId}`, data) this.$axios.patch(`Clients/${this.$state.user.id}`, data)
.then(res => { .then(res => {
this.$q.notify(this.$t('addressSetAsDefault')) this.$q.notify(this.$t('addressSetAsDefault'))
}) })

View File

@ -132,7 +132,7 @@ export default {
'email' 'email'
] ]
} }
this.$axios.get(`Accounts/${this.$state.userId}`, { params: { filter } }) this.$axios.get(`Accounts/${this.$state.user.id}`, { params: { filter } })
.then(res => (this.user = res.data)) .then(res => (this.user = res.data))
filter = { filter = {
@ -173,7 +173,7 @@ export default {
}) })
}, },
onSave () { onSave () {
this.$axios.patch(`Accounts/${this.$state.userId}`, this.user) this.$axios.patch(`Accounts/${this.$state.user.id}`, this.user)
.then(res => (this.$q.notify({ .then(res => (this.$q.notify({
message: this.$t('dataSaved'), message: this.$t('dataSaved'),
icon: 'check', icon: 'check',

View File

@ -4,6 +4,6 @@
<script> <script>
export default { export default {
name: 'Admin' name: 'AdminIndex'
} }
</script> </script>

View File

@ -152,7 +152,7 @@ export default {
.then(res => (this.myNew = res.data)) .then(res => (this.myNew = res.data))
} else { } else {
this.myNew = { this.myNew = {
userFk: this.$state.userId, userFk: this.$state.user.id,
tag: 'new', tag: 'new',
priority: 1, priority: 1,
text: '' text: ''

View File

@ -16,10 +16,12 @@
v-model="email" v-model="email"
:label="$t('email')" :label="$t('email')"
:rules="[ val => !!val || $t('inputEmail')]" :rules="[ val => !!val || $t('inputEmail')]"
autofocus
filled filled
/> />
<q-input <q-input
v-model="password" v-model="password"
ref="password"
:label="$t('password')" :label="$t('password')"
:type="showPwd ? 'password' : 'text'" :type="showPwd ? 'password' : 'text'"
:rules="[ val => !!val || $t('inputPassword')]" :rules="[ val => !!val || $t('inputPassword')]"
@ -81,12 +83,30 @@ export default {
showPwd: true showPwd: true
} }
}, },
mounted () {
if (this.$route.query.emailConfirmed !== undefined) {
this.$q.notify({
message: this.$t('emailConfirmedSuccessfully'),
type: 'positive'
})
}
if (this.$route.params.email) {
this.email = this.$route.params.email
this.$refs.password.focus()
}
},
methods: { methods: {
async onLogin () { async onLogin () {
const params = { const params = {
username: this.email,
password: this.password password: this.password
} }
if (this.email.indexOf('@') !== -1) {
params.email = this.email
} else {
params.username = this.email
}
const res = await this.$axios.post('users/login', params) const res = await this.$axios.post('users/login', params)
localStorage.setItem('token', res.data.id) localStorage.setItem('token', res.data.id)
Object.assign(this.$state.user, { Object.assign(this.$state.user, {

View File

@ -2,7 +2,7 @@
<div> <div>
<q-card-section> <q-card-section>
<q-icon <q-icon
name="nature_people" name="local_florist"
class="block q-mx-auto text-accent" class="block q-mx-auto text-accent"
style="font-size: 120px;" style="font-size: 120px;"
/> />
@ -16,6 +16,7 @@
<q-input <q-input
v-model="email" v-model="email"
:label="$t('email')" :label="$t('email')"
autofocus
hint="" hint=""
filled filled
/> />

View File

@ -21,6 +21,7 @@
v-model="email" v-model="email"
:label="$t('email')" :label="$t('email')"
:rules="[ val => !!val || $t('inputEmail')]" :rules="[ val => !!val || $t('inputEmail')]"
autofocus
filled filled
/> />
<div> <div>

View File

@ -17,6 +17,7 @@
v-model="password" v-model="password"
:label="$t('password')" :label="$t('password')"
:type="showPwd ? 'password' : 'text'" :type="showPwd ? 'password' : 'text'"
autofocus
hint="" hint=""
filled> filled>
<template v-slot:append> <template v-slot:append>

View File

@ -1,6 +1,6 @@
<template> <template>
<div style="padding-bottom: 5em;"> <div style="padding-bottom: 5em;">
<toolbar> <portal place="top">
<q-input <q-input
v-model="search" v-model="search"
debounce="500" debounce="500"
@ -21,7 +21,7 @@
/> />
</template> </template>
</q-input> </q-input>
</toolbar> </portal>
<q-drawer <q-drawer
v-model="$state.rightDrawerOpen" v-model="$state.rightDrawerOpen"
side="right" side="right"
@ -276,9 +276,9 @@
height 40px height 40px
overflow hidden overflow hidden
.category .category
width 25% width 33%
&.active &.active
background: rgba(0, 0, 0, .08) background rgba(0, 0, 0, .08)
.category-img .category-img
height 3.5em height 3.5em
.tags .tags

View File

@ -53,7 +53,7 @@
import Page from 'components/Page' import Page from 'components/Page'
export default { export default {
name: 'Orders', name: 'OrdersConfirmedIndex',
mixins: [Page], mixins: [Page],
data () { data () {
return { return {
@ -79,7 +79,7 @@ export default {
let params = { filter: { let params = { filter: {
where: { where: {
clientFk: this.$state.userId, clientFk: this.$state.user.id,
landed: { between: [start, end] } landed: { between: [start, end] }
}, },
include: [ include: [

View File

@ -9,6 +9,6 @@
<script> <script>
export default { export default {
name: 'Ticket' name: 'OrdersConfirmedView'
} }
</script> </script>

View File

@ -12,7 +12,7 @@
<q-item <q-item
v-for="order in orders" v-for="order in orders"
:key="order.id" :key="order.id"
:to="`/order/${order.id}/view`" :to="`/order/${order.id}/`"
clickable clickable
v-ripple> v-ripple>
<q-item-section> <q-item-section>
@ -40,24 +40,24 @@
<script> <script>
export default { export default {
name: 'Orders', name: 'OrdersPendingIndex',
data () { data () {
return { return {
orders: null orders: null
} }
}, },
mounted () { async mounted () {
let filter = { const filter = {
where: { where: {
clientFk: this.$state.userId, clientFk: this.$state.user.id,
isConfirmed: false isConfirmed: false
}, },
include: 'address', include: 'address',
order: 'created DESC', order: 'created DESC',
limit: 20 limit: 20
} }
this.$axios.get('Orders', { params: { filter } }) const res = await this.$axios.get('Orders', { params: { filter } })
.then(res => (this.orders = res.data)) this.orders = res.data
} }
} }
</script> </script>

View File

@ -150,7 +150,7 @@
import Page from 'components/Page' import Page from 'components/Page'
export default { export default {
name: 'OrderCheckout', name: 'OrdersPendingCheckout',
mixins: [Page], mixins: [Page],
data () { data () {
return { return {

View File

@ -0,0 +1,201 @@
<template>
<div class="vn-pp row justify-center">
<q-stepper
v-model="step"
vertical
color="primary"
animated
class="vn-w-lg">
<q-step
name="method"
:title="$t('deliveryMethod')"
icon="person_pin"
:done="step != 'method'">
<section>
<q-radio
v-model="method"
val="delivery"
:label="$t('homeDelivery')"
/>
</section>
<section>
<q-radio
v-model="method"
val="pickup"
:label="$t('storePickup')"
/>
</section>
<q-stepper-navigation>
<q-btn
@click="step = 'when'"
color="primary"
label="Continue"
/>
</q-stepper-navigation>
</q-step>
<q-step
name="when"
:title="$t(isPickup ? 'pickupDate' : 'deliveryDate')"
icon="event"
:done="step == 'how'">
<div class="q-gutter-y-md">
<q-select
:label="$t(isPickup ? 'optionalAddress' : 'deliveryAddress')"
v-model="address"
:options="addresses"
option-label="nickname">
<template v-slot:append>
<q-icon name="info">
<q-tooltip>
<div style="max-width: 250px;">
{{ $t(isPickup ? 'optionalAddressInfo' : 'addressHint') }}
</div>
</q-tooltip>
</q-icon>
</template>
<template v-slot:option="scope">
<q-item
v-bind="scope.itemProps"
v-on="scope.itemEvents">
<q-item-section>
<q-item-label>{{ scope.opt.nickname }}</q-item-label>
<q-item-label caption>{{ scope.opt.street }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
<q-date
v-model="date"
:options="datesFn"
minimal
flat
bordered
class="full-width"
/>
</div>
<q-stepper-navigation>
<q-btn
label="Continue"
@click="step = 'how'"
color="primary"
/>
<q-btn
label="Back"
@click="step = 'method'"
color="primary"
class="q-ml-sm"
flat
/>
</q-stepper-navigation>
</q-step>
<q-step
name="how"
:title="$t(isPickup ? 'pickupStore' : 'deliveryAgency')"
:icon="isPickup ? 'store' : 'local_shipping'">
<q-select
:label="$t(isPickup ? 'warehouse' : 'agency')"
v-model="agency"
:options="agencies"
option-label="description"
option-value="id"
/>
<q-stepper-navigation>
<q-btn
label="Finish"
to="./"
color="primary"
/>
<q-btn
flat
label="Back"
@click="step = 'when'"
color="primary"
class="q-ml-sm"
/>
</q-stepper-navigation>
</q-step>
</q-stepper>
<q-page-sticky position="bottom-right" :offset="[18, 18]">
<q-btn
fab
icon="shopping_basket"
color="accent"
:title="$t('rows')"
to="./"
/>
</q-page-sticky>
</div>
</template>
<style lang="stylus" scoped>
.border
border-top $layout-border
.method-desc
margin-left 3.2em
color $grey-8
</style>
<script>
import Page from 'components/Page'
import { date } from 'quasar'
export default {
name: 'OrderConfigure',
mixins: [Page],
data () {
return {
step: 'method',
method: 'delivery',
address: null,
addresses: null,
date: date.formatDate(new Date(), 'YYYY/MM/DD'),
agency: null,
agencies: null,
today: date.formatDate(new Date(), 'YYYY/MM/DD')
}
},
computed: {
isPickup () {
return this.method === 'pickup'
}
},
async mounted () {
let res
let filter
let uid = this.$state.user.id
filter = {
fields: ['id', 'nickname', 'street'],
where: { isActive: true }
}
res = await this.$axios.get(`Clients/${uid}/addresses`, { filter })
this.addresses = res.data
filter = {
fields: ['id', 'defaultAddressFk'],
include: {
relation: 'defaultAddress',
scope: filter
}
}
res = await this.$axios.get(`Clients/${uid}`, { filter })
this.address = res.data.defaultAddress
res = await this.$axios.get(`AgencyModes`)
this.agencies = res.data
},
methods: {
datesFn (date) {
return date >= this.today
}
},
watch: {
method () {
this.step = 'when'
},
date () {
this.step = 'how'
}
}
}
</script>

View File

@ -77,7 +77,7 @@
import Page from 'components/Page' import Page from 'components/Page'
export default { export default {
name: 'OrderView', name: 'OrdersPendingRows',
mixins: [Page], mixins: [Page],
data () { data () {
return { return {

View File

@ -26,12 +26,9 @@
:label="$t('agency')" :label="$t('agency')"
:value="order.agencyMode.name" :value="order.agencyMode.name"
readonly/> readonly/>
<q-input
v-model="notes"
:label="$t('notes')"
type="textarea"/>
<q-btn <q-btn
:label="$t('configure')" :label="$t('configure')"
to="configure"
color="primary" color="primary"
class="q-mt-md" class="q-mt-md"
style="width: 100%;" style="width: 100%;"
@ -45,7 +42,7 @@
import Page from 'components/Page' import Page from 'components/Page'
export default { export default {
name: 'Order', name: 'OrdersPendingView',
mixins: [Page], mixins: [Page],
data () { data () {
return { return {

View File

@ -6,20 +6,20 @@ const routes = [
children: [ children: [
{ {
name: 'login', name: 'login',
path: '', path: '/login/:email?',
component: () => import('pages/Login.vue') component: () => import('pages/Login/Login.vue')
}, { }, {
name: 'register', name: 'register',
path: '/register', path: '/register',
component: () => import('pages/Register.vue') component: () => import('pages/Login/Register.vue')
}, { }, {
name: 'rememberPassword', name: 'rememberPassword',
path: '/remember-password', path: '/remember-password',
component: () => import('pages/RememberPassword.vue') component: () => import('pages/Login/RememberPassword.vue')
}, { }, {
name: 'resetPassword', name: 'resetPassword',
path: '/reset-password', path: '/reset-password',
component: () => import('pages/ResetPassword.vue') component: () => import('pages/Login/ResetPassword.vue')
} }
] ]
}, { }, {
@ -29,115 +29,119 @@ const routes = [
{ {
name: '', name: '',
path: '', path: '',
component: () => import('pages/Index.vue') component: () => import('pages/Cms/Home.vue')
}, { }, {
name: 'home', name: 'home',
path: '/home', path: '/home',
component: () => import('pages/Index.vue') component: () => import('pages/Cms/Home.vue')
}, { }, {
name: 'catalog', name: 'catalog',
path: '/catalog/:category?/:type?', path: '/catalog/:category?/:type?',
component: () => import('pages/Catalog.vue') component: () => import('pages/Webshop/Catalog.vue')
}, { }, {
name: 'orders', name: 'orders',
path: '/orders', path: '/orders',
component: () => import('pages/Orders.vue'), component: () => import('pages/Webshop/Orders.vue'),
children: [ children: [
{ {
name: 'pending', name: 'pending',
path: 'pending', path: 'pending',
component: () => import('pages/Pending.vue') component: () => import('pages/Webshop/Pending.vue')
}, { }, {
name: 'confirmed', name: 'confirmed',
path: 'confirmed', path: 'confirmed',
component: () => import('pages/Confirmed.vue') component: () => import('pages/Webshop/Confirmed.vue')
} }
] ]
}, { }, {
name: 'order', name: 'order',
path: '/order/:id', path: '/order/:id',
component: () => import('pages/Order.vue'), component: () => import('pages/Webshop/Pending/View.vue'),
children: [ children: [
{ {
name: 'view', name: '',
path: 'view', path: '',
component: () => import('pages/OrderView.vue') component: () => import('pages/Webshop/Pending/Rows.vue')
}, {
name: 'configure',
path: 'configure',
component: () => import('pages/Webshop/Pending/Configure.vue')
}, { }, {
name: 'checkout', name: 'checkout',
path: 'checkout', path: 'checkout',
component: () => import('pages/OrderCheckout.vue') component: () => import('pages/Webshop/Pending/Checkout.vue')
} }
] ]
}, { }, {
name: 'ticket', name: 'ticket',
path: '/ticket/:id', path: '/ticket/:id',
component: () => import('pages/Ticket.vue') component: () => import('pages/Webshop/Confirmed/View.vue')
}, { }, {
name: 'conditions', name: 'conditions',
path: '/conditions', path: '/conditions',
component: () => import('pages/Conditions.vue') component: () => import('pages/Cms/Conditions.vue')
}, { }, {
name: 'about', name: 'about',
path: '/about', path: '/about',
component: () => import('pages/About.vue') component: () => import('pages/Cms/About.vue')
}, { }, {
name: 'admin', name: 'admin',
path: '/admin', path: '/admin',
component: () => import('pages/Admin.vue'), component: () => import('pages/Admin/Index.vue'),
children: [ children: [
{ {
name: 'panel', name: 'panel',
path: 'panel', path: 'panel',
component: () => import('pages/Panel.vue') component: () => import('pages/Admin/Panel.vue')
}, { }, {
name: 'users', name: 'users',
path: 'users', path: 'users',
component: () => import('pages/Users.vue') component: () => import('pages/Admin/Users.vue')
}, { }, {
name: 'connections', name: 'connections',
path: 'connections', path: 'connections',
component: () => import('pages/Connections.vue') component: () => import('pages/Admin/Connections.vue')
}, { }, {
name: 'visits', name: 'visits',
path: 'visits', path: 'visits',
component: () => import('pages/Visits.vue') component: () => import('pages/Admin/Visits.vue')
}, { }, {
name: 'news', name: 'news',
path: 'news', path: 'news',
component: () => import('pages/News.vue') component: () => import('pages/Admin/News.vue')
}, { }, {
name: 'images', name: 'images',
path: 'images', path: 'images',
component: () => import('pages/Images.vue') component: () => import('pages/Admin/Images.vue')
}, { }, {
name: 'items', name: 'items',
path: 'items', path: 'items',
component: () => import('pages/Items.vue') component: () => import('pages/Admin/Items.vue')
} }
] ]
}, { }, {
name: 'accessLog', name: 'accessLog',
path: '/access-log/:user', path: '/access-log/:user',
component: () => import('pages/AccessLog.vue') component: () => import('pages/Admin/AccessLog.vue')
}, { }, {
name: 'newEdit', name: 'newEdit',
path: '/new/:id?', path: '/new/:id?',
component: () => import('pages/New.vue') component: () => import('pages/Admin/New.vue')
}, { }, {
name: 'user', name: 'user',
path: '/user', path: '/user',
component: () => import('pages/User.vue'), component: () => import('pages/Account/User.vue'),
props: route => ({ props: route => ({
changePassword: String(route.query.changePassword) === 'true' changePassword: String(route.query.changePassword) === 'true'
}) })
}, { }, {
name: 'addresses', name: 'addresses',
path: '/addresses', path: '/addresses',
component: () => import('pages/Addresses.vue') component: () => import('pages/Account/Addresses.vue')
}, { }, {
name: 'addressEdit', name: 'addressEdit',
path: '/address/:id?', path: '/address/:id?',
component: () => import('pages/Address.vue') component: () => import('pages/Account/Address.vue')
} }
] ]
} }