Merge branch 'dev' into performance_mixins
gitea/salix-front/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Jorge Penadés 2024-12-04 12:22:09 +00:00
commit e0090c29c9
20 changed files with 148 additions and 124 deletions

View File

@ -1,6 +1,6 @@
{
"name": "salix-front",
"version": "24.50.0",
"version": "24.52.0",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",

View File

@ -8,7 +8,14 @@ import dataByOrder from 'src/utils/dataByOrder';
const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);
const $attrs = useAttrs();
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({
modelValue: {
type: [String, Number, Object],

View File

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

View File

@ -2,8 +2,14 @@ import { useValidator } from 'src/composables/useValidator';
export function useRequired($attrs) {
const { validations } = useValidator();
const isRequired = Object.keys($attrs).includes('required');
const hasRequired = 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);
return {

View File

@ -1,12 +1,18 @@
export default function (phone, prefix = 34) {
if (phone.startsWith('+')) {
return `${phone.slice(1)}`;
}
if (phone.startsWith('00')) {
return `${phone.slice(2)}`;
}
if (phone.startsWith(prefix) && phone.length === prefix.length + 9) {
return `${prefix}${phone.slice(prefix.length)}`;
import axios from 'axios';
export default async function parsePhone(phone, country) {
if (!phone) return;
if (phone.startsWith('+')) return `${phone.slice(1)}`;
if (phone.startsWith('00')) return `${phone.slice(2)}`;
let prefix;
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}`;
}

View File

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

View File

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

View File

@ -42,13 +42,13 @@ async function hasCustomerRole() {
>
<template #form="{ data, validate }">
<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
:label="t('Recovery email')"
:rules="validate('client.email')"
clearable
type="email"
v-model="data.email"
v-model="data.account.email"
class="q-mt-sm"
: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 urlCreate = ref('');
const agencyModes = ref([]);
const incoterms = ref([]);
const customsAgents = ref([]);
@ -40,13 +38,18 @@ function handleLocation(data, location) {
data.countryFk = countryFk;
}
function onAgentCreated(requestResponse, data) {
customsAgents.value.push(requestResponse);
data.customsAgentFk = requestResponse.id;
function onAgentCreated({ id, fiscalName }, data) {
customsAgents.value.push({ id, fiscalName });
data.customsAgentFk = id;
}
</script>
<template>
<FetchData
@on-fetch="(data) => (customsAgents = data)"
auto-load
url="CustomsAgents"
/>
<FetchData
@on-fetch="(data) => (agencyModes = data)"
auto-load
@ -57,7 +60,7 @@ function onAgentCreated(requestResponse, data) {
<FormModel
:form-initial-data="formInitialData"
:observe-form-changes="false"
:url-create="urlCreate"
:url-create="`Clients/${route.params.id}/createAddress`"
@on-data-saved="toCustomerAddress()"
model="client"
>
@ -141,8 +144,7 @@ function onAgentCreated(requestResponse, data) {
<template #form>
<CustomerNewCustomsAgent
@on-data-saved="
(_, requestResponse) =>
onAgentCreated(requestResponse, data)
(requestResponse) => onAgentCreated(requestResponse, data)
"
/>
</template>

View File

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

View File

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

View File

@ -1,9 +1,8 @@
<script setup>
import { useRouter } from 'vue-router';
import { reactive, onMounted, ref } from 'vue';
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useState } from 'composables/useState';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'components/common/VnSelect.vue';
@ -11,29 +10,12 @@ import VnInputDate from 'components/common/VnInputDate.vue';
import { useDialogPluginComponent } from 'quasar';
const { t } = useI18n();
const state = useState();
const ORDER_MODEL = 'order';
const router = useRouter();
const agencyList = ref([]);
const addressList = ref([]);
defineEmits(['confirm', ...useDialogPluginComponent.emits]);
const fetchAddressList = async (addressId) => {
const { data } = await axios.get('addresses', {
params: {
filter: JSON.stringify({
fields: ['id', 'nickname', 'street', 'city'],
where: { id: addressId },
}),
},
});
addressList.value = data;
if (addressList.value?.length === 1) {
state.get(ORDER_MODEL).addressId = addressList.value[0].id;
}
};
const fetchAgencyList = async (landed, addressFk) => {
if (!landed || !addressFk) {
return;
@ -59,17 +41,9 @@ const initialFormState = reactive({
clientFk: $props.clientFk,
});
const onClientChange = async (clientId = $props.clientFk) => {
const { data } = await axios.get(`Clients/${clientId}`);
await fetchAddressList(data.defaultAddressFk);
};
async function onDataSaved(_, id) {
await router.push({ path: `/order/${id}/catalog` });
}
onMounted(async () => {
await onClientChange();
});
</script>
<template>
@ -90,10 +64,9 @@ onMounted(async () => {
option-value="id"
option-label="name"
:filter="{
fields: ['id', 'name', 'defaultAddressFk'],
fields: ['id', 'name'],
}"
hide-selected
@update:model-value="onClientChange"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
@ -110,7 +83,7 @@ onMounted(async () => {
:label="t('order.form.addressFk')"
v-model="data.addressId"
url="addresses"
:fields="['id', 'nickname', 'defaultAddressFk', 'street', 'city']"
:fields="['id', 'nickname', 'street', 'city']"
sort-by="id"
option-value="id"
option-label="street"

View File

@ -1,7 +1,7 @@
<script setup>
import axios from 'axios';
import { useI18n } from 'vue-i18n';
import { computed, onMounted, ref } from 'vue';
import { computed, ref, onMounted } from 'vue';
import { dashIfEmpty, toCurrency, toDate } from 'src/filters';
import OrderSummary from 'pages/Order/Card/OrderSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
@ -15,14 +15,13 @@ import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vu
import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue';
import { toDateTimeFormat } from 'src/filters/date';
import { useRoute } from 'vue-router';
import dataByOrder from 'src/utils/dataByOrder';
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const tableRef = ref();
const agencyList = ref([]);
const addressesList = ref([]);
const route = useRoute();
const addressOptions = ref([]);
const columns = computed(() => [
{
align: 'left',
@ -148,16 +147,12 @@ onMounted(() => {
const id = JSON.parse(clientId);
fetchClientAddress(id.clientFk);
});
async function fetchClientAddress(id, formData = {}) {
const { data } = await axios.get(`Clients/${id}`, {
params: {
filter: {
order: ['isDefaultAddress DESC', 'isActive DESC', 'nickname ASC'],
include: { relation: 'addresses' },
},
},
});
addressesList.value = data.addresses;
const { data } = await axios.get(
`Clients/${id}/addresses?filter[order]=isActive DESC`
);
addressOptions.value = data;
formData.addressId = data.defaultAddressFk;
fetchAgencies(formData);
}
@ -168,7 +163,7 @@ async function fetchAgencies({ landed, addressId }) {
const { data } = await axios.get('Agencies/landsThatDay', {
params: { addressFk: addressId, landed },
});
agencyList.value = dataByOrder(data, 'agencyMode ASC');
agencyList.value = data;
}
const getDateColor = (date) => {
@ -252,34 +247,29 @@ const getDateColor = (date) => {
</VnSelect>
<VnSelect
v-model="data.addressId"
:options="addressesList"
:options="addressOptions"
:label="t('module.address')"
option-value="id"
option-label="nickname"
@update:model-value="() => fetchAgencies(data)"
>
<template #option="scope">
<QItem
v-bind="scope.itemProps"
:class="{ disabled: !scope.opt.isActive }"
>
<QItemSection style="min-width: min-content" avatar>
<QIcon
v-if="
scope.opt.isActive && data.addressId === scope.opt.id
"
size="sm"
color="grey"
name="star"
class="fill-icon"
/>
</QItemSection>
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt.nickname }}
</QItemLabel>
<QItemLabel caption>
{{ `${scope.opt.street}, ${scope.opt.city}` }}
<QItemLabel
:class="{
'color-vn-label': !scope.opt?.isActive,
}"
>
{{
`${
!scope.opt?.isActive
? t('basicData.inactive')
: ''
} `
}}
{{ scope.opt?.nickname }}: {{ scope.opt?.street }},
{{ scope.opt?.city }}
</QItemLabel>
</QItemSection>
</QItem>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,11 +3,16 @@ describe('Client fiscal data', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1110/fiscal-data', {
cy.visit('#/customer/1107/fiscal-data', {
timeout: 5000,
});
});
it('Should load layout', () => {
it('Should change required value when change customer', () => {
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

@ -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';
describe('parsePhone filter', () => {
it("adds prefix +34 if it doesn't have one", () => {
const resultado = parsePhone('123456789', '34');
expect(resultado).toBe('34123456789');
beforeAll(async () => {
vi.spyOn(axios, 'get').mockReturnValue({ data: { prefix: '34' } });
});
it('maintains prefix +34 if it is already correct', () => {
const resultado = parsePhone('+34123456789', '34');
expect(resultado).toBe('34123456789');
it('no phone', async () => {
const phone = await parsePhone(null, '34');
expect(phone).toBe(undefined);
});
it('converts prefix 0034 to +34', () => {
const resultado = parsePhone('0034123456789', '34');
expect(resultado).toBe('34123456789');
it("adds prefix +34 if it doesn't have one", async () => {
const phone = await parsePhone('123456789', '34');
expect(phone).toBe('34123456789');
});
it('converts prefix 34 without symbol to +34', () => {
const resultado = parsePhone('34123456789', '34');
expect(resultado).toBe('34123456789');
it('maintains prefix +34 if it is already correct', async () => {
const phone = await parsePhone('+34123456789', '34');
expect(phone).toBe('34123456789');
});
it('replaces incorrect prefix with the correct one', () => {
const resultado = parsePhone('+44123456789', '34');
expect(resultado).toBe('44123456789');
it('converts prefix 0034 to +34', async () => {
const phone = await parsePhone('0034123456789', '34');
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');
});
});