Modulo de rutas #195

Merged
jsegarra merged 49 commits from :feature/route-module into dev 2024-03-14 12:44:43 +00:00
4 changed files with 338 additions and 48 deletions
Showing only changes of commit b3f8d369ee - Show all commits

View File

@ -0,0 +1,153 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import VnInput from 'components/common/VnInput.vue';
import axios from 'axios';
const MESSAGE_MAX_LENGTH = 160;
const { t } = useI18n();
const props = defineProps({
title: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
destination: {
type: String,
required: true,
},
destinationFk: {
type: String,
required: true,
},
data: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits([...useDialogPluginComponent.emits, 'sent']);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const smsRules = [
(val) => (val && val.length > 0) || t("The message can't be empty"),
(val) =>
(val && new Blob([val]).size <= MESSAGE_MAX_LENGTH) ||
t("The message it's too long"),
];
const message = ref('');
const charactersRemaining = computed(
() => MESSAGE_MAX_LENGTH - new Blob([message.value]).size
);
const charactersChipColor = computed(() => {
if (charactersRemaining.value < 0) {
return 'negative';
}
if (charactersRemaining.value <= 25) {
return 'warning';
}
return 'primary';
});
const onSubmit = async () => {
if (!props.destination) {
throw new Error(`The destination can't be empty`);
}
alexm marked this conversation as resolved
Review

Teniendo arriba:

    destination: {
        type: String,
        required: true,
    }

Hace falta estos ifs?

Teniendo arriba: ``` destination: { type: String, required: true, } ``` Hace falta estos ifs?
Review

Mmm...es por si se modifica a posteriori. De todas maneras si borras el campo sin el clearable, te deja el valor del campo a "", por lo que la validación no entraría, no?
Quizás falta integrar lo del hover_clearable o cambiar la validación.
@kevin, tus has podido mostrar el throw de la validación?
Gracias

Mmm...es por si se modifica a posteriori. De todas maneras si borras el campo sin el clearable, te deja el valor del campo a "", por lo que la validación no entraría, no? Quizás falta integrar lo del hover_clearable o cambiar la validación. @kevin, tus has podido mostrar el throw de la validación? Gracias
if (!message.value) {
throw new Error(`The message can't be empty`);
}
if (charactersRemaining.value < 0) {
throw new Error(`The message it's too long`);
}
const response = await axios.post(props.url, {
destination: props.destination,
destinationFk: props.destinationFk,
message: message.value,
...props.data,
});
if (response.data) {
emit('sent', response.data);
}
emit('ok', response.data);
emit('hide', response.data);
};
</script>
<template>
<QDialog ref="dialogRef" @hide="onDialogHide">
<QCard class="full-width dialog">
<QCardSection class="row">
<span v-if="title" class="text-h6">{{ title }}</span>
<QSpace />
<QBtn icon="close" flat round dense v-close-popup />
</QCardSection>
<QForm @submit="onSubmit">
<QCardSection>
<VnInput
v-model="message"
type="textarea"
:rules="smsRules"
:label="t('Message')"
:placeholder="t('Message')"
:rows="5"
required
clearable
no-error-icon
>
<template #append>
<QIcon name="info">
<QTooltip>
{{
t(
'Special characters like accents counts as a multiple'
)
}}
</QTooltip>
</QIcon>
</template>
</VnInput>
<p class="q-mb-none q-mt-md">
{{ t('Characters remaining') }}:
<QChip :color="charactersChipColor">
{{ charactersRemaining }}
</QChip>
</p>
</QCardSection>
<QCardActions align="right">
<QBtn type="button" flat v-close-popup class="text-primary">
{{ t('globals.cancel') }}
</QBtn>
<QBtn type="submit" color="primary">{{ t('Send') }}</QBtn>
</QCardActions>
</QForm>
</QCard>
</QDialog>
</template>
<style lang="scss" scoped>
.dialog {
max-width: 450px;
}
</style>
<i18n>
es:
Message: Mensaje
Send: Enviar
Characters remaining: Carácteres restantes
Special characters like accents counts as a multiple: Carácteres especiales como los acentos cuentan como varios
The destination can't be empty: El destinatario no puede estar vacio
The message can't be empty: El mensaje no puede estar vacio
The message it's too long: El mensaje es demasiado largo
</i18n>

View File

@ -1,11 +1,11 @@
<script setup> <script setup>
import { useDialogPluginComponent, useQuasar } from 'quasar'; import { useDialogPluginComponent } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue'; import { ref } from 'vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import axios from 'axios'; import axios from 'axios';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import TicketDescriptorProxy from "pages/Ticket/Card/TicketDescriptorProxy.vue"; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
const { t } = useI18n(); const { t } = useI18n();
const $props = defineProps({ const $props = defineProps({
@ -19,7 +19,7 @@ const emit = defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent(); const { dialogRef, onDialogHide } = useDialogPluginComponent();
const columns = computed(() => [ const columns = ref([
{ {
name: 'ticket', name: 'ticket',
label: t('Ticket'), label: t('Ticket'),

View File

@ -7,15 +7,17 @@ import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue'; import VnInput from 'components/common/VnInput.vue';
import axios from 'axios'; import axios from 'axios';
import { QIcon, useQuasar } from 'quasar'; import { QIcon, useQuasar } from 'quasar';
import { useSession } from 'composables/useSession';
import RouteListTicketsDialog from 'pages/Route/Card/RouteListTicketsDialog.vue'; import RouteListTicketsDialog from 'pages/Route/Card/RouteListTicketsDialog.vue';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue'; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
jgallego marked this conversation as resolved
Review

Al cargar esta seccion desaparece el buscardor de la parte superior, en basic-data sí aparece

Al cargar esta seccion desaparece el buscardor de la parte superior, en basic-data sí aparece
Review

Corregido 77e29a2b87.

Corregido 77e29a2b876988253cc867d29168ee44471ea5f0.
import VnConfirm from 'components/ui/VnConfirm.vue';
import FetchData from 'components/FetchData.vue';
import { openBuscaman } from 'src/utils/buscaman';
import SendSmsDialog from 'components/common/SendSmsDialog.vue';
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
const session = useSession();
const route = useRoute(); const route = useRoute();
const selectedRows = ref([]); const selectedRows = ref([]);
@ -102,6 +104,8 @@ const columns = computed(() => [
const refreshKey = ref(0); const refreshKey = ref(0);
const confirmationDialog = ref(false); const confirmationDialog = ref(false);
const startingDate = ref(null); const startingDate = ref(null);
const routeEntity = ref(null);
const ticketList = ref([]);
const cloneRoutes = () => { const cloneRoutes = () => {
axios.post('Routes/clone', { axios.post('Routes/clone', {
@ -112,31 +116,54 @@ const cloneRoutes = () => {
startingDate.value = null; startingDate.value = null;
}; };
const showRouteReport = () => { const deletePriorities = async () => {
const ids = selectedRows.value.map((row) => row?.id); try {
const idString = ids.join(','); await Promise.all(
let url; selectedRows.value.map((ticket) =>
axios.patch(`Tickets/${ticket?.id}/`, { priority: null })
if (selectedRows.value.length <= 1) { )
url = `api/Routes/${idString}/driver-route-pdf?access_token=${session.getToken()}`; );
} else { } finally {
const params = new URLSearchParams({ refreshKey.value++;
access_token: session.getToken(),
id: idString,
});
url = `api/Routes/downloadZip?${params.toString()}`;
} }
window.open(url, '_blank');
}; };
const markAsServed = () => { const setOrderedPriority = async () => {
selectedRows.value.forEach((row) => { try {
if (row?.id) { await Promise.all(
axios.patch(`Routes/${row?.id}`, { isOk: true }); ticketList.value.map((ticket, index) =>
} axios.patch(`Tickets/${ticket?.id}/`, { priority: index + 1 })
}); )
);
} finally {
refreshKey.value++;
}
};
const sortRoutes = async () => {
await axios.get(`Routes/${route.params?.id}/guessPriority/`);
refreshKey.value++; refreshKey.value++;
startingDate.value = null; };
const updatePriority = async (ticket, priority = null) => {
return axios.patch(`Tickets/${ticket?.id}/`, {
priority: priority || ticket?.priority,
});
};
const setHighestPriority = async (ticket, ticketList) => {
const highestPriority = Math.max(...ticketList.map((item) => item.priority)) + 1;
if (highestPriority - 1 !== ticket.priority) {
const response = await updatePriority(ticket, highestPriority);
ticket.priority = response.data.priority;
}
};
const goToBuscaman = async (ticket = null) => {
await openBuscaman(
routeEntity.value?.vehicleFk,
ticket ? [ticket] : selectedRows.value
);
}; };
const openTicketsDialog = () => { const openTicketsDialog = () => {
@ -149,9 +176,57 @@ const openTicketsDialog = () => {
}) })
.onOk(() => refreshKey.value++); .onOk(() => refreshKey.value++);
}; };
const removeTicket = async (ticket) => {
await axios.patch(`Tickets/${ticket?.id}/`, { routeFk: null });
await axios.post(`Routes/${route?.params?.id}/updateVolume`);
refreshKey.value++;
};
const confirmRemove = (ticket) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('confirmDeletion'),
message: t('confirmDeletionMessage'),
promise: () => removeTicket(ticket),
},
})
.onOk(() => refreshKey.value++);
};
const openSmsDialog = async () => {
const clientsId = [];
const clientsPhone = [];
for (let ticket of selectedRows.value) {
clientsId.push(ticket?.clientFk);
const { data: client } = await axios.get(`Clients/${ticket?.clientFk}`);
clientsPhone.push(client.phone);
}
quasar.dialog({
component: SendSmsDialog,
componentProps: {
title: t('Send SMS to the selected tickets'),
url: 'Routes/sendSms',
destinationFk: clientsId.toString(),
destination: clientsPhone.toString(),
},
});
};
</script> </script>
<template> <template>
<FetchData
@on-fetch="(data) => (routeEntity = data)"
auto-load
:url="`Routes/${route.params?.id}`"
:filter="{
fields: ['vehicleFk'],
}"
/>
<QDialog v-model="confirmationDialog"> <QDialog v-model="confirmationDialog">
<QCard style="min-width: 350px"> <QCard style="min-width: 350px">
<QCardSection> <QCardSection>
@ -176,32 +251,47 @@ const openTicketsDialog = () => {
<QPage class="column items-center"> <QPage class="column items-center">
<QToolbar class="bg-vn-dark justify-end"> <QToolbar class="bg-vn-dark justify-end">
<div id="st-actions" class="q-pa-sm"> <div id="st-actions" class="q-pa-sm">
<QBtn <QBtn icon="vn:wand" color="primary" class="q-mr-sm" @click="sortRoutes">
icon="vn:clone" <QTooltip>{{ t('Sort routes') }}</QTooltip>
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="confirmationDialog = true"
>
<QTooltip>{{ t('Clone Selected Routes') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
icon="cloud_download" icon="vn:buscaman"
color="primary" color="primary"
class="q-mr-sm" class="q-mr-sm"
:disable="!selectedRows?.length" :disable="!selectedRows?.length"
@click="showRouteReport" @click="goToBuscaman()"
> >
<QTooltip>{{ t('Download selected routes as PDF') }}</QTooltip> <QTooltip>{{ t('Open buscaman') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
icon="check" icon="filter_alt"
jsegarra marked this conversation as resolved Outdated

font-variation-settings: 'FILL' 1;

font-variation-settings: 'FILL' 1;

Corregido 7974725da0.

Corregido 7974725da0.
color="primary" color="primary"
class="q-mr-sm" class="q-mr-sm"
:disable="!selectedRows?.length" :disable="!selectedRows?.length"
@click="markAsServed" @click="deletePriorities"
> >
<QTooltip>{{ t('Mark as served') }}</QTooltip> <QTooltip>{{ t('Delete priority') }}</QTooltip>
</QBtn>
<QBtn
icon="format_list_numbered"
color="primary"
class="q-mr-sm"
@click="setOrderedPriority"
>
<QTooltip
>{{
t('Renumber all tickets in the order you see on the screen')
}}
</QTooltip>
</QBtn>
<QBtn
icon="sms"
jsegarra marked this conversation as resolved
Review

añadir: font-variation-settings: 'FILL' 1;

añadir: font-variation-settings: 'FILL' 1;
Review

Corregido 7974725da0.

Corregido 7974725da0.
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="openSmsDialog"
>
<QTooltip>{{ t('Send SMS to all clients') }}</QTooltip>
</QBtn> </QBtn>
</div> </div>
</QToolbar> </QToolbar>
@ -213,6 +303,7 @@ const openTicketsDialog = () => {
:filter="{ id: route.params.id }" :filter="{ id: route.params.id }"
:order="['priority ASC']" :order="['priority ASC']"
auto-load auto-load
@on-fetch="(data) => (ticketList = data)"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<div class="q-pa-md"> <div class="q-pa-md">
@ -228,19 +319,30 @@ const openTicketsDialog = () => {
> >
<template #body-cell-order="{ row }"> <template #body-cell-order="{ row }">
<QTd class="order-field"> <QTd class="order-field">
<VnInput <div class="flex no-wrap items-center">
v-model="row.priority" <QIcon
is-outlined name="low_priority"
/> size="xs"
class="q-mr-md cursor-pointer"
color="primary"
@click="setHighestPriority(row, rows)"
/>
<VnInput
v-model="row.priority"
is-outlined
@blur="updatePriority(row)"
/>
</div>
</QTd> </QTd>
</template> </template>
<template #body-cell-city="{ value, row }"> <template #body-cell-city="{ value, row }">
<QTd auto-width> <QTd auto-width>
<span <span
class="text-primary cursor-pointer" class="text-primary cursor-pointer"
@click="openBuscaman(entity?.route, row)" @click="goToBuscaman(row)"
> >
{{ value }} {{ value }}
<QTooltip>{{ t('Open buscaman') }}</QTooltip>
</span> </span>
</QTd> </QTd>
</template> </template>
@ -260,7 +362,7 @@ const openTicketsDialog = () => {
</span> </span>
</QTd> </QTd>
</template> </template>
<template #body-cell-observations="{ value }"> <template #body-cell-observations="{ value, row }">
<QTd auto-width> <QTd auto-width>
<div class="flex items-center no-wrap table-actions"> <div class="flex items-center no-wrap table-actions">
<QIcon <QIcon
@ -268,6 +370,7 @@ const openTicketsDialog = () => {
color="primary" color="primary"
class="cursor-pointer" class="cursor-pointer"
size="xs" size="xs"
@click="confirmRemove(row)"
> >
<QTooltip>{{ t('globals.remove') }}</QTooltip> <QTooltip>{{ t('globals.remove') }}</QTooltip>
</QIcon> </QIcon>
@ -303,7 +406,8 @@ const openTicketsDialog = () => {
} }
.order-field { .order-field {
max-width: 50px; max-width: 140px;
min-width: 120px;
} }
.table-actions { .table-actions {
@ -313,6 +417,8 @@ const openTicketsDialog = () => {
<i18n> <i18n>
en: en:
newRoute: New Route newRoute: New Route
confirmDeletion: Confirm deletion
confirmDeletionMessage: Are you sure you want to delete this ticket?
es: es:
Order: Orden Order: Orden
Street: Dirección fiscal Street: Dirección fiscal
@ -322,4 +428,13 @@ es:
Warehouse: Almacén Warehouse: Almacén
Packages: Bultos Packages: Bultos
Packaging: Encajado Packaging: Encajado
confirmDeletion: Confirmar eliminación
confirmDeletionMessage: Seguro que quieres eliminar este ticket?
Sort routes: Ordenar rutas
Open buscaman: Abrir buscaman
Delete priority: Borrar orden

usar el verbo quitar en lugar de remover

usar el verbo quitar en lugar de remover

Corregido 77e29a2b87.

Corregido 77e29a2b876988253cc867d29168ee44471ea5f0.
Renumber all tickets in the order you see on the screen: Renumerar todos los tickets con el orden que ves por pantalla

usar el verbo quitar en lugar de remover

usar el verbo quitar en lugar de remover

Corregido 77e29a2b87.

Corregido 77e29a2b876988253cc867d29168ee44471ea5f0.
Assign highest priority: Asignar máxima prioridad
Send SMS to all clients: Mandar sms a todos los clientes de las rutas
Send SMS to the selected tickets: Enviar SMS a los tickets seleccionados
</i18n> </i18n>

22
src/utils/buscaman.js Normal file
View File

@ -0,0 +1,22 @@
import axios from 'axios';
const BUSCAMAN_URL = 'https://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=';
export async function openBuscaman(vehicleId, tickets) {
if (!vehicleId) throw new Error(`The route doesn't have a vehicle`);
const response = await axios.get(`Routes/${vehicleId}/getDeliveryPoint`);
if (!response.data) {
throw new Error(`The route's vehicle doesn't have a delivery point`);
}
let addresses = response.data;
tickets.forEach((ticket, index) => {
const previousLine = tickets[index - 1] ? tickets[index - 1].street : null;
if (previousLine !== tickets.street) {
addresses += `+to:${ticket.postalCode} ${ticket.city} ${ticket.street}`;
}
});
window.open(BUSCAMAN_URL + encodeURI(addresses), '_blank');
}