forked from verdnatura/salix-front
649 lines
26 KiB
Vue
649 lines
26 KiB
Vue
<script setup>
|
|
import RouteDescriptorProxy from 'pages/Route/Card/RouteDescriptorProxy.vue';
|
|
import { onMounted, ref, computed } from 'vue';
|
|
import { useRoute } from 'vue-router';
|
|
import { useI18n } from 'vue-i18n';
|
|
import axios from 'axios';
|
|
import { dashIfEmpty, toDate, toCurrency } from 'src/filters';
|
|
import CardSummary from 'components/ui/CardSummary.vue';
|
|
import FetchData from 'components/FetchData.vue';
|
|
import FetchedTags from 'components/ui/FetchedTags.vue';
|
|
import InvoiceOutDescriptorProxy from 'pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue';
|
|
import VnLv from 'src/components/ui/VnLv.vue';
|
|
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
|
|
import { getUrl } from 'src/composables/getUrl';
|
|
import useNotify from 'src/composables/useNotify.js';
|
|
import { useArrayData } from 'composables/useArrayData';
|
|
import VnUserLink from 'src/components/ui/VnUserLink.vue';
|
|
import VnTitle from 'src/components/common/VnTitle.vue';
|
|
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
|
|
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
|
|
import VnSelect from 'src/components/common/VnSelect.vue';
|
|
|
|
const route = useRoute();
|
|
const { notify } = useNotify();
|
|
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 ticket = computed(() => summaryRef.value?.entity);
|
|
const editableStates = ref([]);
|
|
const ticketUrl = ref();
|
|
const grafanaUrl = 'https://grafana.verdnatura.es';
|
|
const stateBtnDropdownRef = ref();
|
|
const descriptorData = useArrayData('ticketData');
|
|
|
|
onMounted(async () => {
|
|
ticketUrl.value = (await getUrl('ticket/')) + entityId.value + '/';
|
|
});
|
|
|
|
function formattedAddress() {
|
|
if (!ticket.value) return '';
|
|
|
|
const address = ticket.value.address;
|
|
const postcode = address.postalCode;
|
|
const province = address.province ? `(${address.province.name})` : '';
|
|
|
|
return `${address.street} - ${postcode} - ${address.city} ${province}`;
|
|
}
|
|
|
|
function isEditable() {
|
|
try {
|
|
return !ticket.value.ticketState?.state?.alertLevel;
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async function changeState(value) {
|
|
try {
|
|
stateBtnDropdownRef.value.hide();
|
|
const formData = {
|
|
ticketFk: entityId.value,
|
|
code: value,
|
|
};
|
|
await axios.post(`Tickets/state`, formData);
|
|
notify('globals.dataSaved', 'positive');
|
|
summaryRef.value?.fetch();
|
|
descriptorData.fetch({});
|
|
} catch (err) {
|
|
console.error('Error changing ticket state', err);
|
|
}
|
|
}
|
|
|
|
function getNoteValue(description) {
|
|
switch (description) {
|
|
case 'ItemPicker':
|
|
return t('ItemPicker');
|
|
case 'Packager':
|
|
return t('Packager');
|
|
case 'Delivery':
|
|
return t('Delivery');
|
|
case 'SalesPerson':
|
|
return t('SalesPerson');
|
|
case 'Administrative':
|
|
return t('Administrative');
|
|
case 'Weight':
|
|
return t('Weight');
|
|
case 'InvoiceOut':
|
|
return t('InvoiceOut');
|
|
case 'DropOff':
|
|
return t('DropOff');
|
|
case 'Sustitución':
|
|
return t('Sustitución');
|
|
}
|
|
}
|
|
|
|
function toTicketUrl(section) {
|
|
return '#/ticket/' + entityId.value + '/' + section;
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<FetchData
|
|
url="States/editableStates"
|
|
:filter="{ fields: ['code', 'name', 'id', 'alertLevel'], order: 'name ASC' }"
|
|
auto-load
|
|
@on-fetch="(data) => (editableStates = data)"
|
|
/>
|
|
<CardSummary
|
|
ref="summaryRef"
|
|
:url="`Tickets/${entityId}/summary`"
|
|
data-key="TicketSummary"
|
|
>
|
|
<template #header="{ entity }">
|
|
<div>
|
|
Ticket #{{ entity.id }} - {{ entity.client?.name }} ({{
|
|
entity.client?.id
|
|
}}) -
|
|
{{ entity.nickname }}
|
|
</div>
|
|
</template>
|
|
<template #header-right>
|
|
<QBtnDropdown
|
|
ref="stateBtnDropdownRef"
|
|
color="black"
|
|
text-color="white"
|
|
:label="t('ticket.summary.changeState')"
|
|
:disable="!isEditable()"
|
|
>
|
|
<VnSelect
|
|
:options="editableStates"
|
|
hide-selected
|
|
option-label="name"
|
|
option-value="code"
|
|
hide-dropdown-icon
|
|
focus-on-mount
|
|
@update:model-value="changeState"
|
|
/>
|
|
</QBtnDropdown>
|
|
</template>
|
|
<template #body="{ entity }">
|
|
<QCard class="vn-one">
|
|
<VnTitle
|
|
:url="toTicketUrl('basic-data')"
|
|
:text="t('globals.summary.basicData')"
|
|
/>
|
|
<VnLv v-if="entity.ticketState" :label="t('ticket.summary.state')">
|
|
<template #value>
|
|
<QBadge
|
|
text-color="black"
|
|
:color="entity.ticketState.state.classColor"
|
|
>
|
|
{{ entity.ticketState.state.name }}
|
|
</QBadge>
|
|
</template>
|
|
</VnLv>
|
|
<VnLv :label="t('ticket.summary.salesPerson')">
|
|
<template #value>
|
|
<VnUserLink
|
|
:name="entity.client?.salesPersonUser?.name"
|
|
:worker-id="entity.client?.salesPersonFk"
|
|
/>
|
|
</template>
|
|
</VnLv>
|
|
<VnLv
|
|
:label="t('ticket.summary.agency')"
|
|
:value="entity.agencyMode?.name"
|
|
/>
|
|
<VnLv :label="t('ticket.summary.zone')">
|
|
<template #value>
|
|
<span class="link" @click.stop>
|
|
{{ entity?.zone?.name }}
|
|
<ZoneDescriptorProxy :id="entity.zoneFk" />
|
|
</span>
|
|
</template>
|
|
</VnLv>
|
|
<VnLv
|
|
:label="t('ticket.summary.warehouse')"
|
|
:value="entity.warehouse?.name"
|
|
/>
|
|
<VnLv
|
|
v-if="ticket?.ticketCollections?.length > 0"
|
|
:label="t('ticket.summary.collection')"
|
|
:value="ticket?.ticketCollections[0]?.collectionFk"
|
|
>
|
|
<template #value>
|
|
<a
|
|
:href="`${grafanaUrl}/d/d552ab74-85b4-4e7f-a279-fab7cd9c6124/control-de-expediciones?orgId=1&var-collectionFk=${entity.ticketCollections[0]?.collectionFk}`"
|
|
target="_blank"
|
|
class="grafana"
|
|
>
|
|
{{ entity.ticketCollections[0]?.collectionFk }}
|
|
</a>
|
|
</template>
|
|
</VnLv>
|
|
<VnLv :label="t('ticket.summary.route')">
|
|
<template #value>
|
|
<span class="link">
|
|
{{ entity.routeFk }}
|
|
<RouteDescriptorProxy :id="entity.routeFk" />
|
|
</span>
|
|
</template>
|
|
</VnLv>
|
|
<VnLv :label="t('ticket.summary.invoice')">
|
|
<template #value>
|
|
<span :class="{ link: entity.refFk }">
|
|
{{ dashIfEmpty(entity.refFk) }}
|
|
<InvoiceOutDescriptorProxy
|
|
:id="entity.invoiceOut.id"
|
|
v-if="entity.refFk"
|
|
/>
|
|
</span>
|
|
</template>
|
|
</VnLv>
|
|
<VnLv
|
|
:label="t('ticket.summary.weight')"
|
|
:value="dashIfEmpty(entity.weight)"
|
|
/>
|
|
</QCard>
|
|
<QCard class="vn-one" style="flex: 2 1">
|
|
<VnTitle
|
|
:url="toTicketUrl('basic-data')"
|
|
:text="t('globals.summary.basicData')"
|
|
/>
|
|
<VnLv
|
|
:label="t('ticket.summary.shipped')"
|
|
:value="toDate(entity.shipped)"
|
|
/>
|
|
<VnLv
|
|
:label="t('ticket.summary.landed')"
|
|
:value="toDate(entity.landed)"
|
|
/>
|
|
<VnLv :label="t('globals.packages')" :value="entity.packages" />
|
|
<VnLv :value="entity.address.phone">
|
|
<template #label>
|
|
{{ t('ticket.summary.consigneePhone') }}
|
|
<VnLinkPhone :phone-number="entity.address.phone" />
|
|
</template>
|
|
</VnLv>
|
|
<VnLv :value="entity.address.mobile">
|
|
<template #label>
|
|
{{ t('ticket.summary.consigneeMobile') }}
|
|
<VnLinkPhone :phone-number="entity.address.mobile" />
|
|
</template>
|
|
</VnLv>
|
|
<VnLv :value="entity.client.phone">
|
|
<template #label>
|
|
{{ t('ticket.summary.clientPhone') }}
|
|
<VnLinkPhone :phone-number="entity.client.phone" />
|
|
</template>
|
|
</VnLv>
|
|
<VnLv :value="entity.client.mobile">
|
|
<template #label>
|
|
{{ t('ticket.summary.clientMobile') }}
|
|
<VnLinkPhone :phone-number="entity.client.mobile" />
|
|
</template>
|
|
</VnLv>
|
|
<VnLv
|
|
:label="t('ticket.summary.consignee')"
|
|
:value="formattedAddress()"
|
|
/>
|
|
</QCard>
|
|
<QCard class="vn-one" v-if="entity.notes.length">
|
|
<VnTitle
|
|
:url="toTicketUrl('observation')"
|
|
:text="t('ticket.pageTitles.notes')"
|
|
/>
|
|
<VnLv
|
|
v-for="note in entity.notes"
|
|
:key="note.id"
|
|
:label="getNoteValue(note.observationType.description)"
|
|
:value="note.description"
|
|
>
|
|
<template #value>
|
|
<QInput
|
|
v-model="note.description"
|
|
filled
|
|
type="textarea"
|
|
class="notes"
|
|
readonly
|
|
autogrow
|
|
/>
|
|
</template>
|
|
</VnLv>
|
|
</QCard>
|
|
<QCard class="vn-one">
|
|
<VnTitle :text="t('ticket.summary.summaryAmount')" />
|
|
<div class="bodyCard">
|
|
<VnLv
|
|
:label="t('ticket.summary.subtotal')"
|
|
:value="toCurrency(entity.totalWithoutVat)"
|
|
/>
|
|
<VnLv
|
|
:label="t('ticket.summary.vat')"
|
|
:value="toCurrency(entity.totalWithVat - entity.totalWithoutVat)"
|
|
/>
|
|
<VnLv
|
|
:label="t('ticket.summary.total')"
|
|
:value="toCurrency(ticket.totalWithVat)"
|
|
style="font-weight: bold"
|
|
/>
|
|
</div>
|
|
</QCard>
|
|
<QCard class="vn-max">
|
|
<VnTitle
|
|
:url="toTicketUrl('sale')"
|
|
:text="t('ticket.summary.saleLines')"
|
|
/>
|
|
<QTable :rows="entity.sales" style="text-align: center">
|
|
<template #body-cell="{ value }">
|
|
<QTd>{{ value }}</QTd>
|
|
</template>
|
|
<template #header="props">
|
|
<QTr class="tr-header" :props="props">
|
|
<QTh auto-width></QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.item') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.visible') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.available') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh>
|
|
<QTh auto-width>{{ t('globals.description') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.price') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.discount') }}</QTh>
|
|
<QTh auto-width>{{ t('globals.amount') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.packing') }}</QTh>
|
|
</QTr>
|
|
</template>
|
|
<template #body="props">
|
|
<QTr :props="props">
|
|
<QTd class="q-gutter-x-xs">
|
|
<QBtn
|
|
flat
|
|
round
|
|
icon="vn:claims"
|
|
v-if="props.row.claim"
|
|
color="primary"
|
|
:to="{
|
|
name: 'ClaimCard',
|
|
params: {
|
|
id: props.row.claim.claimFk,
|
|
},
|
|
}"
|
|
>
|
|
<QTooltip>
|
|
{{ t('ticket.summary.claim') }}:
|
|
{{ props.row.claim.claimFk }}
|
|
</QTooltip>
|
|
</QBtn>
|
|
<QBtn
|
|
flat
|
|
round
|
|
icon="vn:claims"
|
|
v-if="props.row.claimBeginning"
|
|
color="primary"
|
|
:to="{
|
|
name: 'ClaimCard',
|
|
params: {
|
|
id: props.row.claimBeginning.claimFk,
|
|
},
|
|
}"
|
|
>
|
|
<QTooltip>
|
|
{{ t('ticket.summary.claim') }}:
|
|
{{ props.row.claimBeginning.claimFk }}
|
|
</QTooltip>
|
|
</QBtn>
|
|
<QIcon
|
|
name="warning"
|
|
v-show="props.row.visible < 0"
|
|
color="primary"
|
|
size="xs"
|
|
>
|
|
<QTooltip>
|
|
{{ t('ticket.summary.visible') }}:
|
|
{{ props.row.visible }}
|
|
</QTooltip>
|
|
</QIcon>
|
|
<QIcon
|
|
name="vn:reserved"
|
|
v-show="props.row.reserved"
|
|
color="primary"
|
|
size="xs"
|
|
>
|
|
<QTooltip>
|
|
{{ t('ticket.summary.reserved') }}
|
|
</QTooltip>
|
|
</QIcon>
|
|
<QIcon
|
|
name="vn:unavailable"
|
|
v-show="props.row.itemShortage"
|
|
color="primary"
|
|
size="xs"
|
|
>
|
|
<QTooltip>
|
|
{{ t('ticket.summary.itemShortage') }}
|
|
</QTooltip>
|
|
</QIcon>
|
|
<QIcon
|
|
name="vn:components"
|
|
v-show="props.row.hasComponentLack"
|
|
color="primary"
|
|
size="xs"
|
|
>
|
|
<QTooltip>
|
|
{{ t('ticket.summary.hasComponentLack') }}
|
|
</QTooltip>
|
|
</QIcon>
|
|
</QTd>
|
|
<QTd>
|
|
<QBtn class="link" flat>
|
|
{{ props.row.itemFk }}
|
|
<ItemDescriptorProxy
|
|
:id="props.row.itemFk"
|
|
:sale-fk="props.row.id"
|
|
:warehouse-fk="ticket.warehouseFk"
|
|
/>
|
|
</QBtn>
|
|
</QTd>
|
|
<QTd>
|
|
<QChip
|
|
v-if="props.row.visible < 0"
|
|
dense
|
|
rounded
|
|
:color="'negative'"
|
|
text-color="white"
|
|
>
|
|
{{ props.row.visible }}
|
|
</QChip>
|
|
<span v-else>
|
|
{{ props.row.visible }}
|
|
</span>
|
|
</QTd>
|
|
<QTd>
|
|
<QChip
|
|
v-if="props.row.available < 0"
|
|
dense
|
|
rounded
|
|
:color="'negative'"
|
|
text-color="white"
|
|
>
|
|
{{ props.row.available }}
|
|
</QChip>
|
|
<span v-else>
|
|
{{ props.row.available }}
|
|
</span>
|
|
</QTd>
|
|
<QTd>{{ props.row.quantity }}</QTd>
|
|
<QTd class="description-cell">
|
|
<div class="row full-width justify-between">
|
|
{{ props.row.concept }}
|
|
<div v-if="props.row.item.subName" class="subName">
|
|
{{ props.row.item.subName.toUpperCase() }}
|
|
</div>
|
|
</div>
|
|
<FetchedTags
|
|
class="fetched-tags"
|
|
:item="props.row.item"
|
|
></FetchedTags>
|
|
</QTd>
|
|
<QTd>{{ props.row.price }} €</QTd>
|
|
<QTd>{{ props.row.discount }} %</QTd>
|
|
<QTd
|
|
>{{
|
|
toCurrency(
|
|
props.row.quantity *
|
|
props.row.price *
|
|
((100 - props.row.discount) / 100)
|
|
)
|
|
}}
|
|
</QTd>
|
|
<QTd>{{ dashIfEmpty(props.row.item.itemPackingTypeFk) }}</QTd>
|
|
</QTr>
|
|
</template>
|
|
</QTable>
|
|
</QCard>
|
|
<QCard class="vn-max" v-if="ticket.packagings.length != 0">
|
|
<VnTitle :url="toTicketUrl('package')" :text="t('globals.packages')" />
|
|
<QTable :rows="ticket.packagings" flat style="text-align: center">
|
|
<template #header="props">
|
|
<QTr class="tr-header" :props="props">
|
|
<QTh auto-width>{{ t('ticket.summary.created') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.package') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh>
|
|
</QTr>
|
|
</template>
|
|
<template #body="props">
|
|
<QTr :props="props">
|
|
<QTd>{{ toDate(props.row.created) }}</QTd>
|
|
<QTd>{{ props.row.packaging.item.name }}</QTd>
|
|
<QTd>{{ props.row.quantity }}</QTd>
|
|
</QTr>
|
|
</template>
|
|
</QTable>
|
|
</QCard>
|
|
<QCard class="vn-max" v-if="ticket.services.length != 0">
|
|
<VnTitle
|
|
:url="toTicketUrl('service')"
|
|
:text="t('ticket.summary.service')"
|
|
/>
|
|
<QTable :rows="ticket.services" flat style="text-align: center">
|
|
<template #header="props">
|
|
<QTr class="tr-header" :props="props">
|
|
<QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh>
|
|
<QTh auto-width>{{ t('globals.description') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.price') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.taxClass') }}</QTh>
|
|
<QTh auto-width>{{ t('globals.amount') }}</QTh>
|
|
</QTr>
|
|
</template>
|
|
<template #body="props">
|
|
<QTr :props="props">
|
|
<QTd>{{ props.row.quantity }}</QTd>
|
|
<QTd>{{ props.row.description }}</QTd>
|
|
<QTd>{{ toCurrency(props.row.price) }}</QTd>
|
|
<QTd>{{ props.row.taxClass.description }}</QTd>
|
|
<QTd>{{
|
|
toCurrency(props.row.quantity * props.row.price)
|
|
}}</QTd>
|
|
</QTr>
|
|
</template>
|
|
</QTable>
|
|
</QCard>
|
|
<QCard class="vn-max" v-if="ticket.requests.length != 0">
|
|
<VnTitle
|
|
:url="toTicketUrl('request')"
|
|
:text="t('ticket.summary.purchaseRequest')"
|
|
/>
|
|
<QTable :rows="ticket.requests" flat style="text-align: center">
|
|
<template #header="props">
|
|
<QTr class="tr-header" :props="props">
|
|
<QTh auto-width>{{ t('ticket.summary.description') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.created') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.requester') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.attender') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.price') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.item') }}</QTh>
|
|
<QTh auto-width>{{ t('ticket.summary.ok') }}</QTh>
|
|
</QTr>
|
|
</template>
|
|
<template #body="props">
|
|
<QTr :props="props">
|
|
<QTd>{{ props.row.description }}</QTd>
|
|
<QTd>{{ toDate(props.row.created) }}</QTd>
|
|
<QTd>{{ props.row.requester?.user?.username }}</QTd>
|
|
<QTd>{{ props.row.atender?.user?.username }}</QTd>
|
|
<QTd>{{ props.row.quantity }}</QTd>
|
|
<QTd>{{ toCurrency(props.row.price) }}</QTd>
|
|
<QTd>
|
|
<span class="link" v-if="props.row.isOk">
|
|
{{ props.row.itemFk }}
|
|
<ItemDescriptorProxy :id="props.row.itemFk" />
|
|
</span>
|
|
</QTd>
|
|
<QTd>
|
|
<QCheckbox
|
|
v-model="props.row.isOk"
|
|
disable
|
|
:toggle-indeterminate="false"
|
|
>
|
|
<QTooltip v-if="props.row.isOk">
|
|
{{ t('Accepted') }}
|
|
</QTooltip>
|
|
<QTooltip v-else>
|
|
{{ t('Denied') }}
|
|
</QTooltip>
|
|
</QCheckbox>
|
|
</QTd>
|
|
</QTr>
|
|
</template>
|
|
</QTable>
|
|
</QCard>
|
|
</template>
|
|
</CardSummary>
|
|
</template>
|
|
<style lang="scss" scoped>
|
|
.notes {
|
|
width: fit-content;
|
|
}
|
|
|
|
.q-card.q-card--dark.q-dark.vn-one {
|
|
& > .bodyCard {
|
|
padding: 1%;
|
|
}
|
|
}
|
|
.q-table {
|
|
tr,
|
|
th,
|
|
.q-td {
|
|
border-bottom: 1px solid black;
|
|
}
|
|
}
|
|
|
|
.subName {
|
|
margin-left: 10%;
|
|
}
|
|
|
|
.tr-header,
|
|
.subName {
|
|
color: var(--vn-label-color);
|
|
}
|
|
|
|
.description-cell {
|
|
width: 25%;
|
|
}
|
|
|
|
.fetched-tags {
|
|
max-width: 70%;
|
|
}
|
|
.grafana {
|
|
color: $primary-light;
|
|
}
|
|
</style>
|
|
|
|
<i18n>
|
|
en:
|
|
ItemPicker: Item Picker
|
|
Packager: Packager
|
|
Delivery: Delivery
|
|
SalesPerson: Sales Person
|
|
Administrative: Administrative
|
|
Weight: Weight
|
|
InvoiceOut: Invoice Out
|
|
DropOff: Drop Off
|
|
Sustitución: Sustitution
|
|
es:
|
|
ItemPicker: Items
|
|
Packager: Encajador
|
|
Delivery: Envío
|
|
SalesPerson: Comercial
|
|
Administrative: Administrativa
|
|
Weight: Peso
|
|
InvoiceOut: Facturas
|
|
DropOff: Despacho
|
|
Sustitución: Sustitución
|
|
Accepted: Aceptado
|
|
Denied: Denegado
|
|
</i18n>
|