salix-front/src/pages/Ticket/Card/TicketSummary.vue

612 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';
import VnToSummary from 'src/components/ui/VnToSummary.vue';
import TicketDescriptorMenu from './TicketDescriptorMenu.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) {
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({});
}
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"
data-cy="ticketSummary"
>
<template #header-left>
<VnToSummary
v-if="route?.name !== 'TicketSummary'"
:route-name="'TicketSummary'"
:entity-id="entityId"
:url="ticketUrl"
/>
</template>
<template #header="{ entity }">
<div>
Ticket #{{ entity.id }} - {{ entity.client?.name }} ({{
entity.client?.id
}}) -
{{ entity.nickname }}
</div>
</template>
<template #header-right="{ entity }">
<div>
<QBtnDropdown
ref="stateBtnDropdownRef"
color="black"
text-color="white"
:label="t('globals.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>
<QBtn color="white" dense flat icon="more_vert" round size="md">
<QTooltip>
{{ t('components.cardDescriptor.moreOptions') }}
</QTooltip>
<QMenu>
<QList>
<TicketDescriptorMenu :ticket="entity" />
</QList>
</QMenu>
</QBtn>
</div>
</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('globals.state')">
<template #value>
<QBadge
text-color="black"
:color="entity.ticketState.state.classColor"
>
{{ entity.ticketState.state.name }}
</QBadge>
</template>
</VnLv>
<VnLv :label="t('globals.salesPerson')">
<template #value>
<VnUserLink
:name="entity.client?.salesPersonUser?.name"
:worker-id="entity.client?.salesPersonFk"
/>
</template>
</VnLv>
<VnLv :label="t('globals.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('globals.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('globals.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('globals.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('globals.pageTitles.notes')"
/>
<QVirtualScroll
:items="entity.notes"
v-slot="{ item, index }"
style="max-height: 300px"
separator
>
<QItem
:key="index"
class="vn-label-value"
style="
display: inline-block;
padding: unset;
min-height: max-content;
"
>
<span class="label" style="margin-right: 4px">
({{ item.observationType.description }}):
</span>
<span>{{ item.description }}</span>
</QItem>
</QVirtualScroll>
</QCard>
<QCard class="vn-one">
<VnTitle :text="t('ticket.summary.summaryAmount')" />
<div class="bodyCard">
<VnLv
:label="t('globals.subtotal')"
:value="toCurrency(entity.totalWithoutVat)"
/>
<VnLv
:label="t('globals.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('globals.item') }}</QTh>
<QTh auto-width>{{ t('globals.visible') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.available') }}</QTh>
<QTh auto-width>{{ t('globals.quantity') }}</QTh>
<QTh auto-width>{{ t('globals.description') }}</QTh>
<QTh auto-width>{{ t('globals.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('globals.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('globals.created') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.package') }}</QTh>
<QTh auto-width>{{ t('globals.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('globals.quantity') }}</QTh>
<QTh auto-width>{{ t('globals.description') }}</QTh>
<QTh auto-width>{{ t('globals.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('globals.description') }}</QTh>
<QTh auto-width>{{ t('globals.created') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.requester') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.attender') }}</QTh>
<QTh auto-width>{{ t('globals.quantity') }}</QTh>
<QTh auto-width>{{ t('globals.price') }}</QTh>
<QTh auto-width>{{ t('globals.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>