salix-front/src/pages/Entry/Card/EntryBuys.vue

813 lines
24 KiB
Vue

<script setup>
import { useStateStore } from 'stores/useStateStore';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { onMounted, ref } from 'vue';
import { useState } from 'src/composables/useState';
import FetchData from 'src/components/FetchData.vue';
import VnTable from 'src/components/VnTable/VnTable.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import FetchedTags from 'src/components/ui/FetchedTags.vue';
import VnColor from 'src/components/common/VnColor.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import ItemDescriptor from 'src/pages/Item/Card/ItemDescriptor.vue';
import axios from 'axios';
import VnSelectEnum from 'src/components/common/VnSelectEnum.vue';
import { checkEntryLock } from 'src/composables/checkEntryLock';
const $props = defineProps({
id: {
type: Number,
default: null,
},
editableMode: {
type: Boolean,
default: true,
},
tableHeight: {
type: String,
default: null,
},
});
const state = useState();
const user = state.getUser().fn();
const stateStore = useStateStore();
const { t } = useI18n();
const route = useRoute();
const selectedRows = ref([]);
const entityId = ref($props.id ?? route.params.id);
const entryBuysRef = ref();
const footerFetchDataRef = ref();
const footer = ref({});
const columns = [
{
align: 'center',
labelAbbreviation: 'NV',
label: t('Ignore'),
toolTip: t('Ignored for available'),
name: 'isIgnored',
component: 'checkbox',
attrs: {
toggleIndeterminate: false,
},
create: true,
createOrder: 12,
width: '25px',
},
{
label: t('Buyer'),
name: 'workerFk',
component: 'select',
attrs: {
url: 'TicketRequests/getItemTypeWorker',
fields: ['id', 'nickname'],
optionLabel: 'nickname',
sortBy: 'nickname ASC',
optionValue: 'id',
},
visible: false,
},
{
label: t('Family'),
name: 'itemTypeFk',
component: 'select',
attrs: {
url: 'itemTypes',
fields: ['id', 'name'],
optionLabel: 'name',
optionValue: 'id',
},
visible: false,
},
{
name: 'id',
isId: true,
visible: false,
isEditable: false,
columnFilter: false,
},
{
align: 'center',
label: 'Id',
name: 'itemFk',
component: 'number',
isEditable: false,
width: '35px',
},
{
labelAbbreviation: '',
label: 'Color',
name: 'hex',
columnSearch: false,
isEditable: false,
width: '9px',
component: 'select',
attrs: {
url: 'Inks',
fields: ['id', 'name'],
},
},
{
align: 'center',
label: t('Article'),
name: 'name',
component: 'select',
attrs: {
url: 'Items',
fields: ['id', 'name'],
optionLabel: 'name',
optionValue: 'id',
},
width: '85px',
isEditable: false,
},
{
align: 'center',
label: t('Article'),
name: 'itemFk',
visible: false,
create: true,
createOrder: 0,
columnFilter: false,
},
{
align: 'center',
labelAbbreviation: t('Siz.'),
label: t('Size'),
toolTip: t('Size'),
component: 'number',
name: 'size',
width: '35px',
isEditable: false,
style: () => {
return { color: 'var(--vn-label-color)' };
},
},
{
align: 'center',
labelAbbreviation: t('Sti.'),
label: t('Stickers'),
toolTip: t('Printed Stickers/Stickers'),
name: 'stickers',
component: 'input',
create: true,
createOrder: 1,
attrs: {
positive: false,
},
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
row['quantity'] = value * row['packing'];
row['amount'] = row['quantity'] * row['buyingValue'];
},
},
width: '35px',
},
{
align: 'center',
label: t('Bucket'),
name: 'packagingFk',
component: 'select',
attrs: {
url: 'packagings',
fields: ['id'],
optionLabel: 'id',
optionValue: 'id',
},
create: true,
width: '40px',
},
{
align: 'center',
label: 'Kg',
name: 'weight',
component: 'number',
create: true,
width: '35px',
format: (row) => parseFloat(row['weight']).toFixed(1),
},
{
labelAbbreviation: 'P',
label: 'Packing',
toolTip: 'Packing',
name: 'packing',
component: 'number',
create: true,
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
const oldPacking = oldValue === 1 || oldValue === null ? 1 : oldValue;
row['weight'] = (row['weight'] * value) / oldPacking;
row['quantity'] = row['stickers'] * value;
row['amount'] = row['quantity'] * row['buyingValue'];
},
},
width: '30px',
style: (row) => {
if (row.groupingMode === 'grouping')
return { color: 'var(--vn-label-color)' };
},
},
{
align: 'center',
labelAbbreviation: 'GM',
label: t('Grouping selector'),
toolTip: t('Grouping selector'),
name: 'groupingMode',
component: 'toggle',
attrs: {
'toggle-indeterminate': true,
trueValue: 'grouping',
falseValue: 'packing',
indeterminateValue: null,
},
size: 'xs',
width: '25px',
create: true,
rightFilter: false,
getIcon: (value) => {
switch (value) {
case 'grouping':
return 'toggle_on';
case 'packing':
return 'toggle_off';
default:
return 'minimize';
}
},
},
{
align: 'center',
labelAbbreviation: 'G',
label: 'Grouping',
toolTip: 'Grouping',
name: 'grouping',
component: 'number',
width: '30px',
create: true,
style: (row) => {
if (row.groupingMode === 'packing') return { color: 'var(--vn-label-color)' };
},
},
{
align: 'center',
label: t('Quantity'),
name: 'quantity',
component: 'number',
attrs: {
positive: false,
},
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
row['amount'] = value * row['buyingValue'];
},
},
width: '45px',
create: true,
createOrder: 3,
style: getQuantityStyle,
},
{
align: 'center',
labelAbbreviation: t('Cost'),
label: t('Buying value'),
toolTip: t('Buying value'),
name: 'buyingValue',
create: true,
createOrder: 2,
component: 'number',
attrs: {
positive: false,
},
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
row['amount'] = row['quantity'] * value;
},
},
width: '45px',
format: (row) => parseFloat(row['buyingValue']).toFixed(3),
},
{
align: 'center',
label: t('Amount'),
name: 'amount',
width: '45px',
component: 'number',
attrs: {
positive: false,
},
isEditable: false,
format: (row) => parseFloat(row['amount']).toFixed(2),
style: getAmountStyle,
},
{
align: 'center',
labelAbbreviation: t('Pack.'),
label: t('Package'),
toolTip: t('Package'),
name: 'price2',
component: 'number',
createDisable: true,
width: '35px',
create: true,
format: (row) => parseFloat(row['price2']).toFixed(2),
},
{
align: 'center',
label: t('Box'),
name: 'price3',
component: 'number',
createDisable: true,
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
row['price2'] = row['price2'] * (value / oldValue);
},
},
width: '35px',
create: true,
format: (row) => parseFloat(row['price3']).toFixed(2),
},
{
align: 'center',
labelAbbreviation: 'CM',
label: t('Check min price'),
toolTip: t('Check min price'),
name: 'hasMinPrice',
attrs: {
toggleIndeterminate: false,
},
component: 'checkbox',
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
await axios.patch(`Items/${row['itemFk']}`, {
hasMinPrice: value,
});
},
},
width: '25px',
},
{
align: 'center',
labelAbbreviation: 'Min.',
label: t('Minimum price'),
toolTip: t('Minimum price'),
name: 'minPrice',
component: 'number',
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
await axios.patch(`Items/${row['itemFk']}`, {
minPrice: value,
});
},
},
width: '35px',
style: (row) => {
if (!row?.hasMinPrice) return { color: 'var(--vn-label-color)' };
},
format: (row) => parseFloat(row['minPrice']).toFixed(2),
},
{
align: 'center',
labelAbbreviation: t('P.Sen'),
label: t('Packing sent'),
toolTip: t('Packing sent'),
name: 'packingOut',
component: 'number',
isEditable: false,
width: '40px',
style: () => {
return { color: 'var(--vn-label-color)' };
},
},
{
align: 'center',
labelAbbreviation: t('Com.'),
label: t('Comment'),
toolTip: t('Comment'),
name: 'comment',
component: 'input',
isEditable: false,
width: '50px',
},
{
align: 'center',
labelAbbreviation: 'Prod.',
label: t('Producer'),
toolTip: t('Producer'),
name: 'subName',
isEditable: false,
width: '45px',
style: () => {
return { color: 'var(--vn-label-color)' };
},
},
{
align: 'center',
label: t('Tags'),
name: 'tags',
width: '125px',
columnSearch: false,
},
{
align: 'center',
labelAbbreviation: 'Comp.',
label: t('Company'),
toolTip: t('Company'),
name: 'company_name',
component: 'input',
isEditable: false,
width: '35px',
style: () => {
return { color: 'var(--vn-label-color)' };
},
},
];
function getQuantityStyle(row) {
if (row?.quantity !== row?.stickers * row?.packing)
return { color: 'var(--q-negative)' };
}
function getAmountStyle(row) {
if (row?.isChecked) return { color: 'var(--q-positive)' };
return { color: 'var(--vn-label-color)' };
}
async function beforeSave(data, getChanges) {
try {
const changes = data.updates;
if (!changes) return data;
const patchPromises = [];
for (const change of changes) {
let patchData = {};
if ('hasMinPrice' in change.data) {
patchData.hasMinPrice = change.data?.hasMinPrice;
delete change.data.hasMinPrice;
}
if ('minPrice' in change.data) {
patchData.minPrice = change.data?.minPrice;
delete change.data.minPrice;
}
if (Object.keys(patchData).length > 0) {
const promise = axios
.get('Buys/findOne', {
params: {
filter: {
fields: ['itemFk'],
where: { id: change.where.id },
},
},
})
.then((buy) => {
return axios.patch(`Items/${buy.data.itemFk}`, patchData);
})
.catch((error) => {
console.error('Error processing change: ', change, error);
});
patchPromises.push(promise);
}
}
await Promise.all(patchPromises);
data.updates = changes.filter((change) => Object.keys(change.data).length > 0);
return data;
} catch (error) {
console.error('Error in beforeSave:', error);
throw error;
}
}
function invertQuantitySign(rows, sign) {
for (const row of rows) {
if (sign > 0) row.quantity = Math.abs(row.quantity);
else if (row.quantity > 0) row.quantity = -row.quantity;
}
}
function setIsChecked(rows, value) {
for (const row of rows) {
row.isChecked = value;
}
footerFetchDataRef.value.fetch();
}
async function setBuyUltimate(itemFk, data) {
if (!itemFk) return;
const buyUltimate = await axios.get(`Entries/getBuyUltimate`, {
params: {
itemFk,
warehouseFk: user.warehouseFk,
date: Date.vnNew(),
},
});
const buyUltimateData = buyUltimate.data[0];
if (!buyUltimateData) return;
const allowedKeys = columns
.filter((col) => col.create === true)
.map((col) => col.name);
allowedKeys.forEach((key) => {
if (buyUltimateData?.hasOwnProperty(key) && key !== 'entryFk') {
if (!['stickers', 'quantity'].includes(key)) data[key] = buyUltimateData[key];
}
});
}
onMounted(() => {
stateStore.rightDrawer = false;
if ($props.editableMode) checkEntryLock(entityId.value, user.id);
});
</script>
<template>
<Teleport to="#st-data" v-if="stateStore?.isSubToolbarShown() && editableMode">
<QBtnGroup push style="column-gap: 1px">
<QBtnDropdown
label="+/-"
color="primary"
flat
:title="t('Invert quantity value')"
:disable="!selectedRows.length"
data-cy="change-quantity-sign"
>
<QList>
<QItem>
<QItemSection>
<QBtn
flat
@click="invertQuantitySign(selectedRows, -1)"
data-cy="set-negative-quantity"
>
<span style="font-size: large">-</span>
</QBtn>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QBtn
flat
@click="invertQuantitySign(selectedRows, 1)"
data-cy="set-positive-quantity"
>
<span style="font-size: large">+</span>
</QBtn>
</QItemSection>
</QItem>
</QList>
</QBtnDropdown>
<QBtnDropdown
icon="price_check"
color="primary"
flat
:title="t('Check buy amount')"
:disable="!selectedRows.length"
data-cy="check-buy-amount"
>
<QList>
<QItem>
<QItemSection>
<QBtn
size="sm"
icon="check"
flat
@click="setIsChecked(selectedRows, true)"
data-cy="check-amount"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QBtn
size="sm"
icon="close"
flat
@click="setIsChecked(selectedRows, false)"
data-cy="uncheck-amount"
/>
</QItemSection>
</QItem>
</QList>
</QBtnDropdown>
</QBtnGroup>
</Teleport>
<FetchData
ref="footerFetchDataRef"
:url="`Entries/${entityId}/getBuyList`"
:params="{ groupBy: 'GROUP BY b.entryFk' }"
@on-fetch="(data) => (footer = data[0])"
auto-load
/>
<VnTable
ref="entryBuysRef"
data-key="EntryBuys"
:url="`Entries/${entityId}/getBuyList`"
search-url="EntryBuys"
save-url="Buys/crud"
:disable-option="{ card: true }"
v-model:selected="selectedRows"
@on-fetch="() => footerFetchDataRef.fetch()"
:table="
editableMode
? {
'row-key': 'id',
selection: 'multiple',
}
: {}
"
:create="
editableMode
? {
urlCreate: 'Buys',
title: t('Create buy'),
onDataSaved: () => {
entryBuysRef.reload();
},
formInitialData: { entryFk: entityId, isIgnored: false },
showSaveAndContinueBtn: true,
}
: null
"
:create-complement="{
isFullWidth: true,
containerStyle: {
display: 'flex',
gap: '16px',
position: 'relative',
},
columnGridStyle: {
'max-width': '50%',
'margin-right': '30px',
flex: 1,
},
previousStyle: {
'max-width': '30%',
height: '500px',
},
displayPrevious: true,
}"
:is-editable="editableMode"
:without-header="!editableMode"
:with-filters="editableMode"
:right-search="editableMode"
:right-search-icon="true"
:row-click="false"
:columns="columns"
:beforeSaveFn="beforeSave"
class="buyList"
:table-height="$props.tableHeight ?? '84vh'"
auto-load
footer
data-cy="entry-buys"
overlay
>
<template #column-hex="{ row }">
<VnColor :colors="row?.hexJson" style="height: 100%; min-width: 2000px" />
</template>
<template #column-name="{ row }">
<span class="link">
{{ row?.name }}
<ItemDescriptorProxy :id="row?.itemFk" />
</span>
</template>
<template #column-tags="{ row }">
<FetchedTags :item="row" :columns="3" />
</template>
<template #column-stickers="{ row }">
<span :class="editableMode ? 'editable-text' : ''">
<span style="color: var(--vn-label-color)">
{{ row.printedStickers }}
</span>
<span>/{{ row.stickers }}</span>
</span>
</template>
<template #column-footer-stickers>
<div>
<span style="color: var(--vn-label-color)">
{{ footer?.printedStickers }}</span
>
<span>/</span>
<span data-cy="footer-stickers">{{ footer?.stickers }}</span>
</div>
</template>
<template #column-footer-weight>
{{ footer?.weight }}
</template>
<template #column-footer-quantity>
<span :style="getQuantityStyle(footer)" data-cy="footer-quantity">
{{ footer?.quantity }}
</span>
</template>
<template #column-footer-amount>
<span :style="getAmountStyle(footer)" data-cy="footer-amount">
{{ footer?.amount }}
</span>
</template>
<template #column-create-itemFk="{ data }">
<VnSelect
url="Items/search"
v-model="data.itemFk"
:label="t('Article')"
:fields="['id', 'name', 'size', 'producerName']"
:filter-options="['id', 'name', 'size', 'producerName']"
option-label="name"
option-value="id"
@update:modelValue="
async (value) => {
await setBuyUltimate(value, data);
}
"
:required="true"
data-cy="itemFk-create-popup"
sort-by="nickname DESC"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt.name }}
</QItemLabel>
<QItemLabel caption>
#{{ scope.opt.id }}, {{ scope.opt?.size }},
{{ scope.opt?.producerName }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</template>
<template #column-create-groupingMode="{ data }">
<VnSelectEnum
:label="t('Grouping mode')"
v-model="data.groupingMode"
schema="vn"
table="buy"
column="groupingMode"
option-value="groupingMode"
option-label="groupingMode"
/>
</template>
<template #previous-create-dialog="{ data }">
<div
style="position: absolute"
:class="{ 'centered-container': !data.itemFk }"
>
<ItemDescriptor :id="data.itemFk" v-if="data.itemFk" />
<div v-else>
<span>{{ t('globals.noData') }}</span>
</div>
</div>
</template>
</VnTable>
</template>
<i18n>
es:
Article: Artículo
Siz.: Med.
Size: Medida
Sti.: Eti.
Bucket: Cubo
Quantity: Cantidad
Amount: Importe
Pack.: Paq.
Package: Paquete
Box: Caja
P.Sen: P.Env
Packing sent: Packing envíos
Com.: Ref.
Comment: Referencia
Minimum price: Precio mínimo
Stickers: Etiquetas
Printed Stickers/Stickers: Etiquetas impresas/Etiquetas
Cost: Cost.
Buying value: Coste
Producer: Productor
Company: Compañia
Tags: Etiquetas
Grouping mode: Modo de agrupación
C.min: P.min
Ignore: Ignorar
Ignored for available: Ignorado para disponible
Grouping selector: Selector de grouping
Check min price: Marcar precio mínimo
Create buy: Crear compra
Invert quantity value: Invertir valor de cantidad
Check buy amount: Marcar como correcta la cantidad de compra
</i18n>
<style lang="scss" scoped>
.centered-container {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
width: 40%;
height: 100%;
}
</style>