Init config #68

Merged
jsegarra merged 6 commits from wbuezas/hedera-web-mindshore:feature/InitConfig into 4922-vueMigration 2024-07-19 11:13:56 +00:00
45 changed files with 29084 additions and 28301 deletions

View File

@ -1,79 +1,82 @@
module.exports = {
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
// This option interrupts the configuration hierarchy at this file
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
root: true,
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
// This option interrupts the configuration hierarchy at this file
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
root: true,
parserOptions: {
parser: '@babel/eslint-parser',
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: 'module' // Allows for the use of imports
},
parserOptions: {
parser: '@babel/eslint-parser',
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
},
env: {
browser: true,
'vue/setup-compiler-macros': true
},
env: {
browser: true,
'vue/setup-compiler-macros': true,
},
// Rules order is important, please avoid shuffling them
extends: [
// Base ESLint recommended rules
// 'eslint:recommended',
extends: [
// Base ESLint recommended rules
// 'eslint:recommended',
// Uncomment any of the lines below to choose desired strictness,
// but leave only one uncommented!
// See https://eslint.vuejs.org/rules/#available-rules
'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
// 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
// Uncomment any of the lines below to choose desired strictness,
// but leave only one uncommented!
// See https://eslint.vuejs.org/rules/#available-rules
'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
// 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
'standard'
'standard',
],
],
plugins: ['vue', 'prettier'],
plugins: [
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
// required to lint *.vue files
'vue',
globals: {
ga: 'readonly', // Google Analytics
cordova: 'readonly',
__statics: 'readonly',
__QUASAR_SSR__: 'readonly',
__QUASAR_SSR_SERVER__: 'readonly',
__QUASAR_SSR_CLIENT__: 'readonly',
__QUASAR_SSR_PWA__: 'readonly',
process: 'readonly',
Capacitor: 'readonly',
chrome: 'readonly',
},
],
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow paren-less arrow functions
'arrow-parens': 'off',
'one-var': 'off',
'no-void': 'off',
'multiline-ternary': 'off',
globals: {
ga: 'readonly', // Google Analytics
cordova: 'readonly',
__statics: 'readonly',
__QUASAR_SSR__: 'readonly',
__QUASAR_SSR_SERVER__: 'readonly',
__QUASAR_SSR_CLIENT__: 'readonly',
__QUASAR_SSR_PWA__: 'readonly',
process: 'readonly',
Capacitor: 'readonly',
chrome: 'readonly'
},
'import/first': 'off',
'import/named': 'error',
'import/namespace': 'error',
'import/default': 'error',
'import/export': 'error',
'import/extensions': 'off',
'import/no-unresolved': 'off',
'import/no-extraneous-dependencies': 'off',
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow paren-less arrow functions
'arrow-parens': 'off',
'one-var': 'off',
'no-void': 'off',
'multiline-ternary': 'off',
'import/first': 'off',
'import/named': 'error',
'import/namespace': 'error',
'import/default': 'error',
'import/export': 'error',
'import/extensions': 'off',
'import/no-unresolved': 'off',
'import/no-extraneous-dependencies': 'off',
'prefer-promise-reject-errors': 'off',
// allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}
'prefer-promise-reject-errors': 'off',
semi: 'off',
// allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
},
overrides: [
{
extends: ['plugin:vue/vue3-essential'],
files: ['src/**/*.{js,vue,scss}'], // Aplica ESLint solo a archivos .js, .vue y .scss dentro de src (Proyecto de quasar)
rules: {
semi: 'off',
indent: ['error', 4, { SwitchCase: 1 }],
'space-before-function-paren': 'off',
},
},
],
};

9
.prettierrc.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
printWidth: 80,
tabWidth: 4,
useTabs: false,
singleQuote: true,
bracketSpacing: true,
arrowParens: 'avoid',
trailingComma: 'none'
};

13
.vscode/settings.json vendored
View File

@ -4,14 +4,7 @@
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": [
"source.fixAll.eslint"
],
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"vue"
]
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"]
}

100
Jenkinsfile vendored
View File

@ -1,34 +1,25 @@
#!/usr/bin/env groovy
def BRANCH_ENV = [
test: 'test',
master: 'production'
]
def remote = [:]
node {
stage('Setup') {
env.NODE_ENV = BRANCH_ENV[env.BRANCH_NAME] ?: 'dev'
echo "NODE_NAME: ${env.NODE_NAME}"
echo "WORKSPACE: ${env.WORKSPACE}"
}
}
pipeline {
agent any
environment {
PROJECT_NAME = 'hedera-web'
STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}"
}
stages {
stage('Checkout') {
steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version
switch (env.BRANCH_NAME) {
case 'master':
env.NODE_ENV = 'production'
env.MAIN_REPLICAS = 3
env.CRON_REPLICAS = 1
break
case 'test':
env.NODE_ENV = 'test'
env.MAIN_REPLICAS = 1
env.CRON_REPLICAS = 0
break
}
}
setEnv()
}
}
stage('Debuild') {
when {
anyOf {
@ -38,31 +29,28 @@ pipeline {
}
agent {
docker {
image 'registry.verdnatura.es/debuild:2.21.3-vn2'
image 'registry.verdnatura.es/verdnatura/debuild:2.23.4-vn7'
registryUrl 'https://registry.verdnatura.es/'
registryCredentialsId 'docker-registry'
args '-v /mnt/appdata/reprepro:/reprepro'
}
}
steps {
sh 'debuild -us -uc -b'
sh 'vn-includedeb stretch'
}
}
stage('Container') {
when {
anyOf {
branch 'master'
branch 'test'
sh 'mkdir -p debuild'
sh 'mv ../hedera-web_* debuild'
script {
def files = findFiles(glob: 'debuild/*.changes')
files.each { file -> env.CHANGES_FILE = file.name }
}
configFileProvider([
configFile(fileId: "dput.cf", variable: 'DPUT_CONFIG')
]) {
sshagent(credentials: ['jenkins-agent']) {
sh 'dput --config "$DPUT_CONFIG" verdnatura "debuild/$CHANGES_FILE"'
}
}
}
environment {
CREDS = credentials('docker-registry')
}
steps {
sh 'docker login --username $CREDS_USR --password $CREDS_PSW $REGISTRY'
sh 'docker-compose build --build-arg BUILD_ID=$BUILD_ID --parallel'
sh 'docker-compose push'
}
}
stage('Deploy') {
@ -73,15 +61,41 @@ pipeline {
}
}
environment {
DOCKER_HOST = "${env.SWARM_HOST}"
CREDS = credentials('docker-registry')
IMAGE = "$REGISTRY/verdnatura/hedera-web"
}
steps {
sh "docker stack deploy --with-registry-auth --compose-file docker-compose.yml ${env.STACK_NAME}"
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}"
env.TAG = "${packageJson.version}-build${env.BUILD_ID}"
}
sh 'docker-compose build --build-arg BUILD_ID=$BUILD_ID --parallel'
sh 'docker login --username $CREDS_USR --password $CREDS_PSW $REGISTRY'
sh 'docker push $IMAGE:$TAG'
script {
if (env.BRANCH_NAME == 'master') {
sh 'docker tag $IMAGE:$TAG $IMAGE:latest'
sh 'docker push $IMAGE:latest'
}
}
withKubeConfig([
serverUrl: "$KUBERNETES_API",
credentialsId: 'kubernetes',
namespace: 'salix'
]) {
sh 'kubectl set image deployment/hedera-web-$BRANCH_NAME hedera-web-$BRANCH_NAME=$IMAGE:$TAG'
sh 'kubectl set image deployment/hedera-web-cron-$BRANCH_NAME hedera-web-cron-$BRANCH_NAME=$IMAGE:$TAG'
}
}
}
}
post {
unsuccessful {
setEnv()
sendEmail()
}
}

51320
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,82 +1,90 @@
{
"name": "hedera-web",
"version": "22.48.2",
"description": "Verdnatura web page",
"license": "GPL-3.0",
"author": "Juan Ferrer Toribio <juan@verdnatura.es>",
"repository": {
"type": "git",
"url": "https://git.verdnatura.es/hedera-web"
},
"devDependencies": {
"@babel/eslint-parser": "^7.13.14",
"@babel/preset-env": "^7.20.2",
"@intlify/vue-i18n-loader": "^4.2.0",
"@quasar/app-webpack": "^3.0.0",
"archiver": "^5.3.1",
"assets-webpack-plugin": "^7.1.1",
"babel-loader": "^9.1.0",
"bundle-loader": "^0.5.6",
"eslint": "^8.10.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-n": "^15.0.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^9.0.0",
"eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.2.0",
"fs-extra": "^10.1.0",
"glob": "^8.0.3",
"html-webpack-plugin": "^5.5.0",
"json-loader": "^0.5.7",
"mini-css-extract-plugin": "^2.7.0",
"node-sass": "^7.0.1",
"raw-loader": "^4.0.2",
"sass-loader": "^12.6.0",
"style-loader": "^3.3.1",
"url-loader": "^4.1.1",
"webpack": "^5.75.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1",
"webpack-merge": "^5.8.0",
"yaml-loader": "^0.5.0"
},
"dependencies": {
"@quasar/extras": "^1.0.0",
"axios": "^0.21.1",
"core-js": "^3.6.5",
"js-yaml": "^3.12.1",
"mootools": "^1.5.2",
"pinia": "^2.0.11",
"promise-polyfill": "^8.2.3",
"quasar": "^2.6.0",
"require-yaml": "0.0.1",
"tinymce": "^6.3.0",
"vue": "^3.0.0",
"vue-i18n": "^9.0.0",
"vue-router": "^4.0.0"
},
"scripts": {
"front": "webpack serve --open",
"back": "cd ../vn-database && myvc start && cd ../salix && gulp backOnly",
"build": "rm -rf build/ ; webpack",
"clean": "rm -rf build/",
"lint": "eslint --ext .js,.vue ./"
},
"browserslist": [
"last 10 Chrome versions",
"last 10 Firefox versions",
"last 4 Edge versions",
"last 7 Safari versions",
"last 8 Android versions",
"last 8 ChromeAndroid versions",
"last 8 FirefoxAndroid versions",
"last 10 iOS versions",
"last 5 Opera versions"
],
"engines": {
"node": ">= 12.22.1",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
}
"name": "hedera-web",
"version": "22.48.2",
"description": "Verdnatura web page",
"license": "GPL-3.0",
"productName": "Salix",
"author": "Verdnatura",
"repository": {
"type": "git",
"url": "https://git.verdnatura.es/hedera-web"
},
"devDependencies": {
"@babel/eslint-parser": "^7.13.14",
"@babel/preset-env": "^7.20.2",
"@intlify/vue-i18n-loader": "^4.2.0",
"@quasar/app-webpack": "^3.0.0",
"archiver": "^5.3.1",
"assets-webpack-plugin": "^7.1.1",
"babel-loader": "^9.1.0",
"bundle-loader": "^0.5.6",
"css-loader": "^5.2.7",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-n": "^15.0.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^9.27.0",
"eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.2.0",
"fs-extra": "^10.1.0",
"glob": "^8.0.3",
"html-webpack-plugin": "^5.5.0",
"json-loader": "^0.5.7",
"mini-css-extract-plugin": "^2.7.0",
"node-sass": "^7.0.1",
"postcss": "^8.4.39",
"postcss-import": "^13.0.0",
"postcss-loader": "^4.3.0",
"postcss-url": "^10.1.3",
"raw-loader": "^4.0.2",
"sass-loader": "^12.6.0",
"style-loader": "^3.3.1",
"url-loader": "^4.1.1",
"webpack": "^5.75.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1",
"webpack-merge": "^5.8.0",
"yaml-loader": "^0.5.0"
},
"dependencies": {
"@quasar/extras": "^1.0.0",
"axios": "^0.21.1",
"core-js": "^3.6.5",
"js-yaml": "^3.12.1",
"mootools": "^1.5.2",
"pinia": "^2.0.11",
"promise-polyfill": "^8.2.3",
"quasar": "^2.6.0",
"require-yaml": "0.0.1",
"tinymce": "^6.3.0",
"vue": "^3.0.0",
"vue-i18n": "^9.0.0",
"vue-router": "^4.0.0"
},
"scripts": {
"front": "webpack serve --open",
"back": "cd ../vn-database && myvc start && cd ../salix && gulp backOnly",
"build": "rm -rf build/ ; webpack",
"clean": "rm -rf build/",
"lint": "eslint --ext .js,.vue ./"
},
"browserslist": [
"last 10 Chrome versions",
"last 10 Firefox versions",
"last 4 Edge versions",
"last 7 Safari versions",
"last 8 Android versions",
"last 8 ChromeAndroid versions",
"last 8 FirefoxAndroid versions",
"last 10 iOS versions",
"last 5 Opera versions"
],
"engines": {
"node": ">= 12.22.1",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
}
}

View File

@ -104,15 +104,14 @@ module.exports = configure(function (ctx) {
type: 'http'
},
port: 8080,
open: true, // opens browser window automatically
open: false,
// static: __dirname,
headers: { 'Access-Control-Allow-Origin': '*' },
// stats: { chunks: false },
proxy: {
'/api': 'http://localhost:3000',
'/': {
target: 'http://localhost/projects/hedera-web',
target: 'http://localhost:3001',
bypass: (req) => req.path !== '/' ? req.path : null
}
}
@ -121,7 +120,7 @@ module.exports = configure(function (ctx) {
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-framework
framework: {
config: {},
autoImportComponentCase: 'pascal',
// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack

View File

@ -1,11 +1,11 @@
<template>
<router-view />
<router-view />
</template>
<script>
import { defineComponent } from 'vue'
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App'
})
name: 'App'
});
</script>

View File

@ -3,8 +3,8 @@ import { appStore } from 'stores/app'
import { userStore } from 'stores/user'
export default boot(({ app }) => {
const props = app.config.globalProperties
props.$app = appStore()
props.$user = userStore()
props.$actions = document.createElement('div')
const props = app.config.globalProperties
props.$app = appStore()
props.$user = userStore()
props.$actions = document.createElement('div')
})

View File

