Modulo Administración #78
56
.eslintrc.js
56
.eslintrc.js
|
@ -15,22 +15,8 @@ module.exports = {
|
||||||
'vue/setup-compiler-macros': true
|
'vue/setup-compiler-macros': true
|
||||||
},
|
},
|
||||||
|
|
||||||
extends: [
|
extends: ['standard'],
|
||||||
// Base ESLint recommended rules
|
|
||||||
// 'eslint:recommended',
|
|
||||||
|
|
||||||
// Uncomment any of the lines below to choose desired strictness,
|
|
||||||
// but leave only one uncommented!
|
|
||||||
// See https://eslint.vuejs.org/rules/#available-rules
|
|
||||||
// 'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
|
|
||||||
'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
|
|
||||||
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
|
|
||||||
|
|
||||||
'standard'
|
|
||||||
],
|
|
||||||
|
|
||||||
plugins: ['vue', 'prettier'],
|
plugins: ['vue', 'prettier'],
|
||||||
|
|
||||||
globals: {
|
globals: {
|
||||||
ga: 'readonly', // Google Analytics
|
ga: 'readonly', // Google Analytics
|
||||||
cordova: 'readonly',
|
cordova: 'readonly',
|
||||||
|
@ -66,28 +52,34 @@ module.exports = {
|
||||||
'prefer-promise-reject-errors': 'off',
|
'prefer-promise-reject-errors': 'off',
|
||||||
semi: 'off',
|
semi: 'off',
|
||||||
// allow debugger during development only
|
// allow debugger during development only
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||||
'vue/html-indent': [
|
|
||||||
'error',
|
|
||||||
4,
|
|
||||||
{
|
|
||||||
attribute: 1,
|
|
||||||
baseIndent: 1,
|
|
||||||
closeBracket: 0,
|
|
||||||
alignAttributesVertically: true,
|
|
||||||
ignores: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
extends: ['plugin:vue/vue3-essential'],
|
files: ['src/**/*.{js,vue,scss}', 'quasar.config.js'], // Aplica ESLint solo a archivos .js, .vue y .scss dentro de src (Proyecto de quasar)
|
||||||
files: ['src/**/*.{js,vue,scss}'], // Aplica ESLint solo a archivos .js, .vue y .scss dentro de src (Proyecto de quasar)
|
extends: [
|
||||||
|
// Base ESLint recommended rules
|
||||||
|
'eslint:recommended',
|
||||||
|
// Uncomment any of the lines below to choose desired strictness,
|
||||||
|
// but leave only one uncommented!
|
||||||
|
// See https://eslint.vuejs.org/rules/#available-rules
|
||||||
|
// 'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
|
||||||
|
'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
|
||||||
|
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
|
||||||
|
// https://github.com/prettier/eslint-config-prettier#installation
|
||||||
|
// usage with Prettier, provided by 'eslint-config-prettier'.
|
||||||
|
'prettier'
|
||||||
|
],
|
||||||
rules: {
|
rules: {
|
||||||
semi: 'off',
|
semi: 'off',
|
||||||
indent: ['error', 4, { SwitchCase: 1 }],
|
'space-before-function-paren': 'off',
|
||||||
'space-before-function-paren': 'off'
|
'prefer-promise-reject-errors': 'off',
|
||||||
}
|
'vue/no-multiple-template-root': 'off'
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: '2021'
|
||||||
|
},
|
||||||
|
plugins: ['vue']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
{
|
{
|
||||||
"files.eol": "\n",
|
|
||||||
"eslint.autoFixOnSave": true,
|
|
||||||
"editor.bracketPairColorization.enabled": true,
|
"editor.bracketPairColorization.enabled": true,
|
||||||
"editor.guides.bracketPairs": true,
|
"editor.guides.bracketPairs": true,
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": null,
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
|
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
|
||||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
|
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
|
||||||
"[sql]": {
|
"cSpell.words": ["axios", "composables"]
|
||||||
"editor.formatOnSave": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,6 @@ const { configure } = require('quasar/wrappers');
|
||||||
|
|
||||||
module.exports = configure(function (ctx) {
|
module.exports = configure(function (ctx) {
|
||||||
return {
|
return {
|
||||||
// fix: true,
|
|
||||||
// include = [],
|
|
||||||
// exclude = [],
|
|
||||||
// rawOptions = {},
|
|
||||||
warnings: true,
|
|
||||||
errors: true,
|
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/supporting-ts
|
// https://v2.quasar.dev/quasar-cli-webpack/supporting-ts
|
||||||
supportTS: false,
|
supportTS: false,
|
||||||
|
|
||||||
|
@ -73,7 +67,7 @@ module.exports = configure(function (ctx) {
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/handling-webpack
|
// https://v2.quasar.dev/quasar-cli-webpack/handling-webpack
|
||||||
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
||||||
|
|
||||||
chainWebpack (chain) {
|
chainWebpack(chain) {
|
||||||
chain
|
chain
|
||||||
.plugin('eslint-webpack-plugin')
|
.plugin('eslint-webpack-plugin')
|
||||||
.use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]);
|
.use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]);
|
||||||
|
@ -108,7 +102,7 @@ module.exports = configure(function (ctx) {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': 'http://localhost:3000',
|
'/api': 'http://localhost:3000',
|
||||||
'/': {
|
'/': {
|
||||||
target: 'http://localhost:3001',
|
target: 'http://localhost:3002',
|
||||||
bypass: req => (req.path !== '/' ? req.path : null)
|
bypass: req => (req.path !== '/' ? req.path : null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,7 +143,7 @@ module.exports = configure(function (ctx) {
|
||||||
maxAge: 1000 * 60 * 60 * 24 * 30,
|
maxAge: 1000 * 60 * 60 * 24 * 30,
|
||||||
// Tell browser when a file from the server should expire from cache (in ms)
|
// Tell browser when a file from the server should expire from cache (in ms)
|
||||||
|
|
||||||
chainWebpackWebserver (chain) {
|
chainWebpackWebserver(chain) {
|
||||||
chain
|
chain
|
||||||
.plugin('eslint-webpack-plugin')
|
.plugin('eslint-webpack-plugin')
|
||||||
.use(ESLintPlugin, [{ extensions: ['js'] }]);
|
.use(ESLintPlugin, [{ extensions: ['js'] }]);
|
||||||
|
@ -169,7 +163,7 @@ module.exports = configure(function (ctx) {
|
||||||
// for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts])
|
// for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts])
|
||||||
// if using workbox in InjectManifest mode
|
// if using workbox in InjectManifest mode
|
||||||
|
|
||||||
chainWebpackCustomSW (chain) {
|
chainWebpackCustomSW(chain) {
|
||||||
chain
|
chain
|
||||||
.plugin('eslint-webpack-plugin')
|
.plugin('eslint-webpack-plugin')
|
||||||
.use(ESLintPlugin, [{ extensions: ['js'] }]);
|
.use(ESLintPlugin, [{ extensions: ['js'] }]);
|
||||||
|
@ -246,13 +240,13 @@ module.exports = configure(function (ctx) {
|
||||||
|
|
||||||
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
||||||
|
|
||||||
chainWebpackMain (chain) {
|
chainWebpackMain(chain) {
|
||||||
chain
|
chain
|
||||||
.plugin('eslint-webpack-plugin')
|
.plugin('eslint-webpack-plugin')
|
||||||
.use(ESLintPlugin, [{ extensions: ['js'] }]);
|
.use(ESLintPlugin, [{ extensions: ['js'] }]);
|
||||||
},
|
},
|
||||||
|
|
||||||
chainWebpackPreload (chain) {
|
chainWebpackPreload(chain) {
|
||||||
chain
|
chain
|
||||||
.plugin('eslint-webpack-plugin')
|
.plugin('eslint-webpack-plugin')
|
||||||
.use(ESLintPlugin, [{ extensions: ['js'] }]);
|
.use(ESLintPlugin, [{ extensions: ['js'] }]);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
import { useAppStore } from 'stores/app';
|
import { useAppStore } from 'stores/app';
|
||||||
import { userStore } from 'stores/user';
|
import { useUserStore } from 'stores/user';
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
export default boot(({ app }) => {
|
||||||
const props = app.config.globalProperties;
|
const props = app.config.globalProperties;
|
||||||
|
const userStore = useUserStore();
|
||||||
props.$app = useAppStore();
|
props.$app = useAppStore();
|
||||||
props.$user = userStore();
|
props.$user = userStore.user;
|
||||||
props.$actions = document.createElement('div');
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
import { Connection } from '../js/db/connection';
|
import { Connection } from '../js/db/connection';
|
||||||
import { userStore } from 'stores/user';
|
import { useUserStore } from 'stores/user';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import useNotify from 'src/composables/useNotify.js';
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
|
|
||||||
|
@ -36,10 +36,10 @@ const onResponseError = error => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
export default boot(({ app }) => {
|
||||||
const user = userStore();
|
const userStore = useUserStore();
|
||||||
function addToken(config) {
|
function addToken(config) {
|
||||||
if (user.token) {
|
if (userStore.token) {
|
||||||
config.headers.Authorization = user.token;
|
config.headers.Authorization = userStore.token;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
import { ref, inject, onMounted, computed, Teleport } from 'vue';
|
import { ref, inject, onMounted, computed, Teleport } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
import useNotify from 'src/composables/useNotify.js';
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
import {
|
import {
|
||||||
generateUpdateSqlQuery,
|
generateUpdateSqlQuery,
|
||||||
|
@ -73,6 +75,10 @@ const props = defineProps({
|
||||||
saveFn: {
|
saveFn: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
separationBetweenInputs: {
|
||||||
|
type: String,
|
||||||
|
default: 'xs'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -81,6 +87,8 @@ const emit = defineEmits(['onDataSaved']);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const jApi = inject('jApi');
|
const jApi = inject('jApi');
|
||||||
const { notify } = useNotify();
|
const { notify } = useNotify();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const formData = ref({});
|
const formData = ref({});
|
||||||
|
@ -101,6 +109,10 @@ const updatedColumns = computed(() => {
|
||||||
|
|
||||||
const hasChanges = computed(() => !!updatedColumns.value.length);
|
const hasChanges = computed(() => !!updatedColumns.value.length);
|
||||||
|
|
||||||
|
const separationBetweenInputs = computed(() => {
|
||||||
|
return `q-gutter-y-${props.separationBetweenInputs}`;
|
||||||
|
});
|
||||||
|
|
||||||
const fetchFormData = async () => {
|
const fetchFormData = async () => {
|
||||||
if (!props.fetchFormDataSql.query) return;
|
if (!props.fetchFormDataSql.query) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
@ -167,8 +179,17 @@ const generateSqlQuery = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!props.formInitialData && props.autoLoad) {
|
if (!props.formInitialData) {
|
||||||
fetchFormData();
|
fetchFormData();
|
||||||
|
} else {
|
||||||
|
formData.value = { ...props.formInitialData };
|
||||||
|
// Como no se ejecuta la query fetchFormData y no se obtienen las columnas de la tabla, se inicializan con las keys del objeto formInitialData
|
||||||
|
modelInfo.value = {
|
||||||
|
columns: Object.keys(props.formInitialData).map(col => ({
|
||||||
|
name: col
|
||||||
|
})),
|
||||||
|
data: [props.formInitialData]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -186,9 +207,10 @@ defineExpose({
|
||||||
<QForm
|
<QForm
|
||||||
v-if="!loading"
|
v-if="!loading"
|
||||||
ref="addressFormRef"
|
ref="addressFormRef"
|
||||||
class="column full-width q-gutter-y-xs"
|
class="form"
|
||||||
|
:class="separationBetweenInputs"
|
||||||
>
|
>
|
||||||
<span class="text-h6 text-bold">
|
<span v-if="title" class="text-h6 text-bold">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</span>
|
</span>
|
||||||
<slot
|
<slot
|
||||||
|
@ -200,8 +222,9 @@ defineExpose({
|
||||||
:data="formData"
|
:data="formData"
|
||||||
/>
|
/>
|
||||||
<component
|
<component
|
||||||
|
v-if="isHeaderMounted"
|
||||||
:is="showBottomActions ? 'div' : Teleport"
|
:is="showBottomActions ? 'div' : Teleport"
|
||||||
:to="$actions"
|
to="#actions"
|
||||||
class="flex row justify-end q-gutter-x-sm"
|
class="flex row justify-end q-gutter-x-sm"
|
||||||
:class="{ 'q-mt-md': showBottomActions }"
|
:class="{ 'q-mt-md': showBottomActions }"
|
||||||
>
|
>
|
||||||
|
@ -213,7 +236,9 @@ defineExpose({
|
||||||
no-caps
|
no-caps
|
||||||
flat
|
flat
|
||||||
v-close-popup
|
v-close-popup
|
||||||
/>
|
>
|
||||||
|
<QTooltip>{{ t('cancel') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
<QBtn
|
<QBtn
|
||||||
v-if="defaultActions"
|
v-if="defaultActions"
|
||||||
:label="t('save')"
|
:label="t('save')"
|
||||||
|
@ -223,8 +248,10 @@ defineExpose({
|
||||||
flat
|
flat
|
||||||
:disabled="!showBottomActions && !updatedColumns.length"
|
:disabled="!showBottomActions && !updatedColumns.length"
|
||||||
@click="submit()"
|
@click="submit()"
|
||||||
/>
|
>
|
||||||
<slot name="actions" />
|
<QTooltip>{{ t('save') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<slot name="actions" :data="formData" />
|
||||||
</component>
|
</component>
|
||||||
</QForm>
|
</QForm>
|
||||||
<QSpinner
|
<QSpinner
|
||||||
|
@ -244,9 +271,16 @@ defineExpose({
|
||||||
.form-container {
|
.form-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: max-content;
|
height: max-content;
|
||||||
|
padding: 0 !important;
|
||||||
max-width: 544px;
|
max-width: 544px;
|
||||||
padding: 32px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 32px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, watch, computed, ref } from 'vue';
|
||||||
|
import { date } from 'quasar';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const model = defineModel({ type: String });
|
||||||
|
const props = defineProps({
|
||||||
|
isOutlined: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const requiredFieldRule = val => !!val || t('globals.fieldRequired');
|
||||||
|
|
||||||
|
const dateFormat = 'DD/MM/YYYY';
|
||||||
|
const isPopupOpen = ref();
|
||||||
|
const hover = ref();
|
||||||
|
const mask = ref();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// fix quasar bug
|
||||||
|
mask.value = '##/##/####';
|
||||||
|
});
|
||||||
|
|
||||||
|
const styleAttrs = computed(() => {
|
||||||
|
return props.isOutlined
|
||||||
|
? {
|
||||||
|
dense: true,
|
||||||
|
outlined: true,
|
||||||
|
rounded: true
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedDate = computed({
|
||||||
|
get() {
|
||||||
|
if (!model.value) return model.value;
|
||||||
|
return date.formatDate(new Date(model.value), dateFormat);
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (value === model.value) return;
|
||||||
|
let newDate;
|
||||||
|
if (value) {
|
||||||
|
// parse input
|
||||||
|
if (value.includes('/')) {
|
||||||
|
if (value.length === 6) {
|
||||||
|
value = value + new Date().getFullYear();
|
||||||
|
}
|
||||||
|
if (value.length >= 10) {
|
||||||
|
if (value.at(2) === '/') {
|
||||||
|
value = value.split('/').reverse().join('/');
|
||||||
|
}
|
||||||
|
value = date.formatDate(
|
||||||
|
new Date(value).toISOString(),
|
||||||
|
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [year, month, day] = value.split('-').map(e => parseInt(e));
|
||||||
|
newDate = new Date(year, month - 1, day);
|
||||||
|
if (model.value) {
|
||||||
|
const orgDate =
|
||||||
|
model.value instanceof Date
|
||||||
|
? model.value
|
||||||
|
: new Date(model.value);
|
||||||
|
|
||||||
|
newDate.setHours(
|
||||||
|
orgDate.getHours(),
|
||||||
|
orgDate.getMinutes(),
|
||||||
|
orgDate.getSeconds(),
|
||||||
|
orgDate.getMilliseconds()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isNaN(newDate)) model.value = newDate.toISOString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const popupDate = computed(() =>
|
||||||
|
model.value
|
||||||
|
? date.formatDate(new Date(model.value), 'YYYY/MM/DD')
|
||||||
|
: model.value
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => model.value,
|
||||||
|
val => (formattedDate.value = val),
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div @mouseover="hover = true" @mouseleave="hover = false">
|
||||||
|
<QInput
|
||||||
|
v-model="formattedDate"
|
||||||
|
class="vn-input-date"
|
||||||
|
:mask="mask"
|
||||||
|
placeholder="dd/mm/aaaa"
|
||||||
|
v-bind="{ ...$attrs, ...styleAttrs }"
|
||||||
|
:class="{ required: $attrs.required }"
|
||||||
|
:rules="$attrs.required ? [requiredFieldRule] : null"
|
||||||
|
:clearable="false"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<QIcon
|
||||||
|
name="close"
|
||||||
|
size="xs"
|
||||||
|
v-if="
|
||||||
|
($attrs.clearable == undefined || $attrs.clearable) &&
|
||||||
|
hover &&
|
||||||
|
model &&
|
||||||
|
!$attrs.disable
|
||||||
|
"
|
||||||
|
@click="
|
||||||
|
model = null;
|
||||||
|
isPopupOpen = false;
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<QIcon
|
||||||
|
name="event"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="isPopupOpen = !isPopupOpen"
|
||||||
|
:title="t('openDate')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<QMenu
|
||||||
|
transition-show="scale"
|
||||||
|
transition-hide="scale"
|
||||||
|
v-model="isPopupOpen"
|
||||||
|
anchor="bottom left"
|
||||||
|
self="top start"
|
||||||
|
:no-focus="true"
|
||||||
|
:no-parent-event="true"
|
||||||
|
>
|
||||||
|
<QDate
|
||||||
|
v-model="popupDate"
|
||||||
|
:landscape="true"
|
||||||
|
:today-btn="true"
|
||||||
|
color="accent"
|
||||||
|
@update:model-value="
|
||||||
|
date => {
|
||||||
|
formattedDate = date;
|
||||||
|
isPopupOpen = false;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</QMenu>
|
||||||
|
</QInput>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
.vn-input-date.q-field--standard.q-field--readonly .q-field__control:before {
|
||||||
|
border-bottom-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vn-input-date.q-field--outlined.q-field--readonly .q-field__control:before {
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
openDate: Open date
|
||||||
|
es-ES:
|
||||||
|
openDate: Abrir fecha
|
||||||
|
ca-ES:
|
||||||
|
openDate: Obrir data
|
||||||
|
fr-FR:
|
||||||
|
openDate: Ouvrir la date
|
||||||
|
pt-PT:
|
||||||
|
openDate: Abrir data
|
||||||
|
</i18n>
|
|
@ -22,20 +22,15 @@ const handleClick = () => {
|
||||||
:class="{ 'cursor-pointer': clickable, 'no-radius': !rounded }"
|
:class="{ 'cursor-pointer': clickable, 'no-radius': !rounded }"
|
||||||
@click="handleClick()"
|
@click="handleClick()"
|
||||||
>
|
>
|
||||||
<QItemSection class="no-padding">
|
<div class="no-padding content-container col-10">
|
||||||
<div class="row no-wrap">
|
|
||||||
<slot name="prepend" />
|
<slot name="prepend" />
|
||||||
<div class="column full-width">
|
<div class="content">
|
||||||
<slot name="content" />
|
<slot name="content" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</QItemSection>
|
<div class="no-padding flex full-width justify-center">
|
||||||
<QItemSection
|
|
||||||
class="no-padding"
|
|
||||||
side
|
|
||||||
>
|
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
</QItemSection>
|
</div>
|
||||||
</QItem>
|
</QItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -44,4 +39,20 @@ const handleClick = () => {
|
||||||
border-bottom: 1px solid $gray-light;
|
border-bottom: 1px solid $gray-light;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
* {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,8 +5,10 @@ import { useI18n } from 'vue-i18n';
|
||||||
import VnInput from 'src/components/common/VnInput.vue';
|
import VnInput from 'src/components/common/VnInput.vue';
|
||||||
import VnForm from 'src/components/common/VnForm.vue';
|
import VnForm from 'src/components/common/VnForm.vue';
|
||||||
|
|
||||||
import { userStore as useUserStore } from 'stores/user';
|
import { useUserStore } from 'stores/user';
|
||||||
import useNotify from 'src/composables/useNotify.js';
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
verificationToken: {
|
verificationToken: {
|
||||||
|
@ -24,6 +26,8 @@ const { t } = useI18n();
|
||||||
const api = inject('api');
|
const api = inject('api');
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { notify } = useNotify();
|
const { notify } = useNotify();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
const oldPasswordRef = ref(null);
|
const oldPasswordRef = ref(null);
|
||||||
const newPasswordRef = ref(null);
|
const newPasswordRef = ref(null);
|
||||||
|
@ -33,7 +37,7 @@ const repeatPassword = ref('');
|
||||||
const passwordRequirements = ref(null);
|
const passwordRequirements = ref(null);
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
userId: userStore.id,
|
userId: userStore?.user?.id,
|
||||||
oldPassword: '',
|
oldPassword: '',
|
||||||
newPassword: ''
|
newPassword: ''
|
||||||
});
|
});
|
||||||
|
@ -75,7 +79,7 @@ const getPasswordRequirements = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
await userStore.login(userStore.name, formData.value.newPassword);
|
await userStore.login(userStore.user.name, formData.value.newPassword);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPasswordChanged = async () => {
|
const onPasswordChanged = async () => {
|
||||||
|
@ -149,7 +153,7 @@ onMounted(async () => {
|
||||||
</template>
|
</template>
|
||||||
</VnInput>
|
</VnInput>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template v-if="isHeaderMounted" #actions>
|
||||||
<QBtn
|
<QBtn
|
||||||
:label="t('requirements')"
|
:label="t('requirements')"
|
||||||
rounded
|
rounded
|
||||||
|
@ -159,10 +163,10 @@ onMounted(async () => {
|
||||||
/>
|
/>
|
||||||
<QBtn
|
<QBtn
|
||||||
:label="t('modify')"
|
:label="t('modify')"
|
||||||
type="submit"
|
|
||||||
rounded
|
rounded
|
||||||
no-caps
|
no-caps
|
||||||
flat
|
flat
|
||||||
|
@click="vnFormRef.submit()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</VnForm>
|
</VnForm>
|
||||||
|
@ -171,10 +175,7 @@ onMounted(async () => {
|
||||||
<span class="text-h6 text-bold q-mb-md">
|
<span class="text-h6 text-bold q-mb-md">
|
||||||
{{ t('passwordRequirements') }}
|
{{ t('passwordRequirements') }}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div class="column" style="max-width: max-content">
|
||||||
class="column"
|
|
||||||
style="max-width: max-content"
|
|
||||||
>
|
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
t('charactersLong', {
|
t('charactersLong', {
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, inject } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import VnInput from 'src/components/common/VnInput.vue';
|
||||||
|
|
||||||
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
schema: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
imageName: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const api = inject('api');
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { notify } = useNotify();
|
||||||
|
|
||||||
|
const inputFileRef = ref(null);
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const name = ref(props.imageName ?? '');
|
||||||
|
const file = ref(null);
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', name.value);
|
||||||
|
formData.append('image', file.value);
|
||||||
|
formData.append('schema', props.schema);
|
||||||
|
formData.append('srv', 'json:image/upload');
|
||||||
|
await api({
|
||||||
|
method: 'post',
|
||||||
|
url: location.origin,
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
notify(t('imageAdded'), 'positive');
|
||||||
|
emit('close');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading image:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<QForm @submit="onSubmit">
|
||||||
|
<QCard class="q-pa-lg">
|
||||||
|
<VnInput v-model="name" :label="t('name')" />
|
||||||
|
<QFile
|
||||||
|
ref="inputFileRef"
|
||||||
|
:label="t('file')"
|
||||||
|
v-model="file"
|
||||||
|
:multiple="false"
|
||||||
|
class="q-mb-xs"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<QIcon
|
||||||
|
name="attach_file"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="inputFileRef.pickFiles()"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</QFile>
|
||||||
|
<div class="flex row justify-end q-gutter-x-sm">
|
||||||
|
<QSpinner
|
||||||
|
v-if="loading"
|
||||||
|
color="primary"
|
||||||
|
size="3em"
|
||||||
|
:thickness="2"
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
v-else
|
||||||
|
type="submit"
|
||||||
|
:label="t('send')"
|
||||||
|
flat
|
||||||
|
class="q-mt-md"
|
||||||
|
rounded
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</QCard>
|
||||||
|
</QForm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
name: Name
|
||||||
|
file: File
|
||||||
|
send: Send
|
||||||
|
imageAdded: Image added successfully
|
||||||
|
es-ES:
|
||||||
|
name: Nombre
|
||||||
|
file: Archivo
|
||||||
|
send: Enviar
|
||||||
|
imageAdded: Imagen añadida correctamente
|
||||||
|
ca-ES:
|
||||||
|
name: Nom
|
||||||
|
file: Arxiu
|
||||||
|
send: Enviar
|
||||||
|
imageAdded: Imatge afegida correctament
|
||||||
|
fr-FR:
|
||||||
|
name: Nom
|
||||||
|
file: Fichier
|
||||||
|
send: Envoyer
|
||||||
|
imageAdded: Image ajoutée correctement
|
||||||
|
pt-PT:
|
||||||
|
name: Nome
|
||||||
|
file: Arquivo
|
||||||
|
send: Enviar
|
||||||
|
imageAdded: Imagen adicionada corretamente
|
||||||
|
</i18n>
|
|
@ -2,6 +2,10 @@
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useAppStore } from 'stores/app';
|
import { useAppStore } from 'stores/app';
|
||||||
|
|
||||||
|
import ImageEditor from 'src/components/ui/ImageEditor.vue';
|
||||||
|
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
baseURL: {
|
baseURL: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -27,26 +31,84 @@ const props = defineProps({
|
||||||
rounded: {
|
rounded: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
fullRounded: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: '100%'
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '100%'
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
editSchema: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
editImageName: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
alwaysShowEditButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const show = ref(false);
|
const showZoom = ref(false);
|
||||||
|
const showEditForm = ref(false);
|
||||||
const url = computed(() => {
|
const url = computed(() => {
|
||||||
return `${props.baseURL ?? app.imageUrl}/${props.storage}/${props.size}/${props.id}`;
|
return `${props.baseURL ?? app.imageUrl}/${props.storage}/${props.size}/${props.id}`;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
<div class="relative-position main-image-container">
|
||||||
|
<QBtn
|
||||||
|
v-if="props.editable"
|
||||||
|
icon="add_a_photo"
|
||||||
|
class="show-edit-button absolute-top-left"
|
||||||
|
:class="{ hide: !props.alwaysShowEditButton }"
|
||||||
|
round
|
||||||
|
text-color="black"
|
||||||
|
@click.stop.prevent="showEditForm = !showEditForm"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('addOrEditImage') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
<QImg
|
<QImg
|
||||||
:class="{ zoomIn: props.zoomSize, rounded: props.rounded }"
|
:class="{
|
||||||
|
zoomIn: props.zoomSize,
|
||||||
|
rounded: props.rounded,
|
||||||
|
'full-rounded': props.fullRounded
|
||||||
|
}"
|
||||||
|
class="main-image"
|
||||||
:src="url"
|
:src="url"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
@click="show = !show"
|
@click="showZoom = !showZoom"
|
||||||
spinner-color="primary"
|
spinner-color="primary"
|
||||||
/>
|
:width="props.width"
|
||||||
<QDialog
|
:height="props.height"
|
||||||
v-model="show"
|
|
||||||
v-if="props.zoomSize"
|
|
||||||
>
|
>
|
||||||
|
<template #error>
|
||||||
|
<div
|
||||||
|
class="full-width full-height flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<QIcon name="image" size="sm" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</QImg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<QDialog v-if="props.zoomSize" v-model="showZoom">
|
||||||
<QImg
|
<QImg
|
||||||
:src="url"
|
:src="url"
|
||||||
size="full"
|
size="full"
|
||||||
|
@ -55,19 +117,65 @@ const url = computed(() => {
|
||||||
spinner-color="primary"
|
spinner-color="primary"
|
||||||
/>
|
/>
|
||||||
</QDialog>
|
</QDialog>
|
||||||
|
<QDialog v-if="props.editable" v-model="showEditForm">
|
||||||
|
<ImageEditor
|
||||||
|
class="all-pointer-events"
|
||||||
|
:schema="props.editSchema"
|
||||||
|
:image-name="props.editImageName"
|
||||||
|
@close="showEditForm = false"
|
||||||
|
/>
|
||||||
|
</QDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.q-img {
|
.main-image-container {
|
||||||
|
&:hover {
|
||||||
|
.show-edit-button {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
.main-image {
|
||||||
|
filter: brightness(80%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-image {
|
||||||
&.zoomIn {
|
&.zoomIn {
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
}
|
}
|
||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-edit-button {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: $gray-light;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.rounded {
|
.rounded {
|
||||||
border-radius: 50%;
|
border-radius: 0.6em;
|
||||||
|
}
|
||||||
|
.full-rounded {
|
||||||
|
border-radius: 50px;
|
||||||
}
|
}
|
||||||
.img_zoom {
|
.img_zoom {
|
||||||
border-radius: 0%;
|
border-radius: 0%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
addOrEditImage: Add or update an image
|
||||||
|
es-ES:
|
||||||
|
addOrEditImage: Añadir o actualizar imagen
|
||||||
|
ca-ES:
|
||||||
|
addOrEditImage: Afegir o actualitzar Imatge
|
||||||
|
fr-FR:
|
||||||
|
addOrEditImage: Ajouter our mettre à jour l'image
|
||||||
|
pt-PT:
|
||||||
|
addOrEditImage: Adicionar ou atualizar imagem
|
||||||
|
</i18n>
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, inject } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import VnInput from 'src/components/common/VnInput.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
searchFn: {
|
||||||
|
type: Function,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: 'Search'
|
||||||
|
},
|
||||||
|
sqlQuery: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
searchField: {
|
||||||
|
type: String,
|
||||||
|
default: 'search'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['onSearch', 'onSearchError']);
|
||||||
|
|
||||||
|
const jApi = inject('jApi');
|
||||||
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const searchTerm = ref('');
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
try {
|
||||||
|
let data = null;
|
||||||
|
router.replace({
|
||||||
|
query: searchTerm.value ? { search: searchTerm.value } : {}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (props.sqlQuery) {
|
||||||
|
data = await jApi.query(props.sqlQuery, {
|
||||||
|
[props.searchField]: searchTerm.value
|
||||||
|
});
|
||||||
|
} else if (props.searchFn) {
|
||||||
|
data = props.searchFn(searchTerm.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('onSearch', data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error searching:', error);
|
||||||
|
emit('onSearchError');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (route.query.search) {
|
||||||
|
searchTerm.value = route.query.search;
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VnInput
|
||||||
|
v-model="searchTerm"
|
||||||
|
@keyup.enter="search()"
|
||||||
|
:placeholder="props.placeholder || t('search')"
|
||||||
|
bg-color="white"
|
||||||
|
is-outlined
|
||||||
|
:clearable="false"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<QIcon name="search" class="cursor-pointer" @click="search()" />
|
||||||
|
</template>
|
||||||
|
</VnInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
search: Search
|
||||||
|
es-ES:
|
||||||
|
search: Buscar
|
||||||
|
ca-ES:
|
||||||
|
search: Cercar
|
||||||
|
fr-FR:
|
||||||
|
search: Rechercher
|
||||||
|
pt-PT:
|
||||||
|
search: Pesquisar
|
||||||
|
</i18n>
|
|
@ -10,7 +10,7 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
hideBottom: {
|
hideBottom: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: false
|
||||||
},
|
},
|
||||||
rowsPerPageOptions: {
|
rowsPerPageOptions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -22,19 +22,13 @@ const props = defineProps({
|
||||||
<template>
|
<template>
|
||||||
<QTable
|
<QTable
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:no-data-label="props.noDataLabel || t('noInvoicesFound')"
|
:no-data-label="props.noDataLabel || t('noData')"
|
||||||
:hide-bottom="props.hideBottom"
|
:hide-bottom="props.hideBottom"
|
||||||
:rows-per-page-options="props.rowsPerPageOptions"
|
:rows-per-page-options="props.rowsPerPageOptions"
|
||||||
table-header-class="vntable-header-default"
|
table-header-class="vntable-header-default"
|
||||||
>
|
>
|
||||||
<template
|
<template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
|
||||||
v-for="(_, slotName) in $slots"
|
<slot :name="slotName" v-bind="slotProps" />
|
||||||
#[slotName]="slotProps"
|
|
||||||
>
|
|
||||||
<slot
|
|
||||||
:name="slotName"
|
|
||||||
v-bind="slotProps"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</QTable>
|
</QTable>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { userStore as useUserStore } from 'stores/user';
|
import { useUserStore } from 'stores/user';
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
|
|
|
@ -51,6 +51,18 @@ export default {
|
||||||
addressesList: 'Adreces',
|
addressesList: 'Adreces',
|
||||||
addressDetails: 'Configuració',
|
addressDetails: 'Configuració',
|
||||||
checkout: 'Configurar encàrrec',
|
checkout: 'Configurar encàrrec',
|
||||||
|
controlPanel: 'Panell de control',
|
||||||
|
adminConnections: 'Connexions',
|
||||||
|
adminItems: 'Articles',
|
||||||
|
adminVisits: 'Visites',
|
||||||
|
adminUsers: "Gestió d'usuaris",
|
||||||
|
adminPhotos: 'Imatges',
|
||||||
|
adminNews: 'Gestió de noticies',
|
||||||
|
adminNewsDetails: 'Afegir o editar notícia',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: 'Comanda carregada a la cistella!'
|
orderLoadedIntoBasket: 'Comanda carregada a la cistella!',
|
||||||
|
at: 'a les',
|
||||||
|
back: 'Tornar',
|
||||||
|
remove: 'Esborrar',
|
||||||
|
noData: 'Sense dades'
|
||||||
};
|
};
|
||||||
|
|
|
@ -64,8 +64,20 @@ export default {
|
||||||
addressesList: 'Addresses',
|
addressesList: 'Addresses',
|
||||||
addressDetails: 'Configuration',
|
addressDetails: 'Configuration',
|
||||||
checkout: 'Configure order',
|
checkout: 'Configure order',
|
||||||
|
controlPanel: 'Control panel',
|
||||||
|
adminConnections: 'Connections',
|
||||||
|
adminItems: 'Items',
|
||||||
|
adminVisits: 'Visits',
|
||||||
|
adminUsers: 'User management',
|
||||||
|
adminPhotos: 'Images',
|
||||||
|
adminNews: 'News management',
|
||||||
|
adminNewsDetails: 'Add or edit new',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: 'Order loaded into basket!',
|
orderLoadedIntoBasket: 'Order loaded into basket!',
|
||||||
|
at: 'at',
|
||||||
|
back: 'Back',
|
||||||
|
remove: 'Remove',
|
||||||
|
noData: 'No data',
|
||||||
|
|
||||||
orders: 'Orders',
|
orders: 'Orders',
|
||||||
order: 'Pending order',
|
order: 'Pending order',
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
// This is just an example,
|
|
||||||
// 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',
|
||||||
|
@ -73,8 +70,20 @@ export default {
|
||||||
addressesList: 'Direcciones',
|
addressesList: 'Direcciones',
|
||||||
addressDetails: 'Configuración',
|
addressDetails: 'Configuración',
|
||||||
checkout: 'Configurar pedido',
|
checkout: 'Configurar pedido',
|
||||||
|
controlPanel: 'Panel de control',
|
||||||
|
adminConnections: 'Conexiones',
|
||||||
|
adminItems: 'Artículos',
|
||||||
|
adminVisits: 'Visitas',
|
||||||
|
adminUsers: 'Gestión de usuarios',
|
||||||
|
adminPhotos: 'Imágenes',
|
||||||
|
adminNews: 'Gestión de noticias',
|
||||||
|
adminNewsDetails: 'Añadir o editar noticia',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: '¡Pedido cargado en la cesta!',
|
orderLoadedIntoBasket: '¡Pedido cargado en la cesta!',
|
||||||
|
at: 'a las',
|
||||||
|
back: 'Volver',
|
||||||
|
remove: 'Borrar',
|
||||||
|
noData: 'Sin datos',
|
||||||
|
|
||||||
orders: 'Pedidos',
|
orders: 'Pedidos',
|
||||||
order: 'Pedido pendiente',
|
order: 'Pedido pendiente',
|
||||||
|
|
|
@ -51,6 +51,18 @@ export default {
|
||||||
addressesList: 'Adresses',
|
addressesList: 'Adresses',
|
||||||
addressDetails: 'Configuration',
|
addressDetails: 'Configuration',
|
||||||
checkout: "Définissez l'ordre",
|
checkout: "Définissez l'ordre",
|
||||||
|
controlPanel: 'Panneau de configuration',
|
||||||
|
adminConnections: 'Connexions',
|
||||||
|
adminItems: 'Articles',
|
||||||
|
adminVisits: 'Visites',
|
||||||
|
adminUsers: 'Gestion des utilisateurs',
|
||||||
|
adminPhotos: 'Images',
|
||||||
|
adminNews: 'Gestion des nouvelles',
|
||||||
|
adminNewsDetails: 'Ajouter ou editer nouvelles',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: 'Commande chargée dans le panier!'
|
orderLoadedIntoBasket: 'Commande chargée dans le panier!',
|
||||||
|
at: 'à',
|
||||||
|
back: 'Retour',
|
||||||
|
remove: 'Effacer',
|
||||||
|
noData: 'Aucune donnée'
|
||||||
};
|
};
|
||||||
|
|
|
@ -52,6 +52,18 @@ export default {
|
||||||
addressesList: 'Moradas',
|
addressesList: 'Moradas',
|
||||||
addressDetails: 'Configuração',
|
addressDetails: 'Configuração',
|
||||||
checkout: 'Configurar encomenda',
|
checkout: 'Configurar encomenda',
|
||||||
|
controlPanel: 'Painel de controle',
|
||||||
|
adminConnections: 'Conexões',
|
||||||
|
adminItems: 'Artigos',
|
||||||
|
adminVisits: 'Visitas',
|
||||||
|
adminUsers: 'Gestão de usuários',
|
||||||
|
adminPhotos: 'Imagens',
|
||||||
|
adminNews: 'Gestão de notícias',
|
||||||
|
adminNewsDetails: 'Ajouter ou editer nouvelles',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: 'Pedido carregado na cesta!'
|
orderLoadedIntoBasket: 'Pedido carregado na cesta!',
|
||||||
|
at: 'às',
|
||||||
|
back: 'Voltar',
|
||||||
|
remove: 'Eliminar',
|
||||||
|
noData: 'Sem dados'
|
||||||
};
|
};
|
||||||
|
|
|
@ -37,12 +37,18 @@ export const generateInsertSqlQuery = (
|
||||||
columnsUpdated,
|
columnsUpdated,
|
||||||
createModelDefault
|
createModelDefault
|
||||||
) => {
|
) => {
|
||||||
const columns = [createModelDefault.field, ...columnsUpdated].join(', ');
|
const columns = createModelDefault.field
|
||||||
const values = [
|
? [createModelDefault.field, ...columnsUpdated].join(', ')
|
||||||
|
: columnsUpdated.join(', ');
|
||||||
|
|
||||||
|
const values = createModelDefault.value
|
||||||
|
? [
|
||||||
createModelDefault.value,
|
createModelDefault.value,
|
||||||
...columnsUpdated.map(colName => sanitizeValue(formData[colName]))
|
...columnsUpdated.map(colName => sanitizeValue(formData[colName]))
|
||||||
].join(', ');
|
].join(', ')
|
||||||
|
: columnsUpdated
|
||||||
|
.map(colName => sanitizeValue(formData[colName]))
|
||||||
|
.join(', ');
|
||||||
return `
|
return `
|
||||||
START TRANSACTION;
|
START TRANSACTION;
|
||||||
INSERT INTO ${schema}.${table} (${columns}) VALUES (${values});
|
INSERT INTO ${schema}.${table} (${columns}) VALUES (${values});
|
||||||
|
|
|
@ -1,3 +1,45 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { useUserStore } from 'stores/user';
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const { user, supplantedUser } = storeToRefs(userStore);
|
||||||
|
const { menuEssentialLinks, title, subtitle, useRightDrawer, rightDrawerOpen } =
|
||||||
|
storeToRefs(appStore);
|
||||||
|
const actions = ref(null);
|
||||||
|
const leftDrawerOpen = ref(false);
|
||||||
|
|
||||||
|
const toggleLeftDrawer = () => {
|
||||||
|
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
appStore.isHeaderMounted = true;
|
||||||
|
await userStore.fetchUser();
|
||||||
|
await appStore.loadConfig();
|
||||||
|
await userStore.supplantInit();
|
||||||
|
await appStore.getMenuLinks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
await userStore.logout();
|
||||||
|
router.push('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoutSupplantedUser = async () => {
|
||||||
|
await userStore.logoutSupplantedUser();
|
||||||
|
await appStore.getMenuLinks();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<QLayout view="lHh Lpr lFf">
|
<QLayout view="lHh Lpr lFf">
|
||||||
<QHeader>
|
<QHeader>
|
||||||
|
@ -11,21 +53,15 @@
|
||||||
@click="toggleLeftDrawer"
|
@click="toggleLeftDrawer"
|
||||||
/>
|
/>
|
||||||
<QToolbarTitle>
|
<QToolbarTitle>
|
||||||
{{ $app.title }}
|
{{ title }}
|
||||||
<div
|
<div v-if="subtitle" class="subtitle text-caption">
|
||||||
v-if="$app.subtitle"
|
{{ subtitle }}
|
||||||
class="subtitle text-caption"
|
|
||||||
>
|
|
||||||
{{ $app.subtitle }}
|
|
||||||
</div>
|
</div>
|
||||||
</QToolbarTitle>
|
</QToolbarTitle>
|
||||||
<div
|
<div id="actions" ref="actions" class="flex items-center"></div>
|
||||||
id="actions"
|
|
||||||
ref="actions"
|
|
||||||
/>
|
|
||||||
<QBtn
|
<QBtn
|
||||||
v-if="$app.useRightDrawer"
|
v-if="useRightDrawer"
|
||||||
@click="$app.rightDrawerOpen = !$app.rightDrawerOpen"
|
@click="rightDrawerOpen = !rightDrawerOpen"
|
||||||
aria-label="Menu"
|
aria-label="Menu"
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
|
@ -35,44 +71,29 @@
|
||||||
</QBtn>
|
</QBtn>
|
||||||
</QToolbar>
|
</QToolbar>
|
||||||
</QHeader>
|
</QHeader>
|
||||||
<QDrawer
|
<QDrawer v-model="leftDrawerOpen" :width="250" show-if-above>
|
||||||
v-model="leftDrawerOpen"
|
|
||||||
:width="250"
|
|
||||||
show-if-above
|
|
||||||
>
|
|
||||||
<QToolbar class="logo">
|
<QToolbar class="logo">
|
||||||
<img src="statics/logo-dark.svg">
|
<img src="statics/logo-dark.svg" />
|
||||||
</QToolbar>
|
</QToolbar>
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div>
|
<div>
|
||||||
<span id="user-name">{{ user.nickname }}</span>
|
<span id="user-name">{{ user?.nickname }}</span>
|
||||||
<QBtn
|
<QBtn flat icon="logout" alt="_Exit" @click="logout()" />
|
||||||
flat
|
</div>
|
||||||
icon="logout"
|
<div v-if="supplantedUser" id="supplant" class="supplant">
|
||||||
alt="_Exit"
|
<span id="supplanted">
|
||||||
@click="logout()"
|
{{ supplantedUser?.nickname }}
|
||||||
/>
|
</span>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="supplant"
|
|
||||||
class="supplant"
|
|
||||||
>
|
|
||||||
<span id="supplanted">{{ supplantedUser }}</span>
|
|
||||||
<QBtn
|
<QBtn
|
||||||
flat
|
flat
|
||||||
icon="logout"
|
icon="logout"
|
||||||
alt="_Exit"
|
alt="_Exit"
|
||||||
|
@click="logoutSupplantedUser()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<QList
|
<QList v-for="item in menuEssentialLinks" :key="item.id">
|
||||||
v-for="item in essentialLinks"
|
<QItem v-if="!item.childs" :to="`/${item.path}`">
|
||||||
:key="item.id"
|
|
||||||
>
|
|
||||||
<QItem
|
|
||||||
v-if="!item.childs"
|
|
||||||
:to="`/${item.path}`"
|
|
||||||
>
|
|
||||||
<QItemSection>
|
<QItemSection>
|
||||||
<QItemLabel>{{ item.description }}</QItemLabel>
|
<QItemLabel>{{ item.description }}</QItemLabel>
|
||||||
</QItemSection>
|
</QItemSection>
|
||||||
|
@ -146,12 +167,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.supplant {
|
&.supplant {
|
||||||
display: none;
|
|
||||||
border-top: none;
|
border-top: none;
|
||||||
|
|
||||||
&.show {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,12 +187,9 @@
|
||||||
.q-page-container > * {
|
.q-page-container > * {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
#actions > div {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
#actions > div {
|
#actions {
|
||||||
.q-btn {
|
.q-btn {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
@ -194,71 +207,6 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
|
||||||
import { defineComponent, ref } from 'vue';
|
|
||||||
import { userStore } from 'stores/user';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'MainLayout',
|
|
||||||
props: {},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const leftDrawerOpen = ref(false);
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: userStore(),
|
|
||||||
supplantedUser: ref(''),
|
|
||||||
essentialLinks: ref(null),
|
|
||||||
leftDrawerOpen,
|
|
||||||
toggleLeftDrawer() {
|
|
||||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
this.$refs.actions.appendChild(this.$actions);
|
|
||||||
await this.user.loadData();
|
|
||||||
await this.$app.loadConfig();
|
|
||||||
await this.fetchData();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async fetchData() {
|
|
||||||
const sections = await this.$jApi.query('SELECT * FROM myMenu');
|
|
||||||
|
|
||||||
const sectionMap = new Map();
|
|
||||||
for (const section of sections) {
|
|
||||||
sectionMap.set(section.id, section);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sectionTree = [];
|
|
||||||
for (const section of sections) {
|
|
||||||
const parent = section.parentFk;
|
|
||||||
if (parent) {
|
|
||||||
const parentSection = sectionMap.get(parent);
|
|
||||||
if (!parentSection) continue;
|
|
||||||
let childs = parentSection.childs;
|
|
||||||
if (!childs) {
|
|
||||||
childs = parentSection.childs = [];
|
|
||||||
}
|
|
||||||
childs.push(section);
|
|
||||||
} else {
|
|
||||||
sectionTree.push(section);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.essentialLinks = sectionTree;
|
|
||||||
},
|
|
||||||
|
|
||||||
async logout() {
|
|
||||||
this.user.logout();
|
|
||||||
this.$router.push('/login');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
en-US:
|
en-US:
|
||||||
visitor: Visitor
|
visitor: Visitor
|
||||||
|
|
|
@ -6,7 +6,7 @@ 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 = 'YYYY-MM-DD') {
|
||||||
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);
|
||||||
|
@ -43,8 +43,8 @@ export const formatDateTitle = (
|
||||||
|
|
||||||
const timeFormat = options.showTime
|
const timeFormat = options.showTime
|
||||||
? options.showSeconds
|
? options.showSeconds
|
||||||
? ` [${t('at')}] hh:mm:ss`
|
? ` [${t('at')}] HH:mm:ss`
|
||||||
: ` [${t('at')}] hh:mm`
|
: ` [${t('at')}] HH:mm`
|
||||||
: '';
|
: '';
|
||||||
const day = options.shortDay ? 'dd' : 'dddd';
|
const day = options.shortDay ? 'dd' : 'dddd';
|
||||||
|
|
||||||
|
|
|
@ -6,18 +6,22 @@ import VnSelect from 'src/components/common/VnSelect.vue';
|
||||||
import VnForm from 'src/components/common/VnForm.vue';
|
import VnForm from 'src/components/common/VnForm.vue';
|
||||||
import ChangePasswordForm from 'src/components/ui/ChangePasswordForm.vue';
|
import ChangePasswordForm from 'src/components/ui/ChangePasswordForm.vue';
|
||||||
|
|
||||||
import { userStore as useUserStore } from 'stores/user';
|
import { useUserStore } from 'stores/user';
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const jApi = inject('jApi');
|
const jApi = inject('jApi');
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
const vnFormRef = ref(null);
|
const vnFormRef = ref(null);
|
||||||
const vnFormRef2 = ref(null);
|
const vnFormRef2 = ref(null);
|
||||||
const changePasswordFormDialog = ref(null);
|
const changePasswordFormDialog = ref(null);
|
||||||
const showChangePasswordForm = ref(false);
|
const showChangePasswordForm = ref(false);
|
||||||
const langOptions = ref([]);
|
const langOptions = ref([]);
|
||||||
const pks = computed(() => ({ id: userStore.id }));
|
const pks = computed(() => ({ id: userStore?.user?.id }));
|
||||||
const fetchConfigDataSql = {
|
const fetchConfigDataSql = {
|
||||||
query: `
|
query: `
|
||||||
SELECT u.id, u.name, u.email, u.nickname,
|
SELECT u.id, u.name, u.email, u.nickname,
|
||||||
|
@ -45,7 +49,7 @@ onMounted(() => fetchLanguagesSql());
|
||||||
<template>
|
<template>
|
||||||
<QPage>
|
<QPage>
|
||||||
<QPage class="q-pa-md flex justify-center">
|
<QPage class="q-pa-md flex justify-center">
|
||||||
<Teleport :to="$actions">
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
<QBtn
|
<QBtn
|
||||||
:label="t('addresses')"
|
:label="t('addresses')"
|
||||||
icon="location_on"
|
icon="location_on"
|
||||||
|
|
|
@ -7,10 +7,15 @@ import VnInput from 'src/components/common/VnInput.vue';
|
||||||
import VnSelect from 'src/components/common/VnSelect.vue';
|
import VnSelect from 'src/components/common/VnSelect.vue';
|
||||||
import VnForm from 'src/components/common/VnForm.vue';
|
import VnForm from 'src/components/common/VnForm.vue';
|
||||||
|
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const jApi = inject('jApi');
|
const jApi = inject('jApi');
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
const vnFormRef = ref(null);
|
const vnFormRef = ref(null);
|
||||||
const countriesOptions = ref([]);
|
const countriesOptions = ref([]);
|
||||||
|
@ -56,14 +61,18 @@ onMounted(() => getCountries());
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<QPage class="q-pa-md flex justify-center">
|
<QPage class="q-pa-md flex justify-center">
|
||||||
<Teleport :to="$actions">
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
<QBtn
|
<QBtn
|
||||||
:label="t('back')"
|
:label="t('back')"
|
||||||
icon="close"
|
icon="close"
|
||||||
rounded
|
rounded
|
||||||
no-caps
|
no-caps
|
||||||
@click="goBack()"
|
@click="goBack()"
|
||||||
/>
|
>
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('back') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<VnForm
|
<VnForm
|
||||||
ref="vnFormRef"
|
ref="vnFormRef"
|
||||||
|
@ -124,7 +133,6 @@ onMounted(() => getCountries());
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
en-US:
|
en-US:
|
||||||
back: Back
|
|
||||||
accept: Accept
|
accept: Accept
|
||||||
name: Consignee
|
name: Consignee
|
||||||
address: Address
|
address: Address
|
||||||
|
@ -136,7 +144,6 @@ en-US:
|
||||||
addAddress: Add address
|
addAddress: Add address
|
||||||
editAddress: Edit address
|
editAddress: Edit address
|
||||||
es-ES:
|
es-ES:
|
||||||
back: Volver
|
|
||||||
accept: Aceptar
|
accept: Aceptar
|
||||||
name: Consignatario
|
name: Consignatario
|
||||||
address: Dirección
|
address: Dirección
|
||||||
|
@ -148,7 +155,6 @@ es-ES:
|
||||||
addAddress: Añadir dirección
|
addAddress: Añadir dirección
|
||||||
editAddress: Modificar dirección
|
editAddress: Modificar dirección
|
||||||
ca-ES:
|
ca-ES:
|
||||||
back: Tornar
|
|
||||||
accept: Acceptar
|
accept: Acceptar
|
||||||
name: Consignatari
|
name: Consignatari
|
||||||
address: Direcció
|
address: Direcció
|
||||||
|
@ -160,7 +166,6 @@ ca-ES:
|
||||||
addAddress: Afegir adreça
|
addAddress: Afegir adreça
|
||||||
editAddress: Modificar adreça
|
editAddress: Modificar adreça
|
||||||
fr-FR:
|
fr-FR:
|
||||||
back: Retour
|
|
||||||
accept: Accepter
|
accept: Accepter
|
||||||
name: Destinataire
|
name: Destinataire
|
||||||
address: Numéro Rue
|
address: Numéro Rue
|
||||||
|
@ -172,7 +177,6 @@ fr-FR:
|
||||||
addAddress: Ajouter adresse
|
addAddress: Ajouter adresse
|
||||||
editAddress: Modifier adresse
|
editAddress: Modifier adresse
|
||||||
pt-PT:
|
pt-PT:
|
||||||
back: Voltar
|
|
||||||
accept: Aceitar
|
accept: Aceitar
|
||||||
name: Consignatario
|
name: Consignatario
|
||||||
address: Morada
|
address: Morada
|
||||||
|
|
|
@ -7,12 +7,16 @@ import CardList from 'src/components/ui/CardList.vue';
|
||||||
|
|
||||||
import useNotify from 'src/composables/useNotify.js';
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
import { useVnConfirm } from 'src/composables/useVnConfirm.js';
|
import { useVnConfirm } from 'src/composables/useVnConfirm.js';
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const jApi = inject('jApi');
|
const jApi = inject('jApi');
|
||||||
const { notify } = useNotify();
|
const { notify } = useNotify();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { openConfirmationModal } = useVnConfirm();
|
const { openConfirmationModal } = useVnConfirm();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
const addresses = ref([]);
|
const addresses = ref([]);
|
||||||
const defaultAddress = ref(null);
|
const defaultAddress = ref(null);
|
||||||
|
@ -86,14 +90,18 @@ onMounted(async () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="$actions">
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
<QBtn
|
<QBtn
|
||||||
:label="t('addAddress')"
|
:label="t('addAddress')"
|
||||||
icon="add"
|
icon="add"
|
||||||
@click="goToAddressDetails()"
|
@click="goToAddressDetails()"
|
||||||
rounded
|
rounded
|
||||||
no-caps
|
no-caps
|
||||||
/>
|
>
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('addAddress') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<QPage class="vn-w-sm">
|
<QPage class="vn-w-sm">
|
||||||
<QList
|
<QList
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, inject, onBeforeUnmount } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import CardList from 'src/components/ui/CardList.vue';
|
||||||
|
|
||||||
|
import { date as qdate } from 'quasar';
|
||||||
|
import { useUserStore } from 'stores/user';
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const jApi = inject('jApi');
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
|
const connections = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const intervalId = ref(null);
|
||||||
|
|
||||||
|
const getConnections = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
connections.value = await jApi.query(
|
||||||
|
`SELECT vu.userFk userId, vu.stamp, u.nickname, s.lastUpdate,
|
||||||
|
a.platform, a.browser, a.version, u.name user
|
||||||
|
FROM userSession s
|
||||||
|
JOIN visitUser vu ON vu.id = s.userVisitFk
|
||||||
|
JOIN visitAccess ac ON ac.id = vu.accessFk
|
||||||
|
JOIN visitAgent a ON a.id = ac.agentFk
|
||||||
|
JOIN visit v ON v.id = a.visitFk
|
||||||
|
JOIN account.user u ON u.id = vu.userFk
|
||||||
|
ORDER BY lastUpdate DESC`
|
||||||
|
);
|
||||||
|
loading.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting connections:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const supplantUser = async user => {
|
||||||
|
try {
|
||||||
|
await userStore.supplantUser(user);
|
||||||
|
await appStore.getMenuLinks();
|
||||||
|
router.push({ name: 'confirmedOrders' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error supplanting user:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
getConnections();
|
||||||
|
intervalId.value = setInterval(getConnections, 60000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => clearInterval(intervalId.value));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
|
<div class="flex">
|
||||||
|
<QBtn
|
||||||
|
:label="t('refresh')"
|
||||||
|
icon="refresh"
|
||||||
|
@click="getConnections()"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
class="q-mr-sm"
|
||||||
|
>
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('refresh') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<QBadge class="q-pa-sm" v-if="connections.length" color="blue">
|
||||||
|
{{ connections?.length }} {{ t('connections') }}
|
||||||
|
</QBadge>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
<QPage class="vn-w-xs">
|
||||||
|
<QList class="flex justify-center">
|
||||||
|
<QSpinner
|
||||||
|
v-if="loading"
|
||||||
|
color="primary"
|
||||||
|
size="3em"
|
||||||
|
:thickness="2"
|
||||||
|
/>
|
||||||
|
<CardList
|
||||||
|
v-else
|
||||||
|
v-for="(connection, index) in connections"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<span class="text-bold q-mb-sm">
|
||||||
|
{{ connection.nickname }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
qdate.formatDate(connection.stamp, 'dd, hh:mm:ss A')
|
||||||
|
}}
|
||||||
|
-
|
||||||
|
{{
|
||||||
|
qdate.formatDate(
|
||||||
|
connection.lastUpdate,
|
||||||
|
'hh:mm:ss A'
|
||||||
|
)
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
connection.platform &&
|
||||||
|
connection.browser &&
|
||||||
|
connection.version
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ connection.platform }} - {{ connection.browser }} -
|
||||||
|
{{ connection.version }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<QBtn
|
||||||
|
icon="people"
|
||||||
|
flat
|
||||||
|
rounded
|
||||||
|
@click="supplantUser(connection.user)"
|
||||||
|
>
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('supplantUser') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</template>
|
||||||
|
</CardList>
|
||||||
|
</QList>
|
||||||
|
</QPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
refresh: Refresh
|
||||||
|
connections: Connections
|
||||||
|
supplantUser: Supplant user
|
||||||
|
es-ES:
|
||||||
|
refresh: Actualizar
|
||||||
|
connections: Conexiones
|
||||||
|
supplantUser: Suplantar usuario
|
||||||
|
ca-ES:
|
||||||
|
refresh: Actualitzar
|
||||||
|
connections: Connexions
|
||||||
|
supplantUser: Suplantar usuari
|
||||||
|
fr-FR:
|
||||||
|
refresh: Rafraîchir
|
||||||
|
connections: Connexions
|
||||||
|
supplantUser: Supplanter l'utilisateur
|
||||||
|
pt-PT:
|
||||||
|
refresh: Atualizar
|
||||||
|
connections: Conexões
|
||||||
|
supplantUser: Suplantar usuário
|
||||||
|
</i18n>
|
|
@ -0,0 +1,100 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import CardList from 'src/components/ui/CardList.vue';
|
||||||
|
import VnImg from 'src/components/ui/VnImg.vue';
|
||||||
|
import VnSearchBar from 'src/components/ui/VnSearchBar.vue';
|
||||||
|
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const items = ref([]);
|
||||||
|
|
||||||
|
const query = `SELECT i.id, i.longName, i.size, i.category,
|
||||||
|
i.value5, i.value6, i.value7,
|
||||||
|
i.image, im.updated
|
||||||
|
FROM vn.item i
|
||||||
|
LEFT JOIN image im
|
||||||
|
ON im.collectionFk = 'catalog'
|
||||||
|
AND im.name = i.image
|
||||||
|
WHERE i.longName LIKE CONCAT('%', #search, '%')
|
||||||
|
OR i.id = #search
|
||||||
|
ORDER BY i.longName LIMIT 50`;
|
||||||
|
|
||||||
|
const onSearch = data => (items.value = data || []);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
|
<VnSearchBar
|
||||||
|
:sqlQuery="query"
|
||||||
|
@onSearch="onSearch"
|
||||||
|
@onSearchError="items = []"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<QPage class="vn-w-xs">
|
||||||
|
<QList class="flex justify-center">
|
||||||
|
<span v-if="!loading && !items.length" class="flex items-center">
|
||||||
|
<QIcon name="refresh" size="sm" class="q-mr-sm" />
|
||||||
|
{{ t('introduceSearchTerm') }}
|
||||||
|
</span>
|
||||||
|
<QSpinner
|
||||||
|
v-if="loading"
|
||||||
|
color="primary"
|
||||||
|
size="3em"
|
||||||
|
:thickness="2"
|
||||||
|
/>
|
||||||
|
<CardList
|
||||||
|
v-else
|
||||||
|
v-for="(item, index) in items"
|
||||||
|
:key="index"
|
||||||
|
:clickable="false"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VnImg
|
||||||
|
storage="catalog"
|
||||||
|
size="200x200"
|
||||||
|
:id="item.id"
|
||||||
|
width="80px"
|
||||||
|
height="80px"
|
||||||
|
class="q-mr-md"
|
||||||
|
rounded
|
||||||
|
editable
|
||||||
|
editSchema="catalog"
|
||||||
|
:editImageName="item.image"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<span class="text-bold q-mb-sm">
|
||||||
|
{{ item.longName }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ item.value5 }} {{ item.value6 }}
|
||||||
|
{{ item.value7 }}
|
||||||
|
</span>
|
||||||
|
<span>{{ item.id }}</span>
|
||||||
|
<span>{{ item.image }}</span>
|
||||||
|
</template>
|
||||||
|
</CardList>
|
||||||
|
</QList>
|
||||||
|
</QPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
introduceSearchTerm: Enter a search term
|
||||||
|
es-ES:
|
||||||
|
introduceSearchTerm: Introduce un término de búsqueda
|
||||||
|
ca-ES:
|
||||||
|
introduceSearchTerm: Introdueix un terme de cerca
|
||||||
|
fr-FR:
|
||||||
|
introduceSearchTerm: Entrez un terme de recherche
|
||||||
|
pt-PT:
|
||||||
|
introduceSearchTerm: Digite um termo de pesquisa
|
||||||
|
</i18n>
|
|
@ -0,0 +1,66 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, inject } from 'vue';
|
||||||
|
|
||||||
|
const jApi = inject('jApi');
|
||||||
|
|
||||||
|
const links = ref([]);
|
||||||
|
|
||||||
|
const getLinks = async () => {
|
||||||
|
try {
|
||||||
|
links.value = await jApi.query(
|
||||||
|
`SELECT image, name, description, link FROM link
|
||||||
|
ORDER BY name`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting links:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => getLinks());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<QPage>
|
||||||
|
<QList class="flex justify-center q-gutter-md">
|
||||||
|
<QItem
|
||||||
|
v-for="(link, index) in links"
|
||||||
|
:key="index"
|
||||||
|
:href="link.link"
|
||||||
|
target="_blank"
|
||||||
|
class="flex no-padding"
|
||||||
|
>
|
||||||
|
<QCard class="card-container">
|
||||||
|
<QImg
|
||||||
|
:src="`http://cdn.verdnatura.es/image/link/full/${link.image}`"
|
||||||
|
width="60px"
|
||||||
|
height="60px"
|
||||||
|
/>
|
||||||
|
<span class="card-title q-mt-md">{{ link.name }}</span>
|
||||||
|
<p class="card-description">{{ link.description }}</p>
|
||||||
|
</QCard>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</QPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card-container {
|
||||||
|
width: 140px;
|
||||||
|
height: 170px;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-description {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,250 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, inject, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
import VnImg from 'src/components/ui/VnImg.vue';
|
||||||
|
import VnForm from 'src/components/common/VnForm.vue';
|
||||||
|
import VnSelect from 'src/components/common/VnSelect.vue';
|
||||||
|
import VnInput from 'src/components/common/VnInput.vue';
|
||||||
|
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
const jApi = inject('jApi');
|
||||||
|
const { t } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
|
const newsTags = ref([]);
|
||||||
|
const pks = computed(() => ({ id: route.params.id }));
|
||||||
|
const isEditMode = !!route.params.id;
|
||||||
|
const formData = ref(
|
||||||
|
!route.params.id
|
||||||
|
? {
|
||||||
|
title: '',
|
||||||
|
tag: '',
|
||||||
|
priority: '',
|
||||||
|
text: ''
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchNewDataSql = computed(() => {
|
||||||
|
if (!route.params.id) return undefined;
|
||||||
|
return {
|
||||||
|
query: `
|
||||||
|
SELECT id, title, text, tag, priority, image
|
||||||
|
FROM news WHERE id = #id`,
|
||||||
|
params: { id: route.params.id }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const getNewsTag = async () => {
|
||||||
|
try {
|
||||||
|
newsTags.value = await jApi.query(
|
||||||
|
`SELECT name, description FROM newsTag
|
||||||
|
ORDER BY description`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting newsTag:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBack = () => router.push({ name: 'adminNews' });
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
getNewsTag();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<QPage class="vn-w-sm">
|
||||||
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
|
<QBtn
|
||||||
|
:label="t('back')"
|
||||||
|
icon="close"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
@click="goBack()"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('back') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</Teleport>
|
||||||
|
<VnForm
|
||||||
|
ref="vnFormRef"
|
||||||
|
:fetch-form-data-sql="fetchNewDataSql"
|
||||||
|
:form-initial-data="formData"
|
||||||
|
:create-model-default="{
|
||||||
|
field: 'userFk',
|
||||||
|
value: 'account.myUser_getId()'
|
||||||
|
}"
|
||||||
|
:pks="pks"
|
||||||
|
:is-edit-mode="isEditMode"
|
||||||
|
table="news"
|
||||||
|
schema="hedera"
|
||||||
|
separation-between-inputs="lg"
|
||||||
|
@on-data-saved="goBack()"
|
||||||
|
>
|
||||||
|
<template #form="{ data }">
|
||||||
|
<VnImg
|
||||||
|
:id="data.image"
|
||||||
|
:edit-image-name="data.image"
|
||||||
|
storage="news"
|
||||||
|
edit-schema="news"
|
||||||
|
size="200x200"
|
||||||
|
width="80px"
|
||||||
|
height="80px"
|
||||||
|
class="full-width"
|
||||||
|
rounded
|
||||||
|
editable
|
||||||
|
always-show-edit-button
|
||||||
|
/>
|
||||||
|
<VnInput
|
||||||
|
v-model="data.title"
|
||||||
|
:label="t('title')"
|
||||||
|
:clearable="false"
|
||||||
|
/>
|
||||||
|
<div class="row justify-between q-gutter-x-md">
|
||||||
|
<VnSelect
|
||||||
|
v-model="data.tag"
|
||||||
|
:label="t('tag')"
|
||||||
|
option-label="description"
|
||||||
|
option-value="name"
|
||||||
|
:options="newsTags"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
<VnInput
|
||||||
|
v-model="data.priority"
|
||||||
|
:label="t('priority')"
|
||||||
|
:clearable="false"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<QEditor
|
||||||
|
v-model="data.text"
|
||||||
|
:toolbar="[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
|||||||
|
label: $q.lang.editor.align,
|
||||||
|
icon: $q.iconSet.editor.align,
|
||||||
|
fixedLabel: true,
|
||||||
|
options: ['left', 'center', 'right', 'justify']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bold',
|
||||||
|
'italic',
|
||||||
|
'strike',
|
||||||
|
'underline',
|
||||||
|
'subscript',
|
||||||
|
'superscript'
|
||||||
|
],
|
||||||
|
['token', 'hr', 'link', 'custom_btn'],
|
||||||
|
['print', 'fullscreen'],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: $q.lang.editor.formatting,
|
||||||
|
icon: $q.iconSet.editor.formatting,
|
||||||
|
list: 'no-icons',
|
||||||
|
options: [
|
||||||
|
'p',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
'code'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $q.lang.editor.fontSize,
|
||||||
|
icon: $q.iconSet.editor.fontSize,
|
||||||
|
fixedLabel: true,
|
||||||
|
fixedIcon: true,
|
||||||
|
list: 'no-icons',
|
||||||
|
options: [
|
||||||
|
'size-1',
|
||||||
|
'size-2',
|
||||||
|
'size-3',
|
||||||
|
'size-4',
|
||||||
|
'size-5',
|
||||||
|
'size-6',
|
||||||
|
'size-7'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $q.lang.editor.defaultFont,
|
||||||
|
icon: $q.iconSet.editor.font,
|
||||||
|
fixedIcon: true,
|
||||||
|
list: 'no-icons',
|
||||||
|
options: [
|
||||||
|
'default_font',
|
||||||
|
'arial',
|
||||||
|
'arial_black',
|
||||||
|
'comic_sans',
|
||||||
|
'courier_new',
|
||||||
|
'impact',
|
||||||
|
'lucida_grande',
|
||||||
|
'times_new_roman',
|
||||||
|
'verdana'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'removeFormat'
|
||||||
|
],
|
||||||
|
['quote', 'unordered', 'ordered', 'outdent', 'indent'],
|
||||||
|
|
||||||
|
['undo', 'redo'],
|
||||||
|
['viewsource']
|
||||||
|
]"
|
||||||
|
:fonts="{
|
||||||
|
arial: 'Arial',
|
||||||
|
arial_black: 'Arial Black',
|
||||||
|
comic_sans: 'Comic Sans MS',
|
||||||
|
courier_new: 'Courier New',
|
||||||
|
impact: 'Impact',
|
||||||
|
lucida_grande: 'Lucida Grande',
|
||||||
|
times_new_roman: 'Times New Roman',
|
||||||
|
verdana: 'Verdana'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VnForm>
|
||||||
|
</QPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
addNew: Add new
|
||||||
|
confirmDeleteAddress: Are you sure you want to delete this new?
|
||||||
|
title: Title
|
||||||
|
tag: Tag
|
||||||
|
priority: Priority
|
||||||
|
es-ES:
|
||||||
|
addNew: Añadir noticia
|
||||||
|
confirmDeleteAddress: ¿Estás seguro de que quieres eliminar esta noticia?
|
||||||
|
title: Título
|
||||||
|
tag: Etiqueta
|
||||||
|
priority: Prioridad
|
||||||
|
ca-ES:
|
||||||
|
addNew: Afegir noticia
|
||||||
|
confirmDeleteAddress: Estàs segur que vols eliminar aquesta notícia?
|
||||||
|
title: Títol
|
||||||
|
tag: Etiqueta
|
||||||
|
priority: Prioritat
|
||||||
|
fr-FR:
|
||||||
|
addNew: Ajouter nouvelles
|
||||||
|
confirmDeleteAddress: Êtes-vous sûr de vouloir supprimer cette nouvelle?
|
||||||
|
title: Titre
|
||||||
|
tag: Tag
|
||||||
|
priority: Priorité
|
||||||
|
pt-PT:
|
||||||
|
addNew: Adicionar noticia
|
||||||
|
confirmDeleteAddress: Tem a certeza que deseja eliminar esta notícia?
|
||||||
|
title: Título
|
||||||
|
tag: Etiqueta
|
||||||
|
priority: Prioridade
|
||||||
|
</i18n>
|
|
@ -0,0 +1,138 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, inject } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import CardList from 'src/components/ui/CardList.vue';
|
||||||
|
import VnImg from 'src/components/ui/VnImg.vue';
|
||||||
|
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useVnConfirm } from 'src/composables/useVnConfirm.js';
|
||||||
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
|
|
||||||
|
const jApi = inject('jApi');
|
||||||
|
const { t } = useI18n();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { openConfirmationModal } = useVnConfirm();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
const { notify } = useNotify();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const news = ref([]);
|
||||||
|
|
||||||
|
const getNews = async () => {
|
||||||
|
try {
|
||||||
|
news.value = await jApi.query(
|
||||||
|
`SELECT n.id, u.nickname, n.priority, n.image, n.title
|
||||||
|
FROM news n
|
||||||
|
JOIN account.user u ON u.id = n.userFk
|
||||||
|
ORDER BY priority, n.created DESC`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting news:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteNew = async (id, index) => {
|
||||||
|
try {
|
||||||
|
await jApi.execQuery(
|
||||||
|
`START TRANSACTION;
|
||||||
|
DELETE FROM hedera.news WHERE ((id = #id));
|
||||||
|
COMMIT`,
|
||||||
|
{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
);
|
||||||
|
news.value.splice(index, 1);
|
||||||
|
notify(t('dataSaved'), 'positive');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting news:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => getNews());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
|
<QBtn
|
||||||
|
:label="t('addNew')"
|
||||||
|
icon="add"
|
||||||
|
:to="{ name: 'adminNewsDetails' }"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('addNew') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</Teleport>
|
||||||
|
<QPage class="vn-w-xs">
|
||||||
|
<QList class="flex justify-center">
|
||||||
|
<QSpinner
|
||||||
|
v-if="loading"
|
||||||
|
color="primary"
|
||||||
|
size="3em"
|
||||||
|
:thickness="2"
|
||||||
|
/>
|
||||||
|
<CardList
|
||||||
|
v-else
|
||||||
|
v-for="(newsItem, index) in news"
|
||||||
|
:key="index"
|
||||||
|
:to="{ name: 'adminNewsDetails', params: { id: newsItem.id } }"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VnImg
|
||||||
|
:id="newsItem.image"
|
||||||
|
:edit-image-name="newsItem.image"
|
||||||
|
storage="news"
|
||||||
|
edit-schema="news"
|
||||||
|
size="200x200"
|
||||||
|
width="80px"
|
||||||
|
height="80px"
|
||||||
|
class="q-mr-md"
|
||||||
|
rounded
|
||||||
|
editable
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<span class="text-bold q-mb-sm">{{ newsItem.title }} </span>
|
||||||
|
<span>{{ newsItem.nickname }} </span>
|
||||||
|
<span>{{ newsItem.priority }}</span>
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<QBtn
|
||||||
|
icon="delete"
|
||||||
|
flat
|
||||||
|
rounded
|
||||||
|
@click.stop.prevent="
|
||||||
|
openConfirmationModal(
|
||||||
|
null,
|
||||||
|
t('confirmDeleteAddress'),
|
||||||
|
() => deleteNew(newsItem.id, index)
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('remove') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</template>
|
||||||
|
</CardList>
|
||||||
|
</QList>
|
||||||
|
</QPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
addNew: Add new
|
||||||
|
confirmDeleteAddress: Are you sure you want to delete this new?
|
||||||
|
es-ES:
|
||||||
|
addNew: Añadir noticia
|
||||||
|
confirmDeleteAddress: ¿Estás seguro de que quieres eliminar esta noticia?
|
||||||
|
ca-ES:
|
||||||
|
addNew: Afegir noticia
|
||||||
|
confirmDeleteAddress: Estàs segur que vols eliminar aquesta notícia?
|
||||||
|
fr-FR:
|
||||||
|
addNew: Ajouter nouvelles
|
||||||
|
confirmDeleteAddress: Êtes-vous sûr de vouloir supprimer cette nouvelle?
|
||||||
|
pt-PT:
|
||||||
|
addNew: Adicionar noticia
|
||||||
|
confirmDeleteAddress: Tem a certeza que deseja eliminar esta notícia?
|
||||||
|
</i18n>
|
|
@ -0,0 +1,278 @@
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { ref, onMounted, inject, reactive, computed } from 'vue';
|
||||||
|
|
||||||
|
import VnSelect from 'src/components/common/VnSelect.vue';
|
||||||
|
import VnForm from 'src/components/common/VnForm.vue';
|
||||||
|
import VnInput from 'src/components/common/VnInput.vue';
|
||||||
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
|
|
||||||
|
const jApi = inject('jApi');
|
||||||
|
const api = inject('api');
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { notify } = useNotify();
|
||||||
|
|
||||||
|
const fileUploaderRef = ref(null);
|
||||||
|
const statusIcons = {
|
||||||
|
uploading: 'cloud_upload',
|
||||||
|
fulfilled: 'cloud_done',
|
||||||
|
rejected: 'error',
|
||||||
|
pending: 'add'
|
||||||
|
};
|
||||||
|
const formInitialData = reactive({
|
||||||
|
schema: 'catalog',
|
||||||
|
updateMatching: true
|
||||||
|
});
|
||||||
|
const imageCollections = ref([]);
|
||||||
|
const addedFiles = ref([]);
|
||||||
|
|
||||||
|
const isSubmitable = computed(() =>
|
||||||
|
addedFiles.value.some(file => file.uploadStatus === 'pending')
|
||||||
|
);
|
||||||
|
|
||||||
|
const getImageCollections = async () => {
|
||||||
|
try {
|
||||||
|
imageCollections.value = await jApi.query(
|
||||||
|
'SELECT name, `desc` FROM imageCollection ORDER BY `desc`'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting image collections:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async data => {
|
||||||
|
if (!addedFiles.value.length) {
|
||||||
|
notify(t('noFilesToUpload'), 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const filteredFiles = addedFiles.value.filter(
|
||||||
|
file => file.uploadStatus === 'pending'
|
||||||
|
);
|
||||||
|
const promises = filteredFiles.map((file, index) => {
|
||||||
|
const fileIndex = filteredFiles[index].index;
|
||||||
|
addedFiles.value[fileIndex].uploadStatus = 'uploading';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('updateMatching', data.updateMatching);
|
||||||
|
formData.append('image', file.file);
|
||||||
|
formData.append('name', file.name);
|
||||||
|
formData.append('schema', data.schema);
|
||||||
|
formData.append('srv', 'json:image/upload');
|
||||||
|
return api({
|
||||||
|
method: 'post',
|
||||||
|
url: location.origin,
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(promises);
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
const fileIndex = filteredFiles[index].index;
|
||||||
|
addedFiles.value[fileIndex].uploadStatus = result.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
const allSuccessful = results.every(
|
||||||
|
result => result.status === 'fulfilled'
|
||||||
|
);
|
||||||
|
if (allSuccessful) {
|
||||||
|
notify(t('uploadSuccess'), 'positive');
|
||||||
|
} else {
|
||||||
|
notify(t('uploadError'), 'negative');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFilesAdded = files => {
|
||||||
|
const initialFilesLength = addedFiles.value.length;
|
||||||
|
files.forEach((file, index) => {
|
||||||
|
const [name] = file.name.split('.');
|
||||||
|
const fileData = {
|
||||||
|
name,
|
||||||
|
file,
|
||||||
|
index: initialFilesLength + index,
|
||||||
|
uploadStatus: 'pending'
|
||||||
|
};
|
||||||
|
addedFiles.value.push(fileData);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const recalculateFilesIndexes = () => {
|
||||||
|
addedFiles.value.forEach((_, index) => {
|
||||||
|
addedFiles.value[index].index = index;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFile = (file, index) => {
|
||||||
|
fileUploaderRef.value.removeFile(file);
|
||||||
|
addedFiles.value.splice(index, 1);
|
||||||
|
recalculateFilesIndexes();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFiles = () => {
|
||||||
|
fileUploaderRef.value.reset();
|
||||||
|
addedFiles.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => getImageCollections());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<QPage class="vn-w-sm">
|
||||||
|
<VnForm
|
||||||
|
ref="vnFormRef"
|
||||||
|
:defaultActions="false"
|
||||||
|
:formInitialData="formInitialData"
|
||||||
|
separationBetweenInputs="md"
|
||||||
|
showBottomActions
|
||||||
|
>
|
||||||
|
<template #form="{ data }">
|
||||||
|
<VnSelect
|
||||||
|
v-model="data.schema"
|
||||||
|
:label="t('collection')"
|
||||||
|
option-label="desc"
|
||||||
|
option-value="name"
|
||||||
|
:options="imageCollections"
|
||||||
|
/>
|
||||||
|
<QUploader
|
||||||
|
ref="fileUploaderRef"
|
||||||
|
:label="t('dropYourFiles')"
|
||||||
|
class="full-width"
|
||||||
|
square
|
||||||
|
flat
|
||||||
|
multiple
|
||||||
|
bordered
|
||||||
|
hide-upload-btn
|
||||||
|
@added="onFilesAdded"
|
||||||
|
>
|
||||||
|
<template v-slot:list="scope">
|
||||||
|
<QList v-if="addedFiles.length" separator>
|
||||||
|
<QItem
|
||||||
|
v-for="(file, index) in scope.files"
|
||||||
|
:key="file.__key"
|
||||||
|
class="flex full-width row items-center justify-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="file.__img.src"
|
||||||
|
style="width: 28px; height: 21px"
|
||||||
|
class="q-mr-md"
|
||||||
|
/>
|
||||||
|
<VnInput
|
||||||
|
v-model="addedFiles[index].name"
|
||||||
|
:clearable="false"
|
||||||
|
dense
|
||||||
|
class="full-width"
|
||||||
|
/>
|
||||||
|
<QSpinner
|
||||||
|
v-if="
|
||||||
|
addedFiles[index].uploadStatus ===
|
||||||
|
'uploading'
|
||||||
|
"
|
||||||
|
color="primary"
|
||||||
|
size="2em"
|
||||||
|
:thickness="1"
|
||||||
|
/>
|
||||||
|
<QIcon
|
||||||
|
v-else-if="
|
||||||
|
addedFiles[index].uploadStatus &&
|
||||||
|
addedFiles[index].uploadStatus !==
|
||||||
|
'uploading'
|
||||||
|
"
|
||||||
|
:name="
|
||||||
|
statusIcons[
|
||||||
|
addedFiles[index].uploadStatus
|
||||||
|
]
|
||||||
|
"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
v-if="
|
||||||
|
addedFiles[index].uploadStatus !==
|
||||||
|
'uploading'
|
||||||
|
"
|
||||||
|
class="gt-xs"
|
||||||
|
size="md"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
round
|
||||||
|
icon="delete"
|
||||||
|
@click="removeFile(file, index)"
|
||||||
|
/>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</template>
|
||||||
|
</QUploader>
|
||||||
|
<QCheckbox
|
||||||
|
v-model="data.updateMatching"
|
||||||
|
:label="t('updateMatching')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ data }">
|
||||||
|
<QBtn
|
||||||
|
:label="t('clearAll')"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
flat
|
||||||
|
@click="clearFiles()"
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
:label="t('uploadFiles')"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
flat
|
||||||
|
:disable="!isSubmitable"
|
||||||
|
@click="onSubmit(data)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VnForm>
|
||||||
|
</QPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
collection: Collection
|
||||||
|
updateMatching: Update items with matching id
|
||||||
|
dropYourFiles: Click or drop files here
|
||||||
|
clearAll: Clear all
|
||||||
|
uploadFiles: Upload files
|
||||||
|
uploadSuccess: Upload finished successfully
|
||||||
|
uploadError: Some errors happened on upload
|
||||||
|
noFilesToUpload: There are no files to upload
|
||||||
|
es-ES:
|
||||||
|
collection: Colección
|
||||||
|
updateMatching: Actualizar artículos con id coincidente
|
||||||
|
dropYourFiles: Pulsa o suelta los archivos aquí
|
||||||
|
clearAll: Limpiar todo
|
||||||
|
uploadFiles: Subir archivos
|
||||||
|
uploadSuccess: Imágenes subidas correctamente
|
||||||
|
uploadError: Ocurrieron errores al subir alguna de las imágenes
|
||||||
|
noFilesToUpload: No se han seleccionado archivos para subir
|
||||||
|
ca-ES:
|
||||||
|
collection: Col·lecció
|
||||||
|
updateMatching: Actualitzar els elements amb id coincident
|
||||||
|
dropYourFiles: Prem o deixa anar els arxius aquí
|
||||||
|
clearAll: Netejar tot
|
||||||
|
uploadFiles: Pujar arxius
|
||||||
|
uploadSuccess: Imatges pujades correctament
|
||||||
|
uploadError: Van ocórrer errors en pujar alguna de les imatges
|
||||||
|
noFilesToUpload: No s'ha seleccionat arxius per pujar
|
||||||
|
fr-FR:
|
||||||
|
collection: Collection
|
||||||
|
updateMatching: Mettre à jour les éléments avec l'identifiant correspondant
|
||||||
|
dropYourFiles: Cliquez ici ou déposer des fichiers
|
||||||
|
clearAll: Tout effacer
|
||||||
|
uploadFiles: Upload Files
|
||||||
|
uploadSuccess: Les images téléchargées correctement
|
||||||
|
uploadError: Des erreurs sont survenues lors du téléchargement des images
|
||||||
|
noFilesToUpload: Aucun fichier sélectionné pour télécharger
|
||||||
|
pt-PT:
|
||||||
|
collection: Coleção
|
||||||
|
updateMatching: Atualizar itens com id correspondente
|
||||||
|
dropYourFiles: Clique ou solte arquivos aqui
|
||||||
|
clearAll: Limpar tudo
|
||||||
|
uploadFiles: Fazer upload de arquivos
|
||||||
|
uploadSuccess: Upload concluído com sucesso
|
||||||
|
uploadError: Ocorreram erros ao subir alguma das imagens
|
||||||
|
noFilesToUpload: Não há arquivos selecionados para upload
|
||||||
|
</i18n>
|
|
@ -0,0 +1,120 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import CardList from 'src/components/ui/CardList.vue';
|
||||||
|
import VnSearchBar from 'src/components/ui/VnSearchBar.vue';
|
||||||
|
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useUserStore } from 'stores/user';
|
||||||
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
const { notify } = useNotify();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const users = ref([]);
|
||||||
|
|
||||||
|
const query = `SELECT u.id, u.name, u.nickname, u.active
|
||||||
|
FROM account.user u
|
||||||
|
WHERE u.name LIKE CONCAT('%', #user, '%')
|
||||||
|
OR u.nickname LIKE CONCAT('%', #user, '%')
|
||||||
|
OR u.id = #user
|
||||||
|
ORDER BY u.name LIMIT 200`;
|
||||||
|
|
||||||
|
const onSearch = data => (users.value = data || []);
|
||||||
|
|
||||||
|
const supplantUser = async user => {
|
||||||
|
try {
|
||||||
|
await userStore.supplantUser(user);
|
||||||
|
await appStore.getMenuLinks();
|
||||||
|
router.push({ name: 'confirmedOrders' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error supplanting user:', error);
|
||||||
|
notify(error.message, 'negative');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
|
<VnSearchBar
|
||||||
|
:sql-query="query"
|
||||||
|
search-field="user"
|
||||||
|
@on-search="onSearch"
|
||||||
|
@on-search-error="users = []"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<QPage class="vn-w-xs">
|
||||||
|
<QList class="flex justify-center">
|
||||||
|
<span v-if="!loading && !users.length" class="flex items-center">
|
||||||
|
<QIcon name="refresh" size="sm" class="q-mr-sm" />
|
||||||
|
{{ t('noData') }}
|
||||||
|
</span>
|
||||||
|
<QSpinner
|
||||||
|
v-if="loading"
|
||||||
|
color="primary"
|
||||||
|
size="3em"
|
||||||
|
:thickness="2"
|
||||||
|
/>
|
||||||
|
<CardList
|
||||||
|
v-else
|
||||||
|
v-for="(user, index) in users"
|
||||||
|
:key="index"
|
||||||
|
:clickable="false"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<span class="text-bold q-mb-sm">
|
||||||
|
{{ user.nickname }}
|
||||||
|
</span>
|
||||||
|
<span>#{{ user.id }} - {{ user.name }} </span>
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<QBtn
|
||||||
|
icon="people"
|
||||||
jsegarra
commented
tooltip tooltip
jsegarra
commented
053b9f845787404b46118f2a99ebacd3602f753e
|
|||||||
|
flat
|
||||||
|
rounded
|
||||||
|
@click="supplantUser(user.name)"
|
||||||
|
><QTooltip>
|
||||||
|
{{ t('Impersonate user') }}
|
||||||
|
</QTooltip></QBtn
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</CardList>
|
||||||
|
</QList>
|
||||||
|
</QPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
User management: User management
|
||||||
|
Disabled: Disabled
|
||||||
|
Impersonate user: Impersonate user
|
||||||
|
Access log: Access log
|
||||||
|
es-ES:
|
||||||
|
User management: Gestión de usuarios
|
||||||
|
Disabled: Desactivado
|
||||||
|
Impersonate user: Suplantar usuario
|
||||||
|
Access log: Registro de accesos
|
||||||
|
ca-ES:
|
||||||
|
User management: Gestió d'usuaris
|
||||||
|
Disabled: Deshabilitat
|
||||||
|
Impersonate user: Suplantar usuari
|
||||||
|
Access log: Registre d'accessos
|
||||||
|
fr-FR:
|
||||||
|
User management: Gestion des utilisateurs
|
||||||
|
Disabled: Désactivé
|
||||||
|
Impersonate user: Accès utilisateur
|
||||||
|
Access log: Journal des accès
|
||||||
|
pt-PT:
|
||||||
|
User management: Gestão de usuarios
|
||||||
|
Disabled: Desativado
|
||||||
|
Impersonate user: Suplantar usuario
|
||||||
|
Access log: Registro de acessos
|
||||||
|
</i18n>
|
|
@ -0,0 +1,176 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, inject, watch, computed } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import VnInputDate from 'src/components/common/VnInputDate.vue';
|
||||||
|
|
||||||
|
import { formatDateTitle, date } from 'src/lib/filters.js';
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const jApi = inject('jApi');
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const from = ref(Date.vnNew(route.query.from) || Date.vnNew());
|
||||||
|
const to = ref(Date.vnNew(route.query.to) || Date.vnNew());
|
||||||
|
const visitsData = ref(null);
|
||||||
|
|
||||||
|
const getVisits = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const [visitsResponse] = await jApi.query(
|
||||||
|
`SELECT browser,
|
||||||
|
MIN(CAST(version AS DECIMAL(4,1))) minVersion,
|
||||||
|
MAX(CAST(version AS DECIMAL(4,1))) maxVersion,
|
||||||
|
MAX(c.stamp) lastVisit,
|
||||||
|
COUNT(DISTINCT c.id) visits,
|
||||||
|
SUM(a.firstAccessFk = c.id AND v.firstAgentFk = a.id) newVisits
|
||||||
|
FROM visitUser e
|
||||||
|
JOIN visitAccess c ON c.id = e.accessFk
|
||||||
|
JOIN visitAgent a ON a.id = c.agentFk
|
||||||
|
JOIN visit v ON v.id = a.visitFk
|
||||||
|
WHERE c.stamp BETWEEN TIMESTAMP(#from,'00:00:00') AND TIMESTAMP(#to,'23:59:59')
|
||||||
|
GROUP BY browser ORDER BY visits DESC`,
|
||||||
|
{
|
||||||
|
from: date(from.value),
|
||||||
|
to: date(to.value)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
visitsData.value = visitsResponse;
|
||||||
|
loading.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting visits:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const visitsCardText = computed(
|
||||||
|
() =>
|
||||||
|
`${visitsData?.value?.visits || 0} ${t('visits')}, ${visitsData?.value?.newVisits || 0} ${t('news')}`
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => from.value, () => to.value],
|
||||||
|
async () => {
|
||||||
|
await router.replace({
|
||||||
|
query: {
|
||||||
|
from: date(from.value),
|
||||||
|
to: date(to.value)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await getVisits();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
|
<QBtn
|
||||||
|
:label="t('refresh')"
|
||||||
|
icon="refresh"
|
||||||
|
@click="getVisits()"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
class="q-mr-sm"
|
||||||
|
>
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('refresh') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<QBtn
|
||||||
|
:label="t('connections')"
|
||||||
|
icon="visibility"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
:to="{ name: 'adminConnections' }"
|
||||||
|
>
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('connections') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</Teleport>
|
||||||
|
<QPage class="vn-w-xs column">
|
||||||
|
<QCard class="column q-pa-lg q-mb-md">
|
||||||
|
<VnInputDate :label="t('from')" v-model="from" class="q-mb-sm" />
|
||||||
|
<VnInputDate :label="t('to')" v-model="to" />
|
||||||
|
</QCard>
|
||||||
|
<QCard v-if="!loading" class="q-pa-lg flex q-mb-md">
|
||||||
|
<span class="full-width text-right text-h6">
|
||||||
|
{{ visitsCardText }}
|
||||||
|
</span>
|
||||||
|
</QCard>
|
||||||
|
<QCard v-if="!loading" class="q-pa-lg column">
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
visitsData?.browser &&
|
||||||
|
visitsData?.minVersion &&
|
||||||
|
visitsData?.maxVersion
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ visitsData?.browser }} - {{ visitsData?.minVersion }} -
|
||||||
|
{{ visitsData?.maxVersion }}
|
||||||
|
</span>
|
||||||
|
<span>{{ visitsCardText }}</span>
|
||||||
|
<span v-if="visitsData">
|
||||||
|
{{
|
||||||
|
formatDateTitle(visitsData.lastVisit, {
|
||||||
|
showTime: true,
|
||||||
|
showSeconds: true,
|
||||||
|
shortDay: true
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</QCard>
|
||||||
|
<QSpinner
|
||||||
|
v-else
|
||||||
|
color="primary"
|
||||||
|
size="3em"
|
||||||
|
:thickness="2"
|
||||||
|
style="margin: 0 auto"
|
||||||
|
/>
|
||||||
|
</QPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
from: From
|
||||||
|
to: To
|
||||||
|
visits: Visits
|
||||||
|
news: New
|
||||||
|
connections: Connections
|
||||||
|
refresh: Refresh
|
||||||
|
es-ES:
|
||||||
|
from: Desde
|
||||||
|
to: Hasta
|
||||||
|
visits: Visitas
|
||||||
|
news: Nuevas
|
||||||
|
connections: Conexiones
|
||||||
|
refresh: Actualizar
|
||||||
|
ca-ES:
|
||||||
|
from: Desde
|
||||||
|
to: Fins
|
||||||
|
visits: Visites
|
||||||
|
news: Noves
|
||||||
|
connections: Connexions
|
||||||
|
refresh: Actualitzar
|
||||||
|
fr-FR:
|
||||||
|
from: À partir de
|
||||||
|
to: Jusqu'à
|
||||||
|
visits: Visites
|
||||||
|
news: Nouveau
|
||||||
|
connections: Connexions
|
||||||
|
refresh: Rafraîchir
|
||||||
|
pt-PT:
|
||||||
|
from: Desde
|
||||||
|
to: Até
|
||||||
|
visits: Visitas
|
||||||
|
news: Novo
|
||||||
|
connections: Conexões
|
||||||
|
refresh: Atualizar
|
||||||
|
</i18n>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="$actions">
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
<QInput
|
<QInput
|
||||||
:placeholder="$t('search')"
|
:placeholder="$t('search')"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
|
@ -11,10 +11,7 @@
|
||||||
standout
|
standout
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<QIcon
|
<QIcon v-if="search === ''" name="search" />
|
||||||
v-if="search === ''"
|
|
||||||
name="search"
|
|
||||||
/>
|
|
||||||
<QIcon
|
<QIcon
|
||||||
v-else
|
v-else
|
||||||
name="clear"
|
name="clear"
|
||||||
|
@ -32,11 +29,7 @@
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<div style="padding-bottom: 5em">
|
<div style="padding-bottom: 5em">
|
||||||
<QDrawer
|
<QDrawer v-model="$app.rightDrawerOpen" side="right" :width="250">
|
||||||
v-model="$app.rightDrawerOpen"
|
|
||||||
side="right"
|
|
||||||
:width="250"
|
|
||||||
>
|
|
||||||
<div class="q-pa-md">
|
<div class="q-pa-md">
|
||||||
<div class="basket-info">
|
<div class="basket-info">
|
||||||
<p>{{ date(new Date()) }}</p>
|
<p>{{ date(new Date()) }}</p>
|
||||||
|
@ -44,11 +37,7 @@
|
||||||
{{ $t('warehouse') }}
|
{{ $t('warehouse') }}
|
||||||
{{ 'Algemesi' }}
|
{{ 'Algemesi' }}
|
||||||
</p>
|
</p>
|
||||||
<QBtn
|
<QBtn flat rounded no-caps>
|
||||||
flat
|
|
||||||
rounded
|
|
||||||
no-caps
|
|
||||||
>
|
|
||||||
{{ $t('modify') }}
|
{{ $t('modify') }}
|
||||||
</QBtn>
|
</QBtn>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,14 +66,11 @@
|
||||||
:title="cat.name"
|
:title="cat.name"
|
||||||
:to="{ params: { category: cat.id, type: null } }"
|
:to="{ params: { category: cat.id, type: null } }"
|
||||||
>
|
>
|
||||||
<img :src="`statics/category/${cat.code}.svg`">
|
<img :src="`statics/category/${cat.code}.svg`" />
|
||||||
</QBtn>
|
</QBtn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="q-mt-md" v-if="category || search">
|
||||||
class="q-mt-md"
|
|
||||||
v-if="category || search"
|
|
||||||
>
|
|
||||||
<div class="q-mb-xs text-grey-7">
|
<div class="q-mb-xs text-grey-7">
|
||||||
{{ $t('filterBy') }}
|
{{ $t('filterBy') }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,15 +95,8 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="q-pa-md" v-if="typeId || search">
|
||||||
class="q-pa-md"
|
<div class="q-mb-md" v-for="tag in tags" :key="tag.uid">
|
||||||
v-if="typeId || search"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="q-mb-md"
|
|
||||||
v-for="tag in tags"
|
|
||||||
:key="tag.uid"
|
|
||||||
>
|
|
||||||
<div class="q-mb-xs text-caption text-grey-7">
|
<div class="q-mb-xs text-caption text-grey-7">
|
||||||
{{ tag.name }}
|
{{ tag.name }}
|
||||||
<QIcon
|
<QIcon
|
||||||
|
@ -187,11 +166,7 @@
|
||||||
:disable="disableScroll"
|
:disable="disableScroll"
|
||||||
>
|
>
|
||||||
<div class="q-pa-md row justify-center q-gutter-md">
|
<div class="q-pa-md row justify-center q-gutter-md">
|
||||||
<QSpinner
|
<QSpinner v-if="isLoading" color="primary" size="50px" />
|
||||||
v-if="isLoading"
|
|
||||||
color="primary"
|
|
||||||
size="50px"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
v-if="items && !items.length"
|
v-if="items && !items.length"
|
||||||
class="text-subtitle1 text-grey-7 q-pa-md"
|
class="text-subtitle1 text-grey-7 q-pa-md"
|
||||||
|
@ -204,12 +179,10 @@
|
||||||
>
|
>
|
||||||
{{ $t('pleaseSetFilter') }}
|
{{ $t('pleaseSetFilter') }}
|
||||||
</div>
|
</div>
|
||||||
<QCard
|
<QCard class="my-card" v-for="_item in items" :key="_item.id">
|
||||||
class="my-card"
|
<img
|
||||||
v-for="_item in items"
|
:src="`${$imageBase}/catalog/200x200/${_item.image}`"
|
||||||
:key="_item.id"
|
/>
|
||||||
>
|
|
||||||
<img :src="`${$imageBase}/catalog/200x200/${_item.image}`">
|
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
<div class="name text-subtitle1">
|
<div class="name text-subtitle1">
|
||||||
{{ _item.longName }}
|
{{ _item.longName }}
|
||||||
|
@ -220,10 +193,7 @@
|
||||||
{{ _item.subName }}
|
{{ _item.subName }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tags q-pt-xs">
|
<div class="tags q-pt-xs">
|
||||||
<div
|
<div v-for="tag in _item.tags" :key="tag.tagFk">
|
||||||
v-for="tag in _item.tags"
|
|
||||||
:key="tag.tagFk"
|
|
||||||
>
|
|
||||||
<span class="text-grey-7">{{
|
<span class="text-grey-7">{{
|
||||||
tag.tag.name
|
tag.tag.name
|
||||||
}}</span>
|
}}</span>
|
||||||
|
@ -252,11 +222,7 @@
|
||||||
</div>
|
</div>
|
||||||
<template #loading>
|
<template #loading>
|
||||||
<div class="row justify-center q-my-md">
|
<div class="row justify-center q-my-md">
|
||||||
<QSpinner
|
<QSpinner color="primary" name="dots" size="40px" />
|
||||||
color="primary"
|
|
||||||
name="dots"
|
|
||||||
size="40px"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</QInfiniteScroll>
|
</QInfiniteScroll>
|
||||||
|
@ -278,30 +244,19 @@
|
||||||
>
|
>
|
||||||
{{ item.subName }}
|
{{ item.subName }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-grey-7">
|
<div class="text-grey-7">#{{ item.id }}</div>
|
||||||
#{{ item.id }}
|
|
||||||
</div>
|
|
||||||
</QCardSection>
|
</QCardSection>
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
<div
|
<div v-for="tag in item.tags" :key="tag.tagFk">
|
||||||
v-for="tag in item.tags"
|
|
||||||
:key="tag.tagFk"
|
|
||||||
>
|
|
||||||
<span class="text-grey-7">{{ tag.tag.name }}</span>
|
<span class="text-grey-7">{{ tag.tag.name }}</span>
|
||||||
{{ tag.value }}
|
{{ tag.value }}
|
||||||
</div>
|
</div>
|
||||||
</QCardSection>
|
</QCardSection>
|
||||||
<QCardActions align="right">
|
<QCardActions align="right">
|
||||||
<QBtn
|
<QBtn @click="showItemDialog = false" flat>
|
||||||
@click="showItemDialog = false"
|
|
||||||
flat
|
|
||||||
>
|
|
||||||
{{ $t('cancel') }}
|
{{ $t('cancel') }}
|
||||||
</QBtn>
|
</QBtn>
|
||||||
<QBtn
|
<QBtn @click="showItemDialog = false" flat>
|
||||||
@click="showItemDialog = false"
|
|
||||||
flat
|
|
||||||
>
|
|
||||||
{{ $t('accept') }}
|
{{ $t('accept') }}
|
||||||
</QBtn>
|
</QBtn>
|
||||||
</QCardActions>
|
</QCardActions>
|
||||||
|
@ -391,6 +346,7 @@
|
||||||
import { date, currency, formatDate } from 'src/lib/filters.js';
|
import { date, currency, formatDate } from 'src/lib/filters.js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useAppStore } from 'stores/app';
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const CancelToken = axios.CancelToken;
|
const CancelToken = axios.CancelToken;
|
||||||
|
|
||||||
|
@ -398,7 +354,8 @@ export default {
|
||||||
name: 'HederaCatalog',
|
name: 'HederaCatalog',
|
||||||
setup() {
|
setup() {
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
return { appStore };
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
return { isHeaderMounted };
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -6,11 +6,14 @@ import VnTable from 'src/components/ui/VnTable.vue';
|
||||||
|
|
||||||
import { currency, formatDate } from 'src/lib/filters.js';
|
import { currency, formatDate } from 'src/lib/filters.js';
|
||||||
import { usePrintService } from 'src/composables/usePrintService';
|
import { usePrintService } from 'src/composables/usePrintService';
|
||||||
// import { date as qdate } from 'quasar';
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const jApi = inject('jApi');
|
const jApi = inject('jApi');
|
||||||
const { openReport } = usePrintService();
|
const { openReport } = usePrintService();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
const currentYear = ref(Date.vnNew().getFullYear());
|
const currentYear = ref(Date.vnNew().getFullYear());
|
||||||
const years = ref([]);
|
const years = ref([]);
|
||||||
|
@ -36,6 +39,7 @@ const columns = computed(() => [
|
||||||
name: 'amount',
|
name: 'amount',
|
||||||
label: t('amount'),
|
label: t('amount'),
|
||||||
field: 'amount',
|
field: 'amount',
|
||||||
|
align: 'right',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
format: val => currency(val)
|
format: val => currency(val)
|
||||||
},
|
},
|
||||||
|
@ -59,6 +63,7 @@ const fetchInvoices = async () => {
|
||||||
LIMIT 100`,
|
LIMIT 100`,
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
|
console.log(invoices.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
@ -70,7 +75,7 @@ onMounted(async () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="$actions">
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
<QSelect
|
<QSelect
|
||||||
v-model="currentYear"
|
v-model="currentYear"
|
||||||
:options="years"
|
:options="years"
|
||||||
|
@ -86,7 +91,7 @@ onMounted(async () => {
|
||||||
<VnTable
|
<VnTable
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="invoices"
|
:rows="invoices"
|
||||||
:hide-header="!invoices.length"
|
:hide-header="!invoices?.length"
|
||||||
>
|
>
|
||||||
<template #body-cell-hasPdf="{ row }">
|
<template #body-cell-hasPdf="{ row }">
|
||||||
<QTd
|
<QTd
|
||||||
|
|
|
@ -10,11 +10,15 @@ import VnConfirm from 'src/components/ui/VnConfirm.vue';
|
||||||
import useNotify from 'src/composables/useNotify.js';
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
import { currency, formatDateTitle } from 'src/lib/filters.js';
|
import { currency, formatDateTitle } from 'src/lib/filters.js';
|
||||||
import { tpvStore } from 'stores/tpv';
|
import { tpvStore } from 'stores/tpv';
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const jApi = inject('jApi');
|
const jApi = inject('jApi');
|
||||||
const { notify } = useNotify();
|
const { notify } = useNotify();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
const showAmountToPayDialog = ref(null);
|
const showAmountToPayDialog = ref(null);
|
||||||
const amountToPay = ref(null);
|
const amountToPay = ref(null);
|
||||||
|
@ -38,33 +42,25 @@ const onPayClick = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onConfirmPay = async () => {
|
const onConfirmPay = async () => {
|
||||||
if (amountToPay.value <= 0) {
|
if (!amountToPay.value || amountToPay.value <= 0) {
|
||||||
notify(t('amountError'), 'negative');
|
notify(t('amountError'), 'negative');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (amountToPay.value) {
|
|
||||||
const amount = amountToPay.value.toString().replace('.', ',');
|
const amount = amountToPay.value.toString().replace('.', ',');
|
||||||
amountToPay.value = parseFloat(amount);
|
amountToPay.value = parseFloat(amount);
|
||||||
await tpv.pay(amountToPay.value);
|
await tpv.pay(amountToPay.value);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="$actions">
|
<Teleport v-if="isHeaderMounted" 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"
|
|
||||||
:class="{ negative: debt < 0 }"
|
|
||||||
>
|
|
||||||
{{ currency(debt || 0) }}
|
{{ currency(debt || 0) }}
|
||||||
</span>
|
</span>
|
||||||
<QIcon
|
<QIcon name="info" class="info" size="sm">
|
||||||
name="info"
|
|
||||||
class="info"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<QTooltip max-width="450px">
|
<QTooltip max-width="450px">
|
||||||
{{ t('paymentInfo') }}
|
{{ t('paymentInfo') }}
|
||||||
</QTooltip>
|
</QTooltip>
|
||||||
|
|
|
@ -7,13 +7,15 @@ import CardList from 'src/components/ui/CardList.vue';
|
||||||
import { currency, formatDateTitle } from 'src/lib/filters.js';
|
import { currency, formatDateTitle } from 'src/lib/filters.js';
|
||||||
import { useVnConfirm } from 'src/composables/useVnConfirm.js';
|
import { useVnConfirm } from 'src/composables/useVnConfirm.js';
|
||||||
import useNotify from 'src/composables/useNotify.js';
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
import { useAppStore } from 'src/stores/app.js';
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const jApi = inject('jApi');
|
const jApi = inject('jApi');
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { openConfirmationModal } = useVnConfirm();
|
const { openConfirmationModal } = useVnConfirm();
|
||||||
const { notify } = useNotify();
|
const { notify } = useNotify();
|
||||||
const store = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const orders = ref([]);
|
const orders = ref([]);
|
||||||
|
@ -52,7 +54,7 @@ const removeOrder = async (id, index) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadOrder = orderId => {
|
const loadOrder = orderId => {
|
||||||
store.loadIntoBasket(orderId);
|
appStore.loadIntoBasket(orderId);
|
||||||
router.push({ name: 'catalog' });
|
router.push({ name: 'catalog' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -62,7 +64,7 @@ onMounted(async () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="$actions">
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
<QBtn
|
<QBtn
|
||||||
:to="{ name: 'checkout' }"
|
:to="{ name: 'checkout' }"
|
||||||
icon="add_shopping_cart"
|
icon="add_shopping_cart"
|
||||||
|
|
|
@ -5,12 +5,16 @@ import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import TicketDetails from 'src/pages/Ecomerce/TicketDetails.vue';
|
import TicketDetails from 'src/pages/Ecomerce/TicketDetails.vue';
|
||||||
|
|
||||||
import { userStore as useUserStore } from 'stores/user';
|
import { useUserStore } from 'stores/user';
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const jApi = inject('jApi');
|
const jApi = inject('jApi');
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
const ticket = ref({});
|
const ticket = ref({});
|
||||||
const rows = ref([]);
|
const rows = ref([]);
|
||||||
|
@ -46,7 +50,7 @@ const onPrintClick = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="$actions">
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
<QBtn
|
<QBtn
|
||||||
icon="print"
|
icon="print"
|
||||||
:label="t('printDeliveryNote')"
|
:label="t('printDeliveryNote')"
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { userStore } from 'stores/user';
|
import { useUserStore } from 'stores/user';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import useNotify from 'src/composables/useNotify.js';
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
const { notify } = useNotify();
|
const { notify } = useNotify();
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const user = userStore();
|
const userStore = useUserStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const email = ref(null);
|
const email = ref(null);
|
||||||
|
@ -29,7 +29,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
async function onLogin() {
|
async function onLogin() {
|
||||||
await user.login(email.value, password.value, remember.value);
|
await userStore.login(email.value, password.value, remember.value);
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -37,27 +37,13 @@ async function onLogin() {
|
||||||
<template>
|
<template>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<router-link
|
<router-link to="/" class="block">
|
||||||
to="/"
|
<img src="statics/logo.svg" alt="Verdnatura" class="block" />
|
||||||
class="block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="statics/logo.svg"
|
|
||||||
alt="Verdnatura"
|
|
||||||
class="block"
|
|
||||||
>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<QForm
|
<QForm @submit="onLogin" class="q-gutter-y-md">
|
||||||
@submit="onLogin"
|
|
||||||
class="q-gutter-y-md"
|
|
||||||
>
|
|
||||||
<div class="q-gutter-y-sm">
|
<div class="q-gutter-y-sm">
|
||||||
<QInput
|
<QInput v-model="email" :label="$t('user')" autofocus />
|
||||||
v-model="email"
|
|
||||||
:label="$t('user')"
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
<QInput
|
<QInput
|
||||||
v-model="password"
|
v-model="password"
|
||||||
:label="$t('password')"
|
:label="$t('password')"
|
||||||
|
@ -87,16 +73,8 @@ async function onLogin() {
|
||||||
rounded
|
rounded
|
||||||
>
|
>
|
||||||
<QMenu auto-close>
|
<QMenu auto-close>
|
||||||
<QList
|
<QList dense v-for="lang in langs" :key="lang">
|
||||||
dense
|
<QItem disabled v-ripple clickable>
|
||||||
v-for="lang in langs"
|
|
||||||
:key="lang"
|
|
||||||
>
|
|
||||||
<QItem
|
|
||||||
disabled
|
|
||||||
v-ripple
|
|
||||||
clickable
|
|
||||||
>
|
|
||||||
{{ $t(`langs.${lang}`) }}
|
{{ $t(`langs.${lang}`) }}
|
||||||
</QItem>
|
</QItem>
|
||||||
</QList>
|
</QList>
|
||||||
|
@ -127,10 +105,7 @@ async function onLogin() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="password-forgotten text-center q-mt-lg">
|
<p class="password-forgotten text-center q-mt-lg">
|
||||||
<router-link
|
<router-link to="/remember-password" class="link">
|
||||||
to="/remember-password"
|
|
||||||
class="link"
|
|
||||||
>
|
|
||||||
{{ $t('haveForgottenPassword') }}
|
{{ $t('haveForgottenPassword') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -88,6 +88,46 @@ const routes = [
|
||||||
name: 'addressDetails',
|
name: 'addressDetails',
|
||||||
path: '/account/address/:id?',
|
path: '/account/address/:id?',
|
||||||
component: () => import('pages/Account/AddressDetails.vue')
|
component: () => import('pages/Account/AddressDetails.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'controlPanel',
|
||||||
|
path: 'admin/links',
|
||||||
|
component: () => import('pages/Admin/LinksView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'adminUsers',
|
||||||
|
path: 'admin/users',
|
||||||
|
component: () => import('pages/Admin/UsersView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'adminConnections',
|
||||||
|
path: 'admin/connections',
|
||||||
|
component: () => import('pages/Admin/ConnectionsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'adminVisits',
|
||||||
|
path: 'admin/visits',
|
||||||
|
component: () => import('pages/Admin/VisitsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'adminNews',
|
||||||
|
path: 'news/news',
|
||||||
|
component: () => import('pages/Admin/NewsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'adminNewsDetails',
|
||||||
|
path: 'news/new/:id?',
|
||||||
|
component: () => import('pages/Admin/NewsDetails.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'adminPhotos',
|
||||||
|
path: 'admin/photos',
|
||||||
|
component: () => import('pages/Admin/PhotosView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'adminItems',
|
||||||
|
path: 'admin/items',
|
||||||
|
component: () => import('pages/Admin/ItemsView.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,11 +11,43 @@ export const useAppStore = defineStore('hedera', {
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
useRightDrawer: false,
|
useRightDrawer: false,
|
||||||
rightDrawerOpen: false,
|
rightDrawerOpen: false,
|
||||||
basketOrderId: null,
|
isHeaderMounted: false,
|
||||||
isHeaderMounted: false
|
menuEssentialLinks: [],
|
||||||
|
basketOrderId: null
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
async getMenuLinks() {
|
||||||
|
const sections = await 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.menuEssentialLinks = sectionTree;
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadConfig() {
|
||||||
|
const imageUrl = await jApi.getValue('SELECT url FROM imageConfig');
|
||||||
|
this.$patch({ imageUrl });
|
||||||
|
},
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
this.getBasketOrderId();
|
this.getBasketOrderId();
|
||||||
},
|
},
|
||||||
|
@ -24,11 +56,6 @@ export const useAppStore = defineStore('hedera', {
|
||||||
this.basketOrderId = localStorage.getItem('hederaBasket');
|
this.basketOrderId = localStorage.getItem('hederaBasket');
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadConfig() {
|
|
||||||
const imageUrl = await jApi.getValue('SELECT url FROM imageConfig');
|
|
||||||
this.$patch({ imageUrl });
|
|
||||||
},
|
|
||||||
|
|
||||||
async checkOrder(orderId) {
|
async checkOrder(orderId) {
|
||||||
try {
|
try {
|
||||||
const resultSet = await jApi.execQuery(
|
const resultSet = await jApi.execQuery(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
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 useUserStore = defineStore('user', {
|
||||||
state: () => {
|
state: () => {
|
||||||
const token =
|
const token =
|
||||||
sessionStorage.getItem('vnToken') ||
|
sessionStorage.getItem('vnToken') ||
|
||||||
|
@ -9,10 +9,9 @@ export const userStore = defineStore('user', {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token,
|
token,
|
||||||
id: null,
|
isGuest: false,
|
||||||
name: null,
|
user: null,
|
||||||
nickname: null,
|
supplantedUser: null
|
||||||
isGuest: false
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -21,6 +20,11 @@ export const userStore = defineStore('user', {
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
async getToken() {
|
||||||
|
this.token =
|
||||||
|
sessionStorage.getItem('vnToken') ||
|
||||||
|
localStorage.getItem('vnToken');
|
||||||
|
},
|
||||||
async login(user, password, remember) {
|
async login(user, password, remember) {
|
||||||
const params = { user, password };
|
const params = { user, password };
|
||||||
const res = await api.post('Accounts/login', params);
|
const res = await api.post('Accounts/login', params);
|
||||||
|
@ -36,7 +40,6 @@ export const userStore = defineStore('user', {
|
||||||
name: user
|
name: user
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
if (this.token != null) {
|
if (this.token != null) {
|
||||||
try {
|
try {
|
||||||
|
@ -48,16 +51,38 @@ export const userStore = defineStore('user', {
|
||||||
this.$reset();
|
this.$reset();
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadData() {
|
async fetchUser(userType = 'user') {
|
||||||
|
try {
|
||||||
const userData = await jApi.getObject(
|
const userData = await jApi.getObject(
|
||||||
'SELECT id, nickname, name FROM account.myUser'
|
'SELECT id, nickname, name FROM account.myUser'
|
||||||
);
|
);
|
||||||
|
this.$patch({ [userType]: userData });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user: ', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
this.$patch({
|
async supplantUser(supplantUser) {
|
||||||
id: userData.id,
|
const json = await jApi.send('client/supplant', {
|
||||||
nickname: userData.nickname,
|
supplantUser
|
||||||
name: userData.name
|
|
||||||
});
|
});
|
||||||
|
this.token = json;
|
||||||
|
sessionStorage.setItem('supplantUser', supplantUser);
|
||||||
|
await this.fetchUser('supplantedUser');
|
||||||
|
},
|
||||||
|
|
||||||
|
async supplantInit() {
|
||||||
|
const user = sessionStorage.getItem('supplantUser');
|
||||||
|
if (user == null) return;
|
||||||
|
await this.supplantUser(user);
|
||||||
|
},
|
||||||
|
|
||||||
|
async logoutSupplantedUser() {
|
||||||
|
sessionStorage.removeItem('supplantUser');
|
||||||
|
this.supplantedUser = null;
|
||||||
|
await api.post('Accounts/logout');
|
||||||
|
this.getToken();
|
||||||
|
await this.fetchUser();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Está 2 veces
053b9f8457