0
0
Fork 0

fix: refs #6942 wip: formModel

This commit is contained in:
Jorge Penadés 2024-05-22 11:22:46 +02:00
parent 467531d029
commit 5bddc6e04d
8 changed files with 179 additions and 226 deletions

View File

@ -11,6 +11,7 @@ import useNotify from 'src/composables/useNotify.js';
import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile';
import { useArrayData } from 'src/composables/useArrayData';
const { push } = useRouter();
const quasar = useQuasar();
@ -85,28 +86,66 @@ const $props = defineProps({
const emit = defineEmits(['onFetch', 'onDataSaved']);
const componentIsRendered = ref(false);
const arrayData = useArrayData($props.model);
const isLoading = ref(false);
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({});
const formData = computed(() => state.get($props.model));
const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({
save: {
color: 'primary',
icon: 'save',
label: 'globals.save',
},
reset: {
color: 'primary',
icon: 'restart_alt',
label: 'globals.reset',
},
...$props.defaultButtons,
}));
onMounted(async () => {
originalData.value = $props.formInitialData;
nextTick(() => {
componentIsRendered.value = true;
});
nextTick(() => (componentIsRendered.value = true));
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
state.set($props.model, $props.formInitialData);
if ($props.autoLoad && !$props.formInitialData) {
await fetch();
if ($props.autoLoad && !$props.formInitialData && $props.url) await fetch();
else if (arrayData.store.data) {
state.set($props.model, arrayData.store.data);
emit('onFetch', state.get($props.model));
}
// Si así se desea disparamos el watcher del form después de 100ms, asi darle tiempo de que se haya cargado la data inicial
// para evitar que detecte cambios cuando es data inicial default
if ($props.observeFormChanges) {
setTimeout(() => {
startFormWatcher();
}, 100);
watch(
() => formData.value,
(val) => {
hasChanges.value = !isResetting.value && val;
isResetting.value = false;
},
{ deep: true }
);
}
});
if (!$props.url)
watch(
() => arrayData.store.data,
(val) => updateAndEmit(val, 'onFetch')
);
watch(formUrl, async () => {
originalData.value = null;
reset();
fetch();
});
onBeforeRouteLeave((to, from, next) => {
if (hasChanges.value && $props.observeFormChanges)
quasar.dialog({
@ -129,49 +168,14 @@ onUnmounted(() => {
if ($props.clearStoreOnUnmount) state.unset($props.model);
});
const isLoading = ref(false);
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({});
const formData = computed(() => state.get($props.model));
const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({
save: {
color: 'primary',
icon: 'save',
label: 'globals.save',
},
reset: {
color: 'primary',
icon: 'restart_alt',
label: 'globals.reset',
},
...$props.defaultButtons,
}));
const startFormWatcher = () => {
watch(
() => formData.value,
(val) => {
hasChanges.value = !isResetting.value && val;
isResetting.value = false;
},
{ deep: true }
);
};
async function fetch() {
try {
let { data } = await axios.get($props.url, {
params: { filter: JSON.stringify($props.filter) },
});
if (Array.isArray(data)) data = data[0] ?? {};
state.set($props.model, data);
originalData.value = data && JSON.parse(JSON.stringify(data));
emit('onFetch', state.get($props.model));
updateAndEmit(data, 'onFetch');
} catch (error) {
state.set($props.model, {});
originalData.value = {};
@ -179,31 +183,30 @@ async function fetch() {
}
async function save() {
if ($props.observeFormChanges && !hasChanges.value) {
notify('globals.noChanges', 'negative');
return;
}
isLoading.value = true;
if ($props.observeFormChanges && !hasChanges.value)
return notify('globals.noChanges', 'negative');
isLoading.value = true;
try {
const body = $props.mapper ? $props.mapper(formData.value) : formData.value;
const method = $props.urlCreate ? 'post' : 'patch';
const url =
$props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url;
let response;
if ($props.saveFn) response = await $props.saveFn(body);
else
response = await axios[$props.urlCreate ? 'post' : 'patch'](
$props.urlCreate || $props.urlUpdate || $props.url,
body
);
else response = await axios[method](url, body);
if ($props.urlCreate) notify('globals.dataCreated', 'positive');
emit('onDataSaved', formData.value, response?.data);
originalData.value = JSON.parse(JSON.stringify(formData.value));
updateAndEmit(response?.data, 'onDataSaved');
hasChanges.value = false;
} catch (err) {
console.error(err);
notify('errors.writeRequest', 'negative');
}
} finally {
isLoading.value = false;
}
}
async function saveAndGo() {
@ -212,10 +215,7 @@ async function saveAndGo() {
}
function reset() {
state.set($props.model, originalData.value);
originalData.value = JSON.parse(JSON.stringify(originalData.value));
emit('onFetch', state.get($props.model));
updateAndEmit(originalData.value, 'onFetch');
if ($props.observeFormChanges) {
hasChanges.value = false;
isResetting.value = true;
@ -237,17 +237,15 @@ function filter(value, update, filterOptions) {
);
}
watch(formUrl, async () => {
originalData.value = null;
reset();
fetch();
});
function updateAndEmit(val, evt) {
state.set($props.model, val);
originalData.value = val && JSON.parse(JSON.stringify(val));
if (!$props.url) arrayData.store.data = val;
defineExpose({
save,
isLoading,
hasChanges,
});
emit(evt, state.get($props.model));
}
defineExpose({ save, isLoading, hasChanges });
</script>
<template>
<div class="column items-center full-width">

View File

@ -49,12 +49,13 @@ const { store } = arrayData;
const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
const isLoading = ref(false);
defineExpose({
getData,
});
defineExpose({ getData });
onBeforeMount(async () => {
await getData();
watch($props, async () => await getData());
watch(
() => [$props.url, $props.filter],
async () => await getData()
);
});
async function getData() {

View File

@ -24,6 +24,7 @@ globals:
create: Create
edit: Edit
save: Save
saveAndContinue: Save and continue
remove: Remove
reset: Reset
close: Close

View File

@ -24,6 +24,7 @@ globals:
create: Crear
edit: Modificar
save: Guardar
saveAndContinue: Guardar y continuar
remove: Eliminar
reset: Restaurar
close: Cerrar

View File

@ -1,6 +1,6 @@
<script setup>
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
@ -14,15 +14,13 @@ import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInput from 'src/components/common/VnInput.vue';
const quasar = useQuasar();
const { currentRoute } = useRouter();
const { t } = useI18n();
const dms = ref({});
const editDownloadDisabled = ref(false);
const arrayData = useArrayData('InvoiceIn');
const invoiceIn = computed(() => arrayData.store.data);
const invoiceIn = computed(() => useArrayData('InvoiceIn').store.data);
const userConfig = ref(null);
const invoiceId = currentRoute.value.params.id;
const invoiceId = computed(() => +useRoute().params.id);
const expenses = ref([]);
const currencies = ref([]);
@ -180,13 +178,7 @@ async function upsert() {
auto-load
@on-fetch="(data) => (sageWithholdings = data)"
/>
<FormModel
v-if="invoiceIn"
:url="`InvoiceIns/${invoiceId}`"
model="InvoiceIn"
:go-to="`/invoice-in/${invoiceId}/vat`"
auto-load
>
<FormModel model="InvoiceIn" :go-to="`/invoice-in/${invoiceId}/vat`" auto-load>
<template #form="{ data }">
<VnRow>
<VnSelect

View File

@ -6,7 +6,6 @@ import { useQuasar } from 'quasar';
import axios from 'axios';
import { toCurrency, toDate } from 'src/filters';
import { useRole } from 'src/composables/useRole';
import useCardDescription from 'src/composables/useCardDescription';
import { downloadFile } from 'src/composables/downloadFile';
import { useArrayData } from 'src/composables/useArrayData';
import { usePrintService } from 'composables/usePrintService';
@ -20,9 +19,7 @@ import { useCapitalize } from 'src/composables/useCapitalize';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import InvoiceInToBook from '../InvoiceInToBook.vue';
const $props = defineProps({
id: { type: Number, default: null },
});
const $props = defineProps({ id: { type: Number, default: null } });
const { push, currentRoute } = useRouter();
@ -33,7 +30,6 @@ const { openReport, sendEmail } = usePrintService();
const { store } = useArrayData('InvoiceIn');
const invoiceIn = computed(() => store.data);
const isBooked = ref();
const cardDescriptorRef = ref();
const correctionDialogRef = ref();
const entityId = computed(() => $props.id || +currentRoute.value.params.id);
@ -92,11 +88,7 @@ const filter = {
},
],
};
const data = ref(useCardDescription());
const invoiceInCorrection = reactive({
correcting: [],
corrected: null,
});
const invoiceInCorrection = reactive({ correcting: [], corrected: null });
const routes = reactive({
getSupplier: (id) => {
return { name: 'SupplierCard', params: { id } };
@ -177,16 +169,9 @@ async function setInvoiceCorrection(id) {
);
}
async function setData(entity) {
data.value = useCardDescription(entity.supplierRef, entity.id);
isBooked.value = entity.isBooked;
const { totalDueDay } = await getTotals();
totalAmount.value = totalDueDay;
}
async function getTotals() {
async function setTotals() {
const { data } = await axios.get(`InvoiceIns/${entityId.value}/getTotals`);
return data;
totalAmount.value = data.totalDueDay;
}
function openDialog() {
@ -303,16 +288,17 @@ const createInvoiceInCorrection = async () => {
auto-load
/>
<CardDescriptor
v-if="invoiceIn"
ref="cardDescriptorRef"
module="InvoiceIn"
:url="`InvoiceIns/${entityId}`"
:filter="filter"
:title="data.title"
:subtitle="data.subtitle"
@on-fetch="setData"
data-key="invoiceInData"
:title="invoiceIn.supplierRef"
:subtitle="invoiceIn.id"
data-key="InvoiceIn"
@on-fetch="setTotals"
>
<template #menu="{ entity }">
<template #menu>
<InvoiceInToBook>
<template #content="{ book }">
<QItem
@ -378,17 +364,20 @@ const createInvoiceInCorrection = async () => {
<QItemSection>{{ t('Create rectificative invoice') }}...</QItemSection>
</QItem>
<QItem
v-if="entity.dmsFk"
v-if="invoiceIn.dmsFk"
v-ripple
clickable
@click="downloadFile(entity.dmsFk)"
@click="downloadFile(invoiceIn.dmsFk)"
>
<QItemSection>{{ t('components.smartCard.downloadFile') }}</QItemSection>
</QItem>
</template>
<template #body="{ entity }">
<VnLv :label="t('invoiceIn.card.issued')" :value="toDate(entity.issued)" />
<VnLv :label="t('invoiceIn.summary.booked')" :value="toDate(entity.booked)" />
<template #body>
<VnLv :label="t('invoiceIn.card.issued')" :value="toDate(invoiceIn.issued)" />
<VnLv
:label="t('invoiceIn.summary.booked')"
:value="toDate(invoiceIn.booked)"
/>
<VnLv :label="t('invoiceIn.card.amount')" :value="toCurrency(totalAmount)" />
<VnLv :label="t('invoiceIn.summary.supplier')">
<template #value>
@ -399,13 +388,13 @@ const createInvoiceInCorrection = async () => {
</template>
</VnLv>
</template>
<template #actions="{ entity }">
<template #action>
<QCardActions>
<QBtn
size="md"
icon="vn:supplier"
color="primary"
:to="routes.getSupplier(entity.supplierFk)"
:to="routes.getSupplier(invoiceIn.supplierFk)"
>
<QTooltip>{{ t('invoiceIn.list.supplier') }}</QTooltip>
</QBtn>
@ -413,7 +402,7 @@ const createInvoiceInCorrection = async () => {
size="md"
icon="vn:entry"
color="primary"
:to="routes.getEntry(entity.entryFk)"
:to="routes.getEntry(invoiceIn.entryFk)"
>
<QTooltip>{{ t('Entry') }}</QTooltip>
</QBtn>
@ -421,7 +410,7 @@ const createInvoiceInCorrection = async () => {
size="md"
icon="vn:ticket"
color="primary"
:to="routes.getTickets(entity.supplierFk)"
:to="routes.getTickets(invoiceIn.supplierFk)"
>
<QTooltip>{{ t('invoiceOut.card.ticketList') }}</QTooltip>
</QBtn>

View File

@ -11,7 +11,7 @@ import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorP
import InvoiceIntoBook from '../InvoiceInToBook.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
const props = defineProps({ id: { type: Number, default: 0 } });
const props = defineProps({ id: { type: [Number, String], default: 0 } });
const { t } = useI18n();
const entityId = computed(() => props.id || useRoute().params.id);
@ -20,7 +20,6 @@ const invoiceIn = computed(() => useArrayData('InvoiceIn').store.data);
const invoiceInUrl = ref();
const amountsNotMatch = ref(null);
const intrastatTotals = ref({ amount: 0, net: 0, stems: 0 });
const isBooked = ref();
const vatColumns = ref([
{
@ -156,37 +155,26 @@ onMounted(async () => {
invoiceInUrl.value = `${await getUrl('')}invoiceIn/${entityId.value}/`;
});
function getAmountNotMatch(totals) {
return (
totals.totalDueDay != totals.totalTaxableBase &&
totals.totalDueDay != totals.totalVat
);
}
function getTaxTotal(tax) {
return tax.reduce(
(acc, cur) => acc + taxRate(cur.taxableBase, cur.taxTypeSage?.rate),
0
);
}
const init = (data) => {
if (!data) return;
isBooked.value = data.isBooked;
amountsNotMatch.value = getAmountNotMatch(data.totals);
const { totals, invoiceInIntrastat } = data;
amountsNotMatch.value =
totals.totalDueDay != totals.totalTaxableBase &&
totals.totalDueDay != totals.totalVat;
if (data.invoiceInIntrastat.length) {
data.invoiceInIntrastat.forEach((val) => {
invoiceInIntrastat.forEach((val) => {
intrastatTotals.value.amount += val.amount;
intrastatTotals.value.net += val.net;
intrastatTotals.value.stems += val.stems;
});
}
};
const taxRate = (taxableBase = 0, rate = 0) => (rate / 100) * taxableBase;
const getTotalTax = (tax) =>
tax.reduce((acc, cur) => acc + taxRate(cur.taxableBase, cur.taxTypeSage?.rate), 0);
const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</script>
@ -199,7 +187,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
<template #header="{ entity }">
<div>{{ entity.id }} - {{ entity.supplier?.name }}</div>
</template>
<template #header-right v-if="!isBooked">
<template #header-right v-if="!invoiceIn?.isBooked">
<InvoiceIntoBook>
<template #content="{ book }">
<QBtn
@ -288,11 +276,9 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
:label="t('invoiceIn.summary.company')"
:value="entity.company?.code"
/>
<QCheckbox
v-if="invoiceIn"
<VnLv
:label="t('invoiceIn.summary.booked')"
v-model="invoiceIn.isBooked"
:disable="true"
:value="invoiceIn.isBooked"
/>
</QCard>
<QCard class="vn-one">
@ -352,7 +338,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
<QTd>{{ toCurrency(entity.totals.totalTaxableBase) }}</QTd>
<QTd></QTd>
<QTd></QTd>
<QTd>{{ toCurrency(getTaxTotal(entity.invoiceInTax)) }}</QTd>
<QTd>{{ toCurrency(getTotalTax(entity.invoiceInTax)) }}</QTd>
<QTd></QTd>
</QTr>
</template>

View File

@ -4,33 +4,17 @@ import { useI18n } from 'vue-i18n';
import VnSelect from 'components/common/VnSelect.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
defineProps({ dataKey: { type: String, required: true } });
const suppliers = ref([]);
const suppliersRef = ref();
</script>
<template>
<FetchData
ref="suppliersRef"
url="Suppliers"
:filter="{ fields: ['id', 'nickname'] }"
order="nickname"
limit="30"
@on-fetch="(data) => (suppliers = data)"
auto-load
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<VnFilterPanel :data-key="dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
@ -38,22 +22,6 @@ const suppliersRef = ref();
</div>
</template>
<template #body="{ params, searchFn }">
<QItem>
<QItemSection>
<VnSelect
:label="t('params.supplierFk')"
v-model="params.supplierFk"
:options="suppliers"
option-value="id"
option-label="nickname"
@input-value="suppliersRef.fetch()"
dense
outlined
rounded
>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
@ -68,21 +36,11 @@ const suppliersRef = ref();
</VnInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate :label="t('From')" v-model="params.from" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate :label="t('To')" v-model="params.to" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('params.serial')"
v-model="params.serial"
:label="t('params.fi')"
v-model="params.fi"
is-outlined
lazy-rules
>
@ -92,11 +50,61 @@ const suppliersRef = ref();
</VnInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
v-model="params.supplierFk"
url="Suppliers"
:fields="['id', 'nickname']"
:label="t('params.supplierFk')"
option-value="id"
option-label="nickname"
:options="suppliers"
dense
outlined
rounded
>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('params.account')"
v-model="params.account"
is-outlined
lazy-rules
>
<template #prepend>
<QIcon name="person" size="sm" />
</template>
</VnInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnCurrency v-model="params.amount" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate :label="t('From')" v-model="params.from" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate :label="t('To')" v-model="params.to" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('Issued')"
v-model="params.issued"
is-outlined
/>
</QItemSection>
</QItem>
<QItem class="q-mb-md">
<QItemSection>
<QCheckbox
@ -111,8 +119,8 @@ const suppliersRef = ref();
<QItem>
<QItemSection>
<VnInput
:label="t('params.fi')"
v-model="params.fi"
:label="t('params.serialNumber')"
v-model="params.serialNumber"
is-outlined
lazy-rules
>
@ -125,8 +133,8 @@ const suppliersRef = ref();
<QItem>
<QItemSection>
<VnInput
:label="t('params.serialNumber')"
v-model="params.serialNumber"
:label="t('params.serial')"
v-model="params.serial"
is-outlined
lazy-rules
>
@ -150,29 +158,6 @@ const suppliersRef = ref();
</VnInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('params.account')"
v-model="params.account"
is-outlined
lazy-rules
>
<template #prepend>
<QIcon name="person" size="sm" />
</template>
</VnInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('Issued')"
v-model="params.issued"
is-outlined
/>
</QItemSection>
</QItem>
</QExpansionItem>
</template>
</VnFilterPanel>