Compare commits

..

No commits in common. "dev" and "fix_vnselect_scroll" have entirely different histories.

373 changed files with 4569 additions and 11320 deletions

View File

@ -26,7 +26,7 @@ if (branchName) {
const splitedMsg = msg.split(':');
if (splitedMsg.length > 1) {
const finalMsg = `${splitedMsg[0]}: ${referenceTag}${splitedMsg.slice(1).join(':')}`;
const finalMsg = splitedMsg[0] + ': ' + referenceTag + splitedMsg.slice(1).join(':');
writeFileSync(msgPath, finalMsg);
}
}

87
Jenkinsfile vendored
View File

@ -108,6 +108,7 @@ pipeline {
}
stage('E2E') {
environment {
CREDS = credentials('docker-registry')
COMPOSE_PROJECT = "${PROJECT_NAME}-${env.BUILD_ID}"
COMPOSE_PARAMS = "-p ${env.COMPOSE_PROJECT} -f test/cypress/docker-compose.yml --project-directory ."
}
@ -117,24 +118,23 @@ pipeline {
sh 'rm -rf test/cypress/screenshots'
env.COMPOSE_TAG = PROTECTED_BRANCH.contains(env.CHANGE_TARGET) ? env.CHANGE_TARGET : 'dev'
def modules = sh(
script: "node test/cypress/docker/find/find.js ${env.COMPOSE_TAG}",
returnStdout: true
).trim()
echo "E2E MODULES: ${modules}"
def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs')
script {
def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs')
sh "docker compose ${env.COMPOSE_PARAMS} up -d"
image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") {
sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'"
}
sh 'docker login --username $CREDS_USR --password $CREDS_PSW $REGISTRY'
sh "docker-compose ${env.COMPOSE_PARAMS} pull back"
sh "docker-compose ${env.COMPOSE_PARAMS} pull db"
sh "docker-compose ${env.COMPOSE_PARAMS} up -d"
def modules = sh(script: "node test/cypress/docker/find/find.js ${env.COMPOSE_TAG}", returnStdout: true).trim()
echo "E2E MODULES: ${modules}"
image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") {
sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'"
}
}
}
post {
always {
sh "docker compose ${env.COMPOSE_PARAMS} down -v"
sh "docker-compose ${env.COMPOSE_PARAMS} down -v"
archiveArtifacts artifacts: 'test/cypress/screenshots/**/*', allowEmptyArchive: true
junit(
testResults: 'junit/e2e-*.xml',
@ -153,8 +153,17 @@ pipeline {
VERSION = readFile 'VERSION.txt'
}
steps {
sh 'quasar build'
dockerBuild 'salix-frontend', '.'
script {
sh 'quasar build'
def baseImage = "salix-frontend:${env.VERSION}"
def image = docker.build(baseImage, ".")
docker.withRegistry("https://${env.REGISTRY}", 'docker-registry') {
image.push()
image.push(env.BRANCH_NAME)
if (IS_LATEST) image.push('latest')
}
}
}
}
stage('Deploy') {
@ -177,53 +186,3 @@ pipeline {
}
}
def dockerBuild(imageName, context, dockerfile = null) {
if (dockerfile == null)
dockerfile = "${context}/Dockerfile"
def certDir = '/home/jenkins/.buildkit/certs'
def buildxArgs = [
"--name=buildkitd",
"--driver=remote",
"--driver-opt="
+ "cacert=${certDir}/ca.pem,"
+ "cert=${certDir}/cert.pem,"
+ "key=${certDir}/key.pem,"
+ "servername=buildkitd",
"tcp://buildkitd:1234"
]
def cacheImage = "${env.REGISTRY_CACHE}/${imageName}"
def pushImage = "${env.REGISTRY}/${imageName}"
def baseImage = "${pushImage}:${env.VERSION}"
def buildArgs = [
context,
"--push",
"--builder=buildkitd",
"--file=${dockerfile}",
"--cache-from=type=registry,ref=${cacheImage}:cache-${env.BRANCH_NAME}",
"--cache-from=type=registry,ref=${cacheImage}:cache-dev",
"--cache-to=type=registry,ref=${cacheImage}:cache-${env.BRANCH_NAME},mode=max",
"--tag=${pushImage}:${env.BRANCH_NAME}"
]
def isLatest = ['master', 'main'].contains(env.BRANCH_NAME)
if (isLatest)
buildArgs.push("--tag=${pushImage}:latest")
// FIXME: Nested docker.withRegistry() does not work
// https://issues.jenkins.io/browse/JENKINS-59777
withCredentials([usernamePassword(
credentialsId: 'registry-cache',
usernameVariable: 'CACHE_USR',
passwordVariable: 'CACHE_PSW')
]) {
docker.withRegistry("https://${env.REGISTRY}", 'docker-registry') {
sh 'echo "$CACHE_PSW" | docker login --username "$CACHE_USR" --password-stdin "http://$REGISTRY_CACHE"'
sh "docker buildx create ${buildxArgs.join(' ')}"
docker.build(baseImage, buildArgs.join(' '))
}
}
}

View File

@ -1,9 +1,6 @@
import { defineConfig } from 'cypress';
let urlHost;
let reporter;
let reporterOptions;
let timeouts;
let urlHost, reporter, reporterOptions, timeouts;
if (process.env.CI) {
urlHost = 'front';
@ -64,6 +61,5 @@ export default defineConfig({
...timeouts,
includeShadowDom: true,
waitForAnimations: true,
testIsolation: false,
},
});

View File

@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<title><%= productName %></title>
@ -12,12 +12,7 @@
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"
/>
<link
rel="icon"
type="image/png"
sizes="128x128"
href="icons/favicon-128x128.png"
/>
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png" />
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png" />
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" />

View File

@ -1,6 +1,6 @@
{
"name": "salix-front",
"version": "25.22.0",
"version": "25.18.0",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",
@ -89,4 +89,4 @@
"vite": "^6.0.11",
"vitest": "^0.31.1"
}
}
}

View File

