Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 5835-migrateInvoiceIn
gitea/salix-front/pipeline/head This commit looks good Details

This commit is contained in:
Jorge Penadés 2023-12-11 12:36:07 +01:00
commit 61955268f4
19 changed files with 1348 additions and 199 deletions

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "23.40.01", "version": "23.36.01",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

View File

@ -9,7 +9,7 @@
"lint": "eslint --ext .js,.vue ./", "lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test:e2e": "cypress open", "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": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "vitest", "test:unit": "vitest",
"test:unit:ci": "vitest run" "test:unit:ci": "vitest run"

View File

@ -29,7 +29,7 @@ module.exports = configure(function (/* ctx */) {
// app boot file (/src/boot) // app boot file (/src/boot)
// --> boot files are part of "main.js" // --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files // 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 // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ['app.scss'], css: ['app.scss'],

6
src/boot/validations.js Normal file
View File

@ -0,0 +1,6 @@
import { boot } from 'quasar/wrappers';
import { useValidationsStore } from 'src/stores/useValidationsStore';
export default boot(async ({ store }) => {
await useValidationsStore(store).fetchModels();
});

View File

@ -46,7 +46,7 @@ async function fetch() {
if ($props.limit) filter.limit = $props.limit; if ($props.limit) filter.limit = $props.limit;
const { data } = await axios.get($props.url, { const { data } = await axios.get($props.url, {
params: { filter }, params: { filter: JSON.stringify(filter) },
}); });
emit('onFetch', data); emit('onFetch', data);

View File

@ -0,0 +1,88 @@
<script setup>
import { watch } from 'vue';
import { toDateString } from 'src/filters';
const props = defineProps({
value: { type: [String, Number, Boolean, Object], default: undefined },
});
const maxStrLen = 512;
let t = '';
let cssClass = '';
let type;
const updateValue = () => {
type = typeof props.value;
if (props.value == null) {
t = '∅';
cssClass = 'json-null';
} else {
cssClass = `json-${type}`;
switch (type) {
case 'number':
if (Number.isInteger(props.value)) {
t = props.value.toString();
} else {
t = (
Math.round((props.value + Number.EPSILON) * 1000) / 1000
).toString();
}
break;
case 'boolean':
t = props.value ? '✓' : '✗';
cssClass = `json-${props.value ? 'true' : 'false'}`;
break;
case 'string':
t =
props.value.length <= maxStrLen
? props.value
: props.value.substring(0, maxStrLen) + '...';
break;
case 'object':
if (props.value instanceof Date) {
t = toDateString(props.value);
} else {
t = props.value.toString();
}
break;
default:
t = props.value.toString();
}
}
};
watch(() => props.value, updateValue);
updateValue();
</script>
<template>
<span
:title="type === 'string' && props.value.length > maxStrLen ? props.value : ''"
:class="{ [cssClass]: t !== '' }"
>
{{ t }}
</span>
</template>
<style scoped>
.json-string {
color: #d172cc;
}
.json-object {
color: #d1a572;
}
.json-number {
color: #85d0ff;
}
.json-true {
color: #7dc489;
}
.json-false {
color: #c74949;
}
.json-null {
color: #cd7c7c;
font-style: italic;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -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
];

View File

@ -1,19 +1,12 @@
import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios';
import validator from 'validator'; import validator from 'validator';
import { useValidationsStore } from 'src/stores/useValidationsStore';
const models = ref(null);
export function useValidator() { export function useValidator() {
if (!models.value) fetch(); const models = useValidationsStore().validations;
function fetch() {
axios.get('Schemas/ModelInfo').then((response) => (models.value = response.data));
}
function validate(propertyRule) { function validate(propertyRule) {
const modelInfo = models.value; const modelInfo = models;
if (!modelInfo || !propertyRule) return; if (!modelInfo || !propertyRule) return;
const rule = propertyRule.split('.'); const rule = propertyRule.split('.');
@ -75,5 +68,6 @@ export function useValidator() {
return { return {
validate, validate,
models,
}; };
} }

View File

@ -2,6 +2,7 @@ import toLowerCase from './toLowerCase';
import toDate from './toDate'; import toDate from './toDate';
import toDateString from './toDateString'; import toDateString from './toDateString';
import toDateHour from './toDateHour'; import toDateHour from './toDateHour';
import toRelativeDate from './toRelativeDate';
import toCurrency from './toCurrency'; import toCurrency from './toCurrency';
import toPercentage from './toPercentage'; import toPercentage from './toPercentage';
import toLowerCamel from './toLowerCamel'; import toLowerCamel from './toLowerCamel';
@ -13,6 +14,7 @@ export {
toDate, toDate,
toDateString, toDateString,
toDateHour, toDateHour,
toRelativeDate,
toCurrency, toCurrency,
toPercentage, toPercentage,
dashIfEmpty, dashIfEmpty,

View File

@ -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}`;
}

View File

@ -5,6 +5,9 @@ export default {
en: 'English', en: 'English',
}, },
language: 'Language', language: 'Language',
entity: 'Entity',
user: 'User',
details: 'Details',
collapseMenu: 'Collapse left menu', collapseMenu: 'Collapse left menu',
backToDashboard: 'Return to dashboard', backToDashboard: 'Return to dashboard',
notifications: 'Notifications', notifications: 'Notifications',
@ -13,8 +16,11 @@ export default {
pinnedModules: 'Pinned modules', pinnedModules: 'Pinned modules',
darkMode: 'Dark mode', darkMode: 'Dark mode',
logOut: 'Log out', logOut: 'Log out',
date: 'Date',
dataSaved: 'Data saved', dataSaved: 'Data saved',
dataDeleted: 'Data deleted', dataDeleted: 'Data deleted',
search: 'Search',
changes: 'Changes',
add: 'Add', add: 'Add',
create: 'Create', create: 'Create',
save: 'Save', save: 'Save',
@ -36,6 +42,9 @@ export default {
summary: { summary: {
basicData: 'Basic data', basicData: 'Basic data',
}, },
today: 'Today',
yesterday: 'Yesterday',
dateFormat: 'en-GB',
microsip: 'Open in MicroSIP', microsip: 'Open in MicroSIP',
noSelectedRows: `You don't have any line selected`, noSelectedRows: `You don't have any line selected`,
}, },

View File

@ -5,6 +5,9 @@ export default {
en: 'Inglés', en: 'Inglés',
}, },
language: 'Idioma', language: 'Idioma',
entity: 'Entidad',
user: 'Usuario',
details: 'Detalles',
collapseMenu: 'Contraer menú lateral', collapseMenu: 'Contraer menú lateral',
backToDashboard: 'Volver al tablón', backToDashboard: 'Volver al tablón',
notifications: 'Notificaciones', notifications: 'Notificaciones',
@ -13,8 +16,11 @@ export default {
pinnedModules: 'Módulos fijados', pinnedModules: 'Módulos fijados',
darkMode: 'Modo oscuro', darkMode: 'Modo oscuro',
logOut: 'Cerrar sesión', logOut: 'Cerrar sesión',
date: 'Fecha',
dataSaved: 'Datos guardados', dataSaved: 'Datos guardados',
dataDeleted: 'Datos eliminados', dataDeleted: 'Datos eliminados',
search: 'Buscar',
changes: 'Cambios',
add: 'Añadir', add: 'Añadir',
create: 'Crear', create: 'Crear',
save: 'Guardar', save: 'Guardar',
@ -36,6 +42,9 @@ export default {
summary: { summary: {
basicData: 'Datos básicos', basicData: 'Datos básicos',
}, },
today: 'Hoy',
yesterday: 'Ayer',
dateFormat: 'es-ES',
noSelectedRows: `No tienes ninguna línea seleccionada`, noSelectedRows: `No tienes ninguna línea seleccionada`,
microsip: 'Abrir en MicroSIP', microsip: 'Abrir en MicroSIP',
}, },

View File

@ -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);
}
},
},
});

View File

@ -0,0 +1,25 @@
/// <reference types="cypress" />
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');
});
});

View File

@ -1,4 +1,6 @@
describe('WorkerList', () => { describe('WorkerList', () => {
const workerFieldNames =
'.card-list-body > .list-items > :nth-child(2) > .value > span';
beforeEach(() => { beforeEach(() => {
cy.viewport(1280, 720); cy.viewport(1280, 720);
cy.login('developer'); cy.login('developer');
@ -6,20 +8,14 @@ describe('WorkerList', () => {
}); });
it('should load workers', () => { it('should load workers', () => {
cy.get('.card-list-body > .list-items > :nth-child(2) > .value > span') cy.get(workerFieldNames).eq(0).should('have.text', 'JessicaJones');
.eq(0) cy.get(workerFieldNames).eq(1).should('have.text', 'BruceBanner');
.should('have.text', 'JessicaJones'); cy.get(workerFieldNames).eq(2).should('have.text', 'CharlesXavier');
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');
}); });
it('should open the worker summary', () => { it('should open the worker summary', () => {
cy.get('.card-list-body .actions .q-btn:nth-child(2)').eq(1).click(); cy.openListSummary(0);
cy.get('.summaryHeader div').should('have.text', '1109 - Bruce Banner'); 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(0).invoke('text').should('include', 'Basic data');
cy.get('.summary .header').eq(1).should('have.text', 'User data'); cy.get('.summary .header').eq(1).should('have.text', 'User data');
}); });

View File

@ -51,15 +51,14 @@ Cypress.Commands.add('getValue', (selector) => {
return cy.get(selector + '.q-checkbox__inner'); return cy.get(selector + '.q-checkbox__inner');
} }
// Si es un QSelect // Si es un QSelect
else if ($el.find('.q-select__dropdown-icon').length) { if ($el.find('.q-select__dropdown-icon').length) {
return cy.get( return cy.get(
selector + selector +
'> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input' '> .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 // Puedes añadir un log o lanzar un error si el elemento no es reconocido
cy.log('Elemento no soportado'); cy.log('Elemento no soportado');
}
}); });
}); });
@ -70,7 +69,7 @@ Cypress.Commands.add('selectOption', (selector, option) => {
}); });
Cypress.Commands.add('checkOption', (selector) => { Cypress.Commands.add('checkOption', (selector) => {
cy.wrap(selector).find('.q-checkbox__inner').click(); cy.get(selector).find('.q-checkbox__inner').click();
}); });
// Global buttons // 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(); // registerCommands();

View File

@ -1,75 +1,134 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; 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'; import VnLog from 'src/components/common/VnLog.vue';
describe('VnLog', () => { describe('VnLog', () => {
let vm; let vm;
beforeAll(() => { 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, { vm = createWrapper(VnLog, {
global: { global: {
stubs: ['FetchData', 'VnPaginate'], stubs: [],
mocks: { mocks: {},
fetch: vi.fn(),
},
}, },
propsData: { propsData: {
model: "Claim", model: 'Claim',
}, },
}).vm; }).vm;
vm.validations = mockValidations;
}); });
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
describe('formatValue()', () => { it('should correctly set logTree', async () => {
it('should return Yes if has a true boolean', async () => { vm.logTree = vm.getLogs(fakeLogTreeData);
const result = vm.formatValue(true); expect(vm.logTree[0].originFk).toEqual(1);
expect(vm.logTree[0].logs[0].user.name).toEqual('salesPerson');
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");
});
}); });
describe('actionColor()', () => { it('should correctly set the selectedFilters when filtering', () => {
it('should return positive if insert', async () => { vm.searchInput = '1';
const result = vm.actionColor('insert'); vm.userSelect = '21';
vm.checkboxOptions.insert.selected = true;
vm.checkboxOptions.update.selected = true;
expect(result).toEqual('positive'); vm.selectFilter('search');
}); vm.selectFilter('userSelect');
it('should return positive if update', async () => {
const result = vm.actionColor('update');
expect(result).toEqual('positive'); expect(vm.selectedFilters.changedModelId).toEqual('1');
expect(vm.selectedFilters.userFk).toEqual('21');
expect(vm.selectedFilters.action).toEqual({ inq: ['insert', 'update'] });
}); });
it('should return negative if delete', async () => {
const result = vm.actionColor('delete');
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'),
]);
}); });
}); });

View File

@ -13,7 +13,6 @@ installQuasarPlugin({
Dialog, Dialog,
}, },
}); });
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false }); const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false });
const mockPush = vi.fn(); const mockPush = vi.fn();
@ -35,8 +34,10 @@ vi.mock('vue-router', () => ({
}), }),
})); }));
vi.mock('axios');
vi.spyOn(useValidator, 'useValidator').mockImplementation(() => { vi.spyOn(useValidator, 'useValidator').mockImplementation(() => {
return { validate: vi.fn(), fetch: vi.fn() }; return { validate: vi.fn() };
}); });
class FormDataMock { class FormDataMock {