0
0
Fork 0
This commit is contained in:
Alex Moreno 2024-12-09 14:17:41 +01:00
commit cd4b2a92dc
17 changed files with 222 additions and 121 deletions

View File

@ -249,7 +249,7 @@ function getChanges() {
for (const [i, row] of formData.value.entries()) {
if (!row[pk]) {
creates.push(row);
} else if (originalData.value) {
} else if (originalData.value[i]) {
const data = getDifferences(originalData.value[i], row);
if (!isEmpty(data)) {
updates.push({

View File

@ -85,12 +85,14 @@ const closeForm = () => {
hide-selected
option-label="label"
v-model="selectedField"
data-cy="field-to-edit"
/>
<component
:is="inputs[selectedField?.component || 'input']"
v-bind="selectedField?.attrs || {}"
v-model="newValue"
:label="t('Value')"
data-cy="value-to-edit"
style="width: 200px"
/>
</VnRow>

View File

@ -9,6 +9,8 @@ import VnSelect from 'components/common/VnSelect.vue';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
import axios from 'axios';
import { getParamWhere } from 'src/filters';
import { useRoute } from 'vue-router';
const { t } = useI18n();
const props = defineProps({
@ -26,28 +28,21 @@ const props = defineProps({
},
});
const itemCategories = ref([]);
const selectedCategoryFk = ref(null);
const selectedTypeFk = ref(null);
const route = useRoute();
const itemTypesOptions = ref([]);
const suppliersOptions = ref([]);
const tagOptions = ref([]);
const tagValues = ref([]);
const categoryList = ref(null);
const selectedCategoryFk = ref(getParamWhere(route.query.table, 'categoryFk', false));
const selectedTypeFk = ref(getParamWhere(route.query.table, 'typeFk', false));
const categoryList = computed(() => {
return (itemCategories.value || [])
.filter((category) => category.display)
.map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
});
const selectedCategory = computed(() =>
(itemCategories.value || []).find(
const selectedCategory = computed(() => {
return (categoryList.value || []).find(
(category) => category?.id === selectedCategoryFk.value
)
);
);
});
const selectedType = computed(() => {
return (itemTypesOptions.value || []).find(
@ -87,7 +82,7 @@ const applyTags = (params, search) => {
search();
};
const fetchItemTypes = async (id) => {
const fetchItemTypes = async (id = selectedCategoryFk.value) => {
const filter = {
fields: ['id', 'name', 'categoryFk'],
where: { categoryFk: id },
@ -126,15 +121,19 @@ const removeTag = (index, params, search) => {
(tagValues.value || []).splice(index, 1);
applyTags(params, search);
};
const setCategoryList = (data) => {
categoryList.value = (data || [])
.filter((category) => category.display)
.map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
fetchItemTypes();
};
</script>
<template>
<FetchData
url="ItemCategories"
limit="30"
auto-load
@on-fetch="(data) => (itemCategories = data)"
/>
<FetchData url="ItemCategories" limit="30" auto-load @on-fetch="setCategoryList" />
<FetchData
url="Suppliers"
limit="30"

View File

@ -67,7 +67,7 @@ const dialog = ref(null);
<QTooltip>{{ t('globals.add') }}</QTooltip>
<QPopupProxy ref="dialog">
<OrderCatalogItemDialog
:prices="item.prices"
:item="item"
@added="() => dialog.hide()"
/>
</QPopupProxy>

View File

@ -6,7 +6,6 @@ import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { toDateHourMin } from 'src/filters';
import { useState } from 'src/composables/useState';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnUserLink from 'components/ui/VnUserLink.vue';
@ -26,9 +25,7 @@ const $props = defineProps({
});
const { t } = useI18n();
const state = useState();
const quasar = useQuasar();
const currentUser = ref(state.getUser());
const newNote = reactive({ text: null, observationTypeFk: null });
const observationTypes = ref([]);
const vnPaginateRef = ref();

View File

@ -20,7 +20,7 @@ export function useRole() {
function hasAny(roles) {
const roleStore = state.getRoles();
if (typeof roles === 'string') roles = [roles];
for (const role of roles) {
if (roleStore.value.indexOf(role) !== -1) return true;
}

View File

@ -1,4 +1,3 @@
// parsing JSON safely
function parseJSON(str, fallback) {
try {
return JSON.parse(str ?? '{}');

View File

@ -1,12 +1,11 @@
<script setup>
import { computed, ref, onMounted } from 'vue';
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnUserLink from 'src/components/ui/VnUserLink.vue';
import { toCurrency, toPercentage, toDate, dashOrCurrency } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue';
import { getUrl } from 'src/composables/getUrl';
import VnLv from 'src/components/ui/VnLv.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import VnLinkMail from 'src/components/ui/VnLinkMail.vue';

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, reactive, computed } from 'vue';
import { ref, reactive, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';

View File

@ -1,5 +1,5 @@
<script setup>
import { onMounted, computed, onUnmounted, reactive, ref, nextTick, watch } from 'vue';
import { onMounted, computed, reactive, ref, nextTick, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
@ -12,20 +12,18 @@ import FetchData from 'components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDateFormat } from 'src/filters/date.js';
import { dashIfEmpty } from 'src/filters';
import { date } from 'quasar';
import { useState } from 'src/composables/useState';
import { useArrayData } from 'src/composables/useArrayData';
import axios from 'axios';
import VnSubToolbar from 'components/ui/VnSubToolbar.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const stateStore = useStateStore();
const state = useState();
const user = state.getUser();
const today = ref(Date.vnNew());
const warehousesOptions = ref([]);
@ -145,8 +143,6 @@ onMounted(async () => {
await updateWarehouse(warehouseFk.value);
});
onUnmounted(() => (stateStore.rightDrawer = false));
watch(
() => router.currentRoute.value.params.id,
(newId) => {
@ -205,8 +201,8 @@ async function updateWarehouse(warehouseFk) {
auto-load
@on-fetch="(data) => (warehousesOptions = data)"
/>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#st-data">
<VnSubToolbar class="q-mb-md">
<template #st-data>
<div class="row">
<VnSelect
:label="t('itemDiary.warehouse')"
@ -235,9 +231,8 @@ async function updateWarehouse(warehouseFk) {
@update:model-value="fetchItemBalances"
/>
</div>
</Teleport>
<Teleport to="#st-actions"> </Teleport>
</template>
</template>
</VnSubToolbar>
<QPage class="column items-center q-pa-md">
<QTable
:rows="itemBalances"

View File

@ -1,5 +1,5 @@
<script setup>
import { onMounted, ref, reactive, onUnmounted, nextTick, computed } from 'vue';
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';
@ -37,11 +37,9 @@ const fixedPrices = ref([]);
const warehousesOptions = ref([]);
const rowsSelected = ref([]);
const itemFixedPriceFilterRef = ref();
const params = reactive({});
onMounted(async () => {
stateStore.rightDrawer = true;
params.warehouseFk = user.value.warehouseFk;
});
onUnmounted(() => (stateStore.rightDrawer = false));
@ -137,8 +135,17 @@ const columns = computed(() => [
...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',
@ -210,8 +217,6 @@ const getRowUpdateInputEvents = (props, resetMinPrice, inputType = 'text') => {
};
const updateMinPrice = async (value, props) => {
// El checkbox hasMinPrice se encuentra en la misma columna que el input hasMinPrice
// Por lo tanto le mandamos otro objeto con las mismas propiedades pero con el campo 'field' cambiado
props.row.hasMinPrice = value;
await upsertPrice({
row: props.row,
@ -220,12 +225,33 @@ const updateMinPrice = async (value, props) => {
});
};
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 { row } = props;
if (tableRef.value.CrudModelRef.getChanges().updates.length > 0) {
if (resetMinPrice) row.hasMinPrice = 0;
await upsertFixedPrice(row);
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);
tableRef.value.CrudModelRef.formData[props.rowIndex] = data;
};
async function upsertFixedPrice(row) {
@ -233,13 +259,6 @@ async function upsertFixedPrice(row) {
return data;
}
async function saveOnRowChange(row) {
if (rowsSelected.value.length > 1) return;
if (rowsSelected.value[0]?.id === row.id) return;
else if (rowsSelected.value.length === 1) await upsertPrice(rowsSelected.value[0]);
rowsSelected.value = [row];
}
function checkLastVisibleRow() {
let lastVisibleRow = null;
@ -255,39 +274,18 @@ function checkLastVisibleRow() {
const addRow = (original = null) => {
let copy = null;
if (!original) {
const today = Date.vnNew();
const millisecsInDay = 86400000;
const daysInWeek = 7;
const nextWeek = new Date(today.getTime() + daysInWeek * millisecsInDay);
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,
};
} else
copy = {
$index: original.$index - 1,
itemFk: original.itemFk,
name: original.name,
subName: original.subName,
value5: original.value5,
value6: original.value6,
value7: original.value7,
value8: original.value8,
value9: original.value9,
value10: original.value10,
warehouseFk: original.warehouseFk,
rate2: original.rate2,
rate3: original.rate3,
hasMinPrice: original.hasMinPrice,
minPrice: original.minPrice,
started: Date.vnNew(),
ended: Date.vnNew(),
};
copy = {
id: 0,
started: today,
ended: nextWeek,
hasMinPrice: 0,
$index: 0,
};
return { original, copy };
};
@ -300,7 +298,7 @@ function highlightNewRow({ $index: index }) {
row.classList.add('highlight');
setTimeout(() => {
row.classList.remove('highlight');
}, 3000); // Duración de la animación en milisegundos
}, 3000);
}
}
const openEditTableCellDialog = () => {
@ -411,9 +409,13 @@ function handleOnDataSave({ CrudModelRef }) {
url="FixedPrices/filter"
:order="['itemFk DESC', 'name DESC']"
save-url="FixedPrices/crud"
:user-params="{ warehouseFk: user.warehouseFk }"
ref="tableRef"
dense
:filter="{
where: {
warehouseFk: user.warehouseFk,
},
}"
:columns="columns"
default-mode="table"
auto-load
@ -427,7 +429,6 @@ function handleOnDataSave({ CrudModelRef }) {
disableInfiniteScroll: true,
}"
v-model:selected="rowsSelected"
:row-click="saveOnRowChange"
:create-as-dialog="false"
:create="{
onDataSaved: handleOnDataSave,

View File

@ -1,7 +1,7 @@
<script setup>
import { useStateStore } from 'stores/useStateStore';
import { useRoute, useRouter } from 'vue-router';
import { onMounted, onUnmounted, ref, computed, watch } from 'vue';
import { onMounted, onUnmounted, ref, computed, watch, provide, nextTick } from 'vue';
import axios from 'axios';
import { useI18n } from 'vue-i18n';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
@ -18,6 +18,7 @@ const dataKey = 'OrderCatalogList';
const arrayData = useArrayData(dataKey);
const store = arrayData.store;
const tags = ref([]);
const itemRefs = ref({});
let catalogParams = {
orderFk: route.params.id,
@ -76,6 +77,19 @@ watch(
},
{ immediate: true }
);
const onItemSaved = (updatedItem) => {
requestAnimationFrame(() => {
scrollToItem(updatedItem.items[0].itemFk);
});
};
const scrollToItem = async (id) => {
const element = itemRefs.value[id]?.$el;
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
};
provide('onItemSaved', onItemSaved);
</script>
<template>
@ -115,6 +129,7 @@ watch(
<CatalogItem
v-for="row in rows"
:key="row.id"
:ref="(el) => (itemRefs[row.id] = el)"
:item="row"
is-catalog
class="fill-icon"

View File

@ -1,41 +1,53 @@
<script setup>
import toCurrency from '../../../filters/toCurrency';
import { ref } from 'vue';
import toCurrency from 'src/filters/toCurrency';
import { inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useRoute } from 'vue-router';
import useNotify from 'composables/useNotify';
import { useArrayData } from 'composables/useArrayData';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n();
const { notify } = useNotify();
const emit = defineEmits(['added']);
const route = useRoute();
const props = defineProps({
prices: {
item: {
type: Array,
required: true,
},
});
const fields = ref((props.prices || []).map((item) => ({ ...item, quantity: 0 })));
const onItemSaved = inject('onItemSaved');
const prices = ref((props.item.prices || []).map((item) => ({ ...item, quantity: 0 })));
const descriptorData = useArrayData('orderData');
const isLoading = ref(false);
const addToOrder = async () => {
if (isLoading.value) return;
isLoading.value = true;
const items = (fields.value || []).filter((item) => Number(item.quantity) > 0);
const items = (prices.value || []).filter((item) => Number(item.quantity) > 0);
await axios.post('/OrderRows/addToOrder', {
items,
orderFk: Number(route.params.id),
});
notify(t('globals.dataSaved'), 'positive');
emit('added');
descriptorData.fetch({});
await descriptorData.fetch({});
onItemSaved({ ...props, items, saved: true });
emit('added', items);
isLoading.value = false;
};
const canAddToOrder = () => {
return (fields.value || []).some((item) => Number(item.quantity) > 0);
let canAddToOrder = (prices.value || []).some((price) => Number(price.quantity) > 0);
if (canAddToOrder) {
const excedQuantity = prices.value.reduce(
(acc, { quantity }) => acc + quantity,
0
);
if (excedQuantity > props.item.available) {
canAddToOrder = false;
}
}
return canAddToOrder;
};
</script>
@ -44,30 +56,33 @@ const canAddToOrder = () => {
<QForm @submit="addToOrder">
<QMarkupTable class="shadow-0">
<tbody>
<tr v-for="item in fields" :key="item.warehouse">
<tr v-for="price in prices" :key="price.warehouse">
<td class="text-bold q-pr-md td" style="width: 35%">
{{ item.warehouse }}
{{ price.warehouse }}
</td>
<td class="text-right" style="width: 35%">
<span
class="link"
@click="
@click.shift="
() => {
item.quantity += item.grouping;
price.quantity -= price.grouping;
}
"
@click.exact="
() => {
price.quantity += price.grouping;
}
"
>
{{ item.grouping }}
{{ price.grouping }}
</span>
x {{ toCurrency(item.price) }}
x {{ toCurrency(price.price) }}
</td>
<td class="text-right">
<QInput
v-model.number="item.quantity"
type="number"
:step="item.grouping"
<VnInputNumber
v-model.number="price.quantity"
:step="price.grouping"
min="0"
dense
/>
</td>
</tr>

View File

@ -11,7 +11,7 @@ import VnInput from 'src/components/common/VnInput.vue';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
import { toDateFormat } from 'src/filters/date';
import { useRole } from 'src/composables/useRole';
import { useAcl } from 'src/composables/useAcl';
import { useVnConfirm } from 'composables/useVnConfirm';
const emit = defineEmits(['updateDiscounts', 'getMana', 'refreshTable']);
@ -48,7 +48,7 @@ const { push } = useRouter();
const { t } = useI18n();
const { dialog } = useQuasar();
const { notify } = useNotify();
const role = useRole();
const acl = useAcl();
const btnDropdownRef = ref(null);
const { openConfirmationModal } = useVnConfirm();
@ -58,8 +58,10 @@ const isClaimable = computed(() => {
if (ticket.value) {
const landedPlusWeek = new Date(ticket.value.landed);
landedPlusWeek.setDate(landedPlusWeek.getDate() + 7);
const hasClaimManagerRole = role.hasAny('claimManager');
return landedPlusWeek >= Date.vnNew() || hasClaimManagerRole;
const createAfterDeadline = acl.hasAny([
{ model: 'Claim', props: 'createAfterDeadline', accessType: 'WRITE' },
]);
return landedPlusWeek >= Date.vnNew() || createAfterDeadline;
}
return false;
});

View File

@ -15,6 +15,9 @@ const columns = computed(() => [
name: 'paymentDate',
label: t('worker.balance.tableVisibleColumns.paymentDate'),
create: true,
columnCreate: {
required: true,
},
component: 'date',
field: 'paymentDate',
cardVisible: true,
@ -24,6 +27,9 @@ const columns = computed(() => [
name: 'incomeTypeFk',
label: t('worker.balance.tableVisibleColumns.incomeType'),
create: true,
columnCreate: {
required: true,
},
component: 'select',
attrs: {
options: payrollComponents,
@ -37,6 +43,9 @@ const columns = computed(() => [
name: 'debit',
label: t('worker.balance.tableVisibleColumns.debit'),
create: true,
columnCreate: {
required: true,
},
component: 'input',
field: 'debit',
cardVisible: true,
@ -46,6 +55,9 @@ const columns = computed(() => [
name: 'credit',
label: t('worker.balance.tableVisibleColumns.credit'),
create: true,
columnCreate: {
required: true,
},
component: 'input',
field: 'credit',
cardVisible: true,

View File

@ -206,6 +206,8 @@ const handlePhotoUpdated = (evt = false) => {
<i18n>
es:
Go to client: Ir a cliente
Go to user: Ir al usuario
Click to allow the user to be disabled: Marcar para deshabilitar
Click to exclude the user from getting disabled: Marcar para no deshabilitar
</i18n>

View File

@ -0,0 +1,63 @@
/// <reference types="cypress" />
function goTo(n = 1) {
return `.q-virtual-scroll__content > :nth-child(${n})`;
}
const firstRow = goTo();
`.q-virtual-scroll__content > :nth-child(2)`;
describe('Handle Items FixedPrice', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('/#/item/fixed-price', { timeout: 5000 });
cy.waitForElement('.q-table');
cy.get(
'.q-header > .q-toolbar > :nth-child(1) > .q-btn__content > .q-icon'
).click();
});
it('filter', function () {
cy.get('.category-filter > :nth-child(1) > .q-btn__content > .q-icon').click();
cy.selectOption('.list > :nth-child(2)', 'Alstroemeria');
cy.get('.q-gutter-x-sm > .q-btn > .q-btn__content > .q-icon').click();
cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click();
cy.selectOption(`${firstRow} > :nth-child(2)`, '#13');
cy.get(`${firstRow} > :nth-child(4)`).find('input').type(1);
cy.get(`${firstRow} > :nth-child(5)`).find('input').type('2');
cy.selectOption(`${firstRow} > :nth-child(9)`, 'Warehouse One');
cy.get('.q-notification__message').should('have.text', 'Data saved');
/* ==== End Cypress Studio ==== */
});
it('Create and delete ', function () {
cy.get('.q-gutter-x-sm > .q-btn > .q-btn__content > .q-icon').click();
cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click();
cy.selectOption(`${firstRow} > :nth-child(2)`, '#11');
cy.get(`${firstRow} > :nth-child(4)`).type('1');
cy.get(`${firstRow} > :nth-child(5)`).type('2');
cy.selectOption(`${firstRow} > :nth-child(9)`, 'Warehouse One');
cy.get('.q-notification__message').should('have.text', 'Data saved');
cy.get('.q-gutter-x-sm > .q-btn > .q-btn__content > .q-icon').click();
cy.get(`${firstRow} > .text-right > .q-btn > .q-btn__content > .q-icon`).click();
cy.get(
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block'
).click();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
it('Massive edit', function () {
cy.get(' .bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner ').click();
cy.get('#subToolbar > .q-btn--standard').click();
cy.selectOption("[data-cy='field-to-edit']", 'Min price');
cy.dataCy('value-to-edit').find('input').type('1');
cy.get('.countLines').should('have.text', ' 1 ');
cy.get('.q-mt-lg > .q-btn--standard').click();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
it('Massive remove', function () {
cy.get(' .bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner ').click();
cy.get('#subToolbar > .q-btn--flat').click();
cy.get(
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block'
).click();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
});