#6696 - mana #1052

Open
jgallego wants to merge 8 commits from 6696-mana into dev
30 changed files with 274 additions and 218 deletions
Showing only changes of commit d790907e79 - Show all commits

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "24.50.0", "version": "24.52.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
@ -64,4 +64,4 @@
"vite": "^5.1.4", "vite": "^5.1.4",
"vitest": "^0.31.1" "vitest": "^0.31.1"
} }
} }

View File

@ -40,6 +40,7 @@ const onDataSaved = (...args) => {
url-create="towns" url-create="towns"
model="city" model="city"
@on-data-saved="onDataSaved" @on-data-saved="onDataSaved"
data-cy="newCityForm"
> >
<template #form-inputs="{ data, validate }"> <template #form-inputs="{ data, validate }">
<VnRow> <VnRow>
@ -48,12 +49,14 @@ const onDataSaved = (...args) => {
v-model="data.name" v-model="data.name"
:rules="validate('city.name')" :rules="validate('city.name')"
required required
data-cy="cityName"
/> />
<VnSelectProvince <VnSelectProvince
:province-selected="$props.provinceSelected" :province-selected="$props.provinceSelected"
:country-fk="$props.countryFk" :country-fk="$props.countryFk"
v-model="data.provinceFk" v-model="data.provinceFk"
required required
data-cy="provinceCity"
/> />
</VnRow> </VnRow>
</template> </template>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
@ -21,14 +21,14 @@ const postcodeFormData = reactive({
provinceFk: null, provinceFk: null,
townFk: null, townFk: null,
}); });
const townsFetchDataRef = ref(false); const townsFetchDataRef = ref(false);
const townFilter = ref({});
const countriesRef = ref(false); const countriesRef = ref(false);
const townsRef = ref(false);
const provincesFetchDataRef = ref(false); const provincesFetchDataRef = ref(false);
const provincesOptions = ref([]); const provincesOptions = ref([]);
const townsOptions = ref([]);
const town = ref({}); const town = ref({});
const townFilter = ref({});
const countryFilter = ref({}); const countryFilter = ref({});
function onDataSaved(formData) { function onDataSaved(formData) {
@ -48,6 +48,49 @@ function onDataSaved(formData) {
emit('onDataSaved', newPostcode); emit('onDataSaved', newPostcode);
} }
async function setCountry(countryFk, data) {
data.townFk = null;
data.provinceFk = null;
data.countryFk = countryFk;
await fetchTowns();
}
// Province
async function handleProvinces(data) {
provincesOptions.value = data;
if (postcodeFormData.countryFk) {
await fetchTowns();
}
}
async function setProvince(id, data) {
if (data.provinceFk === id) return;
const newProvince = provincesOptions.value.find((province) => province.id == id);
if (newProvince) data.countryFk = newProvince.countryFk;
postcodeFormData.provinceFk = id;
await fetchTowns();
}
async function onProvinceCreated(data) {
await provincesFetchDataRef.value.fetch({
where: { countryFk: postcodeFormData.countryFk },
});
postcodeFormData.provinceFk = data.id;
}
function provinceByCountry(countryFk = postcodeFormData.countryFk) {
return provincesOptions.value
.filter((province) => province.countryFk === countryFk)
.map(({ id }) => id);
}
// Town
async function handleTowns(data) {
townsOptions.value = data;
}
function setTown(newTown, data) {
town.value = newTown;
data.provinceFk = newTown?.provinceFk ?? newTown;
data.countryFk = newTown?.province?.countryFk ?? newTown;
}
async function onCityCreated(newTown, formData) { async function onCityCreated(newTown, formData) {
await provincesFetchDataRef.value.fetch(); await provincesFetchDataRef.value.fetch();
newTown.province = provincesOptions.value.find( newTown.province = provincesOptions.value.find(
@ -56,44 +99,29 @@ async function onCityCreated(newTown, formData) {
formData.townFk = newTown; formData.townFk = newTown;
setTown(newTown, formData); setTown(newTown, formData);
} }
async function fetchTowns(countryFk = postcodeFormData.countryFk) {
function setTown(newTown, data) { if (!countryFk) return;
town.value = newTown; const provinces = postcodeFormData.provinceFk
data.provinceFk = newTown?.provinceFk ?? newTown; ? [postcodeFormData.provinceFk]
data.countryFk = newTown?.province?.countryFk ?? newTown; : provinceByCountry();
} townFilter.value.where = {
async function setCountry(countryFk, data) {
data.townFk = null;
data.provinceFk = null;
data.countryFk = countryFk;
}
async function handleProvinces(data) {
provincesOptions.value = data;
}
async function setProvince(id, data) {
const newProvince = provincesOptions.value.find((province) => province.id == id);
if (!newProvince) return;
data.countryFk = newProvince.countryFk;
}
async function onProvinceCreated(data) {
await provincesFetchDataRef.value.fetch({
where: { countryFk: postcodeFormData.countryFk },
});
postcodeFormData.provinceFk = data.id;
}
const whereTowns = computed(() => {
return {
provinceFk: { provinceFk: {
inq: provincesOptions.value.map(({ id }) => id), inq: provinces,
}, },
}; };
}); await townsFetchDataRef.value?.fetch();
}
async function filterTowns(name) {
if (name !== '') {
townFilter.value.where = {
name: {
like: `%${name}%`,
},
};
await townsFetchDataRef.value?.fetch();
}
}
</script> </script>
<template> <template>
@ -105,6 +133,15 @@ const whereTowns = computed(() => {
auto-load auto-load
url="Provinces/location" url="Provinces/location"
/> />
<FetchData
ref="townsFetchDataRef"
:sort-by="['name ASC']"
:limit="30"
:filter="townFilter"
@on-fetch="handleTowns"
auto-load
url="Towns/location"
/>
<FormModelPopup <FormModelPopup
url-create="postcodes" url-create="postcodes"
@ -123,25 +160,22 @@ const whereTowns = computed(() => {
:rules="validate('postcode.code')" :rules="validate('postcode.code')"
clearable clearable
required required
data-cy="locationPostcode"
/> />
<VnSelectDialog <VnSelectDialog
ref="townsRef"
:sort-by="['name ASC']"
:limit="30"
auto-load
url="Towns/location"
:where="whereTowns"
:label="t('City')" :label="t('City')"
@update:model-value="(value) => setTown(value, data)" @update:model-value="(value) => setTown(value, data)"
@filter="filterTowns"
:tooltip="t('Create city')" :tooltip="t('Create city')"
v-model="data.townFk" v-model="data.townFk"
:options="townsOptions"
option-label="name" option-label="name"
option-value="id" option-value="id"
:rules="validate('postcode.city')" :rules="validate('postcode.city')"
:acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]" :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
:emit-value="false" :emit-value="false"
:clearable="true"
required required
data-cy="locationTown"
> >
<template #option="{ itemProps, opt }"> <template #option="{ itemProps, opt }">
<QItem v-bind="itemProps"> <QItem v-bind="itemProps">
@ -172,7 +206,6 @@ const whereTowns = computed(() => {
:province-selected="data.provinceFk" :province-selected="data.provinceFk"
@update:model-value="(value) => setProvince(value, data)" @update:model-value="(value) => setProvince(value, data)"
v-model="data.provinceFk" v-model="data.provinceFk"
@on-province-fetched="handleProvinces"
@on-province-created="onProvinceCreated" @on-province-created="onProvinceCreated"
required required
/> />
@ -191,6 +224,7 @@ const whereTowns = computed(() => {
v-model="data.countryFk" v-model="data.countryFk"
:rules="validate('postcode.countryFk')" :rules="validate('postcode.countryFk')"
@update:model-value="(value) => setCountry(value, data)" @update:model-value="(value) => setCountry(value, data)"
data-cy="locationCountry"
/> />
</VnRow> </VnRow>
</template> </template>

View File

@ -53,8 +53,10 @@ const where = computed(() => {
v-model="data.name" v-model="data.name"
:rules="validate('province.name')" :rules="validate('province.name')"
required required
data-cy="provinceName"
/> />
<VnSelect <VnSelect
data-cy="autonomyProvince"
required required
ref="autonomiesRef" ref="autonomiesRef"
auto-load auto-load

View File

@ -64,11 +64,11 @@ watch(
auto-load auto-load
/> />
<VnSelectDialog <VnSelectDialog
data-cy="locationProvince"
:label="t('Province')" :label="t('Province')"
:options="provincesOptions" :options="provincesOptions"
:tooltip="t('Create province')" :tooltip="t('Create province')"
hide-selected hide-selected
:clearable="true"
v-model="provinceFk" v-model="provinceFk"
:rules="validate && validate('postcode.provinceFk')" :rules="validate && validate('postcode.provinceFk')"
:acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]" :acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]"

View File

@ -75,7 +75,6 @@ const handleModelValue = (data) => {
:input-debounce="300" :input-debounce="300"
:class="{ required: isRequired }" :class="{ required: isRequired }"
v-bind="$attrs" v-bind="$attrs"
clearable
:emit-value="false" :emit-value="false"
:tooltip="t('Create new location')" :tooltip="t('Create new location')"
:rules="mixinRules" :rules="mixinRules"

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

View File

@ -43,6 +43,7 @@ const isAllowedToCreate = computed(() => {
> >
<template v-if="isAllowedToCreate" #append> <template v-if="isAllowedToCreate" #append>
<QIcon <QIcon
:data-cy="$attrs.dataCy ?? $attrs.label + '_icon'"
@click.stop.prevent="$refs.dialog.show()" @click.stop.prevent="$refs.dialog.show()"
:name="actionIcon" :name="actionIcon"
:size="actionIcon === 'add' ? 'xs' : 'sm'" :size="actionIcon === 'add' ? 'xs' : 'sm'"

View File

@ -6,7 +6,7 @@ import { useColor } from 'src/composables/useColor';
import { getCssVar } from 'quasar'; import { getCssVar } from 'quasar';
const $props = defineProps({ const $props = defineProps({
workerId: { type: Number, required: true }, workerId: { type: [Number, undefined], default: null },
description: { type: String, default: null }, description: { type: String, default: null },
title: { type: String, default: null }, title: { type: String, default: null },
color: { type: String, default: null }, color: { type: String, default: null },
@ -38,7 +38,13 @@ watch(src, () => (showLetter.value = false));
<template v-if="showLetter"> <template v-if="showLetter">
{{ title.charAt(0) }} {{ title.charAt(0) }}
</template> </template>
<QImg v-else :src="src" spinner-color="white" @error="showLetter = true" /> <QImg
v-else-if="workerId"
:src="src"
spinner-color="white"
@error="showLetter = true"
/>
<QIcon v-else name="mood" size="xs" />
</QAvatar> </QAvatar>
<div class="description"> <div class="description">
<slot name="description" v-if="description"> <slot name="description" v-if="description">

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

@ -169,7 +169,6 @@ function onBeforeSave(formData, originalData) {
url="Clients" url="Clients"
:input-debounce="0" :input-debounce="0"
:label="t('customer.basicData.previousClient')" :label="t('customer.basicData.previousClient')"
:options="clients"
:rules="validate('client.transferorFk')" :rules="validate('client.transferorFk')"
emit-value emit-value
map-options map-options

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

@ -94,6 +94,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

@ -12,6 +12,7 @@ import RightMenu from 'src/components/common/RightMenu.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import CustomerFilter from './CustomerFilter.vue'; import CustomerFilter from './CustomerFilter.vue';
import VnAvatar from 'src/components/ui/VnAvatar.vue';
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();

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

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

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

View File

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

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

@ -43,11 +43,9 @@ describe('VnLocation', () => {
province province
); );
cy.get( cy.get(
`${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(3) > .q-icon` `${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(3) `
).click(); ).click();
cy.get( cy.dataCy('locationProvince').should('have.value', province);
`#q-portal--dialog--5 > .q-dialog > ${createForm.prefix} > .vn-row > .q-select > ${createForm.sufix} > :nth-child(1) input`
).should('have.value', province);
}); });
}); });
describe('Worker Create', () => { describe('Worker Create', () => {
@ -123,32 +121,16 @@ describe('VnLocation', () => {
const province = randomString({ length: 4 }); const province = randomString({ length: 4 });
cy.get(createLocationButton).click(); cy.get(createLocationButton).click();
cy.get(dialogInputs).eq(0).type(postCode); cy.get(dialogInputs).eq(0).type(postCode);
cy.get( cy.dataCy('City_icon').click();
`${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(2) > .q-icon` cy.selectOption('[data-cy="locationProvince"]:last', 'Province one');
).click(); cy.dataCy('cityName').type(province);
cy.selectOption('#q-portal--dialog--3 .q-select', 'one'); cy.dataCy('FormModelPopup_save').eq(1).click();
cy.get('#q-portal--dialog--3 .q-input').type(province); cy.dataCy('FormModelPopup_save').eq(0).click();
cy.get('#q-portal--dialog--3 .q-btn--standard').click();
cy.get('#q-portal--dialog--1 .q-btn--standard').click();
cy.waitForElement('.q-form'); cy.waitForElement('.q-form');
checkVnLocation(postCode, province); checkVnLocation(postCode, province);
}); });
it('Create province without country', () => {
const provinceName = 'Saskatchew'.concat(Math.random(1 * 100));
cy.get(createLocationButton).click();
cy.get(
`${createForm.prefix} > :nth-child(5) > .q-select > ${createForm.sufix} > :nth-child(2) `
)
.eq(0)
.click();
cy.selectOption('#q-portal--dialog--3 .q-select', 'one');
cy.countSelectOptions('#q-portal--dialog--3 .q-select', 4);
cy.get('#q-portal--dialog--3 .q-input').type(provinceName);
cy.get('#q-portal--dialog--3 .q-btn--standard').click();
});
it('Create city with country', () => { it('Create city with country', () => {
const cityName = 'Saskatchew'.concat(Math.random(1 * 100)); const cityName = 'Saskatchew'.concat(Math.random(1 * 100));
cy.get(createLocationButton).click(); cy.get(createLocationButton).click();
@ -156,14 +138,23 @@ describe('VnLocation', () => {
`${createForm.prefix} > :nth-child(5) > :nth-child(3) `, `${createForm.prefix} > :nth-child(5) > :nth-child(3) `,
'Italia' 'Italia'
); );
cy.get( cy.dataCy('City_icon').click();
`${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(2) > .q-icon` cy.selectOption('[data-cy="locationProvince"]:last', 'Province four');
).click(); cy.countSelectOptions('[data-cy="locationProvince"]:last', 1);
cy.selectOption('#q-portal--dialog--4 .q-select', 'Province four');
cy.countSelectOptions('#q-portal--dialog--4 .q-select', 1);
cy.get('#q-portal--dialog--4 .q-input').type(cityName); cy.dataCy('cityName').type(cityName);
cy.get('#q-portal--dialog--4 .q-btn--standard').click(); cy.dataCy('FormModelPopup_save').eq(1).click();
});
it('Create province without country', () => {
const provinceName = 'Saskatchew'.concat(Math.random(1 * 100));
cy.get(createLocationButton).click();
cy.dataCy('Province_icon').click();
cy.selectOption('[data-cy="autonomyProvince"] ', 'Autonomy one');
cy.countSelectOptions('[data-cy="autonomyProvince"]', 4);
cy.dataCy('provinceName').type(provinceName);
cy.dataCy('FormModelPopup_save').eq(1).click();
}); });
it('Create province with country', () => { it('Create province with country', () => {
@ -173,17 +164,13 @@ describe('VnLocation', () => {
`${createForm.prefix} > :nth-child(5) > :nth-child(3) `, `${createForm.prefix} > :nth-child(5) > :nth-child(3) `,
'España' 'España'
); );
cy.get( cy.dataCy('Province_icon').click();
`${createForm.prefix} > :nth-child(5) > .q-select > ${createForm.sufix} > :nth-child(2) `
)
.eq(0)
.click();
cy.selectOption('#q-portal--dialog--4 .q-select', 'one'); cy.selectOption('[data-cy="autonomyProvince"] ', 'Autonomy one');
cy.countSelectOptions('#q-portal--dialog--4 .q-select', 2); cy.countSelectOptions('[data-cy="autonomyProvince"]', 2);
cy.get('#q-portal--dialog--4 .q-input').type(provinceName); cy.dataCy('provinceName').type(provinceName);
cy.get('#q-portal--dialog--4 .q-btn--standard').click(); cy.dataCy('FormModelPopup_save').eq(1).click();
}); });
function checkVnLocation(postCode, province) { function checkVnLocation(postCode, province) {

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