diff --git a/CHANGELOG.md b/CHANGELOG.md index 03812d252..e110e4cd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,87 @@ +# Version 24.50 - 2024-12-10 + +### Added 🆕 + +- feat: add reportFileName option by:Javier Segarra +- feat: all clients just with global series by:jgallego +- feat: improve Merge branch 'test' into dev by:Javier Segarra +- feat: manual invoice in two lines by:jgallego +- feat: manualInvoice with address by:jgallego +- feat: randomize functions and example by:Javier Segarra +- feat: refs #6999 added search when user tabs on a filter with value by:Jon +- feat: refs #6999 added tab to search in VnTable filter by:Jon +- feat: refs #7346 #7346 improve form by:Javier Segarra +- feat: refs #7346 address ordered by:jgallego +- feat: refs #7346 radioButton by:jgallego +- feat: refs #7346 style radioButton by:jgallego +- feat: refs #7346 traducciones en cammelCase (7346-manualInvoice) by:jgallego +- feat: refs #8038 added new functionality in VnSelect and refactor styles by:Jon +- feat: refs #8061 #8061 updates by:Javier Segarra +- feat: refs #8087 reactive data by:jorgep +- feat: refs #8087 refs#8087 Redadas en travel by:Carlos Andrés +- feat: refs #8138 add component ticket problems by:pablone +- feat: refs #8163 add max length and more tests by:wbuezas +- feat: refs #8163 add prop by:wbuezas +- feat: refs #8163 add VnInput insert functionality and e2e test by:wbuezas +- feat: refs #8163 limit with maxLength by:Javier Segarra +- feat: refs #8163 maxLength SupplierFD account by:Javier Segarra +- feat: refs #8163 maxLengthVnInput by:Javier Segarra +- feat: refs #8163 use VnAccountNumber in VnAccountNumber by:Javier Segarra +- feat: refs #8166 show notification by:jorgep + +### Changed 📦 + +- feat: refs #8038 added new functionality in VnSelect and refactor styles by:Jon +- perf: add dataCy by:Javier Segarra +- perf: refs #7346 #7346 Imrpove interface dialog by:Javier Segarra +- perf: refs #7346 #7346 use v-show instead v-if by:Javier Segarra +- perf: refs #8036 currentFilter by:alexm +- perf: refs #8061 filter autonomy by:Javier Segarra +- perf: refs #8061 solve conflicts and random posCode it by:Javier Segarra +- perf: refs #8061 use opts from VnSelect by:Javier Segarra +- perf: refs #8163 #8061 createNewPostCodeForm by:Javier Segarra +- perf: remove console by:Javier Segarra +- perf: remove timeout by:Javier Segarra +- perf: test command fillInForm by:Javier Segarra +- refactor: refs #8162 remove comment by:wbuezas +- refactor: remove unnecesary things by:wbuezas + +### Fixed 🛠️ + +- fix: #8016 fetching data by:Javier Segarra +- fix: icons by:jgallego +- fix: refs #7229 download file by:jorgep +- fix: refs #7229 remove catch by:jorgep +- fix: refs #7229 set url by:jorgep +- fix: refs #7229 test by:jorgep +- fix: refs #7229 url by:jorgep +- fix: refs #7229 url + test by:jorgep +- fix: refs #7304 7304 clean warning by:carlossa +- fix: refs #7304 fix list by:carlossa +- fix: refs #7304 fix warning by:carlossa +- fix: refs #7346 traslations by:jgallego +- fix: refs #7529 add save by:carlossa +- fix: refs #7529 fix e2e by:carlossa +- fix: refs #7529 fix front by:carlossa +- fix: refs #7529 fix scss by:carlossa +- fix: refs #7529 fix te2e by:carlossa +- fix: refs #7529 fix workerPit e2e by:carlossa +- fix: refs #7529 front by:carlossa +- fix: refs #8036 apply exprBuilder after save filters by:alexm +- fix: refs #8036 only add where when required by:alexm +- fix: refs #8038 solve conflicts by:Jon +- fix: refs #8061 improve code dependencies (origin/8061_improve_newCP) by:Javier Segarra +- fix: refs #8138 move component from ui folder by:pablone +- fix: refs #8138 sme minor issues by:pablone +- fix: refs #8163 #8061 createNewPostCodeForm by:Javier Segarra +- fix: refs #8163 minor problem when keypress by:Javier Segarra +- fix: refs #8166 show zone error by:jorgep +- fix: removed selectedClient by:jgallego +- refs #7529 fix workerPit by:carlossa +- revert: refs #8061 test #8061 updates by:Javier Segarra +- test: fix own test by:Javier Segarra +- test: refs #8162 #8162 fix TicketList spec by:Javier Segarra + # Version 24.48 - 2024-11-25 ### Added 🆕 diff --git a/src/boot/quasar.js b/src/boot/quasar.js index d375c2f69..547517682 100644 --- a/src/boot/quasar.js +++ b/src/boot/quasar.js @@ -1,20 +1,18 @@ +import axios from 'axios'; import { boot } from 'quasar/wrappers'; import qFormMixin from './qformMixin'; import keyShortcut from './keyShortcut'; -import useNotify from 'src/composables/useNotify.js'; -import { CanceledError } from 'axios'; import { QForm } from 'quasar'; import { QLayout } from 'quasar'; import mainShortcutMixin from './mainShortcutMixin'; - -const { notify } = useNotify(); +import { useCau } from 'src/composables/useCau'; export default boot(({ app }) => { QForm.mixins = [qFormMixin]; QLayout.mixins = [mainShortcutMixin]; app.directive('shortcut', keyShortcut); - app.config.errorHandler = (error) => { + app.config.errorHandler = async (error) => { let message; const response = error.response; const responseData = response?.data; @@ -45,12 +43,12 @@ export default boot(({ app }) => { } console.error(error); - if (error instanceof CanceledError) { + if (error instanceof axios.CanceledError) { const env = process.env.NODE_ENV; if (env && env !== 'development') return; message = 'Duplicate request'; } - notify(message ?? 'globals.error', 'negative', 'error'); + await useCau(response, message); }; }); diff --git a/src/components/CreateNewPostcodeForm.vue b/src/components/CreateNewPostcodeForm.vue index d3d6708f0..c656fcb2f 100644 --- a/src/components/CreateNewPostcodeForm.vue +++ b/src/components/CreateNewPostcodeForm.vue @@ -25,7 +25,6 @@ const townsFetchDataRef = ref(false); const townFilter = ref({}); const countriesRef = ref(false); -const provincesFetchDataRef = ref(false); const provincesOptions = ref([]); const townsOptions = ref([]); const town = ref({}); @@ -71,9 +70,6 @@ async function setProvince(id, data) { await fetchTowns(); } async function onProvinceCreated(data) { - await provincesFetchDataRef.value.fetch({ - where: { countryFk: postcodeFormData.countryFk }, - }); postcodeFormData.provinceFk = data.id; } function provinceByCountry(countryFk = postcodeFormData.countryFk) { @@ -92,7 +88,6 @@ function setTown(newTown, data) { data.countryFk = newTown?.province?.countryFk ?? newTown; } async function onCityCreated(newTown, formData) { - await provincesFetchDataRef.value.fetch(); newTown.province = provincesOptions.value.find( (province) => province.id === newTown.provinceFk ); @@ -125,14 +120,6 @@ async function filterTowns(name) { - setProvince(value, data)" + @update:options=" + (data) => { + provincesOptions = data; + } + " v-model="data.provinceFk" @on-province-created="onProvinceCreated" required diff --git a/src/components/VnSelectProvince.vue b/src/components/VnSelectProvince.vue index 7d1297abf..d73ee964e 100644 --- a/src/components/VnSelectProvince.vue +++ b/src/components/VnSelectProvince.vue @@ -7,7 +7,7 @@ import VnSelectDialog from 'components/common/VnSelectDialog.vue'; import FetchData from 'components/FetchData.vue'; import CreateNewProvinceForm from './CreateNewProvinceForm.vue'; -const emit = defineEmits(['onProvinceCreated', 'onProvinceFetched']); +const emit = defineEmits(['onProvinceCreated', 'onProvinceFetched', 'update:options']); const $props = defineProps({ countryFk: { type: Number, @@ -41,6 +41,7 @@ async function onProvinceCreated(_, data) { } async function handleProvinces(data) { provincesOptions.value = data; + emit('update:options', data); } watch( diff --git a/src/components/common/VnDateBadge.vue b/src/components/common/VnDateBadge.vue new file mode 100644 index 000000000..fd6c9e8a4 --- /dev/null +++ b/src/components/common/VnDateBadge.vue @@ -0,0 +1,31 @@ + + + + {{ formatShippedDate(date) }} + + diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index e116be32a..e5ac05231 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -268,7 +268,7 @@ async function onScroll({ to, direction, from, index }) { defineExpose({ opts: myOptions }); function handleKeyDown(event) { - if (event.key === 'Tab') { + if (event.key === 'Tab' && !event.shiftKey) { event.preventDefault(); const inputValue = vnSelectRef.value?.inputValue; @@ -286,6 +286,17 @@ function handleKeyDown(event) { } vnSelectRef.value?.hidePopup(); } + + const focusableElements = document.querySelectorAll( + 'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])' + ); + const currentIndex = Array.prototype.indexOf.call( + focusableElements, + event.target + ); + if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) { + focusableElements[currentIndex + 1].focus(); + } } } diff --git a/src/components/ui/CardDescriptor.vue b/src/components/ui/CardDescriptor.vue index 83af77442..f4c0091d2 100644 --- a/src/components/ui/CardDescriptor.vue +++ b/src/components/ui/CardDescriptor.vue @@ -222,8 +222,8 @@ const toModule = computed(() => /> - - diff --git a/src/pages/Item/Card/ItemSummary.vue b/src/pages/Item/Card/ItemSummary.vue index 7606e6a22..e1b97d7c9 100644 --- a/src/pages/Item/Card/ItemSummary.vue +++ b/src/pages/Item/Card/ItemSummary.vue @@ -46,7 +46,7 @@ const getUrl = (id, param) => `#/Item/${id}/${param}`; { /> + { /> + + + + + + + + + + [ align: 'left', format: (row) => row.practicalHour, columnFilter: false, + dense: true, }, { label: t('salesTicketsTable.preparation'), @@ -190,6 +196,7 @@ const columns = computed(() => [ 'false-value': 0, 'true-value': 1, }, + component: false, }, { label: t('salesTicketsTable.zone'), @@ -206,6 +213,12 @@ const columns = computed(() => [ }, }, }, + { + label: t('salesTicketsTable.payMethod'), + name: 'payMethod', + align: 'left', + columnFilter: false, + }, { label: t('salesTicketsTable.total'), name: 'totalWithVat', @@ -219,6 +232,36 @@ const columns = computed(() => [ }, }, }, + { + label: t('salesTicketsTable.department'), + name: 'department', + align: 'left', + columnFilter: { + component: 'select', + url: 'Departments', + attrs: { + options: DepartmentOpts.value, + optionValue: 'name', + optionLabel: 'name', + dense: true, + }, + }, + }, + { + label: t('salesTicketsTable.packing'), + name: 'packing', + align: 'left', + columnFilter: { + component: 'select', + url: 'ItemPackingTypes', + attrs: { + options: ItemPackingTypeOpts.value, + 'option-value': 'code', + 'option-label': 'code', + dense: true, + }, + }, + }, { align: 'right', name: 'tableActions', @@ -250,19 +293,6 @@ const columns = computed(() => [ }, ]); -const getBadgeAttrs = (date) => { - let today = Date.vnNew(); - today.setHours(0, 0, 0, 0); - let timeTicket = new Date(date); - timeTicket.setHours(0, 0, 0, 0); - - let timeDiff = today - timeTicket; - - if (timeDiff == 0) return { color: 'warning', 'text-color': 'black' }; - if (timeDiff < 0) return { color: 'success', 'text-color': 'black' }; - return { color: 'transparent', 'text-color': 'white' }; -}; - let refreshTimer = null; const autoRefreshHandler = (value) => { @@ -279,14 +309,6 @@ const totalPriceColor = (ticket) => { if (total > 0 && total < 50) return 'warning'; }; -const formatShippedDate = (date) => { - if (!date) return '-'; - const dateSplit = date.split('T'); - const [year, month, day] = dateSplit[0].split('-'); - const newDate = new Date(year, month - 1, day); - return toDateFormat(newDate); -}; - const openTab = (id) => window.open(`#/ticket/${id}/sale`, '_blank', 'noopener, noreferrer'); @@ -318,6 +340,24 @@ const openTab = (id) => auto-load @on-fetch="(data) => (zoneOpts = data)" /> + (ItemPackingTypeOpts = data)" + /> + (DepartmentOpts = data)" + /> @@ -337,7 +377,7 @@ const openTab = (id) => auto-load :row-click="({ id }) => openTab(id)" :disable-option="{ card: true }" - :user-params="{ from, to, scopeDays: 0 }" + :user-params="{ from, to, scopeDays: 0, packing }" > - - {{ formatShippedDate(row.shippedDate) }} - + diff --git a/src/pages/Monitor/locale/en.yml b/src/pages/Monitor/locale/en.yml index ff4031654..e61a24979 100644 --- a/src/pages/Monitor/locale/en.yml +++ b/src/pages/Monitor/locale/en.yml @@ -26,8 +26,8 @@ salesTicketsTable: componentLack: Component lack tooLittle: Ticket too little identifier: Identifier - theoretical: Theoretical - practical: Practical + theoretical: H.Theor + practical: H.Prac province: Province state: State isFragile: Is fragile @@ -35,7 +35,10 @@ salesTicketsTable: goToLines: Go to lines preview: Preview total: Total - preparation: Preparation + preparation: H.Prep + payMethod: Pay method + department: Department + packing: ITP searchBar: label: Search tickets info: Search tickets by id or alias diff --git a/src/pages/Monitor/locale/es.yml b/src/pages/Monitor/locale/es.yml index a2ed3bb1a..30afb1904 100644 --- a/src/pages/Monitor/locale/es.yml +++ b/src/pages/Monitor/locale/es.yml @@ -26,8 +26,8 @@ salesTicketsTable: componentLack: Faltan componentes tooLittle: Ticket demasiado pequeño identifier: Identificador - theoretical: Teórica - practical: Práctica + theoretical: H.Teór + practical: H.Prác province: Provincia state: Estado isFragile: Es frágil @@ -35,7 +35,10 @@ salesTicketsTable: goToLines: Ir a líneas preview: Vista previa total: Total - preparation: Preparación + preparation: H.Prep + payMethod: Método de pago + department: Departamento + packing: ITP searchBar: label: Buscar tickets info: Buscar tickets por identificador o alias diff --git a/src/pages/Order/Card/OrderCatalog.vue b/src/pages/Order/Card/OrderCatalog.vue index 453037f15..b7af615bb 100644 --- a/src/pages/Order/Card/OrderCatalog.vue +++ b/src/pages/Order/Card/OrderCatalog.vue @@ -75,19 +75,6 @@ watch( }, { immediate: true } ); -const onItemSaved = (updatedItem) => { - requestAnimationFrame(() => { - scrollToItem(updatedItem.items[0].itemFk); - }); -}; - -const scrollToItem = async (id) => { - const element = itemRefs.value[id]?.$el; - if (element) { - element.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } -}; -provide('onItemSaved', onItemSaved); diff --git a/src/pages/Order/Card/OrderCatalogFilter.vue b/src/pages/Order/Card/OrderCatalogFilter.vue index 1dd569fb5..39627595d 100644 --- a/src/pages/Order/Card/OrderCatalogFilter.vue +++ b/src/pages/Order/Card/OrderCatalogFilter.vue @@ -65,7 +65,6 @@ const selectCategory = async (params, category, search) => { params.typeFk = null; params.categoryFk = category.id; await loadTypes(category?.id); - await search(); }; const loadTypes = async (id) => { diff --git a/src/pages/Order/Card/OrderCatalogItemDialog.vue b/src/pages/Order/Card/OrderCatalogItemDialog.vue index b1cd8ed6b..0d55b7de1 100644 --- a/src/pages/Order/Card/OrderCatalogItemDialog.vue +++ b/src/pages/Order/Card/OrderCatalogItemDialog.vue @@ -1,12 +1,12 @@ (total = response)" + @on-fetch=" + (response) => { + total = response; + } + " /> - + diff --git a/src/pages/Route/RouteExtendedList.vue b/src/pages/Route/RouteExtendedList.vue index dbf646935..38e907ce0 100644 --- a/src/pages/Route/RouteExtendedList.vue +++ b/src/pages/Route/RouteExtendedList.vue @@ -223,10 +223,10 @@ function navigate(id) { router.push({ path: `/route/${id}` }); } -const cloneRoutes = () => { +const cloneRoutes = async () => { if (!selectedRows.value.length || !startingDate.value) return; - axios.post('Routes/clone', { - created: startingDate.value, + await axios.post('Routes/clone', { + dated: startingDate.value, ids: selectedRows.value.map((row) => row?.id), }); startingDate.value = null; @@ -274,7 +274,6 @@ const openTicketsDialog = (id) => { {{ t('route.Select the starting date') }} - import axios from 'axios'; -import { computed, ref, toRefs } from 'vue'; +import { computed, onMounted, ref, toRefs, watch } from 'vue'; import { useQuasar } from 'quasar'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; @@ -24,6 +24,15 @@ const props = defineProps({ }, }); +onMounted(() => { + restoreTicket(); +}); + +watch( + () => props.ticket, + () => restoreTicket +); + const { push, currentRoute } = useRouter(); const { dialog, notify } = useQuasar(); const { t } = useI18n(); @@ -42,6 +51,7 @@ const hasPdf = ref(); const weight = ref(); const hasDocuwareFile = ref(); const quasar = useQuasar(); +const canRestoreTicket = ref(false); const actions = { clone: async () => { const opts = { message: t('Ticket cloned'), type: 'positive' }; @@ -373,6 +383,54 @@ async function uploadDocuware(force) { if (data) notify({ message: t('PDF sent!'), type: 'positive' }); } + +const restoreTicket = async () => { + const filter = { + fields: ['id', 'originFk', 'creationDate', 'newInstance'], + where: { + originFk: ticketId.value, + newInstance: { like: '%"isDeleted":true%' }, + }, + order: 'creationDate DESC', + limit: 1, + }; + const params = { filter: JSON.stringify(filter) }; + + const { data } = await axios.get(`TicketLogs`, { params }); + + if (data && data.length) { + const now = Date.vnNew(); + const maxDate = new Date(data[0].creationDate); + maxDate.setHours(maxDate.getHours() + 1); + if (now <= maxDate) { + return (canRestoreTicket.value = true); + } + return (canRestoreTicket.value = false); + } + return (canRestoreTicket.value = false); +}; + +async function openRestoreConfirmation(force) { + if (!force) + return quasar + .dialog({ + component: VnConfirm, + componentProps: { + title: t('Are you sure you want to restore the ticket?'), + message: t('You are going to restore this ticket'), + }, + }) + .onOk(async () => { + ticketToRestore(); + }); +} + +async function ticketToRestore() { + const { data } = await axios.post(`Tickets/${ticketId.value}/restore`); + if (data) { + notify({ message: t('Ticket restored'), type: 'positive' }); + } +} {{ t('Show Proforma') }} + + + + + {{ t('Restore ticket') }} + diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index 73ea34fe9..f09aec816 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -10,6 +10,7 @@ import { useState } from 'src/composables/useState'; import axios from 'axios'; import VnImg from 'src/components/ui/VnImg.vue'; import EditPictureForm from 'components/EditPictureForm.vue'; +import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; const $props = defineProps({ id: { @@ -143,10 +144,14 @@ const handlePhotoUpdated = (evt = false) => { :value="entity.user?.emailUser?.email" copy /> - + + + + + + {{ t('globals.phone') }} diff --git a/src/pages/Zone/Card/ZoneBasicData.vue b/src/pages/Zone/Card/ZoneBasicData.vue index 731e03ba7..2d65476ce 100644 --- a/src/pages/Zone/Card/ZoneBasicData.vue +++ b/src/pages/Zone/Card/ZoneBasicData.vue @@ -108,7 +108,20 @@ const agencyOptions = ref([]); clearable /> - + + + diff --git a/test/cypress/integration/item/itemLastEntries.spec.js b/test/cypress/integration/item/itemLastEntries.spec.js new file mode 100644 index 000000000..c94cfa480 --- /dev/null +++ b/test/cypress/integration/item/itemLastEntries.spec.js @@ -0,0 +1,20 @@ +describe('ItemLastEntries', () => { + beforeEach(() => { + cy.viewport(1280, 720); + cy.login('buyer'); + cy.visit('/#/item/1/last-entries'); + cy.intercept('GET', /.*lastEntriesFilter/).as('item'); + cy.waitForElement('tbody'); + }); + + it('should filter by agency', () => { + cy.get('tbody > tr') + .its('length') + .then((rowCount) => { + cy.get('[data-cy="hideInventory"]').click(); + cy.wait('@item'); + cy.waitForElement('tbody'); + cy.get('tbody > tr').should('have.length.greaterThan', rowCount); + }); + }); +}); diff --git a/test/cypress/integration/zone/zoneBasicData.spec.js b/test/cypress/integration/zone/zoneBasicData.spec.js index de85dac94..6229039b7 100644 --- a/test/cypress/integration/zone/zoneBasicData.spec.js +++ b/test/cypress/integration/zone/zoneBasicData.spec.js @@ -1,5 +1,6 @@ describe('ZoneBasicData', () => { const notification = '.q-notification__message'; + const priceBasicData = '[data-cy="Price_input"]'; beforeEach(() => { cy.viewport(1280, 720); @@ -13,9 +14,15 @@ describe('ZoneBasicData', () => { cy.get(notification).should('contains.text', "can't be blank"); }); + it('should throw an error if the price is empty', () => { + cy.get(priceBasicData).clear(); + cy.get('.q-btn-group > .q-btn--standard').click(); + cy.get(notification).should('contains.text', 'cannot be blank'); + }); + it("should edit the basicData's zone", () => { cy.get('.q-card > :nth-child(1)').type(' modified'); cy.get('.q-btn-group > .q-btn--standard').click(); - cy.get(notification).should('contains.text', 'Data saved'); + cy.checkNotification('Data saved'); }); }); diff --git a/test/vitest/__tests__/components/common/VnDiscount.spec.js b/test/vitest/__tests__/components/common/VnDiscount.spec.js new file mode 100644 index 000000000..5d5be61ac --- /dev/null +++ b/test/vitest/__tests__/components/common/VnDiscount.spec.js @@ -0,0 +1,28 @@ +import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; +import { createWrapper } from 'app/test/vitest/helper'; +import VnDiscount from 'components/common/vnDiscount.vue'; + +describe('VnDiscount', () => { + let vm; + + beforeAll(() => { + vm = createWrapper(VnDiscount, { + props: { + data: {}, + price: 100, + quantity: 2, + discount: 10, + } + }).vm; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('total', () => { + it('should calculate total correctly', () => { + expect(vm.total).toBe(180); + }); + }); +}); \ No newline at end of file
{{ t('route.Select the starting date') }}