Init config #68

Merged
jsegarra merged 6 commits from wbuezas/hedera-web-mindshore:feature/InitConfig into 4922-vueMigration 2024-07-19 11:13:56 +00:00
42 changed files with 2564 additions and 2249 deletions
Showing only changes of commit 47c6fe02ec - Show all commits

View File

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

9
.prettierrc.js Normal file
View File

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

View File

@ -14,4 +14,4 @@
"typescript",
"vue"
]
}
}

589
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,12 +17,14 @@
"assets-webpack-plugin": "^7.1.1",
"babel-loader": "^9.1.0",
"bundle-loader": "^0.5.6",
"eslint": "^8.10.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-n": "^15.0.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^9.0.0",
"eslint-plugin-vue": "^9.27.0",
"eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.2.0",
"fs-extra": "^10.1.0",

View File

@ -6,6 +6,6 @@
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App'
name: 'App'
})
</script>

View File

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

View File

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

View File

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

View File

@ -3,16 +3,16 @@ import { createI18n } from 'vue-i18n'
import messages from 'src/i18n'
export default boot(({ app }) => {

Propuesta, lo dejamos con Lilium, moviendo la variable afuera

Propuesta, lo dejamos con Lilium, moviendo la variable afuera
const i18n = createI18n({
locale: 'es-ES',
globalInjection: true,
silentTranslationWarn: true,
silentFallbackWarn: true,
messages
})
const i18n = createI18n({
locale: 'es-ES',
globalInjection: true,
silentTranslationWarn: true,
silentFallbackWarn: true,
messages
})
// Set i18n instance on app
app.use(i18n)
// Set i18n instance on app
app.use(i18n)
window.i18n = i18n.global
window.i18n = i18n.global
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
<template>
<q-layout id="bg" class="fullscreen row justify-center items-center layout-view scroll">
<q-layout
id="bg"
class="fullscreen row justify-center items-center layout-view scroll"
>
<div class="column q-pa-md row items-center justify-center">
<router-view v-slot="{ Component }">
<transition>
@ -26,6 +29,6 @@
<script>
export default {
name: 'LoginLayout'
name: 'LoginLayout'
}
</script>

View File

@ -8,67 +8,61 @@
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"/>
@click="toggleLeftDrawer"
/>
<q-toolbar-title>
{{$app.title}}
<div
v-if="$app.subtitle"
class="subtitle text-caption">
{{$app.subtitle}}
{{ $app.title }}
<div v-if="$app.subtitle" class="subtitle text-caption">
{{ $app.subtitle }}
</div>
</q-toolbar-title>
<div id="actions" ref="actions">
</div>
<div id="actions" ref="actions"></div>
<q-btn
v-if="$app.useRightDrawer"
@click="$app.rightDrawerOpen = !$app.rightDrawerOpen"
aria-label="Menu"
flat
dense
round>
<q-icon name="menu"/>
round
>
<q-icon name="menu" />
</q-btn>
</q-toolbar>
</q-header>
<q-drawer
v-model="leftDrawerOpen"
:width="250"
show-if-above>
<q-drawer v-model="leftDrawerOpen" :width="250" show-if-above>
<q-toolbar class="logo">
<img src="statics/logo-dark.svg">
<img src="statics/logo-dark.svg" />
</q-toolbar>
<div class="user-info">
<div>
<span id="user-name">{{(user.nickname)}}</span>
<q-btn flat icon="logout" alt="_Exit" @click="logout()"/>
<span id="user-name">{{ user.nickname }}</span>
<q-btn flat icon="logout" alt="_Exit" @click="logout()" />
</div>
<div id="supplant" class="supplant">
<span id="supplanted">{{supplantedUser}}</span>
<q-btn flat icon="logout" alt="_Exit"/>
<span id="supplanted">{{ supplantedUser }}</span>
<q-btn flat icon="logout" alt="_Exit" />
</div>
</div>
<q-list
v-for="item in essentialLinks"
:key="item.id">
<q-item
v-if="!item.childs"
:to="`/${item.path}`">
<q-list v-for="item in essentialLinks" :key="item.id">
<q-item v-if="!item.childs" :to="`/${item.path}`">
<q-item-section>
<q-item-label>{{item.description}}</q-item-label>
<q-item-label>{{ item.description }}</q-item-label>
</q-item-section>
</q-item>
<q-expansion-item
v-if="item.childs"
:label="item.description"
expand-separator>
expand-separator
>
<q-list>
<q-item
v-for="subitem in item.childs"
:key="subitem.id"
:to="`/${subitem.path}`"
class="q-pl-lg">
class="q-pl-lg"
>
<q-item-section>
<q-item-label>{{subitem.description}}</q-item-label>
<q-item-label>{{ subitem.description }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
@ -134,13 +128,13 @@
</style>
<style lang="scss">
@import "src/css/responsive";
@import 'src/css/responsive';
.q-drawer {
.q-item {
padding-left: 38px;
}
.q-list .q-list .q-item{
.q-list .q-list .q-item {
padding-left: 50px;
}
}
@ -175,67 +169,69 @@ import { defineComponent, ref } from 'vue'
import { userStore } from 'stores/user'
export default defineComponent({
name: 'MainLayout',
props: {},
name: 'MainLayout',
props: {},
setup () {
const leftDrawerOpen = ref(false)
setup () {
const leftDrawerOpen = ref(false)
return {
user: userStore(),
supplantedUser: ref(''),
essentialLinks: ref(null),
leftDrawerOpen,
toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value
}
}
},
async mounted () {
this.$refs.actions.appendChild(this.$actions)
await this.user.loadData()
await this.$app.loadConfig()
await this.fetchData()
},
methods: {
async fetchData () {
const sections = await this.$jApi.query('SELECT * FROM myMenu')
const sectionMap = new Map()
for (const section of sections) {
sectionMap.set(section.id, section)
}
const sectionTree = []
for (const section of sections) {
const parent = section.parentFk
if (parent) {
const parentSection = sectionMap.get(parent)
if (!parentSection) continue
let childs = parentSection.childs
if (!childs) { childs = parentSection.childs = [] }
childs.push(section)
} else {
sectionTree.push(section)
return {
user: userStore(),
supplantedUser: ref(''),
essentialLinks: ref(null),
leftDrawerOpen,
toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value
}
}
}
this.essentialLinks = sectionTree
},
async logout () {
this.user.logout()
this.$router.push('/login')
async mounted () {
this.$refs.actions.appendChild(this.$actions)
await this.user.loadData()
await this.$app.loadConfig()
await this.fetchData()
},
methods: {
async fetchData () {
const sections = await this.$jApi.query('SELECT * FROM myMenu')
const sectionMap = new Map()
for (const section of sections) {
sectionMap.set(section.id, section)
}
const sectionTree = []
for (const section of sections) {
const parent = section.parentFk
if (parent) {
const parentSection = sectionMap.get(parent)
if (!parentSection) continue
let childs = parentSection.childs
if (!childs) {
childs = parentSection.childs = []
}
childs.push(section)
} else {
sectionTree.push(section)
}
}
this.essentialLinks = sectionTree
},
async logout () {
this.user.logout()
this.$router.push('/login')
}
}
}
})
</script>
<i18n lang="yaml">
en-US:
visitor: Visitor
es-ES:
visitor: Visitante
en-US:
visitor: Visitor
es-ES:
visitor: Visitante
</i18n>

View File

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

View File

@ -1,18 +1,14 @@
<template>
<div style="padding: 0;">
<div style="padding: 0">
<div class="q-pa-sm row items-start">
<div
class="new-card q-pa-sm"
v-for="myNew in news"
:key="myNew.id">
<div class="new-card q-pa-sm" v-for="myNew in news" :key="myNew.id">
<q-card>
<q-img :src="`${$app.imageUrl}/news/full/${myNew.image}`">
</q-img>
<q-img :src="`${$app.imageUrl}/news/full/${myNew.image}`"> </q-img>
<q-card-section>
<div class="text-h5">{{ myNew.title }}</div>
</q-card-section>
<q-card-section class="new-body">
<div v-html="myNew.text"/>
<div v-html="myNew.text" />
</q-card-section>
</q-card>
</div>
@ -50,31 +46,31 @@
<script>
export default {
name: 'PageIndex',
data () {
return {
news: []
}
},
async mounted () {
this.news = await this.$jApi.query(
`SELECT title, text, image, id
name: 'PageIndex',
data () {
return {
news: []
}
},
async mounted () {
this.news = await this.$jApi.query(
`SELECT title, text, image, id
FROM news
ORDER BY priority, created DESC`
)
}
)
}
}
</script>
<i18n lang="yaml">
en-US:
startOrder: Start order
es-ES:
startOrder: Empezar pedido
ca-ES:
startOrder: Començar comanda
fr-FR:
startOrder: Lancer commande
pt-PT:
startOrder: Comece uma encomenda
en-US:
startOrder: Start order
es-ES:
startOrder: Empezar pedido
ca-ES:
startOrder: Començar comanda
fr-FR:
startOrder: Lancer commande
pt-PT:
startOrder: Comece uma encomenda
</i18n>

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,13 +1,11 @@
<template>
<div class="fullscreen bg-accent text-white text-center q-pa-md flex flex-center">
<div
class="fullscreen bg-accent text-white text-center q-pa-md flex flex-center"
>
<div>
<div style="font-size: 30vh">
404
</div>
<div style="font-size: 30vh">404</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
</div>
<div class="text-h2" style="opacity: 0.4">Oops. Nothing here...</div>
<q-btn
class="q-mt-xl"
@ -26,6 +24,6 @@
import { defineComponent } from 'vue'
export default defineComponent({
name: 'ErrorNotFound'
name: 'ErrorNotFound'
})
</script>

View File

@ -4,7 +4,7 @@
alt="Quasar logo"
src="~assets/quasar-logo-vertical.svg"
style="width: 200px; height: 200px"
>
/>
</q-page>
</template>
@ -12,6 +12,6 @@
import { defineComponent } from 'vue'
export default defineComponent({
name: 'IndexPage'
name: 'IndexPage'
})
</script>

View File

@ -2,25 +2,18 @@
<div class="main">
<div class="header">
<router-link to="/" class="block">
<img
src="statics/logo.svg"
alt="Verdnatura"
class="block"
/>
<img src="statics/logo.svg" alt="Verdnatura" class="block" />
</router-link>
</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="email" :label="$t('user')" autofocus />
<q-input
v-model="password"
ref="password"
:label="$t('password')"
:type="showPwd ? 'password' : 'text'">
:type="showPwd ? 'password' : 'text'"
>
<template v-slot:append>
<q-icon
:name="showPwd ? 'visibility_off' : 'visibility'"
@ -60,20 +53,18 @@
</div>
<p class="password-forgotten text-center q-mt-lg">
<router-link to="/remember-password" class="link">
{{$t('haveForgottenPassword')}}
{{ $t('haveForgottenPassword') }}
</router-link>
</p>
</q-form>
<div class="footer text-center">
<p>
{{$t('notACustomerYet')}}
{{ $t('notACustomerYet') }}
<a href="//verdnatura.es/register/" target="_blank" class="link">
{{$t('signUp')}}
{{ $t('signUp') }}
</a>
</p>
<p class="contact">
{{$t('loginPhone')}} · {{$t('loginMail')}}
</p>
<p class="contact">{{ $t('loginPhone') }} · {{ $t('loginMail') }}</p>
</div>
</div>
</template>
@ -106,13 +97,13 @@ a {
height: 50px;
}
.password-forgotten {
font-size: .8rem;
font-size: 0.8rem;
}
.footer {
margin-bottom: $login-margin-top;
margin-top: $login-margin-between;
text-align: center;
font-size: .8rem;
font-size: 0.8rem;
.contact {
margin-top: 15px;
@ -128,37 +119,37 @@ a {
import { userStore } from 'stores/user'
export default {
name: 'VnLogin',
name: 'VnLogin',
data () {
return {
user: userStore(),
email: '',
password: '',
remember: false,
showPwd: true
}
},
data () {
return {
user: userStore(),
email: '',
password: '',
remember: false,
showPwd: true
}
},
mounted () {
if (this.$route.query.emailConfirmed !== undefined) {
this.$q.notify({
message: this.$t('emailConfirmedSuccessfully'),
type: 'positive'
})
}
if (this.$route.params.email) {
this.email = this.$route.params.email
this.$refs.password.focus()
}
},
mounted () {
if (this.$route.query.emailConfirmed !== undefined) {
this.$q.notify({
message: this.$t('emailConfirmedSuccessfully'),
type: 'positive'
})
}
if (this.$route.params.email) {
this.email = this.$route.params.email
this.$refs.password.focus()
}
},
methods: {
async onLogin () {
await this.user.login(this.email, this.password, this.remember)
this.$router.push('/')
methods: {
async onLogin () {
await this.user.login(this.email, this.password, this.remember)
this.$router.push('/')
}
}
}
}
</script>

View File

@ -4,27 +4,27 @@
<q-icon
name="contact_support"
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')}}
{{ $t('dontWorry') }}
</div>
<div>
{{$t('fillData')}}
{{ $t('fillData') }}
</div>
</div>
<q-input
v-model="email"
:label="$t('user')"
:rules="[ val => !!val || $t('inputEmail')]"
:rules="[(val) => !!val || $t('inputEmail')]"
autofocus
/>
<div class="q-mt-lg">
{{$t('weSendEmail')}}
{{ $t('weSendEmail') }}
</div>
<div>
<q-btn
@ -38,7 +38,7 @@
/>
<div class="text-center q-mt-md">
<router-link to="/login" class="link">
{{$t('return')}}
{{ $t('return') }}
</router-link>
</div>
</div>
@ -56,53 +56,53 @@
}
a {
color: inherit;
font-size: .8rem;
font-size: 0.8rem;
}
</style>
<script>
export default {
name: 'VnRememberPasword',
data () {
return {
email: ''
name: 'VnRememberPasword',
data () {
return {
email: ''
}
},
methods: {
async onSend () {
const params = {
email: this.email
}
await this.$axios.post('Users/reset', params)
this.$q.notify({
message: this.$t('weHaveSentEmailToRecover'),
type: 'positive'
})
this.$router.push('/login')
}
}
},
methods: {
async onSend () {
const params = {
email: this.email
}
await this.$axios.post('Users/reset', params)
this.$q.notify({
message: this.$t('weHaveSentEmailToRecover'),
type: 'positive'
})
this.$router.push('/login')
}
}
}
</script>
<i18n lang="yaml">
en-US:
user: User
inputEmail: Input email
rememberPassword: Rememeber password
dontWorry: Don't worry!
fillData: Fill the data
weSendEmail: We will sent you an email to recover your password
weHaveSentEmailToRecover: We've sent you an email where you can recover your password
send: Send
return: Return
es-ES:
user: Usuario
inputEmail: Introduce el correo electrónico
rememberPassword: Recordar contraseña
dontWorry: ¡No te preocupes!
fillData: Rellena los datos
weSendEmail: Te enviaremos un correo para restablecer tu contraseña
weHaveSentEmailToRecover: Te hemos enviado un correo donde podrás recuperar tu contraseña
send: Enviar
return: Volver
en-US:
user: User
inputEmail: Input email
rememberPassword: Rememeber password
dontWorry: Don't worry!
fillData: Fill the data
weSendEmail: We will sent you an email to recover your password
weHaveSentEmailToRecover: We've sent you an email where you can recover your password
send: Send
return: Return
es-ES:
user: Usuario
inputEmail: Introduce el correo electrónico
rememberPassword: Recordar contraseña
dontWorry: ¡No te preocupes!
fillData: Rellena los datos
weSendEmail: Te enviaremos un correo para restablecer tu contraseña
weHaveSentEmailToRecover: Te hemos enviado un correo donde podrás recuperar tu contraseña
send: Enviar
return: Volver
</i18n>

View File

@ -4,13 +4,13 @@
<q-icon
name="check"
class="block q-mx-auto text-accent"
style="font-size: 120px;"
style="font-size: 120px"
/>
</q-card-section>
<q-card-section>
<q-form @submit="onRegister" ref="form" class="q-gutter-y-md">
<div class="text-grey-8 text-h5 text-center">
{{$t('fillData')}}
{{ $t('fillData') }}
</div>
<div class="q-gutter-y-sm">
<q-input
@ -19,7 +19,8 @@
:type="showPwd ? 'password' : 'text'"
autofocus
hint=""
filled>
filled
>
<template v-slot:append>
<q-icon
:name="showPwd ? 'visibility_off' : 'visibility'"
@ -32,9 +33,10 @@
v-model="repeatPassword"
:label="$t('repeatPassword')"
:type="showRpPwd ? 'password' : 'text'"
:rules="[value => value == password || $t('repeatPasswordError')]"
:rules="[(value) => value == password || $t('repeatPasswordError')]"
hint=""
filled>
filled
>
<template v-slot:append>
<q-icon
:name="showRpPwd ? 'visibility_off' : 'visibility'"
@ -53,7 +55,7 @@
/>
<div class="text-center q-mt-xs">
<router-link to="/login" class="link">
{{$t('return')}}
{{ $t('return') }}
</router-link>
</div>
</div>
@ -64,30 +66,34 @@
<script>
export default {
name: 'VnRegister',
data () {
return {
password: '',
repeatPassword: '',
showPwd: true,
showRpPwd: true
}
},
methods: {
async onRegister () {
const headers = {
Authorization: this.$route.query.access_token
}
await this.$axios.post('users/reset-password', {
newPassword: this.password
}, { headers })
name: 'VnRegister',
data () {
return {
password: '',
repeatPassword: '',
showPwd: true,
showRpPwd: true
}
},
methods: {
async onRegister () {
const headers = {
Authorization: this.$route.query.access_token
}
await this.$axios.post(
'users/reset-password',
{
newPassword: this.password
},
{ headers }
)
this.$q.notify({
message: this.$t('passwordResetSuccessfully'),
type: 'positive'
})
this.$router.push('/login')
this.$q.notify({
message: this.$t('passwordResetSuccessfully'),
type: 'positive'
})
this.$router.push('/login')
}
}
}
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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