diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..3c3629e64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/Jenkinsfile b/Jenkinsfile index c20da8ab2..341fffefa 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,7 @@ #!/usr/bin/env groovy def PROTECTED_BRANCH +def IS_LATEST def BRANCH_ENV = [ test: 'test', @@ -10,16 +11,18 @@ def BRANCH_ENV = [ node { stage('Setup') { - env.FRONT_REPLICAS = 1 env.NODE_ENV = BRANCH_ENV[env.BRANCH_NAME] ?: 'dev' PROTECTED_BRANCH = [ 'dev', 'test', 'master', + 'main', 'beta' ].contains(env.BRANCH_NAME) + IS_LATEST = ['master', 'main'].contains(env.BRANCH_NAME) + // https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables echo "NODE_NAME: ${env.NODE_NAME}" echo "WORKSPACE: ${env.WORKSPACE}" @@ -58,6 +61,19 @@ pipeline { PROJECT_NAME = 'lilium' } stages { + stage('Version') { + when { + expression { PROTECTED_BRANCH } + } + steps { + script { + def packageJson = readJSON file: 'package.json' + def version = "${packageJson.version}-build${env.BUILD_ID}" + writeFile(file: 'VERSION.txt', text: version) + echo "VERSION: ${version}" + } + } + } stage('Install') { environment { NODE_ENV = "" @@ -71,17 +87,48 @@ pipeline { expression { !PROTECTED_BRANCH } } environment { - NODE_ENV = "" + NODE_ENV = '' + CI = 'true' + TZ = 'Europe/Madrid' } - steps { - sh 'pnpm run test:unit:ci' - } - post { - always { - junit( - testResults: 'junitresults.xml', - allowEmptyResults: true - ) + parallel { + stage('Unit') { + steps { + sh 'pnpm run test:unit:ci' + } + post { + always { + junit( + testResults: 'junit/vitest.xml', + allowEmptyResults: true + ) + } + } + } + stage('E2E') { + environment { + CREDENTIALS = credentials('docker-registry') + COMPOSE_PROJECT = "${PROJECT_NAME}-${env.BUILD_ID}" + COMPOSE_PARAMS = "-p ${env.COMPOSE_PROJECT} -f test/cypress/docker-compose.yml --project-directory ." + } + steps { + 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") { + sh 'cypress run --browser chromium' + } + } + } + post { + always { + sh "docker-compose ${env.COMPOSE_PARAMS} down -v" + junit( + testResults: 'junit/e2e.xml', + allowEmptyResults: true + ) + } + } } } } @@ -91,25 +138,30 @@ pipeline { } environment { CREDENTIALS = credentials('docker-registry') + VERSION = readFile 'VERSION.txt' } steps { - sh 'quasar build' script { - def packageJson = readJSON file: 'package.json' - env.VERSION = "${packageJson.version}-build${env.BUILD_ID}" + 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') + } } - dockerBuild() } } stage('Deploy') { when { expression { PROTECTED_BRANCH } } + environment { + VERSION = readFile 'VERSION.txt' + } steps { - script { - def packageJson = readJSON file: 'package.json' - env.VERSION = "${packageJson.version}-build${env.BUILD_ID}" - } withKubeConfig([ serverUrl: "$KUBERNETES_API", credentialsId: 'kubernetes', diff --git a/cypress.config.js b/cypress.config.js index a9e27fcfd..dfe963a12 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,12 +1,37 @@ import { defineConfig } from 'cypress'; -// https://docs.cypress.io/app/tooling/reporters -// https://docs.cypress.io/app/references/configuration -// https://www.npmjs.com/package/cypress-mochawesome-reporter + +let urlHost, reporter, reporterOptions; + +if (process.env.CI) { + urlHost = 'front'; + reporter = 'junit'; + reporterOptions = { + mochaFile: 'junit/e2e.xml', + toConsole: false, + }; +} else { + urlHost = 'localhost'; + reporter = 'cypress-mochawesome-reporter'; + reporterOptions = { + charts: true, + reportPageTitle: 'Cypress Inline Reporter', + reportFilename: '[status]_[datetime]-report', + embeddedScreenshots: true, + reportDir: 'test/cypress/reports', + inlineAssets: true, + }; +} export default defineConfig({ e2e: { - baseUrl: 'http://localhost:9000/', - experimentalStudio: true, + baseUrl: `http://${urlHost}:9000`, + experimentalStudio: false, + defaultCommandTimeout: 10000, + trashAssetsBeforeRuns: false, + requestTimeout: 10000, + responseTimeout: 30000, + pageLoadTimeout: 60000, + defaultBrowser: 'chromium', fixturesFolder: 'test/cypress/fixtures', screenshotsFolder: 'test/cypress/screenshots', supportFile: 'test/cypress/support/index.js', @@ -14,29 +39,19 @@ export default defineConfig({ downloadsFolder: 'test/cypress/downloads', video: false, specPattern: 'test/cypress/integration/**/*.spec.js', - experimentalRunAllSpecs: false, - watchForFileChanges: false, - reporter: 'cypress-mochawesome-reporter', - reporterOptions: { - charts: true, - reportPageTitle: 'Cypress Inline Reporter', - reportFilename: '[status]_[datetime]-report', - embeddedScreenshots: true, - reportDir: 'test/cypress/reports', - inlineAssets: true, - }, + experimentalRunAllSpecs: true, + watchForFileChanges: true, + reporter, + reporterOptions, component: { componentFolder: 'src', testFiles: '**/*.spec.js', supportFile: 'test/cypress/support/unit.js', }, - setupNodeEvents: async (on, config) => { - const plugin = await import('cypress-mochawesome-reporter/plugin'); - plugin.default(on); - - return config; - }, viewportWidth: 1280, viewportHeight: 720, }, + experimentalMemoryManagement: true, + defaultCommandTimeout: 10000, + numTestsKeptInMemory: 2, }); diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index df793fc75..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: '3.7' -services: - main: - image: registry.verdnatura.es/salix-frontend:${VERSION:?} - build: - context: . - dockerfile: ./Dockerfile diff --git a/docs/Dockerfile.dev b/docs/Dockerfile.dev new file mode 100644 index 000000000..29b194ffa --- /dev/null +++ b/docs/Dockerfile.dev @@ -0,0 +1,45 @@ +FROM debian:12.9-slim + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + gnupg2 \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && npm install -g corepack@0.31.0 \ + && corepack enable pnpm \ + && rm -rf /var/lib/apt/lists/* + +RUN apt-get update \ + && apt-get -y --no-install-recommends install \ + apt-utils \ + chromium \ + libasound2 \ + libgbm-dev \ + libgtk-3-0 \ + libgtk2.0-0 \ + libnotify-dev \ + libnss3 \ + libxss1 \ + libxtst6 \ + xauth \ + xvfb \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd -r -g 1000 app \ + && useradd -r -u 1000 -g app -m -d /home/app app +USER app + +ENV SHELL=bash +ENV PNPM_HOME="/home/app/.local/share/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +RUN pnpm setup \ + && pnpm install --global cypress@13.6.6 \ + && cypress install + +WORKDIR /app diff --git a/proxy-serve.js b/proxy-serve.js index 415968c85..1e9bcf96b 100644 --- a/proxy-serve.js +++ b/proxy-serve.js @@ -1,6 +1,6 @@ export default [ { path: '/api', - rule: { target: 'http://0.0.0.0:3000' }, + rule: { target: 'http://127.0.0.1:3000' }, }, ]; diff --git a/quasar.config.js b/quasar.config.js index 9467c92af..8b6125a90 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -11,6 +11,7 @@ import { configure } from 'quasar/wrappers'; import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; import path from 'path'; +const target = `http://${process.env.CI ? 'back' : 'localhost'}:3000`; export default configure(function (/* ctx */) { return { @@ -108,13 +109,17 @@ export default configure(function (/* ctx */) { }, proxy: { '/api': { - target: 'http://0.0.0.0:3000', + target: 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 diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 04ef13d45..182eeaafe 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -12,6 +12,7 @@ import SkeletonForm from 'components/ui/SkeletonForm.vue'; import VnConfirm from './ui/VnConfirm.vue'; import { tMobile } from 'src/composables/tMobile'; import { useArrayData } from 'src/composables/useArrayData'; +import { getDifferences, getUpdatedValues } from 'src/filters'; const { push } = useRouter(); const quasar = useQuasar(); @@ -284,7 +285,12 @@ function trimData(data) { } return data; } - +function onBeforeSave(formData, originalData) { + return getUpdatedValues( + Object.keys(getDifferences(formData, originalData)), + formData, + ); +} async function onKeyup(evt) { if (evt.key === 'Enter' && !('prevent-submit' in attrs)) { const input = evt.target; @@ -321,6 +327,7 @@ defineExpose({ class="q-pa-md" :style="maxWidth ? 'max-width: ' + maxWidth : ''" id="formModel" + :mapper="onBeforeSave" > <QCard> <slot diff --git a/src/components/FormModelPopup.vue b/src/components/FormModelPopup.vue index 672eeff7a..85943e91e 100644 --- a/src/components/FormModelPopup.vue +++ b/src/components/FormModelPopup.vue @@ -1,12 +1,13 @@ <script setup> -import { ref, computed } from 'vue'; +import { ref, computed, useAttrs, nextTick } from 'vue'; import { useI18n } from 'vue-i18n'; +import { useState } from 'src/composables/useState'; import FormModel from 'components/FormModel.vue'; const emit = defineEmits(['onDataSaved', 'onDataCanceled']); -defineProps({ +const props = defineProps({ title: { type: String, default: '', @@ -22,12 +23,21 @@ defineProps({ }); const { t } = useI18n(); - +const attrs = useAttrs(); +const state = useState(); const formModelRef = ref(null); const closeButton = ref(null); -const isSaveAndContinue = ref(false); -const onDataSaved = (formData, requestResponse) => { - if (closeButton.value && !isSaveAndContinue.value) closeButton.value.click(); +const isSaveAndContinue = ref(props.showSaveAndContinueBtn); +const isLoading = computed(() => formModelRef.value?.isLoading); +const reset = computed(() => formModelRef.value?.reset); + +const onDataSaved = async (formData, requestResponse) => { + if (!isSaveAndContinue.value) closeButton.value?.click(); + if (isSaveAndContinue.value) { + await nextTick(); + state.set(attrs.model, attrs.formInitialData); + } + isSaveAndContinue.value = props.showSaveAndContinueBtn; emit('onDataSaved', formData, requestResponse); }; @@ -36,9 +46,6 @@ const onClick = async (saveAndContinue) => { await formModelRef.value.save(); }; -const isLoading = computed(() => formModelRef.value?.isLoading); -const reset = computed(() => formModelRef.value?.reset); - defineExpose({ isLoading, onDataSaved, @@ -74,10 +81,7 @@ defineExpose({ data-cy="FormModelPopup_cancel" v-close-popup z-max - @click=" - isSaveAndContinue = false; - emit('onDataCanceled'); - " + @click="emit('onDataCanceled')" /> <QBtn :flat="showSaveAndContinueBtn" diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index e4b19988a..3e92c93a9 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -85,7 +85,15 @@ const refresh = () => window.location.reload(); </QTooltip> <PinnedModules ref="pinnedModulesRef" /> </QBtn> - <QBtn class="q-pa-none" rounded dense flat no-wrap id="user"> + <QBtn + class="q-pa-none" + rounded + dense + flat + no-wrap + id="user" + data-cy="userPanel_btn" + > <VnAvatar :worker-id="user.id" :title="user.name" diff --git a/src/components/VnTable/VnFilter.vue b/src/components/VnTable/VnFilter.vue index 2dad8fe52..e9660e4c2 100644 --- a/src/components/VnTable/VnFilter.vue +++ b/src/components/VnTable/VnFilter.vue @@ -91,7 +91,6 @@ const components = { event: updateEvent, attrs: { ...defaultAttrs, - style: 'min-width: 150px', }, forceAttrs, }, @@ -152,7 +151,7 @@ const onTabPressed = async () => { }; </script> <template> - <div v-if="showFilter" class="full-width flex-center" style="overflow: hidden"> + <div v-if="showFilter" class="full-width" style="overflow: hidden"> <VnColumn :column="$props.column" default="input" diff --git a/src/components/VnTable/VnOrder.vue b/src/components/VnTable/VnOrder.vue index e3795cc4b..47ed9acf4 100644 --- a/src/components/VnTable/VnOrder.vue +++ b/src/components/VnTable/VnOrder.vue @@ -23,6 +23,10 @@ const $props = defineProps({ type: Boolean, default: false, }, + align: { + type: String, + default: 'end', + }, }); const hover = ref(); const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl }); @@ -46,16 +50,27 @@ async function orderBy(name, direction) { } defineExpose({ orderBy }); + +function textAlignToFlex(textAlign) { + return `justify-content: ${ + { + 'text-center': 'center', + 'text-left': 'start', + 'text-right': 'end', + }[textAlign] || 'start' + };`; +} </script> <template> <div @mouseenter="hover = true" @mouseleave="hover = false" @click="orderBy(name, model?.direction)" - class="row items-center no-wrap cursor-pointer title" + class="items-center no-wrap cursor-pointer title" + :style="textAlignToFlex(align)" > <span :title="label">{{ label }}</span> - <sup v-if="name && model?.index"> + <div v-if="name && model?.index"> <QChip :label="!vertical ? model?.index : ''" :icon=" @@ -92,20 +107,16 @@ defineExpose({ orderBy }); /> </div> </QChip> - </sup> + </div> </div> </template> <style lang="scss" scoped> .title { display: flex; - justify-content: center; align-items: center; height: 30px; width: 100%; color: var(--vn-label-color); -} -sup { - vertical-align: super; /* Valor predeterminado */ - /* También puedes usar otros valores como "baseline", "top", "text-top", etc. */ + white-space: nowrap; } </style> diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 805a6fbd5..c1e541abb 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -31,6 +31,7 @@ import VnLv from 'components/ui/VnLv.vue'; 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'; const arrayData = useArrayData(useAttrs()['data-key']); const $props = defineProps({ @@ -50,10 +51,6 @@ const $props = defineProps({ type: Boolean, default: true, }, - rightSearchIcon: { - type: Boolean, - default: true, - }, rowClick: { type: [Function, Boolean], default: null, @@ -413,20 +410,13 @@ async function renderInput(rowId, field, clickedElement) { eventHandlers: { 'update:modelValue': async (value) => { if (isSelect && value) { - row[column.name] = value[column.attrs?.optionValue ?? 'id']; - row[column?.name + 'TextValue'] = - value[column.attrs?.optionLabel ?? 'name']; - await column?.cellEvent?.['update:modelValue']?.( - value, - oldValue, - row, - ); + await updateSelectValue(value, column, row, oldValue); } else row[column.name] = value; await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row); }, keyup: async (event) => { if (event.key === 'Enter') - await destroyInput(rowIndex, field, clickedElement); + await destroyInput(rowId, field, clickedElement); }, keydown: async (event) => { switch (event.key) { @@ -457,6 +447,17 @@ async function renderInput(rowId, field, clickedElement) { node.el?.querySelector('span > div > div').focus(); } +async function updateSelectValue(value, column, row, oldValue) { + row[column.name] = value[column.attrs?.optionValue ?? 'id']; + + row[column?.name + 'VnTableTextValue'] = value[column.attrs?.optionLabel ?? 'name']; + + if (column?.attrs?.find?.label) + row[column?.attrs?.find?.label] = value[column.attrs?.optionLabel ?? 'name']; + + await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row); +} + async function destroyInput(rowIndex, field, clickedElement) { if (!clickedElement) clickedElement = document.querySelector( @@ -519,9 +520,9 @@ function getToggleIcon(value) { } function formatColumnValue(col, row, dashIfEmpty) { - if (col?.format) { - if (selectRegex.test(col?.component) && row[col?.name + 'TextValue']) { - return dashIfEmpty(row[col?.name + 'TextValue']); + if (col?.format || row[col?.name + 'VnTableTextValue']) { + if (selectRegex.test(col?.component) && row[col?.name + 'VnTableTextValue']) { + return dashIfEmpty(row[col?.name + 'VnTableTextValue']); } else { return col.format(row, dashIfEmpty); } @@ -551,23 +552,36 @@ function formatColumnValue(col, row, dashIfEmpty) { return dashIfEmpty(row[urlRelation][col?.attrs.optionLabel ?? 'name']); } if (typeof row[urlRelation] == 'string') return dashIfEmpty(row[urlRelation]); - } else { - return dashIfEmpty(row[col?.name]); } + return dashIfEmpty(row[col?.name]); } + function cardClick(_, row) { if ($props.redirect) router.push({ path: `/${$props.redirect}/${row.id}` }); } + +function removeTextValue(data, getChanges) { + let changes = data.updates; + if (!changes) return data; + + for (const change of changes) { + for (const key in change.data) { + if (key.endsWith('VnTableTextValue')) { + delete change.data[key]; + } + } + } + + data.updates = changes.filter((change) => Object.keys(change.data).length > 0); + + if ($attrs?.beforeSaveFn) data = $attrs.beforeSaveFn(data, getChanges); + + return data; +} </script> <template> - <QDrawer - v-if="$props.rightSearch" - v-model="stateStore.rightDrawer" - side="right" - :width="256" - :overlay="$props.overlay" - > - <QScrollArea class="fit"> + <RightMenu v-if="$props.rightSearch"> + <template #right-panel> <VnTableFilter :data-key="$attrs['data-key']" :columns="columns" @@ -581,8 +595,8 @@ function cardClick(_, row) { <slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" /> </template> </VnTableFilter> - </QScrollArea> - </QDrawer> + </template> + </RightMenu> <CrudModel v-bind="$attrs" :class="$attrs['class'] ?? 'q-px-md'" @@ -591,6 +605,7 @@ function cardClick(_, row) { @on-fetch="(...args) => emit('onFetch', ...args)" :search-url="searchUrl" :disable-infinite-scroll="isTableMode" + :before-save-fn="removeTextValue" @save-changes="reload" :has-sub-toolbar="$props.hasSubToolbar ?? isEditable" :auto-load="hasParams || $attrs['auto-load']" @@ -635,6 +650,7 @@ function cardClick(_, row) { :skip="columnsVisibilitySkipped" /> <QBtnToggle + v-if="!tableModes.some((mode) => mode.disable)" v-model="mode" toggle-color="primary" class="bg-vn-section-color" @@ -648,15 +664,14 @@ function cardClick(_, row) { v-bind:class="col.headerClass" class="body-cell" :style="col?.width ? `max-width: ${col?.width}` : ''" - style="padding: inherit" > <div class="no-padding" - :style=" - withFilters && $props.columnSearch ? 'height: 75px' : '' - " + :style="[ + withFilters && $props.columnSearch ? 'height: 75px' : '', + ]" > - <div class="text-center" style="height: 30px"> + <div style="height: 30px"> <QTooltip v-if="col.toolTip">{{ col.toolTip }}</QTooltip> <VnTableOrder v-model="orders[col.orderBy ?? col.name]" @@ -664,6 +679,7 @@ function cardClick(_, row) { :label="col?.labelAbbreviation ?? col?.label" :data-key="$attrs['data-key']" :search-url="searchUrl" + :align="getColAlign(col)" /> </div> <VnFilter @@ -954,14 +970,6 @@ function cardClick(_, row) { transition-show="scale" transition-hide="scale" :full-width="createComplement?.isFullWidth ?? false" - @before-hide=" - () => { - if (createRef.isSaveAndContinue) { - showForm = true; - createForm.formInitialData = { ...create.formInitialData }; - } - } - " data-cy="vn-table-create-dialog" > <FormModelPopup @@ -1045,8 +1053,8 @@ es: } .body-cell { - padding-left: 2px !important; - padding-right: 2px !important; + padding-left: 4px !important; + padding-right: 4px !important; position: relative; } .bg-chip-secondary { diff --git a/src/components/common/VnComponent.vue b/src/components/common/VnComponent.vue index d9d1ea26b..a9e1c8cff 100644 --- a/src/components/common/VnComponent.vue +++ b/src/components/common/VnComponent.vue @@ -48,7 +48,8 @@ function toValueAttrs(attrs) { <span v-for="toComponent of componentArray" :key="toComponent.name" - class="column flex-center fit" + class="column fit" + :class="toComponent?.component == 'checkbox' ? 'flex-center' : ''" > <component v-if="toComponent?.component" diff --git a/src/components/common/VnInputDate.vue b/src/components/common/VnInputDate.vue index 73c825e1e..1f4705faa 100644 --- a/src/components/common/VnInputDate.vue +++ b/src/components/common/VnInputDate.vue @@ -107,6 +107,7 @@ const manageDate = (date) => { @click="isPopupOpen = !isPopupOpen" @keydown="isPopupOpen = false" hide-bottom-space + :data-cy="$attrs.dataCy ?? $attrs.label + '_inputDate'" > <template #append> <QIcon diff --git a/src/components/ui/CardDescriptor.vue b/src/components/ui/CardDescriptor.vue index 6f122ecd2..a29d1d429 100644 --- a/src/components/ui/CardDescriptor.vue +++ b/src/components/ui/CardDescriptor.vue @@ -76,6 +76,15 @@ onBeforeMount(async () => { ); }); +const routeName = computed(() => { + const DESCRIPTOR_PROXY = 'DescriptorProxy'; + + let name = $props.dataKey; + if ($props.dataKey.includes(DESCRIPTOR_PROXY)) { + name = name.split(DESCRIPTOR_PROXY)[0]; + } + return `${name}Summary`; +}); async function getData() { store.url = $props.url; store.filter = $props.filter ?? {}; @@ -154,9 +163,7 @@ const toModule = computed(() => {{ t('components.smartCard.openSummary') }} </QTooltip> </QBtn> - <RouterLink - :to="{ name: `${dataKey}Summary`, params: { id: entity.id } }" - > + <RouterLink :to="{ name: routeName, params: { id: entity.id } }"> <QBtn class="link" color="white" @@ -196,7 +203,6 @@ const toModule = computed(() => <QItemLabel class="subtitle"> #{{ getValueFromPath(subtitle) ?? entity.id }} </QItemLabel> - <QBtn round flat @@ -210,7 +216,6 @@ const toModule = computed(() => {{ t('globals.copyId') }} </QTooltip> </QBtn> - <!-- </QItemLabel> --> </QItem> </QList> <div class="list-box q-mt-xs"> @@ -220,7 +225,7 @@ const toModule = computed(() => <div class="icons"> <slot name="icons" :entity="entity" /> </div> - <div class="actions justify-center"> + <div class="actions justify-center" data-cy="descriptor_actions"> <slot name="actions" :entity="entity" /> </div> <slot name="after" /> diff --git a/src/composables/getColAlign.js b/src/composables/getColAlign.js index 6e963b437..a930fd7d8 100644 --- a/src/composables/getColAlign.js +++ b/src/composables/getColAlign.js @@ -1,14 +1,14 @@ export function getColAlign(col) { let align; switch (col.component) { + case 'time': + case 'date': case 'select': align = 'left'; break; case 'number': align = 'right'; break; - case 'time': - case 'date': case 'checkbox': align = 'center'; break; diff --git a/src/filters/toDate.js b/src/filters/toDate.js index 8fe8f3836..002797af5 100644 --- a/src/filters/toDate.js +++ b/src/filters/toDate.js @@ -3,6 +3,8 @@ import { useI18n } from 'vue-i18n'; export default function (value, options = {}) { if (!value) return; + if (!isValidDate(value)) return null; + if (!options.dateStyle && !options.timeStyle) { options.day = '2-digit'; options.month = '2-digit'; @@ -10,7 +12,12 @@ export default function (value, options = {}) { } const { locale } = useI18n(); - const date = new Date(value); + const newDate = new Date(value); - return new Intl.DateTimeFormat(locale.value, options).format(date); + return new Intl.DateTimeFormat(locale.value, options).format(newDate); +} +// handle 0000-00-00 +function isValidDate(date) { + const parsedDate = new Date(date); + return parsedDate instanceof Date && !isNaN(parsedDate.getTime()); } diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 9a60e9da1..9e46c54e3 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -153,6 +153,7 @@ globals: maxTemperature: Max minTemperature: Min changePass: Change password + setPass: Set password deleteConfirmTitle: Delete selected elements changeState: Change state raid: 'Raid {daysInForward} days' diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 846c442ea..fd42d37c7 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -157,6 +157,7 @@ globals: maxTemperature: Máx minTemperature: Mín changePass: Cambiar contraseña + setPass: Establecer contraseña deleteConfirmTitle: Eliminar los elementos seleccionados changeState: Cambiar estado raid: 'Redada {daysInForward} días' @@ -786,7 +787,7 @@ worker: notes: Notas operator: numberOfWagons: Número de vagones - train: tren + train: Tren itemPackingType: Tipo de embalaje warehouse: Almacén sector: Sector diff --git a/src/pages/Account/Card/AccountDescriptorMenu.vue b/src/pages/Account/Card/AccountDescriptorMenu.vue index 30584c61f..eafd62df6 100644 --- a/src/pages/Account/Card/AccountDescriptorMenu.vue +++ b/src/pages/Account/Card/AccountDescriptorMenu.vue @@ -25,12 +25,13 @@ const $props = defineProps({ const { t } = useI18n(); const { hasAccount } = toRefs($props); const { openConfirmationModal } = useVnConfirm(); +const arrayData = useArrayData('Account'); const route = useRoute(); const router = useRouter(); const state = useState(); const user = state.getUser(); const { notify } = useQuasar(); -const account = computed(() => useArrayData('Account').store.data[0]); +const account = computed(() => arrayData.store.data); account.value.hasAccount = hasAccount.value; const entityId = computed(() => +route.params.id); const hasitManagementAccess = ref(); @@ -39,7 +40,7 @@ const isHimself = computed(() => user.value.id === account.value.id); const url = computed(() => isHimself.value ? 'Accounts/change-password' - : `Accounts/${entityId.value}/setPassword` + : `Accounts/${entityId.value}/setPassword`, ); async function updateStatusAccount(active) { @@ -153,6 +154,7 @@ onMounted(() => { t('account.card.actions.disableAccount.title'), t('account.card.actions.disableAccount.subtitle'), () => deleteAccount(), + () => deleteAccount(), ) " > @@ -172,6 +174,7 @@ onMounted(() => { t('account.card.actions.enableAccount.title'), t('account.card.actions.enableAccount.subtitle'), () => updateStatusAccount(true), + () => updateStatusAccount(true), ) " > @@ -186,6 +189,7 @@ onMounted(() => { t('account.card.actions.disableAccount.title'), t('account.card.actions.disableAccount.subtitle'), () => updateStatusAccount(false), + () => updateStatusAccount(false), ) " > @@ -201,6 +205,7 @@ onMounted(() => { t('account.card.actions.activateUser.title'), t('account.card.actions.activateUser.title'), () => updateStatusUser(true), + () => updateStatusUser(true), ) " > @@ -215,6 +220,7 @@ onMounted(() => { t('account.card.actions.deactivateUser.title'), t('account.card.actions.deactivateUser.title'), () => updateStatusUser(false), + () => updateStatusUser(false), ) " > diff --git a/src/pages/Claim/Card/ClaimAction.vue b/src/pages/Claim/Card/ClaimAction.vue index 8ac7c224f..baa36710c 100644 --- a/src/pages/Claim/Card/ClaimAction.vue +++ b/src/pages/Claim/Card/ClaimAction.vue @@ -27,6 +27,7 @@ const claimActionsForm = ref(); const rows = ref([]); const selectedRows = ref([]); const destinationTypes = ref([]); +const shelvings = ref([]); const totalClaimed = ref(null); const DEFAULT_MAX_RESPONSABILITY = 5; const DEFAULT_MIN_RESPONSABILITY = 1; @@ -56,6 +57,12 @@ const columns = computed(() => [ field: (row) => row.claimDestinationFk, align: 'left', }, + { + name: 'shelving', + label: t('shelvings.shelving'), + field: (row) => row.shelvingFk, + align: 'left', + }, { name: 'Landed', label: t('Landed'), @@ -125,6 +132,10 @@ async function updateDestination(claimDestinationFk, row, options = {}) { options.reload && claimActionsForm.value.reload(); } } +async function updateShelving(shelvingFk, row) { + await axios.patch(`ClaimEnds/${row.id}`, { shelvingFk }); + claimActionsForm.value.reload(); +} async function regularizeClaim() { await post(`Claims/${claimId}/regularizeClaim`); @@ -200,6 +211,7 @@ async function post(query, params) { auto-load @on-fetch="(data) => (destinationTypes = data)" /> + <FetchData url="Shelvings" auto-load @on-fetch="(data) => (shelvings = data)" /> <RightMenu v-if="claim"> <template #right-panel> <QCard class="totalClaim q-my-md q-pa-sm no-box-shadow"> @@ -312,6 +324,20 @@ async function post(query, params) { /> </QTd> </template> + <template #body-cell-shelving="{ row }"> + <QTd> + <VnSelect + v-model="row.shelvingFk" + :options="shelvings" + option-label="code" + option-value="id" + style="width: 100px" + hide-selected + @update:model-value="(value) => updateShelving(value, row)" + /> + </QTd> + </template> + <template #body-cell-price="{ value }"> <QTd align="center"> {{ toCurrency(value) }} @@ -354,7 +380,7 @@ async function post(query, params) { (value) => updateDestination( value, - props.row + props.row, ) " /> @@ -371,6 +397,17 @@ async function post(query, params) { </QTable> </template> <template #moreBeforeActions> + <QBtn + color="primary" + text-color="white" + :unelevated="true" + :label="tMobile('Import claim')" + :title="t('Import claim')" + icon="Download" + @click="importToNewRefundTicket" + :disable="claim.claimStateFk == resolvedStateId" + :loading="loading" + /> <QBtn color="primary" text-color="white" @@ -394,17 +431,6 @@ async function post(query, params) { @click="dialogDestination = !dialogDestination" :loading="loading" /> - <QBtn - color="primary" - text-color="white" - :unelevated="true" - :label="tMobile('Import claim')" - :title="t('Import claim')" - icon="Upload" - @click="importToNewRefundTicket" - :disable="claim.claimStateFk == resolvedStateId" - :loading="loading" - /> </template> </CrudModel> <QDialog v-model="dialogDestination"> diff --git a/src/pages/Claim/Card/ClaimBasicData.vue b/src/pages/Claim/Card/ClaimBasicData.vue index 67034da1a..43941d1dc 100644 --- a/src/pages/Claim/Card/ClaimBasicData.vue +++ b/src/pages/Claim/Card/ClaimBasicData.vue @@ -40,7 +40,7 @@ const workersOptions = ref([]); </VnRow> <VnRow> <VnSelect - :label="t('claim.assignedTo')" + :label="t('claim.attendedBy')" v-model="data.workerFk" :options="workersOptions" option-value="id" diff --git a/src/pages/Claim/Card/ClaimSummary.vue b/src/pages/Claim/Card/ClaimSummary.vue index 66fb151e5..210b0c982 100644 --- a/src/pages/Claim/Card/ClaimSummary.vue +++ b/src/pages/Claim/Card/ClaimSummary.vue @@ -233,20 +233,27 @@ function claimUrl(section) { <ClaimDescriptorMenu :claim="entity.claim" /> </template> <template #body="{ entity: { claim, salesClaimed, developments } }"> - <QCard class="vn-one" v-if="$route.name != 'ClaimSummary'"> + <QCard class="vn-one"> <VnTitle :url="claimUrl('basic-data')" :text="t('globals.pageTitles.basicData')" /> - <VnLv :label="t('claim.created')" :value="toDate(claim.created)" /> - <VnLv :label="t('claim.state')"> + <VnLv + v-if="$route.name != 'ClaimSummary'" + :label="t('claim.created')" + :value="toDate(claim.created)" + /> + <VnLv v-if="$route.name != 'ClaimSummary'" :label="t('claim.state')"> <template #value> <QChip :color="stateColor(claim.claimState.code)" dense> {{ claim.claimState.description }} </QChip> </template> </VnLv> - <VnLv :label="t('globals.salesPerson')"> + <VnLv + v-if="$route.name != 'ClaimSummary'" + :label="t('globals.salesPerson')" + > <template #value> <VnUserLink :name="claim.client?.salesPersonUser?.name" @@ -254,7 +261,7 @@ function claimUrl(section) { /> </template> </VnLv> - <VnLv :label="t('claim.attendedBy')"> + <VnLv v-if="$route.name != 'ClaimSummary'" :label="t('claim.attendedBy')"> <template #value> <VnUserLink :name="claim.worker?.user?.nickname" @@ -262,7 +269,7 @@ function claimUrl(section) { /> </template> </VnLv> - <VnLv :label="t('claim.customer')"> + <VnLv v-if="$route.name != 'ClaimSummary'" :label="t('claim.customer')"> <template #value> <span class="link cursor-pointer"> {{ claim.client?.name }} @@ -274,6 +281,11 @@ function claimUrl(section) { :label="t('claim.pickup')" :value="`${dashIfEmpty(claim.pickup)}`" /> + <VnLv + :label="t('globals.packages')" + :value="`${dashIfEmpty(claim.packages)}`" + :translation="(value) => t(`claim.packages`)" + /> </QCard> <QCard class="vn-two"> <VnTitle :url="claimUrl('notes')" :text="t('claim.notes')" /> diff --git a/src/pages/Claim/Card/ClaimSummaryAction.vue b/src/pages/Claim/Card/ClaimSummaryAction.vue index d875126cb..e5273902c 100644 --- a/src/pages/Claim/Card/ClaimSummaryAction.vue +++ b/src/pages/Claim/Card/ClaimSummaryAction.vue @@ -19,30 +19,36 @@ const columns = [ name: 'itemFk', label: t('Id item'), columnFilter: false, - align: 'left', + align: 'right', }, { name: 'ticketFk', label: t('Ticket'), columnFilter: false, - align: 'left', + align: 'right', }, { name: 'claimDestinationFk', label: t('Destination'), columnFilter: false, - align: 'left', + align: 'right', + }, + { + name: 'shelvingCode', + label: t('Shelving'), + columnFilter: false, + align: 'right', }, { name: 'landed', label: t('Landed'), format: (row) => toDate(row.landed), - align: 'left', + align: 'center', }, { name: 'quantity', label: t('Quantity'), - align: 'left', + align: 'right', }, { name: 'concept', @@ -52,18 +58,18 @@ const columns = [ { name: 'price', label: t('Price'), - align: 'left', + align: 'right', }, { name: 'discount', label: t('Discount'), format: ({ discount }) => toPercentage(discount / 100), - align: 'left', + align: 'right', }, { name: 'total', label: t('Total'), - align: 'left', + align: 'right', }, ]; </script> diff --git a/src/pages/Claim/ClaimFilter.vue b/src/pages/Claim/ClaimFilter.vue index 6c941f59e..0fe7fc588 100644 --- a/src/pages/Claim/ClaimFilter.vue +++ b/src/pages/Claim/ClaimFilter.vue @@ -106,7 +106,6 @@ const props = defineProps({ :label="t('claim.zone')" v-model="params.zoneFk" url="Zones" - :use-like="false" outlined rounded dense diff --git a/src/pages/Claim/locale/en.yml b/src/pages/Claim/locale/en.yml index 11b4a2ca4..cdfa3963b 100644 --- a/src/pages/Claim/locale/en.yml +++ b/src/pages/Claim/locale/en.yml @@ -13,7 +13,6 @@ claim: province: Province zone: Zone customerId: client ID - assignedTo: Assigned created: Created details: Details item: Item diff --git a/src/pages/Claim/locale/es.yml b/src/pages/Claim/locale/es.yml index d35d2c8e7..00f880f0c 100644 --- a/src/pages/Claim/locale/es.yml +++ b/src/pages/Claim/locale/es.yml @@ -13,7 +13,6 @@ claim: province: Provincia zone: Zona customerId: ID de cliente - assignedTo: Asignado a created: Creado details: Detalles item: Artículo diff --git a/src/pages/Customer/CustomerFilter.vue b/src/pages/Customer/CustomerFilter.vue index 21de8fa9b..1c5a08304 100644 --- a/src/pages/Customer/CustomerFilter.vue +++ b/src/pages/Customer/CustomerFilter.vue @@ -143,6 +143,7 @@ const exprBuilder = (param, value) => { outlined rounded auto-load + sortBy="name ASC" /></QItemSection> </QItem> <QItem class="q-mb-sm"> diff --git a/src/pages/Customer/CustomerList.vue b/src/pages/Customer/CustomerList.vue index b8c1a743e..0bfca7910 100644 --- a/src/pages/Customer/CustomerList.vue +++ b/src/pages/Customer/CustomerList.vue @@ -78,10 +78,20 @@ const columns = computed(() => [ component: 'select', attrs: { url: 'Workers/activeWithInheritedRole', - fields: ['id', 'name'], + fields: ['id', 'name', 'firstName'], where: { role: 'salesPerson' }, optionFilter: 'firstName', }, + columnFilter: { + component: 'select', + attrs: { + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name', 'firstName'], + where: { role: 'salesPerson' }, + optionLabel: 'firstName', + optionValue: 'id', + }, + }, create: false, columnField: { component: null, diff --git a/src/pages/Customer/components/CustomerNewPayment.vue b/src/pages/Customer/components/CustomerNewPayment.vue index 8f61bac89..6ecccc544 100644 --- a/src/pages/Customer/components/CustomerNewPayment.vue +++ b/src/pages/Customer/components/CustomerNewPayment.vue @@ -77,7 +77,6 @@ onBeforeMount(() => { function setPaymentType(accounting) { if (!accounting) return; accountingType.value = accounting.accountingType; - initialData.description = []; initialData.payed = Date.vnNew(); isCash.value = accountingType.value.code == 'cash'; @@ -87,14 +86,14 @@ function setPaymentType(accounting) { initialData.payed.getDate() + accountingType.value.daysInFuture, ); maxAmount.value = accountingType.value && accountingType.value.maxAmount; - if (accountingType.value.code == 'compensation') return (initialData.description = ''); - if (accountingType.value.receiptDescription) - initialData.description.push(accountingType.value.receiptDescription); - if (initialData.description) initialData.description.push(initialData.description); - initialData.description = initialData.description.join(', '); + let descriptions = []; + if (accountingType.value.receiptDescription) + descriptions.push(accountingType.value.receiptDescription); + if (initialData.description) descriptions.push(initialData.description); + initialData.description = descriptions.join(', '); } const calculateFromAmount = (event) => { diff --git a/src/pages/Customer/components/CustomerSamplesCreate.vue b/src/pages/Customer/components/CustomerSamplesCreate.vue index 8d241441d..1294a5d25 100644 --- a/src/pages/Customer/components/CustomerSamplesCreate.vue +++ b/src/pages/Customer/components/CustomerSamplesCreate.vue @@ -18,6 +18,7 @@ import VnInput from 'src/components/common/VnInput.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import CustomerSamplesPreview from 'src/pages/Customer/components/CustomerSamplesPreview.vue'; import FormPopup from 'src/components/FormPopup.vue'; +import { useArrayData } from 'src/composables/useArrayData'; const { dialogRef, onDialogOK } = useDialogPluginComponent(); @@ -39,7 +40,7 @@ const optionsSamplesVisible = ref([]); const sampleType = ref({ hasPreview: false }); const initialData = reactive({}); const entityId = computed(() => route.params.id); -const customer = computed(() => state.get('Customer')); +const customer = computed(() => useArrayData('Customer').store?.data); const filterEmailUsers = { where: { userFk: user.value.id } }; const filterClientsAddresses = { include: [ @@ -65,9 +66,9 @@ const filterSamplesVisible = { defineEmits(['confirm', ...useDialogPluginComponent.emits]); onBeforeMount(async () => { - initialData.clientFk = customer.value.id; - initialData.recipient = customer.value.email; - initialData.recipientId = customer.value.id; + initialData.clientFk = customer.value?.id; + initialData.recipient = customer.value?.email; + initialData.recipientId = customer.value?.id; }); const setEmailUser = (data) => { diff --git a/src/pages/Entry/Card/EntryBuys.vue b/src/pages/Entry/Card/EntryBuys.vue index f3b73cb04..67333b5bd 100644 --- a/src/pages/Entry/Card/EntryBuys.vue +++ b/src/pages/Entry/Card/EntryBuys.vue @@ -16,7 +16,6 @@ import ItemDescriptor from 'src/pages/Item/Card/ItemDescriptor.vue'; import axios from 'axios'; import VnSelectEnum from 'src/components/common/VnSelectEnum.vue'; import { checkEntryLock } from 'src/composables/checkEntryLock'; -import SkeletonDescriptor from 'src/components/ui/SkeletonDescriptor.vue'; const $props = defineProps({ id: { @@ -103,7 +102,7 @@ const columns = [ name: 'itemFk', component: 'number', isEditable: false, - width: '40px', + width: '35px', }, { labelAbbreviation: '', @@ -111,7 +110,7 @@ const columns = [ name: 'hex', columnSearch: false, isEditable: false, - width: '5px', + width: '9px', component: 'select', attrs: { url: 'Inks', @@ -181,6 +180,7 @@ const columns = [ url: 'packagings', fields: ['id'], optionLabel: 'id', + optionValue: 'id', }, create: true, width: '40px', @@ -192,7 +192,7 @@ const columns = [ component: 'number', create: true, width: '35px', - format: (row, dashIfEmpty) => parseFloat(row['weight']).toFixed(1), + format: (row) => parseFloat(row['weight']).toFixed(1), }, { labelAbbreviation: 'P', @@ -330,6 +330,25 @@ const columns = [ create: true, format: (row) => parseFloat(row['price3']).toFixed(2), }, + { + align: 'center', + labelAbbreviation: 'CM', + label: t('Check min price'), + toolTip: t('Check min price'), + name: 'hasMinPrice', + attrs: { + toggleIndeterminate: false, + }, + component: 'checkbox', + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + await axios.patch(`Items/${row['itemFk']}`, { + hasMinPrice: value, + }); + }, + }, + width: '25px', + }, { align: 'center', labelAbbreviation: 'Min.', @@ -350,25 +369,6 @@ const columns = [ }, format: (row) => parseFloat(row['minPrice']).toFixed(2), }, - { - align: 'center', - labelAbbreviation: 'CM', - label: t('Check min price'), - toolTip: t('Check min price'), - name: 'hasMinPrice', - attrs: { - toggleIndeterminate: false, - }, - component: 'checkbox', - cellEvent: { - 'update:modelValue': async (value, oldValue, row) => { - await axios.patch(`Items/${row['itemFk']}`, { - hasMinPrice: value, - }); - }, - }, - width: '25px', - }, { align: 'center', labelAbbreviation: t('P.Sen'), @@ -378,6 +378,9 @@ const columns = [ component: 'number', isEditable: false, width: '40px', + style: () => { + return { color: 'var(--vn-label-color)' }; + }, }, { align: 'center', @@ -417,6 +420,9 @@ const columns = [ component: 'input', isEditable: false, width: '35px', + style: () => { + return { color: 'var(--vn-label-color)' }; + }, }, ]; @@ -633,7 +639,7 @@ onMounted(() => { 'flex-wrap': 'wrap', gap: '16px', position: 'relative', - height: '450px', + height: '500px', }, columnGridStyle: { 'max-width': '50%', @@ -644,8 +650,8 @@ onMounted(() => { :is-editable="editableMode" :without-header="!editableMode" :with-filters="editableMode" - :right-search="false" - :right-search-icon="false" + :right-search="editableMode" + :right-search-icon="true" :row-click="false" :columns="columns" :beforeSaveFn="beforeSave" diff --git a/src/pages/Entry/EntryList.vue b/src/pages/Entry/EntryList.vue index d50f6b219..a9cf2a5e2 100644 --- a/src/pages/Entry/EntryList.vue +++ b/src/pages/Entry/EntryList.vue @@ -199,7 +199,6 @@ const columns = computed(() => [ optionValue: 'code', optionLabel: 'description', }, - cardVisible: true, width: '65px', format: (row, dashIfEmpty) => dashIfEmpty(row.entryTypeDescription), }, @@ -275,7 +274,7 @@ onBeforeMount(async () => { :array-data-props="{ url: 'Entries/filter', order: 'landed DESC', - userFilter: EntryFilter, + userFilter: entryQueryFilter, }" > <template #advanced-menu> diff --git a/src/pages/Entry/EntryStockBought.vue b/src/pages/Entry/EntryStockBought.vue index da8557828..4bd0fe640 100644 --- a/src/pages/Entry/EntryStockBought.vue +++ b/src/pages/Entry/EntryStockBought.vue @@ -57,7 +57,7 @@ const columns = computed(() => [ create: true, component: 'number', summation: true, - width: '60px', + width: '50px', }, { align: 'center', @@ -286,7 +286,7 @@ function round(value) { justify-content: center; } .column { - min-width: 30%; + min-width: 40%; margin-top: 5%; display: flex; flex-direction: column; diff --git a/src/pages/Entry/EntryStockBoughtDetail.vue b/src/pages/Entry/EntryStockBoughtDetail.vue index 9d382f23a..1a37994d9 100644 --- a/src/pages/Entry/EntryStockBoughtDetail.vue +++ b/src/pages/Entry/EntryStockBoughtDetail.vue @@ -101,7 +101,8 @@ const columns = [ </template> <style lang="css" scoped> .container { - max-width: 50vw; + max-width: 100%; + width: 50%; overflow: auto; justify-content: center; align-items: center; @@ -109,9 +110,6 @@ const columns = [ background-color: var(--vn-section-color); padding: 2%; } -.container > div > div > .q-table__top.relative-position.row.items-center { - background-color: red !important; -} </style> <i18n> es: diff --git a/src/pages/InvoiceOut/Card/InvoiceOutDescriptorMenu.vue b/src/pages/InvoiceOut/Card/InvoiceOutDescriptorMenu.vue index 1fd9f3e92..8be928134 100644 --- a/src/pages/InvoiceOut/Card/InvoiceOutDescriptorMenu.vue +++ b/src/pages/InvoiceOut/Card/InvoiceOutDescriptorMenu.vue @@ -163,10 +163,14 @@ const showExportationLetter = () => { <QMenu anchor="top end" self="top start"> <QList> <QItem v-ripple clickable @click="showSendInvoiceDialog('pdf')"> - <QItemSection>{{ t('Send PDF') }}</QItemSection> + <QItemSection data-cy="InvoiceOutDescriptorMenuSendPdfOption"> + {{ t('Send PDF') }} + </QItemSection> </QItem> <QItem v-ripple clickable @click="showSendInvoiceDialog('csv')"> - <QItemSection>{{ t('Send CSV') }}</QItemSection> + <QItemSection data-cy="InvoiceOutDescriptorMenuSendCsvOption"> + {{ t('Send CSV') }} + </QItemSection> </QItem> </QList> </QMenu> diff --git a/src/pages/InvoiceOut/InvoiceOutList.vue b/src/pages/InvoiceOut/InvoiceOutList.vue index 873ab030f..a6ec9923e 100644 --- a/src/pages/InvoiceOut/InvoiceOutList.vue +++ b/src/pages/InvoiceOut/InvoiceOutList.vue @@ -21,7 +21,6 @@ import VnSection from 'src/components/common/VnSection.vue'; const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const tableRef = ref(); -const invoiceOutSerialsOptions = ref([]); const customerOptions = ref([]); const selectedRows = ref([]); const hasSelectedCards = computed(() => selectedRows.value.length > 0); @@ -368,7 +367,6 @@ watchEffect(selectedRows); url="InvoiceOutSerials" v-model="data.serial" :label="t('invoiceOutModule.serial')" - :options="invoiceOutSerialsOptions" option-label="description" option-value="code" option-filter diff --git a/src/pages/InvoiceOut/locale/en.yml b/src/pages/InvoiceOut/locale/en.yml index 9dd31d186..f1baef432 100644 --- a/src/pages/InvoiceOut/locale/en.yml +++ b/src/pages/InvoiceOut/locale/en.yml @@ -24,6 +24,7 @@ invoiceOut: min: Min max: Max hasPdf: Has PDF + search: Contains card: issued: Issued customerCard: Customer card diff --git a/src/pages/InvoiceOut/locale/es.yml b/src/pages/InvoiceOut/locale/es.yml index 79ceb4aa8..afca27871 100644 --- a/src/pages/InvoiceOut/locale/es.yml +++ b/src/pages/InvoiceOut/locale/es.yml @@ -24,6 +24,7 @@ invoiceOut: min: Min max: Max hasPdf: Tiene PDF + search: Contiene card: issued: Fecha emisión customerCard: Ficha del cliente diff --git a/src/pages/Item/Card/ItemDiary.vue b/src/pages/Item/Card/ItemDiary.vue index 4b6775183..31b3c328e 100644 --- a/src/pages/Item/Card/ItemDiary.vue +++ b/src/pages/Item/Card/ItemDiary.vue @@ -27,7 +27,7 @@ const user = state.getUser(); const today = Date.vnNew(); today.setHours(0, 0, 0, 0); const warehousesOptions = ref([]); -const itemBalances = computed(() => arrayDataItemBalances.store.data); +const itemBalances = computed(() => arrayDataItemBalances.store.data || []); const where = computed(() => arrayDataItemBalances.store.filter.where || {}); const showWhatsBeforeInventory = ref(false); const inventoriedDate = ref(null); @@ -313,8 +313,8 @@ async function updateWarehouse(warehouseFk) { row.lineFk == row.lastPreparedLineFk ? 'black' : row.balance < 0 - ? 'negative' - : '' + ? 'negative' + : '' " dense style="font-size: 14px" diff --git a/src/pages/Item/Card/ItemTags.vue b/src/pages/Item/Card/ItemTags.vue index ed23ab5a6..ab26b9cae 100644 --- a/src/pages/Item/Card/ItemTags.vue +++ b/src/pages/Item/Card/ItemTags.vue @@ -87,7 +87,7 @@ const insertTag = (rows) => { tagFk: undefined, }" :default-remove="false" - :filter="{ + :user-filter="{ fields: ['id', 'itemFk', 'tagFk', 'value', 'priority'], where: { itemFk: route.params.id }, include: { @@ -119,6 +119,7 @@ const insertTag = (rows) => { " :required="true" :rules="validate('itemTag.tagFk')" + :data-cy="`tag${row?.tag?.name}`" /> <VnSelect v-if="row.tag?.isFree === false" @@ -145,6 +146,7 @@ const insertTag = (rows) => { :label="t('itemTags.value')" :is-clearable="false" @keyup.enter.stop="(data) => itemTagsRef.onSubmit(data)" + :data-cy="`tag${row?.tag?.name}Value`" /> <VnInput :label="t('itemBasicData.relevancy')" @@ -162,6 +164,7 @@ const insertTag = (rows) => { name="delete" size="sm" dense + :data-cy="`deleteTag${row?.tag?.name}`" > <QTooltip> {{ t('itemTags.removeTag') }} @@ -177,6 +180,7 @@ const insertTag = (rows) => { icon="add" v-shortcut="'+'" fab + data-cy="createNewTag" > <QTooltip> {{ t('itemTags.addTag') }} diff --git a/src/pages/Item/ItemRequestFilter.vue b/src/pages/Item/ItemRequestFilter.vue index af48f7f5c..c2a63ddd9 100644 --- a/src/pages/Item/ItemRequestFilter.vue +++ b/src/pages/Item/ItemRequestFilter.vue @@ -8,6 +8,7 @@ import VnInput from 'src/components/common/VnInput.vue'; import FetchData from 'components/FetchData.vue'; import { useArrayData } from 'src/composables/useArrayData'; import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; +import VnInputDate from 'src/components/common/VnInputDate.vue'; const { t } = useI18n(); const props = defineProps({ @@ -52,7 +53,7 @@ onMounted(async () => { name: key, value, selectedField: { name: key, label: t(`params.${key}`) }, - }) + }), ); } exprBuilder('state', arrayData.store?.userParams?.state); @@ -157,6 +158,32 @@ onMounted(async () => { /> </QItemSection> </QItem> + <QItem> + <QItemSection> + <VnInputDate + v-model="params.from" + :label="t('params.from')" + is-outlined + /> + </QItemSection> + <QItemSection> + <VnInputDate + v-model="params.to" + :label="t('params.to')" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + :label="t('params.daysOnward')" + v-model="params.daysOnward" + lazy-rules + is-outlined + /> + </QItemSection> + </QItem> <QItem> <QItemSection> <VnSelect @@ -175,11 +202,10 @@ onMounted(async () => { </QItem> <QItem> <QItemSection> - <VnInput - :label="t('params.daysOnward')" - v-model="params.daysOnward" - lazy-rules - is-outlined + <QCheckbox + :label="t('params.mine')" + v-model="params.mine" + :toggle-indeterminate="false" /> </QItemSection> </QItem> diff --git a/src/pages/Route/RouteExtendedList.vue b/src/pages/Route/RouteExtendedList.vue index 46bc1a690..a7e192765 100644 --- a/src/pages/Route/RouteExtendedList.vue +++ b/src/pages/Route/RouteExtendedList.vue @@ -3,7 +3,7 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useQuasar } from 'quasar'; -import { dashIfEmpty, toDate, toHour } from 'src/filters'; +import { toDate, toHour } from 'src/filters'; import { useRouter } from 'vue-router'; import { usePrintService } from 'src/composables/usePrintService'; @@ -280,7 +280,7 @@ const openTicketsDialog = (id) => { </QCardSection> <QCardSection class="q-pt-none"> <VnInputDate - :label="t('route.Stating date')" + :label="t('route.Starting date')" v-model="startingDate" autofocus /> diff --git a/src/pages/Shelving/Parking/Card/ParkingSummary.vue b/src/pages/Shelving/Parking/Card/ParkingSummary.vue index 95620ebfd..7188ebeb6 100644 --- a/src/pages/Shelving/Parking/Card/ParkingSummary.vue +++ b/src/pages/Shelving/Parking/Card/ParkingSummary.vue @@ -45,8 +45,6 @@ const filter = { :label="t('parking.sector')" :value="entity.sector?.description" /> - <VnLv :label="t('parking.row')" :value="entity.row" /> - <VnLv :label="t('parking.column')" :value="entity.column" /> </QCard> </template> </CardSummary> diff --git a/src/pages/Shelving/Parking/ParkingList.vue b/src/pages/Shelving/Parking/ParkingList.vue index fe6c93ba5..7c5058a74 100644 --- a/src/pages/Shelving/Parking/ParkingList.vue +++ b/src/pages/Shelving/Parking/ParkingList.vue @@ -1,19 +1,15 @@ <script setup> -import { onMounted, onUnmounted } from 'vue'; -import { useRouter } from 'vue-router'; +import { computed, onMounted, onUnmounted } from 'vue'; import { useI18n } from 'vue-i18n'; import { useStateStore } from 'stores/useStateStore'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; -import VnPaginate from 'components/ui/VnPaginate.vue'; -import CardList from 'components/ui/CardList.vue'; -import VnLv from 'components/ui/VnLv.vue'; -import ParkingFilter from './ParkingFilter.vue'; -import ParkingSummary from './Card/ParkingSummary.vue'; -import exprBuilder from './ParkingExprBuilder.js'; +import VnTable from 'components/VnTable/VnTable.vue'; import VnSection from 'src/components/common/VnSection.vue'; +import ParkingFilter from './ParkingFilter.vue'; +import exprBuilder from './ParkingExprBuilder.js'; +import ParkingSummary from './Card/ParkingSummary.vue'; const stateStore = useStateStore(); -const { push } = useRouter(); const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const dataKey = 'ParkingList'; @@ -24,7 +20,48 @@ onUnmounted(() => (stateStore.rightDrawer = false)); const filter = { fields: ['id', 'sectorFk', 'code', 'pickingOrder'], }; + +const columns = computed(() => [ + { + align: 'left', + name: 'code', + label: t('globals.code'), + isId: true, + isTitle: true, + columnFilter: false, + sortable: true, + }, + { + align: 'left', + name: 'sector', + label: t('parking.sector'), + format: (val) => val.sector.description ?? '', + sortable: true, + cardVisible: true, + }, + { + align: 'left', + name: 'pickingOrder', + label: t('parking.pickingOrder'), + sortable: true, + cardVisible: true, + }, + { + align: 'right', + label: '', + name: 'tableActions', + actions: [ + { + title: t('components.smartCard.viewSummary'), + icon: 'preview', + action: (row) => viewSummary(row.id, ParkingSummary), + isPrimary: true, + }, + ], + }, +]); </script> + <template> <VnSection :data-key="dataKey" @@ -40,41 +77,24 @@ const filter = { <ParkingFilter data-key="ParkingList" /> </template> <template #body> - <QPage class="column items-center q-pa-md"> - <div class="vn-card-list"> - <VnPaginate :data-key="dataKey"> - <template #body="{ rows }"> - <CardList - v-for="row of rows" - :key="row.id" - :id="row.id" - :title="row.code" - @click=" - push({ path: `/shelving/parking/${row.id}/summary` }) - " - > - <template #list-items> - <VnLv - label="Sector" - :value="row.sector?.description" - /> - <VnLv - :label="t('parking.pickingOrder')" - :value="row.pickingOrder" - /> - </template> - <template #actions> - <QBtn - :label="t('components.smartCard.openSummary')" - @click.stop="viewSummary(row.id, ParkingSummary)" - color="primary" - /> - </template> - </CardList> - </template> - </VnPaginate> - </div> - </QPage> + <VnTable + :data-key="dataKey" + :columns="columns" + is-editable="false" + :right-search="false" + :use-model="true" + :disable-option="{ table: true }" + redirect="shelving/parking" + default-mode="card" + > + <template #actions="{ row }"> + <QBtn + :label="t('components.smartCard.openSummary')" + @click.stop="viewSummary(row.id, ParkingSummary)" + color="primary" + /> + </template> + </VnTable> </template> </VnSection> </template> diff --git a/src/pages/Ticket/Card/TicketEditMana.vue b/src/pages/Ticket/Card/TicketEditMana.vue index 14eec9db9..ff40a6592 100644 --- a/src/pages/Ticket/Card/TicketEditMana.vue +++ b/src/pages/Ticket/Card/TicketEditMana.vue @@ -1,32 +1,26 @@ <script setup> -import { ref } from 'vue'; +import axios from 'axios'; import { useI18n } from 'vue-i18n'; +import { computed, ref } from 'vue'; +import { useRoute } from 'vue-router'; import { toCurrency } from 'src/filters'; import VnUsesMana from 'components/ui/VnUsesMana.vue'; const $props = defineProps({ - mana: { - type: Number, - default: null, - }, newPrice: { type: Number, default: 0, }, - usesMana: { - type: Boolean, - default: false, - }, - manaCode: { - type: String, - default: 'mana', - }, sale: { type: Object, default: null, }, }); +const route = useRoute(); +const mana = ref(null); +const usesMana = ref(false); + const emit = defineEmits(['save', 'cancel']); const { t } = useI18n(); @@ -38,32 +32,47 @@ const save = (sale = $props.sale) => { QPopupProxyRef.value.hide(); }; +const getMana = async () => { + const { data } = await axios.get(`Tickets/${route.params.id}/getSalesPersonMana`); + mana.value = data; + await getUsesMana(); +}; + +const getUsesMana = async () => { + const { data } = await axios.get('Sales/usesMana'); + usesMana.value = data; +}; + const cancel = () => { emit('cancel'); QPopupProxyRef.value.hide(); }; +const hasMana = computed(() => typeof mana.value === 'number'); defineExpose({ save }); </script> <template> - <QPopupProxy ref="QPopupProxyRef" data-cy="ticketEditManaProxy"> + <QPopupProxy + ref="QPopupProxyRef" + @before-show="getMana" + data-cy="ticketEditManaProxy" + > <div class="container"> - <QSpinner v-if="!mana" color="primary" size="md" /> - <div v-else> - <div class="header">Mana: {{ toCurrency(mana) }}</div> - <div class="q-pa-md"> - <slot :popup="QPopupProxyRef" /> - <div v-if="usesMana" class="column q-gutter-y-sm q-mt-sm"> - <VnUsesMana :mana-code="manaCode" /> - </div> - <div v-if="newPrice" class="column items-center q-mt-lg"> - <span class="text-primary">{{ t('New price') }}</span> - <span class="text-subtitle1"> - {{ toCurrency($props.newPrice) }} - </span> - </div> + <div class="header">Mana: {{ toCurrency(mana) }}</div> + <QSpinner v-if="!hasMana" color="primary" size="md" /> + <div class="q-pa-md" v-else> + <slot :popup="QPopupProxyRef" /> + <div v-if="usesMana" class="column q-gutter-y-sm q-mt-sm"> + <VnUsesMana :mana-code="manaCode" /> + </div> + <div v-if="newPrice" class="column items-center q-mt-lg"> + <span class="text-primary">{{ t('New price') }}</span> + <span class="text-subtitle1"> + {{ toCurrency($props.newPrice) }} + </span> </div> </div> + <div class="row"> <QBtn color="primary" diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index e680fb290..e88133ff1 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -45,7 +45,6 @@ const isTicketEditable = ref(false); const sales = ref([]); const editableStatesOptions = ref([]); const selectedSales = ref([]); -const mana = ref(null); const manaCode = ref('mana'); const ticketState = computed(() => store.data?.ticketState?.state?.code); const transfer = ref({ @@ -175,17 +174,21 @@ const getSaleTotal = (sale) => { return price - discount; }; +const getRowUpdateInputEvents = (sale) => ({ + 'keyup.enter': () => { + changeQuantity(sale); + }, + blur: () => { + changeQuantity(sale); + }, +}); + const resetChanges = async () => { arrayData.fetch({ append: false }); tableRef.value.reload(); }; -const rowToUpdate = ref(null); const changeQuantity = async (sale) => { - if ( - !sale.itemFk || - sale.quantity == null || - edit.value?.oldQuantity === sale.quantity - ) + if (!sale.itemFk || sale.quantity == null || sale?.originalQuantity === sale.quantity) return; if (!sale.id) return addSale(sale); @@ -197,11 +200,8 @@ const changeQuantity = async (sale) => { const updateQuantity = async (sale) => { try { let { quantity, id } = sale; - if (!rowToUpdate.value) return; - rowToUpdate.value = null; sale.isNew = false; - const params = { quantity: quantity }; - await axios.post(`Sales/${id}/updateQuantity`, params); + await axios.post(`Sales/${id}/updateQuantity`, { quantity }); notify('globals.dataSaved', 'positive'); tableRef.value.reload(); } catch (e) { @@ -258,18 +258,6 @@ const DEFAULT_EDIT = { oldQuantity: null, }; const edit = ref({ ...DEFAULT_EDIT }); -const usesMana = ref(null); - -const getUsesMana = async () => { - const { data } = await axios.get('Sales/usesMana'); - usesMana.value = data; -}; - -const getMana = async () => { - const { data } = await axios.get(`Tickets/${route.params.id}/getSalesPersonMana`); - mana.value = data; - await getUsesMana(); -}; const selectedValidSales = computed(() => { if (!sales.value) return; @@ -277,7 +265,6 @@ const selectedValidSales = computed(() => { }); const onOpenEditPricePopover = async (sale) => { - await getMana(); edit.value = { sale: JSON.parse(JSON.stringify(sale)), price: sale.price, @@ -285,7 +272,6 @@ const onOpenEditPricePopover = async (sale) => { }; const onOpenEditDiscountPopover = async (sale) => { - await getMana(); if (isLocked.value) return; if (sale) { edit.value = { @@ -306,7 +292,6 @@ const changePrice = async (sale) => { await confirmUpdate(() => updatePrice(sale, newPrice)); } else updatePrice(sale, newPrice); } - await getMana(); }; const updatePrice = async (sale, newPrice) => { await axios.post(`Sales/${sale.id}/updatePrice`, { newPrice }); @@ -599,9 +584,7 @@ watch( :is-ticket-editable="isTicketEditable" :sales="selectedValidSales" :disable="!hasSelectedRows" - :mana="mana" :ticket-config="ticketConfig" - @get-mana="getMana()" @update-discounts="updateDiscounts" @refresh-table="resetChanges" /> @@ -772,9 +755,7 @@ watch( v-if="row.isNew || isTicketEditable" type="number" v-model.number="row.quantity" - @blur="changeQuantity(row)" - @keyup.enter.stop="changeQuantity(row)" - @update:model-value="() => (rowToUpdate = row)" + v-on="getRowUpdateInputEvents(row)" @focus="edit.oldQuantity = row.quantity" /> <span v-else>{{ row.quantity }}</span> @@ -786,7 +767,6 @@ watch( </QBtn> <TicketEditManaProxy ref="editPriceProxyRef" - :mana="mana" :sale="row" :new-price="getNewPrice" @save="changePrice" @@ -809,10 +789,8 @@ watch( <TicketEditManaProxy ref="editManaProxyRef" - :mana="mana" :sale="row" :new-price="getNewPrice" - :uses-mana="usesMana" :mana-code="manaCode" @save="changeDiscount" > diff --git a/src/pages/Ticket/Card/TicketSaleMoreActions.vue b/src/pages/Ticket/Card/TicketSaleMoreActions.vue index 4cc96e9e2..840b62507 100644 --- a/src/pages/Ticket/Card/TicketSaleMoreActions.vue +++ b/src/pages/Ticket/Card/TicketSaleMoreActions.vue @@ -34,10 +34,6 @@ const props = defineProps({ type: Array, default: () => [], }, - mana: { - type: Number, - default: null, - }, ticketConfig: { type: Array, default: () => [], @@ -50,6 +46,7 @@ const { dialog } = useQuasar(); const { notify } = useNotify(); const acl = useAcl(); const btnDropdownRef = ref(null); +const editManaProxyRef = ref(null); const { openConfirmationModal } = useVnConfirm(); const newDiscount = ref(null); @@ -131,13 +128,13 @@ const createClaim = () => { openConfirmationModal( t('Claim out of time'), t('Do you want to continue?'), - onCreateClaimAccepted + onCreateClaimAccepted, ); else openConfirmationModal( t('Do you want to create a claim?'), false, - onCreateClaimAccepted + onCreateClaimAccepted, ); }; @@ -216,8 +213,14 @@ const createRefund = async (withWarehouse) => { <QItemSection> <QItemLabel>{{ t('Update discount') }}</QItemLabel> </QItemSection> - <TicketEditManaProxy :mana="props.mana" @save="changeMultipleDiscount()"> + <TicketEditManaProxy + ref="editManaProxyRef" + :sale="row" + @save="changeMultipleDiscount" + > <VnInput + autofocus + @keyup.enter.stop="() => editManaProxyRef.save(row)" v-model.number="newDiscount" :label="t('ticketSale.discount')" type="number" diff --git a/src/pages/Ticket/Card/TicketSaleTracking.vue b/src/pages/Ticket/Card/TicketSaleTracking.vue index 7a33df795..723caacf5 100644 --- a/src/pages/Ticket/Card/TicketSaleTracking.vue +++ b/src/pages/Ticket/Card/TicketSaleTracking.vue @@ -31,7 +31,7 @@ const oldQuantity = ref(null); watch( () => route.params.id, - async () => nextTick(async () => await saleTrackingFetchDataRef.value.fetch()) + async () => nextTick(async () => await saleTrackingFetchDataRef.value.fetch()), ); const columns = computed(() => [ @@ -212,7 +212,7 @@ const updateShelving = async (sale) => { const { data: patchResponseData } = await axios.patch( `ItemShelvings/${sale.itemShelvingFk}`, - params + params, ); const filter = { fields: ['parkingFk'], @@ -385,7 +385,7 @@ const qCheckBoxController = (sale, action) => { </template> <template #body-cell-parking="{ row }"> <QTd style="width: 10%"> - {{ dashIfEmpty(row.parkingFk) }} + {{ dashIfEmpty(row.parkingCode) }} </QTd> </template> <template #body-cell-actions="{ row }"> diff --git a/src/pages/Ticket/Card/TicketSummary.vue b/src/pages/Ticket/Card/TicketSummary.vue index 5df08b881..5838efa88 100644 --- a/src/pages/Ticket/Card/TicketSummary.vue +++ b/src/pages/Ticket/Card/TicketSummary.vue @@ -46,6 +46,15 @@ const descriptorData = useArrayData('Ticket'); onMounted(async () => { ticketUrl.value = (await getUrl('ticket/')) + entityId.value + '/'; }); +const formattedAddress = computed(() => { + if (!ticket.value) return ''; + + const address = ticket.value.address; + const postcode = address.postalCode; + const province = address.province ? `(${address.province.name})` : ''; + + return `${address.street} - ${postcode} - ${address.city} ${province}`; +}); function isEditable() { try { @@ -238,7 +247,7 @@ onMounted(async () => { /> <VnLv :label="t('ticket.summary.consigneeStreet')" - :value="entity.address?.street" + :value="formattedAddress" /> </QCard> <QCard class="vn-one" v-if="entity.notes.length"> diff --git a/src/pages/Ticket/TicketFilter.vue b/src/pages/Ticket/TicketFilter.vue index 4b50892b0..c82c0067f 100644 --- a/src/pages/Ticket/TicketFilter.vue +++ b/src/pages/Ticket/TicketFilter.vue @@ -293,6 +293,7 @@ en: clientFk: Customer orderFk: Order from: From + shipped: Shipped to: To salesPersonFk: Salesperson stateFk: State @@ -320,6 +321,7 @@ es: clientFk: Cliente orderFk: Pedido from: Desde + shipped: F. envío to: Hasta salesPersonFk: Comercial stateFk: Estado diff --git a/src/pages/Ticket/TicketList.vue b/src/pages/Ticket/TicketList.vue index 88878076d..78bebc297 100644 --- a/src/pages/Ticket/TicketList.vue +++ b/src/pages/Ticket/TicketList.vue @@ -108,13 +108,11 @@ const columns = computed(() => [ }, { align: 'left', - name: 'shippedDate', + name: 'shipped', cardVisible: true, label: t('ticketList.shipped'), columnFilter: { component: 'date', - alias: 't', - inWhere: true, }, format: ({ shippedDate }) => toDate(shippedDate), }, diff --git a/src/pages/Worker/Card/WorkerBasicData.vue b/src/pages/Worker/Card/WorkerBasicData.vue index fcf0f0369..56a9548c6 100644 --- a/src/pages/Worker/Card/WorkerBasicData.vue +++ b/src/pages/Worker/Card/WorkerBasicData.vue @@ -17,6 +17,12 @@ const maritalStatus = [ { code: 'M', name: t('Married') }, { code: 'S', name: t('Single') }, ]; +async function setAdvancedSummary(data) { + const advanced = (await useAdvancedSummary('Workers', data.id)) ?? {}; + Object.assign(form.value.formData, advanced); + await nextTick(); + if (form.value) form.value.hasChanges = false; +} </script> <template> <FetchData @@ -36,18 +42,22 @@ const maritalStatus = [ :url-update="`Workers/${$route.params.id}`" auto-load model="Worker" - @on-fetch=" - async (data) => { - Object.assign(data, (await useAdvancedSummary('Workers', data.id)) ?? {}); - await $nextTick(); - if (form) form.hasChanges = false; - } - " + @on-fetch="setAdvancedSummary" > <template #form="{ data }"> <VnRow> - <VnInput :label="t('Name')" clearable v-model="data.firstName" /> - <VnInput :label="t('Last name')" clearable v-model="data.lastName" /> + <VnInput + :label="t('Name')" + clearable + v-model="data.firstName" + :required="true" + /> + <VnInput + :label="t('Last name')" + clearable + v-model="data.lastName" + :required="true" + /> </VnRow> <VnRow> <VnInput v-model="data.phone" :label="t('Business phone')" clearable /> diff --git a/src/pages/Worker/Card/WorkerBusiness.vue b/src/pages/Worker/Card/WorkerBusiness.vue index 6025ae289..e3582a2d5 100644 --- a/src/pages/Worker/Card/WorkerBusiness.vue +++ b/src/pages/Worker/Card/WorkerBusiness.vue @@ -3,7 +3,7 @@ import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import VnTable from 'components/VnTable/VnTable.vue'; -import { toDate } from 'src/filters'; +import { dashIfEmpty, toDate } from 'src/filters'; import { useQuasar } from 'quasar'; import axios from 'axios'; @@ -15,7 +15,7 @@ const quasar = useQuasar(); async function reactivateWorker() { const hasToReactive = tableRef.value.CrudModelRef.formData.find( - (data) => !data.ended + (data) => !data.ended, ); if (hasToReactive) { quasar @@ -38,25 +38,25 @@ const columns = computed(() => [ { name: 'started', label: t('worker.business.tableVisibleColumns.started'), - align: 'left', format: ({ started }) => toDate(started), component: 'date', cardVisible: true, create: true, + width: '90px', }, { name: 'ended', label: t('worker.business.tableVisibleColumns.ended'), - align: 'left', format: ({ ended }) => toDate(ended), component: 'date', cardVisible: true, create: true, + width: '90px', }, { label: t('worker.business.tableVisibleColumns.company'), - align: 'left', + toolTip: t('worker.business.tableVisibleColumns.company'), name: 'companyCodeFk', component: 'select', attrs: { @@ -65,23 +65,23 @@ const columns = computed(() => [ optionLabel: 'code', optionValue: 'code', }, - cardVisible: true, create: true, + width: '60px', }, { - align: 'left', name: 'reasonEndFk', component: 'select', label: t('worker.business.tableVisibleColumns.reasonEnd'), + toolTip: t('worker.business.tableVisibleColumns.reasonEnd'), attrs: { url: 'BusinessReasonEnds', fields: ['id', 'reason'], optionLabel: 'reason', }, cardVisible: true, + format: ({ reason }, dashIfEmpty) => dashIfEmpty(reason), }, { - align: 'left', name: 'departmentFk', component: 'select', label: t('worker.business.tableVisibleColumns.department'), @@ -89,15 +89,19 @@ const columns = computed(() => [ url: 'Departments', fields: ['id', 'name'], optionLabel: 'name', + optionValue: 'id', }, cardVisible: true, create: true, + width: '80px', + format: ({ departmentName }, dashIfEmpty) => dashIfEmpty(departmentName), }, { align: 'left', name: 'workerBusinessProfessionalCategoryFk', component: 'select', label: t('worker.business.tableVisibleColumns.professionalCategory'), + toolTip: t('worker.business.tableVisibleColumns.professionalCategory'), attrs: { url: 'WorkerBusinessProfessionalCategories', fields: ['id', 'description', 'code'], @@ -105,6 +109,9 @@ const columns = computed(() => [ }, cardVisible: true, create: true, + width: '100px', + format: ({ professionalDescription }, dashIfEmpty) => + dashIfEmpty(professionalDescription), }, { align: 'left', @@ -118,6 +125,8 @@ const columns = computed(() => [ }, cardVisible: true, create: true, + format: ({ calendarTypeDescription }, dashIfEmpty) => + dashIfEmpty(calendarTypeDescription), }, { align: 'left', @@ -131,6 +140,8 @@ const columns = computed(() => [ }, cardVisible: true, create: true, + width: '100px', + format: ({ workCenterName }, dashIfEmpty) => dashIfEmpty(workCenterName), }, { align: 'left', @@ -144,6 +155,7 @@ const columns = computed(() => [ }, cardVisible: true, create: true, + format: ({ payrollDescription }, dashIfEmpty) => dashIfEmpty(payrollDescription), }, { align: 'left', @@ -157,6 +169,7 @@ const columns = computed(() => [ }, cardVisible: true, create: true, + format: ({ occupationName }, dashIfEmpty) => dashIfEmpty(occupationName), }, { align: 'left', @@ -165,6 +178,7 @@ const columns = computed(() => [ component: 'input', cardVisible: true, create: true, + width: '50px', }, { align: 'left', @@ -177,6 +191,8 @@ const columns = computed(() => [ }, cardVisible: true, create: true, + format: ({ workerBusinessTypeName }, dashIfEmpty) => + dashIfEmpty(workerBusinessTypeName), }, { align: 'left', @@ -185,6 +201,7 @@ const columns = computed(() => [ component: 'input', cardVisible: true, create: true, + width: '70px', }, { align: 'left', @@ -193,6 +210,7 @@ const columns = computed(() => [ component: 'input', cardVisible: true, create: true, + width: '70px', }, { name: 'notes', @@ -208,7 +226,7 @@ const columns = computed(() => [ <VnTable ref="tableRef" data-key="WorkerBusiness" - :url="`Workers/${entityId}/Business`" + :url="`Workers/${entityId}/getWorkerBusiness`" save-url="/Businesses/crud" :create="{ urlCreate: `Workers/${entityId}/Business`, @@ -218,13 +236,12 @@ const columns = computed(() => [ }" order="id DESC" :columns="columns" - default-mode="card" auto-load - :disable-option="{ table: true }" + :disable-option="{ card: true }" :right-search="false" - card-class="grid-two q-gutter-x-xl q-gutter-y-md q-pr-lg q-py-lg" :is-editable="true" :use-model="true" + :right-search-icon="false" @save-changes="(data) => reactivateWorker(data)" /> </template> diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index de3f634e2..0e946f1dd 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -111,6 +111,7 @@ const handlePhotoUpdated = (evt = false) => { <template #body="{ entity }"> <VnLv :label="t('globals.user')" :value="entity.user?.name" /> <VnLv + class="ellipsis-text" :label="t('globals.params.email')" :value="entity.user?.emailUser?.email" copy @@ -177,6 +178,12 @@ const handlePhotoUpdated = (evt = false) => { .photo { height: 256px; } +.ellipsis-text { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} </style> <i18n> diff --git a/src/pages/Worker/Card/WorkerDescriptorProxy.vue b/src/pages/Worker/Card/WorkerDescriptorProxy.vue index a142570f9..5f71abbea 100644 --- a/src/pages/Worker/Card/WorkerDescriptorProxy.vue +++ b/src/pages/Worker/Card/WorkerDescriptorProxy.vue @@ -12,6 +12,11 @@ const $props = defineProps({ <template> <QPopupProxy> - <WorkerDescriptor v-if="$props.id" :id="$props.id" :summary="WorkerSummary" /> + <WorkerDescriptor + v-if="$props.id" + :id="$props.id" + :summary="WorkerSummary" + data-key="WorkerDescriptorProxy" + /> </QPopupProxy> </template> diff --git a/src/pages/Worker/Card/WorkerMedical.vue b/src/pages/Worker/Card/WorkerMedical.vue index b3a599af7..c04f6496b 100644 --- a/src/pages/Worker/Card/WorkerMedical.vue +++ b/src/pages/Worker/Card/WorkerMedical.vue @@ -3,6 +3,7 @@ import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import VnTable from 'components/VnTable/VnTable.vue'; +import { dashIfEmpty } from 'src/filters'; const tableRef = ref(); const { t } = useI18n(); const route = useRoute(); @@ -44,9 +45,12 @@ const columns = [ create: true, component: 'select', attrs: { - url: 'centers', + url: 'medicalCenters', fields: ['id', 'name'], }, + format: (row, dashIfEmpty) => { + return dashIfEmpty(row.center?.name); + }, }, { align: 'left', diff --git a/src/pages/Worker/Card/WorkerOperator.vue b/src/pages/Worker/Card/WorkerOperator.vue index 6faeefe67..8ab802b9f 100644 --- a/src/pages/Worker/Card/WorkerOperator.vue +++ b/src/pages/Worker/Card/WorkerOperator.vue @@ -54,9 +54,8 @@ watch( selected.value = []; } }, - { immediate: true, deep: true } + { immediate: true, deep: true }, ); - </script> <template> @@ -105,6 +104,7 @@ watch( :options="trainsData" hide-selected v-model="row.trainFk" + :required="true" /> </VnRow> <VnRow> @@ -115,12 +115,14 @@ watch( option-label="code" option-value="code" v-model="row.itemPackingTypeFk" + :required="true" /> <VnSelect :label="t('worker.operator.warehouse')" :options="warehousesData" hide-selected v-model="row.warehouseFk" + :required="true" /> </VnRow> <VnRow> @@ -175,6 +177,7 @@ watch( :label="t('worker.operator.isOnReservationMode')" v-model="row.isOnReservationMode" lazy-rules + :required="true" /> </VnRow> <VnRow> diff --git a/src/pages/Worker/Card/WorkerPBX.vue b/src/pages/Worker/Card/WorkerPBX.vue index 12f2a4b23..dae198438 100644 --- a/src/pages/Worker/Card/WorkerPBX.vue +++ b/src/pages/Worker/Card/WorkerPBX.vue @@ -1,8 +1,8 @@ -src/pages/Worker/Card/WorkerPBX.vue - <script setup> +import { useI18n } from 'vue-i18n'; import FormModel from 'src/components/FormModel.vue'; import VnInput from 'src/components/common/VnInput.vue'; +const { t } = useI18n(); </script> <template> @@ -26,3 +26,8 @@ import VnInput from 'src/components/common/VnInput.vue'; </template> </FormModel> </template> + +<i18n> + es: + It must be a 4-digit number and must not end in 00: Debe ser un número de 4 cifras y no terminar en 00 +</i18n> diff --git a/src/pages/Worker/Card/WorkerPda.vue b/src/pages/Worker/Card/WorkerPda.vue index 47e13cf6d..d32941494 100644 --- a/src/pages/Worker/Card/WorkerPda.vue +++ b/src/pages/Worker/Card/WorkerPda.vue @@ -140,6 +140,7 @@ function reloadData() { id="deviceProductionFk" hide-selected data-cy="pda-dialog-select" + :required="true" > <template #option="scope"> <QItem v-bind="scope.itemProps"> diff --git a/src/pages/Worker/WorkerDepartment.vue b/src/pages/Worker/WorkerDepartment.vue index baf6db154..e1411250b 100644 --- a/src/pages/Worker/WorkerDepartment.vue +++ b/src/pages/Worker/WorkerDepartment.vue @@ -1,16 +1,9 @@ <script setup> -import VnSection from 'src/components/common/VnSection.vue'; import WorkerDepartmentTree from './WorkerDepartmentTree.vue'; </script> <template> - <VnSection data-key="WorkerDepartment" :search-bar="false"> - <template #body> - <div class="flex flex-center q-pa-md"> - <WorkerDepartmentTree /> - </div> - </template> - </VnSection> + <QPage class="q-pa-md flex justify-center"> <WorkerDepartmentTree /> </QPage> </template> <i18n> diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue index a82bbb285..4df84e4bd 100644 --- a/src/pages/Zone/ZoneList.vue +++ b/src/pages/Zone/ZoneList.vue @@ -65,6 +65,7 @@ const tableFilter = { const columns = computed(() => [ { + align: 'left', name: 'id', label: t('list.id'), chip: { @@ -74,8 +75,6 @@ const columns = computed(() => [ columnFilter: { inWhere: true, }, - columnClass: 'shrink-column', - component: 'number', }, { align: 'left', @@ -107,6 +106,7 @@ const columns = computed(() => [ format: (row, dashIfEmpty) => dashIfEmpty(row?.agencyMode?.name), }, { + align: 'left', name: 'price', label: t('list.price'), cardVisible: true, @@ -114,11 +114,9 @@ const columns = computed(() => [ columnFilter: { inWhere: true, }, - columnClass: 'shrink-column', - component: 'number', }, { - align: 'center', + align: 'left', name: 'hour', label: t('list.close'), cardVisible: true, @@ -180,73 +178,67 @@ function formatRow(row) { <ZoneFilterPanel data-key="ZonesList" /> </template> </RightMenu> - <div class="table-container"> - <div class="column items-center"> - <VnTable - ref="tableRef" - data-key="ZonesList" - url="Zones" - :create="{ - urlCreate: 'Zones', - title: t('list.createZone'), - onDataSaved: ({ id }) => tableRef.redirect(`${id}/location`), - formInitialData: {}, - }" - :user-filter="tableFilter" - :columns="columns" - redirect="zone" - :right-search="false" - table-height="85vh" - order="id ASC" - > - <template #column-addressFk="{ row }"> - {{ dashIfEmpty(formatRow(row)) }} - </template> - <template #more-create-dialog="{ data }"> - <VnSelect - url="AgencyModes" - v-model="data.agencyModeFk" - option-value="id" - option-label="name" - :label="t('list.agency')" - /> - <VnInput - v-model="data.price" - :label="t('list.price')" - min="0" - type="number" - required="true" - /> - <VnInput - v-model="data.bonus" - :label="t('zone.bonus')" - min="0" - type="number" - /> - <VnInput - v-model="data.travelingDays" - :label="t('zone.travelingDays')" - type="number" - min="0" - /> - <VnInputTime v-model="data.hour" :label="t('list.close')" /> - <VnSelect - url="Warehouses" - v-model="data.warehouseFK" - option-value="id" - option-label="name" - :label="t('list.warehouse')" - :options="warehouseOptions" - /> - <QCheckbox - v-model="data.isVolumetric" - :label="t('list.isVolumetric')" - :toggle-indeterminate="false" - /> - </template> - </VnTable> - </div> - </div> + <VnTable + ref="tableRef" + data-key="ZonesList" + url="Zones" + :create="{ + urlCreate: 'Zones', + title: t('list.createZone'), + onDataSaved: ({ id }) => tableRef.redirect(`${id}/location`), + formInitialData: {}, + }" + :user-filter="tableFilter" + :columns="columns" + redirect="zone" + :right-search="false" + > + <template #column-addressFk="{ row }"> + {{ dashIfEmpty(formatRow(row)) }} + </template> + <template #more-create-dialog="{ data }"> + <VnSelect + url="AgencyModes" + v-model="data.agencyModeFk" + option-value="id" + option-label="name" + :label="t('list.agency')" + /> + <VnInput + v-model="data.price" + :label="t('list.price')" + min="0" + type="number" + required="true" + /> + <VnInput + v-model="data.bonus" + :label="t('zone.bonus')" + min="0" + type="number" + /> + <VnInput + v-model="data.travelingDays" + :label="t('zone.travelingDays')" + type="number" + min="0" + /> + <VnInputTime v-model="data.hour" :label="t('list.close')" /> + <VnSelect + url="Warehouses" + v-model="data.warehouseFK" + option-value="id" + option-label="name" + :label="t('list.warehouse')" + :options="warehouseOptions" + /> + <QCheckbox + v-model="data.isVolumetric" + :label="t('list.isVolumetric')" + :toggle-indeterminate="false" + /> + </template> + </VnTable> </template> <i18n> @@ -254,20 +246,3 @@ es: Search zone: Buscar zona You can search zones by id or name: Puedes buscar zonas por id o nombre </i18n> - -<style lang="scss" scoped> -.table-container { - display: flex; - justify-content: center; -} -.column { - display: flex; - flex-direction: column; - align-items: center; - min-width: 70%; -} - -:deep(.shrink-column) { - width: 8%; -} -</style> diff --git a/test/cypress/.gitignore b/test/cypress/.gitignore index c9793a5f2..3a1fcbf37 100644 --- a/test/cypress/.gitignore +++ b/test/cypress/.gitignore @@ -1,3 +1,7 @@ reports/* +videos/* screenshots/* -downloads/* \ No newline at end of file +downloads/* +storage/* +reports/* +docker/logs/* diff --git a/test/cypress/back/datasources.json b/test/cypress/back/datasources.json new file mode 100644 index 000000000..fa7b81e1c --- /dev/null +++ b/test/cypress/back/datasources.json @@ -0,0 +1,149 @@ +{ + "db": { + "connector": "memory", + "timezone": "local" + }, + "vn": { + "connector": "vn-mysql", + "database": "vn", + "debug": false, + "host": "db", + "port": "3306", + "username": "root", + "password": "root", + "connectionLimit": 100, + "queueLimit": 100, + "multipleStatements": true, + "legacyUtcDateProcessing": false, + "timezone": "local", + "connectTimeout": 40000, + "acquireTimeout": 90000, + "waitForConnections": true, + "maxIdleTime": 60000, + "idleTimeout": 60000 + }, + "osticket": { + "connector": "memory", + "timezone": "local" + }, + "tempStorage": { + "name": "tempStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/tmp", + "maxFileSize": "262144000", + "allowedContentTypes": [ + "application/x-7z-compressed", + "application/x-zip-compressed", + "application/x-rar-compressed", + "application/octet-stream", + "application/pdf", + "application/zip", + "application/rar", + "multipart/x-zip", + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "video/mp4" + ] + }, + "dmsStorage": { + "name": "dmsStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/dms", + "maxFileSize": "262144000", + "allowedContentTypes": [ + "application/x-7z-compressed", + "application/x-zip-compressed", + "application/x-rar-compressed", + "application/octet-stream", + "application/pdf", + "application/zip", + "application/rar", + "multipart/x-zip", + "image/png", + "image/jpeg", + "image/jpg", + "image/webp" + ] + }, + "imageStorage": { + "name": "imageStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/image", + "maxFileSize": "52428800", + "allowedContentTypes": [ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp" + ] + }, + "invoiceStorage": { + "name": "invoiceStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/pdfs/invoice", + "maxFileSize": "52428800", + "allowedContentTypes": [ + "application/octet-stream", + "application/pdf" + ] + }, + "claimStorage": { + "name": "claimStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/dms", + "maxFileSize": "31457280", + "allowedContentTypes": [ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "video/mp4" + ] + }, + "entryStorage": { + "name": "entryStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/dms", + "maxFileSize": "31457280", + "allowedContentTypes": [ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "video/mp4" + ] + }, + "supplierStorage": { + "name": "supplierStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/dms", + "maxFileSize": "31457280", + "allowedContentTypes": [ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "video/mp4", + "application/pdf" + ] + }, + "accessStorage": { + "name": "accessStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/access", + "maxFileSize": "524288000", + "allowedContentTypes": [ + "application/x-7z-compressed" + ] + } +} \ No newline at end of file diff --git a/test/cypress/docker-compose.yml b/test/cypress/docker-compose.yml new file mode 100644 index 000000000..9d51ee345 --- /dev/null +++ b/test/cypress/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.7' +services: + back: + image: registry.verdnatura.es/salix-back:dev + volumes: + - ./test/cypress/storage:/salix/storage + - ./test/cypress/back/datasources.json:/salix/loopback/server/datasources.json + depends_on: + - db + dns_search: . + front: + image: lilium-dev:latest + command: pnpm exec quasar dev + volumes: + - .:/app + environment: + - CI + - TZ + dns_search: . + db: + image: registry.verdnatura.es/salix-db:dev diff --git a/test/cypress/downloads/.keep b/test/cypress/downloads/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/cypress/integration/Order/orderCatalog.spec.js b/test/cypress/integration/Order/orderCatalog.spec.js index 1770a6b56..a106d0e8a 100644 --- a/test/cypress/integration/Order/orderCatalog.spec.js +++ b/test/cypress/integration/Order/orderCatalog.spec.js @@ -41,7 +41,7 @@ describe('OrderCatalog', () => { } }); cy.get( - '[data-cy="vn-searchbar"] > .q-field > .q-field__inner > .q-field__control' + '[data-cy="vn-searchbar"] > .q-field > .q-field__inner > .q-field__control', ).type('{enter}'); cy.get(':nth-child(1) > [data-cy="catalogFilterCategory"]').click(); cy.dataCy('catalogFilterValueDialogBtn').last().click(); diff --git a/test/cypress/integration/account/accountDescriptorMenu.spec.js b/test/cypress/integration/account/accountDescriptorMenu.spec.js new file mode 100644 index 000000000..67a7d8ef6 --- /dev/null +++ b/test/cypress/integration/account/accountDescriptorMenu.spec.js @@ -0,0 +1,24 @@ +describe('ClaimNotes', () => { + const descriptorOptions = '[data-cy="descriptor-more-opts-menu"] > .q-list'; + const url = '/#/account/1/summary'; + + it('should see all the account options', () => { + cy.login('itManagement'); + cy.visit(url); + cy.dataCy('descriptor-more-opts').click(); + cy.get(descriptorOptions) + .find('.q-item') + .its('length') + .then((count) => { + cy.log('Número de opciones:', count); + expect(count).to.equal(5); + }); + }); + + it('should not see any option', () => { + cy.login('salesPerson'); + cy.visit(url); + cy.dataCy('descriptor-more-opts').click(); + cy.get(descriptorOptions).should('not.be.visible'); + }); +}); diff --git a/test/cypress/integration/claim/claimAction.spec.js b/test/cypress/integration/claim/claimAction.spec.js index 685e120ce..b0a16a2ad 100644 --- a/test/cypress/integration/claim/claimAction.spec.js +++ b/test/cypress/integration/claim/claimAction.spec.js @@ -1,6 +1,6 @@ /// <reference types="cypress" /> describe('ClaimAction', () => { - const claimId = 2; + const claimId = 1; const firstRow = 'tbody > :nth-child(1)'; const destinationRow = '.q-item__section > .q-field'; @@ -24,9 +24,9 @@ describe('ClaimAction', () => { const rowData = [true]; cy.fillRow(firstRow, rowData); - cy.get('[title="Change destination"]').click(); + cy.get('[title="Change destination"]').click({ force: true }); cy.selectOption(destinationRow, 'Confeccion'); - cy.get('.q-card > .q-card__actions > .q-btn--standard').click(); + cy.get('.q-card > .q-card__actions > .q-btn--standard').click({ force: true }); }); it('should regularize', () => { diff --git a/test/cypress/integration/claim/claimDevelopment.spec.js b/test/cypress/integration/claim/claimDevelopment.spec.js index df9d09a49..7ca6472af 100755 --- a/test/cypress/integration/claim/claimDevelopment.spec.js +++ b/test/cypress/integration/claim/claimDevelopment.spec.js @@ -35,8 +35,7 @@ describe('ClaimDevelopment', () => { cy.saveCard(); }); - // TODO: #8112 - xit('should add and remove new line', () => { + it('should add and remove new line', () => { cy.wait(['@workers', '@workers']); cy.addCard(); diff --git a/test/cypress/integration/claim/claimNotes.spec.js b/test/cypress/integration/claim/claimNotes.spec.js index d7a918db1..fa4a214a1 100644 --- a/test/cypress/integration/claim/claimNotes.spec.js +++ b/test/cypress/integration/claim/claimNotes.spec.js @@ -8,7 +8,8 @@ describe('ClaimNotes', () => { it('should add a new note', () => { const message = 'This is a new message.'; - cy.get('.q-textarea').type(message); + cy.get('.q-textarea').should('not.be.disabled').type(message); + cy.get(saveBtn).click(); cy.get(firstNote).should('have.text', message); }); diff --git a/test/cypress/integration/claim/claimPhoto.spec.js b/test/cypress/integration/claim/claimPhoto.spec.js index 0a7320060..c3522cbfe 100755 --- a/test/cypress/integration/claim/claimPhoto.spec.js +++ b/test/cypress/integration/claim/claimPhoto.spec.js @@ -23,37 +23,35 @@ describe.skip('ClaimPhoto', () => { }); it('should open first image dialog change to second and close', () => { - cy.get( - ':nth-child(1) > .q-card > .q-img > .q-img__container > .q-img__image' - ).click(); + cy.get(':nth-last-child(1) > .q-card').click(); cy.get('.q-carousel__slide > .q-img > .q-img__container > .q-img__image').should( - 'be.visible' + 'be.visible', ); - cy.get('.q-carousel__control > .q-btn > .q-btn__content > .q-icon').click(); + cy.get('.q-carousel__control > button').click(); cy.get( - '.q-dialog__inner > .q-toolbar > .q-btn > .q-btn__content > .q-icon' + '.q-dialog__inner > .q-toolbar > .q-btn > .q-btn__content > .q-icon', ).click(); cy.get('.q-carousel__slide > .q-img > .q-img__container > .q-img__image').should( - 'not.be.visible' + 'not.be.visible', ); }); it('should remove third and fourth file', () => { cy.get( - '.multimediaParent > :nth-child(3) > .q-btn > .q-btn__content > .q-icon' + '.multimediaParent > :nth-last-child(1) > .q-btn > .q-btn__content > .q-icon', ).click(); cy.get( - '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block' + '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block', ).click(); cy.get('.q-notification__message').should('have.text', 'Data deleted'); cy.get( - '.multimediaParent > :nth-child(3) > .q-btn > .q-btn__content > .q-icon' + '.multimediaParent > :nth-last-child(1) > .q-btn > .q-btn__content > .q-icon', ).click(); cy.get( - '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block' + '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block', ).click(); cy.get('.q-notification__message').should('have.text', 'Data deleted'); }); diff --git a/test/cypress/integration/client/clientAddress.spec.js b/test/cypress/integration/client/clientAddress.spec.js index 434180047..8673c9083 100644 --- a/test/cypress/integration/client/clientAddress.spec.js +++ b/test/cypress/integration/client/clientAddress.spec.js @@ -4,7 +4,6 @@ describe('Client consignee', () => { cy.viewport(1280, 720); cy.login('developer'); cy.visit('#/customer/1107/address'); - cy.domContentLoad(); }); it('Should load layout', () => { cy.get('.q-card').should('be.visible'); diff --git a/test/cypress/integration/client/clientBasicData.spec.js b/test/cypress/integration/client/clientBasicData.spec.js index bed28dc22..8e0f0f6bd 100644 --- a/test/cypress/integration/client/clientBasicData.spec.js +++ b/test/cypress/integration/client/clientBasicData.spec.js @@ -8,7 +8,7 @@ describe('Client basic data', () => { it('Should load layout', () => { cy.get('.q-card').should('be.visible'); cy.dataCy('customerPhone').find('input').should('be.visible'); - cy.dataCy('customerPhone').find('input').type('123456789'); + cy.dataCy('customerPhone').find('input').clear().type('123456789'); cy.get('.q-btn-group > .q-btn--standard').click(); cy.intercept('PATCH', '/api/Clients/1102', (req) => { const { body } = req; diff --git a/test/cypress/integration/client/clientFiscalData.spec.js b/test/cypress/integration/client/clientFiscalData.spec.js index d189f896a..58d2d956f 100644 --- a/test/cypress/integration/client/clientFiscalData.spec.js +++ b/test/cypress/integration/client/clientFiscalData.spec.js @@ -4,7 +4,6 @@ describe('Client fiscal data', () => { cy.viewport(1280, 720); cy.login('developer'); cy.visit('#/customer/1107/fiscal-data'); - cy.domContentLoad(); }); it('Should change required value when change customer', () => { cy.get('.q-card').should('be.visible'); diff --git a/test/cypress/integration/client/clientList.spec.js b/test/cypress/integration/client/clientList.spec.js index f2e3671ba..f83d29278 100644 --- a/test/cypress/integration/client/clientList.spec.js +++ b/test/cypress/integration/client/clientList.spec.js @@ -1,7 +1,6 @@ /// <reference types="cypress" /> describe('Client list', () => { beforeEach(() => { - cy.viewport(1280, 720); cy.login('developer'); cy.visit('/#/customer/list', { timeout: 5000, @@ -28,7 +27,7 @@ describe('Client list', () => { Email: { val: `user.test${randomInt}@cypress.com` }, 'Sales person': { val: 'salesPerson', type: 'select' }, Location: { val: '46000', type: 'select' }, - 'Business type': { val: 'Otros', type: 'select' }, + 'Business type': { val: 'others', type: 'select' }, }; cy.fillInForm(data); @@ -37,6 +36,7 @@ describe('Client list', () => { cy.checkNotification('Data created'); cy.url().should('include', '/summary'); }); + it('Client list search client', () => { const search = 'Jessica Jones'; cy.searchByLabel('Name', search); @@ -59,6 +59,7 @@ describe('Client list', () => { cy.checkValueForm(1, search); cy.checkValueForm(2, search); }); + it('Client founded create order', () => { const search = 'Jessica Jones'; cy.searchByLabel('Name', search); diff --git a/test/cypress/integration/entry/entryList.spec.js b/test/cypress/integration/entry/entryList.spec.js index 4f99f0cb6..6a700c093 100644 --- a/test/cypress/integration/entry/entryList.spec.js +++ b/test/cypress/integration/entry/entryList.spec.js @@ -184,9 +184,8 @@ describe('Entry', () => { } function deleteEntry() { - cy.get('[data-cy="descriptor-more-opts"]').click(); - cy.waitForElement('div[data-cy="delete-entry"]'); - cy.get('div[data-cy="delete-entry"]').should('be.visible').click(); + cy.get('[data-cy="descriptor-more-opts"]').should('be.visible').click(); + cy.waitForElement('div[data-cy="delete-entry"]').click(); cy.url().should('include', 'list'); } diff --git a/test/cypress/integration/entry/stockBought.spec.js b/test/cypress/integration/entry/stockBought.spec.js index bc36156b4..b282a19a5 100644 --- a/test/cypress/integration/entry/stockBought.spec.js +++ b/test/cypress/integration/entry/stockBought.spec.js @@ -9,7 +9,7 @@ describe('EntryStockBought', () => { cy.get('[data-col-field="reserve"][data-row-index="0"]').click(); cy.get('input[name="reserve"]').type('10{enter}'); cy.get('button[title="Save"]').click(); - cy.get('.q-notification__message').should('have.text', 'Data saved'); + cy.checkNotification('Data saved'); }); it('Should add a new reserved space for buyerBoss', () => { cy.addBtnClick(); diff --git a/test/cypress/integration/invoiceIn/invoiceInList.spec.js b/test/cypress/integration/invoiceIn/invoiceInList.spec.js index 4e2b8f9cc..d9ab3f7e7 100644 --- a/test/cypress/integration/invoiceIn/invoiceInList.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInList.spec.js @@ -9,7 +9,7 @@ describe('InvoiceInList', () => { cy.viewport(1920, 1080); cy.login('developer'); cy.visit(`/#/invoice-in/list`); - cy.get('#searchbar input').should('be.visible').type('{enter}'); + cy.get('#searchbar input').type('{enter}'); }); it('should redirect on clicking a invoice', () => { @@ -21,7 +21,7 @@ describe('InvoiceInList', () => { cy.url().should('include', `/invoice-in/${id}/summary`); }); }); - // https://redmine.verdnatura.es/issues/8420 + it('should open the details', () => { cy.get(firstDetailBtn).click(); cy.get(summaryHeaders).eq(1).contains('Basic data'); diff --git a/test/cypress/integration/invoiceOut/invoiceOutList.spec.js b/test/cypress/integration/invoiceOut/invoiceOutList.spec.js index 82f0fa3b6..d3a84d226 100644 --- a/test/cypress/integration/invoiceOut/invoiceOutList.spec.js +++ b/test/cypress/integration/invoiceOut/invoiceOutList.spec.js @@ -1,6 +1,16 @@ /// <reference types="cypress" /> describe('InvoiceOut list', () => { const serial = 'Española rapida'; + const columnCheckbox = + '.bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner'; + const firstRowDescriptor = + 'tbody > :nth-child(1) > [data-col-field="clientFk"] > .no-padding > .link'; + const firstRowCheckbox = + 'tbody > :nth-child(1) > :nth-child(1) > .q-checkbox > .q-checkbox__inner '; + const summaryPopupIcon = '.header > :nth-child(2) > .q-btn__content > .q-icon'; + const filterBtn = '.q-scrollarea__content > .q-btn--standard > .q-btn__content'; + const firstSummaryIcon = + ':nth-child(1) > .text-right > [data-cy="tableAction-0"] > .q-btn__content > .q-icon'; beforeEach(() => { cy.viewport(1920, 1080); @@ -9,18 +19,32 @@ describe('InvoiceOut list', () => { cy.typeSearchbar('{enter}'); }); - it('should search and filter an invoice and enter to the summary', () => { - cy.typeSearchbar('1{enter}'); - cy.get('.q-virtual-scroll__content > :nth-child(2) > :nth-child(7)').click(); - cy.get('.header > a.q-btn > .q-btn__content').click(); - cy.typeSearchbar('{enter}'); - cy.dataCy('InvoiceOutFilterAmountBtn').find('input').type('8.88{enter}'); + it('should download one pdf from the subtoolbar button', () => { + cy.get(firstRowCheckbox).click(); + cy.dataCy('InvoiceOutDownloadPdfBtn').click(); }); it('should download all pdfs', () => { - cy.get('.bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner').click(); + cy.get(columnCheckbox).click(); cy.dataCy('InvoiceOutDownloadPdfBtn').click(); - cy.get('.bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner').click(); + }); + + it('should open the invoice descriptor from table icon', () => { + cy.get(firstSummaryIcon).click(); + cy.get('.cardSummary').should('be.visible'); + cy.get('.summaryHeader > div').should('include.text', 'A1111111'); + }); + + it('should open the client descriptor', () => { + cy.get(firstRowDescriptor).click(); + cy.get(summaryPopupIcon).click(); + }); + + it('should filter the results by client ID, then check the first result is correct', () => { + cy.dataCy('Customer ID_input').type('1103'); + cy.get(filterBtn).click(); + cy.get(firstRowDescriptor).click(); + cy.get('.q-item > .q-item__label').should('include.text', '1103'); }); it('should give an error when manual invoicing a ticket that is already invoiced', () => { @@ -31,11 +55,14 @@ describe('InvoiceOut list', () => { cy.checkNotification('This ticket is already invoiced'); }); - it('should create a manual invoice and enter to its summary', () => { + it('should create a manual invoice and enter to its summary, then delete that invoice', () => { cy.dataCy('vnTableCreateBtn').click(); - cy.dataCy('InvoiceOutCreateTicketinput').type(8); + cy.dataCy('InvoiceOutCreateTicketinput').type(9); cy.selectOption('[data-cy="InvoiceOutCreateSerialSelect"]', serial); cy.dataCy('FormModelPopup_save').click(); cy.checkNotification('Data created'); + cy.dataCy('descriptor-more-opts').click(); + cy.get('.q-menu > .q-list > :nth-child(4)').click(); + cy.dataCy('VnConfirm_confirm').click(); }); }); diff --git a/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js b/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js index 02b7fbb43..4d530de05 100644 --- a/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js +++ b/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js @@ -1,11 +1,26 @@ /// <reference types="cypress" /> describe('InvoiceOut negative bases', () => { + const getDescriptors = (opt) => + `:nth-child(1) > [data-col-field="${opt}"] > .no-padding > .link`; + beforeEach(() => { cy.viewport(1920, 1080); cy.login('developer'); cy.visit(`/#/invoice-out/negative-bases`); }); + it('should open the posible descriptors', () => { + cy.get(getDescriptors('clientId')).click(); + cy.get('.descriptor').should('be.visible'); + cy.get('.q-item > .q-item__label').should('include.text', '1101'); + cy.get(getDescriptors('ticketFk')).click(); + cy.get('.descriptor').should('be.visible'); + cy.get('.q-item > .q-item__label').should('include.text', '23'); + cy.get(getDescriptors('workerName')).click(); + cy.get('.descriptor').should('be.visible'); + cy.get('.q-item > .q-item__label').should('include.text', '18'); + }); + it('should filter and download as CSV', () => { cy.get('input[name="ticketFk"]').type('23{enter}'); cy.get('#subToolbar > .q-btn').click(); diff --git a/test/cypress/integration/invoiceOut/invoiceOutSummary.spec.js b/test/cypress/integration/invoiceOut/invoiceOutSummary.spec.js index 44b0a9961..7ebaf3ef3 100644 --- a/test/cypress/integration/invoiceOut/invoiceOutSummary.spec.js +++ b/test/cypress/integration/invoiceOut/invoiceOutSummary.spec.js @@ -1,44 +1,95 @@ /// <reference types="cypress" /> -describe('InvoiceOut summary', () => { +describe.skip('InvoiceOut summary', () => { const transferInvoice = { Client: { val: 'employee', type: 'select' }, Type: { val: 'Error in customer data', type: 'select' }, }; + const firstRowDescriptors = (opt) => + `tbody > :nth-child(1) > :nth-child(${opt}) > .q-btn`; + const toCustomerSummary = '[href="#/customer/1101"]'; + const toTicketList = '[href="#/ticket/list?table={%22refFk%22:%22T1111111%22}"]'; + const selectMenuOption = (opt) => `.q-menu > .q-list > :nth-child(${opt})`; + const confirmSend = '.q-btn--unelevated'; + beforeEach(() => { cy.viewport(1920, 1080); cy.login('developer'); - cy.visit(`/#/invoice-out/list`); + cy.visit(`/#/invoice-out/1/summary`); }); - it('should generate the invoice PDF', () => { - cy.typeSearchbar('T1111111{enter}'); - cy.dataCy('descriptor-more-opts').click(); - cy.get('.q-menu > .q-list > :nth-child(6)').click(); - cy.dataCy('VnConfirm_confirm').click(); - cy.checkNotification('The invoice PDF document has been regenerated'); + it('open the descriptors', () => { + cy.get(firstRowDescriptors(1)).click(); + cy.get('.descriptor').should('be.visible'); + cy.get('.q-item > .q-item__label').should('include.text', '1'); + cy.get(firstRowDescriptors(2)).click(); + cy.get('.descriptor').should('be.visible'); + cy.get('.q-item > .q-item__label').should('include.text', '1101'); }); - it('should refund the invoice ', () => { + + it('should open the client summary and the ticket list', () => { + cy.get(toCustomerSummary).click(); + cy.get('.descriptor').should('be.visible'); + cy.get('.q-item > .q-item__label').should('include.text', '1101'); + }); + + it('should open the ticket list', () => { + cy.get(toTicketList).click(); + cy.get('.descriptor').should('be.visible'); + cy.dataCy('vnFilterPanelChip').should('include.text', 'T1111111'); + }); + + it('should transfer the invoice ', () => { cy.typeSearchbar('T1111111{enter}'); cy.dataCy('descriptor-more-opts').click(); - cy.get('.q-menu > .q-list > :nth-child(7)').click(); - cy.get('#q-portal--menu--3 > .q-menu > .q-list > :nth-child(2)').click(); - cy.checkNotification('The following refund ticket have been created'); + cy.get(selectMenuOption(1)).click(); + cy.fillInForm(transferInvoice); + cy.get('.q-mt-lg > .q-btn').click(); + cy.checkNotification('Transferred invoice'); + }); + + it('should send the invoice as PDF', () => { + cy.dataCy('descriptor-more-opts').click(); + cy.get(selectMenuOption(3)).click(); + cy.dataCy('InvoiceOutDescriptorMenuSendPdfOption').click(); + cy.get(confirmSend).click(); + cy.checkNotification('Notification sent'); + }); + + it('should send the invoice as CSV', () => { + cy.dataCy('descriptor-more-opts').click(); + cy.get(selectMenuOption(3)).click(); + cy.dataCy('InvoiceOutDescriptorMenuSendCsvOption').click(); + cy.get(confirmSend).click(); + cy.checkNotification('Notification sent'); }); it('should delete an invoice ', () => { cy.typeSearchbar('T2222222{enter}'); cy.dataCy('descriptor-more-opts').click(); - cy.get('.q-menu > .q-list > :nth-child(4)').click(); + cy.get(selectMenuOption(4)).click(); cy.dataCy('VnConfirm_confirm').click(); cy.checkNotification('InvoiceOut deleted'); }); - it('should transfer the invoice ', () => { - cy.typeSearchbar('T1111111{enter}'); + + it('should book the invoice', () => { cy.dataCy('descriptor-more-opts').click(); - cy.get('.q-menu > .q-list > :nth-child(1)').click(); - cy.fillInForm(transferInvoice); - cy.get('.q-mt-lg > .q-btn').click(); - cy.checkNotification('Transferred invoice'); + cy.get(selectMenuOption(5)).click(); + cy.dataCy('VnConfirm_confirm').click(); + cy.checkNotification('InvoiceOut booked'); + }); + + it('should generate the invoice PDF', () => { + cy.dataCy('descriptor-more-opts').click(); + cy.get(selectMenuOption(6)).click(); + cy.dataCy('VnConfirm_confirm').click(); + cy.checkNotification('The invoice PDF document has been regenerated'); + }); + + it('should refund the invoice ', () => { + cy.dataCy('descriptor-more-opts').click(); + cy.get(selectMenuOption(7)).click(); + cy.get('#q-portal--menu--3 > .q-menu > .q-list > :nth-child(2)').click(); + cy.checkNotification('The following refund ticket have been created'); }); }); diff --git a/test/cypress/integration/item/ItemFixedPrice.spec.js b/test/cypress/integration/item/ItemFixedPrice.spec.js index edb6a63fe..2cf9c2caf 100644 --- a/test/cypress/integration/item/ItemFixedPrice.spec.js +++ b/test/cypress/integration/item/ItemFixedPrice.spec.js @@ -11,7 +11,7 @@ describe('Handle Items FixedPrice', () => { cy.visit('/#/item/fixed-price', { timeout: 5000 }); cy.waitForElement('.q-table'); cy.get( - '.q-header > .q-toolbar > :nth-child(1) > .q-btn__content > .q-icon' + '.q-header > .q-toolbar > :nth-child(1) > .q-btn__content > .q-icon', ).click(); }); it.skip('filter', function () { @@ -38,7 +38,7 @@ describe('Handle Items FixedPrice', () => { cy.get('.q-gutter-x-sm > .q-btn > .q-btn__content > .q-icon').click(); cy.get(`${firstRow} > .text-right > .q-btn > .q-btn__content > .q-icon`).click(); cy.get( - '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block' + '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block', ).click(); cy.get('.q-notification__message').should('have.text', 'Data saved'); }); @@ -56,7 +56,7 @@ describe('Handle Items FixedPrice', () => { cy.get(' .bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner ').click(); cy.get('#subToolbar > .q-btn--flat').click(); cy.get( - '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block' + '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block', ).click(); cy.get('.q-notification__message').should('have.text', 'Data saved'); }); diff --git a/test/cypress/integration/item/itemList.spec.js b/test/cypress/integration/item/itemList.spec.js index 97e85a212..f0c744f21 100644 --- a/test/cypress/integration/item/itemList.spec.js +++ b/test/cypress/integration/item/itemList.spec.js @@ -15,6 +15,7 @@ describe('Item list', () => { cy.get('.q-menu .q-item').contains('Anthurium').click(); cy.get('.q-virtual-scroll__content > :nth-child(4) > :nth-child(4)').click(); }); + // https://redmine.verdnatura.es/issues/8421 it.skip('should create an item', () => { const data = { @@ -28,7 +29,7 @@ describe('Item list', () => { cy.dataCy('FormModelPopup_save').click(); cy.checkNotification('Data created'); cy.get( - ':nth-child(2) > .q-drawer > .q-drawer__content > .q-scrollarea > .q-scrollarea__container > .q-scrollarea__content' + ':nth-child(2) > .q-drawer > .q-drawer__content > .q-scrollarea > .q-scrollarea__container > .q-scrollarea__content', ).should('be.visible'); }); }); diff --git a/test/cypress/integration/item/itemTag.spec.js b/test/cypress/integration/item/itemTag.spec.js index d1596f693..425eaffe6 100644 --- a/test/cypress/integration/item/itemTag.spec.js +++ b/test/cypress/integration/item/itemTag.spec.js @@ -1,36 +1,33 @@ -/// <reference types="cypress" /> describe('Item tag', () => { beforeEach(() => { cy.viewport(1920, 1080); cy.login('developer'); cy.visit(`/#/item/1/tags`); + cy.get('.q-page').should('be.visible'); + cy.waitForElement('[data-cy="itemTags"]'); }); + const createNewTag = 'createNewTag'; + const saveBtn = 'crudModelDefaultSaveBtn'; + const newTag = 'tagundefined'; + it('should throw an error adding an existent tag', () => { - cy.get('.q-page').should('be.visible'); - cy.get('.q-page-sticky > div').click(); - cy.get('.q-page-sticky > div').click(); - cy.dataCy('Tag_select').eq(7).type('Tallos'); - cy.get('.q-menu .q-item').contains('Tallos').click(); - cy.get(':nth-child(8) > [label="Value"]').type('1'); - cy.dataCy('crudModelDefaultSaveBtn').click(); + cy.dataCy(createNewTag).click(); + cy.dataCy(newTag).should('be.visible').click().type('Genero{enter}'); + cy.dataCy('tagGeneroValue').eq(1).should('be.visible'); + cy.dataCy(saveBtn).click(); cy.checkNotification("The tag or priority can't be repeated for an item"); }); it('should add a new tag', () => { - cy.get('.q-page').should('be.visible'); - cy.get('.q-page-sticky > div').click(); - cy.get('.q-page-sticky > div').click(); - cy.dataCy('Tag_select').eq(7).click(); - cy.get('.q-menu .q-item').contains('Ancho de la base').type('{enter}'); - cy.get(':nth-child(8) > [label="Value"]').type('50'); - cy.dataCy('crudModelDefaultSaveBtn').click(); + cy.dataCy(createNewTag).click(); + cy.dataCy(newTag).should('be.visible').click().type('Forma{enter}'); + cy.dataCy('tagFormaValue').should('be.visible').type('50'); + cy.dataCy(saveBtn).click(); + cy.checkNotification('Data saved'); - cy.dataCy('itemTags') - .children(':nth-child(8)') - .find('.justify-center > .q-icon') - .click(); - cy.dataCy('VnConfirm_confirm').click(); + cy.dataCy('deleteTagForma').should('be.visible').click(); + cy.dataCy('VnConfirm_confirm').should('be.visible').click(); cy.checkNotification('Data saved'); }); }); diff --git a/test/cypress/integration/outLogin/recoverPassword.spec.js b/test/cypress/integration/outLogin/recoverPassword.spec.js index eec81b661..48f6f8563 100755 --- a/test/cypress/integration/outLogin/recoverPassword.spec.js +++ b/test/cypress/integration/outLogin/recoverPassword.spec.js @@ -24,7 +24,7 @@ describe('Recover Password', () => { it('should change password to user', () => { // Get token from mail cy.request( - `http://localhost:3000/api/Mails?filter=%7B%22where%22%3A%20%7B%22receiver%22%3A%20%22${username}%40mydomain.com%22%7D%2C%20%22order%22%3A%20%5B%22id%20DESC%22%5D%7D&access_token=DEFAULT_TOKEN` + `/api/Mails?filter=%7B%22where%22%3A%20%7B%22receiver%22%3A%20%22${username}%40mydomain.com%22%7D%2C%20%22order%22%3A%20%5B%22id%20DESC%22%5D%7D&access_token=DEFAULT_TOKEN` ).then((response) => { const regex = /access_token=([a-zA-Z0-9]+)/; const [match] = response.body[0].body.match(regex); diff --git a/test/cypress/integration/outLogin/twoFactor.spec.js b/test/cypress/integration/outLogin/twoFactor.spec.js index 4d8561f0f..6a8ac9c06 100755 --- a/test/cypress/integration/outLogin/twoFactor.spec.js +++ b/test/cypress/integration/outLogin/twoFactor.spec.js @@ -11,7 +11,7 @@ describe('Two Factor', () => { it('should enable two factor to sysadmin', () => { cy.request( 'PATCH', - `http://localhost:3000/api/VnUsers/${userId}/update-user?access_token=DEFAULT_TOKEN`, + `/api/VnUsers/${userId}/update-user?access_token=DEFAULT_TOKEN`, { twoFactor: 'email' } ); }); @@ -41,7 +41,7 @@ describe('Two Factor', () => { // Get code from mail cy.request( - `http://localhost:3000/api/Mails?filter=%7B%22where%22%3A%20%7B%22receiver%22%3A%20%22${username}%40mydomain.com%22%7D%2C%20%22order%22%3A%20%5B%22id%20DESC%22%5D%7D&access_token=DEFAULT_TOKEN` + `/api/Mails?filter=%7B%22where%22%3A%20%7B%22receiver%22%3A%20%22${username}%40mydomain.com%22%7D%2C%20%22order%22%3A%20%5B%22id%20DESC%22%5D%7D&access_token=DEFAULT_TOKEN` ).then((response) => { const tempDiv = document.createElement('div'); tempDiv.innerHTML = response.body[0].body; diff --git a/test/cypress/integration/parking/parkingBasicData.spec.js b/test/cypress/integration/parking/parkingBasicData.spec.js deleted file mode 100644 index f64f23ec8..000000000 --- a/test/cypress/integration/parking/parkingBasicData.spec.js +++ /dev/null @@ -1,23 +0,0 @@ -/// <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'; - beforeEach(() => { - cy.login('developer'); - cy.visit(`/#/shelving/parking/1/basic-data`); - }); - - it('should edit the code and sector', () => { - cy.get(sectorSelect).type('Second'); - cy.get(sectorOpt).click(); - - cy.get(codeInput).eq(0).clear(); - cy.get(codeInput).eq(0).type('900-001'); - - cy.saveCard(); - - cy.get(sectorSelect).should('have.value', 'Second sector'); - cy.get(codeInput).should('have.value', '900-001'); - }); -}); diff --git a/test/cypress/integration/parking/parkingList.spec.js b/test/cypress/integration/parking/parkingList.spec.js deleted file mode 100644 index 8b7152ca4..000000000 --- a/test/cypress/integration/parking/parkingList.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -/// <reference types="cypress" /> -describe('ParkingList', () => { - const searchbar = '#searchbar input'; - const firstCard = '.q-card:nth-child(1)'; - const firstChipId = - ':nth-child(1) > :nth-child(1) > .justify-between > .flex > .q-chip > .q-chip__content'; - const firstDetailBtn = - ':nth-child(1) > :nth-child(1) > .card-list-body > .actions > .q-btn'; - const summaryHeader = '.summaryBody .header'; - - beforeEach(() => { - cy.viewport(1920, 1080); - cy.login('developer'); - cy.visit(`/#/shelving/parking/list`); - }); - - it('should redirect on clicking a parking', () => { - cy.get(searchbar).type('{enter}'); - cy.get(firstChipId) - .invoke('text') - .then((content) => { - const id = content.substring(4); - cy.get(firstCard).click(); - cy.url().should('include', `/parking/${id}/summary`); - }); - }); - - it('should open the details', () => { - cy.get(searchbar).type('{enter}'); - cy.get(firstDetailBtn).click(); - cy.get(summaryHeader).contains('Basic data'); - }); -}); diff --git a/test/cypress/integration/route/agency/agencyWorkCenter.spec.js b/test/cypress/integration/route/agency/agencyWorkCenter.spec.js index 82ec6626d..5679ceba1 100644 --- a/test/cypress/integration/route/agency/agencyWorkCenter.spec.js +++ b/test/cypress/integration/route/agency/agencyWorkCenter.spec.js @@ -1,4 +1,4 @@ -describe('AgencyWorkCenter', () => { +describe.skip('AgencyWorkCenter', () => { beforeEach(() => { cy.viewport(1920, 1080); cy.login('developer'); diff --git a/test/cypress/integration/route/routeExtendedList.spec.js b/test/cypress/integration/route/routeExtendedList.spec.js new file mode 100644 index 000000000..e3505ad60 --- /dev/null +++ b/test/cypress/integration/route/routeExtendedList.spec.js @@ -0,0 +1,205 @@ +describe.skip('Route extended list', () => { + const getSelector = (colField) => `tr:last-child > [data-col-field="${colField}"]`; + + const selectors = { + worker: getSelector('workerFk'), + agency: getSelector('agencyModeFk'), + vehicle: getSelector('vehicleFk'), + date: getSelector('dated'), + description: getSelector('description'), + served: getSelector('isOk'), + lastRowSelectCheckBox: 'tbody > tr:last-child > :nth-child(1) .q-checkbox__inner', + removeBtn: '[title="Remove"]', + resetBtn: '[title="Reset"]', + confirmBtn: 'VnConfirm_confirm', + saveBtn: 'crudModelDefaultSaveBtn', + saveFormBtn: 'FormModelPopup_save', + cloneBtn: '#st-actions > .q-btn-group > :nth-child(1)', + downloadBtn: '#st-actions > .q-btn-group > :nth-child(2)', + markServedBtn: '#st-actions > .q-btn-group > :nth-child(3)', + searchbar: 'searchbar', + firstTicketsRowSelectCheckBox: + '.q-card > :nth-child(2) > .q-table__container > .q-table__middle > .q-table > tbody > :nth-child(1) > .q-table--col-auto-width > .q-checkbox > .q-checkbox__inner > .q-checkbox__bg > .q-checkbox__svg', + }; + + const checkboxState = { + check: 'check', + uncheck: 'close', + }; + const url = '/#/route/extended-list'; + const dataCreated = 'Data created'; + const dataSaved = 'Data saved'; + + const originalFields = [ + { selector: selectors.worker, type: 'select', value: 'logistic' }, + { selector: selectors.agency, type: 'select', value: 'Super-Man delivery' }, + { selector: selectors.vehicle, type: 'select', value: '3333-IMK' }, + { selector: selectors.date, type: 'date', value: '01/02/2024' }, + { selector: selectors.description, type: 'input', value: 'Test route' }, + { selector: selectors.served, type: 'checkbox', value: checkboxState.uncheck }, + ]; + + const updateFields = [ + { selector: selectors.worker, type: 'select', value: 'salesperson' }, + { selector: selectors.agency, type: 'select', value: 'inhouse pickup' }, + { selector: selectors.vehicle, type: 'select', value: '1111-IMK' }, + { selector: selectors.date, type: 'date', value: '01/01/2001' }, + { selector: selectors.description, type: 'input', value: 'Description updated' }, + { selector: selectors.served, type: 'checkbox', value: checkboxState.check }, + ]; + + function fillField(selector, type, value) { + switch (type) { + case 'select': + cy.get(selector).should('be.visible').click(); + cy.dataCy('null_select').clear().type(value); + cy.get('.q-item').contains(value).click(); + break; + case 'input': + cy.get(selector).should('be.visible').click(); + cy.dataCy('null_input').clear().type(`${value}{enter}`); + break; + case 'date': + cy.get(selector).should('be.visible').click(); + cy.dataCy('null_inputDate').clear().type(`${value}{enter}`); + break; + case 'checkbox': + cy.get(selector).should('be.visible').click().click(); + break; + } + } + + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit(url); + cy.typeSearchbar('{enter}'); + }); + + after(() => { + cy.visit(url); + cy.typeSearchbar('{enter}'); + cy.get(selectors.lastRowSelectCheckBox).click(); + + cy.get(selectors.removeBtn).click(); + cy.dataCy(selectors.confirmBtn).click(); + }); + + it('Should list routes', () => { + cy.get('.q-table') + .children() + .should('be.visible') + .should('have.length.greaterThan', 0); + }); + + it('Should create new route', () => { + cy.addBtnClick(); + + const data = { + Worker: { val: 'logistic', type: 'select' }, + Agency: { val: 'Super-Man delivery', type: 'select' }, + Vehicle: { val: '3333-IMK', type: 'select' }, + Date: { val: '02-01-2024', type: 'date' }, + From: { val: '01-01-2024', type: 'date' }, + To: { val: '10-01-2024', type: 'date' }, + 'Km start': { val: 1000 }, + 'Km end': { val: 1200 }, + Description: { val: 'Test route' }, + }; + + cy.fillInForm(data); + + cy.dataCy(selectors.saveFormBtn).click(); + cy.checkNotification(dataCreated); + cy.url().should('include', '/summary'); + }); + + it('Should reset changed values when click reset button', () => { + updateFields.forEach(({ selector, type, value }) => { + fillField(selector, type, value); + }); + + cy.get('[title="Reset"]').click(); + + originalFields.forEach(({ selector, value }) => { + cy.validateContent(selector, value); + }); + }); + + it('Should clone selected route', () => { + cy.get(selectors.lastRowSelectCheckBox).click(); + cy.get(selectors.cloneBtn).click(); + cy.dataCy('route.Starting date_inputDate').type('10-05-2001{enter}'); + cy.get('.q-card__actions > .q-btn--standard > .q-btn__content').click(); + cy.validateContent(selectors.date, '05/10/2001'); + }); + + it('Should download selected route', () => { + const downloadsFolder = Cypress.config('downloadsFolder'); + cy.get(selectors.lastRowSelectCheckBox).click(); + cy.get(selectors.downloadBtn).click(); + cy.wait(5000); + + const fileName = 'download.zip'; + cy.readFile(`${downloadsFolder}/${fileName}`).should('exist'); + + cy.task('deleteFile', `${downloadsFolder}/${fileName}`).then((deleted) => { + expect(deleted).to.be.true; + }); + }); + + it('Should mark as served the selected route', () => { + cy.get(selectors.lastRowSelectCheckBox).click(); + cy.get(selectors.markServedBtn).click(); + + cy.typeSearchbar('{enter}'); + cy.validateContent(selectors.served, checkboxState.check); + }); + + it('Should delete the selected route', () => { + cy.get(selectors.lastRowSelectCheckBox).click(); + + cy.get(selectors.removeBtn).click(); + cy.dataCy(selectors.confirmBtn).click(); + + cy.checkNotification(dataSaved); + }); + + it('Should save changes in route', () => { + updateFields.forEach(({ selector, type, value }) => { + fillField(selector, type, value); + }); + + cy.dataCy(selectors.saveBtn).should('not.be.disabled').click(); + cy.checkNotification(dataSaved); + + cy.typeSearchbar('{enter}'); + + updateFields.forEach(({ selector, value }) => { + cy.validateContent(selector, value); + }); + }); + + it('Should add ticket to route', () => { + cy.dataCy('tableAction-0').last().click(); + cy.get(selectors.firstTicketsRowSelectCheckBox).click(); + cy.get('.q-card__actions > .q-btn--standard > .q-btn__content').click(); + cy.checkNotification(dataSaved); + }); + + it('Should open summary pop-up when click summuary icon', () => { + cy.dataCy('tableAction-1').last().click(); + cy.get('.summaryHeader > :nth-child(2').should('contain', updateFields[4].value); + }); + + it('Should redirect to the summary from the route summary pop-up', () => { + cy.dataCy('tableAction-1').last().click(); + cy.get('.header > .q-icon').should('be.visible').click(); + cy.url().should('include', '/summary'); + }); + + it('Should redirect to the summary when click go to summary icon', () => { + cy.dataCy('tableAction-2').last().click(); + cy.url().should('include', '/summary'); + }); +}); diff --git a/test/cypress/integration/shelving/parking/parkingBasicData.spec.js b/test/cypress/integration/shelving/parking/parkingBasicData.spec.js new file mode 100644 index 000000000..e28d7eeca --- /dev/null +++ b/test/cypress/integration/shelving/parking/parkingBasicData.spec.js @@ -0,0 +1,32 @@ +/// <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'; + beforeEach(() => { + cy.login('developer'); + cy.visit(`/#/shelving/parking/1/basic-data`); + }); + + it('should give an error if the code aldready exists', () => { + cy.get(codeInput).eq(0).should('have.value', '700-01').clear(); + cy.get(codeInput).eq(0).type('700-02'); + cy.saveCard(); + cy.get('.q-notification__message').should('have.text', 'The code already exists'); + }); + + it('should edit the code and sector', () => { + cy.get(sectorSelect).type('First'); + cy.get(sectorOpt).click(); + + cy.get(codeInput).eq(0).clear(); + cy.get(codeInput).eq(0).type('700-01'); + cy.dataCy('Picking order_input').clear().type(80230); + + cy.saveCard(); + cy.get('.q-notification__message').should('have.text', 'Data saved'); + cy.get(sectorSelect).should('have.value', 'First sector'); + cy.get(codeInput).should('have.value', '700-01'); + cy.dataCy('Picking order_input').should('have.value', 80230); + }); +}); diff --git a/test/cypress/integration/shelving/parking/parkingList.spec.js b/test/cypress/integration/shelving/parking/parkingList.spec.js new file mode 100644 index 000000000..ecee8aab7 --- /dev/null +++ b/test/cypress/integration/shelving/parking/parkingList.spec.js @@ -0,0 +1,30 @@ +/// <reference types="cypress" /> +describe('ParkingList', () => { + const searchbar = '#searchbar input'; + const firstCard = ':nth-child(1) > .q-card > .no-margin > .q-py-none'; + const summaryHeader = '.summaryBody .header'; + + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit(`/#/shelving/parking/list`); + }); + + it('should redirect on clicking a parking', () => { + cy.get(searchbar).type('{enter}'); + cy.get(firstCard).click(); + cy.get(summaryHeader).contains('Basic data'); + }); + + it('should filter and redirect if there is only one result', () => { + cy.dataCy('Code_input').type('1{enter}'); + cy.dataCy('Sector_select').type('Normal{enter}'); + cy.get(summaryHeader).contains('Basic data'); + }); + + it('should filter and redirect to summary if only one result', () => { + cy.dataCy('Code_input').type('A{enter}'); + cy.dataCy('Sector_select').type('First Sector{enter}'); + cy.url().should('match', /\/shelving\/parking\/\d+\/summary/); + }); +}); diff --git a/test/cypress/integration/ticket/ticketDescriptor.spec.js b/test/cypress/integration/ticket/ticketDescriptor.spec.js index cd9f288f5..3fc2842d3 100644 --- a/test/cypress/integration/ticket/ticketDescriptor.spec.js +++ b/test/cypress/integration/ticket/ticketDescriptor.spec.js @@ -30,8 +30,6 @@ describe('Ticket descriptor', () => { it('should set the weight of the ticket', () => { cy.visit('/#/ticket/10/summary'); - cy.intercept('GET', /\/api\/Tickets\/\d/).as('ticket'); - cy.wait('@ticket'); cy.openActionsDescriptor(); cy.contains(listItem, setWeightOpt).click(); cy.intercept('POST', /\/api\/Tickets\/\d+\/setWeight/).as('weight'); diff --git a/test/cypress/integration/ticket/ticketExpedition.spec.js b/test/cypress/integration/ticket/ticketExpedition.spec.js index d957f2136..6d7dc6721 100644 --- a/test/cypress/integration/ticket/ticketExpedition.spec.js +++ b/test/cypress/integration/ticket/ticketExpedition.spec.js @@ -1,6 +1,5 @@ /// <reference types="cypress" /> -// https://redmine.verdnatura.es/issues/8423 -describe.skip('Ticket expedtion', () => { +describe('Ticket expedtion', () => { const tableContent = '.q-table .q-virtual-scroll__content'; const stateTd = 'td:nth-child(9)'; diff --git a/test/cypress/integration/ticket/ticketSale.spec.js b/test/cypress/integration/ticket/ticketSale.spec.js index 63562bd26..b59765ca6 100644 --- a/test/cypress/integration/ticket/ticketSale.spec.js +++ b/test/cypress/integration/ticket/ticketSale.spec.js @@ -1,7 +1,7 @@ /// <reference types="cypress" /> describe('TicketSale', () => { - describe('Free ticket #31', () => { + describe.skip('Free ticket #31', () => { beforeEach(() => { cy.login('developer'); cy.viewport(1920, 1080); @@ -121,7 +121,7 @@ describe('TicketSale', () => { cy.url().should('match', /\/ticket\/31\/log/); }); }); - describe('Ticket prepared #23', () => { + describe.skip('Ticket prepared #23', () => { beforeEach(() => { cy.login('developer'); cy.viewport(1920, 1080); diff --git a/test/cypress/integration/vnComponent/VnAccountNumber.spec.js b/test/cypress/integration/vnComponent/VnAccountNumber.spec.js index 000c2151d..63ab646fe 100644 --- a/test/cypress/integration/vnComponent/VnAccountNumber.spec.js +++ b/test/cypress/integration/vnComponent/VnAccountNumber.spec.js @@ -3,7 +3,6 @@ describe('VnInput Component', () => { cy.login('developer'); cy.viewport(1920, 1080); cy.visit('/#/supplier/1/fiscal-data'); - cy.domContentLoad(); }); it('should replace character at cursor position in insert mode', () => { @@ -14,8 +13,7 @@ describe('VnInput Component', () => { cy.dataCy('supplierFiscalDataAccount').type('{movetostart}'); // Escribe un número y verifica que se reemplace correctamente cy.dataCy('supplierFiscalDataAccount').type('999'); - cy.dataCy('supplierFiscalDataAccount') - .should('have.value', '9990000001'); + cy.dataCy('supplierFiscalDataAccount').should('have.value', '9990000001'); }); it('should replace character at cursor position in insert mode', () => { @@ -26,14 +24,12 @@ describe('VnInput Component', () => { cy.dataCy('supplierFiscalDataAccount').type('{movetostart}'); // Escribe un número y verifica que se reemplace correctamente en la posicion incial cy.dataCy('supplierFiscalDataAccount').type('999'); - cy.dataCy('supplierFiscalDataAccount') - .should('have.value', '9990000001'); + cy.dataCy('supplierFiscalDataAccount').should('have.value', '9990000001'); }); it('should respect maxlength prop', () => { cy.dataCy('supplierFiscalDataAccount').clear(); cy.dataCy('supplierFiscalDataAccount').type('123456789012345'); - cy.dataCy('supplierFiscalDataAccount') - .should('have.value', '1234567890'); // asumiendo que maxlength es 10 + cy.dataCy('supplierFiscalDataAccount').should('have.value', '1234567890'); // asumiendo que maxlength es 10 }); }); diff --git a/test/cypress/integration/vnComponent/VnLocation.spec.js b/test/cypress/integration/vnComponent/VnLocation.spec.js index 751b3a065..986cbcaaf 100644 --- a/test/cypress/integration/vnComponent/VnLocation.spec.js +++ b/test/cypress/integration/vnComponent/VnLocation.spec.js @@ -17,7 +17,6 @@ describe('VnLocation', () => { cy.viewport(1280, 720); cy.login('developer'); cy.visit('/#/supplier/567/fiscal-data', { timeout: 7000 }); - cy.domContentLoad(); cy.get(createLocationButton).click(); }); it('should filter provinces based on selected country', () => { @@ -40,7 +39,7 @@ describe('VnLocation', () => { cy.selectOption(countrySelector, country); cy.dataCy('locationProvince').type(`${province}{enter}`); 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(); cy.dataCy('locationProvince').should('have.value', province); }); @@ -87,7 +86,7 @@ describe('VnLocation', () => { .get(':nth-child(1)') .should('have.length.at.least', 2); cy.get( - firstOption.concat(' > .q-item__section > .q-item__label--caption') + firstOption.concat(' > .q-item__section > .q-item__label--caption'), ).should('have.text', postCodeLabel); cy.get(firstOption).click(); cy.get('.q-btn-group > .q-btn--standard > .q-btn__content > .q-icon').click(); @@ -103,7 +102,7 @@ describe('VnLocation', () => { cy.get('.q-card > h1').should('have.text', 'New postcode'); cy.selectOption( `${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix}`, - province + province, ); cy.get(dialogInputs).eq(0).clear(); cy.get(dialogInputs).eq(0).type(postCode); @@ -156,7 +155,7 @@ describe('VnLocation', () => { cy.get(createLocationButton).click(); cy.selectOption( `${createForm.prefix} > :nth-child(5) > :nth-child(3) `, - 'España' + 'España', ); cy.dataCy('Province_icon').click(); diff --git a/test/cypress/integration/wagon/wagonCreate.spec.js b/test/cypress/integration/wagon/wagonCreate.spec.js index ce3723e54..4e78e57de 100644 --- a/test/cypress/integration/wagon/wagonCreate.spec.js +++ b/test/cypress/integration/wagon/wagonCreate.spec.js @@ -18,6 +18,6 @@ describe('WagonCreate', () => { ).type('100'); cy.dataCy('Type_select').type('{downarrow}{enter}'); - cy.get('[title="Remove"] > .q-btn__content > .q-icon').click(); + cy.get('[title="Remove"] > .q-btn__content > .q-icon').first().click(); }); }); diff --git a/test/cypress/integration/worker/workerCreate.spec.js b/test/cypress/integration/worker/workerCreate.spec.js index 71fd6b347..6349f04a0 100644 --- a/test/cypress/integration/worker/workerCreate.spec.js +++ b/test/cypress/integration/worker/workerCreate.spec.js @@ -1,4 +1,4 @@ -describe('WorkerCreate', () => { +describe.skip('WorkerCreate', () => { const externalRadio = '.q-radio:nth-child(2)'; const developerBossId = 120; const payMethodCross = diff --git a/test/cypress/integration/worker/workerNotificationsManager.spec.js b/test/cypress/integration/worker/workerNotificationsManager.spec.js index ad48d8a6c..0907cc4ad 100644 --- a/test/cypress/integration/worker/workerNotificationsManager.spec.js +++ b/test/cypress/integration/worker/workerNotificationsManager.spec.js @@ -22,7 +22,7 @@ describe('WorkerNotificationsManager', () => { ); }); - it('should active a notification that is yours', () => { + it.skip('should active a notification that is yours', () => { cy.login('developer'); cy.visit(`/#/worker/${developerId}/notifications`); cy.waitForElement(activeList); diff --git a/test/cypress/integration/zone/zoneBasicData.spec.js b/test/cypress/integration/zone/zoneBasicData.spec.js index 70ded3f79..6db39b072 100644 --- a/test/cypress/integration/zone/zoneBasicData.spec.js +++ b/test/cypress/integration/zone/zoneBasicData.spec.js @@ -9,13 +9,7 @@ describe('ZoneBasicData', () => { }); it('should throw an error if the name is empty', () => { - cy.intercept('GET', /\/api\/Zones\/4./).as('zone'); - - cy.wait('@zone').then(() => { - cy.get('[data-cy="zone-basic-data-name"] input').type( - '{selectall}{backspace}', - ); - }); + cy.get('[data-cy="zone-basic-data-name"] input').type('{selectall}{backspace}'); cy.get(saveBtn).click(); cy.checkNotification("can't be blank"); diff --git a/test/cypress/integration/zone/zoneWarehouse.spec.js b/test/cypress/integration/zone/zoneWarehouse.spec.js index 4a100a762..0f646f33a 100644 --- a/test/cypress/integration/zone/zoneWarehouse.spec.js +++ b/test/cypress/integration/zone/zoneWarehouse.spec.js @@ -3,7 +3,7 @@ describe('ZoneWarehouse', () => { Warehouse: { val: 'Warehouse One', type: 'select' }, }; - const dataError = 'ER_DUP_ENTRY: Duplicate entry'; + const dataError = 'The introduced warehouse already exists'; const saveBtn = '.q-btn--standard > .q-btn__content > .block'; beforeEach(() => { @@ -18,7 +18,7 @@ describe('ZoneWarehouse', () => { cy.get(saveBtn).click(); cy.checkNotification(dataError); }); - + it('should create & remove a warehouse', () => { cy.addBtnClick(); cy.fillInForm(data); diff --git a/test/cypress/storage/access/.keep b/test/cypress/storage/access/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/cypress/storage/dms/.keep b/test/cypress/storage/dms/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/cypress/storage/image/catalog/.keep b/test/cypress/storage/image/catalog/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/cypress/storage/image/user/.keep b/test/cypress/storage/image/user/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/cypress/storage/pdfs/invoice/.keep b/test/cypress/storage/pdfs/invoice/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/cypress/storage/tmp/.keep b/test/cypress/storage/tmp/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index aa4a1219e..096a29dc1 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -27,11 +27,14 @@ // DO NOT REMOVE // Imports Quasar Cypress AE predefined commands // import { registerCommands } from '@quasar/quasar-app-extension-testing-e2e-cypress'; -Cypress.Commands.add('waitUntil', { prevSubject: 'optional' }, require('./waitUntil')); +import waitUntil from './waitUntil'; +Cypress.Commands.add('waitUntil', { prevSubject: 'optional' }, waitUntil); + Cypress.Commands.add('resetDB', () => { cy.exec('pnpm run resetDatabase'); }); -Cypress.Commands.add('login', (user) => { + +Cypress.Commands.add('login', (user = 'developer') => { //cy.visit('/#/login'); cy.request({ method: 'POST', @@ -54,10 +57,13 @@ Cypress.Commands.add('login', (user) => { }); }); -Cypress.Commands.add('domContentLoad', (element, timeout = 5000) => { +Cypress.Commands.overwrite('visit', (originalFn, url, options) => { + originalFn(url, options); cy.waitUntil(() => cy.document().then((doc) => doc.readyState === 'complete')); + cy.waitUntil(() => cy.get('main').should('exist')); }); -Cypress.Commands.add('waitForElement', (element, timeout = 5000) => { + +Cypress.Commands.add('waitForElement', (element, timeout = 10000) => { cy.get(element, { timeout }).should('be.visible').and('not.be.disabled'); }); @@ -110,7 +116,7 @@ function selectItem(selector, option, ariaControl, hasWrite = true) { .find((item) => item.innerText.includes(option)); if (matchingItem) return cy.wrap(matchingItem).click(); - if (hasWrite) cy.get(selector).clear().type(option, { delay: 0 }); + if (hasWrite) cy.get(selector).clear().type(option); return selectItem(selector, option, ariaControl, false); }); } @@ -321,18 +327,18 @@ Cypress.Commands.add('clickButtonDescriptor', (id) => { }); Cypress.Commands.add('openUserPanel', () => { - cy.get( - '.column > .q-avatar > .q-avatar__content > .q-img > .q-img__container > .q-img__image', - ).click(); + cy.dataCy('userPanel_btn').click(); }); Cypress.Commands.add('checkNotification', (text) => { - cy.get('.q-notification') + cy.get('.q-notification', { timeout: 10000 }) .should('be.visible') - .last() - .then(($lastNotification) => { - if (!Cypress.$($lastNotification).text().includes(text)) - throw new Error(`Notification not found: "${text}"`); + .should('have.length.greaterThan', 0) + .should(($elements) => { + const found = $elements + .toArray() + .some((el) => Cypress.$(el).text().includes(text)); + expect(found).to.be.true; }); }); @@ -379,7 +385,13 @@ Cypress.Commands.add('clickButtonWith', (type, value) => { } }); Cypress.Commands.add('clickButtonWithIcon', (iconClass) => { - cy.get(`.q-icon.${iconClass}`).parent().click(); + cy.waitForElement('[data-cy="descriptor_actions"]'); + cy.get('[data-cy="loading-spinner"]', { timeout: 10000 }).should('not.be.visible'); + cy.get('.q-btn') + .filter((index, el) => Cypress.$(el).find('.q-icon.' + iconClass).length > 0) + .then(($btn) => { + cy.wrap($btn).click(); + }); }); Cypress.Commands.add('clickButtonWithText', (buttonText) => { cy.get('.q-btn').contains(buttonText).click(); diff --git a/test/cypress/support/index.js b/test/cypress/support/index.js index c57c1a303..075e0c8eb 100644 --- a/test/cypress/support/index.js +++ b/test/cypress/support/index.js @@ -27,7 +27,17 @@ function randomNumber(options = { length: 10 }) { function randomizeValue(characterSet, options) { return Array.from({ length: options.length }, () => - characterSet.charAt(Math.floor(Math.random() * characterSet.length)) + characterSet.charAt(Math.floor(Math.random() * characterSet.length)), ).join(''); } + +const style = document.createElement('style'); +style.innerHTML = ` + * { + transition: none !important; + animation: none !important; + } +`; +document.head.appendChild(style); + export { randomString, randomNumber, randomizeValue }; diff --git a/vitest.config.js b/vitest.config.js index a465f0e2d..f856a1dc9 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -5,9 +5,21 @@ import jsconfigPaths from 'vite-jsconfig-paths'; import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; import path from 'path'; +let reporters, + outputFile; + +if (process.env.CI) { + reporters = ['junit', 'default']; + outputFile = {junit: './junit/vitest.xml'}; +} else { + reporters = 'default'; +} + // https://vitejs.dev/config/ export default defineConfig({ test: { + reporters, + outputFile, environment: 'happy-dom', setupFiles: 'test/vitest/setup-file.js', include: [