Merge branch 'test' into warmfix_ticket_newPayment
gitea/salix-front/pipeline/pr-test This commit looks good Details

This commit is contained in:
Javier Segarra 2025-05-19 11:10:27 +00:00
commit f76305fdac
21 changed files with 640 additions and 274 deletions

View File

@ -0,0 +1,184 @@
<script setup>
import { onMounted, watch, computed, ref, useAttrs } from 'vue';
import { date } from 'quasar';
import VnDate from './VnDate.vue';
import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const $props = defineProps({
isOutlined: {
type: Boolean,
default: false,
},
isPopupOpen: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: true,
},
});
const model = defineModel({
type: [String, Date, Array],
default: null,
});
const vnInputDateRef = ref(null);
const dateFormat = 'DD/MM/YYYY';
const isPopupOpen = ref();
const hover = ref();
const mask = ref();
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
const formattedDate = computed({
get() {
if (!model.value) return model.value;
if ($props.multiple) {
return model.value
.map((d) => date.formatDate(new Date(d), dateFormat))
.join(', ');
}
return date.formatDate(new Date(model.value), dateFormat);
},
set(value) {
if (value == model.value) return;
if ($props.multiple) return; // No permitir edición manual en modo múltiple
let newDate;
if (value) {
// parse input
if (value.includes('/') && value.length >= 10) {
if (value.at(2) == '/') value = value.split('/').reverse().join('/');
value = date.formatDate(
new Date(value).toISOString(),
'YYYY-MM-DDTHH:mm:ss.SSSZ',
);
}
const [year, month, day] = value.split('-').map((e) => parseInt(e));
newDate = new Date(year, month - 1, day);
if (model.value && !$props.multiple) {
const orgDate =
model.value instanceof Date ? model.value : new Date(model.value);
newDate.setHours(
orgDate.getHours(),
orgDate.getMinutes(),
orgDate.getSeconds(),
orgDate.getMilliseconds(),
);
}
}
if (!isNaN(newDate)) model.value = newDate.toISOString();
},
});
const popupDate = computed(() => {
if (!model.value) return model.value;
if ($props.multiple) {
return model.value.map((d) => date.formatDate(new Date(d), 'YYYY/MM/DD'));
}
return date.formatDate(new Date(model.value), 'YYYY/MM/DD');
});
onMounted(() => {
// fix quasar bug
mask.value = '##/##/####';
if ($props.multiple && !model.value) {
model.value = [];
}
});
watch(
() => model.value,
(val) => (formattedDate.value = val),
{ immediate: true },
);
const styleAttrs = computed(() => {
return $props.isOutlined
? {
dense: true,
outlined: true,
rounded: true,
}
: {};
});
const manageDate = (dates) => {
if ($props.multiple) {
model.value = dates.map((d) => new Date(d).toISOString());
} else {
formattedDate.value = dates;
}
if ($props.isPopupOpen) isPopupOpen.value = false;
};
</script>
<template>
<div @mouseover="hover = true" @mouseleave="hover = false">
<QInput
ref="vnInputDateRef"
v-model="formattedDate"
class="vn-input-date"
:mask="$props.multiple ? undefined : mask"
placeholder="dd/mm/aaaa"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: isRequired }"
:rules="mixinRules"
:clearable="false"
@click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
hide-bottom-space
:data-cy="($attrs['data-cy'] ?? $attrs.label) + '_inputDate'"
>
<template #append>
<QIcon
name="close"
size="xs"
v-if="
($attrs.clearable == undefined || $attrs.clearable) &&
hover &&
model &&
!$attrs.disable
"
@click="
vnInputDateRef.focus();
model = null;
isPopupOpen = false;
"
/>
</template>
<QMenu
v-if="$q.screen.gt.xs"
transition-show="scale"
transition-hide="scale"
v-model="isPopupOpen"
anchor="bottom left"
self="top start"
:no-focus="true"
:no-parent-event="true"
>
<VnDate
v-model="popupDate"
@update:model-value="manageDate"
:multiple="multiple"
/>
</QMenu>
<QDialog v-else v-model="isPopupOpen">
<VnDate
v-model="popupDate"
@update:model-value="manageDate"
:multiple="multiple"
/>
</QDialog>
</QInput>
</div>
</template>
<i18n>
es:
Open date: Abrir fecha
</i18n>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { toPercentage } from 'filters/index'; import { toCurrency, toPercentage } from 'filters/index';
import { computed } from 'vue'; import { computed } from 'vue';
@ -8,6 +8,10 @@ const props = defineProps({
type: Number, type: Number,
required: true, required: true,
}, },
format: {
type: String,
default: 'percentage', // 'currency'
},
}); });
const valueClass = computed(() => const valueClass = computed(() =>
@ -21,7 +25,10 @@ const formattedValue = computed(() => props.value);
<template> <template>
<span :class="valueClass"> <span :class="valueClass">
<QIcon :name="iconName" size="sm" class="value-icon" /> <QIcon :name="iconName" size="sm" class="value-icon" />
{{ toPercentage(formattedValue) }} <span v-if="$props.format === 'percentage'">{{
toPercentage(formattedValue)
}}</span>
<span v-if="$props.format === 'currency'">{{ toCurrency(formattedValue) }}</span>
</span> </span>
</template> </template>

View File

@ -8,10 +8,11 @@ import VnTable from 'src/components/VnTable/VnTable.vue';
import axios from 'axios'; import axios from 'axios';
import { displayResults } from 'src/pages/Ticket/Negative/composables/notifyResults'; import { displayResults } from 'src/pages/Ticket/Negative/composables/notifyResults';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import { useState } from 'src/composables/useState'; import useNotify from 'src/composables/useNotify.js';
const MATCH = 'match'; const MATCH = 'match';
const { notifyResults } = displayResults(); const { notifyResults } = displayResults();
const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const $props = defineProps({ const $props = defineProps({
@ -42,7 +43,7 @@ const ticketConfig = ref({});
const proposalTableRef = ref(null); const proposalTableRef = ref(null);
const sale = computed(() => $props.sales[0]); const sale = computed(() => $props.sales[0]);
const saleFk = computed(() => sale.value.saleFk); const saleFk = computed(() => sale.value?.saleFk);
const filter = computed(() => ({ const filter = computed(() => ({
where: $props.filter, where: $props.filter,
@ -56,8 +57,24 @@ const defaultColumnAttrs = {
}; };
const emit = defineEmits(['onDialogClosed', 'itemReplaced']); const emit = defineEmits(['onDialogClosed', 'itemReplaced']);
const conditionalValuePrice = (price) => const priceStatusClass = (proposalPrice) => {
price > 1 + ticketConfig.value.lackAlertPrice / 100 ? 'match' : 'not-match'; const originalPrice = sale.value?.price;
if (
!originalPrice ||
!ticketConfig.value ||
typeof ticketConfig.value.lackAlertPrice !== 'number'
) {
return 'price-ok';
}
const priceIncreasePercentage =
((proposalPrice - originalPrice) / originalPrice) * 100;
return priceIncreasePercentage > ticketConfig.value.lackAlertPrice
? 'price-alert'
: 'price-ok';
};
const columns = computed(() => [ const columns = computed(() => [
{ {
@ -97,7 +114,15 @@ const columns = computed(() => [
{ {
align: 'left', align: 'left',
sortable: true, sortable: true,
label: t('item.list.color'), label: t('item.list.producer'),
name: 'subName',
field: 'subName',
columnClass: 'expand',
},
{
align: 'left',
sortable: true,
label: t('proposal.tag5'),
name: 'tag5', name: 'tag5',
field: 'value5', field: 'value5',
columnClass: 'expand', columnClass: 'expand',
@ -105,7 +130,7 @@ const columns = computed(() => [
{ {
align: 'left', align: 'left',
sortable: true, sortable: true,
label: t('item.list.stems'), label: t('proposal.tag6'),
name: 'tag6', name: 'tag6',
field: 'value6', field: 'value6',
columnClass: 'expand', columnClass: 'expand',
@ -113,12 +138,27 @@ const columns = computed(() => [
{ {
align: 'left', align: 'left',
sortable: true, sortable: true,
label: t('item.list.producer'), label: t('proposal.tag7'),
name: 'tag7', name: 'tag7',
field: 'value7', field: 'value7',
columnClass: 'expand', columnClass: 'expand',
}, },
{
align: 'left',
sortable: true,
label: t('proposal.tag8'),
name: 'tag8',
field: 'value8',
columnClass: 'expand',
},
{
align: 'left',
sortable: true,
label: t('proposal.advanceable'),
name: 'advanceable',
field: 'advanceable',
columnClass: 'expand',
},
{ {
...defaultColumnAttrs, ...defaultColumnAttrs,
label: t('proposal.price2'), label: t('proposal.price2'),
@ -169,14 +209,14 @@ function extractMatchValues(obj) {
.filter((key) => key.startsWith(MATCH)) .filter((key) => key.startsWith(MATCH))
.map((key) => parseInt(key.replace(MATCH, ''), 10)); .map((key) => parseInt(key.replace(MATCH, ''), 10));
} }
const gradientStyle = (value) => { const gradientStyleClass = (row) => {
let color = 'white'; let color = 'white';
const perc = parseFloat(value); const value = parseFloat(row);
switch (true) { switch (true) {
case perc >= 0 && perc < 33: case value >= 0 && value < 33:
color = 'primary'; color = 'primary';
break; break;
case perc >= 33 && perc < 66: case value >= 33 && value < 66:
color = 'warning'; color = 'warning';
break; break;
@ -193,52 +233,63 @@ const statusConditionalValue = (row) => {
}; };
const isSelectionAvailable = (itemProposal) => { const isSelectionAvailable = (itemProposal) => {
const { price2 } = itemProposal; const { price2, available } = itemProposal;
const salePrice = sale.value.price; const salePrice = sale.value.price;
const byPrice = (100 * price2) / salePrice > ticketConfig.value.lackAlertPrice; const { lackAlertPrice } = ticketConfig.value;
if (byPrice) { const isPriceTooHigh = (100 * price2) / salePrice > lackAlertPrice;
return byPrice; if (isPriceTooHigh) {
return isPriceTooHigh;
} }
const byQuantity = const hasEnoughQuantity =
(100 * itemProposal.available) / Math.abs($props.itemLack.lack) < (100 * available) / Math.abs($props.itemLack.lack) < lackAlertPrice;
ticketConfig.value.lackAlertPrice; return hasEnoughQuantity;
return byQuantity;
}; };
async function change({ itemFk: substitutionFk }) { async function change(itemSubstitution) {
try { if (!isSelectionAvailable(itemSubstitution)) {
const promises = $props.sales.map(({ saleFk, quantity }) => { notify(t('notAvailable'), 'warning');
const params = { return;
}
const { itemFk: substitutionFk } = itemSubstitution;
let body;
const promises = $props.sales.map(({ saleFk, quantity, ticketFk }) => {
body = {
saleFk, saleFk,
substitutionFk, substitutionFk,
quantity, quantity,
ticketFk,
}; };
return axios.post('Sales/replaceItem', params); return axios.post('Sales/replaceItem', body);
}); });
const results = await Promise.allSettled(promises); const results = await Promise.allSettled(promises);
notifyResults(results, 'saleFk'); notifyResults(results, 'ticketFk');
emit('itemReplaced', { emit('itemReplaced', {
...body,
type: 'refresh', type: 'refresh',
quantity: quantity.value,
itemProposal: proposalSelected.value[0], itemProposal: proposalSelected.value[0],
}); });
proposalSelected.value = []; proposalSelected.value = [];
} catch (error) {
console.error(error);
}
} }
async function handleTicketConfig(data) { async function handleTicketConfig(data) {
ticketConfig.value = data[0]; ticketConfig.value = data[0];
} }
function filterRows(data) {
const filteredRows = data.sort(
(a, b) => isSelectionAvailable(b) - isSelectionAvailable(a),
);
proposalTableRef.value.CrudModelRef.formData = filteredRows;
}
</script> </script>
<template> <template>
<FetchData <FetchData
url="TicketConfigs" url="TicketConfigs"
:filter="{ fields: ['lackAlertPrice'] }" :filter="{ fields: ['lackAlertPrice'] }"
@on-fetch="handleTicketConfig" @on-fetch="handleTicketConfig"
></FetchData> auto-load
/>
<QInnerLoading <QInnerLoading
:showing="isLoading" :showing="isLoading"
:label="t && t('globals.pleaseWait')" :label="t && t('globals.pleaseWait')"
@ -255,13 +306,22 @@ async function handleTicketConfig(data) {
:user-filter="filter" :user-filter="filter"
:columns="columns" :columns="columns"
class="full-width q-mt-md" class="full-width q-mt-md"
@on-fetch="filterRows"
row-key="id" row-key="id"
:row-click="change" :row-click="change"
:is-editable="false" :is-editable="false"
:right-search="false" :right-search="false"
:without-header="true"
:disable-option="{ card: true, table: true }" :disable-option="{ card: true, table: true }"
> >
<template #top-right>
<QBtn
flat
class="q-mr-sm"
color="primary"
icon="refresh"
@click="proposalTableRef.reload()"
/>
</template>
<template #column-longName="{ row }"> <template #column-longName="{ row }">
<QTd <QTd
class="flex" class="flex"
@ -269,15 +329,17 @@ async function handleTicketConfig(data) {
> >
<div <div
class="middle full-width" class="middle full-width"
:class="[`proposal-${gradientStyle(statusConditionalValue(row))}`]" :class="[
`proposal-${gradientStyleClass(statusConditionalValue(row))}`,
]"
> >
<QTooltip> {{ statusConditionalValue(row) }}% </QTooltip> <QTooltip> {{ statusConditionalValue(row) }}% </QTooltip>
</div> </div>
<div style="flex: 2 0 100%; align-content: center"> <div style="flex: 2 0 100%; align-content: center">
<div> <span class="link" @click.stop>
<span class="link">{{ row.longName }}</span> {{ row.longName }}
<ItemDescriptorProxy :id="row.id" /> <ItemDescriptorProxy :id="row.id" />
</div> </span>
</div> </div>
</QTd> </QTd>
</template> </template>
@ -290,6 +352,9 @@ async function handleTicketConfig(data) {
<template #column-tag7="{ row }"> <template #column-tag7="{ row }">
<span :class="{ match: !row.match7 }">{{ row.value7 }}</span> <span :class="{ match: !row.match7 }">{{ row.value7 }}</span>
</template> </template>
<template #column-tag8="{ row }">
<span :class="{ match: !row.match8 }">{{ row.value8 }}</span>
</template>
<template #column-counter="{ row }"> <template #column-counter="{ row }">
<span <span
:class="{ :class="{
@ -304,8 +369,17 @@ async function handleTicketConfig(data) {
</template> </template>
<template #column-price2="{ row }"> <template #column-price2="{ row }">
<div class="flex column items-center content-center"> <div class="flex column items-center content-center">
<VnStockValueDisplay :value="(sales[0].price - row.price2) / 100" /> <!-- Use class binding for tooltip background -->
<span :class="[conditionalValuePrice(row.price2)]">{{ <QTooltip :offset="[0, 5]" anchor="top middle" self="bottom middle">
<div>{{ $t('proposal.price2') }}: {{ toCurrency(row.price2) }}</div>
<div>
{{ $t('proposal.itemOldPrice') }}:
{{ toCurrency(sales[0]?.price) }}
</div>
</QTooltip>
<VnStockValueDisplay :format="'currency'" :value="-row.price2 / 100" />
<!-- Use class binding for text color -->
<span :class="[priceStatusClass(row.price2)]">{{
toCurrency(row.price2) toCurrency(row.price2)
}}</span> }}</span>
</div> </div>
@ -319,12 +393,26 @@ async function handleTicketConfig(data) {
margin-right: 2px; margin-right: 2px;
flex: 2 0 5px; flex: 2 0 5px;
} }
.price-alert {
color: $negative;
&.q-tooltip {
background-color: $negative;
color: white;
}
}
.price-ok {
color: inherit;
&.q-tooltip {
background-color: $positive;
color: white;
}
}
.match { .match {
color: $negative; color: $negative;
} }
.not-match {
color: inherit;
}
.proposal-warning { .proposal-warning {
background-color: $warning; background-color: $warning;
} }
@ -344,3 +432,9 @@ async function handleTicketConfig(data) {
font-size: smaller; font-size: smaller;
} }
</style> </style>
<i18n>
en:
notAvailable: 'Not available for replacement'
es:
notAvailable: 'No disponible para reemplazo'
</i18n>

View File

@ -23,33 +23,32 @@ const $props = defineProps({
default: () => [], default: () => [],
}, },
}); });
const { dialogRef } = useDialogPluginComponent();
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent();
const emit = defineEmits([ const emit = defineEmits([
'onDialogClosed', 'onDialogClosed',
'onDialogOk',
'itemReplaced', 'itemReplaced',
...useDialogPluginComponent.emits, ...useDialogPluginComponent.emits,
]); ]);
defineExpose({ show: () => dialogRef.value.show(), hide: () => dialogRef.value.hide() }); defineExpose({ show: () => dialogRef.value.show(), hide: () => dialogRef.value.hide() });
const itemReplaced = (data) => {
onDialogOK(data);
dialogRef.value.hide();
};
</script> </script>
<template> <template>
<QDialog ref="dialogRef" transition-show="scale" transition-hide="scale"> <QDialog ref="dialogRef" transition-show="scale" transition-hide="scale">
<QCard class="dialog-width"> <QCard class="dialog-width">
<QCardSection class="row items-center q-pb-none"> <QCardSection class="row items-center q-pb-none">
<span class="text-h6 text-grey">{{ $t('itemProposal') }}</span> <span class="text-h6 text-grey" v-text="$t('itemProposal')" />
<QSpace /> <QSpace />
<QBtn icon="close" flat round dense v-close-popup /> <QBtn icon="close" flat round dense v-close-popup />
</QCardSection> </QCardSection>
<QCardSection> <QCardSection>
<ItemProposal <ItemProposal v-bind="$props" @item-replaced="itemReplaced"
v-bind="$props" /></QCardSection>
@item-replaced="
(data) => {
emit('itemReplaced', data);
dialogRef.hide();
}
"
></ItemProposal
></QCardSection>
</QCard> </QCard>
</QDialog> </QDialog>
</template> </template>

View File

@ -231,6 +231,11 @@ proposal:
value6: value6 value6: value6
value7: value7 value7: value7
value8: value8 value8: value8
tag5: Tag5
tag6: Tag6
tag7: Tag7
tag8: Tag8
advanceable: Advanceable
available: Available available: Available
minQuantity: minQuantity minQuantity: minQuantity
price2: Price price2: Price

View File

@ -237,11 +237,16 @@ proposal:
value6: value6 value6: value6
value7: value7 value7: value7
value8: value8 value8: value8
tag5: Tag5
tag6: Tag6
tag7: Tag7
tag8: Tag8
available: Disponible available: Disponible
minQuantity: Min. cantidad minQuantity: Min. cantidad
price2: Precio price2: Precio
located: Ubicado located: Ubicado
counter: Contador counter: Contador
advanceable: Adelantable
difference: Diferencial difference: Diferencial
groupingPrice: Precio Grouping groupingPrice: Precio Grouping
itemOldPrice: Precio itemOld itemOldPrice: Precio itemOld

View File

@ -1,11 +1,11 @@
import axios from 'axios'; import axios from 'axios';
export default async function (data, date) { export default async function (data, landed) {
const reducedData = data.reduce((acc, item) => { const reducedData = data.reduce((acc, item) => {
const existing = acc.find(({ ticketFk }) => ticketFk === item.id); const existing = acc.find(({ ticketFk }) => ticketFk === item.id);
if (existing) { if (existing) {
existing.sales.push(item.saleFk); existing.sales.push(item.saleFk);
} else { } else {
acc.push({ ticketFk: item.ticketFk, sales: [item.saleFk], date }); acc.push({ ticketFk: item.ticketFk, sales: [item.saleFk], landed });
} }
return acc; return acc;
}, []); }, []);

View File

@ -17,7 +17,6 @@ import ItemProposalProxy from 'src/pages/Item/components/ItemProposalProxy.vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const editableStates = ref([]);
const stateStore = useStateStore(); const stateStore = useStateStore();
const tableRef = ref(); const tableRef = ref();
const changeItemDialogRef = ref(null); const changeItemDialogRef = ref(null);
@ -70,14 +69,11 @@ const showItemProposal = () => {
}) })
.onOk(itemProposalEvt); .onOk(itemProposalEvt);
}; };
const isButtonDisabled = computed(() => selectedRows.value.length !== 1);
</script> </script>
<template> <template>
<FetchData
url="States/editableStates"
@on-fetch="(data) => (editableStates = data)"
auto-load
/>
<FetchData <FetchData
:url="`Items/${entityId}/getCard`" :url="`Items/${entityId}/getCard`"
:fields="['longName']" :fields="['longName']"
@ -99,11 +95,7 @@ const showItemProposal = () => {
> >
<template #top-right> <template #top-right>
<QBtnGroup push class="q-mr-lg" style="column-gap: 1px"> <QBtnGroup push class="q-mr-lg" style="column-gap: 1px">
<QBtn <QBtn data-cy="transferLines" color="primary" :disable="isButtonDisabled">
data-cy="transferLines"
color="primary"
:disable="!(selectedRows.length === 1)"
>
<template #default> <template #default>
<QIcon name="vn:splitline" /> <QIcon name="vn:splitline" />
<QIcon name="vn:ticket" /> <QIcon name="vn:ticket" />
@ -124,7 +116,7 @@ const showItemProposal = () => {
<QBtn <QBtn
color="primary" color="primary"
@click="showItemProposal" @click="showItemProposal"
:disable="!(selectedRows.length === 1)" :disable="isButtonDisabled"
data-cy="itemProposal" data-cy="itemProposal"
> >
<QIcon name="import_export" class="rotate-90" /> <QIcon name="import_export" class="rotate-90" />
@ -135,7 +127,7 @@ const showItemProposal = () => {
<VnPopupProxy <VnPopupProxy
data-cy="changeItem" data-cy="changeItem"
icon="sync" icon="sync"
:disable="!(selectedRows.length === 1)" :disable="isButtonDisabled"
:tooltip="t('negative.detail.modal.changeItem.title')" :tooltip="t('negative.detail.modal.changeItem.title')"
> >
<template #extraIcon> <QIcon name="vn:item" /> </template> <template #extraIcon> <QIcon name="vn:item" /> </template>
@ -149,7 +141,7 @@ const showItemProposal = () => {
<VnPopupProxy <VnPopupProxy
data-cy="changeState" data-cy="changeState"
icon="sync" icon="sync"
:disable="!(selectedRows.length === 1)" :disable="isButtonDisabled"
:tooltip="t('negative.detail.modal.changeState.title')" :tooltip="t('negative.detail.modal.changeState.title')"
> >
<template #extraIcon> <QIcon name="vn:eye" /> </template> <template #extraIcon> <QIcon name="vn:eye" /> </template>
@ -163,7 +155,7 @@ const showItemProposal = () => {
<VnPopupProxy <VnPopupProxy
data-cy="changeQuantity" data-cy="changeQuantity"
icon="sync" icon="sync"
:disable="!(selectedRows.length === 1)" :disable="isButtonDisabled"
:tooltip="t('negative.detail.modal.changeQuantity.title')" :tooltip="t('negative.detail.modal.changeQuantity.title')"
@click="showChangeQuantityDialog = true" @click="showChangeQuantityDialog = true"
> >

View File

@ -7,6 +7,8 @@ import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDateTime from 'src/components/common/VnInputDateTime.vue'; import VnInputDateTime from 'src/components/common/VnInputDateTime.vue';
import VnInputDates from 'src/components/common/VnInputDates.vue';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
dataKey: { dataKey: {
@ -73,8 +75,8 @@ const setUserParams = (params) => {
<VnFilterPanel <VnFilterPanel
:data-key="props.dataKey" :data-key="props.dataKey"
:search-button="true" :search-button="true"
:hidden-tags="['excludedDates']"
@set-user-params="setUserParams" @set-user-params="setUserParams"
:unremovable-params="['warehouseFk']"
> >
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
@ -92,7 +94,7 @@ const setUserParams = (params) => {
dense dense
filled filled
@update:model-value=" @update:model-value="
(value) => { () => {
setUserParams(params); setUserParams(params);
} }
" "
@ -127,8 +129,19 @@ const setUserParams = (params) => {
dense dense
filled filled
/> />
</QItemSection> </QItem </QItemSection>
><QItem> </QItem>
<QItem>
<QItemSection>
<VnInputDates
v-model="params.excludedDates"
filled
:label="t('negative.excludedDates')"
>
</VnInputDates>
</QItemSection>
</QItem>
<QItem>
<QItemSection v-if="categoriesOptions"> <QItemSection v-if="categoriesOptions">
<VnSelect <VnSelect
:label="t('negative.categoryFk')" :label="t('negative.categoryFk')"

View File

@ -7,6 +7,7 @@ import { onBeforeMount } from 'vue';
import { dashIfEmpty, toDate, toHour } from 'src/filters'; import { dashIfEmpty, toDate, toHour } from 'src/filters';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import TicketLackFilter from './TicketLackFilter.vue'; import TicketLackFilter from './TicketLackFilter.vue';
@ -45,10 +46,10 @@ const columns = computed(() => [
}, },
{ {
columnClass: 'shrink', columnClass: 'shrink',
name: 'timed', name: 'minTimed',
align: 'center', align: 'center',
label: t('negative.timed'), label: t('negative.timed'),
format: ({ timed }) => toHour(timed), format: ({ minTimed }) => toHour(minTimed),
sortable: true, sortable: true,
cardVisible: true, cardVisible: true,
columnFilter: { columnFilter: {
@ -64,9 +65,25 @@ const columns = computed(() => [
columnFilter: { columnFilter: {
component: 'input', component: 'input',
type: 'number', type: 'number',
inWhere: false, columnClass: 'shrink',
}, },
}, },
{
name: 'nextEntryFk',
align: 'center',
label: t('negative.nextEntryFk'),
format: ({ nextEntryFk }) => nextEntryFk,
sortable: false,
columnFilter: false,
},
{
name: 'nextEntryLanded',
align: 'center',
label: t('negative.nextEntryLanded'),
format: ({ nextEntryLanded }) => toDate(nextEntryLanded),
sortable: false,
columnFilter: false,
},
{ {
name: 'longName', name: 'longName',
align: 'left', align: 'left',
@ -195,6 +212,12 @@ const setUserParams = (params) => {
<span @click.stop>{{ row.itemFk }}</span> <span @click.stop>{{ row.itemFk }}</span>
</div> </div>
</template> </template>
<template #column-nextEntryFk="{ row }">
<span class="link" @click.stop>
{{ row.nextEntryFk }}
<EntryDescriptorProxy :id="row.nextEntryFk" />
</span>
</template>
<template #column-longName="{ row }"> <template #column-longName="{ row }">
<span class="link" @click.stop> <span class="link" @click.stop>
{{ row.longName }} {{ row.longName }}

View File

@ -35,6 +35,7 @@ const filterLack = ref({
order: 'ts.alertLevelCode ASC', order: 'ts.alertLevelCode ASC',
}); });
const editableStates = ref([]);
const selectedRows = ref([]); const selectedRows = ref([]);
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify(); const { notify } = useNotify();
@ -135,9 +136,12 @@ const saveChange = async (field, { row }) => {
try { try {
switch (field) { switch (field) {
case 'alertLevelCode': case 'alertLevelCode':
const { id: code } = editableStates.value.find(
({ name }) => name === row.code,
);
await axios.post(`Tickets/state`, { await axios.post(`Tickets/state`, {
ticketFk: row.ticketFk, ticketFk: row.ticketFk,
code: row[field], code,
}); });
break; break;
@ -160,6 +164,11 @@ function onBuysFetched(data) {
</script> </script>
<template> <template>
<FetchData
url="States/editableStates"
@on-fetch="(data) => (editableStates = data)"
auto-load
/>
<FetchData <FetchData
ref="fetchItemLack" ref="fetchItemLack"
:url="`Tickets/itemLack`" :url="`Tickets/itemLack`"
@ -309,12 +318,12 @@ function onBuysFetched(data) {
</template> </template>
<template #column-alertLevelCode="props"> <template #column-alertLevelCode="props">
<VnSelect <VnSelect
url="States/editableStates" :options="editableStates"
auto-load auto-load
hide-selected hide-selected
option-value="id" option-value="name"
option-label="name" option-label="name"
v-model="props.row.alertLevelCode" v-model="props.row.code"
v-on="getInputEvents(props)" v-on="getInputEvents(props)"
/> />
</template> </template>

View File

@ -19,18 +19,18 @@ const $props = defineProps({
const updateItem = async () => { const updateItem = async () => {
try { try {
showChangeItemDialog.value = true; showChangeItemDialog.value = true;
const rowsToUpdate = $props.selectedRows.map(({ saleFk, quantity }) => const rowsToUpdate = $props.selectedRows.map(({ saleFk, ticketFk, quantity }) =>
axios.post(`Sales/replaceItem`, { axios.post(`Sales/replaceItem`, {
saleFk, saleFk,
ticketFk,
substitutionFk: newItem.value, substitutionFk: newItem.value,
quantity, quantity,
}), }),
); );
const result = await Promise.allSettled(rowsToUpdate); const result = await Promise.allSettled(rowsToUpdate);
notifyResults(result, 'saleFk'); notifyResults(result, 'ticketFk');
emit('update-item', newItem.value); emit('update-item', newItem.value);
} catch (err) { } catch (err) {
console.error('Error updating item:', err);
return err; return err;
} }
}; };
@ -41,6 +41,7 @@ const updateItem = async () => {
<QCardSection class="row items-center justify-center column items-stretch"> <QCardSection class="row items-center justify-center column items-stretch">
<span>{{ $t('negative.detail.modal.changeItem.title') }}</span> <span>{{ $t('negative.detail.modal.changeItem.title') }}</span>
<VnSelect <VnSelect
data-cy="New item_select"
url="Items/WithName" url="Items/WithName"
:fields="['id', 'name']" :fields="['id', 'name']"
:sort-by="['id DESC']" :sort-by="['id DESC']"

View File

@ -19,9 +19,9 @@ const $props = defineProps({
const updateState = async () => { const updateState = async () => {
try { try {
showChangeStateDialog.value = true; showChangeStateDialog.value = true;
const rowsToUpdate = $props.selectedRows.map(({ id }) => const rowsToUpdate = $props.selectedRows.map(({ ticketFk }) =>
axios.post(`Tickets/state`, { axios.post(`Tickets/state`, {
ticketFk: id, ticketFk,
code: newState.value, code: newState.value,
}), }),
); );
@ -49,8 +49,9 @@ const updateState = async () => {
v-model="newState" v-model="newState"
:options="editableStates" :options="editableStates"
option-label="name" option-label="name"
option-value="code" option-value="id"
autofocus autofocus
data-cy="New state_select"
/> />
</QCardSection> </QCardSection>
<QCardActions align="right"> <QCardActions align="right">

View File

@ -206,7 +206,6 @@ ticketList:
toLines: Go to lines toLines: Go to lines
addressNickname: Address nickname addressNickname: Address nickname
ref: Reference ref: Reference
hour: Hour
rounding: Rounding rounding: Rounding
noVerifiedData: No verified data noVerifiedData: No verified data
warehouse: Warehouse warehouse: Warehouse
@ -215,6 +214,8 @@ ticketList:
clientFrozen: Client frozen clientFrozen: Client frozen
componentLack: Component lack componentLack: Component lack
negative: negative:
nextEntryFk: Next entry
nextEntryLanded: Next entry landed
hour: Hour hour: Hour
id: Id Article id: Id Article
longName: Article longName: Article
@ -225,6 +226,7 @@ negative:
value: Negative value: Negative
itemFk: Article itemFk: Article
producer: Producer producer: Producer
excludedDates: Excluded dates
warehouse: Warehouse warehouse: Warehouse
warehouseFk: Warehouse warehouseFk: Warehouse
category: Category category: Category

View File

@ -215,6 +215,8 @@ ticketList:
addressNickname: Alias consignatario addressNickname: Alias consignatario
ref: Referencia ref: Referencia
negative: negative:
nextEntryLanded: F. Entrada
nextEntryFk: Entrada
hour: Hora hour: Hora
id: Id Articulo id: Id Articulo
longName: Artículo longName: Artículo
@ -225,7 +227,8 @@ negative:
origen: Origen origen: Origen
value: Negativo value: Negativo
warehouseFk: Almacen warehouseFk: Almacen
producer: Producer producer: Productor
excludedDates: Fechas excluidas
category: Categoría category: Categoría
categoryFk: Familia categoryFk: Familia
typeFk: Familia typeFk: Familia

View File

@ -45,7 +45,7 @@ describe('OrderCatalog', { testIsolation: true }, () => {
).type('{enter}'); ).type('{enter}');
cy.get(':nth-child(1) > [data-cy="catalogFilterCategory"]').click(); cy.get(':nth-child(1) > [data-cy="catalogFilterCategory"]').click();
cy.dataCy('catalogFilterValueDialogBtn').last().click(); cy.dataCy('catalogFilterValueDialogBtn').last().click();
cy.selectOption("[data-cy='catalogFilterValueDialogTagSelect']", 'Tallos'); cy.selectOption('[data-cy="catalogFilterValueDialogTagSelect"]', 'Tallos');
cy.dataCy('catalogFilterValueDialogValueInput').find('input').focus(); cy.dataCy('catalogFilterValueDialogValueInput').find('input').focus();
cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('2'); cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('2');
cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('{enter}'); cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('{enter}');

View File

@ -1,146 +1,161 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe.skip('Ticket Lack detail', () => { const firstRow = 'tr.cursor-pointer > :nth-child(1)';
const ticketId = 1000000;
const clickNotificationAction = () => {
const notification = '.q-notification';
cy.waitForElement(notification);
cy.get(notification).should('be.visible');
cy.get('.q-notification__actions > .q-btn').click();
cy.get('@open').should((openStub) => {
expect(openStub).to.be.called;
const firstArg = openStub.args[0][0];
expect(firstArg).to.match(/\/ticket\/\d+\/sale/);
expect(firstArg).to.include(`/ticket/${ticketId}/sale`);
});
};
describe('Ticket Lack detail', { testIsolation: true }, () => {
beforeEach(() => { beforeEach(() => {
cy.viewport(1980, 1020);
cy.login('developer'); cy.login('developer');
cy.intercept('GET', /\/api\/Tickets\/itemLack\/5.*$/, { cy.intercept('GET', /\/api\/Tickets\/itemLack\/88.*$/).as('getItemLack');
statusCode: 200, cy.visit('/#/ticket/negative/88');
body: [ cy.window().then((win) => {
{ cy.stub(win, 'open').as('open');
saleFk: 33, });
code: 'OK', cy.wait('@getItemLack').then((interception) => {
ticketFk: 142, const { query } = interception.request;
nickname: 'Malibu Point', const filter = JSON.parse(query.filter);
shipped: '2000-12-31T23:00:00.000Z', expect(filter).to.have.property('where');
hour: 0, expect(filter.where).to.have.property('alertLevelCode', 'FREE');
quantity: 50, });
agName: 'Super-Man delivery', });
alertLevel: 0, describe('Table detail', () => {
stateName: 'OK', it('should open descriptors', () => {
stateId: 3, cy.get('.q-table').should('be.visible');
itemFk: 5, cy.colField('zoneName').click();
price: 1.79, cy.dataCy('ZoneDescriptor').should('be.visible');
alertLevelCode: 'FREE', cy.get('.q-item > .q-item__label').should('have.text', ' #1');
zoneFk: 9, cy.colField('ticketFk').click();
zoneName: 'Zone superMan', cy.dataCy('TicketDescriptor').should('be.visible');
theoreticalhour: '2011-11-01T22:59:00.000Z', cy.get('.q-item > .q-item__label').should('have.text', ` #${ticketId}`);
isRookie: 1, cy.colField('nickname').find('.link').click();
turno: 1, cy.waitForElement('[data-cy="CustomerDescriptor"]');
peticionCompra: 1, cy.dataCy('CustomerDescriptor').should('be.visible');
hasObservation: 1, cy.get('.q-item > .q-item__label').should('have.text', ' #1');
hasToIgnore: 1,
isBasket: 1,
minTimed: 0,
customerId: 1104,
customerName: 'Tony Stark',
observationTypeCode: 'administrative',
},
],
}).as('getItemLack');
cy.visit('/#/ticket/negative/5', false);
cy.wait('@getItemLack');
}); });
describe('Table actions', () => {
it('should display only one row in the lack list', () => { it('should display only one row in the lack list', () => {
cy.location('href').should('contain', '#/ticket/negative/5'); cy.dataCy('changeItem').should('be.disabled');
cy.dataCy('changeState').should('be.disabled');
cy.get('[data-cy="changeItem"]').should('be.disabled'); cy.dataCy('changeQuantity').should('be.disabled');
cy.get('[data-cy="changeState"]').should('be.disabled'); cy.dataCy('itemProposal').should('be.disabled');
cy.get('[data-cy="changeQuantity"]').should('be.disabled'); cy.dataCy('transferLines').should('be.disabled');
cy.get('[data-cy="itemProposal"]').should('be.disabled');
cy.get('[data-cy="transferLines"]').should('be.disabled');
cy.get('tr.cursor-pointer > :nth-child(1)').click(); cy.get('tr.cursor-pointer > :nth-child(1)').click();
cy.get('[data-cy="changeItem"]').should('be.enabled'); cy.dataCy('changeItem').should('be.enabled');
cy.get('[data-cy="changeState"]').should('be.enabled'); cy.dataCy('changeState').should('be.enabled');
cy.get('[data-cy="changeQuantity"]').should('be.enabled'); cy.dataCy('changeQuantity').should('be.enabled');
cy.get('[data-cy="itemProposal"]').should('be.enabled'); cy.dataCy('itemProposal').should('be.enabled');
cy.get('[data-cy="transferLines"]').should('be.enabled'); cy.dataCy('transferLines').should('be.enabled');
}); });
}); });
describe('Split', () => {
beforeEach(() => {
cy.get(firstRow).click();
cy.dataCy('transferLines').click();
});
it('Split', () => {
cy.dataCy('ticketTransferPopup').find('.flex > .q-btn').click();
cy.checkNotification(`Ticket ${ticketId}: No split`);
});
});
describe('change quantity', () => {
beforeEach(() => {
cy.get(firstRow).click();
cy.dataCy('changeQuantity').click();
});
it('by popup', () => {
cy.dataCy('New quantity_input').type(10);
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
clickNotificationAction();
});
});
describe('Change state', () => {
beforeEach(() => {
cy.get(firstRow).click();
cy.dataCy('changeState').click();
});
it('by popup', () => {
cy.dataCy('New state_select').should('be.visible');
cy.selectOption('[data-cy="New state_select"]', 'OK');
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
clickNotificationAction();
});
});
describe('Change Item', () => {
beforeEach(() => {
cy.get(firstRow).click();
cy.dataCy('changeItem').click();
});
it('by popup', () => {
cy.dataCy('New item_select').should('be.visible');
cy.selectOption('[data-cy="New item_select"]', 'Palito rojo');
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
cy.checkNotification('Ticket 1000000: price retrieval failed');
cy.dataCy('changeItem').click();
cy.selectOption('[data-cy="New item_select"]', 'Ranged weapon longbow 200cm');
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
clickNotificationAction();
});
after(() => {
cy.visit(`/#/ticket/${ticketId}/sale`);
const quantity = Math.floor(Math.random() * 100) + 1;
const rowIndex = 1;
cy.dataCy('ticketSaleQuantityInput')
.find('input')
.eq(rowIndex)
.clear()
.type(`${quantity}{enter}`);
cy.dataCy('ticketSaleQuantityInput')
.find('input')
.eq(rowIndex)
.should('have.value', `${quantity}`);
});
});
describe('Item proposal', () => { describe('Item proposal', () => {
beforeEach(() => { beforeEach(() => {
cy.get('tr.cursor-pointer > :nth-child(1)').click(); cy.get(firstRow).click();
cy.dataCy('itemProposal').click();
cy.intercept('GET', /\/api\/Items\/getSimilar\?.*$/, {
statusCode: 200,
body: [
{
id: 1,
longName: 'Ranged weapon longbow 50cm',
subName: 'Stark Industries',
tag5: 'Color',
value5: 'Brown',
match5: 0,
match6: 0,
match7: 0,
match8: 1,
tag6: 'Categoria',
value6: '+1 precission',
tag7: 'Tallos',
value7: '1',
tag8: null,
value8: null,
available: 20,
calc_id: 6,
counter: 0,
minQuantity: 1,
visible: null,
price2: 1,
},
{
id: 2,
longName: 'Ranged weapon longbow 100cm',
subName: 'Stark Industries',
tag5: 'Color',
value5: 'Brown',
match5: 0,
match6: 1,
match7: 0,
match8: 1,
tag6: 'Categoria',
value6: '+1 precission',
tag7: 'Tallos',
value7: '1',
tag8: null,
value8: null,
available: 50,
calc_id: 6,
counter: 1,
minQuantity: 5,
visible: null,
price2: 10,
},
{
id: 3,
longName: 'Ranged weapon longbow 200cm',
subName: 'Stark Industries',
tag5: 'Color',
value5: 'Brown',
match5: 1,
match6: 1,
match7: 1,
match8: 1,
tag6: 'Categoria',
value6: '+1 precission',
tag7: 'Tallos',
value7: '1',
tag8: null,
value8: null,
available: 185,
calc_id: 6,
counter: 10,
minQuantity: 10,
visible: null,
price2: 100,
},
],
}).as('getItemGetSimilar');
cy.get('[data-cy="itemProposal"]').click();
cy.wait('@getItemGetSimilar');
}); });
describe.skip('Replace item if', () => { describe('Replace item if', () => {
it('Quantity is less than available', () => { it('Quantity is less than available', () => {
cy.get(':nth-child(1) > .text-right > .q-btn').click(); const index = 2;
cy.colField('tag7', index).click();
cy.checkNotification('Not available for replacement');
});
it('item proposal cells', () => {
const index = 1;
cy.colField('longName', index)
.find('.no-padding > .q-td > .middle')
.should('have.class', 'proposal-primary');
cy.colField('tag5', index)
.find('.no-padding > .match')
.should('have.class', 'match');
cy.colField('tag6', index)
.find('.no-padding > .match')
.should('have.class', 'match');
cy.colField('tag7', index).click();
clickNotificationAction();
}); });
}); });
}); });

View File

@ -1,34 +1,16 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('Ticket Lack list', () => { describe('Ticket Lack list', () => {
beforeEach(() => { beforeEach(() => {
cy.login('developer'); cy.viewport(1980, 1020);
cy.intercept('GET', /Tickets\/itemLack\?.*$/, {
statusCode: 200,
body: [
{
itemFk: 5,
longName: 'Ranged weapon pistol 9mm',
warehouseFk: 1,
producer: null,
size: 15,
category: null,
warehouse: 'Warehouse One',
lack: -50,
inkFk: 'SLV',
timed: '2025-01-25T22:59:00.000Z',
minTimed: '23:59',
originFk: 'Holand',
},
],
}).as('getLack');
cy.login('developer');
cy.visit('/#/ticket/negative'); cy.visit('/#/ticket/negative');
}); });
describe('Table actions', () => { describe('Table actions', () => {
it('should display only one row in the lack list', () => { it('should display only one row in the lack list', () => {
cy.wait('@getLack', { timeout: 10000 }); cy.get('[data-col-field="longName"]').first().click();
cy.dataCy('ItemDescriptor').should('be.visible');
cy.get('.q-virtual-scroll__content > :nth-child(1) > .sticky').click(); cy.get('.q-virtual-scroll__content > :nth-child(1) > .sticky').click();
cy.location('href').should('contain', '#/ticket/negative/5'); cy.location('href').should('contain', '#/ticket/negative/5');
}); });

View File

@ -22,7 +22,7 @@ describe('TicketSale', { testIsolation: true }, () => {
cy.intercept('POST', /\/api\/Sales\/\d+\/updatePrice/).as('updatePrice'); cy.intercept('POST', /\/api\/Sales\/\d+\/updatePrice/).as('updatePrice');
cy.dataCy('saveManaBtn').click(); cy.dataCy('saveManaBtn').click();
handleVnConfirm(); cy.handleVnConfirm();
cy.wait('@updatePrice').its('response.statusCode').should('eq', 200); cy.wait('@updatePrice').its('response.statusCode').should('eq', 200);
cy.get('[data-col-field="price"]') cy.get('[data-col-field="price"]')
@ -43,7 +43,7 @@ describe('TicketSale', { testIsolation: true }, () => {
); );
cy.dataCy('saveManaBtn').click(); cy.dataCy('saveManaBtn').click();
handleVnConfirm(); cy.handleVnConfirm();
cy.wait('@updateDiscount').its('response.statusCode').should('eq', 204); cy.wait('@updateDiscount').its('response.statusCode').should('eq', 204);
cy.get('[data-col-field="discount"]') cy.get('[data-col-field="discount"]')
@ -61,7 +61,7 @@ describe('TicketSale', { testIsolation: true }, () => {
.find('[data-cy="undefined_input"]') .find('[data-cy="undefined_input"]')
.type(concept) .type(concept)
.type('{enter}'); .type('{enter}');
handleVnConfirm(); cy.handleVnConfirm();
cy.get('[data-col-field="item"]').should('contain.text', `${concept}`); cy.get('[data-col-field="item"]').should('contain.text', `${concept}`);
}); });
@ -71,13 +71,9 @@ describe('TicketSale', { testIsolation: true }, () => {
cy.dataCy('ticketSaleQuantityInput').find('input').clear(); cy.dataCy('ticketSaleQuantityInput').find('input').clear();
cy.intercept('POST', '**/api').as('postRequest'); cy.intercept('POST', '**/api').as('postRequest');
cy.dataCy('ticketSaleQuantityInput') cy.dataCy('ticketSaleQuantityInput').find('input').type(`${quantity}{enter}`);
.find('input')
.type(quantity)
.trigger('tab');
cy.get('.q-page > :nth-child(6)').click();
handleVnConfirm(); cy.handleVnConfirm();
cy.get('[data-cy="ticketSaleQuantityInput"]') cy.get('[data-cy="ticketSaleQuantityInput"]')
.find('input') .find('input')
@ -210,8 +206,3 @@ function selectFirstRow() {
cy.waitForElement(firstRow); cy.waitForElement(firstRow);
cy.get(firstRow).find('.q-checkbox__inner').click(); cy.get(firstRow).find('.q-checkbox__inner').click();
} }
function handleVnConfirm() {
cy.confirmVnConfirm();
cy.checkNotification('Data saved');
}

View File

@ -1,3 +1,9 @@
Cypress.Commands.add('handleVnConfirm', () => {
cy.confirmVnConfirm();
cy.checkNotification('Data saved');
});
Cypress.Commands.add('confirmVnConfirm', () => Cypress.Commands.add('confirmVnConfirm', () =>
cy.dataCy('VnConfirm_confirm').should('exist').click(), cy.dataCy('VnConfirm_confirm').should('exist').click(),
); );

View File

@ -18,3 +18,37 @@ Cypress.Commands.add('tableActions', (n = 0, child = 1) =>
`:nth-child(${child}) > .q-table--col-auto-width > [data-cy="tableAction-${n}"] > .q-btn__content > .q-icon`, `:nth-child(${child}) > .q-table--col-auto-width > [data-cy="tableAction-${n}"] > .q-btn__content > .q-icon`,
), ),
); );
Cypress.Commands.add('validateVnTableRows', (opts = {}) => {
let { cols = [] } = opts;
const { rows = [] } = opts;
if (!Array.isArray(cols)) cols = [cols];
const rowSelector = rows.length
? rows.map((row) => `> :nth-child(${row})`).join(', ')
: '> *';
cy.get(`[data-cy="vnTable"] .q-virtual-scroll__content`).within(() => {
cy.get(`${rowSelector}`).each(($el) => {
for (const { name, type = 'string', val, operation = 'equal' } of cols) {
cy.wrap($el)
.find(`[data-cy="vnTableCell_${name}"]`)
.invoke('text')
.then((text) => {
if (type === 'string')
expect(text.trim().toLowerCase()).to[operation](
val.toLowerCase(),
);
if (type === 'number') cy.checkNumber(text, val, operation);
if (type === 'date') cy.checkDate(text, val, operation);
});
}
});
});
});
Cypress.Commands.add('colField', (name, index = null, key = 'data-col-field') => {
if (index) {
cy.get(`:nth-child(${index}) > [${key}="${name}"]`);
} else {
cy.get(`[${key}="${name}"]`);
}
});