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",
"version": "23.40.01",
"version": "23.36.01",
"lockfileVersion": 3,
"requires": true,
"packages": {

View File

@ -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"

View File

@ -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'],

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;
const { data } = await axios.get($props.url, {
params: { filter },
params: { filter: JSON.stringify(filter) },
});
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 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,
};
}

View File

@ -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,

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',
},
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`,
},

View File

@ -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',
},

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', () => {
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');
});

View File

@ -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();

View File

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

View File

@ -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 {