#8363: Modified ItemFixedPrice #1561

Merged
jon merged 48 commits from 8363-RefactorItemFixedPrice into dev 2025-04-10 10:01:33 +00:00
16 changed files with 474 additions and 641 deletions

View File

@ -332,6 +332,7 @@ watch(formUrl, async () => {
:disable="!selected?.length"
:title="t('globals.remove')"
v-if="$props.defaultRemove"
data-cy="crudModelDefaultRemoveBtn"
/>
<QBtn
:label="tMobile('globals.reset')"

View File

@ -229,6 +229,7 @@ watch(
defineExpose({
create: createForm,
showForm,
reload,
redirect: redirectFn,
selected,

View File

@ -1,57 +0,0 @@
import { createWrapper } from 'app/test/vitest/helper';
import { default as axios } from 'axios';
import EditForm from 'components/EditTableCellValueForm.vue';
import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest';
const fieldA = 'fieldA';
const fieldB = 'fieldB';
describe('EditForm', () => {
let vm;
const mockRows = [
{ id: 1, itemFk: 101 },
{ id: 2, itemFk: 102 },
];
const mockFieldsOptions = [
{ label: 'Field A', field: fieldA, component: 'input', attrs: {} },
{ label: 'Field B', field: fieldB, component: 'date', attrs: {} },
];
const editUrl = '/api/edit';
beforeAll(() => {
vi.spyOn(axios, 'post').mockResolvedValue({ status: 200 });
vm = createWrapper(EditForm, {
props: {
rows: mockRows,
fieldsOptions: mockFieldsOptions,
editUrl,
},
}).vm;
});
afterEach(() => {
vi.clearAllMocks();
});
describe('onSubmit()', () => {
it('should call axios.post with the correct parameters in the payload', async () => {
const selectedField = { field: fieldA, component: 'input', attrs: {} };
const newValue = 'Test Value';
vm.selectedField = selectedField;
vm.newValue = newValue;
await vm.onSubmit();
const payload = axios.post.mock.calls[0][1];
expect(axios.post).toHaveBeenCalledWith(editUrl, expect.any(Object));
expect(payload.field).toEqual(fieldA);
expect(payload.newValue).toEqual(newValue);
expect(payload.lines).toEqual(expect.arrayContaining(mockRows));
expect(vm.isLoading).toEqual(false);
});
});
});

View File

@ -1,4 +1,6 @@
<script setup>
import { computed } from 'vue';
const $props = defineProps({
colors: {
type: String,
@ -6,9 +8,9 @@ const $props = defineProps({
},
});
const colorArray = JSON.parse($props.colors)?.value;
const colorArray = computed(() => JSON.parse($props.colors)?.value);
const maxHeight = 30;
const colorHeight = maxHeight / colorArray?.length;
const colorHeight = maxHeight / colorArray.value?.length;
</script>
<template>
<div v-if="colors" class="color-div" :style="{ height: `${maxHeight}px` }">

View File

@ -0,0 +1,51 @@
import axios from 'axios';
export async function beforeSave(data, getChanges, modelOrigin) {
try {
const changes = data.updates;
if (!changes) return data;
const patchPromises = [];
for (const change of changes) {
let patchData = {};
if ('hasMinPrice' in change.data) {
patchData.hasMinPrice = change.data?.hasMinPrice;
delete change.data.hasMinPrice;
}
if ('minPrice' in change.data) {
patchData.minPrice = change.data?.minPrice;
delete change.data.minPrice;
}
if (Object.keys(patchData).length > 0) {
const promise = axios
.get(`${modelOrigin}/findOne`, {
params: {
filter: {
fields: ['itemFk'],
where: { id: change.where.id },
},
},
})
.then((row) => {
return axios.patch(`Items/${row.data.itemFk}`, patchData);
})
.catch((error) => {
console.error('Error processing change: ', change, error);
});
patchPromises.push(promise);
}
}
await Promise.all(patchPromises);
data.updates = changes.filter((change) => Object.keys(change.data).length > 0);
return data;
} catch (error) {
console.error('Error in beforeSave:', error);
throw error;
}
}

View File

@ -877,6 +877,11 @@ components:
active: Is active
floramondo: Is floramondo
showBadDates: Show future items
name: Nombre
rate2: Grouping price
rate3: Packing price
minPrice: Min. Price
itemFk: Item id
userPanel:
copyToken: Token copied to clipboard
settings: Settings

View File

@ -961,6 +961,11 @@ components:
to: Hasta
floramondo: Floramondo
showBadDates: Ver items a futuro
name: Nombre
rate2: Precio grouping
rate3: Precio packing
minPrice: Precio mínimo
itemFk: Id item
userPanel:
copyToken: Token copiado al portapapeles
settings: Configuración

View File

@ -19,6 +19,7 @@ import { checkEntryLock } from 'src/composables/checkEntryLock';
import VnRow from 'src/components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import { beforeSave } from 'src/composables/updateMinPriceBeforeSave';
const $props = defineProps({
id: {
@ -415,56 +416,6 @@ function getAmountStyle(row) {
return { color: 'var(--vn-label-color)' };
}
async function beforeSave(data, getChanges) {
try {
const changes = data.updates;
if (!changes) return data;
const patchPromises = [];
for (const change of changes) {
let patchData = {};
if ('hasMinPrice' in change.data) {
patchData.hasMinPrice = change.data?.hasMinPrice;
delete change.data.hasMinPrice;
}
if ('minPrice' in change.data) {
patchData.minPrice = change.data?.minPrice;
delete change.data.minPrice;
}
if (Object.keys(patchData).length > 0) {
const promise = axios
.get('Buys/findOne', {
params: {
filter: {
fields: ['itemFk'],
where: { id: change.where.id },
},
},
})
.then((buy) => {
return axios.patch(`Items/${buy.data.itemFk}`, patchData);
})
.catch((error) => {
console.error('Error processing change: ', change, error);
});
patchPromises.push(promise);
}
}
await Promise.all(patchPromises);
data.updates = changes.filter((change) => Object.keys(change.data).length > 0);
return data;
} catch (error) {
console.error('Error in beforeSave:', error);
throw error;
}
}
function invertQuantitySign(rows, sign) {
for (const row of rows) {
if (sign > 0) row.quantity = Math.abs(row.quantity);
@ -697,7 +648,7 @@ onMounted(() => {
:right-search="false"
:row-click="false"
:columns="columns"
:beforeSaveFn="beforeSave"
:beforeSaveFn="(data, getChanges) => beforeSave(data, getChanges, 'Buys')"
class="buyList"
:table-height="$props.tableHeight ?? '84vh'"
auto-load

View File

@ -1,574 +1,386 @@
<script setup>
import { onMounted, ref, onUnmounted, nextTick, computed } from 'vue';
import { onMounted, ref, onUnmounted, computed, watch } 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 { beforeSave } from 'src/composables/updateMinPriceBeforeSave';
import FetchedTags from 'components/ui/FetchedTags.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import EditFixedPriceForm from 'src/pages/Item/components/EditFixedPriceForm.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import VnTable from 'src/components/VnTable/VnTable.vue';
import { QCheckbox } from 'quasar';
import VnColor from 'src/components/common/VnColor.vue';
import { toDate } from 'src/filters';
import { isLower, isBigger } from 'src/filters/date.js';
import ItemFixedPriceFilter from './ItemFixedPriceFilter.vue';
import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue';
import { toCurrency } from 'src/filters';
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 hasSelectedRows = computed(() => rowsSelected.value.length > 0);
const rowsSelected = ref([]);
const itemFixedPriceFilterRef = ref();
const editFixedPriceForm = ref(null);
const selectedRows = ref([]);
const hasSelectedRows = computed(() => selectedRows.value.length > 0);
const isToClone = ref(false);
const dateColor = 'var(--vn-label-text-color)';
const state = useState();
const user = state.getUser().fn();
const warehouse = computed(() => user.warehouseFk);
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: {
label: t('item.fixedPrice.itemFk'),
labelAbbreviation: 'Id',
toolTip: t('item.fixedPrice.itemFk'),
component: 'input',
type: 'number',
},
columnClass: 'shrink',
},
{
label: t('globals.name'),
name: 'name',
...defaultColumnAttrs,
create: true,
columnFilter: {
component: 'select',
attrs: {
component: 'select',
url: 'Items',
fields: ['id', 'name', 'subName'],
optionLabel: 'name',
optionValue: 'name',
optionValue: 'id',
uppercase: false,
},
inWhere: true,
},
width: '55px',
isEditable: false,
},
{
labelAbbreviation: '',
name: 'hex',
columnSearch: false,
isEditable: false,
width: '9px',
pablone marked this conversation as resolved Outdated

yo pondria un input para el filtro de columna "Name" ya que así se puede aplicar un like en el back, es más cómodo para los compradores , y el select lo pondria en la columna del id

yo pondria un input para el filtro de columna "Name" ya que así se puede aplicar un like en el back, es más cómodo para los compradores , y el select lo pondria en la columna del id
component: 'select',
attrs: {
url: 'Inks',
fields: ['id', 'name'],
},
},
{
align: 'left',
label: t('globals.name'),
name: 'name',
create: true,
component: 'input',
isEditable: false,
},
{
label: t('item.fixedPrice.groupingPrice'),
labelAbbreviation: 'P. Group',
toolTip: t('item.fixedPrice.groupingPrice'),
name: 'rate2',
...defaultColumnAttrs,
component: 'input',
type: 'number',
component: 'number',
create: true,
createOrder: 3,
createAttrs: {
required: true,
},
width: '50px',
Review

Al final hablaste con Alex sobre las clases para los tamaños de las columnas

Al final hablaste con Alex sobre las clases para los tamaños de las columnas
Review

Se ha creado un redmine para ello: https://redmine.verdnatura.es/issues/8896

Se ha creado un redmine para ello: https://redmine.verdnatura.es/issues/8896
format: (row) => toCurrency(row.rate2),
},
{
label: t('item.fixedPrice.packingPrice'),
labelAbbreviation: 'P. Pack',
toolTip: t('item.fixedPrice.packingPrice'),
name: 'rate3',
...defaultColumnAttrs,
component: 'input',
type: 'number',
component: 'number',
create: true,
createOrder: 4,
createAttrs: {
required: true,
},
width: '50px',
format: (row) => toCurrency(row.rate3),
},
{
name: 'hasMinPrice',
label: t('item.fixedPrice.hasMinPrice'),
labelAbbreviation: t('item.fixedPrice.MP'),
toolTip: t('item.fixedPrice.hasMinPrice'),
label: t('item.fixedPrice.hasMinPrice'),
component: 'checkbox',
attrs: {
toggleIndeterminate: false,
},
width: '50px',
},
{
label: t('item.fixedPrice.minPrice'),
labelAbbreviation: 'Min.P',
toolTip: t('item.fixedPrice.minPrice'),
name: 'minPrice',
...defaultColumnAttrs,
component: 'input',
type: 'number',
component: 'number',
width: '50px',
style: (row) => {
if (!row?.hasMinPrice) return { color: 'var(--vn-label-color)' };
},
format: (row) => toCurrency(row.minPrice),
},
{
label: t('item.fixedPrice.started'),
field: 'started',
name: 'started',
format: ({ started }) => toDate(started),
...defaultColumnAttrs,
columnField: {
component: 'date',
class: 'shrink',
},
columnFilter: {
component: 'date',
},
columnClass: 'expand',
createAttrs: {
required: true,
},
create: true,
createOrder: 5,
width: '65px',
},
{
label: t('item.fixedPrice.ended'),
name: 'ended',
...defaultColumnAttrs,
columnField: {
component: 'date',
class: 'shrink',
},
columnFilter: {
component: 'date',
},
columnClass: 'expand',
format: (row) => toDate(row.ended),
createAttrs: {
required: true,
},
create: true,
createOrder: 6,
width: '65px',
},
{
align: 'center',
label: t('globals.warehouse'),
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',
},
url: 'Warehouses',
fields: ['id', 'name'],
optionLabel: 'name',
optionValue: 'id',
},
create: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.warehouseName),
width: '80px',
},
{
align: 'right',
name: 'tableActions',
actions: [
{
title: t('delete'),
icon: 'delete',
action: (row) => confirmRemove(row),
title: t('globals.clone'),
icon: 'vn:clone',
action: (row) => openCloneFixedPriceForm(row),
jon marked this conversation as resolved
Review

Si le das a clonar un registro cancelas y despues le das a crear tienes los valores puestos por defecto del item al que anteriormente le habías dado a clonar

Si le das a clonar un registro cancelas y despues le das a crear tienes los valores puestos por defecto del item al que anteriormente le habías dado a clonar
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 openEditFixedPriceForm = () => {
editFixedPriceForm.value.show();
};
const updateMinPrice = async (value, props) => {
props.row.hasMinPrice = value;
await upsertPrice({
row: props.row,
col: { field: 'hasMinPrice' },
rowIndex: props.rowIndex,
});
const openCloneFixedPriceForm = (row) => {
tableRef.value.showForm = true;
isToClone.value = true;
tableRef.value.create.title = t('Clone fixed price');
tableRef.value.create.formInitialData = (({
itemFk,
rate2,
rate3,
started,
ended,
warehouseFk,
}) => ({
itemFk,
rate2,
rate3,
started,
ended,
warehouseFk,
}))(JSON.parse(JSON.stringify(row)));
};
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,
color: 'var(--vn-black-text-color)',
}
: {};
: { color: dateColor, 'background-color': 'transparent' };
function handleOnDataSave({ CrudModelRef }) {
const { original, copy } = addRow(CrudModelRef.formData[checkLastVisibleRow()]);
if (original) {
CrudModelRef.formData.splice(original?.$index ?? 0, 0, copy);
} else {
CrudModelRef.insert(copy);
const onDataSaved = () => {
tableRef.value.CrudModelRef.saveChanges();
selectedRows.value = [];
};
onMounted(() => {
if (tableRef.value) {
tableRef.value.showForm = false;
}
nextTick(() => {
highlightNewRow(original ?? { $index: 0 });
});
}
});
watch(
() => tableRef.value?.showForm,
(newVal) => {
if (!newVal) {
tableRef.value.create.title = '';
tableRef.value.create.formInitialData = { warehouseFk: warehouse };
if (tableRef.value) {
isToClone.value = false;
tableRef.value.create.title = t('Create fixed price');
}
jon marked this conversation as resolved Outdated

Te faltan las traducciones de las tags de algunos filtros, revisa los filtros de las columnas

Te faltan las traducciones de las tags de algunos filtros, revisa los filtros de las columnas
}
},
);
</script>
<template>
<FetchData
@on-fetch="(data) => (warehousesOptions = data)"
auto-load
url="Warehouses"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
/>
<RightMenu>
jon marked this conversation as resolved Outdated

Después de editar múltiples filas y guardar a pesar de no tener seleccionado ningun registro se queda activado

Después de editar múltiples filas y guardar a pesar de no tener seleccionado ningun registro se queda activado
<template #right-panel>
<ItemFixedPriceFilter
data-key="ItemFixedPrices"
ref="itemFixedPriceFilterRef"
/>
<ItemFixedPriceFilter data-key="ItemFixedPrices" />
</template>
</RightMenu>
<VnSubToolbar>
<template #st-actions>
<VnSubToolbar />
<Teleport to="#st-data" v-if="stateStore?.isSubToolbarShown()">
<QBtnGroup push style="column-gap: 10px">
<QBtn
:disable="!hasSelectedRows"
@click="openEditTableCellDialog()"
@click="openEditFixedPriceForm()"
color="primary"
icon="edit"
flat
:label="t('globals.edit')"
data-cy="FixedPriceToolbarEditBtn"
>
<QTooltip>
{{ t('Edit fixed price(s)') }}
</QTooltip>
</QBtn>
<QBtn
:disable="!hasSelectedRows"
:label="tMobile('globals.remove')"
color="primary"
icon="delete"
flat
@click="(row) => confirmRemove(row, true)"
:title="t('globals.remove')"
/>
</template>
</VnSubToolbar>
</QBtnGroup>
</Teleport>
<VnTable
:default-remove="false"
:default-reset="false"
:default-save="false"
ref="tableRef"
data-key="ItemFixedPrices"
url="FixedPrices/filter"
:order="['name DESC', 'itemFk DESC']"
:order="'name 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',
}"
v-model:selected="rowsSelected"
:create-as-dialog="false"
v-model:selected="selectedRows"
:create="{
onDataSaved: handleOnDataSave,
urlCreate: 'FixedPrices',
title: t('Create fixed price'),
formInitialData: { warehouseFk: warehouse },
onDataSaved: () => tableRef.reload(),
showSaveAndContinueBtn: true,
}"
:disable-option="{ card: true }"
:has-sub-toolbar="false"
auto-load
:beforeSaveFn="(data, getChanges) => beforeSave(data, getChanges, 'FixedPrices')"
>
jon marked this conversation as resolved Outdated

pon el item descriptor proxy dentro del span sino al hacer click en la casilla te abre el descriptor

pon el item descriptor proxy dentro del span sino al hacer click en la casilla te abre el descriptor
<template #header-selection="scope">
<QCheckbox v-model="scope.selected" />
<template #column-hex="{ row }">
<VnColor :colors="row?.hexJson" style="height: 100%; min-width: 2000px" />
</template>
<template #body-selection="scope">
{{ scope }}
<QCheckbox flat v-model="scope.selected" />
<template #column-name="{ row }">
<span class="link">
{{ row.name }}
<ItemDescriptorProxy :id="row.itemFk" />
</span>
<span class="subName">{{ row.subName }}</span>
<FetchedTags :item="row" :columns="6" />
</template>
<template #column-itemFk="props">
<template #column-started="{ row }">
<div class="editable-text q-pb-xxs">
<QBadge class="badge" :style="dateStyle(isLower(row?.ended))">
{{ toDate(row?.started) }}
</QBadge>
</div>
</template>
<template #column-ended="{ row }">
<div class="editable-text q-pb-xxs">
<QBadge class="badge" :style="dateStyle(isBigger(row?.ended))">
{{ toDate(row?.ended) }}
</QBadge>
</div>
</template>
<template #column-create-name="{ data }">
<VnSelect
style="max-width: 100px"
url="Items/withName"
hide-selected
option-label="id"
url="Items/search"
v-model="data.itemFk"
:label="t('item.fixedPrice.itemName')"
:fields="['id', 'name']"
:filter-options="['id', 'name']"
option-label="name"
option-value="id"
v-model="props.row.itemFk"
v-on="getRowUpdateInputEvents(props, true, 'select')"
:required="true"
sort-by="name ASC"
data-cy="FixedPriceCreateNameSelect"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel>
{{ scope.opt.name }}
</QItemLabel>
<QItemLabel caption> #{{ scope.opt.id }} </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">
<template #column-create-warehouseFk="{ data }">
<VnSelect
style="max-width: 150px"
:options="warehousesOptions"
hide-selected
:label="t('globals.warehouse')"
url="Warehouses"
v-model="data.warehouseFk"
:fields="['id', 'name']"
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)
)
"
hide-selected
:required="true"
sort-by="name ASC"
data-cy="FixedPriceCreateWarehouseSelect"
>
<QTooltip class="text-no-wrap">
{{ t('globals.delete') }}
</QTooltip>
</QIcon>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt.name }}
</QItemLabel>
<QItemLabel caption> #{{ scope.opt.id }} </QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</template>
</VnTable>
<QDialog ref="editTableCellDialogRef">
<EditTableCellValueForm
<QDialog ref="editFixedPriceForm">
<EditFixedPriceForm
edit-url="FixedPrices/editFixedPrice"
:rows="rowsSelected"
:fields-options="editTableFieldsOptions"
@on-data-saved="onEditCellDataSaved()"
:rows="selectedRows"
:fields-options="
columns.filter(
({ isEditable, component, name }) =>
isEditable !== false && component && name !== 'itemFk',
)
"
:beforeSave="beforeSave"
@on-data-saved="onDataSaved"
/>
</QDialog>
</template>
@ -623,8 +435,17 @@ tbody tr.highlight .q-td {
color: var(--vn-label-color);
}
</style>
<style lang="scss" scoped>
.badge {
background-color: $warning;
}
</style>
<i18n>
es:
Add fixed price: Añadir precio fijado
Edit fixed price(s): Editar precio(s) fijado(s)
Create fixed price: Crear precio fijado
Clone fixed price: Clonar precio fijado
</i18n>

View File

@ -29,6 +29,7 @@ const props = defineProps({
dense
filled
use-input
:use-like="false"
@update:model-value="searchFn()"
sort-by="nickname ASC"
/>
@ -50,21 +51,19 @@ const props = defineProps({
/>
</QItemSection>
</QItem>
<QItem class="q-my-md">
<QItem>
<QItemSection>
<VnInputDate
:label="t('params.started')"
v-model="params.started"
:label="t('params.started')"
filled
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnInputDate
:label="t('params.ended')"
v-model="params.ended"
:label="t('params.ended')"
filled
@update:model-value="searchFn()"
/>

View File

@ -8,11 +8,6 @@ import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnRow from 'components/ui/VnRow.vue';
import { QCheckbox } from 'quasar';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const emit = defineEmits(['onDataSaved']);
const $props = defineProps({
rows: {
type: Array,
@ -26,10 +21,14 @@ const $props = defineProps({
type: String,
default: '',
},
beforeSave: {
Review

hay algunos warning al abrir el create o clone form

hay algunos warning al abrir el create o clone form
Review

Los warnings se han corregido en a74e1102ed

Los warnings se han corregido en https://gitea.verdnatura.es/verdnatura/salix-front/commit/a74e1102ed6a7cdcf6bad98ed2b37312cc2fcb5f
type: Function,
default: () => {},
},
});
const { t } = useI18n();
const { notify } = useNotify();
const emit = defineEmits(['onDataSaved']);
const inputs = {
input: markRaw(VnInput),
@ -44,24 +43,13 @@ const selectedField = ref(null);
const closeButton = ref(null);
const isLoading = ref(false);
const onDataSaved = () => {
notify('globals.dataSaved', 'positive');
emit('onDataSaved');
closeForm();
};
const onSubmit = async () => {
isLoading.value = true;
const rowsToEdit = $props.rows.map((row) => ({ id: row.id, itemFk: row.itemFk }));
const payload = {
field: selectedField.value.field,
newValue: newValue.value,
lines: rowsToEdit,
};
await axios.post($props.editUrl, payload);
onDataSaved();
isLoading.value = false;
$props.rows.forEach((row) => {
row[selectedField.value.name] = newValue.value;
});
emit('onDataSaved', $props.rows);
closeForm();
};
const closeForm = () => {
@ -78,21 +66,24 @@ const closeForm = () => {
<span class="title">{{ t('Edit') }}</span>
<span class="countLines">{{ ` ${rows.length} ` }}</span>
<span class="title">{{ t('buy(s)') }}</span>
<VnRow>
<VnRow class="q-mt-md">
<VnSelect
class="editOption"
:label="t('Field to edit')"
:options="fieldsOptions"
hide-selected
option-label="label"
v-model="selectedField"
data-cy="field-to-edit"
data-cy="EditFixedPriceSelectOption"
@update:model-value="newValue = null"
:class="{ 'is-select': selectedField?.component === 'select' }"
/>
<component
:is="inputs[selectedField?.component || 'input']"
v-bind="selectedField?.attrs || {}"
v-model="newValue"
:label="t('Value')"
data-cy="value-to-edit"
data-cy="EditFixedPriceValueOption"
style="width: 200px"
/>
</VnRow>
@ -140,6 +131,15 @@ const closeForm = () => {
}
</style>
<style lang="scss">
.editOption .q-field__inner .q-field__control {
padding: 0 !important;
}
.editOption.is-select .q-field__inner .q-field__control {
padding: 0 !important;
}
</style>
<i18n>
es:
Edit: Editar

View File

@ -0,0 +1,46 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import EditFixedPriceForm from 'src/pages/Item/components/EditFixedPriceForm.vue';
describe('EditFixedPriceForm.vue', () => {
let wrapper;
let vm;
const mockRows = [
{ id: 1, itemFk: 101 },
{ id: 2, itemFk: 102 },
];
const mockFieldsOptions = [
{
name: 'price',
label: 'Price',
component: 'input',
attrs: { type: 'number' },
},
];
beforeEach(() => {
wrapper = createWrapper(EditFixedPriceForm, {
props: {
rows: JSON.parse(JSON.stringify(mockRows)),
fieldsOptions: mockFieldsOptions,
},
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
});
afterEach(() => {
vi.clearAllMocks();
});
it('should emit "onDataSaved" with updated rows on submit', async () => {
vm.selectedField = mockFieldsOptions[0];
vm.newValue = 199.99;
await vm.onSubmit();
expect(wrapper.emitted('onDataSaved')).toBeTruthy();
});
});

View File

@ -167,6 +167,8 @@ item:
started: Started
ended: Ended
warehouse: Warehouse
MP: MP
itemName: Item
create:
name: Name
tag: Tag

View File

@ -173,6 +173,8 @@ item:
started: Inicio
ended: Fin
warehouse: Almacén
MP: PM
itemName: Nombre
create:
name: Nombre
tag: Etiqueta

View File

@ -295,13 +295,11 @@ watch(
:user-filter="lineFilter"
>
<template #column-image="{ row }">
<div class="image-wrapper">
<VnImg
:id="parseInt(row?.item?.image)"
class="rounded"
zoom-resolution="1600x900"
/>
</div>
</template>
<template #column-id="{ row }">
<span class="link" @click.stop>
@ -361,12 +359,6 @@ watch(
}
}
.image-wrapper {
height: 50px;
width: 50px;
margin-left: 30%;
}
.header {
color: $primary;
font-weight: bold;

View File

@ -1,9 +1,14 @@
/// <reference types="cypress" />
function goTo(n = 1) {
return `.q-virtual-scroll__content > :nth-child(${n})`;
}
const firstRow = goTo();
describe('Handle Items FixedPrice', () => {
const grouping = 'Grouping price';
const saveEditBtn = '.q-mt-lg > .q-btn--standard';
const createForm = {
'Grouping price': { val: '5' },
'Packing price': { val: '5' },
Started: { val: '01-01-2001', type: 'date' },
Ended: { val: '15-01-2001', type: 'date' },
};
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
@ -13,50 +18,57 @@ describe('Handle Items FixedPrice', () => {
'.q-header > .q-toolbar > :nth-child(1) > .q-btn__content > .q-icon',
).click();
});
it.skip('filter', function () {
it('filter by category', () => {
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.addBtnClick();
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.skip('Create and delete ', function () {
cy.get('.q-gutter-x-sm > .q-btn > .q-btn__content > .q-icon').click();
cy.addBtnClick();
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');
cy.get('.q-table__middle').should('be.visible').should('have.length', 1);
});
it.skip('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('should create a new fixed price, then delete it', () => {
cy.dataCy('vnTableCreateBtn').click();
cy.dataCy('FixedPriceCreateNameSelect').type('Melee weapon combat fist 15cm');
cy.get('.q-menu .q-item').contains('Melee weapon combat fist 15cm').click();
cy.dataCy('FixedPriceCreateWarehouseSelect').type('Warehouse One');
cy.get('.q-menu .q-item').contains('Warehouse One').click();
cy.get('.q-menu').then(($menu) => {
if ($menu.is(':visible')) {
cy.dataCy('FixedPriceCreateWarehouseSelect').as('focusedElement').focus();
cy.dataCy('FixedPriceCreateWarehouseSelect').blur();
}
});
it.skip('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');
cy.fillInForm(createForm);
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created');
cy.get('[data-col-field="name"]').each(($el) => {
cy.wrap($el)
.invoke('text')
.then((text) => {
if (text.includes('Melee weapon combat fist 15cm')) {
cy.wrap($el).parent().find('.q-checkbox').click();
cy.get('[data-cy="crudModelDefaultRemoveBtn"]').click();
cy.dataCy('VnConfirm_confirm').click().click();
cy.checkNotification('Data saved');
}
});
});
});
it('should edit all items', () => {
cy.get('.bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner').click();
cy.dataCy('FixedPriceToolbarEditBtn').should('not.be.disabled');
cy.dataCy('FixedPriceToolbarEditBtn').click();
cy.dataCy('EditFixedPriceSelectOption').type(grouping);
cy.get('.q-menu .q-item').contains(grouping).click();
cy.dataCy('EditFixedPriceValueOption').type('5');
cy.get(saveEditBtn).click();
cy.checkNotification('Data saved');
});
it('should remove all items', () => {
cy.get('.bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner').click();
cy.dataCy('crudModelDefaultRemoveBtn').should('not.be.disabled');
cy.dataCy('crudModelDefaultRemoveBtn').click();
cy.dataCy('VnConfirm_confirm').click();
cy.checkNotification('Data saved');
});
});