diff --git a/Jenkinsfile b/Jenkinsfile index a9db9d369..59bf09e22 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -125,7 +125,7 @@ pipeline { sh "docker-compose ${env.COMPOSE_PARAMS} pull db" sh "docker-compose ${env.COMPOSE_PARAMS} up -d" - def modules = sh(script: 'node test/cypress/docker/find/find.js', returnStdout: true).trim() + def modules = sh(script: "node test/cypress/docker/find/find.js ${env.COMPOSE_TAG}", returnStdout: true).trim() echo "E2E MODULES: ${modules}" image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") { sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'" diff --git a/cypress.config.js b/cypress.config.js index d9cdbe728..7458c0b05 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -44,6 +44,7 @@ export default defineConfig({ supportFile: 'test/cypress/support/index.js', videosFolder: 'test/cypress/videos', downloadsFolder: 'test/cypress/downloads', + tmpUploadFolder: 'test/cypress/storage/tmp/dms', video: false, specPattern: 'test/cypress/integration/**/*.spec.js', experimentalRunAllSpecs: true, diff --git a/package.json b/package.json index 19b4c7a6f..b7b04287d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "25.16.0", + "version": "25.18.0", "description": "Salix frontend", "productName": "Salix", "author": "Verdnatura", @@ -89,4 +89,4 @@ "vite": "^6.0.11", "vitest": "^0.31.1" } -} +} \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 27cc34c38..0217c45c2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,6 +2,7 @@ import { onMounted } from 'vue'; import { useQuasar, Dark } from 'quasar'; import { useI18n } from 'vue-i18n'; +import VnScroll from './components/common/VnScroll.vue'; const quasar = useQuasar(); const { availableLocales, locale, fallbackLocale } = useI18n(); @@ -38,6 +39,7 @@ quasar.iconMapFn = (iconName) => { + \ No newline at end of file diff --git a/src/components/EntityCalendarGrid.vue b/src/components/EntityCalendarGrid.vue new file mode 100644 index 000000000..09ccaad07 --- /dev/null +++ b/src/components/EntityCalendarGrid.vue @@ -0,0 +1,126 @@ + + + + + + + {{ headerTitle }} + + + + emit('onDateSelected', data)" + /> + + + \ No newline at end of file diff --git a/src/components/FilterTravelForm.vue b/src/components/FilterTravelForm.vue index 4aad327b2..f2a7a09eb 100644 --- a/src/components/FilterTravelForm.vue +++ b/src/components/FilterTravelForm.vue @@ -156,6 +156,9 @@ const selectTravel = ({ id }) => { option-label="name" option-value="id" v-model="travelFilterParams.warehouseOutFk" + :where="{ + isOrigin: true, + }" /> { option-label="name" option-value="id" v-model="travelFilterParams.warehouseInFk" + :where="{ + isDestiny: true, + }" /> {}, }, + preventSubmit: { + type: Boolean, + default: true, + }, }); -const emit = defineEmits(['onFetch', 'onDataSaved']); +const emit = defineEmits(['onFetch', 'onDataSaved', 'submit']); const modelValue = computed( () => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`, ).value; @@ -301,7 +304,7 @@ function onBeforeSave(formData, originalData) { ); } async function onKeyup(evt) { - if (evt.key === 'Enter' && !('prevent-submit' in attrs)) { + if (evt.key === 'Enter' && !$props.preventSubmit) { const input = evt.target; if (input.type == 'textarea' && evt.shiftKey) { let { selectionStart, selectionEnd } = input; @@ -330,6 +333,7 @@ defineExpose({ { - + { - + diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index 7329ddae2..65139e1e5 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -69,7 +69,7 @@ const refresh = () => window.location.reload(); 'no-visible': !stateQuery.isLoading().value, }" size="sm" - data-cy="loading-spinner" + data-cy="navBar-spinner" /> diff --git a/src/components/RegularizeStockForm.vue b/src/components/RegularizeStockForm.vue index 91a2e5d39..340a18f9d 100644 --- a/src/components/RegularizeStockForm.vue +++ b/src/components/RegularizeStockForm.vue @@ -40,6 +40,9 @@ const onDataSaved = (data) => { url="Warehouses" @on-fetch="(data) => (warehousesOptions = data)" auto-load + :where="{ + isInventory: true, + }" /> -import { markRaw, computed } from 'vue'; -import { QCheckbox, QToggle } from 'quasar'; +import { markRaw, computed, onBeforeMount } from 'vue'; +import { QToggle } from 'quasar'; import { useArrayData } from 'composables/useArrayData'; import VnSelect from 'components/common/VnSelect.vue'; import VnInput from 'components/common/VnInput.vue'; @@ -150,6 +150,16 @@ const showFilter = computed( const onTabPressed = async () => { if (model.value) enterEvent['keyup.enter'](); }; + +onBeforeMount(() => { + const columnFilter = $props.column?.columnFilter; + const component = columnFilter?.component; + const defaultComponent = components[component]; + const events = { update: updateEvent, enter: enterEvent }; + + if (!columnFilter || defaultComponent) return; + $props.column.columnFilter.event = events[columnFilter.event]; +}); diff --git a/src/components/VnTable/VnOrder.vue b/src/components/VnTable/VnOrder.vue index fe071a57f..7a9e1e634 100644 --- a/src/components/VnTable/VnOrder.vue +++ b/src/components/VnTable/VnOrder.vue @@ -16,7 +16,7 @@ const $props = defineProps({ required: true, }, searchUrl: { - type: String, + type: [String, Boolean], default: 'table', }, vertical: { diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 29a9200f0..8915500fc 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -33,6 +33,8 @@ import VnTableOrder from 'src/components/VnTable/VnOrder.vue'; import VnTableFilter from './VnTableFilter.vue'; import { getColAlign } from 'src/composables/getColAlign'; import RightMenu from '../common/RightMenu.vue'; +import VnScroll from '../common/VnScroll.vue'; +import VnMultiCheck from '../common/VnMultiCheck.vue'; const arrayData = useArrayData(useAttrs()['data-key']); const $props = defineProps({ @@ -65,7 +67,7 @@ const $props = defineProps({ default: null, }, create: { - type: Object, + type: [Boolean, Object], default: null, }, createAsDialog: { @@ -112,6 +114,10 @@ const $props = defineProps({ type: Object, default: () => ({}), }, + multiCheck: { + type: Object, + default: () => ({}), + }, crudModel: { type: Object, default: () => ({}), @@ -156,6 +162,7 @@ const CARD_MODE = 'card'; const TABLE_MODE = 'table'; const mode = ref(CARD_MODE); const selected = ref([]); +const selectAll = ref(false); const hasParams = ref(false); const CrudModelRef = ref({}); const showForm = ref(false); @@ -168,6 +175,7 @@ const params = ref(useFilterParams($attrs['data-key']).params); const orders = ref(useFilterParams($attrs['data-key']).orders); const app = inject('app'); const tableHeight = useTableHeight(); +const vnScrollRef = ref(null); const editingRow = ref(null); const editingField = ref(null); @@ -189,6 +197,17 @@ const tableModes = [ }, ]; +const onVirtualScroll = ({ to }) => { + handleScroll(); + const virtualScrollContainer = tableRef.value?.$el?.querySelector('.q-table__middle'); + if (virtualScrollContainer) { + virtualScrollContainer.dispatchEvent(new CustomEvent('scroll')); + if (vnScrollRef.value) { + vnScrollRef.value.updateScrollContainer(virtualScrollContainer); + } + } +}; + onBeforeMount(() => { const urlParams = route.query[$props.searchUrl]; hasParams.value = urlParams && Object.keys(urlParams).length !== 0; @@ -327,16 +346,13 @@ function handleOnDataSaved(_) { if (_.onDataSaved) _.onDataSaved({ CrudModelRef: CrudModelRef.value }); else $props.create.onDataSaved(_); } - function handleScroll() { if ($props.crudModel.disableInfiniteScroll) return; - const tMiddle = tableRef.value.$el.querySelector('.q-table__middle'); const { scrollHeight, scrollTop, clientHeight } = tMiddle; const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) <= 40; if (isAtBottom) CrudModelRef.value.vnPaginateRef.paginate(); } - function handleSelection({ evt, added, rows: selectedRows }, rows) { if (evt?.shiftKey && added) { const rowIndex = selectedRows[0].$index; @@ -628,6 +644,23 @@ const rowCtrlClickFunction = computed(() => { }; return () => {}; }); +const handleMultiCheck = (value) => { + if (value) { + selected.value = tableRef.value.rows; + } else { + selected.value = []; + } + emit('update:selected', selected.value); +}; + +const handleSelectedAll = (data) => { + if (data) { + selected.value = data; + } else { + selected.value = []; + } + emit('update:selected', selected.value); +}; @@ -683,13 +716,24 @@ const rowCtrlClickFunction = computed(() => { flat :style="isTableMode && `max-height: ${$props.tableHeight || tableHeight}`" :virtual-scroll="isTableMode" - @virtual-scroll="handleScroll" + @virtual-scroll="onVirtualScroll" @row-click="(event, row) => handleRowClick(event, row)" @update:selected="emit('update:selected', $event)" @selection="(details) => handleSelection(details, rows)" :hide-selected-banner="true" :data-cy > + + + + @@ -741,6 +785,7 @@ const rowCtrlClickFunction = computed(() => { withFilters " :column="col" + :data-cy="`column-filter-${col.name}`" :show-title="true" :data-key="$attrs['data-key']" v-model="params[columnName(col)]" @@ -1087,6 +1132,11 @@ const rowCtrlClickFunction = computed(() => { + en: diff --git a/src/components/VnTable/VnTableFilter.vue b/src/components/VnTable/VnTableFilter.vue index 109e2b77e..a7b2108ed 100644 --- a/src/components/VnTable/VnTableFilter.vue +++ b/src/components/VnTable/VnTableFilter.vue @@ -30,6 +30,7 @@ function columnName(col) { v-bind="$attrs" :search-button="true" :disable-submit-event="true" + :data-key="$attrs['data-key']" :search-url > diff --git a/src/components/VnTable/VnVisibleColumn.vue b/src/components/VnTable/VnVisibleColumn.vue index 6d15c585e..7d7c0dd29 100644 --- a/src/components/VnTable/VnVisibleColumn.vue +++ b/src/components/VnTable/VnVisibleColumn.vue @@ -58,7 +58,7 @@ async function getConfig(url, filter) { const response = await axios.get(url, { params: { filter: filter }, }); - return response.data && response.data.length > 0 ? response.data[0] : null; + return response?.data && response?.data?.length > 0 ? response.data[0] : null; } async function fetchViewConfigData() { diff --git a/src/components/VnTable/__tests__/VnTable.spec.js b/src/components/VnTable/__tests__/VnTable.spec.js index e5e38a63c..a297ac3dc 100644 --- a/src/components/VnTable/__tests__/VnTable.spec.js +++ b/src/components/VnTable/__tests__/VnTable.spec.js @@ -11,6 +11,9 @@ describe('VnTable', () => { propsData: { columns: [], }, + attrs: { + 'data-key': 'test', + }, }); vm = wrapper.vm; diff --git a/src/components/__tests__/CrudModel.spec.js b/src/components/__tests__/CrudModel.spec.js index c620d29ad..475f56aec 100644 --- a/src/components/__tests__/CrudModel.spec.js +++ b/src/components/__tests__/CrudModel.spec.js @@ -11,13 +11,7 @@ describe('CrudModel', () => { beforeAll(() => { wrapper = createWrapper(CrudModel, { global: { - stubs: [ - 'vnPaginate', - 'useState', - 'arrayData', - 'useStateStore', - 'vue-i18n', - ], + stubs: ['vnPaginate', 'vue-i18n'], mocks: { validate: vi.fn(), }, @@ -29,7 +23,7 @@ describe('CrudModel', () => { dataKey: 'crudModelKey', model: 'crudModel', url: 'crudModelUrl', - saveFn: '', + saveFn: vi.fn(), }, }); wrapper = wrapper.wrapper; @@ -231,7 +225,7 @@ describe('CrudModel', () => { expect(vm.isLoading).toBe(false); expect(vm.hasChanges).toBe(false); - await wrapper.setProps({ saveFn: '' }); + await wrapper.setProps({ saveFn: null }); }); it("should use default url if there's not saveFn", async () => { diff --git a/src/components/__tests__/Leftmenu.spec.js b/src/components/__tests__/Leftmenu.spec.js index 22b2b5fb7..5bddc104c 100644 --- a/src/components/__tests__/Leftmenu.spec.js +++ b/src/components/__tests__/Leftmenu.spec.js @@ -170,7 +170,7 @@ describe('LeftMenu as card', () => { vm = mount('card').vm; }); - it('should get routes for card source', async () => { + it('should get routes for card source', () => { vm.getRoutes(); }); }); @@ -251,7 +251,6 @@ describe('LeftMenu as main', () => { }); it('should get routes for main source', () => { - vm.props.source = 'main'; vm.getRoutes(); expect(navigation.getModules).toHaveBeenCalled(); }); diff --git a/src/components/common/VnAccountNumber.vue b/src/components/common/VnAccountNumber.vue index 8bff3e261..5bce49459 100644 --- a/src/components/common/VnAccountNumber.vue +++ b/src/components/common/VnAccountNumber.vue @@ -8,7 +8,8 @@ const model = defineModel({ prop: 'modelValue' }); diff --git a/src/components/common/VnBankDetailsForm.vue b/src/components/common/VnBankDetailsForm.vue new file mode 100644 index 000000000..4e13a4d86 --- /dev/null +++ b/src/components/common/VnBankDetailsForm.vue @@ -0,0 +1,93 @@ + + + (bankEntities = data)" + /> + + + + {{ t('components.iban_tooltip') }} + + + + + + + + + + + {{ scope.opt.bic }} + {{ scope.opt.name }} + + + + + diff --git a/src/components/common/VnDate.vue b/src/components/common/VnDate.vue index 761ac995e..522078df4 100644 --- a/src/components/common/VnDate.vue +++ b/src/components/common/VnDate.vue @@ -1,5 +1,5 @@ diff --git a/src/components/common/VnDms.vue b/src/components/common/VnDms.vue index de22e4857..128cee069 100644 --- a/src/components/common/VnDms.vue +++ b/src/components/common/VnDms.vue @@ -4,6 +4,7 @@ import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; +import useNotify from 'src/composables/useNotify.js'; import FetchData from 'components/FetchData.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; @@ -12,6 +13,7 @@ import FormModelPopup from 'components/FormModelPopup.vue'; const route = useRoute(); const { t } = useI18n(); +const { notify } = useNotify(); const emit = defineEmits(['onDataSaved']); const $props = defineProps({ @@ -61,8 +63,11 @@ function onFileChange(files) { function mapperDms(data) { const formData = new FormData(); - const { files } = data; - if (files) formData.append(files?.name, files); + let files = data.files; + if (files) { + files = Array.isArray(files) ? files : [files]; + files.forEach((file) => formData.append(file?.name, file)); + } const dms = { hasFile: !!data.hasFile, @@ -83,11 +88,16 @@ function getUrl() { } async function save() { - const body = mapperDms(dms.value); - const response = await axios.post(getUrl(), body[0], body[1]); - emit('onDataSaved', body[1].params, response); - delete dms.value.files; - return response; + try { + const body = mapperDms(dms.value); + const response = await axios.post(getUrl(), body[0], body[1]); + emit('onDataSaved', body[1].params, response); + notify(t('globals.dataSaved'), 'positive'); + delete dms.value.files; + return response; + } catch (e) { + throw e; + } } function defaultData() { @@ -208,7 +218,7 @@ function addDefaultData(data) { } -en: +en: contentTypesInfo: Allowed file types {allowedContentTypes} EntryDmsDescription: Reference {reference} WorkersDescription: Working of employee id {reference} diff --git a/src/components/common/VnDmsList.vue b/src/components/common/VnDmsList.vue index aafa9f4ba..345870aa4 100644 --- a/src/components/common/VnDmsList.vue +++ b/src/components/common/VnDmsList.vue @@ -13,10 +13,12 @@ import VnDms from 'src/components/common/VnDms.vue'; import VnConfirm from 'components/ui/VnConfirm.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; import { useSession } from 'src/composables/useSession'; +import useNotify from 'src/composables/useNotify.js'; const route = useRoute(); const quasar = useQuasar(); const { t } = useI18n(); +const { notify } = useNotify(); const rows = ref([]); const dmsRef = ref(); const formDialog = ref({}); @@ -88,7 +90,6 @@ const dmsFilter = { ], }, }, - where: { [$props.filter]: route.params.id }, }; const columns = computed(() => [ @@ -258,9 +259,16 @@ function deleteDms(dmsFk) { }, }) .onOk(async () => { - await axios.post(`${$props.deleteModel ?? $props.model}/${dmsFk}/removeFile`); - const index = rows.value.findIndex((row) => row.id == dmsFk); - rows.value.splice(index, 1); + try { + await axios.post( + `${$props.deleteModel ?? $props.model}/${dmsFk}/removeFile`, + ); + const index = rows.value.findIndex((row) => row.id == dmsFk); + rows.value.splice(index, 1); + notify(t('globals.dataDeleted'), 'positive'); + } catch (e) { + throw e; + } }); } @@ -298,7 +306,9 @@ defineExpose({ :data-key="$props.model" :url="$props.model" :user-filter="dmsFilter" + search-url="dmsFilter" :order="['dmsFk DESC']" + :filter="{ where: { [$props.filter]: route.params.id } }" auto-load @on-fetch="setData" > diff --git a/src/components/common/VnInputBic.vue b/src/components/common/VnInputBic.vue deleted file mode 100644 index b29644912..000000000 --- a/src/components/common/VnInputBic.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - {{ t('components.iban_tooltip') }} - - - - diff --git a/src/components/common/VnInputDate.vue b/src/components/common/VnInputDate.vue index 343130f1d..0507fddfb 100644 --- a/src/components/common/VnInputDate.vue +++ b/src/components/common/VnInputDate.vue @@ -1,6 +1,6 @@ { v-if=" ($attrs.clearable == undefined || $attrs.clearable) && hover && - model && + inputValue && !$attrs.disable " @click=" vnInputDateRef.focus(); + inputValue = null; model = null; isPopupOpen = false; " diff --git a/src/components/common/VnInputDateTime.vue b/src/components/common/VnInputDateTime.vue new file mode 100644 index 000000000..67d1793b1 --- /dev/null +++ b/src/components/common/VnInputDateTime.vue @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + es: + Open date: Abrir fecha + diff --git a/src/components/common/VnMultiCheck.vue b/src/components/common/VnMultiCheck.vue new file mode 100644 index 000000000..19b93ffa9 --- /dev/null +++ b/src/components/common/VnMultiCheck.vue @@ -0,0 +1,80 @@ + + + + + + + + + + {{ t('Select all', { rows: rows.length }) }} + + + + + + + + +en: + Select all: 'Select all ({rows})' +fr: + Select all: 'Sélectionner tout ({rows})' +es: + Select all: 'Seleccionar todo ({rows})' +de: + Select all: 'Alle auswählen ({rows})' +it: + Select all: 'Seleziona tutto ({rows})' +pt: + Select all: 'Selecionar tudo ({rows})' + diff --git a/src/components/common/VnScroll.vue b/src/components/common/VnScroll.vue new file mode 100644 index 000000000..481371604 --- /dev/null +++ b/src/components/common/VnScroll.vue @@ -0,0 +1,100 @@ + + + + + {{ $t('globals.scrollToTop') }} + + + + + diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index 32a8db16f..2cb72261f 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -54,6 +54,10 @@ const $props = defineProps({ type: [Array], default: () => [], }, + filterFn: { + type: Function, + default: null, + }, exprBuilder: { type: Function, default: null, @@ -62,16 +66,12 @@ const $props = defineProps({ type: Boolean, default: true, }, - defaultFilter: { - type: Boolean, - default: true, - }, fields: { type: Array, default: null, }, include: { - type: [Object, Array], + type: [Object, Array, String], default: null, }, where: { @@ -79,7 +79,7 @@ const $props = defineProps({ default: null, }, sortBy: { - type: String, + type: [String, Array], default: null, }, limit: { @@ -152,10 +152,22 @@ const value = computed({ }, }); +const arrayDataKey = + $props.dataKey ?? + ($props.url?.length > 0 ? $props.url : ($attrs.name ?? $attrs.label)); + +const arrayData = useArrayData(arrayDataKey, { + url: $props.url, + searchUrl: false, + mapKey: $attrs['map-key'], +}); + const computedSortBy = computed(() => { return $props.sortBy || $props.optionLabel + ' ASC'; }); +const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val); + watch(options, (newValue) => { setOptions(newValue); }); @@ -174,16 +186,7 @@ onMounted(() => { if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300); }); -const arrayDataKey = - $props.dataKey ?? - ($props.url?.length > 0 ? $props.url : ($attrs.name ?? $attrs.label)); - -const arrayData = useArrayData(arrayDataKey, { - url: $props.url, - searchUrl: false, - mapKey: $attrs['map-key'], -}); - +const someIsLoading = computed(() => isLoading.value || !!arrayData?.isLoading?.value); function findKeyInOptions() { if (!$props.options) return; return filter($props.modelValue, $props.options)?.length; @@ -252,43 +255,41 @@ async function fetchFilter(val) { } async function filterHandler(val, update) { - if (isLoading.value) return update(); - if (!val && lastVal.value === val) { - lastVal.value = val; - return update(); - } - lastVal.value = val; let newOptions; - if (!$props.defaultFilter) return update(); - if ( - $props.url && - ($props.limit || (!$props.limit && Object.keys(myOptions.value).length === 0)) - ) { - newOptions = await fetchFilter(val); - } else newOptions = filter(val, myOptionsOriginal.value); - update( - () => { - if ($props.noOne && noOneText.toLowerCase().includes(val.toLowerCase())) - newOptions.unshift(noOneOpt.value); + if ($props.filterFn) update($props.filterFn(val)); + else if (!val && lastVal.value === val) update(); + else { + const makeRequest = + ($props.url && $props.limit) || + (!$props.limit && Object.keys(myOptions.value).length === 0); + newOptions = makeRequest + ? await fetchFilter(val) + : filter(val, myOptionsOriginal.value); - myOptions.value = newOptions; - }, - (ref) => { - if (val !== '' && ref.options.length > 0) { - ref.setOptionIndex(-1); - ref.moveOptionSelection(1, true); - } - }, - ); + update( + () => { + if ($props.noOne && noOneText.toLowerCase().includes(val.toLowerCase())) + newOptions.unshift(noOneOpt.value); + + myOptions.value = newOptions; + }, + (ref) => { + if (val !== '' && ref.options.length > 0) { + ref.setOptionIndex(-1); + ref.moveOptionSelection(1, true); + } + }, + ); + } + + lastVal.value = val; } function nullishToTrue(value) { return value ?? true; } -const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val); - async function onScroll({ to, direction, from, index }) { const lastIndex = myOptions.value.length - 1; @@ -366,7 +367,7 @@ function getCaption(opt) { virtual-scroll-slice-size="options.length" hide-bottom-space :input-debounce="useURL ? '300' : '0'" - :loading="isLoading" + :loading="someIsLoading" @virtual-scroll="onScroll" @keydown="handleKeyDown" :data-cy="$attrs.dataCy ?? $attrs.label + '_select'" @@ -374,7 +375,7 @@ function getCaption(opt) { > { @@ -389,7 +390,7 @@ function getCaption(opt) { { @@ -414,7 +415,7 @@ function getCaption(opt) { {{ opt[optionLabel] }} - + {{ `#${getCaption(opt)}` }} diff --git a/src/components/common/VnSmsDialog.vue b/src/components/common/VnSmsDialog.vue index 8851a33b2..ada2d02fa 100644 --- a/src/components/common/VnSmsDialog.vue +++ b/src/components/common/VnSmsDialog.vue @@ -232,7 +232,7 @@ fr: pt: Portugais pt: Send SMS: Enviar SMS - CustomerDefaultLanguage: Este cliente utiliza o {locale} como seu idioma padrão + CustomerDefaultLanguage: Este cliente utiliza o {locale} como seu idioma padrão Language: Linguagem Phone: Móvel Subject: Assunto diff --git a/src/components/common/__tests__/VnBankDetailsForm.spec.js b/src/components/common/__tests__/VnBankDetailsForm.spec.js new file mode 100644 index 000000000..9d6ade902 --- /dev/null +++ b/src/components/common/__tests__/VnBankDetailsForm.spec.js @@ -0,0 +1,43 @@ +import { createWrapper } from 'app/test/vitest/helper'; +import VnBankDetailsForm from 'components/common/VnBankDetailsForm.vue'; +import { vi, afterEach, expect, it, beforeEach, describe } from 'vitest'; + +describe('VnBankDetail Component', () => { + let vm; + let wrapper; + const bankEntities = [ + { id: 2100, bic: 'CAIXESBBXXX', name: 'CaixaBank' }, + { id: 1234, bic: 'TESTBIC', name: 'Test Bank' }, + ]; + const correctIban = 'ES6621000418401234567891'; + + beforeAll(() => { + wrapper = createWrapper(VnBankDetailsForm, { + $props: { + iban: null, + bankEntityFk: null, + disableElement: false, + }, + }); + vm = wrapper.vm; + wrapper = wrapper.wrapper; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should update bankEntityFk when IBAN exists in bankEntities', async () => { + vm.bankEntities = bankEntities; + + await vm.autofillBic(correctIban); + expect(vm.bankEntityFk).toBe(2100); + }); + + it('should set bankEntityFk to null when IBAN bank code is not found', async () => { + vm.bankEntities = bankEntities; + + await vm.autofillBic('ES1234567891324567891234'); + expect(vm.bankEntityFk).toBe(null); + }); +}); diff --git a/src/components/common/__tests__/VnDiscount.spec.js b/src/components/common/__tests__/VnDiscount.spec.js index 5d5be61ac..34c4ff630 100644 --- a/src/components/common/__tests__/VnDiscount.spec.js +++ b/src/components/common/__tests__/VnDiscount.spec.js @@ -4,7 +4,7 @@ import VnDiscount from 'components/common/vnDiscount.vue'; describe('VnDiscount', () => { let vm; - + beforeAll(() => { vm = createWrapper(VnDiscount, { props: { @@ -12,7 +12,9 @@ describe('VnDiscount', () => { price: 100, quantity: 2, discount: 10, - } + mana: 10, + promise: vi.fn(), + }, }).vm; }); @@ -21,8 +23,8 @@ describe('VnDiscount', () => { }); describe('total', () => { - it('should calculate total correctly', () => { + it('should calculate total correctly', () => { expect(vm.total).toBe(180); }); }); -}); \ No newline at end of file +}); diff --git a/src/components/common/__tests__/VnDms.spec.js b/src/components/common/__tests__/VnDms.spec.js index 66d946db3..86f12169b 100644 --- a/src/components/common/__tests__/VnDms.spec.js +++ b/src/components/common/__tests__/VnDms.spec.js @@ -41,10 +41,12 @@ describe('VnDms', () => { companyFk: 2, dmsTypeFk: 3, description: 'This is a test description', - files: { - name: 'example.txt', - content: new Blob(['file content'], { type: 'text/plain' }), - }, + files: [ + { + name: 'example.txt', + content: new Blob(['file content'], { type: 'text/plain' }), + }, + ], }; const expectedBody = { @@ -83,7 +85,7 @@ describe('VnDms', () => { it('should map DMS data correctly and add file to FormData', () => { const [formData, params] = vm.mapperDms(data); - expect(formData.get('example.txt')).toBe(data.files); + expect([formData.get('example.txt')]).toStrictEqual(data.files); expect(expectedBody).toEqual(params.params); }); diff --git a/src/components/common/__tests__/VnInput.spec.js b/src/components/common/__tests__/VnInput.spec.js index 13f9ed804..8eb2c50c8 100644 --- a/src/components/common/__tests__/VnInput.spec.js +++ b/src/components/common/__tests__/VnInput.spec.js @@ -2,7 +2,6 @@ import { createWrapper } from 'app/test/vitest/helper'; import { vi, describe, expect, it } from 'vitest'; import VnInput from 'src/components/common/VnInput.vue'; - describe('VnInput', () => { let vm; let wrapper; @@ -11,26 +10,28 @@ describe('VnInput', () => { function generateWrapper(value, isOutlined, emptyToNull, insertable) { wrapper = createWrapper(VnInput, { props: { - modelValue: value, - isOutlined, emptyToNull, insertable, - maxlength: 101 + modelValue: value, + isOutlined, + emptyToNull, + insertable, + maxlength: 101, }, attrs: { label: 'test', required: true, maxlength: 101, maxLength: 10, - 'max-length':20 + 'max-length': 20, }, }); wrapper = wrapper.wrapper; vm = wrapper.vm; input = wrapper.find('[data-cy="test_input"]'); - }; + } describe('value', () => { it('should emit update:modelValue when value changes', async () => { - generateWrapper('12345', false, false, true) + generateWrapper('12345', false, false, true); await input.setValue('123'); expect(wrapper.emitted('update:modelValue')).toBeTruthy(); expect(wrapper.emitted('update:modelValue')[0]).toEqual(['123']); @@ -46,37 +47,36 @@ describe('VnInput', () => { describe('styleAttrs', () => { it('should return empty styleAttrs when isOutlined is false', async () => { generateWrapper('123', false, false, false); - expect(vm.styleAttrs).toEqual({}); + expect(vm.styleAttrs).toEqual({}); }); - it('should set styleAttrs when isOutlined is true', async () => { + it('should set styleAttrs when isOutlined is true', async () => { generateWrapper('123', true, false, false); expect(vm.styleAttrs.outlined).toBe(true); }); }); - describe('handleKeydown', () => { + describe('handleKeydown', () => { it('should do nothing when "Backspace" key is pressed', async () => { generateWrapper('12345', false, false, true); await input.trigger('keydown', { key: 'Backspace' }); expect(wrapper.emitted('update:modelValue')).toBeUndefined(); const spyhandler = vi.spyOn(vm, 'handleInsertMode'); expect(spyhandler).not.toHaveBeenCalled(); - }); - + /* TODO: #8399 REDMINE */ it.skip('handleKeydown respects insertable behavior', async () => { const expectedValue = '12345'; generateWrapper('1234', false, false, true); - vm.focus() + vm.focus(); await input.trigger('keydown', { key: '5' }); await vm.$nextTick(); expect(wrapper.emitted('update:modelValue')).toBeTruthy(); - expect(wrapper.emitted('update:modelValue')[0]).toEqual([expectedValue ]); - expect(vm.value).toBe( expectedValue); + expect(wrapper.emitted('update:modelValue')[0]).toEqual([expectedValue]); + expect(vm.value).toBe(expectedValue); }); }); @@ -86,6 +86,6 @@ describe('VnInput', () => { const focusSpy = vi.spyOn(input.element, 'focus'); vm.focus(); expect(focusSpy).toHaveBeenCalled(); - }); + }); }); }); diff --git a/src/components/common/__tests__/VnInputDate.spec.js b/src/components/common/__tests__/VnInputDate.spec.js index 21ca91e96..3cb037c63 100644 --- a/src/components/common/__tests__/VnInputDate.spec.js +++ b/src/components/common/__tests__/VnInputDate.spec.js @@ -5,52 +5,71 @@ import VnInputDate from 'components/common/VnInputDate.vue'; let vm; let wrapper; -function generateWrapper(date, outlined, required) { +function generateWrapper(outlined = false, required = false) { wrapper = createWrapper(VnInputDate, { props: { - modelValue: date, + modelValue: '2000-12-31T23:00:00.000Z', + 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }), }, attrs: { isOutlined: outlined, - required: required + required: required, }, }); wrapper = wrapper.wrapper; vm = wrapper.vm; -}; +} describe('VnInputDate', () => { - - describe('formattedDate', () => { - it('formats a valid date correctly', async () => { - generateWrapper('2023-12-25', false, false); + describe('formattedDate', () => { + it('validateAndCleanInput should remove non-numeric characters', async () => { + generateWrapper(); + vm.validateAndCleanInput('10a/1s2/2dd0a23'); await vm.$nextTick(); - expect(vm.formattedDate).toBe('25/12/2023'); + expect(vm.inputValue).toBe('10/12/2023'); }); - it('updates the model value when a new date is set', async () => { - const input = wrapper.find('input'); - await input.setValue('31/12/2023'); - expect(wrapper.emitted()['update:modelValue']).toBeTruthy(); - expect(wrapper.emitted()['update:modelValue'][0][0]).toBe('2023-12-31T00:00:00.000Z'); + it('manageDate should reverse the date', async () => { + generateWrapper(); + vm.manageDate('10/12/2023'); + await vm.$nextTick(); + expect(vm.inputValue).toBe('2023/12/10'); }); - it('should not update the model value when an invalid date is set', async () => { + it('formatDate should format the date correctly when a valid date is entered with full year', async () => { const input = wrapper.find('input'); - await input.setValue('invalid-date'); - expect(wrapper.emitted()['update:modelValue'][0][0]).toBe('2023-12-31T00:00:00.000Z'); - }); + await input.setValue('25.12/2002'); + await vm.$nextTick(); + await vm.formatDate(); + expect(vm.model).toBe('2002-12-24T23:00:00.000Z'); + }); + + it('should format the date correctly when a valid date is entered with short year', async () => { + const input = wrapper.find('input'); + await input.setValue('31.12-23'); + await vm.$nextTick(); + await vm.formatDate(); + expect(vm.model).toBe('2023-12-30T23:00:00.000Z'); + }); + + it('should format the date correctly when a valid date is entered without year', async () => { + const input = wrapper.find('input'); + await input.setValue('12.03'); + await vm.$nextTick(); + await vm.formatDate(); + expect(vm.model).toBe('2001-03-11T23:00:00.000Z'); + }); }); describe('styleAttrs', () => { it('should return empty styleAttrs when isOutlined is false', async () => { - generateWrapper('2023-12-25', false, false); + generateWrapper(); await vm.$nextTick(); - expect(vm.styleAttrs).toEqual({}); + expect(vm.styleAttrs).toEqual({}); }); - it('should set styleAttrs when isOutlined is true', async () => { - generateWrapper('2023-12-25', true, false); + it('should set styleAttrs when isOutlined is true', async () => { + generateWrapper(true, false); await vm.$nextTick(); expect(vm.styleAttrs.outlined).toBe(true); }); @@ -58,15 +77,15 @@ describe('VnInputDate', () => { describe('required', () => { it('should not applies required class when isRequired is false', async () => { - generateWrapper('2023-12-25', false, false); + generateWrapper(); await vm.$nextTick(); expect(wrapper.find('.vn-input-date').classes()).not.toContain('required'); }); - + it('should applies required class when isRequired is true', async () => { - generateWrapper('2023-12-25', false, true); + generateWrapper(false, true); await vm.$nextTick(); expect(wrapper.find('.vn-input-date').classes()).toContain('required'); }); }); -}); \ No newline at end of file +}); diff --git a/src/components/common/__tests__/VnInputDateTime.spec.js b/src/components/common/__tests__/VnInputDateTime.spec.js new file mode 100644 index 000000000..adff1dbc4 --- /dev/null +++ b/src/components/common/__tests__/VnInputDateTime.spec.js @@ -0,0 +1,81 @@ +import { createWrapper } from 'app/test/vitest/helper.js'; +import { describe, it, expect, beforeAll } from 'vitest'; +import VnInputDateTime from 'components/common/VnInputDateTime.vue'; +import vnDateBoot from 'src/boot/vnDate'; + +let vm; +let wrapper; + +beforeAll(() => { + // Initialize the vnDate boot + vnDateBoot(); +}); +function generateWrapper(date, outlined, showEvent) { + wrapper = createWrapper(VnInputDateTime, { + props: { + modelValue: date, + isOutlined: outlined, + showEvent: showEvent, + }, + }); + wrapper = wrapper.wrapper; + vm = wrapper.vm; +} + +describe('VnInputDateTime', () => { + describe('selectedDate', () => { + it('formats a valid datetime correctly', async () => { + generateWrapper('2023-12-25T10:30:00', false, true); + await vm.$nextTick(); + expect(vm.selectedDate).toBe('25-12-2023 10:30'); + }); + + it('handles null date value', async () => { + generateWrapper(null, false, true); + await vm.$nextTick(); + expect(vm.selectedDate).not.toBe(null); + }); + + it('updates the model value when a new datetime is set', async () => { + vm.selectedDate = '31-12-2023 15:45'; + await vm.$nextTick(); + expect(wrapper.emitted()['update:modelValue']).toBeTruthy(); + }); + }); + + describe('styleAttrs', () => { + it('should return empty styleAttrs when isOutlined is false', async () => { + generateWrapper('2023-12-25T10:30:00', false, true); + await vm.$nextTick(); + expect(vm.styleAttrs).toEqual({}); + }); + + it('should set styleAttrs when isOutlined is true', async () => { + generateWrapper('2023-12-25T10:30:00', true, true); + await vm.$nextTick(); + expect(vm.styleAttrs).toEqual({ + dense: true, + outlined: true, + rounded: true, + }); + }); + }); + + describe('component rendering', () => { + it('should render date and time icons', async () => { + generateWrapper('2023-12-25T10:30:00', false, true); + await vm.$nextTick(); + const icons = wrapper.findAllComponents({ name: 'QIcon' }); + expect(icons.length).toBe(2); + expect(icons[0].props('name')).toBe('today'); + expect(icons[1].props('name')).toBe('access_time'); + }); + + it('should render popup proxies for date and time', async () => { + generateWrapper('2023-12-25T10:30:00', false, true); + await vm.$nextTick(); + const popups = wrapper.findAllComponents({ name: 'QPopupProxy' }); + expect(popups.length).toBe(2); + }); + }); +}); diff --git a/src/components/common/__tests__/VnLog.spec.js b/src/components/common/__tests__/VnLog.spec.js index fcb516cc5..399b78a1d 100644 --- a/src/components/common/__tests__/VnLog.spec.js +++ b/src/components/common/__tests__/VnLog.spec.js @@ -90,8 +90,10 @@ describe('VnLog', () => { vm = createWrapper(VnLog, { global: { - stubs: [], - mocks: {}, + stubs: ['FetchData', 'vue-i18n'], + mocks: { + fetch: vi.fn(), + }, }, propsData: { model: 'Claim', diff --git a/src/components/common/__tests__/VnNotes.spec.js b/src/components/common/__tests__/VnNotes.spec.js index e0514cc7b..0d256a736 100644 --- a/src/components/common/__tests__/VnNotes.spec.js +++ b/src/components/common/__tests__/VnNotes.spec.js @@ -26,7 +26,7 @@ describe('VnNotes', () => { ) { vi.spyOn(axios, 'get').mockResolvedValue({ data: [] }); wrapper = createWrapper(VnNotes, { - propsData: options, + propsData: { ...defaultOptions, ...options }, }); wrapper = wrapper.wrapper; vm = wrapper.vm; diff --git a/src/components/ui/EntityDescriptor.vue b/src/components/ui/EntityDescriptor.vue index a5dced551..751a6bd9c 100644 --- a/src/components/ui/EntityDescriptor.vue +++ b/src/components/ui/EntityDescriptor.vue @@ -58,7 +58,8 @@ async function getData() { store.filter = $props.filter ?? {}; isLoading.value = true; try { - const { data } = await arrayData.fetch({ append: false, updateRouter: false }); + await arrayData.fetch({ append: false, updateRouter: false }); + const { data } = store; state.set($props.dataKey, data); emit('onFetch', data); } finally { diff --git a/src/components/ui/VnConfirm.vue b/src/components/ui/VnConfirm.vue index c6f539879..5714db9b2 100644 --- a/src/components/ui/VnConfirm.vue +++ b/src/components/ui/VnConfirm.vue @@ -89,24 +89,26 @@ function cancel() { - - + + + + diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue index dc9e4e776..6460499b0 100644 --- a/src/components/ui/VnFilterPanel.vue +++ b/src/components/ui/VnFilterPanel.vue @@ -212,6 +212,7 @@ const getLocale = (label) => { color="primary" style="position: fixed; z-index: 1; right: 0; bottom: 0" icon="search" + data-cy="vnFilterPanel_search" @click="search()" > @@ -229,6 +230,7 @@ const getLocale = (label) => { { { beforeEach(() => { wrapper = createWrapper(CardSummary, { + global: { + mocks: { + validate: vi.fn(), + }, + }, propsData: { dataKey: 'cardSummaryKey', url: 'cardSummaryUrl', - filter: 'cardFilter', + filter: { key: 'cardFilter' }, }, }); vm = wrapper.vm; @@ -50,7 +55,7 @@ describe('CardSummary', () => { it('should set correct props to the store', () => { expect(vm.store.url).toEqual('cardSummaryUrl'); - expect(vm.store.filter).toEqual('cardFilter'); + expect(vm.store.filter).toEqual({ key: 'cardFilter' }); }); it('should respond to prop changes and refetch data', async () => { diff --git a/src/components/ui/__tests__/VnSearchbar.spec.js b/src/components/ui/__tests__/VnSearchbar.spec.js index 25649194d..64014e8d8 100644 --- a/src/components/ui/__tests__/VnSearchbar.spec.js +++ b/src/components/ui/__tests__/VnSearchbar.spec.js @@ -7,7 +7,7 @@ describe('VnSearchbar', () => { let wrapper; let applyFilterSpy; const searchText = 'Bolas de madera'; - const userParams = {staticKey: 'staticValue'}; + const userParams = { staticKey: 'staticValue' }; beforeEach(async () => { wrapper = createWrapper(VnSearchbar, { @@ -23,8 +23,9 @@ describe('VnSearchbar', () => { vm.searchText = searchText; vm.arrayData.store.userParams = userParams; - applyFilterSpy = vi.spyOn(vm.arrayData, 'applyFilter').mockImplementation(() => {}); - + applyFilterSpy = vi + .spyOn(vm.arrayData, 'applyFilter') + .mockImplementation(() => {}); }); afterEach(() => { @@ -32,7 +33,9 @@ describe('VnSearchbar', () => { }); it('search resets pagination and applies filter', async () => { - const resetPaginationSpy = vi.spyOn(vm.arrayData, 'resetPagination').mockImplementation(() => {}); + const resetPaginationSpy = vi + .spyOn(vm.arrayData, 'resetPagination') + .mockImplementation(() => {}); await vm.search(); expect(resetPaginationSpy).toHaveBeenCalled(); @@ -48,7 +51,7 @@ describe('VnSearchbar', () => { expect(applyFilterSpy).toHaveBeenCalledWith({ params: { staticKey: 'staticValue', search: searchText }, - filter: {skip: 0}, + filter: { skip: 0 }, }); }); @@ -68,4 +71,4 @@ describe('VnSearchbar', () => { }); expect(vm.to.query.searchParam).toBe(expectedQuery); }); -}); \ No newline at end of file +}); diff --git a/src/components/ui/__tests__/VnSms.spec.js b/src/components/ui/__tests__/VnSms.spec.js index 4f4fd7d49..b71d8ccb0 100644 --- a/src/components/ui/__tests__/VnSms.spec.js +++ b/src/components/ui/__tests__/VnSms.spec.js @@ -1,5 +1,4 @@ import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; -import axios from 'axios'; import { createWrapper } from 'app/test/vitest/helper'; import VnSms from 'src/components/ui/VnSms.vue'; @@ -12,6 +11,9 @@ describe('VnSms', () => { stubs: ['VnPaginate'], mocks: {}, }, + propsData: { + url: 'SmsUrl', + }, }).vm; }); diff --git a/src/composables/__tests__/useArrayData.spec.js b/src/composables/__tests__/useArrayData.spec.js index a3fbbdd5d..74be8ccff 100644 --- a/src/composables/__tests__/useArrayData.spec.js +++ b/src/composables/__tests__/useArrayData.spec.js @@ -4,6 +4,8 @@ import { useArrayData } from 'composables/useArrayData'; import { useRouter } from 'vue-router'; import * as vueRouter from 'vue-router'; import { setActivePinia, createPinia } from 'pinia'; +import { defineComponent, h } from 'vue'; +import { mount } from '@vue/test-utils'; describe('useArrayData', () => { const filter = '{"limit":20,"skip":0}'; @@ -43,7 +45,7 @@ describe('useArrayData', () => { it('should fetch and replace url with new params', async () => { vi.spyOn(axios, 'get').mockResolvedValueOnce({ data: [] }); - const arrayData = useArrayData('ArrayData', { + const arrayData = mountArrayData('ArrayData', { url: 'mockUrl', searchUrl: 'params', }); @@ -72,7 +74,7 @@ describe('useArrayData', () => { data: [{ id: 1 }], }); - const arrayData = useArrayData('ArrayData', { + const arrayData = mountArrayData('ArrayData', { url: 'mockUrl', navigate: {}, }); @@ -94,7 +96,7 @@ describe('useArrayData', () => { ], }); - const arrayData = useArrayData('ArrayData', { + const arrayData = mountArrayData('ArrayData', { url: 'mockUrl', oneRecord: true, }); @@ -107,3 +109,17 @@ describe('useArrayData', () => { }); }); }); + +function mountArrayData(...args) { + let arrayData; + + const TestComponent = defineComponent({ + setup() { + arrayData = useArrayData(...args); + return () => h('div'); + }, + }); + + const asd = mount(TestComponent); + return arrayData; +} diff --git a/src/composables/__tests__/useSession.spec.js b/src/composables/__tests__/useSession.spec.js index e86847b70..eb390e096 100644 --- a/src/composables/__tests__/useSession.spec.js +++ b/src/composables/__tests__/useSession.spec.js @@ -64,88 +64,84 @@ describe('session', () => { }); }); - describe( - 'login', - () => { - const expectedUser = { - id: 999, - name: `T'Challa`, - nickname: 'Black Panther', - lang: 'en', - userConfig: { - darkMode: false, + describe('login', () => { + const expectedUser = { + id: 999, + name: `T'Challa`, + nickname: 'Black Panther', + lang: 'en', + userConfig: { + darkMode: false, + }, + worker: { department: { departmentFk: 155 } }, + }; + const rolesData = [ + { + role: { + name: 'salesPerson', }, - worker: { department: { departmentFk: 155 } }, - }; - const rolesData = [ - { - role: { - name: 'salesPerson', - }, + }, + { + role: { + name: 'admin', }, - { - role: { - name: 'admin', - }, - }, - ]; - beforeEach(() => { - vi.spyOn(axios, 'get').mockImplementation((url) => { - if (url === 'VnUsers/acls') return Promise.resolve({ data: [] }); - return Promise.resolve({ - data: { roles: rolesData, user: expectedUser }, - }); + }, + ]; + beforeEach(() => { + vi.spyOn(axios, 'get').mockImplementation((url) => { + if (url === 'VnUsers/acls') return Promise.resolve({ data: [] }); + return Promise.resolve({ + data: { roles: rolesData, user: expectedUser }, }); }); + }); - it('should fetch the user roles and then set token in the sessionStorage', async () => { - const expectedRoles = ['salesPerson', 'admin']; - const expectedToken = 'mySessionToken'; - const expectedTokenMultimedia = 'mySessionTokenMultimedia'; - const keepLogin = false; + it('should fetch the user roles and then set token in the sessionStorage', async () => { + const expectedRoles = ['salesPerson', 'admin']; + const expectedToken = 'mySessionToken'; + const expectedTokenMultimedia = 'mySessionTokenMultimedia'; + const keepLogin = false; - await session.login({ - token: expectedToken, - tokenMultimedia: expectedTokenMultimedia, - keepLogin, - }); - - const roles = state.getRoles(); - const localToken = localStorage.getItem('token'); - const sessionToken = sessionStorage.getItem('token'); - - expect(roles.value).toEqual(expectedRoles); - expect(localToken).toBeNull(); - expect(sessionToken).toEqual(expectedToken); - - await session.destroy(); // this clears token and user for any other test + await session.login({ + token: expectedToken, + tokenMultimedia: expectedTokenMultimedia, + keepLogin, }); - it('should fetch the user roles and then set token in the localStorage', async () => { - const expectedRoles = ['salesPerson', 'admin']; - const expectedToken = 'myLocalToken'; - const expectedTokenMultimedia = 'myLocalTokenMultimedia'; - const keepLogin = true; + const roles = state.getRoles(); + const localToken = localStorage.getItem('token'); + const sessionToken = sessionStorage.getItem('token'); - await session.login({ - token: expectedToken, - tokenMultimedia: expectedTokenMultimedia, - keepLogin, - }); + expect(roles.value).toEqual(expectedRoles); + expect(localToken).toBeNull(); + expect(sessionToken).toEqual(expectedToken); - const roles = state.getRoles(); - const localToken = localStorage.getItem('token'); - const sessionToken = sessionStorage.getItem('token'); + await session.destroy(); // this clears token and user for any other test + }); - expect(roles.value).toEqual(expectedRoles); - expect(localToken).toEqual(expectedToken); - expect(sessionToken).toBeNull(); + it('should fetch the user roles and then set token in the localStorage', async () => { + const expectedRoles = ['salesPerson', 'admin']; + const expectedToken = 'myLocalToken'; + const expectedTokenMultimedia = 'myLocalTokenMultimedia'; + const keepLogin = true; - await session.destroy(); // this clears token and user for any other test + await session.login({ + token: expectedToken, + tokenMultimedia: expectedTokenMultimedia, + keepLogin, }); - }, - {}, - ); + + const roles = state.getRoles(); + const localToken = localStorage.getItem('token'); + const sessionToken = sessionStorage.getItem('token'); + + expect(roles.value).toEqual(expectedRoles); + expect(localToken).toEqual(expectedToken); + expect(sessionToken).toBeNull(); + + await session.destroy(); // this clears token and user for any other test + }); + }); describe('RenewToken', () => { const expectedToken = 'myToken'; diff --git a/src/composables/getWeekdays.js b/src/composables/getWeekdays.js new file mode 100644 index 000000000..d619103da --- /dev/null +++ b/src/composables/getWeekdays.js @@ -0,0 +1,10 @@ +import { ref } from 'vue'; +import moment from 'moment'; + +export default function useWeekdaysOrder() { + + const firstDay = moment().weekday(1).day(); + const weekdays = [...Array(7).keys()].map(i => (i + firstDay) % 7); + + return ref(weekdays); +} diff --git a/src/composables/useArrayData.js b/src/composables/useArrayData.js index 2e880a16d..9828b35ae 100644 --- a/src/composables/useArrayData.js +++ b/src/composables/useArrayData.js @@ -1,4 +1,4 @@ -import { onMounted, computed } from 'vue'; +import { onMounted, computed, ref } from 'vue'; import { useRouter, useRoute } from 'vue-router'; import axios from 'axios'; import { useArrayDataStore } from 'stores/useArrayDataStore'; @@ -346,7 +346,7 @@ export function useArrayData(key, userOptions) { } const totalRows = computed(() => (store.data && store.data.length) || 0); - const isLoading = computed(() => store.isLoading || false); + const isLoading = ref(store.isLoading || false); return { fetch, diff --git a/src/css/app.scss b/src/css/app.scss index dd5dbe247..351eeb599 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -343,3 +343,20 @@ input::-webkit-inner-spin-button { .q-item__section--main ~ .q-item__section--side { padding-inline: 0; } + +.calendars-header { + height: 45px; + display: flex; + justify-content: space-between; + align-items: center; + background-color: $primary; + font-weight: bold; + font-size: 16px; +} + +.calendars-container { + max-width: 800px; + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; +} \ No newline at end of file diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 4f4d1d5f7..3c1c80954 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -6,6 +6,7 @@ globals: quantity: Quantity entity: Entity preview: Preview + scrollToTop: Go up user: User details: Details collapseMenu: Collapse lateral menu @@ -19,6 +20,7 @@ globals: logOut: Log out date: Date dataSaved: Data saved + openDetail: Open detail dataDeleted: Data deleted delete: Delete search: Search @@ -160,6 +162,9 @@ globals: department: Department noData: No data available vehicle: Vehicle + selectDocumentId: Select document id + document: Document + import: Import from existing pageTitles: logIn: Login addressEdit: Update address @@ -341,6 +346,7 @@ globals: parking: Parking vehicleList: Vehicles vehicle: Vehicle + entryPreAccount: Pre-account unsavedPopup: title: Unsaved changes will be lost subtitle: Are you sure exit without saving? diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 9c808e046..518985831 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -6,6 +6,7 @@ globals: quantity: Cantidad entity: Entidad preview: Vista previa + scrollToTop: Ir arriba user: Usuario details: Detalles collapseMenu: Contraer menú lateral @@ -20,10 +21,11 @@ globals: date: Fecha dataSaved: Datos guardados dataDeleted: Datos eliminados + dataCreated: Datos creados + openDetail: Ver detalle delete: Eliminar search: Buscar changes: Cambios - dataCreated: Datos creados add: Añadir create: Crear edit: Modificar @@ -164,6 +166,9 @@ globals: noData: Datos no disponibles department: Departamento vehicle: Vehículo + selectDocumentId: Seleccione el id de gestión documental + document: Documento + import: Importar desde existente pageTitles: logIn: Inicio de sesión addressEdit: Modificar consignatario @@ -344,6 +349,7 @@ globals: parking: Parking vehicleList: Vehículos vehicle: Vehículo + entryPreAccount: Precontabilizar unsavedPopup: title: Los cambios que no haya guardado se perderán subtitle: ¿Seguro que quiere salir sin guardar? diff --git a/src/pages/Claim/Card/ClaimPhoto.vue b/src/pages/Claim/Card/ClaimPhoto.vue index 4ced7e862..f02038afe 100644 --- a/src/pages/Claim/Card/ClaimPhoto.vue +++ b/src/pages/Claim/Card/ClaimPhoto.vue @@ -23,7 +23,7 @@ const claimDms = ref([ ]); const client = ref({}); const inputFile = ref(); -const files = ref({}); +const files = ref([]); const spinnerRef = ref(); const claimDmsRef = ref(); const dmsType = ref({}); @@ -255,9 +255,8 @@ function onDrag() { icon="add" color="primary" > - { expectedData, { signal: canceller.signal, - } + }, ); }); }); @@ -69,7 +69,7 @@ describe('ClaimLines', () => { expect.objectContaining({ message: 'Discount updated', type: 'positive', - }) + }), ); }); }); diff --git a/src/pages/Claim/Card/__tests__/ClaimLinesImport.spec.js b/src/pages/Claim/Card/__tests__/ClaimLinesImport.spec.js index 2a5176d0a..cec4b1681 100644 --- a/src/pages/Claim/Card/__tests__/ClaimLinesImport.spec.js +++ b/src/pages/Claim/Card/__tests__/ClaimLinesImport.spec.js @@ -14,6 +14,9 @@ describe('ClaimLinesImport', () => { fetch: vi.fn(), }, }, + propsData: { + ticketId: 1, + }, }).vm; }); @@ -40,7 +43,7 @@ describe('ClaimLinesImport', () => { expect.objectContaining({ message: 'Lines added to claim', type: 'positive', - }) + }), ); expect(vm.canceller).toEqual(null); }); diff --git a/src/pages/Claim/Card/__tests__/ClaimPhoto.spec.js b/src/pages/Claim/Card/__tests__/ClaimPhoto.spec.js index b14338b5c..bf3548af3 100644 --- a/src/pages/Claim/Card/__tests__/ClaimPhoto.spec.js +++ b/src/pages/Claim/Card/__tests__/ClaimPhoto.spec.js @@ -41,10 +41,10 @@ describe('ClaimPhoto', () => { await vm.deleteDms({ index: 0 }); expect(axios.post).toHaveBeenCalledWith( - `ClaimDms/${claimMock.claimDms[0].dmsFk}/removeFile` + `ClaimDms/${claimMock.claimDms[0].dmsFk}/removeFile`, ); expect(vm.quasar.notify).toHaveBeenCalledWith( - expect.objectContaining({ type: 'positive' }) + expect.objectContaining({ type: 'positive' }), ); }); }); @@ -63,7 +63,7 @@ describe('ClaimPhoto', () => { data: { index: 1 }, promise: vm.deleteDms, }, - }) + }), ); }); }); @@ -102,10 +102,10 @@ describe('ClaimPhoto', () => { new FormData(), expect.objectContaining({ params: expect.objectContaining({ hasFile: false }), - }) + }), ); expect(vm.quasar.notify).toHaveBeenCalledWith( - expect.objectContaining({ type: 'positive' }) + expect.objectContaining({ type: 'positive' }), ); expect(vm.claimDmsRef.fetch).toHaveBeenCalledOnce(); diff --git a/src/pages/Customer/Card/CustomerAddress.vue b/src/pages/Customer/Card/CustomerAddress.vue index f1799d0cc..418c4ec30 100644 --- a/src/pages/Customer/Card/CustomerAddress.vue +++ b/src/pages/Customer/Card/CustomerAddress.vue @@ -77,10 +77,10 @@ const isDefaultAddress = (address) => { return client?.value?.defaultAddressFk === address.id ? 1 : 0; }; -const setDefault = (address) => { +const setDefault = async (address) => { const url = `Clients/${route.params.id}`; const payload = { defaultAddressFk: address.id }; - axios.patch(url, payload).then((res) => { + await axios.patch(url, payload).then((res) => { if (res.data) { client.value.defaultAddressFk = res.data.defaultAddressFk; sortAddresses(); diff --git a/src/pages/Customer/Card/CustomerBillingData.vue b/src/pages/Customer/Card/CustomerBillingData.vue index e4b6f8365..fdcbf75d4 100644 --- a/src/pages/Customer/Card/CustomerBillingData.vue +++ b/src/pages/Customer/Card/CustomerBillingData.vue @@ -1,5 +1,4 @@ - + { /> - - (data.bankEntityFk = bankEntityFk)" + { + if (!iban || !bankEntityFk) return; + data.iban = iban; + data.bankEntityFk = bankEntityFk; + } + " /> - - - - - - - - {{ scope.opt.bic }} - {{ scope.opt.name }} - - - - - diff --git a/src/pages/Customer/Card/CustomerDescriptor.vue b/src/pages/Customer/Card/CustomerDescriptor.vue index c7461f890..8b4e025a2 100644 --- a/src/pages/Customer/Card/CustomerDescriptor.vue +++ b/src/pages/Customer/Card/CustomerDescriptor.vue @@ -39,7 +39,7 @@ const route = useRoute(); const { t } = useI18n(); const entityId = computed(() => { - return $props.id || route.params.id; + return Number($props.id || route.params.id); }); const data = ref(useCardDescription()); diff --git a/src/pages/Customer/Card/CustomerDescriptorProxy.vue b/src/pages/Customer/Card/CustomerDescriptorProxy.vue index 9f67d02ec..f1e4b42b8 100644 --- a/src/pages/Customer/Card/CustomerDescriptorProxy.vue +++ b/src/pages/Customer/Card/CustomerDescriptorProxy.vue @@ -11,7 +11,7 @@ const $props = defineProps({ - + diff --git a/src/pages/Customer/Card/CustomerFiscalData.vue b/src/pages/Customer/Card/CustomerFiscalData.vue index baa728868..f4efd03b6 100644 --- a/src/pages/Customer/Card/CustomerFiscalData.vue +++ b/src/pages/Customer/Card/CustomerFiscalData.vue @@ -86,7 +86,7 @@ async function acceptPropagate({ isEqualizated }) { :required="true" :rules="validate('client.socialName')" clearable - uppercase="true" + :uppercase="true" v-model="data.socialName" > diff --git a/src/pages/Customer/Card/CustomerSamples.vue b/src/pages/Customer/Card/CustomerSamples.vue index 756ae4667..44fab8e72 100644 --- a/src/pages/Customer/Card/CustomerSamples.vue +++ b/src/pages/Customer/Card/CustomerSamples.vue @@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import { QBtn, useQuasar } from 'quasar'; - import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import { toDateTimeFormat } from 'src/filters/date'; import VnTable from 'src/components/VnTable/VnTable.vue'; @@ -74,12 +73,11 @@ const tableRef = ref(); $props.id || route.params.id); +const entityId = computed(() => Number($props.id || route.params.id)); const customer = computed(() => summary.value.entity); const summary = ref(); const defaulterAmount = computed(() => customer.value.defaulters[0]?.amount); diff --git a/src/pages/Customer/CustomerFilter.vue b/src/pages/Customer/CustomerFilter.vue index c30b11528..48538aa2a 100644 --- a/src/pages/Customer/CustomerFilter.vue +++ b/src/pages/Customer/CustomerFilter.vue @@ -72,7 +72,7 @@ const exprBuilder = (param, value) => { option-value="id" option-label="name" url="Departments" - no-one="true" + :no-one="true" /> diff --git a/src/pages/Customer/Notifications/CustomerNotifications.vue b/src/pages/Customer/Notifications/CustomerNotifications.vue index cbbd6d205..02792182c 100644 --- a/src/pages/Customer/Notifications/CustomerNotifications.vue +++ b/src/pages/Customer/Notifications/CustomerNotifications.vue @@ -100,6 +100,9 @@ const columns = computed(() => [ 'row-key': 'id', selection: 'multiple', }" + :multi-check="{ + expand: true, + }" v-model:selected="selected" :right-search="true" :columns="columns" diff --git a/src/pages/Customer/Notifications/CustomerNotificationsCampaignConsumption.vue b/src/pages/Customer/Notifications/CustomerNotificationsCampaignConsumption.vue index f637c7e0a..141a02bfc 100644 --- a/src/pages/Customer/Notifications/CustomerNotificationsCampaignConsumption.vue +++ b/src/pages/Customer/Notifications/CustomerNotificationsCampaignConsumption.vue @@ -98,7 +98,9 @@ onMounted(async () => { - {{ t('Campaign consumption') }} + {{ + t('Campaign consumption', { rows: $props.clients.length }) + }} { valentinesDay: Valentine's Day mothersDay: Mother's Day allSaints: All Saints' Day + Campaign consumption: Campaign consumption ({rows}) es: params: valentinesDay: Día de San Valentín mothersDay: Día de la Madre allSaints: Día de Todos los Santos - Campaign consumption: Consumo campaña + Campaign consumption: Consumo campaña ({rows}) Campaign: Campaña From: Desde To: Hasta diff --git a/src/pages/Customer/Payments/__tests__/CustomerPayments.spec.js b/src/pages/Customer/Payments/__tests__/CustomerPayments.spec.js index a9c845cec..238545050 100644 --- a/src/pages/Customer/Payments/__tests__/CustomerPayments.spec.js +++ b/src/pages/Customer/Payments/__tests__/CustomerPayments.spec.js @@ -32,7 +32,7 @@ describe('CustomerPayments', () => { expect.objectContaining({ message: 'Payment confirmed', type: 'positive', - }) + }), ); }); }); diff --git a/src/pages/Customer/components/CustomerSamplesCreate.vue b/src/pages/Customer/components/CustomerSamplesCreate.vue index 1294a5d25..dfa944748 100644 --- a/src/pages/Customer/components/CustomerSamplesCreate.vue +++ b/src/pages/Customer/components/CustomerSamplesCreate.vue @@ -41,7 +41,6 @@ const sampleType = ref({ hasPreview: false }); const initialData = reactive({}); const entityId = computed(() => route.params.id); const customer = computed(() => useArrayData('Customer').store?.data); -const filterEmailUsers = { where: { userFk: user.value.id } }; const filterClientsAddresses = { include: [ { relation: 'province', scope: { fields: ['name'] } }, @@ -73,7 +72,7 @@ onBeforeMount(async () => { const setEmailUser = (data) => { optionsEmailUsers.value = data; - initialData.replyTo = data[0]?.email; + initialData.replyTo = data[0]?.notificationEmail; }; const setClientsAddresses = (data) => { @@ -182,10 +181,12 @@ const toCustomerSamples = () => { +import { ref, computed, markRaw, useTemplateRef, onBeforeMount, watch } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { toDate, toCurrency } from 'src/filters'; +import { useArrayData } from 'src/composables/useArrayData'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import FetchData from 'src/components/FetchData.vue'; +import VnSelectSupplier from 'src/components/common/VnSelectSupplier.vue'; +import EntryDescriptorProxy from './Card/EntryDescriptorProxy.vue'; +import SupplierDescriptorProxy from '../Supplier/Card/SupplierDescriptorProxy.vue'; +import VnInputNumber from 'src/components/common/VnInputNumber.vue'; +import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; +import axios from 'axios'; +import useNotify from 'src/composables/useNotify'; +import VnConfirm from 'src/components/ui/VnConfirm.vue'; +import VnDms from 'src/components/common/VnDms.vue'; +import { useState } from 'src/composables/useState'; +import { useQuasar } from 'quasar'; +import InvoiceInDescriptorProxy from '../InvoiceIn/Card/InvoiceInDescriptorProxy.vue'; +import { useStateStore } from 'src/stores/useStateStore'; +import { downloadFile } from 'src/composables/downloadFile'; + +const { t } = useI18n(); +const quasar = useQuasar(); +const { notify } = useNotify(); +const user = useState().getUser(); +const stateStore = useStateStore(); +const updateDialog = ref(); +const uploadDialog = ref(); +let maxDays; +let defaultDays; +const dataKey = 'entryPreaccountingFilter'; +const url = 'Entries/preAccountingFilter'; +const arrayData = useArrayData(dataKey); +const daysAgo = ref(); +const isBooked = ref(); +const dmsData = ref(); +const table = useTemplateRef('table'); +const companies = ref([]); +const countries = ref([]); +const entryTypes = ref([]); +const supplierFiscalTypes = ref([]); +const warehouses = ref([]); +const defaultDmsDescription = ref(); +const dmsTypeId = ref(); +const selectedRows = ref([]); +const totalAmount = ref(); +const totalSelectedAmount = computed(() => { + if (!selectedRows.value.length) return 0; + return selectedRows.value.reduce((acc, entry) => acc + entry.amount, 0); +}); +let supplierRef; +let dmsFk; +const columns = computed(() => [ + { + name: 'id', + label: t('entry.preAccount.id'), + isId: true, + chip: { + condition: () => true, + }, + }, + { + name: 'invoiceNumber', + label: t('entry.preAccount.invoiceNumber'), + }, + { + name: 'company', + label: t('globals.company'), + columnFilter: { + component: 'select', + name: 'companyFk', + optionLabel: 'code', + options: companies.value, + }, + }, + { + name: 'warehouse', + label: t('globals.warehouse'), + columnFilter: { + component: 'select', + name: 'warehouseInFk', + options: warehouses.value, + }, + }, + { + name: 'gestDocFk', + label: t('entry.preAccount.gestDocFk'), + }, + { + name: 'dmsType', + label: t('entry.preAccount.dmsType'), + columnFilter: { + component: 'select', + label: null, + name: 'dmsTypeFk', + url: 'DmsTypes', + fields: ['id', 'name'], + }, + }, + { + name: 'reference', + label: t('entry.preAccount.reference'), + }, + { + name: 'shipped', + label: t('entry.preAccount.shipped'), + format: ({ shipped }, dashIfEmpty) => dashIfEmpty(toDate(shipped)), + columnFilter: { + component: 'date', + name: 'shipped', + }, + }, + { + name: 'landed', + label: t('entry.preAccount.landed'), + format: ({ landed }, dashIfEmpty) => dashIfEmpty(toDate(landed)), + columnFilter: { + component: 'date', + name: 'landed', + }, + }, + { + name: 'invoiceInFk', + label: t('entry.preAccount.invoiceInFk'), + }, + { + name: 'supplier', + label: t('globals.supplier'), + format: (row) => row.supplier, + columnFilter: { + component: markRaw(VnSelectSupplier), + label: null, + name: 'supplierFk', + class: 'fit', + event: 'update', + }, + }, + { + name: 'country', + label: t('globals.country'), + columnFilter: { + component: 'select', + name: 'countryFk', + options: countries.value, + }, + }, + { + name: 'description', + label: t('entry.preAccount.entryType'), + columnFilter: { + component: 'select', + label: null, + name: 'typeFk', + options: entryTypes.value, + optionLabel: 'description', + optionValue: 'code', + }, + }, + { + name: 'payDem', + label: t('entry.preAccount.payDem'), + columnFilter: { + component: 'number', + name: 'payDem', + }, + }, + { + name: 'fiscalCode', + label: t('entry.preAccount.fiscalCode'), + format: ({ fiscalCode }) => t(fiscalCode), + columnFilter: { + component: 'select', + name: 'fiscalCode', + options: supplierFiscalTypes.value, + optionLabel: 'locale', + optionValue: 'code', + sortBy: 'code', + }, + }, + { + name: 'amount', + label: t('globals.amount'), + format: ({ amount }) => toCurrency(amount), + columnFilter: { + component: 'number', + name: 'amount', + }, + }, + { + name: 'isAgricultural', + label: t('entry.preAccount.isAgricultural'), + component: 'checkbox', + isEditable: false, + }, + { + name: 'isBooked', + label: t('entry.preAccount.isBooked'), + component: 'checkbox', + }, + { + name: 'isReceived', + label: t('entry.preAccount.isReceived'), + component: 'checkbox', + isEditable: false, + }, +]); + +onBeforeMount(async () => { + const { data } = await axios.get('EntryConfigs/findOne', { + params: { filter: JSON.stringify({ fields: ['maxDays', 'defaultDays'] }) }, + }); + maxDays = data.maxDays; + defaultDays = data.defaultDays; + daysAgo.value = arrayData.store.userParams.daysAgo || defaultDays; + isBooked.value = arrayData.store.userParams.isBooked || false; + stateStore.leftDrawer = false; +}); + +watch(selectedRows, (nVal, oVal) => { + const lastRow = nVal.at(-1); + if (lastRow?.isBooked) selectedRows.value.pop(); + if (nVal.length > oVal.length && lastRow.invoiceInFk) + quasar.dialog({ + component: VnConfirm, + componentProps: { title: t('entry.preAccount.hasInvoice') }, + }); +}); + +function filterByDaysAgo(val) { + if (!val) val = defaultDays; + else if (val > maxDays) val = maxDays; + daysAgo.value = val; + arrayData.store.userParams.daysAgo = daysAgo.value; + table.value.reload(); +} + +async function preAccount() { + const entries = selectedRows.value; + const { companyFk, isAgricultural, landed } = entries.at(0); + try { + dmsFk = entries.find(({ gestDocFk }) => gestDocFk)?.gestDocFk; + if (isAgricultural) { + const year = new Date(landed).getFullYear(); + supplierRef = ( + await axios.get('InvoiceIns/getMaxRef', { params: { companyFk, year } }) + ).data; + return createInvoice(); + } else if (dmsFk) { + supplierRef = ( + await axios.get(`Dms/${dmsFk}`, { + params: { filter: JSON.stringify({ fields: ['reference'] }) }, + }) + ).data?.reference; + updateDialog.value.show(); + } else { + uploadFile(); + } + } catch (e) { + throw e; + } +} + +async function updateFile() { + await axios.post(`Dms/${dmsFk}/updateFile`, { dmsTypeId: dmsTypeId.value }); + await createInvoice(); +} + +async function uploadFile() { + const firstSelectedEntry = selectedRows.value.at(0); + const { supplier, companyFk, invoiceNumber } = firstSelectedEntry; + dmsData.value = { + dmsTypeFk: dmsTypeId.value, + warehouseFk: user.value.warehouseFk, + companyFk: companyFk, + description: supplier + defaultDmsDescription.value + invoiceNumber, + hasFile: false, + }; + uploadDialog.value.show(); +} + +async function afterUploadFile({ reference }, res) { + supplierRef = reference; + dmsFk = res.data[0].id; + await createInvoice(); +} + +async function createInvoice() { + try { + await axios.post(`Entries/addInvoiceIn`, { + ids: selectedRows.value.map((entry) => entry.id), + supplierRef, + dmsFk, + }); + notify(t('entry.preAccount.success'), 'positive'); + } catch (e) { + throw e; + } finally { + supplierRef = null; + dmsFk = undefined; + selectedRows.value.length = 0; + table.value.reload(); + } +} + + + (countries = data)" + auto-load + /> + (companies = data)" + auto-load + /> + (warehouses = data)" + auto-load + /> + (entryTypes = data)" + auto-load + /> + + (supplierFiscalTypes = data.map((x) => ({ locale: t(x.code), ...x }))) + " + auto-load + /> + (defaultDmsDescription = data?.defaultDmsDescription)" + auto-load + /> + (dmsTypeId = data?.id)" + auto-load + /> + + (totalAmount = data?.reduce((acc, entry) => acc + entry.amount, 0)) + " + auto-load + > + + + {{ t('entry.preAccount.btn') }} + + + + + + {{ row.id }} + + + + + + + + + {{ row.gestDocFk }} + + + + + {{ row.supplier }} + + + + + + {{ row.invoiceInFk }} + + + + + + + + + + + + + + + + + + + + +en: + IntraCommunity: Intra-community + NonCommunity: Non-community + CanaryIslands: Canary Islands +es: + IntraCommunity: Intracomunitaria + NonCommunity: Extracomunitaria + CanaryIslands: Islas Canarias + National: Nacional + diff --git a/src/pages/Entry/__tests__/EntryPreAccount.spec.js b/src/pages/Entry/__tests__/EntryPreAccount.spec.js new file mode 100644 index 000000000..0140a5f1e --- /dev/null +++ b/src/pages/Entry/__tests__/EntryPreAccount.spec.js @@ -0,0 +1,63 @@ +import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; +import { createWrapper } from 'app/test/vitest/helper'; +import EntryPreAccount from '../EntryPreAccount.vue'; +import axios from 'axios'; + +describe('EntryPreAccount', () => { + let wrapper; + let vm; + + beforeAll(() => { + vi.spyOn(axios, 'get').mockImplementation((url) => { + if (url == 'EntryConfigs/findOne') + return { data: { maxDays: 90, defaultDays: 30 } }; + return { data: [] }; + }); + wrapper = createWrapper(EntryPreAccount); + vm = wrapper.vm; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('filterByDaysAgo()', () => { + it('should set daysAgo to defaultDays if no value is provided', () => { + vm.filterByDaysAgo(); + expect(vm.daysAgo).toBe(vm.defaultDays); + expect(vm.arrayData.store.userParams.daysAgo).toBe(vm.defaultDays); + }); + + it('should set daysAgo to maxDays if the value exceeds maxDays', () => { + vm.filterByDaysAgo(500); + expect(vm.daysAgo).toBe(vm.maxDays); + expect(vm.arrayData.store.userParams.daysAgo).toBe(vm.maxDays); + }); + + it('should set daysAgo to the provided value if it is valid', () => { + vm.filterByDaysAgo(30); + expect(vm.daysAgo).toBe(30); + expect(vm.arrayData.store.userParams.daysAgo).toBe(30); + }); + }); + + describe('Dialog behavior when adding a new row', () => { + it('should open the dialog if the new row has invoiceInFk', async () => { + const dialogSpy = vi.spyOn(vm.quasar, 'dialog'); + const selectedRows = [{ id: 1, invoiceInFk: 123 }]; + vm.selectedRows = selectedRows; + await vm.$nextTick(); + expect(dialogSpy).toHaveBeenCalledWith({ + component: vm.VnConfirm, + componentProps: { title: vm.t('entry.preAccount.hasInvoice') }, + }); + }); + + it('should not open the dialog if the new row does not have invoiceInFk', async () => { + const dialogSpy = vi.spyOn(vm.quasar, 'dialog'); + vm.selectedRows = [{ id: 1, invoiceInFk: null }]; + await vm.$nextTick(); + expect(dialogSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/pages/Entry/locale/en.yml b/src/pages/Entry/locale/en.yml index 0bc92a5ea..3c0f078fe 100644 --- a/src/pages/Entry/locale/en.yml +++ b/src/pages/Entry/locale/en.yml @@ -118,6 +118,33 @@ entry: searchInfo: You can search by entry reference descriptorMenu: showEntryReport: Show entry report + preAccount: + gestDocFk: Gestdoc + dmsType: Gestdoc type + invoiceNumber: Entry ref. + reference: Gestdoc ref. + shipped: Shipped + landed: Landed + id: Entry + invoiceInFk: Invoice in + supplierFk: Supplier + country: Country + description: Entry type + payDem: Payment term + isBooked: B + isReceived: R + entryType: Entry type + isAgricultural: Agricultural + fiscalCode: Account type + daysAgo: Max 365 days + search: Search + searchInfo: You can search by supplier name or nickname + btn: Pre-account + hasInvoice: This entry has already an invoice in + success: It has been successfully pre-accounted + dialog: + title: Pre-account entries + message: Do you want the invoice to inherit the entry document? entryFilter: params: isExcludedFromAvailable: Excluded from available diff --git a/src/pages/Entry/locale/es.yml b/src/pages/Entry/locale/es.yml index 2c80299bc..0addbca94 100644 --- a/src/pages/Entry/locale/es.yml +++ b/src/pages/Entry/locale/es.yml @@ -69,6 +69,33 @@ entry: observationType: Tipo de observación search: Buscar entradas searchInfo: Puedes buscar por referencia de entrada + preAccount: + gestDocFk: Gestdoc + dmsType: Tipo gestdoc + invoiceNumber: Ref. Entrada + reference: Ref. GestDoc + shipped: F. envío + landed: F. llegada + id: Entrada + invoiceInFk: Recibida + supplierFk: Proveedor + country: País + description: Tipo de Entrada + payDem: Plazo de pago + isBooked: C + isReceived: R + entryType: Tipo de entrada + isAgricultural: Agricultural + fiscalCode: Tipo de cuenta + daysAgo: Máximo 365 días + search: Buscar + searchInfo: Puedes buscar por nombre o alias de proveedor + btn: Precontabilizar + hasInvoice: Esta entrada ya tiene una f. recibida + success: Se ha precontabilizado correctamente + dialog: + title: Precontabilizar entradas + message: ¿Desea que la factura herede el documento de la entrada? params: entryFk: Entrada observationTypeFk: Tipo de observación diff --git a/src/pages/InvoiceIn/Card/InvoiceInDescriptorProxy.vue b/src/pages/InvoiceIn/Card/InvoiceInDescriptorProxy.vue index e9ca762ed..2c8cab84f 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDescriptorProxy.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDescriptorProxy.vue @@ -5,7 +5,7 @@ import InvoiceInSummary from './InvoiceInSummary.vue'; const $props = defineProps({ id: { type: Number, - required: true, + default: null, }, }); diff --git a/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue b/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue index 53433c56b..6d6dd2f51 100644 --- a/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue +++ b/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue @@ -164,6 +164,7 @@ onMounted(async () => { unelevated filled dense + data-cy="formSubmitBtn" /> { filled dense @click="getStatus = 'stopping'" + data-cy="formStopBtn" /> diff --git a/src/pages/Item/Card/ItemTags.vue b/src/pages/Item/Card/ItemTags.vue index ab26b9cae..87d42e25b 100644 --- a/src/pages/Item/Card/ItemTags.vue +++ b/src/pages/Item/Card/ItemTags.vue @@ -89,7 +89,6 @@ const insertTag = (rows) => { :default-remove="false" :user-filter="{ fields: ['id', 'itemFk', 'tagFk', 'value', 'priority'], - where: { itemFk: route.params.id }, include: { relation: 'tag', scope: { @@ -97,6 +96,7 @@ const insertTag = (rows) => { }, }, }" + :filter="{ where: { itemFk: route.params.id } }" order="priority" auto-load @on-fetch="onItemTagsFetched" diff --git a/src/pages/Item/components/ItemProposal.vue b/src/pages/Item/components/ItemProposal.vue index d2dbea7b3..bd0fdc0c2 100644 --- a/src/pages/Item/components/ItemProposal.vue +++ b/src/pages/Item/components/ItemProposal.vue @@ -6,10 +6,12 @@ import { toCurrency } from 'filters/index'; import VnStockValueDisplay from 'src/components/ui/VnStockValueDisplay.vue'; import VnTable from 'src/components/VnTable/VnTable.vue'; import axios from 'axios'; -import notifyResults from 'src/utils/notifyResults'; +import { displayResults } from 'src/pages/Ticket/Negative/composables/notifyResults'; import FetchData from 'components/FetchData.vue'; +import { useState } from 'src/composables/useState'; const MATCH = 'match'; +const { notifyResults } = displayResults(); const { t } = useI18n(); const $props = defineProps({ @@ -18,14 +20,20 @@ const $props = defineProps({ required: true, default: () => {}, }, + filter: { + type: Object, + required: true, + default: () => {}, + }, replaceAction: { type: Boolean, - required: false, + required: true, default: false, }, + sales: { type: Array, - required: false, + required: true, default: () => [], }, }); @@ -36,6 +44,8 @@ const proposalTableRef = ref(null); const sale = computed(() => $props.sales[0]); const saleFk = computed(() => sale.value.saleFk); const filter = computed(() => ({ + where: $props.filter, + itemFk: $props.itemLack.itemFk, sales: saleFk.value, })); @@ -228,11 +238,15 @@ async function handleTicketConfig(data) { url="TicketConfigs" :filter="{ fields: ['lackAlertPrice'] }" @on-fetch="handleTicketConfig" - auto-load + > + import ItemProposal from './ItemProposal.vue'; import { useDialogPluginComponent } from 'quasar'; - const $props = defineProps({ itemLack: { type: Object, required: true, default: () => {}, }, + filter: { + type: Object, + required: true, + default: () => {}, + }, replaceAction: { type: Boolean, required: false, @@ -31,7 +35,7 @@ defineExpose({ show: () => dialogRef.value.show(), hide: () => dialogRef.value.h - {{ $t('Item proposal') }} + {{ $t('itemProposal') }} diff --git a/src/pages/Item/composables/cloneItem.js b/src/pages/Item/composables/cloneItem.js index 2421c0808..4e19661ca 100644 --- a/src/pages/Item/composables/cloneItem.js +++ b/src/pages/Item/composables/cloneItem.js @@ -11,26 +11,19 @@ export function cloneItem() { const router = useRouter(); const cloneItem = async (entityId) => { const { id } = entityId; - try { - const { data } = await axios.post(`Items/${id ?? entityId}/clone`); - router.push({ name: 'ItemTags', params: { id: data.id } }); - } catch (err) { - console.error('Error cloning item'); - } + const { data } = await axios.post(`Items/${id ?? entityId}/clone`); + router.push({ name: 'ItemTags', params: { id: data.id } }); }; const openCloneDialog = async (entityId) => { - quasar - .dialog({ - component: VnConfirm, - componentProps: { - title: t('item.descriptor.clone.title'), - message: t('item.descriptor.clone.subTitle'), - }, - }) - .onOk(async () => { - await cloneItem(entityId); - }); + quasar.dialog({ + component: VnConfirm, + componentProps: { + title: t('item.descriptor.clone.title'), + message: t('item.descriptor.clone.subTitle'), + promise: () => cloneItem(entityId), + }, + }); }; return { openCloneDialog }; } diff --git a/src/pages/Monitor/MonitorClientsActions.vue b/src/pages/Monitor/MonitorClientsActions.vue index 821773bbf..a6ac3ab0b 100644 --- a/src/pages/Monitor/MonitorClientsActions.vue +++ b/src/pages/Monitor/MonitorClientsActions.vue @@ -8,14 +8,14 @@ import VnRow from 'src/components/ui/VnRow.vue'; class="q-pa-md" :style="{ 'flex-direction': $q.screen.lt.lg ? 'column' : 'row', gap: '0px' }" > - + - + useOpenURL(`#/order/${id}/summary`); - - - {{ toDateFormat(row.date_send) }} - - + diff --git a/src/pages/Monitor/Ticket/MonitorTicketFilter.vue b/src/pages/Monitor/Ticket/MonitorTicketFilter.vue index 1cadd4cb4..1bc194a5c 100644 --- a/src/pages/Monitor/Ticket/MonitorTicketFilter.vue +++ b/src/pages/Monitor/Ticket/MonitorTicketFilter.vue @@ -9,6 +9,7 @@ import VnInput from 'src/components/common/VnInput.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; import FetchData from 'src/components/FetchData.vue'; import { dateRange } from 'src/filters'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; defineProps({ dataKey: { type: String, required: true } }); const { t, te } = useI18n(); @@ -209,7 +210,7 @@ const getLocale = (label) => { - { - { - import { ref, computed, onMounted } from 'vue'; import { useI18n } from 'vue-i18n'; -import FetchData from 'components/FetchData.vue'; import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; @@ -168,9 +167,11 @@ const columns = computed(() => [ component: 'select', name: 'provinceFk', attrs: { - options: provinceOpts.value, - 'option-value': 'id', - 'option-label': 'name', + url: 'Provinces', + fields: ['id', 'name'], + sortBy: ['name ASC'], + optionValue: 'id', + optionLabel: 'name', dense: true, }, }, @@ -183,9 +184,11 @@ const columns = computed(() => [ component: 'select', name: 'stateFk', attrs: { - options: stateOpts.value, - 'option-value': 'id', - 'option-label': 'name', + sortBy: ['name ASC'], + url: 'States', + fields: ['id', 'name'], + optionValue: 'id', + optionLabel: 'name', dense: true, }, }, @@ -212,9 +215,12 @@ const columns = computed(() => [ component: 'select', name: 'zoneFk', attrs: { - options: zoneOpts.value, - 'option-value': 'id', - 'option-label': 'name', + url: 'Zones', + fields: ['id', 'name'], + sortBy: ['name ASC'], + + optionValue: 'id', + optionLabel: 'name', dense: true, }, }, @@ -225,11 +231,12 @@ const columns = computed(() => [ align: 'left', columnFilter: { component: 'select', - url: 'PayMethods', attrs: { - options: PayMethodOpts.value, - optionValue: 'id', + url: 'PayMethods', + fields: ['id', 'name'], + sortBy: ['id ASC'], optionLabel: 'name', + optionValue: 'id', dense: true, }, }, @@ -254,7 +261,9 @@ const columns = computed(() => [ columnFilter: { component: 'select', attrs: { - options: DepartmentOpts.value, + url: 'Departments', + fields: ['id', 'name'], + sortBy: ['id ASC'], dense: true, }, }, @@ -265,11 +274,12 @@ const columns = computed(() => [ align: 'left', columnFilter: { component: 'select', - url: 'ItemPackingTypes', attrs: { - options: ItemPackingTypeOpts.value, - 'option-value': 'code', - 'option-label': 'code', + url: 'ItemPackingTypes', + fields: ['code'], + sortBy: ['code ASC'], + optionValue: 'code', + optionCode: 'code', dense: true, }, }, @@ -324,60 +334,6 @@ const totalPriceColor = (ticket) => { const openTab = (id) => useOpenURL(`#/ticket/${id}/sale`); - (provinceOpts = data)" - /> - (stateOpts = data)" - /> - (zoneOpts = data)" - /> - (ItemPackingTypeOpts = data)" - /> - (DepartmentOpts = data)" - /> - (PayMethodOpts = data)" - /> diff --git a/src/pages/Order/Card/OrderCatalog.vue b/src/pages/Order/Card/OrderCatalog.vue index dbb66c0ec..df39fff3c 100644 --- a/src/pages/Order/Card/OrderCatalog.vue +++ b/src/pages/Order/Card/OrderCatalog.vue @@ -120,7 +120,6 @@ watch( :data-key="dataKey" :tag-value="tagValue" :tags="tags" - :initial-catalog-params="catalogParams" :arrayData /> diff --git a/src/pages/Order/Card/OrderDescriptor.vue b/src/pages/Order/Card/OrderDescriptor.vue index ee66bb57e..434dbb038 100644 --- a/src/pages/Order/Card/OrderDescriptor.vue +++ b/src/pages/Order/Card/OrderDescriptor.vue @@ -27,7 +27,7 @@ const getTotalRef = ref(); const total = ref(0); const entityId = computed(() => { - return $props.id || route.params.id; + return Number($props.id || route.params.id); }); const orderTotal = computed(() => state.get('orderTotal') ?? 0); diff --git a/src/pages/Route/Card/RouteSummary.vue b/src/pages/Route/Card/RouteSummary.vue index 6c1f51625..fa1d2e4a6 100644 --- a/src/pages/Route/Card/RouteSummary.vue +++ b/src/pages/Route/Card/RouteSummary.vue @@ -247,10 +247,10 @@ const ticketColumns = ref([ - - + + - {{ value }} + {{ row.clientFk }} diff --git a/src/pages/Route/Cmr/CmrList.vue b/src/pages/Route/Cmr/CmrList.vue index 170f73bc0..98e1bda02 100644 --- a/src/pages/Route/Cmr/CmrList.vue +++ b/src/pages/Route/Cmr/CmrList.vue @@ -6,13 +6,18 @@ import { useRoute } from 'vue-router'; import { useSession } from 'src/composables/useSession'; import { toDateHourMin } from 'filters/index'; import { useStateStore } from 'src/stores/useStateStore'; +import { dashIfEmpty } from 'src/filters'; +import AgencyDescriptorProxy from '../Agency/Card/AgencyDescriptorProxy.vue'; +import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; +import RouteDescriptorProxy from '../Card/RouteDescriptorProxy.vue'; +import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue'; -import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnTable from 'components/VnTable/VnTable.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; +import VnInput from 'src/components/common/VnInput.vue'; const route = useRoute(); const { t } = useI18n(); @@ -30,39 +35,117 @@ const userParams = { const columns = computed(() => [ { - align: 'left', + align: 'right', name: 'cmrFk', - label: t('route.cmr.params.cmrFk'), + label: t('cmr.params.cmrFk'), chip: { condition: () => true, }, isId: true, }, { - align: 'center', - name: 'hasCmrDms', - label: t('route.cmr.params.hasCmrDms'), - component: 'checkbox', - cardVisible: true, - }, - { - align: 'left', - label: t('route.cmr.params.ticketFk'), + align: 'right', + label: t('cmr.params.ticketFk'), name: 'ticketFk', }, { - align: 'left', - label: t('route.cmr.params.routeFk'), + align: 'right', + label: t('cmr.params.routeFk'), name: 'routeFk', }, { - align: 'left', - label: t('route.cmr.params.clientFk'), + label: t('cmr.params.client'), name: 'clientFk', + component: 'select', + attrs: { + url: 'Clients', + fields: ['id', 'name'], + }, + columnFilter: { + name: 'clientFk', + attrs: { + url: 'Clients', + fields: ['id', 'name'], + }, + }, }, { - align: 'right', - label: t('route.cmr.params.countryFk'), + label: t('cmr.params.agency'), + name: 'agencyModeFk', + component: 'select', + attrs: { + url: 'Agencies', + fields: ['id', 'name'], + }, + columnFilter: { + name: 'agencyModeFk', + attrs: { + url: 'Agencies', + fields: ['id', 'name'], + }, + }, + format: ({ agencyName }) => agencyName, + }, + { + label: t('cmr.params.supplier'), + name: 'supplierFk', + component: 'select', + attrs: { + url: 'suppliers', + fields: ['id', 'name'], + }, + columnFilter: { + name: 'supplierFk', + attrs: { + url: 'suppliers', + fields: ['id', 'name'], + }, + }, + }, + { + label: t('cmr.params.sender'), + name: 'addressFromFk', + component: 'select', + attrs: { + url: 'Addresses', + fields: ['id', 'nickname'], + optionValue: 'id', + optionLabel: 'nickname', + }, + columnFilter: { + name: 'addressFromFk', + attrs: { + url: 'Addresses', + fields: ['id', 'nickname'], + optionValue: 'id', + optionLabel: 'nickname', + }, + }, + format: ({ origin }) => origin, + }, + { + label: t('cmr.params.destination'), + name: 'addressToFk', + component: 'select', + attrs: { + url: 'addresses', + fields: ['id', 'nickname'], + optionValue: 'id', + optionLabel: 'nickname', + }, + columnFilter: { + name: 'addressToFk', + attrs: { + url: 'addresses', + fields: ['id', 'nickname'], + optionValue: 'id', + optionLabel: 'nickname', + }, + }, + format: ({ destination }) => destination, + }, + { + label: t('cmr.params.country'), name: 'countryFk', component: 'select', attrs: { @@ -79,16 +162,61 @@ const columns = computed(() => [ format: ({ countryName }) => countryName, }, { - align: 'right', - label: t('route.cmr.params.shipped'), - name: 'shipped', - cardVisible: true, + label: t('cmr.params.created'), + name: 'created', component: 'date', - format: ({ shipped }) => toDateHourMin(shipped), + format: ({ created }) => dashIfEmpty(toDateHourMin(created)), }, { - align: 'right', - label: t('route.cmr.params.warehouseFk'), + label: t('cmr.params.shipped'), + name: 'shipped', + component: 'date', + format: ({ shipped }) => dashIfEmpty(toDateHourMin(shipped)), + }, + { + label: t('cmr.params.etd'), + name: 'ead', + component: 'date', + format: ({ ead }) => dashIfEmpty(toDateHourMin(ead)), + toolTip: t('cmr.params.etdTooltip'), + }, + { + label: t('globals.landed'), + name: 'landed', + component: 'date', + format: ({ landed }) => dashIfEmpty(toDateHourMin(landed)), + }, + { + align: 'left', + label: t('cmr.params.packageList'), + name: 'packagesList', + columnFilter: false, + }, + { + align: 'left', + label: t('cmr.params.observation'), + name: 'observation', + columnFilter: false, + }, + { + align: 'left', + label: t('cmr.params.senderInstructions'), + name: 'senderInstruccions', + columnFilter: false, + }, + { + align: 'left', + label: t('cmr.params.paymentInstructions'), + name: 'paymentInstruccions', + columnFilter: false, + }, + { + align: 'left', + label: t('cmr.params.vehiclePlate'), + name: 'truckPlate', + }, + { + label: t('cmr.params.warehouse'), name: 'warehouseFk', component: 'select', attrs: { @@ -96,7 +224,6 @@ const columns = computed(() => [ fields: ['id', 'name'], }, columnFilter: { - inWhere: true, name: 'warehouseFk', attrs: { url: 'warehouses', @@ -105,12 +232,23 @@ const columns = computed(() => [ }, format: ({ warehouseName }) => warehouseName, }, + { + align: 'left', + name: 'specialAgreements', + label: t('cmr.params.specialAgreements'), + columnFilter: false, + }, + { + name: 'hasCmrDms', + label: t('cmr.params.hasCmrDms'), + component: 'checkbox', + }, { align: 'center', name: 'tableActions', actions: [ { - title: t('route.cmr.params.viewCmr'), + title: t('cmr.params.viewCmr'), icon: 'visibility', isPrimary: true, action: (row) => window.open(getCmrUrl(row?.cmrFk), '_blank'), @@ -151,11 +289,7 @@ function downloadPdfs() { } - + - {{ t('route.cmr.params.downloadCmrs') }} + {{ t('cmr.params.downloadCmrs') }} @@ -191,11 +325,72 @@ function downloadPdfs() { + + + {{ row.routeFk }} + + + - {{ row.clientFk }} + {{ row.clientName }} + + + {{ row.agencyName }} + + + + + + {{ row.carrierName }} + + + + + + + + + {{ row.packagesList }} + + {{ row.packagesList }} + + + + + + {{ row.senderInstruccions }} + + {{ row.senderInstruccions }} + + + + + + {{ row.paymentInstruccions }} + + {{ row.paymentInstruccions }} + + + + + + {{ row.specialAgreements }} + + {{ row.specialAgreements }} + + + diff --git a/src/pages/Route/Cmr/locale/en.yml b/src/pages/Route/Cmr/locale/en.yml new file mode 100644 index 000000000..49b9895f8 --- /dev/null +++ b/src/pages/Route/Cmr/locale/en.yml @@ -0,0 +1,31 @@ +cmr: + search: Search Cmr + searchInfo: You can search Cmr by Id + params: + agency: Agency + client: Client + cmrFk: CMR id + country: Country + created: Created + destination: Destination + downloadCmrs: Download CMRs + etd: ETD + etdTooltip: Estimated Time Delivery + hasCmrDms: Attached in gestdoc + observation: Observation + packageList: Package List + paymentInstructions: Payment instructions + routeFk: Route id + results: results + search: General search + sender: Sender + senderInstructions: Sender instructions + shipped: Shipped + specialAgreements: Special agreements + supplier: Carrier + ticketFk: Ticket id + vehiclePlate: Vehicle plate + viewCmr: View CMR + warehouse: Warehouse + 'true': 'Yes' + 'false': 'No' \ No newline at end of file diff --git a/src/pages/Route/Cmr/locale/es.yml b/src/pages/Route/Cmr/locale/es.yml new file mode 100644 index 000000000..b419a238b --- /dev/null +++ b/src/pages/Route/Cmr/locale/es.yml @@ -0,0 +1,31 @@ +cmr: + search: Buscar Cmr + searchInfo: Puedes buscar cmr por id + params: + agency: Agencia + client: Cliente + cmrFk: Id cmr + country: País + created: Creado + destination: Destinatario + downloadCmrs: Descargar CMRs + etd: ETD + etdTooltip: Fecha estimada de entrega + hasCmrDms: Adjunto en gestdoc + observation: Observaciones + packageList: Listado embalajes + paymentInstructions: Instrucciones de pago + routeFk: Id ruta + results: Resultados + search: Busqueda general + sender: Remitente + senderInstructions: Instrucciones de envío + shipped: F. envío + specialAgreements: Acuerdos especiales + supplier: Transportista + ticketFk: Id ticket + vehiclePlate: Matrícula + viewCmr: Ver CMR + warehouse: Almacén + 'true': 'Si' + 'false': 'No' \ No newline at end of file diff --git a/src/pages/Route/Vehicle/Card/VehicleDmsImportForm.vue b/src/pages/Route/Vehicle/Card/VehicleDmsImportForm.vue new file mode 100644 index 000000000..ade3e6dc5 --- /dev/null +++ b/src/pages/Route/Vehicle/Card/VehicleDmsImportForm.vue @@ -0,0 +1,65 @@ + + + + (dmsOptions = data)" + /> + + + + + + diff --git a/src/pages/Route/Vehicle/Card/VehicleEventInclusionForm.vue b/src/pages/Route/Vehicle/Card/VehicleEventInclusionForm.vue new file mode 100644 index 000000000..06141736c --- /dev/null +++ b/src/pages/Route/Vehicle/Card/VehicleEventInclusionForm.vue @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + deleteVehicleEvent(), + ) + " + /> + + + + + + + es: + Started: Inicio + Finished: Fin + Add vehicle event: Agregar evento + Edit vehicle event: Editar evento + diff --git a/src/pages/Route/Vehicle/Card/VehicleEvents.vue b/src/pages/Route/Vehicle/Card/VehicleEvents.vue new file mode 100644 index 000000000..5a90c9586 --- /dev/null +++ b/src/pages/Route/Vehicle/Card/VehicleEvents.vue @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + {{ t('eventsInclusionForm.addEvent') }} + + + + diff --git a/src/pages/Route/Vehicle/Card/VehicleEventsPanel.vue b/src/pages/Route/Vehicle/Card/VehicleEventsPanel.vue new file mode 100644 index 000000000..e3f22e93b --- /dev/null +++ b/src/pages/Route/Vehicle/Card/VehicleEventsPanel.vue @@ -0,0 +1,196 @@ + + + + + + {{ + t('eventsPanel.events') + }} + + + + + ({{ toDateFormat(event.started) }} - + {{ toDateFormat(event.finished) }}) + + {{ t('globals.description') }}: + {{ + dashIfEmpty(event.description) + }} + + {{ t('globals.state') }}: + {{ + getVehicleStateName(event.vehicleStateFk).state + }} + + + + deleteEvent(event.id), + ) + " + > + {{ t('eventsPanel.delete') }} + + + + + {{ t('globals.noResults') }} + + + + + + diff --git a/src/pages/Route/Vehicle/VehicleCalendar.vue b/src/pages/Route/Vehicle/VehicleCalendar.vue new file mode 100644 index 000000000..681bb966a --- /dev/null +++ b/src/pages/Route/Vehicle/VehicleCalendar.vue @@ -0,0 +1,13 @@ + + + + emit('onDateSelected', e)" + /> + \ No newline at end of file diff --git a/src/pages/Route/Vehicle/VehicleCalendarGrid.vue b/src/pages/Route/Vehicle/VehicleCalendarGrid.vue new file mode 100644 index 000000000..99b6602c8 --- /dev/null +++ b/src/pages/Route/Vehicle/VehicleCalendarGrid.vue @@ -0,0 +1,97 @@ + + + + + \ No newline at end of file diff --git a/src/pages/Route/Vehicle/VehicleDms.vue b/src/pages/Route/Vehicle/VehicleDms.vue new file mode 100644 index 000000000..61f608d6c --- /dev/null +++ b/src/pages/Route/Vehicle/VehicleDms.vue @@ -0,0 +1,42 @@ + + + + + + + + + + {{ t('globals.import') }} + + + + diff --git a/src/pages/Route/Vehicle/locale/en.yml b/src/pages/Route/Vehicle/locale/en.yml index c92022f9d..aa1c6d185 100644 --- a/src/pages/Route/Vehicle/locale/en.yml +++ b/src/pages/Route/Vehicle/locale/en.yml @@ -15,6 +15,10 @@ vehicle: remove: Vehicle removed search: Search Vehicle searchInfo: Search by id or number plate + deleteTitle: This item will be deleted + deleteSubtitle: Are you sure you want to continue? params: vehicleTypeFk: Type vehicleStateFk: State + errors: + documentIdEmpty: The document identifier can't be empty diff --git a/src/pages/Route/Vehicle/locale/es.yml b/src/pages/Route/Vehicle/locale/es.yml index c878f97ac..463784c55 100644 --- a/src/pages/Route/Vehicle/locale/es.yml +++ b/src/pages/Route/Vehicle/locale/es.yml @@ -15,6 +15,10 @@ vehicle: remove: Vehículo eliminado search: Buscar Vehículo searchInfo: Buscar por id o matrícula + deleteTitle: Este elemento será eliminado + deleteSubtitle: ¿Seguro que quieres continuar? params: vehicleTypeFk: Tipo vehicleStateFk: Estado + errors: + documentIdEmpty: El número de documento no puede estar vacío diff --git a/src/pages/Route/locale/en.yml b/src/pages/Route/locale/en.yml index b9e31effa..089fd1788 100644 --- a/src/pages/Route/locale/en.yml +++ b/src/pages/Route/locale/en.yml @@ -78,6 +78,11 @@ route: agencyModeName: Agency route isOwn: Own isAnyVolumeAllowed: Any volume allowed + created: Created + addressFromFk: Sender + addressToFk: Destination + landed: Landed + ead: EAD Worker: Worker Agency: Agency Vehicle: Vehicle diff --git a/src/pages/Route/locale/es.yml b/src/pages/Route/locale/es.yml index a2acf26e5..60fbfa310 100644 --- a/src/pages/Route/locale/es.yml +++ b/src/pages/Route/locale/es.yml @@ -72,11 +72,16 @@ route: routeFk: Id ruta clientFk: Id cliente countryFk: Pais - shipped: Fecha preparación + shipped: F. envío agencyModeName: Agencia Ruta agencyAgreement: Agencia Acuerdo isOwn: Propio isAnyVolumeAllowed: Cualquier volumen + created: Creado + addressFromFk: Remitente + addressToFk: Destinatario + landed: F. entrega + ead: ETD Worker: Trabajador Agency: Agencia Vehicle: Vehículo diff --git a/src/pages/Shelving/Parking/ParkingList.vue b/src/pages/Shelving/Parking/ParkingList.vue index 7c5058a74..eb5be5747 100644 --- a/src/pages/Shelving/Parking/ParkingList.vue +++ b/src/pages/Shelving/Parking/ParkingList.vue @@ -80,7 +80,7 @@ const columns = computed(() => [ { { - await bankEntitiesRef.value.fetch(); - rowData.bankEntityFk = dataSaved.id; -}; - const onChangesSaved = async () => { if (supplier.value.payMethodFk !== wireTransferFk.value) quasar @@ -56,25 +50,6 @@ const setWireTransfer = async () => { await axios.patch(`Suppliers/${route.params.id}`, params); notify('globals.dataSaved', 'positive'); }; - -function findBankFk(value, row) { - row.bankEntityFk = null; - if (!value) return; - - const bankEntityFk = bankEntitiesOptions.value.find((b) => b.id == value.slice(4, 8)); - if (bankEntityFk) row.bankEntityFk = bankEntityFk.id; -} - -function bankEntityFilter(val, update) { - update(() => { - const needle = val.toLowerCase(); - filteredBankEntitiesOptions.value = bankEntitiesOptions.value.filter( - (bank) => - bank.bic.toLowerCase().startsWith(needle) || - bank.name.toLowerCase().includes(needle), - ); - }); -} { - (bankEntitiesOptions = data), (filteredBankEntitiesOptions = data); + bankEntitiesOptions = data; + filteredBankEntitiesOptions = data; } " auto-load @@ -119,49 +95,16 @@ function bankEntityFilter(val, update) { :key="index" class="row q-gutter-md q-mb-md" > - findBankFk(value, row)" - :required="true" - > - - - {{ t('components.iban_tooltip') }} - - - - bankEntityFilter(val, update)" - option-label="bic" - option-value="id" - hide-selected - :required="true" - :roles-allowed-to-create="['financial']" - > - - - onBankEntityCreated(requestResponse, row) - " - /> - - - - - {{ scope.opt.bic }} - {{ scope.opt.name }} - - - - + { + row.iban = iban; + row.bankEntityFk = bankEntityFk; + } + " + /> diff --git a/src/pages/Supplier/Card/SupplierFiscalData.vue b/src/pages/Supplier/Card/SupplierFiscalData.vue index 4293bd41a..2feb0e39a 100644 --- a/src/pages/Supplier/Card/SupplierFiscalData.vue +++ b/src/pages/Supplier/Card/SupplierFiscalData.vue @@ -11,10 +11,10 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import VnLocation from 'src/components/common/VnLocation.vue'; import VnAccountNumber from 'src/components/common/VnAccountNumber.vue'; import VnCheckbox from 'src/components/common/VnCheckbox.vue'; - +import { useArrayData } from 'src/composables/useArrayData'; const route = useRoute(); const { t } = useI18n(); - +const arrayData = useArrayData('Supplier'); const sageTaxTypesOptions = ref([]); const sageWithholdingsOptions = ref([]); const sageTransactionTypesOptions = ref([]); @@ -89,6 +89,7 @@ function handleLocation(data, location) { }" auto-load :clear-store-on-unmount="false" + @on-data-saved="arrayData.fetch({})" > diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue index 61932468c..d157916ac 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue @@ -25,7 +25,9 @@ const { validate } = useValidator(); const { notify } = useNotify(); const router = useRouter(); const { t } = useI18n(); -const canEditZone = useAcl().hasAcl('Ticket', 'editZone', 'WRITE'); +const canEditZone = useAcl().hasAny([ + { model: 'Ticket', props: 'editZone', accessType: 'WRITE' }, +]); const agencyFetchRef = ref(); const warehousesOptions = ref([]); @@ -75,8 +77,15 @@ async function getDate(query, params) { if (!data) return notify(t('basicData.noDeliveryZoneAvailable'), 'negative'); formData.value.zoneFk = data.zoneFk; - if (data.landed) formData.value.landed = data.landed; - if (data.shipped) formData.value.shipped = data.shipped; + formData.value.landed = data.landed; + const shippedDate = new Date(params.shipped); + const landedDate = new Date(data.landed); + shippedDate.setHours( + landedDate.getHours(), + landedDate.getMinutes(), + landedDate.getSeconds(), + ); + formData.value.shipped = shippedDate.toISOString(); } const onChangeZone = async (zoneId) => { @@ -125,6 +134,7 @@ const addressId = computed({ formData.value.addressFk = val; onChangeAddress(val); getShipped({ + shipped: formData.value?.shipped, landed: formData.value?.landed, addressFk: val, agencyModeFk: formData.value?.agencyModeFk, @@ -239,6 +249,9 @@ async function getZone(options) { (warehousesOptions = data)" + :where="{ + isForTicket: true, + }" auto-load /> - { t('basicData.negativesConfirmMessage'), submitWithNegatives, ); - else submit(); + else await submit(); } }; diff --git a/src/pages/Ticket/Card/TicketDescriptorMenu.vue b/src/pages/Ticket/Card/TicketDescriptorMenu.vue index f7389b592..30024fb26 100644 --- a/src/pages/Ticket/Card/TicketDescriptorMenu.vue +++ b/src/pages/Ticket/Card/TicketDescriptorMenu.vue @@ -28,6 +28,7 @@ const props = defineProps({ onMounted(() => { restoreTicket(); + hasDocuware(); }); watch( diff --git a/src/pages/Ticket/Card/TicketDescriptorProxy.vue b/src/pages/Ticket/Card/TicketDescriptorProxy.vue index 583ba35e7..8b872733d 100644 --- a/src/pages/Ticket/Card/TicketDescriptorProxy.vue +++ b/src/pages/Ticket/Card/TicketDescriptorProxy.vue @@ -1,7 +1,6 @@ - + diff --git a/src/pages/Ticket/Card/TicketDmsImportForm.vue b/src/pages/Ticket/Card/TicketDmsImportForm.vue index 4b6b9c6cd..04cb3d75e 100644 --- a/src/pages/Ticket/Card/TicketDmsImportForm.vue +++ b/src/pages/Ticket/Card/TicketDmsImportForm.vue @@ -34,7 +34,7 @@ const importDms = async () => { dmsId.value = null; emit('onDataSaved'); } catch (e) { - throw new Error(e.message); + throw e; } }; @@ -49,7 +49,7 @@ const importDms = async () => { @@ -70,7 +70,6 @@ const importDms = async () => { es: - Select document id: Introduzca id de gestion documental Document: Documento The document indentifier can't be empty: El número de documento no puede estar vacío diff --git a/src/pages/Ticket/Card/TicketNotes.vue b/src/pages/Ticket/Card/TicketNotes.vue index a3e25d63e..f2b474156 100644 --- a/src/pages/Ticket/Card/TicketNotes.vue +++ b/src/pages/Ticket/Card/TicketNotes.vue @@ -55,73 +55,75 @@ async function handleSave(e) { auto-load url="ObservationTypes" /> - - - - - - - - + + + + + - - {{ t('ticketNotes.removeNote') }} - - - - - - - {{ t('ticketNotes.addNote') }} - - - - - - + + + + + {{ t('ticketNotes.removeNote') }} + + + + + + + {{ t('ticketNotes.addNote') }} + + + + + + + diff --git a/src/pages/Ticket/Card/TicketPackage.vue b/src/pages/Ticket/Card/TicketPackage.vue index 5fbf4c800..ff55bf9d4 100644 --- a/src/pages/Ticket/Card/TicketPackage.vue +++ b/src/pages/Ticket/Card/TicketPackage.vue @@ -49,88 +49,95 @@ watch( (listPackagingsOptions = data)" auto-load - :filter="{ fields: ['packagingFk', 'name'], order: 'name ASC' }" url="Packagings/listPackaging" + :filter="{ + fields: ['packagingFk', 'name'], + order: ['name ASC'], + }" /> - - - - - - + + + + + - - - - - {{ scope.opt?.name }} - - - #{{ scope.opt?.itemFk }} - - - - - - - - - - {{ t('package.removePackage') }} - - - - - - - {{ t('package.addPackage') }} - - - - - - + + + + + + {{ scope.opt?.name }} + + + #{{ scope.opt?.itemFk }} + + + + + + + + + + {{ t('package.removePackage') }} + + + + + + + {{ t('package.addPackage') }} + + + + + + + diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index 421085142..4975afffb 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -773,6 +773,7 @@ watch( v-model="row.itemFk" :use-like="false" @update:model-value="changeItem(row)" + autofocus > diff --git a/src/pages/Ticket/Card/TicketSplit.vue b/src/pages/Ticket/Card/TicketSplit.vue index e79057266..462c22264 100644 --- a/src/pages/Ticket/Card/TicketSplit.vue +++ b/src/pages/Ticket/Card/TicketSplit.vue @@ -3,7 +3,9 @@ import { ref } from 'vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import split from './components/split'; -const emit = defineEmits(['ticketTransfered']); +import { displayResults } from 'src/pages/Ticket/Negative/composables/notifyResults'; +const { notifyResults } = displayResults(); +const emit = defineEmits(['ticketTransferred']); const $props = defineProps({ ticket: { @@ -16,13 +18,20 @@ const splitDate = ref(Date.vnNew()); const splitSelectedRows = async () => { const tickets = Array.isArray($props.ticket) ? $props.ticket : [$props.ticket]; - await split(tickets, splitDate.value); - emit('ticketTransfered', tickets); + const results = await split(tickets, splitDate.value); + notifyResults(results, 'ticketFk'); + emit('ticketTransferred', tickets); }; - + diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue index 8d7c4a165..c8e2f1646 100644 --- a/src/pages/Zone/ZoneList.vue +++ b/src/pages/Zone/ZoneList.vue @@ -93,6 +93,7 @@ const columns = computed(() => [ optionLabel: 'name', optionValue: 'id', }, + columnClass: 'expand', }, { align: 'left', @@ -188,16 +189,14 @@ const exprBuilder = (param, value) => { return { code: { like: `%${value}%` }, }; + case 'id': + case 'price': case 'agencyModeFk': return { - agencyModeFk: value, + [param]: value, }; case 'search': return /^\d+$/.test(value) ? { id: value } : { name: { like: `%${value}%` } }; - case 'price': - return { - price: value, - }; } }; @@ -247,74 +246,70 @@ const closeEventForm = () => { - - - - - {{ dashIfEmpty(formatRow(row)) }} - - - - - - - - - - - - - + + + {{ dashIfEmpty(formatRow(row)) }} + + + + + + + + + + + @@ -333,24 +328,6 @@ const closeEventForm = () => { /> - - - es: Search zone: Buscar zona diff --git a/src/router/modules/entry.js b/src/router/modules/entry.js index 02eea8c6c..da380313b 100644 --- a/src/router/modules/entry.js +++ b/src/router/modules/entry.js @@ -85,6 +85,7 @@ export default { 'EntryLatestBuys', 'EntryStockBought', 'EntryWasteRecalc', + 'EntryPreAccount', ], }, component: RouterView, @@ -94,6 +95,7 @@ export default { name: 'EntryMain', path: '', component: () => import('src/components/common/VnModule.vue'), + props: (route) => ({ leftDrawer: route.name !== 'EntryPreAccount' }), redirect: { name: 'EntryIndexMain' }, children: [ { @@ -150,6 +152,15 @@ export default { }, component: () => import('src/pages/Entry/EntryWasteRecalc.vue'), }, + { + path: 'pre-account', + name: 'EntryPreAccount', + meta: { + title: 'entryPreAccount', + icon: 'account_balance', + }, + component: () => import('src/pages/Entry/EntryPreAccount.vue'), + }, ], }, ], diff --git a/src/router/modules/route.js b/src/router/modules/route.js index 0dd41c86e..08ba1701f 100644 --- a/src/router/modules/route.js +++ b/src/router/modules/route.js @@ -166,7 +166,12 @@ const vehicleCard = { component: () => import('src/pages/Route/Vehicle/Card/VehicleCard.vue'), redirect: { name: 'VehicleSummary' }, meta: { - menu: ['VehicleBasicData', 'VehicleNotes'], + menu: [ + 'VehicleBasicData', + 'VehicleNotes', + 'VehicleDms', + 'VehicleEvents' + ], }, children: [ { @@ -195,7 +200,25 @@ const vehicleCard = { icon: 'vn:notes', }, component: () => import('src/pages/Route/Vehicle/Card/VehicleNotes.vue'), - } + }, + { + name: 'VehicleDms', + path: 'dms', + meta: { + title: 'dms', + icon: 'cloud_upload', + }, + component: () => import('src/pages/Route/Vehicle/VehicleDms.vue'), + }, + { + name: 'VehicleEvents', + path: 'events', + meta: { + title: 'calendar', + icon: 'vn:calendar', + }, + component: () => import('src/pages/Route/Vehicle/Card/VehicleEvents.vue'), + }, ], }; diff --git a/src/router/modules/ticket.js b/src/router/modules/ticket.js index d80997257..b6b9f71a2 100644 --- a/src/router/modules/ticket.js +++ b/src/router/modules/ticket.js @@ -251,7 +251,7 @@ export default { }, { name: 'NegativeDetail', - path: ':id', + path: ':itemFk', meta: { title: 'summary', icon: 'launch', diff --git a/src/stores/__tests__/useStateQueryStore.spec.js b/src/stores/__tests__/useStateQueryStore.spec.js index ab3afb007..7bdb87ced 100644 --- a/src/stores/__tests__/useStateQueryStore.spec.js +++ b/src/stores/__tests__/useStateQueryStore.spec.js @@ -1,22 +1,23 @@ import { describe, expect, it, beforeEach, beforeAll } from 'vitest'; -import { createWrapper } from 'app/test/vitest/helper'; +import { setActivePinia, createPinia } from 'pinia'; import { useStateQueryStore } from 'src/stores/useStateQueryStore'; describe('useStateQueryStore', () => { - beforeAll(() => { - createWrapper({}, {}); - }); - - const stateQueryStore = useStateQueryStore(); - const { add, isLoading, remove, reset } = useStateQueryStore(); + let stateQueryStore; + let add, isLoading, remove, reset; const firstQuery = { url: 'myQuery' }; function getQueries() { return stateQueryStore.queries; } + beforeAll(() => { + setActivePinia(createPinia()); + }); beforeEach(() => { + stateQueryStore = useStateQueryStore(); + ({ add, isLoading, remove, reset } = useStateQueryStore()); reset(); expect(getQueries().size).toBeFalsy(); }); diff --git a/src/utils/notifyResults.js b/src/utils/notifyResults.js deleted file mode 100644 index e87ad6c6f..000000000 --- a/src/utils/notifyResults.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Notify } from 'quasar'; - -export default function (results, key) { - results.forEach((result, index) => { - if (result.status === 'fulfilled') { - const data = JSON.parse(result.value.config.data); - Notify.create({ - type: 'positive', - message: `Operación (${index + 1}) ${data[key]} completada con éxito.`, - }); - } else { - const data = JSON.parse(result.reason.config.data); - Notify.create({ - type: 'negative', - message: `Operación (${index + 1}) ${data[key]} fallida: ${result.reason.message}`, - }); - } - }); -} diff --git a/test/cypress/docker/find/find.js b/test/cypress/docker/find/find.js index 4f8063c86..9570dafaa 100644 --- a/test/cypress/docker/find/find.js +++ b/test/cypress/docker/find/find.js @@ -6,6 +6,9 @@ const FINDED_PATHS = ['src', E2E_PATH]; function getGitDiff(options) { const TARGET_BRANCH = options[2] || 'dev'; + execSync(`git fetch origin ${TARGET_BRANCH}`, { + encoding: 'utf-8', + }); const diff = execSync(`git diff --name-only origin/${TARGET_BRANCH}`, { encoding: 'utf-8', }); diff --git a/test/cypress/integration/claim/claimAction.spec.js b/test/cypress/integration/claim/claimAction.spec.js index 6e916451c..674313a5a 100644 --- a/test/cypress/integration/claim/claimAction.spec.js +++ b/test/cypress/integration/claim/claimAction.spec.js @@ -1,5 +1,5 @@ /// -describe.skip('ClaimAction', () => { +describe('ClaimAction', () => { const claimId = 1; const firstRow = 'tbody > :nth-child(1)'; @@ -15,13 +15,13 @@ describe.skip('ClaimAction', () => { }); // https://redmine.verdnatura.es/issues/8756 - xit('should change destination', () => { + it.skip('should change destination', () => { const rowData = [true, null, null, 'Bueno']; cy.fillRow(firstRow, rowData); }); // https://redmine.verdnatura.es/issues/8756 - xit('should change destination from other button', () => { + it.skip('should change destination from other button', () => { const rowData = [true]; cy.fillRow(firstRow, rowData); @@ -35,7 +35,7 @@ describe.skip('ClaimAction', () => { }); // https://redmine.verdnatura.es/issues/8756 - xit('should remove the line', () => { + it.skip('should remove the line', () => { cy.fillRow(firstRow, [true]); cy.removeCard(); cy.clickConfirm(); diff --git a/test/cypress/integration/claim/claimDevelopment.spec.js b/test/cypress/integration/claim/claimDevelopment.spec.js index 1fb77fe20..ed1e7c0a5 100755 --- a/test/cypress/integration/claim/claimDevelopment.spec.js +++ b/test/cypress/integration/claim/claimDevelopment.spec.js @@ -1,5 +1,5 @@ /// -describe.skip('ClaimDevelopment', () => { +describe('ClaimDevelopment', () => { const claimId = 1; const firstLineReason = 'tbody > :nth-child(1) > :nth-child(2)'; const thirdRow = 'tbody > :nth-child(3)'; diff --git a/test/cypress/integration/customer/clientList.spec.js b/test/cypress/integration/customer/clientList.spec.js index caf94b8bd..7b1da6d89 100644 --- a/test/cypress/integration/customer/clientList.spec.js +++ b/test/cypress/integration/customer/clientList.spec.js @@ -1,5 +1,5 @@ /// -describe.skip('Client list', () => { +describe('Client list', () => { beforeEach(() => { cy.login('developer'); cy.visit('/#/customer/list', { diff --git a/test/cypress/integration/entry/commands.js b/test/cypress/integration/entry/commands.js index 4d4a8f980..87e3c3bfa 100644 --- a/test/cypress/integration/entry/commands.js +++ b/test/cypress/integration/entry/commands.js @@ -7,7 +7,7 @@ Cypress.Commands.add('selectTravel', (warehouse = '1') => { }); Cypress.Commands.add('deleteEntry', () => { - cy.get('[data-cy="descriptor-more-opts"]').should('be.visible').click(); + cy.dataCy('descriptor-more-opts').should('be.visible').click(); cy.waitForElement('div[data-cy="delete-entry"]').click(); }); diff --git a/test/cypress/integration/entry/entryCard/entryBasicData.spec.js b/test/cypress/integration/entry/entryCard/entryBasicData.spec.js index de8bc6bc9..11643c566 100644 --- a/test/cypress/integration/entry/entryCard/entryBasicData.spec.js +++ b/test/cypress/integration/entry/entryCard/entryBasicData.spec.js @@ -1,6 +1,6 @@ import '../commands.js'; -describe('EntryBasicData', () => { +describe.skip('EntryBasicData', () => { beforeEach(() => { cy.login('buyer'); cy.visit(`/#/entry/list`); diff --git a/test/cypress/integration/entry/entryCard/entryDescriptor.spec.js b/test/cypress/integration/entry/entryCard/entryDescriptor.spec.js index d6f2b2543..2e121064c 100644 --- a/test/cypress/integration/entry/entryCard/entryDescriptor.spec.js +++ b/test/cypress/integration/entry/entryCard/entryDescriptor.spec.js @@ -1,5 +1,5 @@ import '../commands.js'; -describe('EntryDescriptor', () => { +describe.skip('EntryDescriptor', () => { beforeEach(() => { cy.login('buyer'); cy.visit(`/#/entry/list`); diff --git a/test/cypress/integration/entry/entryPreAccount.spec.js b/test/cypress/integration/entry/entryPreAccount.spec.js new file mode 100644 index 000000000..59fa4ee45 --- /dev/null +++ b/test/cypress/integration/entry/entryPreAccount.spec.js @@ -0,0 +1,48 @@ +/// +describe('Entry PreAccount Functionality', () => { + beforeEach(() => { + cy.login('administrative'); + cy.visit('/#/entry/pre-account'); + }); + + it("should pre-account without questions if it's agricultural", () => { + selectRowsByCol('id', [2]); + cy.dataCy('preAccount_btn').click(); + cy.checkNotification('It has been successfully pre-accounted'); + }); + + it("should ask to upload a doc. if it's not agricultural and doesn't have doc. ", () => { + selectRowsByCol('id', [3]); + cy.dataCy('preAccount_btn').click(); + cy.dataCy('Reference_input').type('{selectall}234343fh', { delay: 0 }); + cy.dataCy('VnDms_inputFile').selectFile('test/cypress/fixtures/image.jpg', { + force: true, + }); + cy.dataCy('FormModelPopup_save').click(); + cy.checkNotification('It has been successfully pre-accounted'); + }); + + it('should ask to inherit the doc. and open VnDms popup if user choose "no"', () => { + selectRowsByCol('id', [101]); + cy.dataCy('preAccount_btn').click(); + cy.dataCy('updateFileNo').click(); + cy.get('#formModel').should('be.visible'); + }); + + it('should ask to inherit the doc. and open VnDms popup if user choose "yes" and pre-account', () => { + selectRowsByCol('id', [101]); + cy.dataCy('preAccount_btn').click(); + cy.dataCy('updateFileYes').click(); + cy.checkNotification('It has been successfully pre-accounted'); + }); +}); + +function selectRowsByCol(col = 'id', vals = []) { + for (const val of vals) { + const regex = new RegExp(`^\\s*(${val})\\s*$`); + cy.contains(`[data-col-field="${col}"]`, regex) + .parent() + .find('td > .q-checkbox') + .click(); + } +} diff --git a/test/cypress/integration/entry/entryWasteRecalc.spec.js b/test/cypress/integration/entry/entryWasteRecalc.spec.js index bd50e9c19..902d5bbc5 100644 --- a/test/cypress/integration/entry/entryWasteRecalc.spec.js +++ b/test/cypress/integration/entry/entryWasteRecalc.spec.js @@ -10,7 +10,7 @@ describe('EntryDms', () => { cy.dataCy('recalc').should('be.disabled'); cy.dataCy('dateFrom').should('be.visible').click().type('01-01-2001'); - cy.dataCy('dateTo').should('be.visible').click().type('01-01-2001'); + cy.dataCy('dateTo').should('be.visible').click().type('01-01-2001{enter}'); cy.dataCy('recalc').should('be.enabled').click(); cy.get('.q-notification__message').should( diff --git a/test/cypress/integration/invoiceIn/invoiceInCorrective.spec.js b/test/cypress/integration/invoiceIn/invoiceInCorrective.spec.js index 275fa1358..495e4d43b 100644 --- a/test/cypress/integration/invoiceIn/invoiceInCorrective.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInCorrective.spec.js @@ -53,7 +53,7 @@ describe('invoiceInCorrective', () => { it('should show/hide the section if it is a corrective invoice', () => { cy.visit('/#/invoice-in/1/summary'); cy.get('[data-cy="InvoiceInCorrective-menu-item"]').should('not.exist'); - cy.clicDescriptorAction(4); + cy.clickDescriptorAction(4); cy.get('[data-cy="InvoiceInCorrective-menu-item"]').should('exist'); }); }); diff --git a/test/cypress/integration/invoiceIn/invoiceInDescriptor.spec.js b/test/cypress/integration/invoiceIn/invoiceInDescriptor.spec.js index 9744486e0..fd6f1c238 100644 --- a/test/cypress/integration/invoiceIn/invoiceInDescriptor.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInDescriptor.spec.js @@ -64,17 +64,17 @@ describe('InvoiceInDescriptor', () => { beforeEach(() => cy.visit('/#/invoice-in/1/summary')); it('should navigate to the supplier summary', () => { - cy.clicDescriptorAction(1); + cy.clickDescriptorAction(1); cy.url().should('to.match', /supplier\/\d+\/summary/); }); it('should navigate to the entry summary', () => { - cy.clicDescriptorAction(2); + cy.clickDescriptorAction(2); cy.url().should('to.match', /entry\/\d+\/summary/); }); it('should navigate to the invoiceIn list', () => { - cy.clicDescriptorAction(3); + cy.clickDescriptorAction(3); cy.url().should('to.match', /invoice-in\/list\?table=\{.*supplierFk.+\}/); }); }); @@ -84,7 +84,7 @@ describe('InvoiceInDescriptor', () => { beforeEach(() => cy.visit(`/#/invoice-in/${originalId}/summary`)); - it('should create a correcting invoice and redirect to original invoice', () => { + it.skip('should create a correcting invoice and redirect to original invoice', () => { createCorrective(); redirect(originalId); }); @@ -93,7 +93,7 @@ describe('InvoiceInDescriptor', () => { createCorrective(); redirect(originalId); - cy.clicDescriptorAction(4); + cy.clickDescriptorAction(4); cy.validateVnTableRows({ cols: [ { @@ -141,7 +141,7 @@ function createCorrective() { function redirect(subtitle) { const regex = new RegExp(`InvoiceIns/${subtitle}\\?filter=.*`); cy.intercept('GET', regex).as('getOriginal'); - cy.clicDescriptorAction(4); + cy.clickDescriptorAction(4); cy.wait('@getOriginal'); cy.validateDescriptor({ subtitle }); } diff --git a/test/cypress/integration/invoiceOut/invoiceOutList.spec.js b/test/cypress/integration/invoiceOut/invoiceOutList.spec.js index ba6f3e122..3059a974b 100644 --- a/test/cypress/integration/invoiceOut/invoiceOutList.spec.js +++ b/test/cypress/integration/invoiceOut/invoiceOutList.spec.js @@ -28,10 +28,10 @@ describe('InvoiceOut list', () => { cy.dataCy('InvoiceOutDownloadPdfBtn').click(); }); - it.skip('should open the invoice descriptor from table icon', () => { + 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'); + cy.get('.summaryHeader > div').should('include.text', 'V10100001'); }); it('should open the client descriptor', () => { diff --git a/test/cypress/integration/invoiceOut/invoiceOutSummary.spec.js b/test/cypress/integration/invoiceOut/invoiceOutSummary.spec.js index 029165bb8..49eed32c7 100644 --- a/test/cypress/integration/invoiceOut/invoiceOutSummary.spec.js +++ b/test/cypress/integration/invoiceOut/invoiceOutSummary.spec.js @@ -15,7 +15,7 @@ describe('InvoiceOut summary', () => { cy.login('developer'); cy.visit(`/#/invoice-out/1/summary`); }); - it.skip('open the descriptors', () => { + 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'); @@ -30,7 +30,7 @@ describe('InvoiceOut summary', () => { cy.get('.q-item > .q-item__label').should('include.text', '1101'); }); - it.skip('should open the ticket list', () => { + it('should open the ticket list', () => { cy.get(toTicketList).click(); cy.get('[data-col-field="stateFk"]').each(($el) => { cy.wrap($el).contains('T1111111'); diff --git a/test/cypress/integration/invoiceOut/invvoiceOutGlobal.spec.js b/test/cypress/integration/invoiceOut/invvoiceOutGlobal.spec.js index 0170970a5..c6f75ef5f 100644 --- a/test/cypress/integration/invoiceOut/invvoiceOutGlobal.spec.js +++ b/test/cypress/integration/invoiceOut/invvoiceOutGlobal.spec.js @@ -22,6 +22,7 @@ describe('InvoiceOut global invoicing', () => { cy.get('.q-date__years-content > :nth-child(2) > .q-btn').click(); cy.get('.q-date__calendar-days > :nth-child(6) > .q-btn').click(); cy.get('[label="Max date ticket"]').type('01-01-2001{enter}'); + cy.dataCy('formSubmitBtn').click(); cy.get('.q-card').should('be.visible'); }); }); diff --git a/test/cypress/integration/item/ItemFixedPrice.spec.js b/test/cypress/integration/item/ItemFixedPrice.spec.js index 41230f570..2b92c861e 100644 --- a/test/cypress/integration/item/ItemFixedPrice.spec.js +++ b/test/cypress/integration/item/ItemFixedPrice.spec.js @@ -54,7 +54,7 @@ describe('Handle Items FixedPrice', () => { }); it('should edit all items', () => { - cy.get('.bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner').click(); + cy.get('.bg-header > :nth-child(1) [data-cy="vnCheckbox"]').click(); cy.dataCy('FixedPriceToolbarEditBtn').should('not.be.disabled'); cy.dataCy('FixedPriceToolbarEditBtn').click(); cy.dataCy('EditFixedPriceSelectOption').type(grouping); @@ -65,7 +65,7 @@ describe('Handle Items FixedPrice', () => { }); it('should remove all items', () => { - cy.get('.bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner').click(); + cy.get('.bg-header > :nth-child(1) [data-cy="vnCheckbox"]').click(); cy.dataCy('crudModelDefaultRemoveBtn').should('not.be.disabled'); cy.dataCy('crudModelDefaultRemoveBtn').click(); cy.dataCy('VnConfirm_confirm').click(); diff --git a/test/cypress/integration/item/itemList.spec.js b/test/cypress/integration/item/itemList.spec.js index bd8108344..7950f2bda 100644 --- a/test/cypress/integration/item/itemList.spec.js +++ b/test/cypress/integration/item/itemList.spec.js @@ -1,6 +1,6 @@ /// -describe.skip('Item list', () => { +describe('Item list', () => { beforeEach(() => { cy.login('developer'); cy.visit(`/#/item/list`); diff --git a/test/cypress/integration/item/itemTax.spec.js b/test/cypress/integration/item/itemTax.spec.js index 971e3a732..10c3ee889 100644 --- a/test/cypress/integration/item/itemTax.spec.js +++ b/test/cypress/integration/item/itemTax.spec.js @@ -6,7 +6,7 @@ describe('Item tax', () => { }); it('should modify the tax for Spain', () => { - cy.dataCy('Class_select').eq(1).type('General VAT{enter}'); + cy.dataCy('Class_select').eq(1).type('IVA General{enter}'); cy.dataCy('crudModelDefaultSaveBtn').click(); cy.checkNotification('Data saved'); }); diff --git a/test/cypress/integration/login/logout.spec.js b/test/cypress/integration/login/logout.spec.js index 9f022617d..b17e42794 100644 --- a/test/cypress/integration/login/logout.spec.js +++ b/test/cypress/integration/login/logout.spec.js @@ -1,5 +1,5 @@ /// -describe.skip('Logout', () => { +describe('Logout', () => { beforeEach(() => { cy.login('developer'); cy.visit(`/#/dashboard`); diff --git a/test/cypress/integration/monitor/clientActions.spec.js b/test/cypress/integration/monitor/clientActions.spec.js new file mode 100644 index 000000000..80f4de379 --- /dev/null +++ b/test/cypress/integration/monitor/clientActions.spec.js @@ -0,0 +1,47 @@ +/// + +describe('Monitor Clients actions', () => { + beforeEach(() => { + cy.login('salesPerson'); + cy.intercept('GET', '**/Departments**').as('departments'); + cy.visit('/#/monitor/clients-actions'); + cy.waitForElement('.q-page'); + cy.wait('@departments').then((xhr) => { + cy.window().then((win) => { + const user = JSON.parse(win.sessionStorage.getItem('user')); + const { where } = JSON.parse(xhr.request.query.filter); + expect(where.id.like).to.include(user.departmentFk.toString()); + }); + }); + cy.intercept('GET', '**/SalesMonitors/ordersFilter*').as('ordersFilter'); + cy.intercept('GET', '**/SalesMonitors/clientsFilter*').as('clientsFilter'); + }); + it('Should filter by field', () => { + cy.get('.q-page').should('be.visible'); + cy.dataCy('clientsOnWebsite') + .find('[data-cy="column-filter-departmentFk"] [data-cy="_select"]') + .click(); + cy.dataCy('recentOrderActions').within(() => { + cy.getRowCol('clientFk').find('span').should('have.class', 'link').click(); + }); + cy.checkVisibleDescriptor('Customer'); + + cy.dataCy('recentOrderActions').within(() => { + cy.getRowCol('departmentFk', 2) + .find('span') + .should('have.class', 'link') + .click(); + }); + + cy.checkVisibleDescriptor('Department'); + + cy.dataCy('clientsOnWebsite') + .find('.q-ml-md') + .should('have.text', 'Clients on website'); + cy.dataCy('recentOrderActions') + .find('.q-ml-md') + .should('have.text', 'Recent order actions'); + cy.dataCy('From_inputDate').should('have.value', '01/01/2001'); + cy.dataCy('To_inputDate').should('have.value', '01/01/2001'); + }); +}); diff --git a/test/cypress/integration/monitor/monitorTicket.spec.js b/test/cypress/integration/monitor/monitorTicket.spec.js new file mode 100644 index 000000000..72c6bf936 --- /dev/null +++ b/test/cypress/integration/monitor/monitorTicket.spec.js @@ -0,0 +1,69 @@ +/// +describe('Monitor Tickets Table', () => { + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('salesPerson'); + cy.visit('/#/monitor/tickets'); + cy.waitForElement('.q-page'); + cy.intercept('GET', '**/SalesMonitors/salesFilter*').as('filterRequest'); + cy.openRightMenu(); + }); + it('should open new tab when ctrl+click on client link', () => { + cy.intercept('GET', '**/SalesMonitors/salesFilter*').as('filterRequest'); + + cy.window().then((win) => { + cy.stub(win, 'open').as('windowOpen'); + }); + + cy.getRowCol('provinceFk').click({ ctrlKey: true }); + cy.get('@windowOpen').should('be.calledWithMatch', /\/ticket\/\d+\/sale/); + }); + it('should open the descriptorProxy and SummaryPopup', () => { + cy.getRowCol('totalProblems'); + + cy.getRowCol('id').find('span').should('have.class', 'link').click(); + cy.checkVisibleDescriptor('Ticket'); + + cy.getRowCol('zoneFk').find('span').should('have.class', 'link').click(); + cy.checkVisibleDescriptor('Zone'); + + cy.getRowCol('clientFk').find('span').should('have.class', 'link').click(); + cy.checkVisibleDescriptor('Customer'); + + cy.getRowCol('departmentFk').find('span').should('have.class', 'link').click(); + cy.checkVisibleDescriptor('Department'); + + cy.getRowCol('shippedDate').find('.q-badge'); + cy.tableActions().click({ ctrlKey: true }); + cy.tableActions(1).click(); + cy.get('.summaryHeader').should('exist'); + }); + + it('clear scopeDays', () => { + cy.get('[data-cy="Days onward_input"]').clear().type('2'); + cy.searchInFilterPanel(); + cy.get('.q-chip__content > span').should('have.text', '"2"'); + cy.waitSpinner(); + checkScopeDays(2); + cy.get('[data-cy="Days onward_input"]').clear(); + cy.searchInFilterPanel(); + cy.get('.q-chip__content > span').should('have.text', '"0"'); + cy.waitSpinner(); + checkScopeDays(0); + }); +}); + +function checkScopeDays(scopeDays) { + cy.url().then((url) => { + const urlParams = new URLSearchParams(url.split('?')[1]); + const saleMonitorTickets = JSON.parse( + decodeURIComponent(urlParams.get('saleMonitorTickets')), + ); + expect(saleMonitorTickets.scopeDays).to.equal(scopeDays); + const fromDate = new Date(saleMonitorTickets.from); + const toDate = new Date(saleMonitorTickets.to); + expect(toDate.getDate() - fromDate.getDate()).to.equal( + saleMonitorTickets.scopeDays, + ); + }); +} diff --git a/test/cypress/integration/order/orderCatalog.spec.js b/test/cypress/integration/order/orderCatalog.spec.js index 050dd396c..4f6371f32 100644 --- a/test/cypress/integration/order/orderCatalog.spec.js +++ b/test/cypress/integration/order/orderCatalog.spec.js @@ -2,7 +2,7 @@ describe('OrderCatalog', () => { beforeEach(() => { cy.login('developer'); - cy.viewport(1920, 720); + cy.viewport(1920, 1080); cy.visit('/#/order/8/catalog'); }); @@ -34,7 +34,7 @@ describe('OrderCatalog', () => { searchByCustomTagInput('Silver'); }); - it.skip('filters by custom value dialog', () => { + it('filters by custom value dialog', () => { Cypress.on('uncaught:exception', (err) => { if (err.message.includes('canceled')) { return false; diff --git a/test/cypress/integration/order/orderList.spec.js b/test/cypress/integration/order/orderList.spec.js index b77ef8fca..56c4b6a32 100644 --- a/test/cypress/integration/order/orderList.spec.js +++ b/test/cypress/integration/order/orderList.spec.js @@ -11,14 +11,15 @@ describe('OrderList', () => { it('create order', () => { cy.get('[data-cy="vnTableCreateBtn"]').click(); - cy.selectOption(clientCreateSelect, 1101); - cy.get(addressCreateSelect).click(); + cy.selectOption('[data-cy="Client_select"]', 1101); + cy.dataCy('landedDate').find('input').type('06/01/2001'); + cy.get('[data-cy="Address_select"]').click(); cy.get( '.q-menu > div> div.q-item:nth-child(1) >div.q-item__section--avatar > i', ).should('have.text', 'star'); - cy.dataCy('landedDate').find('input').type('06/01/2001'); - cy.selectOption(agencyCreateSelect, 1); - + cy.get('.q-menu > div> .q-item:nth-child(1)').click(); + cy.get('.q-card [data-cy="Agency_select"]').click(); + cy.get('.q-menu > div> .q-item:nth-child(1)').click(); cy.intercept('GET', /\/api\/Orders\/\d/).as('orderSale'); cy.get('[data-cy="FormModelPopup_save"] > .q-btn__content > .block').click(); cy.wait('@orderSale'); diff --git a/test/cypress/integration/route/cmr/cmrList.spec.js b/test/cypress/integration/route/cmr/cmrList.spec.js index a25a0c10a..fe05ab255 100644 --- a/test/cypress/integration/route/cmr/cmrList.spec.js +++ b/test/cypress/integration/route/cmr/cmrList.spec.js @@ -5,6 +5,9 @@ describe('Cmr list', () => { const selectors = { ticket: getLinkSelector('ticketFk'), client: getLinkSelector('clientFk'), + route: getLinkSelector('routeFk'), + agency: getLinkSelector('agencyModeFk'), + carrier: getLinkSelector('supplierFk'), lastRowSelectCheckBox: '.q-virtual-scroll__content > tr:last-child > :nth-child(1) > .q-checkbox', downloadBtn: '#subToolbar > .q-btn', @@ -21,6 +24,10 @@ describe('Cmr list', () => { const data = { ticket: '1', client: 'Bruce Wayne', + route: 'first route', + routeId: '1', + agency: 'inhouse pickup', + carrier: 'PLANTS SL', }; beforeEach(() => { @@ -68,6 +75,26 @@ describe('Cmr list', () => { }); }); + describe('Route pop-ups', () => { + it('Should redirect to the route summary from the route descriptor pop-up', () => { + cy.get(selectors.route).should('be.visible').click(); + cy.containContent(selectors.descriptorId, data.routeId); + cy.get(selectors.descriptorGoToSummaryBtn).should('be.visible').click(); + cy.url().should('include', '/route/1/summary'); + cy.containContent(selectors.summaryTitle, data.route); + }); + + it('Should redirect to the route summary from summary pop-up from the route descriptor pop-up', () => { + cy.get(selectors.route).should('be.visible').click(); + cy.containContent(selectors.descriptorId, data.routeId); + cy.get(selectors.descriptorOpenSummaryBtn).should('be.visible').click(); + cy.containContent(selectors.summaryTitle, data.route); + cy.get(selectors.summaryGoToSummaryBtn).should('be.visible').click(); + cy.url().should('include', '/route/1/summary'); + cy.containContent(selectors.summaryTitle, data.route); + }); + }); + describe('Client pop-ups', () => { it('Should redirect to the client summary from the client descriptor pop-up', () => { cy.get(selectors.client).should('be.visible').click(); @@ -87,4 +114,44 @@ describe('Cmr list', () => { cy.containContent(selectors.summaryTitle, data.client); }); }); + + describe('Agency pop-ups', () => { + it('Should redirect to the agency summary from the agency descriptor pop-up', () => { + cy.get(selectors.agency).should('be.visible').click(); + cy.containContent(selectors.descriptorTitle, data.agency); + cy.get(selectors.descriptorGoToSummaryBtn).should('be.visible').click(); + cy.url().should('include', '/agency/1/summary'); + cy.containContent(selectors.summaryTitle, data.agency); + }); + + it('Should redirect to the agency summary from summary pop-up from the agency descriptor pop-up', () => { + cy.get(selectors.agency).should('be.visible').click(); + cy.containContent(selectors.descriptorTitle, data.agency); + cy.get(selectors.descriptorOpenSummaryBtn).should('be.visible').click(); + cy.containContent(selectors.summaryTitle, data.agency); + cy.get(selectors.summaryGoToSummaryBtn).should('be.visible').click(); + cy.url().should('include', '/agency/1/summary'); + cy.containContent(selectors.summaryTitle, data.agency); + }); + }); + + describe('Carrier pop-ups', () => { + it('Should redirect to the supplier summary from the supplier descriptor pop-up', () => { + cy.get(selectors.carrier).should('be.visible').click(); + cy.containContent(selectors.descriptorTitle, data.carrier); + cy.get(selectors.descriptorGoToSummaryBtn).should('be.visible').click(); + cy.url().should('include', '/supplier/1/summary'); + cy.containContent(selectors.summaryTitle, data.carrier); + }); + + it('Should redirect to the supplier summary from summary pop-up from the supplier descriptor pop-up', () => { + cy.get(selectors.carrier).should('be.visible').click(); + cy.containContent(selectors.descriptorTitle, data.carrier); + cy.get(selectors.descriptorOpenSummaryBtn).should('be.visible').click(); + cy.containContent(selectors.summaryTitle, data.carrier); + cy.get(selectors.summaryGoToSummaryBtn).should('be.visible').click(); + cy.url().should('include', '/supplier/1/summary'); + cy.containContent(selectors.summaryTitle, data.carrier); + }); + }); }); diff --git a/test/cypress/integration/route/routeAutonomous.spec.js b/test/cypress/integration/route/routeAutonomous.spec.js index 6aaa2a85e..b61431bfb 100644 --- a/test/cypress/integration/route/routeAutonomous.spec.js +++ b/test/cypress/integration/route/routeAutonomous.spec.js @@ -1,11 +1,12 @@ -describe.skip('RouteAutonomous', () => { - const getLinkSelector = (colField) => - `tr:first-child > [data-col-field="${colField}"] > .no-padding > .link`; +describe('RouteAutonomous', () => { + const getLinkSelector = (colField, link = true) => + `tr:first-child > [data-col-field="${colField}"] > .no-padding${link ? ' > .link' : ''}`; const selectors = { - reference: 'Reference_input', - date: 'tr:first-child > [data-col-field="dated"]', total: '.value > .text-h6', + routeId: getLinkSelector('routeFk', false), + agencyRoute: getLinkSelector('agencyModeName'), + agencyAgreement: getLinkSelector('agencyAgreement'), received: getLinkSelector('invoiceInFk'), autonomous: getLinkSelector('supplierName'), firstRowCheckbox: '.q-virtual-scroll__content tr:first-child .q-checkbox__bg', @@ -13,22 +14,30 @@ describe.skip('RouteAutonomous', () => { createInvoiceBtn: '.q-card > .q-btn', saveFormBtn: 'FormModelPopup_save', summaryIcon: 'tableAction-0', - summaryPopupBtn: '.header > :nth-child(2) > .q-btn__content > .q-icon', - summaryHeader: '.summaryHeader > :nth-child(2)', - descriptorHeader: '.summaryHeader > div', - descriptorTitle: '.q-item__label--header > .title > span', - summaryGoToSummaryBtn: '.header > .q-icon', - descriptorGoToSummaryBtn: '.descriptor > .header > a[href] > .q-btn', + descriptorRouteSubtitle: '[data-cy="vnDescriptor_subtitle"]', + descriptorAgencyAndSupplierTitle: '[data-cy="vnDescriptor_description"]', + descriptorInvoiceInTitle: '[data-cy="vnDescriptor_title"]', + descriptorOpenSummaryBtn: '.q-menu > .descriptor [data-cy="openSummaryBtn"]', + descriptorGoToSummaryBtn: '.q-menu > .descriptor [data-cy="goToSummaryBtn"]', + summaryGoToSummaryBtn: '.summaryHeader [data-cy="goToSummaryBtn"]', }; - const data = { - reference: 'Test invoice', - total: '€206.40', - supplier: 'PLANTS SL', - route: 'first route', + const newInvoice = { + Reference: { val: 'Test invoice' }, + Company: { val: 'VNL', type: 'select' }, + Warehouse: { val: 'Warehouse One', type: 'select' }, + Type: { val: 'Vehiculos', type: 'select' }, + Description: { val: 'Test description' }, }; - const summaryUrl = '/summary'; + const total = '€206.40'; + + const urls = { + summaryAgencyUrlRegex: /agency\/\d+\/summary/, + summaryInvoiceInUrlRegex: /invoice-in\/\d+\/summary/, + summarySupplierUrlRegex: /supplier\/\d+\/summary/, + summaryRouteUrlRegex: /route\/\d+\/summary/, + }; const dataSaved = 'Data saved'; beforeEach(() => { @@ -44,10 +53,10 @@ describe.skip('RouteAutonomous', () => { .should('have.length.greaterThan', 0); }); - it('Should create invoice in to selected route', () => { + it.skip('Should create invoice in to selected route', () => { cy.get(selectors.firstRowCheckbox).click(); cy.get(selectors.createInvoiceBtn).click(); - cy.dataCy(selectors.reference).type(data.reference); + cy.fillInForm(newInvoice); cy.dataCy('attachFile').click(); cy.get('.q-file').selectFile('test/cypress/fixtures/image.jpg', { force: true, @@ -59,62 +68,120 @@ describe.skip('RouteAutonomous', () => { it('Should display the total price of the selected rows', () => { cy.get(selectors.firstRowCheckbox).click(); cy.get(selectors.secondRowCheckbox).click(); - cy.validateContent(selectors.total, data.total); + cy.validateContent(selectors.total, total); }); it('Should redirect to the summary when clicking a route', () => { - cy.get(selectors.date).click(); - cy.get(selectors.summaryHeader).should('contain', data.route); - cy.url().should('include', summaryUrl); + cy.checkRedirectionFromPopUp({ + selectorToClick: selectors.routeId, + expectedUrlRegex: urls.summaryRouteUrlRegex, + expectedTextSelector: selectors.descriptorRouteSubtitle, + }); + }); + + describe('Agency route pop-ups', () => { + it('Should redirect to the agency route summary from the agency route descriptor pop-up', () => { + cy.checkRedirectionFromPopUp({ + selectorToClick: selectors.agencyRoute, + steps: [selectors.descriptorGoToSummaryBtn], + expectedUrlRegex: urls.summaryAgencyUrlRegex, + expectedTextSelector: selectors.descriptorAgencyAndSupplierTitle, + }); + }); + + it('Should redirect to the agency route summary from summary pop-up from the agency route descriptor pop-up', () => { + cy.checkRedirectionFromPopUp({ + selectorToClick: selectors.agencyRoute, + steps: [ + selectors.descriptorOpenSummaryBtn, + selectors.summaryGoToSummaryBtn, + ], + expectedUrlRegex: urls.summaryAgencyUrlRegex, + expectedTextSelector: selectors.descriptorAgencyAndSupplierTitle, + }); + }); + }); + + describe('Agency route pop-ups', () => { + it('Should redirect to the agency agreement summary from the agency agreement descriptor pop-up', () => { + cy.checkRedirectionFromPopUp({ + selectorToClick: selectors.agencyAgreement, + steps: [selectors.descriptorGoToSummaryBtn], + expectedUrlRegex: urls.summaryAgencyUrlRegex, + expectedTextSelector: selectors.descriptorAgencyAndSupplierTitle, + }); + }); + + it('Should redirect to the agency agreement summary from summary pop-up from the agency agreement descriptor pop-up', () => { + cy.checkRedirectionFromPopUp({ + selectorToClick: selectors.agencyAgreement, + steps: [ + selectors.descriptorOpenSummaryBtn, + selectors.summaryGoToSummaryBtn, + ], + expectedUrlRegex: urls.summaryAgencyUrlRegex, + expectedTextSelector: selectors.descriptorAgencyAndSupplierTitle, + }); + }); }); describe('Received pop-ups', () => { - it('Should redirect to invoice in summary from the received descriptor pop-up', () => { - cy.get(selectors.received).click(); - cy.validateContent(selectors.descriptorTitle, data.reference); - cy.get(selectors.descriptorGoToSummaryBtn).click(); - cy.get(selectors.descriptorHeader).should('contain', data.supplier); - cy.url().should('include', summaryUrl); + it('Should redirect to the invoice in summary from the received descriptor pop-up', () => { + cy.checkRedirectionFromPopUp({ + selectorToClick: selectors.received, + steps: [selectors.descriptorGoToSummaryBtn], + expectedUrlRegex: urls.summaryInvoiceInUrlRegex, + expectedTextSelector: selectors.descriptorInvoiceInTitle, + }); }); it('Should redirect to the invoiceIn summary from summary pop-up from the received descriptor pop-up', () => { - cy.get(selectors.received).click(); - cy.validateContent(selectors.descriptorTitle, data.reference); - cy.get(selectors.summaryPopupBtn).click(); - cy.get(selectors.descriptorHeader).should('contain', data.supplier); - cy.get(selectors.summaryGoToSummaryBtn).click(); - cy.get(selectors.descriptorHeader).should('contain', data.supplier); - cy.url().should('include', summaryUrl); + cy.checkRedirectionFromPopUp({ + selectorToClick: selectors.received, + steps: [ + selectors.descriptorOpenSummaryBtn, + selectors.summaryGoToSummaryBtn, + ], + expectedUrlRegex: urls.summaryInvoiceInUrlRegex, + expectedTextSelector: selectors.descriptorInvoiceInTitle, + }); }); }); describe('Autonomous pop-ups', () => { it('Should redirect to the supplier summary from the received descriptor pop-up', () => { - cy.get(selectors.autonomous).click(); - cy.validateContent(selectors.descriptorTitle, data.supplier); - cy.get(selectors.descriptorGoToSummaryBtn).click(); - cy.get(selectors.summaryHeader).should('contain', data.supplier); - cy.url().should('include', summaryUrl); + cy.checkRedirectionFromPopUp({ + selectorToClick: selectors.autonomous, + steps: [selectors.descriptorGoToSummaryBtn], + expectedUrlRegex: urls.summarySupplierUrlRegex, + expectedTextSelector: selectors.descriptorAgencyAndSupplierTitle, + }); }); it('Should redirect to the supplier summary from summary pop-up from the autonomous descriptor pop-up', () => { - cy.get(selectors.autonomous).click(); - cy.get(selectors.descriptorTitle).should('contain', data.supplier); - cy.get(selectors.summaryPopupBtn).click(); - cy.get(selectors.summaryHeader).should('contain', data.supplier); - cy.get(selectors.summaryGoToSummaryBtn).click(); - cy.get(selectors.summaryHeader).should('contain', data.supplier); - cy.url().should('include', summaryUrl); + cy.checkRedirectionFromPopUp({ + selectorToClick: selectors.autonomous, + steps: [ + selectors.descriptorOpenSummaryBtn, + selectors.summaryGoToSummaryBtn, + ], + expectedUrlRegex: urls.summarySupplierUrlRegex, + expectedTextSelector: selectors.descriptorAgencyAndSupplierTitle, + }); }); }); describe('Route pop-ups', () => { it('Should redirect to the summary from the route summary pop-up', () => { - cy.dataCy(selectors.summaryIcon).first().click(); - cy.get(selectors.summaryHeader).should('contain', data.route); - cy.get(selectors.summaryGoToSummaryBtn).click(); - cy.get(selectors.summaryHeader).should('contain', data.route); - cy.url().should('include', summaryUrl); + cy.get(selectors.routeId) + .invoke('text') + .then((routeId) => { + routeId = routeId.trim(); + cy.dataCy(selectors.summaryIcon).first().click(); + cy.get(selectors.summaryGoToSummaryBtn).click(); + cy.url().should('match', urls.summaryRouteUrlRegex); + cy.containContent(selectors.descriptorRouteSubtitle, routeId); + }); }); }); }); diff --git a/test/cypress/integration/route/routeExtendedList.spec.js b/test/cypress/integration/route/routeExtendedList.spec.js index 83ecac122..d2b4e2108 100644 --- a/test/cypress/integration/route/routeExtendedList.spec.js +++ b/test/cypress/integration/route/routeExtendedList.spec.js @@ -69,7 +69,8 @@ describe.skip('Route extended list', () => { .type(`{selectall}{backspace}${value}`); break; case 'checkbox': - cy.get(selector).should('be.visible').click().click(); + cy.get(selector).should('be.visible').click() + cy.get(selector).click(); break; } } diff --git a/test/cypress/integration/route/vehicle/vehicleDms.spec.js b/test/cypress/integration/route/vehicle/vehicleDms.spec.js new file mode 100644 index 000000000..4d9250e0f --- /dev/null +++ b/test/cypress/integration/route/vehicle/vehicleDms.spec.js @@ -0,0 +1,147 @@ +describe('Vehicle DMS', () => { + const getSelector = (btnPosition) => + `tr:last-child > .text-right > .no-wrap > :nth-child(${btnPosition}) > .q-btn > .q-btn__content > .q-icon`; + + const selectors = { + lastRowDownloadBtn: getSelector(1), + lastRowEditBtn: getSelector(2), + lastRowDeleteBtn: getSelector(3), + lastRowReference: 'tr:last-child > :nth-child(5) > .q-tr > :nth-child(1) > span', + firstRowReference: + 'tr:first-child > :nth-child(5) > .q-tr > :nth-child(1) > span', + firstRowId: 'tr:first-child > :nth-child(2) > .q-tr > :nth-child(1) > span', + lastRowWorkerLink: 'tr:last-child > :nth-child(8) > .q-tr > .link', + descriptorTitle: '.descriptor .title', + descriptorOpenSummaryBtn: '.q-menu .descriptor [data-cy="openSummaryBtn"]', + descriptorGoToSummaryBtn: '.q-menu .descriptor [data-cy="goToSummaryBtn"]', + summaryGoToSummaryBtn: '.summaryHeader [data-cy="goToSummaryBtn"]', + summaryTitle: '.summaryHeader', + referenceInput: 'Reference_input', + companySelect: 'Company_select', + warehouseSelect: 'Warehouse_select', + typeSelect: 'Type_select', + fileInput: 'VnDms_inputFile', + importBtn: '[data-cy="importBtn"]', + addBtn: '[data-cy="addButton"]', + saveFormBtn: 'FormModelPopup_save', + }; + + const data = { + Reference: { val: 'Vehicle:1234-ABC' }, + Company: { val: 'VNL', type: 'select' }, + Warehouse: { val: 'Warehouse One', type: 'select' }, + Type: { val: 'Vehiculos', type: 'select' }, + }; + + const updateData = { + Reference: { val: 'Vehicle:4598-FGH' }, + Company: { val: 'CCs', type: 'select' }, + Warehouse: { val: 'Warehouse Two', type: 'select' }, + Type: { val: 'Facturas Recibidas', type: 'select' }, + }; + + const workerSummaryUrlRegex = /worker\/\d+\/summary/; + + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit(`/#/route/vehicle/1/dms`); + }); + + it('should display vehicle DMS', () => { + cy.get('.q-table') + .children() + .should('be.visible') + .should('have.length.greaterThan', 0); + }); + + it.skip('Should download DMS', () => { + const fileName = '11.jpg'; + cy.intercept('GET', /\/api\/dms\/11\/downloadFile/).as('download'); + cy.get(selectors.lastRowDownloadBtn).click(); + + cy.wait('@download').then((interception) => { + expect(interception.response.statusCode).to.equal(200); + expect(interception.response.headers['content-disposition']).to.contain( + fileName, + ); + }); + }); + + it('Should create new DMS', () => { + const formSelectors = { + actionBtn: selectors.addBtn, + fileInput: selectors.fileInput, + saveFormBtn: selectors.saveFormBtn, + }; + + cy.testDmsAction('create', formSelectors, data, 'Data saved'); + }); + + it('Should import DMS', () => { + const data = { + Document: { val: '10', type: 'select' }, + }; + const formSelectors = { + actionBtn: selectors.importBtn, + selectorContentToCheck: selectors.lastRowReference, + saveFormBtn: selectors.saveFormBtn, + }; + + cy.testDmsAction('import', formSelectors, data, 'Data saved', '1'); + }); + + it('Should edit DMS', () => { + const formSelectors = { + actionBtn: selectors.lastRowEditBtn, + selectorContentToCheck: selectors.lastRowReference, + saveFormBtn: selectors.saveFormBtn, + }; + + cy.testDmsAction( + 'edit', + formSelectors, + updateData, + 'Data saved', + updateData.Reference.val, + ); + }); + + it('Should delete DMS', () => { + const formSelectors = { + actionBtn: selectors.lastRowDeleteBtn, + selectorContentToCheck: selectors.lastRowReference, + }; + + cy.testDmsAction( + 'delete', + formSelectors, + null, + 'Data deleted', + 'Vehicle:3333-BAT', + ); + }); + + describe('Worker pop-ups', () => { + it('Should redirect to worker summary from worker descriptor pop-up', () => { + cy.checkRedirectionFromPopUp({ + selectorToClick: selectors.lastRowWorkerLink, + steps: [selectors.descriptorGoToSummaryBtn], + expectedUrlRegex: workerSummaryUrlRegex, + expectedTextSelector: selectors.descriptorTitle, + }); + }); + + it('Should redirect to worker summary from summary pop-up from worker descriptor pop-up', () => { + cy.checkRedirectionFromPopUp({ + selectorToClick: selectors.lastRowWorkerLink, + steps: [ + selectors.descriptorOpenSummaryBtn, + selectors.summaryGoToSummaryBtn, + ], + expectedUrlRegex: workerSummaryUrlRegex, + expectedTextSelector: selectors.descriptorTitle, + }); + }); + }); +}); diff --git a/test/cypress/integration/route/vehicle/vehicleEvents.spec.js b/test/cypress/integration/route/vehicle/vehicleEvents.spec.js new file mode 100644 index 000000000..f03e11b29 --- /dev/null +++ b/test/cypress/integration/route/vehicle/vehicleEvents.spec.js @@ -0,0 +1,27 @@ +describe('Vehicle', () => { + + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('deliveryAssistant'); + cy.visit(`/#/route/vehicle/3/events`); + }); + + it('should add, edit and delete a vehicle event', () => { + cy.get('.q-page-sticky > div > .q-btn').click(); + cy.dataCy('Started_inputDate').type('01/01/2001'); + cy.dataCy('Finished_inputDate').type('08/02/2001'); + cy.get(':nth-child(5)').find('[data-cy="Description_input"]').clear().type('Test'); + cy.selectOption('[data-cy="State_input"]', 3); + cy.get('.q-mt-lg > .q-btn--standard').click(); + + cy.get('.q-current-day > .q-calendar-month__day--content > .q-btn').click(); + cy.dataCy('Started_inputDate').clear().type('03/02/2001'); + cy.dataCy('Finished_inputDate').clear().type('15/03/2001'); + cy.get(':nth-child(5)').find('[data-cy="Description_input"]').clear().type('Test2'); + cy.selectOption('[data-cy="State_input"]', 5); + cy.get('.q-mt-lg > .q-btn--standard').click(); + + cy.dataCy('delete_event').eq(0).click(); + cy.dataCy('VnConfirm_confirm').click(); + }); +}); \ No newline at end of file diff --git a/test/cypress/integration/shelving/parking/parkingBasicData.spec.js b/test/cypress/integration/shelving/parking/parkingBasicData.spec.js index 81c158684..e3f454058 100644 --- a/test/cypress/integration/shelving/parking/parkingBasicData.spec.js +++ b/test/cypress/integration/shelving/parking/parkingBasicData.spec.js @@ -6,9 +6,7 @@ describe('ParkingBasicData', () => { beforeEach(() => { cy.login('developer'); cy.visit(`/#/shelving/parking/1/basic-data`); - cy.get('[data-cy="loading-spinner"]', { timeout: 10000 }).should( - 'not.be.visible', - ); + cy.get('[data-cy="navBar-spinner"]', { timeout: 10000 }).should('not.be.visible'); }); it('should give an error if the code aldready exists', () => { diff --git a/test/cypress/integration/ticket/ticketList.spec.js b/test/cypress/integration/ticket/ticketList.spec.js index 302707601..95649f51c 100644 --- a/test/cypress/integration/ticket/ticketList.spec.js +++ b/test/cypress/integration/ticket/ticketList.spec.js @@ -76,16 +76,13 @@ describe('TicketList', () => { }); }).as('ticket'); - cy.get('[data-cy="Warehouse_select"]').type('Warehouse Five'); - cy.get('.q-menu .q-item').contains('Warehouse Five').click(); + cy.get('[data-cy="Warehouse_select"]').type('Warehouse One'); + cy.get('.q-menu .q-item').contains('Warehouse One').click(); cy.wait('@ticket').then((interception) => { - const data = interception.response.body[1]; + const data = interception.response.body[0]; expect(data.hasComponentLack).to.equal(1); - expect(data.isTooLittle).to.equal(1); - expect(data.hasItemShortage).to.equal(1); }); cy.get('.icon-components').should('exist'); cy.get('.icon-unavailable').should('exist'); - cy.get('.icon-isTooLittle').should('exist'); }); }); diff --git a/test/cypress/integration/ticket/ticketSale.spec.js b/test/cypress/integration/ticket/ticketSale.spec.js index 0ae599e15..b87dfab71 100644 --- a/test/cypress/integration/ticket/ticketSale.spec.js +++ b/test/cypress/integration/ticket/ticketSale.spec.js @@ -2,7 +2,7 @@ const firstRow = 'tbody > :nth-child(1)'; describe('TicketSale', () => { - describe.skip('#23', () => { + describe('Ticket #23', () => { beforeEach(() => { cy.login('claimManager'); cy.viewport(1920, 1080); diff --git a/test/cypress/integration/vnComponent/VnDescriptor.commands.js b/test/cypress/integration/vnComponent/VnDescriptor.commands.js new file mode 100644 index 000000000..f03db8244 --- /dev/null +++ b/test/cypress/integration/vnComponent/VnDescriptor.commands.js @@ -0,0 +1,6 @@ +Cypress.Commands.add('checkVisibleDescriptor', (alias) => + cy + .get(`[data-cy="${alias}Descriptor"] [data-cy="vnDescriptor"] > .header`) + .should('exist') + .and('be.visible'), +); diff --git a/test/cypress/integration/vnComponent/VnShortcut.spec.js b/test/cypress/integration/vnComponent/VnShortcut.spec.js index cc5cacbe4..83249d15e 100644 --- a/test/cypress/integration/vnComponent/VnShortcut.spec.js +++ b/test/cypress/integration/vnComponent/VnShortcut.spec.js @@ -1,6 +1,6 @@ /// // https://redmine.verdnatura.es/issues/8848 -describe.skip('VnShortcuts', () => { +describe('VnShortcuts', () => { const modules = { item: 'a', customer: 'c', diff --git a/test/cypress/integration/vnComponent/vnTable.commands.js b/test/cypress/integration/vnComponent/vnTable.commands.js index 6c7e71e13..316fc12f1 100644 --- a/test/cypress/integration/vnComponent/vnTable.commands.js +++ b/test/cypress/integration/vnComponent/vnTable.commands.js @@ -2,9 +2,7 @@ Cypress.Commands.add('getRow', (index = 1) => cy.get(`.vnTable .q-virtual-scroll__content tr:nth-child(${index})`), ); Cypress.Commands.add('getRowCol', (field, index = 1) => - cy.get( - `.vnTable .q-virtual-scroll__content > :nth-child(${index}) > [data-col-field="${field}"]`, - ), + cy.getRow(index).find(`[data-col-field="${field}"]`), ); Cypress.Commands.add('vnTableCreateBtn', () => @@ -14,3 +12,9 @@ Cypress.Commands.add('vnTableCreateBtn', () => Cypress.Commands.add('waitTableScrollLoad', () => cy.waitForElement('[data-q-vs-anchor]'), ); + +Cypress.Commands.add('tableActions', (n = 0, child = 1) => + cy.get( + `:nth-child(${child}) > .q-table--col-auto-width > [data-cy="tableAction-${n}"] > .q-btn__content > .q-icon`, + ), +); diff --git a/test/cypress/integration/wagon/wagonType/wagonTypeEdit.spec.js b/test/cypress/integration/wagon/wagonType/wagonTypeEdit.spec.js index d82f9a10d..b185b61b4 100644 --- a/test/cypress/integration/wagon/wagonType/wagonTypeEdit.spec.js +++ b/test/cypress/integration/wagon/wagonType/wagonTypeEdit.spec.js @@ -10,7 +10,7 @@ describe('WagonTypeEdit', () => { cy.get('.q-card'); cy.get('input').first().type(' changed'); cy.get('div.q-checkbox__bg').first().click(); - cy.get('.q-btn--standard').click(); + cy.dataCy('saveDefaultBtn').click(); }); it('should delete a tray', () => { diff --git a/test/cypress/integration/worker/workerBusiness.spec.js b/test/cypress/integration/worker/workerBusiness.spec.js index 46da28cd6..1650b66c7 100644 --- a/test/cypress/integration/worker/workerBusiness.spec.js +++ b/test/cypress/integration/worker/workerBusiness.spec.js @@ -1,4 +1,4 @@ -describe.skip('WorkerBusiness', () => { +describe('WorkerBusiness', () => { const saveBtn = '.q-mt-lg > .q-btn--standard'; const contributionCode = `Representantes de comercio`; const contractType = `INDEFINIDO A TIEMPO COMPLETO`; diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 41f91e855..f990c1774 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -78,20 +78,21 @@ Cypress.Commands.add('waitForElement', (element) => { Cypress.Commands.add('getValue', (selector) => { cy.get(selector).then(($el) => { if ($el.find('.q-checkbox__inner').length > 0) { - return cy.get(selector + '.q-checkbox__inner'); + return cy.get(`${selector}.q-checkbox__inner`); } // Si es un QSelect if ($el.find('.q-select__dropdown-icon').length) { return cy .get( - selector + - '> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input', + `${ + selector + }> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input`, ) .invoke('val'); } // Si es un QSelect if ($el.find('span').length) { - return cy.get(selector + ' span').then(($span) => { + return cy.get(`${selector} span`).then(($span) => { return $span[0].innerText; }); } @@ -100,10 +101,15 @@ Cypress.Commands.add('getValue', (selector) => { }); }); -Cypress.Commands.add('waitSpinner', () => { +Cypress.Commands.add('waitSpinner', (_spinner = 'navBar') => { + const spinners = { + navBar: '[data-cy="navBar-spinner"]', + filterPanel: '[data-cy="filterPanel-spinner"]', + }; + const spinner = spinners[_spinner]; cy.get('body').then(($body) => { - if ($body.find('[data-cy="loading-spinner"]').length) { - cy.get('[data-cy="loading-spinner"]').should('not.be.visible'); + if ($body.find(spinner).length) { + cy.get(spinner).should('not.be.visible'); } }); }); @@ -142,7 +148,7 @@ function selectItem(selector, option, ariaControl, hasWrite = true) { function getItems(ariaControl, startTime = Cypress._.now(), timeout = 2500) { // Se intenta obtener la lista de opciones del desplegable de manera recursiva return cy - .get('#' + ariaControl, { timeout }) + .get(`#${ariaControl}`, { timeout }) .should('exist') .find('.q-item') .should('exist') @@ -189,8 +195,8 @@ Cypress.Commands.add('fillInForm', (obj, opts = {}) => { break; case 'date': cy.get(el).type( - `{selectall}{backspace}${val.split('-').join('')}`, - ); + `{selectall}{backspace}${val}`, + ).blur(); break; case 'time': cy.get(el).click(); @@ -352,11 +358,21 @@ Cypress.Commands.add('openListSummary', (row) => { cy.get('.card-list-body .actions .q-btn:nth-child(2)').eq(row).click(); }); -Cypress.Commands.add('openRightMenu', (element) => { - if (element) cy.waitForElement(element); - cy.get('[data-cy="toggle-right-drawer"]').click(); +Cypress.Commands.add('openRightMenu', (element = 'toggle-right-drawer') => { + if (element) cy.waitForElement(`[data-cy="${element}"]`); + cy.dataCy(element).click(); }); +Cypress.Commands.add('cleanFilterPanel', (element = 'clearFilters') => { + cy.get('#filterPanelForm').scrollIntoView(); + if (element) cy.waitForElement(`[data-cy="${element}"]`); + cy.dataCy(element).click(); +}); + +Cypress.Commands.add('searchInFilterPanel', (element = 'vnFilterPanel_search') => { + if (element) cy.waitForElement(`[data-cy="${element}"]`); + cy.dataCy(element).click(); +}); Cypress.Commands.add('openLeftMenu', (element) => { if (element) cy.waitForElement(element); cy.get('.q-toolbar > .q-btn--round.q-btn--dense > .q-btn__content > .q-icon').click(); @@ -454,9 +470,9 @@ Cypress.Commands.add('clickButtonWith', (type, value) => { Cypress.Commands.add('clickButtonWithIcon', (iconClass) => { cy.waitForElement('[data-cy="descriptor_actions"]'); - cy.get('[data-cy="loading-spinner"]', { timeout: 10000 }).should('not.be.visible'); + cy.waitSpinner(); cy.get('.q-btn') - .filter((index, el) => Cypress.$(el).find('.q-icon.' + iconClass).length > 0) + .filter((index, el) => Cypress.$(el).find(`.q-icon.${iconClass}`).length > 0) .then(($btn) => { cy.wrap($btn).click(); }); @@ -591,7 +607,7 @@ Cypress.Commands.add('validatePdfDownload', (match, trigger) => { }); }); -Cypress.Commands.add('clicDescriptorAction', (index = 1) => { +Cypress.Commands.add('clickDescriptorAction', (index = 1) => { cy.get(`[data-cy="descriptor_actions"] .q-btn:nth-of-type(${index})`).click(); }); @@ -619,3 +635,41 @@ Cypress.Commands.add('validateScrollContent', (validations) => { ); }); }); + +Cypress.Commands.add( + 'checkRedirectionFromPopUp', + ({ selectorToClick, steps = [], expectedUrlRegex, expectedTextSelector }) => { + cy.get(selectorToClick) + .click() + .invoke('text') + .then((label) => { + label = label.trim(); + + steps.forEach((stepSelector) => { + cy.get(stepSelector).should('be.visible').click(); + }); + + cy.location().should('match', expectedUrlRegex); + cy.containContent(expectedTextSelector, label); + }); + }, +); + +Cypress.Commands.add('testDmsAction', (action, selectors, data, message, content) => { + cy.get(selectors.actionBtn).click(); + + if (action === 'create') { + cy.dataCy(selectors.fileInput).selectFile('test/cypress/fixtures/image.jpg', { + force: true, + }); + } + + if (action !== 'delete') { + cy.fillInForm(data); + cy.dataCy(selectors.saveFormBtn).click(); + } else cy.clickConfirm(); + + cy.checkNotification(message); + + if (action !== 'create') cy.containContent(selectors.selectorContentToCheck, content); +}); diff --git a/test/cypress/support/index.js b/test/cypress/support/index.js index b0f0fb3b1..e9042c8fc 100644 --- a/test/cypress/support/index.js +++ b/test/cypress/support/index.js @@ -68,6 +68,7 @@ const waitForApiReady = (url, maxRetries = 20, delay = 1000) => { }; before(() => { + cy.viewport(1920, 1080); waitForApiReady('/api/Applications/status'); }); diff --git a/test/vitest/helper.js b/test/vitest/helper.js index be0029ee8..f70325254 100644 --- a/test/vitest/helper.js +++ b/test/vitest/helper.js @@ -4,6 +4,7 @@ import { createTestingPinia } from '@pinia/testing'; import { vi } from 'vitest'; import { i18n } from 'src/boot/i18n'; import { Notify, Dialog } from 'quasar'; +import keyShortcut from 'src/boot/keyShortcut'; import * as useValidator from 'src/composables/useValidator'; installQuasarPlugin({ @@ -16,6 +17,26 @@ const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false }); const mockPush = vi.fn(); const mockReplace = vi.fn(); +vi.mock('vue', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + inject: vi.fn((key) => { + if (key === 'app') { + return {}; + } + return actual.inject(key); + }), + onMounted: vi.fn((fn) => (fn && typeof fn === 'function' ? fn() : undefined)), + onBeforeMount: vi.fn((fn) => (fn && typeof fn === 'function' ? fn() : undefined)), + onUpdated: vi.fn((fn) => (fn && typeof fn === 'function' ? fn() : undefined)), + onUnmounted: vi.fn((fn) => (fn && typeof fn === 'function' ? fn() : undefined)), + onBeforeUnmount: vi.fn((fn) => + fn && typeof fn === 'function' ? fn() : undefined, + ), + }; +}); + vi.mock('vue-router', () => ({ useRouter: () => ({ push: mockPush, @@ -87,6 +108,10 @@ export function createWrapper(component, options) { const defaultOptions = { global: { plugins: [i18n, pinia], + directives: { + shortcut: keyShortcut, + }, + stubs: ['useState', 'arrayData', 'useStateStore', 'vue-i18n', 'RouterLink'], }, mocks: { t: (tKey) => tKey, @@ -94,15 +119,11 @@ export function createWrapper(component, options) { }, }; - const mountOptions = Object.assign({}, defaultOptions); - - if (options instanceof Object) { - Object.assign(mountOptions, options); - - if (options.global) { - mountOptions.global.plugins = defaultOptions.global.plugins; - } - } + const mountOptions = { + ...defaultOptions, + ...options, + global: { ...defaultOptions.global, ...options?.global }, + }; const wrapper = mount(component, mountOptions); const vm = wrapper.vm; diff --git a/test/vitest/setup-file.js b/test/vitest/setup-file.js index 0ba9e53c2..6b49d958f 100644 --- a/test/vitest/setup-file.js +++ b/test/vitest/setup-file.js @@ -1,5 +1,26 @@ -// This file will be run before each test file, don't delete or vitest will not work. -import { vi } from 'vitest'; +import { afterAll, beforeAll, vi } from 'vitest'; + +let vueWarnings = []; + +const originalConsoleWarn = console.warn; + +beforeAll(() => { + console.warn = (...args) => { + vueWarnings.push(args.join(' ')); + }; +}); + +afterEach(() => { + if (vueWarnings.length > 0) { + const allWarnings = vueWarnings.join('\n'); + vueWarnings = []; + throw new Error(`Vue warnings detected during test:\n${allWarnings}`); + } +}); + +afterAll(() => { + console.warn = originalConsoleWarn; +}); vi.mock('axios'); vi.mock('vue-router', () => ({