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

This commit is contained in:
Javier Segarra 2024-11-25 23:03:10 +01:00
commit 08f673f12c
24 changed files with 198 additions and 205 deletions

View File

@ -12,7 +12,7 @@ module.exports = defineConfig({
supportFile: 'test/cypress/support/index.js',
videosFolder: 'test/cypress/videos',
video: false,
specPattern: 'test/cypress/integration/vnComponent/vnLocation.spec.js',
specPattern: 'test/cypress/integration/**/*.spec.js',
experimentalRunAllSpecs: true,
watchForFileChanges: true,
reporter: 'cypress-mochawesome-reporter',

View File

@ -10,6 +10,7 @@ import VnPaginate from 'components/ui/VnPaginate.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import SkeletonTable from 'components/ui/SkeletonTable.vue';
import { tMobile } from 'src/composables/tMobile';
import getDifferences from 'src/filters/getDifferences';
const { push } = useRouter();
const quasar = useQuasar();
@ -267,28 +268,6 @@ function getChanges() {
return changes;
}
function getDifferences(obj1, obj2) {
let diff = {};
delete obj1.$index;
delete obj2.$index;
for (let key in obj1) {
if (obj2[key] && JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
diff[key] = obj2[key];
}
}
for (let key in obj2) {
if (
obj1[key] === undefined ||
JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])
) {
diff[key] = obj2[key];
}
}
return diff;
}
function isEmpty(obj) {
if (obj == null) return true;
if (obj === undefined) return true;

View File

@ -106,6 +106,7 @@ const originalData = ref({});
const formData = computed(() => state.get(modelValue));
const defaultButtons = computed(() => ({
save: {
dataCy: 'saveDefaultBtn',
color: 'primary',
icon: 'save',
label: 'globals.save',
@ -113,6 +114,7 @@ const defaultButtons = computed(() => ({
type: 'submit',
},
reset: {
dataCy: 'resetDefaultBtn',
color: 'primary',
icon: 'restart_alt',
label: 'globals.reset',
@ -203,7 +205,9 @@ async function save() {
isLoading.value = true;
try {
formData.value = trimData(formData.value);
const body = $props.mapper ? $props.mapper(formData.value) : formData.value;
const body = $props.mapper
? $props.mapper(formData.value, originalData.value)
: formData.value;
const method = $props.urlCreate ? 'post' : 'patch';
const url =
$props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url;
@ -317,6 +321,7 @@ defineExpose({
:title="t(defaultButtons.reset.label)"
/>
<QBtnDropdown
data-cy="saveAndContinueDefaultBtn"
v-if="$props.goTo"
@click="saveAndGo"
:label="tMobile('globals.saveAndContinue')"

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
const emit = defineEmits(['onSubmit']);
defineProps({
const $props = defineProps({
title: {
type: String,
default: '',
@ -25,16 +25,21 @@ defineProps({
type: String,
default: '',
},
submitOnEnter: {
type: Boolean,
default: true,
},
});
const { t } = useI18n();
const closeButton = ref(null);
const isLoading = ref(false);
const onSubmit = () => {
emit('onSubmit');
closeForm();
if ($props.submitOnEnter) {
emit('onSubmit');
closeForm();
}
};
const closeForm = () => {

View File

@ -101,6 +101,7 @@ onBeforeRouteLeave((to, from, next) => {
@click="insert"
class="q-mb-xs"
dense
data-cy="saveNote"
/>
</template>
</VnInput>

View File

@ -132,10 +132,24 @@ const addFilter = async (filter, params) => {
async function fetch(params) {
useArrayData(props.dataKey, params);
arrayData.reset(['filter.skip', 'skip']);
arrayData.reset(['filter.skip', 'skip', 'page']);
await arrayData.fetch({ append: false });
if (!store.hasMoreData) isLoading.value = false;
return emitStoreData();
}
async function update(params) {
useArrayData(props.dataKey, params);
const { limit, skip } = store;
store.limit = limit + skip;
store.skip = 0;
await arrayData.fetch({ append: false });
store.limit = limit;
store.skip = skip;
return emitStoreData();
}
function emitStoreData() {
if (!store.hasMoreData) isLoading.value = false;
emit('onFetch', store.data);
return store.data;
}
@ -181,7 +195,7 @@ async function onLoad(index, done) {
done(isDone);
}
defineExpose({ fetch, addFilter, paginate });
defineExpose({ fetch, update, addFilter, paginate });
</script>
<template>

View File

@ -0,0 +1,21 @@
export default function getDifferences(obj1, obj2) {
let diff = {};
delete obj1.$index;
delete obj2.$index;
for (let key in obj1) {
if (obj2[key] && JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
diff[key] = obj2[key];
}
}
for (let key in obj2) {
if (
obj1[key] === undefined ||
JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])
) {
diff[key] = obj2[key];
}
}
return diff;
}

View File

@ -0,0 +1,6 @@
export default function getUpdatedValues(keys, formData) {
return keys.reduce((acc, key) => {
acc[key] = formData[key];
return acc;
}, {});
}

View File

@ -11,11 +11,15 @@ import dashIfEmpty from './dashIfEmpty';
import dateRange from './dateRange';
import toHour from './toHour';
import dashOrCurrency from './dashOrCurrency';
import getDifferences from './getDifferences';
import getUpdatedValues from './getUpdatedValues';
import getParamWhere from './getParamWhere';
import isDialogOpened from './isDialogOpened';
export {
isDialogOpened,
getUpdatedValues,
getDifferences,
toLowerCase,
toLowerCamel,
toDate,

View File

@ -2,6 +2,7 @@
import { onBeforeMount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import axios from 'axios';
@ -52,7 +53,6 @@ const addressFilter = {
onBeforeMount(() => {
const { id } = route.params;
getAddressesData(id);
getClientData(id);
});
@ -60,23 +60,10 @@ watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
getAddressesData(newValue);
getClientData(newValue);
}
);
const getAddressesData = async (id) => {
try {
const { data } = await axios.get(`Clients/${id}/addresses`, {
params: { filter: JSON.stringify(addressFilter) },
});
addresses.value = data;
sortAddresses();
} catch (error) {
return error;
}
};
const getClientData = async (id) => {
try {
const { data } = await axios.get(`Clients/${id}`);
@ -101,9 +88,9 @@ const setDefault = (address) => {
});
};
const sortAddresses = () => {
if (!client.value || !addresses.value) return;
addresses.value = addresses.value.sort((a, b) => {
const sortAddresses = (data) => {
if (!client.value || !data) return;
addresses.value = data.sort((a, b) => {
return isDefaultAddress(b) - isDefaultAddress(a);
});
};
@ -124,8 +111,17 @@ const toCustomerAddressEdit = (addressId) => {
</script>
<template>
<FetchData
@on-fetch="sortAddresses"
auto-load
data-key="CustomerAddresses"
order="id DESC"
ref="vnPaginateRef"
:filter="addressFilter"
:url="`Clients/${route.params.id}/addresses`"
/>
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg" v-if="addresses.length">
<QCard class="card-width q-pa-lg">
<QCardSection>
<div
v-for="(item, index) in addresses"

View File

@ -9,6 +9,7 @@ import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnAvatar from 'src/components/ui/VnAvatar.vue';
import { getDifferences, getUpdatedValues } from 'src/filters';
const route = useRoute();
const { t } = useI18n();
@ -30,6 +31,13 @@ const exprBuilder = (param, value) => {
and: [{ active: { neq: false } }, handleSalesModelValue(value)],
};
};
function onBeforeSave(formData, originalData) {
return getUpdatedValues(
Object.keys(getDifferences(formData, originalData)),
formData
);
}
</script>
<template>
<FetchData
@ -43,7 +51,12 @@ const exprBuilder = (param, value) => {
@on-fetch="(data) => (businessTypes = data)"
auto-load
/>
<FormModel :url="`Clients/${route.params.id}`" auto-load model="customer">
<FormModel
:url="`Clients/${route.params.id}`"
auto-load
model="customer"
:mapper="onBeforeSave"
>
<template #form="{ data, validate }">
<VnRow>
<VnInput
@ -94,6 +107,7 @@ const exprBuilder = (param, value) => {
:rules="validate('client.phone')"
clearable
v-model="data.phone"
data-cy="customerPhone"
/>
<VnInput
:label="t('customer.summary.mobile')"

View File

@ -28,12 +28,7 @@ const getBankEntities = (data, formData) => {
</script>
<template>
<FormModel
:url-update="`Clients/${route.params.id}`"
:url="`Clients/${route.params.id}/getCard`"
auto-load
model="customer"
>
<FormModel :url-update="`Clients/${route.params.id}`" auto-load model="customer">
<template #form="{ data, validate }">
<VnRow>
<VnSelect

View File

@ -93,22 +93,6 @@ const columns = computed(() => [
<WorkerDescriptorProxy :id="row.worker.id" />
</template>
</VnTable>
<!-- <QTable
:columns="columns"
:pagination="{ rowsPerPage: 0 }"
:rows="rows"
hide-bottom
row-key="id"
v-model:selected="selected"
class="card-width q-px-lg"
>
<template #body-cell-employee="{ row }">
<QTd @click.stop>
<span class="link">{{ row.worker.user.nickname }}</span>
<WorkerDescriptorProxy :id="row.clientFk" />
</QTd>
</template>
</QTable> -->
</template>
<i18n>

View File

@ -36,7 +36,10 @@ const entityId = computed(() => {
});
const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity?.name, entity?.id));
const setData = (entity) => {
data.value = useCardDescription(entity?.name, entity?.id);
if (customer.value) customer.value.webAccess = data.value?.account?.isActive;
};
const debtWarning = computed(() => {
return customer.value?.debt > customer.value?.credit ? 'negative' : 'primary';
});

View File

@ -34,7 +34,6 @@ function handleLocation(data, location) {
/>
<FormModel
:url-update="`Clients/${route.params.id}/updateFiscalData`"
:url="`Clients/${route.params.id}/getCard`"
auto-load
model="customer"
>

View File

@ -25,10 +25,9 @@ async function hasCustomerRole() {
</script>
<template>
<FormModel
url="VnUsers/preview"
:url-update="`Clients/${route.params.id}/updateUser`"
:filter="filter"
model="webAccess"
model="customer"
:mapper="
({ active, name, email }) => {
return {
@ -42,7 +41,7 @@ async function hasCustomerRole() {
auto-load
>
<template #form="{ data, validate }">
<QCheckbox :label="t('Enable web access')" v-model="data.active" />
<QCheckbox :label="t('Enable web access')" v-model="data.account.active" />
<VnInput :label="t('User')" clearable v-model="data.name" />
<VnInput
:label="t('Recovery email')"

View File

@ -1,9 +1,9 @@
<script setup>
import { computed, onBeforeMount, ref, watch } from 'vue';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import axios from 'axios';
import FetchData from 'src/components/FetchData.vue';
import { toCurrency, toDateHourMin } from 'src/filters';
@ -20,10 +20,11 @@ const filter = {
{ relation: 'mandateType', scope: { fields: ['id', 'name'] } },
{ relation: 'company', scope: { fields: ['id', 'code'] } },
],
where: { clientFk: null },
where: { clientFk: route.params.id },
order: ['created DESC'],
limit: 20,
};
const ClientDmsRef = ref(false);
const tableColumnComponents = {
state: {
@ -50,7 +51,7 @@ const tableColumnComponents = {
component: CustomerCheckIconTooltip,
props: ({ row }) => ({
transaction: row,
promise: refreshData,
promise: () => ClientDmsRef.value.fetch(),
}),
event: () => {},
},
@ -89,72 +90,45 @@ const columns = computed(() => [
name: 'validate',
},
]);
onBeforeMount(() => {
getData(route.params.id);
});
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
getData(newValue);
}
);
const getData = async (id) => {
filter.where.clientFk = id;
try {
const { data } = await axios.get('clients/transactions', {
params: { filter: JSON.stringify(filter) },
});
rows.value = data;
} catch (error) {
return error;
}
};
const refreshData = () => {
getData(route.params.id);
};
</script>
<template>
<div class="full-width flex justify-center">
<QPage class="card-width q-pa-lg">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
v-if="rows?.length"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props">
<component
:is="tableColumnComponents[props.col.name].component"
@click="
tableColumnComponents[props.col.name].event(props)
"
class="rounded-borders q-pa-sm"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
>
{{ props.value }}
</component>
</QTr>
</QTd>
</template>
</QTable>
<FetchData
ref="ClientDmsRef"
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="Clients/transactions"
/>
<QPage class="card-width q-pa-lg">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
v-if="rows?.length"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props">
<component
:is="tableColumnComponents[props.col.name].component"
@click="tableColumnComponents[props.col.name].event(props)"
class="rounded-borders q-pa-sm"
v-bind="tableColumnComponents[props.col.name].props(props)"
>
{{ props.value }}
</component>
</QTr>
</QTd>
</template>
</QTable>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</QPage>
</div>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</QPage>
</template>
<i18n>

View File

@ -1,9 +1,8 @@
<script setup>
import { onBeforeMount, reactive, ref } from 'vue';
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import VnLocation from 'src/components/common/VnLocation.vue';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
@ -25,20 +24,6 @@ const agencyModes = ref([]);
const incoterms = ref([]);
const customsAgents = ref([]);
onBeforeMount(() => {
urlCreate.value = `Clients/${route.params.id}/createAddress`;
getCustomsAgents();
});
const getCustomsAgents = async () => {
const { data } = await axios.get('CustomsAgents');
customsAgents.value = data;
};
const refreshData = () => {
getCustomsAgents();
};
const toCustomerAddress = () => {
router.push({
name: 'CustomerAddress',
@ -54,6 +39,11 @@ function handleLocation(data, location) {
data.provinceFk = provinceFk;
data.countryFk = countryFk;
}
function onAgentCreated(requestResponse, data) {
customsAgents.value.push(requestResponse);
data.customsAgentFk = requestResponse.id;
}
</script>
<template>
@ -139,6 +129,7 @@ function handleLocation(data, location) {
/>
<VnSelectDialog
url="CustomsAgents"
:label="t('Customs agent')"
:options="customsAgents"
hide-selected
@ -148,7 +139,12 @@ function handleLocation(data, location) {
:tooltip="t('Create a new expense')"
>
<template #form>
<CustomerNewCustomsAgent @on-data-saved="refreshData()" />
<CustomerNewCustomsAgent
@on-data-saved="
(_, requestResponse) =>
onAgentCreated(requestResponse, data)
"
/>
</template>
</VnSelectDialog>
</VnRow>

View File

@ -144,7 +144,7 @@ function handleLocation(data, location) {
:url="`Addresses/${route.params.addressId}`"
@on-data-saved="onDataSaved()"
auto-load
model="client"
model="customer"
>
<template #moreActions>
<QBtn

View File

@ -350,7 +350,7 @@ const openTab = (id) =>
class="q-mr-sm"
dense
flat
@click="$refs.tableRef.reload()"
@click="tableRef.CrudModelRef.vnPaginateRef.update()"
>
<QTooltip>{{ $t('globals.refresh') }}</QTooltip>
</QBtn>

View File

@ -43,7 +43,7 @@ const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
const isNew = computed(() => props.isNewMode);
const dated = ref(null);
const dated = ref(props.date);
const tickedNodes = ref();
const _excludeType = ref('all');
@ -73,7 +73,6 @@ const exclusionCreate = async () => {
await axios.post(`Zones/${route.params.id}/exclusions`, {
dated: dated.value,
});
await refetchEvents();
};
@ -115,13 +114,14 @@ onMounted(() => {
@on-submit="onSubmit()"
:default-cancel-button="false"
:default-submit-button="false"
:submit-on-enter="false"
>
<template #form-inputs>
<VnRow class="row q-gutter-md q-mb-lg">
<VnInputDate
:label="t('eventsInclusionForm.day')"
v-model="dated"
:model-value="props.date"
:required="true"
/>
</VnRow>
<div class="column q-gutter-y-sm q-mb-md">
@ -182,6 +182,7 @@ onMounted(() => {
:label="isNew ? t('globals.add') : t('globals.save')"
type="submit"
color="primary"
@click="onSubmit()"
/>
</template>
</FormPopup>

View File

@ -38,6 +38,7 @@ const datakey = 'ZoneLocations';
const url = computed(() => `Zones/${route.params.id}/getLeaves`);
const arrayData = useArrayData(datakey, {
url: url.value,
limit: null,
});
const store = arrayData.store;
@ -74,6 +75,7 @@ const onNodeExpanded = async (nodeKeysArray) => {
if (response.data) {
node.childs = response.data.map((n) => {
if (n.sons > 0) n.childs = [{}];
n.selected = isSelected(n.selected);
return n;
});
}
@ -90,21 +92,16 @@ const onNodeExpanded = async (nodeKeysArray) => {
previousExpandedNodes.value = nodeKeysSet;
};
const formatNodeSelected = (node) => {
if (node.selected === 1) node.selected = true;
else if (node.selected === 0) node.selected = false;
if (node.sons > 0 && !node.childs) node.childs = [{}];
};
const fetchNodeLeaves = async (nodeKey) => {
if (!treeRef.value) return;
const node = treeRef.value?.getNodeByKey(nodeKey);
if (node.selected === 1) node.selected = true;
else if (node.selected === 0) node.selected = false;
if (typeof node.selected === 'number') node.selected = !!node.selected;
if (node.sons > 0 && !node.childs) {
node.childs = [{}];
const index = expanded.value.indexOf(node.id);
expanded.value.splice(index, 1);
}
if (!node || node.sons === 0) return;
state.set('Tree', node);
};
function getNodeIds(node) {
@ -119,6 +116,10 @@ function getNodeIds(node) {
return ids;
}
function isSelected(selected) {
if (typeof selected === 'number') return !!selected;
}
watch(
() => store.data,
async (val) => {
@ -128,18 +129,9 @@ watch(
nodes.value[0].childs = [...val];
const fetchedNodeKeys = val.flatMap(getNodeIds);
state.set('Tree', [...fetchedNodeKeys]);
if (!store.userParams?.search) {
val.forEach((n) => {
formatNodeSelected(n);
});
store.data = null;
expanded.value = [null];
} else {
for (let n of state.get('Tree')) {
await fetchNodeLeaves(n);
}
expanded.value = [null, ...fetchedNodeKeys];
expanded.value = [null, ...fetchedNodeKeys];
for (let n of state.get('Tree')) {
await fetchNodeLeaves(n);
}
previousExpandedNodes.value = new Set(expanded.value);
},
@ -147,13 +139,11 @@ watch(
);
const reFetch = async () => {
const { data } = await arrayData.fetch({ append: false });
nodes.value = data;
expanded.value = [null];
await arrayData.fetch({});
};
onMounted(async () => {
if (store.userParams?.search) await arrayData.fetch({});
await reFetch();
});
onUnmounted(() => {
@ -167,13 +157,13 @@ onUnmounted(() => {
v-if="showSearchBar"
v-model="store.userParams.search"
:placeholder="$t('globals.search')"
@update:model-value="reFetch()"
@keydown.enter.stop.prevent="reFetch"
>
<template #prepend>
<QBtn color="primary" icon="search" dense flat @click="reFetch()" />
</template>
</VnInput>
<VnSearchbar :data-key="datakey" :url="url" :redirect="false" />
<VnSearchbar v-if="!showSearchBar" :data-key="datakey" :url="url" :redirect="false" />
<QTree
ref="treeRef"
:nodes="nodes"

View File

@ -3,11 +3,18 @@ describe('Client basic data', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1110/basic-data', {
timeout: 5000,
});
cy.visit('#/customer/1102/basic-data');
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
cy.dataCy('customerPhone').filter('input').should('be.visible');
cy.dataCy('customerPhone').filter('input').type('123456789');
cy.get('.q-btn-group > .q-btn--standard').click();
cy.intercept('PATCH', '/api/Clients/1102', (req) => {
const { body } = req;
cy.wrap(body).should('have.property', 'phone', '123456789');
});
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
});

View File

@ -300,22 +300,22 @@ Cypress.Commands.add('checkNotification', (text) => {
});
});
Cypress.Commands.add('checkValueForm', (id, search) => {
cy.get(
`.grid-create > :nth-child(${id}) > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > .q-field__input`
).should('have.value', search);
});
Cypress.Commands.add('checkValueSelectForm', (id, search) => {
cy.get(
`.grid-create > :nth-child(${id}) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > .q-field__input`
).should('have.value', search);
});
Cypress.Commands.add('checkValueForm', (id, search) => {
cy.get(
`.grid-create > :nth-child(${id}) > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > .q-field__input`
).should('have.value', search);
});
Cypress.Commands.add('searchByLabel', (label, value) => {
cy.get(`[label="${label}"] > .q-field > .q-field__inner`).type(`${value}{enter}`);
});
Cypress.Commands.add('dataCy', (dataTestId, attr = 'data-cy') => {
return cy.get(`[${attr}="${dataTestId}"]`);
Cypress.Commands.add('dataCy', (tag, attr = 'data-cy') => {
return cy.get(`[${attr}="${tag}"]`);
});