hotfix_ticketNegative #1590

Open
jsegarra wants to merge 15 commits from hotfix_ticketNegative into master
32 changed files with 929 additions and 385 deletions

View File

@ -1,4 +1,6 @@
import { boot } from 'quasar/wrappers';
import { date as quasarDate } from 'quasar';
const { formatDate } = quasarDate;
export default boot(() => {
Date.vnUTC = () => {
@ -25,4 +27,26 @@ export default boot(() => {
const date = new Date(Date.vnUTC());
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
};
Date.getCurrentDateTimeFormatted = (
Review

No se si meteria estas funciones aqui

No se si meteria estas funciones aqui
endOfDay = true,
options = { iso: true, mask: 'DD-MM-YYYY HH:mm' },
) => {
const date = Date.vnUTC();
if (endOfDay) {
date.setHours(23, 59, 0);
}
if (options.iso) {
return date.toISOString();
}
return formatDate(date, options.mask);
};
Date.convertToISODateTime = (dateTimeStr) => {
// Convertir de formato "DD-MM-YYYY HH:mm" a ISO
const [datePart, timePart] = dateTimeStr.split(' ');
const [day, month, year] = datePart.split('-');
const [hours, minutes] = timePart.split(':');
const isoDate = new Date(year, month - 1, day, hours, minutes);
return isoDate.toISOString();
};
});

View File

@ -776,7 +776,7 @@ const rowCtrlClickFunction = computed(() => {
:data-col-field="col?.name"
>
<div
class="no-padding no-margin peter"
class="no-padding no-margin"
style="
overflow: hidden;
text-overflow: ellipsis;

View File

@ -0,0 +1,83 @@
<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';
import VnTime from './VnTime.vue';
const $attrs = useAttrs();
const model = defineModel({ type: [Date] });
const $props = defineProps({
isOutlined: {
type: Boolean,
default: false,
},
showEvent: {
type: Boolean,
default: true,
},
});
const styleAttrs = computed(() => {
return $props.isOutlined
? {
dense: true,
outlined: true,
rounded: true,
}
: {};
});
const mask = 'DD-MM-YYYY HH:mm';
const selectedDate = computed({
get() {
if (!model.value) return new Date(model.value);
return date.formatDate(new Date(model.value), mask);
},
set(value) {
model.value = Date.convertToISODateTime(value);
},
});
</script>
<template>
<div @mouseover="hover = true" @mouseleave="hover = false">
<QInput
ref="vnInputDateRef"
v-model="selectedDate"
class="vn-input-date"
placeholder="dd/mm/aaaa HH:mm"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: isRequired }"
:rules="mixinRules"
:clearable="false"
@click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
hide-bottom-space
:data-cy="$attrs.dataCy ?? $attrs.label + '_inputDate'"
>
<template #prepend>
<QIcon name="today" size="xs">
<QPopupProxy cover transition-show="scale" transition-hide="scale">
<VnDate
:mask="mask"
v-model="selectedDate"
></VnDate> </QPopupProxy
></QIcon>
</template>
<template #append>
<QIcon name="access_time" size="xs">
<QPopupProxy cover transition-show="scale" transition-hide="scale">
<VnTime
format24h
:mask="mask"
v-model="selectedDate"
></VnTime> </QPopupProxy
></QIcon>
</template>
</QInput>
</div>
</template>
<i18n>
es:
Open date: Abrir fecha
</i18n>

View File

@ -10,6 +10,10 @@ import { useClipboard } from 'src/composables/useClipboard';
import VnMoreOptions from './VnMoreOptions.vue';
const $props = defineProps({
id: {
type: Number,
default: null,
},
url: {
type: String,
default: '',
@ -67,7 +71,8 @@ onBeforeMount(async () => {
});
// It enables to load data only once if the module is the same as the dataKey
if (!isSameDataKey.value || !route.params.id) await getData();
if (!isSameDataKey.value || !route.params.id || $props.id !== route.params.id)
await getData();
watch(
() => [$props.url, $props.filter],
async () => {

View File

@ -0,0 +1,31 @@
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
export function showResultsAsTable() {
const quasar = useQuasar();
const { t } = useI18n();
function openTable(dialog, results, tag, action, title) {
quasar.notify({
message: t('bulkEdit.completed'),
color: 'positive',
actions: [
{
label: t('bulkEdit.openTable'),
color: 'white',
handler: () => {
quasar.dialog({
component: dialog,
componentProps: {
title,
results,
tag,
action,
},
});
},
},
],
});
}
return { openTable };
}

View File

@ -914,3 +914,8 @@ months:
oct: October
nov: November
dec: December
bulkEdit:
completed: Bulk edit completed
failed: Bulk edit failed
edit: Bulk edit
inProgress: Bulk edit in progress

View File

@ -1001,3 +1001,9 @@ months:
oct: Octubre
nov: Noviembre
dec: Diciembre
bulkEdit:
completed: Edición masiva completada
failed: Edición masiva fallida
edit: Edición masiva
inProgress: Edición masiva en progreso
openTable: Abrir resultados

View File

@ -11,7 +11,7 @@ const $props = defineProps({
</script>
<template>
<QPopupProxy>
<QPopupProxy data-cy="customerDescriptorProxy">
<CustomerDescriptor v-if="$props.id" :id="$props.id" :summary="CustomerSummary" />
</QPopupProxy>
</template>

View File

@ -22,7 +22,7 @@ const $props = defineProps({
});
</script>
<template>
<QPopupProxy style="max-width: 10px">
<QPopupProxy style="max-width: 10px" data-cy="itemDescriptorProxy">
<ItemDescriptor
v-if="$props.id"
:id="$props.id"

View File

@ -6,9 +6,10 @@ import { toCurrency } from 'filters/index';
import VnStockValueDisplay from 'src/components/ui/VnStockValueDisplay.vue';
import VnTable from 'src/components/VnTable/VnTable.vue';
import axios from 'axios';
import notifyResults from 'src/utils/notifyResults';
import { showResultsAsTable } from 'src/composables/showResultsTable';
import FetchData from 'components/FetchData.vue';
const { openTable } = showResultsAsTable();
import HandleLackDialog from 'src/pages/Ticket/Negative/components/HandleLackDialog.vue';
const MATCH = 'match';
const { t } = useI18n();
@ -32,7 +33,10 @@ const $props = defineProps({
const proposalSelected = ref([]);
const ticketConfig = ref({});
const proposalTableRef = ref(null);
const isLoading = ref(true);
const filterTicketConfig = {
fields: ['lackAlertPrice'],
};
const sale = computed(() => $props.sales[0]);
const saleFk = computed(() => sale.value.saleFk);
const filter = computed(() => ({
@ -77,7 +81,7 @@ const columns = computed(() => [
},
{
align: 'left',
align: 'center',
sortable: true,
label: t('proposal.longName'),
name: 'longName',
@ -85,7 +89,7 @@ const columns = computed(() => [
columnClass: 'expand',
},
{
align: 'left',
align: 'center',
sortable: true,
label: t('item.list.color'),
name: 'tag5',
@ -93,7 +97,7 @@ const columns = computed(() => [
columnClass: 'expand',
},
{
align: 'left',
align: 'center',
sortable: true,
label: t('item.list.stems'),
name: 'tag6',
@ -101,7 +105,7 @@ const columns = computed(() => [
columnClass: 'expand',
},
{
align: 'left',
align: 'center',
sortable: true,
label: t('item.list.producer'),
name: 'tag7',
@ -197,9 +201,10 @@ const isSelectionAvailable = (itemProposal) => {
async function change({ itemFk: substitutionFk }) {
try {
const promises = $props.sales.map(({ saleFk, quantity }) => {
const promises = $props.sales.map(({ saleFk, quantity, ticketFk }) => {
const params = {
saleFk,
ticketFk,
substitutionFk,
quantity,
};
@ -207,10 +212,9 @@ async function change({ itemFk: substitutionFk }) {
});
const results = await Promise.allSettled(promises);
notifyResults(results, 'saleFk');
openTable(HandleLackDialog, results, 'ticketFk', 'changeItem');
emit('itemReplaced', {
type: 'refresh',
quantity: quantity.value,
itemProposal: proposalSelected.value[0],
});
proposalSelected.value = [];
@ -221,18 +225,20 @@ async function change({ itemFk: substitutionFk }) {
async function handleTicketConfig(data) {
ticketConfig.value = data[0];
isLoading.value = false;
}
</script>
<template>
<FetchData
url="TicketConfigs"
:filter="{ fields: ['lackAlertPrice'] }"
@on-fetch="handleTicketConfig"
:filter="filterTicketConfig"
auto-load
/>
@on-fetch="handleTicketConfig"
></FetchData>
<QSpinner v-if="isLoading" color="primary" size="md"></QSpinner>
<VnTable
v-if="ticketConfig"
v-else
auto-load
data-cy="proposalTable"
ref="proposalTableRef"
@ -242,7 +248,7 @@ async function handleTicketConfig(data) {
:columns="columns"
class="full-width q-mt-md"
row-key="id"
:row-click="change"
redirect="false"
:is-editable="false"
:right-search="false"
:without-header="true"
@ -260,10 +266,10 @@ async function handleTicketConfig(data) {
<QTooltip> {{ statusConditionalValue(row) }}% </QTooltip>
</div>
<div style="flex: 2 0 100%; align-content: center">
<div>
<span class="link">{{ row.longName }}</span>
<ItemDescriptorProxy :id="row.id" />
</div>
<span class="link" @click.stop>
{{ row.longName }}
<ItemDescriptorProxy :id="row.itemFk" />
</span>
</div>
</QTd>
</template>

View File

@ -19,16 +19,18 @@ const $props = defineProps({
default: () => [],
},
});
const { dialogRef } = useDialogPluginComponent();
const emit = defineEmits([
'onDialogClosed',
'itemReplaced',
...useDialogPluginComponent.emits,
]);
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent();
const emit = defineEmits(['onDialogClosed', ...useDialogPluginComponent.emits]);
defineExpose({ show: () => dialogRef.value.show(), hide: () => dialogRef.value.hide() });
</script>
<template>
<QDialog ref="dialogRef" transition-show="scale" transition-hide="scale">
<QDialog
ref="dialogRef"
transition-show="scale"
transition-hide="scale"
@hide="onDialogHide"
>
<QCard class="dialog-width">
<QCardSection class="row items-center q-pb-none">
<span class="text-h6 text-grey">{{ $t('Item proposal') }}</span>
@ -39,8 +41,8 @@ defineExpose({ show: () => dialogRef.value.show(), hide: () => dialogRef.value.h
<ItemProposal
v-bind="$props"
@item-replaced="
(data) => {
emit('itemReplaced', data);
() => {
emit('onDialogClosed', true);
dialogRef.hide();
}
"

View File

@ -44,6 +44,7 @@ function ticketFilter(ticket) {
@on-fetch="(data) => ([problems] = data)"
/>
<CardDescriptor
:id="entityId"
:url="`Tickets/${entityId}`"
:filter="filter"
data-key="Ticket"

View File

@ -10,7 +10,7 @@ const $props = defineProps({
});
</script>
<template>
<QPopupProxy>
<QPopupProxy data-cy="ticketDescriptorProxy">
<TicketDescriptor v-if="$props.id" :id="$props.id" :summary="TicketSummary" />
</QPopupProxy>
</template>

View File

@ -2,7 +2,7 @@
import { ref } from 'vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import split from './components/split';
import { splitTicket } from './components/split';
const emit = defineEmits(['ticketTransfered']);
const $props = defineProps({
@ -12,6 +12,8 @@ const $props = defineProps({
},
});
const { split } = splitTicket();
const splitDate = ref(Date.vnNew());
const splitSelectedRows = async () => {

View File

@ -0,0 +1,112 @@
<script setup>
import TicketProblems from 'src/components/TicketProblems.vue';
import FetchedTags from 'src/components/ui/FetchedTags.vue';
import { dashIfEmpty, toDate, toCurrency } from 'src/filters';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import { ref } from 'vue';
const props = defineProps({
sales: {
type: Array,
required: true,
},
ticket: {
type: Object,
required: true,
},
});
const sales = ref(props?.sales);
</script>
<template>
<QTable v-if="sales" :rows="sales" style="text-align: center">
<template #body-cell="{ value }">
<QTd>{{ value }}</QTd>
</template>
<template #header="props">
<QTr class="tr-header" :props="props">
<QTh auto-width></QTh>
<QTh auto-width>{{ $t('globals.item') }}</QTh>
<QTh auto-width>{{ $t('globals.visible') }}</QTh>
<QTh auto-width>{{ $t('ticket.summary.available') }}</QTh>
<QTh auto-width>{{ $t('globals.quantity') }}</QTh>
<QTh auto-width>{{ $t('globals.description') }}</QTh>
<QTh auto-width>{{ $t('globals.price') }}</QTh>
<QTh auto-width>{{ $t('ticket.summary.discount') }}</QTh>
<QTh auto-width>{{ $t('globals.amount') }}</QTh>
<QTh auto-width>{{ $t('ticket.summary.packing') }}</QTh>
</QTr>
</template>
<template #body="props">
<QTr :props="props">
<QTd class="q-gutter-x-xs">
<TicketProblems :row="props.row" />
</QTd>
<QTd>
<QBtn class="link" flat>
{{ props.row.itemFk }}
<ItemDescriptorProxy
:id="props.row.itemFk"
:sale-fk="props.row.id"
:warehouse-fk="$props.ticket.warehouseFk"
/>
</QBtn>
</QTd>
<QTd>
<QChip
v-if="props.row.visible < 0"
dense
rounded
:color="'negative'"
text-color="white"
>
{{ props.row.visible }}
</QChip>
<span v-else>
{{ props.row.visible }}
</span>
</QTd>
<QTd>
<QChip
v-if="props.row.available < 0"
dense
rounded
:color="'negative'"
text-color="white"
>
{{ props.row.available }}
</QChip>
<span v-else>
{{ props.row.available }}
</span>
</QTd>
<QTd>{{ props.row.quantity }}</QTd>
<QTd class="description-cell">
<div class="row full-width justify-between">
{{ props.row.concept }}
<div v-if="props.row.item.subName" class="subName">
{{ props.row.item.subName.toUpperCase() }}
</div>
</div>
<FetchedTags
class="fetched-tags"
:item="props.row.item"
></FetchedTags>
</QTd>
<QTd>{{ props.row.price }} </QTd>
<QTd>{{ props.row.discount }} %</QTd>
<QTd
>{{
toCurrency(
props.row.quantity *
props.row.price *
((100 - props.row.discount) / 100),
)
}}
</QTd>
<QTd>{{ dashIfEmpty(props.row.item.itemPackingTypeFk) }}</QTd>
</QTr>
</template>
</QTable>
</template>

View File

@ -0,0 +1,42 @@
<script setup>
import FetchData from 'src/components/FetchData.vue';
import { dashIfEmpty, toDate, toCurrency } from 'src/filters';
import { ref } from 'vue';
import TicketSaleTable from './TicketSaleTable.vue';
const props = defineProps({
sales: {
type: Array,
required: true,
},
ticket: {
type: Number,
required: true,
},
});
const sales = ref(props?.sales);
</script>
<template>
<QDialog ref="dialogRef" full-width data-cy="ticketSaleTableDialog">
<FetchData
v-if="!sales"
:url="`Tickets/${$props.ticket}/getSales`"
@on-fetch="(data) => (sales = data)"
auto-load
></FetchData>
<QCard class="q-pa-sm">
<QCardSection class="row items-center q-pb-none">
<span class="text-h6 text-grey">{{
$t('ticket.summary.saleLines')
}}</span>
<QSpace />
<QBtn icon="close" flat round dense v-close-popup />
</QCardSection>
<QCardSection>
<TicketSaleTable v-if="sales" :ticket="$props.ticket" :sales="sales" />
</QCardSection>
</QCard>
</QDialog>
</template>

View File

@ -1,22 +1,24 @@
import axios from 'axios';
import notifyResults from 'src/utils/notifyResults';
import { showResultsAsTable } from 'src/composables/showResultsTable';
import HandleLackDialog from 'src/pages/Ticket/Negative/components/HandleLackDialog.vue';
export function splitTicket() {
const { openTable } = showResultsAsTable();
export default async function (data, date) {
async function split(data, date) {
const reducedData = data.reduce((acc, item) => {
const existing = acc.find(({ ticketFk }) => ticketFk === item.id);
const existing = acc.find(({ ticketFk }) => ticketFk === item.ticketFk);
if (existing) {
existing.sales.push(item.saleFk);
} else {
acc.push({ ticketFk: item.id, sales: [item.saleFk], date });
acc.push({ ticketFk: item.ticketFk, sales: [item.saleFk], landed: date });
}
return acc;
}, []);
const promises = reducedData.map((params) => axios.post(`Tickets/split`, params));
const results = await Promise.allSettled(promises);
notifyResults(results, 'ticketFk');
return results;
const result = await Promise.allSettled(promises);
openTable(HandleLackDialog, result, 'ticket', 'split');
}
return { split };
}

View File

@ -68,7 +68,8 @@ const showItemProposal = () => {
sales: selectedRows.value,
},
})
.onOk(itemProposalEvt);
.onOk(reload)
.onCancel(reload);
};
</script>
@ -146,7 +147,12 @@ const showItemProposal = () => {
<template v-slot="{ popup }">
<ChangeItemDialog
ref="changeItemDialogRef"
@update-item="popup.hide()"
@update-item="
() => {
popup.hide();
reload();
}
"
:selected-rows="selectedRows"
/></template>
</VnPopupProxy>
@ -160,7 +166,12 @@ const showItemProposal = () => {
<template v-slot="{ popup }">
<ChangeStateDialog
ref="changeStateDialogRef"
@update-state="popup.hide()"
@update-state="
() => {
popup.hide();
reload();
}
"
:selected-rows="selectedRows"
/></template>
</VnPopupProxy>
@ -175,7 +186,12 @@ const showItemProposal = () => {
<template v-slot="{ popup }">
<ChangeQuantityDialog
ref="changeQuantityDialogRef"
@update-quantity="popup.hide()"
@update-quantity="
() => {
popup.hide();
reload();
}
"
:selected-rows="selectedRows"
/></template>
</VnPopupProxy> </QBtnGroup

View File

@ -6,6 +6,7 @@ import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDateTime from 'src/components/common/VnInputDateTime.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
@ -77,17 +78,11 @@ const setUserParams = (params) => {
<QList dense class="q-gutter-y-sm q-mt-sm">
<QItem>
<QItemSection>
<VnInput
v-model="params.days"
:label="t('negative.days')"
<VnInputDateTime
v-model="params.availabled"
:label="t('negative.availabled')"
dense
is-outlined
type="number"
@update:model-value="
(value) => {
setUserParams(params);
}
"
/>
</QItemSection>
</QItem>
@ -102,25 +97,6 @@ const setUserParams = (params) => {
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.producer"
:label="t('negative.producer')"
dense
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.origen"
:label="t('negative.origen')"
dense
is-outlined
/>
</QItemSection> </QItem
><QItem>
<QItemSection v-if="categoriesOptions">
<VnSelect
:label="t('negative.categoryFk')"

View File

@ -21,14 +21,13 @@ const selectedRows = ref([]);
const tableRef = ref();
const filterParams = ref({});
const negativeParams = reactive({
days: useRole().likeAny('buyer') ? 2 : 0,
warehouseFk: useState().getUser().value.warehouseFk,
availabled: Date.getCurrentDateTimeFormatted(),
});
const redirectToCreateView = ({ itemFk }) => {
router.push({
name: 'NegativeDetail',
params: { id: itemFk },
query: { days: filterParams.value.days ?? negativeParams.days },
});
};
const columns = computed(() => [
@ -65,15 +64,19 @@ const columns = computed(() => [
columnFilter: {
component: 'input',
type: 'number',
columnClass: 'shrink',
inWhere: false,
},
},
{
name: 'longName',
align: 'center',
align: 'left',
label: t('negative.longName'),
field: ({ longName }) => longName,
columnFilter: {
component: 'input',
inWhere: false,
useLike: true,
},
sortable: true,
headerStyle: 'width: 350px',
cardVisible: true,
@ -94,6 +97,11 @@ const columns = computed(() => [
field: ({ inkFk }) => inkFk,
sortable: true,
cardVisible: true,
columnFilter: {
component: 'input',
columnClass: 'shrink',
inWhere: false,
},
},
{
name: 'size',
@ -155,7 +163,6 @@ const setUserParams = (params) => {
<TicketLackFilter data-key="NegativeList" @set-user-params="setUserParams" />
</template>
</RightMenu>
{{ filterRef }}
<VnTable
ref="tableRef"
data-key="NegativeList"

View File

@ -5,7 +5,7 @@ import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import FetchData from 'src/components/FetchData.vue';
import { toDate, toHour } from 'src/filters';
import { toDate } from 'src/filters';
import useNotify from 'src/composables/useNotify.js';
import ZoneDescriptorProxy from 'pages/Zone/Card/ZoneDescriptorProxy.vue';
import { useRoute } from 'vue-router';
@ -22,14 +22,6 @@ const $props = defineProps({
},
});
watch(
() => $props.filter,
(v) => {
filterLack.value.where = v;
tableRef.value.reload(filterLack);
},
);
const filterLack = ref({
include: [
{
@ -90,6 +82,7 @@ const columns = computed(() => [
},
{
name: 'alertLevelCode',
field: 'alertLevelCode',
label: t('negative.detail.state'),
columnFilter: {
name: 'alertLevelCode',
@ -108,14 +101,14 @@ const columns = computed(() => [
name: 'zoneName',
label: t('negative.detail.zoneName'),
field: 'zoneName',
align: 'center',
align: 'left',
sortable: true,
},
{
name: 'nickname',
label: t('negative.detail.nickname'),
field: 'nickname',
align: 'center',
align: 'left',
sortable: true,
},
{
@ -139,7 +132,6 @@ const getInputEvents = ({ col, ...rows }) => ({
'keyup.enter': () => saveChange(col.name, rows),
});
const saveChange = async (field, { row }) => {
try {
switch (field) {
case 'alertLevelCode':
await axios.post(`Tickets/state`, {
@ -156,10 +148,6 @@ const saveChange = async (field, { row }) => {
}
notify('globals.dataSaved', 'positive');
fetchItemLack.value.fetch();
} catch (err) {
console.error('Error saving changes', err);
f;
}
};
function onBuysFetched(data) {
@ -171,7 +159,7 @@ function onBuysFetched(data) {
<FetchData
ref="fetchItemLack"
:url="`Tickets/itemLack`"
:params="{ id: entityId }"
:params="{ itemFk: entityId, warehouseFk: $props.filter.warehouseFk }"
@on-fetch="(data) => (itemLack = data[0])"
auto-load
/>
@ -238,8 +226,6 @@ function onBuysFetched(data) {
</template>
<template #column-status="{ row }">
<QTd style="min-width: 150px">
<div class="icon-container">
<QIcon
v-if="row.isBasket"
name="vn:basket"
@ -265,9 +251,7 @@ function onBuysFetched(data) {
class="cursor-pointer"
size="xs"
>
<QTooltip>{{
t('negative.detail.hasObservation')
}}</QTooltip> </QIcon
<QTooltip>{{ t('negative.detail.hasObservation') }}</QTooltip> </QIcon
><QIcon
v-if="row.isRookie"
name="vn:Person"
@ -295,8 +279,6 @@ function onBuysFetched(data) {
>
<QTooltip>{{ t('negative.detail.turno') }}</QTooltip>
</QIcon>
</div></QTd
>
</template>
<template #column-nickname="{ row }">
<span class="link" @click.stop>
@ -305,9 +287,9 @@ function onBuysFetched(data) {
</span>
</template>
<template #column-ticketFk="{ row }">
<span class="q-pa-sm link">
{{ row.id }}
<TicketDescriptorProxy :id="row.id" />
<span class="link" @click.stop>
{{ row.ticketFk }}
<TicketDescriptorProxy :id="row.ticketFk" />
</span>
</template>
<template #column-alertLevelCode="props">
@ -335,15 +317,6 @@ function onBuysFetched(data) {
</VnTable>
</template>
<style lang="scss" scoped>
.icon-container {
display: grid;
grid-template-columns: repeat(3, 0.2fr);
row-gap: 5px; /* Ajusta el espacio entre los iconos según sea necesario */
}
.icon-container > * {
width: 100%;
height: auto;
}
.list-enter-active,
.list-leave-active {
transition: all 1s ease;

View File

@ -1,8 +1,11 @@
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import VnSelect from 'src/components/common/VnSelect.vue';
import notifyResults from 'src/utils/notifyResults';
import { showResultsAsTable } from 'src/composables/showResultsTable';
import HandleLackDialog from './HandleLackDialog.vue';
const { openTable } = showResultsAsTable();
const emit = defineEmits(['update-item']);
const showChangeItemDialog = ref(false);
@ -17,15 +20,17 @@ const $props = defineProps({
const updateItem = async () => {
try {
showChangeItemDialog.value = true;
const rowsToUpdate = $props.selectedRows.map(({ saleFk, quantity }) =>
const rowsToUpdate = $props.selectedRows.map(({ ticketFk, saleFk, quantity }) =>
axios.post(`Sales/replaceItem`, {
ticketFk,
saleFk,
substitutionFk: newItem.value,
quantity,
}),
);
const result = await Promise.allSettled(rowsToUpdate);
notifyResults(result, 'saleFk');
openTable(HandleLackDialog, result, 'ticketFk', 'changeItem');
emit('update-item', newItem.value);
} catch (err) {
console.error('Error updating item:', err);
@ -37,9 +42,9 @@ const updateItem = async () => {
<template>
<QCard class="q-pa-sm">
<QCardSection class="row items-center justify-center column items-stretch">
{{ showChangeItemDialog }}
<span>{{ $t('negative.detail.modal.changeItem.title') }}</span>
<VnSelect
:label="$t('negative.detail.modal.changeItem.placeholder')"
url="Items/WithName"
:fields="['id', 'name']"
:sort-by="['id DESC']"

View File

@ -2,8 +2,9 @@
import { ref } from 'vue';
import axios from 'axios';
import VnInput from 'src/components/common/VnInput.vue';
import notifyResults from 'src/utils/notifyResults';
import { showResultsAsTable } from 'src/composables/showResultsTable';
import HandleLackDialog from './HandleLackDialog.vue';
const { openTable } = showResultsAsTable();
const showChangeQuantityDialog = ref(false);
const newQuantity = ref(null);
const $props = defineProps({
@ -14,7 +15,6 @@ const $props = defineProps({
});
const emit = defineEmits(['update-quantity']);
const updateQuantity = async () => {
try {
showChangeQuantityDialog.value = true;
const rowsToUpdate = $props.selectedRows.map(({ saleFk }) =>
axios.post(`Sales/${saleFk}/updateQuantity`, {
@ -24,12 +24,8 @@ const updateQuantity = async () => {
);
const result = await Promise.allSettled(rowsToUpdate);
notifyResults(result, 'saleFk');
openTable(HandleLackDialog, result, 'ticketFk', 'changeQuantity');
emit('update-quantity', newQuantity.value);
} catch (err) {
return err;
}
};
</script>

View File

@ -3,7 +3,9 @@ import { ref } from 'vue';
import axios from 'axios';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import notifyResults from 'src/utils/notifyResults';
import { showResultsAsTable } from 'src/composables/showResultsTable';
import HandleLackDialog from './HandleLackDialog.vue';
const { openTable } = showResultsAsTable();
const emit = defineEmits(['update-state']);
const editableStates = ref([]);
@ -16,21 +18,18 @@ const $props = defineProps({
},
});
const updateState = async () => {
try {
showChangeStateDialog.value = true;
const rowsToUpdate = $props.selectedRows.map(({ id }) =>
const rowsToUpdate = $props.selectedRows.map(({ ticketFk }) =>
axios.post(`Tickets/state`, {
ticketFk: id,
ticketFk,
code: newState.value,
}),
);
const result = await Promise.allSettled(rowsToUpdate);
notifyResults(result, 'ticketFk');
const result = await Promise.allSettled(rowsToUpdate);
openTable(HandleLackDialog, result, 'ticketFk', 'changeState');
emit('update-state', newState.value);
} catch (err) {
return err;
}
};
</script>
@ -47,8 +46,6 @@ const updateState = async () => {
:label="$t('negative.detail.modal.changeState.placeholder')"
v-model="newState"
:options="editableStates"
option-label="name"
option-value="code"
/>
</QCardSection>
<QCardActions align="right">

View File

@ -0,0 +1,226 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import FetchData from 'components/FetchData.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import TicketSaleTable from 'src/pages/Ticket/Card/components/TicketSaleTable.vue';
import { useQuasar } from 'quasar';
import TicketSaleTableDialog from '../../Card/components/TicketSaleTableDialog.vue';
const quasar = useQuasar();
const { t } = useI18n();
const $props = defineProps({
results: {
type: Array,
default: () => [],
},
tag: {
type: String,
default: 'ticketFk',
},
title: {
type: String,
default: 'bulkEdit.edit',
},
action: {
type: String,
default: '',
},
});
const results = ref($props.results);
const states = ref([]);
const columns = computed(() => [
{
name: 'status',
label: t('negative.split.status'),
field: ({ status }) => status,
sortable: true,
align: 'center',
},
{
name: 'value',
label: t('negative.split.ticket'),
sortable: true,
align: 'center',
},
{
name: 'statusText',
label: t('negative.split.message'),
sortable: true,
align: 'center',
},
{
name: 'actions',
label: '',
sortable: false,
align: 'right',
},
]);
function getState(value) {
return states.value.find((state) => state.id === value)?.name;
}
function getTicketRejected(row) {
return JSON.parse(row.reason.config.data)?.ticketFk;
}
function getTicketFulFilled(row) {
return JSON.parse(row.value.config.data).ticketFk;
}
function getIcon(value) {
const icons = {
fulfilled: {
name: 'check_circle',
color: 'secondary',
},
noSplit: {
name: 'warning',
color: 'primary',
},
rejected: {
name: 'close',
color: 'negative',
},
};
return icons[value];
}
function openLinesDialog(row) {
const ticket =
row.status === 'fulfilled'
? (row.value.data[$props.tag] ?? getTicketFulFilled(row))
: getTicketRejected(row);
quasar.dialog({
component: TicketSaleTableDialog,
componentProps: {
ticket: ticket,
sales: null,
},
});
}
</script>
<template>
<QDialog ref="dialogRef" @hide="onDialogHide" data-cy="HandleLackDialog" persistent>
<FetchData
v-if="$props.action === 'changeState'"
url="States"
@on-fetch="(data) => (states = data)"
auto-load
/>
<QCard class="q-pa-sm" style="width: 60vw; max-width: 80vw">
<QCardSection class="row items-center q-pb-none">
<QAvatar
:icon="icon"
color="primary"
text-color="white"
size="xl"
v-if="icon"
/>
<span class="text-h6 text-grey"
>{{ t($props.title) }} ({{ $props.action }})
</span>
<QSpace />
<QBtn icon="close" flat round dense v-close-popup />
</QCardSection>
<QCardSection class="row items-center justify-center column items-stretch">
<QTable
:rows="results"
:columns="columns"
row-key="newTicket"
:no-data-label="t('globals.noResults')"
flat
dense
hide-bottom
auto-load
>
<template #header="props">
<QTr :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</QTh>
</QTr>
</template>
<template #body-cell-status="{ value }">
<QTd align="center">
<QIcon
:name="`${getIcon(value)?.name}`"
size="xs"
class="cursor-pointer"
:color="getIcon(value)?.color"
>
</QIcon
></QTd>
</template>
<template #body-cell-value="{ row }">
<QTd align="center">
<div class="link" @click.stop>
<div v-if="row.status === 'fulfilled'">
<span v-if="$props.action !== 'changeItem'">
{{ row.value.data[$props.tag] }}
<TicketDescriptorProxy
:id="row.value.data[$props.tag]"
>
</TicketDescriptorProxy>
</span>
<span v-else
>{{ getTicketFulFilled(row)
}}<TicketDescriptorProxy
:id="getTicketFulFilled(row)"
>
</TicketDescriptorProxy>
</span>
</div>
<span v-else>
{{ getTicketRejected(row)
}}<TicketDescriptorProxy :id="getTicketRejected(row)">
</TicketDescriptorProxy
></span>
</div>
</QTd>
</template>
<template #body-cell-statusText="{ row }">
<QTd align="center">
<div v-if="row.status === 'fulfilled'">
<div v-if="$props.action === 'changeState'">
{{ getState(row.value.data.stateFk) }}
</div>
<div v-if="$props.action === 'split'">
<span>
{{ row.value.data.status }}
</span>
</div>
<div v-if="$props.action === 'changeItem'">
<span class="link" @click.stop>
{{ value }}
<ItemDescriptorProxy :id="value">
</ItemDescriptorProxy>
</span>
</div>
<div v-if="$props.action === 'changeQuantity'">
{{ row.value.data.quantity }}
</div>
</div>
<div v-else>{{ row.reason.response.data.error.message }}</div>
</QTd>
</template>
<template #body-cell-actions="props">
<QTd align="center">
<div v-if="['changeItem'].includes($props.action)">
<QIcon
name="list"
size="xs"
color="primary"
class="cursor-pointer"
@click="openLinesDialog(props.row)"
>
<QTooltip>{{ t('ticketList.toLines') }} </QTooltip>
</QIcon>
</div>
</QTd>
</template>
</QTable>
</QCardSection>
</QCard>
</QDialog>
</template>

View File

@ -237,6 +237,7 @@ negative:
minTimed: minTimed
negativeAction: Negative
totalNegative: Total negatives
availabled: Availabled
days: Days
buttonsUpdate:
item: Item

View File

@ -218,7 +218,8 @@ ticketList:
negative:
hour: Hora
id: Id Articulo
longName: Articulo
longName: Artículo
itemFk: Artículo
supplier: Productor
colour: Color
size: Medida
@ -234,6 +235,7 @@ negative:
inkFk: Color
timed: Hora
date: Fecha
availabled: Disponible
minTimed: Hora
type: Tipo
negativeAction: Negativo
@ -286,7 +288,7 @@ negative:
title: Gestionar tickets spliteados
subTitle: Confir fecha y agencia
split:
ticket: Ticket viejo
ticket: Ticket origen
newTicket: Ticket nuevo
status: Estado
message: Mensaje

View File

@ -11,7 +11,12 @@ const $props = defineProps({
</script>
<template>
<QPopupProxy>
<ZoneDescriptor v-if="$props.id" :id="$props.id" :summary="ZoneSummary" />
<QPopupProxy data-cy="zoneDescriptorProxy">
<ZoneDescriptor
v-if="$props.id"
:id="$props.id"
:summary="ZoneSummary"
data-cy="zoneDescriptorProxy"
/>
</QPopupProxy>
</template>

View File

@ -1,147 +1,178 @@
/// <reference types="cypress" />
const firstRow = 'tr.cursor-pointer > :nth-child(1)';
const ticketId = 1000000;
const findTr = (index) => `tbody > tr > :nth-child(${index}) > div`;
const clickNotificationAction = () => {
const notification = '.q-notification';
cy.waitForElement(notification);
cy.get(notification).should('be.visible');
cy.get(notification).find('.q-btn').click();
cy.get(notification).should('not.be.visible');
};
describe('Ticket Lack detail', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.intercept('GET', /\/api\/Tickets\/itemLack\/5.*$/, {
statusCode: 200,
body: [
{
saleFk: 33,
code: 'OK',
ticketFk: 142,
nickname: 'Malibu Point',
shipped: '2000-12-31T23:00:00.000Z',
hour: 0,
quantity: 50,
agName: 'Super-Man delivery',
alertLevel: 0,
stateName: 'OK',
stateId: 3,
itemFk: 5,
price: 1.79,
alertLevelCode: 'FREE',
zoneFk: 9,
zoneName: 'Zone superMan',
theoreticalhour: '2011-11-01T22:59:00.000Z',
isRookie: 1,
turno: 1,
peticionCompra: 1,
hasObservation: 1,
hasToIgnore: 1,
isBasket: 1,
minTimed: 0,
customerId: 1104,
customerName: 'Tony Stark',
observationTypeCode: 'administrative',
},
],
}).as('getItemLack');
cy.intercept('GET', /\/api\/Tickets\/itemLack\/88.*$/).as('getItemLack');
cy.visit('/#/ticket/negative/88');
cy.visit('/#/ticket/negative/5');
cy.wait('@getItemLack');
cy.wait('@getItemLack').then((interception) => {
const { query } = interception.request;
const filter = JSON.parse(query.filter);
expect(filter).to.have.property('where');
expect(filter.where).to.have.property('alertLevelCode', 'FREE');
});
describe('Table actions', () => {
it.skip('should display only one row in the lack list', () => {
cy.location('href').should('contain', '#/ticket/negative/5');
cy.get('[data-cy="changeItem"]').should('be.disabled');
cy.get('[data-cy="changeState"]').should('be.disabled');
cy.get('[data-cy="changeQuantity"]').should('be.disabled');
cy.get('[data-cy="itemProposal"]').should('be.disabled');
cy.get('[data-cy="transferLines"]').should('be.disabled');
cy.domContentLoad();
cy.waitForElement('.q-table');
});
describe('Table detail', () => {
it('should open descriptors', () => {
cy.get('.q-table').should('be.visible');
cy.colField('zoneName').click();
cy.dataCy('zoneDescriptorProxy').should('be.visible');
cy.get('.q-item > .q-item__label').should('have.text', ' #1');
cy.colField('ticketFk').click();
cy.dataCy('ticketDescriptorProxy').should('be.visible');
cy.get('.q-item > .q-item__label').should('have.text', ` #${ticketId}`);
cy.colField('nickname').find('.link').click();
cy.waitForElement('[data-cy="customerDescriptorProxy"]');
cy.dataCy('customerDescriptorProxy').should('be.visible');
cy.get('.q-item > .q-item__label').should('have.text', ' #1');
});
it('should display only one row in the lack list', () => {
cy.dataCy('changeItem').should('be.disabled');
cy.dataCy('changeState').should('be.disabled');
cy.dataCy('changeQuantity').should('be.disabled');
cy.dataCy('itemProposal').should('be.disabled');
cy.dataCy('transferLines').should('be.disabled');
cy.get('tr.cursor-pointer > :nth-child(1)').click();
cy.get('[data-cy="changeItem"]').should('be.enabled');
cy.get('[data-cy="changeState"]').should('be.enabled');
cy.get('[data-cy="changeQuantity"]').should('be.enabled');
cy.get('[data-cy="itemProposal"]').should('be.enabled');
cy.get('[data-cy="transferLines"]').should('be.enabled');
cy.dataCy('changeItem').should('be.enabled');
cy.dataCy('changeState').should('be.enabled');
cy.dataCy('changeQuantity').should('be.enabled');
cy.dataCy('itemProposal').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();
clickNotificationAction();
cy.dataCy('HandleLackDialog').should('be.visible');
cy.dataCy('HandleLackDialog').find('tbody > tr > :nth-child(1) > .q-icon');
cy.dataCy('HandleLackDialog').find(findTr(2)).should('have.class', 'link');
cy.dataCy('HandleLackDialog')
.find(`${findTr(2)}.link > div > span`)
.should('have.text', `${ticketId} `);
cy.dataCy('HandleLackDialog').find(findTr(3)).should('have.text', 'noSplit');
});
});
describe('Change Item', () => {
beforeEach(() => {
cy.get(firstRow).click();
cy.dataCy('changeItem').click();
});
it('Change failed', () => {
cy.dataCy('New item_select').should('be.visible').click();
cy.get('.q-item').contains('Palito rojo').click();
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
clickNotificationAction();
cy.dataCy('HandleLackDialog').should('be.visible');
cy.dataCy('HandleLackDialog').find('tbody > tr > :nth-child(1) > .q-icon');
cy.dataCy('HandleLackDialog').find(findTr(2)).should('have.class', 'link');
cy.dataCy('HandleLackDialog')
.find(`${findTr(2)}.link > span`)
.should('have.text', `${ticketId}`);
cy.dataCy('HandleLackDialog')
.find(findTr(3))
.should('have.text', 'price retrieval failed');
});
});
describe('Change state', () => {
beforeEach(() => {
cy.get(firstRow).click();
cy.dataCy('changeState').click();
});
it('Change success', () => {
cy.dataCy('New state_select').should('be.visible').click();
cy.get('.q-item').contains('OK').click();
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
clickNotificationAction();
cy.dataCy('HandleLackDialog')
.should('be.visible')
.find('tbody > tr > :nth-child(1) > .q-icon');
cy.dataCy('HandleLackDialog').find(findTr(2)).should('have.class', 'link');
cy.dataCy('HandleLackDialog')
.find(`${findTr(2)}.link > div > span`)
.should('have.text', `${ticketId} `);
cy.dataCy('HandleLackDialog').find(findTr(3)).should('have.text', 'OK');
});
});
describe('change quantity', () => {
beforeEach(() => {
cy.get(firstRow).click();
cy.dataCy('changeQuantity').click();
});
it('Change success', () => {
cy.dataCy('New quantity_input').type(10);
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
clickNotificationAction();
cy.dataCy('HandleLackDialog')
.should('be.visible')
.find('tbody > tr > :nth-child(1) > .q-icon');
cy.dataCy('HandleLackDialog').find(findTr(2)).should('have.class', 'link');
cy.dataCy('HandleLackDialog')
.find(`${findTr(2)}.link > div > span`)
.should('have.text', `${ticketId} `);
cy.dataCy('HandleLackDialog').find(findTr(3)).should('have.text', '10');
});
});
describe('Item proposal', () => {
beforeEach(() => {
cy.get('tr.cursor-pointer > :nth-child(1)').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');
cy.get(firstRow).click();
cy.dataCy('itemProposal').click();
});
describe.skip('Replace item if', () => {
it('Quantity is less than available', () => {
cy.get(':nth-child(1) > .text-right > .q-btn').click();
});
it('item proposal cells', () => {
// TODO: Fix this step
Review

Quitar?

Quitar?
// cy.get(
// ':nth-child(1) > .q-table--col-auto-width > .q-btn > .q-btn__content > .q-icon',
// ).should('not.be.visible');
cy.get('[data-col-field="longName"] .link').first().click();
cy.dataCy('itemDescriptorProxy').should('be.visible');
cy.colField('longName', 2)
.find('.no-padding > .q-td > .middle')
.should('have.class', 'proposal-primary');
cy.colField('tag5', 2)
.find('.no-padding > .match')
.should('have.class', 'match');
cy.colField('tag6', 2)
.find('.no-padding > .match')
.should('have.class', 'match');
cy.colField('tag7', 2).click();
});
});
});
});

View File

@ -1,34 +1,16 @@
/// <reference types="cypress" />
describe('Ticket Lack list', () => {
beforeEach(() => {
cy.login('developer');
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.viewport(1280, 720);
cy.login('developer');
cy.visit('/#/ticket/negative');
});
describe('Table actions', () => {
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('itemDescriptorProxy').should('be.visible');
cy.get('.q-virtual-scroll__content > :nth-child(1) > .sticky').click();
cy.location('href').should('contain', '#/ticket/negative/5');
});

View File

@ -126,7 +126,6 @@ describe('TicketSale', () => {
cy.dataCy('recalculatePriceItem').click();
cy.wait('@recalculatePrice').its('response.statusCode').should('eq', 200);
cy.checkNotification('Data saved');
cy.dataCy('ticketSaleMoreActionsDropdown').should('be.disabled');
});
it('should update discount when "Update discount" is clicked', () => {
@ -141,7 +140,6 @@ describe('TicketSale', () => {
cy.dataCy('saveManaBtn').click();
cy.waitForElement('.q-notification__message');
cy.checkNotification('Data saved');
cy.dataCy('ticketSaleMoreActionsDropdown').should('be.disabled');
});
it('adds claim', () => {
@ -195,7 +193,7 @@ describe('TicketSale', () => {
cy.viewport(1920, 1080);
cy.visit('/#/ticket/32/sale');
});
it('transfer sale to a new ticket', () => {
it.only('transfer sale to a new ticket', () => {
Review

Quitar el only

Quitar el only
cy.get('.q-item > .q-item__label').should('have.text', ' #32');
selectFirstRow();
cy.dataCy('ticketSaleTransferBtn').click();

View File

@ -408,6 +408,14 @@ Cypress.Commands.add('clickButtonWithText', (buttonText) => {
cy.get('.q-btn').contains(buttonText).click();
});
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}"]`);
}
});
Cypress.Commands.add('getOption', (index = 1) => {
cy.waitForElement('[role="listbox"]');
cy.get(`[role="listbox"] .q-item:nth-child(${index})`).click();