Address views refactor

This commit is contained in:
William Buezas 2025-03-04 10:32:12 -03:00
parent 47c61a7b35
commit bd1e9b7037
8 changed files with 322 additions and 119 deletions

View File

@ -23,7 +23,7 @@ module.exports = configure(function (ctx) {
// app boot file (/src/boot) // app boot file (/src/boot)
// --> boot files are part of "main.js" // --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-webpack/boot-files // https://v2.quasar.dev/quasar-cli-webpack/boot-files
boot: ['i18n', 'axios', 'vnDate', 'error-handler', 'app'], boot: ['i18n', 'axios', 'vnDate', 'app'],
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
css: ['app.scss', 'width.scss', 'responsive.scss'], css: ['app.scss', 'width.scss', 'responsive.scss'],

View File

@ -1,66 +0,0 @@
export default async ({ app }) => {
/*
window.addEventListener('error',
e => onWindowError(e));
window.addEventListener('unhandledrejection',
e => onWindowRejection(e));
,onWindowError(event) {
errorHandler(event.error);
}
,onWindowRejection(event) {
errorHandler(event.reason);
}
*/
app.config.errorHandler = (err, vm, info) => {
errorHandler(err, vm)
}
function errorHandler (err, vm) {
let message
let tMessage
let res = err.response
// XXX: Compatibility with old JSON service
if (err.name === 'JsonException') {
res = {
status: err.statusCode,
data: { error: { message: err.message } }
}
}
if (res) {
const status = res.status
if (status >= 400 && status < 500) {
switch (status) {
case 401:
tMessage = 'loginFailed'
break
case 403:
tMessage = 'authenticationRequired'
vm.$router.push('/login')
break
case 404:
tMessage = 'notFound'
break
default:
message = res.data.error.message
}
} else if (status >= 500) {
tMessage = 'internalServerError'
}
} else {
tMessage = 'somethingWentWrong'
console.error(err)
}
if (tMessage) {
message = vm.$t(tMessage)
}
vm.$q.notify({
message,
type: 'negative'
})
}
}

View File

@ -0,0 +1,232 @@
<script setup>
import { ref, inject, onMounted, computed, Teleport, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
import useNotify from 'src/composables/useNotify.js';
const props = defineProps({
title: {
type: String,
default: ''
},
table: {
type: String,
default: ''
},
schema: {
type: String,
default: ''
},
// Objeto con los datos iniciales del form, si este objeto es definido, no se ejecuta la query fetch
formInitialData: {
type: Object,
default: () => {}
},
autoLoad: {
type: Boolean,
default: true
},
defaultActions: {
type: Boolean,
default: true
},
showBottomActions: {
type: Boolean,
default: false
},
saveFn: {
type: Function,
default: null
},
separationBetweenInputs: {
type: String,
default: 'xs'
},
url: {
type: String,
default: ''
},
urlUpdate: {
type: String,
default: null
},
urlCreate: {
type: String,
default: null
},
observeFormChanges: {
type: Boolean,
default: true,
description:
'Esto se usa principalmente para permitir guardar sin hacer cambios (Útil para la feature de clonar ya que en este caso queremos poder guardar de primeras)'
},
mapper: {
type: Function,
default: null
},
filter: {
type: Object,
default: null
}
});
const emit = defineEmits(['onDataSaved', 'onDataFetched']);
const api = inject('api');
const { t } = useI18n();
const { notify } = useNotify();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const isLoading = ref(false);
const formData = ref({});
const addressFormRef = ref(null);
const hasChanges = ref(!props.observeFormChanges);
const isResetting = ref(false);
const originalData = ref(null);
const separationBetweenInputs = computed(() => {
return `q-gutter-y-${props.separationBetweenInputs}`;
});
const onSubmitSuccess = () => {
emit('onDataSaved');
notify(t('dataSaved'), 'positive');
};
onMounted(async () => {
if (!props.formInitialData) {
if (props.autoLoad && props.url) await fetch();
originalData.value = { ...formData.value };
} else {
formData.value = { ...props.formInitialData };
originalData.value = { ...props.formInitialData };
}
if (props.observeFormChanges) {
watch(
() => formData.value,
(newVal, oldVal) => {
if (!oldVal) return;
hasChanges.value =
!isResetting.value &&
JSON.stringify(newVal) !==
JSON.stringify(originalData.value);
isResetting.value = false;
},
{ deep: true }
);
}
});
async function fetch() {
try {
let { data } = await api.get(props.url, {
params: { filter: JSON.stringify(props.filter) }
});
if (Array.isArray(data)) data = data[0] ?? {};
formData.value = { ...data };
emit('onDataFetched', formData.value);
} catch (e) {
throw e;
}
}
async function submit() {
console.log('submit: ');
if (props.observeFormChanges && !hasChanges.value)
return notify('globals.noChanges', 'negative');
isLoading.value = true;
try {
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;
let response;
if (props.saveFn) await props.saveFn(body);
else await api[method](url, body);
onSubmitSuccess();
hasChanges.value = false;
} finally {
isLoading.value = false;
}
}
defineExpose({
formData,
submit
});
</script>
<template>
<QCard class="form-container" v-bind="$attrs">
<QForm
v-if="!isLoading"
ref="addressFormRef"
class="form"
:class="separationBetweenInputs"
>
<span v-if="title" class="text-h6 text-bold">
{{ title }}
</span>
<slot name="form" :data="formData" />
<slot name="extraForm" :data="formData" />
<component
v-if="isHeaderMounted"
:is="showBottomActions ? 'div' : Teleport"
to="#actions"
class="flex row justify-end q-gutter-x-sm"
:class="{ 'q-mt-md': showBottomActions }"
>
<QBtn
v-if="defaultActions && showBottomActions"
:label="t('cancel')"
:icon="showBottomActions ? undefined : 'check'"
rounded
no-caps
flat
v-close-popup
>
<QTooltip>{{ t('cancel') }}</QTooltip>
</QBtn>
<QBtn
v-if="defaultActions"
:label="t('save')"
:icon="showBottomActions ? undefined : 'check'"
rounded
no-caps
flat
:disabled="!showBottomActions && !hasChanges"
@click="submit()"
data-cy="formModelDefaultSaveButton"
>
<QTooltip>{{ t('save') }}</QTooltip>
</QBtn>
<slot name="actions" :data="formData" />
</component>
</QForm>
<QSpinner v-else color="primary" size="3em" :thickness="2" />
</QCard>
</template>
<style lang="scss" scoped>
.form-container {
width: 100%;
height: max-content;
padding: 32px;
max-width: 544px;
display: flex;
justify-content: center;
}
.form {
display: flex;
flex-direction: column;
width: 100%;
}
</style>

View File

@ -5,32 +5,26 @@ import { useRouter, useRoute } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnForm from 'src/components/common/VnForm.vue'; import FormModel from 'src/components/common/FormModel.vue';
import { useUserStore } from 'stores/user';
import { useAppStore } from 'stores/app'; import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const jApi = inject('jApi'); const api = inject('api');
const appStore = useAppStore(); const appStore = useAppStore();
const userStore = useUserStore();
const { isHeaderMounted } = storeToRefs(appStore); const { isHeaderMounted } = storeToRefs(appStore);
const vnFormRef = ref(null); const vnFormRef = ref(null);
const countriesOptions = ref([]); const countriesOptions = ref([]);
const provincesOptions = ref([]); const provincesOptions = ref([]);
const pks = { id: route.params.id };
const isEditMode = route.params.id !== '0'; const isEditMode = route.params.id !== '0';
const fetchAddressDataSql = { const editAddressData = ref(null);
query: ` const showForm = ref(false);
SELECT a.id, a.street, a.nickname, a.city, a.postalCode, a.provinceFk, p.countryFk
FROM myAddress a
LEFT JOIN vn.province p ON p.id = a.provinceFk
WHERE a.id = #address
`,
params: { address: route.params.id }
};
watch( watch(
() => vnFormRef?.value?.formData?.countryFk, () => vnFormRef?.value?.formData?.countryFk,
@ -40,23 +34,49 @@ watch(
const goBack = () => router.push({ name: 'addressesList' }); const goBack = () => router.push({ name: 'addressesList' });
const getCountries = async () => { const getCountries = async () => {
countriesOptions.value = await jApi.query( const filter = { fields: ['id', 'name'], order: 'name' };
`SELECT id, name FROM vn.country const { data } = await api.get('Countries', {
ORDER BY name` params: { filter: JSON.stringify(filter) }
); });
countriesOptions.value = data;
}; };
const getProvinces = async countryFk => { const getProvinces = async countryFk => {
if (!countryFk) return; if (!countryFk) return;
provincesOptions.value = await jApi.query(
`SELECT id, name FROM vn.province const filter = {
WHERE countryFk = #id where: { countryFk },
ORDER BY name`, fields: ['id', 'name'],
{ id: countryFk } order: 'name'
); };
const { data } = await api.get('Provinces', {
params: { filter: JSON.stringify(filter) }
});
provincesOptions.value = data;
}; };
onMounted(() => getCountries()); const getAddressDetails = async () => {
const { data } = await api.get(`Addresses/${route.params.id}`);
if (!data) return;
const { nickname, street, city, postalCode, province, provinceFk } = data;
editAddressData.value = {
nickname,
street,
city,
postalCode,
countryFk: province?.countryFk,
provinceFk
};
};
onMounted(async () => {
if (isEditMode) {
await getAddressDetails();
}
getCountries();
showForm.value = true;
});
</script> </script>
<template> <template>
@ -74,19 +94,21 @@ onMounted(() => getCountries());
</QTooltip> </QTooltip>
</QBtn> </QBtn>
</Teleport> </Teleport>
<VnForm <FormModel
v-if="showForm"
ref="vnFormRef" ref="vnFormRef"
:fetch-form-data-sql="fetchAddressDataSql" :url-create="
:columns-to-ignore-update="['countryFk']" !isEditMode
:create-model-default="{ ? `Clients/${userStore?.user?.id}/createAddress`
field: 'clientFk', : ''
value: 'account.myUser_getId()' "
}" :url-update="
:pks="pks" isEditMode
:is-edit-mode="isEditMode" ? `Clients/${userStore?.user?.id}/updateAddress/${route?.params?.id}`
: ''
"
:form-initial-data="editAddressData"
:title="t(isEditMode ? 'editAddress' : 'addAddress')" :title="t(isEditMode ? 'editAddress' : 'addAddress')"
table="myAddress"
schema="hedera"
@on-data-saved="goBack()" @on-data-saved="goBack()"
> >
<template #form="{ data }"> <template #form="{ data }">
@ -125,7 +147,7 @@ onMounted(() => getCountries());
data-cy="addressFormProvince" data-cy="addressFormProvince"
/> />
</template> </template>
</VnForm> </FormModel>
</QPage> </QPage>
</template> </template>

View File

@ -10,13 +10,16 @@ import useNotify from 'src/composables/useNotify.js';
import { useVnConfirm } from 'src/composables/useVnConfirm.js'; import { useVnConfirm } from 'src/composables/useVnConfirm.js';
import { useAppStore } from 'stores/app'; import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useUserStore } from 'stores/user';
const router = useRouter(); const router = useRouter();
const jApi = inject('jApi'); const jApi = inject('jApi');
const api = inject('api');
const { notify } = useNotify(); const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const appStore = useAppStore(); const appStore = useAppStore();
const userStore = useUserStore();
const { isHeaderMounted } = storeToRefs(appStore); const { isHeaderMounted } = storeToRefs(appStore);
const addresses = ref([]); const addresses = ref([]);
@ -65,16 +68,13 @@ const changeDefaultAddress = async () => {
notify(t('defaultAddressModified'), 'positive'); notify(t('defaultAddressModified'), 'positive');
}; };
const removeAddress = async id => { async function removeAddress(address) {
try { try {
await jApi.execQuery( await api.patch(
`START TRANSACTION; `/Clients/${userStore?.user?.id}/updateAddress/${address.id}`,
UPDATE hedera.myAddress SET isActive = FALSE
WHERE ((id = #id));
SELECT isActive FROM hedera.myAddress WHERE ((id = #id));
COMMIT`,
{ {
id ...address,
isActive: false
} }
); );
getActiveAddresses(); getActiveAddresses();
@ -82,7 +82,7 @@ const removeAddress = async id => {
} catch (error) { } catch (error) {
console.error('Error removing address:', error); console.error('Error removing address:', error);
} }
}; }
onMounted(async () => { onMounted(async () => {
getDefaultAddress(); getDefaultAddress();
@ -145,7 +145,7 @@ onMounted(async () => {
openConfirmationModal( openConfirmationModal(
null, null,
t('confirmDeleteAddress'), t('confirmDeleteAddress'),
() => removeAddress(address.id) () => removeAddress(address)
) )
" "
> >

View File

@ -46,15 +46,28 @@ describe('PendingOrders', () => {
.should('contain', data.postcode); .should('contain', data.postcode);
}; };
it('should fail if we enter a wrong postcode', () => {
cy.dataCy('newAddressBtn').should('exist');
cy.dataCy('newAddressBtn').click();
cy.dataCy('formModelDefaultSaveButton').should('exist');
cy.dataCy('formModelDefaultSaveButton').should('be.disabled');
const addressFormData = getRandomAddressFormData();
fillFormWithData(addressFormData);
cy.dataCy('formModelDefaultSaveButton').should('not.be.disabled');
cy.dataCy('formModelDefaultSaveButton').click();
cy.checkNotify('negative');
});
it('should create a new address', () => { it('should create a new address', () => {
cy.dataCy('newAddressBtn').should('exist'); cy.dataCy('newAddressBtn').should('exist');
cy.dataCy('newAddressBtn').click(); cy.dataCy('newAddressBtn').click();
cy.dataCy('formDefaultSaveButton').should('exist'); cy.dataCy('formModelDefaultSaveButton').should('exist');
cy.dataCy('formDefaultSaveButton').should('be.disabled'); cy.dataCy('formModelDefaultSaveButton').should('be.disabled');
const addressFormData = getRandomAddressFormData(); const addressFormData = getRandomAddressFormData();
addressFormData.postcode = '46460'; // Usamos un postcode válido
fillFormWithData(addressFormData); fillFormWithData(addressFormData);
cy.dataCy('formDefaultSaveButton').should('not.be.disabled'); cy.dataCy('formModelDefaultSaveButton').should('not.be.disabled');
cy.dataCy('formDefaultSaveButton').click(); cy.dataCy('formModelDefaultSaveButton').click();
cy.checkNotify('positive', 'Datos guardados'); cy.checkNotify('positive', 'Datos guardados');
verifyAddressCardData(addressFormData); verifyAddressCardData(addressFormData);
}); });
@ -71,9 +84,10 @@ describe('PendingOrders', () => {
}); });
// Fill form with new data // Fill form with new data
const addressFormData = getRandomAddressFormData(); const addressFormData = getRandomAddressFormData();
addressFormData.postcode = '46460'; // Usamos un postcode válido
fillFormWithData(addressFormData); fillFormWithData(addressFormData);
cy.dataCy('formDefaultSaveButton').should('not.be.disabled'); cy.dataCy('formModelDefaultSaveButton').should('not.be.disabled');
cy.dataCy('formDefaultSaveButton').click(); cy.dataCy('formModelDefaultSaveButton').click();
cy.checkNotify('positive', 'Datos guardados'); cy.checkNotify('positive', 'Datos guardados');
verifyAddressCardData(addressFormData); verifyAddressCardData(addressFormData);
}); });

View File

@ -90,5 +90,6 @@ Cypress.Commands.add('setConfirmDialog', () => {
}); });
Cypress.Commands.add('checkNotify', (status, content) => { Cypress.Commands.add('checkNotify', (status, content) => {
cy.dataCy(`${status}Notify`).should('contain', content); if (content) cy.dataCy(`${status}Notify`).should('contain', content);
else cy.dataCy(`${status}Notify`).should('exist');
}); });