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 toCurrency from './toCurrency';
import toPercentage from './toPercentage';
import toNumber from './toNumber';
import toLowerCamel from './toLowerCamel';
import dashIfEmpty from './dashIfEmpty';
import dateRange from './dateRange';
@ -34,6 +35,7 @@ export {
toRelativeDate,
toCurrency,
toPercentage,
toNumber,
dashIfEmpty,
dateRange,
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
origin: Origin
state: State
total: Total
subtotal: Subtotal
visible: Visible
price: Price

View File

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

View File

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