@ -1,3 +1,4 @@
/* eslint-disable */
// https://github.com/michael-ciniawsky/postcss-load-config
import autoprefixer from 'autoprefixer';

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -13,7 +13,7 @@ import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
import path from 'path';
const target = `http://${process.env.CI ? 'back' : 'localhost'}:3000`;
export default configure((/* ctx */) => {
export default configure(function (/* ctx */) {
return {
eslint: {
// fix: true,

View File

@ -0,0 +1,227 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* 1. DO NOT edit this file directly as it won't do anything.
* 2. EDIT the original quasar.config file INSTEAD.
* 3. DO NOT git commit this file. It should be ignored.
*
* This file is still here because there was an error in
* the original quasar.config file and this allows you to
* investigate the Node.js stack error.
*
* After you fix the original file, this file will be
* deleted automatically.
**/
// quasar.config.js
import { configure } from "quasar/wrappers";
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
import path from "path";
var __quasar_inject_dirname__ = "/home/jsegarra/Projects/salix-front";
var target = `http://${process.env.CI ? "back" : "localhost"}:3000`;
var quasar_config_default = configure(function() {
return {
eslint: {
// fix: true,
// include = [],
// exclude = [],
// rawOptions = {},
warnings: true,
errors: true
},
// https://v2.quasar.dev/quasar-cli/prefetch-feature
// preFetch: true,
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files
boot: ["i18n", "axios", "vnDate", "validations", "quasar", "quasar.defaults"],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ["app.scss"],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
// 'mdi-v5',
// 'fontawesome-v6',
// 'eva-icons',
// 'themify',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
"roboto-font",
"material-icons-outlined",
"material-symbols-outlined"
],
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
build: {
target: {
browser: ["es2022", "edge88", "firefox78", "chrome87", "safari13.1"],
node: "node20"
},
vueRouterMode: "hash",
// available values: 'hash', 'history'
// vueRouterBase,
// vueDevtools,
// vueOptionsAPI: false,
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
// publicPath: '/',
// analyze: true,
// env: {},
rawDefine: {
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV)
},
// ignorePublicFolder: true,
// minify: false,
// polyfillModulePreload: true,
// distDir
extendViteConf(viteConf) {
delete viteConf.build.polyfillModulePreload;
viteConf.build.modulePreload = {
polyfill: false
};
},
// viteVuePluginOptions: {},
alias: {
composables: path.join(__quasar_inject_dirname__, "./src/composables"),
filters: path.join(__quasar_inject_dirname__, "./src/filters")
},
vitePlugins: [
[
VueI18nPlugin({
strictMessage: false,
runtimeOnly: false,
include: [
path.resolve(__quasar_inject_dirname__, "./src/i18n/locale/**"),
path.resolve(__quasar_inject_dirname__, "./src/pages/**/locale/**")
]
})
]
]
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
server: {
type: "http"
},
proxy: {
"/api": {
target,
logLevel: "debug",
changeOrigin: true,
secure: false
}
},
open: false,
allowedHosts: [
"front",
// Agrega este nombre de host
"localhost"
// Opcional, para pruebas locales
]
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
framework: {
config: {
config: {
dark: "auto"
}
},
lang: "en-GB",
// 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"],
all: "auto",
autoImportComponentCase: "pascal"
},
// animations: 'all', // --- includes all animations
// https://v2.quasar.dev/options/animations
animations: [],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
// sourceFiles: {
// rootComponent: 'src/App.vue',
// router: 'src/router/index',
// store: 'src/store/index',
// registerServiceWorker: 'src-pwa/register-service-worker',
// serviceWorker: 'src-pwa/custom-service-worker',
// pwaManifestFile: 'src-pwa/manifest.json',
// electronMain: 'src-electron/electron-main',
// electronPreload: 'src-electron/electron-preload'
// },
// https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr
ssr: {
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
// will mess up SSR
// extendSSRWebserverConf (esbuildConf) {},
// extendPackageJson (json) {},
pwa: false,
// manualStoreHydration: true,
// manualPostHydrationTrigger: true,
prodPort: 3e3,
// The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
middlewares: [
"render"
// keep this as last one
]
},
// https://v2.quasar.dev/quasar-cli/developing-pwa/configuring-pwa
pwa: {
workboxMode: "generateSW",
// or 'injectManifest'
injectPwaMetaTags: true,
swFilename: "sw.js",
manifestFilename: "manifest.json",
useCredentialsForManifestTag: false
// useFilenameHashes: true,
// extendGenerateSWOptions (cfg) {}
// extendInjectManifestOptions (cfg) {},
// extendManifestJson (json) {}
// extendPWACustomSWConf (esbuildConf) {}
},
// Full list of options: https://v2.quasar.dev/quasar-cli/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/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
},
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
electron: {
// extendElectronMainConf (esbuildConf)
// extendElectronPreloadConf (esbuildConf)
inspectPort: 5858,
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: "salix-frontend"
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
bex: {
contentScripts: ["my-content-script"]
// extendBexScriptsConf (esbuildConf) {}
// extendBexManifestJson (json) {}
}
};
});
export {
quasar_config_default as default
};

View File

@ -1,6 +1,8 @@
{
"@quasar/testing-unit-vitest": {
"options": ["scripts"]
"options": [
"scripts"
]
},
"@quasar/qcalendar": {}
}

View File

@ -1,5 +1,5 @@
{
"unit-vitest": {
"runnerCommand": "vitest run"
}
}
"unit-vitest": {
"runnerCommand": "vitest run"
}
}

View File

@ -39,7 +39,7 @@ quasar.iconMapFn = (iconName) => {
<template>
<RouterView />
<VnScroll />
<VnScroll/>
</template>
<style lang="scss">

View File

@ -11,8 +11,8 @@ export default function (component, key, value) {
};
break;
case 'undefined':
throw new Error(`unknown prop: ${key}`);
throw new Error('unknown prop: ' + key);
default:
throw new Error(`unhandled type: ${typeof prop}`);
throw new Error('unhandled type: ' + typeof prop);
}
}

View File

@ -9,7 +9,7 @@ export default {
const keyBindingMap = routes
.filter((route) => route.meta.keyBinding)
.reduce((map, route) => {
map[`Key${route.meta.keyBinding.toUpperCase()}`] = route.path;
map['Key' + route.meta.keyBinding.toUpperCase()] = route.path;
return map;
}, {});

View File

@ -1,3 +1,4 @@
/* eslint-disable eslint/export */
export * from './defaults/qTable';
export * from './defaults/qInput';
export * from './defaults/qSelect';

View File

@ -7,7 +7,7 @@ import { QLayout } from 'quasar';
import mainShortcutMixin from './mainShortcutMixin';
import { useCau } from 'src/composables/useCau';
export default boot(({ app, router }) => {
export default boot(({ app }) => {
QForm.mixins = [qFormMixin];
QLayout.mixins = [mainShortcutMixin];
@ -22,19 +22,14 @@ export default boot(({ app, router }) => {
}
switch (response?.status) {
case 401:
if (!router.currentRoute.value.name.toLowerCase().includes('login')) {
message = 'errors.sessionExpired';
} else message = 'login.loginError';
break;
case 403:
if (!message || message.toLowerCase() === 'access denied')
message = 'errors.accessDenied';
case 422:
if (error.name == 'ValidationError')
message += ` "${responseError.details.context}.${Object.keys(
responseError.details.codes,
).join(',')}"`;
message +=
' "' +
responseError.details.context +
'.' +
Object.keys(responseError.details.codes).join(',') +
'"';
break;
case 500:
message = 'errors.statusInternalServerError';

View File

@ -25,7 +25,7 @@ const autonomiesRef = ref([]);
const onDataSaved = (dataSaved, requestResponse) => {
requestResponse.autonomy = autonomiesRef.value.opts.find(
(autonomy) => autonomy.id == requestResponse.autonomyFk,
(autonomy) => autonomy.id == requestResponse.autonomyFk
);
emit('onDataSaved', dataSaved, requestResponse);
};

View File

@ -1,6 +1,6 @@
<script setup>
import axios from 'axios';
import { computed, ref, useAttrs, watch, nextTick } from 'vue';
import { computed, ref, useAttrs, watch } from 'vue';
import { useRouter, onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
@ -42,15 +42,7 @@ const $props = defineProps({
},
dataRequired: {
type: Object,
default: () => ({}),
},
dataDefault: {
type: Object,
default: () => ({}),
},
insertOnLoad: {
type: Boolean,
default: false,
default: () => {},
},
defaultSave: {
type: Boolean,
@ -95,7 +87,6 @@ const formData = ref();
const saveButtonRef = ref(null);
const watchChanges = ref();
const formUrl = computed(() => $props.url);
const rowsContainer = ref(null);
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
@ -131,7 +122,6 @@ async function fetch(data) {
const rows = keyData ? data[keyData] : data;
resetData(rows);
emit('onFetch', rows);
$props.insertOnLoad && (await insert());
return rows;
}
@ -145,23 +135,9 @@ function resetData(data) {
formData.value = JSON.parse(JSON.stringify(data));
if (watchChanges.value) watchChanges.value(); //destroy watcher
watchChanges.value = watch(
formData,
(nVal) => {
hasChanges.value = false;
const filteredNewData = nVal.filter(
(row) => !isRowEmpty(row) || row[$props.primaryKey],
);
const filteredOriginal = originalData.value.filter(
(row) => row[$props.primaryKey],
);
const changes = getDifferences(filteredOriginal, filteredNewData);
hasChanges.value = !isEmpty(changes);
},
{ deep: true },
);
watchChanges.value = watch(formData, () => (hasChanges.value = true), { deep: true });
}
async function reset() {
await fetch(originalData.value);
hasChanges.value = false;
@ -189,7 +165,6 @@ async function onSubmit() {
});
}
isLoading.value = true;
await saveChanges($props.saveFn ? formData.value : null);
}
@ -199,10 +174,6 @@ async function onSubmitAndGo() {
}
async function saveChanges(data) {
formData.value = formData.value.filter(
(row) => row[$props.primaryKey] || !isRowEmpty(row),
);
if ($props.saveFn) {
$props.saveFn(data, getChanges);
isLoading.value = false;
@ -232,28 +203,12 @@ async function saveChanges(data) {
});
}
async function insert(pushData = { ...$props.dataRequired, ...$props.dataDefault }) {
formData.value = formData.value.filter((row) => !isRowEmpty(row));
const lastRow = formData.value.at(-1);
const isLastRowEmpty = lastRow ? isRowEmpty(lastRow) : false;
if (formData.value.length && isLastRowEmpty) return;
const $index = formData.value.length ? formData.value.at(-1).$index + 1 : 0;
const nRow = Object.assign({ $index }, pushData);
formData.value.push(nRow);
const hasChange = Object.keys(nRow).some((key) => !isChange(nRow, key));
if (hasChange) hasChanges.value = true;
}
function isRowEmpty(row) {
return Object.keys(row).every((key) => isChange(row, key));
}
function isChange(row, key) {
return !row[key] || key == '$index' || Object.hasOwn($props.dataRequired || {}, key);
async function insert(pushData = $props.dataRequired) {
const $index = formData.value.length
? formData.value[formData.value.length - 1].$index + 1
: 0;
formData.value.push(Object.assign({ $index }, pushData));
hasChanges.value = true;
}
async function remove(data) {
@ -272,10 +227,10 @@ async function remove(data) {
newData = newData.filter(
(form) => !preRemove.some((index) => index == form.$index),
);
formData.value = newData;
hasChanges.value =
JSON.stringify(removeIndexField(formData.value)) !==
JSON.stringify(removeIndexField(originalData.value));
const changes = getChanges();
if (!changes.creates?.length && !changes.updates?.length)
hasChanges.value = false;
fetch(newData);
}
if (ids.length) {
quasar
@ -291,10 +246,11 @@ async function remove(data) {
})
.onOk(async () => {
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
await reload();
fetch(newData);
});
} else {
reset();
}
emit('update:selected', []);
}
@ -305,7 +261,7 @@ function getChanges() {
const pk = $props.primaryKey;
for (const [i, row] of formData.value.entries()) {
if (!row[pk]) {
creates.push(Object.assign(row, { ...$props.dataRequired }));
creates.push(row);
} else if (originalData.value[i]) {
const data = getDifferences(originalData.value[i], row);
if (!isEmpty(data)) {
@ -331,33 +287,6 @@ function isEmpty(obj) {
return !Object.keys(obj).length;
}
function removeIndexField(data) {
if (Array.isArray(data)) {
return data.map(({ $index, ...rest }) => rest);
} else if (typeof data === 'object' && data !== null) {
const { $index, ...rest } = data;
return rest;
}
}
async function handleTab(event) {
event.preventDefault();
const { shiftKey, target } = event;
const focusableSelector = `tbody tr td:not(:first-child) :is(a, button, input, textarea, select, details):not([disabled])`;
const focusableElements = rowsContainer.value?.querySelectorAll(focusableSelector);
const currentIndex = Array.prototype.indexOf.call(focusableElements, target);
const index = shiftKey ? currentIndex - 1 : currentIndex + 1;
const isLast = target === focusableElements[focusableElements.length - 1];
const isFirst = currentIndex === 0;
if ((shiftKey && !isFirst) || (!shiftKey && !isLast))
focusableElements[index]?.focus();
else if (isLast) {
await insert();
await nextTick();
}
}
async function reload(params) {
const data = await vnPaginateRef.value.fetch(params);
fetch(data);
@ -383,14 +312,12 @@ watch(formUrl, async () => {
v-bind="$attrs"
>
<template #body v-if="formData">
<div ref="rowsContainer" @keydown.tab="handleTab">
<slot
name="body"
:rows="formData"
:validate="validate"
:filter="filter"
></slot>
</div>
<slot
name="body"
:rows="formData"
:validate="validate"
:filter="filter"
></slot>
</template>
</VnPaginate>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubToolbar">
@ -420,16 +347,8 @@ watch(formUrl, async () => {
<QBtnDropdown
v-if="$props.goTo && $props.defaultSave"
@click="onSubmitAndGo"
:label="
tMobile('globals.saveAndContinue') +
' ' +
t('globals.' + $props.goTo.split('/').pop())
"
:title="
t('globals.saveAndContinue') +
' ' +
t('globals.' + $props.goTo.split('/').pop())
"
:label="tMobile('globals.saveAndContinue')"
:title="t('globals.saveAndContinue')"
:disable="!hasChanges"
color="primary"
icon="save"

View File

@ -8,8 +8,10 @@ import '@quasar/quasar-ui-qcalendar/src/QCalendarVariables.scss';
import { useWeekdayStore } from 'src/stores/useWeekdayStore';
import useWeekdaysOrder from 'src/composables/getWeekdays';
const formatDate = (dateToFormat, format = 'YYYY-MM-DD') =>
date.formatDate(dateToFormat, format);
const formatDate = (dateToFormat, format = 'YYYY-MM-DD') => (
date.formatDate(dateToFormat, format)
);
const props = defineProps({
year: {
@ -62,10 +64,10 @@ const handleDateClick = (timestamp) => {
const event = getEventByTimestamp(timestamp);
const { year, month, day } = timestamp;
const date = new Date(year, month - 1, day);
emit('onDateSelected', {
date,
emit('onDateSelected', {
date,
isNewMode: !event,
event: event?.[0] || null,
event: event?.[0] || null
});
};
@ -105,11 +107,7 @@ defineExpose({ getEventByTimestamp, handleDateClick });
mini-mode
>
<template #day="{ scope: { timestamp } }">
<slot
name="day"
:timestamp="timestamp"
:getEventAttrs="getEventAttrs"
>
<slot name="day" :timestamp="timestamp" :getEventAttrs="getEventAttrs">
<QBtn
v-if="getEventByTimestamp(timestamp)"
v-bind="{ ...getEventAttrs(timestamp) }"
@ -151,4 +149,4 @@ defineExpose({ getEventByTimestamp, handleDateClick });
opacity: 0.8;
}
}
</style>
</style>

View File

@ -5,18 +5,18 @@ import { useWeekdayStore } from 'src/stores/useWeekdayStore';
import { useArrayData } from 'src/composables/useArrayData';
const props = defineProps({
dataKey: {
type: String,
required: true,
},
calendarComponent: {
type: Object,
required: true,
},
additionalProps: {
type: Object,
default: () => ({}),
},
dataKey: {
type: String,
required: true,
},
calendarComponent: {
type: Object,
required: true,
},
additionalProps: {
type: Object,
default: () => ({}),
}
});
const stateStore = useStateStore();
@ -28,99 +28,99 @@ const lastDay = ref(Date.vnNew());
const months = ref([]);
const arrayData = useArrayData(props.dataKey);
onMounted(async () => {
const initialDate = Date.vnNew();
initialDate.setDate(1);
initialDate.setHours(0, 0, 0, 0);
date.value = initialDate;
await nextTick();
stateStore.rightDrawer = true;
const initialDate = Date.vnNew();
initialDate.setDate(1);
initialDate.setHours(0, 0, 0, 0);
date.value = initialDate;
await nextTick();
stateStore.rightDrawer = true;
});
onUnmounted(() => arrayData.destroy());
const emit = defineEmits([
'update:firstDay',
'update:lastDay',
'update:events',
'onDateSelected',
'update:firstDay',
'update:lastDay',
'update:events',
'onDateSelected',
]);
const date = computed({
get: () => _date.value,
set: (value) => {
if (!(value instanceof Date)) return;
_date.value = value;
const stamp = value.getTime();
get: () => _date.value,
set: (value) => {
if (!(value instanceof Date)) return;
_date.value = value;
const stamp = value.getTime();
firstDay.value = new Date(stamp);
firstDay.value.setDate(1);
firstDay.value = new Date(stamp);
firstDay.value.setDate(1);
lastDay.value = new Date(stamp);
lastDay.value.setMonth(lastDay.value.getMonth() + nMonths.value);
lastDay.value.setDate(0);
lastDay.value = new Date(stamp);
lastDay.value.setMonth(lastDay.value.getMonth() + nMonths.value);
lastDay.value.setDate(0);
months.value = [];
for (let i = 0; i < nMonths.value; i++) {
const monthDate = new Date(stamp);
monthDate.setMonth(value.getMonth() + i);
months.value.push(monthDate);
}
months.value = [];
for (let i = 0; i < nMonths.value; i++) {
const monthDate = new Date(stamp);
monthDate.setMonth(value.getMonth() + i);
months.value.push(monthDate);
}
emit('update:firstDay', firstDay.value);
emit('update:lastDay', lastDay.value);
emit('refresh-events');
},
emit('update:firstDay', firstDay.value);
emit('update:lastDay', lastDay.value);
emit('refresh-events');
},
});
const headerTitle = computed(() => {
if (!months.value?.length) return '';
const getMonthName = (date) =>
`${weekdayStore.getLocaleMonths[date.getMonth()].locale} ${date.getFullYear()}`;
return `${getMonthName(months.value[0])} - ${getMonthName(months.value[months.value.length - 1])}`;
if (!months.value?.length) return '';
const getMonthName = date =>
`${weekdayStore.getLocaleMonths[date.getMonth()].locale} ${date.getFullYear()}`;
return `${getMonthName(months.value[0])} - ${getMonthName(months.value[months.value.length - 1])}`;
});
const step = (direction) => {
const newDate = new Date(date.value);
newDate.setMonth(newDate.getMonth() + nMonths.value * direction);
date.value = newDate;
const newDate = new Date(date.value);
newDate.setMonth(newDate.getMonth() + nMonths.value * direction);
date.value = newDate;
};
defineExpose({
firstDay,
lastDay,
lastDay
});
</script>
<template>
<QCard style="height: max-content">
<div class="calendars-header">
<QBtn
icon="arrow_left"
size="sm"
flat
class="full-height"
@click="step(-1)"
/>
<span>{{ headerTitle }}</span>
<QBtn
icon="arrow_right"
size="sm"
flat
class="full-height"
@click="step(1)"
/>
</div>
<div class="calendars-container">
<component
:is="calendarComponent"
v-for="(month, index) in months"
:key="index"
:month="month.getMonth() + 1"
:year="month.getFullYear()"
:month-date="month"
v-bind="additionalProps"
@on-date-selected="(data) => emit('onDateSelected', data)"
/>
</div>
</QCard>
</template>
<QCard style="height: max-content">
<div class="calendars-header">
<QBtn
icon="arrow_left"
size="sm"
flat
class="full-height"
@click="step(-1)"
/>
<span>{{ headerTitle }}</span>
<QBtn
icon="arrow_right"
size="sm"
flat
class="full-height"
@click="step(1)"
/>
</div>
<div class="calendars-container">
<component
:is="calendarComponent"
v-for="(month, index) in months"
:key="index"
:month="month.getMonth() + 1"
:year="month.getFullYear()"
:month-date="month"
v-bind="additionalProps"
@on-date-selected="data => emit('onDateSelected', data)"
/>
</div>
</QCard>
</template>

View File

@ -102,10 +102,6 @@ const $props = defineProps({
type: Boolean,
default: false,
},
customMethod: {
type: String,
default: null,
},
});
const emit = defineEmits(['onFetch', 'onDataSaved', 'submit']);
const modelValue = computed(
@ -241,9 +237,7 @@ async function save() {
const url =
$props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url;
const response = await Promise.resolve(
$props.saveFn
? $props.saveFn(body)
: axios[$props.customMethod ?? method](url, body),
$props.saveFn ? $props.saveFn(body) : axios[method](url, body),
);
if ($props.urlCreate) notify('globals.dataCreated', 'positive');
@ -254,13 +248,17 @@ async function save() {
old: originalData.value,
});
if ($props.reload) await arrayData.fetch({});
if ($props.goTo) push({ path: $props.goTo });
hasChanges.value = false;
} finally {
isLoading.value = false;
}
}
async function saveAndGo() {
await save();
push({ path: $props.goTo });
}
function reset() {
formData.value = JSON.parse(JSON.stringify(originalData.value));
updateAndEmit('onFetch', { val: originalData.value });
@ -381,17 +379,9 @@ defineExpose({
<QBtnDropdown
data-cy="saveAndContinueDefaultBtn"
v-if="$props.goTo"
@click="submitForm"
:label="
tMobile('globals.saveAndContinue') +
' ' +
t('globals.' + $props.goTo.split('/').pop())
"
:title="
t('globals.saveAndContinue') +
' ' +
t('globals.' + $props.goTo.split('/').pop())
"
@click="saveAndGo"
:label="tMobile('globals.saveAndContinue')"
:title="t('globals.saveAndContinue')"
:disable="!hasChanges"
color="primary"
icon="save"
@ -401,7 +391,7 @@ defineExpose({
<QItem
clickable
v-close-popup
@click="submitForm"
@click="save"
:title="t('globals.save')"
>
<QItemSection>

View File

@ -26,7 +26,7 @@ async function redirect() {
if (route?.params?.id)
return (window.location.href = await getUrl(
`${section}/${route.params.id}/summary`,
`${section}/${route.params.id}/summary`
));
return (window.location.href = await getUrl(section + '/index'));
}

View File

@ -55,7 +55,7 @@ const refund = async () => {
(data) => (
(rectificativeTypeOptions = data),
(invoiceParams.cplusRectificationTypeFk = data.filter(
(type) => type.description == 'I Por diferencias',
(type) => type.description == 'I Por diferencias'
)[0].id)
)
"
@ -68,7 +68,7 @@ const refund = async () => {
(data) => (
(siiTypeInvoiceOutsOptions = data),
(invoiceParams.siiTypeInvoiceOutFk = data.filter(
(type) => type.code == 'R4',
(type) => type.code == 'R4'
)[0].id)
)
"

View File

@ -104,7 +104,7 @@ function showProblem(problem) {
<QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip>
</QIcon>
<QIcon
v-if="showProblem('isNotTaxDataChecked')"
v-if="showProblem('isTaxDataChecked')"
name="vn:no036"
color="primary"
size="xs"

View File

@ -52,7 +52,7 @@ watch(
} else filter.value.where = {};
await provincesFetchDataRef.value.fetch({});
emit('onProvinceFetched', provincesOptions.value);
},
}
);
</script>

View File

@ -181,11 +181,6 @@ const col = computed(() => {
newColumn.component = 'checkbox';
if ($props.default && !newColumn.component) newColumn.component = $props.default;
if (typeof newColumn.component !== 'string') {
newColumn.attrs = { ...newColumn.component?.attrs, autofocus: $props.autofocus };
newColumn.event = { ...newColumn.component?.event, ...$props?.eventHandlers };
}
return newColumn;
});

View File

@ -1,136 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useClipboard } from 'src/composables/useClipboard';
const { copyText } = useClipboard();
const target = ref();
const qmenuRef = ref();
const colField = ref();
let colValue = '';
let textValue = '';
defineExpose({ handler });
const arrayData = defineModel({
type: Object,
});
function handler(event) {
const clickedElement = event.target.closest('td');
if (!clickedElement) return;
event.preventDefault();
target.value = event.target;
qmenuRef.value.show();
colField.value = clickedElement.getAttribute('data-col-field');
colValue = isNaN(+clickedElement.getAttribute('data-col-value'))
? clickedElement.getAttribute('data-col-value')
: +clickedElement.getAttribute('data-col-value');
textValue = getDeepestText(clickedElement);
}
function getDeepestText(node) {
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
acceptNode: (textNode) => {
return textNode.nodeValue.trim()
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
},
});
let lastText = '';
while (walker.nextNode()) {
lastText = walker.currentNode.nodeValue.trim();
}
return lastText;
}
async function selectionFilter() {
await arrayData.value.addFilter({ params: { [colField.value]: colValue } });
}
async function selectionExclude() {
await arrayData.value.addFilter({
params: { [colField.value]: { neq: colValue } },
});
}
async function selectionRemoveFilter() {
await arrayData.value.addFilter({ params: { [colField.value]: undefined } });
}
async function removeAllFilters() {
await arrayData.value.applyFilter({ params: {} });
}
function copyValue() {
copyText(textValue, {
component: {
copyValue: textValue,
},
});
}
</script>
<template>
<QMenu
ref="qmenuRef"
:target
class="column q-pa-sm justify-left"
auto-close
no-parent-event
>
<QBtn
flat
icon="filter_list"
@click="selectionFilter()"
class="text-weight-regular"
align="left"
:label="$t('Filter by selection')"
no-caps
/>
<QBtn
flat
icon="dangerous"
@click="selectionExclude()"
class="text-weight-regular"
align="left"
:label="$t('Exclude selection')"
no-caps
/>
<QBtn
flat
icon="filter_list_off"
@click="selectionRemoveFilter()"
class="text-weight-regular"
align="left"
:label="$t('Remove filter')"
no-caps
/>
<QBtn
flat
icon="filter_list_off"
@click="removeAllFilters()"
class="text-weight-regular"
align="left"
:label="$t('Remove all filters')"
no-caps
/>
<QBtn
flat
icon="file_copy"
@click="copyValue()"
class="text-weight-regular"
align="left"
:label="$t('Copy value')"
no-caps
/>
</QMenu>
</template>
<i18n>
es:
Filter by selection: Filtro por selección
Exclude selection: Excluir selección
Remove filter: Quitar filtro por selección
Remove all filters: Eliminar todos los filtros
Copy value: Copiar valor
</i18n>

View File

@ -43,9 +43,7 @@ const columnFilter = computed(() => $props.column?.columnFilter);
const updateEvent = { 'update:modelValue': addFilter };
const enterEvent = {
keyup: ({ key }) => {
if (key === 'Enter') addFilter(model.value);
},
'keyup.enter': () => addFilter(model.value),
remove: () => addFilter(null),
};
@ -112,6 +110,7 @@ const components = {
component: markRaw(VnCheckbox),
event: updateEvent,
attrs: {
class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit',
'toggle-indeterminate': true,
size: 'sm',
},
@ -137,9 +136,6 @@ async function addFilter(value, name) {
value = value === '' ? undefined : value;
let field = columnFilter.value?.name ?? $props.column.name ?? name;
delete arrayData.store?.userParams?.[field];
delete arrayData.store?.filter?.where?.[field];
if (columnFilter.value?.inWhere) {
if (columnFilter.value.alias) field = columnFilter.value.alias + '.' + field;
return await arrayData.addFilterWhere({ [field]: value });

View File

@ -33,10 +33,8 @@ import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
import VnTableFilter from './VnTableFilter.vue';
import { getColAlign } from 'src/composables/getColAlign';
import RightMenu from '../common/RightMenu.vue';
import VnContextMenu from './VnContextMenu.vue';
import VnScroll from '../common/VnScroll.vue';
import VnCheckboxMenu from '../common/VnCheckboxMenu.vue';
import VnCheckbox from '../common/VnCheckbox.vue';
import VnMultiCheck from '../common/VnMultiCheck.vue';
const arrayData = useArrayData(useAttrs()['data-key']);
const $props = defineProps({
@ -151,10 +149,6 @@ const $props = defineProps({
type: String,
default: 'vnTable',
},
selectionFn: {
type: Function,
default: null,
},
});
const { t } = useI18n();
@ -180,12 +174,11 @@ const tableRef = ref();
const params = ref(useFilterParams($attrs['data-key']).params);
const orders = ref(useFilterParams($attrs['data-key']).orders);
const app = inject('app');
const tableHeight = useTableHeight({ customHeight: $props.tableHeight });
const tableHeight = useTableHeight();
const vnScrollRef = ref(null);
const editingRow = ref();
const editingField = ref();
const contextMenuRef = ref({});
const editingRow = ref(null);
const editingField = ref(null);
const isTableMode = computed(() => mode.value == TABLE_MODE);
const selectRegex = /select/;
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
@ -222,7 +215,6 @@ onBeforeMount(() => {
onMounted(async () => {
if ($props.isEditable) document.addEventListener('click', clickHandler);
document.addEventListener('contextmenu', contextMenuRef.value.handler);
mode.value =
quasar.platform.is.mobile && !$props.disableOption?.card
? CARD_MODE
@ -246,7 +238,6 @@ onMounted(async () => {
onUnmounted(async () => {
if ($props.isEditable) document.removeEventListener('click', clickHandler);
document.removeEventListener('contextmenu', {});
});
watch(
@ -339,10 +330,9 @@ function stopEventPropagation(event) {
event.stopPropagation();
}
async function reload(params) {
function reload(params) {
selected.value = [];
selectAll.value = false;
await CrudModelRef.value.reload(params);
CrudModelRef.value.reload(params);
}
function columnName(col) {
@ -383,34 +373,28 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
}
}
function isEditableColumn(column, row) {
const isEditableCol =
typeof column?.isEditable == 'function'
? column?.isEditable(row)
: (column?.isEditable ?? true);
function isEditableColumn(column) {
const isEditableCol = column?.isEditable ?? true;
const isVisible = column?.visible ?? true;
const hasComponent = column?.component;
return $props.isEditable && isVisible && hasComponent && isEditableCol;
}
function hasEditableFormat(column, row) {
if (isEditableColumn(column, row)) return 'editable-text';
function hasEditableFormat(column) {
if (isEditableColumn(column)) return 'editable-text';
}
const clickHandler = async (event) => {
const el = event.target;
const clickedElement = el.closest('td');
const isDateElement = el.closest('.q-date');
const isTimeElement = el.closest('.q-time');
const isQSelectDropDown = el.closest('.q-select__dropdown-icon');
const isDialog = el.closest('.q-dialog');
const clickedElement = event.target.closest('td');
const isDateElement = event.target.closest('.q-date');
const isTimeElement = event.target.closest('.q-time');
const isQSelectDropDown = event.target.closest('.q-select__dropdown-icon');
if (isDateElement || isTimeElement || isQSelectDropDown || isDialog) return;
if (isDateElement || isTimeElement || isQSelectDropDown) return;
if (clickedElement === null) {
destroyInput(editingRow.value, editingField.value);
await destroyInput(editingRow.value, editingField.value);
return;
}
const rowIndex = clickedElement.getAttribute('data-row-index');
@ -420,25 +404,20 @@ const clickHandler = async (event) => {
if (editingRow.value !== null && editingField.value !== null) {
if (editingRow.value == rowIndex && editingField.value == colField) return;
destroyInput(editingRow.value, editingField.value);
await destroyInput(editingRow.value, editingField.value);
}
if (
isEditableColumn(
column,
CrudModelRef.value.formData[rowIndex ?? editingRow.value],
)
) {
renderInput(Number(rowIndex), colField, clickedElement);
if (isEditableColumn(column)) {
await renderInput(Number(rowIndex), colField, clickedElement);
}
};
function handleTabKey(event, rowIndex, colField) {
async function handleTabKey(event, rowIndex, colField) {
if (editingRow.value == rowIndex && editingField.value == colField)
destroyInput(editingRow.value, editingField.value);
await destroyInput(editingRow.value, editingField.value);
const direction = event.shiftKey ? -1 : 1;
const { nextRowIndex, nextColumnName } = handleTabNavigation(
const { nextRowIndex, nextColumnName } = await handleTabNavigation(
rowIndex,
colField,
direction,
@ -447,10 +426,10 @@ function handleTabKey(event, rowIndex, colField) {
if (nextRowIndex < 0 || nextRowIndex >= arrayData.store.data.length) return;
event.preventDefault();
renderInput(nextRowIndex, nextColumnName, null);
await renderInput(nextRowIndex, nextColumnName, null);
}
function renderInput(rowId, field, clickedElement) {
async function renderInput(rowId, field, clickedElement) {
editingField.value = field;
editingRow.value = rowId;
@ -459,7 +438,6 @@ function renderInput(rowId, field, clickedElement) {
const row = CrudModelRef.value.formData[rowId];
const oldValue = CrudModelRef.value.formData[rowId][column?.name];
if (column.disable) return;
if (!clickedElement)
clickedElement = document.querySelector(
`[data-row-index="${rowId}"][data-col-field="${field}"]`,
@ -488,22 +466,18 @@ function renderInput(rowId, field, clickedElement) {
} else row[column.name] = value;
await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
},
keyup: (event) => {
if (event.key === 'Enter') {
destroyInput(rowId, field, clickedElement);
event.stopPropagation();
}
keyup: async (event) => {
if (event.key === 'Enter')
await destroyInput(rowId, field, clickedElement);
},
keydown: (event) => {
column?.cellEvent?.['keydown']?.(event, row);
keydown: async (event) => {
switch (event.key) {
case 'Tab':
handleTabKey(event, rowId, field);
await handleTabKey(event, rowId, field);
event.stopPropagation();
break;
case 'Escape':
destroyInput(rowId, field, clickedElement);
event.stopPropagation();
await destroyInput(rowId, field, clickedElement);
break;
default:
break;
@ -512,9 +486,6 @@ function renderInput(rowId, field, clickedElement) {
click: (event) => {
column?.cellEvent?.['click']?.(event, row);
},
blur: () => {
column?.cellEvent?.['blur']?.(row);
},
},
});
@ -539,32 +510,25 @@ async function updateSelectValue(value, column, row, oldValue) {
await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
}
function destroyInput(rowIndex, field, clickedElement) {
async function destroyInput(rowIndex, field, clickedElement) {
if (!clickedElement)
clickedElement = document.querySelector(
`[data-row-index="${rowIndex}"][data-col-field="${field}"]`,
);
if (clickedElement) {
const column = $props.columns.find((col) => col.name === field);
if (typeof column?.beforeDestroy === 'function')
column.beforeDestroy(CrudModelRef.value.formData[rowIndex]);
nextTick().then(() => {
render(null, clickedElement);
Array.from(clickedElement.childNodes).forEach((child) => {
child.style.visibility = 'visible';
child.style.position = '';
});
await nextTick();
render(null, clickedElement);
Array.from(clickedElement.childNodes).forEach((child) => {
child.style.visibility = 'visible';
child.style.position = '';
});
}
if (editingRow.value !== rowIndex || editingField.value !== field) return;
editingRow.value = null;
editingField.value = null;
}
function handleTabNavigation(rowIndex, colName, direction) {
async function handleTabNavigation(rowIndex, colName, direction) {
const columns = $props.columns;
const totalColumns = columns.length;
let currentColumnIndex = columns.findIndex((col) => col.name === colName);
@ -576,13 +540,7 @@ function handleTabNavigation(rowIndex, colName, direction) {
iterations++;
newColumnIndex = (newColumnIndex + direction + totalColumns) % totalColumns;
if (
isEditableColumn(
columns[newColumnIndex],
CrudModelRef.value.formData[rowIndex],
)
)
break;
if (isEditableColumn(columns[newColumnIndex])) break;
} while (iterations < totalColumns);
if (iterations >= totalColumns + 1) return;
@ -686,17 +644,21 @@ const rowCtrlClickFunction = computed(() => {
};
return () => {};
});
const handleHeaderSelection = (evt, data) => {
if (evt === 'updateSelected' && selectAll.value) {
const fn = $props.selectionFn;
const rows = tableRef.value.rows;
selected.value = fn ? fn(rows) : rows;
} else if (evt === 'selectAll') {
const handleMultiCheck = (value) => {
if (value) {
selected.value = tableRef.value.rows;
} else {
selected.value = [];
}
emit('update:selected', selected.value);
};
const handleSelectedAll = (data) => {
if (data) {
selected.value = data;
} else {
selected.value = [];
}
emit('update:selected', selected.value);
};
</script>
@ -724,18 +686,11 @@ const handleHeaderSelection = (evt, data) => {
:class="$attrs['class'] ?? 'q-px-md'"
:limit="$attrs['limit'] ?? 100"
ref="CrudModelRef"
@on-fetch="
(...args) => {
if ($props.multiCheck.expand) {
selectAll = false;
selected = [];
}
emit('onFetch', ...args);
}
"
@on-fetch="(...args) => emit('onFetch', ...args)"
:search-url="searchUrl"
:disable-infinite-scroll="isTableMode"
:before-save-fn="removeTextValue"
@save-changes="reload"
:has-sub-toolbar="$props.hasSubToolbar ?? isEditable"
:auto-load="hasParams || $attrs['auto-load']"
>
@ -759,41 +714,24 @@ const handleHeaderSelection = (evt, data) => {
table-header-class="bg-header"
card-container-class="grid-three"
flat
:style="isTableMode && `max-height: ${tableHeight}`"
:style="isTableMode && `max-height: ${$props.tableHeight || tableHeight}`"
:virtual-scroll="isTableMode"
@virtual-scroll="onVirtualScroll"
@row-click="(event, row) => handleRowClick(event, row)"
@update:selected="
(evt) => {
if ($props.selectionFn) selected = $props.selectionFn(evt);
emit(
'update:selected',
selectionFn ? selectionFn(selected) : selected,
);
}
"
@update:selected="emit('update:selected', $event)"
@selection="(details) => handleSelection(details, rows)"
:hide-selected-banner="true"
:data-cy
>
<template #header-selection>
<div class="flex items-center no-wrap" style="display: flex">
<VnCheckbox
v-model="selectAll"
@click="handleHeaderSelection('updateSelected', $event)"
/>
<VnCheckboxMenu
v-if="selectAll && $props.multiCheck.expand"
:searchUrl="searchUrl"
v-model="selectAll"
:url="$attrs['url']"
@update:selected="
handleHeaderSelection('updateSelected', $event)
"
@select:all="handleHeaderSelection('selectAll', $event)"
/>
</div>
<VnMultiCheck
:searchUrl="searchUrl"
:expand="$props.multiCheck.expand"
v-model="selectAll"
:url="$attrs['url']"
@update:selected="handleMultiCheck"
@select:all="handleSelectedAll"
></VnMultiCheck>
</template>
<template #top-left v-if="!$props.withoutHeader">
@ -884,7 +822,6 @@ const handleHeaderSelection = (evt, data) => {
]"
:data-row-index="rowIndex"
:data-col-field="col?.name"
:data-col-value="row?.[col?.name]"
>
<div
class="no-padding no-margin"
@ -909,19 +846,19 @@ const handleHeaderSelection = (evt, data) => {
: getToggleIcon(row[col?.name])
"
style="color: var(--vn-text-color)"
:class="hasEditableFormat(col, row)"
:class="hasEditableFormat(col)"
size="14px"
/>
<QIcon
v-else-if="col?.component === 'checkbox'"
:name="getCheckboxIcon(row[col?.name])"
style="color: var(--vn-text-color)"
:class="hasEditableFormat(col, row)"
:class="hasEditableFormat(col)"
size="14px"
/>
<span
v-else
:class="hasEditableFormat(col, row)"
:class="hasEditableFormat(col)"
:style="
typeof col?.style == 'function'
? col.style(row)
@ -947,11 +884,7 @@ const handleHeaderSelection = (evt, data) => {
v-for="(btn, index) of col.actions"
v-show="btn.show ? btn.show(row) : true"
:key="index"
:title="
typeof btn.title === 'function'
? btn.title(row)
: btn.title
"
:title="btn.title"
:icon="btn.icon"
class="q-pa-xs"
flat
@ -1199,7 +1132,6 @@ const handleHeaderSelection = (evt, data) => {
</template>
</FormModelPopup>
</QDialog>
<VnContextMenu ref="contextMenuRef" v-model="arrayData" />
<VnScroll
ref="vnScrollRef"
v-if="isTableMode"
@ -1260,7 +1192,7 @@ es:
}
.bg-header {
background-color: var(--vn-section-color);
background-color: var(--vn-accent-color);
color: var(--vn-text-color);
}

View File

@ -77,12 +77,7 @@ function columnName(col) {
<template #tags="{ tag, formatFn, getLocale }">
<div class="q-gutter-x-xs">
<strong>{{ getLocale(`${tag.label}`) }}: </strong>
<span
:class="{
'text-decoration-line-through': typeof chip === 'object',
}"
>{{ formatFn(tag) }}</span
>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">

View File

@ -6,7 +6,7 @@ export default function (initialFooter, data) {
});
return acc;
},
{ ...initialFooter },
{ ...initialFooter }
);
return footer;
}

View File

@ -1,31 +1,18 @@
import { onMounted, nextTick, ref } from 'vue';
const MARGIN_BOTTOM = {
xs: 10,
sm: 8,
md: 6,
lg: 4,
xl: 2,
xxl: 0,
};
const defaultOptions = { defaultHeight: 100, space: 'xs' };
export function useTableHeight(options) {
const tableHeight = ref(options.customHeight ?? '90vh');
export function useTableHeight() {
const tableHeight = ref('90vh');
onMounted(async () => {
const { defaultHeight, space, customHeight } = { ...defaultOptions, ...options };
await nextTick();
if (customHeight) {
return customHeight;
}
let height = defaultHeight;
let height = 100;
Array.from(document.querySelectorAll('[role="toolbar"]'))
.filter((element) => window.getComputedStyle(element).display !== 'none')
.forEach(() => {
height -= MARGIN_BOTTOM[space];
height -= 10;
});
tableHeight.value = `${height}vh`;
});
return tableHeight;
}

View File

@ -193,11 +193,11 @@ describe('CrudModel', () => {
});
it('should set originalData and formatData with data and generate watchChanges', async () => {
data = [{
data = {
name: 'Tony',
lastName: 'Stark',
age: 42,
}];
};
vm.resetData(data);
@ -241,7 +241,7 @@ describe('CrudModel', () => {
await vm.saveChanges(data);
expect(postMock).toHaveBeenCalledWith(`${vm.url}/crud`, data);
expect(postMock).toHaveBeenCalledWith(vm.url + '/crud', data);
expect(vm.isLoading).toBe(false);
expect(vm.hasChanges).toBe(false);
expect(vm.originalData).toEqual(JSON.parse(JSON.stringify(vm.formData)));

View File

@ -142,14 +142,14 @@ describe('getRoutes', () => {
const fn = (props) => getRoutes(props, getMethodA, getMethodB);
it('should call getMethodB when source is card', () => {
const props = { source: 'methodB' };
let props = { source: 'methodB' };
fn(props);
expect(getMethodB).toHaveBeenCalled();
expect(getMethodA).not.toHaveBeenCalled();
});
it('should call getMethodA when source is main', () => {
const props = { source: 'methodA' };
let props = { source: 'methodA' };
fn(props);
expect(getMethodA).toHaveBeenCalled();
@ -157,7 +157,7 @@ describe('getRoutes', () => {
});
it('should call getMethodA when source is not exists or undefined', () => {
const props = { source: 'methodC' };
let props = { source: 'methodC' };
expect(() => fn(props)).toThrowError('Method not defined');
expect(getMethodA).not.toHaveBeenCalled();

View File

@ -1,43 +0,0 @@
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import VnAccountNumber from 'src/components/common/VnAccountNumber.vue';
describe('VnAccountNumber', () => {
let wrapper;
let input;
let vnInput;
let spyShort;
beforeEach(() => {
wrapper = createWrapper(VnAccountNumber);
wrapper = wrapper.wrapper;
input = wrapper.find('input');
vnInput = wrapper.findComponent({ name: 'VnInput' });
spyShort = vi.spyOn(wrapper.vm, 'useAccountShortToStandard');
});
it('should filter out non-numeric characters on input event', async () => {
await input.setValue('abc123.45!@#');
const emitted = wrapper.emitted('update:modelValue');
expect(emitted.pop()[0]).toBe('123.45');
expect(spyShort).not.toHaveBeenCalled();
});
it('should apply conversion on blur when valid short value is provided', async () => {
await input.setValue('123.45');
await vnInput.trigger('blur');
const emitted = wrapper.emitted('update:modelValue');
expect(emitted.pop()[0]).toBe('1230000045');
expect(spyShort).toHaveBeenCalled();
});
it('should not change value for invalid input values', async () => {
await input.setValue('123');
await vnInput.trigger('blur');
const emitted = wrapper.emitted('update:modelValue');
expect(emitted.pop()[0]).toBe('123');
expect(spyShort).toHaveBeenCalled();
});
});

View File

@ -72,7 +72,7 @@ const getBankEntities = (data) => {
:acls="[{ model: 'BankEntity', props: '*', accessType: 'WRITE' }]"
:options="bankEntities"
hide-selected
option-label="bic"
option-label="name"
option-value="id"
v-model="bankEntityFk"
@update:model-value="$emit('updateBic', { iban, bankEntityFk })"

View File

@ -15,7 +15,7 @@ let root = ref(null);
watchEffect(() => {
matched.value = currentRoute.value.matched.filter(
(matched) => !!matched?.meta?.title || !!matched?.meta?.icon,
(matched) => !!matched?.meta?.title || !!matched?.meta?.icon
);
breadcrumbs.value.length = 0;
if (!matched.value[0]) return;

View File

@ -36,6 +36,8 @@ const validate = async () => {
isLoading.value = true;
await props.submitFn(newPassword, oldPassword);
emit('onSubmit');
} catch (e) {
notify('errors.writeRequest', 'negative');
} finally {
changePassDialog.value.hide();
isLoading.value = false;

View File

@ -1,104 +0,0 @@
<script setup>
import { ref } from 'vue';
import VnCheckbox from './VnCheckbox.vue';
import axios from 'axios';
import { toRaw } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
const route = useRoute();
const { t } = useI18n();
const model = defineModel({ type: [Boolean] });
const props = defineProps({
url: {
type: String,
required: true,
},
searchUrl: {
type: [String, Boolean],
default: 'table',
},
});
const menuRef = ref(null);
const errorMessage = ref(null);
const rows = ref(0);
const onClick = async () => {
errorMessage.value = null;
const { filter } = JSON.parse(route.query[props.searchUrl]);
filter.limit = 0;
const params = {
params: { filter: JSON.stringify(filter) },
};
try {
const { data } = await axios.get(props.url, params);
rows.value = data;
} catch (error) {
const response = error.response;
if (response.data.error.name === 'UserError') {
errorMessage.value = t('tooManyResults');
} else {
errorMessage.value = response.data.error.message;
}
}
};
defineEmits(['update:selected', 'select:all']);
</script>
<template>
<QIcon
style="margin-left: -10px"
data-cy="btnMultiCheck"
name="expand_more"
@click="onClick"
class="cursor-pointer"
color="primary"
size="xs"
>
<QMenu
fit
anchor="bottom start"
self="top left"
ref="menuRef"
data-cy="menuMultiCheck"
>
<QList separator>
<QItem
data-cy="selectAll"
v-ripple
clickable
@click="
$refs.menuRef.hide();
$emit('select:all', toRaw(rows));
"
>
<QItemSection>
<QItemLabel>
<span v-text="t('Select all')" />
</QItemLabel>
<QItemLabel overline caption>
<span
v-if="errorMessage"
class="text-negative"
v-text="errorMessage"
/>
<span
v-else
v-text="t('records', { rows: rows?.length ?? 0 })"
/>
</QItemLabel>
</QItemSection>
</QItem>
<slot name="more-options"></slot>
</QList>
</QMenu>
</QIcon>
</template>
<i18n lang="yml">
en:
tooManyResults: Too many results. Please narrow down your search.
records: '{rows} records'
es:
Select all: Seleccionar todo
tooManyResults: Demasiados registros. Restringe la búsqueda.
records: '{rows} registros'
</i18n>

View File

@ -17,6 +17,8 @@ const $props = defineProps({
},
});
const emit = defineEmits(['blur']);
const componentArray = computed(() => {
if (typeof $props.prop === 'object') return [$props.prop];
return $props.prop;
@ -55,6 +57,7 @@ function toValueAttrs(attrs) {
v-bind="mix(toComponent).attrs"
v-on="mix(toComponent).event ?? {}"
v-model="model"
@blur="emit('blur')"
/>
</span>
</template>

View File

@ -7,6 +7,7 @@ import axios from 'axios';
import { usePrintService } from 'composables/usePrintService';
import VnUserLink from '../ui/VnUserLink.vue';
import { downloadFile } from 'src/composables/downloadFile';
import VnImg from 'components/ui/VnImg.vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnDms from 'src/components/common/VnDms.vue';

View File

@ -6,7 +6,13 @@ import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const { t } = useI18n();
const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);
const emit = defineEmits([
'update:modelValue',
'update:options',
'keyup.enter',
'remove',
'blur',
]);
const $props = defineProps({
modelValue: {
@ -130,6 +136,8 @@ const handleUppercase = () => {
v-bind="{ ...$attrs, ...styleAttrs }"
:type="$attrs.type"
:class="{ required: isRequired }"
@keyup.enter="emit('keyup.enter')"
@blur="emit('blur')"
@keydown="handleKeydown"
:clearable="false"
:rules="mixinRules"

View File

@ -51,7 +51,6 @@ const validateAndCleanInput = (value) => {
const manageDate = (date) => {
inputValue.value = date.split('/').reverse().join('/');
formatDate();
isPopupOpen.value = false;
};
@ -171,7 +170,7 @@ const handleEnter = (event) => {
:input-style="{ color: textColor }"
@click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
@focusout="formatDate"
@blur="formatDate"
@keydown.enter.prevent="handleEnter"
hide-bottom-space
:data-cy="($attrs['data-cy'] ?? $attrs.label) + '_inputDate'"

View File

@ -1,184 +0,0 @@
<script setup>
import { onMounted, watch, computed, ref, useAttrs } from 'vue';
import { date } from 'quasar';
import VnDate from './VnDate.vue';
import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const $props = defineProps({
isOutlined: {
type: Boolean,
default: false,
},
isPopupOpen: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: true,
},
});
const model = defineModel({
type: [String, Date, Array],
default: null,
});
const vnInputDateRef = ref(null);
const dateFormat = 'DD/MM/YYYY';
const isPopupOpen = ref();
const hover = ref();
const mask = ref();
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
const formattedDate = computed({
get() {
if (!model.value) return model.value;
if ($props.multiple) {
return model.value
.map((d) => date.formatDate(new Date(d), dateFormat))
.join(', ');
}
return date.formatDate(new Date(model.value), dateFormat);
},
set(value) {
if (value == model.value) return;
if ($props.multiple) return; // No permitir edición manual en modo múltiple
let newDate;
if (value) {
// parse input
if (value.includes('/') && 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 && !$props.multiple) {
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(() => {
if (!model.value) return model.value;
if ($props.multiple) {
return model.value.map((d) => date.formatDate(new Date(d), 'YYYY/MM/DD'));
}
return date.formatDate(new Date(model.value), 'YYYY/MM/DD');
});
onMounted(() => {
// fix quasar bug
mask.value = '##/##/####';
if ($props.multiple && !model.value) {
model.value = [];
}
});
watch(
() => model.value,
(val) => (formattedDate.value = val),
{ immediate: true },
);
const styleAttrs = computed(() => {
return $props.isOutlined
? {
dense: true,
outlined: true,
rounded: true,
}
: {};
});
const manageDate = (dates) => {
if ($props.multiple) {
model.value = dates.map((d) => new Date(d).toISOString());
} else {
formattedDate.value = dates;
}
if ($props.isPopupOpen) isPopupOpen.value = false;
};
</script>
<template>
<div @mouseover="hover = true" @mouseleave="hover = false">
<QInput
ref="vnInputDateRef"
v-model="formattedDate"
class="vn-input-date"
:mask="$props.multiple ? undefined : mask"
placeholder="dd/mm/aaaa"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: isRequired }"
:rules="mixinRules"
:clearable="false"
@click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
hide-bottom-space
:data-cy="($attrs['data-cy'] ?? $attrs.label) + '_inputDate'"
>
<template #append>
<QIcon
name="close"
size="xs"
v-if="
($attrs.clearable == undefined || $attrs.clearable) &&
hover &&
model &&
!$attrs.disable
"
@click="
vnInputDateRef.focus();
model = null;
isPopupOpen = false;
"
/>
</template>
<QMenu
v-if="$q.screen.gt.xs"
transition-show="scale"
transition-hide="scale"
v-model="isPopupOpen"
anchor="bottom left"
self="top start"
:no-focus="true"
:no-parent-event="true"
>
<VnDate
v-model="popupDate"
@update:model-value="manageDate"
:multiple="multiple"
/>
</QMenu>
<QDialog v-else v-model="isPopupOpen">
<VnDate
v-model="popupDate"
@update:model-value="manageDate"
:multiple="multiple"
/>
</QDialog>
</QInput>
</div>
</template>
<i18n>
es:
Open date: Abrir fecha
</i18n>

View File

@ -21,7 +21,7 @@ watch(
(newValue) => {
if (!modelValue.value) return;
modelValue.value = formatLocation(newValue) ?? null;
},
}
);
const mixinRules = [requiredFieldRule];
@ -45,7 +45,7 @@ const formatLocation = (obj, properties = locationProperties) => {
});
const filteredParts = parts.filter(
(part) => part !== null && part !== undefined && part !== '',
(part) => part !== null && part !== undefined && part !== ''
);
return filteredParts.join(', ');

View File

@ -1,7 +1,7 @@
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { date } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
@ -21,6 +21,7 @@ const stateStore = useStateStore();
const validationsStore = useValidator();
const { models } = validationsStore;
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const props = defineProps({
model: {
@ -272,7 +273,7 @@ onUnmounted(() => {
:data-key
:url="dataKey + 's'"
:user-filter="filter"
:user-params="{ originFk: route.params.id }"
:filter="{ where: { and: [{ originFk: route.params.id }] } }"
:skeleton="false"
auto-load
@on-fetch="setLogTree"

View File

@ -0,0 +1,80 @@
<script setup>
import { ref } from 'vue';
import VnCheckbox from './VnCheckbox.vue';
import axios from 'axios';
import { toRaw } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
const route = useRoute();
const { t } = useI18n();
const model = defineModel({ type: [Boolean] });
const props = defineProps({
expand: {
type: Boolean,
default: false,
},
url: {
type: String,
default: null,
required: true,
},
searchUrl: {
type: [String, Boolean],
default: 'table',
},
});
const value = ref(false);
const rows = ref(0);
const onClick = () => {
if (value.value) {
const { filter } = JSON.parse(route.query[props.searchUrl]);
filter.limit = 0;
const params = {
params: { filter: JSON.stringify(filter) },
};
axios
.get(props.url, params)
.then(({ data }) => {
rows.value = data;
})
.catch(console.error);
}
};
defineEmits(['update:selected', 'select:all']);
</script>
<template>
<div style="display: flex">
<VnCheckbox v-model="value" @click="$emit('update:selected', value)" />
<QBtn
v-if="value && $props.expand"
flat
dense
icon="expand_more"
@click="onClick"
>
<QMenu anchor="bottom right" self="top right">
<QList>
<QItem v-ripple clickable @click="$emit('select:all', toRaw(rows))">
{{ t('Select all', { rows: rows.length }) }}
</QItem>
<slot name="more-options"></slot>
</QList>
</QMenu>
</QBtn>
</div>
</template>
<i18n lang="yml">
en:
Select all: 'Select all ({rows})'
fr:
Select all: 'Sélectionner tout ({rows})'
es:
Select all: 'Seleccionar todo ({rows})'
de:
Select all: 'Alle auswählen ({rows})'
it:
Select all: 'Seleziona tutto ({rows})'
pt:
Select all: 'Selecionar tudo ({rows})'
</i18n>

View File

@ -2,7 +2,7 @@
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
const props = defineProps({
scrollTarget: { type: [String, Object], default: 'window' },
scrollTarget: { type: [String, Object], default: 'window' }
});
const scrollPosition = ref(0);
@ -11,9 +11,9 @@ let scrollContainer = null;
const onScroll = () => {
if (!scrollContainer) return;
scrollPosition.value =
typeof props.scrollTarget === 'object'
? scrollContainer.scrollTop
scrollPosition.value =
typeof props.scrollTarget === 'object'
? scrollContainer.scrollTop
: window.scrollY;
};
@ -28,18 +28,18 @@ const scrollToTop = () => {
};
const updateScrollContainer = (container) => {
if (container) {
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', onScroll);
}
scrollContainer = container;
scrollContainer.addEventListener('scroll', onScroll);
onScroll();
if (container) {
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', onScroll);
}
scrollContainer = container;
scrollContainer.addEventListener('scroll', onScroll);
onScroll();
}
};
defineExpose({
updateScrollContainer,
updateScrollContainer
});
const initScrollContainer = async () => {
@ -51,10 +51,11 @@ const initScrollContainer = async () => {
scrollContainer = window;
}
if (!scrollContainer) return;
scrollContainer.addEventListener('scroll', onScroll);
if (!scrollContainer) return
scrollContainer.addEventListener('scroll', onScroll);
};
onMounted(() => {
initScrollContainer();
});
@ -81,18 +82,19 @@ onUnmounted(() => {
<style scoped>
.scroll-to-top {
position: fixed;
top: 70px;
font-size: 65px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
transition: transform 0.2s ease-in-out;
position: fixed;
top: 70px;
font-size: 65px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
transition: transform 0.2s ease-in-out;
}
.scroll-to-top:hover {
transform: translateX(-50%) scale(1.2);
cursor: pointer;
filter: brightness(0.8);
transform: translateX(-50%) scale(1.2);
cursor: pointer;
filter: brightness(0.8);
}
</style>

View File

@ -124,7 +124,6 @@ const {
} = toRefs($props);
const myOptions = ref([]);
const myOptionsOriginal = ref([]);
const myOptionsMap = ref(new Map());
const vnSelectRef = ref();
const lastVal = ref();
const noOneText = t('globals.noOne');
@ -141,7 +140,7 @@ const styleAttrs = computed(() => {
}
: {};
});
const hasFocus = ref(false);
const isLoading = ref(false);
const useURL = computed(() => $props.url);
const value = computed({
get() {
@ -167,10 +166,6 @@ const computedSortBy = computed(() => {
return $props.sortBy || $props.optionLabel + ' ASC';
});
const valueIsObject = computed(
() => modelValue.value && typeof modelValue.value == 'object',
);
const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
watch(options, (newValue) => {
@ -178,22 +173,12 @@ watch(options, (newValue) => {
});
watch(modelValue, async (newValue) => {
if (newValue?.neq) newValue = newValue.neq;
if (!myOptions?.value?.some((option) => option[optionValue.value] == newValue))
await fetchFilter(newValue);
if ($props.noOne) myOptions.value.unshift(noOneOpt.value);
});
watch(
() => myOptionsOriginal.value,
(newValue) => {
for (const item of newValue) {
myOptionsMap.value.set(item[optionValue.value], item);
}
},
);
onMounted(() => {
setOptions(options.value);
if (useURL.value && $props.modelValue && !findKeyInOptions())
@ -202,7 +187,7 @@ onMounted(() => {
});
const someIsLoading = computed(
() => !!arrayData?.isLoading?.value && !isMenuOpened.value,
() => (isLoading.value || !!arrayData?.isLoading?.value) && !isMenuOpened.value,
);
function findKeyInOptions() {
if (!$props.options) return;
@ -239,9 +224,6 @@ function filter(val, options) {
async function fetchFilter(val) {
if (!$props.url) return;
if (val && typeof val == 'object') {
val = val.neq;
}
const { fields, include, limit } = $props;
const sortBy = computedSortBy.value;
@ -316,11 +298,13 @@ async function onScroll({ to, direction, from, index }) {
if (from === 0 && index === 0) return;
if (!useURL.value && !$props.fetchRef) return;
if (direction === 'decrease') return;
if (to === lastIndex && arrayData.store.hasMoreData) {
if (to === lastIndex && arrayData.store.hasMoreData && !isLoading.value) {
isLoading.value = true;
await arrayData.loadMore();
setOptions(arrayData.store.data);
vnSelectRef.value.scrollTo(lastIndex);
await nextTick();
isLoading.value = false;
}
}
@ -363,30 +347,22 @@ function getCaption(opt) {
if (optionCaption.value === false) return;
return opt[optionCaption.value] || opt[optionValue.value];
}
function getOptionLabel(property) {
if (!myOptionsMap.value.size) return;
let value = modelValue.value;
if (property) {
value = modelValue.value[property];
}
return myOptionsMap.value.get(value)?.[optionLabel.value];
}
</script>
<template>
<QSelect
ref="vnSelectRef"
v-model="value"
:options="myOptions"
:option-label="optionLabel"
:option-value="optionValue"
v-bind="{ ...$attrs, ...styleAttrs, hideSelected: hasFocus }"
v-bind="{ ...$attrs, ...styleAttrs }"
@filter="filterHandler"
:emit-value="nullishToTrue($attrs['emit-value'])"
:map-options="nullishToTrue($attrs['map-options'])"
:use-input="hasFocus || !value"
:fill-input="false"
:use-input="nullishToTrue($attrs['use-input'])"
:hide-selected="nullishToTrue($attrs['hide-selected'])"
:fill-input="nullishToTrue($attrs['fill-input'])"
ref="vnSelectRef"
lazy-rules
:class="{ required: isRequired }"
:rules="mixinRules"
@ -396,20 +372,10 @@ function getOptionLabel(property) {
:loading="someIsLoading"
@virtual-scroll="onScroll"
@popup-hide="isMenuOpened = false"
@popup-show="
async () => {
isMenuOpened = true;
hasFocus = true;
await $nextTick();
vnSelectRef?.$el?.querySelector('input')?.focus();
}
"
@popup-show="isMenuOpened = true"
@keydown="handleKeyDown"
:data-cy="$attrs.dataCy ?? $attrs.label + '_select'"
:data-url="url"
@blur="hasFocus = false"
@update:model-value="() => vnSelectRef.blur()"
:is-required="false"
>
<template #append>
<QIcon
@ -445,12 +411,7 @@ function getOptionLabel(property) {
</template>
<template #option="{ opt, itemProps }">
<QItem v-bind="itemProps">
<QItemSection v-if="typeof optionLabel === 'function'">{{
optionLabel(opt)
}}</QItemSection>
<QItemSection v-else-if="typeof opt !== 'object'">
{{ opt }}</QItemSection
>
<QItemSection v-if="typeof opt !== 'object'"> {{ opt }}</QItemSection>
<QItemSection v-else-if="opt[optionValue] == opt[optionLabel]">
<QItemLabel>{{ opt[optionLabel] }}</QItemLabel>
</QItemSection>
@ -464,17 +425,6 @@ function getOptionLabel(property) {
</QItemSection>
</QItem>
</template>
<template #selected v-if="valueIsObject && nullishToTrue($attrs['emit-value'])">
<span class="nowrap">
<span
class="text-strike"
v-if="modelValue?.neq"
v-text="getOptionLabel('neq')"
:title="getOptionLabel('neq')"
/>
<span v-else>{{ JSON.stringify(modelValue) }}</span>
</span>
</template>
</QSelect>
</template>
@ -482,12 +432,4 @@ function getOptionLabel(property) {
.q-field--outlined {
max-width: 100%;
}
.q-field__native {
@extend .nowrap;
}
.nowrap {
display: flex;
flex-wrap: nowrap !important;
}
</style>

View File

@ -1,40 +0,0 @@
<script setup>
import { computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import VnSelectDialog from './VnSelectDialog.vue';
import CreateNewExpenseForm from '../CreateNewExpenseForm.vue';
import { useArrayData } from 'src/composables/useArrayData';
const model = defineModel({ type: [String, Number, Object] });
const { t } = useI18n();
const arrayData = useArrayData('expenses', { url: 'Expenses' });
const expenses = computed(() => arrayData.store.data);
onMounted(async () => await arrayData.fetch({}));
async function updateModel(evt) {
await arrayData.fetch({});
model.value = evt.id;
}
</script>
<template>
<VnSelectDialog
v-bind="$attrs"
ref="selectDialogRef"
v-model="model"
:options="expenses"
option-value="id"
:option-label="(x) => `${x.id}: ${x.name}`"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
:acls="[{ model: 'Expense', props: '*', accessType: 'WRITE' }]"
>
<template #form>
<CreateNewExpenseForm @on-data-saved="updateModel" />
</template>
</VnSelectDialog>
</template>
<i18n>
es:
Create a new expense: Crear nuevo gasto
</i18n>

View File

@ -40,11 +40,4 @@ describe('VnBankDetail Component', () => {
await vm.autofillBic('ES1234567891324567891234');
expect(vm.bankEntityFk).toBe(null);
});
it('should not update bankEntityFk if IBAN country is not ES', async () => {
vm.bankEntities = bankEntities;
await vm.autofillBic('FR1420041010050500013M02606');
expect(vm.bankEntityFk).toBe(null);
});
});

View File

@ -35,7 +35,7 @@ describe('VnSmsDialog', () => {
expect.objectContaining({
message: 'You must enter a new password',
type: 'negative',
}),
})
);
});
@ -47,7 +47,7 @@ describe('VnSmsDialog', () => {
expect.objectContaining({
message: `Passwords don't match`,
type: 'negative',
}),
})
);
});

View File

@ -25,9 +25,6 @@ describe('VnDmsList', () => {
deleteModel: 'WorkerDms',
downloadModel: 'WorkerDms',
},
global: {
stubs: ['VnUserLink'],
},
}).vm;
});

View File

@ -60,4 +60,4 @@ describe('VnInputTime', () => {
expect(vm.model).toBe(previousModel);
});
});
});
});

View File

@ -6,8 +6,8 @@ function buildComponent(data) {
return createWrapper(VnLocation, {
global: {
props: {
location: data,
},
location: data
}
},
}).vm;
}
@ -24,7 +24,7 @@ describe('formatLocation', () => {
postcode: '46680',
city: 'Algemesi',
province: { name: 'Valencia' },
country: { name: 'Spain' },
country: { name: 'Spain' }
};
});
@ -47,12 +47,7 @@ describe('formatLocation', () => {
});
it('should return the country', () => {
const location = {
...locationBase,
postcode: undefined,
city: undefined,
province: undefined,
};
const location = { ...locationBase, postcode: undefined, city: undefined, province: undefined };
const vm = buildComponent(location);
expect(vm.formatLocation(location)).toEqual('Spain');
});
@ -66,7 +61,7 @@ describe('showLabel', () => {
code: '46680',
town: 'Algemesi',
province: 'Valencia',
country: 'Spain',
country: 'Spain'
};
});
@ -89,13 +84,8 @@ describe('showLabel', () => {
});
it('should show the label with country', () => {
const location = {
...locationBase,
code: undefined,
town: undefined,
province: undefined,
};
const location = { ...locationBase, code: undefined, town: undefined, province: undefined };
const vm = buildComponent(location);
expect(vm.showLabel(location)).toEqual('Spain');
});
});
});

View File

@ -90,7 +90,7 @@ describe('VnLog', () => {
vm = createWrapper(VnLog, {
global: {
stubs: ['FetchData', 'vue-i18n', 'VnUserLink'],
stubs: ['FetchData', 'vue-i18n'],
mocks: {
fetch: vi.fn(),
},

View File

@ -2,14 +2,13 @@ import { createWrapper } from 'app/test/vitest/helper';
import VnSmsDialog from 'components/common/VnSmsDialog.vue';
import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest';
describe('VnSmsDialog', () => {
let vm;
const orderId = 1;
const shipped = new Date();
const phone = '012345678';
const promise = (response) => {
return response;
};
const promise = (response) => {return response;};
const template = 'minAmount';
const locale = 'en';
@ -18,13 +17,13 @@ describe('VnSmsDialog', () => {
propsData: {
data: {
orderId,
shipped,
shipped
},
template,
locale,
phone,
promise,
},
promise
}
}).vm;
});
@ -36,9 +35,7 @@ describe('VnSmsDialog', () => {
it('should update the message value with the correct template and parameters', () => {
vm.updateMessage();
expect(vm.message).toEqual(
`A minimum amount of 50€ (VAT excluded) is required for your order ${orderId} of ${shipped} to receive it without additional shipping costs.`,
);
expect(vm.message).toEqual(`A minimum amount of 50€ (VAT excluded) is required for your order ${orderId} of ${shipped} to receive it without additional shipping costs.`);
});
});
@ -50,7 +47,7 @@ describe('VnSmsDialog', () => {
orderId,
shipped,
destination: phone,
message: vm.message,
message: vm.message
};
await vm.send();

View File

@ -162,6 +162,7 @@ async function fetch() {
align-items: start;
.label {
color: var(--vn-label-color);
width: 9em;
overflow: hidden;
white-space: wrap;
text-overflow: ellipsis;

View File

@ -42,7 +42,7 @@ const card = toRef(props, 'item');
</div>
<div class="content">
<span class="link" @click.stop>
{{ card.longName }}
{{ card.name }}
<ItemDescriptorProxy :id="card.id" />
</span>
<p class="subName">{{ card.subName }}</p>
@ -57,12 +57,11 @@ const card = toRef(props, 'item');
<QIcon name="production_quantity_limits" size="xs" />
{{ card.minQuantity }}
</div>
<div class="footer q-mt-auto">
<div class="footer">
<div class="price">
<p v-if="isCatalog">
<span class="text-primary">{{ card.available }}</span>
{{ t('to') }}
<span class="text-bold" >{{ toCurrency(card.price) }}</span>
{{ card.available }} {{ t('to') }}
{{ toCurrency(card.price) }}
</p>
<slot name="price" />
<QIcon v-if="isCatalog" name="add_circle" class="icon">
@ -133,7 +132,8 @@ const card = toRef(props, 'item');
display: flex;
flex-direction: column;
gap: 4px;
white-space: nowrap;
width: 192px;
p {
margin-bottom: 0;
}
@ -145,29 +145,28 @@ const card = toRef(props, 'item');
}
.footer {
.price {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
align-items: center;
justify-content: space-between;
.price {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
align-items: center;
justify-content: space-between;
p {
font-size: 12px;
}
p {
font-size: 12px;
.icon {
color: $primary;
font-size: 24px;
cursor: pointer;
}
}
.icon {
color: $primary;
font-size: 24px;
cursor: pointer;
.price-kg {
font-size: 12px;
}
}
.price-kg {
font-size: 12px;
}
}
.item-color-container {

View File

@ -1,14 +1,14 @@
<template>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square />
</div>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square />
</div>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square />
</div>
</template>
</template>

View File

@ -17,7 +17,7 @@ const token = getTokenMultimedia();
const { t } = useI18n();
const src = computed(
() => `/api/Images/user/160x160/${$props.workerId}/download?access_token=${token}`,
() => `/api/Images/user/160x160/${$props.workerId}/download?access_token=${token}`
);
const title = computed(() => $props.title?.toUpperCase() || t('globals.system'));
const showLetter = ref(false);

View File

@ -236,6 +236,10 @@ const toModule = computed(() => {
.label {
color: var(--vn-label-color);
font-size: 14px;
&:not(:has(a))::after {
content: ':';
}
}
&.ellipsis > .value {
text-overflow: ellipsis;

View File

@ -186,7 +186,6 @@ async function remove(key) {
function formatValue(value) {
if (typeof value === 'boolean') return value ? t('Yes') : t('No');
if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value);
if (value && typeof value === 'object') return '';
return `"${value}"`;
}

View File

@ -1,9 +1,8 @@
<script setup>
import { ref } from 'vue';
import { useSession } from 'src/composables/useSession';
import noUser from '/no-user.png';
import noImage from '/no-user.png';
import { useRole } from 'src/composables/useRole';
import { getDarkSuffix } from 'src/composables/getDarkSuffix';
const $props = defineProps({
storage: {
@ -44,7 +43,7 @@ const getUrl = (zoom = false) => {
return `/api/${$props.storage}/${$props.id}/downloadFile?access_token=${token}`;
return isEmployee
? `/api/${$props.storage}/${$props.collection}/${curResolution}/${$props.id}/download?access_token=${token}&${timeStamp.value}`
: noUser;
: noImage;
};
const reload = () => {
timeStamp.value = `timestamp=${Date.now()}`;
@ -61,7 +60,6 @@ defineExpose({
v-bind="$attrs"
@click.stop="show = $props.zoom"
spinner-color="primary"
:error-src="`/no_image${getDarkSuffix()}.png`"
/>
<QDialog v-if="$props.zoom" v-model="show">
<QImg

View File

@ -1,6 +1,6 @@
<script setup>
import { Dark } from 'quasar';
import { computed } from 'vue';
import { getDarkSuffix } from 'src/composables/getDarkSuffix';
const $props = defineProps({
logo: {
@ -12,8 +12,8 @@ const $props = defineProps({
const src = computed({
get() {
return new URL(
`../../assets/${$props.logo}${getDarkSuffix()}.svg`,
import.meta.url,
`../../assets/${$props.logo}${Dark.isActive ? '_dark' : ''}.svg`,
import.meta.url
).href;
},
});

View File

@ -42,10 +42,12 @@ const val = computed(() => $props.value);
<div v-if="label || $slots.label" class="label">
<slot name="label">
<QTooltip v-if="tooltip">{{ tooltip }}</QTooltip>
<span style="color: var(--vn-label-color)"> {{ label }}: </span>
<span style="color: var(--vn-label-color)">
{{ label }}
</span>
</slot>
</div>
<div class="value">
<div class="value" v-if="value || $slots.value">
<slot name="value">
<span :title="value" style="text-overflow: ellipsis">
{{ dash ? dashIfEmpty(value) : value }}
@ -73,13 +75,21 @@ const val = computed(() => $props.value);
visibility: visible;
cursor: pointer;
}
.label,
.value {
white-space: pre-line;
word-wrap: break-word;
}
.copy {
visibility: hidden;
}
.info {
margin-left: 5px;
}
}
:deep(.q-checkbox.disabled) {
opacity: 1 !important;
}

View File

@ -1,11 +1,10 @@
<script setup>
import axios from 'axios';
import { ref, reactive, useAttrs, computed, onMounted, nextTick } from 'vue';
import { onBeforeRouteLeave, useRouter, useRoute } from 'vue-router';
import { ref, reactive, useAttrs, computed } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { tMobile } from 'src/composables/tMobile';
import { toDateHourMin } from 'src/filters';
import VnPaginate from 'components/ui/VnPaginate.vue';
@ -34,47 +33,20 @@ const $props = defineProps({
addNote: { type: Boolean, default: false },
selectType: { type: Boolean, default: false },
justInput: { type: Boolean, default: false },
goTo: { type: String, default: '' },
useUserRelation: { type: Boolean, default: true },
});
const { t } = useI18n();
const quasar = useQuasar();
const stateStore = useStateStore();
const router = useRouter();
const route = useRoute();
const componentIsRendered = ref(false);
const newNote = reactive({ text: null, observationTypeFk: null });
const observationTypes = ref([]);
const vnPaginateRef = ref();
const defaultObservationType = computed(
() => observationTypes.value.find((ot) => ot.code === 'salesPerson')?.id,
const defaultObservationType = computed(() =>
observationTypes.value.find(ot => ot.code === 'salesPerson')?.id
);
let savedNote = false;
let originalText;
onBeforeRouteLeave((to, from, next) => {
if (
(newNote.text && !$props.justInput) ||
(newNote.text !== originalText && $props.justInput)
)
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('globals.unsavedPopup.title'),
message: t('globals.unsavedPopup.subtitle'),
promise: () => next(),
},
});
else next();
});
onMounted(() => {
nextTick(() => (componentIsRendered.value = true));
});
function handleClick(e) {
if (e.shiftKey && e.key === 'Enter') return;
if ($props.justInput) confirmAndUpdate();
@ -96,7 +68,6 @@ async function insert() {
};
await axios.post($props.url, newBody);
await vnPaginateRef.value.fetch();
savedNote = true;
}
function confirmAndUpdate() {
@ -129,6 +100,22 @@ async function update() {
);
}
onBeforeRouteLeave((to, from, next) => {
if (
(newNote.text && !$props.justInput) ||
(newNote.text !== originalText && $props.justInput)
)
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('globals.unsavedPopup.title'),
message: t('globals.unsavedPopup.subtitle'),
promise: () => next(),
},
});
else next();
});
function fetchData([data]) {
newNote.text = data?.notes;
originalText = data?.notes;
@ -137,71 +124,13 @@ function fetchData([data]) {
const handleObservationTypes = (data) => {
observationTypes.value = data;
if (defaultObservationType.value) {
if(defaultObservationType.value) {
newNote.observationTypeFk = defaultObservationType.value;
}
};
async function saveAndGo() {
savedNote = false;
await insert();
router.push({ path: $props.goTo });
}
function getUserFilter() {
const newUserFilter = $props.userFilter ?? {};
const userInclude = {
relation: 'user',
scope: {
fields: ['id', 'nickname', 'name'],
},
};
if (newUserFilter.include) {
if (Array.isArray(newUserFilter.include)) {
newUserFilter.include.push(userInclude);
} else {
newUserFilter.include = [userInclude, newUserFilter.include];
}
} else {
newUserFilter.include = userInclude;
}
if ($props.useUserRelation) {
return {
...newUserFilter,
...$props.userFilter,
};
}
return $props.filter;
}
</script>
<template>
<Teleport
to="#st-actions"
v-if="
stateStore?.isSubToolbarShown() &&
componentIsRendered &&
$props.goTo &&
!route.path.includes('summary')
"
>
<QBtn
:label="
tMobile('globals.saveAndContinue') +
' ' +
t('globals.' + $props.goTo.split('/').pop())
"
:title="
t('globals.saveAndContinue') +
' ' +
t('globals.' + $props.goTo.split('/').pop())
"
color="primary"
icon="save"
@click="saveAndGo"
data-cy="saveContinueNoteButton"
/>
</Teleport>
<FetchData
v-if="selectType"
url="ObservationTypes"
@ -247,7 +176,7 @@ function getUserFilter() {
:required="'required' in originalAttrs"
clearable
>
<template #append v-if="!$props.goTo">
<template #append>
<QBtn
:title="t('Save (Enter)')"
icon="save"
@ -269,14 +198,16 @@ function getUserFilter() {
:url="$props.url"
order="created DESC"
:limit="0"
:user-filter="getUserFilter()"
:user-filter="userFilter"
:filter="filter"
auto-load
ref="vnPaginateRef"
class="show"
v-bind="$attrs"
:search-url="false"
@on-fetch="newNote.text = ''"
@on-fetch="
newNote.text = '';
"
>
<template #body="{ rows }">
<TransitionGroup name="list" tag="div" class="column items-center full-width">
@ -288,15 +219,15 @@ function getUserFilter() {
<QCardSection horizontal>
<VnAvatar
:descriptor="false"
:worker-id="note.user?.id"
:worker-id="note.workerFk"
size="md"
:title="note.user?.nickname"
:title="note.worker?.user.nickname"
/>
<div class="full-width row justify-between q-pa-xs">
<div>
<VnUserLink
:name="`${note.user?.name}`"
:worker-id="note.user?.id"
:name="`${note.worker.user.name}`"
:worker-id="note.worker.id"
/>
<QBadge
class="q-ml-xs"

View File

@ -87,7 +87,7 @@ function formatNumber(number) {
<QItemLabel caption>{{
date.formatDate(
row.sms.created,
'YYYY-MM-DD HH:mm:ss',
'YYYY-MM-DD HH:mm:ss'
)
}}</QItemLabel>
<QItemLabel class="row center">

View File

@ -1,5 +1,5 @@
<script setup>
import { toCurrency, toPercentage } from 'filters/index';
import { toPercentage } from 'filters/index';
import { computed } from 'vue';
@ -8,10 +8,6 @@ const props = defineProps({
type: Number,
required: true,
},
format: {
type: String,
default: 'percentage', // 'currency'
},
});
const valueClass = computed(() =>
@ -25,10 +21,7 @@ const formattedValue = computed(() => props.value);
<template>
<span :class="valueClass">
<QIcon :name="iconName" size="sm" class="value-icon" />
<span v-if="$props.format === 'percentage'">{{
toPercentage(formattedValue)
}}</span>
<span v-if="$props.format === 'currency'">{{ toCurrency(formattedValue) }}</span>
{{ toPercentage(formattedValue) }}
</span>
</template>

View File

@ -37,7 +37,8 @@ onBeforeUnmount(() => stateStore.toggleSubToolbar() && hasSubToolbar);
class="justify-end sticky"
>
<slot name="st-data">
<div id="st-data" :class="{ 'full-width': !actionsChildCount() }"></div>
<div id="st-data" :class="{ 'full-width': !actionsChildCount() }">
</div>
</slot>
<QSpace />
<slot name="st-actions">

View File

@ -1,39 +1,18 @@
<script setup>
import AccountDescriptorProxy from 'src/pages/Account/Card/AccountDescriptorProxy.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import { ref, onMounted } from 'vue';
import axios from 'axios';
const $props = defineProps({
defineProps({
name: { type: String, default: null },
tag: { type: String, default: null },
workerId: { type: Number, default: null },
defaultName: { type: Boolean, default: false },
});
const isWorker = ref(false);
onMounted(async () => {
if (!$props.workerId) return;
try {
const {
data: { exists },
} = await axios(`/Workers/${$props.workerId}/exists`);
isWorker.value = exists;
} catch (error) {
if (error.status === 403) return;
throw error;
}
});
</script>
<template>
<slot name="link">
<span :class="{ link: workerId }">
{{ defaultName ? (name ?? $t('globals.system')) : name }}
{{ defaultName ? name ?? $t('globals.system') : name }}
</span>
</slot>
<WorkerDescriptorProxy
v-if="isWorker"
:id="workerId"
@on-fetch="(data) => (isWorker = data?.workerId !== undefined)"
/>
<AccountDescriptorProxy v-else :id="workerId" />
<WorkerDescriptorProxy v-if="workerId" :id="workerId" />
</template>

View File

@ -54,7 +54,7 @@ describe('tags computed property', () => {
const expectedStyle = {
'grid-template-columns': 'repeat(2, 1fr)',
'max-width': '8rem',
'max-width': '8rem',
};
expect(vm.columnStyle).toEqual(expectedStyle);

View File

@ -9,29 +9,30 @@ const isEmployeeMock = vi.fn();
function generateWrapper(storage = 'images') {
wrapper = createWrapper(VnImg, {
props: {
id: 123,
id: 123,
zoomResolution: '400x400',
storage,
},
storage,
}
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
vm.timeStamp = 'timestamp';
}
vm = wrapper.vm;
vm.timeStamp = 'timestamp';
};
vi.mock('src/composables/useSession', () => ({
useSession: () => ({
getTokenMultimedia: () => 'token',
getTokenMultimedia: () => 'token',
}),
}));
vi.mock('src/composables/useRole', () => ({
useRole: () => ({
isEmployee: isEmployeeMock,
}),
}));
describe('VnImg', () => {
describe('VnImg', () => {
beforeEach(() => {
isEmployeeMock.mockReset();
});
@ -46,13 +47,13 @@ describe('VnImg', () => {
generateWrapper('dms');
await vm.$nextTick();
const url = vm.getUrl();
expect(url).toBe('/api/dms/123/downloadFile?access_token=token');
expect(url).toBe('/api/dms/123/downloadFile?access_token=token');
});
it('should return /no-user.png when role is not employee and storage is not dms', async () => {
isEmployeeMock.mockReturnValue(false);
generateWrapper();
await vm.$nextTick();
await vm.$nextTick();
const url = vm.getUrl();
expect(url).toBe('/no-user.png');
});
@ -62,9 +63,7 @@ describe('VnImg', () => {
generateWrapper();
await vm.$nextTick();
const url = vm.getUrl();
expect(url).toBe(
'/api/images/catalog/200x200/123/download?access_token=token&timestamp',
);
expect(url).toBe('/api/images/catalog/200x200/123/download?access_token=token&timestamp');
});
it('should return /api/{storage}/{collection}/{curResolution}/{id}/download?access_token={token}&{timeStamp} when zoom is true and role is employee and storage is not dms', async () => {
@ -72,9 +71,7 @@ describe('VnImg', () => {
generateWrapper();
await vm.$nextTick();
const url = vm.getUrl(true);
expect(url).toBe(
'/api/images/catalog/400x400/123/download?access_token=token&timestamp',
);
expect(url).toBe('/api/images/catalog/400x400/123/download?access_token=token&timestamp');
});
});
@ -85,8 +82,8 @@ describe('VnImg', () => {
wrapper.vm.reload();
const newTimestamp = wrapper.vm.timeStamp;
expect(initialTimestamp).not.toEqual(newTimestamp);
});
});
});
});

View File

@ -53,26 +53,26 @@ describe('useAcl', () => {
expect(
acl.hasAny([
{ model: 'Worker', props: 'updateAttributes', accessType: 'WRITE' },
]),
])
).toBeFalsy();
});
it('should return false if no roles matched', async () => {
expect(
acl.hasAny([{ model: 'Worker', props: 'holidays', accessType: 'READ' }]),
acl.hasAny([{ model: 'Worker', props: 'holidays', accessType: 'READ' }])
).toBeTruthy();
});
describe('*', () => {
it('should return true if an acl matched', async () => {
expect(
acl.hasAny([{ model: 'Address', props: '*', accessType: 'WRITE' }]),
acl.hasAny([{ model: 'Address', props: '*', accessType: 'WRITE' }])
).toBeTruthy();
});
it('should return false if no acls matched', async () => {
expect(
acl.hasAny([{ model: 'Worker', props: '*', accessType: 'READ' }]),
acl.hasAny([{ model: 'Worker', props: '*', accessType: 'READ' }])
).toBeFalsy();
});
});
@ -80,15 +80,13 @@ describe('useAcl', () => {
describe('$authenticated', () => {
it('should return false if no acls matched', async () => {
expect(
acl.hasAny([{ model: 'Url', props: 'getByUser', accessType: '*' }]),
acl.hasAny([{ model: 'Url', props: 'getByUser', accessType: '*' }])
).toBeFalsy();
});
it('should return true if an acl matched', async () => {
expect(
acl.hasAny([
{ model: 'Url', props: 'getByUser', accessType: 'READ' },
]),
acl.hasAny([{ model: 'Url', props: 'getByUser', accessType: 'READ' }])
).toBeTruthy();
});
});
@ -98,7 +96,7 @@ describe('useAcl', () => {
expect(
acl.hasAny([
{ model: 'TpvTransaction', props: 'start', accessType: 'READ' },
]),
])
).toBeFalsy();
});
@ -106,7 +104,7 @@ describe('useAcl', () => {
expect(
acl.hasAny([
{ model: 'TpvTransaction', props: 'start', accessType: 'WRITE' },
]),
])
).toBeTruthy();
});
});

View File

@ -18,7 +18,7 @@ export async function downloadFile(id, model = 'dms', urlPath = '/downloadFile',
export async function downloadDocuware(url, params) {
const appUrl = await getAppUrl();
const response = await axios.get(`${appUrl}/api/${url}`, {
const response = await axios.get(`${appUrl}/api/` + url, {
responseType: 'blob',
params,
});

View File

@ -9,6 +9,8 @@ export function getColAlign(col) {
case 'number':
align = 'right';
break;
case 'time':
case 'date':
case 'checkbox':
align = 'center';
break;
@ -18,5 +20,5 @@ export function getColAlign(col) {
if (/^is[A-Z]/.test(col.name) || /^has[A-Z]/.test(col.name)) align = 'center';
return `text-${align ?? 'center'}`;
return 'text-' + (align ?? 'center');
}

View File

@ -1,4 +0,0 @@
import { Dark } from 'quasar';
export function getDarkSuffix() {
return Dark.isActive ? '_dark' : '';
}

View File

@ -1,10 +1,10 @@
export function getDateQBadgeColor(date) {
const today = Date.vnNew();
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
const timeTicket = new Date(date);
let timeTicket = new Date(date);
timeTicket.setHours(0, 0, 0, 0);
const comparation = today - timeTicket;
let comparation = today - timeTicket;
if (comparation == 0) return 'warning';
if (comparation < 0) return 'success';

View File

@ -8,4 +8,4 @@ export function getValueFromPath(root, path) {
else current = current[key];
}
return current;
}
}

View File

@ -2,8 +2,9 @@ import { ref } from 'vue';
import moment from 'moment';
export default function useWeekdaysOrder() {
const firstDay = moment().weekday(1).day();
const weekdays = [...Array(7).keys()].map((i) => (i + firstDay) % 7);
return ref(weekdays);
const firstDay = moment().weekday(1).day();
const weekdays = [...Array(7).keys()].map(i => (i + firstDay) % 7);
return ref(weekdays);
}

View File

@ -7,7 +7,7 @@ export async function beforeSave(data, getChanges, modelOrigin) {
const patchPromises = [];
for (const change of changes) {
const patchData = {};
let patchData = {};
if ('hasMinPrice' in change.data) {
patchData.hasMinPrice = change.data?.hasMinPrice;

View File

@ -19,7 +19,7 @@ export function useArrayData(key, userOptions) {
let canceller = null;
onMounted(() => {
setOptions(userOptions ?? {});
setOptions();
reset(['skip']);
const query = route.query;
@ -39,10 +39,9 @@ export function useArrayData(key, userOptions) {
setCurrentFilter();
});
if (userOptions) setOptions(userOptions);
if (key && userOptions) setOptions();
function setOptions(params) {
if (!params) return;
function setOptions(params = userOptions) {
const allowedOptions = [
'url',
'filter',
@ -313,7 +312,6 @@ export function useArrayData(key, userOptions) {
const { params, limit } = getCurrentFilter();
store.currentFilter = JSON.parse(JSON.stringify(params));
delete store.currentFilter.filter.include;
delete store.currentFilter.filter.fields;
store.currentFilter.filter = JSON.stringify(store.currentFilter.filter);
return { params, limit };
}

View File

@ -1,6 +1,7 @@
import { useQuasar } from 'quasar';
export default function () {
export default function() {
const quasar = useQuasar();
return quasar.screen.gt.xs ? 'q-pa-md' : 'q-pa-xs';
return quasar.screen.gt.xs ? 'q-pa-md': 'q-pa-xs';
}

View File

@ -6,7 +6,7 @@ export function djb2a(string) {
}
export function useColor(value) {
return `#${colors[djb2a(value || '') % colors.length]}`;
return '#' + colors[djb2a(value || '') % colors.length];
}
const colors = [

View File

@ -15,16 +15,18 @@ export function usePrintService() {
message: t('globals.notificationSent'),
type: 'positive',
icon: 'check',
}),
})
);
}
function openReport(path, params, isNewTab = '_self') {
if (typeof params === 'string') params = JSON.parse(params);
params = {
access_token: getTokenMultimedia(),
...params,
};
params = Object.assign(
{
access_token: getTokenMultimedia(),
},
params
);
const query = new URLSearchParams(params).toString();
window.open(`api/${path}?${query}`, isNewTab);

View File

@ -17,7 +17,7 @@ export function useSession() {
let intervalId = null;
function setSession(data) {
const keepLogin = data.keepLogin;
let keepLogin = data.keepLogin;
const storage = keepLogin ? localStorage : sessionStorage;
storage.setItem(TOKEN, data.token);
storage.setItem(TOKEN_MULTIMEDIA, data.tokenMultimedia);

View File

@ -47,9 +47,7 @@ export function useValidator() {
return !validator.isEmpty(value ? String(value) : '') || message;
},
required: (required, value) => {
return required
? value === 0 || !!value || t('globals.fieldRequired')
: null;
return required ? !!value || t('globals.fieldRequired') : null;
},
length: (value) => {
const options = {

View File

@ -8,7 +8,7 @@ export function useVnConfirm() {
message,
promise,
successFn,
customHTML = {},
customHTML = {}
) => {
const { component, props } = customHTML;
Dialog.create({
@ -19,7 +19,7 @@ export function useVnConfirm() {
message: message,
promise: promise,
},
{ customHTML: () => h(component, props) },
{ customHTML: () => h(component, props) }
),
}).onOk(async () => {
if (successFn) successFn();

View File

@ -16,8 +16,6 @@ body.body--light {
--vn-black-text-color: black;
--vn-text-color-contrast: white;
--vn-link-color: #1e90ff;
--vn-input-underline-color: #bdbdbd;
--vn-input-icons-color: #797979;
background-color: var(--vn-page-color);
@ -31,7 +29,7 @@ body.body--light {
body.body--dark {
--vn-header-color: #5d5d5d;
--vn-page-color: #222;
--vn-section-color: #3c3b3b;
--vn-section-color: #3d3d3d;
--vn-section-hover-color: #747474;
--vn-text-color: white;
--vn-label-color: #a8a8a8;
@ -42,8 +40,6 @@ body.body--dark {
--vn-black-text-color: black;
--vn-text-color-contrast: black;
--vn-link-color: #66bfff;
--vn-input-underline-color: #545353;
--vn-input-icons-color: #888787;
background-color: var(--vn-page-color);
@ -159,6 +155,7 @@ select:-webkit-autofill {
cursor: pointer;
}
/* Estilo para el asterisco en campos requeridos */
.q-field.required .q-field__label:after {
content: ' *';
}
@ -293,18 +290,6 @@ input::-webkit-inner-spin-button {
.expand {
max-width: 400px;
}
th {
border-bottom: 1px solid var(--vn-page-color) !important;
}
td {
border-color: var(--vn-page-color);
}
div.q-field__append.q-field__marginal {
color: var(--vn-input-icons-color) !important;
}
.q-field__control:before {
border-color: var(--vn-input-underline-color) !important;
}
}
.edit-photo-btn {
@ -374,4 +359,4 @@ input::-webkit-inner-spin-button {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,455 +1,453 @@
@font-face {
font-family: 'icon';
src: url('fonts/icon.eot?uocffs');
src:
url('fonts/icon.eot?uocffs#iefix') format('embedded-opentype'),
url('fonts/icon.ttf?uocffs') format('truetype'),
url('fonts/icon.woff?uocffs') format('woff'),
url('fonts/icon.svg?uocffs#icon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
font-family: 'icon';
src: url('fonts/icon.eot?uocffs');
src: url('fonts/icon.eot?uocffs#iefix') format('embedded-opentype'),
url('fonts/icon.ttf?uocffs') format('truetype'),
url('fonts/icon.woff?uocffs') format('woff'),
url('fonts/icon.svg?uocffs#icon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
[class^='icon-'],
[class*=' icon-'] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icon' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
[class^="icon-"], [class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icon' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-inactive-car:before {
content: '\e978';
content: "\e978";
}
.icon-hasItemLost:before {
content: '\e957';
content: "\e957";
}
.icon-hasItemDelay:before {
content: '\e96d';
content: "\e96d";
}
.icon-add_entries:before {
content: '\e953';
content: "\e953";
}
.icon-100:before {
content: '\e901';
content: "\e901";
}
.icon-Client_unpaid:before {
content: '\e98c';
content: "\e98c";
}
.icon-History:before {
content: '\e902';
content: "\e902";
}
.icon-Person:before {
content: '\e903';
content: "\e903";
}
.icon-accessory:before {
content: '\e904';
content: "\e904";
}
.icon-account:before {
content: '\e905';
content: "\e905";
}
.icon-actions:before {
content: '\e907';
content: "\e907";
}
.icon-addperson:before {
content: '\e908';
content: "\e908";
}
.icon-agencia_tributaria:before {
content: '\e948';
content: "\e948";
}
.icon-agency:before {
content: '\e92a';
content: "\e92a";
}
.icon-agency-term:before {
content: '\e909';
content: "\e909";
}
.icon-albaran:before {
content: '\e92c';
content: "\e92c";
}
.icon-anonymous:before {
content: '\e90b';
content: "\e90b";
}
.icon-apps:before {
content: '\e90c';
content: "\e90c";
}
.icon-artificial:before {
content: '\e90d';
content: "\e90d";
}
.icon-attach:before {
content: '\e90e';
content: "\e90e";
}
.icon-barcode:before {
content: '\e90f';
content: "\e90f";
}
.icon-basket:before {
content: '\e910';
content: "\e910";
}
.icon-basketadd:before {
content: '\e911';
content: "\e911";
}
.icon-bin:before {
content: '\e913';
content: "\e913";
}
.icon-botanical:before {
content: '\e914';
content: "\e914";
}
.icon-bucket:before {
content: '\e915';
content: "\e915";
}
.icon-buscaman:before {
content: '\e916';
content: "\e916";
}
.icon-buyrequest:before {
content: '\e917';
content: "\e917";
}
.icon-calc_volum .path1:before {
content: '\e918';
color: rgb(0, 0, 0);
content: "\e918";
color: rgb(0, 0, 0);
}
.icon-calc_volum .path2:before {
content: '\e919';
margin-left: -1em;
color: rgb(0, 0, 0);
content: "\e919";
margin-left: -1em;
color: rgb(0, 0, 0);
}
.icon-calc_volum .path3:before {
content: '\e91c';
margin-left: -1em;
color: rgb(0, 0, 0);
content: "\e91c";
margin-left: -1em;
color: rgb(0, 0, 0);
}
.icon-calc_volum .path4:before {
content: '\e91d';
margin-left: -1em;
color: rgb(0, 0, 0);
content: "\e91d";
margin-left: -1em;
color: rgb(0, 0, 0);
}
.icon-calc_volum .path5:before {
content: '\e91e';
margin-left: -1em;
color: rgb(0, 0, 0);
content: "\e91e";
margin-left: -1em;
color: rgb(0, 0, 0);
}
.icon-calc_volum .path6:before {
content: '\e91f';
margin-left: -1em;
color: rgb(255, 255, 255);
content: "\e91f";
margin-left: -1em;
color: rgb(255, 255, 255);
}
.icon-calendar:before {
content: '\e920';
content: "\e920";
}
.icon-catalog:before {
content: '\e921';
content: "\e921";
}
.icon-claims:before {
content: '\e922';
content: "\e922";
}
.icon-client:before {
content: '\e923';
content: "\e923";
}
.icon-clone:before {
content: '\e924';
content: "\e924";
}
.icon-columnadd:before {
content: '\e925';
content: "\e925";
}
.icon-columndelete:before {
content: '\e926';
content: "\e926";
}
.icon-components:before {
content: '\e927';
content: "\e927";
}
.icon-consignatarios:before {
content: '\e928';
content: "\e928";
}
.icon-control:before {
content: '\e929';
content: "\e929";
}
.icon-credit:before {
content: '\e92b';
content: "\e92b";
}
.icon-defaulter:before {
content: '\e92d';
content: "\e92d";
}
.icon-deletedTicket:before {
content: '\e92e';
content: "\e92e";
}
.icon-deleteline:before {
content: '\e92f';
content: "\e92f";
}
.icon-delivery:before {
content: '\e930';
content: "\e930";
}
.icon-deliveryprices:before {
content: '\e932';
content: "\e932";
}
.icon-details:before {
content: '\e933';
content: "\e933";
}
.icon-dfiscales:before {
content: '\e934';
content: "\e934";
}
.icon-disabled:before {
content: '\e935';
content: "\e935";
}
.icon-doc:before {
content: '\e936';
content: "\e936";
}
.icon-entry:before {
content: '\e937';
content: "\e937";
}
.icon-entry_lastbuys:before {
content: '\e91a';
content: "\e91a";
}
.icon-exit:before {
content: '\e938';
content: "\e938";
}
.icon-eye:before {
content: '\e939';
content: "\e939";
}
.icon-fixedPrice:before {
content: '\e93a';
content: "\e93a";
}
.icon-flower:before {
content: '\e93b';
content: "\e93b";
}
.icon-frozen:before {
content: '\e93c';
content: "\e93c";
}
.icon-fruit:before {
content: '\e93d';
content: "\e93d";
}
.icon-funeral:before {
content: '\e93e';
content: "\e93e";
}
.icon-grafana:before {
content: '\e906';
content: "\e906";
}
.icon-greenery:before {
content: '\e93f';
content: "\e93f";
}
.icon-greuge:before {
content: '\e940';
content: "\e940";
}
.icon-grid:before {
content: '\e941';
content: "\e941";
}
.icon-handmade:before {
content: '\e942';
content: "\e942";
}
.icon-handmadeArtificial:before {
content: '\e943';
content: "\e943";
}
.icon-headercol:before {
content: '\e945';
content: "\e945";
}
.icon-info:before {
content: '\e946';
content: "\e946";
}
.icon-inventory:before {
content: '\e947';
content: "\e947";
}
.icon-invoice:before {
content: '\e968';
color: #5f5f5f;
content: "\e968";
color: #5f5f5f;
}
.icon-invoice-in:before {
content: '\e949';
content: "\e949";
}
.icon-invoice-in-create:before {
content: '\e94a';
content: "\e94a";
}
.icon-invoice-out:before {
content: '\e94b';
content: "\e94b";
}
.icon-isTooLittle:before {
content: '\e94c';
content: "\e94c";
}
.icon-item:before {
content: '\e94d';
content: "\e94d";
}
.icon-languaje:before {
content: '\e970';
content: "\e970";
}
.icon-lines:before {
content: '\e94e';
content: "\e94e";
}
.icon-linesprepaired:before {
content: '\e94f';
content: "\e94f";
}
.icon-link-to-corrected:before {
content: '\e931';
content: "\e931";
}
.icon-link-to-correcting:before {
content: '\e944';
content: "\e944";
}
.icon-logout:before {
content: '\e973';
content: "\e973";
}
.icon-mana:before {
content: '\e950';
content: "\e950";
}
.icon-mandatory:before {
content: '\e951';
content: "\e951";
}
.icon-net:before {
content: '\e952';
content: "\e952";
}
.icon-newalbaran:before {
content: '\e954';
content: "\e954";
}
.icon-niche:before {
content: '\e955';
content: "\e955";
}
.icon-no036:before {
content: '\e956';
content: "\e956";
}
.icon-noPayMethod:before {
content: '\e958';
content: "\e958";
}
.icon-notes:before {
content: '\e959';
content: "\e959";
}
.icon-noweb:before {
content: '\e95a';
content: "\e95a";
}
.icon-onlinepayment:before {
content: '\e95b';
content: "\e95b";
}
.icon-package:before {
content: '\e95c';
content: "\e95c";
}
.icon-payment:before {
content: '\e95d';
content: "\e95d";
}
.icon-pbx:before {
content: '\e95e';
content: "\e95e";
}
.icon-pets:before {
content: '\e95f';
content: "\e95f";
}
.icon-photo:before {
content: '\e960';
content: "\e960";
}
.icon-plant:before {
content: '\e961';
content: "\e961";
}
.icon-polizon:before {
content: '\e962';
content: "\e962";
}
.icon-preserved:before {
content: '\e963';
content: "\e963";
}
.icon-recovery:before {
content: '\e964';
content: "\e964";
}
.icon-regentry:before {
content: '\e965';
content: "\e965";
}
.icon-reserva:before {
content: '\e966';
content: "\e966";
}
.icon-revision:before {
content: '\e967';
content: "\e967";
}
.icon-risk:before {
content: '\e969';
content: "\e969";
}
.icon-saysimple:before {
content: '\e912';
content: "\e912";
}
.icon-services:before {
content: '\e96a';
content: "\e96a";
}
.icon-settings:before {
content: '\e96b';
content: "\e96b";
}
.icon-shipment:before {
content: '\e96c';
content: "\e96c";
}
.icon-sign:before {
content: '\e90a';
content: "\e90a";
}
.icon-sms:before {
content: '\e96e';
content: "\e96e";
}
.icon-solclaim:before {
content: '\e96f';
content: "\e96f";
}
.icon-solunion:before {
content: '\e971';
content: "\e971";
}
.icon-splitline:before {
content: '\e972';
content: "\e972";
}
.icon-splur:before {
content: '\e974';
content: "\e974";
}
.icon-stowaway:before {
content: '\e975';
content: "\e975";
}
.icon-supplier:before {
content: '\e976';
content: "\e976";
}
.icon-supplierfalse:before {
content: '\e977';
content: "\e977";
}
.icon-tags:before {
content: '\e979';
content: "\e979";
}
.icon-tax:before {
content: '\e97a';
content: "\e97a";
}
.icon-thermometer:before {
content: '\e97b';
content: "\e97b";
}
.icon-ticket:before {
content: '\e97c';
content: "\e97c";
}
.icon-ticketAdd:before {
content: '\e97e';
content: "\e97e";
}
.icon-traceability:before {
content: '\e97f';
content: "\e97f";
}
.icon-transaction:before {
content: '\e91b';
content: "\e91b";
}
.icon-treatments:before {
content: '\e980';
content: "\e980";
}
.icon-trolley:before {
content: '\e900';
content: "\e900";
}
.icon-troncales:before {
content: '\e982';
content: "\e982";
}
.icon-unavailable:before {
content: '\e983';
content: "\e983";
}
.icon-visible_columns:before {
content: '\e984';
content: "\e984";
}
.icon-volume:before {
content: '\e985';
content: "\e985";
}
.icon-wand:before {
content: '\e986';
content: "\e986";
}
.icon-web:before {
content: '\e987';
content: "\e987";
}
.icon-wiki:before {
content: '\e989';
content: "\e989";
}
.icon-worker:before {
content: '\e98a';
content: "\e98a";
}
.icon-zone:before {
content: '\e98b';
content: "\e98b";
}

View File

@ -30,12 +30,10 @@ export function isValidDate(date) {
export function toDateFormat(date, locale = 'es-ES', opts = {}) {
if (!isValidDate(date)) return '';
const format = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
...opts,
};
const format = Object.assign(
{ year: 'numeric', month: '2-digit', day: '2-digit' },
opts
);
return new Date(date).toLocaleDateString(locale, format);
}
@ -106,17 +104,17 @@ export function secondsToHoursMinutes(seconds, includeHSuffix = true) {
const hours = Math.floor(seconds / 3600);
const remainingMinutes = seconds % 3600;
const minutes = Math.floor(remainingMinutes / 60);
const formattedHours = hours < 10 ? `0${hours}` : hours;
const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes;
const formattedHours = hours < 10 ? '0' + hours : hours;
const formattedMinutes = minutes < 10 ? '0' + minutes : minutes;
// Append "h." if includeHSuffix is true
const suffix = includeHSuffix ? ' h.' : '';
// Return formatted string
return `${formattedHours}:${formattedMinutes}${suffix}`;
return formattedHours + ':' + formattedMinutes + suffix;
}
export function getTimeDifferenceWithToday(date) {
const today = Date.vnNew();
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
date = new Date(date);

View File

@ -5,12 +5,12 @@
* @return {Object} The fields as object
*/
function fieldsToObject(fields) {
const fieldsObj = {};
let fieldsObj = {};
if (Array.isArray(fields)) {
for (const field of fields) fieldsObj[field] = true;
for (let field of fields) fieldsObj[field] = true;
} else if (typeof fields == 'object') {
for (const field in fields) {
for (let field in fields) {
if (fields[field]) fieldsObj[field] = true;
}
}
@ -26,7 +26,7 @@ function fieldsToObject(fields) {
* @return {Array} The merged fields as an array
*/
function mergeFields(src, dst) {
const fields = {};
let fields = {};
Object.assign(fields, fieldsToObject(src), fieldsToObject(dst));
return Object.keys(fields);
}
@ -39,7 +39,7 @@ function mergeFields(src, dst) {
* @return {Array} The merged wheres
*/
function mergeWhere(src, dst) {
const and = [];
let and = [];
if (src) and.push(src);
if (dst) and.push(dst);
return simplifyOperation(and, 'and');
@ -53,7 +53,7 @@ function mergeWhere(src, dst) {
* @return {Object} The result filter
*/
function mergeFilters(src, dst) {
const res = { ...dst };
let res = Object.assign({}, dst);
if (!src) return res;
@ -80,12 +80,12 @@ function simplifyOperation(operation, operator) {
}
function buildFilter(params, builderFunc) {
const and = [];
let and = [];
for (const param in params) {
const value = params[param];
for (let param in params) {
let value = params[param];
if (value == null) continue;
const expr = builderFunc(param, value);
let expr = builderFunc(param, value);
if (expr) and.push(expr);
}
return simplifyOperation(and, 'and');

View File

@ -1,14 +1,14 @@
export default function getDifferences(obj1, obj2) {
const diff = {};
let diff = {};
delete obj1.$index;
delete obj2.$index;
for (const key in obj1) {
for (let key in obj1) {
if (obj2[key] && JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
diff[key] = obj2[key];
}
}
for (const key in obj2) {
for (let key in obj2) {
if (
obj1[key] === undefined ||
JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])

View File

@ -6,7 +6,6 @@ import toDateHourMinSec from './toDateHourMinSec';
import toRelativeDate from './toRelativeDate';
import toCurrency from './toCurrency';
import toPercentage from './toPercentage';
import toNumber from './toNumber';
import toLowerCamel from './toLowerCamel';
import dashIfEmpty from './dashIfEmpty';
import dateRange from './dateRange';
@ -35,7 +34,6 @@ export {
toRelativeDate,
toCurrency,
toPercentage,
toNumber,
dashIfEmpty,
dateRange,
getParamWhere,

View File

@ -6,9 +6,9 @@ export default function (value, options = {}) {
if (!isValidDate(value)) return null;
if (!options.dateStyle && !options.timeStyle) {
options.day = options.day ?? '2-digit';
options.month = options.month ?? '2-digit';
options.year = options.year ?? 'numeric';
options.day = '2-digit';
options.month = '2-digit';
options.year = 'numeric';
}
const { locale } = useI18n();

View File

@ -1,10 +1,10 @@
export default function toDateString(date) {
let day = date.getDate();
let month = date.getMonth() + 1;
const year = date.getFullYear();
let year = date.getFullYear();
if (day < 10) day = `0${day}`;
if (month < 10) month = `0${month}`;
return `${year}-${month}-${day}`;
}
return `${year}-${month}-${day}`
}

View File

@ -1,5 +1,5 @@
export default function toLowerCamel(value) {
if (!value) return;
if (typeof value !== 'string') return value;
if (typeof (value) !== 'string') return value;
return value.charAt(0).toLowerCase() + value.slice(1);
}
}

View File

@ -1,8 +0,0 @@
export default function (value, fractionSize = 2) {
if (isNaN(value)) return value;
return new Intl.NumberFormat('es-ES', {
style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: fractionSize,
}).format(value);
}

Some files were not shown because too many files have changed in this diff Show More