From 5b870c2c2c24e1517dce63388492b3f904d5421d Mon Sep 17 00:00:00 2001 From: jorgep Date: Thu, 17 Apr 2025 11:39:48 +0200 Subject: [PATCH 01/12] fix: refs #7939 show userErr & useNotify composable --- src/pages/Login/LoginMain.vue | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/pages/Login/LoginMain.vue b/src/pages/Login/LoginMain.vue index a4c3566a9..ce945e0ad 100644 --- a/src/pages/Login/LoginMain.vue +++ b/src/pages/Login/LoginMain.vue @@ -6,7 +6,7 @@ import { useRouter } from 'vue-router'; import VnInputPassword from 'src/components/common/VnInputPassword.vue'; import { useSession } from 'src/composables/useSession'; import { useLogin } from 'src/composables/useLogin'; - +import useNotify from 'src/composables/useNotify'; import VnLogo from 'components/ui/VnLogo.vue'; import VnInput from 'src/components/common/VnInput.vue'; import axios from 'axios'; @@ -15,6 +15,7 @@ const session = useSession(); const loginCache = useLogin(); const router = useRouter(); const { t } = useI18n(); +const { notify } = useNotify(); const username = ref(''); const password = ref(''); @@ -32,23 +33,20 @@ async function onSubmit() { data.keepLogin = keepLogin.value; await session.setLogin(data); } catch (res) { - if (res.response?.data?.error?.code === 'REQUIRES_2FA') { - Notify.create({ - message: t('login.twoFactorRequired'), - icon: 'phoneLink_lock', - type: 'warning', - }); + const err = res.response?.data?.error; + if (err?.code === 'REQUIRES_2FA') { + notify(t('login.twoFactorRequired'), 'warning', 'phoneLink_lock'); params.keepLogin = keepLogin.value; loginCache.setUser(params); return router.push({ name: 'TwoFactor', query: router.currentRoute.value?.query, }); + } else if (err?.name == 'UserError') { + notify(err.message, 'negative'); + } else { + notify(t('login.loginError'), 'negative'); } - Notify.create({ - message: t('login.loginError'), - type: 'negative', - }); } } From a8d39a9b9663a4510c0be8ad180060588da0c30c Mon Sep 17 00:00:00 2001 From: jorgep Date: Tue, 22 Apr 2025 16:48:13 +0200 Subject: [PATCH 02/12] feat(login): refs #7939 add session expiration handling and improve error notifications --- src/boot/quasar.js | 10 +++++++++- src/i18n/locale/en.yml | 2 ++ src/i18n/locale/es.yml | 2 ++ src/pages/Login/LoginMain.vue | 14 +++----------- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/boot/quasar.js b/src/boot/quasar.js index a8c397b83..8ef748c01 100644 --- a/src/boot/quasar.js +++ b/src/boot/quasar.js @@ -7,7 +7,7 @@ import { QLayout } from 'quasar'; import mainShortcutMixin from './mainShortcutMixin'; import { useCau } from 'src/composables/useCau'; -export default boot(({ app }) => { +export default boot(({ app, router }) => { QForm.mixins = [qFormMixin]; QLayout.mixins = [mainShortcutMixin]; @@ -22,6 +22,14 @@ export default boot(({ app }) => { } switch (response?.status) { + case 401: + if (!router.currentRoute.value.name.includes('login')) { + message = 'errors.sessionExpired'; + } else message = 'login.loginError'; + break; + case 403: + if (!message || message.toLowerCase() === 'access denied') + message = 'errors.accessDenied'; case 422: if (error.name == 'ValidationError') message += diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 3c1c80954..efcad468e 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -395,6 +395,8 @@ errors: updateUserConfig: Error updating user config tokenConfig: Error fetching token config writeRequest: The requested operation could not be completed + sessionExpired: Your session has expired. Please log in again + accessDenied: Access denied login: title: Login username: Username diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 518985831..71105db3e 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -391,6 +391,8 @@ errors: updateUserConfig: Error al actualizar la configuración de usuario tokenConfig: Error al obtener configuración de token writeRequest: No se pudo completar la operación solicitada + sessionExpired: Tu sesión ha expirado, por favor vuelve a iniciar sesión + accessDenied: Acceso denegado login: title: Inicio de sesión username: Nombre de usuario diff --git a/src/pages/Login/LoginMain.vue b/src/pages/Login/LoginMain.vue index ce945e0ad..0a84f8d02 100644 --- a/src/pages/Login/LoginMain.vue +++ b/src/pages/Login/LoginMain.vue @@ -1,6 +1,5 @@ From b85407d8c2280fd927bce8b177d7770997c71a87 Mon Sep 17 00:00:00 2001 From: jon Date: Thu, 24 Apr 2025 14:24:20 +0200 Subject: [PATCH 03/12] feat: refs #8180 not allow to close a zone if there are tickets for that day --- .../Zone/Card/ZoneEventExclusionForm.vue | 27 +++++++++++++++++++ src/pages/Zone/locale/en.yml | 1 + src/pages/Zone/locale/es.yml | 1 + .../integration/zone/zoneCalendar.spec.js | 12 +++++++++ 4 files changed, 41 insertions(+) diff --git a/src/pages/Zone/Card/ZoneEventExclusionForm.vue b/src/pages/Zone/Card/ZoneEventExclusionForm.vue index 89a6e02f8..bf82c1786 100644 --- a/src/pages/Zone/Card/ZoneEventExclusionForm.vue +++ b/src/pages/Zone/Card/ZoneEventExclusionForm.vue @@ -66,6 +66,8 @@ const excludeType = computed({ const arrayData = useArrayData('ZoneEvents'); const exclusionGeoCreate = async () => { + if (await hasTicketsForDay(route.params.id, dated.value)) return; + const params = { zoneFk: parseInt(route.params.id), date: dated.value, @@ -87,6 +89,8 @@ const exclusionCreate = async () => { }; const zoneIds = props.zoneIds?.length ? props.zoneIds : [route.params.id]; for (const id of zoneIds) { + if (await hasTicketsForDay(id, dated.value)) return; + const url = `Zones/${id}/exclusions`; let today = moment(dated.value); let lastDay = today.clone().add(nMonths, 'months').endOf('month'); @@ -123,6 +127,29 @@ const exclusionCreate = async () => { await refetchEvents(); }; +const hasTickets = async (zoneId, date) => { + let hasDayTickets = await axios.get('Zones/getZoneTickets', { + params: { + zoneFk: zoneId, + date: date, + }, + }); + + if (hasDayTickets.data.length > 0) { + quasar.notify({ + message: t('eventsExclusionForm.cantCloseZone'), + type: 'negative', + }); + return true; + } + return false; +}; + +const hasTicketsForDay = async (zoneId, date) => { + const tickets = await hasTickets(zoneId, date); + return tickets; +}; + const onSubmit = async () => { if (excludeType.value === 'all') exclusionCreate(); else exclusionGeoCreate(); diff --git a/src/pages/Zone/locale/en.yml b/src/pages/Zone/locale/en.yml index f46a98ee6..b69a05719 100644 --- a/src/pages/Zone/locale/en.yml +++ b/src/pages/Zone/locale/en.yml @@ -80,6 +80,7 @@ eventsExclusionForm: all: All specificLocations: Specific locations rootTreeLabel: Locations where it is not distributed + cantCloseZone: Can not close this zone because there are tickets programmed for that day eventsInclusionForm: addEvent: Add event editEvent: Edit event diff --git a/src/pages/Zone/locale/es.yml b/src/pages/Zone/locale/es.yml index 7a23bdc02..942a61162 100644 --- a/src/pages/Zone/locale/es.yml +++ b/src/pages/Zone/locale/es.yml @@ -81,6 +81,7 @@ eventsExclusionForm: all: Todo specificLocations: Localizaciones concretas rootTreeLabel: Localizaciones en las que no se reparte + cantCloseZone: No se puede cerrar la zona porque hay tickets programados para ese día eventsInclusionForm: addEvent: Añadir evento editEvent: Editar evento diff --git a/test/cypress/integration/zone/zoneCalendar.spec.js b/test/cypress/integration/zone/zoneCalendar.spec.js index 68b85d1d2..3d330f9f3 100644 --- a/test/cypress/integration/zone/zoneCalendar.spec.js +++ b/test/cypress/integration/zone/zoneCalendar.spec.js @@ -47,4 +47,16 @@ describe('ZoneCalendar', () => { cy.dataCy('ZoneEventExclusionDeleteBtn').click(); cy.dataCy('VnConfirm_confirm').click(); }); + + it('should not exclude an event if there are tickets for that zone and day', () => { + cy.visit(`/#/zone/3/events`); + cy.get('.q-mb-sm > .q-radio__inner').click(); + cy.get( + '.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]', + ).click(); + cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.checkNotification( + 'Can not close this zone because there are tickets programmed for that day', + ); + }); }); From 8dd67efeb99bd7e5bf4942282c6e783065205a43 Mon Sep 17 00:00:00 2001 From: jorgep Date: Fri, 25 Apr 2025 13:16:50 +0200 Subject: [PATCH 04/12] fix: refs #7939 normalize login route check to be case insensitive --- src/boot/quasar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/boot/quasar.js b/src/boot/quasar.js index 8ef748c01..a5d915305 100644 --- a/src/boot/quasar.js +++ b/src/boot/quasar.js @@ -23,7 +23,7 @@ export default boot(({ app, router }) => { switch (response?.status) { case 401: - if (!router.currentRoute.value.name.includes('login')) { + if (!router.currentRoute.value.name.toLowerCase().includes('login')) { message = 'errors.sessionExpired'; } else message = 'login.loginError'; break; From 3ceabd5831ebbb22341281ec6f05c9705993984f Mon Sep 17 00:00:00 2001 From: jorgep Date: Fri, 25 Apr 2025 13:28:32 +0200 Subject: [PATCH 05/12] refactor: refs #7939 streamline logout tests and improve session expiration error handling --- test/cypress/integration/login/logout.spec.js | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/test/cypress/integration/login/logout.spec.js b/test/cypress/integration/login/logout.spec.js index b17e42794..174ca6066 100644 --- a/test/cypress/integration/login/logout.spec.js +++ b/test/cypress/integration/login/logout.spec.js @@ -5,33 +5,28 @@ describe('Logout', () => { cy.visit(`/#/dashboard`); cy.waitForElement('.q-page', 6000); }); - describe('by user', () => { - it('should logout', () => { - cy.get('#user').click(); - cy.get('#logout').click(); - }); + + it('should logout', () => { + cy.get('#user').click(); + cy.get('#logout').click(); }); - describe('not user', () => { - beforeEach(() => { - cy.intercept('GET', '**StarredModules**', { - statusCode: 401, - body: { - error: { - statusCode: 401, - name: 'Error', - message: 'Authorization Required', - code: 'AUTHORIZATION_REQUIRED', - }, + + it('should throw session expired error if token has expired or is not valid during navigation', () => { + cy.intercept('GET', '**StarredModules**', { + statusCode: 401, + body: { + error: { + statusCode: 401, + name: 'Error', + message: 'Authorization Required', + code: 'AUTHORIZATION_REQUIRED', }, - statusMessage: 'AUTHORIZATION_REQUIRED', - }).as('badRequest'); - }); + }, + statusMessage: 'AUTHORIZATION_REQUIRED', + }).as('badRequest'); + cy.get('.q-list').should('be.visible').first().should('be.visible').click(); + cy.wait('@badRequest'); - it('when token not exists', () => { - cy.get('.q-list').should('be.visible').first().should('be.visible').click(); - cy.wait('@badRequest'); - - cy.checkNotification('Authorization Required'); - }); + cy.checkNotification('Your session has expired. Please log in again'); }); }); From dd218361f0dcb20045f89b35b25ad3b9de1a7ce5 Mon Sep 17 00:00:00 2001 From: jorgep Date: Fri, 25 Apr 2025 14:17:46 +0200 Subject: [PATCH 06/12] fix: refs #7939 add orderBy property to columns configuration in EntryPreAccount.vue --- src/pages/Entry/EntryPreAccount.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Entry/EntryPreAccount.vue b/src/pages/Entry/EntryPreAccount.vue index 26683b6f4..feb26604c 100644 --- a/src/pages/Entry/EntryPreAccount.vue +++ b/src/pages/Entry/EntryPreAccount.vue @@ -73,6 +73,7 @@ const columns = computed(() => [ optionLabel: 'code', options: companies.value, }, + orderBy: false, }, { name: 'warehouse', From c432709a04bccead6769a4f72e36a3d2304669cf Mon Sep 17 00:00:00 2001 From: jorgep Date: Fri, 25 Apr 2025 17:20:45 +0200 Subject: [PATCH 07/12] feat: refs #7939 add VnSelectExpense --- src/components/VnTable/VnColumn.vue | 6 +- src/components/common/VnSelectExpense.vue | 49 ++ src/pages/InvoiceIn/Card/InvoiceInVat.vue | 531 ++++++---------------- 3 files changed, 181 insertions(+), 405 deletions(-) create mode 100644 src/components/common/VnSelectExpense.vue diff --git a/src/components/VnTable/VnColumn.vue b/src/components/VnTable/VnColumn.vue index 3ce62c5de..b6cc5665f 100644 --- a/src/components/VnTable/VnColumn.vue +++ b/src/components/VnTable/VnColumn.vue @@ -180,7 +180,11 @@ const col = computed(() => { ) newColumn.component = 'checkbox'; if ($props.default && !newColumn.component) newColumn.component = $props.default; - + + if (typeof newColumn.component !== 'string') { + newColumn.attrs = { ...newColumn.component.attrs, autofocus: $props.autofocus }; + newColumn.event = { ...newColumn.component.event, ...$props?.eventHandlers }; + } return newColumn; }); diff --git a/src/components/common/VnSelectExpense.vue b/src/components/common/VnSelectExpense.vue new file mode 100644 index 000000000..406715b84 --- /dev/null +++ b/src/components/common/VnSelectExpense.vue @@ -0,0 +1,49 @@ + + + +es: + Create a new expense: Crear nuevo gasto + diff --git a/src/pages/InvoiceIn/Card/InvoiceInVat.vue b/src/pages/InvoiceIn/Card/InvoiceInVat.vue index 61c3040ae..aa9c671d9 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInVat.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInVat.vue @@ -1,21 +1,16 @@