Merge pull request 'Create Route module' (#73) from feature/routes into dev

Reviewed-on: hyervoni/salix-front-mindshore#73
This commit is contained in:
William Buezas 2024-01-18 13:22:20 +00:00
commit f66af0e663
18 changed files with 1732 additions and 6 deletions

View File

@ -128,13 +128,14 @@ async function save() {
try { try {
const body = $props.mapper ? $props.mapper(formData.value) : formData.value; const body = $props.mapper ? $props.mapper(formData.value) : formData.value;
let response
if ($props.urlCreate) { if ($props.urlCreate) {
await axios.post($props.urlCreate, body); response = await axios.post($props.urlCreate, body);
notify('globals.dataCreated', 'positive'); notify('globals.dataCreated', 'positive');
} else { } else {
await axios.patch($props.urlUpdate || $props.url, body); response = await axios.patch($props.urlUpdate || $props.url, body);
} }
emit('onDataSaved', formData.value); emit('onDataSaved', formData.value, response);
originalData.value = JSON.parse(JSON.stringify(formData.value)); originalData.value = JSON.parse(JSON.stringify(formData.value));
hasChanges.value = false; hasChanges.value = false;
} catch (err) { } catch (err) {

View File

@ -0,0 +1,108 @@
<script setup>
import {computed, ref} from 'vue';
import { toHour} from 'src/filters';
import {useI18n} from "vue-i18n";
import isValidDate from "filters/isValidDate";
const props = defineProps({
modelValue: {
type: String,
default: null,
},
readonly: {
type: Boolean,
default: false,
},
isOutlined: {
type: Boolean,
default: false,
},
});
const { t } = useI18n();
const emit = defineEmits(['update:modelValue']);
const value = computed({
get() {
return props.modelValue;
},
set(value) {
const [hours, minutes] = value.split(':')
const date = new Date()
date.setUTCHours(Number.parseInt(hours) || 0, Number.parseInt(minutes) || 0, 0, 0)
emit('update:modelValue', value ? date.toISOString() : null);
},
});
const onDateUpdate = (date) => {
internalValue.value = date;
};
const save = () => {
value.value = internalValue.value;
};
const formatTime = (dateString) => {
if (!isValidDate(dateString)){
return ''
}
const date = new Date(dateString || '');
return `${date.getUTCHours().toString().padStart(2, '0')}:${date.getUTCMinutes().toString().padStart(2, '0')}`;
};
const internalValue = ref(formatTime(value))
const styleAttrs = computed(() => {
return props.isOutlined
? {
dense: true,
outlined: true,
rounded: true,
}
: {};
});
</script>
<template>
<QInput
class="vn-input-time"
rounded
readonly
:model-value="toHour(value)"
v-bind="{ ...$attrs, ...styleAttrs }"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
:no-parent-event="props.readonly"
>
<QTime
:format24h="false"
:model-value="formatTime(value)"
@update:model-value="onDateUpdate"
>
<div class="row items-center justify-end q-gutter-sm">
<QBtn :label="t('Cancel')" color="primary" flat v-close-popup />
<QBtn label="Ok" color="primary" flat @click="save" v-close-popup />
</div>
</QTime>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</template>
<style lang="scss">
.vn-input-time.q-field--standard.q-field--readonly .q-field__control:before {
border-bottom-style: solid;
}
.vn-input-time.q-field--outlined.q-field--readonly .q-field__control:before {
border-style: solid;
}
</style>
<i18n>
es:
Cancel: Cancelar
</i18n>

View File

@ -8,11 +8,13 @@ import toPercentage from './toPercentage';
import toLowerCamel from './toLowerCamel'; import toLowerCamel from './toLowerCamel';
import dashIfEmpty from './dashIfEmpty'; import dashIfEmpty from './dashIfEmpty';
import dateRange from './dateRange'; import dateRange from './dateRange';
import toHour from './toHour';
export { export {
toLowerCase, toLowerCase,
toLowerCamel, toLowerCamel,
toDate, toDate,
toHour,
toDateString, toDateString,
toDateHour, toDateHour,
toRelativeDate, toRelativeDate,

View File

@ -0,0 +1,3 @@
export default function isValidDate(date) {
return !isNaN(new Date(date).getTime());
}

16
src/filters/toHour.js Normal file
View File

@ -0,0 +1,16 @@
import isValidDate from 'filters/isValidDate';
export default function toHour(date) {
if (!isValidDate(date)) {
return '--:--';
}
const dateHour = new Date(date);
let hours = dateHour.getUTCHours();
hours = hours % 12;
hours = hours ? hours : 12;
let minutes = dateHour.getUTCMinutes();
minutes = minutes < 10 ? minutes.toString().padStart(2, '0') : minutes;
return `${hours}:${minutes} ${dateHour.getUTCHours() >= 12 ? 'PM' : 'AM'}`;
}

View File

@ -883,6 +883,10 @@ export default {
pageTitles: { pageTitles: {
routes: 'Routes', routes: 'Routes',
cmrsList: 'External CMRs list', cmrsList: 'External CMRs list',
RouteList: 'List',
create: 'Create',
basicData: 'Basic Data',
summary: 'Summary'
}, },
cmr: { cmr: {
list: { list: {

View File

@ -883,6 +883,10 @@ export default {
pageTitles: { pageTitles: {
routes: 'Rutas', routes: 'Rutas',
cmrsList: 'Listado de CMRs externos', cmrsList: 'Listado de CMRs externos',
RouteList: 'Listado',
create: 'Crear',
basicData: 'Datos básicos',
summary: 'Summary',
}, },
cmr: { cmr: {
list: { list: {

View File

@ -0,0 +1,22 @@
<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();
</script>
<template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<RouteDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<RouterView></RouterView>
</QPage>
</QPageContainer>
</template>

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

@ -0,0 +1,234 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const emit = defineEmits(['search']);
const workerList = ref([]);
const agencyList = ref([]);
const vehicleList = ref([]);
const warehouseList = ref([]);
</script>
<template>
<FetchData
url="Workers/search"
:filter="{ fields: ['id', 'nickname'] }"
sort-by="nickname ASC"
limit="30"
@on-fetch="(data) => (workerList = data)"
auto-load
/>
<FetchData
url="AgencyModes/isActive"
:filter="{ fields: ['id', 'name'] }"
sort-by="name ASC"
limit="30"
@on-fetch="(data) => (agencyList = data)"
auto-load
/>
<FetchData
url="Vehicles"
:filter="{ fields: ['id', 'numberPlate'] }"
sort-by="numberPlate ASC"
limit="30"
@on-fetch="(data) => (vehicleList = data)"
auto-load
/>
<FetchData url="Warehouses" @on-fetch="(data) => (warehouseList = data)" auto-load />
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
@search="emit('search')"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params }">
<QList dense>
<QItem class="q-my-sm">
<QItemSection v-if="workerList">
<VnSelectFilter
:label="t('Worker')"
v-model="params.workerFk"
:options="workerList"
option-value="id"
option-label="nickname"
dense
outlined
rounded
emit-value
map-options
use-input
:input-debounce="0"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.nickname }},{{ opt.code }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection v-if="agencyList">
<VnSelectFilter
:label="t('Agency')"
v-model="params.agencyModeFk"
:options="agencyList"
option-value="id"
option-label="name"
dense
outlined
rounded
emit-value
map-options
use-input
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInputDate
v-model="params.from"
:label="t('From')"
is-outlined
:disable="Boolean(params.scopeDays)"
@update:model-value="params.scopeDays = null"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInputDate
v-model="params.to"
:label="t('To')"
is-outlined
:disable="Boolean(params.scopeDays)"
@update:model-value="params.scopeDays = null"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput
v-model="params.scopeDays"
type="number"
:label="t('Days Onward')"
is-outlined
clearable
:disable="Boolean(params.from || params.to)"
@update:model-value="
params.to = null;
params.from = null;
"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection v-if="vehicleList">
<VnSelectFilter
:label="t('Vehicle')"
v-model="params.vehicleFk"
:options="vehicleList"
option-value="id"
option-label="numberPlate"
dense
outlined
rounded
emit-value
map-options
use-input
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput v-model="params.m3" label="m³" is-outlined clearable />
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection v-if="vehicleList">
<VnSelectFilter
:label="t('Warehouse')"
v-model="params.warehouseFk"
:options="warehouseList"
option-value="id"
option-label="name"
dense
outlined
rounded
emit-value
map-options
use-input
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput
v-model="params.description"
:label="t('Description')"
is-outlined
clearable
/>
</QItemSection>
</QItem>
</QList>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
warehouseFk: Warehouse
description: Description
m3:
vehicleFk: Vehicle
agencyModeFk: Agency
workerFk: Worker
from: From
to: To
es:
params:
warehouseFk: Almacén
description: Descripción
m3:
vehicleFk: Vehículo
agencyModeFk: Agencia
workerFk: Trabajador
from: Desde
to: Hasta
Warehouse: Almacén
Description: Descripción
Vehicle: Vehículo
Agency: Agencia
Worker: Trabajador
From: Desde
To: Hasta
</i18n>

View File

@ -0,0 +1,214 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
import axios from 'axios';
import VnInputTime from 'components/common/VnInputTime.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const shelvingId = route.params?.id || null;
const isNew = Boolean(!shelvingId);
const defaultInitialData = {
agencyModeFk: null,
created: null,
description: '',
vehicleFk: null,
workerFk: null,
};
const workerList = ref([]);
const agencyList = ref([]);
const vehicleList = ref([]);
const routeFilter = {
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 onSave = (data, response) => {
if (isNew) {
axios.post(`Routes/${response.data?.id}/updateWorkCenter`);
router.push({ name: 'RouteSummary', params: { id: response.data?.id } });
}
};
</script>
<template>
<VnSubToolbar />
<FetchData
url="Workers/search"
:filter="{ fields: ['id', 'nickname'] }"
sort-by="nickname ASC"
limit="30"
@on-fetch="(data) => (workerList = data)"
auto-load
/>
<FetchData
url="AgencyModes/isActive"
:filter="{ fields: ['id', 'name'] }"
sort-by="name ASC"
limit="30"
@on-fetch="(data) => (agencyList = data)"
auto-load
/>
<FetchData
url="Vehicles"
:filter="{ fields: ['id', 'numberPlate'] }"
sort-by="numberPlate ASC"
limit="30"
@on-fetch="(data) => (vehicleList = data)"
auto-load
/>
<FormModel
:url="isNew ? null : `Routes/${shelvingId}`"
:url-create="isNew ? 'Routes' : null"
:observe-form-changes="!isNew"
:filter="routeFilter"
model="route"
:auto-load="!isNew"
:form-initial-data="defaultInitialData"
@on-data-saved="onSave"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Worker')"
v-model="data.workerFk"
:options="workerList"
option-value="id"
option-label="nickname"
emit-value
map-options
use-input
:input-debounce="0"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.nickname }},{{ opt.code }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('Vehicle')"
v-model="data.vehicleFk"
:options="vehicleList"
option-value="id"
option-label="numberPlate"
emit-value
map-options
use-input
:input-debounce="0"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Agency')"
v-model="data.agencyModeFk"
:options="agencyList"
option-value="id"
option-label="name"
emit-value
map-options
use-input
:input-debounce="0"
/>
</div>
<div class="col">
<VnInputDate v-model="data.created" :label="t('Created')" />
</div>
</VnRow>
<template v-if="!isNew">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.kmStart"
:label="t('Km Start')"
clearable
/>
</div>
<div class="col">
<VnInput v-model="data.kmEnd" :label="t('Km End')" clearable />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInputTime
v-model="data.started"
:label="t('Hour started')"
clearable
/>
</div>
<div class="col">
<VnInputTime
v-model="data.finished"
:label="t('Hour finished')"
clearable
/>
</div>
</VnRow>
</template>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.description"
:label="t('Description')"
clearable
/>
</div>
</VnRow>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,20 @@
<script setup>
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import {useI18n} from "vue-i18n";
const { t } = useI18n();
</script>
<template>
<VnSearchbar
data-key="RouteList"
:label="t('Search route')"
:info="t('You can search by route reference')"
/>
</template>
<style scoped lang="scss"></style>
<i18n>
es:
Search route: Buscar rutas
You can search by route reference: Puedes buscar por referencia de la ruta
</i18n>

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

@ -0,0 +1,530 @@
<script setup>
import VnPaginate from 'components/ui/VnPaginate.vue';
import { useStateStore } from 'stores/useStateStore';
import { useI18n } from 'vue-i18n';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { dashIfEmpty, toDate, toHour } from 'src/filters';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import FetchData from 'components/FetchData.vue';
import { useValidator } from 'composables/useValidator';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
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 { t } = useI18n();
const { validate } = useValidator();
const quasar = useQuasar();
const to = Date.vnNew();
to.setDate(to.getDate() + 1);
to.setHours(0, 0, 0, 0);
const from = Date.vnNew();
from.setDate(from.getDate());
from.setHours(0, 0, 0, 0);
const params = ref({ from, to });
onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false));
const selectedRows = ref([]);
const columns = computed(() => [
{
name: 'ID',
label: t('ID'),
field: (row) => row.id,
sortable: true,
align: 'left',
},
{
name: 'worker',
label: t('Worker'),
field: (row) => row.workerUserName,
sortable: true,
align: 'left',
},
{
name: 'agency',
label: t('Agency'),
field: (row) => row.agencyName,
sortable: true,
align: 'left',
},
{
name: 'vehicle',
label: t('Vehicle'),
field: (row) => row.vehiclePlateNumber,
sortable: true,
align: 'left',
},
{
name: 'date',
label: t('Date'),
field: (row) => row.created,
sortable: true,
align: 'left',
},
{
name: 'volume',
label: 'm³',
field: (row) => dashIfEmpty(row.m3),
sortable: true,
align: 'left',
},
{
name: 'description',
label: t('Description'),
field: (row) => row.description,
sortable: true,
align: 'left',
},
{
name: 'started',
label: t('Hour started'),
field: (row) => toHour(row.started),
sortable: true,
align: 'left',
},
{
name: 'finished',
label: t('Hour finished'),
field: (row) => toHour(row.finished),
sortable: true,
align: 'left',
},
{
name: 'actions',
label: '',
sortable: false,
align: 'right',
},
]);
const refreshKey = ref(0);
const workers = ref([]);
const agencyList = ref([]);
const vehicleList = ref([]);
const updateRoute = async (route) => {
try {
return await axios.patch(`Routes/${route.id}`, route);
} catch (err) {
return err;
}
};
const updateVehicle = (row, vehicle) => {
row.vehicleFk = vehicle.id;
row.vehiclePlateNumber = vehicle.numberPlate;
updateRoute(row);
};
const updateAgency = (row, agency) => {
row.agencyModeFk = agency.id;
row.agencyName = agency.name;
updateRoute(row);
};
const updateWorker = (row, worker) => {
row.workerFk = worker.id;
row.workerUserName = worker.name;
updateRoute(row);
};
const confirmationDialog = ref(false);
const startingDate = ref(null);
const cloneRoutes = () => {
axios.post('Routes/clone', {
created: startingDate.value,
ids: selectedRows.value.map((row) => row?.id),
});
refreshKey.value++;
startingDate.value = null;
};
const markAsServed = () => {
selectedRows.value.forEach((row) => {
if (row?.id) {
axios.patch(`Routes/${row?.id}`, { isOk: true });
}
});
refreshKey.value++;
startingDate.value = null;
};
function previewRoute(id) {
if (!id) {
return;
}
quasar.dialog({
component: RouteSummaryDialog,
componentProps: {
id,
},
});
}
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<RouteSearchbar />
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDialog v-model="confirmationDialog">
<QCard style="min-width: 350px">
<QCardSection>
<p class="text-h6 q-ma-none">{{ t('Select the starting date') }}</p>
</QCardSection>
<QCardSection class="q-pt-none">
<VnInputDate
:label="t('Stating date')"
v-model="startingDate"
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">
{{ t('Clone') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<RouteFilter data-key="RouteList" />
</QScrollArea>
</QDrawer>
<FetchData
url="Workers/activeWithInheritedRole"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<FetchData url="AgencyModes" @on-fetch="(data) => (agencyList = data)" auto-load />
<FetchData url="Vehicles" @on-fetch="(data) => (vehicleList = data)" auto-load />
<QPage class="column items-center">
<QToolbar class="bg-vn-dark justify-end">
<div id="st-actions" class="q-pa-sm">
<QBtn
icon="vn:clone"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="confirmationDialog = true"
>
<QTooltip>{{ t('Clone Selected Routes') }}</QTooltip>
</QBtn>
<QBtn
icon="check"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="markAsServed"
>
<QTooltip>{{ t('Mark as served') }}</QTooltip>
</QBtn>
</div>
</QToolbar>
<div class="route-list">
<VnPaginate
:key="refreshKey"
data-key="RouteList"
url="Routes/filter"
:order="['created DESC', 'id DESC']"
:limit="20"
auto-load
>
<template #body="{ rows }">
<div class="q-pa-md">
<QTable
v-model:selected="selectedRows"
:columns="columns"
:rows="rows"
flat
row-key="id"
selection="multiple"
:rows-per-page-options="[0]"
hide-pagination
>
<template #body-cell-worker="props">
<QTd :props="props">
{{ props.row?.workerUserName }}
<QPopupEdit
:model-value="props.row.workerFk"
v-slot="scope"
buttons
@update:model-value="
(worker) => updateWorker(props.row, worker)
"
>
<VnSelectFilter
:label="t('Worker')"
v-model="scope.value"
:options="workers"
option-value="id"
option-label="name"
hide-selected
autofocus
:emit-value="false"
:rules="validate('Route.workerFk')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
>
<template #option="{ opt, itemProps }">
<QItem
v-bind="itemProps"
class="q-pa-xs row items-center"
>
<QItemSection
class="col-9 justify-center"
>
<span>{{ opt.name }}</span>
<span class="text-grey">{{
opt.nickname
}}</span>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-agency="props">
<QTd :props="props">
{{ props.row?.agencyName }}
<QPopupEdit
:model-value="props.row.agencyModeFk"
v-slot="scope"
buttons
@update:model-value="
(agency) => updateAgency(props.row, agency)
"
>
<VnSelectFilter
:label="t('Agency')"
v-model="scope.value"
:options="agencyList"
option-value="id"
option-label="name"
hide-selected
autofocus
:emit-value="false"
:rules="validate('route.agencyFk')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
/>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-vehicle="props">
<QTd :props="props">
{{ props.row?.vehiclePlateNumber }}
<QPopupEdit
:model-value="props.row.vehicleFk"
v-slot="scope"
buttons
@update:model-value="
(vehicle) => updateVehicle(props.row, vehicle)
"
>
<VnSelectFilter
:label="t('Vehicle')"
v-model="scope.value"
:options="vehicleList"
option-value="id"
option-label="numberPlate"
hide-selected
autofocus
:emit-value="false"
:rules="validate('route.vehicleFk')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
/>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-date="props">
<QTd :props="props">
{{ toDate(props.row?.created) }}
<QPopupEdit
v-model="props.row.created"
v-slot="scope"
@update:model-value="updateRoute(props.row)"
buttons
>
<VnInputDate
v-model="scope.value"
autofocus
:label="t('Date')"
:rules="validate('route.created')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
/>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-description="props">
<QTd :props="props">
{{ props.row?.description }}
<QPopupEdit
v-model="props.row.description"
v-slot="scope"
@update:model-value="updateRoute(props.row)"
buttons
>
<VnInput
v-model="scope.value"
autofocus
:label="t('Description')"
:rules="validate('route.description')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
/>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-started="props">
<QTd :props="props">
{{ toHour(props.row.started) }}
<QPopupEdit
v-model="props.row.started"
v-slot="scope"
buttons
@update:model-value="updateRoute(props.row)"
>
<VnInputTime
v-model="scope.value"
autofocus
:label="t('Hour started')"
:rules="validate('route.started')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
/>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-finished="props">
<QTd :props="props">
{{ toHour(props.row.finished) }}
<QPopupEdit
v-model="props.row.finished"
v-slot="scope"
buttons
@update:model-value="updateRoute(props.row)"
>
<VnInputTime
v-model="scope.value"
autofocus
:label="t('Hour finished')"
:rules="validate('route.finished')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
/>
</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" />
<QTooltip>
{{ t('newRoute') }}
</QTooltip>
</RouterLink>
</QPageSticky>
</QPage>
</template>
<style lang="scss" scoped>
.route-list {
width: 100%;
}
.table-actions {
display: flex;
align-items: center;
gap: 12px;
i {
cursor: pointer;
}
}
</style>
<i18n>
en:
newRoute: New Route
es:
ID: ID
Worker: Trabajador
Agency: Agencia
Vehicle: Vehículo
Date: Fecha
Description: Descripción
Hour started: Hora inicio
Hour finished: Hora fin
newRoute: Nueva Ruta
Clone Selected Routes: Clonar rutas seleccionadas
Select the starting date: Seleccione la fecha de inicio
Stating date: Fecha de inicio
Cancel: Cancelar
Clone: Clonar
Mark as served: Marcar como servidas
</i18n>

View File

@ -10,15 +10,15 @@ export default {
component: RouterView, component: RouterView,
redirect: { name: 'RouteMain' }, redirect: { name: 'RouteMain' },
menus: { menus: {
main: ['CmrList'], main: ['RouteList', 'CmrList'],
card: [], card: ['RouteBasicData'],
}, },
children: [ children: [
{ {
path: '/route', path: '/route',
name: 'RouteMain', name: 'RouteMain',
component: () => import('src/pages/Route/RouteMain.vue'), component: () => import('src/pages/Route/RouteMain.vue'),
redirect: { name: 'CmrList' }, redirect: { name: 'RouteList' },
children: [ children: [
{ {
path: 'cmr', path: 'cmr',
@ -29,6 +29,49 @@ export default {
}, },
component: () => import('src/pages/Route/Cmr/CmrList.vue'), component: () => import('src/pages/Route/Cmr/CmrList.vue'),
}, },
{
path: 'list',
name: 'RouteList',
meta: {
title: 'RouteList',
icon: 'view_list',
},
component: () => import('src/pages/Route/RouteList.vue'),
},
{
path: 'create',
name: 'RouteCreate',
meta: {
title: 'create',
},
component: () => import('src/pages/Route/Card/RouteForm.vue'),
},
],
},
{
name: 'RouteCard',
path: ':id',
component: () => import('src/pages/Route/Card/RouteCard.vue'),
redirect: { name: 'RouteSummary' },
children: [
{
name: 'RouteBasicData',
path: 'basic-data',
meta: {
title: 'basicData',
icon: 'vn:settings',
},
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'),
},
], ],
}, },
], ],