Merge pull request 'feature/ms-115-EntryListView' (#68) from feature/ms-115-EntryListView into dev

Reviewed-on: hyervoni/salix-front-mindshore#68
This commit is contained in:
William Buezas 2024-01-14 23:57:12 +00:00
commit 129cacb88b
8 changed files with 629 additions and 25 deletions

View File

@ -9,6 +9,7 @@ const $props = defineProps({
isSelected: { type: Boolean, default: false }, isSelected: { type: Boolean, default: false },
title: { type: String, default: null }, title: { type: String, default: null },
showCheckbox: { type: Boolean, default: false }, showCheckbox: { type: Boolean, default: false },
hasInfoIcons: { type: Boolean, default: false },
}); });
const emit = defineEmits(['toggleCardCheck']); const emit = defineEmits(['toggleCardCheck']);
@ -39,6 +40,9 @@ const toggleCardCheck = (item) => {
</div> </div>
</slot> </slot>
<div class="card-list-body"> <div class="card-list-body">
<div v-if="hasInfoIcons" class="column q-mr-md q-gutter-y-xs">
<slot name="info-icons" />
</div>
<div class="list-items row flex-wrap-wrap"> <div class="list-items row flex-wrap-wrap">
<slot name="list-items" /> <slot name="list-items" />
</div> </div>

View File

@ -267,6 +267,42 @@ export default {
}, },
list: { list: {
newEntry: 'New entry', newEntry: 'New entry',
landed: 'Landed',
invoiceNumber: 'Invoice number',
supplier: 'Supplier',
booked: 'Booked',
confirmed: 'Confirmed',
ordered: 'Ordered',
},
summary: {
commission: 'Commission',
currency: 'Currency',
company: 'Company',
reference: 'Reference',
invoiceNumber: 'Invoice number',
ordered: 'Ordered',
confirmed: 'Confirmed',
booked: 'Booked',
raid: 'Raid',
excludedFromAvailable: 'Inventory',
travelReference: 'Reference',
travelAgency: 'Agency',
travelShipped: 'Shipped',
travelWarehouseOut: 'Warehouse Out',
travelDelivered: 'Delivered',
travelLanded: 'Landed',
travelWarehouseIn: 'Warehouse In',
travelReceived: 'Received',
buys: 'Buys',
quantity: 'Quantity',
stickers: 'Stickers',
package: 'Package',
weight: 'Weight',
packing: 'Packing',
grouping: 'Grouping',
buyingValue: 'Buying value',
import: 'Import',
pvp: 'PVP',
}, },
}, },
ticket: { ticket: {

View File

@ -265,6 +265,42 @@ export default {
}, },
list: { list: {
newEntry: 'Nueva entrada', newEntry: 'Nueva entrada',
landed: 'F. entrega',
invoiceNumber: 'Núm. factura',
supplier: 'Proveedor',
booked: 'Asentado',
confirmed: 'Confirmado',
ordered: 'Pedida',
},
summary: {
commission: 'Comisión',
currency: 'Moneda',
company: 'Empresa',
reference: 'Referencia',
invoiceNumber: 'Núm. factura',
ordered: 'Pedida',
confirmed: 'Confirmado',
booked: 'Asentado',
raid: 'Redada',
excludedFromAvailable: 'Inventario',
travelReference: 'Referencia',
travelAgency: 'Agencia',
travelShipped: 'F. envio',
travelWarehouseOut: 'Alm. salida',
travelDelivered: 'Enviada',
travelLanded: 'F. entrega',
travelWarehouseIn: 'Alm. entrada',
travelReceived: 'Recibida',
buys: 'Compras',
quantity: 'Cantidad',
stickers: 'Etiquetas',
package: 'Embalaje',
weight: 'Peso',
packing: 'Packing',
grouping: 'Grouping',
buyingValue: 'Coste',
import: 'Importe',
pvp: 'PVP',
}, },
}, },
ticket: { ticket: {

View File

@ -0,0 +1,373 @@
<script setup>
import { onMounted, ref, computed, onUpdated } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import VnRow from 'components/ui/VnRow.vue';
import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import { toDate, toCurrency } from 'src/filters';
import { getUrl } from 'src/composables/getUrl';
import axios from 'axios';
onUpdated(() => summaryRef.value.fetch());
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const entityId = computed(() => $props.id || route.params.id);
const summaryRef = ref();
const entry = ref();
const entryBuys = ref([]);
const entryUrl = ref();
onMounted(async () => {
entryUrl.value = (await getUrl('entry/')) + entityId.value;
});
const tableColumnComponents = {
quantity: {
component: () => 'span',
props: () => {},
},
stickers: {
component: () => 'span',
props: () => {},
event: () => {},
},
packagingFk: {
component: () => 'span',
props: () => {},
event: () => {},
},
weight: {
component: () => 'span',
props: () => {},
event: () => {},
},
packing: {
component: () => 'span',
props: () => {},
event: () => {},
},
grouping: {
component: () => 'span',
props: () => {},
event: () => {},
},
buyingValue: {
component: () => 'span',
props: () => {},
event: () => {},
},
amount: {
component: () => 'span',
props: () => {},
event: () => {},
},
pvp: {
component: () => 'span',
props: () => {},
event: () => {},
},
};
const entriesTableColumns = computed(() => {
return [
{
label: t('entry.summary.quantity'),
field: 'quantity',
name: 'quantity',
align: 'left',
},
{
label: t('entry.summary.stickers'),
field: 'stickers',
name: 'stickers',
align: 'left',
},
{
label: t('entry.summary.package'),
field: 'packagingFk',
name: 'packagingFk',
align: 'left',
},
{
label: t('entry.summary.weight'),
field: 'weight',
name: 'weight',
align: 'left',
},
{
label: t('entry.summary.packing'),
field: 'packing',
name: 'packing',
align: 'left',
},
{
label: t('entry.summary.grouping'),
field: 'grouping',
name: 'grouping',
align: 'left',
},
{
label: t('entry.summary.buyingValue'),
field: 'buyingValue',
name: 'buyingValue',
align: 'left',
format: (value) => toCurrency(value),
},
{
label: t('entry.summary.import'),
name: 'amount',
align: 'left',
format: (_, row) => toCurrency(row.buyingValue * row.quantity),
},
{
label: t('entry.summary.pvp'),
name: 'pvp',
align: 'left',
format: (_, row) => toCurrency(row.price2) + ' / ' + toCurrency(row.price3),
},
];
});
async function setEntryData(data) {
if (data) entry.value = data;
await fetchEntryBuys();
}
const fetchEntryBuys = async () => {
try {
const { data } = await axios.get(`Entries/${entry.value.id}/getBuys`);
if (data) entryBuys.value = data;
} catch (err) {
console.error('Error fetching entry buys');
}
};
</script>
<template>
<CardSummary
ref="summaryRef"
:url="`Entries/${entityId}/getEntry`"
@on-fetch="(data) => setEntryData(data)"
>
<template #header-left>
<a class="header link" :href="entryUrl">
<QIcon name="open_in_new" color="white" size="sm" />
</a>
</template>
<template #header>
<span>{{ entry.id }} - {{ entry.supplier.nickname }}</span>
</template>
<template #body>
<QCard class="vn-one">
<a class="header link" :href="entryUrl">
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" color="primary" />
</a>
<VnRow>
<div class="col">
<VnLv
:label="t('entry.summary.commission')"
:value="entry.commission"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.currency')"
:value="entry.currency.name"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.company')"
:value="entry.company.code"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.reference')"
:value="entry.reference"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.invoiceNumber')"
:value="entry.invoiceNumber"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.ordered')"
:value="entry.isOrdered"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.confirmed')"
:value="entry.isConfirmed"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.booked')"
:value="entry.isBooked"
/>
</div>
<div class="col">
<VnLv :label="t('entry.summary.raid')" :value="entry.isRaid" />
</div>
<div class="col">
<VnLv
:label="t('entry.summary.excludedFromAvailable')"
:value="entry.isExcludedFromAvailable"
/>
</div>
</VnRow>
</QCard>
<QCard class="vn-one">
<a class="header link" :href="entryUrl">
{{ t('Travel data') }}
<QIcon name="open_in_new" color="primary" />
</a>
<VnRow>
<div class="col">
<VnLv :label="t('entry.summary.travelReference')">
<template #value>
<span class="link">
{{ entry.travel.ref }}
<TravelDescriptorProxy :id="entry.travel.id" />
</span>
</template>
</VnLv>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelAgency')"
:value="entry.travel.agency.name"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelShipped')"
:value="toDate(entry.travel.shipped)"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelWarehouseOut')"
:value="entry.travel.warehouseOut.name"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelDelivered')"
:value="entry.travel.isDelivered"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelLanded')"
:value="toDate(entry.travel.landed)"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelWarehouseIn')"
:value="entry.travel.warehouseIn.name"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelReceived')"
:value="entry.travel.isReceived"
/>
</div>
</VnRow>
</QCard>
<QCard class="vn-two" style="min-width: 100%">
<a class="header">
{{ t('entry.summary.buys') }}
</a>
<QTable
:rows="entryBuys"
:columns="entriesTableColumns"
hide-bottom
row-key="index"
class="full-width q-mt-md"
>
<template #body="{ cols, row, rowIndex }">
<QTr no-hover>
<QTd v-for="col in cols" :key="col.name">
<component
:is="tableColumnComponents[col.name].component(props)"
v-bind="tableColumnComponents[col.name].props(props)"
@click="tableColumnComponents[col.name].event(props)"
class="col-content"
>
<template
v-if="
col.name !== 'observation' &&
col.name !== 'isConfirmed'
"
>{{ col.value }}</template
>
<QTooltip v-if="col.toolTip">{{
col.toolTip
}}</QTooltip>
</component>
</QTd>
</QTr>
<QTr no-hover>
<QTd>
<span>{{ row.item.itemType.code }}</span>
</QTd>
<QTd>
<span>{{ row.item.id }}</span>
</QTd>
<QTd>
<span>{{ row.item.size }}</span>
</QTd>
<QTd>
<span>{{ toCurrency(row.item.minPrice) }}</span>
</QTd>
<QTd colspan="6">
<span>{{ row.item.concept }}</span>
<span v-if="row.item.subName" class="subName">
{{ row.item.subName }}
</span>
<fetched-tags :item="row.item" :max-length="5" />
</QTd>
</QTr>
<!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys -->
<QTr
v-if="rowIndex !== entryBuys.length - 1"
style="height: 30px"
>
<QTd colspan="10" />
</QTr>
</template>
</QTable>
</QCard>
</template>
</CardSummary>
</template>
<i18n>
es:
Travel data: 'Datos envío'
</i18n>

View File

@ -0,0 +1,29 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import EntrySummary from './EntrySummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<QDialog ref="dialogRef" @hide="onDialogHide">
<EntrySummary v-if="$props.id" :id="$props.id" />
</QDialog>
</template>
<style lang="scss">
.q-dialog .summary .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -1,22 +1,150 @@
<script setup> <script setup>
import { onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import EntrySummaryDialog from './Card/EntrySummaryDialog.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters/index';
const stateStore = useStateStore();
const router = useRouter(); const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
onMounted(async () => {
stateStore.rightDrawer = true;
});
onUnmounted(() => (stateStore.rightDrawer = false));
function navigate(id) {
router.push({ path: `/entry/${id}` });
}
const redirectToCreateView = () => { const redirectToCreateView = () => {
router.push({ name: 'EntryCreate' }); router.push({ name: 'EntryCreate' });
}; };
function viewSummary(id) {
quasar.dialog({
component: EntrySummaryDialog,
componentProps: {
id,
},
});
}
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="EntryList"
:label="t('Search entries')"
:info="t('You can search by entry reference')"
/>
</Teleport>
</template>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<QPageSticky :offset="[20, 20]"> <div class="card-list">
<QBtn fab icon="add" color="primary" @click="redirectToCreateView()" /> <VnPaginate
<QTooltip> data-key="EntryList"
{{ t('entry.list.newEntry') }} url="Entries/filter"
</QTooltip> order="landed DESC, id DESC"
</QPageSticky> auto-load
>
<template #body="{ rows }">
<CardList
v-for="row of rows"
:key="row.id"
:title="row.reference"
@click="navigate(row.id)"
:id="row.id"
:has-info-icons="!!row.isExcludedFromAvailable || !!row.isRaid"
>
<template #info-icons>
<QIcon
v-if="row.isExcludedFromAvailable"
name="vn:inventory"
color="primary"
size="xs"
>
<QTooltip>{{ t('Inventory entry') }}</QTooltip>
</QIcon>
<QIcon
v-if="row.isRaid"
name="vn:web"
color="primary"
size="xs"
>
<QTooltip>{{ t('Virtual entry') }}</QTooltip>
</QIcon>
</template>
<template #list-items>
<VnLv
:label="t('entry.list.landed')"
:value="toDate(row.landed)"
/>
<VnLv
:label="t('entry.list.booked')"
:value="!!row.isBooked"
/>
<VnLv
:label="t('entry.list.invoiceNumber')"
:value="row.invoiceNumber"
/>
<VnLv
:label="t('entry.list.confirmed')"
:value="!!row.isConfirmed"
/>
<VnLv
:label="t('entry.list.supplier')"
:value="row.supplierName"
/>
<VnLv
:label="t('entry.list.ordered')"
:value="!!row.isOrdered"
/>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id)"
color="primary"
type="submit"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
</QPage> </QPage>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="redirectToCreateView()" />
<QTooltip>
{{ t('entry.list.newEntry') }}
</QTooltip>
</QPageSticky>
</template> </template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search entries: Buscar entradas
You can search by entry reference: Puedes buscar por referencia de la entrada
Inventory entry: Es inventario
Virtual entry: Es una redada
</i18n>

View File

@ -182,4 +182,3 @@ const isAdministrative = computed(() => {
</template> </template>
</CardSummary> </CardSummary>
</template> </template>
<style lang="scss" scoped></style>

View File

@ -39,23 +39,22 @@ export default {
}, },
], ],
}, },
// { {
// name: 'EntryCard', name: 'EntryCard',
// path: ':id', path: ':id',
// component: () => import('src/pages/Entry/Card/EntryCard.vue'), component: () => import('src/pages/Entry/Card/EntryCard.vue'),
// redirect: { name: 'EntrySummary' }, redirect: { name: 'EntrySummary' },
// children: [ children: [
// { {
// name: 'EntrySummary', name: 'EntrySummary',
// path: 'summary', path: 'summary',
// meta: { meta: {
// title: 'summary', title: 'summary',
// icon: 'launch', icon: 'launch',
// }, },
// component: () => component: () => import('src/pages/Entry/Card/EntrySummary.vue'),
// import('src/pages/Entry/Card/EntrySummary.vue'), },
// }, ],
// ], },
// },
], ],
}; };