This commit is contained in:
Jorge Penadés 2025-05-19 10:14:47 +02:00
commit e89cf14447
59 changed files with 1102 additions and 718 deletions

81
Jenkinsfile vendored
View File

@ -108,7 +108,6 @@ pipeline {
} }
stage('E2E') { stage('E2E') {
environment { environment {
CREDS = credentials('docker-registry')
COMPOSE_PROJECT = "${PROJECT_NAME}-${env.BUILD_ID}" COMPOSE_PROJECT = "${PROJECT_NAME}-${env.BUILD_ID}"
COMPOSE_PARAMS = "-p ${env.COMPOSE_PROJECT} -f test/cypress/docker-compose.yml --project-directory ." COMPOSE_PARAMS = "-p ${env.COMPOSE_PROJECT} -f test/cypress/docker-compose.yml --project-directory ."
} }
@ -118,23 +117,24 @@ pipeline {
sh 'rm -rf test/cypress/screenshots' sh 'rm -rf test/cypress/screenshots'
env.COMPOSE_TAG = PROTECTED_BRANCH.contains(env.CHANGE_TARGET) ? env.CHANGE_TARGET : 'dev' env.COMPOSE_TAG = PROTECTED_BRANCH.contains(env.CHANGE_TARGET) ? env.CHANGE_TARGET : 'dev'
def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs') def modules = sh(
script: "node test/cypress/docker/find/find.js ${env.COMPOSE_TAG}",
sh 'docker login --username $CREDS_USR --password $CREDS_PSW $REGISTRY' returnStdout: true
sh "docker-compose ${env.COMPOSE_PARAMS} pull back" ).trim()
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}" echo "E2E MODULES: ${modules}"
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") { image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") {
sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'" sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'"
} }
} }
} }
}
post { post {
always { always {
sh "docker-compose ${env.COMPOSE_PARAMS} down -v" sh "docker compose ${env.COMPOSE_PARAMS} down -v"
archiveArtifacts artifacts: 'test/cypress/screenshots/**/*', allowEmptyArchive: true archiveArtifacts artifacts: 'test/cypress/screenshots/**/*', allowEmptyArchive: true
junit( junit(
testResults: 'junit/e2e-*.xml', testResults: 'junit/e2e-*.xml',
@ -153,17 +153,8 @@ pipeline {
VERSION = readFile 'VERSION.txt' VERSION = readFile 'VERSION.txt'
} }
steps { steps {
script {
sh 'quasar build' sh 'quasar build'
dockerBuild 'salix-frontend', '.'
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') { stage('Deploy') {
@ -186,3 +177,53 @@ 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,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "25.18.0", "version": "25.22.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",

View File

@ -1,227 +0,0 @@
/* 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

@ -7,7 +7,7 @@ import { QLayout } from 'quasar';
import mainShortcutMixin from './mainShortcutMixin'; import mainShortcutMixin from './mainShortcutMixin';
import { useCau } from 'src/composables/useCau'; import { useCau } from 'src/composables/useCau';
export default boot(({ app }) => { export default boot(({ app, router }) => {
QForm.mixins = [qFormMixin]; QForm.mixins = [qFormMixin];
QLayout.mixins = [mainShortcutMixin]; QLayout.mixins = [mainShortcutMixin];
@ -22,6 +22,14 @@ export default boot(({ app }) => {
} }
switch (response?.status) { 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: case 422:
if (error.name == 'ValidationError') if (error.name == 'ValidationError')
message += ` "${responseError.details.context}.${Object.keys( message += ` "${responseError.details.context}.${Object.keys(

View File

@ -102,6 +102,10 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
customMethod: {
type: String,
default: null,
},
}); });
const emit = defineEmits(['onFetch', 'onDataSaved', 'submit']); const emit = defineEmits(['onFetch', 'onDataSaved', 'submit']);
const modelValue = computed( const modelValue = computed(
@ -237,7 +241,9 @@ async function save() {
const url = const url =
$props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url; $props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url;
const response = await Promise.resolve( const response = await Promise.resolve(
$props.saveFn ? $props.saveFn(body) : axios[method](url, body), $props.saveFn
? $props.saveFn(body)
: axios[$props.customMethod ?? method](url, body),
); );
if ($props.urlCreate) notify('globals.dataCreated', 'positive'); if ($props.urlCreate) notify('globals.dataCreated', 'positive');

View File

@ -181,6 +181,10 @@ const col = computed(() => {
newColumn.component = 'checkbox'; newColumn.component = 'checkbox';
if ($props.default && !newColumn.component) newColumn.component = $props.default; 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; return newColumn;
}); });

View File

@ -0,0 +1,136 @@
<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;
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

@ -136,6 +136,9 @@ async function addFilter(value, name) {
value = value === '' ? undefined : value; value = value === '' ? undefined : value;
let field = columnFilter.value?.name ?? $props.column.name ?? name; 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?.inWhere) {
if (columnFilter.value.alias) field = columnFilter.value.alias + '.' + field; if (columnFilter.value.alias) field = columnFilter.value.alias + '.' + field;
return await arrayData.addFilterWhere({ [field]: value }); return await arrayData.addFilterWhere({ [field]: value });

View File

@ -33,6 +33,7 @@ import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
import VnTableFilter from './VnTableFilter.vue'; import VnTableFilter from './VnTableFilter.vue';
import { getColAlign } from 'src/composables/getColAlign'; import { getColAlign } from 'src/composables/getColAlign';
import RightMenu from '../common/RightMenu.vue'; import RightMenu from '../common/RightMenu.vue';
import VnContextMenu from './VnContextMenu.vue';
import VnScroll from '../common/VnScroll.vue'; import VnScroll from '../common/VnScroll.vue';
import VnCheckboxMenu from '../common/VnCheckboxMenu.vue'; import VnCheckboxMenu from '../common/VnCheckboxMenu.vue';
import VnCheckbox from '../common/VnCheckbox.vue'; import VnCheckbox from '../common/VnCheckbox.vue';
@ -178,8 +179,9 @@ const app = inject('app');
const tableHeight = useTableHeight(); const tableHeight = useTableHeight();
const vnScrollRef = ref(null); const vnScrollRef = ref(null);
const editingRow = ref(null); const editingRow = ref();
const editingField = ref(null); const editingField = ref();
const contextMenuRef = ref({});
const isTableMode = computed(() => mode.value == TABLE_MODE); const isTableMode = computed(() => mode.value == TABLE_MODE);
const selectRegex = /select/; const selectRegex = /select/;
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']); const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
@ -216,6 +218,10 @@ onBeforeMount(() => {
onMounted(async () => { onMounted(async () => {
if ($props.isEditable) document.addEventListener('click', clickHandler); if ($props.isEditable) document.addEventListener('click', clickHandler);
document.addEventListener('contextmenu', (event) => {
event.preventDefault();
contextMenuRef.value.handler(event);
});
mode.value = mode.value =
quasar.platform.is.mobile && !$props.disableOption?.card quasar.platform.is.mobile && !$props.disableOption?.card
? CARD_MODE ? CARD_MODE
@ -239,6 +245,7 @@ onMounted(async () => {
onUnmounted(async () => { onUnmounted(async () => {
if ($props.isEditable) document.removeEventListener('click', clickHandler); if ($props.isEditable) document.removeEventListener('click', clickHandler);
document.removeEventListener('contextmenu', {});
}); });
watch( watch(
@ -835,6 +842,7 @@ const handleHeaderSelection = (evt, data) => {
]" ]"
:data-row-index="rowIndex" :data-row-index="rowIndex"
:data-col-field="col?.name" :data-col-field="col?.name"
:data-col-value="row?.[col?.name]"
> >
<div <div
class="no-padding no-margin" class="no-padding no-margin"
@ -1145,6 +1153,7 @@ const handleHeaderSelection = (evt, data) => {
</template> </template>
</FormModelPopup> </FormModelPopup>
</QDialog> </QDialog>
<VnContextMenu ref="contextMenuRef" v-model="arrayData" />
<VnScroll <VnScroll
ref="vnScrollRef" ref="vnScrollRef"
v-if="isTableMode" v-if="isTableMode"

View File

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

View File

@ -0,0 +1,43 @@
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

@ -6,13 +6,7 @@ import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs(); const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs); const { isRequired, requiredFieldRule } = useRequired($attrs);
const { t } = useI18n(); const { t } = useI18n();
const emit = defineEmits([ const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);
'update:modelValue',
'update:options',
'keyup.enter',
'remove',
'blur',
]);
const $props = defineProps({ const $props = defineProps({
modelValue: { modelValue: {
@ -126,6 +120,14 @@ const handleInsertMode = (e) => {
const handleUppercase = () => { const handleUppercase = () => {
value.value = value.value?.toUpperCase() || ''; value.value = value.value?.toUpperCase() || '';
}; };
const listeners = computed(() =>
Object.fromEntries(
Object.entries($attrs).filter(
([key, val]) => key.startsWith('on') && typeof val === 'function',
),
),
);
</script> </script>
<template> <template>
@ -134,10 +136,9 @@ const handleUppercase = () => {
ref="vnInputRef" ref="vnInputRef"
v-model="value" v-model="value"
v-bind="{ ...$attrs, ...styleAttrs }" v-bind="{ ...$attrs, ...styleAttrs }"
v-on="listeners"
:type="$attrs.type" :type="$attrs.type"
:class="{ required: isRequired }" :class="{ required: isRequired }"
@keyup.enter="emit('keyup.enter')"
@blur="emit('blur')"
@keydown="handleKeydown" @keydown="handleKeydown"
:clearable="false" :clearable="false"
:rules="mixinRules" :rules="mixinRules"

View File

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

View File

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

View File

@ -0,0 +1,49 @@
<script setup>
import { ref, useTemplateRef } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
import VnSelectDialog from './VnSelectDialog.vue';
import CreateNewExpenseForm from '../CreateNewExpenseForm.vue';
import FetchData from '../FetchData.vue';
const model = defineModel({ type: [String, Number, Object] });
const { t } = useI18n();
const expenses = ref([]);
const selectDialogRef = useTemplateRef('selectDialogRef');
async function autocompleteExpense(evt) {
const val = evt.target.value;
if (!val || isNaN(val)) return;
const lookup = expenses.value.find(({ id }) => id == useAccountShortToStandard(val));
if (selectDialogRef.value)
selectDialogRef.value.vnSelectDialogRef.vnSelectRef.toggleOption(lookup);
}
</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' }]"
@keydown.tab.prevent="autocompleteExpense"
>
<template #form>
<CreateNewExpenseForm @on-data-saved="$refs.expensesRef.fetch()" />
</template>
</VnSelectDialog>
<FetchData
ref="expensesRef"
url="Expenses"
auto-load
@on-fetch="(data) => (expenses = data)"
/>
</template>
<i18n>
es:
Create a new expense: Crear nuevo gasto
</i18n>

View File

@ -42,7 +42,7 @@ const card = toRef(props, 'item');
</div> </div>
<div class="content"> <div class="content">
<span class="link" @click.stop> <span class="link" @click.stop>
{{ card.name }} {{ card.longName }}
<ItemDescriptorProxy :id="card.id" /> <ItemDescriptorProxy :id="card.id" />
</span> </span>
<p class="subName">{{ card.subName }}</p> <p class="subName">{{ card.subName }}</p>
@ -57,11 +57,12 @@ const card = toRef(props, 'item');
<QIcon name="production_quantity_limits" size="xs" /> <QIcon name="production_quantity_limits" size="xs" />
{{ card.minQuantity }} {{ card.minQuantity }}
</div> </div>
<div class="footer"> <div class="footer q-mt-auto">
<div class="price"> <div class="price">
<p v-if="isCatalog"> <p v-if="isCatalog">
{{ card.available }} {{ t('to') }} <span class="text-primary">{{ card.available }}</span>
{{ toCurrency(card.price) }} {{ t('to') }}
<span class="text-bold" >{{ toCurrency(card.price) }}</span>
</p> </p>
<slot name="price" /> <slot name="price" />
<QIcon v-if="isCatalog" name="add_circle" class="icon"> <QIcon v-if="isCatalog" name="add_circle" class="icon">
@ -144,6 +145,7 @@ const card = toRef(props, 'item');
} }
.footer { .footer {
.price { .price {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;

View File

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

View File

@ -313,6 +313,7 @@ export function useArrayData(key, userOptions) {
const { params, limit } = getCurrentFilter(); const { params, limit } = getCurrentFilter();
store.currentFilter = JSON.parse(JSON.stringify(params)); store.currentFilter = JSON.parse(JSON.stringify(params));
delete store.currentFilter.filter.include; delete store.currentFilter.filter.include;
delete store.currentFilter.filter.fields;
store.currentFilter.filter = JSON.stringify(store.currentFilter.filter); store.currentFilter.filter = JSON.stringify(store.currentFilter.filter);
return { params, limit }; return { params, limit };
} }

View File

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

View File

@ -400,6 +400,8 @@ errors:
updateUserConfig: Error updating user config updateUserConfig: Error updating user config
tokenConfig: Error fetching token config tokenConfig: Error fetching token config
writeRequest: The requested operation could not be completed writeRequest: The requested operation could not be completed
sessionExpired: Your session has expired. Please log in again
accessDenied: Access denied
claimBeginningQuantity: Cannot import a line with a claimed quantity of 0 claimBeginningQuantity: Cannot import a line with a claimed quantity of 0
login: login:
title: Login title: Login
@ -894,6 +896,7 @@ components:
rate3: Packing price rate3: Packing price
minPrice: Min. Price minPrice: Min. Price
itemFk: Item id itemFk: Item id
dated: Date
userPanel: userPanel:
copyToken: Token copied to clipboard copyToken: Token copied to clipboard
settings: Settings settings: Settings

View File

@ -396,6 +396,8 @@ errors:
updateUserConfig: Error al actualizar la configuración de usuario updateUserConfig: Error al actualizar la configuración de usuario
tokenConfig: Error al obtener configuración de token tokenConfig: Error al obtener configuración de token
writeRequest: No se pudo completar la operación solicitada writeRequest: No se pudo completar la operación solicitada
sessionExpired: Tu sesión ha expirado, por favor vuelve a iniciar sesión
accessDenied: Acceso denegado
claimBeginningQuantity: No se puede importar una linea sin una cantidad reclamada claimBeginningQuantity: No se puede importar una linea sin una cantidad reclamada
login: login:
title: Inicio de sesión title: Inicio de sesión
@ -978,6 +980,7 @@ components:
rate3: Precio packing rate3: Precio packing
minPrice: Precio mínimo minPrice: Precio mínimo
itemFk: Id item itemFk: Id item
dated: Fecha
userPanel: userPanel:
copyToken: Token copiado al portapapeles copyToken: Token copiado al portapapeles
settings: Configuración settings: Configuración

View File

@ -2,14 +2,20 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useQuasar } from 'quasar';
import { toCurrency, toDateHourMin } from 'src/filters'; import { toCurrency, toDateHourMin } from 'src/filters';
import VnTable from 'src/components/VnTable/VnTable.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnRow from 'components/ui/VnRow.vue';
import axios from 'axios';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const quasar = useQuasar();
const tableRef = ref(); const tableRef = ref();
@ -45,26 +51,45 @@ const columns = computed(() => [
align: 'right', align: 'right',
field: 'rating', field: 'rating',
label: t('customer.summary.rating'), label: t('customer.summary.rating'),
name: 'rating', name: 'rating'
create: true,
columnCreate: {
component: 'number',
autofocus: true,
},
}, },
{ {
align: 'right', align: 'right',
field: 'recommendedCredit', field: 'recommendedCredit',
format: ({ recommendedCredit }) => toCurrency(recommendedCredit), format: ({ recommendedCredit }) => toCurrency(recommendedCredit),
label: t('customer.summary.recommendCredit'), label: t('customer.summary.recommendCredit'),
name: 'recommendedCredit', name: 'recommendedCredit'
create: true,
columnCreate: {
component: 'number',
autofocus: true,
},
}, },
]); ]);
const defaultInitialData = {
rating: null,
recommendedCredit: null
};
const createRating = async (data) => {
await axios.post(`Clients/${route.params.id}/setRating`, data);
tableRef.value?.reload();
};
const handleSave = async (data) => {
if (data.rating || data.recommendedCredit) {
await createRating(data);
return;
}
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('terminationTitle'),
message: t('terminationMessage'),
},
})
.onOk(async () => {
await createRating({ rating: 0, recommendedCredit: 0 });
});
};
</script> </script>
<template> <template>
@ -72,8 +97,7 @@ const columns = computed(() => [
ref="tableRef" ref="tableRef"
data-key="ClientInformas" data-key="ClientInformas"
url="ClientInformas" url="ClientInformas"
:filter="filter" :user-filter="filter"
:order="['created DESC']"
:columns="columns" :columns="columns"
:right-search="false" :right-search="false"
:is-editable="false" :is-editable="false"
@ -82,22 +106,43 @@ const columns = computed(() => [
:disable-option="{ card: true }" :disable-option="{ card: true }"
auto-load auto-load
:create="{ :create="{
urlCreate: `Clients/${route.params.id}/setRating`,
title: 'Create rating', title: 'Create rating',
onDataSaved: () => tableRef.reload(), onDataSaved: ()=> tableRef.reload(),
formInitialData: {}, formInitialData: defaultInitialData,
saveFn: handleSave
}" }"
> >
<template #column-employee="{ row }"> <template #column-employee="{ row }">
<span class="link">{{ row.worker.user.nickname }}</span> <span class="link">{{ row.worker.user.nickname }}</span>
<WorkerDescriptorProxy :id="row.worker.id" /> <WorkerDescriptorProxy :id="row.worker.id" />
</template> </template>
<template #more-create-dialog="{ data }">
<VnRow>
<VnInputNumber
v-model="data.rating"
:label="t('customer.summary.rating')"
:required="!!data.recommendedCredit"
/>
<VnInputNumber
v-model="data.recommendedCredit"
:label="t('customer.summary.recommendCredit')"
:required="!!data.rating"
/>
</VnRow>
</template>
</VnTable> </VnTable>
</template> </template>
<i18n> <i18n>
en:
terminationTitle: Confirm contract termination
terminationMessage: Are you sure you want to terminate the contract? This action will set the rating and recommended credit to 0.
es: es:
Recommended credit: Crédito recomendado Recommended credit: Crédito recomendado
Since: Desde Since: Desde
Employee: Empleado Employee: Empleado
Create rating: Crear calificación
terminationTitle: Confirmar baja de contrato
terminationMessage: ¿Está seguro que desea dar de baja el contrato? Esta acción establecerá la calificación y el crédito recomendado en 0.
</i18n> </i18n>

View File

@ -6,7 +6,6 @@ import { useQuasar } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
@ -21,9 +20,6 @@ const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const { notify } = useNotify(); const { notify } = useNotify();
const typesTaxes = ref([]);
const typesTransactions = ref([]);
function handleLocation(data, location) { function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {}; const { town, code, provinceFk, countryFk } = location ?? {};
data.postcode = code; data.postcode = code;
@ -39,6 +35,7 @@ function onBeforeSave(formData, originalData) {
} }
async function checkEtChanges(data, _, originalData) { async function checkEtChanges(data, _, originalData) {
isTaxDataChecked.value = data.isTaxDataChecked;
const equalizatedHasChanged = originalData.isEqualizated != data.isEqualizated; const equalizatedHasChanged = originalData.isEqualizated != data.isEqualizated;
const hasToInvoiceByAddress = const hasToInvoiceByAddress =
originalData.hasToInvoiceByAddress || data.hasToInvoiceByAddress; originalData.hasToInvoiceByAddress || data.hasToInvoiceByAddress;
@ -62,15 +59,18 @@ async function acceptPropagate({ isEqualizated }) {
}); });
notify(t('Equivalent tax spreaded'), 'warning'); notify(t('Equivalent tax spreaded'), 'warning');
} }
const isTaxDataChecked = ref(false);
function isRequired({ isTaxDataChecked: taxDataChecked }) {
if (!isTaxDataChecked.value) {
return false;
} else {
return taxDataChecked;
}
}
</script> </script>
<template> <template>
<FetchData auto-load @on-fetch="(data) => (typesTaxes = data)" url="SageTaxTypes" />
<FetchData
auto-load
@on-fetch="(data) => (typesTransactions = data)"
url="SageTransactionTypes"
/>
<FormModel <FormModel
:url-update="`Clients/${route.params.id}/updateFiscalData`" :url-update="`Clients/${route.params.id}/updateFiscalData`"
auto-load auto-load
@ -111,21 +111,27 @@ async function acceptPropagate({ isEqualizated }) {
<VnRow> <VnRow>
<VnSelect <VnSelect
:label="t('Sage tax type')" :label="t('Sage tax type')"
:options="typesTaxes" url="SageTaxTypes"
hide-selected hide-selected
option-label="vat" option-label="vat"
option-value="id" option-value="id"
v-model="data.sageTaxTypeFk" v-model="data.sageTaxTypeFk"
data-cy="sageTaxTypeFk" data-cy="sageTaxTypeFk"
:required="isRequired(data)"
:rules="[(val) => validations.required(data.sageTaxTypeFk, val)]"
/> />
<VnSelect <VnSelect
:label="t('Sage transaction type')" :label="t('Sage transaction type')"
:options="typesTransactions" url="SageTransactionTypes"
hide-selected hide-selected
option-label="transaction" option-label="transaction"
option-value="id" option-value="id"
data-cy="sageTransactionTypeFk" data-cy="sageTransactionTypeFk"
v-model="data.sageTransactionTypeFk" v-model="data.sageTransactionTypeFk"
:required="isRequired(data)"
:rules="[
(val) => validations.required(data.sageTransactionTypeFk, val),
]"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -151,11 +157,11 @@ async function acceptPropagate({ isEqualizated }) {
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
<QCheckbox :label="t('Active')" v-model="data.isActive" /> <VnCheckbox :label="t('Active')" v-model="data.isActive" />
<QCheckbox :label="t('Frozen')" v-model="data.isFreezed" /> <VnCheckbox :label="t('Frozen')" v-model="data.isFreezed" />
</VnRow> </VnRow>
<VnRow> <VnRow>
<QCheckbox :label="t('Has to invoice')" v-model="data.hasToInvoice" /> <VnCheckbox :label="t('Has to invoice')" v-model="data.hasToInvoice" />
<VnCheckbox <VnCheckbox
v-model="data.isVies" v-model="data.isVies"
:label="t('globals.isVies')" :label="t('globals.isVies')"
@ -164,8 +170,8 @@ async function acceptPropagate({ isEqualizated }) {
</VnRow> </VnRow>
<VnRow> <VnRow>
<QCheckbox :label="t('Notify by email')" v-model="data.isToBeMailed" /> <VnCheckbox :label="t('Notify by email')" v-model="data.isToBeMailed" />
<QCheckbox <VnCheckbox
:label="t('Invoice by address')" :label="t('Invoice by address')"
v-model="data.hasToInvoiceByAddress" v-model="data.hasToInvoiceByAddress"
/> />
@ -177,16 +183,18 @@ async function acceptPropagate({ isEqualizated }) {
:label="t('Is equalizated')" :label="t('Is equalizated')"
:info="t('inOrderToInvoice')" :info="t('inOrderToInvoice')"
/> />
<QCheckbox :label="t('Daily invoice')" v-model="data.hasDailyInvoice" /> <VnCheckbox :label="t('Daily invoice')" v-model="data.hasDailyInvoice" />
</VnRow> </VnRow>
<VnRow> <VnRow>
<QCheckbox <VnCheckbox
:label="t('Electronic invoice')" :label="t('Electronic invoice')"
v-model="data.hasElectronicInvoice" v-model="data.hasElectronicInvoice"
/><QCheckbox />
<VnCheckbox
:label="t('Verified data')" :label="t('Verified data')"
v-model="data.isTaxDataChecked" v-model="data.isTaxDataChecked"
@update:model-value="isTaxDataChecked = !isTaxDataChecked"
/> />
</VnRow> </VnRow>
</template> </template>

View File

@ -179,6 +179,11 @@ function handleLocation(data, location) {
data.provinceFk = provinceFk; data.provinceFk = provinceFk;
data.countryFk = countryFk; data.countryFk = countryFk;
} }
function onAgentCreated({ id, fiscalName }, data) {
customsAgents.value.push({ id, fiscalName });
data.customsAgentFk = id;
}
</script> </script>
<template> <template>
@ -292,9 +297,14 @@ function handleLocation(data, location) {
option-value="id" option-value="id"
v-model="data.customsAgentFk" v-model="data.customsAgentFk"
:tooltip="t('New customs agent')" :tooltip="t('New customs agent')"
:acls="[{ model: 'CustomsAgent', props: '*', accessType: 'WRITE' }]"
> >
<template #form> <template #form>
<CustomerNewCustomsAgent /> <CustomerNewCustomsAgent
@on-data-saved="
(requestResponse) => onAgentCreated(requestResponse, data)
"
/>
</template> </template>
</VnSelectDialog> </VnSelectDialog>
</VnRow> </VnRow>

View File

@ -16,6 +16,7 @@ const onDataSaved = (dataSaved) => {
<template> <template>
<FormModelPopup <FormModelPopup
:form-initial-data="{}"
:title="t('New customs agent')" :title="t('New customs agent')"
@on-data-saved="onDataSaved($event)" @on-data-saved="onDataSaved($event)"
model="customer" model="customer"

View File

@ -73,6 +73,7 @@ const columns = computed(() => [
optionLabel: 'code', optionLabel: 'code',
options: companies.value, options: companies.value,
}, },
orderBy: false,
}, },
{ {
name: 'warehouse', name: 'warehouse',

View File

@ -1,9 +1,11 @@
<script setup> <script setup>
import { onMounted, ref, onUnmounted, computed, watch } from 'vue'; import { onMounted, ref, onUnmounted, computed, watch } from 'vue';
import axios from 'axios';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { beforeSave } from 'src/composables/updateMinPriceBeforeSave'; import { beforeSave } from 'src/composables/updateMinPriceBeforeSave';
import FetchedTags from 'components/ui/FetchedTags.vue'; import FetchedTags from 'components/ui/FetchedTags.vue';
@ -14,13 +16,13 @@ import RightMenu from 'src/components/common/RightMenu.vue';
import VnTable from 'src/components/VnTable/VnTable.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
import VnColor from 'src/components/common/VnColor.vue'; import VnColor from 'src/components/common/VnColor.vue';
import { toDate } from 'src/filters'; import { toDate, toCurrency } from 'src/filters';
import { isLower, isBigger } from 'src/filters/date.js'; import { isLower, isBigger } from 'src/filters/date.js';
import ItemFixedPriceFilter from './ItemFixedPriceFilter.vue'; import ItemFixedPriceFilter from './ItemFixedPriceFilter.vue';
import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue';
import { toCurrency } from 'src/filters';
const stateStore = useStateStore(); const stateStore = useStateStore();
const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const tableRef = ref(); const tableRef = ref();
const editFixedPriceForm = ref(null); const editFixedPriceForm = ref(null);
@ -218,11 +220,36 @@ const dateStyle = (date) =>
} }
: { color: dateColor, 'background-color': 'transparent' }; : { color: dateColor, 'background-color': 'transparent' };
const onDataSaved = () => { const onDataSaved = async (data) => {
tableRef.value.CrudModelRef.saveChanges(); for (const row of data) {
await axios.patch('FixedPrices/upsertFixedPrice', row);
}
selectedRows.value = []; selectedRows.value = [];
tableRef.value.reload();
tableRef.value.CrudModelRef.reset();
}; };
async function saveData(data, getChanges) {
const changes = getChanges();
if (changes?.updates?.length) {
for (const change of changes.updates) {
const row = data.find((row) => row.id === change.where.id);
await axios.patch('FixedPrices/upsertFixedPrice', row);
}
}
if (data?.deletes?.length) {
for (const deleteItem of data.deletes) {
await axios.delete(`FixedPrices/${deleteItem}`);
quasar.notify({
message: t('globals.dataDeleted'),
color: 'positive',
});
}
}
tableRef.value.reload();
}
onMounted(() => { onMounted(() => {
if (tableRef.value) { if (tableRef.value) {
tableRef.value.showForm = false; tableRef.value.showForm = false;
@ -273,7 +300,8 @@ watch(
data-key="ItemFixedPrices" data-key="ItemFixedPrices"
url="FixedPrices/filter" url="FixedPrices/filter"
:order="'name DESC'" :order="'name DESC'"
save-url="FixedPrices/crud" save-url="FixedPrices/upsertFixedPrice"
:saveFn="saveData"
:columns="columns" :columns="columns"
:is-editable="true" :is-editable="true"
:right-search="false" :right-search="false"
@ -283,7 +311,8 @@ watch(
}" }"
v-model:selected="selectedRows" v-model:selected="selectedRows"
:create="{ :create="{
urlCreate: 'FixedPrices', urlCreate: 'FixedPrices/upsertFixedPrice',
customMethod: 'patch',
title: t('Create fixed price'), title: t('Create fixed price'),
formInitialData: { warehouseFk: warehouse }, formInitialData: { warehouseFk: warehouse },
onDataSaved: () => tableRef.reload(), onDataSaved: () => tableRef.reload(),

View File

@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n';
import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
import ItemsFilterPanel from 'src/components/ItemsFilterPanel.vue'; import ItemsFilterPanel from 'src/components/ItemsFilterPanel.vue';
const { t } = useI18n(); const { t } = useI18n();
@ -51,42 +52,41 @@ const props = defineProps({
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QSeparator />
<QItemSection>
<QIcon name="info" size="sm" class="info-icon cursor-pointer">
<QTooltip>{{ t('params.incompatibleFilters') }}</QTooltip>
</QIcon>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
v-model="params.started" v-model="params.dated"
:label="t('params.started')" :label="t('params.date')"
filled filled
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection>
<VnInputDate
v-model="params.ended"
:label="t('params.ended')"
filled
@update:model-value="searchFn()"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<QCheckbox <VnCheckbox
v-model="params.showBadDates"
:label="t(`params.showBadDates`)"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
</QItemSection>
<QSeparator />
<QItem>
<QItemSection>
<VnCheckbox
:label="t('params.mine')" :label="t('params.mine')"
v-model="params.mine" v-model="params.mine"
toggle-indeterminate toggle-indeterminate
@update:model-value="searchFn()" @update:model-value="searchFn()"
/> />
<VnCheckbox
<QCheckbox
v-model="params.showBadDates"
:label="t(`params.showBadDates`)"
toggle-indeterminate
@update:model-value="searchFn()"
>
</QCheckbox>
<QCheckbox
:label="t('params.hasMinPrice')" :label="t('params.hasMinPrice')"
v-model="params.hasMinPrice" v-model="params.hasMinPrice"
toggle-indeterminate toggle-indeterminate
@ -97,6 +97,13 @@ const props = defineProps({
</template> </template>
</ItemsFilterPanel> </ItemsFilterPanel>
</template> </template>
<style lang="scss" scoped>
.info-icon {
position: relative;
top: 0;
left: 90%;
}
</style>
<i18n> <i18n>
en: en:
params: params:
@ -107,6 +114,8 @@ en:
mine: Mine mine: Mine
showBadDates: Show future items showBadDates: Show future items
hasMinPrice: Has Min Price hasMinPrice: Has Min Price
date: Date
incompatibleFilters: Cannot select "Date" and "Show future items" at the same time
es: es:
params: params:
buyerFk: Comprador buyerFk: Comprador
@ -116,4 +125,6 @@ es:
mine: Para mi mine: Para mi
showBadDates: Ver items a futuro showBadDates: Ver items a futuro
hasMinPrice: Precio mínimo hasMinPrice: Precio mínimo
date: Fecha
incompatibleFilters: No se puede seleccionar "Fecha" y "Ver items a futuro" a la vez
</i18n> </i18n>

View File

@ -26,7 +26,6 @@ const $props = defineProps({
default: () => {}, default: () => {},
}, },
}); });
const { t } = useI18n(); const { t } = useI18n();
const emit = defineEmits(['onDataSaved']); const emit = defineEmits(['onDataSaved']);
@ -38,7 +37,7 @@ const inputs = {
select: markRaw(VnSelect), select: markRaw(VnSelect),
}; };
const newValue = ref(null); const newFieldValue = ref(null);
const selectedField = ref(null); const selectedField = ref(null);
const closeButton = ref(null); const closeButton = ref(null);
const isLoading = ref(false); const isLoading = ref(false);
@ -46,7 +45,11 @@ const isLoading = ref(false);
const onSubmit = async () => { const onSubmit = async () => {
isLoading.value = true; isLoading.value = true;
$props.rows.forEach((row) => { $props.rows.forEach((row) => {
row[selectedField.value.name] = newValue.value; const newValue =
selectedField.value.component === 'number'
? parseInt(newFieldValue.value)
: newFieldValue.value;
row[selectedField.value.name] = newValue;
}); });
emit('onDataSaved', $props.rows); emit('onDataSaved', $props.rows);
closeForm(); closeForm();
@ -71,17 +74,26 @@ const closeForm = () => {
class="editOption" class="editOption"
:label="t('Field to edit')" :label="t('Field to edit')"
:options="fieldsOptions" :options="fieldsOptions"
hide-selected
option-label="label" option-label="label"
option-value="name"
v-model="selectedField" v-model="selectedField"
data-cy="EditFixedPriceSelectOption" data-cy="EditFixedPriceSelectOption"
@update:model-value="newValue = null" @update:model-value="newValue = null"
:class="{ 'is-select': selectedField?.component === 'select' }" :class="{ 'is-select': selectedField?.component === 'select' }"
/> :emit-value="false"
>
<template #option="{ opt, itemProps }">
<QItem v-bind="itemProps" class="q-pa-xs row items-center">
<QItemSection class="col-9 justify-center">
<span>{{ opt?.label }}</span>
</QItemSection>
</QItem>
</template>
</VnSelect>
<component <component
:is="inputs[selectedField?.component || 'input']" :is="inputs[selectedField?.component || 'input']"
v-bind="selectedField?.attrs || {}" v-bind="selectedField?.attrs || {}"
v-model="newValue" v-model="newFieldValue"
:label="t('Value')" :label="t('Value')"
data-cy="EditFixedPriceValueOption" data-cy="EditFixedPriceValueOption"
style="width: 200px" style="width: 200px"

View File

@ -1,12 +1,11 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { Notify } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import VnInputPassword from 'src/components/common/VnInputPassword.vue'; import VnInputPassword from 'src/components/common/VnInputPassword.vue';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { useLogin } from 'src/composables/useLogin'; import { useLogin } from 'src/composables/useLogin';
import useNotify from 'src/composables/useNotify';
import VnLogo from 'components/ui/VnLogo.vue'; import VnLogo from 'components/ui/VnLogo.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import axios from 'axios'; import axios from 'axios';
@ -15,16 +14,14 @@ const session = useSession();
const loginCache = useLogin(); const loginCache = useLogin();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify();
const username = ref(''); const username = ref('');
const password = ref(''); const password = ref('');
const keepLogin = ref(true); const keepLogin = ref(true);
async function onSubmit() { async function onSubmit() {
const params = { const params = { user: username.value, password: password.value };
user: username.value,
password: password.value,
};
try { try {
const { data } = await axios.post('Accounts/login', params); const { data } = await axios.post('Accounts/login', params);
if (!data) return; if (!data) return;
@ -33,11 +30,7 @@ async function onSubmit() {
await session.setLogin(data); await session.setLogin(data);
} catch (res) { } catch (res) {
if (res.response?.data?.error?.code === 'REQUIRES_2FA') { if (res.response?.data?.error?.code === 'REQUIRES_2FA') {
Notify.create({ notify(t('login.twoFactorRequired'), 'warning', 'phoneLink_lock');
message: t('login.twoFactorRequired'),
icon: 'phoneLink_lock',
type: 'warning',
});
params.keepLogin = keepLogin.value; params.keepLogin = keepLogin.value;
loginCache.setUser(params); loginCache.setUser(params);
return router.push({ return router.push({
@ -45,10 +38,7 @@ async function onSubmit() {
query: router.currentRoute.value?.query, query: router.currentRoute.value?.query,
}); });
} }
Notify.create({ throw res;
message: t('login.loginError'),
type: 'negative',
});
} }
} }
</script> </script>

View File

@ -19,7 +19,7 @@ const { t } = useI18n();
const dataKey = 'OrderCatalogList'; const dataKey = 'OrderCatalogList';
const catalogParams = { const catalogParams = {
orderFk: route.params.id, orderFk: route.params.id,
orderBy: JSON.stringify({ field: 'relevancy DESC, name', way: 'ASC', isTag: false }), orderBy: JSON.stringify({ field: 'relevancy DESC, longName', way: 'ASC', isTag: false }),
}; };
const arrayData = useArrayData(dataKey, { const arrayData = useArrayData(dataKey, {
url: 'Orders/CatalogFilter', url: 'Orders/CatalogFilter',

View File

@ -68,6 +68,19 @@ onMounted(async () => {
<template #menu="{ entity }"> <template #menu="{ entity }">
<RouteDescriptorMenu :route="entity" /> <RouteDescriptorMenu :route="entity" />
</template> </template>
<template #actions="{ entity }">
<QCardActions class="flex justify-center" style="padding-inline: 0">
<QBtn
size="md"
icon="vn:delivery"
color="primary"
:href="`https://grafana.verdnatura.es/d/edkvyi479dbeob/pronostico-de-entregas?orgId=1&var-vRouteFk=${entity.id}`"
target="_blank"
>
<QTooltip>{{ $t('route.deliveryForecast') }}</QTooltip>
</QBtn>
</QCardActions>
</template>
</EntityDescriptor> </EntityDescriptor>
</template> </template>
<i18n> <i18n>

View File

@ -80,6 +80,20 @@ const ticketColumns = ref([
sortable: false, sortable: false,
align: 'left', align: 'left',
}, },
{
name: 'delivered',
label: t('route.delivered'),
field: (row) => dashIfEmpty(toDate(row?.delivered)),
sortable: false,
align: 'center',
},
{
name: 'forecast',
label: t('route.forecast'),
field: (row) => dashIfEmpty(toDate(row?.forecast)),
sortable: false,
align: 'center',
},
{ {
name: 'packages', name: 'packages',
label: t('route.summary.packages'), label: t('route.summary.packages'),
@ -267,61 +281,3 @@ const ticketColumns = ref([
</CardSummary> </CardSummary>
</div> </div>
</template> </template>
<i18n>
en:
route:
summary:
date: Date
agency: Agency
vehicle: Vehicle
driver: Driver
cost: Cost
started: Started time
finished: Finished time
kmStart: Km start
kmEnd: Km end
volume: Volume
packages: Packages
description: Description
tickets: Tickets
order: Order
street: Street
city: City
pc: PC
client: Client
state: State
m3:
packaging: Packaging
ticket: Ticket
closed: Closed
open: Open
yes: Yes
no: No
es:
route:
summary:
date: Fecha
agency: Agencia
vehicle: Vehículo
driver: Conductor
cost: Costo
started: Hora inicio
finished: Hora fin
kmStart: Km inicio
kmEnd: Km fin
volume: Volumen
packages: Bultos
description: Descripción
tickets: Tickets
order: Orden
street: Dirección fiscal
city: Población
pc: CP
client: Cliente
state: Estado
packaging: Encajado
closed: Cerrada
open: Abierta
yes:
no: No
</i18n>

View File

@ -24,49 +24,63 @@ const selectedRows = ref([]);
const columns = computed(() => [ const columns = computed(() => [
{ {
name: 'order', name: 'order',
label: t('Order'), label: t('route.ticket.order'),
field: (row) => dashIfEmpty(row?.priority), field: (row) => dashIfEmpty(row?.priority),
sortable: false, sortable: false,
align: 'center', align: 'center',
}, },
{ {
name: 'client', name: 'client',
label: t('Client'), label: t('route.ticket.client'),
field: (row) => row?.nickname, field: (row) => row?.nickname,
sortable: false, sortable: false,
align: 'left', align: 'left',
}, },
{ {
name: 'street', name: 'street',
label: t('Street'), label: t('route.ticket.street'),
field: (row) => row?.street, field: (row) => row?.street,
sortable: false, sortable: false,
align: 'left', align: 'left',
}, },
{ {
name: 'pc', name: 'pc',
label: t('PC'), label: t('route.ticket.PC'),
field: (row) => row?.postalCode, field: (row) => row?.postalCode,
sortable: false, sortable: false,
align: 'center', align: 'center',
}, },
{ {
name: 'city', name: 'city',
label: t('City'), label: t('route.ticket.city'),
field: (row) => row?.city, field: (row) => row?.city,
sortable: false, sortable: false,
align: 'left', align: 'left',
}, },
{ {
name: 'warehouse', name: 'warehouse',
label: t('Warehouse'), label: t('route.ticket.warehouse'),
field: (row) => row?.warehouseName, field: (row) => row?.warehouseName,
sortable: false, sortable: false,
align: 'left', align: 'left',
}, },
{
name: 'delivered',
label: t('route.ticket.delivered'),
field: (row) => dashIfEmpty(row?.delivered),
sortable: false,
align: 'left',
},
{
name: 'estimated',
label: t('route.ticket.estimated'),
field: (row) => dashIfEmpty(row?.estimated),
sortable: false,
align: 'left',
},
{ {
name: 'packages', name: 'packages',
label: t('Packages'), label: t('route.ticket.packages'),
field: (row) => row?.packages, field: (row) => row?.packages,
sortable: false, sortable: false,
align: 'center', align: 'center',
@ -80,14 +94,14 @@ const columns = computed(() => [
}, },
{ {
name: 'packaging', name: 'packaging',
label: t('Packaging'), label: t('route.ticket.packaging'),
field: (row) => row?.ipt, field: (row) => row?.ipt,
sortable: false, sortable: false,
align: 'center', align: 'center',
}, },
{ {
name: 'ticket', name: 'ticket',
label: t('Ticket'), label: t('route.ticket.ticket'),
field: (row) => row?.id, field: (row) => row?.id,
sortable: false, sortable: false,
align: 'center', align: 'center',
@ -188,8 +202,8 @@ const confirmRemove = (ticket) => {
.dialog({ .dialog({
component: VnConfirm, component: VnConfirm,
componentProps: { componentProps: {
title: t('Confirm removal from route'), title: t('route.ticket.confirmRemoval'),
message: t('Are you sure you want to remove this ticket from the route?'), message: t('route.ticket.confirmRemovalConfirmation'),
promise: () => removeTicket(ticket), promise: () => removeTicket(ticket),
}, },
}) })
@ -219,7 +233,7 @@ const openSmsDialog = async () => {
quasar.dialog({ quasar.dialog({
component: SendSmsDialog, component: SendSmsDialog,
componentProps: { componentProps: {
title: t('Send SMS to the selected tickets'), title: t('route.ticket.sendSmsTickets'),
url: 'Routes/sendSms', url: 'Routes/sendSms',
destinationFk: clientsId.toString(), destinationFk: clientsId.toString(),
destination: clientsPhone.toString(), destination: clientsPhone.toString(),
@ -240,18 +254,18 @@ const openSmsDialog = async () => {
<QDialog v-model="confirmationDialog"> <QDialog v-model="confirmationDialog">
<QCard style="min-width: 350px"> <QCard style="min-width: 350px">
<QCardSection> <QCardSection>
<p class="text-h6 q-ma-none">{{ t('Select the starting date') }}</p> <p class="text-h6 q-ma-none">{{ t('route.ticket.selectStartingDate') }}</p>
</QCardSection> </QCardSection>
<QCardSection class="q-pt-none"> <QCardSection class="q-pt-none">
<VnInputDate <VnInputDate
:label="t('Stating date')" :label="t('route.ticket.startingDate')"
v-model="startingDate" v-model="startingDate"
autofocus autofocus
/> />
</QCardSection> </QCardSection>
<QCardActions align="right"> <QCardActions align="right">
<QBtn flat :label="t('Cancel')" v-close-popup class="text-primary" /> <QBtn flat :label="t('globals.cancel')" v-close-popup class="text-primary" />
<QBtn color="primary" v-close-popup @click="cloneRoutes"> <QBtn color="primary" v-close-popup @click="cloneRoutes">
{{ t('globals.clone') }} {{ t('globals.clone') }}
</QBtn> </QBtn>
@ -262,7 +276,7 @@ const openSmsDialog = async () => {
<QToolbar class="justify-end"> <QToolbar class="justify-end">
<div id="st-actions" class="q-pa-sm"> <div id="st-actions" class="q-pa-sm">
<QBtn icon="vn:wand" color="primary" class="q-mr-sm" @click="sortRoutes"> <QBtn icon="vn:wand" color="primary" class="q-mr-sm" @click="sortRoutes">
<QTooltip>{{ t('Sort routes') }}</QTooltip> <QTooltip>{{ t('route.ticket.sortRoutes') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
icon="vn:buscaman" icon="vn:buscaman"
@ -271,7 +285,7 @@ const openSmsDialog = async () => {
:disable="!selectedRows?.length" :disable="!selectedRows?.length"
@click="goToBuscaman()" @click="goToBuscaman()"
> >
<QTooltip>{{ t('Open buscaman') }}</QTooltip> <QTooltip>{{ t('route.ticket.openBuscaman') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
icon="filter_alt" icon="filter_alt"
@ -280,7 +294,7 @@ const openSmsDialog = async () => {
:disable="!selectedRows?.length" :disable="!selectedRows?.length"
@click="deletePriorities" @click="deletePriorities"
> >
<QTooltip>{{ t('Delete priority') }}</QTooltip> <QTooltip>{{ t('route.ticket.deletePriority') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
icon="format_list_numbered" icon="format_list_numbered"
@ -290,7 +304,7 @@ const openSmsDialog = async () => {
> >
<QTooltip <QTooltip
>{{ >{{
t('Renumber all tickets in the order you see on the screen') t('route.ticket.renumberAllTickets')
}} }}
</QTooltip> </QTooltip>
</QBtn> </QBtn>
@ -301,7 +315,7 @@ const openSmsDialog = async () => {
:disable="!selectedRows?.length" :disable="!selectedRows?.length"
@click="openSmsDialog" @click="openSmsDialog"
> >
<QTooltip>{{ t('Send SMS to all clients') }}</QTooltip> <QTooltip>{{ t('route.ticket.sendSmsClients') }}</QTooltip>
</QBtn> </QBtn>
</div> </div>
</QToolbar> </QToolbar>
@ -339,7 +353,7 @@ const openSmsDialog = async () => {
@click="setHighestPriority(row, rows)" @click="setHighestPriority(row, rows)"
> >
<QTooltip> <QTooltip>
{{ t('Assign highest priority') }} {{ t('route.ticket.assignHighestPriority') }}
</QTooltip> </QTooltip>
</QIcon> </QIcon>
<VnInput <VnInput
@ -354,7 +368,7 @@ const openSmsDialog = async () => {
<QTd> <QTd>
<span class="link" @click="goToBuscaman(row)"> <span class="link" @click="goToBuscaman(row)">
{{ value }} {{ value }}
<QTooltip>{{ t('Open buscaman') }}</QTooltip> <QTooltip>{{ t('route.ticket.openBuscaman') }}</QTooltip>
</span> </span>
</QTd> </QTd>
</template> </template>
@ -411,7 +425,7 @@ const openSmsDialog = async () => {
@click="openTicketsDialog" @click="openTicketsDialog"
> >
<QTooltip> <QTooltip>
{{ t('Add ticket') }} {{ t('route.ticket.addTicket') }}
</QTooltip> </QTooltip>
</QBtn> </QBtn>
</QPageSticky> </QPageSticky>
@ -432,24 +446,3 @@ const openSmsDialog = async () => {
gap: 12px; gap: 12px;
} }
</style> </style>
<i18n>
es:
Order: Orden
Street: Dirección fiscal
City: Población
PC: CP
Client: Cliente
Warehouse: Almacén
Packages: Bultos
Packaging: Encajado
Confirm removal from route: Quitar de la ruta
Are you sure you want to remove this ticket from the route?: ¿Seguro que quieres quitar este ticket de la ruta?
Sort routes: Ordenar rutas
Open buscaman: Abrir buscaman
Delete priority: Borrar orden
Renumber all tickets in the order you see on the screen: Renumerar todos los tickets con el orden que ves por pantalla
Assign highest priority: Asignar máxima prioridad
Send SMS to all clients: Mandar sms a todos los clientes de las rutas
Send SMS to the selected tickets: Enviar SMS a los tickets seleccionados
Add ticket: Añadir ticket
</i18n>

View File

@ -1,6 +1,33 @@
route: route:
filter: filter:
Served: Served Served: Served
summary:
date: Date
agency: Agency
vehicle: Vehicle
driver: Driver
cost: Cost
started: Started time
finished: Finished time
kmStart: Km start
kmEnd: Km end
volume: Volume
packages: Packages
description: Description
tickets: Tickets
order: Order
street: Street
city: City
pc: PC
client: Client
state: State
m3:
packaging: Packaging
ticket: Ticket
closed: Closed
open: Open
yes: Yes
no: No
extendedList: extendedList:
selectStartingDate: Select the starting date selectStartingDate: Select the starting date
startingDate: Starting date startingDate: Starting date
@ -75,3 +102,47 @@ route:
searchInfo: You can search by route reference searchInfo: You can search by route reference
dated: Dated dated: Dated
preview: Preview preview: Preview
delivered: Delivered
forecast: Forecast
cmr:
search: Search Cmr
searchInfo: You can search Cmr by Id
params:
results: results
cmrFk: CMR id
hasCmrDms: Attached in gestdoc
true: Yes
false: No
ticketFk: Ticketd id
routeFk: Route id
countryFk: Country
clientFk: Client id
warehouseFk: Warehouse
shipped: Preparation date
viewCmr: View CMR
downloadCmrs: Download CMRs
search: General search
ticket:
order: Order
street: Street
city: City
PC: PC
client: Client
warehouse: Warehouse
delivered: Delivered
estimated: Estimated
packages: Packages
packaging: Packaging
ticket: Ticket
confirmRemoval: Confirm removal from route
confirmRemovalConfirmation: Are you sure you want to remove this ticket from the route?
selectStartingDate: Select the starting date
startingDate: Starting date
sortRoutes: Sort routes
openBuscaman: Open buscaman
deletePriority: Delete priority
renumberAllTickets: Renumber all tickets in the order you see on the screen
assignHighest: Assign highest priority
sendSmsTickets: Send SMS to the selected tickets
sendSmsClients: Send SMS to all clients
addTicket: Add ticket

View File

@ -1,6 +1,31 @@
route: route:
filter: filter:
Served: Servida Served: Servida
summary:
date: Fecha
agency: Agencia
vehicle: Vehículo
driver: Conductor
cost: Costo
started: Hora inicio
finished: Hora fin
kmStart: Km inicio
kmEnd: Km fin
volume: Volumen
packages: Bultos
description: Descripción
tickets: Tickets
order: Orden
street: Dirección fiscal
city: Población
pc: CP
client: Cliente
state: Estado
packaging: Encajado
closed: Cerrada
open: Abierta
yes:
no: No
extendedList: extendedList:
selectStartingDate: Seleccione la fecha de inicio selectStartingDate: Seleccione la fecha de inicio
statingDate: Fecha de inicio statingDate: Fecha de inicio
@ -76,13 +101,15 @@ route:
searchInfo: Puedes buscar por referencia de la ruta searchInfo: Puedes buscar por referencia de la ruta
dated: Fecha dated: Fecha
preview: Vista previa preview: Vista previa
delivered: Entregado
forecast: Pronóstico
cmr: cmr:
list: list:
results: resultados results: resultados
cmrFk: Id CMR cmrFk: Id CMR
hasCmrDms: Gestdoc hasCmrDms: Gestdoc
'true': true:
'false': 'No' false: No
ticketFk: Id ticket ticketFk: Id ticket
routeFk: Id ruta routeFk: Id ruta
country: País country: País
@ -90,3 +117,27 @@ route:
shipped: Fecha preparación shipped: Fecha preparación
viewCmr: Ver CMR viewCmr: Ver CMR
downloadCmrs: Descargar CMRs downloadCmrs: Descargar CMRs
ticket:
order: Orden
street: Dirección fiscal
city: Población
PC: CP
client: Cliente
warehouse: Almacén
delivered: Entregado
estimated: Pronóstico
packages: Bultos
packaging: Encajado
ticket: Ticket
confirmRemoval: Quitar de la ruta
confirmRemovalConfirmation: ¿Seguro que quieres quitar este ticket de la ruta?
selectStartingDate: Seleccionar fecha de inicio
startingDate: F. Inicio
sortRoutes: Ordenar rutas
openBuscaman: Abrir buscaman
deletePriority: Borrar orden
renumberAllTickets: Renumerar todos los tickets con el orden que ves por pantalla
assignHighest: Asignar máxima prioridad
sendSmsTickets: Enviar SMS a los tickets seleccionados
sendSmsClients: Mandar sms a todos los clientes de las rutas
addTicket: Añadir ticket

View File

@ -463,6 +463,32 @@ function setReference(data) {
dialogData.value.value.description = newDescription; dialogData.value.value.description = newDescription;
} }
function exprBuilder(param, value) {
switch (param) {
case 'stateFk':
return { 'ts.stateFk': value };
case 'provinceFk':
return { 'a.provinceFk': value };
case 'hour':
return { 'z.hour': value };
case 'shipped':
return {
't.shipped': {
between: this.dateRange(value),
},
};
case 'departmentFk':
return { 'c.departmentFk': value };
case 'id':
case 'refFk':
case 'zoneFk':
case 'nickname':
case 'agencyModeFk':
case 'warehouseFk':
return { [`t.${param}`]: value };
}
}
</script> </script>
<template> <template>
@ -657,17 +683,14 @@ function setReference(data) {
</VnSelect> </VnSelect>
</VnRow> </VnRow>
<VnRow> <VnRow>
<div class="col">
<VnInputDate <VnInputDate
placeholder="dd-mm-aaa" placeholder="dd-mm-aaa"
:label="t('globals.landed')" :label="t('globals.landed')"
v-model="data.landed" v-model="data.landed"
@update:model-value="() => fetchAvailableAgencies(data)" @update:model-value="() => fetchAvailableAgencies(data)"
/> />
</div>
</VnRow> </VnRow>
<VnRow> <VnRow>
<div class="col">
<VnSelect <VnSelect
url="Warehouses" url="Warehouses"
:sort-by="['name']" :sort-by="['name']"
@ -680,10 +703,8 @@ function setReference(data) {
}" }"
@update:model-value="() => fetchAvailableAgencies(data)" @update:model-value="() => fetchAvailableAgencies(data)"
/> />
</div>
</VnRow> </VnRow>
<VnRow> <VnRow>
<div class="col">
<VnSelect <VnSelect
:label="t('globals.agency')" :label="t('globals.agency')"
v-model="data.agencyModeId" v-model="data.agencyModeId"
@ -692,7 +713,6 @@ function setReference(data) {
option-label="agencyMode" option-label="agencyMode"
hide-selected hide-selected
/> />
</div>
</VnRow> </VnRow>
</template> </template>
</VnTable> </VnTable>

View File

@ -46,6 +46,28 @@ const { openConfirmationModal } = useVnConfirm();
:summary="$props.summary" :summary="$props.summary"
:to-module="{ name: 'WorkerDepartment' }" :to-module="{ name: 'WorkerDepartment' }"
data-key="Department" data-key="Department"
:filter="{
include: [
{
relation: 'client',
scope: {
fields: ['id', 'name'],
},
},
{
relation: 'worker',
scope: {
fields: ['id', 'name'],
include: {
relation: 'user',
scope: {
fields: ['id', 'name'],
},
},
},
},
],
}"
> >
<template #menu="{}"> <template #menu="{}">
<QItem <QItem

View File

@ -1,4 +1,3 @@
version: '3.7'
services: services:
back: back:
image: 'registry.verdnatura.es/salix-back:${COMPOSE_TAG:-dev}' image: 'registry.verdnatura.es/salix-back:${COMPOSE_TAG:-dev}'

View File

@ -24,13 +24,8 @@ export CI=true
export TZ=Europe/Madrid export TZ=Europe/Madrid
# IMAGES # IMAGES
docker build -t registry.verdnatura.es/salix-back:dev -f "$salix_dir/back/Dockerfile" "$salix_dir" docker-compose -f test/cypress/docker-compose.yml --project-directory . pull db
cd "$salix_dir" && npx myt run -t docker-compose -f test/cypress/docker-compose.yml --project-directory . pull back
docker exec vn-database sh -c "rm -rf /mysql-template"
docker exec vn-database sh -c "cp -a /var/lib/mysql /mysql-template"
docker commit vn-database registry.verdnatura.es/salix-db:dev
docker rm -f vn-database
cd "$current_dir"
docker build -f ./docs/Dockerfile.dev -t lilium-dev . docker build -f ./docs/Dockerfile.dev -t lilium-dev .
# END IMAGES # END IMAGES

View File

@ -7,7 +7,27 @@ describe('Client credits', () => {
timeout: 5000, timeout: 5000,
}); });
}); });
it('Should load layout', () => {
it('Should put a new credit', () => {
cy.get('.q-page').should('be.visible'); cy.get('.q-page').should('be.visible');
cy.dataCy('vnTableCreateBtn').click();
cy.dataCy('Credit_input').type('100');
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data saved');
});
it('Should put a new credit with value 0 to close the client card', () => {
cy.get('.q-page').should('be.visible');
cy.dataCy('vnTableCreateBtn').click();
cy.dataCy('Credit_input').type('0');
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data saved');
});
it('Should not create the credit if there is no value in the input', () => {
cy.get('.q-page').should('be.visible');
cy.dataCy('vnTableCreateBtn').click();
cy.dataCy('FormModelPopup_save').click();
cy.get('.q-notification__message').should('not.exist');
}); });
}); });

View File

@ -1,30 +1,55 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('Client fiscal data', { testIsolation: true }, () => { function checkSageFields(isRequired = false) {
const haveAttr = isRequired ? 'have.attr' : 'not.have.attr';
cy.dataCy('sageTaxTypeFk').filter('input').should(haveAttr, 'required');
cy.dataCy('sageTransactionTypeFk').filter('input').should(haveAttr, 'required');
}
describe('Client fiscal data', () => {
describe('#1008', () => {
beforeEach(() => { beforeEach(() => {
cy.viewport(1280, 720); cy.viewport(1920, 1080);
cy.login('developer'); cy.login('developer');
cy.visit('#/customer/1107/fiscal-data'); cy.visit('#/customer/1108/fiscal-data');
}); });
it.skip('Should change required value when change customer', () => { it('Should change required value when change customer', () => {
cy.get('.q-card').should('be.visible'); cy.get('.q-card').should('be.visible');
cy.dataCy('sageTaxTypeFk').filter('input').should('not.have.attr', 'required'); checkSageFields();
cy.get('[data-cy="vnCheckboxVerified data"]').click();
cy.get('.q-btn-group > .q-btn--standard > .q-btn__content').click();
checkSageFields();
cy.get('[data-cy="vnCheckboxVerified data"]').click();
checkSageFields(true);
cy.get('#searchbar input').clear(); cy.get('#searchbar input').clear();
cy.get('#searchbar input').type('1{enter}'); cy.get('#searchbar input').type('1{enter}');
cy.get('.q-item > .q-item__label').should('have.text', ' #1'); cy.get('.q-item > .q-item__label').should('have.text', ' #1');
cy.dataCy('sageTaxTypeFk').filter('input').should('have.attr', 'required'); checkSageFields();
});
});
describe('#1007', () => {
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit('#/customer/1107/fiscal-data');
}); });
it('check as equalizated', () => { it.skip('check as equalizated', () => {
cy.get('[data-cy="vnCheckboxInvoice by address"] > .q-checkbox__inner').then(
($el) => {
if (!$el.hasClass('q-checkbox__inner--truthy')) {
cy.wrap($el).click({ force: true });
}
},
);
cy.get( cy.get(
':nth-child(1) > .q-checkbox > .q-checkbox__inner > .q-checkbox__bg', '[data-cy="vnCheckboxInvoice by address"] > .q-checkbox__inner',
).click(); ).should('have.class', 'q-checkbox__inner--truthy');
cy.dataCy('vnCheckboxIs equalizated').click();
cy.get('.q-btn-group > .q-btn--standard > .q-btn__content').click(); cy.get('.q-btn-group > .q-btn--standard > .q-btn__content').click();
cy.get('.q-card > :nth-child(1) > span').should( cy.get('.q-card > :nth-child(1) > span').should(
'contain', 'contain',
'You changed the equalization tax', 'You changed the equalization tax',
); );
cy.get('.q-card > :nth-child(2) > span').should( cy.get('.q-card > :nth-child(2) > span').should(
'have.text', 'have.text',
'Do you want to spread the change?', 'Do you want to spread the change?',
@ -34,4 +59,5 @@ describe('Client fiscal data', { testIsolation: true }, () => {
'.bg-warning > .q-notification__wrapper > .q-notification__content > .q-notification__message', '.bg-warning > .q-notification__wrapper > .q-notification__content > .q-notification__message',
).should('have.text', 'Equivalent tax spreaded'); ).should('have.text', 'Equivalent tax spreaded');
}); });
});
}); });

View File

@ -58,8 +58,8 @@ describe('Client list', { testIsolation: true }, () => {
cy.waitForElement('.q-form'); cy.waitForElement('.q-form');
cy.checkValueForm(1, search); cy.checkValueForm(1, search);
cy.checkValueForm(2, search); cy.checkValueForm(2, search);
cy.dataCy('Customer_select').should('have.value', search); cy.dataCy('Customer_select').contains(search);
cy.dataCy('Address_select').should('have.value', search); cy.dataCy('Address_select').contains(search);
}); });
it('Client founded create order', () => { it('Client founded create order', () => {
@ -74,7 +74,7 @@ describe('Client list', { testIsolation: true }, () => {
cy.waitForElement('#formModel'); cy.waitForElement('#formModel');
cy.waitForElement('.q-form'); cy.waitForElement('.q-form');
cy.checkValueForm(1, search); cy.checkValueForm(1, search);
cy.dataCy('Client_select').should('have.value', search); cy.dataCy('Client_select').contains(search);
cy.dataCy('Address_select').should('have.value', search); cy.dataCy('Address_select').contains(search);
}); });
}); });

View File

@ -1,5 +1,6 @@
import '../commands.js'; import '../commands.js';
describe('EntryBuys', () => { // https://redmine.verdnatura.es/issues/9008
describe.skip('EntryBuys', () => {
beforeEach(() => { beforeEach(() => {
cy.viewport(1920, 1080); cy.viewport(1920, 1080);
cy.login('buyer'); cy.login('buyer');
@ -95,7 +96,7 @@ describe('EntryBuys', () => {
cy.get('input[data-cy="itemFk-create-popup"]').type('1'); cy.get('input[data-cy="itemFk-create-popup"]').type('1');
cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click(); cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click();
cy.get('input[data-cy="Grouping mode_select"]').should('have.value', 'packing'); cy.dataCy('Grouping mode_select').contains('packing');
cy.get('button[data-cy="FormModelPopup_save"]').click(); cy.get('button[data-cy="FormModelPopup_save"]').click();
} }
}); });

View File

@ -17,11 +17,9 @@ describe('EntryNotes', () => {
const editObservation = (rowIndex, type, description) => { const editObservation = (rowIndex, type, description) => {
cy.get(`td[data-col-field="description"][data-row-index="${rowIndex}"]`) cy.get(`td[data-col-field="description"][data-row-index="${rowIndex}"]`)
.click() .click()
.clear()
.type(description); .type(description);
cy.get(`td[data-col-field="observationTypeFk"][data-row-index="${rowIndex}"]`) cy.get(`td[data-col-field="observationTypeFk"][data-row-index="${rowIndex}"]`)
.click() .click()
.clear()
.type(type); .type(type);
cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click(); cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click();
cy.saveCard(); cy.saveCard();

View File

@ -44,9 +44,9 @@ describe('invoiceInCorrective', { testIsolation: true }, () => {
cy.url().should('include', `/invoice-in/${correctingFk}/summary`); cy.url().should('include', `/invoice-in/${correctingFk}/summary`);
cy.visit(`/#/invoice-in/${correctingFk}/corrective`); cy.visit(`/#/invoice-in/${correctingFk}/corrective`);
cy.dataCy('invoiceInCorrective_class').should('be.disabled'); checkIsDisabled('class');
cy.dataCy('invoiceInCorrective_type').should('be.disabled'); checkIsDisabled('type');
cy.dataCy('invoiceInCorrective_reason').should('be.disabled'); checkIsDisabled('reason');
}); });
}); });
@ -56,4 +56,10 @@ describe('invoiceInCorrective', { testIsolation: true }, () => {
cy.clickDescriptorAction(4); cy.clickDescriptorAction(4);
cy.get('[data-cy="InvoiceInCorrective-menu-item"]').should('exist'); cy.get('[data-cy="InvoiceInCorrective-menu-item"]').should('exist');
}); });
function checkIsDisabled(column) {
cy.dataCy(`invoiceInCorrective_${column}`)
.parents('.q-field')
.should('have.class', 'q-field--disabled');
}
}); });

View File

@ -132,9 +132,9 @@ function createCorrective() {
const correctingId = response.body; const correctingId = response.body;
cy.url().should('include', `/invoice-in/${correctingId}/summary`); cy.url().should('include', `/invoice-in/${correctingId}/summary`);
cy.visit(`/#/invoice-in/${correctingId}/corrective`); cy.visit(`/#/invoice-in/${correctingId}/corrective`);
cy.dataCy('invoiceInCorrective_class').should('contain.value', 'R2'); cy.dataCy('invoiceInCorrective_class').contains('R2');
cy.dataCy('invoiceInCorrective_type').should('contain.value', 'diferencias'); cy.dataCy('invoiceInCorrective_type').contains('diferencias');
cy.dataCy('invoiceInCorrective_reason').should('contain.value', 'sales details'); cy.dataCy('invoiceInCorrective_reason').contains('sales details');
}); });
} }

View File

@ -12,11 +12,8 @@ describe('InvoiceInIntrastat', () => {
it('should edit the first line', () => { it('should edit the first line', () => {
cy.selectOption(`${firstRow} ${codes}`, 'Plantas vivas: Esqueje/injerto, Vid'); cy.selectOption(`${firstRow} ${codes}`, 'Plantas vivas: Esqueje/injerto, Vid');
cy.get(firstRowAmount).clear();
cy.saveCard(); cy.saveCard();
cy.get(codes) cy.get(codes).eq(0).contains('6021010: Plantas vivas: Esqueje/injerto, Vid');
.eq(0)
.should('have.value', '6021010: Plantas vivas: Esqueje/injerto, Vid');
}); });
it('should add a new row', () => { it('should add a new row', () => {
@ -30,7 +27,7 @@ describe('InvoiceInIntrastat', () => {
'FR', 'FR',
]); ]);
cy.saveCard(); cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved'); cy.checkNotification('Data saved');
}); });
it('should remove the first line', () => { it('should remove the first line', () => {

View File

@ -52,7 +52,7 @@ describe('InvoiceInList', () => {
title: mockInvoiceRef, title: mockInvoiceRef,
listBox: { 0: '11/16/2001', 3: 'The farmer' }, listBox: { 0: '11/16/2001', 3: 'The farmer' },
}); });
cy.dataCy('invoiceInBasicDataCompanyFk').should('have.value', 'ORN'); cy.dataCy('invoiceInBasicDataCompanyFk').contains('ORN');
}); });
}); });
}); });

View File

@ -15,7 +15,7 @@ describe('InvoiceInVat', () => {
it('should edit the sage iva', () => { it('should edit the sage iva', () => {
cy.selectOption(`${firstLineVat} ${vats}`, 'H.P. IVA 21% CEE'); cy.selectOption(`${firstLineVat} ${vats}`, 'H.P. IVA 21% CEE');
cy.saveCard(); cy.saveCard();
cy.get(vats).eq(0).should('have.value', '8: H.P. IVA 21% CEE'); cy.get(vats).eq(0).contains('8: H.P. IVA 21% CEE');
}); });
it('should mark the line as deductible VAT', () => { it('should mark the line as deductible VAT', () => {
@ -23,9 +23,7 @@ describe('InvoiceInVat', () => {
cy.saveCard(); cy.saveCard();
cy.get(`${firstLineVat} [data-cy="isDeductible_checkbox"]`) cy.get(`${firstLineVat} [data-cy="isDeductible_checkbox"]`).click();
.click();
cy.saveCard(); cy.saveCard();
}); });

View File

@ -5,14 +5,13 @@ describe('Logout', { testIsolation: true }, () => {
cy.visit(`/#/dashboard`); cy.visit(`/#/dashboard`);
cy.waitForElement('.q-page', 6000); cy.waitForElement('.q-page', 6000);
}); });
describe('by user', () => {
it('should logout', () => { it('should logout', () => {
cy.get('#user').click(); cy.get('#user').click();
cy.get('#logout').click(); cy.get('#logout').click();
}); });
});
describe('not user', () => { it('should throw session expired error if token has expired or is not valid during navigation', () => {
beforeEach(() => {
cy.intercept('GET', '**StarredModules**', { cy.intercept('GET', '**StarredModules**', {
statusCode: 401, statusCode: 401,
body: { body: {
@ -25,13 +24,9 @@ describe('Logout', { testIsolation: true }, () => {
}, },
statusMessage: 'AUTHORIZATION_REQUIRED', statusMessage: 'AUTHORIZATION_REQUIRED',
}).as('badRequest'); }).as('badRequest');
});
it('when token not exists', () => {
cy.get('.q-list').should('be.visible').first().should('be.visible').click(); cy.get('.q-list').should('be.visible').first().should('be.visible').click();
cy.wait('@badRequest'); cy.wait('@badRequest');
cy.checkNotification('Authorization Required'); cy.checkNotification('Your session has expired. Please log in again');
});
}); });
}); });

View File

@ -59,8 +59,8 @@ describe('OrderList', { testIsolation: true }, () => {
).click(); ).click();
cy.dataCy('vnTableCreateBtn').click(); cy.dataCy('vnTableCreateBtn').click();
cy.get(clientCreateSelect).should('have.value', 'Bruce Wayne'); cy.get(clientCreateSelect).contains('Bruce Wayne');
cy.get(addressCreateSelect).should('have.value', 'Bruce Wayne'); cy.get(addressCreateSelect).contains('Bruce Wayne');
cy.dataCy('landedDate').find('input').type('06/01/2001'); cy.dataCy('landedDate').find('input').type('06/01/2001');
cy.selectOption(agencyCreateSelect, 1); cy.selectOption(agencyCreateSelect, 1);

View File

@ -1,8 +1,7 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('ParkingBasicData', () => { describe('ParkingBasicData', () => {
const codeInput = 'form .q-card .q-input input'; const codeInput = 'form .q-card .q-input input';
const sectorSelect = 'form .q-card .q-select input'; const sectorSelect = 'form .q-card .q-select';
const sectorOpt = '.q-menu .q-item';
beforeEach(() => { beforeEach(() => {
cy.login('developer'); cy.login('developer');
cy.visit(`/#/shelving/parking/1/basic-data`); cy.visit(`/#/shelving/parking/1/basic-data`);
@ -17,8 +16,7 @@ describe('ParkingBasicData', () => {
}); });
it('should edit the code and sector', () => { it('should edit the code and sector', () => {
cy.get(sectorSelect).type('First'); cy.selectOption(sectorSelect, 'First');
cy.get(sectorOpt).click();
cy.get(codeInput).eq(0).clear(); cy.get(codeInput).eq(0).clear();
cy.get(codeInput).eq(0).type('700-01'); cy.get(codeInput).eq(0).type('700-01');
@ -27,7 +25,7 @@ describe('ParkingBasicData', () => {
cy.saveCard(); cy.saveCard();
cy.checkNotification('Data saved'); cy.checkNotification('Data saved');
cy.get(sectorSelect).should('have.value', 'First sector'); cy.get(sectorSelect).contains('First sector');
cy.get(codeInput).should('have.value', '700-01'); cy.get(codeInput).should('have.value', '700-01');
cy.dataCy('Picking order_input').should('have.value', 80230); cy.dataCy('Picking order_input').should('have.value', 80230);
}); });

View File

@ -44,11 +44,11 @@ describe('TicketList', () => {
cy.intercept('GET', /\/api\/Clients\?filter/).as('clientFilter'); cy.intercept('GET', /\/api\/Clients\?filter/).as('clientFilter');
cy.vnTableCreateBtn(); cy.vnTableCreateBtn();
cy.wait('@clientFilter'); cy.wait('@clientFilter');
cy.dataCy('Customer_select').should('have.value', 'Bruce Wayne'); cy.dataCy('Customer_select').contains('Bruce Wayne');
cy.dataCy('Address_select').click(); cy.dataCy('Address_select').click();
cy.getOption().click(); cy.getOption().click();
cy.dataCy('Address_select').should('have.value', 'Bruce Wayne'); cy.dataCy('Address_select').contains('Bruce Wayne');
}); });
it('Client list create new ticket', () => { it('Client list create new ticket', () => {
cy.vnTableCreateBtn(); cy.vnTableCreateBtn();

View File

@ -5,48 +5,27 @@ describe('UserPanel', { testIsolation: true }, () => {
cy.login('developer'); cy.login('developer');
cy.visit(`/#dashboard`); cy.visit(`/#dashboard`);
cy.waitForElement('.q-page', 6000); cy.waitForElement('.q-page', 6000);
});
it('should notify when update user warehouse', () => {
const userWarehouse =
'.q-menu .q-gutter-xs > :nth-child(3) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native> .q-field__input';
// Abro el panel
cy.openUserPanel(); cy.openUserPanel();
// Compruebo la opcion inicial
cy.get(userWarehouse).should('have.value', 'VNL').click();
// Actualizo la opción
cy.getOption(3);
//Compruebo la notificación
cy.get('.q-notification').should('be.visible');
cy.get(userWarehouse).should('have.value', 'VNH');
//Restauro el valor
cy.get(userWarehouse).click();
cy.getOption(2);
}); });
it('should notify when update user company', () => { it('should notify when update user company', () => {
const userCompany = changeSelect('User company', 'VNH', 'VNL');
'.q-menu .q-gutter-xs > :nth-child(2) > .q-field--float > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native> .q-field__input';
// Abro el panel
cy.openUserPanel();
// Compruebo la opcion inicial
cy.get(userCompany).should('have.value', 'Warehouse One').click();
//Actualizo la opción
cy.getOption(3);
//Compruebo la notificación
cy.get('.q-notification').should('be.visible');
cy.get(userCompany).should('have.value', 'TestingWarehouse');
//Restauro el valor
cy.get(userCompany).click();
cy.getOption(1);
}); });
it('should notify when update user warehouse', () => {
changeSelect('User warehouse', 'TestingWarehouse', 'Warehouse One');
});
function changeSelect(field, newOption, oldOption) {
cy.get('.q-menu')
.contains(field)
.then(($field) => {
cy.wrap($field).contains(oldOption);
cy.selectOption($field, newOption);
cy.checkNotification('Data saved');
cy.wrap($field).contains(newOption);
// Restore
cy.selectOption($field, oldOption);
});
}
}); });

View File

@ -41,7 +41,7 @@ describe('VnLocation', { testIsolation: true }, () => {
cy.get( cy.get(
`${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(3) `, `${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(3) `,
).click(); ).click();
cy.dataCy('locationProvince').should('have.value', province); cy.dataCy('locationProvince').contains(province);
}); });
}); });
describe('Worker Create', () => { describe('Worker Create', () => {

View File

@ -11,6 +11,6 @@ describe('WorkerLocker', () => {
it('should allocates a locker', () => { it('should allocates a locker', () => {
cy.selectOption(lockerSelect, lockerCode); cy.selectOption(lockerSelect, lockerCode);
cy.saveCard(); cy.saveCard();
cy.get(lockerSelect).invoke('val').should('eq', lockerCode); cy.get(lockerSelect).contains(lockerCode);
}); });
}); });

View File

@ -40,7 +40,7 @@ describe('ZoneCalendar', { testIsolation: true }, () => {
it('should exclude an event', () => { it('should exclude an event', () => {
cy.get('.q-mb-sm > .q-radio__inner').click(); cy.get('.q-mb-sm > .q-radio__inner').click();
cy.get('.q-current-day > .q-calendar-month__day--label__wrapper').click(); cy.get('.q-current-day > .q-calendar-month__day--label__wrapper').click();
cy.get('.q-mt-lg > .q-btn--standard').click(); cy.get(submitBtn).click();
cy.get( cy.get(
'.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]', '.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]',
).click(); ).click();
@ -48,19 +48,15 @@ describe('ZoneCalendar', { testIsolation: true }, () => {
cy.dataCy('VnConfirm_confirm').click(); cy.dataCy('VnConfirm_confirm').click();
}); });
it( it('should not exclude an event if there are tickets for that zone and day', () => {
'should not exclude an event if there are tickets for that zone and day', cy.get('[data-cy="vn-searchbar_input"]').type('3{enter}');
{ testIsoaltion: true }, cy.get('[aria-label="Exclude"] > .q-radio__label').click();
() => {
cy.visit(`/#/zone/3/events`);
cy.get('.q-mb-sm > .q-radio__inner').click();
cy.get( cy.get(
'.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]', '.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]',
).click(); ).click();
cy.get('.q-mt-lg > .q-btn--standard').click(); cy.get(submitBtn).click();
cy.checkNotification( cy.checkNotification(
'Can not close this zone because there are tickets programmed for that day', 'Can not close this zone because there are tickets programmed for that day',
); );
}, });
);
}); });

View File

@ -178,33 +178,34 @@ Cypress.Commands.add('fillInForm', (obj, opts = {}) => {
cy.waitSpinner(); cy.waitSpinner();
const { form = '.q-form > .q-card', attr = 'aria-label' } = opts; const { form = '.q-form > .q-card', attr = 'aria-label' } = opts;
cy.waitForElement(form); cy.waitForElement(form);
cy.get(`${form} input`).each(([el]) => {
cy.wrap(el) cy.get(`${form} .q-field`).each(($el) => {
.invoke('attr', attr) cy.wrap($el).then(($element) => {
.then((key) => { const key = $element.attr(attr) || $element.find(`[${attr}]`).attr(attr);
const field = obj[key]; const field = obj[key];
if (!field) return; if (!field) return;
if (typeof field == 'string')
return cy
.wrap(el)
.type(`{selectall}{backspace}${field}`, { delay: 0 });
const { type, val } = field; const { type, val } =
typeof field === 'string' ? { type: 'string', val: field } : field;
switch (type) { switch (type) {
case 'select': case 'select':
cy.selectOption(el, val); cy.selectOption($el, val);
break; break;
case 'date': case 'date':
cy.get(el).type(`{selectall}{backspace}${val}`).blur(); cy.wrap($el)
.find('input')
.type(`{selectall}{backspace}${val}`)
.blur();
break; break;
case 'time': case 'time':
cy.get(el).click(); cy.wrap($el).click();
cy.get('.q-time .q-time__clock').contains(val.h).click(); cy.get('.q-time .q-time__clock').contains(val.h).click();
cy.get('.q-time .q-time__clock').contains(val.m).click(); cy.get('.q-time .q-time__clock').contains(val.m).click();
cy.get('.q-time .q-time__link').contains(val.x).click(); cy.get('.q-time .q-time__link').contains(val.x).click();
break; break;
default: default:
cy.wrap(el).type(`{selectall}${val}`, { delay: 0 }); cy.wrap($el).find('input').type(`{selectall}${val}`, { delay: 0 });
break; break;
} }
}); });