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

881 lines
28 KiB
Vue

<script setup>
import axios from 'axios';
import { computed, onMounted, ref, toRefs, watch } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { usePrintService } from 'composables/usePrintService';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnSmsDialog from 'components/common/VnSmsDialog.vue';
import toDate from 'filters/toDate';
import FormPopup from 'components/FormPopup.vue';
import VnSelect from 'components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnInputTime from 'src/components/common/VnInputTime.vue';
import { useAcl } from 'src/composables/useAcl';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import { useArrayData } from 'src/composables/useArrayData';
const props = defineProps({
ticket: {
type: Object,
required: true,
},
});
onMounted(() => {
restoreTicket();
});
watch(
() => props.ticket,
() => restoreTicket
);
const { push, currentRoute } = useRouter();
const { dialog, notify } = useQuasar();
const { t } = useI18n();
const { openReport, sendEmail } = usePrintService();
const ticketSummary = useArrayData('TicketSummary');
const { ticket } = toRefs(props);
const ticketId = computed(() => props.ticket.id ?? currentRoute.value.params.id);
const client = ref();
const address = ref();
const addressesOptions = ref([]);
const selectedClient = ref();
const showTransferDialog = ref(false);
const showTurnDialog = ref(false);
const showChangeTimeDialog = ref(false);
const dialogRef = ref();
const isEditable = ref();
const hasInvoicing = useAcl('invoicing');
const hasPdf = ref();
const weight = ref();
const hasDocuwareFile = ref();
const quasar = useQuasar();
const canRestoreTicket = ref(false);
const onClientSelected = async(clientId) =>{
await fetchClient(clientId);
await fetchAddresses(clientId);
};
const fetchClient = async (clientId) => {
const filter = {
include: {
relation: 'defaultAddress',
scope: {
fields: ['id', 'nickname'],
},
},
where: { id: clientId },
};
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get('Clients', { params });
const [client] = data;
selectedClient.value = client;
};
const fetchAddresses = async (clientId) => {
if (!clientId) return;
const filter = {
fields: ['nickname', 'street', 'city', 'id', 'isActive'],
order: ['isDefaultAddress DESC', 'isActive DESC', 'nickname ASC'],
};
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(`Clients/${clientId}/addresses`, {
params,
});
addressesOptions.value = data;
const { defaultAddress } = selectedClient.value;
address.value = defaultAddress.id;
};
const actions = {
clone: async () => {
const opts = { message: t('Ticket cloned'), type: 'positive' };
let clonedTicketId;
try {
const { data } = await axios.post(`Tickets/cloneAll`, {
ticketsIds: [ticket.value.id],
withWarehouse: true,
negative: false,
});
clonedTicketId = data[0].id;
} catch (e) {
opts.message = t('It was not able to clone the ticket');
opts.type = 'negative';
} finally {
notify(opts);
if (clonedTicketId)
push({ name: 'TicketSummary', params: { id: clonedTicketId } });
}
},
setWeight: async () => {
try {
const invoiceIds = (
await axios.post(`Tickets/${ticketId.value}/setWeight`, {
weight: weight.value,
})
).data;
notify({ message: t('Weight set'), type: 'positive' });
if (invoiceIds.length)
notify({
message: t('invoiceIds', { invoiceIds: invoiceIds.join() }),
type: 'positive',
});
await ticketSummary.fetch({ updateRouter: false });
} catch (e) {
notify({ message: e.message, type: 'negative' });
}
},
remove: async () => {
try {
await axios.post(`Tickets/${ticketId.value}/setDeleted`);
notify({ message: t('Ticket deleted'), type: 'positive' });
notify({
message: t('You can undo this action within the first hour'),
icon: 'info',
type: 'warning',
});
push({ name: 'TicketList' });
} catch (e) {
notify({ message: e.message, type: 'negative' });
}
},
};
function openDeliveryNote(type = 'deliveryNote', documentType = 'pdf') {
const path = `Tickets/${ticket.value.id}/delivery-note-${documentType}`;
openReport(
path,
{
recipientId: ticket.value.clientFk,
type: type,
},
'_blank'
);
}
function sendDeliveryNoteConfirmation(type = 'deliveryNote', documentType = 'pdf') {
const customer = ticket.value.client;
dialog({
component: SendEmailDialog,
componentProps: {
data: {
address: customer.email,
type: type,
documentType: documentType,
},
promise: sendDeliveryNote,
},
});
}
async function sendDeliveryNote({ address, type, documentType }) {
const id = ticket.value.id;
const customer = ticket.value.client;
let pathName = 'delivery-note-email';
if (documentType == 'csv') pathName = 'delivery-note-csv-email';
const path = `Tickets/${id}/${pathName}`;
return sendEmail(path, {
recipientId: customer.id,
recipient: address,
type: type,
});
}
const shipped = toDate(ticket.value.shipped);
function showSmsDialog(template, customData) {
const address = ticket.value.address;
const client = ticket.value.client;
const phone =
currentRoute.value.params.phone ||
address.mobile ||
address.phone ||
client.mobile ||
client.phone;
const data = {
orderId: ticket.value.id,
shipped: shipped,
};
if (typeof customData === 'object') {
Object.assign(data, customData);
}
dialog({
component: VnSmsDialog,
componentProps: {
phone: phone,
template: template,
locale: client?.user?.lang ?? 'default_locale',
data: data,
promise: sendSms,
},
});
}
async function showSmsDialogWithChanges() {
const query = `TicketLogs/${ticketId.value}/getChanges`;
const response = await axios.get(query);
showSmsDialog('orderChanges', { changes: response.data });
}
async function sendSms(body) {
await axios.post(`Tickets/${ticketId.value}/sendSms`, body);
notify({
message: 'Notification sent',
type: 'positive',
});
}
function openConfirmDialog(callback) {
dialog({
component: VnConfirm,
componentProps: {
title: t('This ticket will be removed from current route! Continue anyway?'),
message: t('You are going to delete this ticket'),
promise: actions[callback],
},
});
}
function generatePdfDialog() {
dialog({
component: VnConfirm,
componentProps: {
promise: generatePdfInvoice,
},
});
}
async function generatePdfInvoice() {
const { data } = await axios.get('invoiceOuts', {
params: {
filter: JSON.stringify({
where: { ref: ticket.value.refFk },
}),
},
});
const invoiceId = data[0].id;
const { response } = await axios.post(`InvoiceOuts/${invoiceId}/createPdf`);
if (!response) {
notify({
message: 'The invoice PDF document has been regenerated',
type: 'positive',
});
}
}
function makeInvoiceDialog() {
dialog({
component: VnConfirm,
componentProps: {
title: t('Are you sure you want to invoice this ticket?'),
message: t('You are going to invoice this ticket'),
promise: makeInvoice,
},
});
}
async function makeInvoice() {
const params = {
ticketsIds: [parseInt(ticketId.value)],
};
await axios.post(`Tickets/invoiceTicketsAndPdf`, params);
notify({
message: t('Ticket invoiced'),
type: 'positive',
});
window.location.reload();
}
async function transferClient(client, address) {
const params = {
clientFk: client,
addressFk: address,
};
await axios.patch( `Tickets/${ticketId.value}/transferClient`, params );
window.location.reload();
}
async function addTurn(day) {
const params = {
ticketFk: parseInt(ticketId.value),
weekDay: day,
agencyModeFk: ticket.value.agencyModeFk,
};
await axios.patch(`TicketWeeklies`, params);
notify({
message: t('Current ticket deleted and added to shift'),
type: 'positive',
});
window.location.reload();
}
async function createRefund(withWarehouse) {
const params = {
ticketsIds: [parseInt(ticketId.value)],
withWarehouse: withWarehouse,
negative: true,
};
const { data } = await axios.post(`Tickets/cloneAll`, params);
if (data) {
const [refundTicket] = data;
notify(t('refundTicketCreated', { ticketId: refundTicket.id }), 'positive');
push({ name: 'TicketSale', params: { id: refundTicket.id } });
}
}
async function changeShippedHour(time) {
const params = {
shipped: time,
};
const { data } = await axios.post(
`Tickets/${ticketId.value}/updateEditableTicket`,
params
);
if (data) window.location.reload();
}
function openRecalculateDialog() {
dialog({
component: VnConfirm,
componentProps: {
title: t('Recalculate components'),
message: t('Are you sure you want to recalculate components?'),
promise: recalculateComponents,
},
});
}
async function recalculateComponents() {
await axios.post(`Tickets/${ticketId.value}/recalculateComponents`);
notify({
message: t('Data saved'),
type: 'positive',
});
window.location.reload();
}
const handleFetchData = (data) => {
isEditable.value = data;
handleInvoiceOutData();
};
async function handleInvoiceOutData() {
const { data } = await axios.get(`InvoiceOuts`, {
params: {
filter: JSON.stringify({ where: { ref: ticket.value.refFk } }),
},
});
hasPdf.value = data[0]?.hasPdf;
}
async function docuwareDownload() {
await axios.get(`Tickets/${ticketId.value}/docuwareDownload`);
}
async function hasDocuware() {
const { data } = await axios.post(`Docuwares/${ticketId.value}/checkFile`, {
fileCabinet: 'deliveryNote',
signed: true,
});
hasDocuwareFile.value = data;
}
async function uploadDocuware(force) {
if (!force)
return quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Send PDF to tablet'),
message: t('Are you sure you want to replace this delivery note?'),
},
})
.onOk(async () => {
uploadDocuware(true);
});
const { data } = await axios.post(`Docuwares/upload`, {
fileCabinet: 'deliveryNote',
ticketIds: [parseInt(ticketId.value)],
});
if (data) notify({ message: t('PDF sent!'), type: 'positive' });
}
const restoreTicket = async () => {
const filter = {
fields: ['id', 'originFk', 'creationDate', 'newInstance'],
where: {
originFk: ticketId.value,
newInstance: { like: '%"isDeleted":true%' },
},
order: 'creationDate DESC',
limit: 1,
};
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(`TicketLogs`, { params });
if (data && data.length) {
const now = Date.vnNew();
const maxDate = new Date(data[0].creationDate);
maxDate.setHours(maxDate.getHours() + 1);
if (now <= maxDate) {
return (canRestoreTicket.value = true);
}
return (canRestoreTicket.value = false);
}
return (canRestoreTicket.value = false);
};
async function openRestoreConfirmation(force) {
if (!force)
return quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Are you sure you want to restore the ticket?'),
message: t('You are going to restore this ticket'),
},
})
.onOk(async () => {
ticketToRestore();
});
}
async function ticketToRestore() {
const { data } = await axios.post(`Tickets/${ticketId.value}/restore`);
if (data) {
notify({ message: t('Ticket restored'), type: 'positive' });
}
}
</script>
<template>
<FetchData
:url="`Tickets/${ticketId}/isEditable`"
auto-load
@on-fetch="handleFetchData"
/>
<QItem @click="showTransferDialog = !showTransferDialog" v-ripple clickable>
<QItemSection avatar>
<QIcon name="content_paste" />
</QItemSection>
<QItemSection>{{ t('Transfer client') }}</QItemSection>
</QItem>
<QDialog ref="dialogRef" v-model="showTransferDialog">
<FormPopup
@on-submit="transferClient(client, address)"
:title="t('Transfer client')"
:custom-submit-button-label="t('Transfer client')"
:default-cancel-button="false"
>
<template #form-inputs>
<VnSelect
url="Clients"
:fields="['id', 'name', 'defaultAddressFk']"
v-model="client"
:label="t('Client')"
auto-load
@update:model-value="() => onClientSelected(client)"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ `#${scope.opt.id} - ` }}
{{ scope.opt.name }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
:disable="!client"
:options="addressesOptions"
:fields="['id', 'nickname']"
option-value="id"
option-label="nickname"
v-model="address"
:label="t('ticketList.addressNickname')"
auto-load
:hint="!client ? t('Select a client to enable') : ''"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ `#${scope.opt.id} - ` }}
{{ scope.opt.nickname }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</template>
</FormPopup>
</QDialog>
<QItem @click="showTurnDialog = !showTurnDialog" v-ripple clickable>
<QItemSection avatar>
<QIcon name="vn:calendar" />
</QItemSection>
<QItemSection>{{ t('addTurn') }}</QItemSection>
</QItem>
<QDialog ref="dialogRef" v-model="showTurnDialog">
<FormPopup
@on-submit="addTurn"
:title="t('What is the day of receipt of the ticket?')"
:default-submit-button="false"
:default-cancel-button="false"
style="text-align: center"
>
<template #form-inputs>
<QBtnGroup spread>
<QBtn
:label="t('weekdays.mon')"
color="primary"
@click="addTurn(0)"
v-ripple
class="weekdaysBtn"
/>
<QBtn
:label="t('weekdays.tue')"
color="primary"
@click="addTurn(1)"
v-ripple
class="weekdaysBtn"
/>
<QBtn
:label="t('weekdays.wed')"
color="primary"
@click="addTurn(2)"
v-ripple
class="weekdaysBtn"
/>
<QBtn
:label="t('weekdays.thu')"
color="primary"
@click="addTurn(3)"
v-ripple
class="weekdaysBtn"
/>
<QBtn
:label="t('weekdays.fri')"
color="primary"
@click="addTurn(4)"
v-ripple
class="weekdaysBtn"
/>
<QBtn
:label="t('weekdays.sat')"
color="primary"
@click="addTurn(5)"
v-ripple
class="weekdaysBtn"
/>
<QBtn
:label="t('weekdays.sun')"
color="primary"
@click="addTurn(6)"
v-ripple
class="weekdaysBtn"
/>
</QBtnGroup>
</template>
</FormPopup>
</QDialog>
<QItem v-ripple clickable>
<QItemSection avatar>
<QIcon name="picture_as_pdf" />
</QItemSection>
<QItemSection>{{ t('Show Delivery Note...') }}</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu
anchor="top end"
self="top start"
auto-close
bordered
@click="hasDocuware()"
>
<QList>
<QItem @click="openDeliveryNote('deliveryNote')" v-ripple clickable>
<QItemSection>{{ t('as PDF') }}</QItemSection>
</QItem>
<QItem @click="openDeliveryNote('withoutPrices')" v-ripple clickable>
<QItemSection>{{ t('as PDF without prices') }}</QItemSection>
</QItem>
<QItem
v-if="hasDocuwareFile"
@click="docuwareDownload()"
v-ripple
clickable
>
<QItemSection>{{ t('as PDF signed') }}</QItemSection>
</QItem>
<QItem
@click="openDeliveryNote('deliveryNote', 'csv')"
v-ripple
clickable
>
<QItemSection>{{ t('as CSV') }}</QItemSection>
</QItem>
</QList>
</QMenu>
</QItem>
<QItem v-ripple clickable>
<QItemSection avatar>
<QIcon name="send" />
</QItemSection>
<QItemSection>{{ t('Send Delivery Note...') }}</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu anchor="top end" self="top start" auto-close @click="hasDocuware()">
<QList>
<QItem
@click="sendDeliveryNoteConfirmation('deliveryNote')"
v-ripple
clickable
>
<QItemSection>{{ t('Send PDF') }}</QItemSection>
</QItem>
<QItem @click="uploadDocuware(!hasDocuwareFile)" v-ripple clickable>
<QItemSection>{{ t('Send PDF to tablet') }}</QItemSection>
</QItem>
<QItem
@click="sendDeliveryNoteConfirmation('deliveryNote', 'csv')"
v-ripple
clickable
>
<QItemSection>{{ t('Send CSV') }}</QItemSection>
</QItem>
</QList>
</QMenu>
</QItem>
<QItem @click="openDeliveryNote('proforma')" v-ripple clickable>
<QItemSection avatar>
<QIcon name="receipt" />
</QItemSection>
<QItemSection>{{ t('Show Proforma') }}</QItemSection>
</QItem>
<QItem v-if="canRestoreTicket" @click="openRestoreConfirmation()" v-ripple clickable>
<QItemSection avatar>
<QIcon name="restore" />
</QItemSection>
<QItemSection>{{ t('Restore ticket') }}</QItemSection>
</QItem>
<QItem
v-if="isEditable"
@click="showChangeTimeDialog = !showChangeTimeDialog"
v-ripple
clickable
>
<QItemSection avatar>
<QIcon name="schedule" />
</QItemSection>
<QItemSection>{{ t('Change shipped hour') }}</QItemSection>
</QItem>
<QDialog ref="dialogRef" v-model="showChangeTimeDialog">
<FormPopup @on-submit="changeShippedHour(time)" :title="t('Change shipped hour')">
<template #form-inputs>
<VnInputTime v-model="time" :label="t('Shipped hour')" clearable />
</template>
</FormPopup>
</QDialog>
<QItem v-ripple clickable>
<QItemSection avatar>
<QIcon name="sms" />
</QItemSection>
<QItemSection>{{ t('Send SMS...') }}</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu anchor="top end" self="top start" auto-close>
<QList>
<QItem @click="showSmsDialog('pendingPayment')" v-ripple clickable>
<QItemSection>{{ t('Pending payment') }}</QItemSection>
</QItem>
<QItem @click="showSmsDialog('minAmount')" v-ripple clickable>
<QItemSection>{{ t('Minimum import') }}</QItemSection>
</QItem>
<QItem
@click="showSmsDialogWithChanges('orderChanges')"
v-ripple
clickable
>
<QItemSection>{{ t('Notify changes') }}</QItemSection>
</QItem>
</QList>
</QMenu>
</QItem>
<QItem @click="makeInvoiceDialog()" v-ripple clickable v-if="isEditable">
<QItemSection avatar>
<QIcon name="picture_as_pdf" />
</QItemSection>
<QItemSection>{{ t('Make invoice') }}</QItemSection>
</QItem>
<QItem
@click="generatePdfDialog()"
v-ripple
clickable
v-if="ticket.refFk !== null && (hasInvoicing || hasPdf)"
>
<QItemSection avatar>
<QIcon name="picture_as_pdf" />
</QItemSection>
<QItemSection>{{
hasPdf ? t('Regenerate PDF invoice') : t('Generate PDF invoice')
}}</QItemSection>
</QItem>
<QItem @click="openConfirmDialog('clone')" v-ripple clickable>
<QItemSection avatar>
<QIcon name="content_copy" />
</QItemSection>
<QItemSection>{{ t('To clone ticket') }}</QItemSection>
</QItem>
<QItem v-if="isEditable" @click="openRecalculateDialog()" v-ripple clickable>
<QItemSection avatar>
<QIcon name="refresh" />
</QItemSection>
<QItemSection>{{ t('Recalculate components') }}</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection avatar>
<QIcon name="monetization_on" />
</QItemSection>
<QItemSection>{{ t('Refund all...') }}</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu anchor="top end" self="top start" auto-close bordered>
<QList>
<QItem v-ripple clickable @click="createRefund(true)">
<QItemSection>
{{ t('with warehouse') }}
</QItemSection>
</QItem>
<QItem v-ripple clickable @click="createRefund(false)">
<QItemSection>
{{ t('without warehouse') }}
</QItemSection>
</QItem>
</QList>
</QMenu>
</QItem>
<QItem @click="$refs.weightDialog.show()" v-ripple clickable>
<QItemSection avatar>
<QIcon name="weight" />
</QItemSection>
<QItemSection>{{ t('Set weight') }}</QItemSection>
</QItem>
<template v-if="!ticket.isDeleted">
<QSeparator />
<QItem @click="openConfirmDialog('remove')" v-ripple clickable>
<QItemSection avatar>
<QIcon name="delete" />
</QItemSection>
<QItemSection>{{ t('Delete ticket') }}</QItemSection>
</QItem>
</template>
<VnConfirm
ref="weightDialog"
:title="t('Set weight')"
:message="false"
:promise="actions.setWeight"
>
<template #customHTML>
<VnInputNumber
:label="t('globals.weight')"
v-model="weight"
class="full-width"
/>
</template>
</VnConfirm>
</template>
<style lang="scss" scoped>
.weekdaysBtn {
margin: 1%;
}
</style>
<i18n>
en:
addTurn: Add turn
invoiceIds: "Invoices have been generated with the following ids: {invoiceIds}"
es:
Show Delivery Note...: Ver albarán...
Send Delivery Note...: Enviar albarán...
as PDF: como PDF
as PDF without prices: como PDF sin precios
as CSV: Como CSV
Send PDF: Enviar PDF
Send PDF to tablet: Enviar PDF a tablet
Send CSV: Enviar CSV
Show Proforma: Ver proforma
Delete ticket: Eliminar ticket
Send SMS...: Enviar SMS...
Pending payment: Pago pendiente
Minimum import: Importe mínimo
Notify changes: Notificar cambios
Ticket deleted: Ticket eliminado
You can undo this action within the first hour: Puedes deshacer esta acción dentro de la primera hora
To clone ticket: Clonar ticket
Ticket cloned: Ticked clonado
It was not able to clone the ticket: No se pudo clonar el ticket
Generate PDF invoice: Generar PDF factura
Regenerate PDF invoice: Regenerar PDF factura
The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado
Transfer client: Transferir cliente
Client: Cliente
addTurn: Añadir a turno
What is the day of receipt of the ticket?: ¿Cuál es el día de preparación del pedido?
Current ticket deleted and added to shift: Ticket actual eliminado y añadido al turno
Refund all...: Abonar todo...
with warehouse: con almacén
without warehouse: sin almacén
Make invoice: Crear factura
Change shipped hour: Cambiar hora de envío
Shipped hour: Hora de envío
Recalculate components: Recalcular componentes
Are you sure you want to recalculate components?: ¿Seguro que quieres recalcular los componentes?
Data saved: Datos guardados
Are you sure you want to invoice this ticket?: ¿Seguro que quieres facturar este ticket?
You are going to invoice this ticket: Vas a facturar este ticket
Ticket invoiced: Ticket facturado
Set weight: Establecer peso
Weight set: Peso establecido
invoiceIds: "Se han generado las facturas con los siguientes ids: {invoiceIds}"
This ticket will be removed from current route! Continue anyway?: ¡Se eliminará el ticket de la ruta actual! ¿Continuar de todas formas?
You are going to delete this ticket: Vas a eliminar este ticket
as PDF signed: como PDF firmado
Are you sure you want to replace this delivery note?: ¿Seguro que quieres reemplazar este albarán?
Restore ticket: Restaurar ticket
Are you sure you want to restore the ticket?: ¿Seguro que quieres restaurar el ticket?
You are going to restore this ticket: Vas a restaurar este ticket
Ticket restored: Ticket restaurado
Select a client to enable: Selecciona un cliente para habilitar
</i18n>