Merge branch 'master' into 8197-VnCardMain
gitea/salix-front/pipeline/pr-master This commit looks good Details

This commit is contained in:
Alex Moreno 2024-12-04 08:45:11 +00:00
commit 0c4d6c782e
26 changed files with 208 additions and 73 deletions

View File

@ -176,14 +176,13 @@ async function saveChanges(data) {
const changes = data || getChanges(); const changes = data || getChanges();
try { try {
await axios.post($props.saveUrl || $props.url + '/crud', changes); await axios.post($props.saveUrl || $props.url + '/crud', changes);
} catch (e) { } finally {
return (isLoading.value = false); isLoading.value = false;
} }
originalData.value = JSON.parse(JSON.stringify(formData.value)); originalData.value = JSON.parse(JSON.stringify(formData.value));
if (changes.creates?.length) await vnPaginateRef.value.fetch(); if (changes.creates?.length) await vnPaginateRef.value.fetch();
hasChanges.value = false; hasChanges.value = false;
isLoading.value = false;
emit('saveChanges', data); emit('saveChanges', data);
quasar.notify({ quasar.notify({
type: 'positive', type: 'positive',

View File

@ -8,7 +8,14 @@ import dataByOrder from 'src/utils/dataByOrder';
const emit = defineEmits(['update:modelValue', 'update:options', 'remove']); const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);
const $attrs = useAttrs(); const $attrs = useAttrs();
const { t } = useI18n(); const { t } = useI18n();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const isRequired = computed(() => {
return useRequired($attrs).isRequired;
});
const requiredFieldRule = computed(() => {
return useRequired($attrs).requiredFieldRule;
});
const $props = defineProps({ const $props = defineProps({
modelValue: { modelValue: {
type: [String, Number, Object], type: [String, Number, Object],
@ -284,9 +291,9 @@ async function onScroll({ to, direction, from, index }) {
:loading="isLoading" :loading="isLoading"
@virtual-scroll="onScroll" @virtual-scroll="onScroll"
> >
<template v-if="isClearable" #append> <template #append>
<QIcon <QIcon
v-show="value" v-show="isClearable && value"
name="close" name="close"
@click.stop=" @click.stop="
() => { () => {
@ -299,7 +306,22 @@ async function onScroll({ to, direction, from, index }) {
/> />
</template> </template>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName"> <template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" /> <div v-if="slotName == 'append'">
<QIcon
v-show="isClearable && value"
name="close"
@click.stop="
() => {
value = null;
emit('remove');
}
"
class="cursor-pointer"
size="xs"
/>
<slot name="append" v-if="$slots.append" v-bind="slotData ?? {}" />
</div>
<slot v-else :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template> </template>
</QSelect> </QSelect>
</template> </template>

View File

@ -79,7 +79,7 @@ const userParams = ref({});
defineExpose({ search, sanitizer, params: userParams }); defineExpose({ search, sanitizer, params: userParams });
onMounted(() => { onMounted(() => {
userParams.value = $props.modelValue ?? {}; if (!userParams.value) userParams.value = $props.modelValue ?? {};
emit('init', { params: userParams.value }); emit('init', { params: userParams.value });
}); });
@ -105,7 +105,8 @@ watch(
watch( watch(
() => arrayData.store.userParams, () => arrayData.store.userParams,
(val, oldValue) => (val || oldValue) && setUserParams(val) (val, oldValue) => (val || oldValue) && setUserParams(val),
{ immediate: true }
); );
watch( watch(

View File

@ -1,23 +1,28 @@
<script setup> <script setup>
import { reactive, useAttrs, onBeforeMount, capitalize } from 'vue'; import { ref, reactive, useAttrs, onBeforeMount, capitalize } from 'vue';
import axios from 'axios'; import axios from 'axios';
import { parsePhone } from 'src/filters'; import { parsePhone } from 'src/filters';
import useOpenURL from 'src/composables/useOpenURL';
const props = defineProps({ const props = defineProps({
phoneNumber: { type: [String, Number], default: null }, phoneNumber: { type: [String, Number], default: null },
channel: { type: Number, default: null }, channel: { type: Number, default: null },
country: { type: String, default: null },
}); });
const phone = ref(props.phoneNumber);
const config = reactive({ const config = reactive({
sip: { icon: 'phone', href: `sip:${props.phoneNumber}` }, sip: { icon: 'phone', href: `sip:${props.phoneNumber}` },
'say-simple': { 'say-simple': {
icon: 'vn:saysimple', icon: 'vn:saysimple',
href: null, url: null,
channel: props.channel, channel: props.channel,
}, },
}); });
const type = Object.keys(config).find((key) => key in useAttrs()) || 'sip'; const type = Object.keys(config).find((key) => key in useAttrs()) || 'sip';
onBeforeMount(async () => { onBeforeMount(async () => {
if (!phone.value) return;
let { channel } = config[type]; let { channel } = config[type];
if (type === 'say-simple') { if (type === 'say-simple') {
@ -25,23 +30,28 @@ onBeforeMount(async () => {
.data; .data;
if (!channel) channel = defaultChannel; if (!channel) channel = defaultChannel;
config[type].href = `${url}?customerIdentity=%2B${parsePhone( phone.value = await parsePhone(props.phoneNumber, props.country.toLowerCase());
props.phoneNumber config[
)}&channelId=${channel}`; type
].url = `${url}?customerIdentity=%2B${phone.value}&channelId=${channel}`;
} }
}); });
function handleClick() {
if (config[type].url) useOpenURL(config[type].url);
else if (config[type].href) window.location.href = config[type].href;
}
</script> </script>
<template> <template>
<QBtn <QBtn
v-if="phoneNumber" v-if="phone"
flat flat
round round
:icon="config[type].icon" :icon="config[type].icon"
size="sm" size="sm"
color="primary" color="primary"
padding="none" padding="none"
:href="config[type].href" @click.stop="handleClick"
@click.stop
> >
<QTooltip> <QTooltip>
{{ capitalize(type).replace('-', '') }} {{ capitalize(type).replace('-', '') }}

View File

@ -2,8 +2,14 @@ import { useValidator } from 'src/composables/useValidator';
export function useRequired($attrs) { export function useRequired($attrs) {
const { validations } = useValidator(); const { validations } = useValidator();
const hasRequired = Object.keys($attrs).includes('required');
const isRequired = Object.keys($attrs).includes('required'); let isRequired = false;
if (hasRequired) {
const required = $attrs['required'];
if (typeof required === 'boolean') {
isRequired = required;
}
}
const requiredFieldRule = (val) => validations().required(isRequired, val); const requiredFieldRule = (val) => validations().required(isRequired, val);
return { return {

View File

@ -1,12 +1,18 @@
export default function (phone, prefix = 34) { import axios from 'axios';
if (phone.startsWith('+')) {
return `${phone.slice(1)}`; export default async function parsePhone(phone, country) {
} if (!phone) return;
if (phone.startsWith('00')) { if (phone.startsWith('+')) return `${phone.slice(1)}`;
return `${phone.slice(2)}`; if (phone.startsWith('00')) return `${phone.slice(2)}`;
}
if (phone.startsWith(prefix) && phone.length === prefix.length + 9) { let prefix;
return `${prefix}${phone.slice(prefix.length)}`; try {
prefix = (await axios.get(`Prefixes/${country.toLowerCase()}`)).data?.prefix;
} catch (e) {
prefix = (await axios.get('PbxConfigs/findOne')).data?.defaultPrefix;
} }
prefix = prefix.replace(/^0+/, '');
if (phone.startsWith(prefix)) return phone;
return `${prefix}${phone}`; return `${prefix}${phone}`;
} }

View File

@ -330,6 +330,7 @@ globals:
fi: FI fi: FI
myTeam: My team myTeam: My team
departmentFk: Department departmentFk: Department
countryFk: Country
changePass: Change password changePass: Change password
deleteConfirmTitle: Delete selected elements deleteConfirmTitle: Delete selected elements
changeState: Change state changeState: Change state

View File

@ -334,6 +334,7 @@ globals:
SSN: NSS SSN: NSS
fi: NIF fi: NIF
myTeam: Mi equipo myTeam: Mi equipo
countryFk: País
changePass: Cambiar contraseña changePass: Cambiar contraseña
deleteConfirmTitle: Eliminar los elementos seleccionados deleteConfirmTitle: Eliminar los elementos seleccionados
changeState: Cambiar estado changeState: Cambiar estado

View File

@ -67,6 +67,7 @@ function handleLocation(data, location) {
option-label="vat" option-label="vat"
option-value="id" option-value="id"
v-model="data.sageTaxTypeFk" v-model="data.sageTaxTypeFk"
data-cy="sageTaxTypeFk"
:required="data.isTaxDataChecked" :required="data.isTaxDataChecked"
/> />
<VnSelect <VnSelect
@ -75,6 +76,7 @@ function handleLocation(data, location) {
hide-selected hide-selected
option-label="transaction" option-label="transaction"
option-value="id" option-value="id"
data-cy="sageTransactionTypeFk"
v-model="data.sageTransactionTypeFk" v-model="data.sageTransactionTypeFk"
:required="data.isTaxDataChecked" :required="data.isTaxDataChecked"
> >

View File

@ -95,6 +95,7 @@ const sumRisk = ({ clientRisks }) => {
:phone-number="entity.mobile" :phone-number="entity.mobile"
:channel="entity.country?.saySimpleCountry?.channel" :channel="entity.country?.saySimpleCountry?.channel"
class="q-ml-xs" class="q-ml-xs"
:country="entity.country?.code"
/> />
</template> </template>
</VnLv> </VnLv>

View File

@ -42,13 +42,13 @@ async function hasCustomerRole() {
> >
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<QCheckbox :label="t('Enable web access')" v-model="data.account.active" /> <QCheckbox :label="t('Enable web access')" v-model="data.account.active" />
<VnInput :label="t('User')" clearable v-model="data.name" /> <VnInput :label="t('User')" clearable v-model="data.account.name" />
<VnInput <VnInput
:label="t('Recovery email')" :label="t('Recovery email')"
:rules="validate('client.email')" :rules="validate('client.email')"
clearable clearable
type="email" type="email"
v-model="data.email" v-model="data.account.email"
class="q-mt-sm" class="q-mt-sm"
:info="t('This email is used for user to regain access their account')" :info="t('This email is used for user to regain access their account')"
/> />

View File

@ -18,8 +18,6 @@ const router = useRouter();
const formInitialData = reactive({ isDefaultAddress: false }); const formInitialData = reactive({ isDefaultAddress: false });
const urlCreate = ref('');
const agencyModes = ref([]); const agencyModes = ref([]);
const incoterms = ref([]); const incoterms = ref([]);
const customsAgents = ref([]); const customsAgents = ref([]);
@ -40,13 +38,18 @@ function handleLocation(data, location) {
data.countryFk = countryFk; data.countryFk = countryFk;
} }
function onAgentCreated(requestResponse, data) { function onAgentCreated({ id, fiscalName }, data) {
customsAgents.value.push(requestResponse); customsAgents.value.push({ id, fiscalName });
data.customsAgentFk = requestResponse.id; data.customsAgentFk = id;
} }
</script> </script>
<template> <template>
<FetchData
@on-fetch="(data) => (customsAgents = data)"
auto-load
url="CustomsAgents"
/>
<FetchData <FetchData
@on-fetch="(data) => (agencyModes = data)" @on-fetch="(data) => (agencyModes = data)"
auto-load auto-load
@ -57,7 +60,7 @@ function onAgentCreated(requestResponse, data) {
<FormModel <FormModel
:form-initial-data="formInitialData" :form-initial-data="formInitialData"
:observe-form-changes="false" :observe-form-changes="false"
:url-create="urlCreate" :url-create="`Clients/${route.params.id}/createAddress`"
@on-data-saved="toCustomerAddress()" @on-data-saved="toCustomerAddress()"
model="client" model="client"
> >
@ -141,8 +144,7 @@ function onAgentCreated(requestResponse, data) {
<template #form> <template #form>
<CustomerNewCustomsAgent <CustomerNewCustomsAgent
@on-data-saved=" @on-data-saved="
(_, requestResponse) => (requestResponse) => onAgentCreated(requestResponse, data)
onAgentCreated(requestResponse, data)
" "
/> />
</template> </template>

View File

@ -37,7 +37,7 @@ const columns = computed(() => [
}, },
isId: true, isId: true,
columnFilter: { columnFilter: {
name: 'search', name: 'id',
}, },
}, },
{ {
@ -74,7 +74,9 @@ const columns = computed(() => [
component: 'select', component: 'select',
attrs: { attrs: {
url: 'Clients', url: 'Clients',
fields: ['id', 'name'], fields: ['id', 'socialName'],
optionLabel: 'socialName',
optionValue: 'id',
}, },
columnField: { columnField: {
component: null, component: null,

View File

@ -10,7 +10,7 @@ invoiceOutList:
ref: Referencia ref: Referencia
issued: Fecha emisión issued: Fecha emisión
created: F. creación created: F. creación
dueDate: F. máxima dueDate: Fecha vencimiento
invoiceOutSerial: Serial invoiceOutSerial: Serial
ticket: Ticket ticket: Ticket
taxArea: Area taxArea: Area

View File

@ -138,6 +138,7 @@ const insertTag = (rows) => {
:required="false" :required="false"
:rules="validate('itemTag.tagFk')" :rules="validate('itemTag.tagFk')"
:use-like="false" :use-like="false"
sort-by="value"
/> />
<VnInput <VnInput
v-else-if=" v-else-if="

View File

@ -59,7 +59,11 @@ const getLocale = (label) => {
</template> </template>
<template #customTags="{ params, searchFn, formatFn }"> <template #customTags="{ params, searchFn, formatFn }">
<VnFilterPanelChip <VnFilterPanelChip
v-if="params.scopeDays !== null" v-if="
params.scopeDays !== undefined ||
params.scopeDays !== '' ||
params.scopeDays !== null
"
removable removable
@remove="handleScopeDays(params, null, searchFn)" @remove="handleScopeDays(params, null, searchFn)"
> >
@ -197,6 +201,18 @@ const getLocale = (label) => {
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem>
<QItemSection>
<VnSelect
outlined
dense
rounded
:label="t('globals.params.countryFk')"
v-model="params.countryFk"
url="Countries"
/>
</QItemSection>
</QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnSelect <VnSelect

View File

@ -24,13 +24,14 @@ const supplier = ref(null);
const supplierAccountRef = ref(null); const supplierAccountRef = ref(null);
const wireTransferFk = ref(null); const wireTransferFk = ref(null);
const bankEntitiesOptions = ref([]); const bankEntitiesOptions = ref([]);
const filteredBankEntitiesOptions = ref([]);
const onBankEntityCreated = async (dataSaved, rowData) => { const onBankEntityCreated = async (dataSaved, rowData) => {
await bankEntitiesRef.value.fetch(); await bankEntitiesRef.value.fetch();
rowData.bankEntityFk = dataSaved.id; rowData.bankEntityFk = dataSaved.id;
}; };
const onChangesSaved = () => { const onChangesSaved = async () => {
if (supplier.value.payMethodFk !== wireTransferFk.value) if (supplier.value.payMethodFk !== wireTransferFk.value)
quasar quasar
.dialog({ .dialog({
@ -55,12 +56,35 @@ const setWireTransfer = async () => {
await axios.patch(`Suppliers/${route.params.id}`, params); await axios.patch(`Suppliers/${route.params.id}`, params);
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
}; };
function findBankFk(value, row) {
row.bankEntityFk = null;
if (!value) return;
const bankEntityFk = bankEntitiesOptions.value.find((b) => b.id == value.slice(4, 8));
if (bankEntityFk) row.bankEntityFk = bankEntityFk.id;
}
function bankEntityFilter(val, update) {
update(() => {
const needle = val.toLowerCase();
filteredBankEntitiesOptions.value = bankEntitiesOptions.value.filter(
(bank) =>
bank.bic.toLowerCase().startsWith(needle) ||
bank.name.toLowerCase().includes(needle)
);
});
}
</script> </script>
<template> <template>
<FetchData <FetchData
ref="bankEntitiesRef" ref="bankEntitiesRef"
url="BankEntities" url="BankEntities"
@on-fetch="(data) => (bankEntitiesOptions = data)" @on-fetch="
(data) => {
(bankEntitiesOptions = data), (filteredBankEntitiesOptions = data);
}
"
auto-load auto-load
/> />
<FetchData <FetchData
@ -98,6 +122,7 @@ const setWireTransfer = async () => {
<VnInput <VnInput
:label="t('supplier.accounts.iban')" :label="t('supplier.accounts.iban')"
v-model="row.iban" v-model="row.iban"
@update:model-value="(value) => findBankFk(value, row)"
:required="true" :required="true"
> >
<template #append> <template #append>
@ -109,7 +134,9 @@ const setWireTransfer = async () => {
<VnSelectDialog <VnSelectDialog
:label="t('worker.create.bankEntity')" :label="t('worker.create.bankEntity')"
v-model="row.bankEntityFk" v-model="row.bankEntityFk"
:options="bankEntitiesOptions" :options="filteredBankEntitiesOptions"
:default-filter="false"
@filter="(val, update) => bankEntityFilter(val, update)"
option-label="bic" option-label="bic"
option-value="id" option-value="id"
hide-selected hide-selected

View File

@ -7,13 +7,13 @@ import filter from './TravelFilter.js';
<VnCard <VnCard
data-key="Travel" data-key="Travel"
base-url="Travels" base-url="Travels"
search-data-key="TravelList"
:filter="filter"
:descriptor="TravelDescriptor" :descriptor="TravelDescriptor"
:filter="filter"
search-data-key="TravelList"
:searchbar-props="{ :searchbar-props="{
url: 'Travels', url: 'Travels/filter',
searchUrl: 'table',
label: 'Search travel', label: 'Search travel',
info: 'You can search by travel id or name',
}" }"
/> />
</template> </template>

View File

@ -208,6 +208,7 @@ const columns = computed(() => [
ref="tableRef" ref="tableRef"
data-key="TravelList" data-key="TravelList"
url="Travels/filter" url="Travels/filter"
redirect="travel"
:create="{ :create="{
urlCreate: 'Travels', urlCreate: 'Travels',
title: t('Create Travels'), title: t('Create Travels'),
@ -221,9 +222,7 @@ const columns = computed(() => [
order="landed DESC" order="landed DESC"
:columns="columns" :columns="columns"
auto-load auto-load
redirect="travel"
:is-editable="false" :is-editable="false"
:use-model="true"
> >
<template #column-status="{ row }"> <template #column-status="{ row }">
<div class="row"> <div class="row">

View File

@ -75,9 +75,9 @@ export default {
}, },
{ {
name: 'TravelHistory', name: 'TravelHistory',
path: 'history', path: 'log',
meta: { meta: {
title: 'history', title: 'log',
icon: 'history', icon: 'history',
}, },
component: () => import('src/pages/Travel/Card/TravelLog.vue'), component: () => import('src/pages/Travel/Card/TravelLog.vue'),

View File

@ -106,7 +106,7 @@ export default {
}, },
{ {
name: 'ZoneHistory', name: 'ZoneHistory',
path: 'history', path: 'log',
meta: { meta: {
title: 'log', title: 'log',
icon: 'history', icon: 'history',

View File

@ -1,6 +1,7 @@
function orderData(data, order) { function orderData(data, order) {
if (typeof order === 'function') return data.sort(data); if (typeof order === 'function') return data.sort(data);
if (typeof order === 'string') order = [order]; if (typeof order === 'string') order = [order];
if (!Array.isArray(data)) return [];
if (Array.isArray(order)) { if (Array.isArray(order)) {
let orderComp = []; let orderComp = [];

View File

@ -3,11 +3,16 @@ describe('Client fiscal data', () => {
beforeEach(() => { beforeEach(() => {
cy.viewport(1280, 720); cy.viewport(1280, 720);
cy.login('developer'); cy.login('developer');
cy.visit('#/customer/1110/fiscal-data', { cy.visit('#/customer/1107/fiscal-data', {
timeout: 5000, timeout: 5000,
}); });
}); });
it('Should load layout', () => { it('Should change required value when change customer', () => {
cy.get('.q-card').should('be.visible'); cy.get('.q-card').should('be.visible');
cy.dataCy('sageTaxTypeFk').filter('input').should('not.have.attr', 'required');
cy.get('#searchbar input').clear();
cy.get('#searchbar input').type('1{enter}');
cy.get('.q-item > .q-item__label').should('have.text', ' #1');
cy.dataCy('sageTaxTypeFk').filter('input').should('have.attr', 'required');
}); });
}); });

View File

@ -3,6 +3,7 @@ describe('VnBreadcrumbs', () => {
const firstCard = '.q-infinite-scroll > :nth-child(1)'; const firstCard = '.q-infinite-scroll > :nth-child(1)';
const lastBreadcrumb = '.q-breadcrumbs--last > .q-breadcrumbs__el'; const lastBreadcrumb = '.q-breadcrumbs--last > .q-breadcrumbs__el';
beforeEach(() => { beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer'); cy.login('developer');
cy.visit('/'); cy.visit('/');
}); });

View File

@ -1,29 +1,50 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect, beforeAll, vi } from 'vitest';
import { axios } from 'app/test/vitest/helper';
import parsePhone from 'src/filters/parsePhone'; import parsePhone from 'src/filters/parsePhone';
describe('parsePhone filter', () => { describe('parsePhone filter', () => {
it("adds prefix +34 if it doesn't have one", () => { beforeAll(async () => {
const resultado = parsePhone('123456789', '34'); vi.spyOn(axios, 'get').mockReturnValue({ data: { prefix: '34' } });
expect(resultado).toBe('34123456789');
}); });
it('maintains prefix +34 if it is already correct', () => { it('no phone', async () => {
const resultado = parsePhone('+34123456789', '34'); const phone = await parsePhone(null, '34');
expect(resultado).toBe('34123456789'); expect(phone).toBe(undefined);
}); });
it('converts prefix 0034 to +34', () => { it("adds prefix +34 if it doesn't have one", async () => {
const resultado = parsePhone('0034123456789', '34'); const phone = await parsePhone('123456789', '34');
expect(resultado).toBe('34123456789'); expect(phone).toBe('34123456789');
}); });
it('converts prefix 34 without symbol to +34', () => { it('maintains prefix +34 if it is already correct', async () => {
const resultado = parsePhone('34123456789', '34'); const phone = await parsePhone('+34123456789', '34');
expect(resultado).toBe('34123456789'); expect(phone).toBe('34123456789');
}); });
it('replaces incorrect prefix with the correct one', () => { it('converts prefix 0034 to +34', async () => {
const resultado = parsePhone('+44123456789', '34'); const phone = await parsePhone('0034123456789', '34');
expect(resultado).toBe('44123456789'); expect(phone).toBe('34123456789');
});
it('converts prefix 34 without symbol to +34', async () => {
const phone = await parsePhone('34123456789', '34');
expect(phone).toBe('34123456789');
});
it('replaces incorrect prefix with the correct one', async () => {
const phone = await parsePhone('+44123456789', '34');
expect(phone).toBe('44123456789');
});
it('adds default prefix on error', async () => {
vi.spyOn(axios, 'get').mockImplementation((url) => {
if (url.includes('Prefixes'))
return Promise.reject(new Error('Network error'));
else if (url.includes('PbxConfigs'))
return Promise.resolve({ data: { defaultPrefix: '39' } });
});
const phone = await parsePhone('123456789', '34');
expect(phone).toBe('39123456789');
}); });
}); });

View File

@ -44,7 +44,18 @@ vi.mock('vue-router', () => ({
vi.mock('axios'); vi.mock('axios');
vi.spyOn(useValidator, 'useValidator').mockImplementation(() => { vi.spyOn(useValidator, 'useValidator').mockImplementation(() => {
return { validate: vi.fn() }; return {
validate: vi.fn(),
validations: () => ({
format: vi.fn(),
presence: vi.fn(),
required: vi.fn(),
length: vi.fn(),
numericality: vi.fn(),
min: vi.fn(),
custom: vi.fn(),
}),
};
}); });
class FormDataMock { class FormDataMock {