319 lines
8.6 KiB
Vue
319 lines
8.6 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 notifyResults from 'src/utils/notifyResults';
|
|
|
|
const MATCH_VALUES = [5, 6, 7, 8];
|
|
const { t } = useI18n();
|
|
const $props = defineProps({
|
|
itemLack: {
|
|
type: Object,
|
|
required: true,
|
|
default: () => {},
|
|
},
|
|
replaceAction: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
sales: {
|
|
type: Array,
|
|
required: false,
|
|
default: () => [],
|
|
},
|
|
});
|
|
const proposalSelected = ref([]);
|
|
const quantity = ref(-1);
|
|
const sale = computed(() => $props.sales[0]);
|
|
const saleFk = computed(() => sale.value.saleFk);
|
|
const filter = computed(() => ({
|
|
itemFk: $props.itemLack.itemFk,
|
|
sales: saleFk.value,
|
|
}));
|
|
const proposalTableRef = ref(null);
|
|
const defaultColumnAttrs = {
|
|
align: 'center',
|
|
sortable: false,
|
|
};
|
|
const columns = computed(() => [
|
|
{
|
|
...defaultColumnAttrs,
|
|
label: t('proposal.available'),
|
|
name: 'available',
|
|
field: 'available',
|
|
columnClass: 'shrink',
|
|
style: 'max-width: 75px',
|
|
columnFilter: {
|
|
component: 'input',
|
|
type: 'number',
|
|
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,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
const extractNumericValue = (percentageString) => {
|
|
const match = percentageString.match(/(\d+(\.\d+)?)/);
|
|
return match ? parseFloat(match[0]) : null;
|
|
};
|
|
const compatibilityItem = (value) => `${100 * (value / MATCH_VALUES.length)}%`;
|
|
|
|
const gradientStyle = (value) => {
|
|
let color = 'white';
|
|
const perc = extractNumericValue(compatibilityItem(value));
|
|
switch (true) {
|
|
case perc >= 0 && perc < 33:
|
|
color = 'orange';
|
|
break;
|
|
case perc >= 33 && perc < 66:
|
|
color = 'yellow';
|
|
break;
|
|
|
|
default:
|
|
color = 'green';
|
|
break;
|
|
}
|
|
return color;
|
|
};
|
|
const statusConditionalValue = (row) => {
|
|
const total = MATCH_VALUES.reduce((acc, i) => acc + row[`match${i}`], 0);
|
|
return total;
|
|
};
|
|
|
|
const emit = defineEmits(['onDialogClosed', 'itemReplaced']);
|
|
|
|
const conditionalValuePrice = (price) => (price > 1.3 ? 'match' : 'not-match');
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
const isSelectionAvailable = (itemProposal) => {
|
|
const { price2 } = itemProposal;
|
|
const salePrice = sale.value.price;
|
|
const byPrice = (100 * price2) / salePrice > 30;
|
|
if (byPrice) {
|
|
return byPrice;
|
|
}
|
|
const byQuantity =
|
|
(100 * itemProposal.available) / Math.abs($props.itemLack.lack) < 30;
|
|
return byQuantity;
|
|
};
|
|
</script>
|
|
<template>
|
|
<VnTable
|
|
data-cy="proposalTable"
|
|
ref="proposalTableRef"
|
|
data-key="ItemsGetSimilar"
|
|
url="Items/getSimilar"
|
|
:user-filter="filter"
|
|
auto-load
|
|
: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"
|
|
>
|
|
<QTooltip>
|
|
{{ row.id }}
|
|
</QTooltip>
|
|
|
|
<div
|
|
class="middle compatibility"
|
|
:style="{
|
|
background: gradientStyle(statusConditionalValue(row)),
|
|
}"
|
|
>
|
|
<QTooltip>
|
|
{{ compatibilityItem(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">
|
|
{{ toCurrency(sales[0].price) }}
|
|
{{ toCurrency(row.price2) }}
|
|
<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>
|
|
.compatibility {
|
|
width: 100%;
|
|
}
|
|
.middle {
|
|
float: left;
|
|
margin-right: 2px;
|
|
flex: 2 0 5px;
|
|
}
|
|
.match {
|
|
color: $negative;
|
|
}
|
|
.not-match {
|
|
color: inherit;
|
|
}
|
|
.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>
|