Modulo Administración #78

Merged
jsegarra merged 19 commits from wbuezas/hedera-web-mindshore:feature/Administracion into 4922-vueMigration 2024-08-23 19:29:46 +00:00
42 changed files with 2481 additions and 615 deletions

View File

@ -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']
} }
] ]
}; };

10
.vscode/settings.json vendored
View File

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

View File

@ -14,249 +14,243 @@ const path = require('path');
const { configure } = require('quasar/wrappers'); const { configure } = require('quasar/wrappers');
module.exports = configure(function (ctx) { module.exports = configure(function (ctx) {
return { return {
// fix: true, // https://v2.quasar.dev/quasar-cli-webpack/supporting-ts
// include = [], supportTS: false,
// exclude = [],
// rawOptions = {},
warnings: true,
errors: true,
// https://v2.quasar.dev/quasar-cli-webpack/supporting-ts
supportTS: false,
// https://v2.quasar.dev/quasar-cli-webpack/prefetch-feature // https://v2.quasar.dev/quasar-cli-webpack/prefetch-feature
// preFetch: true, // preFetch: true,
// app boot file (/src/boot) // app boot file (/src/boot)
// --> boot files are part of "main.js" // --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-webpack/boot-files // https://v2.quasar.dev/quasar-cli-webpack/boot-files
boot: ['i18n', 'axios', 'vnDate', 'error-handler', 'app'], boot: ['i18n', 'axios', 'vnDate', 'error-handler', 'app'],
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
css: ['app.scss', 'width.scss', 'responsive.scss'], css: ['app.scss', 'width.scss', 'responsive.scss'],
// https://github.com/quasarframework/quasar/tree/dev/extras // https://github.com/quasarframework/quasar/tree/dev/extras
extras: [ extras: [
// 'ionicons-v4', // 'ionicons-v4',
// 'mdi-v5', // 'mdi-v5',
// 'fontawesome-v6', // 'fontawesome-v6',
// 'eva-icons', // 'eva-icons',
// 'themify', // 'themify',
// 'line-awesome', // 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
'roboto-font', // optional, you are not bound to it 'roboto-font', // optional, you are not bound to it
'material-icons' // optional, you are not bound to it 'material-icons' // optional, you are not bound to it
], ],
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-build // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-build
build: { build: {
vueRouterMode: 'hash', // available values: 'hash', 'history' vueRouterMode: 'hash', // available values: 'hash', 'history'
// transpile: false, // transpile: false,
// publicPath: '/', // publicPath: '/',
// Add dependencies for transpiling with Babel (Array of string/regex) // Add dependencies for transpiling with Babel (Array of string/regex)
// (from node_modules, which are by default not transpiled). // (from node_modules, which are by default not transpiled).
// Applies only if "transpile" is set to true. // Applies only if "transpile" is set to true.
// transpileDependencies: [], // transpileDependencies: [],
// rtl: true, // https://quasar.dev/options/rtl-support // rtl: true, // https://quasar.dev/options/rtl-support
// preloadChunks: true, // preloadChunks: true,
// showProgress: false, // showProgress: false,
// gzip: true, // gzip: true,
// analyze: true, // analyze: true,
// Options below are automatically set depending on the env, set them if you want to override // Options below are automatically set depending on the env, set them if you want to override
// extractCSS: false, // extractCSS: false,
// 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'] }]);
chain.module chain.module
.rule('i18n-resource') .rule('i18n-resource')
.test(/\.(json5?|ya?ml)$/) .test(/\.(json5?|ya?ml)$/)
.include.add(path.resolve(__dirname, './src/i18n')) .include.add(path.resolve(__dirname, './src/i18n'))
.end() .end()
.type('javascript/auto') .type('javascript/auto')
.use('i18n-resource') .use('i18n-resource')
.loader('@intlify/vue-i18n-loader'); .loader('@intlify/vue-i18n-loader');
chain.module chain.module
.rule('i18n') .rule('i18n')
.resourceQuery(/blockType=i18n/) .resourceQuery(/blockType=i18n/)
.type('javascript/auto') .type('javascript/auto')
.use('i18n') .use('i18n')
.loader('@intlify/vue-i18n-loader'); .loader('@intlify/vue-i18n-loader');
} }
}, },
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-devServer // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-devServer
devServer: { devServer: {
server: { server: {
type: 'http' type: 'http'
}, },
port: 8080, port: 8080,
open: false, open: false,
// static: __dirname, // static: __dirname,
headers: { 'Access-Control-Allow-Origin': '*' }, headers: { 'Access-Control-Allow-Origin': '*' },
// stats: { chunks: false }, // stats: { chunks: false },
proxy: { proxy: {
'/api': 'http://localhost:3000', '/api': 'http://localhost:3000',
'/': { '/': {
target: 'http://localhost:3001', target: 'http://localhost:3002',
bypass: req => (req.path !== '/' ? req.path : null) bypass: req => (req.path !== '/' ? req.path : null)
}
}
},
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-framework
framework: {
config: {},
autoImportComponentCase: 'pascal',
// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples),
// you can manually specify Quasar components/directives to be available everywhere:
//
// components: [],
// directives: [],
// Quasar plugins
plugins: ['Notify', 'Dialog']
},
// animations: 'all', // --- includes all animations
// https://quasar.dev/options/animations
animations: [],
// https://v2.quasar.dev/quasar-cli-webpack/developing-ssr/configuring-ssr
ssr: {
pwa: false,
// manualStoreHydration: true,
// manualPostHydrationTrigger: true,
prodPort: 3000, // The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
maxAge: 1000 * 60 * 60 * 24 * 30,
// Tell browser when a file from the server should expire from cache (in ms)
chainWebpackWebserver(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
middlewares: [
ctx.prod ? 'compression' : '',
'render' // keep this as last one
]
},
// https://v2.quasar.dev/quasar-cli-webpack/developing-pwa/configuring-pwa
pwa: {
workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
workboxOptions: {}, // only for GenerateSW
// for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts])
// if using workbox in InjectManifest mode
chainWebpackCustomSW(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
manifest: {
name: 'Hedera',
short_name: 'Hedera',
description: "Verdnatura's webshop",
display: 'standalone',
orientation: 'portrait',
background_color: '#ffffff',
theme_color: '#027be3',
icons: [
{
src: 'icons/icon-128x128.png',
sizes: '128x128',
type: 'image/png'
},
{
src: 'icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'icons/icon-256x256.png',
sizes: '256x256',
type: 'image/png'
},
{
src: 'icons/icon-384x384.png',
sizes: '384x384',
type: 'image/png'
},
{
src: 'icons/icon-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-cordova-apps/configuring-cordova
cordova: {
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-electron-apps/configuring-electron
electron: {
bundler: 'packager', // 'packager' or 'builder'
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
builder: {
// https://www.electron.build/configuration/configuration
appId: 'hedera-web'
},
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpackMain(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
chainWebpackPreload(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
}
} }
} };
},
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-framework
framework: {
config: {},
autoImportComponentCase: 'pascal',
// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples),
// you can manually specify Quasar components/directives to be available everywhere:
//
// components: [],
// directives: [],
// Quasar plugins
plugins: ['Notify', 'Dialog']
},
// animations: 'all', // --- includes all animations
// https://quasar.dev/options/animations
animations: [],
// https://v2.quasar.dev/quasar-cli-webpack/developing-ssr/configuring-ssr
ssr: {
pwa: false,
// manualStoreHydration: true,
// manualPostHydrationTrigger: true,
prodPort: 3000, // The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
maxAge: 1000 * 60 * 60 * 24 * 30,
// Tell browser when a file from the server should expire from cache (in ms)
chainWebpackWebserver (chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
middlewares: [
ctx.prod ? 'compression' : '',
'render' // keep this as last one
]
},
// https://v2.quasar.dev/quasar-cli-webpack/developing-pwa/configuring-pwa
pwa: {
workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
workboxOptions: {}, // only for GenerateSW
// for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts])
// if using workbox in InjectManifest mode
chainWebpackCustomSW (chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
manifest: {
name: 'Hedera',
short_name: 'Hedera',
description: "Verdnatura's webshop",
display: 'standalone',
orientation: 'portrait',
background_color: '#ffffff',
theme_color: '#027be3',
icons: [
{
src: 'icons/icon-128x128.png',
sizes: '128x128',
type: 'image/png'
},
{
src: 'icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'icons/icon-256x256.png',
sizes: '256x256',
type: 'image/png'
},
{
src: 'icons/icon-384x384.png',
sizes: '384x384',
type: 'image/png'
},
{
src: 'icons/icon-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-cordova-apps/configuring-cordova
cordova: {
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-electron-apps/configuring-electron
electron: {
bundler: 'packager', // 'packager' or 'builder'
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
builder: {
// https://www.electron.build/configuration/configuration
appId: 'hedera-web'
},
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpackMain (chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
chainWebpackPreload (chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
}
}
};
}); });

View File

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

View File

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

View File

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

View File

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

View File

@ -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="content">
<div class="column full-width"> <slot name="content" />
<slot name="content" />
</div>
</div> </div>
</QItemSection> </div>
<QItemSection <div class="no-padding flex full-width justify-center">
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>

View File

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

View File

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

View File

@ -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>
<QImg <div class="relative-position main-image-container">
:class="{ zoomIn: props.zoomSize, rounded: props.rounded }" <QBtn
:src="url" v-if="props.editable"
v-bind="$attrs" icon="add_a_photo"
@click="show = !show" class="show-edit-button absolute-top-left"
spinner-color="primary" :class="{ hide: !props.alwaysShowEditButton }"
/> round
<QDialog text-color="black"
v-model="show" @click.stop.prevent="showEditForm = !showEditForm"
v-if="props.zoomSize" >
> <QTooltip>{{ t('addOrEditImage') }}</QTooltip>
</QBtn>
<QImg
:class="{
zoomIn: props.zoomSize,
rounded: props.rounded,
'full-rounded': props.fullRounded
}"
class="main-image"
:src="url"
v-bind="$attrs"
@click="showZoom = !showZoom"
spinner-color="primary"
:width="props.width"
:height="props.height"
>
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(', ')
createModelDefault.value, : columnsUpdated.join(', ');
...columnsUpdated.map(colName => sanitizeValue(formData[colName]))
].join(', ');
const values = createModelDefault.value
? [
createModelDefault.value,
...columnsUpdated.map(colName => sanitizeValue(formData[colName]))
].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});

View File

@ -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
icon="logout"
alt="_Exit"
@click="logout()"
/>
</div> </div>
<div <div v-if="supplantedUser" id="supplant" class="supplant">
id="supplant" <span id="supplanted">
class="supplant" {{ supplantedUser?.nickname }}
> </span>
<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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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="[
[
{
Review

Está 2 veces

Está 2 veces
Review
053b9f845787404b46118f2a99ebacd3602f753e
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>

View File

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

View File

@ -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 arquivos selecionados para upload
</i18n>

View File

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

tooltip

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

View File

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

En esta linea y la 62, asi como la contigua se repite la lógica.

Nos juntamos para centralizar/refactorizar

En esta linea y la 62, asi como la contigua se repite la lógica. Nos juntamos para centralizar/refactorizar
5456db8addc3a2f5f377ce8deecdea250eaaf8dc
}
);
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>

View File

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

View File

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

View File

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

View File

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

View File

@ -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')"

View File

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

View File

@ -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')
} }
] ]
}, },

View File

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

View File

@ -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') {
const userData = await jApi.getObject( try {
'SELECT id, nickname, name FROM account.myUser' const userData = await jApi.getObject(
); '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();
} }
} }
}); });