0
0
Fork 0

Merge pull request '5387-descriptor_sms' (!42) from 5387-descriptor_sms into dev

Reviewed-on: verdnatura/salix-front#42
This commit is contained in:
Joan Sanchez 2023-03-10 12:20:14 +00:00
commit 97f3604fc4
14 changed files with 739 additions and 146 deletions

View File

@ -44,6 +44,11 @@ function responseError(error) {
let message = error.message; let message = error.message;
let logOut = false; let logOut = false;
const response = error.response;
if (response && response.data.error) {
message = response.data.error.message;
}
switch (error.response?.status) { switch (error.response?.status) {
case 401: case 401:
message = 'login.loginError'; message = 'login.loginError';

View File

@ -3,12 +3,13 @@ import { ref } from 'vue';
import { useDialogPluginComponent } from 'quasar'; import { useDialogPluginComponent } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const $props = defineProps({ const props = defineProps({
address: { data: {
type: String, type: Object,
default: '', requied: true,
default: null,
}, },
send: { promise: {
type: Function, type: Function,
required: true, required: true,
}, },
@ -19,24 +20,30 @@ defineEmits(['confirm', ...useDialogPluginComponent.emits]);
const { dialogRef, onDialogOK } = useDialogPluginComponent(); const { dialogRef, onDialogOK } = useDialogPluginComponent();
const { t } = useI18n(); const { t } = useI18n();
const address = ref($props.address); const address = ref(props.data.address);
const isLoading = ref(false); const isLoading = ref(false);
async function confirm() { async function confirm() {
isLoading.value = true; const response = { address };
await $props.send(address.value);
isLoading.value = false;
onDialogOK(); if (props.promise) {
isLoading.value = true;
try {
Object.assign(response, props.data);
await props.promise(response);
} finally {
isLoading.value = false;
}
}
onDialogOK(response);
} }
</script> </script>
<template> <template>
<q-dialog ref="dialogRef" persistent> <q-dialog ref="dialogRef" persistent>
<q-card class="q-pa-sm"> <q-card class="q-pa-sm">
<q-card-section class="row items-center q-pb-none"> <q-card-section class="row items-center q-pb-none">
<span class="text-h6 text-grey">{{ <span class="text-h6 text-grey">{{ t('Send email notification') }}</span>
t('Send email notification: Send email notification')
}}</span>
<q-space /> <q-space />
<q-btn icon="close" flat round dense v-close-popup /> <q-btn icon="close" flat round dense v-close-popup />
</q-card-section> </q-card-section>
@ -53,6 +60,7 @@ async function confirm() {
color="primary" color="primary"
:loading="isLoading" :loading="isLoading"
@click="confirm" @click="confirm"
unelevated
/> />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
@ -67,6 +75,6 @@ async function confirm() {
<i18n> <i18n>
es: es:
Send email notification: Enviar notificación por correo, Send email notification: Enviar notificación por correo
The notification will be sent to the following address: La notificación se enviará a la siguiente dirección The notification will be sent to the following address: La notificación se enviará a la siguiente dirección
</i18n> </i18n>

View File

@ -0,0 +1,243 @@
<script setup>
import { ref, computed } from 'vue';
import { useDialogPluginComponent } from 'quasar';
import { useI18n } from 'vue-i18n';
const { dialogRef, onDialogOK } = useDialogPluginComponent();
const { t, availableLocales } = useI18n();
defineEmits(['confirm', ...useDialogPluginComponent.emits]);
const props = defineProps({
subject: {
type: String,
required: false,
default: 'Verdnatura',
},
phone: {
type: String,
required: true,
},
template: {
type: String,
required: true,
},
locale: {
type: String,
required: false,
default: 'es',
},
data: {
type: Object,
required: false,
default: null,
},
promise: {
type: Function,
required: true,
},
});
const maxLength = 160;
const locale = ref(props.locale);
const subject = ref(props.subject);
const phone = ref(props.phone);
const message = ref('');
updateMessage();
function updateMessage() {
const params = props.data;
const key = `templates['${props.template}']`;
message.value = t(key, params, { locale: locale.value });
}
const totalLength = computed(() => message.value.length);
const color = computed(() => {
if (totalLength.value == maxLength) return 'negative';
if ((totalLength.value / maxLength) * 100 > 90) return 'warning';
return 'positive';
});
const languages = availableLocales.map((locale) => ({ label: t(locale), value: locale }));
const isLoading = ref(false);
async function send() {
const response = {
destination: phone.value,
message: message.value,
};
if (props.promise) {
isLoading.value = true;
try {
Object.assign(response, props.data);
await props.promise(response);
} finally {
isLoading.value = false;
}
}
onDialogOK(response);
}
</script>
<template>
<q-dialog ref="dialogRef" persistent>
<q-card class="q-pa-sm">
<q-card-section class="row items-center q-pb-none">
<span class="text-h6 text-grey">
{{ t('Send SMS') }}
</span>
<q-space />
<q-btn icon="close" :disable="isLoading" flat round dense v-close-popup />
</q-card-section>
<q-card-section>
<q-banner class="bg-amber text-white" rounded dense>
<template #avatar>
<q-icon name="warning" />
</template>
<span
v-html="t('CustomerDefaultLanguage', { locale: t(props.locale) })"
></span>
</q-banner>
</q-card-section>
<q-card-section class="q-pb-xs">
<q-select
:label="t('Language')"
:options="languages"
v-model="locale"
@update:model-value="updateMessage()"
emit-value
map-options
:input-debounce="0"
rounded
outlined
dense
/>
</q-card-section>
<q-card-section class="q-pb-xs">
<q-input
:label="t('Phone')"
v-model="phone"
rounded
outlined
autofocus
dense
/>
</q-card-section>
<q-card-section class="q-pb-xs">
<q-input
:label="t('Subject')"
v-model="subject"
rounded
outlined
autofocus
dense
/>
</q-card-section>
<q-card-section class="q-mb-md" q-input>
<q-input
:label="t('Message')"
v-model="message"
type="textarea"
:maxlength="maxLength"
:counter="true"
:autogrow="true"
:bottom-slots="true"
:rules="[(value) => value.length < maxLength || 'Error!']"
stack-label
outlined
autofocus
>
<template #append>
<q-icon
v-if="message !== ''"
name="close"
@click="message = ''"
class="cursor-pointer"
/>
</template>
<template #counter>
<q-chip :color="color" dense>
{{ totalLength }}/{{ maxLength }}
</q-chip>
</template>
</q-input>
</q-card-section>
<q-card-actions align="right">
<q-btn
:label="t('globals.cancel')"
color="primary"
:disable="isLoading"
flat
v-close-popup
/>
<q-btn
:label="t('globals.confirm')"
@click="send()"
:loading="isLoading"
color="primary"
unelevated
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<style lang="scss" scoped>
.q-chip {
transition: background 0.36s;
}
.q-card {
width: 500px;
}
</style>
<i18n>
en:
CustomerDefaultLanguage: This customer uses <strong>{locale}</strong> as their default language
en: English
es: Spanish
fr: French
templates:
pendingPayment: 'Your order is pending of payment.
Please, enter the website and make the payment with a credit card. Thank you.'
minAmount: 'A minimum amount of 50 (VAT excluded) is required for your order
{ orderId } of { shipped } to receive it without additional shipping costs.'
orderChanges: 'Order {orderId} of { shipped }\r
{ changes }'
es:
Send SMS: Enviar SMS
CustomerDefaultLanguage: Este cliente utiliza <strong>{locale}</strong> como idioma por defecto
Language: Idioma
Phone: Móvil
Subject: Asunto
Message: Mensaje
templates:
pendingPayment: 'Su pedido está pendiente de pago.
Por favor, entre en la página web y efectue el pago con tarjeta. Muchas gracias.'
minAmount: 'Es necesario un importe mínimo de 50 (Sin IVA) en su pedido
{ orderId } del día { shipped } para recibirlo sin portes adicionales.'
orderChanges: 'Pedido {orderId} día { shipped }
{ changes }'
en: Inglés
es: Español
fr: Francés
fr:
Send SMS: Envoyer SMS
CustomerDefaultLanguage: Ce client utilise l'{locale} comme langue par défaut
Language: Langage
Phone: Mobile
Subject: Affaire
Message: Message
templates:
pendingPayment: 'Votre commande est en attente de paiement.
Veuillez vous connecter sur le site web et effectuer le paiement par carte. Merci beaucoup.'
minAmount: 'Un montant minimum de 50 (TVA non incluse) est requis pour votre commande
{ orderId } du { shipped } afin de la recevoir sans frais de port supplémentaires.'
orderChanges: 'Commande { orderId } du { shipped }\r { changes }'
en: Anglais
es: Espagnol
fr: Français
</i18n>

View File

@ -59,9 +59,9 @@ watch(props, async () => {
:to="{ name: `${module}Summary`, params: { id: entity.id } }" :to="{ name: `${module}Summary`, params: { id: entity.id } }"
> >
<q-btn round flat dense size="md" icon="launch" color="white"> <q-btn round flat dense size="md" icon="launch" color="white">
<q-tooltip>{{ <q-tooltip>
t('components.cardDescriptor.summary') {{ t('components.cardDescriptor.summary') }}
}}</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
</router-link> </router-link>

View File

@ -10,7 +10,7 @@ const props = defineProps({
type: String, type: String,
default: null, default: null,
}, },
question: { title: {
type: String, type: String,
default: null, default: null,
}, },
@ -18,15 +18,37 @@ const props = defineProps({
type: String, type: String,
default: null, default: null,
}, },
data: {
type: Object,
required: false,
default: null,
},
promise: {
type: Function,
required: false,
default: null,
},
}); });
defineEmits(['confirm', ...useDialogPluginComponent.emits]); defineEmits(['confirm', ...useDialogPluginComponent.emits]);
const { dialogRef, onDialogOK } = useDialogPluginComponent(); const { dialogRef, onDialogOK } = useDialogPluginComponent();
const question = props.question || t('question'); const title = props.title || t('Confirm');
const message = props.message || t('message'); const message = props.message || t('Are you sure you want to continue?');
const isLoading = ref(false); const isLoading = ref(false);
async function confirm() {
isLoading.value = true;
if (props.promise) {
try {
await props.promise(props.data);
} finally {
isLoading.value = false;
}
}
onDialogOK(props.data);
}
</script> </script>
<template> <template>
<q-dialog ref="dialogRef" persistent> <q-dialog ref="dialogRef" persistent>
@ -39,20 +61,28 @@ const isLoading = ref(false);
size="xl" size="xl"
v-if="icon" v-if="icon"
/> />
<span class="text-h6 text-grey">{{ message }}</span> <span class="text-h6 text-grey">{{ title }}</span>
<q-space /> <q-space />
<q-btn icon="close" flat round dense v-close-popup /> <q-btn icon="close" :disable="isLoading" flat round dense v-close-popup />
</q-card-section> </q-card-section>
<q-card-section class="row items-center"> <q-card-section class="row items-center">
{{ question }} {{ message }}
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn :label="t('globals.cancel')" color="primary" flat v-close-popup /> <q-btn
:label="t('globals.cancel')"
color="primary"
:disable="isLoading"
flat
v-close-popup
/>
<q-btn <q-btn
:label="t('globals.confirm')" :label="t('globals.confirm')"
color="primary" color="primary"
:loading="isLoading" :loading="isLoading"
@click="onDialogOK()" @click="confirm()"
unelevated
autofocus
/> />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
@ -66,13 +96,7 @@ const isLoading = ref(false);
</style> </style>
<i18n> <i18n>
"en": { es:
"question": "Are you sure you want to continue?", Confirm: Confirmar
"message": "Confirm" Are you sure you want to continue?: ¿Seguro que quieres continuar?
}
"es": {
"question": "¿Seguro que quieres continuar?",
"message": "Confirmar"
}
</i18n> </i18n>

View File

@ -6,6 +6,7 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { usePrintService } from 'composables/usePrintService'; import { usePrintService } from 'composables/usePrintService';
import SendEmailDialog from 'components/common/SendEmailDialog.vue'; import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
const $props = defineProps({ const $props = defineProps({
claim: { claim: {
@ -33,13 +34,15 @@ function confirmPickupOrder() {
quasar.dialog({ quasar.dialog({
component: SendEmailDialog, component: SendEmailDialog,
componentProps: { componentProps: {
address: customer.email, data: {
address: customer.email,
},
send: sendPickupOrder, send: sendPickupOrder,
}, },
}); });
} }
function sendPickupOrder(address) { function sendPickupOrder({ address }) {
const id = claim.value.id; const id = claim.value.id;
const customer = claim.value.client; const customer = claim.value.client;
return sendEmail(`Claims/${id}/claim-pickup-email`, { return sendEmail(`Claims/${id}/claim-pickup-email`, {
@ -48,16 +51,26 @@ function sendPickupOrder(address) {
}); });
} }
const showConfirmDialog = ref(false); function confirmRemove() {
async function deleteClaim() { quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('confirmDeletion'),
message: t('confirmDeletionMessage'),
promise: remove,
},
})
.onOk(async () => await router.push({ name: 'ClaimList' }));
}
async function remove() {
const id = claim.value.id; const id = claim.value.id;
await axios.delete(`Claims/${id}`); await axios.delete(`Claims/${id}`);
quasar.notify({ quasar.notify({
message: t('globals.dataDeleted'), message: t('globals.dataDeleted'),
type: 'positive', type: 'positive'
icon: 'check',
}); });
await router.push({ name: 'ClaimList' });
} }
</script> </script>
<template> <template>
@ -87,27 +100,12 @@ async function deleteClaim() {
</q-menu> </q-menu>
</q-item> </q-item>
<q-separator /> <q-separator />
<q-item @click="showConfirmDialog = true" v-ripple clickable> <q-item @click="confirmRemove()" v-ripple clickable>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="delete" /> <q-icon name="delete" />
</q-item-section> </q-item-section>
<q-item-section>{{ t('deleteClaim') }}</q-item-section> <q-item-section>{{ t('deleteClaim') }}</q-item-section>
</q-item> </q-item>
<q-dialog v-model="showConfirmDialog">
<q-card class="q-pa-sm">
<q-card-section class="row items-center q-pb-none">
<span class="text-h6 text-grey">{{ t('confirmDeletion') }}</span>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-card-section class="row items-center">{{ t('confirmDeletionMessage') }}</q-card-section>
<q-card-actions align="right">
<q-btn :label="t('globals.cancel')" color="primary" flat v-close-popup />
<q-btn :label="t('globals.confirm')" color="primary" @click="deleteClaim" />
</q-card-actions>
</q-card>
</q-dialog>
</template> </template>
<i18n> <i18n>

View File

@ -64,23 +64,23 @@ function openDialog(dmsId) {
multimediaDialog.value = true; multimediaDialog.value = true;
} }
function viewDeleteDms(dmsId) { function viewDeleteDms(index) {
quasar quasar
.dialog({ .dialog({
component: VnConfirm, component: VnConfirm,
componentProps: { componentProps: {
message: t('This file will be deleted'), title: t('This file will be deleted'),
icon: 'delete', icon: 'delete',
data: { index },
promise: deleteDms,
}, },
}) })
.onOk(() => deleteDms(dmsId)); .onOk(() => claimDms.value.splice(index, 1));
} }
async function deleteDms(index) { async function deleteDms({ index }) {
const dmsId = claimDms.value[index].dmsFk; const dmsId = claimDms.value[index].dmsFk;
await axios.post(`ClaimDms/${dmsId}/removeFile`); await axios.post(`ClaimDms/${dmsId}/removeFile`);
claimDms.value.splice(index, 1);
quasar.notify({ quasar.notify({
message: t('globals.dataDeleted'), message: t('globals.dataDeleted'),
type: 'positive', type: 'positive',

View File

@ -69,18 +69,19 @@ function confirmRemove(id) {
quasar quasar
.dialog({ .dialog({
component: VnConfirm, component: VnConfirm,
componentProps: {
data: { id },
promise: remove,
},
}) })
.onOk(() => remove(id)); .onOk(async () => await arrayData.refresh());
} }
async function remove(id) { async function remove({ id }) {
await axios.delete(`ClaimRmas/${id}`); await axios.delete(`ClaimRmas/${id}`);
await arrayData.refresh();
quasar.notify({ quasar.notify({
type: 'positive', type: 'positive',
message: t('globals.rowRemoved'), message: t('globals.rowRemoved'),
icon: 'check',
}); });
} }
</script> </script>

View File

@ -43,23 +43,25 @@ function confirm(id) {
quasar quasar
.dialog({ .dialog({
component: VnConfirm, component: VnConfirm,
componentProps: {
data: { id },
promise: remove,
},
}) })
.onOk(() => remove(id)); .onOk(async () => await arrayData.refresh());
} }
async function remove(id) { async function remove({ id }) {
await axios.delete(`ClaimRmas/${id}`); await axios.delete(`ClaimRmas/${id}`);
await arrayData.refresh();
quasar.notify({ quasar.notify({
type: 'positive', type: 'positive',
message: t('globals.rowRemoved'), message: t('globals.rowRemoved'),
icon: 'check',
}); });
} }
</script> </script>
<template> <template>
<q-page class="q-pa-md sticky"> <q-page class="column items-center q-pa-md sticky">
<q-page-sticky expand position="top" :offset="[16, 16]"> <q-page-sticky expand position="top" :offset="[16, 16]">
<q-card class="card q-pa-md"> <q-card class="card q-pa-md">
<q-form @submit="submit"> <q-form @submit="submit">
@ -79,68 +81,76 @@ async function remove(id) {
</q-form> </q-form>
</q-card> </q-card>
</q-page-sticky> </q-page-sticky>
<paginate <div class="card-list">
data-key="ClaimRmaList" <paginate
url="ClaimRmas" data-key="ClaimRmaList"
order="id DESC" url="ClaimRmas"
:offset="50" order="id DESC"
auto-load :offset="50"
> auto-load
<template #body="{ rows }"> >
<q-card class="card"> <template #body="{ rows }">
<template v-if="isLoading"> <q-card class="card">
<q-item class="q-pa-none items-start"> <template v-if="isLoading">
<q-item-section class="q-pa-md"> <q-item class="q-pa-none items-start">
<q-list> <q-item-section class="q-pa-md">
<q-item class="q-pa-none"> <q-list>
<q-item-section> <q-item class="q-pa-none">
<q-item-label caption> <q-item-section>
<q-skeleton /> <q-item-label caption>
</q-item-label> <q-skeleton />
<q-item-label </q-item-label>
><q-skeleton type="text" <q-item-label
/></q-item-label> ><q-skeleton type="text"
</q-item-section> /></q-item-label>
</q-item> </q-item-section>
</q-list> </q-item>
</q-item-section> </q-list>
<q-card-actions vertical class="justify-between"> </q-item-section>
<q-skeleton type="circle" class="q-mb-md" size="40px" /> <q-card-actions vertical class="justify-between">
</q-card-actions> <q-skeleton
</q-item> type="circle"
<q-separator /> class="q-mb-md"
</template> size="40px"
<template v-for="row of rows" :key="row.id"> />
<q-item class="q-pa-none items-start"> </q-card-actions>
<q-item-section class="q-pa-md"> </q-item>
<q-list> <q-separator />
<q-item class="q-pa-none"> </template>
<q-item-section> <template v-for="row of rows" :key="row.id">
<q-item-label caption>{{ <q-item class="q-pa-none items-start">
t('claim.rmaList.code') <q-item-section class="q-pa-md">
}}</q-item-label> <q-list>
<q-item-label>{{ row.code }}</q-item-label> <q-item class="q-pa-none">
</q-item-section> <q-item-section>
</q-item> <q-item-label caption>{{
</q-list> t('claim.rmaList.code')
</q-item-section> }}</q-item-label>
<q-card-actions vertical class="justify-between"> <q-item-label>{{
<q-btn row.code
flat }}</q-item-label>
round </q-item-section>
color="primary" </q-item>
icon="vn:bin" </q-list>
@click="confirm(row.id)" </q-item-section>
> <q-card-actions vertical class="justify-between">
<q-tooltip>{{ t('globals.remove') }}</q-tooltip> <q-btn
</q-btn> flat
</q-card-actions> round
</q-item> color="primary"
<q-separator /> icon="vn:bin"
</template> @click="confirm(row.id)"
</q-card> >
</template> <q-tooltip>{{ t('globals.remove') }}</q-tooltip>
</paginate> </q-btn>
</q-card-actions>
</q-item>
<q-separator />
</template>
</q-card>
</template>
</paginate>
</div>
</q-page> </q-page>
</template> </template>
@ -149,7 +159,7 @@ async function remove(id) {
padding-top: 156px; padding-top: 156px;
} }
.card { .card-list, .card {
width: 100%; width: 100%;
max-width: 60em; max-width: 60em;
} }

View File

@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue'; import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue';
import CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import TicketDescriptorMenu from './TicketDescriptorMenu.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -23,11 +24,25 @@ const entityId = computed(() => {
const filter = { const filter = {
include: [ include: [
{
relation: 'address',
scope: {
fields: ['id', 'name', 'mobile', 'phone'],
},
},
{ {
relation: 'client', relation: 'client',
scope: { scope: {
fields: ['id', 'name', 'salesPersonFk'], fields: ['id', 'name', 'salesPersonFk', 'phone', 'mobile', 'email'],
include: { relation: 'salesPersonUser' }, include: [
{
relation: 'user',
scope: {
fields: ['id', 'lang'],
},
},
{ relation: 'salesPersonUser' },
],
}, },
}, },
{ {
@ -61,6 +76,9 @@ function stateColor(state) {
<template> <template>
<card-descriptor module="Ticket" :url="`Tickets/${entityId}`" :filter="filter"> <card-descriptor module="Ticket" :url="`Tickets/${entityId}`" :filter="filter">
<template #menu="{ entity }">
<TicketDescriptorMenu :ticket="entity" />
</template>
<template #description="{ entity }"> <template #description="{ entity }">
<span> <span>
{{ entity.client.name }} {{ entity.client.name }}
@ -121,6 +139,16 @@ function stateColor(state) {
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
<q-card-actions class="q-gutter-md">
<q-icon
v-if="entity.isDeleted == true"
name="vn:deletedTicket"
size="xs"
color="primary"
>
<q-tooltip>{{ t('This ticket is deleted') }}</q-tooltip>
</q-icon>
</q-card-actions>
<q-card-actions> <q-card-actions>
<q-btn <q-btn
@ -135,3 +163,8 @@ function stateColor(state) {
</template> </template>
</card-descriptor> </card-descriptor>
</template> </template>
<i18n>
es:
This ticket is deleted: Este ticket está eliminado
</i18n>

View File

@ -0,0 +1,256 @@
<script setup>
import axios from 'axios';
import { ref } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } 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';
const props = defineProps({
ticket: {
type: Object,
required: true,
},
});
const router = useRouter();
const route = useRoute();
const quasar = useQuasar();
const { t } = useI18n();
const { openReport, sendEmail } = usePrintService();
const ticket = ref(props.ticket);
function openDeliveryNote(type = 'deliveryNote', documentType = 'pdf') {
const path = `Tickets/${ticket.value.id}/delivery-note-${documentType}`;
openReport(path, {
recipientId: ticket.value.clientFk,
type: type,
});
}
function sendDeliveryNoteConfirmation(type = 'deliveryNote', documentType = 'pdf') {
const customer = ticket.value.client;
quasar.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 =
route.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);
}
quasar.dialog({
component: VnSmsDialog,
componentProps: {
phone: phone,
template: template,
locale: client.user.lang,
data: data,
promise: sendSms,
},
});
}
async function showSmsDialogWithChanges() {
const query = `TicketLogs/${route.params.id}/getChanges`;
const response = await axios.get(query);
showSmsDialog('orderChanges', { changes: response.data });
}
async function sendSms(body) {
await axios.post(`Tickets/${route.params.id}/sendSms`, body);
quasar.notify({
message: 'Notification sent',
type: 'positive',
});
}
function confirmDelete() {
quasar
.dialog({
component: VnConfirm,
componentProps: {
promise: remove,
},
})
.onOk(async () => await router.push({ name: 'TicketList' }));
}
async function remove() {
const id = route.params.id;
await axios.post(`Tickets/${id}/setDeleted`);
quasar.notify({
message: t('Ticket deleted'),
type: 'positive',
});
quasar.notify({
message: t('You can undo this action within the first hour'),
icon: 'info',
});
}
</script>
<template>
<q-item v-ripple clickable>
<q-item-section avatar>
<q-icon name="picture_as_pdf" />
</q-item-section>
<q-item-section>{{ t('Open Delivery Note...') }}</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top end" self="top start" auto-close bordered>
<q-list>
<q-item @click="openDeliveryNote('deliveryNote')" v-ripple clickable>
<q-item-section>{{ t('With prices') }}</q-item-section>
</q-item>
<q-item @click="openDeliveryNote('withoutPrices')" v-ripple clickable>
<q-item-section>{{ t('Without prices') }}</q-item-section>
</q-item>
<q-item
@click="openDeliveryNote('deliveryNote', 'csv')"
v-ripple
clickable
>
<q-item-section>{{ t('As CSV') }}</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item v-ripple clickable>
<q-item-section avatar>
<q-icon name="send" />
</q-item-section>
<q-item-section>{{ t('Send Delivery Note...') }}</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top end" self="top start" auto-close>
<q-list>
<q-item
@click="sendDeliveryNoteConfirmation('deliveryNote')"
v-ripple
clickable
>
<q-item-section>{{ t('With prices') }}</q-item-section>
</q-item>
<q-item
@click="sendDeliveryNoteConfirmation('withoutPrices')"
v-ripple
clickable
>
<q-item-section>{{ t('Without prices') }}</q-item-section>
</q-item>
<q-item
@click="sendDeliveryNoteConfirmation('deliveryNote', 'csv')"
v-ripple
clickable
>
<q-item-section>{{ t('As CSV') }}</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item @click="openDeliveryNote('proforma')" v-ripple clickable>
<q-item-section avatar>
<q-icon name="receipt" />
</q-item-section>
<q-item-section>{{ t('Open Proforma Invoice') }}</q-item-section>
</q-item>
<q-item v-ripple clickable>
<q-item-section avatar>
<q-icon name="sms" />
</q-item-section>
<q-item-section>{{ t('Send SMS...') }}</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top end" self="top start" auto-close>
<q-list>
<q-item @click="showSmsDialog('pendingPayment')" v-ripple clickable>
<q-item-section>{{ t('Pending payment') }}</q-item-section>
</q-item>
<q-item @click="showSmsDialog('minAmount')" v-ripple clickable>
<q-item-section>{{ t('Minimum amount') }}</q-item-section>
</q-item>
<q-item
@click="showSmsDialogWithChanges('orderChanges')"
v-ripple
clickable
>
<q-item-section>{{ t('Order changes') }}</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<template v-if="!ticket.isDeleted">
<q-separator />
<q-item @click="confirmDelete()" v-ripple clickable>
<q-item-section avatar>
<q-icon name="delete" />
</q-item-section>
<q-item-section>{{ t('Delete ticket') }}</q-item-section>
</q-item>
</template>
</template>
<i18n>
es:
Open Delivery Note...: Abrir albarán...
Send Delivery Note...: Enviar albarán...
With prices: Con precios
Without prices: Sin precios
As CSV: Como CSV
Open Proforma Invoice: Abrir factura proforma
Delete ticket: Eliminar ticket
Send SMS...: Enviar SMS
Pending payment: Pago pendiente
Minimum amount: Importe mínimo
Order changes: Cambios del pedido
Ticket deleted: Ticket eliminado
You can undo this action within the first hour: Puedes deshacer esta acción dentro de la primera hora
</i18n>

View File

@ -34,10 +34,17 @@ describe('App', () => {
const response = { const response = {
response: { response: {
status: 401, status: 401,
data: {
error: {
message: 'Invalid username or password',
},
},
}, },
}; };
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response)); expect(vm.responseError(response)).rejects.toEqual(
expect.objectContaining(response)
);
expect(vm.quasar.notify).toHaveBeenCalledWith( expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
message: 'Invalid username or password', message: 'Invalid username or password',
@ -54,10 +61,17 @@ describe('App', () => {
const response = { const response = {
response: { response: {
status: 401, status: 401,
data: {
error: {
message: 'Access denied',
},
},
}, },
}; };
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response)); expect(vm.responseError(response)).rejects.toEqual(
expect.objectContaining(response)
);
expect(vm.quasar.notify).toHaveBeenCalledWith( expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
message: 'Access denied', message: 'Access denied',

View File

@ -18,12 +18,12 @@ describe('ClaimDescriptorMenu', () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
describe('deleteClaim()', () => { describe('remove()', () => {
it('should delete the claim', async () => { it('should delete the claim', async () => {
vi.spyOn(axios, 'delete').mockResolvedValue({ data: true }); vi.spyOn(axios, 'delete').mockResolvedValue({ data: true });
vi.spyOn(vm.quasar, 'notify'); vi.spyOn(vm.quasar, 'notify');
await vm.deleteClaim(); await vm.remove();
expect(vm.quasar.notify).toHaveBeenCalledWith( expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'positive' }) expect.objectContaining({ type: 'positive' })

View File

@ -21,7 +21,7 @@ describe('ClaimPhoto', () => {
beforeAll(() => { beforeAll(() => {
vm = createWrapper(ClaimPhoto, { vm = createWrapper(ClaimPhoto, {
global: { global: {
stubs: ['FetchData', 'TeleportSlot', 'vue-i18n'], stubs: ['FetchData', 'vue-i18n'],
mocks: { mocks: {
fetch: vi.fn(), fetch: vi.fn(),
}, },
@ -38,7 +38,7 @@ describe('ClaimPhoto', () => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: true }); vi.spyOn(axios, 'post').mockResolvedValue({ data: true });
vi.spyOn(vm.quasar, 'notify'); vi.spyOn(vm.quasar, 'notify');
await vm.deleteDms(0); await vm.deleteDms({ index: 0 });
expect(axios.post).toHaveBeenCalledWith( expect(axios.post).toHaveBeenCalledWith(
`ClaimDms/${claimMock.claimDms[0].dmsFk}/removeFile` `ClaimDms/${claimMock.claimDms[0].dmsFk}/removeFile`
@ -46,7 +46,6 @@ describe('ClaimPhoto', () => {
expect(vm.quasar.notify).toHaveBeenCalledWith( expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'positive' }) expect.objectContaining({ type: 'positive' })
); );
expect(vm.claimDms).toEqual([]);
}); });
}); });
@ -59,8 +58,10 @@ describe('ClaimPhoto', () => {
expect(vm.quasar.dialog).toHaveBeenCalledWith( expect(vm.quasar.dialog).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
componentProps: { componentProps: {
message: 'This file will be deleted', title: 'This file will be deleted',
icon: 'delete', icon: 'delete',
data: { index: 1 },
promise: vm.deleteDms
}, },
}) })
); );