2023-08-16 13:04:16 +00:00
|
|
|
<script setup>
|
|
|
|
import axios from 'axios';
|
2023-09-25 10:06:49 +00:00
|
|
|
import { computed, ref, watch } from 'vue';
|
2024-03-06 15:23:19 +00:00
|
|
|
import { useRouter } from 'vue-router';
|
2023-08-16 13:04:16 +00:00
|
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
import { useQuasar } from 'quasar';
|
|
|
|
import { useValidator } from 'src/composables/useValidator';
|
|
|
|
import { useStateStore } from 'stores/useStateStore';
|
|
|
|
import VnPaginate from 'components/ui/VnPaginate.vue';
|
|
|
|
import VnConfirm from 'components/ui/VnConfirm.vue';
|
|
|
|
import SkeletonTable from 'components/ui/SkeletonTable.vue';
|
2023-10-11 13:59:41 +00:00
|
|
|
import { tMobile } from 'src/composables/tMobile';
|
2023-08-16 13:04:16 +00:00
|
|
|
|
2024-03-06 15:23:19 +00:00
|
|
|
const { push } = useRouter();
|
2023-08-16 13:04:16 +00:00
|
|
|
const quasar = useQuasar();
|
|
|
|
const stateStore = useStateStore();
|
|
|
|
const { t } = useI18n();
|
|
|
|
const { validate } = useValidator();
|
|
|
|
|
|
|
|
const $props = defineProps({
|
|
|
|
model: {
|
|
|
|
type: String,
|
|
|
|
default: '',
|
|
|
|
},
|
|
|
|
url: {
|
|
|
|
type: String,
|
|
|
|
default: '',
|
|
|
|
},
|
2024-04-15 06:40:15 +00:00
|
|
|
limit: {
|
|
|
|
type: Number,
|
|
|
|
default: 20,
|
|
|
|
},
|
2023-08-16 13:04:16 +00:00
|
|
|
saveUrl: {
|
|
|
|
type: String,
|
|
|
|
default: null,
|
|
|
|
},
|
|
|
|
primaryKey: {
|
|
|
|
type: String,
|
|
|
|
default: 'id',
|
|
|
|
},
|
|
|
|
dataRequired: {
|
|
|
|
type: Object,
|
2023-09-25 10:06:49 +00:00
|
|
|
default: () => {},
|
2023-08-16 13:04:16 +00:00
|
|
|
},
|
|
|
|
defaultSave: {
|
|
|
|
type: Boolean,
|
|
|
|
default: true,
|
|
|
|
},
|
|
|
|
defaultReset: {
|
|
|
|
type: Boolean,
|
|
|
|
default: true,
|
|
|
|
},
|
|
|
|
defaultRemove: {
|
|
|
|
type: Boolean,
|
|
|
|
default: true,
|
|
|
|
},
|
|
|
|
selected: {
|
|
|
|
type: Object,
|
|
|
|
default: null,
|
|
|
|
},
|
2023-09-19 08:21:24 +00:00
|
|
|
saveFn: {
|
|
|
|
type: Function,
|
|
|
|
default: null,
|
|
|
|
},
|
2024-03-06 15:23:19 +00:00
|
|
|
goTo: {
|
|
|
|
type: String,
|
|
|
|
default: '',
|
|
|
|
description: 'It is used for redirect on click "save and continue"',
|
|
|
|
},
|
2024-06-17 09:29:36 +00:00
|
|
|
hasSubtoolbar: {
|
|
|
|
type: Boolean,
|
|
|
|
default: true,
|
|
|
|
},
|
2023-08-16 13:04:16 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const isLoading = ref(false);
|
|
|
|
const hasChanges = ref(false);
|
|
|
|
const originalData = ref();
|
|
|
|
const vnPaginateRef = ref();
|
2023-09-21 13:04:21 +00:00
|
|
|
const formData = ref();
|
2023-10-11 13:59:41 +00:00
|
|
|
const saveButtonRef = ref(null);
|
2024-06-14 08:53:02 +00:00
|
|
|
const watchChanges = ref();
|
2023-08-16 13:04:16 +00:00
|
|
|
const formUrl = computed(() => $props.url);
|
|
|
|
|
2023-10-11 13:59:41 +00:00
|
|
|
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
|
2023-08-16 13:04:16 +00:00
|
|
|
|
|
|
|
defineExpose({
|
2023-09-25 10:06:49 +00:00
|
|
|
reload,
|
2023-08-16 13:04:16 +00:00
|
|
|
insert,
|
|
|
|
remove,
|
|
|
|
onSubmit,
|
|
|
|
reset,
|
|
|
|
hasChanges,
|
2023-10-11 13:59:41 +00:00
|
|
|
saveChanges,
|
2024-04-15 06:40:15 +00:00
|
|
|
getChanges,
|
2024-04-24 13:31:33 +00:00
|
|
|
formData,
|
2024-06-11 13:00:43 +00:00
|
|
|
vnPaginateRef,
|
2023-08-16 13:04:16 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
async function fetch(data) {
|
|
|
|
if (data && Array.isArray(data)) {
|
|
|
|
let $index = 0;
|
|
|
|
data.map((d) => (d.$index = $index++));
|
|
|
|
}
|
|
|
|
|
2024-06-14 08:53:02 +00:00
|
|
|
resetData(data);
|
2023-08-16 13:04:16 +00:00
|
|
|
|
2023-09-21 13:04:21 +00:00
|
|
|
emit('onFetch', data);
|
2023-11-06 14:33:18 +00:00
|
|
|
return data;
|
2023-08-16 13:04:16 +00:00
|
|
|
}
|
|
|
|
|
2024-06-14 08:53:02 +00:00
|
|
|
function resetData(data) {
|
|
|
|
if (!data) return;
|
|
|
|
originalData.value = JSON.parse(JSON.stringify(data));
|
|
|
|
formData.value = JSON.parse(JSON.stringify(data));
|
|
|
|
|
|
|
|
if (watchChanges.value) watchChanges.value(); //destoy watcher
|
|
|
|
watchChanges.value = watch(formData, () => (hasChanges.value = true), { deep: true });
|
|
|
|
}
|
|
|
|
|
2023-09-25 10:06:49 +00:00
|
|
|
async function reset() {
|
|
|
|
await fetch(originalData.value);
|
2023-08-16 13:04:16 +00:00
|
|
|
hasChanges.value = false;
|
|
|
|
}
|
2024-06-14 08:53:02 +00:00
|
|
|
|
2023-08-16 13:04:16 +00:00
|
|
|
function filter(value, update, filterOptions) {
|
|
|
|
update(
|
|
|
|
() => {
|
|
|
|
const { options, filterFn, field } = filterOptions;
|
|
|
|
|
|
|
|
options.value = filterFn(options, value, field);
|
|
|
|
},
|
|
|
|
(ref) => {
|
|
|
|
ref.setOptionIndex(-1);
|
|
|
|
ref.moveOptionSelection(1, true);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function onSubmit() {
|
|
|
|
if (!hasChanges.value) {
|
|
|
|
return quasar.notify({
|
|
|
|
type: 'negative',
|
|
|
|
message: t('globals.noChanges'),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
isLoading.value = true;
|
2024-04-26 11:02:17 +00:00
|
|
|
await saveChanges($props.saveFn ? formData.value : null);
|
2023-08-16 13:04:16 +00:00
|
|
|
}
|
|
|
|
|
2024-06-21 11:47:42 +00:00
|
|
|
async function onSubmitAndGo() {
|
2024-03-06 15:23:19 +00:00
|
|
|
await onSubmit();
|
|
|
|
push({ path: $props.goTo });
|
|
|
|
}
|
|
|
|
|
2023-08-16 13:04:16 +00:00
|
|
|
async function saveChanges(data) {
|
2024-04-26 11:02:17 +00:00
|
|
|
if ($props.saveFn) {
|
|
|
|
$props.saveFn(data, getChanges);
|
|
|
|
isLoading.value = false;
|
2024-04-29 13:16:37 +00:00
|
|
|
hasChanges.value = false;
|
2024-04-26 11:02:17 +00:00
|
|
|
return;
|
|
|
|
}
|
2023-08-16 13:04:16 +00:00
|
|
|
const changes = data || getChanges();
|
|
|
|
try {
|
|
|
|
await axios.post($props.saveUrl || $props.url + '/crud', changes);
|
|
|
|
} catch (e) {
|
|
|
|
return (isLoading.value = false);
|
|
|
|
}
|
|
|
|
originalData.value = JSON.parse(JSON.stringify(formData.value));
|
2023-09-19 08:21:24 +00:00
|
|
|
if (changes.creates?.length) await vnPaginateRef.value.fetch();
|
|
|
|
|
2023-08-16 13:04:16 +00:00
|
|
|
hasChanges.value = false;
|
|
|
|
isLoading.value = false;
|
2023-10-11 13:59:41 +00:00
|
|
|
emit('saveChanges', data);
|
2023-11-09 09:29:28 +00:00
|
|
|
quasar.notify({
|
|
|
|
type: 'positive',
|
|
|
|
message: t('globals.dataSaved'),
|
|
|
|
});
|
2023-08-16 13:04:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function insert() {
|
|
|
|
const $index = formData.value.length
|
|
|
|
? formData.value[formData.value.length - 1].$index + 1
|
|
|
|
: 0;
|
|
|
|
formData.value.push(Object.assign({ $index }, $props.dataRequired));
|
|
|
|
hasChanges.value = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function remove(data) {
|
|
|
|
if (!data.length)
|
|
|
|
return quasar.notify({
|
|
|
|
type: 'warning',
|
|
|
|
message: t('globals.noChanges'),
|
|
|
|
});
|
|
|
|
|
|
|
|
const pk = $props.primaryKey;
|
|
|
|
let ids = data.map((d) => d[pk]).filter(Boolean);
|
|
|
|
let preRemove = data.map((d) => (d[pk] ? null : d.$index)).filter(Boolean);
|
|
|
|
let newData = formData.value;
|
|
|
|
|
|
|
|
if (preRemove.length) {
|
|
|
|
newData = newData.filter(
|
|
|
|
(form) => !preRemove.some((index) => index == form.$index)
|
|
|
|
);
|
|
|
|
const changes = getChanges();
|
|
|
|
if (!changes.creates?.length && !changes.updates?.length)
|
|
|
|
hasChanges.value = false;
|
2023-09-21 13:04:21 +00:00
|
|
|
fetch(newData);
|
2023-08-16 13:04:16 +00:00
|
|
|
}
|
|
|
|
if (ids.length) {
|
|
|
|
quasar
|
|
|
|
.dialog({
|
|
|
|
component: VnConfirm,
|
|
|
|
componentProps: {
|
2024-02-12 14:06:20 +00:00
|
|
|
title: t('globals.confirmDeletion'),
|
|
|
|
message: t('globals.confirmDeletionMessage'),
|
2023-08-16 13:04:16 +00:00
|
|
|
newData,
|
|
|
|
ids,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
.onOk(async () => {
|
|
|
|
await saveChanges({ deletes: ids });
|
|
|
|
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
|
2023-09-21 13:04:21 +00:00
|
|
|
fetch(newData);
|
2023-08-16 13:04:16 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
emit('update:selected', []);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getChanges() {
|
|
|
|
const updates = [];
|
|
|
|
const creates = [];
|
|
|
|
|
|
|
|
const pk = $props.primaryKey;
|
|
|
|
for (const [i, row] of formData.value.entries()) {
|
|
|
|
if (!row[pk]) {
|
|
|
|
creates.push(row);
|
|
|
|
} else if (originalData.value) {
|
|
|
|
const data = getDifferences(originalData.value[i], row);
|
|
|
|
if (!isEmpty(data)) {
|
|
|
|
updates.push({
|
|
|
|
data,
|
|
|
|
where: { [pk]: row[pk] },
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const changes = { updates, creates };
|
2024-02-06 14:13:43 +00:00
|
|
|
|
2023-08-16 13:04:16 +00:00
|
|
|
for (let prop in changes) {
|
|
|
|
if (changes[prop].length === 0) changes[prop] = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return changes;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getDifferences(obj1, obj2) {
|
|
|
|
let diff = {};
|
2023-09-25 10:06:49 +00:00
|
|
|
delete obj1.$index;
|
|
|
|
delete obj2.$index;
|
|
|
|
|
2023-08-16 13:04:16 +00:00
|
|
|
for (let key in obj1) {
|
2024-01-09 13:46:27 +00:00
|
|
|
if (obj2[key] && JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
|
2023-08-16 13:04:16 +00:00
|
|
|
diff[key] = obj2[key];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (let key in obj2) {
|
2024-01-09 13:46:27 +00:00
|
|
|
if (
|
|
|
|
obj1[key] === undefined ||
|
|
|
|
JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])
|
|
|
|
) {
|
2023-08-16 13:04:16 +00:00
|
|
|
diff[key] = obj2[key];
|
|
|
|
}
|
|
|
|
}
|
2024-01-09 13:46:27 +00:00
|
|
|
|
2023-08-16 13:04:16 +00:00
|
|
|
return diff;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isEmpty(obj) {
|
|
|
|
if (obj == null) return true;
|
|
|
|
if (obj === undefined) return true;
|
|
|
|
if (Object.keys(obj).length === 0) return true;
|
|
|
|
|
|
|
|
if (obj.length > 0) return false;
|
|
|
|
}
|
2023-08-25 10:33:38 +00:00
|
|
|
|
2024-06-11 13:00:43 +00:00
|
|
|
async function reload(params) {
|
|
|
|
const data = await vnPaginateRef.value.fetch(params);
|
|
|
|
fetch(data);
|
2023-09-25 10:06:49 +00:00
|
|
|
}
|
|
|
|
|
2023-08-25 10:33:38 +00:00
|
|
|
watch(formUrl, async () => {
|
|
|
|
originalData.value = null;
|
|
|
|
reset();
|
|
|
|
});
|
2023-08-16 13:04:16 +00:00
|
|
|
</script>
|
|
|
|
<template>
|
|
|
|
<VnPaginate
|
|
|
|
:url="url"
|
2024-04-15 06:40:15 +00:00
|
|
|
:limit="limit"
|
2023-08-16 13:04:16 +00:00
|
|
|
@on-fetch="fetch"
|
2024-06-14 08:53:02 +00:00
|
|
|
@on-change="resetData"
|
2023-08-16 13:04:16 +00:00
|
|
|
:skeleton="false"
|
|
|
|
ref="vnPaginateRef"
|
2024-06-11 13:00:43 +00:00
|
|
|
v-bind="$attrs"
|
2023-08-16 13:04:16 +00:00
|
|
|
>
|
|
|
|
<template #body v-if="formData">
|
|
|
|
<slot
|
|
|
|
name="body"
|
|
|
|
:rows="formData"
|
|
|
|
:validate="validate"
|
|
|
|
:filter="filter"
|
|
|
|
></slot>
|
|
|
|
</template>
|
|
|
|
</VnPaginate>
|
2024-06-14 07:19:51 +00:00
|
|
|
<SkeletonTable v-if="!formData" :columns="$attrs.columns?.length" />
|
2024-06-17 09:29:36 +00:00
|
|
|
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubtoolbar">
|
2023-11-07 11:42:39 +00:00
|
|
|
<QBtnGroup push style="column-gap: 10px">
|
2023-10-11 13:59:41 +00:00
|
|
|
<slot name="moreBeforeActions" />
|
2023-08-16 13:04:16 +00:00
|
|
|
<QBtn
|
|
|
|
:label="tMobile('globals.remove')"
|
|
|
|
color="primary"
|
|
|
|
icon="delete"
|
2023-08-23 13:09:03 +00:00
|
|
|
flat
|
2023-08-16 13:04:16 +00:00
|
|
|
@click="remove(selected)"
|
|
|
|
:disable="!selected?.length"
|
|
|
|
:title="t('globals.remove')"
|
|
|
|
v-if="$props.defaultRemove"
|
|
|
|
/>
|
|
|
|
<QBtn
|
|
|
|
:label="tMobile('globals.reset')"
|
|
|
|
color="primary"
|
|
|
|
icon="restart_alt"
|
|
|
|
flat
|
|
|
|
@click="reset"
|
|
|
|
:disable="!hasChanges"
|
|
|
|
:title="t('globals.reset')"
|
|
|
|
v-if="$props.defaultReset"
|
|
|
|
/>
|
2024-03-06 15:23:19 +00:00
|
|
|
<QBtnDropdown
|
|
|
|
v-if="$props.goTo && $props.defaultSave"
|
2024-06-21 11:47:42 +00:00
|
|
|
@click="onSubmitAndGo"
|
2024-03-07 13:54:20 +00:00
|
|
|
:label="tMobile('globals.saveAndContinue')"
|
|
|
|
:title="t('globals.saveAndContinue')"
|
2024-03-06 15:23:19 +00:00
|
|
|
:disable="!hasChanges"
|
|
|
|
color="primary"
|
|
|
|
icon="save"
|
|
|
|
split
|
|
|
|
>
|
|
|
|
<QList>
|
2024-03-07 13:54:20 +00:00
|
|
|
<QItem
|
|
|
|
color="primary"
|
|
|
|
clickable
|
|
|
|
v-close-popup
|
|
|
|
@click="onSubmit"
|
|
|
|
:title="t('globals.save')"
|
|
|
|
>
|
2024-03-06 15:23:19 +00:00
|
|
|
<QItemSection>
|
2024-03-07 13:54:20 +00:00
|
|
|
<QItemLabel>
|
|
|
|
<QIcon
|
|
|
|
name="save"
|
|
|
|
color="white"
|
|
|
|
class="q-mr-sm"
|
|
|
|
size="sm"
|
|
|
|
/>
|
|
|
|
{{ t('globals.save').toUpperCase() }}
|
|
|
|
</QItemLabel>
|
2024-03-06 15:23:19 +00:00
|
|
|
</QItemSection>
|
|
|
|
</QItem>
|
|
|
|
</QList>
|
|
|
|
</QBtnDropdown>
|
2023-08-16 13:04:16 +00:00
|
|
|
<QBtn
|
2024-03-06 15:23:19 +00:00
|
|
|
v-else-if="!$props.goTo && $props.defaultSave"
|
2023-08-16 13:04:16 +00:00
|
|
|
:label="tMobile('globals.save')"
|
2023-10-11 13:59:41 +00:00
|
|
|
ref="saveButtonRef"
|
2023-08-16 13:04:16 +00:00
|
|
|
color="primary"
|
|
|
|
icon="save"
|
|
|
|
@click="onSubmit"
|
|
|
|
:disable="!hasChanges"
|
|
|
|
:title="t('globals.save')"
|
|
|
|
/>
|
2023-10-11 13:59:41 +00:00
|
|
|
<slot name="moreAfterActions" />
|
2023-08-16 13:04:16 +00:00
|
|
|
</QBtnGroup>
|
|
|
|
</Teleport>
|
|
|
|
<QInnerLoading
|
|
|
|
:showing="isLoading"
|
2023-08-24 13:05:31 +00:00
|
|
|
:label="t && t('globals.pleaseWait')"
|
2023-08-16 13:04:16 +00:00
|
|
|
color="primary"
|
|
|
|
/>
|
|
|
|
</template>
|