0
0
Fork 0

Create route summary

This commit is contained in:
Kevin Martinez 2024-01-18 08:45:41 -04:00
parent 8f47a0bc2b
commit 661e9d4368
11 changed files with 806 additions and 219 deletions

View File

@ -809,7 +809,8 @@ export default {
cmrsList: 'External CMRs list',
RouteList: 'List',
create: 'Create',
basicData: 'Basic Data'
basicData: 'Basic Data',
summary: 'Summary'
},
cmr: {
list: {

View File

@ -809,6 +809,7 @@ export default {
RouteList: 'Listado',
create: 'Crear',
basicData: 'Datos básicos',
summary: 'Summary',
},
cmr: {
list: {

View File

@ -1,6 +1,7 @@
<script setup>
import { useStateStore } from 'stores/useStateStore';
import LeftMenu from 'components/LeftMenu.vue';
import RouteDescriptor from 'pages/Route/Card/RouteDescriptor.vue';
// import ShelvingDescriptor from 'pages/Shelving/Card/ShelvingDescriptor.vue';
const stateStore = useStateStore();
@ -8,7 +9,7 @@ const stateStore = useStateStore();
<template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<!-- <ShelvingDescriptor />-->
<RouteDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>

View File

@ -0,0 +1,104 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'components/ui/VnLv.vue';
import useCardDescription from 'composables/useCardDescription';
import { dashIfEmpty, toDate } from 'src/filters';
import RouteDescriptorMenu from 'pages/Route/Card/RouteDescriptorMenu.vue';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const route = useRoute();
const { t } = useI18n();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const filter = {
fields: [
'id',
'workerFk',
'agencyModeFk',
'created',
'm3',
'warehouseFk',
'description',
'vehicleFk',
'kmStart',
'kmEnd',
'started',
'finished',
'cost',
'zoneFk',
'isOk',
],
include: [
{ relation: 'agencyMode', scope: { fields: ['id', 'name'] } },
{
relation: 'vehicle',
scope: { fields: ['id', 'm3'] },
},
{ relation: 'zone', scope: { fields: ['id', 'name'] } },
{
relation: 'worker',
scope: {
fields: ['id'],
include: {
relation: 'user',
scope: {
fields: ['id'],
include: { relation: 'emailUser', scope: { fields: ['email'] } },
},
},
},
},
],
};
const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id));
</script>
<template>
<CardDescriptor
module="Route"
:url="`Routes/${entityId}`"
:filter="filter"
:title="data.title"
:subtitle="data.subtitle"
data-key="Routes"
@on-fetch="setData"
>
<template #body="{ entity }">
<VnLv :label="t('Date')" :value="toDate(entity?.created)" />
<VnLv :label="t('Agency')" :value="entity?.agencyMode?.name" />
<VnLv :label="t('Zone')" :value="entity?.zone?.name" />
<VnLv
:label="t('Volume')"
:value="`${dashIfEmpty(entity?.m3)} / ${dashIfEmpty(
entity?.vehicle?.m3
)} `"
/>
<VnLv :label="t('Description')" :value="entity?.description" />
</template>
<template #menu="{ entity }">
<RouteDescriptorMenu :route="entity" />
</template>
</CardDescriptor>
</template>
<i18n>
es:
Date: Fecha
Agency: Agencia
Zone: Zona
Volume: Volumen
Description: Descripción
</i18n>

View File

@ -0,0 +1,63 @@
<script setup>
import axios from 'axios';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnConfirm from 'components/ui/VnConfirm.vue';
const props = defineProps({
route: {
type: Object,
required: true,
},
});
const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
function confirmRemove() {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('confirmDeletion'),
message: t('confirmDeletionMessage'),
promise: remove,
},
})
.onOk(async () => await router.push({ name: 'RouteList' }));
}
async function remove() {
if (!props.route.id) {
return;
}
await axios.delete(`Routes/${props.route.id}`);
quasar.notify({
message: t('globals.dataDeleted'),
type: 'positive',
});
}
// TODO: Add reports
</script>
<template>
<QItem @click="confirmRemove" v-ripple clickable>
<QItemSection avatar>
<QIcon name="delete" />
</QItemSection>
<QItemSection>{{ t('deleteRoute') }}</QItemSection>
</QItem>
</template>
<i18n>
en:
confirmDeletion: Confirm deletion
confirmDeletionMessage: Are you sure you want to delete this route?
deleteRoute: Delete route
es:
confirmDeletion: Confirmar eliminación,
confirmDeletionMessage: Seguro que quieres eliminar esta ruta?
deleteRoute: Eliminar ruta
</i18n>

View File

@ -0,0 +1,15 @@
<script setup>
import RouteDescriptor from 'pages/Route/Card/RouteDescriptor.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
</script>
<template>
<QPopupProxy>
<RouteDescriptor v-if="$props.id" :id="$props.id" />
</QPopupProxy>
</template>

View File

@ -72,8 +72,7 @@ const routeFilter = {
const onSave = (data, response) => {
if (isNew) {
axios.post(`Routes/${response.data?.id}/updateWorkCenter`);
// TODO: Add summary
// router.push({ name: 'RouteSummary', params: { id: response.data?.id } });
router.push({ name: 'RouteSummary', params: { id: response.data?.id } });
}
};
</script>

View File

@ -0,0 +1,314 @@
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'components/ui/VnLv.vue';
import { QIcon } from 'quasar';
import { dashIfEmpty, toCurrency, toDate, toHour } from 'src/filters';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
import axios from 'axios';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const route = useRoute();
const stateStore = useStateStore();
const { t } = useI18n();
const entityId = computed(() => $props.id || route.params.id);
const isDialog = Boolean($props.id);
const hideRightDrawer = () => {
if (!isDialog) {
stateStore.rightDrawer = false;
}
};
onMounted(hideRightDrawer);
onUnmounted(hideRightDrawer);
const getTotalPackages = (tickets) => {
return (tickets || []).reduce((sum, ticket) => sum + ticket.packages, 0);
};
const ticketColumns = ref([
{
name: 'order',
label: t('route.summary.order'),
field: (row) => dashIfEmpty(row?.priority),
sortable: false,
align: 'center',
},
{
name: 'street',
label: t('route.summary.street'),
field: (row) => row?.street,
sortable: false,
align: 'left',
},
{
name: 'city',
label: t('route.summary.city'),
field: (row) => row?.city,
sortable: false,
align: 'left',
},
{
name: 'pc',
label: t('route.summary.pc'),
field: (row) => row?.postalCode,
sortable: false,
align: 'center',
},
{
name: 'client',
label: t('route.summary.client'),
field: (row) => row?.nickname,
sortable: false,
align: 'left',
},
{
name: 'warehouse',
label: t('route.summary.warehouse'),
field: (row) => row?.warehouseName,
sortable: false,
align: 'left',
},
{
name: 'packages',
label: t('route.summary.packages'),
field: (row) => row?.packages,
sortable: false,
align: 'center',
},
{
name: 'volume',
label: t('route.summary.m3'),
field: (row) => row?.volume,
sortable: false,
align: 'center',
},
{
name: 'packaging',
label: t('route.summary.packaging'),
field: (row) => row?.ipt,
sortable: false,
align: 'center',
},
{
name: 'ticket',
label: t('route.summary.ticket'),
field: (row) => row?.id,
sortable: false,
align: 'right',
},
{
name: 'observations',
label: '',
field: (row) => row?.ticketObservation,
sortable: false,
align: 'left',
},
]);
const openBuscaman = async (route, ticket) => {
if (!route.vehicleFk) throw new Error(`The route doesn't have a vehicle`);
const response = await axios.get(`Routes/${route.vehicleFk}/getDeliveryPoint`);
if (!response.data)
throw new Error(`The route's vehicle doesn't have a delivery point`);
const address = `${response.data}+to:${ticket.postalCode} ${ticket.city} ${ticket.street}`;
window.open(
'https://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=' +
encodeURI(address),
'_blank'
);
};
</script>
<template>
<div class="q-pa-md">
<CardSummary ref="summary" :url="`Routes/${entityId}/summary`">
<template #header-left>
<RouterLink :to="{ name: `RouteSummary`, params: { id: entityId } }">
<QIcon name="open_in_new" color="white" size="sm" />
</RouterLink>
</template>
<template #header="{ entity }">
<span>{{ `${entity?.route.id} - ${entity?.route?.description}` }}</span>
</template>
<template #body="{ entity }">
<QCard class="vn-one">
<VnLv :label="t('ID')" :value="entity?.route.id" />
<VnLv
:label="t('route.summary.date')"
:value="toDate(entity?.route.created)"
/>
<VnLv
:label="t('route.summary.agency')"
:value="entity?.route?.agencyMode?.name"
/>
<VnLv
:label="t('route.summary.vehicle')"
:value="entity.route?.vehicle?.numberPlate"
/>
<VnLv :label="t('route.summary.driver')">
<template #value>
<span class="link">
{{ dashIfEmpty(entity?.route?.worker?.user?.name) }}
<WorkerDescriptorProxy :id="entity.route?.workerFk" />
</span>
</template>
</VnLv>
<VnLv
:label="t('route.summary.cost')"
:value="toCurrency(entity.route?.cost)"
/>
</QCard>
<QCard class="vn-one">
<VnLv
:label="t('route.summary.started')"
:value="toHour(entity?.route.started)"
/>
<VnLv
:label="t('route.summary.finished')"
:value="toHour(entity?.route.finished)"
/>
<VnLv
:label="t('route.summary.kmStart')"
:value="dashIfEmpty(entity?.route?.kmStart)"
/>
<VnLv
:label="t('route.summary.kmEnd')"
:value="dashIfEmpty(entity?.route?.kmEnd)"
/>
<VnLv
:label="t('route.summary.volume')"
:value="`${dashIfEmpty(entity?.route?.m3)} / ${dashIfEmpty(
entity?.route?.vehicle?.m3
)} `"
/>
<VnLv
:label="t('route.summary.packages')"
:value="getTotalPackages(entity.tickets)"
/>
</QCard>
<QCard class="vn-one">
<div class="header">
{{ t('route.summary.description') }}
</div>
<p>
{{ dashIfEmpty(entity?.route?.description) }}
</p>
</QCard>
<QCard class="vn-max">
<div class="header">
{{ t('route.summary.tickets') }}
</div>
<QTable
:columns="ticketColumns"
:rows="entity?.tickets"
:rows-per-page-options="[0]"
row-key="id"
flat
hide-pagination
>
<template #body-cell-city="{ value, row }">
<QTd auto-width>
<span
class="text-primary cursor-pointer"
@click="openBuscaman(entity?.route, row)"
>
{{ value }}
</span>
</QTd>
</template>
<template #body-cell-client="{ value, row }">
<QTd auto-width>
<span class="text-primary cursor-pointer">
{{ value }}
<CustomerDescriptorProxy :id="row?.clientFk" />
</span>
</QTd>
</template>
<template #body-cell-ticket="{ value, row }">
<QTd auto-width>
<span class="text-primary cursor-pointer">
{{ value }}
<TicketDescriptorProxy :id="row?.id" />
</span>
</QTd>
</template>
<template #body-cell-observations="{ value }">
<QTd auto-width>
<QIcon
v-if="value"
name="vn:notes"
color="primary"
class="cursor-pointer"
>
<QTooltip>{{ value }}</QTooltip>
</QIcon>
</QTd>
</template>
</QTable>
</QCard>
</template>
</CardSummary>
</div>
</template>
<i18n>
en:
route:
summary:
date: Date
agency: Agency
vehicle: Vehicle
driver: Driver
cost: Cost
started: Started time
finished: Finished time
kmStart: Km start
kmEnd: Km end
volume: Volume
packages: Packages
description: Description
tickets: Tickets
order: Order
street: Street
city: City
pc: PC
client: Client
warehouse: Warehouse
m3:
packaging: Packaging
ticket: Ticket
es:
route:
summary:
date: Fecha
agency: Agencia
vehicle: Vehículo
driver: Conductor
cost: Costo
started: Hora inicio
finished: Hora fin
kmStart: Km inicio
kmEnd: Km fin
volume: Volumen
packages: Bultos
description: Descripción
tickets: Tickets
order: Orden
street: Dirección fiscal
city: Población
pc: CP
client: Cliente
warehouse: Almacén
packaging: Encajado
</i18n>

View File

@ -0,0 +1,29 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import RouteSummary from 'pages/Route/Card/RouteSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<QDialog ref="dialogRef" @hide="onDialogHide">
<RouteSummary v-if="$props.id" :id="$props.id" />
</QDialog>
</template>
<style lang="scss">
.q-dialog .route .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -13,12 +13,13 @@ import VnInputTime from 'components/common/VnInputTime.vue';
import axios from 'axios';
import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue';
import RouteFilter from 'pages/Route/Card/RouteFilter.vue';
import { useQuasar } from 'quasar';
import RouteSummaryDialog from 'pages/Route/Card/RouteSummaryDialog.vue';
const stateStore = useStateStore();
// const router = useRouter();
// const quasar = useQuasar();
const { t } = useI18n();
const { validate } = useValidator();
const quasar = useQuasar();
const to = Date.vnNew();
to.setDate(to.getDate() + 1);
@ -98,6 +99,12 @@ const columns = computed(() => [
sortable: true,
align: 'left',
},
{
name: 'actions',
label: '',
sortable: false,
align: 'right',
},
]);
const refreshKey = ref(0);
@ -151,6 +158,18 @@ const markAsServed = () => {
refreshKey.value++;
startingDate.value = null;
};
function previewRoute(id) {
if (!id) {
return;
}
quasar.dialog({
component: RouteSummaryDialog,
componentProps: {
id,
},
});
}
</script>
<template>
@ -187,7 +206,7 @@ const markAsServed = () => {
autofocus
/>
</QCardSection>
<!-- TODO: Add report -->
<QCardActions align="right">
<QBtn flat :label="t('Cancel')" v-close-popup class="text-primary" />
<QBtn color="primary" v-close-popup @click="cloneRoutes">
@ -232,6 +251,7 @@ const markAsServed = () => {
</QBtn>
</div>
</QToolbar>
<div class="route-list">
<VnPaginate
:key="refreshKey"
data-key="RouteList"
@ -250,6 +270,7 @@ const markAsServed = () => {
row-key="id"
selection="multiple"
:rows-per-page-options="[0]"
hide-pagination
>
<template #body-cell-worker="props">
<QTd :props="props">
@ -435,10 +456,32 @@ const markAsServed = () => {
</QPopupEdit>
</QTd>
</template>
<template #body-cell-actions="props">
<QTd :props="props">
<div class="table-actions">
<QIcon
name="vn:ticketAdd"
size="xs"
color="primary"
>
<QTooltip>{{ t('Add ticket') }}</QTooltip>
</QIcon>
<QIcon
name="preview"
size="xs"
color="primary"
@click="previewRoute(props?.row?.id)"
>
<QTooltip>{{ t('Preview') }}</QTooltip>
</QIcon>
</div>
</QTd>
</template>
</QTable>
</div>
</template>
</VnPaginate>
</div>
<QPageSticky :offset="[20, 20]">
<RouterLink :to="{ name: 'RouteCreate' }">
<QBtn fab icon="add" color="primary" />
@ -451,9 +494,18 @@ const markAsServed = () => {
</template>
<style lang="scss" scoped>
.card-list {
.route-list {
width: 100%;
max-width: 60em;
}
.table-actions {
display: flex;
align-items: center;
gap: 12px;
i {
cursor: pointer;
}
}
</style>
<i18n>

View File

@ -11,14 +11,14 @@ export default {
redirect: { name: 'RouteMain' },
menus: {
main: ['RouteList', 'CmrList'],
card: [],
card: ['RouteBasicData'],
},
children: [
{
path: '/route',
name: 'RouteMain',
component: () => import('src/pages/Route/RouteMain.vue'),
redirect: { name: 'CmrList' },
redirect: { name: 'RouteList' },
children: [
{
path: 'cmr',
@ -34,7 +34,7 @@ export default {
name: 'RouteList',
meta: {
title: 'RouteList',
icon: 'vn:delivery',
icon: 'view_list',
},
component: () => import('src/pages/Route/RouteList.vue'),
},
@ -52,8 +52,7 @@ export default {
name: 'RouteCard',
path: ':id',
component: () => import('src/pages/Route/Card/RouteCard.vue'),
// TODO: Add summary
redirect: { name: 'RouteBasicData' },
redirect: { name: 'RouteSummary' },
children: [
{
name: 'RouteBasicData',
@ -64,6 +63,15 @@ export default {
},
component: () => import('pages/Route/Card/RouteForm.vue'),
},
{
name: 'RouteSummary',
path: 'summary',
meta: {
title: 'summary',
icon: 'open_in_new',
},
component: () => import('pages/Route/Card/RouteSummary.vue'),
},
],
},
],