0
1
Fork 0

Merge pull request 'Init config' (!68) from wbuezas/hedera-web-mindshore:feature/InitConfig into 4922-vueMigration

Reviewed-on: verdnatura/hedera-web#68
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
This commit is contained in:
Javier Segarra 2024-07-19 11:13:55 +00:00
commit ce557dc5b9
45 changed files with 29084 additions and 28301 deletions

View File

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

15
.vscode/settings.json vendored
View File

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

100
Jenkinsfile vendored
View File

@ -1,34 +1,25 @@
#!/usr/bin/env groovy #!/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 { pipeline {
agent any agent any
environment { environment {
PROJECT_NAME = 'hedera-web' PROJECT_NAME = 'hedera-web'
STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}"
} }
stages { 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') { stage('Debuild') {
when { when {
anyOf { anyOf {
@ -38,31 +29,28 @@ pipeline {
} }
agent { agent {
docker { 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/' registryUrl 'https://registry.verdnatura.es/'
registryCredentialsId 'docker-registry' registryCredentialsId 'docker-registry'
args '-v /mnt/appdata/reprepro:/reprepro'
} }
} }
steps { steps {
sh 'debuild -us -uc -b' sh 'debuild -us -uc -b'
sh 'vn-includedeb stretch' sh 'mkdir -p debuild'
} sh 'mv ../hedera-web_* debuild'
}
stage('Container') { script {
when { def files = findFiles(glob: 'debuild/*.changes')
anyOf { files.each { file -> env.CHANGES_FILE = file.name }
branch 'master' }
branch 'test'
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') { stage('Deploy') {
@ -73,15 +61,41 @@ pipeline {
} }
} }
environment { environment {
DOCKER_HOST = "${env.SWARM_HOST}" CREDS = credentials('docker-registry')
IMAGE = "$REGISTRY/verdnatura/hedera-web"
} }
steps { 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 { post {
unsuccessful { unsuccessful {
setEnv()
sendEmail() 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", "name": "hedera-web",
"version": "22.48.2", "version": "22.48.2",
"description": "Verdnatura web page", "description": "Verdnatura web page",
"license": "GPL-3.0", "license": "GPL-3.0",
"author": "Juan Ferrer Toribio <juan@verdnatura.es>", "productName": "Salix",
"repository": { "author": "Verdnatura",
"type": "git", "repository": {
"url": "https://git.verdnatura.es/hedera-web" "type": "git",
}, "url": "https://git.verdnatura.es/hedera-web"
"devDependencies": { },
"@babel/eslint-parser": "^7.13.14", "devDependencies": {
"@babel/preset-env": "^7.20.2", "@babel/eslint-parser": "^7.13.14",
"@intlify/vue-i18n-loader": "^4.2.0", "@babel/preset-env": "^7.20.2",
"@quasar/app-webpack": "^3.0.0", "@intlify/vue-i18n-loader": "^4.2.0",
"archiver": "^5.3.1", "@quasar/app-webpack": "^3.0.0",
"assets-webpack-plugin": "^7.1.1", "archiver": "^5.3.1",
"babel-loader": "^9.1.0", "assets-webpack-plugin": "^7.1.1",
"bundle-loader": "^0.5.6", "babel-loader": "^9.1.0",
"eslint": "^8.10.0", "bundle-loader": "^0.5.6",
"eslint-config-standard": "^17.0.0", "css-loader": "^5.2.7",
"eslint-plugin-import": "^2.19.1", "eslint": "^8.57.0",
"eslint-plugin-n": "^15.0.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-promise": "^6.0.0", "eslint-config-standard": "^17.0.0",
"eslint-plugin-vue": "^9.0.0", "eslint-plugin-import": "^2.19.1",
"eslint-webpack-plugin": "^3.1.1", "eslint-plugin-n": "^15.0.0",
"file-loader": "^6.2.0", "eslint-plugin-prettier": "^5.1.3",
"fs-extra": "^10.1.0", "eslint-plugin-promise": "^6.0.0",
"glob": "^8.0.3", "eslint-plugin-vue": "^9.27.0",
"html-webpack-plugin": "^5.5.0", "eslint-webpack-plugin": "^3.1.1",
"json-loader": "^0.5.7", "file-loader": "^6.2.0",
"mini-css-extract-plugin": "^2.7.0", "fs-extra": "^10.1.0",
"node-sass": "^7.0.1", "glob": "^8.0.3",
"raw-loader": "^4.0.2", "html-webpack-plugin": "^5.5.0",
"sass-loader": "^12.6.0", "json-loader": "^0.5.7",
"style-loader": "^3.3.1", "mini-css-extract-plugin": "^2.7.0",
"url-loader": "^4.1.1", "node-sass": "^7.0.1",
"webpack": "^5.75.0", "postcss": "^8.4.39",
"webpack-cli": "^4.10.0", "postcss-import": "^13.0.0",
"webpack-dev-server": "^4.11.1", "postcss-loader": "^4.3.0",
"webpack-merge": "^5.8.0", "postcss-url": "^10.1.3",
"yaml-loader": "^0.5.0" "raw-loader": "^4.0.2",
}, "sass-loader": "^12.6.0",
"dependencies": { "style-loader": "^3.3.1",
"@quasar/extras": "^1.0.0", "url-loader": "^4.1.1",
"axios": "^0.21.1", "webpack": "^5.75.0",
"core-js": "^3.6.5", "webpack-cli": "^4.10.0",
"js-yaml": "^3.12.1", "webpack-dev-server": "^4.11.1",
"mootools": "^1.5.2", "webpack-merge": "^5.8.0",
"pinia": "^2.0.11", "yaml-loader": "^0.5.0"
"promise-polyfill": "^8.2.3", },
"quasar": "^2.6.0", "dependencies": {
"require-yaml": "0.0.1", "@quasar/extras": "^1.0.0",
"tinymce": "^6.3.0", "axios": "^0.21.1",
"vue": "^3.0.0", "core-js": "^3.6.5",
"vue-i18n": "^9.0.0", "js-yaml": "^3.12.1",
"vue-router": "^4.0.0" "mootools": "^1.5.2",
}, "pinia": "^2.0.11",
"scripts": { "promise-polyfill": "^8.2.3",
"front": "webpack serve --open", "quasar": "^2.6.0",
"back": "cd ../vn-database && myvc start && cd ../salix && gulp backOnly", "require-yaml": "0.0.1",
"build": "rm -rf build/ ; webpack", "tinymce": "^6.3.0",
"clean": "rm -rf build/", "vue": "^3.0.0",
"lint": "eslint --ext .js,.vue ./" "vue-i18n": "^9.0.0",
}, "vue-router": "^4.0.0"
"browserslist": [ },
"last 10 Chrome versions", "scripts": {
"last 10 Firefox versions", "front": "webpack serve --open",
"last 4 Edge versions", "back": "cd ../vn-database && myvc start && cd ../salix && gulp backOnly",
"last 7 Safari versions", "build": "rm -rf build/ ; webpack",
"last 8 Android versions", "clean": "rm -rf build/",
"last 8 ChromeAndroid versions", "lint": "eslint --ext .js,.vue ./"
"last 8 FirefoxAndroid versions", },
"last 10 iOS versions", "browserslist": [
"last 5 Opera versions" "last 10 Chrome versions",
], "last 10 Firefox versions",
"engines": { "last 4 Edge versions",
"node": ">= 12.22.1", "last 7 Safari versions",
"npm": ">= 6.13.4", "last 8 Android versions",
"yarn": ">= 1.21.1" "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' type: 'http'
}, },
port: 8080, port: 8080,
open: true, // opens browser window automatically open: false,
// static: __dirname, // static: __dirname,
headers: { 'Access-Control-Allow-Origin': '*' }, headers: { 'Access-Control-Allow-Origin': '*' },
// stats: { chunks: false }, // stats: { chunks: false },
proxy: { proxy: {
'/api': 'http://localhost:3000', '/api': 'http://localhost:3000',
'/': { '/': {
target: 'http://localhost/projects/hedera-web', target: 'http://localhost:3001',
bypass: (req) => req.path !== '/' ? req.path : null 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 // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-framework
framework: { framework: {
config: {}, config: {},
autoImportComponentCase: 'pascal',
// iconSet: 'material-icons', // Quasar icon set // iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack // lang: 'en-US', // Quasar language pack

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
export default async ({ app }) => { export default async ({ app }) => {
/* /*
window.addEventListener('error', window.addEventListener('error',
e => onWindowError(e)); e => onWindowError(e));
window.addEventListener('unhandledrejection', window.addEventListener('unhandledrejection',
@ -13,55 +12,55 @@ export default async ({ app }) => {
errorHandler(event.reason); errorHandler(event.reason);
} }
*/ */
app.config.errorHandler = (err, vm, info) => { app.config.errorHandler = (err, vm, info) => {
errorHandler(err, vm) 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 } }
}
} }
if (res) { function errorHandler (err, vm) {
const status = res.status let message
let tMessage
let res = err.response
if (status >= 400 && status < 500) { // XXX: Compatibility with old JSON service
switch (status) { if (err.name === 'JsonException') {
case 401: res = {
tMessage = 'loginFailed' status: err.statusCode,
break data: { error: { message: err.message } }
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) { if (res) {
message = vm.$t(tMessage) 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 { boot } from 'quasar/wrappers';
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n';
import messages from 'src/i18n' import messages from 'src/i18n';
export default boot(({ app }) => { const i18n = createI18n({
const i18n = createI18n({ locale: navigator.language || navigator.userLanguage,
locale: 'es-ES', fallbackLocale: 'en',
globalInjection: true, globalInjection: true,
missingWarn: false,
fallbackWarn: false,
legacy: false,
silentTranslationWarn: true, silentTranslationWarn: true,
silentFallbackWarn: true, silentFallbackWarn: true,
messages messages
}) });
export default boot(({ app }) => {
// Set i18n instance on app
app.use(i18n);
// Set i18n instance on app window.i18n = i18n.global;
app.use(i18n) });
window.i18n = i18n.global export { i18n };
})

View File

@ -5,8 +5,8 @@
src: url(./poppins.ttf) format('truetype'); src: url(./poppins.ttf) format('truetype');
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: 'Open Sans';
src: url(./opensans.ttf) format('truetype'); src: url(./opensans.ttf) format('truetype');
} }
@mixin mobile { @mixin mobile {
@media screen and (max-width: 960px) { @media screen and (max-width: 960px) {
@ -28,7 +28,7 @@ a.link {
} }
.q-card { .q-card {
border-radius: 7px; 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 { .q-page-sticky.fixed-bottom-right {
margin: 18px; margin: 18px;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,121 +1,130 @@
import { Result } from './result' import { Result } from './result'
/** /**
* This class stores the database results. * This class stores the database results.
*/ */
export class ResultSet { export class ResultSet {
results = null results = null
error = null error = null
/** /**
* Initilizes the resultset object. * Initilizes the resultset object.
*/ */
constructor (results, error) { constructor (results, error) {
this.results = results this.results = results
this.error = error this.error = error
} }
/** /**
* Gets the query error. * Gets the query error.
* *
* @return {Db.Err} the error or null if no errors hapened * @return {Db.Err} the error or null if no errors hapened
*/ */
getError () { getError () {
return this.error return this.error
} }
fetch () { fetch () {
if (this.error) { throw this.error } if (this.error) {
throw this.error
}
if (this.results !== null && if (this.results !== null && this.results.length > 0) {
this.results.length > 0) { return this.results.shift() } return this.results.shift()
}
return null return null
} }
/** /**
* Fetchs the next result from the resultset. * Fetchs the next result from the resultset.
* *
* @return {Db.Result} the result or %null if error or there are no more results * @return {Db.Result} the result or %null if error or there are no more results
*/ */
fetchResult () { fetchResult () {
const result = this.fetch() const result = this.fetch()
if (result !== null) { if (result !== null) {
if (result.data instanceof Array) { if (result.data instanceof Array) {
return new Result(result) return new Result(result)
} else { } else {
return true return true
} }
}
return null
} }
return null /**
}
/**
* Fetchs the first row object from the next resultset. * Fetchs the first row object from the next resultset.
* *
* @return {Array} the row if success, %null otherwise * @return {Array} the row if success, %null otherwise
*/ */
fetchObject () { fetchObject () {
const result = this.fetch() const result = this.fetch()
if (result !== null && if (
result.data instanceof Array && result !== null &&
result.data.length > 0) { return result.data[0] } result.data instanceof Array &&
result.data.length > 0
) {
return result.data[0]
}
return null return null
} }
/** /**
* Fetchs data from the next resultset. * Fetchs data from the next resultset.
* *
* @return {Array} the data * @return {Array} the data
*/ */
fetchData () { fetchData () {
const result = this.fetch() const result = this.fetch()
if (result !== null && if (result !== null && result.data instanceof Array) {
result.data instanceof Array) { return result.data
return result.data }
return null
} }
return null /**
}
/**
* Fetchs the first row and column value from the next resultset. * Fetchs the first row and column value from the next resultset.
* *
* @return {Object} the value if success, %null otherwise * @return {Object} the value if success, %null otherwise
*/ */
fetchValue () { fetchValue () {
const row = this.fetchRow() 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. * Fetchs the first row from the next resultset.
* *
* @return {Array} the row if success, %null otherwise * @return {Array} the row if success, %null otherwise
*/ */
fetchRow () { fetchRow () {
const result = this.fetch() const result = this.fetch()
if (result !== null && if (
result.data instanceof Array && result !== null &&
result.data.length > 0) { result.data instanceof Array &&
const object = result.data[0] result.data.length > 0
const row = new Array(result.columns.length) ) {
for (let i = 0; i < row.length; i++) { const object = result.data[0]
row[i] = object[result.columns[i].name] const row = new Array(result.columns.length)
} for (let i = 0; i < row.length; i++) {
return row 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. * This class stores a database result.
*/ */
export class Result { export class Result {
/** /**
* Initilizes the result object. * Initilizes the result object.
*/ */
constructor (result) { constructor (result) {
this.data = result.data this.data = result.data
this.tables = result.tables this.tables = result.tables
this.columns = result.columns this.columns = result.columns
this.row = -1 this.row = -1
if (this.columns) { if (this.columns) {
this.columnMap = {} this.columnMap = {}
for (let i = 0; i < this.columns.length; i++) { for (let i = 0; i < this.columns.length; i++) {
const col = this.columns[i] const col = this.columns[i]
col.index = i col.index = i
this.columnMap[col.name] = col this.columnMap[col.name] = col
} }
} else { this.columnMap = null } } else {
} this.columnMap = null
}
}
/** /**
* Gets a value from de result. * Gets a value from de result.
* *
* @param {String} columnName The column name * @param {String} columnName The column name
* @return {Object} The cell value * @return {Object} The cell value
*/ */
get (columnName) { get (columnName) {
return this.data[this.row][columnName] return this.data[this.row][columnName]
} }
/** /**
* Gets a row. * Gets a row.
* *
* @return {Object} The cell value * @return {Object} The cell value
*/ */
getObject () { getObject () {
return this.data[this.row] return this.data[this.row]
} }
/** /**
* Resets the result iterator. * Resets the result iterator.
*/ */
reset () { reset () {
this.row = -1 this.row = -1
} }
/** /**
* Moves the internal iterator to the next row. * Moves the internal iterator to the next row.
*/ */
next () { next () {
this.row++ 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 { VnObject } from './object'
import { JsonException } from './json-exception' import { JsonException } from './json-exception'
@ -6,16 +5,16 @@ import { JsonException } from './json-exception'
* Handler for JSON rest connections. * Handler for JSON rest connections.
*/ */
export class JsonConnection extends VnObject { export class JsonConnection extends VnObject {
_connected = false _connected = false
_requestsCount = 0 _requestsCount = 0
token = null token = null
interceptors = [] interceptors = []
use (fn) { use (fn) {
this.interceptors.push(fn) this.interceptors.push(fn)
} }
/** /**
* Executes the specified REST service with the given params and calls * Executes the specified REST service with the given params and calls
* the callback when response is received. * 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 * @param {Object} params The params to pass to the service
* @return {Object} The parsed JSON response * @return {Object} The parsed JSON response
*/ */
async send (url, params) { async send (url, params) {
if (!params) params = {} if (!params) params = {}
params.srv = `json:${url}` params.srv = `json:${url}`
return this.sendWithUrl('POST', '.', params) 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 }
} }
return this.sendWithUrl('POST', form.action, params) async sendForm (form) {
} const params = {}
const elements = form.elements
async sendFormMultipart (form) { for (let i = 0; i < elements.length; i++) {
return this.request({ if (elements[i].name) {
method: 'POST', params[elements[i].name] = elements[i].value
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 return this.sendWithUrl('POST', form.action, params)
}
} catch (e) {
data = null
error = e
} }
if (error) { async sendFormMultipart (form) {
this.emit('error', error) return this.request({
reject(error) method: 'POST',
} else { resolve(data) } 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. * This class stores the database errors.
*/ */
export class JsonException { export class JsonException {
constructor (exception, message, code, file, line, trace, statucCode) { constructor (exception, message, code, file, line, trace, statucCode) {
this.name = 'JsonException' this.name = 'JsonException'
this.exception = exception this.exception = exception
this.message = message this.message = message
this.code = code this.code = code
this.file = file this.file = file
this.line = line this.line = line
this.trace = trace this.trace = trace
this.statusCode = statucCode this.statusCode = statucCode
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,74 +1,73 @@
import { date as qdate, format } from 'quasar' import { date as qdate, format } from 'quasar'
const { pad } = format const { pad } = format
export function currency (val) { 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) { export function date (val, format) {
if (val == null) return val if (val == null) return val
if (!(val instanceof Date)) { if (!(val instanceof Date)) {
val = new Date(val) val = new Date(val)
} }
return qdate.formatDate(val, format, window.i18n.tm('date')) return qdate.formatDate(val, format, window.i18n.tm('date'))
} }
export function relDate (val) { export function relDate (val) {
if (val == null) return val if (val == null) return val
if (!(val instanceof Date)) { if (!(val instanceof Date)) {
val = new Date(val) 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'))
} }
}
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) { export function relTime (val) {
if (val == null) return val if (val == null) return val
if (!(val instanceof Date)) { if (!(val instanceof Date)) {
val = new Date(val) val = new Date(val)
} }
return relDate(val) + ' ' + qdate.formatDate(val, 'H:mm:ss') return relDate(val) + ' ' + qdate.formatDate(val, 'H:mm:ss')
} }
export function elapsedTime (val) { export function elapsedTime (val) {
if (val == null) return val if (val == null) return val
if (!(val instanceof Date)) { if (!(val instanceof Date)) {
val = new Date(val) val = new Date(val)
} }
const now = (new Date()).getTime() const now = new Date().getTime()
val = Math.floor((now - val.getTime()) / 1000) val = Math.floor((now - val.getTime()) / 1000)
const hours = Math.floor(val / 3600) const hours = Math.floor(val / 3600)
val -= hours * 3600 val -= hours * 3600
const minutes = Math.floor(val / 60) const minutes = Math.floor(val / 60)
val -= minutes * 60 val -= minutes * 60
const seconds = val 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> <template>
<div style="padding: 0;"> <div style="padding: 0">
<div class="q-pa-sm row items-start"> <div class="q-pa-sm row items-start">
<div <div class="new-card q-pa-sm" v-for="myNew in news" :key="myNew.id">
class="new-card q-pa-sm" <QCard>
v-for="myNew in news" <QImg :src="`${$app.imageUrl}/news/full/${myNew.image}`">
:key="myNew.id"> </QImg>
<q-card> <QCardSection>
<q-img :src="`${$app.imageUrl}/news/full/${myNew.image}`"> <div class="text-h5">{{ myNew.title }}</div>
</q-img> </QCardSection>
<q-card-section> <QCardSection class="new-body">
<div class="text-h5">{{ myNew.title }}</div> <div v-html="myNew.text" />
</q-card-section> </QCardSection>
<q-card-section class="new-body"> </QCard>
<div v-html="myNew.text"/> </div>
</q-card-section> </div>
</q-card> <QPageSticky>
</div> <QBtn
fab
icon="add_shopping_cart"
color="accent"
to="/ecomerce/catalog"
:title="$t('startOrder')"
/>
</QPageSticky>
</div> </div>
<q-page-sticky>
<q-btn
fab
icon="add_shopping_cart"
color="accent"
to="/ecomerce/catalog"
:title="$t('startOrder')"
/>
</q-page-sticky>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.new-card { .new-card {
width: 100%; width: 100%;
@media screen and (min-width: 800px) and (max-width: 1400px) { @media screen and (min-width: 800px) and (max-width: 1400px) {
width: 50%; width: 50%;
} }
@media screen and (min-width: 1401px) and (max-width: 1920px) { @media screen and (min-width: 1401px) and (max-width: 1920px) {
width: 33.33%; width: 33.33%;
} }
@media screen and (min-width: 19021) { @media screen and (min-width: 19021) {
width: 25%; width: 25%;
} }
} }
.new-body { .new-body {
font-family: 'Open Sans'; font-family: 'Open Sans';
} }
</style> </style>
<script> <script>
export default { export default {
name: 'PageIndex', name: 'PageIndex',
data () { data() {
return { return {
news: [] news: []
} };
}, },
async mounted () { async mounted() {
this.news = await this.$jApi.query( this.news = await this.$jApi.query(
`SELECT title, text, image, id `SELECT title, text, image, id
FROM news FROM news
ORDER BY priority, created DESC` ORDER BY priority, created DESC`
) );
} }
} };
</script> </script>
<i18n lang="yaml"> <i18n lang="yaml">
en-US: en-US:
startOrder: Start order startOrder: Start order
es-ES: es-ES:
startOrder: Empezar pedido startOrder: Empezar pedido
ca-ES: ca-ES:
startOrder: Començar comanda startOrder: Començar comanda
fr-FR: fr-FR:
startOrder: Lancer commande startOrder: Lancer commande
pt-PT: pt-PT:
startOrder: Comece uma encomenda startOrder: Comece uma encomenda
</i18n> </i18n>

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,201 +1,199 @@
<template> <template>
<Teleport :to="$actions"> <Teleport :to="$actions">
<div class="balance"> <div class="balance">
<span class="label">{{$t('balance')}}</span> <span class="label">{{ $t('balance') }}</span>
<span <span class="amount" :class="{ negative: debt < 0 }">
class="amount" {{ currency(debt || 0) }}
:class="{negative: debt < 0}"> </span>
{{currency(debt || 0)}} <QIcon
</span> name="info"
<q-icon :title="$t('paymentInfo')"
name="info" class="info"
:title="$t('paymentInfo')" size="24px"
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> </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> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.balance { .balance {
margin-right: 8px; margin-right: 8px;
white-space: nowrap; white-space: nowrap;
display: inline-block; display: inline-block;
& > * { & > * {
vertical-align: middle; vertical-align: middle;
} }
& > .amount { & > .amount {
padding: 4px; padding: 4px;
margin: 0 4px; margin: 0 4px;
&.negative { &.negative {
background-color: #e55; background-color: #e55;
border-radius: 2px; border-radius: 2px;
box-shadow: 0 0 5px #333; box-shadow: 0 0 5px #333;
}
}
& > .info {
cursor: pointer;
} }
}
& > .info {
cursor: pointer;
}
} }
</style> </style>
<script> <script>
import { date, currency } from 'src/lib/filters.js' import { date, currency } from 'src/lib/filters.js';
import { tpvStore } from 'stores/tpv' import { tpvStore } from 'stores/tpv';
export default { export default {
name: 'OrdersPendingIndex', name: 'OrdersPendingIndex',
data () { data() {
return { return {
orders: null, orders: null,
debt: 0, debt: 0,
tpv: tpvStore() 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> </script>
<i18n lang="yaml"> <i18n lang="yaml">
en-US: en-US:
startOrder: Start order startOrder: Start order
noOrdersFound: No orders found noOrdersFound: No orders found
makePayment: Make payment makePayment: Make payment
shoppingCart: Shopping cart shoppingCart: Shopping cart
balance: 'Balance:' balance: 'Balance:'
paymentInfo: >- paymentInfo: >-
The amount shown is your slope (negative) or favorable balance today, it The amount shown is your slope (negative) or favorable balance today, it
disregards future orders. For get your order shipped, this amount must be 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 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. payment button, delete the suggested amount and enter the amount you want.
es-ES: es-ES:
startOrder: Empezar pedido startOrder: Empezar pedido
noOrdersFound: No se encontrado pedidos noOrdersFound: No se encontrado pedidos
makePayment: Realizar pago makePayment: Realizar pago
shoppingCart: Cesta de la compra shoppingCart: Cesta de la compra
balance: 'Saldo:' balance: 'Saldo:'
paymentInfo: >- paymentInfo: >-
La cantidad mostrada es tu saldo pendiente (negativa) o favorable a día de 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, 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 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 cuenta, pulsa el botón de pago, borra la cantidad sugerida e introduce la
cantidad que desees. cantidad que desees.
ca-ES: ca-ES:
startOrder: Començar encàrrec startOrder: Començar encàrrec
noOrdersFound: No s'han trobat comandes noOrdersFound: No s'han trobat comandes
makePayment: Realitzar pagament makePayment: Realitzar pagament
shoppingCart: Cistella de la compra shoppingCart: Cistella de la compra
balance: 'Saldo:' balance: 'Saldo:'
paymentInfo: >- paymentInfo: >-
La quantitat mostrada és el teu saldo pendent (negatiu) o favorable a dia 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 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 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 lliurament a compte, prem el botó de pagament, esborra la quantitat suggerida
e introdueix la quantitat que vulguis. e introdueix la quantitat que vulguis.
fr-FR: fr-FR:
startOrder: Acheter startOrder: Acheter
noOrdersFound: Aucune commande trouvée noOrdersFound: Aucune commande trouvée
makePayment: Effectuer un paiement makePayment: Effectuer un paiement
shoppingCart: Panier shoppingCart: Panier
balance: 'Balance:' balance: 'Balance:'
paymentInfo: >- paymentInfo: >-
Le montant indiqué est votre pente (négative) ou balance favorable Le montant indiqué est votre pente (négative) ou balance favorable
aujourd'hui, ne tient pas compte pour les commandes futures. Obtenir votre 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 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 voulez faire un versement, le montant suggéré effacé et entrez le montant que
vous souhaitez. vous souhaitez.
pt-PT: pt-PT:
startOrder: Iniciar encomenda startOrder: Iniciar encomenda
noOrdersFound: Nenhum pedido encontrado noOrdersFound: Nenhum pedido encontrado
makePayment: Realizar pagamento makePayment: Realizar pagamento
shoppingCart: Cesta da compra shoppingCart: Cesta da compra
balance: 'Saldo:' balance: 'Saldo:'
paymentInfo: >- paymentInfo: >-
A quantidade mostrada é seu saldo pendente (negativo) ou favorável a dia de 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 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 à 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 conta, clique no botão de pagamento, apague a quantidade sugerida e introduza
a quantidade que deseje. a quantidade que deseje.
</i18n> </i18n>

View File

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

View File

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

View File

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

View File

@ -1,81 +1,78 @@
<template> <template>
<div class="main"> <div class="main">
<div class="header"> <div class="header">
<router-link to="/" class="block"> <router-link to="/" class="block">
<img <img src="statics/logo.svg" alt="Verdnatura" class="block" />
src="statics/logo.svg" </router-link>
alt="Verdnatura" </div>
class="block" <QForm @submit="onLogin" class="q-gutter-y-md">
/> <div class="q-gutter-y-sm">
</router-link> <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> </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> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -83,106 +80,106 @@ $login-margin-top: 50px;
$login-margin-between: 55px; $login-margin-between: 55px;
.main { .main {
max-width: 280px; max-width: 280px;
} }
a { a {
color: inherit; color: inherit;
} }
.header { .header {
margin-top: $login-margin-top; margin-top: $login-margin-top;
margin-bottom: $login-margin-between; margin-bottom: $login-margin-between;
img { img {
display: block; display: block;
margin: 0 auto; margin: 0 auto;
width: 90%; width: 90%;
} }
} }
.remember { .remember {
margin-top: 20px; margin-top: 20px;
margin-bottom: 40px; margin-bottom: 40px;
} }
.q-btn { .q-btn {
height: 50px; height: 50px;
} }
.password-forgotten { .password-forgotten {
font-size: .8rem; font-size: 0.8rem;
} }
.footer { .footer {
margin-bottom: $login-margin-top; margin-bottom: $login-margin-top;
margin-top: $login-margin-between; margin-top: $login-margin-between;
text-align: center; text-align: center;
font-size: .8rem; font-size: 0.8rem;
.contact { .contact {
margin-top: 15px; margin-top: 15px;
color: grey; color: grey;
} }
a { a {
font-weight: bold; font-weight: bold;
} }
} }
</style> </style>
<script> <script>
import { userStore } from 'stores/user' import { userStore } from 'stores/user';
export default { export default {
name: 'VnLogin', name: 'VnLogin',
data () { data() {
return { return {
user: userStore(), user: userStore(),
email: '', email: '',
password: '', password: '',
remember: false, remember: false,
showPwd: true showPwd: true
} };
}, },
mounted () { mounted() {
if (this.$route.query.emailConfirmed !== undefined) { if (this.$route.query.emailConfirmed !== undefined) {
this.$q.notify({ this.$q.notify({
message: this.$t('emailConfirmedSuccessfully'), message: this.$t('emailConfirmedSuccessfully'),
type: 'positive' type: 'positive'
}) });
} }
if (this.$route.params.email) { if (this.$route.params.email) {
this.email = this.$route.params.email this.email = this.$route.params.email;
this.$refs.password.focus() this.$refs.password.focus();
} }
}, },
methods: { methods: {
async onLogin () { async onLogin() {
await this.user.login(this.email, this.password, this.remember) await this.user.login(this.email, this.password, this.remember);
this.$router.push('/') this.$router.push('/');
}
} }
} };
}
</script> </script>
<i18n lang="yaml"> <i18n lang="yaml">
en-US: en-US:
user: User user: User
password: Password password: Password
remindMe: Remind me remindMe: Remind me
logInAsGuest: Log in as guest logInAsGuest: Log in as guest
logIn: Log in logIn: Log in
loginMail: infoverdnatura.es loginMail: infoverdnatura.es
loginPhone: +34 607 562 391 loginPhone: +34 607 562 391
haveForgottenPassword: Have you forgotten your password? haveForgottenPassword: Have you forgotten your password?
notACustomerYet: Not a customer yet? notACustomerYet: Not a customer yet?
signUp: Register signUp: Register
es-ES: es-ES:
user: Usuario user: Usuario
password: Contraseña password: Contraseña
remindMe: Recuérdame remindMe: Recuérdame
logInAsGuest: Entrar como invitado logInAsGuest: Entrar como invitado
logIn: Iniciar sesión logIn: Iniciar sesión
loginMail: infoverdnatura.es loginMail: infoverdnatura.es
loginPhone: +34 963 242 100 loginPhone: +34 963 242 100
haveForgottenPassword: ¿Has olvidado tu contraseña? haveForgottenPassword: ¿Has olvidado tu contraseña?
notACustomerYet: ¿Todavía no eres cliente? notACustomerYet: ¿Todavía no eres cliente?
signUp: Registrarme signUp: Registrarme
</i18n> </i18n>

View File

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

View File

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

View File

@ -1,6 +1,11 @@
import { route } from 'quasar/wrappers' import { route } from 'quasar/wrappers'
import { appStore } from 'stores/app' 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' import routes from './routes'
/* /*
@ -13,30 +18,34 @@ import routes from './routes'
*/ */
export default route(function (/* { store, ssrContext } */) { export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER const createHistory = process.env.SERVER
? createMemoryHistory ? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory) : process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory
const Router = createRouter({ const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }),
routes, routes,
// Leave this as is and make changes in quasar.conf.js instead! // Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode // quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath // quasar.conf.js -> build -> publicPath
history: createHistory(process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE) 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
}) })
})
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 = [ const routes = [
{ {
path: '/login', path: '/login',
component: () => import('layouts/LoginLayout.vue'), component: () => import('layouts/LoginLayout.vue'),
children: [ children: [
{ {
name: 'login', name: 'login',
path: '/login/:email?', path: '/login/:email?',
component: () => import('pages/Login/Login.vue') component: () => import('pages/Login/Login.vue')
}, { },
name: 'rememberPassword', {
path: '/remember-password', name: 'rememberPassword',
component: () => import('pages/Login/RememberPassword.vue') path: '/remember-password',
}, { component: () => import('pages/Login/RememberPassword.vue')
name: 'resetPassword', },
path: '/reset-password', {
component: () => import('pages/Login/ResetPassword.vue') name: 'resetPassword',
} path: '/reset-password',
] component: () => import('pages/Login/ResetPassword.vue')
}, { }
path: '/', ]
component: () => import('layouts/MainLayout.vue'), },
children: [ {
{ path: '/',
name: '', component: () => import('layouts/MainLayout.vue'),
path: '', children: [
component: () => import('src/pages/Cms/Home.vue') {
}, { name: '',
name: 'home', path: '',
path: '/cms/home', component: () => import('src/pages/Cms/Home.vue')
component: () => import('src/pages/Cms/Home.vue') },
}, { {
name: 'orders', name: 'home',
path: '/ecomerce/orders', path: '/cms/home',
component: () => import('pages/Ecomerce/Orders.vue') component: () => import('src/pages/Cms/Home.vue')
}, { },
name: 'ticket', {
path: '/ecomerce/ticket/:id', name: 'orders',
component: () => import('pages/Ecomerce/Ticket.vue') path: '/ecomerce/orders',
}, { component: () => import('pages/Ecomerce/Orders.vue')
name: 'invoices', },
path: '/ecomerce/invoices', {
component: () => import('pages/Ecomerce/Invoices.vue') name: 'ticket',
}, { path: '/ecomerce/ticket/:id',
name: 'catalog', component: () => import('pages/Ecomerce/Ticket.vue')
path: '/ecomerce/catalog/:category?/:type?', },
component: () => import('pages/Ecomerce/Catalog.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, // Always leave this as last one,
// but you can also remove it // but you can also remove it
{ {
path: '/:catchAll(.*)*', path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue') component: () => import('pages/ErrorNotFound.vue')
} }
] ]
export default routes export default routes

View File

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

View File

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

View File

@ -1,10 +1,10 @@
/* eslint-disable */ /* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED, // THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING // 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 { interface QuasarFeatureFlags {
store: true; store: true
} }
} }

View File

@ -2,86 +2,92 @@ import { defineStore } from 'pinia'
import { jApi } from 'boot/axios' import { jApi } from 'boot/axios'
export const tpvStore = defineStore('tpv', { export const tpvStore = defineStore('tpv', {
actions: { actions: {
async check (route) { async check (route) {
const order = route.query.tpvOrder const order = route.query.tpvOrder
const status = route.query.tpvStatus const status = route.query.tpvStatus
if (!(order && status)) return null if (!(order && status)) return null
await jApi.execQuery( await jApi.execQuery('CALL myTpvTransaction_end(#order, #status)', {
'CALL myTpvTransaction_end(#order, #status)', order,
{ order, status } status
) })
if (status === 'ko') { if (status === 'ko') {
const retry = confirm('retryPayQuestion') const retry = confirm('retryPayQuestion')
if (retry) { this.retryPay(order) } if (retry) {
} this.retryPay(order)
}
}
return status return status
}, },
async pay (amount, company) { async pay (amount, company) {
await this.realPay(amount * 100, company) await this.realPay(amount * 100, company)
}, },
async retryPay (order) { async retryPay (order) {
const payment = await jApi.getObject( const payment = await jApi.getObject(
`SELECT t.amount, m.companyFk `SELECT t.amount, m.companyFk
FROM myTpvTransaction t FROM myTpvTransaction t
JOIN tpvMerchant m ON m.id = t.merchantFk JOIN tpvMerchant m ON m.id = t.merchantFk
WHERE t.id = #order`, WHERE t.id = #order`,
{ order } { order }
) )
await this.realPay(payment.amount, payment.companyFk) await this.realPay(payment.amount, payment.companyFk)
}, },
async realPay (amount, company) { async realPay (amount, company) {
if (!isNumeric(amount) || amount <= 0) { if (!isNumeric(amount) || amount <= 0) {
throw new Error('payAmountError') throw new Error('payAmountError')
} }
const json = await jApi.send('tpv/transaction', { const json = await jApi.send('tpv/transaction', {
amount: parseInt(amount), amount: parseInt(amount),
urlOk: this.makeUrl('ok'), urlOk: this.makeUrl('ok'),
urlKo: this.makeUrl('ko'), urlKo: this.makeUrl('ko'),
company company
}) })
const postValues = json.postValues const postValues = json.postValues
const form = document.createElement('form') const form = document.createElement('form')
form.method = 'POST' form.method = 'POST'
form.action = json.url form.action = json.url
document.body.appendChild(form) document.body.appendChild(form)
for (const field in postValues) { for (const field in postValues) {
const input = document.createElement('input') const input = document.createElement('input')
input.type = 'hidden' input.type = 'hidden'
input.name = field input.name = field
form.appendChild(input) form.appendChild(input)
if (postValues[field]) { input.value = postValues[field] } if (postValues[field]) {
} input.value = postValues[field]
}
}
form.submit() form.submit()
}, },
makeUrl (status) { makeUrl (status) {
let path = location.protocol + '//' + location.hostname let path = location.protocol + '//' + location.hostname
path += location.port ? ':' + location.port : '' path += location.port ? ':' + location.port : ''
path += location.pathname path += location.pathname
path += location.search ? location.search : '' path += location.search ? location.search : ''
path += '#/ecomerce/orders' path += '#/ecomerce/orders'
path += '?' + new URLSearchParams({ path +=
tpvStatus: status, '?' +
tpvOrder: '_transactionId_' new URLSearchParams({
}).toString() tpvStatus: status,
return path tpvOrder: '_transactionId_'
}).toString()
return path
}
} }
}
}) })
function isNumeric (n) { 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 { defineStore } from 'pinia';
import { api, jApi } from 'boot/axios' import { api, jApi } from 'boot/axios';
export const userStore = defineStore('user', { export const userStore = defineStore('user', {
state: () => { state: () => {
const token = const token =
sessionStorage.getItem('vnToken') || sessionStorage.getItem('vnToken') ||
localStorage.getItem('vnToken') localStorage.getItem('vnToken');
return { return {
token, token,
id: null, id: null,
name: null, name: null,
nickname: null nickname: null,
} isGuest: false
}, };
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
})
}, },
async logout () { getters: {
if (this.token != null) { loggedIn: state => state.token != null
try {
await api.post('Accounts/logout')
} catch (e) {}
localStorage.removeItem('vnToken')
sessionStorage.removeItem('vnToken')
}
this.$reset()
}, },
async loadData () { actions: {
const userData = await jApi.getObject( async login(user, password, remember) {
'SELECT id, nickname FROM account.myUser' const params = { user, password };
) const res = await api.post('Accounts/login', params);
this.$patch({ if (remember) {
id: userData.id, localStorage.setItem('vnToken', res.data.token);
nickname: userData.nickname } 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 publicPath = '/' + wpConfig.buildDir + '/';
const baseConfig = { const baseConfig = {
entry: wpConfig.entry, entry: wpConfig.entry,
mode: devMode ? 'development' : 'production', mode: devMode ? 'development' : 'production',
output: { output: {
path: outputPath, path: outputPath,
publicPath: publicPath publicPath: publicPath
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.m?js$/, test: /\.m?js$/,
exclude: /(node_modules|bower_components)/, exclude: /(node_modules|bower_components)/,
use: { use: {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
presets: ['@babel/preset-env'] presets: ['@babel/preset-env']
} }
} }
}, { },
test: /tinymce\/.*\/skin\.css$/i, {
use: [MiniCssExtractPlugin.loader, 'css-loader'] test: /tinymce\/.*\/skin\.css$/i,
}, { use: [MiniCssExtractPlugin.loader, 'css-loader']
test: /tinymce\/.*\/content\.css$/i, },
loader: 'css-loader', {
options: {esModule: false} test: /tinymce\/.*\/content\.css$/i,
}, { loader: 'css-loader',
test: /\.css$/, options: { esModule: false }
use: ['style-loader', 'css-loader'], },
exclude: [/node_modules/] {
}, { test: /\.css$/,
test: /\.yml$/, use: ['style-loader', 'css-loader'],
use: ['json-loader', 'yaml-loader'] exclude: [/node_modules/]
}, { },
test: /\.xml$/, {
use: 'raw-loader' test: /\.yml$/,
}, { use: ['json-loader', 'yaml-loader']
test: /\.ttf$/, },
type: 'asset/resource' {
}, { test: /\.xml$/,
test: /\.scss$/, use: 'raw-loader'
use: [ },
'style-loader', {
'css-loader', test: /\.ttf$/,
{ type: 'asset/resource'
loader: 'sass-loader', },
options: { {
sourceMap: true test: /\.scss$/,
} use: [
} 'style-loader',
] 'css-loader',
} {
] loader: 'sass-loader',
}, options: {
resolve: { sourceMap: true
modules: [ }
__dirname +'/js', }
__dirname, ]
__dirname +'/forms', },
'node_modules', {
'/usr/lib/node_modules' test: /\.(woff|woff2)$/,
] use: [
}, {
node: { loader: 'file-loader',
__dirname: true options: {
}, name: '[name].[ext]',
plugins: [ outputPath: 'fonts/'
new AssetsWebpackPlugin({ }
path: outputPath }
}), ]
new webpack.DefinePlugin({ }
_DEV_MODE: devMode, ]
_DEV_SERVER_PORT: wpConfig.devServerPort, },
_PUBLIC_PATH: JSON.stringify(publicPath) resolve: {
}), modules: [
new MiniCssExtractPlugin() __dirname + '/js',
], __dirname,
optimization: { __dirname + '/forms',
runtimeChunk: true, 'node_modules',
splitChunks: { '/usr/lib/node_modules'
chunks: 'all', ]
} },
}, node: {
watchOptions: { __dirname: true
ignored: /node_modules/ },
} 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 = { const prodConfig = {
output: { output: {
filename: '[name].[chunkhash].js', filename: '[name].[chunkhash].js',
chunkFilename: 'chunk.[id].[chunkhash].js' chunkFilename: 'chunk.[id].[chunkhash].js'
}, },
optimization: { optimization: {
moduleIds: 'deterministic' moduleIds: 'deterministic'
}, },
devtool: 'source-map' devtool: 'source-map'
}; };
const devConfig = { const devConfig = {
output: { output: {
filename: '[name].js', filename: '[name].js',
chunkFilename: 'chunk.[id].js' chunkFilename: 'chunk.[id].js'
}, },
optimization: { optimization: {
moduleIds: 'named' moduleIds: 'named'
}, },
devtool: 'eval', devtool: 'eval',
devServer: { devServer: {
host: '0.0.0.0', host: '0.0.0.0',
static: __dirname, static: __dirname,
port: wpConfig.devServerPort, port: wpConfig.devServerPort,
headers: {'Access-Control-Allow-Origin': '*'}, headers: { 'Access-Control-Allow-Origin': '*' },
//stats: { chunks: false }, //stats: { chunks: false },
proxy: { proxy: {
'/api': 'http://localhost:3000', '/api': 'http://localhost:3000',
'/': { '/': {
target: 'http://localhost/projects/hedera-web', target: 'http://localhost/projects/hedera-web',
bypass: (req) => req.path !== '/' ? req.path : null bypass: req => (req.path !== '/' ? req.path : null)
} }
} }
} }
}; };
const mrgConfig = devMode ? devConfig : prodConfig; const mrgConfig = devMode ? devConfig : prodConfig;