<script setup> import { onMounted, ref, onUnmounted, nextTick, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import FetchedTags from 'components/ui/FetchedTags.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import EditTableCellValueForm from 'src/components/EditTableCellValueForm.vue'; import ItemFixedPriceFilter from './ItemFixedPriceFilter.vue'; import { useQuasar } from 'quasar'; import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue'; import { tMobile } from 'src/composables/tMobile'; import VnConfirm from 'components/ui/VnConfirm.vue'; import FetchData from 'src/components/FetchData.vue'; import { useStateStore } from 'stores/useStateStore'; import { toDate } from 'src/filters'; import { useVnConfirm } from 'composables/useVnConfirm'; import { useState } from 'src/composables/useState'; import useNotify from 'src/composables/useNotify.js'; import axios from 'axios'; import { isLower, isBigger } from 'src/filters/date.js'; import RightMenu from 'src/components/common/RightMenu.vue'; import VnTable from 'src/components/VnTable/VnTable.vue'; import { QCheckbox } from 'quasar'; const quasar = useQuasar(); const stateStore = useStateStore(); const { t } = useI18n(); const { openConfirmationModal } = useVnConfirm(); const state = useState(); const { notify } = useNotify(); const tableRef = ref(); const editTableCellDialogRef = ref(null); const user = state.getUser(); const fixedPrices = ref([]); const warehousesOptions = ref([]); const rowsSelected = ref([]); const itemFixedPriceFilterRef = ref(); onMounted(async () => { stateStore.rightDrawer = true; }); onUnmounted(() => (stateStore.rightDrawer = false)); const defaultColumnAttrs = { align: 'left', sortable: true, }; const columns = computed(() => [ { label: t('item.fixedPrice.itemFk'), name: 'itemFk', ...defaultColumnAttrs, isId: true, columnField: { component: 'input', type: 'number', }, columnClass: 'shrink', }, { label: t('globals.name'), name: 'name', ...defaultColumnAttrs, create: true, }, { label: t('item.fixedPrice.groupingPrice'), field: 'rate2', name: 'rate2', ...defaultColumnAttrs, component: 'input', type: 'number', }, { label: t('item.fixedPrice.packingPrice'), field: 'rate3', name: 'rate3', ...defaultColumnAttrs, component: 'input', type: 'number', }, { label: t('item.fixedPrice.minPrice'), field: 'minPrice', name: 'minPrice', ...defaultColumnAttrs, component: 'input', type: 'number', }, { label: t('item.fixedPrice.started'), field: 'started', name: 'started', format: ({ started }) => toDate(started), ...defaultColumnAttrs, columnField: { component: 'date', class: 'shrink', }, columnFilter: { component: 'date', }, columnClass: 'expand', }, { label: t('item.fixedPrice.ended'), field: 'ended', name: 'ended', ...defaultColumnAttrs, columnField: { component: 'date', class: 'shrink', }, columnFilter: { component: 'date', }, columnClass: 'expand', format: (row) => toDate(row.ended), }, { label: t('globals.warehouse'), field: 'warehouseFk', name: 'warehouseFk', ...defaultColumnAttrs, columnClass: 'shrink', component: 'select', options: warehousesOptions, columnFilter: { name: 'warehouseFk', inWhere: true, component: 'select', attrs: { options: warehousesOptions, 'option-label': 'name', 'option-value': 'id', }, }, }, { align: 'right', name: 'tableActions', actions: [ { title: t('delete'), icon: 'delete', action: (row) => confirmRemove(row), isPrimary: true, }, ], }, ]); const editTableFieldsOptions = [ { field: 'rate2', label: t('item.fixedPrice.groupingPrice'), component: 'input', attrs: { type: 'number', }, }, { field: 'rate3', label: t('item.fixedPrice.packingPrice'), component: 'input', attrs: { type: 'number', }, }, { field: 'minPrice', label: t('item.fixedPrice.minPrice'), component: 'input', attrs: { type: 'number', }, }, { field: 'started', label: t('item.fixedPrice.started'), component: 'date', }, { field: 'ended', label: t('item.fixedPrice.ended'), component: 'date', }, { field: 'warehouseFk', label: t('globals.warehouse'), component: 'select', attrs: { options: [], 'option-label': 'name', 'option-value': 'id', }, }, ]; const getRowUpdateInputEvents = (props, resetMinPrice, inputType = 'text') => { return inputType === 'text' ? { 'keyup.enter': () => upsertPrice(props, resetMinPrice), blur: () => upsertPrice(props, resetMinPrice), } : { 'update:modelValue': () => upsertPrice(props, resetMinPrice) }; }; const updateMinPrice = async (value, props) => { props.row.hasMinPrice = value; await upsertPrice({ row: props.row, col: { field: 'hasMinPrice' }, rowIndex: props.rowIndex, }); }; const validations = ({ row }) => { const requiredFields = [ 'itemFk', 'started', 'ended', 'rate2', 'rate3', 'warehouseFk', ]; const isValid = requiredFields.every( (field) => row[field] !== null && row[field] !== undefined ); return isValid; }; const upsertPrice = async (props, resetMinPrice = false) => { const isValid = validations({ ...props }); if (!isValid) { return; } const { row } = props; const changes = tableRef.value.CrudModelRef.getChanges(); if (changes?.updates?.length > 0) { if (resetMinPrice) row.hasMinPrice = 0; } if (!changes.updates && !changes.creates) return; const data = await upsertFixedPrice(row); Object.assign(tableRef.value.CrudModelRef.formData[props.rowIndex], data); notify(t('globals.dataSaved'), 'positive'); tableRef.value.reload(); }; async function upsertFixedPrice(row) { const { data } = await axios.patch('FixedPrices/upsertFixedPrice', row); data.hasMinPrice = data.hasMinPrice ? 1 : 0; return data; } function checkLastVisibleRow() { let lastVisibleRow = null; getTableRows().forEach((row, index) => { const rect = row.getBoundingClientRect(); if (rect.top >= 0 && rect.bottom <= window.innerHeight) { lastVisibleRow = index; } }); return lastVisibleRow; } const addRow = (original = null) => { let copy = null; const today = Date.vnNew(); const millisecsInDay = 86400000; const daysInWeek = 7; const nextWeek = new Date(today.getTime() + daysInWeek * millisecsInDay); copy = { id: 0, started: today, ended: nextWeek, hasMinPrice: 0, $index: 0, }; return { original, copy }; }; const getTableRows = () => document.getElementsByClassName('q-table')[0].querySelectorAll('tr.cursor-pointer'); function highlightNewRow({ $index: index }) { const row = getTableRows()[index]; if (row) { row.classList.add('highlight'); setTimeout(() => { row.classList.remove('highlight'); }, 3000); } } const openEditTableCellDialog = () => { editTableCellDialogRef.value.show(); }; const onEditCellDataSaved = async () => { rowsSelected.value = []; tableRef.value.reload(); }; const removeFuturePrice = async () => { rowsSelected.value.forEach(({ id }) => { const rowIndex = fixedPrices.value.findIndex(({ id }) => id === id); removePrice(id, rowIndex); }); }; function confirmRemove(item, isFuture) { const promise = async () => isFuture ? removeFuturePrice(item.id) : removePrice(item.id); quasar.dialog({ component: VnConfirm, componentProps: { title: t('globals.rowWillBeRemoved'), message: t('globals.confirmDeletion'), promise, }, }); } const removePrice = async (id) => { await axios.delete(`FixedPrices/${id}`); notify(t('globals.dataSaved'), 'positive'); tableRef.value.reload({}); }; const dateStyle = (date) => date ? { 'bg-color': 'warning', 'is-outlined': true, } : {}; function handleOnDataSave({ CrudModelRef }) { const { original, copy } = addRow(CrudModelRef.formData[checkLastVisibleRow()]); if (original) { CrudModelRef.formData.splice(original?.$index ?? 0, 0, copy); } else { CrudModelRef.insert(copy); } nextTick(() => { highlightNewRow(original ?? { $index: 0 }); }); } </script> <template> <FetchData @on-fetch="(data) => (warehousesOptions = data)" auto-load url="Warehouses" :filter="{ fields: ['id', 'name'], order: 'name ASC' }" /> <RightMenu> <template #right-panel> <ItemFixedPriceFilter data-key="ItemFixedPrices" ref="itemFixedPriceFilterRef" /> </template> </RightMenu> <VnSubToolbar> <template #st-data> <QBtn v-if="rowsSelected.length" @click="openEditTableCellDialog()" color="primary" icon="edit" > <QTooltip> {{ t('Edit fixed price(s)') }} </QTooltip> </QBtn> <QBtn :label="tMobile('globals.remove')" color="primary" icon="delete" flat @click="(row) => confirmRemove(row, true)" :title="t('globals.remove')" v-if="rowsSelected.length" /> </template> </VnSubToolbar> <VnTable :default-remove="false" :default-reset="false" :default-save="false" data-key="ItemFixedPrices" url="FixedPrices/filter" :order="['name DESC', 'itemFk DESC']" save-url="FixedPrices/crud" ref="tableRef" dense :filter="{ where: { warehouseFk: user.warehouseFk, }, }" :columns="columns" default-mode="table" auto-load :is-editable="true" :right-search="false" :table="{ 'row-key': 'id', selection: 'multiple', }" :use-model="true" v-model:selected="rowsSelected" :create-as-dialog="false" :create="{ onDataSaved: handleOnDataSave, }" :disable-option="{ card: true }" > <template #header-selection="scope"> <QCheckbox v-model="scope.selected" /> </template> <template #body-selection="scope"> {{ scope }} <QCheckbox flat v-model="scope.selected" /> </template> <template #column-itemFk="props"> <VnSelect style="max-width: 100px" url="Items/withName" hide-selected option-label="id" option-value="id" v-model="props.row.itemFk" v-on="getRowUpdateInputEvents(props, true, 'select')" > <template #option="scope"> <QItem v-bind="scope.itemProps"> <QItemSection> <QItemLabel> #{{ scope.opt?.id }} </QItemLabel> <QItemLabel caption>{{ scope.opt?.name }}</QItemLabel> </QItemSection> </QItem> </template> </VnSelect> </template> <template #column-name="{ row }"> <span class="link"> {{ row.name }} </span> <span class="subName">{{ row.subName }}</span> <ItemDescriptorProxy :id="row.itemFk" /> <FetchedTags :item="row" :columns="3" /> </template> <template #column-rate2="props"> <QTd class="col"> <VnInput type="currency" style="width: 75px" v-model.number="props.row.rate2" v-on="getRowUpdateInputEvents(props)" > <template #append>€</template> </VnInput> </QTd> </template> <template #column-rate3="props"> <QTd class="col"> <VnInput style="width: 75px" type="currency" v-model.number="props.row.rate3" v-on="getRowUpdateInputEvents(props)" > <template #append>€</template> </VnInput> </QTd> </template> <template #column-minPrice="props"> <QTd class="col"> <div class="row" style="align-items: center"> <QCheckbox :model-value="props.row.hasMinPrice" @update:model-value="updateMinPrice($event, props)" :false-value="0" :true-value="1" :toggle-indeterminate="false" /> <VnInput class="col" type="currency" mask="###.##" :disable="props.row.hasMinPrice === 0" v-model.number="props.row.minPrice" v-on="getRowUpdateInputEvents(props)" > <template #append>€</template> </VnInput> </div> </QTd> </template> <template #column-started="props"> <VnInputDate class="vnInputDate" :show-event="true" v-model="props.row.started" v-on="getRowUpdateInputEvents(props, false, 'date')" v-bind="dateStyle(isBigger(props.row.started))" /> </template> <template #column-ended="props"> <VnInputDate class="vnInputDate" :show-event="true" v-model="props.row.ended" v-on="getRowUpdateInputEvents(props, false, 'date')" v-bind="dateStyle(isLower(props.row.ended))" /> </template> <template #column-warehouseFk="props"> <QTd class="col"> <VnSelect style="max-width: 150px" :options="warehousesOptions" hide-selected option-label="name" option-value="id" v-model="props.row.warehouseFk" v-on="getRowUpdateInputEvents(props, false, 'select')" /> </QTd> </template> <template #column-deleteAction="{ row, rowIndex }"> <QIcon name="delete" size="sm" class="cursor-pointer fill-icon-on-hover" color="primary" @click.stop=" openConfirmationModal( t('globals.rowWillBeRemoved'), t('Do you want to clone this item?'), () => removePrice(row.id, rowIndex) ) " > <QTooltip class="text-no-wrap"> {{ t('globals.delete') }} </QTooltip> </QIcon> </template> </VnTable> <QDialog ref="editTableCellDialogRef"> <EditTableCellValueForm edit-url="FixedPrices/editFixedPrice" :rows="rowsSelected" :fields-options="editTableFieldsOptions" @on-data-saved="onEditCellDataSaved()" /> </QDialog> </template> <style lang="scss"> .q-table th, .q-table td { padding-inline: 5px !important; } .q-table tr td { font-size: 10pt; border-top: none; border-collapse: collapse; } .q-table tbody td { max-width: none; .q-td.col { & .vnInputDate { min-width: 90px; } & div.row { & .q-checkbox { & .q-checkbox__inner { position: relative !important; &.q-checkbox__inner--truthy { color: var(--q-primary); } } } } } } .q-field__after, .q-field__append { padding: 0; } tbody tr.highlight .q-td { animation: highlight-animation 4s ease-in-out; } @keyframes highlight-animation { 0% { background-color: $primary-light; } 100% { background-color: transparent; } } .subName { margin-left: 5%; font-size: 0.75rem; text-transform: uppercase; color: var(--vn-label-color); } </style> <i18n> es: Add fixed price: Añadir precio fijado Edit fixed price(s): Editar precio(s) fijado(s) </i18n>