diff --git a/package-lock.json b/package-lock.json index a3a9dcc63..2e96d2ccf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "23.40.01", + "version": "23.36.01", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 583233204..bfaba6f10 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "eslint --ext .js,.vue ./", "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", "test:e2e": "cypress open", - "test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run --browser chromium", + "test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run", "test": "echo \"See package.json => scripts for available tests.\" && exit 0", "test:unit": "vitest", "test:unit:ci": "vitest run" diff --git a/quasar.config.js b/quasar.config.js index 3a7dc1f1e..755e96bd3 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -29,7 +29,7 @@ module.exports = configure(function (/* ctx */) { // app boot file (/src/boot) // --> boot files are part of "main.js" // https://v2.quasar.dev/quasar-cli/boot-files - boot: ['i18n', 'axios', 'vnDate'], + boot: ['i18n', 'axios', 'vnDate', 'validations'], // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css css: ['app.scss'], diff --git a/src/boot/validations.js b/src/boot/validations.js new file mode 100644 index 000000000..31e232f86 --- /dev/null +++ b/src/boot/validations.js @@ -0,0 +1,6 @@ +import { boot } from 'quasar/wrappers'; +import { useValidationsStore } from 'src/stores/useValidationsStore'; + +export default boot(async ({ store }) => { + await useValidationsStore(store).fetchModels(); +}); diff --git a/src/components/FetchData.vue b/src/components/FetchData.vue index 251d7502a..f0d908972 100644 --- a/src/components/FetchData.vue +++ b/src/components/FetchData.vue @@ -46,7 +46,7 @@ async function fetch() { if ($props.limit) filter.limit = $props.limit; const { data } = await axios.get($props.url, { - params: { filter }, + params: { filter: JSON.stringify(filter) }, }); emit('onFetch', data); diff --git a/src/components/common/VnJsonValue.vue b/src/components/common/VnJsonValue.vue new file mode 100644 index 000000000..a2e858d0d --- /dev/null +++ b/src/components/common/VnJsonValue.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/src/components/common/VnLog.vue b/src/components/common/VnLog.vue index 1213c8bbc..e89fce476 100644 --- a/src/components/common/VnLog.vue +++ b/src/components/common/VnLog.vue @@ -1,19 +1,25 @@ - en: + to: To + pointRecord: View record at this point in time + recordChanges: show all record changes + tooltips: + search: Search by id or concept + changes: Search by changes actions: - insert: Creates - update: Updates - delete: Deletes - models: - Claim: Claim - ClaimDms: Document - ClaimBeginning: Claimed Sales - ClaimObservation: Observation + Creates: Creates + Edits: Edits + Deletes: Deletes + Accesses: Accesses + Users: + User: Usuario + All: Todo + System: Sistema properties: id: ID claimFk: Claim ID @@ -172,6 +1030,12 @@ en: responsibility: Responsibility packages: Packages es: + to: Hasta + pointRecord: Ver el registro en este punto + recordChanges: Mostrar todos los cambios realizados en el registro + tooltips: + search: Buscar por identificador o concepto + changes: Buscar por cambios. Los atributos deben buscarse por su nombre interno, para obtenerlo situar el cursor sobre el atributo. Audit logs: Registros de auditoría Property: Propiedad Before: Antes @@ -179,14 +1043,14 @@ es: Yes: Si Nothing: Nada actions: - insert: Crea - update: Actualiza - delete: Elimina - models: - Claim: Reclamación - ClaimDms: Documento - ClaimBeginning: Línea reclamada - ClaimObservation: Observación + Creates: Crea + Edits: Modifica + Deletes: Elimina + Accesses: Accede + Users: + User: Usuario + All: Todo + System: Sistema properties: id: ID claimFk: ID reclamación diff --git a/src/composables/useColor.js b/src/composables/useColor.js new file mode 100644 index 000000000..b325e985f --- /dev/null +++ b/src/composables/useColor.js @@ -0,0 +1,35 @@ +export function djb2a(string) { + let hash = 5381; + for (let i = 0; i < string.length; i++) + hash = ((hash << 5) + hash) ^ string.charCodeAt(i); + return hash >>> 0; +} + +export function useColor(value) { + return '#' + colors[djb2a(value || '') % colors.length]; +} + +const colors = [ + 'b5b941', // Yellow + 'ae9681', // Peach + 'd78767', // Salmon + 'cc7000', // Orange bright + 'e2553d', // Coral + '8B0000', // Red dark + 'de4362', // Red crimson + 'FF1493', // Ping intense + 'be39a2', // Pink light + 'b754cf', // Purple middle + 'a87ba8', // Pink + '8a69cd', // Blue lavender + 'ab20ab', // Purple dark + '00b5b8', // Turquoise + '1fa8a1', // Green ocean + '5681cf', // Blue steel + '3399fe', // Blue sky + '6d9c3e', // Green chartreuse + '51bb51', // Green lime + '518b8b', // Gray board + '7e7e7e', // Gray + '5d5d5d', // Gray dark +]; diff --git a/src/composables/useValidator.js b/src/composables/useValidator.js index bc48332a2..3f9f00367 100644 --- a/src/composables/useValidator.js +++ b/src/composables/useValidator.js @@ -1,19 +1,12 @@ -import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; -import axios from 'axios'; import validator from 'validator'; - -const models = ref(null); +import { useValidationsStore } from 'src/stores/useValidationsStore'; export function useValidator() { - if (!models.value) fetch(); - - function fetch() { - axios.get('Schemas/ModelInfo').then((response) => (models.value = response.data)); - } + const models = useValidationsStore().validations; function validate(propertyRule) { - const modelInfo = models.value; + const modelInfo = models; if (!modelInfo || !propertyRule) return; const rule = propertyRule.split('.'); @@ -75,5 +68,6 @@ export function useValidator() { return { validate, + models, }; } diff --git a/src/filters/index.js b/src/filters/index.js index 158ce1009..b0c441641 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -2,6 +2,7 @@ import toLowerCase from './toLowerCase'; import toDate from './toDate'; import toDateString from './toDateString'; import toDateHour from './toDateHour'; +import toRelativeDate from './toRelativeDate'; import toCurrency from './toCurrency'; import toPercentage from './toPercentage'; import toLowerCamel from './toLowerCamel'; @@ -13,6 +14,7 @@ export { toDate, toDateString, toDateHour, + toRelativeDate, toCurrency, toPercentage, dashIfEmpty, diff --git a/src/filters/toRelativeDate.js b/src/filters/toRelativeDate.js new file mode 100644 index 000000000..76e67dbea --- /dev/null +++ b/src/filters/toRelativeDate.js @@ -0,0 +1,32 @@ +import { useI18n } from 'vue-i18n'; + +export default function formatDate(dateVal) { + const { t } = useI18n(); + const today = new Date(); + if (dateVal == null) return ''; + + const date = new Date(dateVal); + const dateZeroTime = new Date(dateVal); + dateZeroTime.setHours(0, 0, 0, 0); + const diff = Math.trunc( + (today.getTime() - dateZeroTime.getTime()) / (1000 * 3600 * 24) + ); + let format; + if (diff === 0) format = t('globals.today'); + else if (diff === 1) format = t('globals.yesterday'); + else if (diff > 1 && diff < 7) { + const options = { weekday: 'short' }; + format = date.toLocaleDateString(t('globals.dateFormat'), options); + } else if (today.getFullYear() === date.getFullYear()) { + const options = { day: 'numeric', month: 'short' }; + format = date.toLocaleDateString(t('globals.dateFormat'), options); + } else { + const options = { year: 'numeric', month: '2-digit', day: '2-digit' }; + format = date.toLocaleDateString(t('globals.dateFormat'), options); + } + + // Formatear la hora en HH:mm + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + return `${format} ${hours}:${minutes}`; +} diff --git a/src/i18n/en/index.js b/src/i18n/en/index.js index 148ce743d..41bf03c5e 100644 --- a/src/i18n/en/index.js +++ b/src/i18n/en/index.js @@ -5,6 +5,9 @@ export default { en: 'English', }, language: 'Language', + entity: 'Entity', + user: 'User', + details: 'Details', collapseMenu: 'Collapse left menu', backToDashboard: 'Return to dashboard', notifications: 'Notifications', @@ -13,8 +16,11 @@ export default { pinnedModules: 'Pinned modules', darkMode: 'Dark mode', logOut: 'Log out', + date: 'Date', dataSaved: 'Data saved', dataDeleted: 'Data deleted', + search: 'Search', + changes: 'Changes', add: 'Add', create: 'Create', save: 'Save', @@ -36,6 +42,9 @@ export default { summary: { basicData: 'Basic data', }, + today: 'Today', + yesterday: 'Yesterday', + dateFormat: 'en-GB', microsip: 'Open in MicroSIP', noSelectedRows: `You don't have any line selected`, }, diff --git a/src/i18n/es/index.js b/src/i18n/es/index.js index de9a2888b..83893ece4 100644 --- a/src/i18n/es/index.js +++ b/src/i18n/es/index.js @@ -5,6 +5,9 @@ export default { en: 'Inglés', }, language: 'Idioma', + entity: 'Entidad', + user: 'Usuario', + details: 'Detalles', collapseMenu: 'Contraer menú lateral', backToDashboard: 'Volver al tablón', notifications: 'Notificaciones', @@ -13,8 +16,11 @@ export default { pinnedModules: 'Módulos fijados', darkMode: 'Modo oscuro', logOut: 'Cerrar sesión', + date: 'Fecha', dataSaved: 'Datos guardados', dataDeleted: 'Datos eliminados', + search: 'Buscar', + changes: 'Cambios', add: 'Añadir', create: 'Crear', save: 'Guardar', @@ -36,6 +42,9 @@ export default { summary: { basicData: 'Datos básicos', }, + today: 'Hoy', + yesterday: 'Ayer', + dateFormat: 'es-ES', noSelectedRows: `No tienes ninguna línea seleccionada`, microsip: 'Abrir en MicroSIP', }, diff --git a/src/stores/useValidationsStore.js b/src/stores/useValidationsStore.js new file mode 100644 index 000000000..e00812379 --- /dev/null +++ b/src/stores/useValidationsStore.js @@ -0,0 +1,18 @@ +import axios from 'axios'; +import { defineStore } from 'pinia'; + +export const useValidationsStore = defineStore('validationsStore', { + state: () => ({ + validations: null, + }), + actions: { + async fetchModels() { + try { + const { data } = await axios.get('Schemas/modelinfo'); + this.validations = data; + } catch (error) { + console.error('Error al obtener las validaciones:', error); + } + }, + }, +}); diff --git a/test/cypress/integration/vnLog.spec.js b/test/cypress/integration/vnLog.spec.js new file mode 100644 index 000000000..d120b8ad4 --- /dev/null +++ b/test/cypress/integration/vnLog.spec.js @@ -0,0 +1,25 @@ +/// +describe('VnLog', () => { + const chips = [ + ':nth-child(1) > :nth-child(1) > .q-item__label > .q-chip > .q-chip__content', + ':nth-child(2) > :nth-child(1) > .q-item__label > .q-chip > .q-chip__content', + ]; + beforeEach(() => { + cy.login('developer'); + cy.visit(`/#/claim/${1}/log`); + cy.openRightMenu('.timeline'); + }); + + it('should filter by insert actions', () => { + cy.checkOption(':nth-child(7) > .q-checkbox'); + cy.get('.q-page').click(); + cy.validateContent(chips[0], 'Document'); + cy.validateContent(chips[1], 'Beginning'); + }); + + it('should filter by entity', () => { + cy.selectOption('.q-drawer--right .q-item > .q-select', 'Claim'); + cy.get('.q-page').click(); + cy.validateContent(chips[0], 'Claim'); + }); +}); diff --git a/test/cypress/integration/worker/workerList.spec.js b/test/cypress/integration/worker/workerList.spec.js index 219633263..2ab84c623 100644 --- a/test/cypress/integration/worker/workerList.spec.js +++ b/test/cypress/integration/worker/workerList.spec.js @@ -1,4 +1,6 @@ describe('WorkerList', () => { + const workerFieldNames = + '.card-list-body > .list-items > :nth-child(2) > .value > span'; beforeEach(() => { cy.viewport(1280, 720); cy.login('developer'); @@ -6,20 +8,14 @@ describe('WorkerList', () => { }); it('should load workers', () => { - cy.get('.card-list-body > .list-items > :nth-child(2) > .value > span') - .eq(0) - .should('have.text', 'JessicaJones'); - cy.get('.card-list-body > .list-items > :nth-child(2) > .value > span') - .eq(1) - .should('have.text', 'BruceBanner'); - cy.get('.card-list-body > .list-items > :nth-child(2) > .value > span') - .eq(2) - .should('have.text', 'CharlesXavier'); + cy.get(workerFieldNames).eq(0).should('have.text', 'JessicaJones'); + cy.get(workerFieldNames).eq(1).should('have.text', 'BruceBanner'); + cy.get(workerFieldNames).eq(2).should('have.text', 'CharlesXavier'); }); it('should open the worker summary', () => { - cy.get('.card-list-body .actions .q-btn:nth-child(2)').eq(1).click(); - cy.get('.summaryHeader div').should('have.text', '1109 - Bruce Banner'); + cy.openListSummary(0); + cy.get('.summaryHeader div').should('have.text', '1110 - Jessica Jones'); cy.get('.summary .header').eq(0).invoke('text').should('include', 'Basic data'); cy.get('.summary .header').eq(1).should('have.text', 'User data'); }); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 0b518ca26..4dfde6e21 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -51,15 +51,14 @@ Cypress.Commands.add('getValue', (selector) => { return cy.get(selector + '.q-checkbox__inner'); } // Si es un QSelect - else if ($el.find('.q-select__dropdown-icon').length) { + if ($el.find('.q-select__dropdown-icon').length) { return cy.get( selector + '> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input' ); - } else { - // Puedes añadir un log o lanzar un error si el elemento no es reconocido - cy.log('Elemento no soportado'); } + // Puedes añadir un log o lanzar un error si el elemento no es reconocido + cy.log('Elemento no soportado'); }); }); @@ -70,7 +69,7 @@ Cypress.Commands.add('selectOption', (selector, option) => { }); Cypress.Commands.add('checkOption', (selector) => { - cy.wrap(selector).find('.q-checkbox__inner').click(); + cy.get(selector).find('.q-checkbox__inner').click(); }); // Global buttons @@ -158,4 +157,16 @@ Cypress.Commands.add('removeRow', (rowIndex) => { }); }); }); +Cypress.Commands.add('openListSummary', (row) => { + cy.get('.card-list-body .actions .q-btn:nth-child(2)').eq(row).click(); +}); + +Cypress.Commands.add('openRightMenu', (element) => { + if (element) cy.waitForElement(element); + cy.get('#actions-append').click(); +}); + +Cypress.Commands.add('validateContent', (selector, expectedValue) => { + cy.get(selector).should('have.text', expectedValue); +}); // registerCommands(); diff --git a/test/vitest/__tests__/components/common/VnLog.spec.js b/test/vitest/__tests__/components/common/VnLog.spec.js index 6787b6d51..dbcb30272 100644 --- a/test/vitest/__tests__/components/common/VnLog.spec.js +++ b/test/vitest/__tests__/components/common/VnLog.spec.js @@ -1,75 +1,134 @@ import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; -import { createWrapper } from 'app/test/vitest/helper'; +import { createWrapper, axios } from 'app/test/vitest/helper'; import VnLog from 'src/components/common/VnLog.vue'; describe('VnLog', () => { let vm; - beforeAll(() => { - vm = createWrapper(VnLog, { - global: { - stubs: ['FetchData', 'VnPaginate'], - mocks: { - fetch: vi.fn(), + const fakeLogTreeData = [ + { + id: 2, + originFk: 1, + userFk: 18, + action: 'update', + changedModel: 'ClaimObservation', + oldInstance: {}, + newInstance: { + claimFk: 1, + text: 'Waiting for customer', + }, + creationDate: '2023-09-18T12:25:34.000Z', + changedModelId: '1', + changedModelValue: null, + description: null, + user: { + id: 18, + name: 'salesPerson', + nickname: 'salesPersonNick', + image: '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd', + worker: { + id: 18, + userFk: 18, }, }, + }, + { + id: 1, + originFk: 1, + userFk: 18, + action: 'update', + changedModel: 'Claim', + oldInstance: { + hasToPickUp: false, + }, + newInstance: { + hasToPickUp: true, + }, + creationDate: '2023-09-18T12:25:34.000Z', + changedModelId: '1', + changedModelValue: null, + description: null, + user: { + id: 18, + name: 'salesPerson', + nickname: 'salesPersonNick', + image: '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd', + worker: { + id: 18, + userFk: 18, + }, + }, + }, + ]; + const mockValidations = { + Claim: { + locale: { + name: 'reclamación', + }, + }, + ClaimObservation: { + locale: { + name: 'observación', + }, + }, + ClaimDms: { + locale: { + name: 'documento', + }, + }, + ClaimBeginning: { + locale: { + name: 'comienzo', + }, + }, + }; + + beforeAll(async () => { + axios.get.mockImplementation(() => { + return { data: fakeLogTreeData }; + }); + + vm = createWrapper(VnLog, { + global: { + stubs: [], + mocks: {}, + }, propsData: { - model: "Claim", + model: 'Claim', }, }).vm; + vm.validations = mockValidations; }); afterEach(() => { vi.clearAllMocks(); }); - describe('formatValue()', () => { - it('should return Yes if has a true boolean', async () => { - const result = vm.formatValue(true); - - expect(result).toEqual('Yes'); - }); - it('should return No if has a true boolean', async () => { - const result = vm.formatValue(false); - - expect(result).toEqual('No'); - }); - it('should return Nothing if has no params', async () => { - const result = vm.formatValue(); - - expect(result).toEqual('Nothing'); - }); - it('should return a string from a string value', async () => { - const result = vm.formatValue('Something'); - - expect(result).toEqual(`"Something"`); - }); - it('should call to format a date', async () => { - vi.mock('src/filters', () => ({ - toDate: ()=>{ - return "Date formatted" - }, - })); - - const result = vm.formatValue('01-01-1970'); - expect(result).toEqual("Date formatted"); - }); + it('should correctly set logTree', async () => { + vm.logTree = vm.getLogs(fakeLogTreeData); + expect(vm.logTree[0].originFk).toEqual(1); + expect(vm.logTree[0].logs[0].user.name).toEqual('salesPerson'); }); - describe('actionColor()', () => { - it('should return positive if insert', async () => { - const result = vm.actionColor('insert'); + it('should correctly set the selectedFilters when filtering', () => { + vm.searchInput = '1'; + vm.userSelect = '21'; + vm.checkboxOptions.insert.selected = true; + vm.checkboxOptions.update.selected = true; - expect(result).toEqual('positive'); - }); - it('should return positive if update', async () => { - const result = vm.actionColor('update'); + vm.selectFilter('search'); + vm.selectFilter('userSelect'); - expect(result).toEqual('positive'); - }); - it('should return negative if delete', async () => { - const result = vm.actionColor('delete'); + expect(vm.selectedFilters.changedModelId).toEqual('1'); + expect(vm.selectedFilters.userFk).toEqual('21'); + expect(vm.selectedFilters.action).toEqual({ inq: ['insert', 'update'] }); + }); - expect(result).toEqual('negative'); - }); + it('should correctly set the date from', () => { + vm.dateFrom = '18-09-2023'; + vm.selectFilter('date', 'from'); + expect(vm.selectedFilters.creationDate.between).toEqual([ + new Date('2023-09-18T00:00:00.000Z'), + new Date('2023-09-18T21:59:59.999Z'), + ]); }); }); diff --git a/test/vitest/helper.js b/test/vitest/helper.js index 7cc2bdfa5..89cc640fd 100644 --- a/test/vitest/helper.js +++ b/test/vitest/helper.js @@ -13,7 +13,6 @@ installQuasarPlugin({ Dialog, }, }); - const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false }); const mockPush = vi.fn(); @@ -35,8 +34,10 @@ vi.mock('vue-router', () => ({ }), })); +vi.mock('axios'); + vi.spyOn(useValidator, 'useValidator').mockImplementation(() => { - return { validate: vi.fn(), fetch: vi.fn() }; + return { validate: vi.fn() }; }); class FormDataMock {