diff --git a/CHANGELOG.md b/CHANGELOG.md index 58b68b7fa..10b7c73f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +# Version 25.08 - 2025-03-04 + +### Added 🆕 + +- feat: add order for table (origin/8681_ticketAdvance_updates) by:Javier Segarra +- feat: detect when is descriptor proxy by:Javier Segarra +- feat: refs #7356 update CrudModel by:Javier Segarra +- feat: refs #8242 remove teleport by:Javier Segarra +- feat: refs #8242 use stateStore by:Javier Segarra +- fix: fixed negative bases style by:Jon +- fix: fixed style when clicking on icons by:Jon +- refactor: refs #6897 remove debug logs and unused style (origin/6897-fixSomeCaus) by:pablone +- style: refs #7356 eslint format by:Javier Segarra + +### Changed 📦 + +- perf: refs #7356 minor changes (origin/7356_ticketService) by:Javier Segarra +- refactor: refs #6897 remove debug logs and unused style (origin/6897-fixSomeCaus) by:pablone +- refactor: refs #6897 update component props and attributes for consistency and improved functionality (origin/6897-fixMinorIssues) by:pablone +- refactor: refs #6897 update component props and improve UI handling in Entry pages by:pablone +- refactor: refs #6897 update VnTable components for improved value handling and UI adjustments (origin/6897-minorFixes) by:pablone +- refactor: refs #8697 simplify date handling in ItemDiary component by:pablone + +### Fixed 🛠️ + +- fix: add datakey by:Javier Segarra +- fix: fixed account descriptor menu and created e2e by:Jon +- fix: fixed negative bases style by:Jon +- fix: fixed style when clicking on icons by:Jon +- fix: refs #6553 workerBusiness (origin/6553-fixWorkerBusinessV2) by:carlossa +- fix: refs #6553 workerBusiness v3 by:carlossa +- fix: refs #6897 prevent default event behavior in autocompleteExpense function by:pablone +- fix: refs #7356 chaining params by:Javier Segarra +- fix: refs #7356 ticketService by:Javier Segarra +- fix: refs #8242 workerDepartmentTree bug (origin/8242_leftMenu_responsive) by:Javier Segarra +- fix: workerBasicData by:carlossa +- Revert "revert 1015acefb7e400be2d8b5958dba69b4d98276b34" (origin/fix_revert_revert, fix_revert_revert) by:alexm + # Version 25.06 - 2025-02-18 ### Added 🆕 diff --git a/Jenkinsfile b/Jenkinsfile index a52a9e91d..c527d9660 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -26,6 +26,7 @@ node { // https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables echo "NODE_NAME: ${env.NODE_NAME}" echo "WORKSPACE: ${env.WORKSPACE}" + echo "CHANGE_TARGET: ${env.CHANGE_TARGET}" configFileProvider([ configFile(fileId: 'salix-front.properties', @@ -94,7 +95,7 @@ pipeline { parallel { stage('Unit') { steps { - sh 'pnpm run test:unit:ci' + sh 'pnpm run test:front:ci' } post { always { @@ -107,16 +108,19 @@ pipeline { } stage('E2E') { environment { - CREDENTIALS = credentials('docker-registry') + CREDS = credentials('docker-registry') COMPOSE_PROJECT = "${PROJECT_NAME}-${env.BUILD_ID}" COMPOSE_PARAMS = "-p ${env.COMPOSE_PROJECT} -f test/cypress/docker-compose.yml --project-directory ." } steps { script { - sh 'rm junit/e2e-*.xml || true' + sh 'rm -f junit/e2e-*.xml' env.COMPOSE_TAG = PROTECTED_BRANCH.contains(env.CHANGE_TARGET) ? env.CHANGE_TARGET : 'dev' - def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs') + + sh 'docker login --username $CREDS_USR --password $CREDS_PSW $REGISTRY' sh "docker-compose ${env.COMPOSE_PARAMS} up -d" + + def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs') image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ") { sh 'cypress run --browser chromium || true' } diff --git a/README.md b/README.md index e87a84d60..262e12e58 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ quasar dev ### Run unit tests ```bash -pnpm run test:unit +pnpm run test:front ``` ### Run e2e tests diff --git a/package.json b/package.json index e78b0cf3c..80706f895 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "25.10.0", + "version": "25.12.0", "description": "Salix frontend", "productName": "Salix", "author": "Verdnatura", @@ -14,8 +14,8 @@ "test:e2e": "cypress open", "test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run", "test": "echo \"See package.json => scripts for available tests.\" && exit 0", - "test:unit": "vitest", - "test:unit:ci": "vitest run", + "test:front": "vitest", + "test:front:ci": "vitest run", "commitlint": "commitlint --edit", "prepare": "npx husky install", "addReferenceTag": "node .husky/addReferenceTag.js", @@ -71,4 +71,4 @@ "vite": "^6.0.11", "vitest": "^0.31.1" } -} +} \ No newline at end of file diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue index 93a2ac96a..8c4f70f3b 100644 --- a/src/components/CrudModel.vue +++ b/src/components/CrudModel.vue @@ -184,8 +184,11 @@ async function saveChanges(data) { if ($props.beforeSaveFn) { changes = await $props.beforeSaveFn(changes, getChanges); } - try { + if (changes?.creates?.length === 0 && changes?.updates?.length === 0) { + return; + } + await axios.post($props.saveUrl || $props.url + '/crud', changes); } finally { isLoading.value = false; diff --git a/src/components/FilterTravelForm.vue b/src/components/FilterTravelForm.vue index 765d97763..c522d0269 100644 --- a/src/components/FilterTravelForm.vue +++ b/src/components/FilterTravelForm.vue @@ -124,7 +124,7 @@ const selectTravel = ({ id }) => { <FetchData url="AgencyModes" @on-fetch="(data) => (agenciesOptions = data)" - :filter="{ fields: ['id', 'name'], order: 'name ASC' }" + :filter="{ fields: ['id', 'name'], order: ['name ASC'] }" auto-load /> <FetchData diff --git a/src/components/TicketProblems.vue b/src/components/TicketProblems.vue index 783f2556f..255bea9cd 100644 --- a/src/components/TicketProblems.vue +++ b/src/components/TicketProblems.vue @@ -12,20 +12,31 @@ defineProps({ row: { type: Object, required: true } }); > <QIcon name="vn:claims" size="xs"> <QTooltip> - {{ t('ticketSale.claim') }}: + {{ $t('ticketSale.claim') }}: {{ row.claim?.claimFk }} </QTooltip> </QIcon> </router-link> <QIcon - v-if="row?.risk" + v-if="row?.reserved" + color="primary" + name="vn:reserva" + size="xs" + data-cy="ticketSaleReservedIcon" + > + <QTooltip> + {{ t('ticketSale.reserved') }} + </QTooltip> + </QIcon> + <QIcon + v-if="row?.hasRisk" name="vn:risk" :color="row.hasHighRisk ? 'negative' : 'primary'" size="xs" > <QTooltip> {{ $t('salesTicketsTable.risk') }}: - {{ toCurrency(row.risk - row.credit) }} + {{ toCurrency(row.risk - (row.credit ?? 0)) }} </QTooltip> </QIcon> <QIcon @@ -67,12 +78,7 @@ defineProps({ row: { type: Object, required: true } }); > <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> </QIcon> - <QIcon - v-if="row?.isTaxDataChecked !== 0" - name="vn:no036" - color="primary" - size="xs" - > + <QIcon v-if="row?.isTaxDataChecked" name="vn:no036" color="primary" size="xs"> <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> </QIcon> <QIcon v-if="row?.isFreezed" name="vn:frozen" color="primary" size="xs"> diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index dd2cefd89..d0c657f8a 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -55,6 +55,10 @@ const $props = defineProps({ type: [Function, Boolean], default: null, }, + rowCtrlClick: { + type: [Function, Boolean], + default: null, + }, redirect: { type: String, default: null, @@ -681,6 +685,7 @@ const rowCtrlClickFunction = computed(() => { @update:selected="emit('update:selected', $event)" @selection="(details) => handleSelection(details, rows)" :hide-selected-banner="true" + :data-cy="$props.dataCy ?? 'vnTable'" > <template #top-left v-if="!$props.withoutHeader"> <slot name="top-left"> </slot> diff --git a/src/components/__tests__/CrudModel.spec.js b/src/components/__tests__/CrudModel.spec.js index e0afd30ad..f6c93e0d5 100644 --- a/src/components/__tests__/CrudModel.spec.js +++ b/src/components/__tests__/CrudModel.spec.js @@ -30,8 +30,8 @@ describe('CrudModel', () => { saveFn: '', }, }); - wrapper=wrapper.wrapper; - vm=wrapper.vm; + wrapper = wrapper.wrapper; + vm = wrapper.vm; }); beforeEach(() => { @@ -143,14 +143,14 @@ describe('CrudModel', () => { }); it('should return true if object is empty', async () => { - dummyObj ={}; - result = vm.isEmpty(dummyObj); + dummyObj = {}; + result = vm.isEmpty(dummyObj); expect(result).toBe(true); }); it('should return false if object is not empty', async () => { - dummyObj = {a:1, b:2, c:3}; + dummyObj = { a: 1, b: 2, c: 3 }; result = vm.isEmpty(dummyObj); expect(result).toBe(false); @@ -158,29 +158,31 @@ describe('CrudModel', () => { it('should return true if array is empty', async () => { dummyArray = []; - result = vm.isEmpty(dummyArray); + result = vm.isEmpty(dummyArray); expect(result).toBe(true); }); - + it('should return false if array is not empty', async () => { - dummyArray = [1,2,3]; + dummyArray = [1, 2, 3]; result = vm.isEmpty(dummyArray); expect(result).toBe(false); - }) + }); }); describe('resetData()', () => { it('should add $index to elements in data[] and sets originalData and formData with data', async () => { - data = [{ - name: 'Tony', - lastName: 'Stark', - age: 42, - }]; + data = [ + { + name: 'Tony', + lastName: 'Stark', + age: 42, + }, + ]; vm.resetData(data); - + expect(vm.originalData).toEqual(data); expect(vm.originalData[0].$index).toEqual(0); expect(vm.formData).toEqual(data); @@ -200,7 +202,7 @@ describe('CrudModel', () => { lastName: 'Stark', age: 42, }; - + vm.resetData(data); expect(vm.originalData).toEqual(data); @@ -210,17 +212,19 @@ describe('CrudModel', () => { }); describe('saveChanges()', () => { - data = [{ - name: 'Tony', - lastName: 'Stark', - age: 42, - }]; + data = [ + { + name: 'Tony', + lastName: 'Stark', + age: 42, + }, + ]; it('should call saveFn if exists', async () => { await wrapper.setProps({ saveFn: vi.fn() }); vm.saveChanges(data); - + expect(vm.saveFn).toHaveBeenCalledOnce(); expect(vm.isLoading).toBe(false); expect(vm.hasChanges).toBe(false); @@ -229,13 +233,15 @@ describe('CrudModel', () => { }); it("should use default url if there's not saveFn", async () => { - const postMock =vi.spyOn(axios, 'post'); - - vm.formData = [{ - name: 'Bruce', - lastName: 'Wayne', - age: 45, - }] + const postMock = vi.spyOn(axios, 'post'); + + vm.formData = [ + { + name: 'Bruce', + lastName: 'Wayne', + age: 45, + }, + ]; await vm.saveChanges(data); diff --git a/src/components/common/VnAccountNumber.vue b/src/components/common/VnAccountNumber.vue index c4fa78674..56add7329 100644 --- a/src/components/common/VnAccountNumber.vue +++ b/src/components/common/VnAccountNumber.vue @@ -1,12 +1,9 @@ <script setup> -import { nextTick, ref, watch } from 'vue'; -import { QInput } from 'quasar'; +import { nextTick, ref } from 'vue'; +import VnInput from './VnInput.vue'; +import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard'; const $props = defineProps({ - modelValue: { - type: String, - default: '', - }, insertable: { type: Boolean, default: false, @@ -14,70 +11,25 @@ const $props = defineProps({ }); const emit = defineEmits(['update:modelValue', 'accountShortToStandard']); +const model = defineModel({ prop: 'modelValue' }); +const inputRef = ref(false); -let internalValue = ref($props.modelValue); - -watch( - () => $props.modelValue, - (newVal) => { - internalValue.value = newVal; - } -); - -watch( - () => internalValue.value, - (newVal) => { - emit('update:modelValue', newVal); - accountShortToStandard(); - } -); - -const handleKeydown = (e) => { - if (e.key === 'Backspace') return; - if (e.key === '.') { - accountShortToStandard(); - // TODO: Fix this setTimeout, with nextTick doesn't work - setTimeout(() => { - setCursorPosition(0, e.target); - }, 1); - return; - } - - if ($props.insertable && e.key.match(/[0-9]/)) { - handleInsertMode(e); - } -}; -function setCursorPosition(pos, el = vnInputRef.value) { - el.focus(); - el.setSelectionRange(pos, pos); +function setCursorPosition(pos) { + const input = inputRef.value.vnInputRef.$el.querySelector('input'); + input.focus(); + input.setSelectionRange(pos, pos); } -const vnInputRef = ref(false); -const handleInsertMode = (e) => { - e.preventDefault(); - const input = e.target; - const cursorPos = input.selectionStart; - const { maxlength } = vnInputRef.value; - let currentValue = internalValue.value; - if (!currentValue) currentValue = e.key; - const newValue = e.key; - if (newValue && !isNaN(newValue) && cursorPos < maxlength) { - internalValue.value = - currentValue.substring(0, cursorPos) + - newValue + - currentValue.substring(cursorPos + 1); - } - nextTick(() => { - input.setSelectionRange(cursorPos + 1, cursorPos + 1); - }); -}; -function accountShortToStandard() { - internalValue.value = internalValue.value?.replace( - '.', - '0'.repeat(11 - internalValue.value.length) - ); + +async function handleUpdateModel(val) { + model.value = val?.at(-1) === '.' ? useAccountShortToStandard(val) : val; + await nextTick(() => setCursorPosition(0)); } </script> - <template> - <QInput @keydown="handleKeydown" ref="vnInputRef" v-model="internalValue" /> + <VnInput + v-model="model" + ref="inputRef" + :insertable + @update:model-value="handleUpdateModel" + /> </template> diff --git a/src/components/common/VnCardBeta.vue b/src/components/common/VnCardBeta.vue index 7c82316dc..620dc2ad2 100644 --- a/src/components/common/VnCardBeta.vue +++ b/src/components/common/VnCardBeta.vue @@ -1,10 +1,9 @@ <script setup> import { onBeforeMount } from 'vue'; -import { useRouter, onBeforeRouteUpdate } from 'vue-router'; +import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'; import { useArrayData } from 'src/composables/useArrayData'; import { useStateStore } from 'stores/useStateStore'; import useCardSize from 'src/composables/useCardSize'; -import LeftMenu from 'components/LeftMenu.vue'; import VnSubToolbar from '../ui/VnSubToolbar.vue'; const props = defineProps({ @@ -27,7 +26,13 @@ const arrayData = useArrayData(props.dataKey, { oneRecord: true, }); +onBeforeRouteLeave(() => { + stateStore.cardDescriptorChangeValue(null); +}); + onBeforeMount(async () => { + stateStore.cardDescriptorChangeValue(props.descriptor); + const route = router.currentRoute.value; try { await fetch(route.params.id); @@ -62,11 +67,6 @@ function hasRouteParam(params, valueToCheck = ':addressId') { } </script> <template> - <Teleport to="#left-panel" v-if="stateStore.isHeaderMounted()"> - <component :is="descriptor" /> - <QSeparator /> - <LeftMenu source="card" /> - </Teleport> <VnSubToolbar /> <div :class="[useCardSize(), $attrs.class]"> <RouterView :key="$route.path" /> diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue index aeb4a31fd..9821992cb 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -83,7 +83,7 @@ const mixinRules = [ requiredFieldRule, ...($attrs.rules ?? []), (val) => { - const { maxlength } = vnInputRef.value; + const maxlength = $props.maxlength; if (maxlength && +val.length > maxlength) return t(`maxLength`, { value: maxlength }); const { min, max } = vnInputRef.value.$attrs; @@ -108,7 +108,7 @@ const handleInsertMode = (e) => { e.preventDefault(); const input = e.target; const cursorPos = input.selectionStart; - const { maxlength } = vnInputRef.value; + const maxlength = $props.maxlength; let currentValue = value.value; if (!currentValue) currentValue = e.key; const newValue = e.key; @@ -143,7 +143,7 @@ const handleUppercase = () => { :rules="mixinRules" :lazy-rules="true" hide-bottom-space - :data-cy="$attrs.dataCy ?? $attrs.label + '_input'" + :data-cy="($attrs['data-cy'] ?? $attrs.label) + '_input'" > <template #prepend v-if="$slots.prepend"> <slot name="prepend" /> diff --git a/src/components/common/VnLog.vue b/src/components/common/VnLog.vue index d870e487f..8f106a9f1 100644 --- a/src/components/common/VnLog.vue +++ b/src/components/common/VnLog.vue @@ -641,15 +641,7 @@ watch( > {{ prop.nameI18n }}: </span> - <VnJsonValue :value="prop.val.val" /> - <span - v-if="prop.val.id" - class="id-value" - > - #{{ prop.val.id }} - </span> <span v-if="log.action == 'update'"> - ← <VnJsonValue :value="prop.old.val" /> @@ -659,6 +651,26 @@ watch( > #{{ prop.old.id }} </span> + → + <VnJsonValue + :value="prop.val.val" + /> + <span + v-if="prop.val.id" + class="id-value" + > + #{{ prop.val.id }} + </span> + </span> + <span v-else="prop.old.val"> + <VnJsonValue + :value="prop.val.val" + /> + <span + v-if="prop.old.id" + class="id-value" + >#{{ prop.old.id }}</span + > </span> </div> </span> diff --git a/src/components/common/VnModule.vue b/src/components/common/VnModule.vue index 038ee1d60..747a7c951 100644 --- a/src/components/common/VnModule.vue +++ b/src/components/common/VnModule.vue @@ -12,7 +12,7 @@ const $props = defineProps({ }, }); onMounted( - () => (stateStore.leftDrawer = useQuasar().screen.gt.xs ? $props.leftDrawer : false) + () => (stateStore.leftDrawer = useQuasar().screen.gt.xs ? $props.leftDrawer : false), ); const teleportRef = ref({}); @@ -35,8 +35,14 @@ onMounted(() => { <template> <QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256"> <QScrollArea class="fit text-grey-8"> - <div id="left-panel" ref="teleportRef"></div> - <LeftMenu v-if="!hasContent" /> + <div id="left-panel" ref="teleportRef"> + <template v-if="stateStore.cardDescriptor"> + <component :is="stateStore.cardDescriptor" /> + <QSeparator /> + <LeftMenu source="card" /> + </template> + <template v-else> <LeftMenu /></template> + </div> </QScrollArea> </QDrawer> <QPageContainer> diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index d111780bd..339f90e0e 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -302,6 +302,8 @@ defineExpose({ opts: myOptions, vnSelectRef }); function handleKeyDown(event) { if (event.key === 'Tab' && !event.shiftKey) { + event.preventDefault(); + const inputValue = vnSelectRef.value?.inputValue; if (inputValue) { diff --git a/src/components/ui/CatalogItem.vue b/src/components/ui/CatalogItem.vue index 7806562b2..0ae890e37 100644 --- a/src/components/ui/CatalogItem.vue +++ b/src/components/ui/CatalogItem.vue @@ -132,7 +132,8 @@ const card = toRef(props, 'item'); display: flex; flex-direction: column; gap: 4px; - + white-space: nowrap; + width: 192px; p { margin-bottom: 0; } diff --git a/src/components/ui/VnNotes.vue b/src/components/ui/VnNotes.vue index ec6289a67..eb0804af0 100644 --- a/src/components/ui/VnNotes.vue +++ b/src/components/ui/VnNotes.vue @@ -18,20 +18,16 @@ import VnInput from 'components/common/VnInput.vue'; const emit = defineEmits(['onFetch']); -const originalAttrs = useAttrs(); - -const $attrs = computed(() => { - const { style, ...rest } = originalAttrs; - return rest; -}); +const $attrs = useAttrs(); const isRequired = computed(() => { - return Object.keys($attrs).includes('required') + return Object.keys($attrs).includes('required'); }); const $props = defineProps({ url: { type: String, default: null }, - saveUrl: {type: String, default: null}, + saveUrl: { type: String, default: null }, + userFilter: { type: Object, default: () => {} }, filter: { type: Object, default: () => {} }, body: { type: Object, default: () => {} }, addNote: { type: Boolean, default: false }, @@ -65,7 +61,7 @@ async function insert() { } function confirmAndUpdate() { - if(!newNote.text && originalText) + if (!newNote.text && originalText) quasar .dialog({ component: VnConfirm, @@ -88,11 +84,17 @@ async function update() { ...body, ...{ notes: newNote.text }, }; - await axios.patch(`${$props.saveUrl ?? `${$props.url}/${$props.body.workerFk}`}`, newBody); + await axios.patch( + `${$props.saveUrl ?? `${$props.url}/${$props.body.workerFk}`}`, + newBody, + ); } onBeforeRouteLeave((to, from, next) => { - if ((newNote.text && !$props.justInput) || (newNote.text !== originalText) && $props.justInput) + if ( + (newNote.text && !$props.justInput) || + (newNote.text !== originalText && $props.justInput) + ) quasar.dialog({ component: VnConfirm, componentProps: { @@ -104,12 +106,11 @@ onBeforeRouteLeave((to, from, next) => { else next(); }); -function fetchData([ data ]) { +function fetchData([data]) { newNote.text = data?.notes; originalText = data?.notes; emit('onFetch', data); } - </script> <template> <FetchData @@ -126,8 +127,8 @@ function fetchData([ data ]) { @on-fetch="fetchData" auto-load /> - <QCard - class="q-pa-xs q-mb-lg full-width" + <QCard + class="q-pa-xs q-mb-lg full-width" :class="{ 'just-input': $props.justInput }" v-if="$props.addNote || $props.justInput" > @@ -179,7 +180,8 @@ function fetchData([ data ]) { :url="$props.url" order="created DESC" :limit="0" - :user-filter="$props.filter" + :user-filter="userFilter" + :filter="filter" auto-load ref="vnPaginateRef" class="show" @@ -218,7 +220,7 @@ function fetchData([ data ]) { > {{ observationTypes.find( - (ot) => ot.id === note.observationTypeFk + (ot) => ot.id === note.observationTypeFk, )?.description }} </QBadge> diff --git a/src/composables/useArrayData.js b/src/composables/useArrayData.js index fcc61972a..3a171191e 100644 --- a/src/composables/useArrayData.js +++ b/src/composables/useArrayData.js @@ -245,7 +245,7 @@ export function useArrayData(key, userOptions) { async function loadMore() { if (!store.hasMoreData) return; - store.skip = store.limit * store.page; + store.skip = (store?.filter?.limit ?? store.limit) * store.page; store.page += 1; await fetch({ append: true }); diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 5b667555e..d7187371e 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -369,6 +369,7 @@ globals: countryFk: Country countryCodeFk: Country companyFk: Company + nickname: Alias model: Model fuel: Fuel active: Active diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 3f004485d..fc3018f39 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -370,6 +370,7 @@ globals: countryFk: País countryCodeFk: País companyFk: Empresa + nickname: Alias errors: statusUnauthorized: Acceso denegado statusInternalServerError: Ha ocurrido un error interno del servidor diff --git a/src/pages/Claim/Card/ClaimNotes.vue b/src/pages/Claim/Card/ClaimNotes.vue index cc6e33779..68cb220ee 100644 --- a/src/pages/Claim/Card/ClaimNotes.vue +++ b/src/pages/Claim/Card/ClaimNotes.vue @@ -1,5 +1,5 @@ <script setup> -import { computed, useAttrs } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useState } from 'src/composables/useState'; import VnNotes from 'src/components/ui/VnNotes.vue'; @@ -7,7 +7,6 @@ import VnNotes from 'src/components/ui/VnNotes.vue'; const route = useRoute(); const state = useState(); const user = state.getUser(); -const $attrs = useAttrs(); const $props = defineProps({ id: { type: [Number, String], default: null }, @@ -15,24 +14,21 @@ const $props = defineProps({ }); const claimId = computed(() => $props.id || route.params.id); -const claimFilter = computed(() => { - return { - where: { claimFk: claimId.value }, - fields: ['id', 'created', 'workerFk', 'text'], - include: { - relation: 'worker', - scope: { - fields: ['id', 'firstName', 'lastName'], - include: { - relation: 'user', - scope: { - fields: ['id', 'nickname', 'name'], - }, +const claimFilter = { + fields: ['id', 'created', 'workerFk', 'text'], + include: { + relation: 'worker', + scope: { + fields: ['id', 'firstName', 'lastName'], + include: { + relation: 'user', + scope: { + fields: ['id', 'nickname', 'name'], }, }, }, - }; -}); + }, +}; const body = { claimFk: claimId.value, @@ -43,7 +39,8 @@ const body = { <VnNotes url="claimObservations" :add-note="$props.addNote" - :filter="claimFilter" + :user-filter="claimFilter" + :filter="{ where: { claimFk: claimId } }" :body="body" v-bind="$attrs" style="overflow-y: auto" diff --git a/src/pages/Claim/Card/ClaimPhoto.vue b/src/pages/Claim/Card/ClaimPhoto.vue index d4acc9bbe..4ced7e862 100644 --- a/src/pages/Claim/Card/ClaimPhoto.vue +++ b/src/pages/Claim/Card/ClaimPhoto.vue @@ -210,6 +210,7 @@ function onDrag() { class="all-pointer-events absolute delete-button zindex" @click.stop="viewDeleteDms(index)" round + :data-cy="`delete-button-${index+1}`" /> <QIcon name="play_circle" @@ -227,6 +228,7 @@ function onDrag() { class="rounded-borders cursor-pointer fit" @click="openDialog(media.dmsFk)" v-if="!media.isVideo" + :data-cy="`file-${index+1}`" > </QImg> <video @@ -235,6 +237,7 @@ function onDrag() { muted="muted" v-if="media.isVideo" @click="openDialog(media.dmsFk)" + :data-cy="`file-${index+1}`" /> </QCard> </div> diff --git a/src/pages/Customer/Card/CustomerDescriptor.vue b/src/pages/Customer/Card/CustomerDescriptor.vue index 89f9d9449..e3156dd6d 100644 --- a/src/pages/Customer/Card/CustomerDescriptor.vue +++ b/src/pages/Customer/Card/CustomerDescriptor.vue @@ -118,14 +118,6 @@ const debtWarning = computed(() => { > <QTooltip>{{ t('Allowed substitution') }}</QTooltip> </QIcon> - <QIcon - v-if="customer?.isFreezed" - name="vn:frozen" - size="xs" - color="primary" - > - <QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip> - </QIcon> <QIcon v-if="!entity.account?.active" color="primary" @@ -150,6 +142,14 @@ const debtWarning = computed(() => { > <QTooltip>{{ t('customer.card.notChecked') }}</QTooltip> </QIcon> + <QIcon + v-if="entity?.isFreezed" + name="vn:frozen" + size="xs" + color="primary" + > + <QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip> + </QIcon> <QBtn v-if="entity.unpaid" flat @@ -163,13 +163,13 @@ const debtWarning = computed(() => { <br /> {{ t('unpaidDated', { - dated: toDate(customer.unpaid?.dated), + dated: toDate(entity.unpaid?.dated), }) }} <br /> {{ t('unpaidAmount', { - amount: toCurrency(customer.unpaid?.amount), + amount: toCurrency(entity.unpaid?.amount), }) }} </QTooltip> diff --git a/src/pages/Customer/Card/CustomerNotes.vue b/src/pages/Customer/Card/CustomerNotes.vue index 189b59904..5a078b0cb 100644 --- a/src/pages/Customer/Card/CustomerNotes.vue +++ b/src/pages/Customer/Card/CustomerNotes.vue @@ -1,28 +1,15 @@ <script setup> -import { computed } from 'vue'; -import { useRoute } from 'vue-router'; import VnNotes from 'src/components/ui/VnNotes.vue'; - -const route = useRoute(); - -const noteFilter = computed(() => { - return { - order: 'created DESC', - where: { - clientFk: `${route.params.id}`, - }, - }; -}); </script> - <template> <VnNotes url="clientObservations" :add-note="true" - :filter="noteFilter" - :body="{ clientFk: route.params.id }" + :filter="{ where: { clientFk: $route.params.id } }" + :body="{ clientFk: $route.params.id }" style="overflow-y: auto" :select-type="true" required + order="created DESC" /> </template> diff --git a/src/pages/Customer/Card/CustomerSummary.vue b/src/pages/Customer/Card/CustomerSummary.vue index 324da0771..c98bf1ffb 100644 --- a/src/pages/Customer/Card/CustomerSummary.vue +++ b/src/pages/Customer/Card/CustomerSummary.vue @@ -325,7 +325,7 @@ const sumRisk = ({ clientRisks }) => { </QCard> <QCard class="vn-max"> <VnTitle :text="t('Latest tickets')" /> - <CustomerSummaryTable /> + <CustomerSummaryTable :id="entityId" /> </QCard> </template> </CardSummary> diff --git a/src/pages/Customer/components/CustomerSummaryTable.vue b/src/pages/Customer/components/CustomerSummaryTable.vue index bb6f4442b..09c7e714c 100644 --- a/src/pages/Customer/components/CustomerSummaryTable.vue +++ b/src/pages/Customer/components/CustomerSummaryTable.vue @@ -20,7 +20,12 @@ const { t } = useI18n(); const route = useRoute(); const router = useRouter(); const { viewSummary } = useSummaryDialog(); - +const $props = defineProps({ + id: { + type: Number, + default: null, + }, +}); const filter = { include: [ { @@ -43,7 +48,7 @@ const filter = { }, }, ], - where: { clientFk: route.params.id }, + where: { clientFk: $props.id ?? route.params.id }, order: ['shipped DESC', 'id'], limit: 30, }; diff --git a/src/pages/Customer/composables/__tests__/getAddresses.spec.js b/src/pages/Customer/composables/__tests__/getAddresses.spec.js index 9e04a83cc..714693809 100644 --- a/src/pages/Customer/composables/__tests__/getAddresses.spec.js +++ b/src/pages/Customer/composables/__tests__/getAddresses.spec.js @@ -17,9 +17,17 @@ describe('getAddresses', () => { expect(axios.get).toHaveBeenCalledWith(`Clients/${clientId}/addresses`, { params: { filter: JSON.stringify({ - fields: ['nickname', 'street', 'city', 'id'], + include: [ + { + relation: 'client', + scope: { + fields: ['defaultAddressFk'], + }, + }, + ], + fields: ['nickname', 'street', 'city', 'id', 'isActive', 'clientFk'], where: { isActive: true }, - order: 'nickname ASC', + order: ['isDefaultAddress DESC', 'isActive DESC', 'nickname ASC'], }), }, }); @@ -30,4 +38,4 @@ describe('getAddresses', () => { expect(axios.get).not.toHaveBeenCalled(); }); -}); \ No newline at end of file +}); diff --git a/src/pages/Customer/composables/getAddresses.js b/src/pages/Customer/composables/getAddresses.js index e65e64455..1698388ee 100644 --- a/src/pages/Customer/composables/getAddresses.js +++ b/src/pages/Customer/composables/getAddresses.js @@ -1,15 +1,23 @@ import axios from 'axios'; -export async function getAddresses(clientId, _filter = {}) { +export async function getAddresses(clientId, _filter = {}) { if (!clientId) return; const filter = { ..._filter, - fields: ['nickname', 'street', 'city', 'id'], + include: [ + { + relation: 'client', + scope: { + fields: ['defaultAddressFk'], + }, + }, + ], + fields: ['nickname', 'street', 'city', 'id', 'isActive', 'clientFk'], where: { isActive: true }, - order: 'nickname ASC', + order: ['isDefaultAddress DESC', 'isActive DESC', 'nickname ASC'], }; const params = { filter: JSON.stringify(filter) }; return await axios.get(`Clients/${clientId}/addresses`, { params, }); -}; \ No newline at end of file +} diff --git a/src/pages/Entry/Card/EntryBuys.vue b/src/pages/Entry/Card/EntryBuys.vue index 15f8cc20c..684ed5f59 100644 --- a/src/pages/Entry/Card/EntryBuys.vue +++ b/src/pages/Entry/Card/EntryBuys.vue @@ -62,9 +62,10 @@ const columns = [ name: 'workerFk', component: 'select', attrs: { - url: 'Workers/search', + url: 'TicketRequests/getItemTypeWorker', fields: ['id', 'nickname'], optionLabel: 'nickname', + sortBy: 'nickname ASC', optionValue: 'id', }, visible: false, @@ -655,7 +656,6 @@ onMounted(() => { :without-header="!editableMode" :with-filters="editableMode" :right-search="editableMode" - :right-search-icon="true" :row-click="false" :columns="columns" :beforeSaveFn="beforeSave" diff --git a/src/pages/Entry/EntryFilter.vue b/src/pages/Entry/EntryFilter.vue index 8c60918a8..c283e4a0b 100644 --- a/src/pages/Entry/EntryFilter.vue +++ b/src/pages/Entry/EntryFilter.vue @@ -145,6 +145,7 @@ const entryFilterPanel = ref(); v-model="params.agencyModeId" @update:model-value="searchFn()" url="AgencyModes" + sort-by="name ASC" :fields="['id', 'name']" hide-selected dense @@ -248,7 +249,7 @@ const entryFilterPanel = ref(); <i18n> en: params: - isExcludedFromAvailable: Inventory + isExcludedFromAvailable: Is excluded isOrdered: Ordered isReceived: Received isConfirmed: Confirmed diff --git a/src/pages/Entry/EntryStockBought.vue b/src/pages/Entry/EntryStockBought.vue index 888dd205c..41f78617c 100644 --- a/src/pages/Entry/EntryStockBought.vue +++ b/src/pages/Entry/EntryStockBought.vue @@ -19,6 +19,7 @@ const { t } = useI18n(); const quasar = useQuasar(); const state = useState(); const user = state.getUser(); +const footer = ref({ bought: 0, reserve: 0 }); const columns = computed(() => [ { align: 'left', @@ -38,16 +39,14 @@ const columns = computed(() => [ cardVisible: true, create: true, attrs: { - url: 'Workers/activeWithInheritedRole', - fields: ['id', 'name', 'nickname'], - where: { role: 'buyer' }, - optionFilter: 'firstName', + url: 'TicketRequests/getItemTypeWorker', + fields: ['id', 'nickname'], optionLabel: 'nickname', + sortBy: 'nickname ASC', optionValue: 'id', - useLike: false, }, columnFilter: false, - width: '70px', + width: '50px', }, { align: 'center', @@ -58,6 +57,7 @@ const columns = computed(() => [ component: 'number', summation: true, width: '50px', + format: ({ reserve }, dashIfEmpty) => dashIfEmpty(round(reserve)), }, { align: 'center', @@ -65,6 +65,7 @@ const columns = computed(() => [ name: 'bought', summation: true, cardVisible: true, + style: ({ reserve, bought }) => boughtStyle(bought, reserve), columnFilter: false, }, { @@ -95,7 +96,6 @@ const columns = computed(() => [ }, }, ], - dataCy: 'table-actions', }, ]); @@ -137,20 +137,20 @@ function openDialog() { } function setFooter(data) { - const footer = { - bought: 0, - reserve: 0, - }; + footer.value = { bought: 0, reserve: 0 }; data.forEach((row) => { - footer.bought += row?.bought; - footer.reserve += row?.reserve; + footer.value.bought += row?.bought; + footer.value.reserve += row?.reserve; }); - tableRef.value.footer = footer; } function round(value) { return Math.round(value * 100) / 100; } + +function boughtStyle(bought, reserve) { + return reserve < bought ? { color: 'var(--q-negative)' } : ''; +} </script> <template> <VnSubToolbar> @@ -253,24 +253,14 @@ function round(value) { <WorkerDescriptorProxy :id="row?.workerFk" /> </span> </template> - <template #column-bought="{ row }"> - <span :class="{ 'text-negative': row.reserve < row.bought }"> - {{ row?.bought }} - </span> - </template> <template #column-footer-reserve> <span> - {{ round(tableRef.footer.reserve) }} + {{ round(footer.reserve) }} </span> </template> <template #column-footer-bought> - <span - :class="{ - 'text-negative': - tableRef.footer.reserve < tableRef.footer.bought, - }" - > - {{ round(tableRef.footer.bought) }} + <span :style="boughtStyle(footer?.bought, footer?.reserve)"> + {{ round(footer.bought) }} </span> </template> </VnTable> @@ -286,7 +276,7 @@ function round(value) { justify-content: center; } .column { - min-width: 40%; + min-width: 35%; margin-top: 5%; display: flex; flex-direction: column; diff --git a/src/pages/Entry/EntryStockBoughtDetail.vue b/src/pages/Entry/EntryStockBoughtDetail.vue index 1a37994d9..4f002ecb9 100644 --- a/src/pages/Entry/EntryStockBoughtDetail.vue +++ b/src/pages/Entry/EntryStockBoughtDetail.vue @@ -14,7 +14,7 @@ const $props = defineProps({ required: true, }, dated: { - type: Date, + type: [Date, String], required: true, }, }); diff --git a/src/pages/InvoiceIn/Card/InvoiceInSummary.vue b/src/pages/InvoiceIn/Card/InvoiceInSummary.vue index d358601d3..18602f043 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInSummary.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInSummary.vue @@ -185,6 +185,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; data-key="InvoiceInSummary" :url="`InvoiceIns/${entityId}/summary`" @on-fetch="(data) => init(data)" + module-name="InvoiceIn" > <template #header="{ entity }"> <div>{{ entity.id }} - {{ entity.supplier?.name }}</div> diff --git a/src/pages/Item/Card/ItemBarcode.vue b/src/pages/Item/Card/ItemBarcode.vue index 590b524cd..53b4514b7 100644 --- a/src/pages/Item/Card/ItemBarcode.vue +++ b/src/pages/Item/Card/ItemBarcode.vue @@ -94,6 +94,7 @@ const submit = async (rows) => { icon="add_circle" v-shortcut="'+'" flat + data-cy="addBarcode_input" > <QTooltip> {{ t('Add barcode') }} diff --git a/src/pages/Item/Card/ItemDiary.vue b/src/pages/Item/Card/ItemDiary.vue index 31b3c328e..f839c1f71 100644 --- a/src/pages/Item/Card/ItemDiary.vue +++ b/src/pages/Item/Card/ItemDiary.vue @@ -12,7 +12,7 @@ import FetchData from 'components/FetchData.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; -import { toDateFormat } from 'src/filters/date.js'; +import { toDateTimeFormat } from 'src/filters/date.js'; import { dashIfEmpty } from 'src/filters'; import { date } from 'quasar'; import { useState } from 'src/composables/useState'; @@ -143,7 +143,12 @@ onMounted(async () => { const fetchItemBalances = async () => await arrayDataItemBalances.fetch({}); const getBadgeAttrs = (_date) => { - const isSameDate = date.isSameDate(today, _date); + let today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + let timeTicket = new Date(_date); + timeTicket.setHours(0, 0, 0, 0); + + const isSameDate = date.isSameDate(today, timeTicket); const attrs = { 'text-color': isSameDate ? 'black' : 'white', color: isSameDate ? 'warning' : 'transparent', @@ -153,15 +158,10 @@ const getBadgeAttrs = (_date) => { const scrollToToday = async () => { await nextTick(); - const todayCell = document.querySelector(`td[data-date="${today.toISOString()}"]`); - if (todayCell) { - todayCell.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } -}; - -const formatDateForAttribute = (dateValue) => { - if (dateValue instanceof Date) return date.formatDate(dateValue, 'YYYY-MM-DD'); - return dateValue; + const todayCell = document.querySelector( + `td[data-date="${date.formatDate(today, 'YYYY-MM-DD')}"]`, + ); + if (todayCell) todayCell.scrollIntoView({ behavior: 'smooth', block: 'center' }); }; async function updateWarehouse(warehouseFk) { @@ -237,14 +237,14 @@ async function updateWarehouse(warehouseFk) { </QTd> </template> <template #body-cell-date="{ row }"> - <QTd @click.stop :data-date="formatDateForAttribute(row.shipped)"> + <QTd @click.stop :data-date="row?.shipped.substring(0, 10)"> <QBadge v-bind="getBadgeAttrs(row.shipped)" class="q-ma-none" dense style="font-size: 14px" > - {{ toDateFormat(row.shipped) }} + {{ toDateTimeFormat(row.shipped) }} </QBadge> </QTd> </template> diff --git a/src/pages/Item/Card/ItemLastEntries.vue b/src/pages/Item/Card/ItemLastEntries.vue index 7d8890c2b..1fb8bc287 100644 --- a/src/pages/Item/Card/ItemLastEntries.vue +++ b/src/pages/Item/Card/ItemLastEntries.vue @@ -11,7 +11,6 @@ import { toCurrency } from 'filters/index'; import { useArrayData } from 'composables/useArrayData'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; - const { t } = useI18n(); const route = useRoute(); const from = ref(); @@ -41,7 +40,7 @@ const itemLastEntries = ref([]); const columns = computed(() => [ { - label: 'Nv', + label: 'NV', name: 'ig', align: 'center', }, @@ -70,6 +69,7 @@ const columns = computed(() => [ field: 'reference', align: 'center', format: (_, row) => toCurrency(row.price2) + ' / ' + toCurrency(row.price3), + style: (row) => highlightedRow(row), }, { label: t('lastEntries.printedStickers'), @@ -84,6 +84,7 @@ const columns = computed(() => [ field: 'stickers', align: 'center', format: (val) => dashIfEmpty(val), + style: (row) => highlightedRow(row), }, { label: 'Packing', @@ -102,12 +103,14 @@ const columns = computed(() => [ name: 'stems', field: 'stems', align: 'center', + style: (row) => highlightedRow(row), }, { label: t('lastEntries.quantity'), name: 'quantity', field: 'quantity', align: 'center', + style: (row) => highlightedRow(row), }, { label: t('lastEntries.cost'), @@ -120,12 +123,14 @@ const columns = computed(() => [ name: 'weight', field: 'weight', align: 'center', + style: (row) => highlightedRow(row), }, { label: t('lastEntries.cube'), name: 'cube', field: 'packagingFk', align: 'center', + style: (row) => highlightedRow(row), }, { label: t('lastEntries.supplier'), @@ -208,6 +213,14 @@ onMounted(async () => { function getBadgeClass(groupingMode, expectedGrouping) { return groupingMode === expectedGrouping ? 'accent-badge' : 'simple-badge'; } + +function highlightedRow(row) { + return row?.isInventorySupplier + ? { + 'background-color': 'var(--vn-section-hover-color)', + } + : ''; +} </script> <template> <VnSubToolbar> @@ -236,7 +249,7 @@ function getBadgeClass(groupingMode, expectedGrouping) { :no-data-label="t('globals.noResults')" > <template #body-cell-ig="{ row }"> - <QTd class="text-center"> + <QTd class="text-center" :style="highlightedRow(row)"> <QIcon :name="row.isIgnored ? 'check_box' : 'check_box_outline_blank'" style="color: var(--vn-label-color)" @@ -245,38 +258,38 @@ function getBadgeClass(groupingMode, expectedGrouping) { </QTd> </template> <template #body-cell-warehouse="{ row }"> - <QTd> + <QTd :style="highlightedRow(row)"> <span>{{ row.warehouse }}</span> </QTd> </template> <template #body-cell-date="{ row }"> - <QTd class="text-center"> + <QTd class="text-center" :style="highlightedRow(row)"> <VnDateBadge :date="row.landed" /> </QTd> </template> <template #body-cell-entry="{ row }"> - <QTd @click.stop> + <QTd @click.stop :style="highlightedRow(row)"> <div class="full-width flex justify-center"> <EntryDescriptorProxy :id="row.entryFk" class="q-ma-none" dense /> <span class="link">{{ row.entryFk }}</span> </div> </QTd> </template> - <template #body-cell-pvp="{ value }"> - <QTd @click.stop class="text-center"> + <template #body-cell-pvp="{ row, value }"> + <QTd @click.stop class="text-center" :style="highlightedRow(row)"> <span> {{ value }}</span> - <QTooltip> {{ t('lastEntries.grouping') }}/Packing </QTooltip></QTd - > + <QTooltip> {{ t('lastEntries.grouping') }}/Packing </QTooltip> + </QTd> </template> <template #body-cell-printedStickers="{ row }"> - <QTd @click.stop class="text-center"> + <QTd @click.stop class="text-center" :style="highlightedRow(row)"> <span style="color: var(--vn-label-color)"> {{ row.printedStickers }}</span > </QTd> </template> <template #body-cell-packing="{ row }"> - <QTd @click.stop> + <QTd @click.stop :style="highlightedRow(row)"> <QBadge class="center-content" :class="getBadgeClass(row.groupingMode, 'packing')" @@ -288,7 +301,7 @@ function getBadgeClass(groupingMode, expectedGrouping) { </QTd> </template> <template #body-cell-grouping="{ row }"> - <QTd @click.stop> + <QTd @click.stop :style="highlightedRow(row)"> <QBadge class="center-content" :class="getBadgeClass(row.groupingMode, 'grouping')" @@ -300,7 +313,7 @@ function getBadgeClass(groupingMode, expectedGrouping) { </QTd> </template> <template #body-cell-cost="{ row }"> - <QTd @click.stop class="text-center"> + <QTd @click.stop class="text-center" :style="highlightedRow(row)"> <span> {{ toCurrency(row.cost, 'EUR', 3) }} <QTooltip> @@ -319,7 +332,7 @@ function getBadgeClass(groupingMode, expectedGrouping) { </QTd> </template> <template #body-cell-supplier="{ row }"> - <QTd @click.stop> + <QTd @click.stop :style="highlightedRow(row)"> <div class="full-width flex justify-left"> <QBadge :class=" @@ -354,7 +367,6 @@ function getBadgeClass(groupingMode, expectedGrouping) { .th :first-child { .td { text-align: center; - background-color: red; } } .accent-badge { diff --git a/src/pages/Item/ItemRequest.vue b/src/pages/Item/ItemRequest.vue index 76e4b8083..43fc611d8 100644 --- a/src/pages/Item/ItemRequest.vue +++ b/src/pages/Item/ItemRequest.vue @@ -226,7 +226,6 @@ const onDenyAccept = (_, responseData) => { order="shipped ASC, isOk ASC" :columns="columns" :user-params="userParams" - :is-editable="true" :right-search="false" auto-load :disable-option="{ card: true }" diff --git a/src/pages/Monitor/Ticket/MonitorTickets.vue b/src/pages/Monitor/Ticket/MonitorTickets.vue index 2ec862df0..782175cd6 100644 --- a/src/pages/Monitor/Ticket/MonitorTickets.vue +++ b/src/pages/Monitor/Ticket/MonitorTickets.vue @@ -17,6 +17,7 @@ import MonitorTicketFilter from './MonitorTicketFilter.vue'; import TicketProblems from 'src/components/TicketProblems.vue'; import VnDateBadge from 'src/components/common/VnDateBadge.vue'; import { useStateStore } from 'src/stores/useStateStore'; +import useOpenURL from 'src/composables/useOpenURL'; const DEFAULT_AUTO_REFRESH = 2 * 60 * 1000; const { t } = useI18n(); @@ -321,8 +322,7 @@ const totalPriceColor = (ticket) => { if (total > 0 && total < 50) return 'warning'; }; -const openTab = (id) => - window.open(`#/ticket/${id}/sale`, '_blank', 'noopener, noreferrer'); +const openTab = (id) => useOpenURL(`#/ticket/${id}/sale`); </script> <template> <FetchData @@ -397,6 +397,7 @@ const openTab = (id) => default-mode="table" auto-load :row-click="({ id }) => openTab(id)" + :row-ctrl-click="(_, { id }) => openTab(id)" :disable-option="{ card: true }" :user-params="{ from, to, scopeDays: 0 }" > diff --git a/src/pages/Monitor/locale/en.yml b/src/pages/Monitor/locale/en.yml index 496c8761a..c049a5e53 100644 --- a/src/pages/Monitor/locale/en.yml +++ b/src/pages/Monitor/locale/en.yml @@ -22,7 +22,7 @@ salesTicketsTable: notVisible: Not visible purchaseRequest: Purchase request clientFrozen: Client frozen - risk: Risk + risk: Excess risk componentLack: Component lack tooLittle: Ticket too little identifier: Identifier diff --git a/src/pages/Monitor/locale/es.yml b/src/pages/Monitor/locale/es.yml index f6a29879f..a02d7f36f 100644 --- a/src/pages/Monitor/locale/es.yml +++ b/src/pages/Monitor/locale/es.yml @@ -22,7 +22,7 @@ salesTicketsTable: notVisible: No visible purchaseRequest: Petición de compra clientFrozen: Cliente congelado - risk: Riesgo + risk: Exceso de riesgo componentLack: Faltan componentes tooLittle: Ticket demasiado pequeño identifier: Identificador diff --git a/src/pages/Order/Card/OrderCatalog.vue b/src/pages/Order/Card/OrderCatalog.vue index 4b3992f21..dbb66c0ec 100644 --- a/src/pages/Order/Card/OrderCatalog.vue +++ b/src/pages/Order/Card/OrderCatalog.vue @@ -10,6 +10,7 @@ import OrderCatalogFilter from 'src/pages/Order/Card/OrderCatalogFilter.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import { useArrayData } from 'src/composables/useArrayData'; import RightMenu from 'src/components/common/RightMenu.vue'; +import { onUnmounted } from 'vue'; const route = useRoute(); const router = useRouter(); @@ -23,16 +24,40 @@ const catalogParams = { const arrayData = useArrayData(dataKey, { url: 'Orders/CatalogFilter', userParams: catalogParams, + exprBuilder, + searchUrl: 'table', }); const store = arrayData.store; const tags = ref([]); const itemRefs = ref({}); -onMounted(() => { +onMounted(async () => { stateStore.rightDrawer = true; checkOrderConfirmation(); + + if ( + arrayData.store.userParams && + Object.keys(arrayData.store.userParams).some((key) => !key.startsWith('order')) + ) { + await arrayData.fetch({}); + } }); +onUnmounted(() => { + arrayData.destroy(); +}); + +function exprBuilder(param, value) { + switch (param) { + case 'categoryFk': + case 'typeFk': + return { [param]: value }; + case 'search': + if (/^\d+$/.test(value)) return { 'i.id': value }; + else return { 'i.name': { like: `%${value}%` } }; + } +} + async function checkOrderConfirmation() { const response = await axios.get(`Orders/${route.params.id}`); if (response.data.isConfirmed === 1) { @@ -96,6 +121,7 @@ watch( :tag-value="tagValue" :tags="tags" :initial-catalog-params="catalogParams" + :arrayData /> </template> </RightMenu> diff --git a/src/pages/Order/Card/OrderCatalogFilter.vue b/src/pages/Order/Card/OrderCatalogFilter.vue index 76e608983..d16a92017 100644 --- a/src/pages/Order/Card/OrderCatalogFilter.vue +++ b/src/pages/Order/Card/OrderCatalogFilter.vue @@ -24,6 +24,10 @@ const props = defineProps({ type: Array, required: true, }, + arrayData: { + type: Object, + required: true, + }, }); const { t } = useI18n(); @@ -74,17 +78,6 @@ const loadTypes = async (id) => { typeList.value = data; }; -function exprBuilder(param, value) { - switch (param) { - case 'categoryFk': - case 'typeFk': - return { [param]: value }; - case 'search': - if (/^\d+$/.test(value)) return { 'i.id': value }; - else return { 'i.name': { like: `%${value}%` } }; - } -} - const applyTags = (tagInfo, params, search) => { if (!tagInfo || !tagInfo.values.length) { params.tagGroups = null; @@ -152,9 +145,8 @@ function addOrder(value, field, params) { :data-key="props.dataKey" :hidden-tags="['filter', 'orderFk', 'orderBy']" :unremovable-params="['orderFk', 'orderBy']" - :expr-builder="exprBuilder" :custom-tags="['tagGroups', 'categoryFk']" - :redirect="false" + :arrayData > <template #tags="{ tag, formatFn }"> <strong v-if="tag.label === 'typeFk' && typeList"> diff --git a/src/pages/Order/OrderList.vue b/src/pages/Order/OrderList.vue index 40990f329..ff7c46802 100644 --- a/src/pages/Order/OrderList.vue +++ b/src/pages/Order/OrderList.vue @@ -1,6 +1,6 @@ <script setup> import { useI18n } from 'vue-i18n'; -import { computed, ref, onMounted } from 'vue'; +import { computed, ref, onMounted, watch } from 'vue'; import { dashIfEmpty, toCurrency, toDate } from 'src/filters'; import { toDateTimeFormat } from 'src/filters/date'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; @@ -16,6 +16,7 @@ import VnTable from 'src/components/VnTable/VnTable.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnSection from 'src/components/common/VnSection.vue'; +import { getAddresses } from '../Customer/composables/getAddresses'; const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); @@ -24,6 +25,11 @@ const agencyList = ref([]); const route = useRoute(); const addressOptions = ref([]); const dataKey = 'OrderList'; +const formInitialData = ref({ + active: true, + addressId: null, + clientFk: null, +}); const columns = computed(() => [ { @@ -147,27 +153,54 @@ const columns = computed(() => [ ], }, ]); -onMounted(() => { - if (!route.query.createForm) return; - const clientId = route.query.createForm; - const id = JSON.parse(clientId); - fetchClientAddress(id.clientFk); +onMounted(async () => { + if (!route.query) return; + if (route.query?.createForm) { + const query = JSON.parse(route.query?.createForm); + formInitialData.value = query; + await onClientSelected({ ...formInitialData.value, clientId: query?.clientFk }); + } else if (route.query?.table) { + const query = JSON.parse(route.query?.table); + const clientId = query?.clientFk; + if (clientId) await onClientSelected({ clientId }); + } + if (tableRef.value) tableRef.value.create.formInitialData = formInitialData.value; }); -async function fetchClientAddress(id, formData = {}) { - const { data } = await axios.get( - `Clients/${id}/addresses?filter[order]=isActive DESC` - ); +watch( + () => route.query.table, + async (newValue) => { + if (newValue) { + const clientId = +JSON.parse(newValue)?.clientFk; + if (clientId) await onClientSelected({ clientId }); + if (tableRef.value) + tableRef.value.create.formInitialData = formInitialData.value; + } + }, + { immediate: true }, +); + +async function onClientSelected({ clientId: id }, formData = {}) { + const { data } = await getAddresses(id); addressOptions.value = data; - formData.addressId = data.defaultAddressFk; - fetchAgencies(formData); + formData.defaultAddressFk = data[0].client.defaultAddressFk; + formData.addressId = formData.defaultAddressFk; + + formInitialData.value = { addressId: formData.addressId, clientFk: id }; + await fetchAgencies(formData); } async function fetchAgencies({ landed, addressId }) { if (!landed || !addressId) return (agencyList.value = []); const { data } = await axios.get('Agencies/landsThatDay', { - params: { addressFk: addressId, landed }, + params: { + filter: JSON.stringify({ + order: ['name ASC', 'agencyMode DESC', 'agencyModeFk ASC'], + }), + addressFk: addressId, + landed, + }, }); agencyList.value = data; } @@ -206,11 +239,7 @@ const getDateColor = (date) => { onDataSaved: (url) => { tableRef.redirect(`${url}/catalog`); }, - formInitialData: { - active: true, - addressId: null, - clientFk: null, - }, + formInitialData, }" :user-params="{ showEmpty: false }" :columns="columns" @@ -242,7 +271,7 @@ const getDateColor = (date) => { :include="{ relation: 'addresses' }" v-model="data.clientFk" :label="t('module.customer')" - @update:model-value="(id) => fetchClientAddress(id, data)" + @update:model-value="(id) => onClientSelected(id, data)" > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -258,6 +287,7 @@ const getDateColor = (date) => { </template> </VnSelect> <VnSelect + :disable="!data.clientFk" v-model="data.addressId" :options="addressOptions" :label="t('module.address')" @@ -266,7 +296,22 @@ const getDateColor = (date) => { @update:model-value="() => fetchAgencies(data)" > <template #option="scope"> - <QItem v-bind="scope.itemProps"> + <QItem + v-bind="scope.itemProps" + :class="{ disabled: !scope.opt.isActive }" + > + <QItemSection style="min-width: min-content" avatar> + <QIcon + v-if=" + scope.opt.isActive && + data.defaultAddressFk === scope.opt.id + " + size="sm" + color="grey" + name="star" + class="fill-icon" + /> + </QItemSection> <QItemSection> <QItemLabel :class="{ @@ -284,6 +329,9 @@ const getDateColor = (date) => { {{ scope.opt?.street }}, {{ scope.opt?.city }} </QItemLabel> + <QItemLabel caption> + {{ `#${scope.opt?.id}` }} + </QItemLabel> </QItemSection> </QItem> </template> @@ -291,6 +339,7 @@ const getDateColor = (date) => { <VnInputDate v-model="data.landed" :label="t('module.landed')" + data-cy="landedDate" @update:model-value="() => fetchAgencies(data)" /> <VnSelect diff --git a/src/pages/Route/Agency/composables/__tests__/getAgencies.spec.js b/src/pages/Route/Agency/composables/__tests__/getAgencies.spec.js index ccf7872cb..99966569c 100644 --- a/src/pages/Route/Agency/composables/__tests__/getAgencies.spec.js +++ b/src/pages/Route/Agency/composables/__tests__/getAgencies.spec.js @@ -27,14 +27,17 @@ describe('getAgencies', () => { landed: 'true', }; const filter = { - fields: ['nickname', 'street', 'city', 'id'], + fields: ['name', 'street', 'city', 'id'], where: { isActive: true }, - order: 'nickname ASC', + order: ['name ASC'], }; await getAgencies(formData, null, filter); - expect(axios.get).toHaveBeenCalledWith('Agencies/getAgenciesWithWarehouse', generateParams(formData, filter)); + expect(axios.get).toHaveBeenCalledWith( + 'Agencies/getAgenciesWithWarehouse', + generateParams(formData, filter), + ); }); it('should not call API when formData is missing required landed field', async () => { @@ -64,19 +67,19 @@ describe('getAgencies', () => { it('should return options and agency when default agency is found', async () => { const formData = { warehouseId: '123', addressId: '456', landed: 'true' }; const client = { defaultAddress: { agencyModeFk: 'Agency1' } }; - + const { options, agency } = await getAgencies(formData, client); - + expect(options).toEqual(response.data); expect(agency).toEqual(response.data[0]); - }); + }); - it('should return options and agency when client is not provided', async () => { + it('should return options and agency when client is not provided', async () => { const formData = { warehouseId: '123', addressId: '456', landed: 'true' }; - + const { options, agency } = await getAgencies(formData); - + expect(options).toEqual(response.data); expect(agency).toBeNull(); - }); + }); }); diff --git a/src/pages/Route/Agency/composables/getAgencies.js b/src/pages/Route/Agency/composables/getAgencies.js index 850f87456..180ac943e 100644 --- a/src/pages/Route/Agency/composables/getAgencies.js +++ b/src/pages/Route/Agency/composables/getAgencies.js @@ -1,14 +1,14 @@ import axios from 'axios'; -import agency from 'src/router/modules/agency'; export async function getAgencies(formData, client, _filter = {}) { if (!formData.warehouseId || !formData.addressId || !formData.landed) return; - + const filter = { - ..._filter + ..._filter, + order: ['name ASC'], }; - let defaultAgency = null; + let agency = null; let params = { filter: JSON.stringify(filter), warehouseFk: formData.warehouseId, @@ -16,11 +16,15 @@ export async function getAgencies(formData, client, _filter = {}) { landed: formData.landed, }; - const { data } = await axios.get('Agencies/getAgenciesWithWarehouse', { params }); + const { data: options } = await axios.get('Agencies/getAgenciesWithWarehouse', { + params, + }); - if(data && client) { - defaultAgency = data.find((agency) => agency.agencyModeFk === client.defaultAddress.agencyModeFk ); - }; - - return {options: data, agency: defaultAgency} + if (options && client) { + agency = options.find( + ({ agencyModeFk }) => agencyModeFk === client.defaultAddress.agencyModeFk, + ); + } + + return { options, agency }; } diff --git a/src/pages/Route/Card/RouteAutonomousFilter.vue b/src/pages/Route/Card/RouteAutonomousFilter.vue index 3be409ec9..f70f60e1c 100644 --- a/src/pages/Route/Card/RouteAutonomousFilter.vue +++ b/src/pages/Route/Card/RouteAutonomousFilter.vue @@ -44,8 +44,7 @@ const exprBuilder = (param, value) => { <template> <FetchData url="AgencyModes" - :filter="{ fields: ['id', 'name'] }" - sort-by="name ASC" + :filter="{ fields: ['id', 'name'], order: ['name ASC'] }" @on-fetch="(data) => (agencyList = data)" auto-load /> diff --git a/src/pages/Route/RouteAutonomous.vue b/src/pages/Route/RouteAutonomous.vue index 23c920a57..3047cdf86 100644 --- a/src/pages/Route/RouteAutonomous.vue +++ b/src/pages/Route/RouteAutonomous.vue @@ -180,6 +180,7 @@ const onDmsSaved = async (dms, response) => { rows: dmsDialog.value.rowsToCreateInvoiceIn, dms: response.data, }); + notify(t('Data saved'), 'positive'); } dmsDialog.value.show = false; dmsDialog.value.initialForm = null; @@ -243,7 +244,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); </template> <template #column-invoiceInFk="{ row }"> <span class="link" @click.stop> - {{ row.invoiceInFk }} + {{ row.supplierRef }} <InvoiceInDescriptorProxy v-if="row.invoiceInFk" :id="row.invoiceInFk" /> </span> </template> diff --git a/src/pages/Route/Vehicle/Card/VehicleSummary.vue b/src/pages/Route/Vehicle/Card/VehicleSummary.vue index 981870cb2..e4b0a9497 100644 --- a/src/pages/Route/Vehicle/Card/VehicleSummary.vue +++ b/src/pages/Route/Vehicle/Card/VehicleSummary.vue @@ -22,7 +22,12 @@ const links = { }; </script> <template> - <CardSummary data-key="Vehicle" :url="`Vehicles/${entityId}`" :filter="VehicleFilter"> + <CardSummary + data-key="Vehicle" + :url="`Vehicles/${entityId}`" + module-name="Vehicle" + :filter="VehicleFilter" + > <template #header="{ entity }"> <div>{{ entity.id }} - {{ entity.numberPlate }}</div> </template> diff --git a/src/pages/Shelving/ShelvingList.vue b/src/pages/Shelving/ShelvingList.vue index 4e0c21100..651121de8 100644 --- a/src/pages/Shelving/ShelvingList.vue +++ b/src/pages/Shelving/ShelvingList.vue @@ -1,14 +1,17 @@ <script setup> -import VnPaginate from 'components/ui/VnPaginate.vue'; -import CardList from 'components/ui/CardList.vue'; -import VnLv from 'components/ui/VnLv.vue'; +import { computed } from 'vue'; import { useRouter } from 'vue-router'; -import ShelvingFilter from 'pages/Shelving/Card/ShelvingFilter.vue'; -import ShelvingSummary from 'pages/Shelving/Card/ShelvingSummary.vue'; -import { useSummaryDialog } from 'src/composables/useSummaryDialog'; +import { useI18n } from 'vue-i18n'; +import VnTable from 'components/VnTable/VnTable.vue'; import VnSection from 'src/components/common/VnSection.vue'; +import ShelvingFilter from 'pages/Shelving/Card/ShelvingFilter.vue'; +import ShelvingSummary from './Card/ShelvingSummary.vue'; +import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import exprBuilder from './ShelvingExprBuilder.js'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; +const { t } = useI18n(); const router = useRouter(); const { viewSummary } = useSummaryDialog(); const dataKey = 'ShelvingList'; @@ -17,9 +20,56 @@ const filter = { include: [{ relation: 'parking' }], }; -function navigate(id) { - router.push({ path: `/shelving/${id}` }); -} +const columns = computed(() => [ + { + align: 'left', + name: 'code', + label: t('globals.code'), + isId: true, + isTitle: true, + columnFilter: false, + create: true, + }, + { + align: 'left', + name: 'parking', + label: t('shelving.list.parking'), + sortable: true, + format: (val) => val?.code ?? '', + cardVisible: true, + }, + { + align: 'left', + name: 'priority', + label: t('shelving.list.priority'), + sortable: true, + cardVisible: true, + create: true, + }, + { + align: 'left', + name: 'isRecyclable', + label: t('shelving.summary.recyclable'), + sortable: true, + }, + { + align: 'right', + label: '', + name: 'tableActions', + actions: [ + { + title: t('components.smartCard.viewSummary'), + icon: 'preview', + action: (row) => viewSummary(row.id, ShelvingSummary), + isPrimary: true, + }, + ], + }, +]); + +const onDataSaved = ({ id }) => { + router.push({ name: 'ShelvingBasicData', params: { id } }); +}; </script> <template> @@ -37,48 +87,75 @@ function navigate(id) { <ShelvingFilter data-key="ShelvingList" /> </template> <template #body> - <QPage class="column items-center q-pa-md"> - <div class="vn-card-list"> - <VnPaginate :data-key="dataKey"> - <template #body="{ rows }"> - <CardList - v-for="row of rows" - :key="row.id" - :id="row.id" - :title="row.code" - @click="navigate(row.id)" - > - <template #list-items> - <VnLv - :label="$t('shelving.list.parking')" - :title-label="$t('shelving.list.parking')" - :value="row.parking?.code" - /> - <VnLv - :label="$t('shelving.list.priority')" - :value="row?.priority" - /> - </template> - <template #actions> - <QBtn - :label="$t('components.smartCard.openSummary')" - @click.stop="viewSummary(row.id, ShelvingSummary)" - color="primary" - /> - </template> - </CardList> - </template> - </VnPaginate> - </div> - <QPageSticky :offset="[20, 20]"> - <RouterLink :to="{ name: 'ShelvingCreate' }"> - <QBtn fab icon="add" color="primary" v-shortcut="'+'" /> - <QTooltip> - {{ $t('shelving.list.newShelving') }} - </QTooltip> - </RouterLink> - </QPageSticky> - </QPage> + <VnTable + :data-key="dataKey" + :columns="columns" + is-editable="false" + :right-search="false" + :use-model="true" + :disable-option="{ table: true }" + redirect="shelving" + default-mode="card" + :create="{ + urlCreate: 'Shelvings', + title: t('globals.pageTitles.shelvingCreate'), + onDataSaved, + formInitialData: { + parkingFk: null, + priority: null, + code: '', + isRecyclable: false, + }, + }" + > + <template #more-create-dialog="{ data }"> + <VnSelect + v-model="data.parkingFk" + url="Parkings" + option-value="id" + option-label="code" + :label="t('shelving.list.parking')" + :filter-options="['id', 'code']" + :fields="['id', 'code']" + /> + <VnCheckbox + v-model="data.isRecyclable" + :label="t('shelving.summary.recyclable')" + /> + </template> + </VnTable> </template> </VnSection> </template> + +<style lang="scss" scoped> +.list { + display: flex; + flex-direction: column; + align-items: center; + width: 55%; +} +.list-container { + display: flex; + justify-content: center; +} +</style> + +<i18n> + es: + shelving: + list: + parking: Estacionamiento + priority: Prioridad + + summary: + recyclable: Reciclable + en: + shelving: + list: + parking: Parking + priority: Priority + + summary: + recyclable: Recyclable +</i18n> diff --git a/src/pages/Supplier/Card/SupplierFilter.js b/src/pages/Supplier/Card/SupplierFilter.js index 3ce5c3de2..3aabe2c6d 100644 --- a/src/pages/Supplier/Card/SupplierFilter.js +++ b/src/pages/Supplier/Card/SupplierFilter.js @@ -11,6 +11,11 @@ export default { 'isSerious', 'isTrucker', 'account', + 'workerFk', + 'note', + 'isReal', + 'isPayMethodChecked', + 'companySize', ], include: [ { diff --git a/src/pages/Supplier/Card/SupplierFiscalData.vue b/src/pages/Supplier/Card/SupplierFiscalData.vue index ecee5b76b..4293bd41a 100644 --- a/src/pages/Supplier/Card/SupplierFiscalData.vue +++ b/src/pages/Supplier/Card/SupplierFiscalData.vue @@ -108,7 +108,6 @@ function handleLocation(data, location) { <VnAccountNumber v-model="data.account" :label="t('supplier.fiscalData.account')" - clearable data-cy="supplierFiscalDataAccount" insertable :maxlength="10" @@ -185,8 +184,8 @@ function handleLocation(data, location) { /> <VnCheckbox v-model="data.isVies" - :label="t('globals.isVies')" - :info="t('whenActivatingIt')" + :label="t('globals.isVies')" + :info="t('whenActivatingIt')" /> </div> </VnRow> diff --git a/src/pages/Supplier/SupplierList.vue b/src/pages/Supplier/SupplierList.vue index c9625518f..d1d437a19 100644 --- a/src/pages/Supplier/SupplierList.vue +++ b/src/pages/Supplier/SupplierList.vue @@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n'; import VnTable from 'components/VnTable/VnTable.vue'; import VnSection from 'src/components/common/VnSection.vue'; import VnInput from 'src/components/common/VnInput.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; import FetchData from 'src/components/FetchData.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import SupplierSummary from './Card/SupplierSummary.vue'; @@ -53,7 +52,7 @@ const columns = computed(() => [ label: t('globals.alias'), name: 'alias', columnFilter: { - name: 'search', + name: 'nickname', }, cardVisible: true, }, @@ -120,6 +119,21 @@ const columns = computed(() => [ ], }, ]); + +const filterColumns = computed(() => { + const copy = [...columns.value]; + copy.splice(copy.length - 1, 0, { + align: 'left', + label: t('globals.params.provinceFk'), + name: 'provinceFk', + options: provincesOptions.value, + columnFilter: { + component: 'select', + }, + }); + + return copy; +}); </script> <template> <FetchData @@ -130,7 +144,7 @@ const columns = computed(() => [ /> <VnSection :data-key="dataKey" - :columns="columns" + :columns="filterColumns" prefix="supplier" :array-data-props="{ url: 'Suppliers/filter', @@ -165,17 +179,6 @@ const columns = computed(() => [ </template> </VnTable> </template> - <template #moreFilterPanel="{ params, searchFn }"> - <VnSelect - :label="t('globals.params.provinceFk')" - v-model="params.provinceFk" - @update:model-value="searchFn()" - :options="provincesOptions" - filled - dense - class="q-px-sm q-pr-lg" - /> - </template> </VnSection> </template> diff --git a/src/pages/Ticket/Card/TicketDescriptor.vue b/src/pages/Ticket/Card/TicketDescriptor.vue index c5f3233b1..1e585592f 100644 --- a/src/pages/Ticket/Card/TicketDescriptor.vue +++ b/src/pages/Ticket/Card/TicketDescriptor.vue @@ -93,9 +93,9 @@ function ticketFilter(ticket) { <VnLv :label="t('globals.warehouse')" :value="entity.warehouse?.name" /> <VnLv :label="t('globals.alias')" :value="entity.nickname" /> </template> - <template #icons> + <template #icons="{ entity }"> <QCardActions class="q-gutter-x-xs"> - <TicketProblems :row="problems" /> + <TicketProblems :row="{ ...entity?.client, ...problems }" /> </QCardActions> </template> <template #actions="{ entity }"> diff --git a/src/pages/Ticket/Card/TicketExpedition.vue b/src/pages/Ticket/Card/TicketExpedition.vue index f8084ff2f..e9e153b70 100644 --- a/src/pages/Ticket/Card/TicketExpedition.vue +++ b/src/pages/Ticket/Card/TicketExpedition.vue @@ -37,7 +37,6 @@ const expeditionStateTypes = ref([]); const expeditionsFilter = computed(() => ({ where: { ticketFk: route.params.id }, - order: ['created DESC'], })); const ticketArrayData = useArrayData('Ticket'); @@ -105,6 +104,9 @@ const columns = computed(() => [ name: 'created', align: 'left', cardVisible: true, + columnFilter: { + component: 'date', + }, format: (row) => toDateTimeFormat(row.created), }, { @@ -201,7 +203,7 @@ const getExpeditionState = async (expedition) => { const openGrafana = (expeditionFk) => { useOpenURL( - `https://grafana.verdnatura.es/d/de1njb6p5answd/control-de-expediciones?orgId=1&var-expeditionFk=${expeditionFk}` + `https://grafana.verdnatura.es/d/de1njb6p5answd/control-de-expediciones?orgId=1&var-expeditionFk=${expeditionFk}`, ); }; @@ -287,7 +289,7 @@ onMounted(async () => { openConfirmationModal( '', t('expedition.removeExpeditionSubtitle'), - deleteExpedition + deleteExpedition, ) " > @@ -302,7 +304,6 @@ onMounted(async () => { url="Expeditions/filter" search-url="expeditions" :columns="columns" - :filter="expeditionsFilter" v-model:selected="selectedRows" :table="{ 'row-key': 'id', @@ -316,11 +317,14 @@ onMounted(async () => { return { id: value }; case 'packageItemName': return { packagingItemFk: value }; + case 'created': + return { 'e.created': { gte: value } }; } } " :redirect="false" order="created DESC" + :filter="expeditionsFilter" > <template #column-freightItemName="{ row }"> <span class="link" @click.stop> diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index 456a151a3..e69d489c0 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -681,6 +681,17 @@ watch( :disabled-attr="isTicketEditable" > <template #column-statusIcons="{ row }"> + <QIcon + v-if="row.saleGroupFk" + name="inventory_2" + size="xs" + color="primary" + class="cursor-pointer" + > + <QTooltip class="no-pointer-events"> + {{ `saleGroup: ${row.saleGroupFk}` }} + </QTooltip> + </QIcon> <TicketProblems :row="row" /> </template> <template #body-cell-picture="{ row }"> @@ -740,7 +751,7 @@ watch( {{ row?.item?.subName.toUpperCase() }} </div> </div> - <FetchedTags :item="row" :max-length="6" /> + <FetchedTags :item="row.item" :max-length="6" /> <QPopupProxy v-if="row.id && isTicketEditable"> <VnInput v-model="row.concept" diff --git a/src/pages/Ticket/Card/TicketService.vue b/src/pages/Ticket/Card/TicketService.vue index 6ce69a6aa..1bd1548a4 100644 --- a/src/pages/Ticket/Card/TicketService.vue +++ b/src/pages/Ticket/Card/TicketService.vue @@ -121,6 +121,50 @@ async function handleSave() { isSaving.value = false; } } +function validateFields(item) { + // Only validate fields that are being updated + const shouldExist = (field) => !isUpdate || field in item; + + if (!shouldExist('ticketServiceTypeFk') && !item.ticketServiceTypeFk) { + notify('Description is required', 'negative'); + return false; + } + + if (!shouldExist('quantity') && (!item.quantity || item.quantity <= 0)) { + notify('Quantity must be greater than 0', 'negative'); + return false; + } + + if (!shouldExist('price') && (!item.price || item.price < 0)) { + notify('Price must be valid', 'negative'); + return false; + } + + return true; +} + +function beforeSave(data) { + const { creates = [], updates = [] } = data; + const validData = { creates: [], updates: [] }; + + // Validate creates + if (creates.length) { + for (const create of creates) { + create.ticketFk = route.params.id; + if (validateFields(create)) { + validData.creates.push(create); + } + } + } + + // Validate updates + if (updates.length) { + for (const update of updates) { + validData.updates.push(update); + } + } + return validData; +} </script> <template> @@ -141,6 +185,7 @@ async function handleSave() { v-model:selected="selected" :order="['description ASC']" :default-remove="false" + :beforeSaveFn="beforeSave" > <template #moreBeforeActions> <QBtn @@ -170,6 +215,7 @@ async function handleSave() { option-value="id" hide-selected sort-by="name ASC" + :required="true" > <template #form> <TicketCreateServiceType @@ -185,6 +231,7 @@ async function handleSave() { :label="col.label" v-model.number="row.quantity" type="number" + :required="true" min="0" :info="t('service.quantityInfo')" /> @@ -196,6 +243,7 @@ async function handleSave() { :label="col.label" v-model.number="row.price" type="number" + :required="true" min="0" @keyup.enter="handleSave" /> diff --git a/src/pages/Ticket/Card/TicketTransferProxy.vue b/src/pages/Ticket/Card/TicketTransferProxy.vue index 3f3f018df..7d5d82f85 100644 --- a/src/pages/Ticket/Card/TicketTransferProxy.vue +++ b/src/pages/Ticket/Card/TicketTransferProxy.vue @@ -42,7 +42,7 @@ const transferRef = ref(null); /> </div> - <div v-else> + <div style="display: flex; flex-direction: row" v-else> <TicketTransfer ref="transferRef" :ticket="$props.ticket" diff --git a/src/pages/Ticket/Card/TicketVolume.vue b/src/pages/Ticket/Card/TicketVolume.vue index 71b16f878..db78094cf 100644 --- a/src/pages/Ticket/Card/TicketVolume.vue +++ b/src/pages/Ticket/Card/TicketVolume.vue @@ -142,7 +142,7 @@ onMounted(() => (stateStore.rightDrawer = true)); <template #column-concept="{ row }"> <span>{{ row.item.name }}</span> <span class="color-vn-label q-pl-md">{{ row.item.subName }}</span> - <FetchedTags :item="row.item" /> + <FetchedTags :item="row.item" :columns="6" /> </template> <template #column-volume="{ rowIndex }"> <span>{{ packingTypeVolume?.[rowIndex]?.volume }}</span> diff --git a/src/pages/Ticket/TicketAdvance.vue b/src/pages/Ticket/TicketAdvance.vue index 05bd14075..94b4623aa 100644 --- a/src/pages/Ticket/TicketAdvance.vue +++ b/src/pages/Ticket/TicketAdvance.vue @@ -456,6 +456,7 @@ watch( :pagination="{ rowsPerPage: 0 }" :no-data-label="t('globals.noResults')" :right-search="false" + :order="['futureTotalWithVat ASC']" auto-load :disable-option="{ card: true }" > diff --git a/src/pages/Ticket/TicketFilter.vue b/src/pages/Ticket/TicketFilter.vue index c82c0067f..bdf75c826 100644 --- a/src/pages/Ticket/TicketFilter.vue +++ b/src/pages/Ticket/TicketFilter.vue @@ -46,7 +46,12 @@ const getGroupedStates = (data) => { " auto-load /> - <FetchData url="AgencyModes" @on-fetch="(data) => (agencies = data)" auto-load /> + <FetchData + url="AgencyModes" + :filter="{ fields: ['id', 'name'], order: ['name ASC'] }" + @on-fetch="(data) => (agencies = data)" + auto-load + /> <FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load /> <VnFilterPanel :data-key="props.dataKey" :search-button="true"> <template #tags="{ tag, formatFn }"> @@ -74,10 +79,20 @@ const getGroupedStates = (data) => { </QItem> <QItem> <QItemSection> - <VnInputDate v-model="params.from" :label="t('From')" is-outlined /> + <VnInputDate + v-model="params.from" + :label="t('From')" + is-outlined + data-cy="From_date" + /> </QItemSection> <QItemSection> - <VnInputDate v-model="params.to" :label="t('To')" is-outlined /> + <VnInputDate + v-model="params.to" + :label="t('To')" + is-outlined + data-cy="To_date" + /> </QItemSection> </QItem> <QItem> @@ -241,8 +256,6 @@ const getGroupedStates = (data) => { v-model="params.agencyModeFk" @update:model-value="searchFn()" :options="agencies" - option-value="id" - option-label="name" emit-value map-options use-input diff --git a/src/pages/Ticket/TicketList.vue b/src/pages/Ticket/TicketList.vue index 33b841d2d..0fce4a08f 100644 --- a/src/pages/Ticket/TicketList.vue +++ b/src/pages/Ticket/TicketList.vue @@ -1,6 +1,6 @@ <script setup> import axios from 'axios'; -import { computed, ref, onBeforeMount } from 'vue'; +import { computed, ref, onBeforeMount, watch, onMounted } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useStateStore } from 'stores/useStateStore'; import { useI18n } from 'vue-i18n'; @@ -51,10 +51,21 @@ const userParams = { onBeforeMount(() => { initializeFromQuery(); stateStore.rightDrawer = true; - if (!route.query.createForm) return; - onClientSelected(JSON.parse(route.query.createForm)); +}); +onMounted(async () => { + if (!route.query) return; + if (route.query?.createForm) { + formInitialData.value = JSON.parse(route.query?.createForm); + await onClientSelected(formInitialData.value); + } else if (route.query?.table) { + const query = route.query?.table; + const clientId = +JSON.parse(query)?.clientFk; + if (clientId) await onClientSelected({ clientId }); + } + if (tableRef.value) tableRef.value.create.formInitialData = formInitialData.value; }); const initializeFromQuery = () => { + if (!route) return; const query = route.query.table ? JSON.parse(route.query.table) : {}; from.value = query.from || from.toISOString(); to.value = query.to || to.toISOString(); @@ -69,6 +80,7 @@ const companiesOptions = ref([]); const accountingOptions = ref([]); const amountToReturn = ref(); const dataKey = 'TicketList'; +const formInitialData = ref({}); const columns = computed(() => [ { @@ -119,12 +131,16 @@ const columns = computed(() => [ { align: 'left', name: 'shipped', + component: 'time', + columnFilter: false, label: t('ticketList.hour'), format: (row) => toTimeFormat(row.shipped), }, { align: 'left', name: 'zoneLanding', + component: 'time', + columnFilter: false, label: t('ticketList.closure'), format: (row, dashIfEmpty) => dashIfEmpty(toTimeFormat(row.zoneLanding)), }, @@ -144,9 +160,16 @@ const columns = computed(() => [ }, { align: 'left', - name: 'province', + name: 'provinceFk', label: t('ticketList.province'), - columnClass: 'expand', + component: 'select', + attrs: { + url: 'Provinces', + }, + columnField: { + component: null, + }, + format: (row, dashIfEmpty) => dashIfEmpty(row.province), }, { align: 'left', @@ -180,9 +203,19 @@ const columns = computed(() => [ }, { align: 'left', - name: 'warehouse', - label: t('ticketList.warehouse'), - columnClass: 'expand', + name: 'warehouseFk', + label: t('globals.warehouse'), + component: 'select', + attrs: { + url: 'warehouses', + fields: ['id', 'name'], + }, + format: (row) => row.warehouse, + columnField: { + component: null, + }, + cardVisible: false, + create: false, }, { align: 'left', @@ -228,7 +261,32 @@ const columns = computed(() => [ ], }, ]); +const onClientSelected = async (formData) => { + resetAgenciesSelector(formData); + await fetchAddresses(formData); +}; +const fetchAddresses = async (formData) => { + const { data } = await getAddresses(formData.clientId); + formInitialData.value = { clientId: formData.clientId }; + if (!data) return; + addressesOptions.value = data; + selectedClient.value = data[0].client; + formData.addressId = selectedClient.value.defaultAddressFk; + formInitialData.value.addressId = formData.addressId; +}; +watch( + () => route.query.table, + async (newValue) => { + if (newValue) { + const clientId = +JSON.parse(newValue)?.clientFk; + if (clientId) await onClientSelected({ clientId }); + if (tableRef.value) + tableRef.value.create.formInitialData = formInitialData.value; + } + }, + { immediate: true }, +); function resetAgenciesSelector(formData) { agenciesOptions.value = []; if (formData) formData.agencyModeId = null; @@ -239,12 +297,6 @@ function redirectToLines(id) { window.open(url, '_blank'); } -const onClientSelected = async (formData) => { - resetAgenciesSelector(formData); - await fetchClient(formData); - await fetchAddresses(formData); -}; - const fetchAvailableAgencies = async (formData) => { resetAgenciesSelector(formData); const response = await getAgencies(formData, selectedClient.value); @@ -255,22 +307,6 @@ const fetchAvailableAgencies = async (formData) => { if (agency) formData.agencyModeId = agency.agencyModeFk; }; -const fetchClient = async (formData) => { - const response = await getClient(formData.clientId); - if (!response) return; - const [client] = response.data; - selectedClient.value = client; -}; - -const fetchAddresses = async (formData) => { - const response = await getAddresses(formData.clientId); - if (!response) return; - addressesOptions.value = response.data; - - const { defaultAddress } = selectedClient.value; - formData.addressId = defaultAddress.id; -}; - const getColor = (row) => { if (row.alertLevelCode === 'OK') return 'bg-success'; else if (row.alertLevelCode === 'FREE') return 'bg-notice'; @@ -456,7 +492,7 @@ function setReference(data) { urlCreate: 'Tickets/new', title: t('ticketList.createTicket'), onDataSaved: ({ id }) => tableRef.redirect(id), - formInitialData: { clientId: null }, + formInitialData, }" default-mode="table" :columns="columns" @@ -538,11 +574,9 @@ function setReference(data) { :label="t('ticketList.client')" v-model="data.clientId" :options="clientsOptions" - option-value="id" - option-label="name" hide-selected required - @update:model-value="(client) => onClientSelected(data)" + @update:model-value="() => onClientSelected(data)" :sort-by="'id ASC'" > <template #option="scope"> @@ -564,7 +598,6 @@ function setReference(data) { :label="t('basicData.address')" v-model="data.addressId" :options="addressesOptions" - option-value="id" option-label="nickname" hide-selected map-options @@ -610,6 +643,9 @@ function setReference(data) { {{ scope.opt?.city }} </span> </QItemLabel> + <QItemLabel caption> + {{ `#${scope.opt?.id}` }} + </QItemLabel> </QItemSection> </QItem> </template> @@ -633,8 +669,6 @@ function setReference(data) { :label="t('globals.warehouse')" v-model="data.warehouseId" :options="warehousesOptions" - option-value="id" - option-label="name" hide-selected required @update:model-value="() => fetchAvailableAgencies(data)" @@ -694,7 +728,6 @@ function setReference(data) { :label="t('ticketList.company')" v-model="dialogData.companyFk" :options="companiesOptions" - option-value="id" option-label="code" hide-selected > @@ -705,7 +738,6 @@ function setReference(data) { :label="t('ticketList.bank')" v-model="dialogData.bankFk" :options="accountingOptions" - option-value="id" option-label="bank" hide-selected @update:model-value="setReference" diff --git a/src/pages/Travel/ExtraCommunityFilter.vue b/src/pages/Travel/ExtraCommunityFilter.vue index 29d342334..ae6e695be 100644 --- a/src/pages/Travel/ExtraCommunityFilter.vue +++ b/src/pages/Travel/ExtraCommunityFilter.vue @@ -73,6 +73,7 @@ warehouses(); /> <FetchData url="AgencyModes" + :filter="{ fields: ['id', 'name'], order: ['name ASC'] }" @on-fetch="(data) => (agenciesOptions = data)" auto-load /> diff --git a/src/pages/Travel/TravelCreate.vue b/src/pages/Travel/TravelCreate.vue index 72c34aad8..35a936134 100644 --- a/src/pages/Travel/TravelCreate.vue +++ b/src/pages/Travel/TravelCreate.vue @@ -39,6 +39,7 @@ const redirectToTravelBasicData = (_, { id }) => { <template> <FetchData url="AgencyModes" + :filter="{ fields: ['id', 'name'], order: ['name ASC'] }" @on-fetch="(data) => (agenciesOptions = data)" auto-load /> diff --git a/src/pages/Travel/TravelFilter.vue b/src/pages/Travel/TravelFilter.vue index 90901ee4d..4a9c80952 100644 --- a/src/pages/Travel/TravelFilter.vue +++ b/src/pages/Travel/TravelFilter.vue @@ -52,9 +52,8 @@ defineExpose({ states }); v-model="params.agencyModeFk" @update:model-value="searchFn()" url="agencyModes" + sort-by="name ASC" :use-like="false" - option-value="id" - option-label="name" option-filter="name" dense outlined diff --git a/src/pages/Worker/Card/WorkerCalendarItem.vue b/src/pages/Worker/Card/WorkerCalendarItem.vue index 893a81c6d..86d227ad3 100644 --- a/src/pages/Worker/Card/WorkerCalendarItem.vue +++ b/src/pages/Worker/Card/WorkerCalendarItem.vue @@ -79,7 +79,7 @@ const editEvent = async (event) => { }; const { data } = await axios.patch( `Workers/${route.params.id}/updateAbsence`, - params + params, ); if (data) emit('refresh'); @@ -108,14 +108,14 @@ const handleDateSelected = (date) => { if (!event) createEvent(_date); }; -const handleEventSelected = (event, { year, month, day }) => { +const handleEventSelected = async (event, { year, month, day }) => { if (!props.absenceType) { notify(t('Choose an absence type from the right menu'), 'warning'); return; } const date = new Date(year, month - 1, day); - if (!event?.absenceId) createEvent(date); + if (!event?.absenceId) await createEvent(date); else if (event.type == props.absenceType.code) deleteEvent(event, date); else editEvent(event); }; diff --git a/src/pages/Worker/Card/WorkerNotes.vue b/src/pages/Worker/Card/WorkerNotes.vue index 4f123206b..da274f3fa 100644 --- a/src/pages/Worker/Card/WorkerNotes.vue +++ b/src/pages/Worker/Card/WorkerNotes.vue @@ -5,9 +5,9 @@ import VnNotes from 'src/components/ui/VnNotes.vue'; const route = useRoute(); -const filter = { +const userFilter = { order: 'created DESC', - where: { workerFk: route.params.id }, + include: { relation: 'worker', scope: { @@ -22,11 +22,15 @@ const filter = { }, }; -const body = { - workerFk: route.params.id, -}; +const body = { workerFk: route.params.id }; </script> <template> - <VnNotes :add-note="true" url="WorkerObservations" :filter="filter" :body="body" /> + <VnNotes + :add-note="true" + url="WorkerObservations" + :user-filter="userFilter" + :filter="{ where: { workerFk: $route.params.id } }" + :body="body" + /> </template> diff --git a/src/pages/Worker/Card/WorkerTimeControl.vue b/src/pages/Worker/Card/WorkerTimeControl.vue index 7def6e94c..9c0fa6758 100644 --- a/src/pages/Worker/Card/WorkerTimeControl.vue +++ b/src/pages/Worker/Card/WorkerTimeControl.vue @@ -343,19 +343,29 @@ const updateData = async () => { const getMailStates = async (date) => { const url = `WorkerTimeControls/${route.params.id}/getMailStates`; + const year = date.getFullYear(); const month = date.getMonth() + 1; - const prevMonth = month == 1 ? 12 : month - 1; - const params = { - month, - year: date.getFullYear(), + + const getMonthStates = async (month, year) => { + return (await axios.get(url, { params: { month, year } })).data; }; - const curMonthStates = (await axios.get(url, { params })).data; - const prevMonthStates = ( - await axios.get(url, { params: { ...params, month: prevMonth } }) - ).data; + const curMonthStates = await getMonthStates(month, year); - workerTimeControlMails.value = curMonthStates.concat(prevMonthStates); + const prevMonthStates = await getMonthStates( + month === 1 ? 12 : month - 1, + month === 1 ? year - 1 : year, + ); + + const postMonthStates = await getMonthStates( + month === 12 ? 1 : month + 1, + month === 12 ? year + 1 : year, + ); + workerTimeControlMails.value = [ + ...curMonthStates, + ...prevMonthStates, + ...postMonthStates, + ]; }; const showWorkerTimeForm = (propValue, formType) => { diff --git a/src/pages/Zone/Card/ZoneBasicData.vue b/src/pages/Zone/Card/ZoneBasicData.vue index 03013f011..2f771642e 100644 --- a/src/pages/Zone/Card/ZoneBasicData.vue +++ b/src/pages/Zone/Card/ZoneBasicData.vue @@ -9,22 +9,22 @@ import VnInputTime from 'src/components/common/VnInputTime.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; const { t } = useI18n(); -const validAddresses = ref([]); const addresses = ref([]); const setFilteredAddresses = (data) => { - const validIds = new Set(validAddresses.value.map((item) => item.addressFk)); - addresses.value = data.filter((address) => validIds.has(address.id)); + addresses.value = data.map(({ address }) => address); }; </script> <template> <FetchData url="RoadmapAddresses" + :filter="{ + include: { relation: 'address' }, + }" auto-load - @on-fetch="(data) => (validAddresses = data)" + @on-fetch="setFilteredAddresses" /> - <FetchData url="Addresses" auto-load @on-fetch="setFilteredAddresses" /> <FormModel auto-load model="Zone"> <template #form="{ data, validate }"> <VnRow> @@ -120,12 +120,10 @@ const setFilteredAddresses = (data) => { option-label="nickname" :options="addresses" :fields="['id', 'nickname']" - sort-by="id" + sort-by="nickname ASC" hide-selected map-options :rules="validate('data.addressFk')" - :filter-options="['id']" - :where="filterWhere" /> </VnRow> <VnRow> diff --git a/src/pages/Zone/ZoneFilterPanel.vue b/src/pages/Zone/ZoneFilterPanel.vue index bbe12189a..ff65479e4 100644 --- a/src/pages/Zone/ZoneFilterPanel.vue +++ b/src/pages/Zone/ZoneFilterPanel.vue @@ -5,6 +5,7 @@ import VnInput from 'components/common/VnInput.vue'; import FetchData from 'components/FetchData.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnSelect from 'components/common/VnSelect.vue'; +import order from 'src/router/modules/order'; const { t } = useI18n(); const props = defineProps({ @@ -24,7 +25,7 @@ const agencies = ref([]); <template> <FetchData url="AgencyModes" - :filter="{ fields: ['id', 'name'] }" + :filter="{ fields: ['id', 'name'], order: ['name ASC'] }" @on-fetch="(data) => (agencies = data)" auto-load /> diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue index 4df84e4bd..eb54ec15b 100644 --- a/src/pages/Zone/ZoneList.vue +++ b/src/pages/Zone/ZoneList.vue @@ -199,9 +199,8 @@ function formatRow(row) { <template #more-create-dialog="{ data }"> <VnSelect url="AgencyModes" + sort-by="name ASC" v-model="data.agencyModeFk" - option-value="id" - option-label="name" :label="t('list.agency')" /> <VnInput diff --git a/src/router/modules/shelving.js b/src/router/modules/shelving.js index c085dd8dc..94ff274dc 100644 --- a/src/router/modules/shelving.js +++ b/src/router/modules/shelving.js @@ -111,15 +111,6 @@ export default { shelvingCard, ], }, - { - path: 'create', - name: 'ShelvingCreate', - meta: { - title: 'shelvingCreate', - icon: 'add', - }, - component: () => import('src/pages/Shelving/Card/ShelvingForm.vue'), - }, { path: 'parking', name: 'ParkingMain', diff --git a/src/stores/useStateStore.js b/src/stores/useStateStore.js index e48b67279..ca447bc11 100644 --- a/src/stores/useStateStore.js +++ b/src/stores/useStateStore.js @@ -7,7 +7,11 @@ export const useStateStore = defineStore('stateStore', () => { const rightDrawer = ref(false); const rightAdvancedDrawer = ref(false); const subToolbar = ref(false); + const cardDescriptor = ref(null); + function cardDescriptorChangeValue(descriptor) { + cardDescriptor.value = descriptor; + } function toggleLeftDrawer() { leftDrawer.value = !leftDrawer.value; } @@ -49,6 +53,8 @@ export const useStateStore = defineStore('stateStore', () => { } return { + cardDescriptor, + cardDescriptorChangeValue, leftDrawer, rightDrawer, rightAdvancedDrawer, diff --git a/test/cypress/integration/claim/claimNotes.spec.js b/test/cypress/integration/claim/claimNotes.spec.js index fa4a214a1..ae8b4186c 100644 --- a/test/cypress/integration/claim/claimNotes.spec.js +++ b/test/cypress/integration/claim/claimNotes.spec.js @@ -1,4 +1,4 @@ -describe('ClaimNotes', () => { +describe.skip('ClaimNotes', () => { const saveBtn = '.q-field__append > .q-btn > .q-btn__content > .q-icon'; const firstNote = '.q-infinite-scroll :nth-child(1) > .q-card__section--vert'; beforeEach(() => { diff --git a/test/cypress/integration/claim/claimPhoto.spec.js b/test/cypress/integration/claim/claimPhoto.spec.js index c3522cbfe..592642f4d 100755 --- a/test/cypress/integration/claim/claimPhoto.spec.js +++ b/test/cypress/integration/claim/claimPhoto.spec.js @@ -1,6 +1,6 @@ /// <reference types="cypress" /> -// redmine.verdnatura.es/issues/8417 -describe.skip('ClaimPhoto', () => { +describe('ClaimPhoto', () => { + const carrouselClose = '.q-dialog__inner > .q-toolbar > .q-btn > .q-btn__content > .q-icon'; beforeEach(() => { const claimId = 1; cy.login('developer'); @@ -16,6 +16,7 @@ describe.skip('ClaimPhoto', () => { }); it('should add new file with drag and drop', () => { + cy.get('.container').should('be.visible').and('exist'); cy.get('.container').selectFile('test/cypress/fixtures/image.jpg', { action: 'drag-drop', }); @@ -23,33 +24,23 @@ describe.skip('ClaimPhoto', () => { }); it('should open first image dialog change to second and close', () => { - cy.get(':nth-last-child(1) > .q-card').click(); - cy.get('.q-carousel__slide > .q-img > .q-img__container > .q-img__image').should( - 'be.visible', - ); + cy.dataCy('file-1').click(); + cy.get(carrouselClose).click(); - cy.get('.q-carousel__control > button').click(); - - cy.get( - '.q-dialog__inner > .q-toolbar > .q-btn > .q-btn__content > .q-icon', - ).click(); - cy.get('.q-carousel__slide > .q-img > .q-img__container > .q-img__image').should( - 'not.be.visible', - ); + cy.dataCy('file-1').click(); + cy.get('.q-carousel__control > button').as('nextButton').click(); + cy.get('.q-carousel__slide > .q-ma-none').should('be.visible'); + cy.get(carrouselClose).click(); }); it('should remove third and fourth file', () => { - cy.get( - '.multimediaParent > :nth-last-child(1) > .q-btn > .q-btn__content > .q-icon', - ).click(); + cy.dataCy('delete-button-4').click(); cy.get( '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block', ).click(); cy.get('.q-notification__message').should('have.text', 'Data deleted'); - cy.get( - '.multimediaParent > :nth-last-child(1) > .q-btn > .q-btn__content > .q-icon', - ).click(); + cy.dataCy('delete-button-3').click(); cy.get( '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block', ).click(); diff --git a/test/cypress/integration/client/clientAddress.spec.js b/test/cypress/integration/client/clientAddress.spec.js index 8673c9083..5d82aa4bc 100644 --- a/test/cypress/integration/client/clientAddress.spec.js +++ b/test/cypress/integration/client/clientAddress.spec.js @@ -17,7 +17,7 @@ describe('Client consignee', () => { const addressName = 'test'; cy.dataCy('Consignee_input').type(addressName); cy.dataCy('Location_select').click(); - cy.get('[role="listbox"] .q-item:nth-child(1)').click(); + cy.getOption(); cy.dataCy('Street address_input').type('TEST ADDRESS'); cy.get('.q-btn-group > .q-btn--standard').click(); cy.location('href').should('contain', '#/customer/1107/address'); diff --git a/test/cypress/integration/entry/entryList.spec.js b/test/cypress/integration/entry/entryList.spec.js index bdaa66f79..d43ec895a 100644 --- a/test/cypress/integration/entry/entryList.spec.js +++ b/test/cypress/integration/entry/entryList.spec.js @@ -1,4 +1,4 @@ -describe.skip('Entry', () => { +describe('Entry', () => { beforeEach(() => { cy.viewport(1920, 1080); cy.login('buyer'); diff --git a/test/cypress/integration/entry/stockBought.spec.js b/test/cypress/integration/entry/stockBought.spec.js index b282a19a5..91e0d507e 100644 --- a/test/cypress/integration/entry/stockBought.spec.js +++ b/test/cypress/integration/entry/stockBought.spec.js @@ -16,9 +16,9 @@ describe('EntryStockBought', () => { cy.get('input[aria-label="Reserve"]').type('1'); cy.get('input[aria-label="Date"]').eq(1).clear(); cy.get('input[aria-label="Date"]').eq(1).type('01-01'); - cy.get('input[aria-label="Buyer"]').type('buyerBossNick'); + cy.get('input[aria-label="Buyer"]').type('itNick'); cy.get('div[role="listbox"] > div > div[role="option"]') - .eq(0) + .eq(1) .should('be.visible') .click(); diff --git a/test/cypress/integration/invoiceOut/invoiceOutSummary.spec.js b/test/cypress/integration/invoiceOut/invoiceOutSummary.spec.js index 333f7e2c4..8370ac6fd 100644 --- a/test/cypress/integration/invoiceOut/invoiceOutSummary.spec.js +++ b/test/cypress/integration/invoiceOut/invoiceOutSummary.spec.js @@ -17,7 +17,6 @@ describe('InvoiceOut summary', () => { cy.login('developer'); cy.visit(`/#/invoice-out/1/summary`); }); - it('open the descriptors', () => { cy.get(firstRowDescriptors(1)).click(); cy.get('.descriptor').should('be.visible'); @@ -35,7 +34,6 @@ describe('InvoiceOut summary', () => { it('should open the ticket list', () => { cy.get(toTicketList).click(); - cy.get('.descriptor').should('be.visible'); cy.dataCy('vnFilterPanelChip').should('include.text', 'T1111111'); }); diff --git a/test/cypress/integration/item/itemBarcodes.spec.js b/test/cypress/integration/item/itemBarcodes.spec.js index 844768d9e..1f6698f9c 100644 --- a/test/cypress/integration/item/itemBarcodes.spec.js +++ b/test/cypress/integration/item/itemBarcodes.spec.js @@ -3,23 +3,22 @@ describe('ItemBarcodes', () => { beforeEach(() => { cy.viewport(1920, 1080); cy.login('developer'); - cy.visit(`/#/item/list`); - cy.typeSearchbar('1{enter}'); + cy.visit(`/#/item/1/barcode`); }); it('should throw an error if the barcode exists', () => { - cy.get('[href="#/item/1/barcode"]').click(); - cy.get('.q-card > .q-btn > .q-btn__content > .q-icon').click(); - cy.dataCy('Code_input').eq(3).type('1111111111'); - cy.dataCy('crudModelDefaultSaveBtn').click(); + newBarcode('1111111111'); cy.checkNotification('Codes can not be repeated'); }); it('should create a new barcode', () => { - cy.get('[href="#/item/1/barcode"]').click(); - cy.get('.q-card > .q-btn > .q-btn__content > .q-icon').click(); - cy.dataCy('Code_input').eq(3).type('1231231231'); - cy.dataCy('crudModelDefaultSaveBtn').click(); + newBarcode('1231231231'); cy.checkNotification('Data saved'); }); + + function newBarcode(text) { + cy.dataCy('addBarcode_input').click(); + cy.dataCy('Code_input').eq(3).should('exist').type(text); + cy.dataCy('crudModelDefaultSaveBtn').click(); + } }); diff --git a/test/cypress/integration/order/orderList.spec.js b/test/cypress/integration/order/orderList.spec.js new file mode 100644 index 000000000..76214d3a3 --- /dev/null +++ b/test/cypress/integration/order/orderList.spec.js @@ -0,0 +1,31 @@ +/// <reference types="cypress" /> +describe('OrderList', () => { + beforeEach(() => { + cy.login('developer'); + cy.viewport(1920, 1080); + cy.visit('/#/order/list'); + }); + + it('create order', () => { + /* ==== Generated with Cypress Studio ==== */ + cy.get('[data-cy="vnTableCreateBtn"]').click(); + cy.get('[data-cy="Client_select"]').type('1101'); + cy.get('.q-menu').contains('Bruce Wayne').click(); + 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.get('.q-menu > div> .q-item:nth-child(1)').click(); + cy.dataCy('landedDate').find('input').type('06/01/2001'); + 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'); + cy.get('.q-item > .q-item__label.subtitle').then((text) => { + const id = text.text().trim().split('#')[1]; + cy.get('.q-item > .q-item__label').should('have.text', ` #${id}`); + }); + cy.url().should('include', `/order`); + }); +}); diff --git a/test/cypress/integration/route/routeAutonomous.spec.js b/test/cypress/integration/route/routeAutonomous.spec.js new file mode 100644 index 000000000..acf82bd95 --- /dev/null +++ b/test/cypress/integration/route/routeAutonomous.spec.js @@ -0,0 +1,121 @@ +describe('RouteAutonomous', () => { + const getLinkSelector = (colField) => + `tr:first-child > [data-col-field="${colField}"] > .no-padding > .link`; + + const selectors = { + reference: 'Reference_input', + date: 'tr:first-child > [data-col-field="dated"]', + total: '.value > .text-h6', + received: getLinkSelector('invoiceInFk'), + autonomous: getLinkSelector('supplierName'), + firstRowCheckbox: '.q-virtual-scroll__content tr:first-child .q-checkbox__bg', + secondRowCheckbox: '.q-virtual-scroll__content tr:nth-child(2) .q-checkbox__bg', + 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', + }; + + const data = { + reference: 'Test invoice', + total: '€206.40', + supplier: 'PLANTS SL', + route: 'first route', + }; + + const summaryUrl = '/summary'; + const dataSaved = 'Data saved'; + + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit(`/#/route/agency-term`); + cy.typeSearchbar('{enter}'); + }); + + it('Should list autonomous routes', () => { + cy.get('.q-table') + .children() + .should('be.visible') + .should('have.length.greaterThan', 0); + }); + + it('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.get('.q-file').selectFile('test/cypress/fixtures/image.jpg', { + force: true, + }); + cy.dataCy(selectors.saveFormBtn).click(); + cy.checkNotification(dataSaved); + cy.typeSearchbar('{enter}'); + }); + + 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); + }); + + 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); + }); + + 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 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); + }); + }); + + 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); + }); + + 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); + }); + }); + + 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); + }); + }); +}); diff --git a/test/cypress/integration/route/vehicle/vehicleList.spec.js b/test/cypress/integration/route/vehicle/vehicleList.spec.js new file mode 100644 index 000000000..2b3c9cdbc --- /dev/null +++ b/test/cypress/integration/route/vehicle/vehicleList.spec.js @@ -0,0 +1,59 @@ +describe('Vehicle list', () => { + const selectors = { + saveFormBtn: 'FormModelPopup_save', + summaryPopupBtn: 'tr:last-child > .q-table--col-auto-width > .q-btn', + summaryGoToSummaryBtn: '.header > .q-icon', + summaryHeader: '.summaryHeader > div', + numberPlate: 'tr:last-child > [data-col-field="numberPlate"] > .no-padding', + }; + + const data = { + 'Nº Plate': { val: '9465-LPA' }, + 'Trade Mark': { val: 'WAYNE INDUSTRIES' }, + Model: { val: 'BATREMOLQUE' }, + Type: { val: 'remolque', type: 'select' }, + Warehouse: { val: 'Warehouse One', type: 'select' }, + Country: { val: 'Portugal', type: 'select' }, + Description: { val: 'Exclusive for batpod transport' }, + }; + + const expected = data['Nº Plate'].val; + const summaryUrl = '/summary'; + + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit(`/#/route/vehicle/list`); + cy.typeSearchbar('{enter}'); + }); + + it('should list vehicles', () => { + cy.get('.q-table') + .children() + .should('be.visible') + .should('have.length.greaterThan', 0); + }); + + it('Should add new vehicle', () => { + cy.addBtnClick(); + cy.fillInForm(data); + cy.dataCy(selectors.saveFormBtn).should('be.visible').click(); + + cy.checkNotification('Data created'); + cy.get(selectors.summaryHeader).should('contain', expected); + cy.url().should('include', summaryUrl); + }); + + it('should open summary by clicking a vehicle', () => { + cy.get(selectors.numberPlate).click(); + cy.get(selectors.summaryHeader).should('contain', expected); + cy.url().should('include', summaryUrl); + }); + + it('should redirect to vehicle summary when click summary icon on summary pop-up', () => { + cy.get(selectors.summaryPopupBtn).click(); + cy.get(selectors.summaryHeader).should('contain', expected); + cy.get(selectors.summaryGoToSummaryBtn).click(); + cy.url().should('include', summaryUrl); + }); +}); diff --git a/test/cypress/integration/shelving/parking/parkingList.spec.js b/test/cypress/integration/shelving/parking/parkingList.spec.js index ecee8aab7..ca1877621 100644 --- a/test/cypress/integration/shelving/parking/parkingList.spec.js +++ b/test/cypress/integration/shelving/parking/parkingList.spec.js @@ -1,7 +1,7 @@ /// <reference types="cypress" /> describe('ParkingList', () => { const searchbar = '#searchbar input'; - const firstCard = ':nth-child(1) > .q-card > .no-margin > .q-py-none'; + const firstCard = ':nth-child(1) > .q-card > .no-margin > .q-py-none'; const summaryHeader = '.summaryBody .header'; beforeEach(() => { diff --git a/test/cypress/integration/shelving/shelvingBasicData.spec.js b/test/cypress/integration/shelving/shelvingBasicData.spec.js new file mode 100644 index 000000000..d7b0dc692 --- /dev/null +++ b/test/cypress/integration/shelving/shelvingBasicData.spec.js @@ -0,0 +1,24 @@ +/// <reference types="cypress" /> +describe('ShelvingList', () => { + const parking = + '.q-card > :nth-child(1) > .q-select > .q-field__inner > .q-field__control > .q-field__control-container'; + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit(`/#/shelving/1/basic-data`); + }); + + it('should give an error if the code aldready exists', () => { + cy.dataCy('Code_input').should('exist').clear().type('AA7'); + cy.saveCard(); + cy.get('.q-notification__message').should('have.text', 'The code already exists'); + }); + it('should edit the data and save', () => { + cy.selectOption(parking, 'P-01-1'); + cy.dataCy('Code_input').clear().type('AA1'); + cy.dataCy('Priority_input').clear().type('10'); + cy.get(':nth-child(2) > .q-checkbox > .q-checkbox__inner').click(); + cy.saveCard(); + cy.get('.q-notification__message').should('have.text', 'Data saved'); + }); +}); diff --git a/test/cypress/integration/shelving/shelvingList.spec.js b/test/cypress/integration/shelving/shelvingList.spec.js new file mode 100644 index 000000000..745dd1b78 --- /dev/null +++ b/test/cypress/integration/shelving/shelvingList.spec.js @@ -0,0 +1,41 @@ +/// <reference types="cypress" /> +describe('ShelvingList', () => { + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit(`/#/shelving/list`); + }); + + it('should redirect on clicking a shelving', () => { + cy.typeSearchbar('{enter}'); + cy.dataCy('cardBtn').eq(0).click(); + cy.get('.summaryHeader > .header > .q-icon').click(); + cy.url().should('include', '/shelving/1/summary'); + }); + + it('should redirect from preview to basic-data', () => { + cy.typeSearchbar('{enter}'); + cy.dataCy('cardBtn').eq(0).click(); + cy.get('.q-card > .header').click(); + cy.url().should('include', '/shelving/1/basic-data'); + }); + + it('should filter and redirect if only one result', () => { + cy.selectOption('[data-cy="Parking_select"]', 'P-02-2'); + cy.dataCy('Parking_select').type('{enter}'); + cy.url().should('match', /\/shelving\/\d+\/summary/); + }); + + it('should create a new shelving', () => { + cy.dataCy('vnTableCreateBtn').click(); + cy.dataCy('code-create-popup').type('Test'); + cy.dataCy('Priority_input').type('10'); + cy.selectOption( + '.grid-create > .q-select > .q-field__inner > .q-field__control > .q-field__control-container', + '100-01', + ); + cy.dataCy('FormModelPopup_save').click(); + cy.checkNotification('Data created'); + cy.url().should('match', /\/shelving\/\d+\/basic-data/); + }); +}); diff --git a/test/cypress/integration/ticket/ticketFilter.spec.js b/test/cypress/integration/ticket/ticketFilter.spec.js new file mode 100644 index 000000000..2e5a3f3ce --- /dev/null +++ b/test/cypress/integration/ticket/ticketFilter.spec.js @@ -0,0 +1,15 @@ +/// <reference types="cypress" /> +describe('TicketFilter', () => { + beforeEach(() => { + cy.login('developer'); + cy.viewport(1920, 1080); + cy.visit('/#/ticket/list'); + }); + + it('use search button', function () { + cy.waitForElement('.q-page'); + cy.get('[data-cy="Customer ID_input"]').type('1105'); + cy.searchBtnFilterPanel(); + cy.location('href').should('contain', '#/ticket/15/summary'); + }); +}); diff --git a/test/cypress/integration/ticket/ticketList.spec.js b/test/cypress/integration/ticket/ticketList.spec.js index 1c96b027f..8e4818c46 100644 --- a/test/cypress/integration/ticket/ticketList.spec.js +++ b/test/cypress/integration/ticket/ticketList.spec.js @@ -1,6 +1,6 @@ /// <reference types="cypress" /> -describe.skip('TicketList', () => { - const firstRow = 'tbody > :nth-child(1)'; +describe.only('TicketList', () => { + const firstRow = 'tbody.q-virtual-scroll__content tr:nth-child(1)'; beforeEach(() => { cy.login('developer'); @@ -9,15 +9,14 @@ describe.skip('TicketList', () => { }); const searchResults = (search) => { - cy.dataCy('vn-searchbar').find('input').focus(); - if (search) cy.dataCy('vn-searchbar').find('input').type(search); + if (search) cy.typeSearchbar().type(search); cy.dataCy('vn-searchbar').find('input').type('{enter}'); - cy.dataCy('ticketListTable').should('exist'); + // cy.dataCy('ticketListTable').should('exist'); cy.get(firstRow).should('exist'); }; it('should search results', () => { - cy.dataCy('ticketListTable').should('not.exist'); + // cy.dataCy('ticketListTable').should('not.exist'); cy.get('.q-field__control').should('exist'); searchResults(); }); @@ -27,7 +26,7 @@ describe.skip('TicketList', () => { cy.window().then((win) => { cy.stub(win, 'open').as('windowOpen'); }); - cy.get(firstRow).find('.q-btn:first').click(); + cy.get(firstRow).should('be.visible').find('.q-btn:first').click(); cy.get('@windowOpen').should('be.calledWithMatch', /\/ticket\/\d+\/sale/); }); @@ -38,6 +37,21 @@ describe.skip('TicketList', () => { cy.get('.summaryBody').should('exist'); }); + it('filter client and create ticket', () => { + cy.intercept('GET', /\/api\/Tickets\/filter/).as('ticketSearchbar'); + searchResults(); + + cy.intercept('GET', /\/api\/Tickets\/filter/).as('ticketFilter'); + cy.dataCy('Customer ID_input').clear('1'); + cy.dataCy('Customer ID_input').type('1101{enter}'); + + cy.get('[data-cy="vnTableCreateBtn"] > .q-btn__content > .q-icon').click(); + cy.dataCy('Customer_select').should('have.value', 'Bruce Wayne'); + cy.dataCy('Address_select').click(); + + cy.getOption().click(); + cy.dataCy('Address_select').should('have.value', 'Bruce Wayne'); + }); it('Client list create new client', () => { cy.dataCy('vnTableCreateBtn').should('exist'); cy.dataCy('vnTableCreateBtn').click(); @@ -54,7 +68,7 @@ describe.skip('TicketList', () => { cy.url().should('match', /\/ticket\/\d+\/summary/); }); - it('should show the corerct problems', () => { + it('should show the correct problems', () => { cy.intercept('GET', '**/api/Tickets/filter*', (req) => { req.headers['cache-control'] = 'no-cache'; req.headers['pragma'] = 'no-cache'; diff --git a/test/cypress/integration/ticket/ticketSale.spec.js b/test/cypress/integration/ticket/ticketSale.spec.js index b59765ca6..81ea761c4 100644 --- a/test/cypress/integration/ticket/ticketSale.spec.js +++ b/test/cypress/integration/ticket/ticketSale.spec.js @@ -112,7 +112,6 @@ describe('TicketSale', () => { cy.dataCy('ticketSaleTransferBtn').click(); cy.dataCy('ticketTransferPopup').should('exist'); cy.dataCy('ticketTransferNewTicketBtn').click(); - //check the new ticket has been created succesfully cy.get('.q-item > .q-item__label').should('not.have.text', ' #32'); }); @@ -138,7 +137,7 @@ describe('TicketSale', () => { it('update price', () => { const price = Number((Math.random() * 99 + 1).toFixed(2)); cy.waitForElement(firstRow); - cy.get(':nth-child(10) > .q-btn').click(); + cy.get('[data-col-field="price"]').find('.q-btn').click(); cy.waitForElement('[data-cy="ticketEditManaProxy"]'); cy.dataCy('ticketEditManaProxy').should('exist'); cy.waitForElement('[data-cy="Price_input"]'); @@ -147,15 +146,14 @@ describe('TicketSale', () => { cy.dataCy('saveManaBtn').click(); handleVnConfirm(); - cy.get(':nth-child(10) > .q-btn > .q-btn__content').should( - 'have.text', - `€${price}`, - ); + cy.get('[data-col-field="price"]') + .find('.q-btn > .q-btn__content') + .should('have.text', `€${price}`); }); - it('update dicount', () => { + it('update discount', () => { const discount = Math.floor(Math.random() * 100) + 1; selectFirstRow(); - cy.get(':nth-child(11) > .q-btn').click(); + cy.get('[data-col-field="discount"]').find('.q-btn').click(); cy.waitForElement('[data-cy="ticketEditManaProxy"]'); cy.dataCy('ticketEditManaProxy').should('exist'); cy.waitForElement('[data-cy="Disc_input"]'); @@ -164,43 +162,44 @@ describe('TicketSale', () => { cy.dataCy('saveManaBtn').click(); handleVnConfirm(); - cy.get(':nth-child(11) > .q-btn > .q-btn__content').should( - 'have.text', - `${discount}.00%`, - ); + cy.get('[data-col-field="discount"]') + .find('.q-btn > .q-btn__content') + .should('have.text', `${discount}.00%`); }); it('change concept', () => { - const quantity = Math.floor(Math.random() * 100) + 1; + const concept = Math.floor(Math.random() * 100) + 1; cy.waitForElement(firstRow); - cy.get(':nth-child(8) > .row').click(); - cy.get( - '.q-menu > [data-v-ca3f07a4=""] > .q-field > .q-field__inner > .q-field__control > .q-field__control-container > [data-cy="undefined_input"]', - ) - .type(quantity) + cy.get('[data-col-field="item"]').click(); + cy.get('.q-menu') + .find('[data-cy="undefined_input"]') + .type(concept) .type('{enter}'); handleVnConfirm(); - cy.get(':nth-child(8) >.row').should('contain.text', `${quantity}`); + cy.get('[data-col-field="item"]').should('contain.text', `${concept}`); }); - it('changequantity ', () => { + it('change quantity ', () => { const quantity = Math.floor(Math.random() * 100) + 1; cy.waitForElement(firstRow); - cy.dataCy('ticketSaleQuantityInput').clear(); - cy.dataCy('ticketSaleQuantityInput').type(quantity).trigger('tab'); + cy.dataCy('ticketSaleQuantityInput').find('input').clear(); + cy.dataCy('ticketSaleQuantityInput') + .find('input') + .type(quantity) + .trigger('tab'); cy.get('.q-page > :nth-child(6)').click(); handleVnConfirm(); cy.get('[data-cy="ticketSaleQuantityInput"]') - .find('[data-cy="undefined_input"]') + .find('input') .should('have.value', `${quantity}`); }); }); }); function handleVnConfirm() { - cy.get('[data-cy="VnConfirm_confirm"] > .q-btn__content > .block').click(); + cy.get('[data-cy="VnConfirm_confirm"]').click(); cy.waitForElement('.q-notification__message'); cy.get('.q-notification__message').should('be.visible'); diff --git a/test/cypress/integration/vnComponent/UserPanel.spec.js b/test/cypress/integration/vnComponent/UserPanel.spec.js index e83d07954..25724e873 100644 --- a/test/cypress/integration/vnComponent/UserPanel.spec.js +++ b/test/cypress/integration/vnComponent/UserPanel.spec.js @@ -18,7 +18,7 @@ describe('UserPanel', () => { cy.get(userWarehouse).should('have.value', 'VNL').click(); // Actualizo la opción - getOption(3); + cy.getOption(3); //Compruebo la notificación cy.get('.q-notification').should('be.visible'); @@ -26,7 +26,7 @@ describe('UserPanel', () => { //Restauro el valor cy.get(userWarehouse).click(); - getOption(2); + cy.getOption(2); }); it('should notify when update user company', () => { const userCompany = @@ -39,7 +39,7 @@ describe('UserPanel', () => { cy.get(userCompany).should('have.value', 'Warehouse One').click(); //Actualizo la opción - getOption(2); + cy.getOption(2); //Compruebo la notificación cy.get('.q-notification').should('be.visible'); @@ -47,12 +47,6 @@ describe('UserPanel', () => { //Restauro el valor cy.get(userCompany).click(); - getOption(1); + cy.getOption(1); }); }); - -function getOption(index) { - cy.waitForElement('[role="listbox"]'); - const option = `[role="listbox"] .q-item:nth-child(${index})`; - cy.get(option).click(); -} diff --git a/test/cypress/integration/vnComponent/VnAccountNumber.spec.js b/test/cypress/integration/vnComponent/VnAccountNumber.spec.js index 63ab646fe..053902f35 100644 --- a/test/cypress/integration/vnComponent/VnAccountNumber.spec.js +++ b/test/cypress/integration/vnComponent/VnAccountNumber.spec.js @@ -1,35 +1,37 @@ -describe('VnInput Component', () => { +describe('VnAccountNumber', () => { + const accountInput = 'input[data-cy="supplierFiscalDataAccount_input"]'; beforeEach(() => { cy.login('developer'); cy.viewport(1920, 1080); cy.visit('/#/supplier/1/fiscal-data'); }); - it('should replace character at cursor position in insert mode', () => { - // Simula escribir en el input - cy.dataCy('supplierFiscalDataAccount').clear(); - cy.dataCy('supplierFiscalDataAccount').type('4100000001'); - // Coloca el cursor en la posición 0 - cy.dataCy('supplierFiscalDataAccount').type('{movetostart}'); - // Escribe un número y verifica que se reemplace correctamente - cy.dataCy('supplierFiscalDataAccount').type('999'); - cy.dataCy('supplierFiscalDataAccount').should('have.value', '9990000001'); + describe('VnInput handleInsertMode()', () => { + it('should replace character at cursor position in insert mode', () => { + cy.get(accountInput).type('{selectall}4100000001'); + cy.get(accountInput).type('{movetostart}'); + cy.get(accountInput).type('999'); + cy.get(accountInput).should('have.value', '9990000001'); + }); + + it('should replace character at cursor position in insert mode', () => { + cy.get(accountInput).clear(); + cy.get(accountInput).type('4100000001'); + cy.get(accountInput).type('{movetostart}'); + cy.get(accountInput).type('999'); + cy.get(accountInput).should('have.value', '9990000001'); + }); + + it('should respect maxlength prop', () => { + cy.get(accountInput).clear(); + cy.get(accountInput).type('123456789012345'); + cy.get(accountInput).should('have.value', '1234567890'); + }); }); - it('should replace character at cursor position in insert mode', () => { - // Simula escribir en el input - cy.dataCy('supplierFiscalDataAccount').clear(); - cy.dataCy('supplierFiscalDataAccount').type('4100000001'); - // Coloca el cursor en la posición 0 - cy.dataCy('supplierFiscalDataAccount').type('{movetostart}'); - // Escribe un número y verifica que se reemplace correctamente en la posicion incial - cy.dataCy('supplierFiscalDataAccount').type('999'); - cy.dataCy('supplierFiscalDataAccount').should('have.value', '9990000001'); - }); - - it('should respect maxlength prop', () => { - cy.dataCy('supplierFiscalDataAccount').clear(); - cy.dataCy('supplierFiscalDataAccount').type('123456789012345'); - cy.dataCy('supplierFiscalDataAccount').should('have.value', '1234567890'); // asumiendo que maxlength es 10 + it('should convert short account number to standard format', () => { + cy.get(accountInput).clear(); + cy.get(accountInput).type('123.'); + cy.get(accountInput).should('have.value', '1230000000'); }); }); diff --git a/test/cypress/integration/vnComponent/VnLocation.spec.js b/test/cypress/integration/vnComponent/VnLocation.spec.js index 986cbcaaf..ee49d6065 100644 --- a/test/cypress/integration/vnComponent/VnLocation.spec.js +++ b/test/cypress/integration/vnComponent/VnLocation.spec.js @@ -88,7 +88,7 @@ describe('VnLocation', () => { cy.get( firstOption.concat(' > .q-item__section > .q-item__label--caption'), ).should('have.text', postCodeLabel); - cy.get(firstOption).click(); + cy.getOption(); cy.get('.q-btn-group > .q-btn--standard > .q-btn__content > .q-icon').click(); cy.reload(); cy.waitForElement('.q-form'); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 096a29dc1..dfec341cd 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -393,6 +393,22 @@ Cypress.Commands.add('clickButtonWithIcon', (iconClass) => { cy.wrap($btn).click(); }); }); + Cypress.Commands.add('clickButtonWithText', (buttonText) => { cy.get('.q-btn').contains(buttonText).click(); }); + +Cypress.Commands.add('getOption', (index = 1) => { + cy.waitForElement('[role="listbox"]'); + cy.get(`[role="listbox"] .q-item:nth-child(${index})`).click(); +}); + +Cypress.Commands.add('searchBtnFilterPanel', () => { + cy.get( + '.q-scrollarea__content > .q-btn--standard > .q-btn__content > .q-icon', + ).click(); +}); + +Cypress.Commands.add('waitRequest', (alias, cb) => { + cy.wait(alias).then(cb); +});