salix-front/src/pages/Item/components/ItemProposal.vue

347 lines
9.2 KiB
Vue

<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
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 { displayResults } from 'src/pages/Ticket/Negative/composables/notifyResults';
import FetchData from 'components/FetchData.vue';
import { useState } from 'src/composables/useState';
const MATCH = 'match';
const { notifyResults } = displayResults();
const { t } = useI18n();
const $props = defineProps({
itemLack: {
type: Object,
required: true,
default: () => {},
},
filter: {
type: Object,
required: true,
default: () => {},
},
replaceAction: {
type: Boolean,
required: true,
default: false,
},
sales: {
type: Array,
required: true,
default: () => [],
},
});
const proposalSelected = ref([]);
const ticketConfig = ref({});
const proposalTableRef = ref(null);
const sale = computed(() => $props.sales[0]);
const saleFk = computed(() => sale.value.saleFk);
const filter = computed(() => ({
where: $props.filter,
itemFk: $props.itemLack.itemFk,
sales: saleFk.value,
}));
const defaultColumnAttrs = {
align: 'center',
sortable: false,
};
const emit = defineEmits(['onDialogClosed', 'itemReplaced']);
const conditionalValuePrice = (price) =>
price > 1 + ticketConfig.value.lackAlertPrice / 100 ? 'match' : 'not-match';
const columns = computed(() => [
{
...defaultColumnAttrs,
label: t('proposal.available'),
name: 'available',
field: 'available',
columnFilter: {
component: 'input',
type: 'number',
columnClass: 'shrink',
},
columnClass: 'shrink',
},
{
...defaultColumnAttrs,
label: t('proposal.counter'),
name: 'counter',
field: 'counter',
columnClass: 'shrink',
style: 'max-width: 75px',
columnFilter: {
component: 'input',
type: 'number',
columnClass: 'shrink',
},
},
{
align: 'left',
sortable: true,
label: t('proposal.longName'),
name: 'longName',
field: 'longName',
columnClass: 'expand',
},
{
align: 'left',
sortable: true,
label: t('item.list.color'),
name: 'tag5',
field: 'value5',
columnClass: 'expand',
},
{
align: 'left',
sortable: true,
label: t('item.list.stems'),
name: 'tag6',
field: 'value6',
columnClass: 'expand',
},
{
align: 'left',
sortable: true,
label: t('item.list.producer'),
name: 'tag7',
field: 'value7',
columnClass: 'expand',
},
{
...defaultColumnAttrs,
label: t('proposal.price2'),
name: 'price2',
style: 'max-width: 75px',
columnFilter: {
component: 'input',
type: 'number',
columnClass: 'shrink',
},
},
{
...defaultColumnAttrs,
label: t('proposal.minQuantity'),
name: 'minQuantity',
field: 'minQuantity',
style: 'max-width: 75px',
columnFilter: {
component: 'input',
type: 'number',
columnClass: 'shrink',
},
},
{
...defaultColumnAttrs,
label: t('proposal.located'),
name: 'located',
field: 'located',
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('Replace'),
icon: 'change_circle',
show: (row) => isSelectionAvailable(row),
action: change,
isPrimary: true,
},
],
},
]);
function extractMatchValues(obj) {
return Object.keys(obj)
.filter((key) => key.startsWith(MATCH))
.map((key) => parseInt(key.replace(MATCH, ''), 10));
}
const gradientStyle = (value) => {
let color = 'white';
const perc = parseFloat(value);
switch (true) {
case perc >= 0 && perc < 33:
color = 'primary';
break;
case perc >= 33 && perc < 66:
color = 'warning';
break;
default:
color = 'secondary';
break;
}
return color;
};
const statusConditionalValue = (row) => {
const matches = extractMatchValues(row);
const value = matches.reduce((acc, i) => acc + row[`${MATCH}${i}`], 0);
return 100 * (value / matches.length);
};
const isSelectionAvailable = (itemProposal) => {
const { price2 } = itemProposal;
const salePrice = sale.value.price;
const byPrice = (100 * price2) / salePrice > ticketConfig.value.lackAlertPrice;
if (byPrice) {
return byPrice;
}
const byQuantity =
(100 * itemProposal.available) / Math.abs($props.itemLack.lack) <
ticketConfig.value.lackAlertPrice;
return byQuantity;
};
async function change({ itemFk: substitutionFk }) {
try {
const promises = $props.sales.map(({ saleFk, quantity }) => {
const params = {
saleFk,
substitutionFk,
quantity,
};
return axios.post('Sales/replaceItem', params);
});
const results = await Promise.allSettled(promises);
notifyResults(results, 'saleFk');
emit('itemReplaced', {
type: 'refresh',
quantity: quantity.value,
itemProposal: proposalSelected.value[0],
});
proposalSelected.value = [];
} catch (error) {
console.error(error);
}
}
async function handleTicketConfig(data) {
ticketConfig.value = data[0];
}
</script>
<template>
<FetchData
url="TicketConfigs"
:filter="{ fields: ['lackAlertPrice'] }"
@on-fetch="handleTicketConfig"
></FetchData>
<QInnerLoading
:showing="isLoading"
:label="t && t('globals.pleaseWait')"
color="primary"
/>
<VnTable
v-if="!isLoading"
auto-load
data-cy="proposalTable"
ref="proposalTableRef"
data-key="ItemsGetSimilar"
url="Items/getSimilar"
:user-filter="filter"
:columns="columns"
class="full-width q-mt-md"
row-key="id"
:row-click="change"
:is-editable="false"
:right-search="false"
:without-header="true"
:disable-option="{ card: true, table: true }"
>
<template #column-longName="{ row }">
<QTd
class="flex"
style="max-width: 100%; flex-shrink: 50px; flex-wrap: nowrap"
>
<div
class="middle full-width"
:class="[`proposal-${gradientStyle(statusConditionalValue(row))}`]"
>
<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>
</div>
</QTd>
</template>
<template #column-tag5="{ row }">
<span :class="{ match: !row.match5 }">{{ row.value5 }}</span>
</template>
<template #column-tag6="{ row }">
<span :class="{ match: !row.match6 }">{{ row.value6 }}</span>
</template>
<template #column-tag7="{ row }">
<span :class="{ match: !row.match7 }">{{ row.value7 }}</span>
</template>
<template #column-counter="{ row }">
<span
:class="{
match: row.counter === 1,
'not-match': row.counter !== 1,
}"
>{{ row.counter }}</span
>
</template>
<template #column-minQuantity="{ row }">
{{ row.minQuantity }}
</template>
<template #column-price2="{ row }">
<div class="flex column items-center content-center">
<VnStockValueDisplay :value="(sales[0].price - row.price2) / 100" />
<span :class="[conditionalValuePrice(row.price2)]">{{
toCurrency(row.price2)
}}</span>
</div>
</template>
</VnTable>
</template>
<style lang="scss" scoped>
@import 'src/css/quasar.variables.scss';
.middle {
float: left;
margin-right: 2px;
flex: 2 0 5px;
}
.match {
color: $negative;
}
.not-match {
color: inherit;
}
.proposal-warning {
background-color: $warning;
}
.proposal-secondary {
background-color: $secondary;
}
.proposal-primary {
background-color: $primary;
}
.text {
margin: 0.05rem;
padding: 1px;
border: 1px solid var(--vn-label-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: smaller;
}
</style>