fixes #5056 Nuevo sistema de sacado de pedidos: Gestión de vagones #43

Merged
alexandre merged 16 commits from 5056-gestion-vagones into dev 2023-04-17 05:04:30 +00:00
45 changed files with 1428 additions and 495 deletions
Showing only changes of commit 99672200d5 - Show all commits

View File

@ -29,7 +29,7 @@ module.exports = configure(function (/* ctx */) {
// app boot file (/src/boot) // app boot file (/src/boot)
// --> boot files are part of "main.js" // --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files // https://v2.quasar.dev/quasar-cli/boot-files
boot: ['i18n', 'axios'], boot: ['i18n', 'axios', 'vnDate'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ['app.scss'], css: ['app.scss'],

View File

@ -1,16 +1,10 @@
<script setup> <script setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import axios from 'axios';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useSession } from 'src/composables/useSession';
const quasar = useQuasar(); const quasar = useQuasar();
const router = useRouter(); const { availableLocales, locale, fallbackLocale } = useI18n();
const session = useSession();
const { t, availableLocales, locale, fallbackLocale } = useI18n();
const { isLoggedIn } = session;
onMounted(() => { onMounted(() => {
let userLang = window.navigator.language; let userLang = window.navigator.language;
@ -39,63 +33,6 @@ quasar.iconMapFn = (iconName) => {
content: iconName, content: iconName,
}; };
}; };
function responseError(error) {
let message = error.message;
let logOut = false;
switch (error.response?.status) {
case 401:
message = 'login.loginError';
if (isLoggedIn()) {
message = 'errors.statusUnauthorized';
logOut = true;
}
break;
case 403:
message = 'errors.statusUnauthorized';
break;
case 500:
message = 'errors.statusInternalServerError';
break;
case 502:
message = 'errors.statusBadGateway';
break;
case 504:
message = 'errors.statusGatewayTimeout';
break;
}
let translatedMessage = t(message);
if (!translatedMessage) translatedMessage = message;
quasar.notify({
message: translatedMessage,
type: 'negative',
});
if (logOut) {
session.destroy();
router.push({ path: '/login' });
}
return Promise.reject(error);
}
axios.interceptors.response.use((response) => {
const { method } = response.config;
const isSaveRequest = method === 'patch';
if (isSaveRequest) {
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
icon: 'check',
});
}
return response;
}, responseError);
</script> </script>
<template> <template>

View File

@ -1,24 +1,80 @@
import { boot } from 'quasar/wrappers';
import axios from 'axios'; import axios from 'axios';
import { Notify } from 'quasar';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { Router } from 'src/router';
import { i18n } from './i18n';
export default boot(() => { const session = useSession();
const { getToken } = useSession(); const { t } = i18n.global;
axios.defaults.baseURL = '/api/'; axios.defaults.baseURL = '/api/';
axios.interceptors.request.use( const onRequest = (config) => {
function (context) { const token = session.getToken();
const token = getToken(); if (token.length && config.headers) {
config.headers.Authorization = token;
}
if (token.length && context.headers) { return config;
context.headers.Authorization = token; };
}
return context; const onRequestError = (error) => {
}, return Promise.reject(error);
function (error) { };
return Promise.reject(error);
} const onResponse = (response) => {
); const { method } = response.config;
});
const isSaveRequest = method === 'patch';
if (isSaveRequest) {
Notify.create({
message: t('globals.dataSaved'),
type: 'positive',
});
}
return response;
};
const onResponseError = (error) => {
let message = '';
const response = error.response;
const responseData = response && response.data;
const responseError = responseData && response.data.error;
if (responseError) {
message = responseError.message;
}
switch (response.status) {
case 500:
message = 'errors.statusInternalServerError';
break;
case 502:
message = 'errors.statusBadGateway';
break;
case 504:
message = 'errors.statusGatewayTimeout';
break;
}
if (session.isLoggedIn && response.status === 401) {
session.destroy();
Router.push({ path: '/login' });
}
Notify.create({
message: t(message),
type: 'negative',
});
return Promise.reject(error);
};
axios.interceptors.request.use(onRequest, onRequestError);
axios.interceptors.response.use(onResponse, onResponseError);
export {
onRequest,
onResponseError
}

18
src/boot/vnDate.js Normal file
View File

@ -0,0 +1,18 @@
import { boot } from 'quasar/wrappers';
export default boot(() => {
Date.vnUTC = () => {
const env = process.env.NODE_ENV;
if (!env || env === 'development') return new Date(Date.UTC(2001, 0, 1, 11));
return new Date();
};
Date.vnNew = () => {
return new Date(Date.vnUTC());
};
Date.vnNow = () => {
return new Date(Date.vnUTC()).getTime();
};
});

View File

@ -14,14 +14,14 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
url: {
type: String,
default: '',
},
data: { data: {
type: Array, type: Array,
default: null, default: null,
}, },
url: {
type: String,
default: '',
},
filter: { filter: {
type: Object, type: Object,
default: null, default: null,
@ -38,6 +38,10 @@ const props = defineProps({
type: Number, type: Number,
default: 10, default: 10,
}, },
userParams: {
type: Object,
default: null,
},
offset: { offset: {
type: Number, type: Number,
default: 500, default: 500,
@ -58,6 +62,7 @@ const arrayData = useArrayData(props.dataKey, {
where: props.where, where: props.where,
limit: props.limit, limit: props.limit,
order: props.order, order: props.order,
userParams: props.userParams,
}); });
const store = arrayData.store; const store = arrayData.store;
@ -123,12 +128,20 @@ async function onLoad(...params) {
<template> <template>
<div> <div>
<div
v-if="!props.autoLoad && !store.data && !isLoading"
class="info-row q-pa-md text-center"
>
<h5>
{{ t('No data to display') }}
</h5>
</div>
<div <div
v-if="store.data && store.data.length === 0 && !isLoading" v-if="store.data && store.data.length === 0 && !isLoading"
class="info-row q-pa-md text-center" class="info-row q-pa-md text-center"
> >
<h5> <h5>
{{ t('components.smartCard.noData') }} {{ t('No results found') }}
</h5> </h5>
</div> </div>
<div v-if="props.autoLoad && !store.data" class="card-list q-gutter-y-md"> <div v-if="props.autoLoad && !store.data" class="card-list q-gutter-y-md">
@ -160,9 +173,6 @@ async function onLoad(...params) {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
// .q-infinite-scroll {
// width: 100%;
// }
.info-row { .info-row {
width: 100%; width: 100%;
@ -171,3 +181,9 @@ async function onLoad(...params) {
} }
} }
</style> </style>
<i18n>
es:
No data to display: Sin datos que mostrar
No results found: No se han encontrado resultados
</i18n>

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,261 @@
<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 v-if="props.locale">
<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
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 }: { changes }'
en: English
es: Spanish
fr: French
pt: Portuguese
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
pt: Portugué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 }: { changes }'
en: Anglais
es: Espagnol
fr: Français
pt: Portugais
pt:
Send SMS: Enviar SMS
CustomerDefaultLanguage: Este cliente utiliza o <strong>{locale}</strong> como seu idioma padrão
Language: Linguagem
Phone: Móvel
Subject: Assunto
Message: Mensagem
templates:
pendingPayment: 'Seu pedido está pendente de pagamento.
Por favor, acesse o site e faça o pagamento com cartão. Muito obrigado.'
minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido
{ orderId } do dia { shipped } para recebê-lo sem custos de envio adicionais.'
orderChanges: 'Pedido { orderId } dia { shipped }: { changes }'
en: Inglês
es: Espanhol
fr: Francês
pt: Português
</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

@ -15,6 +15,11 @@ const props = defineProps({
required: false, required: false,
default: false, default: false,
}, },
params: {
type: Object,
required: false,
default: null,
},
}); });
const emit = defineEmits(['refresh', 'clear']); const emit = defineEmits(['refresh', 'clear']);
@ -24,8 +29,9 @@ const store = arrayData.store;
const userParams = ref({}); const userParams = ref({});
onMounted(() => { onMounted(() => {
if (props.params) userParams.value = props.params;
const params = store.userParams; const params = store.userParams;
if (params) { if (Object.keys(params).length > 0) {
userParams.value = Object.assign({}, params); userParams.value = Object.assign({}, params);
} }
}); });

