Merge pull request '#7347 - Supplier Consumption layout updates' (!1451) from 7347_supplierConsumption into dev
gitea/salix-front/pipeline/head This commit looks good Details

Reviewed-on: #1451
Reviewed-by: Javi Gallego <jgallego@verdnatura.es>
This commit is contained in:
Javier Segarra 2025-04-30 17:33:31 +00:00
commit 41680c574f
5 changed files with 223 additions and 81 deletions

View File

@ -6,6 +6,7 @@ import toDateHourMinSec from './toDateHourMinSec';
import toRelativeDate from './toRelativeDate'; import toRelativeDate from './toRelativeDate';
import toCurrency from './toCurrency'; import toCurrency from './toCurrency';
import toPercentage from './toPercentage'; import toPercentage from './toPercentage';
import toNumber from './toNumber';
import toLowerCamel from './toLowerCamel'; import toLowerCamel from './toLowerCamel';
import dashIfEmpty from './dashIfEmpty'; import dashIfEmpty from './dashIfEmpty';
import dateRange from './dateRange'; import dateRange from './dateRange';
@ -34,6 +35,7 @@ export {
toRelativeDate, toRelativeDate,
toCurrency, toCurrency,
toPercentage, toPercentage,
toNumber,
dashIfEmpty, dashIfEmpty,
dateRange, dateRange,
getParamWhere, getParamWhere,

8
src/filters/toNumber.js Normal file
View File

@ -0,0 +1,8 @@
export default function (value, fractionSize = 2) {
if (isNaN(value)) return value;
return new Intl.NumberFormat('es-ES', {
style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: fractionSize,
}).format(value);
}

View File

@ -122,6 +122,7 @@ globals:
producer: Producer producer: Producer
origin: Origin origin: Origin
state: State state: State
total: Total
subtotal: Subtotal subtotal: Subtotal
visible: Visible visible: Visible
price: Price price: Price

View File

@ -126,6 +126,7 @@ globals:
producer: Productor producer: Productor
origin: Origen origin: Origen
state: Estado state: Estado
total: Total
subtotal: Subtotal subtotal: Subtotal
visible: Visible visible: Visible
price: Precio price: Precio

View File

@ -1,22 +1,20 @@
<script setup> <script setup>
import { useRoute } from 'vue-router';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import FetchedTags from 'components/ui/FetchedTags.vue';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import SupplierConsumptionFilter from './SupplierConsumptionFilter.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import { dateRange, toDate } from 'src/filters';
import { dashIfEmpty } from 'src/filters';
import { usePrintService } from 'composables/usePrintService';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios'; import axios from 'axios';
import { dateRange, toCurrency, toNumber, toDateHourMin } from 'src/filters';
import { usePrintService } from 'composables/usePrintService';
import useNotify from 'src/composables/useNotify';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import SupplierConsumptionFilter from './SupplierConsumptionFilter.vue';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
const state = useState(); const state = useState();
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -31,7 +29,86 @@ const arrayData = useArrayData('SupplierConsumption', {
order: ['itemTypeFk', 'itemName', 'itemSize'], order: ['itemTypeFk', 'itemName', 'itemSize'],
userFilter: { where: { supplierFk: route.params.id } }, userFilter: { where: { supplierFk: route.params.id } },
}); });
const headerColumns = computed(() => [
{
name: 'id',
label: t('globals.entry'),
align: 'left',
field: 'id',
sortable: true,
},
{
name: 'invoiceNumber',
label: t('globals.params.supplierRef'),
align: 'left',
field: 'invoiceNumber',
sortable: true,
},
{
name: 'shipped',
label: t('globals.shipped'),
align: 'center',
field: 'shipped',
format: toDateHourMin,
sortable: true,
},
{
name: 'quantity',
label: t('item.list.stems'),
align: 'center',
field: 'quantity',
format: (value) => toNumber(value),
sortable: true,
},
{
name: 'total',
label: t('globals.total'),
align: 'center',
field: 'total',
format: (value) => toCurrency(value),
sortable: true,
},
]);
const columns = computed(() => [
{
name: 'itemName',
label: t('globals.item'),
align: 'left',
field: 'itemName',
sortable: true,
},
{
name: 'subName',
align: 'left',
field: 'subName',
sortable: true,
},
{
name: 'quantity',
label: t('globals.quantity'),
align: 'right',
field: 'quantity',
format: (value) => toNumber(value),
sortable: true,
},
{
name: 'price',
label: t('globals.price'),
align: 'right',
field: 'price',
format: (value) => toCurrency(value),
sortable: true,
},
{
name: 'total',
label: t('globals.total'),
align: 'right',
field: 'total',
format: (value) => toCurrency(value),
sortable: true,
},
]);
const store = arrayData.store; const store = arrayData.store;
onUnmounted(() => state.unset('SupplierConsumption')); onUnmounted(() => state.unset('SupplierConsumption'));
@ -100,33 +177,34 @@ const sendCampaignMetricsEmail = ({ address }) => {
}; };
const totalEntryPrice = (rows) => { const totalEntryPrice = (rows) => {
let totalPrice = 0; if (!rows) return [];
let totalQuantity = 0; totalRows.value = rows.reduce(
if (!rows) return totalPrice; (acc, row) => {
for (const row of rows) { if (Array.isArray(row.buys)) {
let total = 0; const { total, quantity } = row.buys.reduce(
let quantity = 0; (buyAcc, buy) => {
buyAcc.total += buy.total || 0;
if (row.buys) { buyAcc.quantity += buy.quantity || 0;
for (const buy of row.buys) { return buyAcc;
total = total + buy.total; },
quantity = quantity + buy.quantity; { total: 0, quantity: 0 },
);
row.total = total;
row.quantity = quantity;
acc.totalPrice += total;
acc.totalQuantity += quantity;
} }
} return acc;
},
row.total = total; { totalPrice: 0, totalQuantity: 0 },
row.quantity = quantity; );
totalPrice = totalPrice + total;
totalQuantity = totalQuantity + quantity;
}
totalRows.value = { totalPrice, totalQuantity };
return rows; return rows;
}; };
onMounted(async () => { onMounted(async () => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
await getSupplierConsumptionData(); await getSupplierConsumptionData();
}); });
const expanded = ref([]);
</script> </script>
<template> <template>
@ -160,14 +238,14 @@ onMounted(async () => {
<div> <div>
{{ t('Total entries') }}: {{ t('Total entries') }}:
<QChip :dense="$q.screen.lt.sm" text-color="white"> <QChip :dense="$q.screen.lt.sm" text-color="white">
{{ totalRows.totalPrice }} {{ toCurrency(totalRows.totalPrice) }}
</QChip> </QChip>
</div> </div>
<QSeparator dark vertical /> <QSeparator dark vertical />
<div> <div>
{{ t('Total stems entries') }}: {{ t('Total stems entries') }}:
<QChip :dense="$q.screen.lt.sm" text-color="white"> <QChip :dense="$q.screen.lt.sm" text-color="white">
{{ totalRows.totalQuantity }} {{ toNumber(totalRows.totalQuantity) }}
</QChip> </QChip>
</div> </div>
</div> </div>
@ -177,59 +255,111 @@ onMounted(async () => {
<SupplierConsumptionFilter data-key="SupplierConsumption" /> <SupplierConsumptionFilter data-key="SupplierConsumption" />
</template> </template>
</RightMenu> </RightMenu>
<QTable <QCard class="full-width q-pa-md">
:rows="rows" <QTable
row-key="id" flat
hide-header bordered
class="full-width q-mt-md" :rows="rows"
:no-data-label="t('No results')" :columns="headerColumns"
> row-key="id"
<template #body="{ row }"> v-model:expanded="expanded"
<QTr> :grid="$q.screen.lt.md"
<QTd no-hover> >
<span class="label">{{ t('supplier.consumption.entry') }}: </span> <template #header="props">
<span>{{ row.id }}</span> <QTr :props="props">
</QTd> <QTh auto-width />
<QTd no-hover>
<span class="label">{{ t('globals.date') }}: </span> <QTh v-for="col in props.cols" :key="col.name" :props="props">
<span>{{ toDate(row.shipped) }}</span></QTd <span v-text="col.label" class="tr-header" />
</QTh>
</QTr>
</template>
<template #body="props">
<QTr
:props="props"
:key="`movement_${props.row.id}`"
class="bg-vn-page cursor-pointer"
@click="props.expand = !props.expand"
> >
<QTd colspan="6" no-hover> <QTd auto-width>
<span class="label">{{ t('globals.reference') }}: </span> <QIcon
<span>{{ row.invoiceNumber }}</span> :class="props.expand ? '' : 'rotate-270'"
</QTd> name="expand_circle_down"
</QTr> size="md"
<QTr v-for="(buy, index) in row.buys" :key="index"> :color="props.expand ? 'primary' : 'white'"
<QTd no-hover> />
<QBtn flat class="link" dense no-caps>{{ buy.itemName }}</QBtn> </QTd>
<ItemDescriptorProxy :id="buy.itemFk" />
</QTd>
<QTd no-hover> <QTd v-for="col in props.cols" :key="col.name" :props="props">
<span>{{ buy.subName }}</span> <span @click.stop class="link" v-if="col.name === 'id'">
<FetchedTags :item="buy" /> {{ col.value }}
</QTd> <EntryDescriptorProxy :id="col.value" />
<QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd> </span>
<QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd>
<QTd colspan="2" no-hover> {{ dashIfEmpty(buy.total) }}</QTd> <span v-else v-text="col.value" />
</QTr> </QTd>
<QTr> </QTr>
<QTd colspan="5" no-hover>
<span class="label">{{ t('Total entry') }}: </span> <QTr
<span>{{ row.total }} </span> v-show="props.expand"
</QTd> :props="props"
<QTd no-hover> :key="`expedition_${props.row.id}`"
<span class="label">{{ t('Total stems') }}: </span> >
<span>{{ row.quantity }}</span> <QTd colspan="12" style="padding: 1px 0">
</QTd> <QTable
</QTr> color="secondary"
</template> card-class="bg-vn-page text-white "
</QTable> style-class="height: 30px"
table-header-class="text-white"
:rows="props.row.buys"
:columns="columns"
row-key="id"
virtual-scroll
v-model:expanded="expanded"
>
<template #header="props">
<QTr :props="props">
<QTh
v-for="col in props.cols"
:key="col.name"
:props="props"
>
<span v-text="col.label" class="tr-header" />
</QTh>
</QTr>
</template>
<template #body="props">
<QTr :props="props" :key="`m_${props.row.id}`">
<QTd
v-for="col in props.cols"
:key="col.name"
:title="col.label"
:props="props"
>
<span
@click.stop
class="link"
v-if="col.name === 'itemName'"
>
{{ col.value }}
<ItemDescriptorProxy :id="props.row.itemFk" />
</span>
<span v-else v-text="col.value" />
</QTd>
</QTr>
</template>
</QTable>
</QTd>
</QTr>
</template>
</QTable>
</QCard>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.label { .q-table thead tr,
color: var(--vn-label-color); .q-table tbody td {
height: 30px;
} }
</style> </style>