diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue index 93a2ac96a..8c4f70f3b 100644 --- a/src/components/CrudModel.vue +++ b/src/components/CrudModel.vue @@ -184,8 +184,11 @@ async function saveChanges(data) { if ($props.beforeSaveFn) { changes = await $props.beforeSaveFn(changes, getChanges); } - try { + if (changes?.creates?.length === 0 && changes?.updates?.length === 0) { + return; + } + await axios.post($props.saveUrl || $props.url + '/crud', changes); } finally { isLoading.value = false; diff --git a/src/components/__tests__/CrudModel.spec.js b/src/components/__tests__/CrudModel.spec.js index e0afd30ad..f6c93e0d5 100644 --- a/src/components/__tests__/CrudModel.spec.js +++ b/src/components/__tests__/CrudModel.spec.js @@ -30,8 +30,8 @@ describe('CrudModel', () => { saveFn: '', }, }); - wrapper=wrapper.wrapper; - vm=wrapper.vm; + wrapper = wrapper.wrapper; + vm = wrapper.vm; }); beforeEach(() => { @@ -143,14 +143,14 @@ describe('CrudModel', () => { }); it('should return true if object is empty', async () => { - dummyObj ={}; - result = vm.isEmpty(dummyObj); + dummyObj = {}; + result = vm.isEmpty(dummyObj); expect(result).toBe(true); }); it('should return false if object is not empty', async () => { - dummyObj = {a:1, b:2, c:3}; + dummyObj = { a: 1, b: 2, c: 3 }; result = vm.isEmpty(dummyObj); expect(result).toBe(false); @@ -158,29 +158,31 @@ describe('CrudModel', () => { it('should return true if array is empty', async () => { dummyArray = []; - result = vm.isEmpty(dummyArray); + result = vm.isEmpty(dummyArray); expect(result).toBe(true); }); - + it('should return false if array is not empty', async () => { - dummyArray = [1,2,3]; + dummyArray = [1, 2, 3]; result = vm.isEmpty(dummyArray); expect(result).toBe(false); - }) + }); }); describe('resetData()', () => { it('should add $index to elements in data[] and sets originalData and formData with data', async () => { - data = [{ - name: 'Tony', - lastName: 'Stark', - age: 42, - }]; + data = [ + { + name: 'Tony', + lastName: 'Stark', + age: 42, + }, + ]; vm.resetData(data); - + expect(vm.originalData).toEqual(data); expect(vm.originalData[0].$index).toEqual(0); expect(vm.formData).toEqual(data); @@ -200,7 +202,7 @@ describe('CrudModel', () => { lastName: 'Stark', age: 42, }; - + vm.resetData(data); expect(vm.originalData).toEqual(data); @@ -210,17 +212,19 @@ describe('CrudModel', () => { }); describe('saveChanges()', () => { - data = [{ - name: 'Tony', - lastName: 'Stark', - age: 42, - }]; + data = [ + { + name: 'Tony', + lastName: 'Stark', + age: 42, + }, + ]; it('should call saveFn if exists', async () => { await wrapper.setProps({ saveFn: vi.fn() }); vm.saveChanges(data); - + expect(vm.saveFn).toHaveBeenCalledOnce(); expect(vm.isLoading).toBe(false); expect(vm.hasChanges).toBe(false); @@ -229,13 +233,15 @@ describe('CrudModel', () => { }); it("should use default url if there's not saveFn", async () => { - const postMock =vi.spyOn(axios, 'post'); - - vm.formData = [{ - name: 'Bruce', - lastName: 'Wayne', - age: 45, - }] + const postMock = vi.spyOn(axios, 'post'); + + vm.formData = [ + { + name: 'Bruce', + lastName: 'Wayne', + age: 45, + }, + ]; await vm.saveChanges(data); diff --git a/src/pages/Customer/Card/CustomerSummary.vue b/src/pages/Customer/Card/CustomerSummary.vue index 324da0771..c98bf1ffb 100644 --- a/src/pages/Customer/Card/CustomerSummary.vue +++ b/src/pages/Customer/Card/CustomerSummary.vue @@ -325,7 +325,7 @@ const sumRisk = ({ clientRisks }) => { </QCard> <QCard class="vn-max"> <VnTitle :text="t('Latest tickets')" /> - <CustomerSummaryTable /> + <CustomerSummaryTable :id="entityId" /> </QCard> </template> </CardSummary> diff --git a/src/pages/Customer/components/CustomerSummaryTable.vue b/src/pages/Customer/components/CustomerSummaryTable.vue index bb6f4442b..09c7e714c 100644 --- a/src/pages/Customer/components/CustomerSummaryTable.vue +++ b/src/pages/Customer/components/CustomerSummaryTable.vue @@ -20,7 +20,12 @@ const { t } = useI18n(); const route = useRoute(); const router = useRouter(); const { viewSummary } = useSummaryDialog(); - +const $props = defineProps({ + id: { + type: Number, + default: null, + }, +}); const filter = { include: [ { @@ -43,7 +48,7 @@ const filter = { }, }, ], - where: { clientFk: route.params.id }, + where: { clientFk: $props.id ?? route.params.id }, order: ['shipped DESC', 'id'], limit: 30, }; diff --git a/src/pages/Item/Card/ItemDiary.vue b/src/pages/Item/Card/ItemDiary.vue index 83cd562a0..f839c1f71 100644 --- a/src/pages/Item/Card/ItemDiary.vue +++ b/src/pages/Item/Card/ItemDiary.vue @@ -158,15 +158,10 @@ const getBadgeAttrs = (_date) => { const scrollToToday = async () => { await nextTick(); - const todayCell = document.querySelector(`td[data-date="${today.toISOString()}"]`); - if (todayCell) { - todayCell.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } -}; - -const formatDateForAttribute = (dateValue) => { - if (dateValue instanceof Date) return date.formatDate(dateValue, 'YYYY-MM-DD'); - return dateValue; + const todayCell = document.querySelector( + `td[data-date="${date.formatDate(today, 'YYYY-MM-DD')}"]`, + ); + if (todayCell) todayCell.scrollIntoView({ behavior: 'smooth', block: 'center' }); }; async function updateWarehouse(warehouseFk) { @@ -242,7 +237,7 @@ async function updateWarehouse(warehouseFk) { </QTd> </template> <template #body-cell-date="{ row }"> - <QTd @click.stop :data-date="formatDateForAttribute(row.shipped)"> + <QTd @click.stop :data-date="row?.shipped.substring(0, 10)"> <QBadge v-bind="getBadgeAttrs(row.shipped)" class="q-ma-none" diff --git a/src/pages/Order/Card/OrderCatalog.vue b/src/pages/Order/Card/OrderCatalog.vue index 4b3992f21..dbb66c0ec 100644 --- a/src/pages/Order/Card/OrderCatalog.vue +++ b/src/pages/Order/Card/OrderCatalog.vue @@ -10,6 +10,7 @@ import OrderCatalogFilter from 'src/pages/Order/Card/OrderCatalogFilter.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import { useArrayData } from 'src/composables/useArrayData'; import RightMenu from 'src/components/common/RightMenu.vue'; +import { onUnmounted } from 'vue'; const route = useRoute(); const router = useRouter(); @@ -23,16 +24,40 @@ const catalogParams = { const arrayData = useArrayData(dataKey, { url: 'Orders/CatalogFilter', userParams: catalogParams, + exprBuilder, + searchUrl: 'table', }); const store = arrayData.store; const tags = ref([]); const itemRefs = ref({}); -onMounted(() => { +onMounted(async () => { stateStore.rightDrawer = true; checkOrderConfirmation(); + + if ( + arrayData.store.userParams && + Object.keys(arrayData.store.userParams).some((key) => !key.startsWith('order')) + ) { + await arrayData.fetch({}); + } }); +onUnmounted(() => { + arrayData.destroy(); +}); + +function exprBuilder(param, value) { + switch (param) { + case 'categoryFk': + case 'typeFk': + return { [param]: value }; + case 'search': + if (/^\d+$/.test(value)) return { 'i.id': value }; + else return { 'i.name': { like: `%${value}%` } }; + } +} + async function checkOrderConfirmation() { const response = await axios.get(`Orders/${route.params.id}`); if (response.data.isConfirmed === 1) { @@ -96,6 +121,7 @@ watch( :tag-value="tagValue" :tags="tags" :initial-catalog-params="catalogParams" + :arrayData /> </template> </RightMenu> diff --git a/src/pages/Order/Card/OrderCatalogFilter.vue b/src/pages/Order/Card/OrderCatalogFilter.vue index 76e608983..d16a92017 100644 --- a/src/pages/Order/Card/OrderCatalogFilter.vue +++ b/src/pages/Order/Card/OrderCatalogFilter.vue @@ -24,6 +24,10 @@ const props = defineProps({ type: Array, required: true, }, + arrayData: { + type: Object, + required: true, + }, }); const { t } = useI18n(); @@ -74,17 +78,6 @@ const loadTypes = async (id) => { typeList.value = data; }; -function exprBuilder(param, value) { - switch (param) { - case 'categoryFk': - case 'typeFk': - return { [param]: value }; - case 'search': - if (/^\d+$/.test(value)) return { 'i.id': value }; - else return { 'i.name': { like: `%${value}%` } }; - } -} - const applyTags = (tagInfo, params, search) => { if (!tagInfo || !tagInfo.values.length) { params.tagGroups = null; @@ -152,9 +145,8 @@ function addOrder(value, field, params) { :data-key="props.dataKey" :hidden-tags="['filter', 'orderFk', 'orderBy']" :unremovable-params="['orderFk', 'orderBy']" - :expr-builder="exprBuilder" :custom-tags="['tagGroups', 'categoryFk']" - :redirect="false" + :arrayData > <template #tags="{ tag, formatFn }"> <strong v-if="tag.label === 'typeFk' && typeList"> diff --git a/src/pages/Ticket/Card/TicketService.vue b/src/pages/Ticket/Card/TicketService.vue index 6ce69a6aa..1bd1548a4 100644 --- a/src/pages/Ticket/Card/TicketService.vue +++ b/src/pages/Ticket/Card/TicketService.vue @@ -121,6 +121,50 @@ async function handleSave() { isSaving.value = false; } } +function validateFields(item) { + // Only validate fields that are being updated + const shouldExist = (field) => !isUpdate || field in item; + + if (!shouldExist('ticketServiceTypeFk') && !item.ticketServiceTypeFk) { + notify('Description is required', 'negative'); + return false; + } + + if (!shouldExist('quantity') && (!item.quantity || item.quantity <= 0)) { + notify('Quantity must be greater than 0', 'negative'); + return false; + } + + if (!shouldExist('price') && (!item.price || item.price < 0)) { + notify('Price must be valid', 'negative'); + return false; + } + + return true; +} + +function beforeSave(data) { + const { creates = [], updates = [] } = data; + const validData = { creates: [], updates: [] }; + + // Validate creates + if (creates.length) { + for (const create of creates) { + create.ticketFk = route.params.id; + if (validateFields(create)) { + validData.creates.push(create); + } + } + } + + // Validate updates + if (updates.length) { + for (const update of updates) { + validData.updates.push(update); + } + } + return validData; +} </script> <template> @@ -141,6 +185,7 @@ async function handleSave() { v-model:selected="selected" :order="['description ASC']" :default-remove="false" + :beforeSaveFn="beforeSave" > <template #moreBeforeActions> <QBtn @@ -170,6 +215,7 @@ async function handleSave() { option-value="id" hide-selected sort-by="name ASC" + :required="true" > <template #form> <TicketCreateServiceType @@ -185,6 +231,7 @@ async function handleSave() { :label="col.label" v-model.number="row.quantity" type="number" + :required="true" min="0" :info="t('service.quantityInfo')" /> @@ -196,6 +243,7 @@ async function handleSave() { :label="col.label" v-model.number="row.price" type="number" + :required="true" min="0" @keyup.enter="handleSave" /> diff --git a/src/pages/Worker/Card/WorkerBasicData.vue b/src/pages/Worker/Card/WorkerBasicData.vue index cf43412af..ace220983 100644 --- a/src/pages/Worker/Card/WorkerBasicData.vue +++ b/src/pages/Worker/Card/WorkerBasicData.vue @@ -1,5 +1,5 @@ <script setup> -import { ref } from 'vue'; +import { ref, nextTick } from 'vue'; import { useI18n } from 'vue-i18n'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import FetchData from 'components/FetchData.vue'; diff --git a/src/pages/Worker/Card/WorkerCalendarItem.vue b/src/pages/Worker/Card/WorkerCalendarItem.vue index 893a81c6d..86d227ad3 100644 --- a/src/pages/Worker/Card/WorkerCalendarItem.vue +++ b/src/pages/Worker/Card/WorkerCalendarItem.vue @@ -79,7 +79,7 @@ const editEvent = async (event) => { }; const { data } = await axios.patch( `Workers/${route.params.id}/updateAbsence`, - params + params, ); if (data) emit('refresh'); @@ -108,14 +108,14 @@ const handleDateSelected = (date) => { if (!event) createEvent(_date); }; -const handleEventSelected = (event, { year, month, day }) => { +const handleEventSelected = async (event, { year, month, day }) => { if (!props.absenceType) { notify(t('Choose an absence type from the right menu'), 'warning'); return; } const date = new Date(year, month - 1, day); - if (!event?.absenceId) createEvent(date); + if (!event?.absenceId) await createEvent(date); else if (event.type == props.absenceType.code) deleteEvent(event, date); else editEvent(event); };