View File

@ -23,11 +23,42 @@ const props = defineProps({
required: false, required: false,
default: true, default: true,
}, },
url: {
type: String,
default: '',
},
filter: {
type: Object,
default: null,
},
where: {
type: Object,
default: null,
},
order: {
type: String,
default: '',
},
limit: {
type: Number,
default: 10,
},
userParams: {
type: Object,
default: null,
},
}); });
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const arrayData = useArrayData(props.dataKey); const arrayData = useArrayData(props.dataKey, {
url: props.url,
filter: props.filter,
where: props.where,
limit: props.limit,
order: props.order,
userParams: props.userParams,
});
const store = arrayData.store; const store = arrayData.store;
const searchText = ref(''); const searchText = ref('');

View File

@ -20,20 +20,27 @@ export function useArrayData(key, userOptions) {
const page = ref(1); const page = ref(1);
if (typeof userOptions === 'object') {
if (userOptions.filter) store.filter = userOptions.filter;
if (userOptions.url) store.url = userOptions.url;
if (userOptions.limit) store.limit = userOptions.limit;
if (userOptions.order) store.order = userOptions.order;
}
onMounted(() => { onMounted(() => {
setOptions();
const query = route.query; const query = route.query;
if (query.params) { if (query.params) {
store.userParams = JSON.parse(query.params); store.userParams = JSON.parse(query.params);
} }
}); });
function setOptions() {
if (typeof userOptions === 'object') {
for (const option in userOptions) {
if (userOptions[option] == null) continue;
if (Object.prototype.hasOwnProperty.call(store, option)) {
store[option] = userOptions[option];
}
}
}
}
async function fetch({ append = false }) { async function fetch({ append = false }) {
if (!store.url) return; if (!store.url) return;

View File

@ -1,5 +1,6 @@
import toLowerCase from './toLowerCase'; import toLowerCase from './toLowerCase';
import toDate from './toDate'; import toDate from './toDate';
import toDateString from './toDateString';
import toCurrency from './toCurrency'; import toCurrency from './toCurrency';
import toPercentage from './toPercentage'; import toPercentage from './toPercentage';
import toLowerCamel from './toLowerCamel'; import toLowerCamel from './toLowerCamel';
@ -9,6 +10,7 @@ export {
toLowerCase, toLowerCase,
toLowerCamel, toLowerCamel,
toDate, toDate,
toDateString,
toCurrency, toCurrency,
toPercentage, toPercentage,
dashIfEmpty, dashIfEmpty,

View File

@ -0,0 +1,10 @@
export default function toDateString(date) {
let day = date.getDate();
let month = date.getMonth() + 1;
let year = date.getFullYear();
if (day < 10) day = `0${day}`;
if (month < 10) month = `0${month}`;
return `${year}-${month}-${day}`
}

View File

@ -434,7 +434,6 @@ export default {
logOut: 'Log Out', logOut: 'Log Out',
}, },
smartCard: { smartCard: {
noData: 'No data to display',
openCard: 'View card', openCard: 'View card',
openSummary: 'Open summary', openSummary: 'Open summary',
viewDescription: 'View description', viewDescription: 'View description',

View File

@ -433,7 +433,6 @@ export default {
logOut: 'Cerrar sesión', logOut: 'Cerrar sesión',
}, },
smartCard: { smartCard: {
noData: 'Sin datos que mostrar',
openCard: 'Ver ficha', openCard: 'Ver ficha',
openSummary: 'Abrir detalles', openSummary: 'Abrir detalles',
viewDescription: 'Ver descripción', viewDescription: 'Ver descripción',

View File

@ -12,6 +12,7 @@ const { t } = useI18n();
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar <VnSearchbar
data-key="ClaimList" data-key="ClaimList"
url="Claims/filter"
:label="t('Search claim')" :label="t('Search claim')"
:info="t('You can search by claim id or customer name')" :info="t('You can search by claim id or customer name')"
/> />

View File

@ -4,7 +4,7 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import TicketDescriptorPopover from 'pages/Ticket/Card/TicketDescriptorPopover.vue'; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue'; import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue';
import CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
@ -86,9 +86,8 @@ function stateColor(code) {
<q-item-label> <q-item-label>
<span class="link"> <span class="link">
{{ entity.ticketFk }} {{ entity.ticketFk }}
<q-popup-proxy>
<ticket-descriptor-popover :id="entity.ticketFk" /> <TicketDescriptorProxy :id="entity.ticketFk" />
</q-popup-proxy>
</span> </span>
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>

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

@ -158,6 +158,12 @@ en:
hasToPickUp: Has to pick Up hasToPickUp: Has to pick Up
dmsFk: Document ID dmsFk: Document ID
text: Description text: Description
claimStateFk: Claim State
workerFk: Worker
clientFk: Customer
rma: RMA
responsibility: Responsibility
packages: Packages
es: es:
Audit logs: Registros de auditoría Audit logs: Registros de auditoría
Property: Propiedad Property: Propiedad
@ -186,4 +192,10 @@ es:
hasToPickUp: Se debe recoger hasToPickUp: Se debe recoger
dmsFk: ID documento dmsFk: ID documento
text: Descripción text: Descripción
claimStateFk: Estado de la reclamación
workerFk: Trabajador
clientFk: Cliente
rma: RMA
responsibility: Responsabilidad
packages: Bultos
</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

@ -4,6 +4,7 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate, toCurrency } from 'src/filters'; import { toDate, toCurrency } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -109,22 +110,30 @@ function stateColor(code) {
</q-item> </q-item>
<q-item> <q-item>
<q-item-section v-if="claim.worker && claim.worker.user"> <q-item-section v-if="claim.worker && claim.worker.user">
<q-item-label caption>{{ <q-item-label caption>
t('claim.summary.assignedTo') {{ t('claim.summary.assignedTo') }}
}}</q-item-label> </q-item-label>
<q-item-label>{{ <q-item-label>
claim.worker.user.nickname <span class="link">
}}</q-item-label> {{ claim.worker.user.nickname }}
<WorkerDescriptorProxy :id="claim.workerFk" />
</span>
</q-item-label>
</q-item-section> </q-item-section>
<q-item-section <q-item-section
v-if="claim.client && claim.client.salesPersonUser" v-if="claim.client && claim.client.salesPersonUser"
> >
<q-item-label caption>{{ <q-item-label caption>
t('claim.summary.attendedBy') {{ t('claim.summary.attendedBy') }}
}}</q-item-label> </q-item-label>
<q-item-label>{{ <q-item-label>
claim.client.salesPersonUser.name <span class="link">
}}</q-item-label> {{ claim.client.salesPersonUser.name }}
<WorkerDescriptorProxy
:id="claim.client.salesPersonFk"
/>
</span>
</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
@ -169,4 +178,4 @@ function stateColor(code) {
</q-card-section> </q-card-section>
</template> </template>
</card-summary> </card-summary>
</template> </template>

View File

@ -6,7 +6,7 @@ import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'filters/index'; import { toDate } from 'filters/index';
import Paginate from 'components/PaginateData.vue'; import Paginate from 'components/PaginateData.vue';
import ClaimSummaryDialog from './Card/ClaimSummaryDialog.vue'; import ClaimSummaryDialog from './Card/ClaimSummaryDialog.vue';
import CustomerDescriptorPopover from 'pages/Customer/Card/CustomerDescriptorPopover.vue'; import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import ClaimFilter from './ClaimFilter.vue'; import ClaimFilter from './ClaimFilter.vue';
@ -173,9 +173,8 @@ function viewSummary(id) {
<q-tooltip> <q-tooltip>
{{ t('components.smartCard.viewDescription') }} {{ t('components.smartCard.viewDescription') }}
</q-tooltip> </q-tooltip>
<q-popup-proxy>
<CustomerDescriptorPopover :id="row.clientFk" /> <CustomerDescriptorProxy :id="row.clientFk" />
</q-popup-proxy>
</q-btn> </q-btn>
</q-card-actions> </q-card-actions>
</q-item> </q-item>

View File

@ -16,7 +16,7 @@ const input = ref();
const newRma = ref({ const newRma = ref({
code: '', code: '',
crated: new Date(), crated: Date.vnNew(),
}); });
function onInputUpdate(value) { function onInputUpdate(value) {
@ -35,7 +35,7 @@ async function submit() {
newRma.value = { newRma.value = {
code: '', code: '',
created: new Date(), created: Date.vnNew(),
}; };
} }
@ -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

@ -12,6 +12,7 @@ const { t } = useI18n();
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar <VnSearchbar
data-key="CustomerList" data-key="CustomerList"
url="Clients/filter"
:label="t('Search customer')" :label="t('Search customer')"
:info="t('You can search by customer id or name')" :info="t('You can search by customer id or name')"
/> />

View File

@ -9,7 +9,7 @@ const $props = defineProps({
}); });
</script> </script>
<template> <template>
<q-card> <q-popup-proxy>
<customer-descriptor v-if="$props.id" :id="$props.id" /> <CustomerDescriptor v-if="$props.id" :id="$props.id" />
</q-card> </q-popup-proxy>
</template> </template>

View File

@ -12,6 +12,7 @@ const { t } = useI18n();
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar <VnSearchbar
data-key="InvoiceOutList" data-key="InvoiceOutList"
url="InvoiceOuts/filter"
:label="t('Search invoice')" :label="t('Search invoice')"
:info="t('You can search by invoice reference')" :info="t('You can search by invoice reference')"
/> />

View File

@ -3,8 +3,8 @@ import { ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toCurrency, toDate } from 'src/filters'; import { toCurrency, toDate } from 'src/filters';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue'; import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -80,9 +80,7 @@ function ticketFilter(invoice) {
</q-item-label> </q-item-label>
<q-item-label class="link"> <q-item-label class="link">
{{ entity.client.name }} {{ entity.client.name }}
<q-popup-proxy> <CustomerDescriptorProxy :id="entity.client.id" />
<customer-descriptor-popover :id="entity.client.id" />
</q-popup-proxy>
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
<q-item-section v-if="entity.company"> <q-item-section v-if="entity.company">

View File

@ -9,7 +9,7 @@ const $props = defineProps({
}); });
</script> </script>
<template> <template>
<q-card> <q-popup-proxy>
<invoiceOut-descriptor v-if="$props.id" :id="$props.id" /> <InvoiceOutDescriptor v-if="$props.id" :id="$props.id" />
</q-card> </q-popup-proxy>
</template> </template>

View File

@ -48,29 +48,25 @@ const password = ref('');
const keepLogin = ref(true); const keepLogin = ref(true);
async function onSubmit() { async function onSubmit() {
try { const { data } = await axios.post('Accounts/login', {
const { data } = await axios.post('Accounts/login', { user: username.value,
user: username.value, password: password.value,
password: password.value, });
});
if (!data) return; if (!data) return;
await session.login(data.token, keepLogin.value); await session.login(data.token, keepLogin.value);
quasar.notify({ quasar.notify({
message: t('login.loginSuccess'), message: t('login.loginSuccess'),
type: 'positive', type: 'positive',
}); });
const currentRoute = router.currentRoute.value; const currentRoute = router.currentRoute.value;
if (currentRoute.query && currentRoute.query.redirect) { if (currentRoute.query && currentRoute.query.redirect) {
router.push(currentRoute.query.redirect); router.push(currentRoute.query.redirect);
} else { } else {
router.push({ name: 'Dashboard' }); router.push({ name: 'Dashboard' });
}
} catch (error) {
//
} }
} }
</script> </script>
@ -92,10 +88,20 @@ async function onSubmit() {
> >
<q-menu auto-close> <q-menu auto-close>
<q-list dense> <q-list dense>
<q-item @click="userLocale = 'en'" :active="userLocale == 'en'" v-ripple clickable> <q-item
@click="userLocale = 'en'"
:active="userLocale == 'en'"
v-ripple
clickable
>
{{ t('globals.lang.en') }} {{ t('globals.lang.en') }}
</q-item> </q-item>
<q-item @click="userLocale = 'es'" :active="userLocale == 'es'" v-ripple clickable> <q-item
@click="userLocale = 'es'"
:active="userLocale == 'es'"
v-ripple
clickable
>
{{ t('globals.lang.es') }} {{ t('globals.lang.es') }}
</q-item> </q-item>
</q-list> </q-list>
@ -104,30 +110,48 @@ async function onSubmit() {
<q-list> <q-list>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t(`globals.darkMode`) }}</q-item-label> <q-item-label caption>{{
t(`globals.darkMode`)
}}</q-item-label>
</q-item-section> </q-item-section>
<q-item-section side> <q-item-section side>
<q-toggle v-model="darkMode" checked-icon="dark_mode" unchecked-icon="light_mode" /> <q-toggle
v-model="darkMode"
checked-icon="dark_mode"
unchecked-icon="light_mode"
/>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
</q-toolbar> </q-toolbar>
</q-page-sticky> </q-page-sticky>
<div class="login-form q-pa-xl"> <div class="login-form q-pa-xl">
<q-img src="~/assets/logo.svg" alt="Logo" fit="contain" :ratio="16 / 9" class="q-mb-md" /> <q-img
src="~/assets/logo.svg"
alt="Logo"
fit="contain"
:ratio="16 / 9"
class="q-mb-md"
/>
<q-form @submit="onSubmit" class="q-gutter-md"> <q-form @submit="onSubmit" class="q-gutter-md">
<q-input <q-input
v-model="username" v-model="username"
:label="t('login.username')" :label="t('login.username')"
lazy-rules lazy-rules
:rules="[(val) => (val && val.length > 0) || t('login.fieldRequired')]" :rules="[
(val) =>
(val && val.length > 0) || t('login.fieldRequired'),
]"
/> />
<q-input <q-input
type="password" type="password"
v-model="password" v-model="password"
:label="t('login.password')" :label="t('login.password')"
lazy-rules lazy-rules
:rules="[(val) => (val && val.length > 0) || t('login.fieldRequired')]" :rules="[
(val) =>
(val && val.length > 0) || t('login.fieldRequired'),
]"
/> />
<q-toggle v-model="keepLogin" :label="t('login.keepLogin')" /> <q-toggle v-model="keepLogin" :label="t('login.keepLogin')" />

View File

@ -12,6 +12,7 @@ const { t } = useI18n();
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar <VnSearchbar
data-key="TicketList" data-key="TicketList"
url="Tickets/filter"
:label="t('Search ticket')" :label="t('Search ticket')"
:info="t('You can search by ticket id or alias')" :info="t('You can search by ticket id or alias')"
/> />

View File

@ -3,8 +3,9 @@ import { computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; 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 CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.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 }}
@ -91,9 +109,7 @@ function stateColor(state) {
<q-item-label> <q-item-label>
<span class="link"> <span class="link">
{{ entity.clientFk }} {{ entity.clientFk }}
<q-popup-proxy> <CustomerDescriptorProxy :id="entity.client.id" />
<customer-descriptor-popover :id="entity.client.id" />
</q-popup-proxy>
</span> </span>
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
@ -121,6 +137,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 +161,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

@ -9,7 +9,7 @@ const $props = defineProps({
}); });
</script> </script>
<template> <template>
<q-card> <q-popup-proxy>
<ticket-descriptor v-if="$props.id" :id="$props.id" /> <TicketDescriptor v-if="$props.id" :id="$props.id" />
</q-card> </q-popup-proxy>
</template> </template>

View File

@ -7,6 +7,8 @@ import { dashIfEmpty, toDate, toCurrency } from 'src/filters';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue'; import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FetchedTags from 'components/ui/FetchedTags.vue'; import FetchedTags from 'components/ui/FetchedTags.vue';
import InvoiceOutDescriptorProxy from 'pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
onMounted(() => fetch()); onMounted(() => fetch());
onUpdated(() => fetch()); onUpdated(() => fetch());
@ -79,14 +81,20 @@ async function changeState(value) {
</script> </script>
<template> <template>
<fetch-data url="States/editableStates" @on-fetch="(data) => (editableStates = data)" auto-load /> <fetch-data
url="States/editableStates"
@on-fetch="(data) => (editableStates = data)"
auto-load
/>
<div class="summary container"> <div class="summary container">
<q-card> <q-card>
<skeleton-summary v-if="!ticket" /> <skeleton-summary v-if="!ticket" />
<template v-if="ticket"> <template v-if="ticket">
<div class="header bg-primary q-pa-sm q-mb-md"> <div class="header bg-primary q-pa-sm q-mb-md">
<span> <span>
Ticket #{{ ticket.id }} - {{ ticket.client.name }} ({{ ticket.client.id }}) - Ticket #{{ ticket.id }} - {{ ticket.client.name }} ({{
ticket.client.id
}}) -
{{ ticket.nickname }} {{ ticket.nickname }}
</span> </span>
<q-btn-dropdown <q-btn-dropdown
@ -104,7 +112,13 @@ async function changeState(value) {
separator separator
v-slot="{ item, index }" v-slot="{ item, index }"
> >
<q-item :key="index" dense clickable v-close-popup @click="changeState(item.code)"> <q-item
:key="index"
dense
clickable
v-close-popup
@click="changeState(item.code)"
>
<q-item-section> <q-item-section>
<q-item-label>{{ item.name }}</q-item-label> <q-item-label>{{ item.name }}</q-item-label>
</q-item-section> </q-item-section>
@ -118,40 +132,72 @@ async function changeState(value) {
<q-list> <q-list>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.state') }}</q-item-label> <q-item-label caption>{{
<q-item-label :class="stateColor(ticket.ticketState.state)"> t('ticket.summary.state')
}}</q-item-label>
<q-item-label
:class="stateColor(ticket.ticketState.state)"
>
{{ ticket.ticketState.state.name }} {{ ticket.ticketState.state.name }}
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.salesPerson') }}</q-item-label> <q-item-label caption>{{
<q-item-label class="link">{{ ticket.client.salesPersonUser.name }}</q-item-label> t('ticket.summary.salesPerson')
}}</q-item-label>
<q-item-label>
<span class="link">
{{ ticket.client.salesPersonUser.name }}
<WorkerDescriptorProxy
:id="ticket.client.salesPersonFk"
/>
</span>
</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.agency') }}</q-item-label> <q-item-label caption>{{
<q-item-label>{{ ticket.agencyMode.name }}</q-item-label> t('ticket.summary.agency')
}}</q-item-label>
<q-item-label>{{
ticket.agencyMode.name
}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.zone') }}</q-item-label> <q-item-label caption>{{
<q-item-label class="link">{{ ticket.routeFk }}</q-item-label> t('ticket.summary.zone')
}}</q-item-label>
<q-item-label class="link">{{
ticket.routeFk
}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.warehouse') }}</q-item-label> <q-item-label caption>{{
<q-item-label>{{ ticket.warehouse.name }}</q-item-label> t('ticket.summary.warehouse')
}}</q-item-label>
<q-item-label>{{
ticket.warehouse.name
}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.invoice') }}</q-item-label> <q-item-label caption>{{
<q-item-label v-if="ticket.refFk" class="link">{{ ticket.refFk }}</q-item-label> t('ticket.summary.invoice')
}}</q-item-label>
<q-item-label v-if="ticket.refFk">
<span class="link">
{{ ticket.refFk }}
<InvoiceOutDescriptorProxy :id="ticket.id" />
</span>
</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
@ -160,49 +206,75 @@ async function changeState(value) {
<q-list> <q-list>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.shipped') }}</q-item-label> <q-item-label caption>{{
<q-item-label>{{ toDate(ticket.shipped) }}</q-item-label> t('ticket.summary.shipped')
}}</q-item-label>
<q-item-label>{{
toDate(ticket.shipped)
}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.landed') }}</q-item-label> <q-item-label caption>{{
<q-item-label>{{ toDate(ticket.landed) }}</q-item-label> t('ticket.summary.landed')
}}</q-item-label>
<q-item-label>{{
toDate(ticket.landed)
}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.packages') }}</q-item-label> <q-item-label caption>{{
t('ticket.summary.packages')
}}</q-item-label>
<q-item-label>{{ ticket.packages }}</q-item-label> <q-item-label>{{ ticket.packages }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.consigneePhone') }}</q-item-label> <q-item-label caption>{{
<q-item-label>{{ ticket.address.phone }}</q-item-label> t('ticket.summary.consigneePhone')
}}</q-item-label>
<q-item-label>{{
ticket.address.phone
}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.consigneeMobile') }}</q-item-label> <q-item-label caption>{{
<q-item-label>{{ ticket.address.mobile }}</q-item-label> t('ticket.summary.consigneeMobile')
}}</q-item-label>
<q-item-label>{{
ticket.address.mobile
}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.clientPhone') }}</q-item-label> <q-item-label caption>{{
t('ticket.summary.clientPhone')
}}</q-item-label>
<q-item-label>{{ ticket.client.phone }}</q-item-label> <q-item-label>{{ ticket.client.phone }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.clientMobile') }}</q-item-label> <q-item-label caption>{{
<q-item-label>{{ ticket.client.mobile }}</q-item-label> t('ticket.summary.clientMobile')
}}</q-item-label>
<q-item-label>{{
ticket.client.mobile
}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.consignee') }}</q-item-label> <q-item-label caption>{{
t('ticket.summary.consignee')
}}</q-item-label>
<q-item-label>{{ formattedAddress() }}</q-item-label> <q-item-label>{{ formattedAddress() }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -226,22 +298,34 @@ async function changeState(value) {
<q-list class="taxes"> <q-list class="taxes">
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.subtotal') }}</q-item-label> <q-item-label caption>{{
<q-item-label>{{ toCurrency(ticket.totalWithoutVat) }}</q-item-label> t('ticket.summary.subtotal')
</q-item-section> }}</q-item-label>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.vat') }}</q-item-label>
<q-item-label>{{ <q-item-label>{{
toCurrency(ticket.totalWithVat - ticket.totalWithoutVat) toCurrency(ticket.totalWithoutVat)
}}</q-item-label> }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('ticket.summary.total') }}</q-item-label> <q-item-label caption>{{
<q-item-label>{{ toCurrency(ticket.totalWithVat) }}</q-item-label> t('ticket.summary.vat')
}}</q-item-label>
<q-item-label>{{
toCurrency(
ticket.totalWithVat - ticket.totalWithoutVat
)
}}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{
t('ticket.summary.total')
}}</q-item-label>
<q-item-label>{{
toCurrency(ticket.totalWithVat)
}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
@ -253,7 +337,10 @@ async function changeState(value) {
<q-item-label header class="text-h6"> <q-item-label header class="text-h6">
{{ t('ticket.summary.saleLines') }} {{ t('ticket.summary.saleLines') }}
<router-link <router-link
:to="{ name: 'TicketBasicData', params: { id: entityId } }" :to="{
name: 'TicketBasicData',
params: { id: entityId },
}"
target="_blank" target="_blank"
> >
<q-icon name="open_in_new" /> <q-icon name="open_in_new" />
@ -263,15 +350,33 @@ async function changeState(value) {
<template #header="props"> <template #header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width></q-th> <q-th auto-width></q-th>
<q-th auto-width>{{ t('ticket.summary.item') }}</q-th> <q-th auto-width>{{
<q-th auto-width>{{ t('ticket.summary.visible') }}</q-th> t('ticket.summary.item')
<q-th auto-width>{{ t('ticket.summary.available') }}</q-th> }}</q-th>
<q-th auto-width>{{ t('ticket.summary.quantity') }}</q-th> <q-th auto-width>{{
<q-th auto-width>{{ t('ticket.summary.description') }}</q-th> t('ticket.summary.visible')
<q-th auto-width>{{ t('ticket.summary.price') }}</q-th> }}</q-th>
<q-th auto-width>{{ t('ticket.summary.discount') }}</q-th> <q-th auto-width>{{
<q-th auto-width>{{ t('ticket.summary.amount') }}</q-th> t('ticket.summary.available')
<q-th auto-width>{{ t('ticket.summary.packing') }}</q-th> }}</q-th>
<q-th auto-width>{{
t('ticket.summary.quantity')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.description')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.price')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.discount')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.amount')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.packing')
}}</q-th>
</q-tr> </q-tr>
</template> </template>
<template #body="props"> <template #body="props">
@ -284,11 +389,18 @@ async function changeState(value) {
icon="vn:claims" icon="vn:claims"
v-if="props.row.claim" v-if="props.row.claim"
color="primary" color="primary"
:to="{ name: 'ClaimCard', params: { id: props.row.claim.claimFk } }" :to="{
name: 'ClaimCard',
params: {
id: props.row.claim.claimFk,
},
}"
> >
<q-tooltip <q-tooltip
>{{ t('ticket.summary.claim') }}: >{{ t('ticket.summary.claim') }}:
{{ props.row.claim.claimFk }}</q-tooltip {{
props.row.claim.claimFk
}}</q-tooltip
> >
</q-btn> </q-btn>
<q-btn <q-btn
@ -300,12 +412,17 @@ async function changeState(value) {
color="primary" color="primary"
:to="{ :to="{
name: 'ClaimCard', name: 'ClaimCard',
params: { id: props.row.claimBeginning.claimFk }, params: {
id: props.row.claimBeginning
.claimFk,
},
}" }"
> >
<q-tooltip <q-tooltip
>{{ t('ticket.summary.claim') }}: >{{ t('ticket.summary.claim') }}:
{{ props.row.claimBeginning.claimFk }}</q-tooltip {{
props.row.claimBeginning.claimFk
}}</q-tooltip
> >
</q-btn> </q-btn>
<q-icon <q-icon
@ -325,7 +442,9 @@ async function changeState(value) {
size="xs" size="xs"
color="primary" color="primary"
> >
<q-tooltip>{{ t('ticket.summary.reserved') }}</q-tooltip> <q-tooltip>{{
t('ticket.summary.reserved')
}}</q-tooltip>
</q-icon> </q-icon>
<q-icon <q-icon
name="vn:unavailable" name="vn:unavailable"
@ -333,7 +452,9 @@ async function changeState(value) {
size="xs" size="xs"
color="primary" color="primary"
> >
<q-tooltip>{{ t('ticket.summary.itemShortage') }}</q-tooltip> <q-tooltip>{{
t('ticket.summary.itemShortage')
}}</q-tooltip>
</q-icon> </q-icon>
<q-icon <q-icon
name="vn:components" name="vn:components"
@ -341,7 +462,9 @@ async function changeState(value) {
size="xs" size="xs"
color="primary" color="primary"
> >
<q-tooltip>{{ t('ticket.summary.hasComponentLack') }}</q-tooltip> <q-tooltip>{{
t('ticket.summary.hasComponentLack')
}}</q-tooltip>
</q-icon> </q-icon>
</q-td> </q-td>
<q-td class="link">{{ props.row.itemFk }}</q-td> <q-td class="link">{{ props.row.itemFk }}</q-td>
@ -351,11 +474,16 @@ async function changeState(value) {
<q-td> <q-td>
<div class="fetched-tags"> <div class="fetched-tags">
<span>{{ props.row.item.name }}</span> <span>{{ props.row.item.name }}</span>
<span v-if="props.row.item.subName" class="subName">{{ <span
props.row.item.subName v-if="props.row.item.subName"
}}</span> class="subName"
>{{ props.row.item.subName }}</span
>
</div> </div>
<fetched-tags :item="props.row.item" :max-length="5"></fetched-tags> <fetched-tags
:item="props.row.item"
:max-length="5"
></fetched-tags>
</q-td> </q-td>
<q-td>{{ props.row.price }}</q-td> <q-td>{{ props.row.price }}</q-td>
<q-td>{{ props.row.discount }} %</q-td> <q-td>{{ props.row.discount }} %</q-td>
@ -368,14 +496,19 @@ async function changeState(value) {
) )
}} }}
</q-td> </q-td>
<q-td>{{ dashIfEmpty(props.row.item.itemPackingTypeFk) }}</q-td> <q-td>{{
dashIfEmpty(props.row.item.itemPackingTypeFk)
}}</q-td>
</q-tr> </q-tr>
</template> </template>
</q-table> </q-table>
</q-list> </q-list>
</div> </div>
</div> </div>
<div class="row q-pa-md" v-if="ticket.packagings.length > 0 || ticket.services.length > 0"> <div
class="row q-pa-md"
v-if="ticket.packagings.length > 0 || ticket.services.length > 0"
>
<div class="col" v-if="ticket.packagings.length > 0"> <div class="col" v-if="ticket.packagings.length > 0">
<q-list> <q-list>
<q-item-label header class="text-h6"> <q-item-label header class="text-h6">
@ -385,9 +518,15 @@ async function changeState(value) {
<q-table :rows="ticket.packagings" flat> <q-table :rows="ticket.packagings" flat>
<template #header="props"> <template #header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width>{{ t('ticket.summary.created') }}</q-th> <q-th auto-width>{{
<q-th auto-width>{{ t('ticket.summary.package') }}</q-th> t('ticket.summary.created')
<q-th auto-width>{{ t('ticket.summary.quantity') }}</q-th> }}</q-th>
<q-th auto-width>{{
t('ticket.summary.package')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.quantity')
}}</q-th>
</q-tr> </q-tr>
</template> </template>
<template #body="props"> <template #body="props">
@ -409,11 +548,21 @@ async function changeState(value) {
<q-table :rows="ticket.services" flat> <q-table :rows="ticket.services" flat>
<template #header="props"> <template #header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width>{{ t('ticket.summary.quantity') }}</q-th> <q-th auto-width>{{
<q-th auto-width>{{ t('ticket.summary.description') }}</q-th> t('ticket.summary.quantity')
<q-th auto-width>{{ t('ticket.summary.price') }}</q-th> }}</q-th>
<q-th auto-width>{{ t('ticket.summary.taxClass') }}</q-th> <q-th auto-width>{{
<q-th auto-width>{{ t('ticket.summary.amount') }}</q-th> t('ticket.summary.description')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.price')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.taxClass')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.amount')
}}</q-th>
</q-tr> </q-tr>
</template> </template>
<template #body="props"> <template #body="props">
@ -422,7 +571,11 @@ async function changeState(value) {
<q-td>{{ props.row.description }}</q-td> <q-td>{{ props.row.description }}</q-td>
<q-td>{{ toCurrency(props.row.price) }}</q-td> <q-td>{{ toCurrency(props.row.price) }}</q-td>
<q-td>{{ props.row.taxClass.description }}</q-td> <q-td>{{ props.row.taxClass.description }}</q-td>
<q-td>{{ toCurrency(props.row.quantity * props.row.price) }}</q-td> <q-td>{{
toCurrency(
props.row.quantity * props.row.price
)
}}</q-td>
</q-tr> </q-tr>
</template> </template>
</q-table> </q-table>
@ -439,13 +592,27 @@ async function changeState(value) {
<q-table :rows="ticket.requests" flat> <q-table :rows="ticket.requests" flat>
<template #header="props"> <template #header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width>{{ t('ticket.summary.description') }}</q-th> <q-th auto-width>{{
<q-th auto-width>{{ t('ticket.summary.created') }}</q-th> t('ticket.summary.description')
<q-th auto-width>{{ t('ticket.summary.requester') }}</q-th> }}</q-th>
<q-th auto-width>{{ t('ticket.summary.atender') }}</q-th> <q-th auto-width>{{
<q-th auto-width>{{ t('ticket.summary.quantity') }}</q-th> t('ticket.summary.created')
<q-th auto-width>{{ t('ticket.summary.price') }}</q-th> }}</q-th>
<q-th auto-width>{{ t('ticket.summary.item') }}</q-th> <q-th auto-width>{{
t('ticket.summary.requester')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.atender')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.quantity')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.price')
}}</q-th>
<q-th auto-width>{{
t('ticket.summary.item')
}}</q-th>
<q-th auto-width>Ok</q-th> <q-th auto-width>Ok</q-th>
</q-tr> </q-tr>
</template> </template>
@ -458,8 +625,14 @@ async function changeState(value) {
<q-td>{{ props.row.quantity }}</q-td> <q-td>{{ props.row.quantity }}</q-td>
<q-td>{{ toCurrency(props.row.price) }}</q-td> <q-td>{{ toCurrency(props.row.price) }}</q-td>
<q-td v-if="!props.row.sale">-</q-td> <q-td v-if="!props.row.sale">-</q-td>
<q-td v-if="props.row.sale" class="link">{{ props.row.sale.itemFk }}</q-td> <q-td v-if="props.row.sale" class="link">{{
<q-td><q-checkbox v-model="props.row.isOk" :disable="true" /></q-td> props.row.sale.itemFk
}}</q-td>
<q-td
><q-checkbox
v-model="props.row.isOk"
:disable="true"
/></q-td>
</q-tr> </q-tr>
</template> </template>
</q-table> </q-table>

View File

@ -3,6 +3,7 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import toDateString from 'filters/toDateString';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -12,6 +13,15 @@ const props = defineProps({
}, },
}); });
const from = Date.vnNew()
const to = Date.vnNew();
to.setDate(to.getDate() + 1)
const defaultParams = {
from: toDateString(from),
to: toDateString(to),
};
const workers = ref(); const workers = ref();
const provinces = ref(); const provinces = ref();
const states = ref(); const states = ref();
@ -30,7 +40,11 @@ const warehouses = ref();
@on-fetch="(data) => (workers = data)" @on-fetch="(data) => (workers = data)"
auto-load auto-load
/> />
<VnFilterPanel :data-key="props.dataKey" :search-button="true"> <VnFilterPanel
:data-key="props.dataKey"
:params="defaultParams"
:search-button="true"
>
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong> <strong>{{ t(`params.${tag.label}`) }}: </strong>
@ -57,7 +71,7 @@ const warehouses = ref();
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-input v-model="params.dateFrom" :label="t('From')" mask="date"> <q-input v-model="params.from" :label="t('From')" mask="date">
<template #append> <template #append>
<q-icon name="event" class="cursor-pointer"> <q-icon name="event" class="cursor-pointer">
<q-popup-proxy <q-popup-proxy
@ -65,7 +79,7 @@ const warehouses = ref();
transition-show="scale" transition-show="scale"
transition-hide="scale" transition-hide="scale"
> >
<q-date v-model="params.dateFrom" landscape> <q-date v-model="params.from" landscape>
<div <div
class="row items-center justify-end q-gutter-sm" class="row items-center justify-end q-gutter-sm"
> >
@ -79,7 +93,6 @@ const warehouses = ref();
:label="t('globals.confirm')" :label="t('globals.confirm')"
color="primary" color="primary"
flat flat
@click="save"
v-close-popup v-close-popup
/> />
</div> </div>
@ -90,7 +103,7 @@ const warehouses = ref();
</q-input> </q-input>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-input v-model="params.dateTo" :label="t('To')" mask="date"> <q-input v-model="params.to" :label="t('To')" mask="date">
<template #append> <template #append>
<q-icon name="event" class="cursor-pointer"> <q-icon name="event" class="cursor-pointer">
<q-popup-proxy <q-popup-proxy
@ -98,7 +111,7 @@ const warehouses = ref();
transition-show="scale" transition-show="scale"
transition-hide="scale" transition-hide="scale"
> >
<q-date v-model="params.dateTo" landscape> <q-date v-model="params.to" landscape>
<div <div
class="row items-center justify-end q-gutter-sm" class="row items-center justify-end q-gutter-sm"
> >
@ -278,8 +291,8 @@ en:
search: Contains search: Contains
clientFk: Customer clientFk: Customer
orderFK: Order orderFK: Order
dateFrom: From from: From
dateTo: To to: To
salesPersonFk: Salesperson salesPersonFk: Salesperson
stateFk: State stateFk: State
refFk: Invoice Ref. refFk: Invoice Ref.
@ -295,8 +308,8 @@ es:
search: Contiene search: Contiene
clientFk: Cliente clientFk: Cliente
orderFK: Pedido orderFK: Pedido
dateFrom: Desde from: Desde
dateTo: Hasta to: Hasta
salesPersonFk: Comercial salesPersonFk: Comercial
stateFk: Estado stateFk: Estado
refFk: Ref. Factura refFk: Ref. Factura

