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

87
Jenkinsfile vendored
View File

@ -108,7 +108,6 @@ pipeline {
}
stage('E2E') {
environment {
CREDS = credentials('docker-registry')
COMPOSE_PROJECT = "${PROJECT_NAME}-${env.BUILD_ID}"
COMPOSE_PARAMS = "-p ${env.COMPOSE_PROJECT} -f test/cypress/docker-compose.yml --project-directory ."
}
@ -118,23 +117,24 @@ pipeline {
sh 'rm -rf test/cypress/screenshots'
env.COMPOSE_TAG = PROTECTED_BRANCH.contains(env.CHANGE_TARGET) ? env.CHANGE_TARGET : 'dev'
def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs')
sh 'docker login --username $CREDS_USR --password $CREDS_PSW $REGISTRY'
sh "docker-compose ${env.COMPOSE_PARAMS} pull back"
sh "docker-compose ${env.COMPOSE_PARAMS} pull db"
sh "docker-compose ${env.COMPOSE_PARAMS} up -d"
def modules = sh(script: "node test/cypress/docker/find/find.js ${env.COMPOSE_TAG}", returnStdout: true).trim()
def modules = sh(
script: "node test/cypress/docker/find/find.js ${env.COMPOSE_TAG}",
returnStdout: true
).trim()
echo "E2E MODULES: ${modules}"
image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") {
sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'"
script {
def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs')
sh "docker compose ${env.COMPOSE_PARAMS} up -d"
image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") {
sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'"
}
}
}
}
post {
always {
sh "docker-compose ${env.COMPOSE_PARAMS} down -v"
sh "docker compose ${env.COMPOSE_PARAMS} down -v"
archiveArtifacts artifacts: 'test/cypress/screenshots/**/*', allowEmptyArchive: true
junit(
testResults: 'junit/e2e-*.xml',
@ -153,17 +153,8 @@ pipeline {
VERSION = readFile 'VERSION.txt'
}
steps {
script {
sh 'quasar build'
def baseImage = "salix-frontend:${env.VERSION}"
def image = docker.build(baseImage, ".")
docker.withRegistry("https://${env.REGISTRY}", 'docker-registry') {
image.push()
image.push(env.BRANCH_NAME)
if (IS_LATEST) image.push('latest')
}
}
sh 'quasar build'
dockerBuild 'salix-frontend', '.'
}
}
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",
"version": "25.18.0",
"version": "25.22.0",
"description": "Salix frontend",
"productName": "Salix",
"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 { useCau } from 'src/composables/useCau';
export default boot(({ app }) => {
export default boot(({ app, router }) => {
QForm.mixins = [qFormMixin];
QLayout.mixins = [mainShortcutMixin];
@ -22,6 +22,14 @@ export default boot(({ app }) => {
}
switch (response?.status) {
case 401:
if (!router.currentRoute.value.name.toLowerCase().includes('login')) {
message = 'errors.sessionExpired';
} else message = 'login.loginError';
break;
case 403:
if (!message || message.toLowerCase() === 'access denied')
message = 'errors.accessDenied';
case 422:
if (error.name == 'ValidationError')
message += ` "${responseError.details.context}.${Object.keys(

View File

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

View File

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

View File

@ -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;
let field = columnFilter.value?.name ?? $props.column.name ?? name;
delete arrayData.store?.userParams?.[field];
delete arrayData.store?.filter?.where?.[field];
if (columnFilter.value?.inWhere) {
if (columnFilter.value.alias) field = columnFilter.value.alias + '.' + field;
return await arrayData.addFilterWhere({ [field]: value });

View File

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

View File

@ -77,7 +77,12 @@ function columnName(col) {
<template #tags="{ tag, formatFn, getLocale }">
<div class="q-gutter-x-xs">
<strong>{{ getLocale(`${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
<span
:class="{
'text-decoration-line-through': typeof chip === 'object',
}"
>{{ formatFn(tag) }}</span
>
</div>
</template>
<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 { isRequired, requiredFieldRule } = useRequired($attrs);
const { t } = useI18n();
const emit = defineEmits([
'update:modelValue',
'update:options',
'keyup.enter',
'remove',
'blur',
]);
const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);
const $props = defineProps({
modelValue: {
@ -126,6 +120,14 @@ const handleInsertMode = (e) => {
const handleUppercase = () => {
value.value = value.value?.toUpperCase() || '';
};
const listeners = computed(() =>
Object.fromEntries(
Object.entries($attrs).filter(
([key, val]) => key.startsWith('on') && typeof val === 'function',
),
),
);
</script>
<template>
@ -134,10 +136,9 @@ const handleUppercase = () => {
ref="vnInputRef"
v-model="value"
v-bind="{ ...$attrs, ...styleAttrs }"
v-on="listeners"
:type="$attrs.type"
:class="{ required: isRequired }"
@keyup.enter="emit('keyup.enter')"
@blur="emit('blur')"
@keydown="handleKeydown"
:clearable="false"
:rules="mixinRules"

View File

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

View File

@ -124,6 +124,7 @@ const {
} = toRefs($props);
const myOptions = ref([]);
const myOptionsOriginal = ref([]);
const myOptionsMap = ref(new Map());
const vnSelectRef = ref();
const lastVal = ref();
const noOneText = t('globals.noOne');
@ -140,7 +141,7 @@ const styleAttrs = computed(() => {
}
: {};
});
const isLoading = ref(false);
const hasFocus = ref(false);
const useURL = computed(() => $props.url);
const value = computed({
get() {
@ -166,6 +167,10 @@ const computedSortBy = computed(() => {
return $props.sortBy || $props.optionLabel + ' ASC';
});
const valueIsObject = computed(
() => modelValue.value && typeof modelValue.value == 'object',
);
const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
watch(options, (newValue) => {
@ -173,12 +178,22 @@ watch(options, (newValue) => {
});
watch(modelValue, async (newValue) => {
if (newValue?.neq) newValue = newValue.neq;
if (!myOptions?.value?.some((option) => option[optionValue.value] == newValue))
await fetchFilter(newValue);
if ($props.noOne) myOptions.value.unshift(noOneOpt.value);
});
watch(
() => myOptionsOriginal.value,
(newValue) => {
for (const item of newValue) {
myOptionsMap.value.set(item[optionValue.value], item);
}
},
);
onMounted(() => {
setOptions(options.value);
if (useURL.value && $props.modelValue && !findKeyInOptions())
@ -187,7 +202,7 @@ onMounted(() => {
});
const someIsLoading = computed(
() => (isLoading.value || !!arrayData?.isLoading?.value) && !isMenuOpened.value,
() => !!arrayData?.isLoading?.value && !isMenuOpened.value,
);
function findKeyInOptions() {
if (!$props.options) return;
@ -224,6 +239,9 @@ function filter(val, options) {
async function fetchFilter(val) {
if (!$props.url) return;
if (val && typeof val == 'object') {
val = val.neq;
}
const { fields, include, limit } = $props;
const sortBy = computedSortBy.value;
@ -298,13 +316,11 @@ async function onScroll({ to, direction, from, index }) {
if (from === 0 && index === 0) return;
if (!useURL.value && !$props.fetchRef) return;
if (direction === 'decrease') return;
if (to === lastIndex && arrayData.store.hasMoreData && !isLoading.value) {
isLoading.value = true;
if (to === lastIndex && arrayData.store.hasMoreData) {
await arrayData.loadMore();
setOptions(arrayData.store.data);
vnSelectRef.value.scrollTo(lastIndex);
await nextTick();
isLoading.value = false;
}
}
@ -347,22 +363,30 @@ function getCaption(opt) {
if (optionCaption.value === false) return;
return opt[optionCaption.value] || opt[optionValue.value];
}
function getOptionLabel(property) {
if (!myOptionsMap.value.size) return;
let value = modelValue.value;
if (property) {
value = modelValue.value[property];
}
return myOptionsMap.value.get(value)?.[optionLabel.value];
}
</script>
<template>
<QSelect
ref="vnSelectRef"
v-model="value"
:options="myOptions"
:option-label="optionLabel"
:option-value="optionValue"
v-bind="{ ...$attrs, ...styleAttrs }"
v-bind="{ ...$attrs, ...styleAttrs, hideSelected: hasFocus }"
@filter="filterHandler"
:emit-value="nullishToTrue($attrs['emit-value'])"
:map-options="nullishToTrue($attrs['map-options'])"
:use-input="nullishToTrue($attrs['use-input'])"
:hide-selected="nullishToTrue($attrs['hide-selected'])"
:fill-input="nullishToTrue($attrs['fill-input'])"
ref="vnSelectRef"
:use-input="hasFocus || !value"
:fill-input="false"
lazy-rules
:class="{ required: isRequired }"
:rules="mixinRules"
@ -372,10 +396,20 @@ function getCaption(opt) {
:loading="someIsLoading"
@virtual-scroll="onScroll"
@popup-hide="isMenuOpened = false"
@popup-show="isMenuOpened = true"
@popup-show="
async () => {
isMenuOpened = true;
hasFocus = true;
await $nextTick();
vnSelectRef?.$el?.querySelector('input')?.focus();
}
"
@keydown="handleKeyDown"
:data-cy="$attrs.dataCy ?? $attrs.label + '_select'"
:data-url="url"
@blur="hasFocus = false"
@update:model-value="() => vnSelectRef.blur()"
:is-required="false"
>
<template #append>
<QIcon
@ -425,6 +459,17 @@ function getCaption(opt) {
</QItemSection>
</QItem>
</template>
<template #selected v-if="valueIsObject && nullishToTrue($attrs['emit-value'])">
<span class="nowrap">
<span
class="text-strike"
v-if="modelValue?.neq"
v-text="getOptionLabel('neq')"
:title="getOptionLabel('neq')"
/>
<span v-else>{{ JSON.stringify(modelValue) }}</span>
</span>
</template>
</QSelect>
</template>
@ -432,4 +477,12 @@ function getCaption(opt) {
.q-field--outlined {
max-width: 100%;
}
.q-field__native {
@extend .nowrap;
}
.nowrap {
display: flex;
flex-wrap: nowrap !important;
}
</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 class="content">
<span class="link" @click.stop>
{{ card.name }}
{{ card.longName }}
<ItemDescriptorProxy :id="card.id" />
</span>
<p class="subName">{{ card.subName }}</p>
@ -57,11 +57,12 @@ const card = toRef(props, 'item');
<QIcon name="production_quantity_limits" size="xs" />
{{ card.minQuantity }}
</div>
<div class="footer">
<div class="footer q-mt-auto">
<div class="price">
<p v-if="isCatalog">
{{ card.available }} {{ t('to') }}
{{ toCurrency(card.price) }}
<span class="text-primary">{{ card.available }}</span>
{{ t('to') }}
<span class="text-bold" >{{ toCurrency(card.price) }}</span>
</p>
<slot name="price" />
<QIcon v-if="isCatalog" name="add_circle" class="icon">
@ -144,28 +145,29 @@ const card = toRef(props, 'item');
}
.footer {
.price {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
align-items: center;
justify-content: space-between;
p {
font-size: 12px;
}
.price {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
align-items: center;
justify-content: space-between;
.icon {
color: $primary;
font-size: 24px;
cursor: pointer;
}
p {
font-size: 12px;
}
.price-kg {
font-size: 12px;
.icon {
color: $primary;
font-size: 24px;
cursor: pointer;
}
}
.price-kg {
font-size: 12px;
}
}
.item-color-container {

View File

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

View File

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

View File

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

View File

@ -400,6 +400,8 @@ errors:
updateUserConfig: Error updating user config
tokenConfig: Error fetching token config
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
login:
title: Login
@ -894,6 +896,7 @@ components:
rate3: Packing price
minPrice: Min. Price
itemFk: Item id
dated: Date
userPanel:
copyToken: Token copied to clipboard
settings: Settings

View File

@ -396,6 +396,8 @@ errors:
updateUserConfig: Error al actualizar la configuración de usuario
tokenConfig: Error al obtener configuración de token
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
login:
title: Inicio de sesión
@ -978,6 +980,7 @@ components:
rate3: Precio packing
minPrice: Precio mínimo
itemFk: Id item
dated: Fecha
userPanel:
copyToken: Token copiado al portapapeles
settings: Configuración

View File

@ -2,14 +2,20 @@
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useQuasar } from 'quasar';
import { toCurrency, toDateHourMin } from 'src/filters';
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 VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n();
const route = useRoute();
const quasar = useQuasar();
const tableRef = ref();
@ -45,26 +51,45 @@ const columns = computed(() => [
align: 'right',
field: 'rating',
label: t('customer.summary.rating'),
name: 'rating',
create: true,
columnCreate: {
component: 'number',
autofocus: true,
},
name: 'rating'
},
{
align: 'right',
field: 'recommendedCredit',
format: ({ recommendedCredit }) => toCurrency(recommendedCredit),
label: t('customer.summary.recommendCredit'),
name: 'recommendedCredit',
create: true,
columnCreate: {
component: 'number',
autofocus: true,
},
name: 'recommendedCredit'
},
]);
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>
<template>
@ -72,8 +97,7 @@ const columns = computed(() => [
ref="tableRef"
data-key="ClientInformas"
url="ClientInformas"
:filter="filter"
:order="['created DESC']"
:user-filter="filter"
:columns="columns"
:right-search="false"
:is-editable="false"
@ -82,22 +106,43 @@ const columns = computed(() => [
:disable-option="{ card: true }"
auto-load
:create="{
urlCreate: `Clients/${route.params.id}/setRating`,
title: 'Create rating',
onDataSaved: () => tableRef.reload(),
formInitialData: {},
onDataSaved: ()=> tableRef.reload(),
formInitialData: defaultInitialData,
saveFn: handleSave
}"
>
<template #column-employee="{ row }">
<span class="link">{{ row.worker.user.nickname }}</span>
<WorkerDescriptorProxy :id="row.worker.id" />
</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>
</template>
<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:
Recommended credit: Crédito recomendado
Since: Desde
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>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,11 @@
<script setup>
import { onMounted, ref, onUnmounted, computed, watch } from 'vue';
import axios from 'axios';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState';
import { beforeSave } from 'src/composables/updateMinPriceBeforeSave';
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 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 ItemFixedPriceFilter from './ItemFixedPriceFilter.vue';
import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue';
import { toCurrency } from 'src/filters';
const stateStore = useStateStore();
const quasar = useQuasar();
const { t } = useI18n();
const tableRef = ref();
const editFixedPriceForm = ref(null);
@ -218,11 +220,36 @@ const dateStyle = (date) =>
}
: { color: dateColor, 'background-color': 'transparent' };
const onDataSaved = () => {
tableRef.value.CrudModelRef.saveChanges();
const onDataSaved = async (data) => {
for (const row of data) {
await axios.patch('FixedPrices/upsertFixedPrice', row);
}
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(() => {
if (tableRef.value) {
tableRef.value.showForm = false;
@ -273,7 +300,8 @@ watch(
data-key="ItemFixedPrices"
url="FixedPrices/filter"
:order="'name DESC'"
save-url="FixedPrices/crud"
save-url="FixedPrices/upsertFixedPrice"
:saveFn="saveData"
:columns="columns"
:is-editable="true"
:right-search="false"
@ -283,7 +311,8 @@ watch(
}"
v-model:selected="selectedRows"
:create="{
urlCreate: 'FixedPrices',
urlCreate: 'FixedPrices/upsertFixedPrice',
customMethod: 'patch',
title: t('Create fixed price'),
formInitialData: { warehouseFk: warehouse },
onDataSaved: () => tableRef.reload(),

View File

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

View File

@ -26,7 +26,6 @@ const $props = defineProps({
default: () => {},
},
});
const { t } = useI18n();
const emit = defineEmits(['onDataSaved']);
@ -38,7 +37,7 @@ const inputs = {
select: markRaw(VnSelect),
};
const newValue = ref(null);
const newFieldValue = ref(null);
const selectedField = ref(null);
const closeButton = ref(null);
const isLoading = ref(false);
@ -46,7 +45,11 @@ const isLoading = ref(false);
const onSubmit = async () => {
isLoading.value = true;
$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);
closeForm();
@ -71,17 +74,26 @@ const closeForm = () => {
class="editOption"
:label="t('Field to edit')"
:options="fieldsOptions"
hide-selected
option-label="label"
option-value="name"
v-model="selectedField"
data-cy="EditFixedPriceSelectOption"
@update:model-value="newValue = null"
: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
:is="inputs[selectedField?.component || 'input']"
v-bind="selectedField?.attrs || {}"
v-model="newValue"
v-model="newFieldValue"
:label="t('Value')"
data-cy="EditFixedPriceValueOption"
style="width: 200px"

View File

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

View File

@ -19,7 +19,7 @@ const { t } = useI18n();
const dataKey = 'OrderCatalogList';
const catalogParams = {
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, {
url: 'Orders/CatalogFilter',

View File

@ -68,6 +68,19 @@ onMounted(async () => {
<template #menu="{ entity }">
<RouteDescriptorMenu :route="entity" />
</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>
</template>
<i18n>

View File

@ -80,6 +80,20 @@ const ticketColumns = ref([
sortable: false,
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',
label: t('route.summary.packages'),
@ -267,61 +281,3 @@ const ticketColumns = ref([
</CardSummary>
</div>
</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(() => [
{
name: 'order',
label: t('Order'),
label: t('route.ticket.order'),
field: (row) => dashIfEmpty(row?.priority),
sortable: false,
align: 'center',
},
{
name: 'client',
label: t('Client'),
label: t('route.ticket.client'),
field: (row) => row?.nickname,
sortable: false,
align: 'left',
},
{
name: 'street',
label: t('Street'),
label: t('route.ticket.street'),
field: (row) => row?.street,
sortable: false,
align: 'left',
},
{
name: 'pc',
label: t('PC'),
label: t('route.ticket.PC'),
field: (row) => row?.postalCode,
sortable: false,
align: 'center',
},
{
name: 'city',
label: t('City'),
label: t('route.ticket.city'),
field: (row) => row?.city,
sortable: false,
align: 'left',
},
{
name: 'warehouse',
label: t('Warehouse'),
label: t('route.ticket.warehouse'),
field: (row) => row?.warehouseName,
sortable: false,
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',
label: t('Packages'),
label: t('route.ticket.packages'),
field: (row) => row?.packages,
sortable: false,
align: 'center',
@ -80,14 +94,14 @@ const columns = computed(() => [
},
{
name: 'packaging',
label: t('Packaging'),
label: t('route.ticket.packaging'),
field: (row) => row?.ipt,
sortable: false,
align: 'center',
},
{
name: 'ticket',
label: t('Ticket'),
label: t('route.ticket.ticket'),
field: (row) => row?.id,
sortable: false,
align: 'center',
@ -188,8 +202,8 @@ const confirmRemove = (ticket) => {
.dialog({
component: VnConfirm,
componentProps: {
title: t('Confirm removal from route'),
message: t('Are you sure you want to remove this ticket from the route?'),
title: t('route.ticket.confirmRemoval'),
message: t('route.ticket.confirmRemovalConfirmation'),
promise: () => removeTicket(ticket),
},
})
@ -219,7 +233,7 @@ const openSmsDialog = async () => {
quasar.dialog({
component: SendSmsDialog,
componentProps: {
title: t('Send SMS to the selected tickets'),
title: t('route.ticket.sendSmsTickets'),
url: 'Routes/sendSms',
destinationFk: clientsId.toString(),
destination: clientsPhone.toString(),
@ -240,18 +254,18 @@ const openSmsDialog = async () => {
<QDialog v-model="confirmationDialog">
<QCard style="min-width: 350px">
<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 class="q-pt-none">
<VnInputDate
:label="t('Stating date')"
:label="t('route.ticket.startingDate')"
v-model="startingDate"
autofocus
/>
</QCardSection>
<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">
{{ t('globals.clone') }}
</QBtn>
@ -262,7 +276,7 @@ const openSmsDialog = async () => {
<QToolbar class="justify-end">
<div id="st-actions" class="q-pa-sm">
<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
icon="vn:buscaman"
@ -271,7 +285,7 @@ const openSmsDialog = async () => {
:disable="!selectedRows?.length"
@click="goToBuscaman()"
>
<QTooltip>{{ t('Open buscaman') }}</QTooltip>
<QTooltip>{{ t('route.ticket.openBuscaman') }}</QTooltip>
</QBtn>
<QBtn
icon="filter_alt"
@ -280,7 +294,7 @@ const openSmsDialog = async () => {
:disable="!selectedRows?.length"
@click="deletePriorities"
>
<QTooltip>{{ t('Delete priority') }}</QTooltip>
<QTooltip>{{ t('route.ticket.deletePriority') }}</QTooltip>
</QBtn>
<QBtn
icon="format_list_numbered"
@ -290,7 +304,7 @@ const openSmsDialog = async () => {
>
<QTooltip
>{{
t('Renumber all tickets in the order you see on the screen')
t('route.ticket.renumberAllTickets')
}}
</QTooltip>
</QBtn>
@ -301,7 +315,7 @@ const openSmsDialog = async () => {
:disable="!selectedRows?.length"
@click="openSmsDialog"
>
<QTooltip>{{ t('Send SMS to all clients') }}</QTooltip>
<QTooltip>{{ t('route.ticket.sendSmsClients') }}</QTooltip>
</QBtn>
</div>
</QToolbar>
@ -339,7 +353,7 @@ const openSmsDialog = async () => {
@click="setHighestPriority(row, rows)"
>
<QTooltip>
{{ t('Assign highest priority') }}
{{ t('route.ticket.assignHighestPriority') }}
</QTooltip>
</QIcon>
<VnInput
@ -354,7 +368,7 @@ const openSmsDialog = async () => {
<QTd>
<span class="link" @click="goToBuscaman(row)">
{{ value }}
<QTooltip>{{ t('Open buscaman') }}</QTooltip>
<QTooltip>{{ t('route.ticket.openBuscaman') }}</QTooltip>
</span>
</QTd>
</template>
@ -411,7 +425,7 @@ const openSmsDialog = async () => {
@click="openTicketsDialog"
>
<QTooltip>
{{ t('Add ticket') }}
{{ t('route.ticket.addTicket') }}
</QTooltip>
</QBtn>
</QPageSticky>
@ -432,24 +446,3 @@ const openSmsDialog = async () => {
gap: 12px;
}
</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:
filter:
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:
selectStartingDate: Select the starting date
startingDate: Starting date
@ -75,3 +102,47 @@ route:
searchInfo: You can search by route reference
dated: Dated
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:
filter:
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:
selectStartingDate: Seleccione la fecha de inicio
statingDate: Fecha de inicio
@ -76,13 +101,15 @@ route:
searchInfo: Puedes buscar por referencia de la ruta
dated: Fecha
preview: Vista previa
delivered: Entregado
forecast: Pronóstico
cmr:
list:
results: resultados
cmrFk: Id CMR
hasCmrDms: Gestdoc
'true':
'false': 'No'
true:
false: No
ticketFk: Id ticket
routeFk: Id ruta
country: País
@ -90,3 +117,27 @@ route:
shipped: Fecha preparación
viewCmr: Ver CMR
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;
}
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>
<template>
@ -657,42 +683,36 @@ function setReference(data) {
</VnSelect>
</VnRow>
<VnRow>
<div class="col">
<VnInputDate
placeholder="dd-mm-aaa"
:label="t('globals.landed')"
v-model="data.landed"
@update:model-value="() => fetchAvailableAgencies(data)"
/>
</div>
<VnInputDate
placeholder="dd-mm-aaa"
:label="t('globals.landed')"
v-model="data.landed"
@update:model-value="() => fetchAvailableAgencies(data)"
/>
</VnRow>
<VnRow>
<div class="col">
<VnSelect
url="Warehouses"
:sort-by="['name']"
:label="t('globals.warehouse')"
v-model="data.warehouseId"
hide-selected
required
:where="{
isForTicket: true,
}"
@update:model-value="() => fetchAvailableAgencies(data)"
/>
</div>
<VnSelect
url="Warehouses"
:sort-by="['name']"
:label="t('globals.warehouse')"
v-model="data.warehouseId"
hide-selected
required
:where="{
isForTicket: true,
}"
@update:model-value="() => fetchAvailableAgencies(data)"
/>
</VnRow>
<VnRow>
<div class="col">
<VnSelect
:label="t('globals.agency')"
v-model="data.agencyModeId"
:options="agenciesOptions"
option-value="agencyModeFk"
option-label="agencyMode"
hide-selected
/>
</div>
<VnSelect
:label="t('globals.agency')"
v-model="data.agencyModeId"
:options="agenciesOptions"
option-value="agencyModeFk"
option-label="agencyMode"
hide-selected
/>
</VnRow>
</template>
</VnTable>

View File

@ -46,6 +46,28 @@ const { openConfirmationModal } = useVnConfirm();
:summary="$props.summary"
:to-module="{ name: 'WorkerDepartment' }"
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="{}">
<QItem

View File

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

View File

@ -24,13 +24,8 @@ export CI=true
export TZ=Europe/Madrid
# IMAGES
docker build -t registry.verdnatura.es/salix-back:dev -f "$salix_dir/back/Dockerfile" "$salix_dir"
cd "$salix_dir" && npx myt run -t
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-compose -f test/cypress/docker-compose.yml --project-directory . pull db
docker-compose -f test/cypress/docker-compose.yml --project-directory . pull back
docker build -f ./docs/Dockerfile.dev -t lilium-dev .
# END IMAGES

View File

@ -7,7 +7,27 @@ describe('Client credits', () => {
timeout: 5000,
});
});
it('Should load layout', () => {
it('Should put a new credit', () => {
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,37 +1,63 @@
/// <reference types="cypress" />
describe('Client fiscal data', { testIsolation: true }, () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1107/fiscal-data');
});
it.skip('Should change required value when change customer', () => {
cy.get('.q-card').should('be.visible');
cy.dataCy('sageTaxTypeFk').filter('input').should('not.have.attr', 'required');
cy.get('#searchbar input').clear();
cy.get('#searchbar input').type('1{enter}');
cy.get('.q-item > .q-item__label').should('have.text', ' #1');
cy.dataCy('sageTaxTypeFk').filter('input').should('have.attr', 'required');
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(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit('#/customer/1108/fiscal-data');
});
it('Should change required value when change customer', () => {
cy.get('.q-card').should('be.visible');
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').type('1{enter}');
cy.get('.q-item > .q-item__label').should('have.text', ' #1');
checkSageFields();
});
});
describe('#1007', () => {
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit('#/customer/1107/fiscal-data');
});
it('check as equalizated', () => {
cy.get(
':nth-child(1) > .q-checkbox > .q-checkbox__inner > .q-checkbox__bg',
).click();
cy.get('.q-btn-group > .q-btn--standard > .q-btn__content').click();
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(
'[data-cy="vnCheckboxInvoice by address"] > .q-checkbox__inner',
).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-card > :nth-child(1) > span').should(
'contain',
'You changed the equalization tax',
);
cy.get('.q-card > :nth-child(2) > span').should(
'have.text',
'Do you want to spread the change?',
);
cy.get('[data-cy="VnConfirm_confirm"] > .q-btn__content > .block').click();
cy.get(
'.bg-warning > .q-notification__wrapper > .q-notification__content > .q-notification__message',
).should('have.text', 'Equivalent tax spreaded');
cy.get('.q-card > :nth-child(1) > span').should(
'contain',
'You changed the equalization tax',
);
cy.get('.q-card > :nth-child(2) > span').should(
'have.text',
'Do you want to spread the change?',
);
cy.get('[data-cy="VnConfirm_confirm"] > .q-btn__content > .block').click();
cy.get(
'.bg-warning > .q-notification__wrapper > .q-notification__content > .q-notification__message',
).should('have.text', 'Equivalent tax spreaded');
});
});
});

View File

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

View File

@ -1,5 +1,6 @@
import '../commands.js';
describe('EntryBuys', () => {
// https://redmine.verdnatura.es/issues/9008
describe.skip('EntryBuys', () => {
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('buyer');
@ -95,7 +96,7 @@ describe('EntryBuys', () => {
cy.get('input[data-cy="itemFk-create-popup"]').type('1');
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();
}
});

View File

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

View File

@ -44,9 +44,9 @@ describe('invoiceInCorrective', { testIsolation: true }, () => {
cy.url().should('include', `/invoice-in/${correctingFk}/summary`);
cy.visit(`/#/invoice-in/${correctingFk}/corrective`);
cy.dataCy('invoiceInCorrective_class').should('be.disabled');
cy.dataCy('invoiceInCorrective_type').should('be.disabled');
cy.dataCy('invoiceInCorrective_reason').should('be.disabled');
checkIsDisabled('class');
checkIsDisabled('type');
checkIsDisabled('reason');
});
});
@ -56,4 +56,10 @@ describe('invoiceInCorrective', { testIsolation: true }, () => {
cy.clickDescriptorAction(4);
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;
cy.url().should('include', `/invoice-in/${correctingId}/summary`);
cy.visit(`/#/invoice-in/${correctingId}/corrective`);
cy.dataCy('invoiceInCorrective_class').should('contain.value', 'R2');
cy.dataCy('invoiceInCorrective_type').should('contain.value', 'diferencias');
cy.dataCy('invoiceInCorrective_reason').should('contain.value', 'sales details');
cy.dataCy('invoiceInCorrective_class').contains('R2');
cy.dataCy('invoiceInCorrective_type').contains('diferencias');
cy.dataCy('invoiceInCorrective_reason').contains('sales details');
});
}

View File

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

View File

@ -52,7 +52,7 @@ describe('InvoiceInList', () => {
title: mockInvoiceRef,
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', () => {
cy.selectOption(`${firstLineVat} ${vats}`, 'H.P. IVA 21% CEE');
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', () => {
@ -23,9 +23,7 @@ describe('InvoiceInVat', () => {
cy.saveCard();
cy.get(`${firstLineVat} [data-cy="isDeductible_checkbox"]`)
.click();
cy.get(`${firstLineVat} [data-cy="isDeductible_checkbox"]`).click();
cy.saveCard();
});

View File

@ -5,33 +5,28 @@ describe('Logout', { testIsolation: true }, () => {
cy.visit(`/#/dashboard`);
cy.waitForElement('.q-page', 6000);
});
describe('by user', () => {
it('should logout', () => {
cy.get('#user').click();
cy.get('#logout').click();
});
it('should logout', () => {
cy.get('#user').click();
cy.get('#logout').click();
});
describe('not user', () => {
beforeEach(() => {
cy.intercept('GET', '**StarredModules**', {
statusCode: 401,
body: {
error: {
statusCode: 401,
name: 'Error',
message: 'Authorization Required',
code: 'AUTHORIZATION_REQUIRED',
},
it('should throw session expired error if token has expired or is not valid during navigation', () => {
cy.intercept('GET', '**StarredModules**', {
statusCode: 401,
body: {
error: {
statusCode: 401,
name: 'Error',
message: 'Authorization Required',
code: 'AUTHORIZATION_REQUIRED',
},
statusMessage: 'AUTHORIZATION_REQUIRED',
}).as('badRequest');
});
},
statusMessage: 'AUTHORIZATION_REQUIRED',
}).as('badRequest');
cy.get('.q-list').should('be.visible').first().should('be.visible').click();
cy.wait('@badRequest');
it('when token not exists', () => {
cy.get('.q-list').should('be.visible').first().should('be.visible').click();
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();
cy.dataCy('vnTableCreateBtn').click();
cy.get(clientCreateSelect).should('have.value', 'Bruce Wayne');
cy.get(addressCreateSelect).should('have.value', 'Bruce Wayne');
cy.get(clientCreateSelect).contains('Bruce Wayne');
cy.get(addressCreateSelect).contains('Bruce Wayne');
cy.dataCy('landedDate').find('input').type('06/01/2001');
cy.selectOption(agencyCreateSelect, 1);

View File

@ -1,8 +1,7 @@
/// <reference types="cypress" />
describe('ParkingBasicData', () => {
const codeInput = 'form .q-card .q-input input';
const sectorSelect = 'form .q-card .q-select input';
const sectorOpt = '.q-menu .q-item';
const sectorSelect = 'form .q-card .q-select';
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/shelving/parking/1/basic-data`);
@ -17,8 +16,7 @@ describe('ParkingBasicData', () => {
});
it('should edit the code and sector', () => {
cy.get(sectorSelect).type('First');
cy.get(sectorOpt).click();
cy.selectOption(sectorSelect, 'First');
cy.get(codeInput).eq(0).clear();
cy.get(codeInput).eq(0).type('700-01');
@ -27,7 +25,7 @@ describe('ParkingBasicData', () => {
cy.saveCard();
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.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.vnTableCreateBtn();
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.getOption().click();
cy.dataCy('Address_select').should('have.value', 'Bruce Wayne');
cy.dataCy('Address_select').contains('Bruce Wayne');
});
it('Client list create new ticket', () => {
cy.vnTableCreateBtn();

View File

@ -5,48 +5,27 @@ describe('UserPanel', { testIsolation: true }, () => {
cy.login('developer');
cy.visit(`/#dashboard`);
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();
// 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', () => {
const userCompany =
'.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);
changeSelect('User company', 'VNH', 'VNL');
});
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(
`${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(3) `,
).click();
cy.dataCy('locationProvince').should('have.value', province);
cy.dataCy('locationProvince').contains(province);
});
});
describe('Worker Create', () => {

View File

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

View File

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