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

@ -271,8 +271,8 @@ module.exports = Self => {
let items = await Self.find({
fields: ['id', 'longName', 'subName', 'image'],
where: {id: {inq: itemIds}},
limit: limit,
order: order,
limit,
order,
include: [
{
relation: 'tags',
@ -308,6 +308,7 @@ module.exports = Self => {
return {items, tags};
};
};
// Array functions
@ -365,4 +366,3 @@ module.exports = Self => {
return array;
}
};

View File

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

View File

@ -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": [

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",
"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",

View File

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

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": {}
},
"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

@ -19,6 +19,13 @@
"mysql": {
"table": "salix.AccessToken"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "user",
"foreignKey": "userId"
}
}
},
"ACL": {
@ -45,19 +52,8 @@
}
}
},
"User": {
"dataSource": "vn",
"public": false
},
"user": {
"dataSource": "db",
"public": true,
"options": {
"mysql": {
"table": "salix.user"
},
"emailVerificationRequired": true
}
"dataSource": "vn"
},
"Email": {
"dataSource": "email"
@ -178,5 +174,8 @@
},
"Warehouse": {
"dataSource": "vn"
},
"Zone": {
"dataSource": "vn"
}
}

View File

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

View File

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

View File

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

View File

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

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
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

View File

@ -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',

View File

@ -1,5 +1,5 @@
<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">
<transition name="slide-right">
<router-view class="child-view"/>
@ -9,6 +9,14 @@
</template>
<style lang="stylus" scoped>
#bg
background: repeating-linear-gradient(
45deg,
#f18d1a,
#f18d1a 20px,
$primary 20px,
$primary 40px
);
.q-card
border-radius 0
width 600px

View File

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

View File

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

View File

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

View File

@ -132,7 +132,7 @@ export default {
'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))
filter = {
@ -173,7 +173,7 @@ export default {
})
},
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({
message: this.$t('dataSaved'),
icon: 'check',

View File

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

View File

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

View File

@ -16,10 +16,12 @@
v-model="email"
:label="$t('email')"
:rules="[ val => !!val || $t('inputEmail')]"
autofocus
filled
/>
<q-input
v-model="password"
ref="password"
:label="$t('password')"
:type="showPwd ? 'password' : 'text'"
:rules="[ val => !!val || $t('inputPassword')]"
@ -81,12 +83,30 @@ export default {
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: {
async onLogin () {
const params = {
username: this.email,
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)
localStorage.setItem('token', res.data.id)
Object.assign(this.$state.user, {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -150,7 +150,7 @@
import Page from 'components/Page'
export default {
name: 'OrderCheckout',
name: 'OrdersPendingCheckout',
mixins: [Page],
data () {
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'
export default {
name: 'OrderView',
name: 'OrdersPendingRows',
mixins: [Page],
data () {
return {

View File

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

View File

@ -6,20 +6,20 @@ const routes = [
children: [
{
name: 'login',
path: '',
component: () => 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')
}
]
}