View File

@ -5,7 +5,7 @@ import { useQuasar } from 'quasar';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import Paginate from 'src/components/PaginateData.vue'; import Paginate from 'src/components/PaginateData.vue';
import { toDate, toCurrency } from 'src/filters/index'; import { toDate, toDateString, toCurrency } from 'src/filters/index';
import TicketSummaryDialog from './Card/TicketSummaryDialog.vue'; import TicketSummaryDialog from './Card/TicketSummaryDialog.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import TicketFilter from './TicketFilter.vue'; import TicketFilter from './TicketFilter.vue';
@ -46,6 +46,15 @@ const filter = {
], ],
}; };
const from = Date.vnNew();
const to = Date.vnNew();
to.setDate(to.getDate() + 1);
const userParams = {
from: toDateString(from),
to: toDateString(to),
};
function stateColor(row) { function stateColor(row) {
if (row.alertLevelCode === 'OK') return 'green'; if (row.alertLevelCode === 'OK') return 'green';
if (row.alertLevelCode === 'FREE') return 'blue-3'; if (row.alertLevelCode === 'FREE') return 'blue-3';
@ -104,6 +113,7 @@ function viewSummary(id) {
data-key="TicketList" data-key="TicketList"
url="Tickets/filter" url="Tickets/filter"
:filter="filter" :filter="filter"
:user-params="userParams"
order="id DESC" order="id DESC"
auto-load auto-load
> >
@ -123,9 +133,9 @@ function viewSummary(id) {
<q-item-label caption> <q-item-label caption>
{{ t('ticket.list.nickname') }} {{ t('ticket.list.nickname') }}
</q-item-label> </q-item-label>
<q-item-label>{{ <q-item-label>
row.nickname {{ row.nickname }}
}}</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label caption> <q-item-label caption>

View File

@ -12,6 +12,7 @@ const { t } = useI18n();
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar <VnSearchbar
data-key="WorkerList" data-key="WorkerList"
url="Workers/filter"
:label="t('Search worker')" :label="t('Search worker')"
:info="t('You can search by worker id or name')" :info="t('You can search by worker id or name')"
/> />

View File

@ -1,5 +1,10 @@
import { route } from 'quasar/wrappers'; import { route } from 'quasar/wrappers';
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'; import {
createRouter,
createMemoryHistory,
createWebHistory,
createWebHashHistory,
} from 'vue-router';
import routes from './routes'; import routes from './routes';
import { i18n } from 'src/boot/i18n'; import { i18n } from 'src/boot/i18n';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
@ -12,6 +17,22 @@ const session = useSession();
const role = useRole(); const role = useRole();
const { t } = i18n.global; const { t } = i18n.global;
const createHistory = process.env.SERVER
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory;
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(process.env.VUE_ROUTER_BASE),
});
/* /*
* If not building with SSR mode, you can * If not building with SSR mode, you can
* directly export the Router instantiation; * directly export the Router instantiation;
@ -20,24 +41,8 @@ const { t } = i18n.global;
* async/await or return a Promise which resolves * async/await or return a Promise which resolves
* with the Router instance. * with the Router instance.
*/ */
export { Router };
export default route(function (/* { store, ssrContext } */) { export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory;
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(process.env.VUE_ROUTER_BASE),
});
Router.beforeEach(async (to, from, next) => { Router.beforeEach(async (to, from, next) => {
const { isLoggedIn } = session; const { isLoggedIn } = session;

View File

@ -1,70 +0,0 @@
import { vi, describe, expect, it, beforeAll } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import App from 'src/App.vue';
import { useSession } from 'src/composables/useSession';
const mockLoggedIn = vi.fn();
const mockDestroy = vi.fn();
const session = useSession();
vi.mock('src/composables/useSession', () => ({
useSession: () => ({
isLoggedIn: mockLoggedIn,
destroy: mockDestroy,
}),
}));
describe('App', () => {
let vm;
beforeAll(() => {
const options = {
global: {
stubs: ['router-view'],
},
};
vm = createWrapper(App, options).vm;
});
it('should return a login error message', async () => {
vi.spyOn(vm.quasar, 'notify');
session.isLoggedIn.mockReturnValue(false);
const response = {
response: {
status: 401,
},
};
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Invalid username or password',
type: 'negative',
})
);
});
it('should return an unauthorized error message', async () => {
vi.spyOn(vm.quasar, 'notify');
session.isLoggedIn.mockReturnValue(true);
const response = {
response: {
status: 401,
},
};
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Access denied',
type: 'negative',
})
);
expect(session.destroy).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,81 @@
import { vi, describe, expect, it } from 'vitest';
import { onRequest, onResponseError } from 'src/boot/axios';
import { Notify } from 'quasar'
vi.mock('src/composables/useSession', () => ({
useSession: () => ({
getToken: () => 'DEFAULT_TOKEN'
}),
}));
vi.mock('src/router', () => ({}));
describe('Axios boot', () => {
describe('onRequest()', async () => {
it('should set the "Authorization" property on the headers', async () => {
const config = { headers: {} };
const resultConfig = onRequest(config);
expect(resultConfig).toEqual(expect.objectContaining({
headers: {
Authorization: 'DEFAULT_TOKEN'
}
}));
});
})
describe('onResponseError()', async () => {
it('should call to the Notify plugin with a message error for an status code "500"', async () => {
Notify.create = vi.fn()
const error = {
response: {
status: 500
}
};
const result = onResponseError(error);
expect(result).rejects.toEqual(
expect.objectContaining(error)
);
expect(Notify.create).toHaveBeenCalledWith(
expect.objectContaining({
message: 'An internal server error has ocurred',
type: 'negative',
})
);
});
it('should call to the Notify plugin with a message from the response property', async () => {
Notify.create = vi.fn()
const error = {
response: {
status: 401,
data: {
error: {
message: 'Invalid user or password'
}
}
}
};
const result = onResponseError(error);
expect(result).rejects.toEqual(
expect.objectContaining(error)
);
expect(Notify.create).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Invalid user or password',
type: 'negative',
})
);
});
})
});

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
}, },
}) })
); );

View File

@ -23,7 +23,9 @@ describe('Login', () => {
}, },
}; };
vi.spyOn(axios, 'post').mockResolvedValue({ data: { token: 'token' } }); vi.spyOn(axios, 'post').mockResolvedValue({ data: { token: 'token' } });
vi.spyOn(axios, 'get').mockResolvedValue({ data: { roles: [], user: expectedUser } }); vi.spyOn(axios, 'get').mockResolvedValue({
data: { roles: [], user: expectedUser },
});
vi.spyOn(vm.quasar, 'notify'); vi.spyOn(vm.quasar, 'notify');
expect(vm.session.getToken()).toEqual(''); expect(vm.session.getToken()).toEqual('');
@ -31,7 +33,9 @@ describe('Login', () => {
await vm.onSubmit(); await vm.onSubmit();
expect(vm.session.getToken()).toEqual('token'); expect(vm.session.getToken()).toEqual('token');
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining({ type: 'positive' })); expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'positive' })
);
vm.session.destroy(); vm.session.destroy();
}); });