@ -10,33 +10,33 @@ import axios from 'axios'
// "export default () => {}" function below (which runs individually
// for each client)
const api = axios.create({
baseURL: `//${location.hostname}:${location.port}/api/`
baseURL: `//${location.hostname}:${location.port}/api/`
})
const jApi = new Connection()
export default boot(({ app }) => {
const user = userStore()
function addToken (config) {
if (user.token) {
config.headers.Authorization = user.token
const user = userStore()
function addToken (config) {
if (user.token) {
config.headers.Authorization = user.token
}
return config
}
return config
}
api.interceptors.request.use(addToken)
jApi.use(addToken)
api.interceptors.request.use(addToken)
jApi.use(addToken)
// for use inside Vue files (Options API) through this.$axios and this.$api
// for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file
app.config.globalProperties.$axios = axios
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file
app.config.globalProperties.$api = api
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
// so you can easily perform requests against your app's API
app.config.globalProperties.$api = api
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
// so you can easily perform requests against your app's API
app.config.globalProperties.$jApi = jApi
app.config.globalProperties.$jApi = jApi
})
export { api, jApi }

View File

@ -1,6 +1,5 @@
export default async ({ app }) => {
/*
/*
window.addEventListener('error',
e => onWindowError(e));
window.addEventListener('unhandledrejection',
@ -13,55 +12,55 @@ export default async ({ app }) => {
errorHandler(event.reason);
}
*/
app.config.errorHandler = (err, vm, info) => {
errorHandler(err, vm)
}
function errorHandler (err, vm) {
let message
let tMessage
let res = err.response
// XXX: Compatibility with old JSON service
if (err.name === 'JsonException') {
res = {
status: err.statusCode,
data: { error: { message: err.message } }
}
app.config.errorHandler = (err, vm, info) => {
errorHandler(err, vm)
}
if (res) {
const status = res.status
function errorHandler (err, vm) {
let message
let tMessage
let res = err.response
if (status >= 400 && status < 500) {
switch (status) {
case 401:
tMessage = 'loginFailed'
break
case 403:
tMessage = 'authenticationRequired'
vm.$router.push('/login')
break
case 404:
tMessage = 'notFound'
break
default:
message = res.data.error.message
// XXX: Compatibility with old JSON service
if (err.name === 'JsonException') {
res = {
status: err.statusCode,
data: { error: { message: err.message } }
}
}
} else if (status >= 500) {
tMessage = 'internalServerError'
}
} else {
tMessage = 'somethingWentWrong'
console.error(err)
}
if (tMessage) {
message = vm.$t(tMessage)
if (res) {
const status = res.status
if (status >= 400 && status < 500) {
switch (status) {
case 401:
tMessage = 'loginFailed'
break
case 403:
tMessage = 'authenticationRequired'
vm.$router.push('/login')
break
case 404:
tMessage = 'notFound'
break
default:
message = res.data.error.message
}
} else if (status >= 500) {
tMessage = 'internalServerError'
}
} else {
tMessage = 'somethingWentWrong'
console.error(err)
}
if (tMessage) {
message = vm.$t(tMessage)
}
vm.$q.notify({
message,
type: 'negative'
})
}
vm.$q.notify({
message,
type: 'negative'
})
}
}

View File

@ -1,18 +1,23 @@
import { boot } from 'quasar/wrappers'
import { createI18n } from 'vue-i18n'
import messages from 'src/i18n'
import { boot } from 'quasar/wrappers';
import { createI18n } from 'vue-i18n';
import messages from 'src/i18n';
export default boot(({ app }) => {
const i18n = createI18n({
locale: 'es-ES',
const i18n = createI18n({
locale: navigator.language || navigator.userLanguage,
fallbackLocale: 'en',
globalInjection: true,
missingWarn: false,
fallbackWarn: false,
legacy: false,
silentTranslationWarn: true,
silentFallbackWarn: true,
messages
})
});
export default boot(({ app }) => {
// Set i18n instance on app
app.use(i18n);
// Set i18n instance on app
app.use(i18n)
window.i18n = i18n.global;
});
window.i18n = i18n.global
})
export { i18n };

View File

@ -5,8 +5,8 @@
src: url(./poppins.ttf) format('truetype');
}
@font-face {
font-family: 'Open Sans';
src: url(./opensans.ttf) format('truetype');
font-family: 'Open Sans';
src: url(./opensans.ttf) format('truetype');
}
@mixin mobile {
@media screen and (max-width: 960px) {
@ -28,7 +28,7 @@ a.link {
}
.q-card {
border-radius: 7px;
box-shadow: 0 0 3px rgba(0, 0, 0, .1);
box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
}
.q-page-sticky.fixed-bottom-right {
margin: 18px;

View File

@ -12,17 +12,17 @@
// to match your app's branding.
// Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary : #1A1A1A;
$secondary : #26A69A;
$accent : #8cc63f;
$primary: #1a1a1a;
$secondary: #26a69a;
$accent: #8cc63f;
$dark : #1D1D1D;
$dark-page : #121212;
$dark: #1d1d1d;
$dark-page: #121212;
$positive : #21BA45;
$negative : #C10015;
$info : #31CCEC;
$warning : #F2C037;
$positive: #21ba45;
$negative: #c10015;
$info: #31ccec;
$warning: #f2c037;
// Width

View File

@ -1,25 +1,24 @@
%margin-auto {
margin-left: auto;
margin-right: auto;
margin-left: auto;
margin-right: auto;
}
.vn-w-xs {
@extend %margin-auto;
max-width: $width-xs;
@extend %margin-auto;
max-width: $width-xs;
}
.vn-w-sm {
@extend %margin-auto;
max-width: $width-sm;
@extend %margin-auto;
max-width: $width-sm;
}
.vn-w-md {
@extend %margin-auto;
max-width: $width-md;
@extend %margin-auto;
max-width: $width-md;
}
.vn-w-lg {
@extend %margin-auto;
max-width: $width-lg;
@extend %margin-auto;
max-width: $width-lg;
}
.vn-w-xl {
@extend %margin-auto;
max-width: $width-xl;
@extend %margin-auto;
max-width: $width-xl;
}

View File

@ -2,84 +2,76 @@
// so you can safely delete all default props below
export default {
failed: 'Action failed',
success: 'Action was successful',
internalServerError: 'Internal server error',
somethingWentWrong: 'Something went wrong',
loginFailed: 'Login failed',
authenticationRequired: 'Authentication required',
notFound: 'Not found',
today: 'Today',
yesterday: 'Yesterday',
tomorrow: 'Tomorrow',
date: {
days: [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday'
],
daysShort: [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat'
],
months: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
],
shortMonths: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
},
failed: 'Action failed',
success: 'Action was successful',
internalServerError: 'Internal server error',
somethingWentWrong: 'Something went wrong',
loginFailed: 'Login failed',
authenticationRequired: 'Authentication required',
notFound: 'Not found',
today: 'Today',
yesterday: 'Yesterday',
tomorrow: 'Tomorrow',
date: {
days: [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday'
],
daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
months: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
],
shortMonths: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
},
// menu
home: 'Home',
catalog: 'Catalog',
orders: 'Orders',
order: 'Pending order',
ticket: 'Order',
conditions: 'Conditions',
about: 'About us',
admin: 'Administration',
panel: 'Control panel',
users: 'Users',
connections: 'Connections',
visits: 'Visits',
news: 'News',
newEdit: 'Edit new',
images: 'Images',
items: 'Items',
config: 'Configuration',
user: 'User',
addresses: 'Addresses',
addressEdit: 'Edit address'
// menu
home: 'Home',
catalog: 'Catalog',
orders: 'Orders',
order: 'Pending order',
ticket: 'Order',
conditions: 'Conditions',
about: 'About us',
admin: 'Administration',
panel: 'Control panel',
users: 'Users',
connections: 'Connections',
visits: 'Visits',
news: 'News',
newEdit: 'Edit new',
images: 'Images',
items: 'Items',
config: 'Configuration',
user: 'User',
addresses: 'Addresses',
addressEdit: 'Edit address'
}

View File

@ -2,84 +2,76 @@
// so you can safely delete all default props below
export default {
failed: 'Acción fallida',
success: 'Acción exitosa',
internalServerError: 'Error interno del servidor',
somethingWentWrong: 'Algo salió mal',
loginFailed: 'Usuario o contraseña incorrectos',
authenticationRequired: 'Autenticación requerida',
notFound: 'No encontrado',
today: 'Hoy',
yesterday: 'Ayer',
tomorrow: 'Mañana',
date: {
days: [
'Domingo',
'Lunes',
'Martes',
'Miércoles',
'Jueves',
'Viernes',
'Sábado'
],
daysShort: [
'Do',
'Lu',
'Mi',
'Mi',
'Ju',
'Vi',
'Sa'
],
months: [
'Enero',
'Febrero',
'Marzo',
'Abril',
'Mayo',
'Junio',
'Julio',
'Agosto',
'Septiembre',
'Octubre',
'Noviembre',
'Diciembre'
],
shortMonths: [
'Ene',
'Feb',
'Mar',
'Abr',
'May',
'Jun',
'Jul',
'Ago',
'Sep',
'Oct',
'Nov',
'Dic'
]
},
failed: 'Acción fallida',
success: 'Acción exitosa',
internalServerError: 'Error interno del servidor',
somethingWentWrong: 'Algo salió mal',
loginFailed: 'Usuario o contraseña incorrectos',
authenticationRequired: 'Autenticación requerida',
notFound: 'No encontrado',
today: 'Hoy',
yesterday: 'Ayer',
tomorrow: 'Mañana',
date: {
days: [
'Domingo',
'Lunes',
'Martes',
'Miércoles',
'Jueves',
'Viernes',
'Sábado'
],
daysShort: ['Do', 'Lu', 'Mi', 'Mi', 'Ju', 'Vi', 'Sa'],
months: [
'Enero',
'Febrero',
'Marzo',
'Abril',
'Mayo',
'Junio',
'Julio',
'Agosto',
'Septiembre',
'Octubre',
'Noviembre',
'Diciembre'
],
shortMonths: [
'Ene',
'Feb',
'Mar',
'Abr',
'May',
'Jun',
'Jul',
'Ago',
'Sep',
'Oct',
'Nov',
'Dic'
]
},
// Menu
home: 'Inicio',
catalog: 'Catálogo',
orders: 'Pedidos',
order: 'Pedido pendiente',
ticket: 'Pedido',
conditions: 'Condiciones',
about: 'Sobre nosotros',
admin: 'Administración',
panel: 'Panel de control',
users: 'Usuarios',
connections: 'Conexiones',
visits: 'Visitas',
news: 'Noticias',
newEdit: 'Editar noticia',
images: 'Imágenes',
items: 'Artículos',
config: 'Configuración',
user: 'Usuario',
addresses: 'Direcciones',
addressEdit: 'Editar dirección'
// Menu
home: 'Inicio',
catalog: 'Catálogo',
orders: 'Pedidos',
order: 'Pedido pendiente',
ticket: 'Pedido',
conditions: 'Condiciones',
about: 'Sobre nosotros',
admin: 'Administración',
panel: 'Panel de control',
users: 'Usuarios',
connections: 'Conexiones',
visits: 'Visitas',
news: 'Noticias',
newEdit: 'Editar noticia',
images: 'Imágenes',
items: 'Artículos',
config: 'Configuración',
user: 'Usuario',
addresses: 'Direcciones',
addressEdit: 'Editar dirección'
}

View File

@ -2,6 +2,6 @@ import enUS from './en-US'
import esES from './es-ES'
export default {
'en-US': enUS,
'es-ES': esES
'en-US': enUS,
'es-ES': esES
}

View File

@ -12,163 +12,173 @@ import { ResultSet } from './result-set'
* the user can send any statement to the server. For example: DROP DATABASE
*/
const Flag = {
NOT_NULL: 1,
PRI_KEY: 2,
AI: 512 | 2 | 1
NOT_NULL: 1,
PRI_KEY: 2,
AI: 512 | 2 | 1
}
const Type = {
BOOLEAN: 1,
INTEGER: 3,
DOUBLE: 4,
STRING: 5,
DATE: 8,
DATE_TIME: 9
BOOLEAN: 1,
INTEGER: 3,
DOUBLE: 4,
STRING: 5,
DATE: 8,
DATE_TIME: 9
}
export class Connection extends JsonConnection {
static Flag = Flag
static Type = Type
static Flag = Flag
static Type = Type
/**
/**
* Runs a SQL query on the database.
*
* @param {String} sql The SQL statement
* @return {ResultSet} The result
*/
async execSql (sql) {
const json = await this.send('core/query', { sql })
const results = []
let err
async execSql (sql) {
const json = await this.send('core/query', { sql })
const results = []
let err
if (json) {
try {
if (json && json instanceof Array) {
for (let i = 0; i < json.length; i++) {
if (json[i] !== true) {
const rows = json[i].data
const columns = json[i].columns
if (json) {
try {
if (json && json instanceof Array) {
for (let i = 0; i < json.length; i++) {
if (json[i] !== true) {
const rows = json[i].data
const columns = json[i].columns
const data = new Array(rows.length)
results.push({
data,
columns,
tables: json[i].tables
})
const data = new Array(rows.length)
results.push({
data,
columns,
tables: json[i].tables
})
for (let j = 0; j < rows.length; j++) {
const row = data[j] = {}
for (let k = 0; k < columns.length; k++) { row[columns[k].name] = rows[j][k] }
}
for (let j = 0; j < rows.length; j++) {
const row = (data[j] = {})
for (let k = 0; k < columns.length; k++) {
row[columns[k].name] = rows[j][k]
}
}
for (let j = 0; j < columns.length; j++) {
let castFunc = null
const col = columns[j]
for (let j = 0; j < columns.length; j++) {
let castFunc = null
const col = columns[j]
switch (col.type) {
case Type.DATE:
case Type.DATE_TIME:
case Type.TIMESTAMP:
castFunc = this.valueToDate
break
switch (col.type) {
case Type.DATE:
case Type.DATE_TIME:
case Type.TIMESTAMP:
castFunc = this.valueToDate
break
}
if (castFunc !== null) {
if (col.def != null) {
col.def = castFunc(col.def)
}
for (let k = 0; k < data.length; k++) {
if (data[k][col.name] != null) {
data[k][col.name] = castFunc(data[k][col.name])
}
}
}
}
} else {
results.push(json[i])
}
}
}
if (castFunc !== null) {
if (col.def != null) { col.def = castFunc(col.def) }
for (let k = 0; k < data.length; k++) {
if (data[k][col.name] != null) { data[k][col.name] = castFunc(data[k][col.name]) }
}
}
}
} else { results.push(json[i]) }
}
} catch (e) {
err = e
}
}
} catch (e) {
err = e
}
return new ResultSet(results, err)
}
return new ResultSet(results, err)
}
/**
/**
* Runs a query on the database.
*
* @param {String} query The SQL statement
* @param {Object} params The query params
* @return {ResultSet} The result
*/
async execQuery (query, params) {
const sql = query.replace(/#\w+/g, key => {
const value = params[key.substring(1)]
return value ? this.renderValue(value) : key
})
async execQuery (query, params) {
const sql = query.replace(/#\w+/g, (key) => {
const value = params[key.substring(1)]
return value ? this.renderValue(value) : key
})
return await this.execSql(sql)
}
async query (query, params) {
const res = await this.execQuery(query, params)
return res.fetchData()
}
async getObject (query, params) {
const res = await this.execQuery(query, params)
return res.fetchObject()
}
async getValue (query, params) {
const res = await this.execQuery(query, params)
return res.fetchValue()
}
renderValue (v) {
switch (typeof v) {
case 'number':
return v
case 'boolean':
return (v) ? 'TRUE' : 'FALSE'
case 'string':
return "'" + v.replace(this.regexp, this.replaceFunc) + "'"
default:
if (v instanceof Date) {
if (!isNaN(v.getTime())) {
const unixTime = parseInt(fixTz(v).getTime() / 1000)
return 'DATE(FROM_UNIXTIME(' + unixTime + '))'
} else { return '0000-00-00' }
} else { return 'NULL' }
return await this.execSql(sql)
}
}
/*
async query (query, params) {
const res = await this.execQuery(query, params)
return res.fetchData()
}
async getObject (query, params) {
const res = await this.execQuery(query, params)
return res.fetchObject()
}
async getValue (query, params) {
const res = await this.execQuery(query, params)
return res.fetchValue()
}
renderValue (v) {
switch (typeof v) {
case 'number':
return v
case 'boolean':
return v ? 'TRUE' : 'FALSE'
case 'string':
return "'" + v.replace(this.regexp, this.replaceFunc) + "'"
default:
if (v instanceof Date) {
if (!isNaN(v.getTime())) {
const unixTime = parseInt(fixTz(v).getTime() / 1000)
return 'DATE(FROM_UNIXTIME(' + unixTime + '))'
} else {
return '0000-00-00'
}
} else {
return 'NULL'
}
}
}
/*
* Parses a value to date.
*/
valueToDate (value) {
return fixTz(new Date(value))
}
valueToDate (value) {
return fixTz(new Date(value))
}
}
// TODO: Read time zone from db configuration
const tz = { timeZone: 'Europe/Madrid' }
const isLocal = Intl
.DateTimeFormat()
.resolvedOptions()
.timeZone === tz.timeZone
const isLocal = Intl.DateTimeFormat().resolvedOptions().timeZone === tz.timeZone
function fixTz (date) {
if (isLocal) return date
if (isLocal) return date
const localDate = new Date(date.toLocaleString('en-US', tz))
const hasTime = localDate.getHours() ||
const localDate = new Date(date.toLocaleString('en-US', tz))
const hasTime =
localDate.getHours() ||
localDate.getMinutes() ||
localDate.getSeconds() ||
localDate.getMilliseconds()
if (!hasTime) {
date.setHours(date.getHours() + 12)
date.setHours(0, 0, 0, 0)
}
if (!hasTime) {
date.setHours(date.getHours() + 12)
date.setHours(0, 0, 0, 0)
}
return date
return date
}

View File

@ -1,121 +1,130 @@
import { Result } from './result'
/**
* This class stores the database results.
*/
export class ResultSet {
results = null
error = null
results = null
error = null
/**
/**
* Initilizes the resultset object.
*/
constructor (results, error) {
this.results = results
this.error = error
}
constructor (results, error) {
this.results = results
this.error = error
}
/**
/**
* Gets the query error.
*
* @return {Db.Err} the error or null if no errors hapened
*/
getError () {
return this.error
}
getError () {
return this.error
}
fetch () {
if (this.error) { throw this.error }
fetch () {
if (this.error) {
throw this.error
}
if (this.results !== null &&
this.results.length > 0) { return this.results.shift() }
if (this.results !== null && this.results.length > 0) {
return this.results.shift()
}
return null
}
return null
}
/**
/**
* Fetchs the next result from the resultset.
*
* @return {Db.Result} the result or %null if error or there are no more results
*/
fetchResult () {
const result = this.fetch()
fetchResult () {
const result = this.fetch()
if (result !== null) {
if (result.data instanceof Array) {
return new Result(result)
} else {
return true
}
if (result !== null) {
if (result.data instanceof Array) {
return new Result(result)
} else {
return true
}
}
return null
}
return null
}
/**
/**
* Fetchs the first row object from the next resultset.
*
* @return {Array} the row if success, %null otherwise
*/
fetchObject () {
const result = this.fetch()
fetchObject () {
const result = this.fetch()
if (result !== null &&
result.data instanceof Array &&
result.data.length > 0) { return result.data[0] }
if (
result !== null &&
result.data instanceof Array &&
result.data.length > 0
) {
return result.data[0]
}
return null
}
return null
}
/**
/**
* Fetchs data from the next resultset.
*
* @return {Array} the data
*/
fetchData () {
const result = this.fetch()
fetchData () {
const result = this.fetch()
if (result !== null &&
result.data instanceof Array) {
return result.data
if (result !== null && result.data instanceof Array) {
return result.data
}
return null
}
return null
}
/**
/**
* Fetchs the first row and column value from the next resultset.
*
* @return {Object} the value if success, %null otherwise
*/
fetchValue () {
const row = this.fetchRow()
fetchValue () {
const row = this.fetchRow()
if (row instanceof Array && row.length > 0) { return row[0] }
if (row instanceof Array && row.length > 0) {
return row[0]
}
return null
}
return null
}
/**
/**
* Fetchs the first row from the next resultset.
*
* @return {Array} the row if success, %null otherwise
*/
fetchRow () {
const result = this.fetch()
fetchRow () {
const result = this.fetch()
if (result !== null &&
result.data instanceof Array &&
result.data.length > 0) {
const object = result.data[0]
const row = new Array(result.columns.length)
for (let i = 0; i < row.length; i++) {
row[i] = object[result.columns[i].name]
}
return row
if (
result !== null &&
result.data instanceof Array &&
result.data.length > 0
) {
const object = result.data[0]
const row = new Array(result.columns.length)
for (let i = 0; i < row.length; i++) {
row[i] = object[result.columns[i].name]
}
return row
}
return null
}
return null
}
}

View File

@ -2,60 +2,64 @@
* This class stores a database result.
*/
export class Result {
/**
/**
* Initilizes the result object.
*/
constructor (result) {
this.data = result.data
this.tables = result.tables
this.columns = result.columns
this.row = -1
constructor (result) {
this.data = result.data
this.tables = result.tables
this.columns = result.columns
this.row = -1
if (this.columns) {
this.columnMap = {}
if (this.columns) {
this.columnMap = {}
for (let i = 0; i < this.columns.length; i++) {
const col = this.columns[i]
col.index = i
this.columnMap[col.name] = col
}
} else { this.columnMap = null }
}
for (let i = 0; i < this.columns.length; i++) {
const col = this.columns[i]
col.index = i
this.columnMap[col.name] = col
}
} else {
this.columnMap = null
}
}
/**
/**
* Gets a value from de result.
*
* @param {String} columnName The column name
* @return {Object} The cell value
*/
get (columnName) {
return this.data[this.row][columnName]
}
get (columnName) {
return this.data[this.row][columnName]
}
/**
/**
* Gets a row.
*
* @return {Object} The cell value
*/
getObject () {
return this.data[this.row]
}
getObject () {
return this.data[this.row]
}
/**
/**
* Resets the result iterator.
*/
reset () {
this.row = -1
}
reset () {
this.row = -1
}
/**
/**
* Moves the internal iterator to the next row.
*/
next () {
this.row++
next () {
this.row++
if (this.row >= this.data.length) { return false }
if (this.row >= this.data.length) {
return false
}
return true
}
return true
}
}

View File

@ -1,4 +1,3 @@
import { VnObject } from './object'
import { JsonException } from './json-exception'
@ -6,16 +5,16 @@ import { JsonException } from './json-exception'
* Handler for JSON rest connections.
*/
export class JsonConnection extends VnObject {
_connected = false
_requestsCount = 0
token = null
interceptors = []
_connected = false
_requestsCount = 0
token = null
interceptors = []
use (fn) {
this.interceptors.push(fn)
}
use (fn) {
this.interceptors.push(fn)
}
/**
/**
* Executes the specified REST service with the given params and calls
* the callback when response is received.
*
@ -23,164 +22,179 @@ export class JsonConnection extends VnObject {
* @param {Object} params The params to pass to the service
* @return {Object} The parsed JSON response
*/
async send (url, params) {
if (!params) params = {}
params.srv = `json:${url}`
return this.sendWithUrl('POST', '.', params)
}
async sendForm (form) {
const params = {}
const elements = form.elements
for (let i = 0; i < elements.length; i++) {
if (elements[i].name) { params[elements[i].name] = elements[i].value }
async send (url, params) {
if (!params) params = {}
params.srv = `json:${url}`
return this.sendWithUrl('POST', '.', params)
}
return this.sendWithUrl('POST', form.action, params)
}
async sendForm (form) {
const params = {}
const elements = form.elements
async sendFormMultipart (form) {
return this.request({
method: 'POST',
url: form.action,
data: new FormData(form)
})
}
async sendFormData (formData) {
return this.request({
method: 'POST',
url: '',
data: formData
})
}
/*
* Called when REST response is received.
*/
async sendWithUrl (method, url, params) {
const urlParams = new URLSearchParams()
for (const key in params) {
if (params[key] != null) {
urlParams.set(key, params[key])
}
}
return this.request({
method,
url,
data: urlParams.toString(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}
async request (config) {
const request = new XMLHttpRequest()
request.open(config.method, config.url, true)
for (const fn of this.interceptors) {
config = fn(config)
}
const headers = config.headers
if (headers) {
for (const header in headers) {
request.setRequestHeader(header, headers[header])
}
}
const promise = new Promise((resolve, reject) => {
request.onreadystatechange =
() => this._onStateChange(request, resolve, reject)
})
request.send(config.data)
this._requestsCount++
if (this._requestsCount === 1) { this.emit('loading-changed', true) }
return promise
}
_onStateChange (request, resolve, reject) {
if (request.readyState !== 4) { return }
this._requestsCount--
if (this._requestsCount === 0) { this.emit('loading-changed', false) }
let data = null
let error = null
try {
if (request.status === 0) {
const err = new JsonException()
err.message = 'The server does not respond, please check your Internet connection'
err.statusCode = request.status
throw err
}
let contentType = null
try {
contentType = request
.getResponseHeader('Content-Type')
.split(';')[0]
.trim()
} catch (err) {
console.warn(err)
}
if (contentType !== 'application/json') {
const err = new JsonException()
err.message = request.statusText
err.statusCode = request.status
throw err
}
let json
let jsData
if (request.responseText) { json = JSON.parse(request.responseText) }
if (json) { jsData = json.data || json }
if (request.status >= 200 && request.status < 300) {
data = jsData
} else {
let exception = jsData.exception
const err = new JsonException()
err.statusCode = request.status
if (exception) {
exception = exception
.replace(/\\/g, '.')
.replace(/Exception$/, '')
.replace(/^Vn\.Web\./, '')
err.exception = exception
err.message = jsData.message
err.code = jsData.code
err.file = jsData.file
err.line = jsData.line
err.trace = jsData.trace
} else {
err.message = request.statusText
for (let i = 0; i < elements.length; i++) {
if (elements[i].name) {
params[elements[i].name] = elements[i].value
}
}
throw err
}
} catch (e) {
data = null
error = e
return this.sendWithUrl('POST', form.action, params)
}
if (error) {
this.emit('error', error)
reject(error)
} else { resolve(data) }
}
async sendFormMultipart (form) {
return this.request({
method: 'POST',
url: form.action,
data: new FormData(form)
})
}
async sendFormData (formData) {
return this.request({
method: 'POST',
url: '',
data: formData
})
}
/*
* Called when REST response is received.
*/
async sendWithUrl (method, url, params) {
const urlParams = new URLSearchParams()
for (const key in params) {
if (params[key] != null) {
urlParams.set(key, params[key])
}
}
return this.request({
method,
url,
data: urlParams.toString(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}
async request (config) {
const request = new XMLHttpRequest()
request.open(config.method, config.url, true)
for (const fn of this.interceptors) {
config = fn(config)
}
const headers = config.headers
if (headers) {
for (const header in headers) {
request.setRequestHeader(header, headers[header])
}
}
const promise = new Promise((resolve, reject) => {
request.onreadystatechange = () =>
this._onStateChange(request, resolve, reject)
})
request.send(config.data)
this._requestsCount++
if (this._requestsCount === 1) {
this.emit('loading-changed', true)
}
return promise
}
_onStateChange (request, resolve, reject) {
if (request.readyState !== 4) {
return
}
this._requestsCount--
if (this._requestsCount === 0) {
this.emit('loading-changed', false)
}
let data = null
let error = null
try {
if (request.status === 0) {
const err = new JsonException()
err.message =
'The server does not respond, please check your Internet connection'
err.statusCode = request.status
throw err
}
let contentType = null
try {
contentType = request
.getResponseHeader('Content-Type')
.split(';')[0]
.trim()
} catch (err) {
console.warn(err)
}
if (contentType !== 'application/json') {
const err = new JsonException()
err.message = request.statusText
err.statusCode = request.status
throw err
}
let json
let jsData
if (request.responseText) {
json = JSON.parse(request.responseText)
}
if (json) {
jsData = json.data || json
}
if (request.status >= 200 && request.status < 300) {
data = jsData
} else {
let exception = jsData.exception
const err = new JsonException()
err.statusCode = request.status
if (exception) {
exception = exception
.replace(/\\/g, '.')
.replace(/Exception$/, '')
.replace(/^Vn\.Web\./, '')
err.exception = exception
err.message = jsData.message
err.code = jsData.code
err.file = jsData.file
err.line = jsData.line
err.trace = jsData.trace
} else {
err.message = request.statusText
}
throw err
}
} catch (e) {
data = null
error = e
}
if (error) {
this.emit('error', error)
reject(error)
} else {
resolve(data)
}
}
}

View File

@ -2,14 +2,14 @@
* This class stores the database errors.
*/
export class JsonException {
constructor (exception, message, code, file, line, trace, statucCode) {
this.name = 'JsonException'
this.exception = exception
this.message = message
this.code = code
this.file = file
this.line = line
this.trace = trace
this.statusCode = statucCode
}
constructor (exception, message, code, file, line, trace, statucCode) {
this.name = 'JsonException'
this.exception = exception
this.message = message
this.code = code
this.file = file
this.line = line
this.trace = trace
this.statusCode = statucCode
}
}

View File

@ -1,248 +1,289 @@
/**
* The main base class. Manages the signal system. Objects based on this class
* can be instantiated declaratively using XML.
*/
export class VnObject {
/**
/**
* Tag to be used when the class instance is defined via XML. All classes
* must define this attribute, even if it is not used.
*/
static Tag = 'vn-object'
static Tag = 'vn-object'
/**
/**
* Class public properties.
*/
static Properties = {}
static Properties = {}
/*
/*
* Reference count.
*/
_refCount = 1
_refCount = 1
/*
/*
* Signal handlers data.
*/
_thisArg = null
_thisArg = null
/**
/**
* Initializes the object and sets all properties passed to the class
* constructor.
*
* @param {Object} props The properties passed to the contructor
*/
constructor (props) {
this.setProperties(props)
}
constructor (props) {
this.setProperties(props)
}
initialize (props) {
this.setProperties(props)
}
initialize (props) {
this.setProperties(props)
}
/**
/**
* Sets a group of object properties.
*
* @param {Object} props Properties
*/
setProperties (props) {
for (const prop in props) { this[prop] = props[prop] }
}
setProperties (props) {
for (const prop in props) {
this[prop] = props[prop]
}
}
/**
/**
* Increases the object reference count.
*/
ref () {
this._refCount++
return this
}
ref () {
this._refCount++
return this
}
/**
/**
* Decreases the object reference count.
*/
unref () {
this._refCount--
unref () {
this._refCount--
if (this._refCount === 0) { this._destroy() }
}
if (this._refCount === 0) {
this._destroy()
}
}
/**
/**
* Called from @Vn.Builder when it finds a custom tag as a child of the
* element.
*
* @param {Vn.Scope} scope The scope instance
* @param {Node} node The custom tag child nodes
*/
loadXml () {}
loadXml () {}
/**
/**
* Called from @Vn.Builder when it finds a a child tag that isn't
* associated to any property.
*
* @param {Object} child The child object instance
*/
appendChild () {}
appendChild () {}
/**
/**
* Conects a signal with a function.
*
* @param {string} id The signal identifier
* @param {function} callback The callback
* @param {Object} instance The instance
*/
on (id, callback, instance) {
if (!(callback instanceof Function)) {
console.warn('Vn.Object: Invalid callback for signal \'%s\'', id)
return
on (id, callback, instance) {
if (!(callback instanceof Function)) {
console.warn("Vn.Object: Invalid callback for signal '%s'", id)
return
}
this._signalInit()
let callbacks = this._thisArg.signals[id]
if (!callbacks) {
callbacks = this._thisArg.signals[id] = []
}
callbacks.push({
blocked: false,
callback,
instance
})
}
this._signalInit()
let callbacks = this._thisArg.signals[id]
if (!callbacks) { callbacks = this._thisArg.signals[id] = [] }
callbacks.push({
blocked: false,
callback,
instance
})
}
/**
/**
* Locks/Unlocks a signal emission to the specified object.
*
* @param {string} id The signal identifier
* @param {function} callback The callback
* @param {boolean} block %true for lock the signal, %false for unlock
*/
blockSignal (id, callback, block, instance) {
if (!this._thisArg) { return }
blockSignal (id, callback, block, instance) {
if (!this._thisArg) {
return
}
const callbacks = this._thisArg.signals[id]
const callbacks = this._thisArg.signals[id]
if (!callbacks) { return }
if (!callbacks) {
return
}
for (let i = 0; i < callbacks.length; i++) {
if (callbacks[i].callback === callback &&
callbacks[i].instance === instance) { callbacks[i].blocked = block }
for (let i = 0; i < callbacks.length; i++) {
if (
callbacks[i].callback === callback &&
callbacks[i].instance === instance
) {
callbacks[i].blocked = block
}
}
}
}
/**
/**
* Emits a signal in the object.
*
* @param {string} id The signal identifier
*/
emit (id) {
if (!this._thisArg) { return }
emit (id) {
if (!this._thisArg) {
return
}
const callbacks = this._thisArg.signals[id]
const callbacks = this._thisArg.signals[id]
if (!callbacks) { return }
if (!callbacks) {
return
}
const callbackArgs = []
callbackArgs.push(this)
const callbackArgs = []
callbackArgs.push(this)
for (let i = 1; i < arguments.length; i++) { callbackArgs.push(arguments[i]) }
for (let i = 1; i < arguments.length; i++) {
callbackArgs.push(arguments[i])
}
for (let i = 0; i < callbacks.length; i++) {
if (!callbacks[i].blocked) { callbacks[i].callback.apply(callbacks[i].instance, callbackArgs) }
for (let i = 0; i < callbacks.length; i++) {
if (!callbacks[i].blocked) {
callbacks[i].callback.apply(callbacks[i].instance, callbackArgs)
}
}
}
}
/**
/**
* Disconnects a signal from current object.
*
* @param {string} id The signal identifier
* @param {function} callback The connected callback
* @param {Object} instance The instance
*/
disconnect (id, callback, instance) {
if (!this._thisArg) { return }
disconnect (id, callback, instance) {
if (!this._thisArg) {
return
}
const callbacks = this._thisArg.signals[id]
const callbacks = this._thisArg.signals[id]
if (callbacks) {
for (let i = callbacks.length; i--;) {
if (callbacks[i].callback === callback &&
callbacks[i].instance === instance) { callbacks.splice(i, 1) }
}
if (callbacks) {
for (let i = callbacks.length; i--;) {
if (
callbacks[i].callback === callback &&
callbacks[i].instance === instance
) {
callbacks.splice(i, 1)
}
}
}
}
}
/**
/**
* Disconnects all signals for the given instance.
*
* @param {Object} instance The instance
*/
disconnectByInstance (instance) {
if (!this._thisArg) { return }
const signals = this._thisArg.signals
for (const signalId in signals) {
const callbacks = signals[signalId]
if (callbacks) {
for (let i = callbacks.length; i--;) {
if (callbacks[i].instance === instance) { callbacks.splice(i, 1) }
disconnectByInstance (instance) {
if (!this._thisArg) {
return
}
}
}
}
/**
const signals = this._thisArg.signals
for (const signalId in signals) {
const callbacks = signals[signalId]
if (callbacks) {
for (let i = callbacks.length; i--;) {
if (callbacks[i].instance === instance) {
callbacks.splice(i, 1)
}
}
}
}
}
/**
* Destroys the object, this method should only be called before losing
* the last reference to the object. It can be overwritten by child classes
* but should always call the parent method.
*/
_destroy () {
if (!this._thisArg) { return }
_destroy () {
if (!this._thisArg) {
return
}
const links = this._thisArg.links
const links = this._thisArg.links
for (const key in links) { this._unlink(links[key]) }
for (const key in links) {
this._unlink(links[key])
}
this._thisArg = null
}
this._thisArg = null
}
/**
/**
* Links the object with another object.
*
* @param {Object} prop The linked property
* @param {Object} handlers The object events to listen with
*/
link (prop, handlers) {
this._signalInit()
const links = this._thisArg.links
link (prop, handlers) {
this._signalInit()
const links = this._thisArg.links
for (const key in prop) {
const newObject = prop[key]
const oldObject = this[key]
for (const key in prop) {
const newObject = prop[key]
const oldObject = this[key]
if (oldObject) { this._unlink(oldObject) }
if (oldObject) {
this._unlink(oldObject)
}
this[key] = newObject
this[key] = newObject
if (newObject) {
links[key] = newObject.ref()
if (newObject) {
links[key] = newObject.ref()
for (const signal in handlers) { newObject.on(signal, handlers[signal], this) }
} else if (oldObject) { links[key] = undefined }
for (const signal in handlers) {
newObject.on(signal, handlers[signal], this)
}
} else if (oldObject) {
links[key] = undefined
}
}
}
}
_unlink (object) {
if (!object) return
object.disconnectByInstance(this)
object.unref()
}
_signalInit () {
if (!this._thisArg) {
this._thisArg = {
signals: {},
links: {}
}
_unlink (object) {
if (!object) return
object.disconnectByInstance(this)
object.unref()
}
_signalInit () {
if (!this._thisArg) {
this._thisArg = {
signals: {},
links: {}
}
}
}
}
}

View File

@ -1,31 +1,34 @@
<template>
<q-layout id="bg" class="fullscreen row justify-center items-center layout-view scroll">
<div class="column q-pa-md row items-center justify-center">
<router-view v-slot="{ Component }">
<transition>
<component :is="Component" />
</transition>
</router-view>
</div>
</q-layout>
<QLayout
id="bg"
class="fullscreen row justify-center items-center layout-view scroll"
>
<div class="column q-pa-md row items-center justify-center">
<router-view v-slot="{ Component }">
<transition>
<component :is="Component" />
</transition>
</router-view>
</div>
</QLayout>
</template>
<style lang="scss" scoped>
#bg {
background: white;
background: white;
}
.column {
width: 270px;
overflow: hidden;
width: 270px;
overflow: hidden;
& > * {
width: 100%;
}
& > * {
width: 100%;
}
}
</style>
<script>
export default {
name: 'LoginLayout'
}
name: 'LoginLayout'
};
</script>

View File

@ -1,241 +1,239 @@
<template>
<q-layout view="lHh Lpr lFf">
<q-header reveal>
<q-toolbar>
<q-btn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"/>
<q-toolbar-title>
{{$app.title}}
<div
v-if="$app.subtitle"
class="subtitle text-caption">
{{$app.subtitle}}
</div>
</q-toolbar-title>
<div id="actions" ref="actions">
</div>
<q-btn
v-if="$app.useRightDrawer"
@click="$app.rightDrawerOpen = !$app.rightDrawerOpen"
aria-label="Menu"
flat
dense
round>
<q-icon name="menu"/>
</q-btn>
</q-toolbar>
</q-header>
<q-drawer
v-model="leftDrawerOpen"
:width="250"
show-if-above>
<q-toolbar class="logo">
<img src="statics/logo-dark.svg">
</q-toolbar>
<div class="user-info">
<div>
<span id="user-name">{{(user.nickname)}}</span>
<q-btn flat icon="logout" alt="_Exit" @click="logout()"/>
</div>
<div id="supplant" class="supplant">
<span id="supplanted">{{supplantedUser}}</span>
<q-btn flat icon="logout" alt="_Exit"/>
</div>
</div>
<q-list
v-for="item in essentialLinks"
:key="item.id">
<q-item
v-if="!item.childs"
:to="`/${item.path}`">
<q-item-section>
<q-item-label>{{item.description}}</q-item-label>
</q-item-section>
</q-item>
<q-expansion-item
v-if="item.childs"
:label="item.description"
expand-separator>
<q-list>
<q-item
v-for="subitem in item.childs"
:key="subitem.id"
:to="`/${subitem.path}`"
class="q-pl-lg">
<q-item-section>
<q-item-label>{{subitem.description}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-expansion-item>
</q-list>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
<QLayout view="lHh Lpr lFf">
<QHeader>
<QToolbar>
<QBtn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<QToolbarTitle>
{{ $app.title }}
<div v-if="$app.subtitle" class="subtitle text-caption">
{{ $app.subtitle }}
</div>
</QToolbarTitle>
<div id="actions" ref="actions"></div>
<QBtn
v-if="$app.useRightDrawer"
@click="$app.rightDrawerOpen = !$app.rightDrawerOpen"
aria-label="Menu"
flat
dense
round
>
<QIcon name="menu" />
</QBtn>
</QToolbar>
</QHeader>
<QDrawer v-model="leftDrawerOpen" :width="250" show-if-above>
<QToolbar class="logo">
<img src="statics/logo-dark.svg" />
</QToolbar>
<div class="user-info">
<div>
<span id="user-name">{{ user.nickname }}</span>
<QBtn flat icon="logout" alt="_Exit" @click="logout()" />
</div>
<div id="supplant" class="supplant">
<span id="supplanted">{{ supplantedUser }}</span>
<QBtn flat icon="logout" alt="_Exit" />
</div>
</div>
<QList v-for="item in essentialLinks" :key="item.id">
<QItem v-if="!item.childs" :to="`/${item.path}`">
<QItemSection>
<QItemLabel>{{ item.description }}</QItemLabel>
</QItemSection>
</QItem>
<QExpansionItem
v-if="item.childs"
:label="item.description"
expand-separator
>
<QList>
<QItem
v-for="subitem in item.childs"
:key="subitem.id"
:to="`/${subitem.path}`"
class="q-pl-lg"
>
<QItemSection>
<QItemLabel>
{{ subitem.description }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QExpansionItem>
</QList>
</QDrawer>
<QPageContainer>
<router-view />
</QPageContainer>
</QLayout>
</template>
<style lang="scss" scoped>
.q-toolbar {
min-height: 64px;
min-height: 64px;
}
.logo {
background-color: $primary;
justify-content: center;
background-color: $primary;
justify-content: center;
& > img {
width: 160px;
}
& > img {
width: 160px;
}
}
.user-info {
margin: 25px;
margin: 25px;
& > div {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
border: 1px solid #eaeaea;
& > span {
padding: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
}
.q-btn {
display: block;
margin: 0;
padding: 9px;
border-radius: 0;
&:hover {
background-color: #1a1a1a;
color: white;
}
}
&.supplant {
display: none;
border-top: none;
&.show {
& > div {
display: flex;
}
justify-content: space-between;
align-items: center;
overflow: hidden;
border: 1px solid #eaeaea;
& > span {
padding: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
}
.q-btn {
display: block;
margin: 0;
padding: 9px;
border-radius: 0;
&:hover {
background-color: #1a1a1a;
color: white;
}
}
&.supplant {
display: none;
border-top: none;
&.show {
display: flex;
}
}
}
}
}
</style>
<style lang="scss">
@import "src/css/responsive";
@import 'src/css/responsive';
.q-drawer {
.q-item {
padding-left: 38px;
}
.q-list .q-list .q-item{
padding-left: 50px;
}
.q-item {
padding-left: 38px;
}
.q-list .q-list .q-item {
padding-left: 50px;
}
}
.q-page-container > * {
padding: 16px;
padding: 16px;
}
#actions > div {
display: flex;
align-items: center;
display: flex;
align-items: center;
}
@include mobile {
#actions > div {
.q-btn {
border-radius: 50%;
padding: 10px;
#actions > div {
.q-btn {
border-radius: 50%;
padding: 10px;
&__content {
& > .q-icon {
margin-right: 0;
&__content {
& > .q-icon {
margin-right: 0;
}
& > .block {
display: none !important;
}
}
}
& > .block {
display: none !important;
}
}
}
}
}
</style>
<script>
import { defineComponent, ref } from 'vue'
import { userStore } from 'stores/user'
import { defineComponent, ref } from 'vue';
import { userStore } from 'stores/user';
export default defineComponent({
name: 'MainLayout',
props: {},
name: 'MainLayout',
props: {},
setup () {
const leftDrawerOpen = ref(false)
setup() {
const leftDrawerOpen = ref(false);
return {
user: userStore(),
supplantedUser: ref(''),
essentialLinks: ref(null),
leftDrawerOpen,
toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value
}
}
},
async mounted () {
this.$refs.actions.appendChild(this.$actions)
await this.user.loadData()
await this.$app.loadConfig()
await this.fetchData()
},
methods: {
async fetchData () {
const sections = await this.$jApi.query('SELECT * FROM myMenu')
const sectionMap = new Map()
for (const section of sections) {
sectionMap.set(section.id, section)
}
const sectionTree = []
for (const section of sections) {
const parent = section.parentFk
if (parent) {
const parentSection = sectionMap.get(parent)
if (!parentSection) continue
let childs = parentSection.childs
if (!childs) { childs = parentSection.childs = [] }
childs.push(section)
} else {
sectionTree.push(section)
}
}
this.essentialLinks = sectionTree
return {
user: userStore(),
supplantedUser: ref(''),
essentialLinks: ref(null),
leftDrawerOpen,
toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
};
},
async logout () {
this.user.logout()
this.$router.push('/login')
async mounted() {
this.$refs.actions.appendChild(this.$actions);
await this.user.loadData();
await this.$app.loadConfig();
await this.fetchData();
},
methods: {
async fetchData() {
const sections = await this.$jApi.query('SELECT * FROM myMenu');
const sectionMap = new Map();
for (const section of sections) {
sectionMap.set(section.id, section);
}
const sectionTree = [];
for (const section of sections) {
const parent = section.parentFk;
if (parent) {
const parentSection = sectionMap.get(parent);
if (!parentSection) continue;
let childs = parentSection.childs;
if (!childs) {
childs = parentSection.childs = [];
}
childs.push(section);
} else {
sectionTree.push(section);
}
}
this.essentialLinks = sectionTree;
},
async logout() {
this.user.logout();
this.$router.push('/login');
}
}
}
})
});
</script>
<i18n lang="yaml">
en-US:
en-US:
visitor: Visitor
es-ES:
es-ES:
visitor: Visitante
</i18n>

View File

@ -1,74 +1,73 @@
import { date as qdate, format } from 'quasar'
const { pad } = format
export function currency (val) {
return typeof val === 'number' ? val.toFixed(2) + '€' : val
return typeof val === 'number' ? val.toFixed(2) + '€' : val
}
export function date (val, format) {
if (val == null) return val
if (!(val instanceof Date)) {
val = new Date(val)
}
return qdate.formatDate(val, format, window.i18n.tm('date'))
if (val == null) return val
if (!(val instanceof Date)) {
val = new Date(val)
}
return qdate.formatDate(val, format, window.i18n.tm('date'))
}
export function relDate (val) {
if (val == null) return val
if (!(val instanceof Date)) {
val = new Date(val)
}
const dif = qdate.getDateDiff(new Date(), val, 'days')
let day
switch (dif) {
case 0:
day = 'today'
break
case 1:
day = 'yesterday'
break
case -1:
day = 'tomorrow'
break
}
if (day) {
day = window.i18n.t(day)
} else {
if (dif > 0 && dif <= 7) {
day = qdate.formatDate(val, 'ddd', window.i18n.tm('date'))
} else {
day = qdate.formatDate(val, 'ddd, MMMM Do', window.i18n.tm('date'))
if (val == null) return val
if (!(val instanceof Date)) {
val = new Date(val)
}
}
return day
const dif = qdate.getDateDiff(new Date(), val, 'days')
let day
switch (dif) {
case 0:
day = 'today'
break
case 1:
day = 'yesterday'
break
case -1:
day = 'tomorrow'
break
}
if (day) {
day = window.i18n.t(day)
} else {
if (dif > 0 && dif <= 7) {
day = qdate.formatDate(val, 'ddd', window.i18n.tm('date'))
} else {
day = qdate.formatDate(val, 'ddd, MMMM Do', window.i18n.tm('date'))
}
}
return day
}
export function relTime (val) {
if (val == null) return val
if (!(val instanceof Date)) {
val = new Date(val)
}
return relDate(val) + ' ' + qdate.formatDate(val, 'H:mm:ss')
if (val == null) return val
if (!(val instanceof Date)) {
val = new Date(val)
}
return relDate(val) + ' ' + qdate.formatDate(val, 'H:mm:ss')
}
export function elapsedTime (val) {
if (val == null) return val
if (!(val instanceof Date)) {
val = new Date(val)
}
const now = (new Date()).getTime()
val = Math.floor((now - val.getTime()) / 1000)
if (val == null) return val
if (!(val instanceof Date)) {
val = new Date(val)
}
const now = new Date().getTime()
val = Math.floor((now - val.getTime()) / 1000)
const hours = Math.floor(val / 3600)
val -= hours * 3600
const minutes = Math.floor(val / 60)
val -= minutes * 60
const seconds = val
const hours = Math.floor(val / 3600)
val -= hours * 3600
const minutes = Math.floor(val / 60)
val -= minutes * 60
const seconds = val
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}`
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}`
}

View File

@ -1,80 +1,77 @@
<template>
<div style="padding: 0;">
<div class="q-pa-sm row items-start">
<div
class="new-card q-pa-sm"
v-for="myNew in news"
:key="myNew.id">
<q-card>
<q-img :src="`${$app.imageUrl}/news/full/${myNew.image}`">
</q-img>
<q-card-section>
<div class="text-h5">{{ myNew.title }}</div>
</q-card-section>
<q-card-section class="new-body">
<div v-html="myNew.text"/>
</q-card-section>
</q-card>
</div>
<div style="padding: 0">
<div class="q-pa-sm row items-start">
<div class="new-card q-pa-sm" v-for="myNew in news" :key="myNew.id">
<QCard>
<QImg :src="`${$app.imageUrl}/news/full/${myNew.image}`">
</QImg>
<QCardSection>
<div class="text-h5">{{ myNew.title }}</div>
</QCardSection>
<QCardSection class="new-body">
<div v-html="myNew.text" />
</QCardSection>
</QCard>
</div>
</div>
<QPageSticky>
<QBtn
fab
icon="add_shopping_cart"
color="accent"
to="/ecomerce/catalog"
:title="$t('startOrder')"
/>
</QPageSticky>
</div>
<q-page-sticky>
<q-btn
fab
icon="add_shopping_cart"
color="accent"
to="/ecomerce/catalog"
:title="$t('startOrder')"
/>
</q-page-sticky>
</div>
</template>
<style lang="scss" scoped>
.new-card {
width: 100%;
width: 100%;
@media screen and (min-width: 800px) and (max-width: 1400px) {
width: 50%;
}
@media screen and (min-width: 1401px) and (max-width: 1920px) {
width: 33.33%;
}
@media screen and (min-width: 19021) {
width: 25%;
}
@media screen and (min-width: 800px) and (max-width: 1400px) {
width: 50%;
}
@media screen and (min-width: 1401px) and (max-width: 1920px) {
width: 33.33%;
}
@media screen and (min-width: 19021) {
width: 25%;
}
}
.new-body {
font-family: 'Open Sans';
font-family: 'Open Sans';
}
</style>
<script>
export default {
name: 'PageIndex',
data () {
return {
news: []
}
},
async mounted () {
this.news = await this.$jApi.query(
`SELECT title, text, image, id
name: 'PageIndex',
data() {
return {
news: []
};
},
async mounted() {
this.news = await this.$jApi.query(
`SELECT title, text, image, id
FROM news
ORDER BY priority, created DESC`
)
}
}
);
}
};
</script>
<i18n lang="yaml">
en-US:
en-US:
startOrder: Start order
es-ES:
es-ES:
startOrder: Empezar pedido
ca-ES:
ca-ES:
startOrder: Començar comanda
fr-FR:
fr-FR:
startOrder: Lancer commande
pt-PT:
pt-PT:
startOrder: Comece uma encomenda
</i18n>

File diff suppressed because it is too large Load Diff

View File

@ -1,161 +1,179 @@
<template>
<Teleport :to="$actions">
<q-select
v-model="year"
:options="years"
color="white"
dark
standout
dense
rounded />
</Teleport>
<div class="vn-w-sm">
<div
v-if="!invoices?.length"
class="text-subtitle1 text-center text-grey-7 q-pa-md">
{{$t('noInvoicesFound')}}
<Teleport :to="$actions">
<QSelect
v-model="year"
:options="years"
color="white"
dark
standout
dense
rounded
/>
</Teleport>
<div class="vn-w-sm">
<div
v-if="!invoices?.length"
class="text-subtitle1 text-center text-grey-7 q-pa-md"
>
{{ $t('noInvoicesFound') }}
</div>
<QCard v-if="invoices?.length">
<QTable
:columns="columns"
:pagination="pagination"
:rows="invoices"
row-key="id"
hide-header
hide-bottom
>
<template v-slot:body="props">
<QTr :props="props">
<QTd key="ref" :props="props">
{{ props.row.ref }}
</QTd>
<QTd key="issued" :props="props">
{{ date(props.row.issued, 'ddd, MMMM Do') }}
</QTd>
<QTd key="amount" :props="props">
{{ currency(props.row.amount) }}
</QTd>
<QTd key="hasPdf" :props="props">
<QBtn
v-if="props.row.hasPdf"
icon="download"
:title="$t('downloadInvoicePdf')"
:href="invoiceUrl(props.row.id)"
target="_blank"
flat
round
/>
<QIcon
v-else
name="warning"
:title="$t('notDownloadable')"
color="warning"
size="24px"
/>
</QTd>
</QTr>
</template>
</QTable>
</QCard>
</div>
<q-card v-if="invoices?.length">
<q-table
:columns="columns"
:pagination="pagination"
:rows="invoices"
row-key="id"
hide-header
hide-bottom>
<template v-slot:body="props">
<q-tr :props="props">
<q-td key="ref" :props="props">
{{ props.row.ref }}
</q-td>
<q-td key="issued" :props="props">
{{ date(props.row.issued, 'ddd, MMMM Do') }}
</q-td>
<q-td key="amount" :props="props">
{{ currency(props.row.amount) }}
</q-td>
<q-td key="hasPdf" :props="props">
<q-btn
v-if="props.row.hasPdf"
icon="download"
:title="$t('downloadInvoicePdf')"
:href="invoiceUrl(props.row.id)"
target="_blank"
flat
round/>
<q-icon
v-else
name="warning"
:title="$t('notDownloadable')"
color="warning"
size="24px"/>
</q-td>
</q-tr>
</template>
</q-table>
</q-card>
</div>
</template>
<script>
import { date, currency } from 'src/lib/filters.js'
import { date, currency } from 'src/lib/filters.js';
export default {
name: 'OrdersPendingIndex',
data () {
const curYear = (new Date()).getFullYear()
const years = []
name: 'OrdersPendingIndex',
data() {
const curYear = new Date().getFullYear();
const years = [];
for (let year = curYear - 5; year <= curYear; year++) {
years.push(year)
}
for (let year = curYear - 5; year <= curYear; year++) {
years.push(year);
}
return {
columns: [
{ name: 'ref', label: 'serial', field: 'ref', align: 'left' },
{ name: 'issued', label: 'issued', field: 'issued', align: 'left' },
{ name: 'amount', label: 'amount', field: 'amount' },
{ name: 'hasPdf', label: 'download', field: 'hasPdf', align: 'center' }
],
pagination: {
rowsPerPage: 0
},
year: curYear,
years,
invoices: null
}
},
return {
columns: [
{ name: 'ref', label: 'serial', field: 'ref', align: 'left' },
{
name: 'issued',
label: 'issued',
field: 'issued',
align: 'left'
},
{ name: 'amount', label: 'amount', field: 'amount' },
{
name: 'hasPdf',
label: 'download',
field: 'hasPdf',
align: 'center'
}
],
pagination: {
rowsPerPage: 0
},
year: curYear,
years,
invoices: null
};
},
async mounted () {
await this.loadData()
},
async mounted() {
await this.loadData();
},
watch: {
async year () {
await this.loadData()
}
},
watch: {
async year() {
await this.loadData();
}
},
methods: {
date,
currency,
methods: {
date,
currency,
async loadData () {
const params = {
from: new Date(this.year, 0),
to: new Date(this.year, 11, 31, 23, 59, 59)
}
this._invoices = await this.$jApi.query(
`SELECT id, ref, issued, amount, hasPdf
async loadData() {
const params = {
from: new Date(this.year, 0),
to: new Date(this.year, 11, 31, 23, 59, 59)
};
this._invoices = await this.$jApi.query(
`SELECT id, ref, issued, amount, hasPdf
FROM myInvoice
WHERE issued BETWEEN #from AND #to
ORDER BY issued DESC
LIMIT 500`,
params
)
},
params
);
},
invoiceUrl (id) {
return '?' + new URLSearchParams({
srv: 'rest:dms/invoice',
invoice: id,
access_token: this.$user.token
}).toString()
invoiceUrl(id) {
return (
'?' +
new URLSearchParams({
srv: 'rest:dms/invoice',
invoice: id,
access_token: this.$user.token
}).toString()
);
}
}
}
}
};
</script>
<i18n lang="yaml">
en-US:
en-US:
noInvoicesFound: No invoices found
serial: Serial
issued: Date
amount: Import
downloadInvoicePdf: Download invoice PDF
notDownloadable: Not available for download, request the invoice to your salesperson
es-ES:
es-ES:
noInvoicesFound: No se han encontrado facturas
serial: Serie
issued: Fecha
amount: Importe
downloadInvoicePdf: Descargar factura en PDF
notDownloadable: No disponible para descarga, solicita la factura a tu comercial
ca-ES:
ca-ES:
noInvoicesFound: No s'han trobat factures
serial: Sèrie
issued: Data
amount: Import
downloadInvoicePdf: Descarregar PDF
notDownloadable: No disponible per cescarrega, sol·licita la factura al teu comercial
fr-FR:
fr-FR:
noInvoicesFound: Aucune facture trouvée
serial: Série
issued: Date
amount: Montant
downloadInvoicePdf: Télécharger le PDF
notDownloadable: Non disponible en téléchargement, demander la facture à votre commercial
pt-PT:
pt-PT:
noInvoicesFound: Nenhuma fatura encontrada
serial: Serie
issued: Data

View File

@ -1,201 +1,199 @@
<template>
<Teleport :to="$actions">
<div class="balance">
<span class="label">{{$t('balance')}}</span>
<span
class="amount"
:class="{negative: debt < 0}">
{{currency(debt || 0)}}
</span>
<q-icon
name="info"
:title="$t('paymentInfo')"
class="info"
size="24px"/>
<Teleport :to="$actions">
<div class="balance">
<span class="label">{{ $t('balance') }}</span>
<span class="amount" :class="{ negative: debt < 0 }">
{{ currency(debt || 0) }}
</span>
<QIcon
name="info"
:title="$t('paymentInfo')"
class="info"
size="24px"
/>
</div>
<QBtn
icon="payments"
:label="$t('makePayment')"
@click="onPayClick()"
rounded
no-caps
/>
<QBtn
to="/ecomerce/basket"
icon="shopping_cart"
:label="$t('shoppingCart')"
rounded
no-caps
/>
</Teleport>
<div class="vn-w-sm">
<div
v-if="!orders?.length"
class="text-subtitle1 text-center text-grey-7 q-pa-md"
>
{{ $t('noOrdersFound') }}
</div>
<QCard v-if="orders?.length">
<QList bordered separator padding>
<QItem
v-for="order in orders"
:key="order.id"
:to="`ticket/${order.id}`"
clickable
v-ripple
>
<QItemSection>
<QItemLabel>
{{ date(order.landed, 'ddd, MMMM Do') }}
</QItemLabel>
<QItemLabel caption>#{{ order.id }}</QItemLabel>
<QItemLabel caption>{{ order.nickname }}</QItemLabel>
<QItemLabel caption>{{ order.agency }}</QItemLabel>
</QItemSection>
<QItemSection side top> {{ order.total }} </QItemSection>
</QItem>
</QList>
</QCard>
<QPageSticky>
<QBtn
fab
icon="add_shopping_cart"
color="accent"
to="/ecomerce/catalog"
:title="$t('startOrder')"
/>
</QPageSticky>
</div>
<q-btn
icon="payments"
:label="$t('makePayment')"
@click="onPayClick()"
rounded
no-caps/>
<q-btn
to="/ecomerce/basket"
icon="shopping_cart"
:label="$t('shoppingCart')"
rounded
no-caps/>
</Teleport>
<div class="vn-w-sm">
<div
v-if="!orders?.length"
class="text-subtitle1 text-center text-grey-7 q-pa-md">
{{$t('noOrdersFound')}}
</div>
<q-card v-if="orders?.length">
<q-list bordered separator padding >
<q-item
v-for="order in orders"
:key="order.id"
:to="`ticket/${order.id}`"
clickable
v-ripple>
<q-item-section>
<q-item-label>
{{date(order.landed, 'ddd, MMMM Do')}}
</q-item-label>
<q-item-label caption>#{{order.id}}</q-item-label>
<q-item-label caption>{{order.nickname}}</q-item-label>
<q-item-label caption>{{order.agency}}</q-item-label>
</q-item-section>
<q-item-section side top>
{{order.total}}
</q-item-section>
</q-item>
</q-list>
</q-card>
<q-page-sticky>
<q-btn
fab
icon="add_shopping_cart"
color="accent"
to="/ecomerce/catalog"
:title="$t('startOrder')"/>
</q-page-sticky>
</div>
</template>
<style lang="scss" scoped>
.balance {
margin-right: 8px;
white-space: nowrap;
display: inline-block;
margin-right: 8px;
white-space: nowrap;
display: inline-block;
& > * {
vertical-align: middle;
}
& > .amount {
padding: 4px;
margin: 0 4px;
&.negative {
background-color: #e55;
border-radius: 2px;
box-shadow: 0 0 5px #333;
& > * {
vertical-align: middle;
}
& > .amount {
padding: 4px;
margin: 0 4px;
&.negative {
background-color: #e55;
border-radius: 2px;
box-shadow: 0 0 5px #333;
}
}
& > .info {
cursor: pointer;
}
}
& > .info {
cursor: pointer;
}
}
</style>
<script>
import { date, currency } from 'src/lib/filters.js'
import { tpvStore } from 'stores/tpv'
import { date, currency } from 'src/lib/filters.js';
import { tpvStore } from 'stores/tpv';
export default {
name: 'OrdersPendingIndex',
data () {
return {
orders: null,
debt: 0,
tpv: tpvStore()
name: 'OrdersPendingIndex',
data() {
return {
orders: null,
debt: 0,
tpv: tpvStore()
};
},
async mounted() {
await this.tpv.check(this.$route);
this.orders = await this.$jApi.query('CALL myTicket_list(NULL, NULL)');
this.debt = await this.$jApi.getValue('SELECT -myClient_getDebt(NULL)');
},
methods: {
date,
currency,
async onPayClick() {
let amount = -this.debt;
amount = amount <= 0 ? null : amount;
let defaultAmountStr = '';
if (amount !== null) {
defaultAmountStr = amount;
}
amount = prompt(this.$t('amountToPay'), defaultAmountStr);
if (amount != null) {
amount = parseFloat(amount.replace(',', '.'));
await this.tpv.pay(amount);
}
}
}
},
async mounted () {
await this.tpv.check(this.$route)
this.orders = await this.$jApi.query(
'CALL myTicket_list(NULL, NULL)'
)
this.debt = await this.$jApi.getValue(
'SELECT -myClient_getDebt(NULL)'
)
},
methods: {
date,
currency,
async onPayClick () {
let amount = -this.debt
amount = amount <= 0 ? null : amount
let defaultAmountStr = ''
if (amount !== null) {
defaultAmountStr = amount
}
amount = prompt(this.$t('amountToPay'), defaultAmountStr)
if (amount != null) {
amount = parseFloat(amount.replace(',', '.'))
await this.tpv.pay(amount)
}
}
}
}
};
</script>
<i18n lang="yaml">
en-US:
en-US:
startOrder: Start order
noOrdersFound: No orders found
makePayment: Make payment
shoppingCart: Shopping cart
balance: 'Balance:'
paymentInfo: >-
The amount shown is your slope (negative) or favorable balance today, it
disregards future orders. For get your order shipped, this amount must be
equal to or greater than 0. If you want to make a down payment, click the
payment button, delete the suggested amount and enter the amount you want.
es-ES:
The amount shown is your slope (negative) or favorable balance today, it
disregards future orders. For get your order shipped, this amount must be
equal to or greater than 0. If you want to make a down payment, click the
payment button, delete the suggested amount and enter the amount you want.
es-ES:
startOrder: Empezar pedido
noOrdersFound: No se encontrado pedidos
makePayment: Realizar pago
shoppingCart: Cesta de la compra
balance: 'Saldo:'
paymentInfo: >-
La cantidad mostrada es tu saldo pendiente (negativa) o favorable a día de
hoy, no tiene en cuenta pedidos del futuro. Para que tu pedido sea enviado,
esta cantidad debe ser igual o mayor que 0. Si quieres realizar una entrega a
cuenta, pulsa el botón de pago, borra la cantidad sugerida e introduce la
cantidad que desees.
ca-ES:
La cantidad mostrada es tu saldo pendiente (negativa) o favorable a día de
hoy, no tiene en cuenta pedidos del futuro. Para que tu pedido sea enviado,
esta cantidad debe ser igual o mayor que 0. Si quieres realizar una entrega a
cuenta, pulsa el botón de pago, borra la cantidad sugerida e introduce la
cantidad que desees.
ca-ES:
startOrder: Començar encàrrec
noOrdersFound: No s'han trobat comandes
makePayment: Realitzar pagament
shoppingCart: Cistella de la compra
balance: 'Saldo:'
paymentInfo: >-
La quantitat mostrada és el teu saldo pendent (negatiu) o favorable a dia
d'avui, no en compte comandes del futur. Perquè la teva comanda sigui
enviat, aquesta quantitat ha de ser igual o més gran que 0. Si vols fer un
lliurament a compte, prem el botó de pagament, esborra la quantitat suggerida
e introdueix la quantitat que vulguis.
fr-FR:
La quantitat mostrada és el teu saldo pendent (negatiu) o favorable a dia
d'avui, no en compte comandes del futur. Perquè la teva comanda sigui
enviat, aquesta quantitat ha de ser igual o més gran que 0. Si vols fer un
lliurament a compte, prem el botó de pagament, esborra la quantitat suggerida
e introdueix la quantitat que vulguis.
fr-FR:
startOrder: Acheter
noOrdersFound: Aucune commande trouvée
makePayment: Effectuer un paiement
shoppingCart: Panier
balance: 'Balance:'
paymentInfo: >-
Le montant indiqué est votre pente (négative) ou balance favorable
aujourd'hui, ne tient pas compte pour les commandes futures. Obtenir votre
commande est expédiée, ce montant doit être égal ou supérieur à 0. Si vous
voulez faire un versement, le montant suggéré effacé et entrez le montant que
vous souhaitez.
pt-PT:
Le montant indiqué est votre pente (négative) ou balance favorable
aujourd'hui, ne tient pas compte pour les commandes futures. Obtenir votre
commande est expédiée, ce montant doit être égal ou supérieur à 0. Si vous
voulez faire un versement, le montant suggéré effacé et entrez le montant que
vous souhaitez.
pt-PT:
startOrder: Iniciar encomenda
noOrdersFound: Nenhum pedido encontrado
makePayment: Realizar pagamento
shoppingCart: Cesta da compra
balance: 'Saldo:'
paymentInfo: >-
A quantidade mostrada é seu saldo pendente (negativo) ou favorável a dia de
hoje, não se vincula a pedidos futuros. Para que seu pedido seja enviado, esta
quantidade deve ser igual ou superior a 0. Se queres realizar um depósito à
conta, clique no botão de pagamento, apague a quantidade sugerida e introduza
a quantidade que deseje.
A quantidade mostrada é seu saldo pendente (negativo) ou favorável a dia de
hoje, não se vincula a pedidos futuros. Para que seu pedido seja enviado, esta
quantidade deve ser igual ou superior a 0. Se queres realizar um depósito à
conta, clique no botão de pagamento, apague a quantidade sugerida e introduza
a quantidade que deseje.
</i18n>

View File

@ -1,127 +1,145 @@
<template>
<Teleport :to="$actions">
<q-btn
icon="print"
:label="$t('printDeliveryNote')"
@click="onPrintClick()"
rounded
no-caps/>
</Teleport>
<div>
<q-card class="vn-w-sm">
<q-card-section>
<div class="text-h6">#{{ticket.id}}</div>
</q-card-section>
<q-card-section>
<div class="text-h6">{{$t('shippingInformation')}}</div>
<div>{{$t('preparation')}} {{date(ticket.shipped, 'ddd, MMMM Do')}}</div>
<div>{{$t('delivery')}} {{date(ticket.shipped, 'ddd, MMMM Do')}}</div>
<div>{{$t(ticket.method != 'PICKUP' ? 'agency' : 'warehouse')}} {{ticket.agency}}</div>
</q-card-section>
<q-card-section>
<div class="text-h6">{{$t('deliveryAddress')}}</div>
<div>{{ticket.nickname}}</div>
<div>{{ticket.street}}</div>
<div>{{ticket.postalCode}} {{ticket.city}} ({{ticket.province}})</div>
</q-card-section>
<q-separator inset />
<q-list v-for="row in rows" :key="row.itemFk">
<q-item>
<q-item-section avatar>
<q-avatar size="68px">
<img :src="`${$app.imageUrl}/catalog/200x200/${row.image}`">
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label lines="1">
{{row.concept}}
</q-item-label>
<q-item-label lines="1" caption>
{{row.value5}} {{row.value6}} {{row.value7}}
</q-item-label>
<q-item-label lines="1">
{{row.quantity}} x {{currency(row.price)}}
</q-item-label>
</q-item-section>
<q-item-section side class="total">
<q-item-label>
<span class="discount" v-if="row.discount">
{{currency(discountSubtotal(row))}} -
{{currency(row.discount)}} =
</span>
{{currency(subtotal(row))}}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-card>
</div>
<Teleport :to="$actions">
<QBtn
icon="print"
:label="$t('printDeliveryNote')"
@click="onPrintClick()"
rounded
no-caps
/>
</Teleport>
<div>
<QCard class="vn-w-sm">
<QCardSection>
<div class="text-h6">#{{ ticket.id }}</div>
</QCardSection>
<QCardSection>
<div class="text-h6">{{ $t('shippingInformation') }}</div>
<div>
{{ $t('preparation') }}
{{ date(ticket.shipped, 'ddd, MMMM Do') }}
</div>
<div>
{{ $t('delivery') }}
{{ date(ticket.shipped, 'ddd, MMMM Do') }}
</div>
<div>
{{ $t(ticket.method != 'PICKUP' ? 'agency' : 'warehouse') }}
{{ ticket.agency }}
</div>
</QCardSection>
<QCardSection>
<div class="text-h6">{{ $t('deliveryAddress') }}</div>
<div>{{ ticket.nickname }}</div>
<div>{{ ticket.street }}</div>
<div>
{{ ticket.postalCode }} {{ ticket.city }} ({{
ticket.province
}})
</div>
</QCardSection>
<QSeparator inset />
<QList v-for="row in rows" :key="row.itemFk">
<QItem>
<QItemSection avatar>
<QAvatar size="68px">
<img
:src="`${$app.imageUrl}/catalog/200x200/${row.image}`"
/>
</QAvatar>
</QItemSection>
<QItemSection>
<QItemLabel lines="1">
{{ row.concept }}
</QItemLabel>
<QItemLabel lines="1" caption>
{{ row.value5 }} {{ row.value6 }} {{ row.value7 }}
</QItemLabel>
<QItemLabel lines="1">
{{ row.quantity }} x {{ currency(row.price) }}
</QItemLabel>
</QItemSection>
<QItemSection side class="total">
<QItemLabel>
<span class="discount" v-if="row.discount">
{{ currency(discountSubtotal(row)) }} -
{{ currency(row.discount) }} =
</span>
{{ currency(subtotal(row)) }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QCard>
</div>
</template>
<style lang="scss" scoped>
.total {
justify-content: flex-end;
justify-content: flex-end;
}
</style>
<script>
import { date, currency } from 'src/lib/filters.js'
import { date, currency } from 'src/lib/filters.js';
export default {
name: 'OrdersConfirmedView',
name: 'OrdersConfirmedView',
data () {
return {
ticket: {},
rows: null,
services: null,
packages: null
}
},
async mounted () {
const params = {
ticket: parseInt(this.$route.params.id)
}
this.ticket = await this.$jApi.getObject(
'CALL myTicket_get(#ticket)',
params
)
this.rows = await this.$jApi.query(
'CALL myTicket_getRows(#ticket)',
params
)
this.services = await this.$jApi.query(
'CALL myTicket_getServices(#ticket)',
params
)
this.packages = await this.$jApi.query(
'CALL myTicket_getPackages(#ticket)',
params
)
},
methods: {
date,
currency,
discountSubtotal (line) {
return line.quantity * line.price
data() {
return {
ticket: {},
rows: null,
services: null,
packages: null
};
},
subtotal (line) {
const discount = line.discount
return this.discountSubtotal(line) * ((100 - discount) / 100)
async mounted() {
const params = {
ticket: parseInt(this.$route.params.id)
};
this.ticket = await this.$jApi.getObject(
'CALL myTicket_get(#ticket)',
params
);
this.rows = await this.$jApi.query(
'CALL myTicket_getRows(#ticket)',
params
);
this.services = await this.$jApi.query(
'CALL myTicket_getServices(#ticket)',
params
);
this.packages = await this.$jApi.query(
'CALL myTicket_getPackages(#ticket)',
params
);
},
onPrintClick () {
const params = new URLSearchParams({
access_token: this.$user.token,
recipientId: this.$user.id,
type: 'deliveryNote'
})
window.open(`/api/Tickets/${this.ticket.id}/delivery-note-pdf?${params.toString()}`)
methods: {
date,
currency,
discountSubtotal(line) {
return line.quantity * line.price;
},
subtotal(line) {
const discount = line.discount;
return this.discountSubtotal(line) * ((100 - discount) / 100);
},
onPrintClick() {
const params = new URLSearchParams({
access_token: this.$user.token,
recipientId: this.$user.id,
type: 'deliveryNote'
});
window.open(
`/api/Tickets/${this.ticket.id}/delivery-note-pdf?${params.toString()}`
);
}
}
}
}
};
</script>

View File

@ -1,31 +1,31 @@
<template>
<div class="fullscreen bg-accent text-white text-center q-pa-md flex flex-center">
<div>
<div style="font-size: 30vh">
404
</div>
<div
class="fullscreen bg-accent text-white text-center q-pa-md flex flex-center"
>
<div>
<div style="font-size: 30vh">404</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
</div>
<div class="text-h2" style="opacity: 0.4">
Oops. Nothing here...
</div>
<q-btn
class="q-mt-xl"
color="white"
text-color="accent"
unelevated
to="/"
label="Go Home"
no-caps
/>
<QBtn
class="q-mt-xl"
color="white"
text-color="accent"
unelevated
to="/"
label="Go Home"
no-caps
/>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue'
import { defineComponent } from 'vue';
export default defineComponent({
name: 'ErrorNotFound'
})
name: 'ErrorNotFound'
});
</script>

View File

@ -1,17 +1,17 @@
<template>
<q-page class="flex flex-center">
<img
alt="Quasar logo"
src="~assets/quasar-logo-vertical.svg"
style="width: 200px; height: 200px"
>
</q-page>
<QPage class="flex flex-center">
<img
alt="Quasar logo"
src="~assets/quasar-logo-vertical.svg"
style="width: 200px; height: 200px"
/>
</QPage>
</template>
<script>
import { defineComponent } from 'vue'
import { defineComponent } from 'vue';
export default defineComponent({
name: 'IndexPage'
})
name: 'IndexPage'
});
</script>

View File

@ -1,81 +1,78 @@
<template>
<div class="main">
<div class="header">
<router-link to="/" class="block">
<img
src="statics/logo.svg"
alt="Verdnatura"
class="block"
/>
</router-link>
<div class="main">
<div class="header">
<router-link to="/" class="block">
<img src="statics/logo.svg" alt="Verdnatura" class="block" />
</router-link>
</div>
<QForm @submit="onLogin" class="q-gutter-y-md">
<div class="q-gutter-y-sm">
<QInput v-model="email" :label="$t('user')" autofocus />
<QInput
v-model="password"
ref="password"
:label="$t('password')"
:type="showPwd ? 'password' : 'text'"
>
<template v-slot:append>
<QIcon
:name="showPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showPwd = !showPwd"
/>
</template>
</QInput>
<QCheckbox
v-model="remember"
:label="$t('remindMe')"
class="remember"
dense
/>
</div>
<div class="justify-center">
<QBtn
type="submit"
:label="$t('logIn')"
class="full-width"
color="primary"
rounded
no-caps
unelevated
/>
</div>
<div class="justify-center">
<QBtn
to="/"
:label="$t('logInAsGuest')"
class="full-width"
color="primary"
rounded
no-caps
outline
/>
</div>
<p class="password-forgotten text-center q-mt-lg">
<router-link to="/remember-password" class="link">
{{ $t('haveForgottenPassword') }}
</router-link>
</p>
</QForm>
<div class="footer text-center">
<p>
{{ $t('notACustomerYet') }}
<a
href="//verdnatura.es/register/"
target="_blank"
class="link"
>
{{ $t('signUp') }}
</a>
</p>
<p class="contact">
{{ $t('loginPhone') }} · {{ $t('loginMail') }}
</p>
</div>
</div>
<q-form @submit="onLogin" class="q-gutter-y-md">
<div class="q-gutter-y-sm">
<q-input
v-model="email"
:label="$t('user')"
autofocus
/>
<q-input
v-model="password"
ref="password"
:label="$t('password')"
:type="showPwd ? 'password' : 'text'">
<template v-slot:append>
<q-icon
:name="showPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showPwd = !showPwd"
/>
</template>
</q-input>
<q-checkbox
v-model="remember"
:label="$t('remindMe')"
class="remember"
dense
/>
</div>
<div class="justify-center">
<q-btn
type="submit"
:label="$t('logIn')"
class="full-width"
color="primary"
rounded
no-caps
unelevated
/>
</div>
<div class="justify-center">
<q-btn
to="/"
:label="$t('logInAsGuest')"
class="full-width"
color="primary"
rounded
no-caps
outline
/>
</div>
<p class="password-forgotten text-center q-mt-lg">
<router-link to="/remember-password" class="link">
{{$t('haveForgottenPassword')}}
</router-link>
</p>
</q-form>
<div class="footer text-center">
<p>
{{$t('notACustomerYet')}}
<a href="//verdnatura.es/register/" target="_blank" class="link">
{{$t('signUp')}}
</a>
</p>
<p class="contact">
{{$t('loginPhone')}} · {{$t('loginMail')}}
</p>
</div>
</div>
</template>
<style lang="scss" scoped>
@ -83,106 +80,106 @@ $login-margin-top: 50px;
$login-margin-between: 55px;
.main {
max-width: 280px;
max-width: 280px;
}
a {
color: inherit;
color: inherit;
}
.header {
margin-top: $login-margin-top;
margin-bottom: $login-margin-between;
margin-top: $login-margin-top;
margin-bottom: $login-margin-between;
img {
display: block;
margin: 0 auto;
width: 90%;
}
img {
display: block;
margin: 0 auto;
width: 90%;
}
}
.remember {
margin-top: 20px;
margin-bottom: 40px;
margin-top: 20px;
margin-bottom: 40px;
}
.q-btn {
height: 50px;
height: 50px;
}
.password-forgotten {
font-size: .8rem;
font-size: 0.8rem;
}
.footer {
margin-bottom: $login-margin-top;
margin-top: $login-margin-between;
text-align: center;
font-size: .8rem;
margin-bottom: $login-margin-top;
margin-top: $login-margin-between;
text-align: center;
font-size: 0.8rem;
.contact {
margin-top: 15px;
color: grey;
}
a {
font-weight: bold;
}
.contact {
margin-top: 15px;
color: grey;
}
a {
font-weight: bold;
}
}
</style>
<script>
import { userStore } from 'stores/user'
import { userStore } from 'stores/user';
export default {
name: 'VnLogin',
name: 'VnLogin',
data () {
return {
user: userStore(),
email: '',
password: '',
remember: false,
showPwd: true
}
},
data() {
return {
user: userStore(),
email: '',
password: '',
remember: false,
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()
}
},
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 () {
await this.user.login(this.email, this.password, this.remember)
this.$router.push('/')
methods: {
async onLogin() {
await this.user.login(this.email, this.password, this.remember);
this.$router.push('/');
}
}
}
}
};
</script>
<i18n lang="yaml">
en-US:
user: User
password: Password
remindMe: Remind me
logInAsGuest: Log in as guest
logIn: Log in
loginMail: infoverdnatura.es
loginPhone: +34 607 562 391
haveForgottenPassword: Have you forgotten your password?
notACustomerYet: Not a customer yet?
signUp: Register
user: User
password: Password
remindMe: Remind me
logInAsGuest: Log in as guest
logIn: Log in
loginMail: infoverdnatura.es
loginPhone: +34 607 562 391
haveForgottenPassword: Have you forgotten your password?
notACustomerYet: Not a customer yet?
signUp: Register
es-ES:
user: Usuario
password: Contraseña
remindMe: Recuérdame
logInAsGuest: Entrar como invitado
logIn: Iniciar sesión
loginMail: infoverdnatura.es
loginPhone: +34 963 242 100
haveForgottenPassword: ¿Has olvidado tu contraseña?
notACustomerYet: ¿Todavía no eres cliente?
signUp: Registrarme
user: Usuario
password: Contraseña
remindMe: Recuérdame
logInAsGuest: Entrar como invitado
logIn: Iniciar sesión
loginMail: infoverdnatura.es
loginPhone: +34 963 242 100
haveForgottenPassword: ¿Has olvidado tu contraseña?
notACustomerYet: ¿Todavía no eres cliente?
signUp: Registrarme
</i18n>

View File

@ -1,91 +1,91 @@
<template>
<div class="text-center">
<div>
<q-icon
name="contact_support"
class="block q-mx-auto text-accent"
style="font-size: 120px;"
/>
</div>
<div>
<q-form @submit="onSend" class="q-gutter-y-md text-grey-8">
<div class="text-h5">
<div>
{{$t('dontWorry')}}
</div>
<div>
{{$t('fillData')}}
</div>
</div>
<q-input
v-model="email"
:label="$t('user')"
:rules="[ val => !!val || $t('inputEmail')]"
autofocus
/>
<div class="q-mt-lg">
{{$t('weSendEmail')}}
<div class="text-center">
<div>
<QIcon
name="contact_support"
class="block q-mx-auto text-accent"
style="font-size: 120px"
/>
</div>
<div>
<q-btn
type="submit"
:label="$t('send')"
class="full-width q-mt-md"
color="primary"
rounded
no-caps
unelevated
/>
<div class="text-center q-mt-md">
<router-link to="/login" class="link">
{{$t('return')}}
</router-link>
</div>
<QForm @submit="onSend" class="q-gutter-y-md text-grey-8">
<div class="text-h5">
<div>
{{ $t('dontWorry') }}
</div>
<div>
{{ $t('fillData') }}
</div>
</div>
<QInput
v-model="email"
:label="$t('user')"
:rules="[val => !!val || $t('inputEmail')]"
autofocus
/>
<div class="q-mt-lg">
{{ $t('weSendEmail') }}
</div>
<div>
<QBtn
type="submit"
:label="$t('send')"
class="full-width q-mt-md"
color="primary"
rounded
no-caps
unelevated
/>
<div class="text-center q-mt-md">
<router-link to="/login" class="link">
{{ $t('return') }}
</router-link>
</div>
</div>
</QForm>
</div>
</q-form>
</div>
</div>
</template>
<style lang="scss" scoped>
#image {
height: 190px;
height: 190px;
}
.q-btn {
height: 50px;
height: 50px;
}
a {
color: inherit;
font-size: .8rem;
color: inherit;
font-size: 0.8rem;
}
</style>
<script>
export default {
name: 'VnRememberPasword',
data () {
return {
email: ''
name: 'VnRememberPasword',
data() {
return {
email: ''
};
},
methods: {
async onSend() {
const params = {
email: this.email
};
await this.$axios.post('Users/reset', params);
this.$q.notify({
message: this.$t('weHaveSentEmailToRecover'),
type: 'positive'
});
this.$router.push('/login');
}
}
},
methods: {
async onSend () {
const params = {
email: this.email
}
await this.$axios.post('Users/reset', params)
this.$q.notify({
message: this.$t('weHaveSentEmailToRecover'),
type: 'positive'
})
this.$router.push('/login')
}
}
}
};
</script>
<i18n lang="yaml">
en-US:
en-US:
user: User
inputEmail: Input email
rememberPassword: Rememeber password
@ -95,7 +95,7 @@ export default {
weHaveSentEmailToRecover: We've sent you an email where you can recover your password
send: Send
return: Return
es-ES:
es-ES:
user: Usuario
inputEmail: Introduce el correo electrónico
rememberPassword: Recordar contraseña

View File

@ -1,93 +1,106 @@
<template>
<div>
<q-card-section>
<q-icon
name="check"
class="block q-mx-auto text-accent"
style="font-size: 120px;"
/>
</q-card-section>
<q-card-section>
<q-form @submit="onRegister" ref="form" class="q-gutter-y-md">
<div class="text-grey-8 text-h5 text-center">
{{$t('fillData')}}
</div>
<div class="q-gutter-y-sm">
<q-input
v-model="password"
:label="$t('password')"
:type="showPwd ? 'password' : 'text'"
autofocus
hint=""
filled>
<template v-slot:append>
<q-icon
:name="showPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showPwd = !showPwd"
/>
</template>
</q-input>
<q-input
v-model="repeatPassword"
:label="$t('repeatPassword')"
:type="showRpPwd ? 'password' : 'text'"
:rules="[value => value == password || $t('repeatPasswordError')]"
hint=""
filled>
<template v-slot:append>
<q-icon
:name="showRpPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showRpPwd = !showRpPwd"
/>
</template>
</q-input>
</div>
<div>
<q-btn
type="submit"
:label="$t('resetPassword')"
class="full-width"
color="primary"
/>
<div class="text-center q-mt-xs">
<router-link to="/login" class="link">
{{$t('return')}}
</router-link>
</div>
</div>
</q-form>
</q-card-section>
</div>
<div>
<QCard-section>
<QIcon
name="check"
class="block q-mx-auto text-accent"
style="font-size: 120px"
/>
</QCard-section>
<QCard-section>
<QForm @submit="onRegister" ref="form" class="q-gutter-y-md">
<div class="text-grey-8 text-h5 text-center">
{{ $t('fillData') }}
</div>
<div class="q-gutter-y-sm">
<QInput
v-model="password"
:label="$t('password')"
:type="showPwd ? 'password' : 'text'"
autofocus
hint=""
filled
>
<template v-slot:append>
<QIcon
:name="
showPwd ? 'visibility_off' : 'visibility'
"
class="cursor-pointer"
@click="showPwd = !showPwd"
/>
</template>
</QInput>
<QInput
v-model="repeatPassword"
:label="$t('repeatPassword')"
:type="showRpPwd ? 'password' : 'text'"
:rules="[
value =>
value == password || $t('repeatPasswordError')
]"
hint=""
filled
>
<template v-slot:append>
<QIcon
:name="
showRpPwd ? 'visibility_off' : 'visibility'
"
class="cursor-pointer"
@click="showRpPwd = !showRpPwd"
/>
</template>
</QInput>
</div>
<div>
<QBtn
type="submit"
:label="$t('resetPassword')"
class="full-width"
color="primary"
/>
<div class="text-center q-mt-xs">
<router-link to="/login" class="link">
{{ $t('return') }}
</router-link>
</div>
</div>
</QForm>
</QCard-section>
</div>
</template>
<script>
export default {
name: 'VnRegister',
data () {
return {
password: '',
repeatPassword: '',
showPwd: true,
showRpPwd: true
}
},
methods: {
async onRegister () {
const headers = {
Authorization: this.$route.query.access_token
}
await this.$axios.post('users/reset-password', {
newPassword: this.password
}, { headers })
name: 'VnRegister',
data() {
return {
password: '',
repeatPassword: '',
showPwd: true,
showRpPwd: true
};
},
methods: {
async onRegister() {
const headers = {
Authorization: this.$route.query.access_token
};
await this.$axios.post(
'users/reset-password',
{
newPassword: this.password
},
{ headers }
);
this.$q.notify({
message: this.$t('passwordResetSuccessfully'),
type: 'positive'
})
this.$router.push('/login')
this.$q.notify({
message: this.$t('passwordResetSuccessfully'),
type: 'positive'
});
this.$router.push('/login');
}
}
}
}
};
</script>

View File

@ -1,6 +1,11 @@
import { route } from 'quasar/wrappers'
import { appStore } from 'stores/app'
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
import {
createRouter,
createMemoryHistory,
createWebHistory,
createWebHashHistory
} from 'vue-router'
import routes from './routes'
/*
@ -13,30 +18,34 @@ import routes from './routes'
*/
export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
const createHistory = process.env.SERVER
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE)
})
Router.afterEach((to, from) => {
if (from.name === to.name) return
const app = appStore()
app.$patch({
title: window.i18n.t(to.name || 'home'),
subtitle: null,
useRightDrawer: false,
rightDrawerOpen: true
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(
process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE
)
})
})
return Router
Router.afterEach((to, from) => {
if (from.name === to.name) return
const app = appStore()
app.$patch({
title: window.i18n.t(to.name || 'home'),
subtitle: null,
useRightDrawer: false,
rightDrawerOpen: true
})
})
return Router
})

View File

@ -1,61 +1,68 @@
const routes = [
{
path: '/login',
component: () => import('layouts/LoginLayout.vue'),
children: [
{
name: 'login',
path: '/login/:email?',
component: () => import('pages/Login/Login.vue')
}, {
name: 'rememberPassword',
path: '/remember-password',
component: () => import('pages/Login/RememberPassword.vue')
}, {
name: 'resetPassword',
path: '/reset-password',
component: () => import('pages/Login/ResetPassword.vue')
}
]
}, {
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{
name: '',
path: '',
component: () => import('src/pages/Cms/Home.vue')
}, {
name: 'home',
path: '/cms/home',
component: () => import('src/pages/Cms/Home.vue')
}, {
name: 'orders',
path: '/ecomerce/orders',
component: () => import('pages/Ecomerce/Orders.vue')
}, {
name: 'ticket',
path: '/ecomerce/ticket/:id',
component: () => import('pages/Ecomerce/Ticket.vue')
}, {
name: 'invoices',
path: '/ecomerce/invoices',
component: () => import('pages/Ecomerce/Invoices.vue')
}, {
name: 'catalog',
path: '/ecomerce/catalog/:category?/:type?',
component: () => import('pages/Ecomerce/Catalog.vue')
}
]
},
{
path: '/login',
component: () => import('layouts/LoginLayout.vue'),
children: [
{
name: 'login',
path: '/login/:email?',
component: () => import('pages/Login/Login.vue')
},
{
name: 'rememberPassword',
path: '/remember-password',
component: () => import('pages/Login/RememberPassword.vue')
},
{
name: 'resetPassword',
path: '/reset-password',
component: () => import('pages/Login/ResetPassword.vue')
}
]
},
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{
name: '',
path: '',
component: () => import('src/pages/Cms/Home.vue')
},
{
name: 'home',
path: '/cms/home',
component: () => import('src/pages/Cms/Home.vue')
},
{
name: 'orders',
path: '/ecomerce/orders',
component: () => import('pages/Ecomerce/Orders.vue')
},
{
name: 'ticket',
path: '/ecomerce/ticket/:id',
component: () => import('pages/Ecomerce/Ticket.vue')
},
{
name: 'invoices',
path: '/ecomerce/invoices',
component: () => import('pages/Ecomerce/Invoices.vue')
},
{
name: 'catalog',
path: '/ecomerce/catalog/:category?/:type?',
component: () => import('pages/Ecomerce/Catalog.vue')
}
]
},
// Always leave this as last one,
// but you can also remove it
{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue')
}
// Always leave this as last one,
// but you can also remove it
{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue')
}
]
export default routes

View File

@ -2,20 +2,18 @@ import { defineStore } from 'pinia'
import { jApi } from 'boot/axios'
export const appStore = defineStore('hedera', {
state: () => ({
title: null,
subtitle: null,
imageUrl: '',
useRightDrawer: false,
rightDrawerOpen: false
}),
state: () => ({
title: null,
subtitle: null,
imageUrl: '',
useRightDrawer: false,
rightDrawerOpen: false
}),
actions: {
async loadConfig () {
const imageUrl = await jApi.getValue(
'SELECT url FROM imageConfig'
)
this.$patch({ imageUrl })
actions: {
async loadConfig () {
const imageUrl = await jApi.getValue('SELECT url FROM imageConfig')
this.$patch({ imageUrl })
}
}
}
})

View File

@ -11,10 +11,10 @@ import { createPinia } from 'pinia'
*/
export default store((/* { ssrContext } */) => {
const pinia = createPinia()
const pinia = createPinia()
// You can add Pinia plugins here
// pinia.use(SomePiniaPlugin)
// You can add Pinia plugins here
// pinia.use(SomePiniaPlugin)
return pinia
return pinia
})

View File

@ -1,10 +1,10 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import "quasar/dist/types/feature-flag";
import 'quasar/dist/types/feature-flag'
declare module "quasar/dist/types/feature-flag" {
declare module 'quasar/dist/types/feature-flag' {
interface QuasarFeatureFlags {
store: true;
store: true
}
}

View File

@ -2,86 +2,92 @@ import { defineStore } from 'pinia'
import { jApi } from 'boot/axios'
export const tpvStore = defineStore('tpv', {
actions: {
async check (route) {
const order = route.query.tpvOrder
const status = route.query.tpvStatus
if (!(order && status)) return null
actions: {
async check (route) {
const order = route.query.tpvOrder
const status = route.query.tpvStatus
if (!(order && status)) return null
await jApi.execQuery(
'CALL myTpvTransaction_end(#order, #status)',
{ order, status }
)
await jApi.execQuery('CALL myTpvTransaction_end(#order, #status)', {
order,
status
})
if (status === 'ko') {
const retry = confirm('retryPayQuestion')
if (retry) { this.retryPay(order) }
}
if (status === 'ko') {
const retry = confirm('retryPayQuestion')
if (retry) {
this.retryPay(order)
}
}
return status
},
return status
},
async pay (amount, company) {
await this.realPay(amount * 100, company)
},
async pay (amount, company) {
await this.realPay(amount * 100, company)
},
async retryPay (order) {
const payment = await jApi.getObject(
`SELECT t.amount, m.companyFk
async retryPay (order) {
const payment = await jApi.getObject(
`SELECT t.amount, m.companyFk
FROM myTpvTransaction t
JOIN tpvMerchant m ON m.id = t.merchantFk
WHERE t.id = #order`,
{ order }
)
await this.realPay(payment.amount, payment.companyFk)
},
{ order }
)
await this.realPay(payment.amount, payment.companyFk)
},
async realPay (amount, company) {
if (!isNumeric(amount) || amount <= 0) {
throw new Error('payAmountError')
}
async realPay (amount, company) {
if (!isNumeric(amount) || amount <= 0) {
throw new Error('payAmountError')
}
const json = await jApi.send('tpv/transaction', {
amount: parseInt(amount),
urlOk: this.makeUrl('ok'),
urlKo: this.makeUrl('ko'),
company
})
const json = await jApi.send('tpv/transaction', {
amount: parseInt(amount),
urlOk: this.makeUrl('ok'),
urlKo: this.makeUrl('ko'),
company
})
const postValues = json.postValues
const postValues = json.postValues
const form = document.createElement('form')
form.method = 'POST'
form.action = json.url
document.body.appendChild(form)
const form = document.createElement('form')
form.method = 'POST'
form.action = json.url
document.body.appendChild(form)
for (const field in postValues) {
const input = document.createElement('input')
input.type = 'hidden'
input.name = field
form.appendChild(input)
for (const field in postValues) {
const input = document.createElement('input')
input.type = 'hidden'
input.name = field
form.appendChild(input)
if (postValues[field]) { input.value = postValues[field] }
}
if (postValues[field]) {
input.value = postValues[field]
}
}
form.submit()
},
form.submit()
},
makeUrl (status) {
let path = location.protocol + '//' + location.hostname
path += location.port ? ':' + location.port : ''
path += location.pathname
path += location.search ? location.search : ''
path += '#/ecomerce/orders'
path += '?' + new URLSearchParams({
tpvStatus: status,
tpvOrder: '_transactionId_'
}).toString()
return path
makeUrl (status) {
let path = location.protocol + '//' + location.hostname
path += location.port ? ':' + location.port : ''
path += location.pathname
path += location.search ? location.search : ''
path += '#/ecomerce/orders'
path +=
'?' +
new URLSearchParams({
tpvStatus: status,
tpvOrder: '_transactionId_'
}).toString()
return path
}
}
}
})
function isNumeric (n) {
return !isNaN(parseFloat(n)) && isFinite(n)
return !isNaN(parseFloat(n)) && isFinite(n)
}

View File

@ -1,61 +1,62 @@
import { defineStore } from 'pinia'
import { api, jApi } from 'boot/axios'
import { defineStore } from 'pinia';
import { api, jApi } from 'boot/axios';
export const userStore = defineStore('user', {
state: () => {
const token =
sessionStorage.getItem('vnToken') ||
localStorage.getItem('vnToken')
state: () => {
const token =
sessionStorage.getItem('vnToken') ||
localStorage.getItem('vnToken');
return {
token,
id: null,
name: null,
nickname: null
}
},
getters: {
loggedIn: state => state.token != null
},
actions: {
async login (user, password, remember) {
const params = { user, password }
const res = await api.post('Accounts/login', params)
if (remember) {
localStorage.setItem('vnToken', res.data.token)
} else {
sessionStorage.setItem('vnToken', res.data.token)
}
this.$patch({
token: res.data.token,
name: user
})
return {
token,
id: null,
name: null,
nickname: null,
isGuest: false
};
},
async logout () {
if (this.token != null) {
try {
await api.post('Accounts/logout')
} catch (e) {}
localStorage.removeItem('vnToken')
sessionStorage.removeItem('vnToken')
}
this.$reset()
getters: {
loggedIn: state => state.token != null
},
async loadData () {
const userData = await jApi.getObject(
'SELECT id, nickname FROM account.myUser'
)
actions: {
async login(user, password, remember) {
const params = { user, password };
const res = await api.post('Accounts/login', params);
this.$patch({
id: userData.id,
nickname: userData.nickname
})
if (remember) {
localStorage.setItem('vnToken', res.data.token);
} else {
sessionStorage.setItem('vnToken', res.data.token);
}
this.$patch({
token: res.data.token,
name: user
});
},
async logout() {
if (this.token != null) {
try {
await api.post('Accounts/logout');
} catch (e) {}
localStorage.removeItem('vnToken');
sessionStorage.removeItem('vnToken');
}
this.$reset();
},
async loadData() {
const userData = await jApi.getObject(
'SELECT id, nickname FROM account.myUser'
);
this.$patch({
id: userData.id,
nickname: userData.nickname
});
}
}
}
})
});

View File

@ -11,126 +11,145 @@ const outputPath = path.join(__dirname, wpConfig.buildDir);
const publicPath = '/' + wpConfig.buildDir + '/';
const baseConfig = {
entry: wpConfig.entry,
mode: devMode ? 'development' : 'production',
output: {
path: outputPath,
publicPath: publicPath
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}, {
test: /tinymce\/.*\/skin\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}, {
test: /tinymce\/.*\/content\.css$/i,
loader: 'css-loader',
options: {esModule: false}
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader'],
exclude: [/node_modules/]
}, {
test: /\.yml$/,
use: ['json-loader', 'yaml-loader']
}, {
test: /\.xml$/,
use: 'raw-loader'
}, {
test: /\.ttf$/,
type: 'asset/resource'
}, {
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
sourceMap: true
}
}
]
}
]
},
resolve: {
modules: [
__dirname +'/js',
__dirname,
__dirname +'/forms',
'node_modules',
'/usr/lib/node_modules'
]
},
node: {
__dirname: true
},
plugins: [
new AssetsWebpackPlugin({
path: outputPath
}),
new webpack.DefinePlugin({
_DEV_MODE: devMode,
_DEV_SERVER_PORT: wpConfig.devServerPort,
_PUBLIC_PATH: JSON.stringify(publicPath)
}),
new MiniCssExtractPlugin()
],
optimization: {
runtimeChunk: true,
splitChunks: {
chunks: 'all',
}
},
watchOptions: {
ignored: /node_modules/
}
entry: wpConfig.entry,
mode: devMode ? 'development' : 'production',
output: {
path: outputPath,
publicPath: publicPath
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /tinymce\/.*\/skin\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /tinymce\/.*\/content\.css$/i,
loader: 'css-loader',
options: { esModule: false }
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
exclude: [/node_modules/]
},
{
test: /\.yml$/,
use: ['json-loader', 'yaml-loader']
},
{
test: /\.xml$/,
use: 'raw-loader'
},
{
test: /\.ttf$/,
type: 'asset/resource'
},
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
sourceMap: true
}
}
]
},
{
test: /\.(woff|woff2)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/'
}
}
]
}
]
},
resolve: {
modules: [
__dirname + '/js',
__dirname,
__dirname + '/forms',
'node_modules',
'/usr/lib/node_modules'
]
},
node: {
__dirname: true
},
plugins: [
new AssetsWebpackPlugin({
path: outputPath
}),
new webpack.DefinePlugin({
_DEV_MODE: devMode,
_DEV_SERVER_PORT: wpConfig.devServerPort,
_PUBLIC_PATH: JSON.stringify(publicPath)
}),
new MiniCssExtractPlugin()
],
optimization: {
runtimeChunk: true,
splitChunks: {
chunks: 'all'
}
},
watchOptions: {
ignored: /node_modules/
}
};
const prodConfig = {
output: {
filename: '[name].[chunkhash].js',
chunkFilename: 'chunk.[id].[chunkhash].js'
},
optimization: {
moduleIds: 'deterministic'
},
devtool: 'source-map'
output: {
filename: '[name].[chunkhash].js',
chunkFilename: 'chunk.[id].[chunkhash].js'
},
optimization: {
moduleIds: 'deterministic'
},
devtool: 'source-map'
};
const devConfig = {
output: {
filename: '[name].js',
chunkFilename: 'chunk.[id].js'
},
optimization: {
moduleIds: 'named'
},
devtool: 'eval',
devServer: {
host: '0.0.0.0',
static: __dirname,
port: wpConfig.devServerPort,
headers: {'Access-Control-Allow-Origin': '*'},
//stats: { chunks: false },
proxy: {
'/api': 'http://localhost:3000',
'/': {
target: 'http://localhost/projects/hedera-web',
bypass: (req) => req.path !== '/' ? req.path : null
}
}
}
output: {
filename: '[name].js',
chunkFilename: 'chunk.[id].js'
},
optimization: {
moduleIds: 'named'
},
devtool: 'eval',
devServer: {
host: '0.0.0.0',
static: __dirname,
port: wpConfig.devServerPort,
headers: { 'Access-Control-Allow-Origin': '*' },
//stats: { chunks: false },
proxy: {
'/api': 'http://localhost:3000',
'/': {
target: 'http://localhost/projects/hedera-web',
bypass: req => (req.path !== '/' ? req.path : null)
}
}
}
};
const mrgConfig = devMode ? devConfig : prodConfig;